尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

STM32定时器编码器模式:从外部中断到4倍频测速实战 STM32H743这个芯片我记得第一次用它做电机测速时差点被外部中断搞到怀疑人生。当时用编码器的A相接PA0B相接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的CH1B相接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_CH1B相输出接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共同决定了中断周期。计算公式是中断频率等于定时器时钟除以Prescaler1再除以Period1。以240MHz的APB1定时器时钟为例要让中断频率为100Hz也就是周期10毫秒可以配置Prescaler为2399Period为999这样对应的是240MHz除以2400再除以1000正好是100Hz。算好这个参数后代码里就不需要再用HAL_Delay做粗糙的周期控制了。最后再分享一个小技巧。很多项目里编码器同时用于测速和位置记录。测速时需要周期清零而记录位置则希望计数器累计不中断。我的做法是设置两个变量一个记录增量用于速度计算一个保存累计位置值。每次采样时把增量加到累计值上再把计数器清零这样速度和位置互不干扰非常实用。我实际使用下来编码器模式这一段稳定性和性能确实比外部中断强太多尤其在闭环控制系统里CPU资源被释放出来控制频率也能提得更高。希望这篇文章能帮你少走弯路早点把测速这块彻底搞定。
返回列表