STM32定时器编码器四倍频原理与配置实战
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介入每次中断都有进出栈开销。编码器转速高的时候脉冲频率可能达到几百kHzCPU根本来不及处理。而且外部中断只能捕获单相边沿要判断方向还得读另一相电平软件逻辑复杂容易出错。STM32的TIMx编码器接口模式是纯硬件解码不占用CPU资源。计数器在后台自动加减你只需要在需要的时候读一下CNT寄存器就行。这对于需要同时处理电机控制、通信、显示等多任务的系统来说优势非常明显。另外TIMx还支持输入滤波可以滤掉编码器信号上的毛刺这是外部中断很难做到的。3. STM32编码器接口模式的配置细节3.1 硬件连接与引脚选择STM32的编码器接口模式对引脚有固定要求TI1和TI2必须映射到定时器的CH1和CH2通道。以常见的TIM3为例CH1对应PA6或PB4CH2对应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设成1999TIM_Prescaler设成0TIM_ClockDivision设成0TIM_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和CH2ICFilter设成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/605004100kHz。这个频率对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编码器模式依然是性价比最高的方案。