资讯动态

STM32增量式编码器速度采集与PWM闭环控制实战指南

发布时间:2026/10/27 1:01:32 来源:尧图企业网站定制
1. 从一次速度读数抖动说起编码器到底给了我们什么很多人第一次用STM32接增量式编码器代码跑通的那一刻是兴奋的——定时器编码器模式一开CNT寄存器里的数字随着轮子转动涨涨跌跌看起来一切正常。但紧接着问题就来了把CNT的值换算成速度显示在屏幕上数字跳得厉害静止时也不归零低速时甚至方向都反了。这时候你才会意识到编码器读到的到底是什么这个问题远比想象中复杂。我自己最早做平衡车的时候就栽在这个坑里。当时用的是常见的霍尔增量式编码器A/B两相输出接到STM32的TIM3和TIM4上配置成编码器模式。代码逻辑很简单每隔10ms读一次CNT减去上次的值除以时间乘以一个系数就得到了速度。理论上没毛病但实际跑起来电机静止时速度读数在±5之间乱跳低速运行时噪声几乎淹没了真实信号。后来花了整整两天才搞明白问题出在我对脉冲和速度之间那层转换关系的理解太粗糙了。这篇文章想做的事情就是把编码器读到的到底是什么这件事从头到尾讲清楚。从物理层面的脉冲信号到MCU定时器如何捕获这些脉冲再到如何把脉冲数转换成可信的速度值最后到PWM调速闭环中速度反馈的正确使用方式。适合正在做电机控制、机器人底盘、CNC进给等需要编码器反馈的开发者也适合那些代码能跑但数据不能用的朋友。我会尽量把每个环节的为什么讲透而不是只丢一段配置代码给你。2. 脉冲的本质A/B两相正交信号里藏着什么信息2.1 正交编码的物理层逻辑增量式编码器输出的A/B两相方波本质上是一组相位差90度的脉冲序列。这个90度的相位差不是随便定的它同时编码了两个维度的信息位置变化量和运动方向。当A相超前B相90度时代表正转当B相超前A相90度时代表反转。每转动一个固定的机械角度A/B两相各输出一个完整的方波周期这个周期对应的机械角度就是编码器的分辨率。举个具体的例子。一个标称500线的增量式编码器意思是电机轴每转一圈A相输出500个完整脉冲周期。但如果你在STM32里配置定时器为编码器模式并开启四倍频计数那么CNT寄存器每转一圈会变化2000。这个4倍频是怎么来的因为A相的上升沿、下降沿B相的上升沿、下降沿每个边沿都可以作为一个计数触发点。STM32的编码器接口模式支持对A/B两相的边沿都进行计数所以实际分辨率是标称线数的4倍。这里有个容易混淆的概念线数Lines和脉冲数Pulses。有些厂家标的是PPRPulses Per Revolution有些标的是LPRLines Per Revolution。如果标的是500 PPR通常指的是A相或B相单独一相的脉冲数四倍频后就是2000个计数。如果标的是500 LPR那四倍频后也是2000。但有些国产编码器标注混乱买回来最好自己手动转一圈数一下CNT的变化量这是最靠谱的验证方法。2.2 定时器编码器模式的硬件计数机制STM32的定时器编码器模式核心原理是把TIMx_CH1和TIMx_CH2配置成编码器接口的输入通道硬件自动根据两相的边沿关系和电平状态决定CNT是加还是减。这个过程完全由硬件完成不需要CPU干预所以即使主循环很忙也不会丢脉冲。配置的时候有几个关键寄存器位需要注意。以STM32F103的TIM3为例SMCR寄存器的SMS位设置为001或010或011分别对应仅在TI1计数、仅在TI2计数、在TI1和TI2都计数。最后一种就是四倍频模式。CCMR1寄存器的CC1S和CC2S位设置为01把通道配置成输入模式并映射到TI1和TI2。CCER寄存器的CC1P和CC2P位决定是否反相输入这个在硬件布线A/B接反的时候可以用来软件纠正不用重新焊线。还有一个细节输入滤波。编码器信号在长线传输或者电机干扰大的环境下边沿会有毛刺导致CNT多计数。STM32的CCMR寄存器的IC1F和IC2F位可以配置数字滤波器从0到15共16档对应不同的采样频率和滤波长度。我一般会在电机驱动场景下设置IC1F0x0F也就是最长滤波牺牲一点响应速度换取计数稳定性。这个取舍在低速高精度场景下尤其重要。2.3 为什么CNT的值不能直接当速度用CNT寄存器本质上是一个位置计数器它记录的是从上次清零到现在累计的脉冲数。要得到速度你需要的是单位时间内的脉冲变化量。这就引出了两个问题采样周期怎么定以及CNT溢出怎么处理。采样周期太短比如1ms低速时脉冲变化量可能只有0到1个计数量化误差极大速度读数会剧烈抖动。采样周期太长比如100ms虽然量化误差小了但速度反馈的延迟变大闭环控制会变得迟钝甚至振荡。我一般会根据电机的最高转速和编码器分辨率来算一个折中值。假设电机最高3000RPM编码器四倍频后2000计数/圈那么最高转速下每秒的计数变化是3000/60*2000100000。如果采样周期10ms最高速时每次采样变化1000个计数低速比如30RPM时变化10个计数量化误差约10%勉强可用。如果要求低速精度更高就需要更长的采样周期或者更高的编码器分辨率。CNT溢出是另一个必须处理的问题。STM32的CNT是16位或32位16位定时器在高速下很快就会溢出。比如100000计数/秒的情况下16位CNT最大值65535不到一秒就溢出了。所以读取CNT的时候必须用有符号数处理并且每次读取后及时更新上次值。我通常的做法是定义一个int16_t或int32_t的变量存储上次CNT每次读取时用当前CNT减去上次CNT结果强制转换为有符号数这样即使CNT从65535跳到0差值仍然是正确的比如0-65535-65535在int16_t下就是1表示正转一个计数。这个技巧在处理16位定时器时非常关键很多人在这里翻车。3. 从脉冲计数到速度值几种主流方案的取舍3.1 M法、T法、M/T法到底怎么选把脉冲数转换成速度常见的有三种方法。M法是在固定时间T内数脉冲个数速度正比于脉冲数除以T。T法是测量两个相邻脉冲之间的时间间隔速度正比于1除以时间。M/T法是结合两者在固定时间内同时测量脉冲数和最后一个脉冲的宽度低速时用T法保证精度高速时用M法保证响应。M法实现最简单适合高速场景。因为高速时固定时间内的脉冲数多量化误差相对小。但低速时脉冲数少误差就大了。T法适合低速因为低速时脉冲间隔长测量精度高但高速时脉冲间隔太短定时器分辨率不够。M/T法理论上兼顾两者但实现复杂需要额外的定时器捕获功能。我在实际项目中用得最多的是M法因为大多数电机控制场景对低速精度的要求没有想象中那么苛刻而且M法代码简单、计算量小。如果确实需要低速高精度我会优先考虑提高编码器分辨率或者加长采样周期而不是切换到T法。因为T法在高速时的问题同样棘手而且切换逻辑容易引入bug。3.2 采样周期的计算与实测调整采样周期的选择需要平衡三个因素量化误差、响应延迟、计算负载。量化误差方面假设你希望速度分辨率达到满量程的1%那么采样周期内最少要有100个计数变化。按前面的例子最高速100000计数/秒1%就是1000计数对应10ms采样周期。响应延迟方面采样周期越长速度反馈越滞后闭环带宽越低。一般电机控制的电流环在10kHz以上速度环在1kHz左右所以速度采样周期在1ms到10ms之间比较合理。我通常的做法是先用10ms跑起来观察速度波形的噪声和响应。如果噪声大适当加长到20ms如果响应慢缩短到5ms。同时配合软件滤波比如一阶低通滤波或者滑动平均。一阶低通的公式是filtered filtered * (1-alpha) raw * alphaalpha越小滤波越强但延迟越大。我一般取alpha0.3到0.5之间根据实际波形调整。这里有个经验不要试图用滤波解决所有噪声问题。如果原始速度信号噪声极大先检查硬件比如编码器供电是否干净、信号线是否屏蔽、地线是否单点接地。软件滤波是最后的手段过度滤波会掩盖真实的动态特性让闭环调试变得困难。3.3 方向判断与符号处理速度是有方向的正转和反转必须区分。在STM32的编码器模式下CNT的增减本身就反映了方向所以用有符号数处理差值就能自然得到方向。但要注意如果A/B相接反了方向就会相反。这时候可以在软件里取反或者配置CCER寄存器的CC1P/CC2P位交换输入极性。还有一个坑静止时的方向抖动。当电机静止但编码器信号有噪声时CNT可能会在0附近微小波动导致速度读数正负跳变。这时候可以加一个死区比如速度绝对值小于某个阈值时强制置零。阈值的大小根据编码器分辨率和采样周期来定一般取满量程的0.5%到1%。4. PWM调速闭环中速度反馈的正确接入方式4.1 PWM频率与编码器采样的相互干扰PWM调速和编码器采样之间有一个容易被忽略的耦合PWM开关噪声会耦合到编码器信号线上。尤其是电机线和大电流走线靠近编码器线时PWM的边沿会在编码器信号上产生尖峰导致多计数。我遇到过最严重的情况是PWM频率20kHz编码器线没有屏蔽结果电机一转速度读数就乱跳静止时反而正常。解决办法有几个层次。硬件层面编码器线用双绞屏蔽线屏蔽层单端接地电机线尽量远离信号线编码器供电加LC滤波。软件层面开启STM32输入滤波适当加长采样周期加死区。还有一个技巧在PWM周期的特定时刻采样编码器。如果PWM和定时器同步可以在PWM计数器归零或者峰值时触发编码器读取避开开关噪声最大的时刻。这个需要定时器主从同步配置稍微复杂但效果明显。4.2 速度环PID的积分限幅与抗饱和有了速度反馈接下来就是PID闭环。速度环PID和位置环不同积分项容易饱和尤其是在启动或者堵转时。积分饱和的表现是电机启动时速度上不去PID输出一直增大等速度终于上来后又超调严重来回振荡。解决办法是积分限幅和积分分离。积分限幅是给积分项设一个上下限比如输出范围的50%。积分分离是当误差大于某个阈值时暂时关闭积分只用PD控制等误差小了再开启积分。我在速度环里一般用积分限幅就够了阈值取PWM满量程的30%到50%。另外PID输出也要限幅防止超过PWM的最大占空比。参数整定的顺序是先调P让速度能跟上给定但不振荡再调I消除稳态误差最后调D抑制超调。速度环的D项一般很小或者不用因为速度信号本身噪声大微分会放大噪声。如果非要用D一定要先对速度做低通滤波。4.3 低速死区与启动策略低速时编码器分辨率不足速度反馈不准闭环容易振荡。这时候可以加一个低速开环策略当给定速度低于某个阈值时直接给一个固定的PWM占空比不依赖速度反馈。等速度超过阈值后再切入闭环。这个阈值根据编码器分辨率和采样周期来定一般取能保证每个采样周期至少有5到10个计数变化的速度值。启动策略也很关键。电机从静止启动时速度反馈为零PID误差最大输出直接饱和。这时候如果积分项没有限幅就会积累很大的积分值导致启动后超调。我一般会在启动时先给一个固定的启动占空比等速度起来后再切闭环同时清零积分项。这个预启动的占空比根据电机和负载来定一般取能克服静摩擦的最小值。5. 实测中那些文档不会告诉你的坑5.1 编码器供电与信号电平匹配编码器常见的有5V供电和3.3V供电两种。STM32的IO是3.3V电平如果编码器是5V输出直接接上去短期可能能用但长期会损坏IO或者导致计数不稳定。正确的做法是用电平转换芯片或者用电阻分压高速时分压电阻会影响边沿速度不推荐。如果编码器是集电极开路输出还需要加上拉电阻阻值根据编码器手册和信号频率来选一般1k到10k之间。还有一个隐蔽的问题编码器电源和MCU电源的共地。如果编码器和MCU用不同的电源地线之间会有电位差导致信号电平判断错误。必须确保两者共地而且地线要足够粗减少地弹。我遇到过编码器单独供电但地线没接的情况结果CNT计数完全随机查了半天才发现是地线问题。5.2 定时器资源冲突与中断优先级STM32的定时器资源有限编码器模式会占用一个完整的定时器。如果你同时需要PWM输出、编码器输入、系统滴答定时器就要仔细分配。比如TIM1做PWMTIM2做编码器TIM3做另一个编码器TIM4做系统定时。注意有些定时器是16位的有些是32位的32位定时器做编码器更不容易溢出但数量少。中断优先级也要注意。编码器读取一般放在定时器中断或者主循环里如果放在中断里优先级不要设得太高否则会影响其他关键中断。我一般把编码器采样放在1kHz的定时器中断里优先级设为中等PWM更新放在主循环或者另一个中断里。5.3 机械安装与同心度问题编码器是精密器件机械安装的同心度直接影响信号质量。如果编码器轴和电机轴不同心转动时会有周期性抖动导致速度读数波动。联轴器要选弹性联轴器不要用刚性联轴器。编码器支架要稳固不能有松动。我见过因为编码器支架螺丝没拧紧导致高速时编码器晃动速度读数周期性跳变查了好久才发现是机械问题。还有一个细节编码器的分辨率要和机械减速比匹配。如果减速比很大电机转很多圈输出轴才转一圈那么编码器装在电机端还是输出端效果完全不同。装在电机端分辨率高但需要乘以减速比装在输出端分辨率低但直接反映最终速度。一般建议装在电机端因为电机端转速高编码器信号频率高抗干扰能力相对好而且可以同时用于电机换相和速度反馈。6. 一套可复用的速度采集与处理代码框架6.1 定时器编码器模式初始化以STM32F103的TIM3为例配置成四倍频编码器模式代码如下void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 65535; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); 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(TIM3, ENABLE); }这段代码的关键点TIM_EncoderMode_TI12开启四倍频TIM_ICFilter 0x0F开启最大输入滤波GPIO配置为浮空输入如果编码器是推挽输出或者上拉输入如果是集电极开路。6.2 速度计算与滤波的完整实现#define ENCODER_RESOLUTION 2000.0f #define SAMPLE_PERIOD_MS 10 #define SPEED_FILTER_ALPHA 0.4f #define DEAD_ZONE 5 static int16_t last_cnt 0; static float filtered_speed 0.0f; float Get_Speed(void) { int16_t curr_cnt (int16_t)TIM_GetCounter(TIM3); int16_t diff curr_cnt - last_cnt; last_cnt curr_cnt; float raw_speed (float)diff / ENCODER_RESOLUTION * (1000.0f / SAMPLE_PERIOD_MS) * 60.0f; if (raw_speed -DEAD_ZONE raw_speed DEAD_ZONE) { raw_speed 0.0f; } filtered_speed filtered_speed * (1.0f - SPEED_FILTER_ALPHA) raw_speed * SPEED_FILTER_ALPHA; return filtered_speed; }这段代码里diff用int16_t处理自动处理了CNT溢出。raw_speed的单位是RPM。死区设为5RPM滤波系数0.4。实际使用时Get_Speed放在10ms定时器中断里调用结果存入全局变量供PID使用。6.3 参数整定的实操记录以一款24V、3000RPM、带500线编码器的直流无刷电机为例我的整定记录如下参数初始值调整后说明采样周期10ms10ms低速噪声可接受滤波系数0.50.4响应和噪声平衡死区05RPM消除静止抖动Kp0.51.2响应加快但不振荡Ki0.010.05消除稳态误差Kd00速度噪声大不用D积分限幅无50%防止启动超调整定过程中Kp从0.5开始每次增加0.2直到速度响应出现轻微超调然后回调到超调消失。Ki从0.01开始每次增加0.01直到稳态误差在1秒内消除。积分限幅设为PWM满量程的50%启动时先给20%占空比预启动速度超过50RPM后切闭环。7. 几个容易被忽略的边界情况7.1 编码器断线检测编码器线断了CNT不再变化速度读数恒为零。如果这时候闭环还在运行PID会以为速度为零不断增大输出导致电机飞车。所以必须加断线检测。简单的方法是如果PWM输出大于某个阈值但速度持续为零超过一定时间就判定为编码器故障关闭PWM输出并报警。更可靠的方法是用编码器的Z相零位信号或者检查A/B相是否有边沿变化。有些编码器还有故障输出引脚可以直接读取。我在安全要求高的场合会用双编码器或者加装独立的转速传感器做冗余。7.2 高速时的计数溢出与定时器频率前面提到16位CNT在高速下会溢出用有符号数处理可以解决。但还有一个问题如果定时器的时钟频率不够高高速时脉冲频率超过定时器采样率会丢计数。STM32的定时器时钟一般是72MHz编码器模式下每个边沿触发一次计数理论上能处理几十MHz的脉冲。但实际受限于输入滤波和GPIO速度一般能稳定处理到1MHz左右。如果编码器脉冲频率超过这个值就需要降低编码器分辨率或者用硬件分频。7.3 多圈绝对位置与增量式的选择增量式编码器只能测相对位置断电后位置丢失。如果需要断电记忆位置要么用绝对式编码器要么加电池备份或者上电回零。绝对式编码器有单圈和多圈之分多圈绝对式可以记录圈数但价格贵。磁编码器如AS5047P是单圈绝对式分辨率高接口简单SPI或ABI在云台、关节控制等场景很受欢迎。选型的时候要根据是否需要断电记忆、分辨率要求、接口资源来综合决定。8. 写在最后一些个人体会做电机控制这些年我越来越觉得编码器这块的难点不在代码而在对物理过程的理解。CNT寄存器的数字只是表象背后是机械运动、电磁干扰、信号完整性、实时调度等一系列问题的综合。很多人代码抄过去能跑但一出问题就抓瞎就是因为没有建立起从脉冲到速度的完整认知链条。如果让我给初学者一个建议那就是先用手转编码器用示波器看A/B波形用调试器看CNT变化把最基础的现象观察清楚再去写代码。这个过程花不了多少时间但能帮你避开后面80%的坑。另外速度环的参数没有万能值每台电机、每个负载都不一样耐心整定记录数据比到处找最优参数靠谱得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑