ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32编码器模式测速:告别外部中断风暴,用定时器硬件计数

STM32编码器模式测速:告别外部中断风暴,用定时器硬件计数 做电机转速测量我踩过最深的坑就是用外部中断去数编码器脉冲。转速一起来中断风暴直接拖垮了串口打印和屏幕刷新明明编码器是 200 线测出来的转速却一卡一顿甚至白丢几十个脉冲。后来换成 STM32CubeMX 直接配置定时器的编码器模式Encoder Interface Mode转速测量才真正变成“读寄存器”这种级别的操作。这篇文章把编码器模式从硬件原理、CubeMX 配置、代码实现到避坑清单完整串一遍。适合正在做小车、云台、转台闭环或者任何需要稳定测量电机转速的嵌入式玩家。尤其是对定时器只有基础认识、想搞明白“为什么这样配”的新手这篇应该能帮你省下至少一个星期的弯路。1. 为什么测速不要再去数外部脉冲编码器模式解决的三个核心问题1.1 外部中断丢脉冲一次真实的过载现场先算一笔账。增量式编码器常见的规格是 200 线意思是电机轴每转一圈A 相和 B 相各输出 200 个完整方波周期。一般直流减速电机空载转速可能到 3000rpm也就是每秒 50 圈。如果只数 A 相上升沿每秒进入中断的次数就是50 × 200 10000 次/秒也就是 10kHz 的中断频率。这还不算四倍频。如果要做四倍频每秒边沿是 50 × 200 × 4 40000 次40kHz。每次外部中断里都要读 GPIO、判断方向、累加计数还要考虑抖动过滤实际每次中断耗时普遍在 2~5us。简单乘一下40kHz 的中断光中断服务函数就占掉 CPU 8%~20% 的资源。更麻烦的是中断里工作没做完的同时下一次边沿已经到了如果 MCU 没有及时响应脉冲就丢了。方向判定的问题更隐蔽。在外部中断里判方向常见写法是检测到 A 相上升沿时读 B 相的电平B 为高是正转B 为低是反转。但四倍频模式下每个边沿都要判方向如果中断响应有延迟读 B 相时它正好处于电平变化的前后沿读到的既不是高也不是低方向就错了。对我知道可以加消抖但消抖一多高速脉冲又被滤掉这是一个怎么调都别扭的死循环。1.2 硬件自动判向与倍频计数编码器模式的底层逻辑STM32 定时器的编码器接口模式本质上把上面这些乱七八糟的事全部搬进了硬件。编码器 A/B 两路信号直接进定时器捕获通道 CH1/CH2定时器根据两路信号的相位关系自动确定方向相位领先的信号决定计数方向正转时计数器向上加反转时向下减。在 CubeMX 里选择编码器模式后有一个 Encoder Mode 下拉框选项是 TI1、TI2、TI1 and TI2。这三个选项的分辨率不一样TI1只在 TI1通常对应 A 相的每个边沿计数每转的计数脉冲数 编码器线数 × 2等效二倍频。TI2同理只在 B 相每个边沿计数也是二倍频。TI1 and TI2A、B 两相每个边沿都计数。200 线编码器每转就是 200 × 4 800 个计数脉冲等效四倍频。看到这里你应该明白硬件四倍频不需要你写任何中断判断代码计数器自己就会根据两个输入信号的电平和边沿组合去加减。CPU 只要在需要的时候读一下计数值其他时间可以安心去跑 PID、刷 OLED完全不用担心丢脉冲。顺带说一句编码器模式下计数器时钟不再来自内部时钟树而是由输入信号边沿直接驱动。所以这项工作的本质不是“定时器在计时”而是“定时器在数信号沿”。理解了这一点后面很多配置选项就不会误解。1.3 方向信息和机械零位是白送的软件数脉冲时方向判断是有额外成本的。编码器模式下方向判断完全由硬件完成你随时可以读 TIMx-CR1 的 DIR 位甚至用宏直接判断if (__HAL_TIM_IS_TIM_COUNTING_DOWN(htim3)) { // 当前是反转 } else { // 当前是正转 }这对那些需要检测反转、统计正反转圈数、或者做堵转保护的项目来说算是白送的额外收益。编码器本身就是增量式的没有绝对位置但结合这个方向位至少能准确知道当前轴往哪转、从某个参考点开始转了多少度。如果需要绝对零点再加一根 Z 相索引信号这里先不展开。理解了这个底层逻辑你就知道为什么网上几乎所有靠谱教程都会说“编码器模式是工业测速的正确打开方式”。不是因为它炫而是因为它把软件里最难处理的边沿响应问题交给了一个永远不会丢边沿的硬件逻辑单元。2. STM32CubeMX 配置实操从选定时器到生成工程的完整姿势2.1 定时器选型不是所有 TIM 都能当编码器口编码器模式不是所有定时器都支持。基本定时器 TIM6、TIM7 只有时基功能没有捕获通道想都不要想。通用定时器 TIM2/3/4/5 是最常见的选择高级定时器 TIM1/8 也支持但高级定时器还带着刹车输入、死区补偿这些复杂功能如果工程里并不需要这些没必要非用它们反而容易在 CubeMX 里多出一些无关配置。以最常用的 STM32F103C8T6 为例我一般用 TIM3编码器信号接 PA6 和 PA7对应的是 TIM3_CH1 和 TIM3_CH2。也可以选 TIM4对应 PB6、PB7。选引脚时注意不要和下载接口、外部晶振、板上的按键 LED 冲突否则后面调试起来很闹心。CubeMX 里操作方法在左侧 Categories 找到 Timers → TIM3Mode 里 Combined Channels 选 Encoder Mode。此时下方 Parameter Settings 会自动出现编码器相关选项同时 PA6/PA7 自动变成复用功能状态。如果你用的是别的芯片引脚会自动分配别急着改先生成看看好不好布线再决定要不要换。2.2 关键选项逐项解释Encoder Mode、Polarity 和 Filter进入 Encoder Mode 后有三个选项是你必须理解清楚的。第一个是 Encoder Mode 下拉框就是上一节说的 TI1、TI2、TI1 and TI2。日常做电机测速我强烈建议选 TI1 and TI2直接四倍频吃满分辨率。只有在某些特定场景比如编码器线数非常高、转速也非常高计数脉冲频率已经逼近你的采样或处理能力时才考虑退回 TI1 或 TI2 用二倍频降一降数据量。第二个是 Polarity。它的本质是决定触发边沿的有效极性。当你发现电机正转时计数值莫名其妙在往下减、反过来却在往上加不必去拆电机换线直接在 CubeMX 里把某一个通道的 Polarity 反转一下即可。但要注意不要两个通道同时反那样方向又会被翻回去。第三个是输入滤波 ICF。CubeMX 里在 GPIO 配置处可以给两个通道都设置 Input Filter对应的是定时器里的 IC1F/IC2F 字段。这个滤波器的原理是以内部时钟为基准对输入信号连续采样只有连续 N 次采样到同一电平才算有效从而滤掉短毛刺。电机电刷、驱动器 PWM 干扰都可能产生毛刺有滤波会安心很多。但滤波不是越大越好。采样窗口越长允许通过的最高信号频率就越低。高速电机如果用了一个过大的滤波系数计数值会偏差转速曲线像被钳住了一样上不去你还以为是机械问题其实是滤波器在丢边沿。我的经验是先用 0 跑通确认波形干净再加滤波需要滤波时从 2~4 这类小值开始试不要一上来就拉到最大档。2.3 PSC 与 ARR 的设定为什么网上教程都让你填 0 或 65535新建 CubeMX 编码器配置时Prescaler 默认是 0这没有错千万别乱改。编码器模式下计数器时钟由外部边沿直接驱动预分频器在这里的分频行为和你做 PWM 时完全不是一回事。如果你给 PSC 填了个 71你会发现计数器要么几乎不动要么计数行为完全超出预期排查半天才发现是分频系数搞错了。记住正常测速项目里PSC 就保持 0。自动重装载 ARR 就有讲究了。常见做法分两派。做法一是保持 ARR 为默认最大值 6553516 位定时器每次采样读当前计数值、和上一次值做差。这种情况下计数器就是个“长了腿”的累加器正转增加反转减少差值天然反映位移。配合 int16_t 类型转换还能自动处理计数溢出产生的回绕。做法二是把 ARR 设为“每转一圈的计数脉冲数减一”。比如 200 线编码器、四倍频、每转 800 个计数脉冲ARR 就设 800 - 1 799。这样计数器从 799 溢出回到 0每次溢出都严格表示“电机转了一圈”在计圈数场景下非常直观。我自己的项目里大部分时候用做法一。原因是做法二在电机频繁正反转来回摆动时溢出中断里还得判断方向才能确定是加一圈还是减一圈逻辑容易绕糊涂。做法一在读差值时不关心它是不是刚好整圈数值量大配合软件协议随便截取。唯一要注意的是采样间隔内差值不能超过 int16_t 范围也就是下一次采样前计数值变化不能超过 ±32767。以四倍频 800 脉冲/转算两次采样之间转了 40 圈之内都不会出问题绝大多数应用根本够用。2.4 生成代码后第一件事核对 GPIO 和初始化顺序CubeMX 生成工程后不要急着写业务代码先做两件小事。第一件核对 GPIO 是不是复用功能模式。正确状态下两个编码器输入引脚应该是 Alternate FunctionTIM3 的 AF 映射对应 GPIO_AF1_TIM3F103 上是 AF2不同系列不一样。如果你看到引脚变成了普通的 Input那是配置没生效计数一定不正常。第二件看一下生成的 MX_TIM3_Init 里 htim3.Init.Period 是否是你想要的 ARR 值。F1 系列的定时器寄存器是 16 位如果你在 CubeMX 里填了个超过 65535 的数写入寄存器时会静默截断这个坑特别隐蔽。生成完代码后在 CubeMX 里看到 ARR 是 100000实际运行却每次到 34464 就溢出这种情况我碰到过不止一次。还要检查 NVIC 里 TIM3 global interrupt 是否勾选。如果你后面要用溢出中断扩展 32 位计数这一步漏了代码里再怎么调回调函数都无效因为中断根本没进。3. 转速计算实战从计数值到稳定 RPM 的代码链路3.1 读取当前计数值的两种方式完成上面的配置启动编码器接口只需要一行HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL);注意第二个参数官方推荐在编码器模式下传 TIM_CHANNEL_ALL让两个通道同时接入。如果只传 TIM_CHANNEL_1某些系列上另一路信号是断开的四倍频模式会变成不可用状态。这是我在升级 HAL 库版本后踩到的一个暗坑旧版本可能没问题新版本严格了。启动之后读取当前计数uint16_t current __HAL_TIM_GET_COUNTER(htim3);清零计数__HAL_TIM_SET_COUNTER(htim3, 0);就这么简单。但实际测速时不能只读一次就完事你得设计好“怎么读才能计算转速”。3.2 M法与T法低速和高速场景的取舍测转速通常有两类主流方法。M 法测频法固定一个时间窗口统计窗口内计数脉冲的变化量。比如每 100ms 读一次两次差值就是 100ms 内的脉冲数转速公式为RPM 差值 / (编码器线数 × 倍频) × (60 / 采样周期秒)200 线编码器、四倍频每转 800 个计数脉冲采样周期 0.1sRPM 差值 / 800 × 600 差值 × 0.75窗口 100ms、脉冲差值为 0 时说明转速是 0但低速时更致命的是分辨率问题如果电机转速只有 0.1rpm100ms 窗口内轴只转过 0.06°对应不到 1 个计数脉冲M 法基本测不出来。T 法测周法测量两个相邻计数脉冲之间的时间用时间倒数换算转速。低速时两个脉冲间隔很长测量精度高但高速时脉冲间隔只有几微秒如果没有硬件捕获支持软件测量很难做准。一般项目里的实用策略是中高速用 M 法低速用 T 法想要全速域准确就用 M/T 法。但多数小车、云台项目不需要这么极限一个 M 法就够用了。我的实际做法是主循环里按 100ms 周期采样同时在做闭环时优先保证控制频率把采样窗口压缩到 20~50ms。窗口越短实时性越好但低速分辨率变差这个取舍根据应用场景定不要照搬。3.3 32位扩展计数器用溢出中断把16位计数器“变大”16 位计数器即使 ARR 拉满 65535在长时间累计位移时也一定会溢出。解决办法不是去清零而是在溢出中断里扩展成一个 32 位计数变量。CubeMX 里勾选 TIM3 全局中断后在 stm32f1xx_it.c 的 TIM3_IRQHandler 里会自动调用 HAL_TIM_IRQHandler接着在 main.c 中覆盖回调volatile int32_t g_encoder_count 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { if (__HAL_TIM_IS_TIM_COUNTING_DOWN(htim3)) { g_encoder_count - (int32_t)(htim3.Init.Period 1); } else { g_encoder_count (int32_t)(htim3.Init.Period 1); } } }这里的核心是判断计数器溢出时的方向。虽然 ARR 设为 65535 时溢出周期看起来很长但电机高速正转时65535 / 800 ≈ 81.9 转也就是 81 转就溢出一次。如果方向和符号不处理累计位移就会在溢出瞬间凭空减少 65535。上面的代码在向上计数溢出时加 65536向下计数溢出时减 65536相当于把计数范围扩成了 32 位足够记录几百万转。需要注意不同 STM32 系列对方向位的宏命名有差异有些老版本 HAL 里__HAL_TIM_IS_TIM_COUNTING_DOWN可能没有定义。遇到编译报错直接用寄存器操作替代if ((TIM3-CR1 TIM_CR1_DIR) ! 0) { /* 向下计数 */ }3.4 封装一个可复用的测速模块刚开始调测速时会到处写读计数器的代码后面会后悔。建议早点把它封装成一个独立模块。我通常提供四个接口void Encoder_Init(void); // 初始化并启动编码器定时器 int32_t Encoder_GetCount(void); // 获取累计计数值32位扩展后 void Encoder_Clear(void); // 清零累计计数值 float Encoder_GetRPM(uint16_t sample_ms); // 采样测速最基础的阻塞测速版长这样float Encoder_GetRPM(uint16_t sample_ms) { int32_t start g_encoder_count; HAL_Delay(sample_ms); int32_t delta g_encoder_count - start; // 假设 200 线编码器、四倍频每转 800 脉冲 return (float)delta / 800.0f * (60000.0f / sample_ms); }但阻塞版会卡住主循环真实项目里我更喜欢配合一个 1ms 或 10ms 的时基在周期里采样计数差值然后做一阶滤波或者滑动平均再交给显示或控制用。转速曲线能不能稳定下来往往不是计数器的问题而是你是否做了滤波。关于滤波系数我的经验是滑动平均窗口 5~10 个点比较均衡太大会让转速反馈滞后闭环 PID 容易振荡。4. 避坑清单这些问题我几乎全部踩过一遍4.1 接线与电平类A/B 相接错与不上拉第一个坑也是新手的第一个错误把编码器输出线直接按颜色接到单片机上结果 A 相接到了 CH2、B 相接到了 CH1代码照抄网上的一上电计数方向就是反的。这种问题不用怀疑人生检查一下接线对应关系就行。A 相对的是 TIMx_CH1B 相对的是 TIMx_CH2前后一定要对上。如果电路上不好改就在 CubeMX 的 Polarity 里把 CH1 或 CH2 反向方向就纠回来了。第二个坑编码器模块和单片机没有共地。编码器输出信号是相对于它自己的电源地而言的如果你的编码器电源地没和 STM32 的 GND 连在一起信号电平就是悬空的读到的东西在零点几伏到电源电压之间胡乱漂移计数值静止时也会乱跳。这不算高级故障但排查起来很费时间。接一根共地问题立刻消失。第三个坑是引脚内部上拉。很多编码器模块是开漏输出需要外部上拉才能输出高电平还有的模块信号是推挽输出上不上拉无所谓。CubeMX 里给两个编码器输入引脚开上拉是个保险做法GPIO 模式选 Alternate FunctionGPIO Pull-up/Pull-down 选 Pull-up如果开漏输出的编码器没配上拉大概率出现的情况是低电平正常高电平电压爬不上去结果计数乱跳。这一点在 F103 上尤其常见因为它的大部分引脚默认不带内部上拉必须手动配。4.2 配置类PSC 乱填与滤波器系数配置 PSC 乱填是我见过最频繁的问题。很多人习惯把一个定时器的 Prescaler 填成 72-1以为所有定时器模式都需要分频。编码器模式下计数器时钟是外部边沿信号不是内部时钟你填 PSC71行为立刻异常。到了论坛提问别人一看截图就看到了 PSC 不是 0直摇头。滤波系数的问题前面说过这里再强调一个实际场景。我接过一个客户的电机驱动器 PWM 频率 20kHz编码器信号线上串进来不少毛刺转速曲线有一圈一圈的规律性抖动。我加了 ICF 滤波值 4抖动明显改善后来为了追求干净把 ICF 拉到了 15结果电机 3000rpm 时计数值明显偏小因为滤波器的采样窗口把高频脉冲滤掉了。最终调到 8 才平衡。这个调试过程一共花了小半天所以写在这提醒你滤波系数不是越大越好要在示波器或逻辑分析仪下边看边调。4.3 代码与调试类DIR 方向假设与计数器清零时机代码里的坑集中在计数读取和清零时机上。最典型的错误是在采样时先把计数器清零再去读它但两次操作之间已经丢掉了几个脉冲。正确做法是先读当前值保存再把计数器清零或者用累加差值不要反过来。另一个坑是直接用 uint16_t 存差值。电机正转 100 个脉冲、反转 200 个脉冲期望结果是 -100但用 uint16_t 得到的是 0xFF9C上层逻辑如果把它当无符号数处理转速就是一串正数乱跳。解决方案很简单差值用 int16_t 去接int16_t delta (int16_t)(__HAL_TIM_GET_COUNTER(htim3) - last_count);利用补码回绕特性正负方向会自动修正。还要提醒一点如果用了多个定时器同时开编码器或者工程里有多个中断一定要在回调里先判断是谁触发的if (htim-Instance TIM3) { ... } else if (htim-Instance TIM4) { ... }别在回调里默认只有一个定时器在跑这是多路电机项目里非常常见的逻辑错误。4.4 避坑汇总表问题 / 原因 / 处理办法问题现象根本原因处理办法静止时计数值持续跳变编码器未共地、无上拉或存在毛刺加入共地线、配置内部上拉、适当增加 ICF转速方向与预期相反A/B 相接反调换接线或在 CubeMX 中反转某一个通道的 Polarity高速时计数值偏小ICF 滤波过大滤掉了有效边沿逐步减小 ICF在示波器下确认转速结果是一个巨大正数差值用无符号类型读取用 int16_t 接收差值累计计数在某个值附近跳变未处理 16 位计数器溢出开启溢出中断扩展 32 位计数编码器模式没反应引脚未配置为复用功能检查 GPIO Mode应为 AF填了 PSC 后计数异常编码器模式不需要预分频将 PSC 设回 0中断回调不触发NVIC 里未使能定时器中断勾选 TIM3 global interrupt启动后没有计数HAL_TIM_Encoder_Start 只启用了单通道使用 TIM_CHANNEL_ALL5. 从测速到闭环控制的几个扩展点5.1 反馈滤波与 PID 的配合测速数据拿到之后最常见的下一步就是做闭环控制。这时有个容易被忽略的细节显示用的转速可以狠狠滤波但反馈进 PID 的转速不能过度滤波。速度反馈太“肉”PID 看到的转速永远慢半拍系统容易振荡太“贼”量化噪声又被放大电机声音会变糙。更好的做法是在控制中断里直接读 32 位扩展计数每 1ms 读一次差值通过一阶低通滤波输出到 PID。一阶系数根据控制周期和编码器分辨率去推比如 200 线四倍频、1ms 控制周期低速时一个控制周期里可能只有 0~1 个脉冲这时候单纯靠当前差值做 PID 是很不稳的。可以考虑在 PID 前累加一个“微脉冲缓冲”或者把控制周期拉长到 5ms效果都比硬调 P 参数好。5.2 编码器线数不同时的计算差异很多教程默认只讲 200 线编码器但实际项目里你可能会碰到 11 线的空心杯电机编码器、360 线或者 500 线的伺服编码器。计算公式不变变的只是“每转脉冲数”每转计数脉冲 编码器线数 × 倍频如果编码器是 11 线四倍频后每转只有 44 个计数脉冲这时做 M 法测速的分辨率会非常差低速下基本没法用必须改用 T 法或者提高采样窗口。如果编码器是 500 线四倍频后每转 2000 个脉冲M 法分辨率很高但同样的采样窗口里差值很大要注意别让 16 位计数器在窗口内溢出。所以代码里千万别把“800”写死建议在模块里留一个参数#define ENCODER_LINES 200 // 编码器线数 #define ENCODER_MULT 4 // 倍频 1/2/4 #define PULSES_PER_REV (ENCODER_LINES * ENCODER_MULT)这样换编码器时只改一个宏不用到处翻代码。5.3 Z 相零点与多定时器资源统筹如果你的项目不只是测转速还要求获取电机轴的绝对参考位置那就必须用上编码器的 Z 相。Z 相每转一圈只输出一个脉冲通常接到任意一个外部中断引脚在中断里把累计计数清零或记下参考值。这样就能知道“从零点开始转了多少圈多少度”做云台归零、机械臂关节复位都靠它。还有一个资源统筹问题值得提前规划一个定时器做了编码器就不能同时输出 PWM 了。所以同时需要测速和驱动时建议编码器占一个通用定时器PWM 占另一个定时器两者在硬件上天然分开避免互相干扰。我习惯把 PWM 放在高级定时器编码器放在通用定时器PCB 布局上再把电机驱动线和编码器信号线拉开距离这套组合在多次项目中都非常稳定。文章写到这里编码器模式的内容算是比较完整了。最后再分享一点个人调试习惯新板子第一次上电我一定是先用手慢慢拨电机在调试器里盯着 TIM3-CNT 的数值看确认它随转动平滑加减、方向正确再写转速公式如果数值跳变先别怀疑代码去检查接线和电平。这个过程我反复用了很多年一直都稳。
返回列表