简介:STM32编码器测速代码是一套面向嵌入式开发者和电子设计竞赛参赛者的实用工程,基于STM32F1系列(如C8T6)实现增量编码器信号采集与电机速度测量,覆盖定时器编码器模式、GPIO输入、中断处理及转速换算等关键环节。压缩包共184个文件,以.c/.h源码、编译生成的.o/.d依赖文件及Keil工程设计文件(uvprojx/uvoptx)为主,另有链接脚本、map映射和axf镜像等,整体仅4.75MB,可通过Keil直接打开并重新编译。该工程已吸引10779人学习,适合正在做控制类项目或备战电赛的开发者作为参考模板。示例工程能够帮助快速理解编码器测速的完整流程,包括正反转判断、脉冲计数和RPM换算,也可在此基础上移植到其他STM32型号,或结合PID实现闭环速度控制,省去从零搭建外设配置的时间;工程中的标准外设库调用和硬件抽象逻辑同样具有学习价值。 搞嵌入式这几年,我接触最多的传感器之一就是编码器,尤其在电机测速和闭环控制这块,STM32+编码器几乎可以说是标配方案。今天这篇不聊虚的,直接说透STM32编码器测速代码的完整思路,从硬件接线、CubeMX配置到代码实现、调试排坑,一次性讲完。
如果你刚接触单片机不久,或者正在做小车、云台、机械臂这类项目,这篇文章应该能帮你少走不少弯路。我会把原理讲得尽量接地气,代码也都是工程里直接可用的水准,不是那种网上抄来抄去的残缺Demo。我自己在实际项目里用这套方法调过好几套电机驱动方案,踩过的坑也会一并列出来。
1. 项目整体设计与思路拆解
1.1 为什么优先选定时器编码器模式而不是外部中断
STM32测速这件事,很多人第一反应是用外部中断数脉冲,也就是把编码器A相接到某个GPIO上,上升沿触发一次中断,计数器加一。这个方法听上去简单,实际用起来问题不少:电机高速旋转时,脉冲频率很容易到几千赫兹甚至几十千赫兹,如果每一路脉冲都进中断,CPU大量时间都花在反复压栈出栈上,主循环、通信、控制算法全都会被拖慢。更麻烦的是,A、B两相的相位关系也需要自己软件判断方向,稍微处理不及时,方向就判断错了。
真正合理的方法是使用STM32的定时器编码器接口模式。这个模式是硬件自动工作的:定时器内部会同时监测A相和B相的电平变化,不仅能自动判断方向,还能对上升沿和下降沿都进行计数,实现四倍频。整个过程不占用CPU,计数全部由硬件完成。这意味着哪怕A、B相频率很高,代码也不会被脉冲中断打爆。对于电机转速测量这种场景,这就是最合适的方案。
我最早做电机小车的时候也图省事用过外部中断,后来电机转速一上去,串口打印数据和PID计算开始肉眼可见地卡顿,换到编码器模式之后,CPU占用率一下降下来,整个控制周期稳定很多。如果你打算做闭环控制,编码器模式基本上是绕不开的。
1.2 增量式编码器测速的核心原理
先把编码器本身说清楚。常见的增量式编码器输出两路方波信号,分别叫A相和B相,两路信号相位差正好90度。电机正转时A相超前B相90度,反转时B相超前A相90度,定时器就是通过检测这个相位关系来判断旋转方向的。
为什么能四倍频?因为A、B两路信号相加,每个完整正交周期里一共有四个跳变沿:A上升沿、A下降沿、B上升沿、B下降沿。定时器编码器模式对这四个边沿全部计数,所以物理上编码器每输出一个脉冲,计数器实际会增加4。假设编码器每圈输出13个脉冲(PPR=13),那转一圈,计数器实际累计的数字是13×4=52。这个知识在后面计算转速时非常关键,很多人算错转速就是因为忘了乘4。
增量式编码器通常还有一路Z信号,每转一圈输出一个脉冲,一般用来做机械零点校准。测速场景基本用不上Z相,只需要A、B两路就够了。知道这些原理之后,再去配置STM32就清楚多了。
2. 硬件连接与工程初始化配置
2.1 编码器与STM32的接线细节
STM32的编码器接口模式并非所有引脚都支持,必须把编码器的A、B两相接到同一个定时器的两个输入捕获通道上。常用的是TIM2、TIM3、TIM4、TIM5这些通用定时器。比如我用TIM3,那A相接PA6也就是TIM3_CH1,B相接PA7也就是TIM3_CH2,对应关系一定不能弄错。选定时器的时候,优先选中型以上型号的32位定时器,例如TIM2或TIM5(视具体型号而定),这样计数器范围更大,溢出处理压力会小很多。但很多入门板子代码都用TIM3,16位也完全够用,只要做好溢出处理就行。
接线方面,编码器一般有VCC、GND、A、B四根线。电源电压必须确认清楚,常见编码器有3.3V和5V两种供电规格,输出电平也对应不同。如果编码器是5V供电,输出高电平5V,直接接到STM32的3.3V引脚上,长期运行有烧毁IO的风险,最好用电平转换芯片或者电阻分压处理一下。供电一定要跟STM32共地,否则信号参考电位不一致,计数必然乱七八糟。
还有一个容易忽略的点是上拉电阻。很多开漏输出的编码器模块,官方原理图里建议MCU内部开启上拉。在CubeMX配置GPIO或者定时器输入引脚时,可以把引脚的上拉打开,防止信号悬空时产生抖动脉冲。如果编码器线比较长,布线环境有电机驱动之类的强干扰源,建议A、B线上各加一个1kΩ到10kΩ的上拉电阻到3.3V,同时并联一个几十皮法的电容滤波,能明显减少误计数。我之前在电机驱动板旁边走线,没做任何滤波,编码器数据在低速时乱跳,加了两颗小电容之后立刻干净了。
2.2 CubeMX配置定时器编码器模式的要点
现在大部分项目都用STM32CubeMX生成初始化代码,配置编码器模式非常直观,但有几个关键参数设置错了会直接影响测速结果。
定时器配置页面里,需要把Combined Channels设置为Encoder Mode,也就是编码器接口模式。编码器模式下面有几种选项:TI1、TI2、TI1 and TI2。我一般选TI1 and TI2,这样A、B两相的边沿都会参与计数,实现四倍频。如果选TI1或者TI2,只对一相信号计数,相当于二倍频,精度会打折。
输入极性Polarity选择上,A、B两相都设置成Rising Edge(上升沿触发)就可以,硬件会自动处理两个通道的组合边沿。但有的编码器A、B相位相反,或者接线接反了,此时计数方向相反,解决方法有两个:一是把编码器A、B线对调,二是在CubeMX里把某个通道的极性改成Falling Edge,效果等同于软件对调A、B相。我个人的习惯是硬件上直接交换A、B接头,因为这样最直观,不容易留下隐患。
自动重载值ARR根据定时器位数来设置。16位定时器设置为65535,这样计数器计数范围就是从0到65535,超过这个范围会产生更新事件。脉冲计数周期也就是定时器时钟分频设置为最低,预分频器PSC设为0,保证每个边沿都能被硬件捕获,不需要任何分频。这些参数配置完成后,生成代码,然后自己在用户代码里启动编码器模式就行了。
3. 核心代码实现与测速计算
3.1 编码器初始化与计数值读取
CubeMX生成的代码已经完成了定时器参数的静态初始化,但编码器模式还需要在运行时启动。在主循环开始之前,调用HAL的编码器启动函数:
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);注意这里第二个参数必须是TIM_CHANNEL_ALL,不是某一个单独通道。新手容易在这里踩坑,只启动了TIM_CHANNEL_1,导致计数始终不对。
启动之后,读取当前计数值用下面这行:
int16_t encoder_count = (int16_t)__HAL_TIM_GET_COUNTER(&htim3);这里有一个非常关键的细节:__HAL_TIM_GET_COUNTER返回的是uint32_t,但16位定时器的计数器实际是16位有符号数。对于正交编码器模式,计数器的值是有方向性的,正转时递增,反转时递减。如果直接把返回值当作uint32_t处理,反转时的负数值会被解读成65535附近的大正数,后续计算转速就会出严重问题。正确做法是把读取值强制转换成int16_t类型,让负数正确还原。同理,如果用了32位定时器,则转换成int32_t。
读完之后记得清零计数器,保证每次采样周期内的计数值是增量值:
__HAL_TIM_SET_COUNTER(&htim3, 0);这两行代码构成了完整的单次采样逻辑:采样开始时计数值从0起步,采样结束时读走当前的累计值再清零,得到的就是一个固定周期内的脉冲增量。这个“定时读取-清零”的模式,是所有编码器测速代码的骨架。
3.2 转速换算公式与单位转换
拿到了固定周期内的计数值增量之后,需要换算成实际物理转速。这里的公式是整个测速代码最核心的部分,我直接给出通用版本,再举一个实际例子。
假设:
- PPR是编码器每圈输出的脉冲数,比如常见的13线编码器PPR=13
- 四倍频模式下,每圈总计数 = 4 × PPR
- 采样周期T的单位是秒
- 一个周期内读到的计数值增量是ΔN
那么转速RPM(圈/分钟)的计算公式是:
RPM = ΔN × 60 / (4 × PPR × T)举个例子:一个13线编码器,四倍频后每圈计数52,采样周期取了20毫秒也就是0.02秒,读到的ΔN是260。那转速就是:
RPM = 260 × 60 / (4 × 13 × 0.02) = 15600 / 1.04 ≈ 15000 RPM等等,这个结果有点奇怪。260增量在0.02秒内,每秒增量就是13000,除以每圈52,每秒就是250圈,每分钟250×60=15000转。电机的空载转速确实可以达到这个数量级,但如果是带减速箱的直流电机,那这里的转速是电机轴的转速,最终输出轴的转速还要除以减速比。比如减速比是30,那输出轴转速就是15000/30=500转/分钟。很多小车测速算出来数值大得离谱,原因就是漏了减速比,这点千万要记住。
如果小车测速还需要线速度,那就要结合轮子直径。线速度v = 输出轴转速RPM_out × π × 轮子直径D / 60。我用的是直径65mm的轮子,RPM_out是500,那线速度就是500×3.14×0.065/60≈1.7米/秒。把这些公式封装成一个函数,每次采样直接把计数值传进去,返回速度,主程序用起来就很舒服。
3.3 计数器溢出保护与中断处理
16位定时器在编码器模式下,计数器范围是-32768到32767。当电机高速运转时,如果两次采样之间的间隔过长,计数增量完全有可能超过32767,导致计数器溢出,读到的数值瞬间变成负数或者大幅跳变。解决这个问题通常有两种思路。
第一种思路是提高采样频率,保证每次采样周期内计数增量不超过32767。如果最大转速对应的每周期计数是5000,那20ms采样周期完全来得及读取,不会溢出。这种方法简单有效,也是大多数项目的默认做法。采样周期太短也有问题,下面会详细说。
第二种思路是使用定时器的更新中断,在计数器溢出时通过中断服务函数把溢出的次数记录下来,跟当前计数值一起合成一个32位扩展计数。如果用的是16位定时器,初始化开启更新中断:
__HAL_TIM_ENABLE_IT(&htim3, TIM_IT_UPDATE); HAL_NVIC_EnableIRQ(TIM3_IRQn);然后在中断回调里处理:
volatile int32_t overflow_count = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { // 根据计数方向判断溢出是向上溢出还是向下溢出 if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3)) { overflow_count--; } else { overflow_count++; } } }读取计数值的时候就合成32位总量:
int32_t total_count = (int32_t)(overflow_count << 16) + (int16_t)__HAL_TIM_GET_COUNTER(&htim3);这个方法适合高速编码器、长采样周期的场景。但说实话,多数小车主控场景用第一种方法就够了,额外增加溢出中断反而增加了代码复杂度和潜在的排查难度。如果你的电机转速不是极端高,优先把采样周期调短就完了。
4. 常见问题与排查技巧实录
4.1 典型故障现象与处理对照表
实际调试编码器测速时,我遇到过的问题基本可以整理成一张表,新手按图索骥就能节省大量时间。
| 故障现象 | 可能原因 | 解决办法 |
|---|---|---|
| 转速数值始终为0 | 编码器模式未启动,或A/B没有接到定时器通道 | 检查HAL_TIM_Encoder_Start是否调用,核对引脚对应关系 |
| 正反转方向相反 | A、B相接反,或极性配置反了 | 交换A、B线,或在CubeMX中调整极性 |
| 数值剧烈跳动,静止也计数 | 信号毛刺、共地不良、电源纹波 | 检查供电和共地,加RC滤波,开启内部上拉 |
| 速度比实际值大一倍 | 编码器模式选成了单通道而非TI1 and TI2 | 确认配置为Encoder Mode + TI1 and TI2 |
| 速度数值偏大好几倍 | 漏算了减速比或倍频系数 | 确认4倍频,确认减速比 |
| 运行时CPU占用率高 | 仍在用外部中断数脉冲 | 切换到定时器编码器模式 |
| 数值在中速时偶尔突变 | 采样周期过长,16位计数器溢出 | 缩短采样周期,或增加溢出中断处理 |
这些故障里,最隐蔽的是供电共地问题。编码器模块如果由独立的5V电源供电,而5V电源的地和STM32的地没有接在一起,A、B信号在MCU眼里完全是一堆不确定电平,因为TTL电平判定必须以同一个参考地为基准。我实际遇到过编码器静止不动但计数持续跳变的情况,排查了半天,最后发现是编码器模块和单片机各用各的电源适配器,地线没连。把两个电源的GND连通之后,数据立刻稳定了。
4.2 用逻辑分析仪验证正交信号波形
当编码器数值异常但接线和代码看起来都没问题时,强烈建议用逻辑分析仪直接抓A、B两相的波形。逻辑分析仪在这里的价值和示波器类似,价格便宜、上手简单,适合观察低速数字信号。把A相接分析仪通道0,B相接通道1,转动电机或者手动拧编码器轴,看波形是不是正常的正交方波。
正常的正交信号应该是这样的:A相和B相都有清晰的方波上升沿和下降沿,两路频率相同,相位差90度。如果看到波形有大量毛刺,或者边沿抖动严重,说明信号完整性有问题,需要检查屏蔽和滤波。如果波形正常但计数不对,问题就在STM32配置上。如果波形缺相或者完全没有脉冲,先查编码器供电和接线。逻辑分析仪能直接把问题定位在硬件还是软件,省得盲猜。
我自己有一次怎么调代码都计数不对,用逻辑分析仪一看,A相波形是对的,B相输出恒定高电平,根本不是方波。最后发现是编码器内部的B相输出引脚虚焊,重新补焊后问题彻底消失。这种问题如果不用仪器,光看代码和配置能排查一整天。
另外,如果采样到的速度曲线毛刺多,测速结果在PID控制里会导致输出抖动,这时可以在速度计算后加一阶低通滤波。滤波系数alpha取0.1到0.3之间,平滑效果比较好。但注意滤波会增加相位滞后,系数不能太大,PID参数也要相应调整。
4.3 环境与工程层面的几个小坑
除了编码器本身的问题,环境配置也容易卡住新手好几天。比如下载程序时提示Error: No STM32 Target Found,这个和编码器没什么直接关系,多半是调试器没识别到芯片。检查一下ST-Link或J-Link是否有单独供电,连接线是否牢固,BOOT0引脚是否被拉高,MCU供电是否正常。还有一个容易被忽略的问题是芯片的Debug接口引脚被复用为其他功能,如果把SWDIO和SWCLK配成了普通GPIO,就会导致调试器连接不稳定。
Keil环境方面,如果之前装了C51版本的Keil,再装MDK时要注意安装路径不能带中文,且需要分别安装对应的芯片包。编译报错No STM32 Target Found这种情况,还得检查工程是否选对了Device型号。如果你的板子是APM32这类国产兼容芯片,程序一般可以直接用STM32工程编译下载,但需要安装对应的芯片支持包,选型时也要额外确认引脚定义是否完全一致。
编码器测速代码虽然只是整个控制系统里的一小块,但它的稳定性直接决定了速度闭环能不能正常工作。我在实际项目里的体会是,先花半小时用逻辑分析仪确认硬件信号没问题,再写代码配置定时器,最后再计算和滤波,这样顺序走下来效率最高。不要上来就堆代码,出了问题反而要回头查硬件。如果只是在做实验验证,可以把测到的速度通过串口打印出来,边转电机边看数据,你会很快建立起对编码器测速的直观感觉。
本文还有配套的精品资源,点击获取