简介:面向STM32F103嵌入式开发者的增量式编码器程序资源,用于解决电机控制、机器人定位、自动化设备等场景中的位置与速度采集难题,适合正在学习定时器编码器接口的初中级开发者。压缩包共934个文件,以C源码和头文件为主,同时包含启动汇编文件、链接脚本、IAR与Keil工程配置文件、文本说明文档,并集成CMSIS/DSP数学库,可直接导入工程编译或按需裁剪移植。资源包大小约9.99MB,文件组织结构完整清晰。已有2329人学习下载。内容覆盖TIM2、TIM3、TIM4定时器的编码器模式配置,预分频器与计数器周期设定,A相B相输入捕获以及更新中断处理,并提供旋转方向判断、位置计数、软件滤波等实现思路;针对低速抖动、高速计数溢出等问题,资料中还给出倍频处理与DMA传输等补充方案,可帮助读者深入理解STM32硬件定时器特性与编码器信号解析流程,有效缩短运动控制模块的开发调试周期。 搞STM32F103的朋友,十有八九迟早要跟编码器打交道。不管是做小车测速、云台角度反馈,还是小型数控滑台的位置闭环,编码器都是最常用的传感器之一。这篇文章就围绕我实际调试过的STM32F103编码器程序,把硬件原理、寄存器配置、CubeMX设置、代码实现和调试踩坑完整过一遍,希望能给正在折腾编码器的你省点时间。
我不会写那种“复制粘贴就能跑”的缝合代码,重点放在每个参数为什么这么设、每个寄存器位到底做了什么,以及真机调试时容易掉进去的坑。
1. 编码器接口方案选型:为什么我直接选定时器编码器模式
1.1 增量式编码器输出信号与判断逻辑
增量式编码器输出的核心信号是两路相位相差90度的方波,通常叫A相和B相。电机正转时,A相超前B相90度;反转时,B相超前A相90度。所以系统只要检测这两路信号的相位关系,就能同时知道两个关键信息:当前转了多远,以及转动的方向。
有些编码器还会输出Z相(零位信号),每转一圈输出一个脉冲,用于找机械原点。但在基础的测速和角度测量场景里,AB两相已经够用,Z相一般留到做绝对值定位时才用到。常见的增量编码器有200线、400线、500线、1024线等规格,“线数”指编码器转一圈A相输出的脉冲个数,而经过STM32定时器4倍频之后,实际计数值会是线数的4倍,这一点在下文计算转速时会重点展开。
1.2 手写外部中断解码的缺点与硬件编码器模式的优势
刚接触编码器时,很多人第一反应是用外部中断:把A相接EXTI,B相接普通GPIO,在A相中断里读B相电平判断方向。这个思路在低速、低线数场景下确实能跑,但存在两个硬伤。
第一是CPU占用率。编码器转速一高,比如电机带着2000线编码器跑到3000转/分,A相频率就是100kHz,意味着每秒10万次外部中断。如果中断函数里还要做方向判断、计数累加和业务逻辑,STM32F103这颗72MHz的Cortex-M3会相当吃力,其他任务基本别想干了。
第二是容易丢脉冲。外部中断的响应需要时间,如果A相脉冲间隔小于中断响应时间,脉冲就被漏掉了,计数自然就不准。而STM32的定时器编码器模式是纯硬件逻辑干活:A、B相直接接到定时器的两个输入捕获通道,硬件自动根据两路信号的边沿顺序判断方向并完成加减计数,整个过程不占用CPU。CPU只在需要的时候读一下计数值就行,这就是我最终选择硬件编码器模式的原因。
注意:STM32F103的TIM1、TIM2、TIM3、TIM4都支持编码器模式。对于只有一个编码器的简单场景,任意选一个空闲定时器即可,但如果后续还想做第二个编码器,就得提前规划好定时器资源分配。
1.3 适合的硬件连接方式
编码器输出一般有差分输出、推挽输出和开集(开路)输出三种。STM32F103的编码器接口输入需要接上拉电阻,所以最常见的方式是:编码器A相接定时器CH1,B相接CH2,VCC接3.3V或5V(看编码器规格),GND共地。如果编码器是开集输出,外部一定要加上拉电阻,否则波形边沿会非常差,直接导致计数抖动。很多朋友在调试时发现计数乱跳,第一怀疑对象就应该是硬件波形问题,而不是代码问题。
2. 编码器模式的原理与关键寄存器位拆解
2.1 方向自动判断的硬件逻辑
STM32的定时器编码器模式本质上是一个“带方向感的计数器”。它内部有两个输入捕获通道TI1和TI2,分别对应CH1和CH2引脚。在编码器模式下,这两个通道不再做普通的输入捕获,而是被当成正交解码器的输入源。
编码器模式的核心逻辑是:硬件实时监测TI1和TI2的边沿跳变,如果检测到某个边沿时另一路信号处于高电平,就向上计数(正方向);反之就向下计数(反方向)。这个逻辑由硬件自动完成,不需要软件参与判断,也不需要中断配合。
具体的工作模式由TIMx_SMCR寄存器的SMS位决定:
- SMS=001:编码器模式1,只在TI1的边沿计数(1倍频)
- SMS=010:编码器模式2,只在TI2的边沿计数(1倍频)
- SMS=011:编码器模式3,在TI1和TI2的边沿都计数(4倍频)
我最常用的是模式3,也就是4倍频模式,理由很简单:同样的编码器线数,4倍频能获得更高的分辨率。比如200线的编码器,4倍频后每圈就是800个计数,角度分辨率能到0.45度左右,对大多数测速场景来说足够用了。
2.2 计数方向由谁决定
方向判断不光跟SMS位有关,还跟TIMx_CCER寄存器的CC1P和CC2P位(输入极性)有关。这两位的组合决定了“什么边沿算有效边沿”,因此会影响方向判断的结果。
举个例子:如果CC1P=0且CC2P=0(上升沿有效),那么当TI1上升沿时TI2为高电平,计数器向上计数;但如果你把CC1P改成1(下降沿有效),那同样的物理转动方向可能会导致计数器反向计数。所以方向反了的时候,最简单的解决办法就是交换AB相的物理接线,或者把CC1P、CC2P反过来设置。交换接线当然更直观,但有时候编码器线已经焊死不方便改动,那就用软件改极性。
2.3 输入滤波器的采样频率计算
编码器接口还有一个很容易被忽视但非常重要的参数:输入滤波器。它的作用是对输入信号进行数字滤波,防止外部毛刺干扰引发误计数。滤波器的配置位在TIMx_CCMR1寄存器的IC1F和IC2F字段。
滤波器不是随便填个数字就行。它的工作原理是:以某个采样频率对输入信号进行连续采样,只有连续N次采到的电平一致时,才认为信号电平有效。采样频率的计算公式是:fSampling = fTimCK / 2^(N+1),其中N是ICxF配置值。比如STM32F103的定时器时钟是72MHz,如果ICxF配置为10(二进制1010),则采样频率约为72MHz / 2048 ≈ 35.15kHz。也就是说,宽度小于1/35.15k≈28.5微秒的毛刺会被滤掉。
这个参数在长线传输、电机电刷火花干扰、编码器供电不干净的场合非常有用。但是也别一味调大,滤波器会引入信号延迟,导致高速计数时跟不上真实脉冲。我一般先设0或4,等实测发现误计数再加。
3. 实操:CubeMX初始化配置与代码实现
3.1 CubeMX图形化配置步骤
用STM32CubeMX做初始化能省不少事,但前提是你要知道每一项配置背后的含义。以下是我常用的配置流程(以TIM2为例):
- 在Pinout页面找到TIM2,把Channel1和Channel2的模式都设为Encoder Mode。注意:编码器模式下,CH1和CH2必须同时配置为Encoder Mode,不能只开一路。
- 进入Parameter Settings,把Prescaler设为0。预分频器在编码器模式下一般不做分频,因为我们要的是原始脉冲计数,分频会降低分辨率。
- Counter Period设为65535(16位定时器最大值)。这里有个小技巧:如果编码器单圈脉冲数比较少,也可以直接把ARR设成“单圈计数值-1”,比如4倍频后单圈2000个计数,就设为1999。这样计数器溢出时正好是一圈,做角度换算更方便。
- Encoder Mode选TI1 and TI2(即4倍频模式)。
- Input Filter根据实际干扰情况调整,先给个4试跑。
- 使能定时器全局中断(如果要做溢出处理的话),NVIC里把TIM2中断优先级设好。
3.2 HAL库初始化代码
CubeMX生成的HAL库代码大致长这样:
/* 编码器模式初始化结构体 */ TIM_Encoder_InitTypeDef sEncoderConfig = {0}; sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; /* 4倍频模式 */ sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING; /* TI1上升沿有效 */ sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1; sEncoderConfig.IC1Filter = 4; /* 输入滤波器 */ sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING; /* TI2上升沿有效 */ sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1; sEncoderConfig.IC2Filter = 4; htim2.Instance = TIM2; htim2.Init.Prescaler = 0; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 65535; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Encoder_Init(&htim2, &sEncoderConfig);初始化完成后,在主循环里启动编码器接口:
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);这一步很多人容易漏。CubeMX只生成了初始化代码,并不会自动启动编码器计数,必须手动调用HAL_TIM_Encoder_Start。
3.3 读取计数与转速/位移计算
编码器数值的读取非常简单,直接操作寄存器即可:
int16_t encoder_count = (int16_t)(TIM2->CNT);这里必须用int16_t进行强制转换。原因在于:TIM2->CNT本身是16位无符号寄存器,范围0到65535,但编码器是有方向的,我们需要用有符号数来表示正反转。把无符号数强转为int16_t后,0到32767表示正方向计数,32768到65535会被解释为-32768到-1,正好对应反方向计数。这是编码器程序中最基础也最关键的细节。
读取到计数值后,转速的计算公式如下:
转速(RPM) = 单位时间内的脉冲变化量 / (编码器线数 × 4) × 60000 / 采样间隔(ms)比如200线编码器,4倍频后每圈800个计数,100ms采样周期内读数增加了400,那么转速就是400 / 800 × 60000 / 100 = 300 RPM。
位移计算类似:
角度(度) = 总脉冲数 / (编码器线数 × 4) × 360如果加了减速器,比如1:30的减速比,那么输出轴每转一圈,电机轴要转30圈,脉冲数就得乘以30倍。
3.4 定时器溢出中断扩展计数
当计数器ARR设为65535时,长时间运行或者高速旋转时,16位计数器很容易溢出(从65535回到0,或者从0下溢到65535)。如果只读一次CNT,不做溢出处理,得到的数值就会出现跳变。
处理方案是开启定时器更新中断,在中断里用一个32位变量做累加:
volatile int32_t encoder_total = 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); /* 判断是上溢还是下溢 */ if ((TIM2->CR1 & TIM_CR1_DIR) == 0) { encoder_total += 65536; /* 向上溢出 */ } else { encoder_total -= 65536; /* 向下溢出 */ } } }有了这个累加变量,实际位置就是encoder_total + TIM2->CNT的当前读数。这种“16位硬件计数+32位软件扩展”的组合,是工程上最常用的办法,既保证了计数精度,又不占用过多CPU资源。
4. 常见问题与排查技巧实录
4.1 计数不稳定、乱跳
这个问题我遇到过太多次了。一般来说,最先应该怀疑的是硬件,不是软件。用示波器看A相和B相的波形,重点观察上升沿是否干净,有没有振铃和毛刺。我踩过的坑包括:编码器供电接了开关电源没加滤波电容,纹波把信号弄得一团糟;还有一次是编码器线束跟电机动力线绑在一起走线,电机一启动计数就开始乱跳,把编码器线单独走线、加磁环后问题立刻消失。
软件侧的对应手段就是加大输入滤波器的ICxF值。如果ICxF=4还不行,可以逐步调到8、10甚至12。但记得滤波器加大会带来延迟,高速场景下要权衡。
4.2 方向判断反了
方向反了是最容易处理的故障。物理上最简单粗暴的方法是把A相和B相交换一下接线。如果不想动线,就把CC1P和CC2P的极性取反,效果一样。我用过的另一种办法是读取计数值后软件取反:int16_t count = -(int16_t)(TIM2->CNT),用于临时验证方向逻辑,确认无误后再改硬件或极性配置。
4.3 编码器接口测不到任何脉冲
先确认编码器供电是否正常,AB相是否有电平变化。如果编码器是集电极开路输出,别忘了上拉电阻。另外检查CubeMX里CH1和CH2是否都正确配置为Encoder Mode模式,以及HAL_TIM_Encoder_Start有没有被调用。经常有人初始化完就以为在计数了,实际上计数根本没启动。
还要注意:编码器模式下,定时器的通道引脚不是普通GPIO,CubeMX会自动重映射,但如果你的板子引脚冲突,就需要手动检查重映射关系,看是否被其他外设占用。
4.4 滤波器参数和实际波形的对应关系
最后再分享一个调试小技巧。滤波器ICxF的值跟“能滤除的最小毛刺宽度”有一个对应关系:设置值越大,能滤掉的毛刺越宽,但输入信号的最高频率也越低。在电机高速运行场景中,如果发现编码器读数比实际转速偏小,很可能是滤波器把有效边沿也滤掉了一部分。此时应该调小滤波值,或者干脆关掉滤波,优先保证信号的完整性。
4.5 舵机或伺服编码器那排开关是什么设置
顺带说一句,很多人搜索“伺服编码器那排开关”其实是在问伺服驱动器侧面的拨码开关。那通常是用来设置编码器分辨率、电子齿轮比或通信地址的,跟STM32这边的编码器接入没什么关系。但如果你在调试过程中发现伺服电机输出的A/B相脉冲频率跟预期不一样,不妨检查一下伺服驱动器的脉冲输出倍频设置,那边设错了,后面程序再对也没用。
5. 编码器程序的功能扩展方向
如果编码器基础测速程序已经稳定跑起来了,我建议往这几个方向做扩展:
一是加入闭环控制。把编码器读数作为反馈量,用PID算法控制电机转速或位置,这是做平衡小车、机械臂、云台的必经之路。编码器接口程序本身已经给了你足够精准的反馈,剩下的就是调PID参数。
二是做零点校准。很多系统要求上电后先找到机械原点,这时候Z相信号就派上用场了。可以配置一个外部中断检测Z相脉冲,让系统在回零过程中一旦收到Z相脉冲就立即停止,精度比单纯靠限位开关高很多。
三是移植到FreeRTOS等实时系统。编码器计数读取完全可以放在一个低优先级任务里,周期性地读取和计算,而定时器硬件继续在后台默默计数。这种架构下,CPU占用率几乎为零,系统还有余力处理通信、显示等任务。
6. 写在最后的经验总结
搞了大半年编码器相关项目,我最大的体会是:编码器程序本身并不复杂,复杂的是外围的工程问题。信号干扰、地线环路、供电纹波、滤波器参数选择,任何一个环节掉链子,代码写得再漂亮也白搭。所以建议先从硬件查起,用示波器把波形看明白了,再动手写代码,能少走很多弯路。
另一个心得是,调试时善用printf和串口。我习惯在调试阶段每50ms打印一次当前计数值和计算出的转速,用手慢慢转动编码器轴,观察数值变化是否符合预期。确认方向和倍频无误后,再接入电机做动态测试。这比直接接电机调试要安全得多,也能更快定位问题所在。
最后一个建议:代码里尽量把编码器线数、减速比、采样周期这些参数做成宏定义,方便后续换不同规格的编码器或电机时快速调整。前期多花几分钟做结构化,后面调试就省几个小时。
本文还有配套的精品资源,点击获取