2. 编码器基础
好,咱们进入正题。编码器接口,说白了就是让定时器自己读懂正交信号,不用CPU去轮询。我刚开始做运动控制那会儿,还傻乎乎地用外部中断去读A、B相,结果高速一转,中断全丢了。后来才发现,STM32的定时器早就把这事儿干好了。
2.1 编码器模式的工作原理
正交编码器输出两路信号:TI1(通常对应A相)和TI2(对应B相)。两路信号相位差90°,定时器通过检测边沿来判断旋转方向和计数。
STM32的编码器模式,说白了就是让定时器工作在特殊的计数方式下。它根据TI1和TI2的边沿组合,自动决定是加1、减1还是不变。
核心逻辑:定时器在TI1和TI2的每个有效边沿都计数一次。这意味着,如果编码器每圈有1000个脉冲,那么定时器每圈会计数4000次(4倍频)。
我遇到过不少新手,上来就问:“为什么我编码器1000线,定时器却计了4000个数?”嗯,这就是4倍频的效果。你想想看,每个脉冲的上升沿和下降沿都计一次,可不就是4倍嘛。
2.2 编码器模式的三种配置
STM32的定时器编码器模式有三种,对应不同的计数策略。我整理了一张表,方便你对照选择:
| 模式 | 计数依据 | 分辨率 | 适用场景 |
|---|---|---|---|
| 模式1 | 仅TI1边沿 | 1倍 | 低速、简单方向判断 |
| 模式2 | 仅TI2边沿 | 1倍 | 低速、简单方向判断 |
| 模式3 | TI1和TI2所有边沿 | 4倍 | 高精度、高速运动控制 |
我个人习惯,只要硬件允许,一律用模式3。为什么?因为4倍频能让你用更便宜的编码器达到同样的分辨率。举个例子,你本来需要4000线的编码器,用模式3之后,1000线的就够了,成本直接降下来。
配置代码其实很简单,以TIM3为例:
// 编码器模式配置 - 模式3
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
// 使能定时器
TIM_Cmd(TIM3, ENABLE);
这里要注意,TIM_ICPolarity_Rising并不是说只检测上升沿,而是指在上升沿采样。模式3下,两个通道的所有边沿都会被检测到。
2.3 TI1与TI2滤波设置
滤波,这是编码器接口里最容易忽略但又最关键的一环。我踩过一个大坑:有一次调试伺服电机,编码器信号在低速时一切正常,一跑到3000转以上,计数就开始乱跳。查了两天,最后发现是信号上的毛刺导致的。
STM32的定时器输入滤波,本质上是一个数字滤波器。它通过连续采样来判断信号是否真的变化了。配置参数有两个:
- ICF(输入捕获滤波器): 决定采样频率和需要的连续采样次数
- ICPSC(输入捕获预分频器): 决定采样时钟的分频系数
滤波器的配置在TIM_ICInitStructure里:
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0F; // 滤波强度,范围0x00~0x0F
TIM_ICInit(TIM3, &TIM_ICInitStructure);
经验值:对于常见的差分编码器(如RS422输出),滤波值设在0x08~0x0C之间比较稳妥。如果是集电极开路输出,信号边沿较缓,滤波值可以适当调低到0x04~0x08。
滤波值越大,抗干扰能力越强,但也会引入延迟。我一般遵循一个原则:在保证不误计的前提下,滤波值越小越好。毕竟运动控制对实时性要求很高。
2.4 计数方向与溢出处理
编码器模式下的计数方向,由硬件自动判断。你只需要读TIMx->CR1寄存器的DIR位就能知道当前方向:
- DIR=0: 向上计数(正转)
- DIR=1: 向下计数(反转)
但真正让人头疼的是溢出处理。定时器是16位的,最大计数值65535。如果编码器转得快,很容易溢出。我见过有人用32位定时器(如TIM2、TIM5)来避免这个问题,但有时候资源不够,只能用16位的。
溢出处理有两种常用方法:
- 中断法: 使能更新中断(UIE),在中断里记录溢出次数
- DMA法: 用DMA把计数器值实时搬运到内存,配合软件做扩展
中断法的代码示例:
// 使能更新中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
// 中断服务函数
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
// 读取当前方向,决定溢出方向
if ((TIM3->CR1 & TIM_CR1_DIR) == 0)
{
overflow_count++; // 正转溢出
}
else
{
overflow_count--; // 反转溢出
}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
// 读取实际位置
int32_t GetEncoderPosition(void)
{
return (int32_t)(overflow_count << 16) + TIM_GetCounter(TIM3);
}
注意:中断法在高频编码器下容易出问题。如果中断响应不及时,可能会丢失溢出事件。我曾经在电机高速运转时遇到过这种情况,后来改用DMA+定时器级联的方式才解决。
还有一种更优雅的方式——定时器级联。把两个16位定时器串起来,组成一个32位计数器。比如用TIM3做主定时器,TIM4做从定时器,TIM3溢出时自动触发TIM4计数。这样硬件自己就把32位扩展做了,CPU完全不用管。
2.5 知识结构总览
下面这张图把本章的核心逻辑串起来了,你可以对照着看:
从这张图可以看得很清楚:编码器信号进来之后,先经过滤波,再进入编码器模式逻辑,然后计数器根据方向和边沿进行加减,最后通过溢出处理得到完整的位置信息。每一步都环环相扣。
2.6 实际项目中的避坑指南
最后,分享几个我实际项目中踩过的坑,希望能帮你少走弯路:
- 滤波值别照搬:不同编码器、不同线缆长度、不同电机噪声,滤波值都不一样。我一般会在样机阶段用示波器抓一下信号,看看毛刺的宽度,再反推滤波参数。
- 初始化顺序有讲究:先配置编码器模式,再配置滤波,最后使能定时器。顺序反了,可能会在使能瞬间产生一个错误的计数脉冲。
- 溢出中断优先级:如果用了中断法处理溢出,记得把中断优先级设高一点。我曾经因为优先级不够,被其他中断堵住,结果丢了好几次溢出。
- 上电初始值:编码器上电时,计数器值是不确定的。我习惯在系统初始化时,把编码器转到机械零点,然后软件清零。这样位置就有基准了。