1. 为什么编码器四倍频值得单独拿出来说
做电机控制或者位置检测的朋友,对增量式编码器肯定不陌生。这种编码器一般输出A、B两路正交信号,转一圈输出固定数量的脉冲。但很多人第一次用STM32的定时器接编码器时,会发现读出来的计数值和手册上的分辨率对不上——明明编码器标称500线,怎么读出来是2000?这多出来的4倍,就是四倍频在起作用。
四倍频不是什么玄学,它是正交解码的自然结果。A、B两路信号相位差90度,一个完整周期内会出现4个边沿:A相上升沿、B相上升沿、A相下降沿、B相下降沿。STM32的TIMx编码器接口模式会在这4个边沿都触发计数,所以最终计数值是编码器线数的4倍。这个机制带来的直接好处是分辨率提升4倍,对于低速场合的位置控制来说,相当于免费获得了更细的刻度。
但问题也来了:很多人配置完发现方向不对、计数抖动、高速时丢脉冲,甚至溢出方向搞反。这些坑我在实际项目里几乎全踩过一遍。这篇文章就把STM32的TIMx编码器四倍频从原理到配置到调试,完整地拆一遍。不管你是刚接触电机控制的新手,还是想从软件计数切换到硬件解码的老手,下面的内容都能直接拿去用。
2. 正交编码器与四倍频的核心原理拆解
2.1 增量式编码器的信号特征
增量式编码器内部一般有一个带狭缝的码盘和两组光电对管。码盘转动时,A、B两路输出方波,两者的相位差固定为90度电角度。这个相位差是判断方向的关键:A相超前B相90度时是正转,B相超前A相90度时是反转。
除了A、B,很多编码器还有Z相信号,也叫零位信号,每转一圈输出一个脉冲。Z相一般用来做绝对位置校准,但在纯四倍频计数场景里不是必须的。我这里主要聊A、B两路的处理,Z相后面会提一下怎么配合使用。
编码器的分辨率通常用线数表示,比如500线、1000线、2500线。这里的“线”指的是码盘上均匀分布的狭缝数量,也就是每转输出的完整周期数。注意,一个完整周期包含A相的一个高电平和低电平,所以A相在每转内输出的脉冲数等于线数。但四倍频之后,计数值变成线数的4倍。
2.2 四倍频到底是怎么“多出来”的
拿一个500线的编码器举例。正转时,A、B两路信号的波形关系是:A相上升沿时B相为低,A相下降沿时B相为高,B相上升沿时A相为高,B相下降沿时A相为低。这四个边沿每一个都对应一个唯一的A、B电平组合,所以定时器可以靠这四个边沿来计数。
STM32的编码器接口模式在硬件层面实现了这个逻辑。它内部有两个输入通道TI1和TI2,分别接A相和B相。定时器的从模式控制器会根据TI1和TI2的电平变化,自动决定计数方向并产生计数脉冲。具体来说,当TI1和TI2的电平组合变化时,计数器加1或减1。一个完整周期内电平组合变化4次,所以计数器变化4次。
这里有个容易混淆的点:四倍频不是软件乘4,而是硬件在每个边沿都计数。软件乘4的做法是先读单相脉冲再乘以4,但那样会丢失方向信息,而且高速时容易丢脉冲。硬件四倍频是实打实地在每个边沿触发计数,精度和实时性都更好。
2.3 为什么选TIMx而不是外部中断
有人可能会想,我用外部中断在A相上升沿计数不就行了?理论上可以,但实际项目里问题很多。外部中断响应需要CPU介入,每次中断都有进出栈开销。编码器转速高的时候,脉冲频率可能达到几百kHz,CPU根本来不及处理。而且外部中断只能捕获单相边沿,要判断方向还得读另一相电平,软件逻辑复杂,容易出错。
STM32的TIMx编码器接口模式是纯硬件解码,不占用CPU资源。计数器在后台自动加减,你只需要在需要的时候读一下CNT寄存器就行。这对于需要同时处理电机控制、通信、显示等多任务的系统来说,优势非常明显。另外,TIMx还支持输入滤波,可以滤掉编码器信号上的毛刺,这是外部中断很难做到的。
3. STM32编码器接口模式的配置细节
3.1 硬件连接与引脚选择
STM32的编码器接口模式对引脚有固定要求:TI1和TI2必须映射到定时器的CH1和CH2通道。以常见的TIM3为例,CH1对应PA6或PB4,CH2对应PA7或PB5。具体用哪组引脚,取决于你的芯片封装和复用功能配置。
我一般优先选PA6和PA7,因为这两个引脚在很多开发板上都引出来了,接线方便。如果PA6被其他外设占了,再考虑重映射到PB4和PB5。重映射需要配置AFIO_MAPR寄存器,用标准库的话调用GPIO_PinRemapConfig函数就行。
接线时注意编码器的电源和地一定要接好,A、B信号线尽量用双绞线或者屏蔽线,尤其是电机干扰大的场合。我遇到过编码器信号线没屏蔽,电机一转计数值就乱跳的情况,后来加了屏蔽层并单点接地才稳定下来。
3.2 定时器时基单元的参数计算
编码器模式下,时基单元的ARR、PSC、CNT三个寄存器的作用需要重新理解。PSC预分频器在编码器模式下不起作用,因为计数时钟不是来自内部时钟源,而是来自编码器边沿。所以PSC设成0就行,设其他值也没影响。
ARR自动重装载值决定了计数范围。编码器模式下,CNT在0到ARR之间循环计数。如果你的应用需要判断绝对位置,ARR应该设成4倍线数减1。比如500线编码器,四倍频后每转2000个计数,ARR设1999。这样CNT从0到1999正好对应一圈,读CNT就能知道当前角度。
但如果只是测速度或者做相对位置控制,ARR可以设成最大值65535,扩大计数范围,减少溢出中断的频率。具体怎么选,取决于你的应用场景。我一般做位置控制时ARR设成4倍线数减1,做速度控制时设65535。
CNT初始值设成0就行。不过有个细节:如果编码器上电时不是零位,CNT的初始值就是随机的。要获得绝对位置,需要配合Z相信号做校准。
3.3 编码器接口模式的寄存器配置
配置编码器模式主要涉及TIMx_CCMR1、TIMx_CCER、TIMx_SMCR这几个寄存器。用标准库或者HAL库的话,有现成的函数可以调用,但理解底层寄存器有助于排查问题。
CCMR1的CC1S和CC2S位要设成01,把CH1和CH2配置成输入模式,并映射到TI1和TI2。CCER的CC1P和CC2P位决定边沿极性,一般设成0,表示上升沿有效。但具体要看编码器输出极性,如果发现计数方向反了,可以把其中一个通道的极性取反。
SMCR的SMS位设成001,选择编码器模式1。编码器模式1和模式2的区别在于计数边沿:模式1只在TI1和TI2的上升沿和下降沿都计数,模式2只在其中一个通道的边沿计数。要实现四倍频,必须选模式1。SMS设成010是模式2,只能实现二倍频。
还有个TI1S位,用来交换TI1和TI2的输入。如果发现A、B接反了,不用改硬件接线,把这个位置1就能交换。
4. 从零搭建四倍频计数工程的完整流程
4.1 工程框架与初始化顺序
我习惯用标准库来配置,因为寄存器操作直观,出了问题好查。HAL库虽然封装得好,但编码器模式这块的抽象层有时候会掩盖细节。下面以TIM3为例,走一遍完整的初始化流程。
初始化顺序很重要:先开时钟,再配GPIO,然后配定时器,最后使能。顺序错了可能导致配置不生效。具体步骤是:RCC_APB1PeriphClockCmd开TIM3时钟,RCC_APB2PeriphClockCmd开GPIOA和AFIO时钟。然后配置PA6和PA7为浮空输入或者上拉输入。编码器输出一般是推挽的,STM32这边设成浮空输入就行,如果信号线较长,设成上拉输入抗干扰更好。
GPIO配好后,调用TIM_DeInit把TIM3复位到默认状态,避免之前的配置残留。然后配置时基单元:TIM_TimeBaseStructure.TIM_Period设成1999,TIM_Prescaler设成0,TIM_ClockDivision设成0,TIM_CounterMode设成TIM_CounterMode_Up。虽然编码器模式下计数方向由硬件控制,但CounterMode还是得填一个值,填Up就行。
4.2 编码器接口的关键配置代码
时基配完后,调用TIM_EncoderInterfaceConfig函数。这个函数的参数是:TIM3、TIM_EncoderMode_TI12、TIM_ICPolarity_Rising、TIM_ICPolarity_Rising。第一个参数选定时器,第二个选编码器模式1,后面两个是通道极性。
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);这行代码背后做了几件事:设置SMCR的SMS位为001,设置CCMR1的CC1S和CC2S为01,设置CCER的CC1P和CC2P为0。如果发现方向反了,把其中一个极性改成Falling就行。
接下来配置输入滤波。编码器信号在电机干扰下可能会有毛刺,滤波参数设大一点更稳。调用TIM_ICInit分别配置CH1和CH2,ICFilter设成0x0F,也就是最大滤波。滤波的原理是采样多个时钟周期,只有连续多次采样值相同才认为信号有效。滤波越强,抗干扰越好,但会引入延迟。对于高转速场合,滤波太强可能导致丢脉冲,需要权衡。
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; TIM_ICInit(TIM3, &TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; TIM_ICInit(TIM3, &TIM_ICInitStructure);最后调用TIM_Cmd使能定时器。注意,编码器模式下不需要开中断就能计数,CNT会自动变化。如果需要溢出中断,再单独配置TIM_ITConfig。
4.3 读取计数值与方向判断
读CNT很简单,调用TIM_GetCounter就行。但要注意CNT是16位的,读的时候如果正好发生溢出,可能读到错误的值。稳妥的做法是读两次,如果两次差值不大就认为有效。或者用定时器的溢出中断来维护一个32位的扩展计数器。
方向判断可以通过读CR1寄存器的DIR位,或者通过比较两次CNT的差值。DIR位为0表示向上计数,为1表示向下计数。不过DIR位反映的是最近一次计数方向,如果编码器停下来了,DIR位保持最后的状态。
int16_t cnt = TIM_GetCounter(TIM3); uint8_t dir = (TIM3->CR1 & TIM_CR1_DIR) ? 1 : 0;如果要计算速度,可以固定时间间隔读一次CNT,算差值。差值除以时间就是速度。注意差值是有符号的,正负代表方向。用int16_t来存差值,避免溢出。
5. 实际调试中绕不开的六个坑
5.1 计数方向与预期相反
这是最常见的问题。接好线一读,正转时CNT在减小,反转时在增大。原因通常是A、B相接反了,或者极性配置反了。解决办法有两个:一是把A、B两线对调,二是把其中一个通道的极性从Rising改成Falling。我一般选后者,不用动硬件。
改极性的时候注意,TIM_EncoderInterfaceConfig里的两个极性参数要改一个,同时TIM_ICInit里的对应通道极性也要改。两处都改才生效,只改一处可能被后面的TIM_ICInit覆盖。
5.2 计数值抖动或跳变
编码器静止时CNT偶尔跳一下,或者低速时计数不均匀。这通常是信号毛刺引起的。先检查硬件:编码器电源是否稳定,信号线是否远离电机线,屏蔽层是否接好。硬件没问题的话,加大输入滤波。ICFilter从0x0F往下调,找到既能滤掉毛刺又不丢脉冲的档位。
还有一种可能是编码器本身的分辨率太低,低速时每个脉冲间隔很长,CNT变化看起来就是跳变的。这种情况只能换更高线数的编码器,或者用软件做插值。
5.3 高速时丢脉冲
电机转速一高,CNT就不准了。先算一下脉冲频率:转速乘以线数乘以4。比如3000转每分钟,500线编码器,脉冲频率是3000/605004=100kHz。这个频率对STM32来说不算高,TIM3挂在APB1上,时钟72MHz,处理100kHz绰绰有余。
丢脉冲更可能是滤波太强导致的。滤波会引入延迟,如果延迟超过脉冲间隔,就会漏掉边沿。把ICFilter调小试试。另外检查一下GPIO的驱动能力,如果编码器输出是开漏的,需要加上拉电阻。
5.4 溢出处理不当导致位置丢失
CNT是16位的,最大65535。如果ARR设成65535,转几圈就溢出了。溢出时如果不处理,位置信息就丢了。解决办法是用溢出中断维护一个32位或64位的扩展计数器。每次溢出中断里,根据DIR位判断是上溢还是下溢,扩展计数器加1或减1。
void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { if (TIM3->CR1 & TIM_CR1_DIR) encoder_overflow--; else encoder_overflow++; TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } }扩展计数器的类型要用有符号的,int32_t或者int64_t。读位置的时候把扩展计数器和CNT拼起来:position = encoder_overflow * 65536 + CNT。
5.5 Z相信号校准的时机问题
Z相信号每转一圈来一个,可以用来校准绝对位置。但Z相脉冲很窄,如果直接用中断捕获,可能因为中断延迟错过。更好的做法是用TIM的输入捕获功能,把Z相接到CH3或CH4上,配置成捕获模式。捕获到Z相时,把CNT清零或者记录当前值。
校准的时机也有讲究。最好在电机低速运转时校准,高速时Z相脉冲间隔太短,容易误触发。校准一次之后,只要不丢脉冲,绝对位置就一直准确。
5.6 多个定时器同时使用时的资源冲突
一个STM32芯片上通常有多个定时器支持编码器模式,比如TIM1、TIM2、TIM3、TIM4、TIM5、TIM8。如果项目需要接多个编码器,要注意引脚复用冲突。比如TIM2和TIM5的CH1都可能在PA0上,不能同时用。
选定时器的时候先列一个引脚分配表,把每个编码器的A、B、Z相都规划好。优先选引脚不冲突的定时器组合。如果实在不够用,可以考虑用外部计数芯片扩展,或者降低采样要求用软件解码。
6. 进阶技巧与性能优化
6.1 用DMA自动搬运计数值
如果CPU需要频繁读CNT,比如做高速位置闭环,可以用DMA把CNT自动搬到内存里。配置一个定时器触发DMA请求,每次更新事件时把CNT搬到缓冲区。这样CPU不用轮询,只在需要的时候处理缓冲区数据就行。
DMA搬运的触发源选TIM3的更新事件。注意更新事件在溢出时产生,如果ARR设得小,更新频率会很高,DMA带宽可能不够。ARR设大一点,或者用比较事件触发。
6.2 速度计算的滤波与平滑
直接用CNT差值算速度,噪声很大。我一般用滑动平均或者一阶低通滤波。滑动平均取最近N次的差值求平均,N取5到10比较合适。一阶低通滤波的系数根据截止频率算,公式是alpha = dt / (RC + dt)。
速度计算还有个细节:低速时差值很小,量化误差大。可以用M/T法,同时测脉冲数和时间,低速时用测时间的方式,高速时用测脉冲的方式。这样全速段都能保持较好的精度。
6.3 编码器断线检测
编码器线断了,CNT就不变了,但程序可能不知道。可以加一个断线检测:如果电机在转动但CNT长时间不变,就报故障。检测方法可以比较速度指令和实际速度,偏差超过阈值就报警。或者监测A、B两路的电平,如果两路同时为高或同时为低超过一定时间,说明信号异常。
这个功能在安全要求高的场合很有必要,比如机器人关节或者医疗设备。实现起来不复杂,但能避免很多意外。
7. 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 计数方向反 | A、B相接反或极性配置错 | 正转时读DIR位 | 对调A、B线或改一个通道极性 |
| 静止时CNT跳动 | 信号毛刺 | 示波器看A、B波形 | 加大ICFilter,检查屏蔽和接地 |
| 高速丢脉冲 | 滤波太强或GPIO驱动不足 | 测脉冲频率,看波形边沿 | 减小ICFilter,加上拉电阻 |
| 溢出后位置错乱 | 未处理溢出中断 | 检查是否开了更新中断 | 用扩展计数器维护高位 |
| Z相校准不准 | 捕获延迟或高速误触发 | 示波器看Z相和CNT关系 | 低速校准,用输入捕获而非中断 |
| 多定时器引脚冲突 | 引脚复用重叠 | 查数据手册引脚分配表 | 重新规划定时器和引脚 |
8. 我在实际项目里总结的几条经验
编码器四倍频这个功能,配置本身不难,难的是在实际电磁环境里稳定运行。我做过一个AGV小车的项目,电机和编码器线捆在一起走,一开始CNT跳得没法用。后来把编码器线单独走屏蔽槽,屏蔽层在控制器端单点接地,问题就解决了。所以硬件布线的重要性不亚于软件配置。
还有一次用TIM4做编码器,发现读出来的值总是比预期少一点。查了半天才发现是ARR设成了2000,但四倍频后每转是2000个计数,CNT从0到1999正好2000个状态,ARR应该设1999。这个差1的错误很隐蔽,但会导致每转累积一个计数的误差。
滤波参数我一般从0x0F开始试,如果高速丢脉冲就降到0x08或0x04。没有万能的参数,得根据实际编码器型号和电机转速来调。示波器是必备工具,看波形比猜原因快得多。
最后提一句,如果项目对成本不敏感,可以考虑用带正交解码硬件的专用芯片,比如某些磁编码器或者霍尔编码器,直接输出SPI或I2C的位置数据,省去STM32这边的配置和调试。但对于成本敏感的场合,STM32的TIMx编码器模式依然是性价比最高的方案。