简介这份STM32编码器测速代码面向嵌入式初学者、电赛控制类参赛者及电机控制开发者围绕STM32定时器的编码器模式解决增量编码器信号采集与电机转速精确测量问题。资源包共184个文件以34个c源文件、35个h头文件、34个o目标文件及33个crf编译中间文件为主另含uvprojx工程文件、axf可执行文件、map映射文件与sct分散加载文件压缩包约4.75MB构成一套可直接编译的完整工程。内容涉及定时器捕获比较配置、A/B相脉冲计数与方向判断、中断服务程序更新计数值以及将脉冲数换算为RPM的速度计算方法示例工程对应STM32F103C8T6平台。目前已有10793人学习下载适合作为电赛控制类项目的参考模板帮助读者理解编码器接口配置、定时器参数设置与测速代码的完整实现思路。1. STM32编码器测速代码从正交解码到转速计算的完整落地路径做电机控制的朋友大概率都经历过这个场景手里一块 STM32 板子电机轴上装了个增量式编码器A/B 两相脉冲引出来想读出转速结果要么计数方向反了要么低速时转速跳得厉害要么干脆定时器配置完读出来一直是零。STM32 编码器测速代码这件事表面看只是「读脉冲、算转速」实际涉及定时器编码器模式配置、正交信号四倍频、采样周期选择、转速滤波、溢出处理这一整条链路任何一环没对齐数据就没法用。这篇笔记面向的是已经能点亮 LED、跑通过串口、会用 CubeMX 或寄存器配置定时器的嵌入式开发者。我会把编码器测速从硬件信号到最终转速值的完整路径拆开讲定时器编码器模式怎么配、计数方向怎么判断、M 法和 T 法怎么选、转速怎么算、溢出和抖动怎么处理。读完你应该能直接在自己的板子上复现一套可用的测速代码并且知道每个参数改了会发生什么。2. 定时器编码器模式正交解码的硬件基础2.1 为什么用定时器硬件解码而不是外部中断增量式编码器输出 A、B 两路方波相位差 90 度。最原始的做法是把 A、B 接到外部中断引脚在中断里判断另一路的电平来决定加减计数。这个方法能跑但有几个硬伤电机转速一高脉冲频率轻松上到几十 kHz中断频繁触发会吃掉大量 CPU 时间更麻烦的是 A、B 两相在换向瞬间可能有毛刺中断里读到的电平不一定可靠计数就会丢或者多算。STM32 的通用定时器和高级定时器自带编码器接口模式A、B 信号直接进定时器的 CH1、CH2硬件负责判断相位关系并自动加减 CNT 寄存器。CPU 只需要在固定周期读一次 CNT 值负担极小。常见做法是选 TIM2、TIM3、TIM4 这类通用定时器它们都有编码器模式。高级定时器 TIM1、TIM8 也支持但通常留给 PWM 输出用。选型上要注意不是所有定时器都支持编码器模式。以常见的 F1 和 F4 系列为例TIM1 到 TIM5 一般支持TIM6、TIM7 是基本定时器不支持。具体查对应型号的参考手册看定时器章节里有没有「Encoder interface mode」这一节。2.2 编码器模式的三种计数方式和四倍频STM32 编码器模式有三个可选档位通过 TIMx_SMCR 寄存器的 SMS 位配置模式SMS 值计数触发等效倍频仅在 TI1 边沿计数001A 相每个边沿2 倍频仅在 TI2 边沿计数010B 相每个边沿2 倍频在 TI1 和 TI2 边沿都计数011A、B 每个边沿4 倍频四倍频是最常用的。假设编码器线数是 500 线电机转一圈 A 相输出 500 个脉冲四倍频后 CNT 变化 2000。分辨率提高了四倍低速时也能测到变化。配置时还要设 TI1、TI2 的输入捕获滤波对应 TIMx_CCMR1 里的 IC1F、IC2F 位。滤波值越大抗毛刺能力越强但会限制最高响应频率。一般电机编码器信号比较干净滤波设 0x3 到 0x5 就够如果线缆长、干扰大可以往上加但要确认不会把有效脉冲滤掉。2.3 用 CubeMX 配置编码器模式的完整步骤我一般先用 CubeMX 把引脚和定时器配好生成初始化代码再在工程里加测速逻辑。步骤如下第一步选一个支持编码器模式的定时器比如 TIM3。在 Pinout 视图里把 Combined Channels 设为 Encoder Mode。第二步在 Parameter Settings 里设置Counter PeriodARR设为 65535让计数器用满 16 位范围减少溢出频率。Encoder Mode选 TI1 and TI2即四倍频。Input Filter先设 3后续根据实际信号调整。Polarity默认 Rising Edge如果发现计数方向反了可以改其中一路的极性或者在代码里对结果取反。第三步生成代码后在 main 函数里调用 HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL)让编码器接口开始工作。生成的初始化代码里关键寄存器配置大致是这样/* TIM3 编码器模式初始化CubeMX 生成后精简 */ htim3.Instance TIM3; htim3.Init.Prescaler 0; // 编码器模式不用预分频 htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 65535; // ARR 拉满减少溢出 htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; sConfig.EncoderMode TIM_ENCODERMODE_TI12; // 四倍频 sConfig.IC1Polarity TIM_ICPOLARITY_RISING; sConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler TIM_ICPSC_DIV1; sConfig.IC1Filter 3; // 输入滤波抗毛刺 sConfig.IC2Polarity TIM_ICPOLARITY_RISING; sConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler TIM_ICPSC_DIV1; sConfig.IC2Filter 3; HAL_TIM_Encoder_Init(htim3, sConfig);这段代码里Prescaler 设 0 是因为编码器模式下计数时钟来自外部脉冲不是内部时钟预分频没意义。Period 设 65535 是为了让 CNT 在 0 到 65535 之间循环读的时候用 uint16_t 接就行。IC1Filter 和 IC2Filter 设 3对应滤波采样次数具体对应关系查手册的 IC1F 位定义。启动编码器接口后CNT 寄存器会随电机转动自动变化。正转时 CNT 增加反转时减少。读 CNT 用 __HAL_TIM_GET_COUNTER(htim3)或者直接读 TIM3-CNT。3. 转速计算方法M法、T法和周期采样3.1 M法测速的原理和适用场景M法是在固定时间窗口内数脉冲个数再换算成转速。公式是转速RPM (ΔCNT / (线数 × 倍频)) / 采样周期秒× 60其中 ΔCNT 是采样周期内 CNT 的变化量线数是编码器每转脉冲数倍频是 4四倍频模式采样周期是你自己定的定时器中断周期。M法适合中高速场景。因为采样周期固定ΔCNT 越大量化误差相对越小。比如采样周期 10ms转速 1000 RPM500 线编码器四倍频ΔCNT 1000/60 × 10ms × 500 × 4 ≈ 333量化误差 1/333 约 0.3%可以接受。但如果转速只有 10 RPMΔCNT 只有 3 左右量化误差就超过 30%数据会跳得没法看。3.2 T法测速的原理和低速优势T法测的是两个脉冲之间的时间。用一个已知频率的定时器做时基记录相邻两次编码器脉冲之间的计数值再换算成转速。公式是转速RPM 60 / (线数 × 倍频 × 脉冲间隔时间)T法在低速时精度高因为脉冲间隔长时基计数值大量化误差小。但高速时脉冲间隔很短时基计数值小误差反而变大。所以T法适合低速场景。实际工程里常用 M/T 法结合高速用 M 法低速用 T 法中间设一个切换阈值。但这样代码复杂度上升如果应用对低速精度要求不高单用 M 法加滤波也能凑合。3.3 用定时器中断实现固定周期采样我一般用一个基本定时器比如 TIM6产生固定周期中断在中断里读 CNT 并计算转速。采样周期设 10ms 到 50ms 之间看电机响应速度和控制环要求。控制环跑 1kHz 的话测速周期可以设 10ms每 10 次控制算一次转速。代码结构如下/* 测速相关变量 */ #define ENCODER_LINES 500 // 编码器线数 #define ENCODER_MULTIPLIER 4 // 四倍频 #define SAMPLE_PERIOD_MS 10 // 采样周期 10ms static int16_t last_cnt 0; static int32_t total_cnt 0; // 累计计数处理溢出 static float speed_rpm 0.0f; /* 定时器中断回调每 10ms 触发一次 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { int16_t curr_cnt (int16_t)__HAL_TIM_GET_COUNTER(htim3); int16_t delta curr_cnt - last_cnt; // 有符号差值自动处理正反转 last_cnt curr_cnt; total_cnt delta; /* M法计算转速ΔCNT / (线数×倍频) / 采样周期(s) × 60 */ float revs (float)delta / (ENCODER_LINES * ENCODER_MULTIPLIER); float seconds SAMPLE_PERIOD_MS / 1000.0f; speed_rpm revs / seconds * 60.0f; } }这里用 int16_t 接 CNT再做有符号差值是为了让正反转都能正确计算。如果 CNT 从 65535 跳到 0正转溢出int16_t 差值仍然是 1不会出错。这是利用补码特性的常见技巧。3.4 转速滤波一阶低通和滑动平均怎么选原始转速值通常有抖动尤其是低速时。我一般加一级一阶低通滤波/* 一阶低通滤波alpha 越小越平滑但延迟越大 */ #define SPEED_FILTER_ALPHA 0.3f static float speed_filtered 0.0f; speed_filtered SPEED_FILTER_ALPHA * speed_rpm (1.0f - SPEED_FILTER_ALPHA) * speed_filtered;alpha 取 0.2 到 0.5 之间比较常见。太小了响应慢电机加减速时转速读数跟不上太大了滤波效果不明显。如果控制环对延迟敏感可以用滑动平均窗口取 4 到 8 个采样点延迟比一阶低通小但需要存历史数据。4. 溢出处理与方向判断那些容易翻车的细节4.1 CNT 溢出为什么会让转速突然跳变ARR 设 65535 时正转 CNT 从 65535 回到 0反转从 0 回到 65535。如果直接用 uint16_t 读 CNT 再做无符号减法溢出瞬间会得到一个巨大的数转速直接飞了。用 int16_t 做有符号差值可以解决这个问题因为 65535 和 0 在补码里相差 1差值正确。但要注意如果采样周期内 CNT 变化超过 32767有符号差值也会出错。所以采样周期不能太长或者 ARR 不能太小。按 500 线编码器四倍频10ms 采样电机转速要超过 9830 RPM 才会溢出 32767一般电机到不了所以安全。4.2 计数方向反了怎么改方向反了有两种改法硬件上把 A、B 两相接线对调软件上在初始化时改其中一路的极性或者在计算 delta 时取反。我一般先在软件里试改 IC1Polarity 或 IC2Polarity重新生成代码烧进去看方向对不对。如果软件改完还是反的再动接线。还有一种情况是方向偶尔跳变这通常是信号干扰或者滤波不够。把 IC1Filter 和 IC2Filter 加大或者在编码器线缆上加屏蔽和磁环。4.3 低速时转速为 0 或跳变的排查思路低速时 M 法量化误差大转速可能显示 0 或者来回跳。排查步骤先确认编码器线数和倍频设置对不对。500 线四倍频转一圈 CNT 应该变化 2000。手动转一圈读 total_cnt 看是不是 2000 左右。如果线数没错看采样周期是不是太短。10ms 采样10 RPM 时 ΔCNT 只有 3 左右量化误差 33%。把采样周期加到 50ms 甚至 100msΔCNT 变大精度改善但测速延迟增加。如果应用必须低速高精度就得换 T 法或者 M/T 法。T 法需要把编码器脉冲引到另一个定时器的外部时钟输入或者捕获通道代码复杂度高一些但低速精度能到 1 RPM 以内。4.4 编码器信号干扰的硬件和软件对策干扰是编码器测速最常见的坑。现象是转速偶尔跳一下或者方向偶尔反一下。硬件上编码器线用双绞屏蔽线屏蔽层单端接地电机线和编码器线分开走不要捆在一起编码器电源加磁珠和电容滤波。软件上加大输入滤波 IC1Filter、IC2Filter转速加低通滤波如果干扰导致 CNT 偶尔跳变很大可以在中断里加一个变化率限制超过阈值的 delta 直接丢弃。5. 避坑与常见问题五条血泪经验5.1 读 CNT 时没关中断导致数据撕裂现象转速偶尔出现极大值或极小值。原因CNT 是 16 位寄存器读的时候如果正好发生溢出高字节和低字节可能来自不同时刻。解决读 CNT 前先关定时器中断读完再开或者用 __HAL_TIM_GET_COUNTER 宏它内部有处理。更稳妥的做法是在定时器更新中断里读此时 CNT 刚溢出值稳定。5.2 忘记启动编码器接口导致 CNT 一直为 0现象配置都对了但 CNT 不变化。原因HAL_TIM_Encoder_Start 没调用或者调用时通道参数写错。解决确认在 main 里调用了 HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL)并且返回值是 HAL_OK。如果用的是寄存器配置确认 TIMx_CR1 的 CEN 位已置 1。5.3 采样周期和转速计算单位不匹配现象转速数值差 60 倍或者 1000 倍。原因采样周期用了毫秒但公式里没换算成秒或者线数忘了乘倍频。解决把公式写清楚转速 RPM (ΔCNT / (线数 × 倍频)) / (采样周期_ms / 1000) × 60。每次改参数后手动验算一遍。5.4 编码器线数记错导致转速整体偏差现象转速读数始终是实际值的某个固定比例。原因编码器实际线数和代码里定义的不一致。解决手动转一圈读 total_cnt除以 4 得到实际线数和编码器标签对照。有些编码器标的是分辨率而不是线数注意区分。5.5 中断优先级冲突导致测速周期不稳定现象转速有周期性波动和 PWM 频率一致。原因测速定时器中断被 PWM 中断或者串口中断打断采样周期实际不固定。解决把测速定时器中断优先级设到合适位置不要被高频中断抢占或者用硬件定时器的输入捕获直接测周期减少软件中断依赖。6. 进阶技巧用输入捕获做 T 法测速和低速验证M 法在低速时的量化误差是原理性的想彻底解决就得用 T 法。T 法的思路是用另一个定时器的输入捕获通道捕获编码器 A 相的上升沿记录两次捕获之间的时基计数值再换算成转速。配置上选一个通用定时器比如 TIM2把 CH1 设为输入捕获模式触发边沿设上升沿预分频器设一个合适值让时基频率在 1MHz 左右。捕获中断里读 CCR1和上一次的值做差得到脉冲间隔的时基计数。/* T法测速输入捕获中断回调 */ static uint32_t last_capture 0; static float speed_t_method 0.0f; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { uint32_t curr_capture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uint32_t interval curr_capture - last_capture; // 时基计数间隔 last_capture curr_capture; if (interval 0) { /* 时基 1MHzinterval 个单位是 1us */ float pulse_time_s interval / 1000000.0f; /* 一个脉冲对应 1/(线数×倍频) 转 */ float revs 1.0f / (ENCODER_LINES * ENCODER_MULTIPLIER); speed_t_method revs / pulse_time_s * 60.0f; } } }这段代码里interval 是两次捕获之间的时基计数。时基 1MHz 时每个计数 1us。pulse_time_s 是单个编码器脉冲的时间。因为四倍频后每个脉冲对应 1/(线数×4) 转所以转速就是 revs / pulse_time_s × 60。T 法在低速时精度很高比如 1 RPM 时500 线四倍频脉冲间隔约 30ms时基计数 30000量化误差 1/30000可以忽略。但高速时脉冲间隔短比如 3000 RPM 时间隔约 10us时基计数只有 10误差 10%所以 T 法适合低速。实际工程里可以两个方法都跑高速用 M 法结果低速用 T 法结果中间加一个过渡区间做加权平滑。切换阈值根据编码器线数和采样周期算一般设在 M 法量化误差超过 5% 的转速点。验证方法用手动匀速转动电机轴或者用另一个已知精度的测速设备对照。我一般先用手转看转速读数是否跟手转速度一致再用电机驱动和驱动器显示的转速对比。如果偏差在 2% 以内基本可用。最后说个习惯每次改完编码器相关参数我都会手动转一圈读 total_cnt确认线数和倍频没错再看转速。这个动作花不了十秒但能省掉很多后面排查的时间。希望帮到你。本文还有配套的精品资源点击获取