简介STM32F103编码器程序是一份面向嵌入式开发者的完整参考工程针对增量型编码器信号读取与运动控制场景解决电机测速、位置反馈和方向判断等常见需求。压缩包共934个文件以C源码和H头文件为主包含汇编启动文件、链接脚本、IAR/Keil工程配置、说明文档等整体约9.99MB可直接导入IDE编译调试。工程基于标准外设库实现覆盖TIM定时器编码器模式配置、A/B相输入捕获、中断服务程序、方向判定逻辑并给出滤波与倍频处理思路便于理解编码器接口底层机制。已有2330人下载学习目录结构清晰代码注释较完整适合作为电机控制或机器人定位项目的直接参考模板。1. STM32F103编码器程序从正交解码到可靠测速一篇讲透第一次做电机闭环时很多人想当然地认为编码器就是两根脉冲线随便开两个外部中断数一下边沿就行。真把电机转起来才发现中断满天飞方向判断一团乱速度一高就开始丢步。折腾一圈后回到正解STM32F103编码器程序最可靠的做法是让内部定时器进入编码器模式用硬件自动捕获A/B相的两个边沿自动判断方向自动四倍频。CPU只在需要时读一下TIMx-CNT既不抢中断也不会漏脉冲。这篇文章从编码器模式的原理讲起给出可直接抄走的TIM3初始化代码、位置和速度换算公式然后把方向反了、Z相清零失效、高速丢步、计数值一直是0这几个高频翻车点逐个拆开。最后补上输入滤波、16位溢出扩展成32位、测速清零这三种进阶配置适合第一次接触编码器的开发者照着走也适合被丢步问题困住的老手对照排查。2. 编码器程序的设计起点定时器为何比外部中断更值得用2.1 正交编码器的信号本质A相、B相与Z相增量式编码器输出三路信号A相、B相、Z相。A/B两路波形频率相同相位差正好90度。正转时A相超前B相反转时B相超前A相这个超前关系就是方向信息。Z相每圈只输出一个脉冲并且在码盘的固定机械位置上出现通常用来做零点校准。很多国产编码器还带差分输出也就是A、A-、B、B-抗干扰更强但STM32F103的定时器输入是单端的差分信号要先经过一个接收芯片转成单端电平再接进单片机。这里有一个让新手绕不开的误区编码器的“线数”PPR指的是码盘一圈有多少个物理栅格对应A/B波一圈有多少个完整周期并不是直接输出多少个脉冲。200线编码器一圈有200个正交周期如果只在A相上升沿计数一圈只能数到200如果把A/B两路的上升沿和下降沿全部用上一圈能数到800。这个“四倍频”就是STM32F103编码器模式帮你做的事。实际上编码器内部输出元器件类型也会影响接线。推挽输出可以直接接单片机开漏输出必须加上拉电阻。如果你在电路上看到A/B信号对地只有一个几百欧下拉、没有上拉那多半是编码器型号和接口板不匹配信号会浮在中间电平导致定时器在边沿附近反复判断计数结果乱跳。所以写STM32F103编码器程序的第一步不是敲代码而是确认A/B/Z三根线上的静态电平是稳定高电平并且边沿干净。2.2 定时器编码器模式的计数原理与四倍频STM32F103的通用定时器和高级定时器都内置编码器接口。开启后定时器不再使用内部时钟计数而是由TI1和TI2两路输入信号的边沿驱动计数器。A相或B相每个边沿到来时硬件会采样另一路信号的电平从而得出计数方向正转加一反转减一。这个方向判断是纯硬件完成的CPU完全不需要参与。参考手册把这个模式称为编码器模式寄存器SMCR的SMS位决定计数方式。SMS001时只在TI1的边沿计数相当于二倍频SMS010时只在TI2的边沿计数也是二倍频SMS011时TI1和TI2的每个边沿都计数这就是四倍频。标准外设库用一行TIM_EncoderInterfaceConfig配置实际上就是设置SMCR的SMS位以及CCER里的CC1P、CC2P极性。如果你希望降低分辨率换取更高的输入频率上限可以把四倍频改成二倍频方向判断仍然有效只是每圈计数变为2×PPR。四倍频不是把信号频率乘四而是把每个正交周期里的4个边沿都变成计数事件。这要求A/B两路的占空比尽量接近50%相位差尽量接近90度。有些低成本编码器输出波形畸变严重A相高电平只有四分之一周期这种情况下四倍频的精度会下降但方向判断一般还能保住。真正让计数失准的是信号上的毛刺一个毛刺被当成一个边沿计数器就会多走一步。所以硬件滤波和软件滤波在编码器模式里不是可选项而是必选项。后面第5章会单独说滤波参数怎么设。2.3 选型TIM1/TIM2/TIM3/TIM4的资源差异STM32F103里几乎每个通用定时器和高级定时器都能做编码器模式但A/B相脚位是固定的选型时先看原理图上编码器接在哪。定时器CH1引脚CH2引脚TIM1PA8PA9TIM2PA0PA1TIM3PA6PA7TIM4PB6PB7TIM1是高级定时器TIM2、TIM3、TIM4是通用定时器编码器模式下功能没有本质区别。但TIM1往往要留给电机PWM互补输出和刹车建议优先用通用定时器。我一般默认选TIM3因为PA6/PA7很少被其他外设占用也不在JTAG默认引脚上调试最方便。如果PA6/PA7被占就选TIM4的PB6/PB7注意PB口要开启AFIO时钟。TIM2的PA0/PA1也可能被DAC、ADC等占用但编码器只是输入复用冲突要看具体项目。还有一个容易忽略的引脚复用部分封装支持重映射比如TIM3可以重映射到PB4/PB5TIM2可以重映射到PA15/PB3。但PB4、PB3、PA15在默认情况下是JTAG口重映射前必须关闭JTAG复用否则引脚功能不生效。我做开发板验证时吃过这个亏以为重映射后就能用结果调试器也连不上了。因此在STM32F103编码器程序里只要原引脚没被复用就不要随便开重映射省得把调试口一起关掉。选完定时器后还有一个基础配置要注意编码器模式下预分频器PSC必须保持为0自动重装载值ARR通常设成0xFFFF。PSC一旦不为0外部边沿会被分频直接丢脉冲ARR一旦设成其他值计数值会提前回绕位置信息就乱了。这两条是所有编码器模式程序的共同地基改不得。3. 用STM32F103编码器程序把位置和速度读出来初始化、读取与换算3.1 最小初始化代码TIM3编码器模式这套初始化针对的是最常见的接线编码器A相接PA6B相接PA7对应TIM3_CH1和TIM3_CH2。代码用标准外设库风格写因为能清楚看到每一步设置的寄存器行为。用HAL库的人也能对照底层原理是一样的。void Encoder_TIM3_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // A相PA6B相PA7配置为带上拉输入 // 编码器输出无论是推挽还是开漏上拉输入都能稳定读电平 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 编码器模式下PSC必须为0否则外部边沿被分频造成丢计数 TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 先配置CH1和CH2为直连输入滤波器放到后面再调 // CH1对应A相CH2对应B相 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM3, TIM_ICInitStructure); // 关键配置TI1和TI2双沿计数即硬件四倍频 // 两个通道极性都设为Rising正反方向由硬件根据相位差判断 TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); // 清空计数器然后启动编码器模式 TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE); }这段代码的逻辑很直接先开时钟再配GPIO输入模式然后配置时基和输入捕获最后调用TIM_EncoderInterfaceConfig进入编码器模式。有一个细节值得多说一句TIM_ICInitStructure先配置了输入捕获随后TIM_EncoderInterfaceConfig会覆盖部分极性位。如果你不小心把顺序颠倒先编码器模式后ICInit输入捕获配置会把编码器模式的计数路径打乱计数器可能一动不动。这是我实际调试时遇到过的坑后面第4章再展开。使用HAL库时对应操作是初始化定时器句柄、配置EncoderMode为TIM_ENCODERMODE_TI12然后调用HAL_TIM_Encoder_Start。HAL把很多事情包起来了查寄存器不如标准库方便。如果HAL初始化后计数异常我通常不看HAL代码直接读TIM3-SMCR、TIM3-CCER、TIM3-CNT三个寄存器马上就能定位问题。这也是我在STM32F103编码器程序上推荐标准库的一个理由。3.2 读取计数值并换算成角度/位移编码器模式启动后A/B相每来一个有效边沿TIM3-CNT就会加一或减一。读取时不要用int去接因为计数器是16位有符号循环要直接保存为int16_t后续做差分才不会被强制转换坑到。换算角度也很直接一圈总计数是4×PPR360度除以总计数就是每个脉冲代表的角度。#define ENCODER_PPR 200 int16_t Encoder_GetCount(void) { return (int16_t)TIM_GetCounter(TIM3); } float Encoder_GetAngleDeg(void) { int16_t raw (int16_t)TIM_GetCounter(TIM3); return (float)raw * 360.0f / (4.0f * ENCODER_PPR); } void Encoder_SetZero(void) { TIM_SetCounter(TIM3, 0); }这段代码里ENCODER_PPR是编码器线数也就是一圈的物理栅格数。200线编码器四倍频后一圈800个计数每个计数正好0.45度。如果你用的是13线编码器那就不能套用这个公式应改成4×1352个计数一圈角度步进接近6.92度这种低线数编码器只适合测速不适合做高精度定位。实际项目里还要考虑减速比。如果编码器装在电机输出轴上而负载是减速箱输出轴那么机械角度的换算需要乘以减速比。比如减速比是30输出轴转一圈电机轴转30圈编码器计数就是30×4×PPR。这个换算不要在中断里做浮点运算放在主循环或者速度任务里做就好中断里只读取整数计数。清零位置也有讲究。TIM_SetCounter(TIM3, 0)看起来简单但如果在电机高速旋转时调用清零瞬间正在到来的边沿可能刚好落在写CNT的过程中。硬件上写CNT不会把边沿吃掉但计数器的绝对参考点会模糊。我的习惯是外部绝对零点用Z相中断清零软件复位时只清自己的位置变量不直接清硬件计数器。这样即使清零时序有偏差后续差分测速也不会受影响。3.3 速度怎么算M法、T法与单位时间采样速度测量有两种主流方法M法和T法。M法在固定采样时间内统计脉冲增量脉冲多则速度快T法测量两个相邻脉冲的时间间隔间隔短则速度快。STM32F103编码器程序里M法实现最简单因为计数器已经把脉冲数累加好了CPU只需要每隔固定时间读一次差分。T法更适用于极低速因为低速时M法在10ms采样周期里可能一个脉冲都没有速度直接算成0。下面这段是M法测速的典型写法我用系统节拍tick做时基每10ms调用一次。static int16_t last_cnt 0; uint32_t Encoder_GetSpeedRpm(uint32_t period_ms) { int16_t curr_cnt (int16_t)TIM_GetCounter(TIM3); int16_t delta curr_cnt - last_cnt; float rpm; last_cnt curr_cnt; // delta是period_ms时间内的脉冲增量 // 一圈计数4*PPR一分钟60000ms rpm (float)delta * 60000.0f / (4.0f * ENCODER_PPR * period_ms); return (uint32_t)(rpm 0.5f); }这里最关键的是对delta的理解。curr_cnt和last_cnt都是int16_t两者相减时即使计数器发生了从0x7FFF到0x8000的回绕减法结果在有符号数语义下依然等于真实的脉冲差。比如上一周期计数是32700这一周期因为正向加了200变成32900但32900在16位里是-32336两者相减得到的仍然是200。这就是为什么必须用int16_t不要强行转成uint16_t。采样周期选多少直接影响速度曲线平滑度。10ms适合大多数电机速度环1秒有100个采样点转速变化能及时反映。如果你做的是高响应伺服可以把周期压到5ms但要注意主循环负载和串口打印的阻塞。反过来如果只是看转速显示20ms更稳低速时的波动会小很多。T法在STM32F103上通常用输入捕获中断实现把A相或B相接到另一个定时器的捕获通道每次捕获时读取CCR寄存器两个相邻CCR之差就是单个脉冲的间隔。因为F103的主频不高T法对中断时延很敏感我建议只在电机转速低于几十转每分钟并且确实需要平滑速度反馈时才引入。大部分项目用M法已经足够不要为了炫技把系统复杂度拉上去。4. STM32F103编码器程序常见问题与排查四个高频坑4.1 计数方向总是不对现象是电机正转计数值反而递减电机反转计数值递增。又或者手动转一圈计数值从0变成负数看起来完全没有规律。原因来自两个层面硬件上A相和B相接反A相接到了CH2B相接到了CH1软件上编码器模式的极性配置和实际电平不匹配。STM32F103编码器模式的方向由CCER里的CC1P、CC2P极性以及接入通道共同决定。如果A/B接线正确两个极性都设为Rising时方向为正把两个极性都改为Falling方向就会反过来。但如果你只改了一路的极性计数方向会乱掉甚至前后抖动。解决时先不要动代码先检查原理图确认A相确实接PA6B相确实接PA7。然后把电机手动正转半圈观察计数值变化。如果方向反了最简单的是把A/B两根线互换或者把TIM_EncoderInterfaceConfig里的两个极性同时从Rising改成Falling。我不建议在应用层把返回值取负来临时修正因为那会让溢出扩展、Z相清零里的方向判断与真实位置出现双重取反后续很难排查。这类问题的排查步骤通常是先看方向再看计数是否四倍频。用手转一圈如果一圈计数等于4×PPR说明硬件和软件配置正确。如果等于2×PPR说明SMS被设成了TI1或TI2模式也就是只在一路信号边沿计数。如果计数完全不动再回去查GPIO和复用配置。4.2 Z相清零失效或丢脉冲现象是Z相接到了外部中断程序里也确实在中断中清零计数器但实际运行中偶尔没有清零有时候一个机械周期清两次位置零点漂移。原因是Z相脉冲宽度很窄通常只有A相周期的几分之一如果中断优先级不够高或者主循环里长时间关中断脉冲边沿会被错过。F103的外部中断检测依赖电平边沿但Z相在高转速时脉宽可能只有几微秒如果刚好落在系统扛负载的瞬间丢失一个脉冲就要再等一整圈。解决时我一般把Z相接到EXTI并配置为双边沿触发。在中断里不直接TIM_SetCounter而是置一个“零点到达”标志主循环看到标志后重新同步位置变量。这样做的好处是中断处理时间极短不容易丢事件坏处是清零动作有延迟但位置同步对几十微秒的延迟不敏感。如果Z相引脚周围干扰比较大EXTI会反复触发导致一遍清零后再次清零。此时最简单的办法是在中断里记录一次时间戳两次Z相中断间隔小于2ms的直接忽略。还有一点如果Z相连接了上拉电阻必须确保Z相空闲是高电平、脉冲是低电平或者反过来要和你配置的触发边沿一致。用示波器挂Z相看一眼比在代码里猜要快得多。4.3 高速时丢步低速却正常现象是电机低速转到某一速度以下计数精度没问题一旦转到3000转以上计数值明显小于实际位置速度反馈也偏低。用示波器看A/B波形波形没问题。原因有几种可能。首先是输入滤波设得太大TIM_ICFilter为0x0F时硬件要求输入连续稳定多个内部时钟周期才认作有效沿高频窄脉冲直接被滤掉。其次是F103定时器的编码器模式下输入频率有上限内部计数采样速度跟不上外部信号时丢脉冲是必然的。最后是线材太长信号上升沿变缓在门限附近反复抖动也会让硬件计数错乱。排查时第一步把TIM_ICFilter从0x0F改成0x03或0x00。第二步确认定时器时钟确实到了72MHz。F103的APB1时钟最大36MHz但通用定时器在APB1分频不等于1时会自动乘2所以TIM3时钟通常就是72MHz。如果初始化代码里误把APB1设成了不分频TIM3时钟就是36MHz编码器模式的最大输入频率直接减半高速丢步就会提前出现。第三步是实实在在降频。如果四倍频后的脉冲频率已经逼近内部时钟的四分之一就应考虑把四倍频改成二倍频也就是用SMS001只对TI1计数。分辨率降一半但最高输入频率也降一半丢步问题立刻缓解。电机控制中速度和位置的精度并不完全依赖编码器分辨率控制周期内只要不丢脉冲零点校准后的角度依然可用。4.4 调试器里看计数值一直是0现象是程序烧进去后手动转编码器在调试器变量的窗口里看TIM_GetCounter数值始终是0。主循环里的串口打印也显示0。换一句程序压根没进入编码器状态。原因按照概率排序GPIO没配成输入定时器没开时钟TIM_Cmd没调用AFIO重映射把引脚占掉以及调试器暂停时计数器被冻结。还有一种比较隐蔽初始化顺序错了先调了编码器模式后调了TIM_ICInit导致计数路径被打断CNT根本不更新。解决时先做一个最小检查清单。第一确认RCC里已经开了TIM3和GPIOA时钟第二确认GPIO_Pin_6|GPIO_Pin_7配置的是输入模式而不是复用功能第三确认TIM_Cmd(TIM3, ENABLE)被调用第四直接在调试器里读TIM3-SMCRSMS位应该是011。第五读TIM3-CCERCC1E和CC2E都应该为1。如果SMCR和CCER都对CNT还是0就用示波器探PA6/PA7有没有脉冲。如果引脚悬空或电平恒定那多半是编码器没供电或者A/B信号压根没到MCU。这里要特别注意编码器是集电极开路输出时没有上拉电阻的话信号会被拉低MCU读不到任何边沿CNT自然不动。最后检查调试器配置。有些调试工具在CPU暂停时会冻结定时器导致你在暂停状态下看不到计数器变化。这不是程序问题把暂停冻结外设的选项关掉或者用串口打印代替调试器观察就能验证。5. 让编码器程序更稳的进阶配置输入滤波、溢出扩展与测速清零5.1 输入滤波参数怎么定CKD位与ETF位编码器模式下硬件输入滤波在每次边沿进入计数逻辑之前先要求信号电平连续保持若干个内部时钟周期。这个“连续保持多少个周期”由TIMx_CCMR1里的IC1F和IC2F字段控制。在标准库里对应TIM_ICInitStructure.TIM_ICFilter。取值越大抗抖动能力越强但能通过的最高输入频率越低。IC1F值等效滤波窗口适用场景0x00无滤波短引线、信号干净、高转速0x03约2个内部时钟周期常规电机线中等转速0x07约4个内部时钟周期线材较长、有干扰0x0F约8个内部时钟周期强干扰环境但高速易丢步我一般从0x00或0x03起步先跑起来再根据丢步和抖动情况调整。如果你一上来就设置0x0F也许低速计数很稳但一到高速就丢脉冲反而让人误以为定时器配置有问题。另一个参数是TIM_TimeBaseStructure.TIM_ClockDivision对应CKD位。这个位影响死区时间以及输入滤波的采样时钟分频编码器模式下通常设DIV1。如果外部信号沿很慢调整CKD不会比直接换一个施密特触发器整形更有效。硬件滤波和软件滤波要配合起来看。我在某图像处理Demo上曾经给A/B信号并了104电容滤得很干净但电机一转起来高速时计数值少了三分之一。原因是RC低通让信号沿变缓边沿到达门限的时间不确定定时器在没有滤波的情况下也会在门限附近误判。后来把RC的值调小同时把定时器的ICFilter调到0x03高速和低速才都能兼顾。经验是总延迟不能超过A相最小脉冲宽度的三分之一否则会吃掉有效边沿。5.2 16位溢出扩展成32位中断里只做加减法TIM3的计数器只有16位一圈800个计数时转41圈左右就会超过32767的符号范围。虽然用int16_t做速度差分没问题但位置累加不能只靠16位寄存器否则长时间运行后位置值会回绕。常见做法是开启TIM3的更新中断在溢出或下溢时维护一个高16位变量把位置扩展成32位。volatile int16_t high_cnt 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // DIR位为1表示向下计数即下溢位置应向负方向移动 if (TIM3-CR1 TIM_CR1_DIR) { high_cnt--; } else { high_cnt; } } } int32_t Encoder_GetCount32(void) { uint16_t low; int32_t value; // 先关中断避免读取高16位和低16位之间插入更新中断 __disable_irq(); low (uint16_t)TIM_GetCounter(TIM3); value ((int32_t)high_cnt 16) | low; __enable_irq(); return value; }这段代码里的中断只做加减法不做任何PID计算或浮点运算所以执行时间极短。每次计数器从0xFFFF回绕到0时high_cnt加一从0下溢到0xFFFF时high_cnt减一。读取32位值时关中断是为了保证高16位和低16位来自同一个瞬间。如果不关中断可能在高16位读取之后、低16位读取之前发生组合出来的值会比真实位置偏差65536。很多人会问为什么不直接在读取时读两次高16位来验证那也是一种办法但在F103上关中断最直接。注意__disable_irq会关掉所有可屏蔽中断如果你的编码器更新中断优先级非常低而另一个更高优先级的中断频繁触发关中断时间可能过长。所以在F103上建议把所有和编码器组合读取相关的中断放在同一个抢占优先级或者至少保证更新中断的优先级不高于读取临界区所在上下文否则临界区保护会失效。还有一个容易翻车的地方更新中断里不要尝试读取TIM_GetCounter来计算方向因为在更新中断触发时计数器已经完成回绕CNT值可能已经变成了0或0xFFFF但方向判断要用CR1的DIR位。DIR位在编码器模式下会随正反转实时更新是正确的方向来源。如果只看CNT在事件前后的变化高速时很容易误判导致high_cnt振荡。5.3 测速清零模式位置和速度用同一个计数器编码器模式下的计数器天然适合“差分测速”定期读取当前值减去上一次读取的值差值就是该周期内的脉冲增量。还有一种写法是每次采样后直接把计数器清零这样下一次采样读到的值就是增量省去减法。int16_t Encoder_GetSpeedByClear(void) { int16_t cur (int16_t)TIM_GetCounter(TIM3); // 清掉计数器下一次读取直接作为增量 TIM_SetCounter(TIM3, 0); return cur; }这段代码在10ms周期中断里调用返回的cur直接就是周期内的脉冲增量反转时为负。这种测速清零方式在只关心速度、不关心绝对位置的场景里非常简洁中断里不用保存last_cnt也不需要溢出扩展。但它的前提是“每次采样后必须成功清零”如果某次周期任务卡顿导致清零推迟了几百微秒那几百微秒内的脉冲就会混入下一次采样周期速度曲线出现毛刺。我自己的习惯是优先用“只读不清零”的差分法。因为读CNT不写CNT和硬件计数器之间没有竞争配合32位扩展后位置和速度两个量可以同时从同一个数据链路得到。清零法虽然代码少但可观测性差一点出了问题不好判断是清零晚了还是采样晚了。如果确实用清零法有一个小技巧不要在速度任务里清零而是在采样点先读再清零清零后立刻把当前计数器值赋给last_cnt这样下次差值天然等于间隔内的增量。这其实和差分法最终等价但多了一次写寄存器操作。两种方法选一个就好不要在同一套代码里混用否则位置变量会越跑越乱。6. 最后一个技巧用编码器程序把系统时钟和中断优先级理顺很多STM32F103编码器程序初始化看起来没问题但跑到高速就丢步问题往往不在编码器模式本身而在系统时钟。F103的APB1最大是36MHz但通用定时器时钟在APB1分频不等于1时会自动乘2。很多人默认TIM3时钟就是72MHz可如果初始化代码把APB1设成了不分频TIM3实际只有36MHz编码器模式最大输入频率直接减半高速丢步就来了。我的建议是初始化时钟后先调用RCC_GetClocksFrequencies把TIM3的时钟打印出来确认是72MHz还是36MHz再往下调编码器。这个动作只需要一分钟却能把“玄学丢步”变成“参数错误”。另一个经验是中断优先级的分配。编码器更新中断负责维护32位高位计数采样时基负责差分测速。如果把采样时基设为最高优先级而更新中断优先级比它低那么当采样中断正在读取高16位时更新中断无法抢占组合读数可能跨越两次更新事件偶尔位置值跳变一大块。我在一次电机闭环调试中就被这种跳变坑过第一次遇到还以为是位置清零逻辑错了后来才意识到是中断抢占顺序惹的祸。解决办法是把更新中断的优先级设得比采样时基更高或者在读取32位组合值时用临界区保护。两条路选一条就够别同时依赖多个手段不然代码变得很难维护。验证编码器程序是否真正写对了我有一个很简单的流程手转电机而不是电转。把编码器接好后程序只在串口打印计数值和方向。静置时计数值必须纹丝不动如果手指轻轻碰一下编码器线计数就跳说明输入滤波太弱或者上拉配置不合理。然后慢速转一圈如果一圈计数值接近4×PPR说明四倍频和方向都对了。最后快速转一下观察速度值不能出现瞬间跳到几千的突变出现突变就回到滤波和时钟上查。这三步能滤掉绝大多数初始化错误之后再接入PID闭环就不会被编码器数据本身误导。这套程序我从第一次接触编码器到现在用了很多年踩过的坑无外乎时钟没查、滤波乱设、优先级颠倒。现在每到一个新平台第一件事不是抄初始化代码而是先把时钟、GPIO、中断优先级三者确认完再动编码器模式配置。希望帮到你。本文还有配套的精品资源点击获取