STM32编码器测速实战:从硬件配置到软件滤波的完整方案
2026/7/29 4:25:11 网站建设 项目流程

1. 项目概述:为什么编码器测速是嵌入式开发的必修课?

在电机控制、机器人底盘、智能小车这些项目中,实时、准确地获取电机转速是控制闭环的基石。你可能会想到用霍尔传感器,但它精度有限;或者用光电对管,但易受干扰。而旋转编码器,尤其是增量式编码器,凭借其高分辨率、强抗干扰能力和双向测速功能,成为了工业与消费级项目中的主流选择。STM32作为嵌入式领域的“瑞士军刀”,其内置的定时器硬件编码器接口模式,正是为高效、精准处理编码器脉冲而生的利器。它能让CPU从繁琐的脉冲计数和方向判断中彻底解放出来,实现“零开销”的测速,这对于需要高频PID控制的应用至关重要。

这个项目,就是带你深入STM32 HAL库的底层,玩转定时器的编码器模式。我们不止步于让编码器转起来、数值读出来,更要深挖其原理,搞懂每一个配置参数背后的意义,并解决实际工程中一定会遇到的“坑”:比如如何应对高速旋转下的计数溢出?如何设计滤波算法让数据更平滑?如何将计数值精准地转换为转速(RPM)或线速度?如果你正在为平衡车、四轴飞行器、云台或者任何需要精密运动控制的项目头疼,那么掌握这套从硬件配置到软件滤波的完整方案,无疑会让你事半功倍。

2. 核心硬件与原理深度解析

2.1 增量式编码器:硬件是如何“说话”的?

增量式编码器通常输出两路相位差90度的方波信号(A相和B相),以及一路每圈一个的零位索引信号(Z相)。A、B两相信号的关系是理解一切的基础。

当编码器正向旋转时,A相领先B相90度。在一个完整的电气周期内(A、B各变化一次),信号跳变顺序是固定的。反之,当反向旋转时,B相则领先A相90度。定时器的编码器模式,其核心智慧就在于硬件自动识别这种相位关系。它并不简单地计数A相的上升沿,而是同时监听A、B两相在每一个时钟边沿(上升沿和下降沿)的变化,通过内置的数字滤波器判断有效的计数事件和旋转方向。例如,在“在TI1和TI2的上升沿和下降沿计数”模式下,编码器旋转一圈产生的计数值是编码器物理线数的4倍,这就是所谓的“4倍频”,它能将分辨率提高4倍。一个500线的编码器,在此模式下每转可产生2000个计数。

注意:编码器的“线数”是指其光栅盘上明暗刻线的数量,它决定了每转一圈A或B相产生的完整方波个数。购买或选型时,这是一个关键参数。

2.2 STM32定时器的编码器接口:硬件自动化的艺术

STM32的通用定时器(TIM2, TIM3, TIM4等)和高级定时器(TIM1, TIM8)大多支持编码器模式。其本质是将定时器的两个输入通道(TI1和TI2)配置为编码器的A、B相输入。硬件内部实现了一个状态机,其工作流程可以概括为:

  1. 信号输入与滤波:来自GPIO的A、B相信号首先经过输入滤波器和边沿检测电路,以消除毛刺。
  2. 方向解码:硬件比较TI1和TI2的电平与边沿关系,实时判断当前是正向计数还是反向计数。
  3. 计数器操作:根据解码出的方向,控制16位或32位向上/向下计数器进行递增或递减。
  4. 溢出管理:当计数器从0向下溢出时,会触发更新事件并可能产生中断/DMA请求;从最大值向上溢出时同理。

这一切都由硬件自动完成,无需CPU干预。CPU只需要在需要的时候(例如,每隔一个固定的采样周期)去读取定时器计数器(CNT)寄存器的值即可。这个值是一个带符号的相对值,其变化量直接反映了在采样周期内编码器产生的净脉冲数。

2.3 项目硬件选型与连接指南

对于大多数学习和中小型项目,STM32F103C8T6(蓝桥杯、平衡车常用)或STM32F407系列是性价比之选。它们拥有多个通用定时器,完全满足编码器接口需求。

编码器选择

  • AB相增量式编码器:最常见,如欧姆龙E6B2系列、国产JGA25-370电机自带编码器。
  • 接口类型:推荐选择“集电极开路输出”或“推挽输出”,直接与STM32的GPIO相连即可。避免使用需要额外上拉/下拉的型号,除非你清楚电路原理。
  • 电压匹配:确保编码器工作电压(常见5V或3.3V)与STM32 GPIO电平兼容。对于5V输出的编码器,如果STM32是3.3V系统,强烈建议使用电平转换芯片(如TXS0108E)或电阻分压电路,长期直接连接有损坏MCU的风险。

电路连接: 连接非常简单,但必须正确:

  1. 编码器的A相接至STM32定时器通道1对应的GPIO(如TIM2的通道1是PA0)。
  2. 编码器的B相接至定时器通道2对应的GPIO(如TIM2的通道2是PA1)。
  3. 编码器的GND接至STM32的GND。
  4. 编码器的VCC接至合适的电源(3.3V或5V)。
  5. (可选)Z相接至另一个GPIO,用于在中断中清零计数器,实现绝对位置校准。

实操心得:在面包板或洞洞板上连接时,务必给编码器的电源引脚就近放置一个0.1uF的瓷片电容进行去耦,这能极大减少因电源噪声导致的脉冲计数错误。另外,如果电机和编码器一体,电机的电源线和编码器的信号线最好分开走线,或使用屏蔽线,以降低电机启停对编码器信号的电磁干扰。

3. 基于STM32CubeMX与HAL库的工程配置详解

3.1 CubeMX工程初始化与定时器配置

我们以STM32F103C8T6的TIM2为例,展示从零开始的配置过程。

  1. 创建工程与选择MCU:打开STM32CubeMX,选择对应的MCU型号。
  2. 配置系统时钟(SYS):将Debug设为Serial Wire,否则可能无法烧录和调试。
  3. 配置时钟树(RCC):将HSE(外部高速时钟)设为Crystal/Ceramic Resonator,然后在时钟树中将系统主频(HCLK)设置到芯片允许的最高值(如72MHz),以获得更精确的定时。
  4. 配置定时器为编码器模式
    • Pinout & Configuration标签页,找到Timers->TIM2
    • Combined Channels设置为Encoder Mode
    • 此时,下方的Channel1Channel2会自动配置为Input Capture direct mode。检查对应的GPIO(PA0, PA1)是否已被正确分配。
  5. 关键参数配置:切换到Parameter Settings选项卡,这里每一个选项都至关重要。
    • Encoder Mode:这是模式选择。Encoder Mode TI1 and TI2意味着在TI1和TI2的边沿都计数,实现4倍频,分辨率最高。如果你的编码器信号质量不佳或转速极高导致CPU读取频率跟不上,可以考虑选择Encoder Mode TI1Encoder Mode TI2,仅在单相边沿计数(2倍频),但这会损失一半分辨率。
    • Polarity:保持默认Rising Edge即可,除非你的编码器逻辑是下降沿有效。
    • Prescaler (PSC):分频器。编码器模式下此参数无效,硬件会直接使用输入信号作为时钟源。这里填0即可。
    • Counter Period (ARR):自动重装载值。这是定时器计数器的上限。对于编码器模式,通常设置为最大值65535(16位定时器)。当计数器达到此值后会溢出并翻转。如果你需要测量绝对位置且圈数不多,可以将其设为一个编码器每转计数值(线数*4)的整数倍,便于软件计算圈数。
    • Counter Mode:会自动变为Up。在编码器模式下,硬件会根据方向控制其向上或向下计数,此设置不影响。
    • AutoReload Preload:建议Enable,确保ARR的更新在下一个更新事件生效,避免中间状态。
  6. 开启定时器中断(可选):在NVIC Settings中使能TIM2 global interrupt。如果你计划在计数器溢出时(即计数超过ARR或低于0)进行软件处理(如扩展为32位或64位计数器),就需要开启这个中断。

3.2 生成代码与用户代码集成

配置完成后,生成代码。CubeMX会生成完整的初始化代码,其中关键函数是HAL_TIM_Encoder_Init()HAL_TIM_Encoder_Start()

在你的主程序main.c中,需要添加以下步骤:

/* 1. 启动编码器接口 */ HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); // 启动通道1和2 /* 2. 定义一个全局变量用于存储速度计算相关的数据 */ int32_t g_encoder_total_count = 0; // 扩展后的总计数(考虑溢出) int16_t g_last_cnt = 0; // 上一次读取的CNT值 float g_current_speed_rpm = 0.0; // 计算出的转速 /* 3. 在定时中断(如SysTick或另一个定时器)中定期执行速度计算 */ void Speed_Calculation_Callback(void) // 假设这个函数每10ms被调用一次 { int16_t current_cnt = TIM2->CNT; // 直接读取计数器寄存器,速度最快 // 或者使用 HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1); 但稍慢 int16_t diff = current_cnt - g_last_cnt; // 计算差值 /* 处理计数器溢出:因为CNT是16位无符号数,但diff我们当作有符号数处理。 在10ms内,正常的diff绝对值不可能超过32767(对应电机极高转速)。 如果diff > 32767,说明发生了向下溢出(如从0减到65535)。 如果diff < -32767,说明发生了向上溢出。*/ if(diff > 32767) { diff -= 65536; } else if(diff < -32767) { diff += 65536; } g_encoder_total_count += diff; // 更新扩展后的总位置计数 g_last_cnt = current_cnt; // 更新上次值 // 速度计算将在下一节详细展开 }

注意事项HAL_TIM_ReadCapturedValue函数内部有一些状态检查和判断,对于超高频率的采样(如100us以下),直接访问TIMx->CNT寄存器是更高效的选择。但直接访问寄存器需要你确保操作的安全性(如考虑中断冲突)。

4. 从计数值到速度值:算法实现与优化

4.1 基础速度计算:M法测速

M法(频率法)是在固定时间间隔内测量脉冲数,适用于中高速测速。公式很简单:

转速 (RPM) = (ΔCount / (4 * P)) * (60 / T)

其中:

  • ΔCount:采样周期内的计数差值(有符号,代表方向)。
  • P:编码器线数(每转的脉冲数)。
  • 4:4倍频系数(如果使用4倍频模式)。
  • T:采样周期,单位是秒。
  • 60:将“转/秒”转换为“转/分钟”。

在代码中实现:

#define ENCODER_LINES 500 // 编码器线数 #define SAMPLE_TIME_S 0.01f // 采样周期10ms void Calculate_Speed_Method(int16_t pulse_diff) { // 计算每秒的转数 float speed_rps = (float)pulse_diff / (4.0f * ENCODER_LINES * SAMPLE_TIME_S); // 转换为每分钟转数 g_current_speed_rpm = speed_rps * 60.0f; // 如果需要线速度(m/s),假设轮子半径是r米 // float wheel_circumference = 2 * 3.1415926f * r; // float linear_speed = speed_rps * wheel_circumference; }

4.2 低速测量的挑战与T法引入

当电机转速很低时,在固定的采样周期T内,可能只捕获到很少几个脉冲,甚至为0。此时M法误差会非常大,分辨率极低。例如,10ms内只收到1个脉冲,算出的转速跳动会非常剧烈。

为了解决低速问题,可以采用T法(周期法):测量两个相邻脉冲之间的时间间隔。转速越低,脉冲间隔越长,测量时间反而越精确。公式为:

转速 (RPM) = 60 / (P * 4 * ΔT)

其中ΔT是连续两个同相位脉冲(如同为A相上升沿)的时间间隔,单位秒。这需要用到定时器的输入捕获功能,而不是编码器模式。一种混合策略是:高速时用M法,低速时自动切换到T法,即MT法。

4.3 软件滤波:让速度曲线更平滑

直接从公式计算出的速度值往往带有高频噪声(由于机械振动、电源纹波等)。直接用于PID控制会引起振荡。必须进行软件滤波。

移动平均滤波:最简单有效。

#define FILTER_WINDOW_SIZE 10 float speed_buffer[FILTER_WINDOW_SIZE] = {0}; uint8_t buffer_index = 0; float Moving_Average_Filter(float new_speed) { speed_buffer[buffer_index] = new_speed; buffer_index = (buffer_index + 1) % FILTER_WINDOW_SIZE; float sum = 0; for(int i = 0; i < FILTER_WINDOW_SIZE; i++) { sum += speed_buffer[i]; } return sum / FILTER_WINDOW_SIZE; }

一阶低通滤波(指数加权平均):更节省内存,反应速度可调。

float g_filtered_speed = 0; float alpha = 0.2; // 滤波系数,越小越平滑,但延迟越大。通常取0.1~0.3 void LowPass_Filter(float new_speed) { g_filtered_speed = alpha * new_speed + (1 - alpha) * g_filtered_speed; }

实操心得:滤波窗口大小或系数需要根据你的采样周期和控制周期来权衡。在平衡车项目中,控制周期通常很短(如5ms),滤波不能引入太大延迟,否则会导致系统不稳定。我个人的经验是,先用一个较小的窗口(如5点移动平均)或较大的alpha(如0.5)进行轻度滤波,将数据送入PID控制器。PID控制器自身的积分项也能起到一定的平滑作用。可以先在滤波后输出数据到串口,用上位机(如SerialPlot)观察波形来调整参数。

5. 高级话题与性能优化技巧

5.1 使用DMA搬运计数器值

在超高实时性要求的系统中,即使是在中断里读取CNT寄存器也可能引入微秒级的抖动。更极致的做法是使用DMA,将定时器的计数器寄存器(CNT)定期自动搬运到内存中。

STM32的某些定时器支持将CNT作为DMA的传输源。你可以配置一个DMA通道,在另一个定时器(如基本定时器TIM6)的更新事件触发下,自动将TIM2->CNT的值搬运到一个内存数组中。这样,CPU完全不用管计数器的读取,只需要在固定的时间点去处理内存数组中已经准备好的连续数据即可,实现了“双缓冲”甚至“环形缓冲”的数据采集,特别适合进行高速、无抖动的数据流分析。

5.2 多倍频与分辨率提升的误区

前面提到4倍频模式将分辨率提升了4倍。那能不能通过软件插值实现8倍频、16倍频呢?理论上,通过捕获A、B相上升沿、下降沿以及它们的组合状态变化,可以识别出更细的状态,但这需要极高的信号质量和软件处理开销,并且受限于编码器光栅的物理精度,边际效益很低。绝大多数情况下,硬件支持的4倍频已经是最优解。盲目追求软件倍频会引入巨大的噪声和不确定性。

5.3 应对极端速度:计数器溢出与扩展

当电机速度极快时,10ms内的计数差值ΔCount可能超过32767(对应方向判断的临界值),我们之前的溢出判断逻辑就会失效。此时有两种解决方案:

  1. 缩短采样周期:将速度计算中断提高到1ms甚至更短,确保单周期内脉冲数不会过多。
  2. 使用32位或64位扩展计数器:在定时器的更新中断(溢出中断)里,维护一个软件计数器。
    volatile int32_t encoder_overflows = 0; // 溢出次数,正向溢出+1,反向溢出-1 void TIM2_IRQHandler(void) // TIM2的溢出中断服务函数 { if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET) { if(TIM2->CR1 & TIM_CR1_DIR) { // 检查定时器方向位,0=向上计数,1=向下计数 // 如果是向下计数时溢出(从0到ARR),说明是反向溢出 encoder_overflows--; } else { // 向上计数时溢出(从ARR到0),说明是正向溢出 encoder_overflows++; } __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); } }
    最终的位置 =encoder_overflows * 65536 + current_cnt。这样就得到了一个完整的32位位置信息。

6. 调试、问题排查与实战心得

6.1 调试手段:让问题可视化

  1. 串口打印原始数据:将g_last_cnt,diff,g_current_speed_rpm通过串口以固定格式打印出来,用串口助手或SerialPlot绘制成曲线。观察电机静止时diff是否在0附近微小波动(应为0),匀速时速度曲线是否平稳。
  2. 逻辑分析仪/示波器:这是最强大的工具。直接测量连接STM32的A、B相GPIO引脚波形,检查信号是否干净,边沿是否陡峭,相位差是否准确。同时可以测量STM32另一个GPIO(调试引脚)的电平,在你的速度计算中断里翻转它,从而精确测量中断的执行周期和抖动。
  3. ST-Link调试器:在CubeIDE或Keil中单步调试,查看定时器寄存器(CNT, SR)的值,确认编码器模式是否真正启用,计数是否随电机转动而变化。

6.2 常见问题速查表

现象可能原因排查步骤与解决方案
计数器CNT不变化1. 编码器电源或接地不良。
2. GPIO模式未正确配置(应为浮空输入或上拉输入)。
3. 定时器未启动(HAL_TIM_Encoder_Start)。
4. 编码器信号线接反(A、B相接错)。
1. 用万用表检查电压和地线连通性。
2. 检查CubeMX生成的GPIO初始化代码。
3. 确保启动函数在while(1)之前被调用。
4. 交换A、B相线序试试。
计数方向与实际旋转方向相反编码器A、B相序与定时器通道映射不符。在CubeMX中尝试交换TI1TI2的极性设置,或者直接在代码里交换物理接线。
低速时速度值跳动大,有零值1. M法在低速下分辨率不足。
2. 软件采样周期过长。
3. 编码器信号有抖动。
1. 考虑改用或混合T法测速。
2. 缩短速度计算周期(如从20ms改为5ms)。
3. 在CubeMX中增加定时器输入滤波器的参数值(IC Filter)。
高速时速度值出现周期性错误计数器溢出处理逻辑有误。检查并优化本章第5.3节所述的溢出处理代码,确保在高速正反转时g_encoder_total_count能连续变化。
速度计算值总是理论值的2倍或4倍编码器线数P或倍频系数设置错误。核对编码器铭牌上的线数(PPR)。确认CubeMX中配置的是Encoder Mode TI1 and TI2(4倍频)。公式中的4*P要准确。

6.3 我的实战心得与避坑指南

  • 上电初始化位置:系统上电时,编码器位置是未知的。如果你的应用需要绝对位置,必须有一个“归零”或“寻参考点”的过程(例如,驱动电机直到碰到限位开关,或将计数器清零)。
  • 中断优先级管理:如果你的系统同时有编码器速度计算中断(如SysTick)和电机PWM控制中断,要合理设置它们的优先级。通常,控制中断的优先级应高于测量中断,以确保控制的实时性。速度计算晚几毫秒影响不大,但PWM输出晚了可能导致电机抖动。
  • 浮点运算开销:在Speed_Calculation_Callback中断函数中进行浮点除法(如/ SAMPLE_TIME_S)和乘法运算,对于主频不高的MCU(如72MHz的F103)可能负担较重。如果中断频率很高(如1kHz),可以考虑使用定点数运算,或者将除法转换为乘法(如预先计算1 / SAMPLE_TIME_S)。更简单的做法是,直接使用“每采样周期的计数值”作为速度反馈量给PID控制器,很多情况下PID参数重新整定后完全可以工作,省去了浮点转换的开销。
  • 电源是万恶之源:我遇到过最诡异的问题——电机一启动,编码器计数就乱跳。最后发现是电机驱动板和MCU共用了同一个廉价的LDO,电机启动瞬间将电源电压拉低,导致MCU和编码器工作异常。务必为数字部分(MCU、编码器)和功率部分(电机驱动)使用独立的电源或进行充分的磁珠/电感隔离,并在靠近芯片处布置足够的钽电容和瓷片电容。

掌握STM32 HAL库的定时器编码器模式,并理解从脉冲到速度的完整链条,你就打通了运动控制项目中最关键的一环。这套方法经过多个平衡车、智能小车项目的验证,稳定可靠。剩下的,就是根据你的具体应用场景,去微调滤波参数、优化控制算法了。记住,好的数据是控制的基础,在编码器测速上多花些时间调试,后续的PID整定才会事半功倍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询