STM32编码器脉冲读取与PID速度闭环控制实战详解 📅 发布时间:2026/9/15 19:05:34 👁 浏览次数: 简介面向STM32电机控制与编码器测速开发需求的完整工程资料包适合嵌入式初学者与进阶开发者适用于直流电机/编码电机调速项目。资源基于STM32F10x系列完整展示编码器脉冲读取、速度检测及PID调速的实现过程可直接参考工程代码进行二次开发。压缩包共含287个文件主要包含C与H源码、uvprojx工程文件、编译生成的hex与axf可执行文件以及编译过程产生的d、o、crf等中间文件整体约14.68MB既能查看源码逻辑也能直接烧录验证。已有504人学习使用。工程结构完整包含定时器输入捕获、正交编码器接口、PWM输出等关键模块并集成了LCD显示与串口调试代码便于观察速度反馈。配套的PID控制与滤波处理思路能帮助读者快速理解从脉冲计数到闭环控制的完整链路适合课程设计、毕业设计及竞赛参考。1. STM32 编码器脉冲读取先分清「数脉冲」和「读方向」带编码器的直流减速电机在嵌入式项目里出现频率极高但真正调过的人都知道最隐蔽的坑往往不在 PID 参数上而在脉冲计数那一层正转计数正常反转却只有一半或者高速转起来后脉冲莫名其妙地丢速度曲线像锯齿。这些问题大多不是编码器本身坏了而是读取方式选错了。用 STM32 读编码器脉冲最省心的路径是定时器编码器模式它把 AB 两相的相位关系交给硬件判断方向、倍频、计数全自动完成CPU 只在需要采样时读一次计数值。另一条路径是外部中断逐边沿计数灵活但成本高适合单脉冲加方向信号的接口。这篇内容面向要自己动手做速度闭环的开发者讲清楚脉冲读取怎么算、单位怎么换算再到 PID 参数怎么整定以及低速停止时的抖动问题怎么处理。2. 编码器脉冲读取的两种实现定时器编码器模式与外部中断2.1 为什么优先用定时器编码器模式先看外部中断方案。把编码器 A 相接 PA0、B 相接 PA1每个上升沿触发一次中断在中断里读另一相电平判断方向。逻辑上没错但每来一个边沿就要打断 CPU 一次中断服务函数里的压栈、跳转、读 GPIO、判断分支加起来上百个周期。如果编码器线数高、电机转速又高脉冲频率能到几十 kHz此时中断占用率已经高到影响其它任务。定时器编码器模式解决的就是这个问题。STM32 的通用定时器可以在编码器接口模式下工作TI1 和 TI2 两个输入引脚直接接编码器 A、B 相硬件根据两路信号的边沿顺序判断旋转方向并自动完成 2 倍频或 4 倍频计数。CPU 不参与逐边沿处理只需要在控制周期里读一次计数值。F103 系列的 TIM2、TIM3、TIM4、TIM5 都支持资源充足编码电机项目里优先用这个方案。2.2 CubeMX 里配置编码器模式的关键参数用 STM32CubeMX 生成工程时选中 TIM2在 Combined Channels 下拉框里选 Encoder Mode然后按下面的表配置配置项推荐值说明Prescaler0编码器计数不做预分频保证每个边沿都被计数Counter ModeUp编码器模式下硬件自动决定计数方向此项实际无效Counter Period6553516 位自动重装值溢出后可配合软件变量扩展计数范围Encoder ModeTI1 and TI2两个通道都参与计数实现 4 倍频Input Filter0x0F对输入信号做数字滤波滤掉机械抖动和毛刺Encoder Mode 选项里还有 TI1 only 和 TI2 only对应 2 倍频模式。选 TI1 and TI2 得到的分辨率最高每个机械周期计 4 个数适合低速高精度场景。输入滤波这项不要设 0电机运行时的电刷火花或接触抖动会产生毛刺硬件滤波在输入端就把问题解决了比在软件里反复判断可靠得多。接线时注意编码器 A 相接 TIM2_CH1PA0B 相接 TIM2_CH2PA1。很多编码电机自带的编码器是开漏输出必须在信号线上加 10k 左右的上拉电阻到 3.3V否则读到的电平不确定。CubeMX 里也记得把这两个引脚的模式设为 GPIO_INPUT并启用内部上拉双保险。2.3 HAL 库启动编码器模式与读取计数值CubeMX 生成代码后在 main 函数里启动编码器接口HAL_TIM_Encoder_Start(htim2, TIM_CHANNEL_ALL);int32_t read_encoder_ticks(void) { return (int32_t)__HAL_TIM_GET_COUNTER(htim2); }__HAL_TIM_GET_COUNTER宏直接读取 CNT 寄存器的当前值。编码器模式下这个值会随着电机的正反转自动增减所以读出来本身是带符号的。但要注意一个问题CNT 寄存器是 16 位的范围 0~65535超过后溢出。如果只是做速度控制相邻两次读值的时间间隔内脉冲增量远小于 65535直接做差就够了int32_t delta current_ticks - last_ticks;这个差值就是控制周期内的脉冲数增量正负代表方向。千万不要在每一拍都想着去处理溢出翻转累加器才需要做差分的话只要差值在 int32 范围内就不会出错。2.4 外部中断方式仍然适用的场景定时器编码器模式虽然好但不是所有编码器都支持。有些电机驱动器输出的是单脉冲加方向信号即只有一根脉冲线另一根是方向电平。这种信号接不进编码器接口只能走外部中断或者输入捕获。外部中断实现方式脉冲线接 PA6方向线接 PA7。volatile int32_t pulse_count 0; void EXTI9_5_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line6) ! RESET) { EXTI_ClearITPendingBit(EXTI_Line6); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7)) { pulse_count; /* 方向线为高正转 */ } else { pulse_count--; /* 方向线为低反转 */ } } }这段逻辑的核心是「边沿触发加电平判断」每个脉冲只进一次中断。相比编码器模式它的成本是每个脉冲都会打断 CPU而且需要自己处理方向。如果脉冲频率非常高建议改用输入捕获加 DMA 的方式或者直接换带正交编码器接口的电机。3. 编码器脉冲换算速度的数学模型倍频、减速比与采样周期3.1 四倍频原理与计数值的物理含义增量式编码器输出 A、B 两路相位差 90 度的方波。一个完整的机械周期里A 和 B 各出现一次上升沿和下降沿一共 4 个边沿。定时器编码器模式在 TI1 and TI2 双沿计数时每转一圈得到的计数值是编码器物理线数的 4 倍。这就是常说的四倍频。举例说明某编码电机标称编码器分辨率 13 线/圈电机内部还有一个减速器。假设减速比是 1:30输出轴转一圈电机轴转 30 圈。那么输出轴转一圈定时器计到的脉冲数就是每圈计数值 13 × 4 × 30 1560这个数就是你做单位换算的基准。如果不知道减速比可以把电机输出轴手动转一圈通过串口打印计数值实测一下到底计了多少个数。常见减速比有 1:30、1:50、1:90实测永远比查手册可靠。3.2 从脉冲增量换算转速的完整代码给定采样周期 Ts单位秒相邻两次读数的脉冲增量为 delta输出轴每转一圈的计数值为 ticks_per_rev则转速为转速(rpm) delta × 60 / (ticks_per_rev × Ts)写成 C 函数#define ENCODER_PPR 13.0f /* 编码器物理线数 */ #define GEAR_RATIO 30.0f /* 减速比 */ #define SAMPLE_TIME_S 0.02f /* 采样周期 20ms */ float encoder_delta_to_rpm(int32_t delta_ticks) { float ticks_per_rev ENCODER_PPR * 4.0f * GEAR_RATIO; float rpm (float)delta_ticks * 60.0f / (ticks_per_rev * SAMPLE_TIME_S); return rpm; }计算逻辑不复杂但有一个隐藏问题采样周期越短每个周期内的脉冲数越少速度分辨率越差。20ms 采样一个 13 线 30 倍减速比的电机每圈 1560 个计数1 rpm 对应每秒 26 个计数每个采样周期平均只有 0.52 个计数。这意味着低速时大多数周期读到的增量是 0偶尔冒出来一个 1速度曲线呈阶梯状。要提高低速分辨率要么提高采样周期要么选更高线数的编码器。3.3 M 法与 T 法测速的取舍上面这种固定周期数脉冲的方法叫 M 法。它的优点是实现简单高速时精度高缺点刚才说过低速时脉冲数太少量化误差明显。反过来T 法是测量相邻两个脉冲之间的时间间隔低速时时间间隔长计时精度高但高速时时间间隔太短反而容易测不准。编码电机速度控制的实际场景里常见做法是分两段处理转速高于某个阈值用 M 法低于阈值切换成 T 法。阈值选多少我一般取每采样周期至少产生 5 个脉冲对应的转速作为分界点。以上面 1560 计数每转为例最低转速(rpm) 5 × 60 / (1560 × 0.02) ≈ 9.6 rpm也就是说低于 10 rpm 时 M 法已经不太可靠需要切 T 法。不过对大多数电机控制项目10 rpm 以下往往是启停阶段短暂经过直接用 M 法也不会出大问题。真正需要全程高精度测速的场合再考虑用输入捕获中断测脉冲间隔。3.4 位置计算与溢出保护的常见误用如果项目中不仅要测速度还要记录电机累计转过的圈数那就需要维护一个累加器。此时做差分已经不适用必须处理 16 位计数器的溢出。标准做法是在更新中断里判断计数方向void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); if (__HAL_TIM_IS_TIM_COUNTING_DOWN(htim2)) { encoder_overflow_count--; } else { encoder_overflow_count; } } HAL_TIM_IRQHandler(htim2); }这里用__HAL_TIM_IS_TIM_COUNTING_DOWN判断溢出方向避免出现「正转溢出计 1反转溢出也计 1」的经典 bug。累计位置值等于溢出计数乘以 65536 加当前 CNT。但如果你是做速度控制每次只做差值根本不需要这段代码加上了反而增加出错的概率。代码越简单越不容易错这是编码器处理的第一原则。4. 速度闭环控制PWM 驱动配置与 PID 参数整定顺序4.1 电机驱动板的 PWM 通道与方向引脚编码电机是直流电机靠 PWM 占空比调速靠驱动芯片的 IN1/IN2 两个引脚控制方向。常用驱动有 TB6612、DRV8833、L298N。以 TB6612 为例逻辑如下功能引脚状态PWM 状态正转IN11, IN20PWM 输出有效反转IN10, IN21PWM 输出有效刹车IN11, IN21任意惰性IN10, IN20任意STM32 上的 PWM 用定时器输出比如 TIM3_CH1 输出 PWM 给驱动板的 PWMA 引脚PA4、PA5 接 IN1、IN2。启动 PWM 的 HAL 代码HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, duty_cycle);duty_cycle 的取值范围由自动重装值 ARR 决定。CubeMX 里把 TIM3 的频率设为 20kHzARR 设为 999那么 duty_cycle 从 0 到 999 对应 0% 到 100% 的占空比。注意 PWM 频率不要选太高20kHz 左右即可超过 50kHz 时驱动芯片的开关损耗明显上升。4.2 位置式 PID 的 C 语言实现与积分限幅速度闭环的控制器用位置式 PID 就够不用上增量式。原因是输出是 PWM 占空比本来就有一个绝对基准位置式输出直接用占空比没有累计残差问题。核心代码typedef struct { float kp; float ki; float kd; float integral; float prev_error; float out_max; float out_min; } pid_controller_t; float pid_update(pid_controller_t *pid, float target, float actual) { float error target - actual; /* 积分项累加并限幅防止积分饱和 */ pid-integral error; if (pid-integral pid-out_max) { pid-integral pid-out_max; } else if (pid-integral pid-out_min) { pid-integral pid-out_min; } float derivative error - pid-prev_error; pid-prev_error error; float output pid-kp * error pid-ki * pid-integral pid-kd * derivative; if (output pid-out_max) output pid-out_max; if (output pid-out_min) output pid-out_min; return output; }积分限幅是必须的。如果不限幅电机长时间堵转时积分项会一直累加堵转解除后输出需要很长时间才能退饱和现象就是电机猛冲一下才恢复正常。把积分限幅设为输出限幅的一半或者直接设为 out_max是工程里面最常见的处理。4.3 PID 参数整定的正确顺序新手最容易犯的错是一上来就填 Kp1、Ki0、Kd0然后看波形调参数。正确顺序是先开环标定再接闭环。开环标定的做法不给 PID直接固定 duty_cycle 值通过串口打印实测转速记录多个占空比对应的转速值。比如PWM 占空比实测转速 rpm20095400210600340800475这一步有两个作用确认 PWM 方向和编码器方向一致正转时转速读数为正同时拿到一个大致的 PWM-转速对应关系方便估计 Kp 的初始值。如果开环下正转读数却是负数说明编码器 AB 相接反了或者 PWM 方向逻辑反了此时调 PID 是没有意义的。闭环整定的顺序是先调 Kp让系统不震荡再加 Ki 消除稳态误差最后加一点点 Kd 抑制超调。Kp 从 0.5 开始Ki 从 0.01 开始Kd 可以先设 0。4.4 控制周期与采样周期的统一PID 的积分项本质是误差对时间的累加因此 PID 必须以固定的周期执行。常见做法是把 PID 计算放在定时器中断里每 10ms 或者 20ms 执行一次。中断里读编码器、算转速、算 PID、更新占空比一气呵成void TIM6_IRQHandler(void) { HAL_TIM_IRQHandler(htim6); int32_t current_ticks read_encoder_ticks(); int32_t delta current_ticks - last_ticks; last_ticks current_ticks; float rpm encoder_delta_to_rpm(delta); float duty pid_update(speed_pid, target_rpm, rpm); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, (uint32_t)duty); }中断周期必须和encoder_delta_to_rpm里的SAMPLE_TIME_S保持一致。如果中断是 20ms但宏定义里写的是 0.01f所有速度读数都会差一倍而且这种错误非常隐蔽串口打印转速和实际转速对不上时先查这两个地方。5. 编码器数据的滤波与停止抖动编码器读到的原始脉冲增量在低速时量化噪声明显一般会做一阶低通滤波。公式很简单当前滤波值等于上一次滤波值乘以系数加新读数乘以系数补量。static float smooth_rpm 0.0f; smooth_rpm 0.8f * smooth_rpm 0.2f * raw_rpm;系数 0.2 的意义是每个采样周期只采信 20% 的新数据等效截止频率约为采样频率除以 4。滤波系数调太小会让速度响应变慢PID 拿到的是延迟后的速度容易引发震荡。用串口同时打印原始转速和滤波后的转速对比能直观看到滤波效果。停止抖动是另一个高频问题。目标转速设 0 时如果 PID 还在运行输出占空比不为零电机会在小范围内来回抖。工程里常用的处理是加死区转速误差绝对值小于某个阈值时不更新 PWM 输出或者在目标转速为 0 时直接给 PWM 输出 0同时清零积分项。另外可以启用驱动芯片的刹车逻辑让两个方向引脚同时输出高电平电机会立即停住。注意如果这时不清积分项下次启动会有一次意外的冲击。验证整个测速链路是否正确可以用另一个测速手段交叉对比。常见做法是用示波器测量编码器 A 相输出的脉冲频率按四倍频公式换算成转速和代码里打印的读数对比。没有示波器的话用手匀速转动电机输出轴看串口打印的转速是否接近你转动的速度方向正转转速为正反转转速为负。做到这一层STM32 编码器脉冲读取和速度控制的基本链路就完整了。本文还有配套的精品资源点击获取