拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

STM32定时器编码器模式:从外部中断到4倍频测速实战

STM32定时器编码器模式:从外部中断到4倍频测速实战

STM32H743这个芯片,我记得第一次用它做电机测速时,差点被外部中断搞到怀疑人生。当时用编码器的A相接PA0,B相接PA1,上升沿触发外部中断,中断里做计数累加,逻辑上一点问题都没有。结果电机转速一起来,一个编码器每圈输出400个脉冲,减速后电机最高3000转,算下来每秒要进2万次中断。这个频率下,H743的主频再高也扛不住,更别提中断里还要做滤波去抖和数值处理。后来我把方案换成了定时器编码器模式,同样是这两根线,中断次数从每秒两万次降到了零,CPU占用率几乎为0,方向判断、4倍频计数全部由硬件完成。

这篇文章就把我踩过坑之后梳理出来的那套配置流程完整写出来。核心思路是先解释为什么编码器模式比外部中断更适合测速,再讲4倍频的原理,然后一步步带你用CubeMX配置STM32H743的定时器,最后给出HAL库的完整测速代码和速度换算公式。不管你用的是H743还是F103、F407,这套思路和代码基本通用,直接查你的芯片定时器资源就能照搬。

1. 为什么说外部中断测速是“下策”

1.1 外部中断测速的原理与困境

很多初学者接触测速时,第一反应就是做外部中断。把编码器A相输出接到单片机的外部中断引脚上,配置成上升沿或下降沿触发,中断服务函数里做一次加1操作。这样确实能得到脉冲数,再结合编码器每圈的脉冲数,就能算出一个采样周期内的转速。

这个方案在空转、低速、短时间测试时挺顺利,但一旦放到真实的电机闭环控制里,问题就全暴露出来了。最直接的问题就是中断风暴。一个增量式编码器,如果电机端每圈输出400个脉冲,4倍频后的分辨率其实是每圈1600个脉冲,但这里先只用A相外部中断,也就是说每圈你会进400次中断。假设电机3000转每分钟,那就是每秒5万次中断。每一次中断,CPU都要压栈、跳转到中断服务函数、执行计数操作、再恢复现场。这一整套动作在H743上再快也要几百个时钟周期,5万次中断下来,主循环和控制中断就被拖垮了。

还有一个隐患是毛刺误触发。机械安装有间隙,光电编码器输出波形边缘可能会出现抖动,如果这时用的是外部中断,每一个毛刺都会被当成一个脉冲计入,速度值就偏高或跳动。虽然可以在中断里做软件滤波,但那相当于给中断服务函数又加了几十条指令的开销,高转速下形势更恶劣。

再者,外部中断模式天然缺少方向判断能力。它只能告诉你“来了多少个脉冲”,却没法直接告诉你电机是正转还是反转。你要么多接一个通道做相位判断,要么在外部中断里用软件读取另一路电平来辅助判断。这不仅增加了代码复杂度,也增加了出错概率。

1.2 编码器模式到底做了什么

定时器编码器模式,本质上是利用定时器内部的硬件状态机,同时监测编码器的A相和B相信号,自动判断旋转方向并完成计数。这个状态机不需要CPU参与,外部的脉冲边沿一进来,计数器硬件自动加1或减1,方向则记录在定时器的DIR位里。

换句话说,你把A相接TIM的CH1,B相接TIM的CH2,配置好编码器模式后,计数器就成了一个“带方向识别的高精度脉冲计数器”。CPU只需要在需要计算速度的时候,读一下计数器的值,再清零即可。电机转不转,往哪个方向转,转了多少,全部由硬件帮你记着。

这和外部中断的差距,就像手动挡和自动挡的区别。外部中断需要你自己挂挡、踩离合、换挡,编码器模式则是硬件自己完成整个换挡逻辑,你只管踩油门看速度表。

1.3 4倍频是怎么来的

编码器的A相和B相是两路相位差刚好90度的正交方波信号。正转时A相超前B相90度,反转时B相超前A相90度。一个完整的编码器信号周期内,A相有上升沿和下降沿两个边沿,B相同样也有两个边沿,合起来一共4个有效边沿。

定时器编码器模式下,如果选择“Encoder Mode TI1 and TI2”这种配置,那么这4个边沿的每一个都会触发计数一次。这就是4倍频的由来。

举个例子,假设你用的增量编码器是每圈输出400个脉冲(也就是400线),4倍频之后,电机转一圈计数器会累加1600。这意味着位置分辨率从原来的1/400圈提升到了1/1600圈,低速时能捕捉更细微的角度变化。这也是编码器模式相比外部中断的一个重大优势,因为你不需要额外硬件,白赚了一个4倍的分辨率。

2. CubeMX配置保姆级步骤

2.1 引脚规划与硬件连接

我这次用的是STM32H743,选TIM2做编码器接口,原因很简单:TIM2是32位计数器,最大可以计数到4294967295,正常转速下几乎不可能溢出,省去了一大堆溢出处理逻辑。如果你手头是F103这类芯片,TIM2同样是32位,也能照抄。

TIM2的编码器模式需要用到CH1和CH2两个通道。在H743上,TIM2_CH1可以映射到PA0或PA5等引脚,TIM2_CH2映射到PA1或PB3等引脚。具体用哪两个引脚,最稳妥的方式是直接在CubeMX引脚图上点选,软件会自动帮你分配。

接线时注意:编码器的A相输出接TIM2_CH1,B相输出接TIM2_CH2。如果你的编码器是集电极开路输出,记得在信号线加上拉电阻,或者使用单片机内部上拉。很多初学者调试时计数值乱跳,十有八九是编码器悬空导致的。此外,如果编码器供电是5V,而H743的引脚只能耐受3.3V,你需要确认所选引脚是否属于FT容忍引脚,或者加电平转换电路,避免烧坏芯片。

2.2 定时器参数配置详解

打开CubeMX,选中TIM2,将Combined Channels保持默认,然后在Parameter Settings中找到Encoder Mode下拉框,选择“Encoder Mode TI1 and TI2”。注意,这里有个很容易踩的坑,Encoder Mode有三种选项:

  • Encoder Mode TI1:只对A相边沿计数,2倍频
  • Encoder Mode TI2:只对B相边沿计数,2倍频
  • Encoder Mode TI1 and TI2:对A、B两相所有边沿计数,4倍频

我们要的是4倍频,所以直接选第三项,也就是“Encoder Mode TI1 and TI2”。

接下来要设置几个关键参数。首先是Counter Period,也就是计数周期。我建议直接设为0xFFFFFFFF。因为TIM2是32位计数器,把自动重载值设为最大值,就等于让计数器在极大范围内自由计数,尽量避免溢出干扰。

然后是输入极性Polarity。这里保持默认的Rising Edge即可,也就是上升沿有效。有人会想通过修改极性来改变计数方向,但这在编码器模式下并不直观,反而容易把解码逻辑搞乱。如果方向反了,最靠谱的做法是硬件交换A/B两相接线,或者在软件里对速度值取负,后文会细说。

Input Filter输入滤波器这个参数值得单独说一下。它用于硬件层面滤除信号中的毛刺。对于高速电机,滤波系数设太大反而会导致信号有效沿被过滤掉,造成漏计数。我的经验是:面对干净的编码器信号,滤波设为0即可;如果布线较长或电机电磁干扰大,可以设4到8之间。这个值需要根据实际信号质量调整,没有万能参数。

最后别忘记检查GPIO配置。CubeMX会在配置编码器模式时自动把CH1和CH2对应的引脚复用为定时器功能,但上下拉电阻需要你手动确认。编码器输出信号处于高阻态时,GPIO要配置成上拉,保证空闲状态电平确定。

2.3 时钟与工程初始化检查

时钟配置要保证定时器内核时钟已经启用。H743的TIM2挂载在APB1总线上,如果APB1定时器时钟是240MHz,那么编码器输入信号的频率理论上可以到达非常高,实际应用中根本不用担心计数跟不上。

很多人在CubeMX里改完时钟树以后,没有检查定时器时钟是否达到预期,结果程序跑起来以后计数明显偏慢。这里有一个简单自查方法:在CubeMX的Clock Configuration页面里,找到APB1 Timer Clocks这一项,确认数值不为0。如果设成了0,定时器就无法工作。

生成代码之前,还要确认采样定时器。我们后续需要在固定周期读取编码器计数值,最简单的方式就是用一个基本定时器产生中断,比如TIM7。在CubeMX里把TIM7的Prescaler和Period算好,让它每10毫秒触发一次更新中断。10毫秒是一个比较通用的测速周期,既不会太频繁占用CPU,又能保证速度环的实时性。

时钟树和TIM7配置完成后,就可以点击Generate Code生成工程了。记得在Project Manager中设置好IDE类型和固件包版本,HAL库选最新稳定版即可。生成后打开工程,先不要急着写业务代码,先把整个工程编译一遍,确认没有初始化错误。这个步骤能帮你在写代码之前就排除掉引脚冲突、时钟配置错误等基础问题。

3. HAL库代码实现与速度计算

3.1 启动编码器并周期性采样

工程生成后,HAL库的初始化函数中已经帮你配置好了TIM2和TIM7的底层寄存器。但请注意,CubeMX生成的代码并不会自动启动编码器,也不会自动启动采样定时器中断。这两步需要你在业务代码里手动完成。

建议在main函数中的while(1)之前,加入以下启动代码:

/* 启动编码器接口,必须使用TIM_CHANNEL_ALL */ HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); /* 启动采样定时器中断,每10ms进入一次回调 */ HAL_TIM_Base_Start_IT(&htim7);

HAL_TIM_Encoder_Start这个函数,第一个参数是定时器句柄,第二个参数指定通道。这里有一个细节需要重点提示:编码器模式下两个通道是配合工作的,所以启动时必须传TIM_CHANNEL_ALL。我曾经见过有人只启动TIM_CHANNEL_1,结果只能计A相的上升沿,完全没有4倍频效果,折腾了半天才找到原因。

采样定时器TIM7启动后,它每10毫秒会产生一次更新事件,进入HAL_TIM_PeriodElapsedCallback回调函数。我们在回调函数里读取TIM2的计数值并清零,再换算成速度。代码框架如下:

volatile float motor_speed_rpm = 0.0f; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM7) { /* 读取当前计数值,注意转为有符号类型 */ int32_t delta = (int32_t)__HAL_TIM_GET_COUNTER(&htim2); /* 清零计数器,准备下一个采样周期 */ __HAL_TIM_SET_COUNTER(&htim2, 0); /* 调用速度换算函数 */ motor_speed_rpm = Encoder_CalculateSpeed(delta); } }

读取计数值之后立刻清零,是最简单的防溢出方案。即使16位计数器也不怕溢出,因为每个采样周期我们都把计数值拿走了。这个做法在常规测速场景中足够可靠,不需要额外处理溢出中断。

3.2 从计数值换算速度

计数值本身没有物理意义,必须结合编码器参数和采样周期换算成转速。换算公式需要先理清几个参数:

  • PPR:编码器每圈输出的脉冲数,也就是编码器线数。比如常见的光电编码器是400线、500线、1024线。
  • 4:编码器4倍频后的倍数。
  • SAMPLE_PERIOD:采样周期,单位是秒。比如10毫秒就是0.01秒。

速度换算的基本逻辑是:先用计数值除以4倍频再除以每圈脉冲数,得到采样周期内电机转了多少圈,再除以采样周期得到每秒转数,最后乘以60得到转每分钟。换算代码如下:

#define ENCODER_PPR 400.0f #define SAMPLE_PERIOD_S 0.01f float Encoder_CalculateSpeed(int32_t delta) { /* 采样周期内的圈数 */ float revolutions = (float)delta / (4.0f * ENCODER_PPR); /* 每秒转数 */ float revolutions_per_sec = revolutions / SAMPLE_PERIOD_S; /* 转换为转每分钟 */ float rpm = revolutions_per_sec * 60.0f; return rpm; }

打个具体的比方。你用的编码器是400线,电机正转,10毫秒采样周期内,读取到的计数值是1600。那么4倍频之后,每圈是1600个计数。用1600除以1600,刚好是1圈。这1圈是在10毫秒内完成的,也就是说每秒100转,换算成rpm就是6000转。计算过程对应的是1除以0.01再乘60,结果是6000,完全吻合。

如果你的电机带了减速箱,输出轴转速还要再除以减速比。比如减速比是30比1,那输出轴就是200转每分钟。带减速比的计算公式为:

#define GEAR_RATIO 30.0f float output_rpm = motor_speed_rpm / GEAR_RATIO;

注意,上面代码里400和30这两个常量在真实项目里一定不能写死,最好通过宏定义或外设参数结构体统一管理。因为不同的编码器线数、不同的减速比,都会直接影响最终速度值,写死以后改参数极其痛苦。

3.3 方向识别与反转处理

编码器模式下方向判断非常智能,硬件会通过A、B两相的相位关系自动决定计数器加还是减。正转时计数器递增,反转时计数器递减。正因为这个特性,我们读取到的计数值天然带有符号。

在我上面的代码里,读取时用了(int32_t)强转,这个步骤很容易被忽略,却是方向处理的关键。假如电机反转,计数器会从0向下计数,在无符号数值里它表现为4294967295这样的巨大值。如果不强转为有符号int32_t,那么计算出来的速度将是一个完全错误的正数。强制转换后,它才能正确变成-1、-2这样的负值,于是速度值自动带上方向。

如果你的电机方向反了,但控制程序里规定电机正转时速度必须为正数,那么最直接的解决方法是交换硬件上A相和B相的接线。交换后,硬件自动认为电机反转,计数方向也就反转了。如果你不想动接线,也可以在代码里对速度取负,但这属于软件补救,逻辑上不够优雅,而且容易在后续维护时造成困惑。

3.4 32位和16位定时器的溢出处理

上面我特意选了TIM2这个32位定时器,目的就是尽量避免溢出问题。但在某些项目里,TIM2可能被其他功能占用,比如PWM输出、输入捕获、DMA搬运等,这时候你可能不得不使用TIM3或TIM4这种16位定时器做编码器接口。

16位计数器的范围是0到65535,如果采样周期较长、转速较高,计数值很容易超过这个范围。比如编码器500线,4倍频后每圈2000个计数,电机以6000转每秒也就是100转的转速运行,10毫秒内会产生2000个计数,没超。但如果采样周期放大到100毫秒,或者电机转速更高,一圈产生的计数值超过65535时,计数器就会溢出翻转。

使用16位定时器时,我建议采用最稳妥的两种方案。

第一种方案是缩短采样周期,保证每个采样周期内的计数值不超过16位范围的一半。比如周期缩短到1毫秒,一个周期内即使高速旋转也只记几千个脉冲,16位计数器完全装得下。代价是采样中断会更频繁,CPU占用率会上升一点。

第二种方案是开启定时器更新中断,在溢出中断里根据方向标志做累加或累减。这需要额外写一个回调函数,把溢出次数记录下来,读取时结合当前计数值和溢出次数算出完整的位置值。这种方案更通用,但代码复杂度更高。

在我的个人项目中,只要硬件资源允许,我坚决选32位定时器。省掉溢出处理,代码简单许多,可靠性也更高。这也是我在这篇文章里一直推荐TIM2的原因。

4. 常见问题与排查技巧实录

4.1 计数值不动或异常跳变

这是编码器模式调试中最常见的问题。计数值不动,排查思路是从外到内逐段检查。先确认编码器供电是否正常,再确认A、B信号线是否连到了正确的引脚上,用示波器或万用表测量信号引脚在手动旋转电机时是否有电平变化。信号正常的话,接着检查CubeMX配置里是否确实选择了“Encoder Mode TI1 and TI2”。最后确认代码里是否调用了HAL_TIM_Encoder_Start启动接口。

我遇到过一种非常隐蔽的情况:CubeMX里配置好了TIM2编码器模式,但引脚复用被后来的GPIO初始化覆盖了。比如在MX_GPIO_Init里对同一个引脚做了普通的GPIO输出配置,导致定时器功能失效。检查时,要确认初始化顺序里没有相互覆盖。直接在调试模式下读GPIO_AFR寄存器,就能看到该引脚被分配到了哪个复用功能编号上,与CubeMX配置核对即可。

计数值异常跳变则多数是信号质量问题。编码器信号线过长没有屏蔽、电机运行中产生较大电磁干扰、A相和B相接成了其他信号源,都会导致计数器忽快忽慢。这时优先检查共地问题和上拉电阻,其次再考虑增大输入滤波系数的值。

4.2 方向反了怎么办

方向反了是最容易判断也最容易解决的问题。现象是你让电机正转,程序里读到的速度却是负数,或者计数值是递减的。

处理方式有两种。第一种是硬件层面交换A相和B相接线,交换后硬件解码方向就会翻转。第二种是软件层面把读取的delta取负,或者把最终计算出来的速度乘以-1。这里我强烈推荐第一种,因为交换接线后,方向判断逻辑始终一致,后续维护代码时不会迷惑。

还有一种情况是A相和B相接线没反,但速度值偶尔出现正负交替,这说明信号质量存在问题。特别是低速时,信号边沿抖动会被硬件解码状态机识别为一次正转后又反转,计数结果会有轻微波动。解决方法是提高输入滤波系数,或者换用抗干扰能力更好的差分编码器。

4.3 高速丢步与滤波参数调整

如果电机转速较高时,速度值出现了明显的偏低偏差,那就要考虑丢计数的情况了。编码器模式是硬件计数,只要信号频率在定时器时钟范围内,理论上是不会丢的。但实际中丢计数的原因主要有两个,一个是输入滤波系数设置过大,把有效边沿也滤掉了;另一个是信号本身抖动剧烈,导致硬件状态机在正反转之间反复切换,净计数减少。

针对滤波系数,我的经验是先用示波器观察A、B相的输出波形。如果边缘非常干净,滤波系数保持0就可以。如果波形上有振铃或毛刺,再逐步调大滤波系数,从2开始往上加,每加一级就观察一下速度值是否回归正常。注意不要为了抗干扰把系数调到很大,我见过有人为了消除毛刺把滤波设到15,结果低速时电机转动半圈,计数值纹丝不动,因为有效脉冲都被滤掉了。

高速场景下,还要注意采样周期不能太大,避免单个采样周期内计数值超过定时器范围。即便是32位定时器,如果采样周期长达几秒,位置值也可能超过范围,所以采样周期和速度量程要互相匹配。

我把常见的调试参数总结成一个速查表,方便你排查问题:

现象排查项处理方案
计数值一直为0编码器供电、引脚复用、模式选择逐段测量信号,核对CubeMX配置
计数值乱跳信号悬空、接线过长、无上拉加电阻上拉,缩短线缆,设置滤波
速度方向反A、B相接反交换A、B接线,重新测试
高速时速度偏低输入滤波过大、采样周期过长降低滤波系数,缩短采样周期
程序无法进入回调TIM7中断未配置或未启动检查CubeMX中的TIM7中断选项,确认调用Start_IT
读取速度数值巨大未将有符号计数强转为int32_t使用(int32_t)强转并做归一化处理

4.4 采样周期与系统时钟的协调

采样周期的选择会直接影响速度环的控制效果和控制噪声。太长的采样周期会让速度反应迟钝,太短则可能引入更多高频噪声。我常用的起点是10毫秒,用于一般的电机速度反馈和精准测速都可以。如果是高动态响应的电流环或速度环,可以缩短到1毫秒到5毫秒,但前提是中断里的计算量足够小。

在HAL库中,采样定时器TIM7的中断回调里只做编码器读取、清零和速度换算,这几步操作加起来也就几十个指令周期,所以即便是1毫秒周期,对H743来说也几乎不占资源。需要避免的是在回调里做浮点打印、串口发送、显示屏刷新这类耗时操作,否则中断会被拖垮。

另一个需要注意的问题是定时器时钟的分频。CubeMX里TIM7的Prescaler和Period共同决定了中断周期。计算公式是:中断频率等于定时器时钟除以(Prescaler+1)再除以(Period+1)。以240MHz的APB1定时器时钟为例,要让中断频率为100Hz,也就是周期10毫秒,可以配置Prescaler为2399,Period为999,这样对应的是240MHz除以2400再除以1000,正好是100Hz。算好这个参数后,代码里就不需要再用HAL_Delay做粗糙的周期控制了。

最后再分享一个小技巧。很多项目里编码器同时用于测速和位置记录。测速时需要周期清零,而记录位置则希望计数器累计不中断。我的做法是设置两个变量,一个记录增量用于速度计算,一个保存累计位置值。每次采样时把增量加到累计值上,再把计数器清零,这样速度和位置互不干扰,非常实用。

我实际使用下来,编码器模式这一段稳定性和性能确实比外部中断强太多,尤其在闭环控制系统里,CPU资源被释放出来,控制频率也能提得更高。希望这篇文章能帮你少走弯路,早点把测速这块彻底搞定。

返回列表