ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32定时器编码器模式:四倍频测速原理与CubeMX+HAL实战

STM32定时器编码器模式:四倍频测速原理与CubeMX+HAL实战 之前有个做运动控制项目的朋友跟我吐槽他用外部中断读增量编码器电机转速一上 3000 RPM 就开始丢步方向判断偶尔还会抽风。我一看代码两个通道的边沿中断全开了ISR 里还要读另一个通道的电平去判方向A/B 相靠得越近中断嵌套越乱CPU 忙得脚不沾地还不讨好。其实 STM32 早就把这活儿用硬件干了就是定时器的编码器模式。尤其是 H743 这颗芯片定时器资源又多又强配合 CubeMX 配一个“四倍频测速”十分钟就能跑起来全程 CPU 零参与。这篇文章我就把这套方案从头到尾拆一遍原理是什么、四倍频怎么来的、CubeMX 怎么点、HAL 库代码怎么写、实际跑起来会踩什么坑一次性说透。适合正在做小车、机械臂、云台、传送带这类需要测转速或者数位置的场景的嵌入式开发者参考。1. 先搞清原理编码器模式为什么比外部中断香1.1 外部中断测速的固有缺陷用外部中断测增量编码器思路很直观A 相或 B 相来一个上升沿就进一次中断在中断里计数。听起来简单但工程上全是问题。首先中断频率和转速成正比。一个每转输出 1000 脉冲的编码器在 3000 RPM 下每秒要产生 50000 个上升沿也就是 50 kHz 的中断频率。如果再用两相配合做四倍频每秒中断直接到 200 kHz。每次中断少说几十个周期光进出中断的开销就把 CPU 吃掉了大半。更致命的是中断里如果还干了别的事比如读引脚、判方向、累加计数这个时间一抖动边沿就漏了测出来的转速忽高忽低。其次方向判断有天然竞态。外部中断方案里通常是在 A 相中断里去读 B 相电平电平高算正转电平低算反转。问题是如果 B 相边沿恰好和 A 相边沿挨得很近你读到的那一瞬间可能 B 相已经跳变了方向就判反了。这种错是随机性的排查起来非常头疼。第三中断里做计数还涉及 volatile 变量共享问题。主循环读计数、中断改计数如果不同步好读到的可能是半新半旧的数据。加临界区保护吧中断已经很高频了再加保护等于雪上加霜。1.2 定时器的编码器接口硬件替你把脏活干了STM32 定时器里的编码器模式本质上是一个硬件正交解码器。A 相和 B 相直接接到定时器的两个输入通道上硬件自己检测边沿、自己根据两相的电平关系判断方向、自己决定计数器加一还是减一。整个过程中 CPU 完全不需要参与计数器自动反映编码器的位置和方向你只需要隔一段时间去读一次 CNT 寄存器算出差值就能得到速度。这种方式的好处太多了。一是零中断开销转速再高也不会打断 CPU 的正常工作。二是方向判断是硬件在边沿发生的同一时刻完成的不存在软件读电平的竞态问题方向结果天然可靠。三是计数过程没有软件延迟边沿来了就立即反应测速精度远高于中断方案。另外硬件编码器接口还自带输入滤波可以滤掉机械开关抖动或者线路噪声带来的毛刺。这一点在电机驱动这种强干扰环境里尤其重要软件外部中断想要达到同样的抗干扰效果得额外做好几层处理。2. 四倍频是怎么来的A/B 相正交信号与计数机制2.1 增量式编码器的 A/B 相与 PPR先明确一个基础概念。增量式编码器输出两路方波信号一般叫 A 相和 B 相两路信号相位相差 90°所以也叫正交信号。电机转一圈A 相输出的方波个数就是编码器的 PPRPulse Per Revolution常见的有 11、13、100、360、1000、2048 这些值。PPR 越高分辨率越高但相应的信号频率也越高。两路正交信号的价值在于不仅可以通过脉冲数知道转了多少角度还能通过两相的相位关系判断旋转方向。比如 A 相超前 B 相 90° 是正转那 B 相超前 A 相就是反转。硬件解码器就是根据这个相位关系决定计数器的增减方向。2.2 模式3计数表每个边沿都不白给STM32 定时器的编码器模式有三种工作模式分别对应 SMS 控制位的 001、010、011模式1SMS001只在 A 相边沿计数B 相只看电平判方向。模式2SMS010只在 B 相边沿计数A 相只看电平判方向。模式3SMS011A 相和 B 相的上升沿、下降沿全部参与计数也就是每个完整正交周期数 4 下。所谓“四倍频”就是工作在模式3。因为编码器一个完整周期里两相有 4 个边沿每个边沿都让计数器加一或减一所以硬件能把每个脉冲拆成 4 个计数步进。这就是四倍频测速的核心。CubeMX 里没有直接标“模式3”这个说法它把 Combined Channels 设置为 Encoder Mode 时生成的就是TIM_ENCODERMODE_TI12对应定时器的编码器模式3也就是四倍频。这一点要记牢别在代码里手滑改成TIM_ENCODERMODE_TI1或TIM_ENCODERMODE_TI2不然你数出来的数值直接变成原来的四分之一或者二分之一。2.3 从四倍频计数到实际转速的换算假设编码器 PPR 为 N工作在四倍频模式那么电机转一圈计数器变化的总量为counts_per_rev N × 4比如编码器是 11 PPR四倍频后每转就是 44 个计数步。如果在采样时间 T单位秒内计数器变化了 D 个步进那么转速就是rev_per_sec D / (N × 4) / T rpm D × 60 / (N × 4 × T)举个例子PPR11T0.1 秒读到的 D440那么转速就是rpm 440 × 60 / (44 × 0.1) 6000 RPM轮子线速度同理把“每转的周长”代入就行speed_mps D × 轮周长 / (N × 4 × T)这里最需要注意的是单位的坑。很多人算出来数字不对就是 PPR 混用了。有的编码器标的是“线数”有的是“脉冲数”还有的是“极对数”一定要看数据手册确认 A 相一旋转圈数多少个完整方波。3. CubeMX 保姆级配置3.1 引脚规划与 GPIO 复用先用 CubeMX 选好芯片型号比如 STM32H743VIT6 或 ZIT6然后打开 Pinout Configuration 界面。编码器 A、B 两相要接到同一个定时器的两个通道上。比如用 TIM3那 A 相接 TIM3_CH1 对应的引脚B 相接 TIM3_CH2 对应的引脚。H743 的引脚复用很灵活TIM3_CH1 可能同时出现在多个引脚上CH2 也是但要注意选的两个引脚最好挨得近方便布线。在 CubeMX 里点一个引脚把功能选为 TIM3_CH1它会自动复用。需要注意有些引脚默认接了 ADC、DAC 或者其他外设如果被占用引脚颜色会变红这时候就要换引脚或者关闭冲突外设。我习惯的做法是先看数据手册的 AF 表格确定一对不冲突的引脚再回到 CubeMX 里配置。3.2 TIM 编码器参数逐项讲解选中 TIM3在 Mode 面板里找到 Combined Channels下拉选择 Encoder Mode。这一步是核心选好之后下方会出现一系列参数配置项。各参数建议如下参数项推荐值说明Prescaler0编码器模式下不要分频否则丢计数Counter ModeUp编码器方向由硬件自动决定此项不影响Counter Period65535设满量程配合 uint16_t 读取最方便Auto-reload preloadDisable可以禁用溢出中断用不到预装载Encoder ModeTI1 and TI2这就是四倍频模式Input Filter IC1/IC20 或根据环境调见下方滤波说明IC1/IC2 Prescaler1不分频输入信号分频通常不用Counter Period 的设置很关键。如果填 65535计数器范围就是 0~65535正好对应 uint16_t 类型读取换算非常方便。有人喜欢按编码器每转计数设一个整数值比如每转四倍频后 1000 就把 ARR 设 999这样读数直接是位置但溢出处理会比较绕不太推荐新手这么做。3.3 输入滤波消除抖动和毛刺CubeMX 里编码器模式的 Input Filter 对应定时器的 ICxF 数字滤波功能。它的原理是输入信号必须在连续 N 个采样周期内保持稳定才认为是一次有效电平跳变否则视为抖动忽略掉。在电机控制场景里电机电刷火花、驱动板 PWM 串扰、接线太长引入的电磁干扰都可能让编码器信号产生毛刺。毛刺如果被计数器当成有效边沿测速就飘了。适当调大滤波值能明显改善。但滤波不是越大越好。滤波本质上是延迟边沿检测信号跳变后要等到采样满足条件才确认。如果编码器频率很高两个有效边沿间隔很短过大的滤波值可能把紧挨着的第二个边沿吞掉导致计数值偏低。转速越高能容忍的滤波值就越小。我的经验值普通直流电机霍尔编码器信号线 20cm 以内滤波值设 0 或者 1 就行布线超过半米或者电机启动瞬间干扰明显从 6~8 开始试工业长线编码器建议买差分输出型再接硬件差分接收器光靠滤波扛不住。3.4 使能更新中断做位置累计的关键如果只是周期性读 CNT 算速度更新中断不需要开。但如果你还要累计圈数、算绝对位置计数器只有 16 位一会儿就溢出回绕了这时候就得开定时器更新中断。在 CubeMX 的 NVIC Settings 里勾上 TIM3 global interrupt。更新事件会在计数器从 65535 跳回 0向上计数溢出或者从 0 跳回 65535向下计数下溢时触发。中断服务函数里根据当前计数方向把累计值加上或者减去一个满量程步长。速度测量本身不用中断但累计位置用中断两个需求可以同时满足。4. HAL 库代码实现4.1 初始化与启动CubeMX 生成工程后定时器初始化函数已经生成好了。在MX_TIM3_Init里编码器初始化的关键结构体长这样TIM_Encoder_InitTypeDef sEncoderConfig {0}; htim3.Instance TIM3; htim3.Init.Prescaler 0; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 65535; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; sEncoderConfig.EncoderMode TIM_ENCODERMODE_TI12; sEncoderConfig.IC1Polarity TIM_ICPOLARITY_RISING; sEncoderConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler TIM_ICPSC_DIV1; sEncoderConfig.IC1Filter 0; sEncoderConfig.IC2Polarity TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Prescaler TIM_ICPSC_DIV1; sEncoderConfig.IC2Filter 0; HAL_TIM_Encoder_Init(htim3, sEncoderConfig);在 main 函数里初始化完成后启动编码器接口HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL);如果是 H7 系列注意一点定时器时钟默认是开启的不需要像某些外设一样单独__HAL_RCC_TIM3_CLK_ENABLE()CubeMX 生成的代码里已经做了。启动之后TIM3-CNT会随着电机转动自动增减你不用管它。4.2 中断回调与溢出累计如果需要开更新中断还要在 NVIC 使能后实现回调函数。在stm32h7xx_it.c里已经有TIM3_IRQHandler调用了HAL_TIM_IRQHandler你只需要在用户文件里重写弱函数volatile int64_t g_encoder_position 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { if (__HAL_TIM_IS_TIM_COUNTING_DOWN(htim3)) { g_encoder_position - (int64_t)(htim3.Instance-ARR 1); } else { g_encoder_position (int64_t)(htim3.Instance-ARR 1); } } }这里__HAL_TIM_IS_TIM_COUNTING_DOWN读取的是 CR1 里的 DIR 位硬件在编码器模式下会根据 A、B 相相位自动维护这个位所以在更新中断里判断方向是可靠的。如果用的是老版本 HAL 库没有这个宏直接写(htim3.Instance-CR1 TIM_CR1_DIR)就行。有了这个累计值再配合当前 CNT 就可以随时得到 64 位的绝对位置。这个方案的精度取决于 ARR 的设置满量程 65536 步进足够绝大多数应用用了。4.3 周期采样测速代码测速最实用的方法是定时采样每 10ms 或者 50ms 去读一次 CNT用先后两次的差值算速度。这段代码可以放在定时器更新的回调里做也可以放在主循环配合HAL_GetTick()做。以 10ms 采样周期为例#define ENCODER_PPR 11 #define SAMPLE_MS 10 #define COUNTS_PER_REV (ENCODER_PPR * 4) static uint16_t last_cnt 0; static uint32_t last_tick 0; void Encoder_Speed_Task(void) { if (HAL_GetTick() - last_tick SAMPLE_MS) { uint16_t now_cnt __HAL_TIM_GET_COUNTER(htim3); int16_t diff (int16_t)(now_cnt - last_cnt); last_cnt now_cnt; float rpm (float)diff * 60.0f / (float)COUNTS_PER_REV / ((float)SAMPLE_MS / 1000.0f); printf(RPM: %.2f\r\n, rpm); last_tick HAL_GetTick(); } }这里最核心的一行是int16_t diff (int16_t)(now_cnt - last_cnt);这个写法不是随手写的它自动处理了 16 位计数器的回绕问题。比如上一次读到的计数值是 65530这次读到的值是 3如果直接算3 - 65530是负数但实际上是正转过了 9 个步进。把结果强转成int16_t后(int16_t)(3 - 65530)就得到了 9这就是有符号回绕运算的妙处。代价是单次采样间隔内的计数值变化不能超过 32767否则会溢出判错。按最大 4 倍频计数率保守估算10ms 采样间隔下只要每秒计数低于 327 万就不会出错实际电机编码器很难做到这个量级所以这个方案非常稳。4.4 方向判断与顺滑滤波方向不用额外读引脚直接看 diff 的正负。diff 大于 0 是正转小于 0 是反转。这点比外部中断方案清爽太多。但是机械安装会导致 A、B 两相接反这时候 diff 的符号正好反过来转速数值是对的方向反了。解决的办法有两个一是交换 A、B 接线二是把代码里now_cnt - last_cnt换成last_cnt - now_cnt或者在初始化时把 IC1Polarity 和 IC2Polarity 改成下降沿让硬件自动帮你反向。哪种都行看布线方便。如果算出来的转速跳变很大可以加一个一阶低通滤波static float rpm_filtered 0; const float alpha 0.6f; rpm_filtered rpm_filtered alpha * (rpm - rpm_filtered);alpha 越小越平滑但延迟越大。实际调参时先不滤波跑一下看原始数据波动幅度再根据控制需求决定 alpha不要上来就猛滤波把真实转速信息也滤没了。5. 实测调参与常见坑5.1 测速结果不对的排查速查表下面是我实际帮人调试时最常遇到的问题和排查思路整理成表建议收藏。现象可能原因排查与解决计数值完全不变化引脚复用不对、编码器模式没启动检查 A/B 相是否接对通道检查HAL_TIM_Encoder_Start是否调用数值倍频不对变成四分之一编码器模式误用成单通道确认 EncoderMode 为TIM_ENCODERMODE_TI12转速数值飘忽不定信号毛刺或采样时间过短调大 IC1Filter/IC2Filter或增大采样周期方向反了A/B 相接线反了交换 A/B 接线或修改 IC 极性反向高速时数值突然偏低滤波值太大吞边沿降低滤波值或检查信号线质量低速时读数不稳定机械抖动或编码器分辨率太低增加滤波和采样时间换更高 PPR 编码器位置累计方向错误溢出中断里方向判断逻辑反了调换__HAL_TIM_IS_TIM_COUNTING_DOWN分支的累加符号排查顺序我建议是“信号 → 配置 → 代码”。先用示波器或者逻辑分析仪看 A/B 相波形是否存在、正交关系是否正常、是否有毛刺再回头看 CubeMX 里的配置有没有选错模式最后才怀疑代码逻辑。5.2 我踩过的三个进坑实录第一个坑是 PWM 输出和编码器输入共用了一个定时器。当时想省事用一个定时器同时输出电机 PWM 和读编码器结果计数器被两路信号反复拉扯完全没法工作。定时器的编码器模式会占用整个定时器的通道不能和 PWM 输出同时复用。解决方法是重新检查引脚分配把编码器换到一个专门用于测速的定时器上。H743 定时器资源很丰富没必要省。第二个坑是 IC1Prescaler 或者 IC2Prescaler 被误设为 2 或者 4。刚接触时可能以为这是用来做四倍频的实际上定时器编码器模式本身就是硬件四倍频IC 分频反而会丢掉中间边沿导致实际计数减半再减半。正常编码器模式这两个值都必须保持TIM_ICPSC_DIV1不要动。第三个坑是中断回调里用了阻塞操作。我在溢出中断里加过一个串口打印日志的调试语句结果电机一转打印还没发完下一个更新中断就来了程序直接卡死。更新中断回调里必须做极轻量级的操作累加位置变量可以打印、延时不不行。5.3 关于采样周期和最大测速上限很多人在意测速上限本质上取决于采样周期和单次采样差值。用前面说的int16_t回绕判差方案单次采样差值不能超过 32767否则符号会判反。所以采样周期 T 和最大计数率 f_max 满足f_max × T 32767如果采样周期是 10ms那最大计数率约 327 万次/秒对应四倍频后每转 1000 计的编码器转速上限接近 196000 RPM。这个余量对绝大多数应用完全够。如果你面对的真是超高转速场景可以缩短采样周期采样越频繁单次差值越小能覆盖的转速上限越高代价是速度更新频率更快数据噪声相对变大。反过来低速场景要注意采样周期不能太短。比如 PPR 是 11 的电机编码器四倍频后一转才 44 个计数。如果采样周期只有 1ms那么转速在 1363 RPM 以下时很多采样周期里 diff 都是 0算出来的速度跳变非常严重。解决办法是拉长采样周期到 50ms 或者 100ms让每个周期内至少有几十个计数步进速度曲线才平滑。5.4 H743 特有的注意点H743 这代芯片性能很强但编码器模式下有些细节还是要留个心眼。一是 D-Cache 问题。如果你用 DMA 去读取编码器计数或者做其他数据传输开启了 D-Cache 之后会有缓存一致性问题需要使用SCB_CleanDCache或SCB_InvalidateDCache等操作。但编码器测速直接读寄存器完全不经过内存所以不受 D-Cache 影响这点不用慌。我见过不少人一听到 H743 就想着 DMA 读编码器其实没必要CNT 寄存器直接读延迟极低。二是引脚电压。H743 的大多数 GPIO 支持 3.3V 电平如果你的编码器输出是 5V 电平很多工业编码器模块都是 5V 供电直接接上去可能损伤引脚。最好用电平转换芯片或者选支持 5V 容忍的引脚并且加上拉。H743 部分引脚的 FTFive-volt tolerant属性要看数据手册确认。三是时钟树配置。H743 的定时器时钟源来自 APB1 和 APB2 定时器时钟CubeMX 默认会根据你选的系统时钟自动计算一般不用手动改。但如果你手工改过时钟树对一下定时器时钟是不是你预期的频率。编码器模式不依赖定时器时钟计数但数字滤波器的采样基准是定时器时钟这个会影响滤波效果。四是电源干扰。H743 开发板如果和电机驱动共用一个电源电机启动瞬间电压跌落会导致芯片复位编码器计数清零。这个和代码无关但确实最容易让调试人员抓狂。建议电机电源和控制电源分开或者至少用大电容稳定电压。6. 扩展玩法不在中断里也能知道自己转了多远前面测速方案已经能解决大多数需求但我还是想多提一句位置累计因为做云台、机械臂、AGV 这类项目速度和位置常常要同时用。最省事的做法就是周期读取 CNT 后把 diff 累进一个int64_t变量static int64_t g_position 0; void Encoder_Position_Task(void) { uint16_t now_cnt __HAL_TIM_GET_COUNTER(htim3); int16_t diff (int16_t)(now_cnt - last_cnt); last_cnt now_cnt; g_position diff; }这个方案不用开更新中断靠周期性高频读取保证不丢计数。前提是读取间隔要足够密不能超过 32767 个计数的时间。一般主循环跑得比这快实际用起来也没什么问题。如果你需要绝对位置并且不想依赖主循环的实时性那就用前面讲到的更新中断累加法两者可以并存。还有一点如果编码器线数不够四倍频之后的分辨率还是不满意可以考虑把减速比也利用起来。比如电机自带 30:1 的减速箱在输出轴测位置一圈的计数步进就是PPR × 4 × 30分辨率立刻高了一个量级。算转速输出轴 RPM 时别忘了把减速比除回去。这套编码器模式测速的方案我前前后后在好几种芯片上改过从 F103 到 H743核心思路完全一致区别只在时钟和引脚。H743 性能强定时器多跑起来可以很从容。你如果之前一直在用外部中断方案我特别建议你花点时间把这套硬件解码思路吃透测速逻辑会清爽很多CPU 也能腾出来干真正需要计算的事。
返回列表