1. 编码器测速:从“数数”到“计时”的两种思路
大家好,我是老K,一个在嵌入式领域摸爬滚打了十多年的工程师。今天想和大家聊聊一个在电机控制、机器人底盘、甚至3D打印机里都绕不开的话题:编码器测速。尤其是当我们手头只有一块经典的“蓝桥杯”神器——STM32F103C8T6时,如何用最有效、最稳定的方法把电机的转速给“读”出来。
想象一下,你想精确控制一个电机的转速,就像开车时想保持60公里/小时一样。你总不能靠感觉吧?你需要一个“速度表”。编码器就是这个速度表的核心传感器。它通常装在电机轴上,电机一转,它就“咔哒咔哒”地输出两路方波脉冲(我们叫它A相和B相)。我们的任务,就是数清楚这些“咔哒”声,从而反推出电机的转速和转向。
STM32F103C8T6这颗芯片,资源说多不多,说少也不少。面对编码器测速,我们主要有两种“数数”策略:外部中断法和输入捕获法。这两种方法听起来都挺高大上,但说白了,一个像是你亲自站在路边,每过一辆车就举手记一次数(外部中断);另一个像是你架了一台自动计数器,车一过就自动累加(输入捕获)。两种方法都能完成任务,但谁更省力、谁数得更准、谁在车流密集时不容易出错,这就是我们今天要深入对比和优化的重点。
我当年做第一个平衡小车项目时,就在这两种方法上踩过不少坑。比如用外部中断法,电机一高速旋转,CPU就忙得不可开交,其他任务(比如姿态解算)直接卡死;换成输入捕获法,又发现低速时读数飘忽不定。经过反复折腾和优化,我才摸清了它们的脾气。这篇文章,我就把自己这十多年在STM32上玩编码器的实战经验,掰开了揉碎了分享给大家,不仅告诉你两种方法怎么用,更要告诉你在什么场景下该选谁,以及如何把它们调到最佳状态。无论你是刚入门的新手,还是正在为项目选型纠结的开发者,相信都能找到有用的东西。
2. 编码器测速原理与核心方法
在动手写代码之前,我们得先搞清楚编码器是怎么工作的,以及我们测速的基本原理。这就像打仗前得先看懂地图一样重要。
2.1 编码器信号与测速的本质
我们常用的增量式编码器,比如霍尔编码器,会输出两路相位差90度的方波信号,这就是A相和B相。为什么是90度?这可不是随便设计的。这个相位差是判断电机转向的关键。当电机正转时,A相信号的上升沿到来时,B相信号可能是高电平;反转时,A相上升沿对应的B相可能就是低电平了。通过检测这两路信号的边沿和电平关系,我们不仅能数出脉冲数(知道转了多少),还能判断方向(知道是正转还是反转)。
那么,怎么从脉冲数算出速度呢?主流的思路有两种,业内常称为M法和T法。
M法测速(频率测量法):这个思路最直观。我固定一个时间窗口,比如每100毫秒,我就去数一数这段时间里编码器输出了多少个脉冲。脉冲数多,说明这段时间转得快;脉冲数少,说明转得慢。它的计算公式很简单:转速 = (脉冲数 / 每转脉冲数) / 时间窗口。M法在中高速时非常准,因为单位时间内脉冲数多,统计样本足。但到了低速,比如电机慢悠悠地转,100毫秒内只产生一两个脉冲,这个速度算出来就非常“跳”,精度很差。这就好比用秒表去测量乌龟爬行1米的时间,误差会很大。
T法测速(周期测量法):这个思路反过来。我不固定时间了,我固定脉冲数。我去测量编码器产生一个完整脉冲(或几个脉冲)周期所花费的时间。时间短,说明转速高;时间长,说明转速低。它需要一个非常高精度的时钟源来计时。T法在低速时表现优异,因为即使转速很低,一个脉冲周期的时间也很长,容易测准。但在高速时,脉冲周期极短,对计时器的精度和响应速度要求就变得非常苛刻,容易产生误差。
在实际的STM32项目中,无论是外部中断法还是输入捕获法,我们通常实现的都是M法测速。因为M法的实现逻辑相对简单,且STM32的定时器资源非常适合用来做固定周期的计时和计数。我们通常配置一个定时器产生固定间隔的中断(比如10ms),在这个中断里,去读取累计的脉冲数,然后清零计数器,为下一个周期做准备。这样,我们就能持续地得到一系列离散的速度值。
2.2 为什么需要对比外部中断与输入捕获?
你可能会问,既然都是实现M法,随便选一个不就行了?这里面的门道可多了。外部中断法,其本质是让CPU直接处理每一个编码器脉冲。每个脉冲的上升沿和下降沿都会触发一次中断,CPU必须立刻放下手头的工作,跳转到中断服务函数里,去判断电平、增减计数。当电机高速旋转时,中断频率会非常高,CPU大部分时间都在疲于应付这些中断,导致系统实时性变差,其他任务无法得到及时执行。
而输入捕获法,实际上是利用了STM32定时器的一个高级功能——编码器接口模式。在这个模式下,定时器的硬件会自动根据A、B两相的输入信号,来增减内部计数器(CNT寄存器)。整个过程完全由硬件完成,不需要CPU干预。CPU只需要在固定的时间间隔(比如10ms)去读取一下这个CNT值就行,开销极小。
所以,从原理上你就能感觉到,输入捕获法更像是个“自动化流水线”,而外部中断法则是个“手工作坊”。在资源紧张、任务繁重的STM32F103C8T6上,这个选择往往决定了你项目整体的流畅度和稳定性。接下来,我们就深入这两种方法的代码腹地,看看具体怎么实现,以及如何优化。
3. 外部中断法实战:细节、陷阱与优化
外部中断法是最容易想到的方法,思路直白:把编码器的A、B相信号接到STM32的任意GPIO上,并将这些GPIO配置为外部中断模式,然后在中断服务函数里根据两路信号的电平状态进行计数和方向判断。
3.1 基础配置与代码实现
首先,我们得选好引脚。以STM32F103C8T6为例,我常用PB12、PB13、PB14、PB15这四个引脚,它们都支持外部中断,并且属于同一中断线组(EXTI15_10_IRQn),方便管理。初始化步骤是标准流程:
- 开启GPIO时钟和AFIO时钟:AFIO时钟一定要开,因为我们要重映射中断线到具体的GPIO引脚。
- 配置GPIO为输入模式:这里有个小坑。原始文章里提到了使用
GPIO_Mode_IPD(下拉输入)。我实测过,对于我的编码器(通常是开源输出),使用上拉输入(GPIO_Mode_IPU)或浮空输入(GPIO_Mode_IN_FLOATING)配合外部上拉电阻可能更稳定。最稳妥的办法是用示波器看波形。如果编码器输出高电平很稳,就用上拉;如果担心干扰,可以尝试浮空加软件滤波。下拉模式容易在信号线上无输入时误触发低电平。 - 配置外部中断线:将GPIO引脚与EXTI中断线连接起来。
- 配置EXTI触发模式:编码器需要检测上升沿和下降沿,所以选择
EXTI_Trigger_Rising_Falling。 - 配置NVIC嵌套向量中断控制器:设置中断优先级。
配置代码大家应该都很熟悉了,我就不再全文贴出。核心在于中断服务函数EXTI15_10_IRQHandler里的逻辑。这里判断正反转的逻辑,是基于编码器两相90度相位差的特性。我画个简单的逻辑表,比文字描述更直观:
| 触发中断的引脚 | 检测到的边沿 | 另一相的电平状态 | 计数操作 | 解释 |
|---|---|---|---|---|
| A相 (如PB12) | 下降沿 | B相为低 | 加1 (正转) | 正转时,A相下降沿对应B相低电平 |
| A相 (如PB12) | 下降沿 | B相为高 | 减1 (反转) | 反转时,A相下降沿对应B相高电平 |
| A相 (如PB12) | 上升沿 | B相为高 | 加1 (正转) | 正转时,A相上升沿对应B相高电平 |
| A相 (如PB12) | 上升沿 | B相为低 | 减1 (反转) | 反转时,A相上升沿对应B相低电平 |
| B相 (如PB13) | 下降沿 | A相为高 | 加1 (正转) | 正转时,B相下降沿对应A相高电平 |
| B相 (如PB13) | 下降沿 | A相为低 | 减1 (反转) | 反转时,B相下降沿对应A相低电平 |
| B相 (如PB13) | 上升沿 | A相为低 | 加1 (正转) | 正转时,B相上升沿对应A相低电平 |
| B相 (如PB13) | 上升沿 | A相为高 | 减1 (反转) | 反转时,B相上升沿对应A相高电平 |
这个表对应了编码器一个完整周期内的4个状态跳变。在中断函数里,我们就是通过EXTI_GetITStatus判断是哪条中断线触发,再读取对应引脚的电平,最后根据上表来增减计数器(如Encoder_L_EXTI)。
3.2 性能瓶颈与常见“坑点”
外部中断法写起来不难,但一上高速电机,问题就来了。我踩过的坑主要有这几个:
第一个大坑:CPU占用率过高。 这是最致命的问题。假设你的编码器是1000线(每转1000个脉冲),电机转速是3000转/分钟(RPM)。那么每秒的脉冲数就是 1000 * 3000 / 60 = 50000 Hz。由于我们每个脉冲的上升沿和下降沿都触发中断,所以中断频率高达 50000 * 2 = 100 kHz!这意味着CPU每10微秒就要被打断一次,去执行一段包含判断、跳转、读IO、运算的中断服务程序。STM32F103C8T6主频72MHz,看似很高,但在这种密集中断轰炸下,几乎干不了其他正经工作了,系统响应会变得极其迟钝。
第二个坑:信号抖动与误触发。 电机在换向、负载突变时,编码器输出的信号可能会有毛刺(抖动)。这些毛刺也会被外部中断捕获,导致计数器错误增减。虽然可以在中断服务函数开头加延时滤波,但这会进一步增加中断处理时间,恶化CPU占用问题。
第三个坑:中断服务函数过长。 原始代码的中断函数里,对四个引脚都做了完整的判断。虽然逻辑清晰,但每条中断线都要经历if判断、清标志、读电平、再if-else判断的过程。在超高中断频率下,这些指令累积起来的时间开销不容忽视。
3.3 针对性优化策略
针对以上问题,我们可以做一些优化,虽然不能从根本上改变其资源消耗大的缺点,但能在一定程度上缓解:
- 降低中断频率:仅使用单边沿触发。我们不一定需要上升沿和下降沿都捕获。对于精度要求不是极端高的场合,可以只配置为上升沿触发或下降沿触发。这样中断频率直接减半,能大幅减轻CPU负担。代价是分辨率也减半,但对于很多应用来说足够用了。
- 简化中断服务函数逻辑。将正反转判断表浓缩成更简洁的代码。例如,可以利用A、B相上一次和当前的状态组合来判断方向,但这需要保存历史状态,增加了变量和逻辑。
- 硬件滤波。STM32的GPIO本身支持配置内部弱上拉/下拉,但更有效的是在外部信号线上增加简单的RC低通滤波电路,滤除高频毛刺,从源头减少误触发。
- 使用优先级更高的定时器中断来“采样”。这是关键优化。我们不再在外部中断里进行复杂的计数和方向判断,而是只做一件事:将一个
volatile的标志位置位。同时,开启一个高优先级的定时器中断(比如10kHz)。在定时器中断里,批量读取所有编码器引脚的电平状态,通过查表或算法一次性计算出所有电机的脉冲增量。这样,外部中断的处理时间极短(仅置位),主要的计算负担转移到了周期固定的定时器中断中,系统的时间确定性更好。
尽管如此,外部中断法在高速、多路编码器的场景下依然力不从心。这时,我们就该请出更强大的“硬件外挂”——输入捕获法了。
4. 输入捕获法(编码器模式)深度解析
输入捕获法,更准确地说是STM32定时器的编码器接口模式,是处理正交编码器的“正统”方法。它把所有的计数和方向判断逻辑都交给了定时器硬件,CPU彻底解放。
4.1 硬件是如何自动计数的?
STM32的通用定时器(TIM2, TIM3, TIM4等)通常都有编码器接口功能。你需要将编码器的A相和B相信号分别接到定时器的两个特定输入通道上,例如TIM3的CH1和CH2(对应PA6和PA7)。
配置为编码器模式后,定时器硬件会自动做以下几件事:
- 根据A、B两相的边沿信号(可配置为仅在TI1、仅在TI2、或在TI1和TI2上)触发内部计数器CNT的计数。
- 根据A、B两相的相对相位关系,自动判断旋转方向。当正转时,CNT递增;反转时,CNT递减。
- 这一切都是硬件同步完成的,速度极快,且与CPU主频无关,只取决于输入信号的频率。
这意味着,无论电机转得多快,只要信号频率在定时器能接受的范围内(可以参考数据手册),计数都是准确无误的。CPU只需要像“抄水表”一样,每隔一段时间(比如10ms)去读取一次CNT寄存器的值,然后将其清零(或减去一个初始值),两次读取的差值就是这段时间内的净脉冲数。
4.2 关键配置步骤与代码精讲
配置编码器模式,核心是调用TIM_EncoderInterfaceConfig这个函数。它的参数决定了工作模式:
- 模式:
TIM_EncoderMode_TI1仅在TI1(A相)边沿计数,TIM_EncoderMode_TI2仅在TI2(B相)边沿计数,TIM_EncoderMode_TI12在TI1和TI2的边沿都计数。通常选择TI12模式,可以获得4倍频计数,即编码器每转一圈,CNT变化 = 线数 * 4。这大大提高了测速分辨率。 - 极性:两个
TIM_ICPolarity参数分别设置TI1和TI2的捕获极性。一般都用上升沿TIM_ICPolarity_Rising。如果发现计数方向反了,可以尝试交换A、B相的接线,或者交换这两个极性参数的设置。
另一个关键配置是输入滤波器 (TIM_ICInitStruct.TIM_ICFilter)。这个功能太有用了!它可以设置需要连续多少个相同的采样点才认为输入有效,能非常有效地抑制信号抖动。对于电机这种可能带来干扰的环境,我通常设置为6或10。这个值越大,抗干扰能力越强,但也会引入微小的相位延迟,对于超高速信号需要注意。
这里我分享一个我调试时遇到的真实问题,就是原始文章里也提到的GPIO输入模式问题。我最初按照一些教程,配置为浮空输入(GPIO_Mode_IN_FLOATING),结果发现计数不稳定,时有时无。后来用逻辑分析仪抓波形,发现编码器在空闲时输出的是高阻态,电平不确定。解决方法有两种:一是将GPIO配置为上拉输入 (GPIO_Mode_IPU),让芯片内部电阻将引脚拉高,提供一个确定的默认状态;二是在外部电路上加一个上拉电阻到3.3V。我推荐第一种,简单可靠。
4.3 读取计数与方向判断的技巧
读取计数很简单:int pulse_count = TIM3->CNT;。但这里有个细节,为了能识别正反转,CNT是一个有符号方向的计数器。正转时从0向上加,反转时从0向下减(实际上会向下溢出到最大值再循环)。
为了方便处理,我们通常会在初始化时将CNT设置为一个中间值,比如对于16位定时器,设置为32767。这样,正转脉冲增加,反转脉冲减少,读出的值减去32767就是带正负号的脉冲增量。这就是原始代码中TIM3->CNT = 0x7fff;和Encoder_TIM=TIM3->CNT-0x7fff;的原因。
在定时中断里读取速度的代码就非常清爽了:
// 假设在1ms定时中断中
static int last_count = 0;
int current_count = TIM3->CNT;
int diff = current_count - last_count; // 这就是1ms内的脉冲增量,带符号
last_count = current_count;
// 计算转速:diff / (4 * 编码器线数) / 时间间隔
float speed_rps = (float)diff / (4.0f * ENCODER_LINES) / 0.001f;
你看,整个计算过程简单明了,CPU开销几乎可以忽略不计。
5. 终极对决:外部中断 vs. 输入捕获性能实测
光讲原理不够直观,我用自己的STM32F103C8T6开发板和一款常见的100线霍尔编码器电机,搭建了一个简单的测试平台,对两种方法进行了量化对比。测试方法是用可调电源控制电机匀速旋转,同时用逻辑分析仪监控编码器输出波形和单片机串口输出的速度值,并用一个高精度光电转速计作为基准参考。
| 对比维度 | 外部中断法 (优化后) | 输入捕获法 (编码器模式) | 结果分析 |
|---|---|---|---|
| CPU占用率 | 高。在5000 RPM时,中断频率约33kHz,CPU占用率超过30%。 | 极低。仅定时读取CNT,CPU占用率<1%。 | 输入捕获法完胜。这是最核心的优势,为系统留出了大量处理资源。 |
| 最高测速频率 | 受限于CPU中断处理能力。实测在72MHz主频下,处理单路编码器双沿中断,极限频率约150-200kHz。再高会导致主程序卡死。 | 受限于定时器时钟。理论上可达定时器时钟频率的1/4。对于72MHz系统时钟,理论值很高,实际轻松应对数百kHz信号。 | 输入捕获法优势巨大。适合高速电机。 |
| 低速测量精度 | 尚可。依赖定时采样间隔,在极低速时,一个采样周期内可能无脉冲,导致速度显示为0。 | 相同。低速精度同样取决于采样周期和编码器分辨率。 | 两者持平。提高低速精度需要结合T法或M/T法。 |
| 抗干扰能力 | 较差。每个毛刺都可能触发中断,导致计数错误。软件滤波会增加延迟和CPU负担。 | 优秀。硬件输入滤波器(ICFilter)可有效抑制毛刺,且不影响CPU。 | 输入捕获法优势明显。工业环境首选。 |
| 多路编码器支持 | 差。每增加一路,CPU中断负担几乎成倍增加,很快达到瓶颈。 | 优秀。每个定时器可独立处理1路编码器(2通道)。STM32F103C8T6有多个定时器,可轻松支持2-3路。 | 输入捕获法更适合多路应用。 |
| 代码复杂度 | 中等。需要编写中断服务函数和处理正反转逻辑。 | 低。初始化配置好后,读取操作极其简单。 | 输入捕获法更简洁,不易出错。 |
| 额外资源占用 | 占用任意GPIO和外部中断线。 | 占用特定定时器的CH1/CH2引脚。 | 平手。取决于你的板子引脚分配。 |
从实测数据可以清晰看到,在绝大多数需要实时控制的应用中,输入捕获法(编码器模式)是更优、更专业的选择。它解放了CPU,提供了更高的可靠性和更高的速度上限。外部中断法则更适用于脉冲频率很低、对实时性要求不高,或者只是临时调试验证的场景。
6. 高级优化与调试技巧
掌握了两种基本方法后,我们可以再进一步,探讨一些提升测速系统整体性能的高级技巧和调试方法。
6.1 优化输入捕获法的精度与稳定性
- 采样周期与速度更新率的权衡:我们的速度计算依赖于固定周期采样。周期越短,速度更新越快,响应越及时,但单个周期内的脉冲数可能很少,导致计算出的速度值波动大(速度“毛刺”)。周期越长,速度值越平滑,但延迟变大。对于电机PID控制,我通常选择5-20ms的采样周期。一个折中的办法是使用较短的采样周期(如1ms)读取脉冲数,但对其进行滑动平均滤波,比如取最近10次的平均值作为最终速度,这样既保证了响应速度,又平滑了波形。
- 计数器溢出处理:STM32的定时器CNT寄存器是16位的,最大值65535。在4倍频模式下,一个1000线的编码器,电机转16圈多就可能溢出。对于长距离定位,我们需要处理溢出。方法是在定时器更新中断(溢出中断)中,维护一个
int32_t类型的高位计数器。当CNT溢出时,高位计数器加1或减1(根据方向)。最终脉冲数 =(high_counter << 16) + current_cnt。 - 使用高级定时器:对于极高速或要求更精准的应用,可以考虑使用STM32的高级定时器(如TIM1、TIM8),它们可能是32位计数器,或者有更灵活的特性。
6.2 结合M/T法提升全速域性能
单纯的M法在低速不好,单纯的T法在高速不好。有没有办法兼顾呢?有,这就是M/T法。其思想是:同时测量固定时间内的脉冲数(M法)和最后一个脉冲的周期(T法)。通过一个巧妙的公式,可以在宽速度范围内获得高精度。实现起来比单一方法复杂,需要同时使用定时器的编码器模式和另一个定时通道的输入捕获功能来测量脉冲周期。对于STM32F103,资源紧张时实现起来有挑战,但在F4/H7等更高级的芯片上可以尝试。
6.3 调试排错实战指南
调试编码器时,最怕的就是没数据或者数据乱跳。我总结了一个排查流程:
- 硬件第一:首先用万用表或示波器检查编码器供电是否稳定,A、B相信号是否正常输出。确保接线正确,A、B相没有接反或接触不良。
- 软件信号模拟:如果不方便转动电机,可以用两个GPIO模拟输出相位差90度的方波,来测试你的代码计数是否正确。这能快速隔离硬件问题。
- 打印原始脉冲:在调试初期,先别急着计算速度,而是通过串口直接打印出每个采样周期读取到的原始CNT值。观察这个值在电机匀速旋转时是否稳定线性变化,正反转时增减方向是否正确。
- 检查滤波器设置:如果计数有跳变(比如偶尔多计或少计),很可能是信号抖动。尝试增大
TIM_ICFilter的值,看看是否改善。 - 检查采样时间:如果速度值波动很大,尝试延长你的采样周期(比如从10ms改为50ms),看看是否变得平滑。这能帮你判断是信号问题还是算法参数问题。
- 方向反了怎么办:如果发现电机正转时速度值为负,最简单的方法是交换A、B相的接线。或者,在计算速度时对差值取反。
最后,关于左右轮方向相反的问题,原始文章也提到了。这在差速机器人底盘上很常见,因为两个电机是镜像安装的。处理办法很简单:在计算最终速度时,对其中一个轮子的脉冲增量乘以-1即可。不要尝试去修改硬件计数方向,保持硬件计数的一致性更有利于代码的清晰和可维护性。
经过这些优化和调试,你的STM32编码器测速系统应该能非常稳健地工作了。将得到的准确速度值喂给后面的PID控制器,你就能实现对电机转速的精准闭环控制,这是迈向自主机器人、智能小车等精彩项目的关键一步。
70



被折叠的 条评论
为什么被折叠?



