2. STM32定时器编码器模式:定时器架构回顾、编码器模式配置(TI1与TI2)、计数方向与速度计算
好,咱们进入第二章。上一章我们把编码器的电气接口和信号类型理清了,这一章要动真格的了——让STM32的定时器直接读懂编码器的A、B相脉冲。
说白了,就是用硬件帮你数脉冲、判方向。你想想看,如果全靠外部中断去数,CPU得累死,而且高速下容易丢步。STM32的定时器编码器模式,就是专门干这个的。
2.1 定时器架构回顾:别小看这个外设
先快速回顾一下STM32定时器的核心骨架。我习惯把定时器想象成一个「万能计数器」:
- 时基单元:预分频器(PSC)、自动重装载寄存器(ARR)、计数器(CNT)。这是定时器的灵魂,决定了计数范围和精度。
- 输入捕获通道:TI1、TI2、TI3、TI4。每个通道都可以配置成捕获模式、PWM输入模式,或者——咱们今天的主角——编码器模式。
- 从模式控制器:这是编码器模式能工作的关键。它让定时器可以「听」外部信号的话,而不是只靠内部时钟。
嗯,这里要注意:不是所有定时器都支持编码器模式。一般通用定时器(TIM2~TIM5)和高级定时器(TIM1、TIM8)都支持。基本定时器(TIM6、TIM7)不行,它们没有输入捕获通道。
核心要点:编码器模式本质上是「从模式」的一种特殊用法。定时器不再用内部时钟驱动,而是用编码器的A、B相信号作为时钟源和方向指示。
我曾经在项目选型时吃过亏,选了个只有基本定时器的低端型号,结果编码器接口得用外部中断模拟,代码写起来那叫一个痛苦。所以,选型时一定要确认定时器资源。
2.2 编码器模式配置:TI1与TI2的配合
编码器模式的核心,就是让定时器同时监视TI1和TI2两个通道。这两个通道分别接编码器的A相和B相。定时器内部有个「编码器逻辑」,它会根据两个信号的边沿顺序,自动判断旋转方向。
配置其实不复杂,但有几个坑。我直接给出一段我常用的初始化代码,咱们边看边讲:
void Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
TIM_Encoder_InitTypeDef EncoderConfig = {0};
// 1. 使能时钟
__HAL_RCC_TIM3_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 2. 配置GPIO:PA6(TIM3_CH1), PA7(TIM3_CH2)
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉,防止浮空
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置编码器模式
EncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 双沿计数
EncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
EncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
EncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1;
EncoderConfig.IC1Filter = 0; // 不滤波,高速场景可加
EncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
EncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
EncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1;
EncoderConfig.IC2Filter = 0;
HAL_TIM_Encoder_Init(&htim3, &EncoderConfig);
// 4. 启动编码器
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}
这段代码里,最关键的参数是 TIM_ENCODERMODE_TI12。它告诉定时器:同时用TI1和TI2的上升沿和下降沿来计数。说白了,就是「四倍频」模式。
| 编码器模式 | 计数边沿 | 每圈脉冲数(举例) | 分辨率 |
|---|---|---|---|
| TIM_ENCODERMODE_TI1 | 仅TI1边沿 | 1000 | 1x |
| TIM_ENCODERMODE_TI2 | 仅TI2边沿 | 1000 | 1x |
| TIM_ENCODERMODE_TI12 | TI1和TI2双沿 | 1000 | 4x |
我个人习惯用TI12模式,因为分辨率直接翻四倍,而且方向判断最可靠。代价是计数器溢出更快,需要合理设置ARR值。
经验之谈:如果编码器线数很高(比如5000线),用TI12模式时,ARR要设得足够大,否则计数器会频繁溢出。我一般把ARR设为0xFFFF,然后用32位变量累加。
2.3 计数方向与速度计算
配置好之后,定时器就开始自动计数了。你只需要读 TIMx->CNT 寄存器,就能知道当前的位置。那方向呢?
方向信息藏在 TIMx->CR1 寄存器的第4位(DIR位)。如果DIR=0,表示正转(向上计数);DIR=1,表示反转(向下计数)。
我一般这样读方向和计数值:
int16_t Get_Encoder_Count(void)
{
return (int16_t)htim3.Instance->CNT;
}
uint8_t Get_Encoder_Direction(void)
{
// 读取DIR位:0=正转,1=反转
return (htim3.Instance->CR1 & TIM_CR1_DIR) ? 1 : 0;
}
注意,我把CNT强制转成了 int16_t。为什么?因为编码器模式下的计数器是双向的,正转时从0往上加,反转时从ARR往下减。用有符号数读取,可以直接得到带符号的位置值,方便后续计算。
速度计算呢?说白了就是「单位时间内的位置变化」。我常用的方法是:
- 每隔固定时间(比如10ms)读取一次计数值。
- 用当前值减去上次值,得到差值。
- 差值除以时间间隔,就是速度。
int16_t last_count = 0;
float speed = 0.0f;
void Calculate_Speed(void)
{
int16_t current_count = Get_Encoder_Count();
int16_t delta = current_count - last_count;
// 处理溢出:如果差值超过半个周期,说明发生了溢出
if (delta > 32767) delta -= 65536;
else if (delta < -32768) delta += 65536;
// 速度 = 差值 / 时间间隔(单位:脉冲/秒)
speed = (float)delta / 0.01f; // 假设10ms采样一次
last_count = current_count;
}
嗯,这里要注意溢出处理。如果计数器从0xFFFF跳到0x0000,直接相减会得到一个巨大的负值。我上面那个if判断就是干这个的——把差值修正到正确的范围内。
避坑指南:我曾经在一个高速电机项目里,采样周期设得太长(100ms),结果电机一转快,计数器来回溢出了好几次,速度计算完全乱套。后来把采样周期缩短到5ms,同时用32位变量做累加,问题才解决。
最后,咱们用一张图来总结整个编码器模式的工作流程:
这张图把整个流程串起来了:编码器信号进入定时器,经过编码器逻辑处理后,自动更新CNT和DIR寄存器。你只需要在应用层定时读取这些寄存器,就能得到位置、方向和速度。