
简介面向嵌入式初学者与电机控制开发者的 STM32F103 编码器程序围绕增量式编码器脉冲采集、TIM 定时器计数与中断处理展开可用于电机转速测量、位置检测及闭环控制等实验场景。压缩包共 58 个文件约 1.36MB包含 Keil 工程与项目配置文件uvproj、uvmpw、uvopt、C 语言源码与启动汇编文件、hex 可执行文件以及 d、o、crf 等编译中间文件和 map 映射文件既方便直接打开工程复现也便于对照编译过程排查问题。已有 2030 人学习下载说明其在 STM32 入门与编码器接口实践中具有一定参考价值。代码覆盖 GPIO 输入配置、TIM 编码器模式或脉冲捕获、中断优先级设置与中断服务函数中读取计数值等关键环节读者可据此理解位置与速度信息的换算方法并进一步结合滤波、PID 调节完善电机控制方案。1. 从一块读不准的编码器说起STM32F103 编码器程序的真实用途把 STM32F103 编码器程序跑通卡住大多数人的不是代码写不出来而是写完数不对。之前帮人看一台小车线接好了、TIM3 也配成了编码器模式串口打出来的计数正转涨、反转也涨电机一快就干脆卡在某个值不动查了两天才发现是 16 位计数器回绕没处理。这套程序真正干的事就四件用定时器硬件做正交解码、把 16 位计数扩成 32 位位置、按固定节拍算出转速、向上一层只暴露三个接口。它适合做电机闭环、云台、小车里程计、旋钮面板的开发者也适合刚摸 F103 想弄明白定时器编码器模式的人。下面按接线、骨架、代码、踩坑、改造的顺序拆。2. 硬件与选型正交信号怎么落到 F103 的定时器上编码器读数不对一半的锅在硬件。软件配得再漂亮A/B 两路只进来一路、或者 5V 信号直接怼进不耐 5V 的引脚后面全是无用功。这一章先把物理层和定时器资源定下来再谈代码。2.1 A/B 两路信号里到底藏了多少信息增量式编码器输出 A、B 两路方波相位差 90 度转向就体现在谁超前谁。单看一路只能数脉冲、判断不了方向两路合起来每个周期内有四个可计数的边沿A 上升、B 上升、A 下降、B 下降这就是所谓四倍频。所以标称 1000 线的编码器电机轴转一圈硬件编码器模式下计数器会走 4000 个数。这里有个最容易算错的账实际每转计数等于「线数 × 4 × 减速比」。带 1:30 减速箱的电机输出轴转一圈计数器要走 120000。用 16 位计数器直接读两圈不到就溢出了这个数字一开始不写清楚后面算转速、算位移全是错的。我一般会在头文件里把这三个量做成宏改减速比只改一处/* encoder.h —— 机械参数集中在这里换电机只改这三行 */ #define ENC_PPR 1000.0f /* 编码器线数每转脉冲数 */ #define ENC_GEAR_RATIO 30.0f /* 减速比直连电机写 1.0 */ #define ENC_QUAD 4.0f /* 硬件四倍频固定为 4 */ /* 电机输出轴转一圈对应的计数值 */ #define ENC_CNT_PER_REV (ENC_PPR * ENC_QUAD * ENC_GEAR_RATIO)参数说明ENC_PPR按编码器铭牌填注意区分「线数」和「分辨率」有些厂家标的是四倍频之后的数ENC_GEAR_RATIO是减速比不是减速箱型号ENC_CNT_PER_REV不要在运行时反复算做成编译期常量除法和乘法交给编译器。用硬件编码器模式而不是外部中断理由很实际1000 线编码器配 3000rpm 电机四倍频后边沿频率能到 200kHz 量级中断进去出来一次就几微秒CPU 基本没空干别的定时器的编码器接口是纯硬件状态机不占 CPU还能自带方向位。2.2 F103 上能接编码器的定时器与引脚对照F103 不是所有定时器都能接编码器。基本定时器 TIM6/TIM7 没有输入捕获通道直接排除TIM1、TIM8 这类高级定时器可以接但通常要留着出互补 PWM 驱动电机占了不划算。真正的选择在 TIM2/3/4/5 这几个通用定时器上。定时器CH1 / CH2 默认引脚重映射实际使用注意TIM1PA8 / PA9无高级定时器一般留给 PWMTIM2PA0 / PA1可全映射到 PA15 / PB3与 TIM5 抢同一组引脚TIM3PA6 / PA7部分映射到 PB4 / PB5最常用注意与 SPI1 冲突TIM4PB6 / PB7映射到 PD12 / PD13默认与 I2C1 冲突TIM5PA0 / PA1无与 TIM2 二选一TIM8PC6 / PC7无高级定时器同上有个血泪经验F103 上所有定时器都是 16 位没有例外。别看到某些系列有 32 位计数器就想当然回绕处理是必答题不是选答题。另外 F103 的 PA0~PA7、PB0/PB1、PC0~PC5 这些带 ADC 通道的引脚不是 5V 容忍的而很多编码器是 5V 推挽输出。也就是说选 TIM3 的 PA6/PA7就必须加分压或者电平转换选 TIM4 的 PB6/PB7 才是 5V 容忍的可以直接接。这一条决定了你画板子时到底引哪几根线出来。2.3 输入级电路5V 编码器接 3.3V MCU 的分压与上拉三种常见编码器输出形式接法完全不同推挽输出直接接5V 的话必须分压。两个电阻 10k/20k 分压到 3.3V 以内即可别用太小的电阻增加功耗。开漏/集电极开路必须外接上拉上拉到 3.3V 而不是 5V否则又回到分压问题。上拉电阻 2.2k~10k线长就取小一点。差分输出线驱动加一片差分接收芯片转成单端长距离、强干扰场合才需要。滤波电容要克制。A/B 线上并 100pF 到地能压毛刺但并 100nF 就开始吃掉高速脉冲的边沿电机一转快直接丢计数这种玄学故障最难查。我一般板上只留 100pF 的位置剩下的交给定时器内部的输入滤波器IC1Filter / IC2Filter去处理可调、可关调试时能改。还有一条容易被忽略编码器地和 MCU 地必须共地而且走线尽量短。屏蔽线的屏蔽层单端接地两端都接反而形成地环路电机一启动计数就开始乱。3. 工程骨架从引脚复用到 1ms 采样节拍的完整链路硬件定了之后接下来是软件怎么分层。编码器这个东西看着简单但它在工程里横跨三层底层定时器、中间解算、上层控制。层次没切干净后面加 PID 的时候会很难受。3.1 文件划分一个 encoder 模块该管到哪一层我习惯这么切Project/ ├── Core/ │ ├── Inc/ │ │ ├── encoder.h # 对外只暴露 Init/Update/Get 三类接口 │ │ ├── bsp_tim.h # 1ms 时基、PWM 等定时器资源 │ │ └── motor.h # 上层控制只依赖 encoder.h │ └── Src/ │ ├── encoder.c # 定时器配置 回绕扩展 测速 │ ├── bsp_tim.c # TIM6 1ms 中断 │ ├── motor.c # 速度环/位置环 │ ├── stm32f1xx_it.c # 中断只是转调不写逻辑 │ └── main.c # 只负责初始化和调度 └── Drivers/ # HAL 库关键约定有三条这是我踩过坑之后固定下来的第一encoder.c里不出现任何控制算法它只回答「现在在哪、多快」第二中断服务函数里只做一件事置标志位或者调用一次极短的更新第三motor.c不许直接碰htim3只能通过encoder.h的接口拿数据。这套约定听着像教条但等你哪天要把 TIM3 换成 TIM4 的时候就知道值了改一个文件的事。encoder.h的对外接口我一般收敛到下面这个程度/* encoder.h —— 上层能看到的全部内容 */ typedef struct { int32_t total_cnt; /* 累计计数已做 16 位回绕扩展有符号 */ float position_deg; /* 输出轴角度单位度多圈累计 */ float speed_rpm; /* 输出轴转速转/分已滤波 */ int8_t dir; /* 1 正转-1 反转0 静止 */ } Encoder_t; void Encoder_Init(void); void Encoder_Task1ms(void); /* 1ms 周期调用更新结构体 */ void Encoder_Get(Encoder_t *out); /* 拷贝快照避免读一半被中断改掉 */ void Encoder_Reset(void); /* 清位置上电对零用 */参数说明total_cnt用int32_t而不是uint32_t因为要支持负方向position_deg是机械角不是电角度电角度还要乘极对数Encoder_Get用结构体拷贝而不是返回指针防止上层算到一半被 1ms 中断打断拿到撕裂数据。3.2 CubeMX 里必改的几项与等效寄存器写法用 CubeMX 配 TIM3几个默认值必须手动改第一Combined Channels 选 Encoder Mode这时 CH1、CH2 的输入捕获会自动打开第二Encoder Mode 选 TI1 and TI2这才是四倍频选 TI1 only 就只有两倍频第三Counter PeriodARR填 65535Pre-scaler 填 0任何分频都会丢计数第四IC Filter 从 0 改成 6 左右具体看线长和干扰第五GPIO 配成 Input Pull-Up不是 Alternate FunctionF103 的定时器输入不需要选 AF。不想依赖 CubeMX 的话等价的寄存器写法是这样的/* 纯寄存器版TIM3 四倍频编码器模式PA6/PA7 */ static void TIM3_Encoder_Mode_Init(void) { RCC-APB1ENR | RCC_APB1ENR_TIM3EN; RCC-APB2ENR | RCC_APB2ENR_IOPAEN; /* PA6/PA7上拉输入CNF10 输入带上下拉MODE00 输入模式 */ GPIOA-CRL ~(GPIO_CRL_MODE6 | GPIO_CRL_CNF6 | GPIO_CRL_MODE7 | GPIO_CRL_CNF7); GPIOA-CRL | (GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_1); GPIOA-ODR | (GPIO_ODR_ODR6 | GPIO_ODR_ODR7); /* 上拉 */ TIM3-PSC 0; /* 不分频编码器模式其实用不到 */ TIM3-ARR 0xFFFF; /* 满量程让回绕来得慢一点 */ TIM3-CCMR1 (1U 0) /* CC1S01IC1 映射到 TI1 */ | (1U 8) /* CC2S01IC2 映射到 TI2 */ | (6U 4) /* IC1F0110输入滤波 6 档 */ | (6U 12);/* IC2F0110 */ TIM3-CCER ~(TIM_CCER_CC1P | TIM_CCER_CC2P); /* 都不反相 */ TIM3-SMCR (1U 0) /* SMS001编码器模式 1TI1 和 TI2 都计数 */ | (0U 4); /* 不滤波极性由 CCER 决定 */ TIM3-CNT 0; TIM3-CR1 | TIM_CR1_CEN; }逻辑说明CCMR1里的 CC1S/CC2S 是把捕获通道的输入源切到 TI1/TI2这两位置了以后 CH1/CH2 的引脚归属就交给定时器了SMCR的 SMS001 才是编码器模式 1配合 TI1 和 TI2 同时有效等效四倍频CCER里的 CC1P/CC2P 是反相位方向反了以后要么软件取负、要么改这两位、要么硬件把 A/B 对调三种做法里我一般选改极性不用动线。注意F103 的 TIM3 挂在 APB1 上如果 APB1 分频系数不是 1定时器时钟会被 2 倍频。72MHz 主频、APB1 分频 2 的常见配置下TIM3 时钟仍然是 72MHz。这个细节在算速度时会用到写错了转速会差两倍。3.3 1ms 时基为什么速度解算不能塞进编码器中断编码器模式下没有「编码器中断」这回事计数器是硬件自己加的。所以速度解算一定要有独立的时基我用 TIM6 开 1ms 中断或者直接挂到 SysTick 上。一个常见的错误是把速度计算写成「每次读到计数变化就更新一次速度」。这样在高速时更新频率很高低速时几秒才更新一次PID 拿到的是一个变周期采样信号微分项直接爆掉。控制环最怕采样周期不稳宁可 1ms 固定、数据略有量化误差。/* bsp_tim.c —— TIM6 提供 1ms 硬时基优先级设成最低 */ void BSP_TimeBase_Init(void) { RCC-APB1ENR | RCC_APB1ENR_TIM6EN; TIM6-PSC 7200 - 1; /* 72MHz / 7200 10kHz */ TIM6-ARR 10 - 1; /* 10kHz / 10 1kHz即 1ms */ TIM6-DIER | TIM_DIER_UIE; TIM6-CR1 | TIM_CR1_CEN; NVIC_SetPriority(TIM6_IRQn, 15); /* 最低优先级不许抢别的中断 */ NVIC_EnableIRQ(TIM6_IRQn); } /* 中断里只做一件极短的事 */ void TIM6_IRQHandler(void) { if (TIM6-SR TIM_SR_UIF) { TIM6-SR ~TIM_SR_UIF; Encoder_Task1ms(); /* 读计数、算速度、滤波 */ } }参数说明PSC和ARR的组合是 72MHz 下最省事的 1ms 方案换主频要重算NVIC_SetPriority给 15 是为了不打断串口、定时器捕获这些对时序敏感的中断中断里没做任何浮点以外的重活Encoder_Task1ms内部也只做一次减法和一次乘加几十个时钟周期就出来了。如果你的控制环是 5ms 或者 10ms 周期也别在这个中断里跳拍累加让Encoder_Task1ms始终 1ms 跑控制环自己按计数分频取数据这样速度数据的时间分辨率永远是 1ms改控制周期时不用动编码器模块。4. 核心代码拆解编码器初始化、回绕扩展与 M/T 测速上面把硬件和骨架铺完了这一章全是能抄的东西。我会按「初始化 → 读计数 → 算速度 → 封装」的顺序逐段过每段都说明白为什么这么写、改哪里。4.1 编码器模式初始化TIM3 四倍频配置逐行看以 HAL 库版本为例这段代码看着长真正有效的就那几个宏/* encoder.c —— TIM3 编码器模式初始化PA6/PA7 */ #include encoder.h TIM_HandleTypeDef htim3; void Encoder_Init(void) { TIM_Encoder_InitTypeDef sConfig {0}; TIM_MasterConfigTypeDef sMaster {0}; /* 1. 时钟TIM3 在 APB1 上72MHz 主频下定时器时钟仍是 72MHz */ __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* 2. 引脚F103 的定时器输入不需要选复用功能配成上拉输入即可 */ GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, gpio); /* 3. 时基不分频计数范围拉满16 位不可避免要回绕 */ htim3.Instance TIM3; htim3.Init.Prescaler 0; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 0xFFFF; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; /* 4. 编码器模式TI1 和 TI2 都参与计数等于四倍频 */ sConfig.EncoderMode TIM_ENCODERMODE_TI12; sConfig.IC1Polarity TIM_ICPOLARITY_RISING; sConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler TIM_ICPSC_DIV1; sConfig.IC1Filter 6; /* 滤波档位线长干扰大就往上加最多 15 */ sConfig.IC2Polarity TIM_ICPOLARITY_RISING; sConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler TIM_ICPSC_DIV1; sConfig.IC2Filter 6; HAL_TIM_Encoder_Init(htim3, sConfig); /* 5. 主从模式不用动编码器模式自带方向判断 */ sMaster.MasterOutputTrigger TIM_TRGO_RESET; sMaster.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim3, sMaster); __HAL_TIM_SET_COUNTER(htim3, 0); HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL); }逻辑说明EncoderMode TIM_ENCODERMODE_TI12是四倍频的唯一开关选成TI1就只有两倍频、分辨率掉一半这个参数选错了不会报错只会让你后面算的所有物理量差两倍。IC1Filter和IC2Filter是数字滤波器取值 0~15值越大滤得越狠代价是能通过的脉冲频率上限下降1000 线编码器高速运转时我一般不超过 8。Period 0xFFFF配上Prescaler 0意思是计数器从 0 数到 65535 然后回绕。有人想着「我设个小一点的 ARR是不是就不会溢出了」——正好相反ARR 越小回绕越快这里必须拉满。还有一点HAL_TIM_Encoder_Start启动的是通道计数器本身在CR1的 CEN 位置 1 之后就开始跑了。如果初始化完之后发现计数一直是 0先查这一步有没有执行到。4.2 16 位计数器的回绕扩展一行有符号差值解决的事这是整个项目里最值钱的一行代码。16 位计数器从 65535 加到 0 的时候如果你用uint16_t直接相减会得到一个巨大的正数位置瞬间跳到几万PID 直接疯掉。正确做法是把差值当成有符号数来解释/* 16 位计数器回绕的软件扩展把有符号差值累加进 32 位位置 */ static int32_t s_total; /* 累计计数4 倍频后的原始脉冲数 */ static uint16_t s_last; /* 上一次的 CNT 快照 */ int32_t Encoder_Update(void) { uint16_t now (uint16_t)__HAL_TIM_GET_COUNTER(htim3); /* 关键先强转 int16_t 再相减跨 0/65535 边界也能得到正确增量 */ int16_t delta (int16_t)(now - s_last); s_last now; s_total delta; /* 累加进 32 位正负都对 */ return s_total; }逻辑说明now - s_last在 C 里会先做整型提升按int算再截断成uint16_t最后强转int16_t时结果落在 -32768~32767 之间。当计数器从 65535 跨到 0now0, s_last65535无符号差是 1截断后还是 1正好对应「又前进了一个脉冲」反过来反转跨边界时得到 -1。整个 trick 成立的前提是两次采样之间增量绝对值小于 327681ms 采样、1000 线编码器、3000rpm 的情况下增量也就几百远远够用。参数说明s_last必须用uint16_t用int32_t存会引入符号问题s_total用int32_t量程 ±21 亿按每转 120000 计数算能记一万七千多圈够用。如果采样周期拉到 100ms 以上或者编码器线数特别高就要评估一下会不会超 32768超了就得改成两次读或者缩短周期。同一套逻辑可以顺手把方向也拿出来delta 0是正转delta 0是反转delta 0是静止比读定时器的方向位还直接。4.3 M/T 法测速与一阶低通速度这块有两种做法一种是固定周期数脉冲M 法低速时一个周期就数到一两个脉冲误差极大另一种是固定脉冲测时间T 法高速时时间太短测不准。工程上常用 M/T 法高速按 M 法算低速切 T 法切换点按「一个采样周期内至少 N 个脉冲」来定。大部分场合 1ms 采样配 M 法已经够用先把简单的写对/* 1ms 周期调用M 法测速 一阶低通滤波 */ #define TS 0.001f /* 采样周期 1ms必须和定时器中断一致 */ static float s_speed_rpm; static int32_t s_last_total; void Encoder_Task1ms(void) { int32_t now Encoder_Update(); int32_t delta now - s_last_total; s_last_total now; /* rpm 增量 / 每转计数 / 采样周期 * 60 */ float rpm (float)delta / ENC_CNT_PER_REV / TS * 60.0f; /* 一阶低通alpha 大跟手小则平滑0.2~0.3 是常用起点 */ s_speed_rpm 0.25f * (rpm - s_speed_rpm); }逻辑说明delta是这 1ms 内走的脉冲数除以每转计数得到「转了多少圈」再除以 0.001 秒就是「每秒转多少圈」乘 60 换成 rpm。单位链条一定要理顺我见过把 60 忘了乘、转速差 60 倍然后去调 PID 参数调一下午的。0.25f这个滤波系数决定了噪声和响应速度的取舍。系数 1.0 等于不滤波原始数据抖得厉害0.1 以下波形很漂亮但阶跃响应迟钝速度环会明显滞后。手动调的时候先给 0.25看串口波形噪声大就往下调、响应慢就往上调。注意Encoder_Task1ms里不要放printf。串口打印一个几十字节的浮点数据轻松超过 1ms中断会被拖死表现是「一开打印电机就失控」。调试时用环形缓冲把数据存起来主循环慢慢发或者干脆用 DA 输出看波形。低速场合要补 T 法在 1ms 判断如果delta的绝对值小于 3就换成测量两个边沿之间的时间用输入捕获通道的 CCER 值算。切换到 T 法时记得把滤波系数调小两种方法的噪声特性不一样。4.4 对上层只暴露三个接口最后把结构体填好交给上层/* 三个对外接口初始化、周期任务、读快照 */ static Encoder_t s_enc; void Encoder_Get(Encoder_t *out) { /* 临界区保护1ms 中断可能正在改结构体 */ __disable_irq(); *out s_enc; __enable_irq(); } void Encoder_Reset(void) { __disable_irq(); s_total 0; s_enc.total_cnt 0; s_enc.position_deg 0.0f; s_enc.speed_rpm 0.0f; s_enc.dir 0; __enable_irq(); }Encoder_Task1ms内部在算完之后补上这几句s_enc.total_cnt s_total; s_enc.position_deg (float)s_total / ENC_CNT_PER_REV * 360.0f; s_enc.speed_rpm s_speed_rpm; s_enc.dir (delta 0) - (delta 0); /* 三元技巧得到 1/0/-1 */参数说明__disable_irq()保护的只是结构体拷贝那一小段几个时钟周期不会影响别的中断响应(delta 0) - (delta 0)是 C 里拿符号的惯用写法比写 if-else 短编译器也不会生成分支。position_deg是多圈累计的掉电不保存需要绝对位置就得上电找零点或者用绝对式编码器。上层电机控制拿到这个结构体之后判断方向直接看dir算位置误差直接减position_deg不需要再关心底层用的是哪个定时器、计数器多少位。5. 避坑与排查读数不涨、方向反了、低速丢脉冲这五件事这套程序在小批量上跑过之后问题基本集中在下面五类。每一条我都按现象、原因、解决写遇到了直接对着查。5.1 计数完全不涨或者只有一个方向涨现象电机转起来串口打出来的total_cnt一直是 0或者正转涨、反转不涨。原因通常有三个。一是引脚配错了把 GPIO 配成了复用推挽输出F103 的定时器输入要求配成输入模式输出模式下定时器根本读不到电平变化二是定时器没启动HAL_TIM_Encoder_Start之前就跑去读 CNT三是编码器是开漏输出但没接上拉两根线一直是低电平。解决先用万用表量 A/B 对地的电压手转电机时应该在 0 和 3.3V或 5V之间跳电压不动就是硬件问题先查上拉和供电。电压正常但还是不涨就把 GPIO 配置改成GPIO_MODE_INPUTGPIO_PULLUP再确认Encoder_Init在main里被调用过、Start返回值是HAL_OK。5.2 转向和实际相反现象程序里给正转指令total_cnt却往负方向跑速度环一闭环直接飞车。原因A/B 两路的相位关系和你定义的「正方向」不一致硬件接线、编码器安装方向、极性配置三者任一不同都会导致这个问题。解决改软件最快把sConfig.IC1Polarity和IC2Polarity里的一个改成TIM_ICPOLARITY_FALLING或者干脆在Encoder_Update里return -s_total;。我一般不推荐调换 A/B 线因为线束做好之后不好改而且下次换批次电机可能又反了软件里留一个#define ENC_DIR_INVERT 1的开关最省事。5.3 高速时计数值突然跳变或者卡住现象低速一切正常转速一上去total_cnt就开始乱跳甚至停在一个值不动。原因两种可能。一是输入滤波器IC1Filter设得太大加上线缆电容高速脉冲的边沿被滤平了二是分频器Prescaler不是 0每个 N 脉冲才计一次高速时直接跟不上。解决先把Prescaler确认成 0再把IC1Filter/IC2Filter从 6 往下降到 2 或者 0 试。手边有示波器的话抓一下 PA6 的波形如果边沿已经变成圆角就说明是外部 RC 或者上拉电阻太大造成的把上拉改到 2.2k、去掉并的 100nF 电容。5.4 正反转交界处位置来回跳现象电机停在原地轻微晃动position_deg在 0 附近来回跳几个数看着像静止时位置在漂。原因这是编码器停在不稳定区间的正常现象A/B 两路都在阈值附近时任何一点机械振动都会让计数器正负各走一步。如果你的回绕扩展用了uint16_t直接相减这里会变成跳几万的灾难。解决先确认回绕处理用的是(int16_t)(now - s_last)这种有符号差值写法。确认之后如果还觉得抖就在上层加一个死区|delta| 1时把速度当 0 处理位置不动。注意死区只能加在速度上位置必须如实累加否则低速时位置会丢。5.5 上电位置不为零多圈跑偏现象每次上电position_deg都是上次断电时的值或者干脆是个随机数。原因s_total是内存变量断电就没了而定时器 CNT 上电默认是 0两者不同步。如果你在Encoder_Init里没清s_last和s_total第一次Encoder_Update算出来的差值是随机的。解决Encoder_Init末尾必须调用一次Encoder_Reset()把s_total、s_last、s_enc全部归零并且s_last要重新读一次 CNT 而不是保留 0。需要绝对位置的话加一个霍尔或者光电开关做上电寻零回零动作放到main里上电后执行一次别塞进编码器模块。6. 二次开发把「能读数」推到「能闭环」的三步改造数据能稳定读出来只是起点真正的活是接到控制环里。改造点有三个按顺序来。第一处是位置环的接入。Encoder_Get拿到的position_deg直接和目标角度做差得到误差但要注意多圈累计的溢出目标角跨过 360 度的时候要判圈否则走一圈回来误差会变成 -360。我一般在motor.c里做一次误差归一化到 ±180超过就加减 360。第二处是限幅和抗饱和。速度环输出接 PWM 之前必须限幅到ARR以内而且积分项要有抗饱和否则堵转时积分憋满松开的瞬间直接满占空比冲出去。这个坑跟编码器没关系但十个自己做闭环的里有八个栽在这。第三处是标定。ENC_PPR和ENC_GEAR_RATIO别信铭牌手转输出轴整整十圈读total_cnt变化量除以 10 就是真实的每转计数填回去。减速比标错、线数当成四倍频后的数这类错误会让所有物理量整体偏一个系数PID 参数怎么调都不对。验证方法我固定用两个手转一整圈看计数是不是等于ENC_CNT_PER_REV误差在 ±2 个计数以内算合格然后给一个阶跃速度指令用环形缓冲把speed_rpm按 1ms 存 500 点发到上位机看波形上升时间、超调量、稳态抖动一眼就能判断。参数典型值改错的后果ENC_PPR1000转速整体偏倍数PID 调不动ENC_GEAR_RATIO30同上且位置环会震荡IC1Filter6太大高速丢脉冲太小计数毛刺多滤波系数 alpha0.25太大噪声进环太小响应滞后采样周期 TS0.001和定时器不一致时转速差整数倍从那以后我每次接新编码器都强制先只跑「手转一圈看计数」这一件事波形和数据对上了再接控制环不然调 PID 的时候永远分不清是编码器错了还是参数错了。希望帮到你。本文还有配套的精品资源点击获取