
简介面向STM32开发者的PID电机自控制完整工程包以标准库工程形式涵盖代码、配置与编译产物适合正在学习嵌入式电机控制或需要快速移植PID算法的工程师与高校学生。该工程基于STM32F10x系列实现了对直流、步进和伺服电机的速度与位置闭环控制包含比例、积分、微分环节的完整算法代码并集成编码器反馈、PWM输出、ADC采样等底层驱动覆盖从传感器数据采集到控制量输出的全链路处理流程。资源共180个文件以h头文件、c源文件、o目标文件及crf编译中间文件为主另有uvproj工程文件、hex烧录文件、map映射文件和sct链接脚本压缩包仅4.97MB便于快速下载并直接导入开发环境。已有83人学习下载。通过清晰的文件分类可对照硬件连接、参数整定与过流/过温保护相关代码理解PID调参与自适应方法是电机控制入门的实用参考。1. 一块 STM32 板子把电机转速锁死的 PID 自控制闭环同样是给电机一个固定占空比的 PWM电池电压不同转速能相差百分之十几负载稍变转速立刻飘。开环 PWM 控制的是“加多大电压”不是“转多快”。基于 STM32 的 PID 电机自控制要解决的正是这件事用编码器实时测速用 PID 控制器动态修正 PWM把转速锁在目标值附近连 Kp、Ki、Kd 三个参数也让控制器在线找出来不需要人反复试凑。这篇不绕理论直接讲清从硬件接法、算法代码、参数自整定到排错的一整条链路。适合刚接触电机控制的嵌入式开发也适合在校做课题的同学照着跑通闭环再改成自己的方案。2. 电机闭环硬件STM32 PWM 输出、编码器接入与驱动选型2.1 直流电机模型先立住一阶惯性与开环偏差把直流电机看成一阶惯性环节输入 PWM 电压输出转速时间常数在几十到几百毫秒之间。输入不变转速会慢慢趋向某个值但这个值由负载力矩和电源电压共同决定。开环控制的问题就出在“共同决定”上负载或者电压一松一紧平衡点就移了转速也跟着跑。闭环要做的事情是每毫秒问一次“现在多快”和目标差多少再把 PWM 往收偏差的方向推。PID 三个分量的分工可以直接从这份物理直觉里来P 项跟当前偏差大小走I 项把一直存在的那点差距攒起来补掉D 项在转速快速摆动时踩一脚刹车。先记住这个对应关系再看代码会顺很多。这里说的测速对象是带减速箱的直流减速电机加增量编码器这是 STM32 电机控制里最容易上手、也最适合讲 PID 的组合。无刷电机和伺服电机也能套同一套控制思想但换相和反电动势处理不是本文范围。2.2 STM32 定时器同时输出 PWM 与读取编码器的接法常见接法是这样一块 STM32F103 或 F4一个直流减速电机一个带 AB 相增量编码器一个 H 桥驱动模块。PWM 从高级定时器出来进驱动芯片的 IN1/IN2编码器 AB 相接普通定时器的两个输入通道配成编码器模式。下面是 TIM2 编码器模式的初始化片段直接在 main 里调用 HAL 库即可TIM_Encoder_InitTypeDef sEncoder {0}; sEncoder.EncoderMode TIM_ENCODERMODE_TI12; // AB 相双沿计数4 倍频 sEncoder.IC1Polarity TIM_ICPOLARITY_RISING; sEncoder.IC2Polarity TIM_ICPOLARITY_RISING; HAL_TIM_Encoder_Init(htim2, sEncoder); __HAL_TIM_ENABLE(htim2);逻辑说明TIM_ENCODERMODE_TI12表示 TI1 和 TI2 双沿都计数512 线的编码器转一圈得到 2048 个计数脉冲方向由硬件自动处理计数器可以加也可以减读取时拿到的是一个带符号的相对量。参数说明IC1Polarity和IC2Polarity在初始方向不对时才需要调换实际项目里保持同一极性即可方向接反优先交换 A/B 相解决。拿到计数之后每 1ms 在定时器中断里做一次差分测速换算成 RPMint16_t cnt_now (int16_t)__HAL_TIM_GET_COUNTER(htim2); int16_t cnt_diff cnt_now - cnt_last; // 带符号差值方向自动体现 cnt_last cnt_now; // 1ms 采样4 倍频后 2048 P/R减速比 30:1 // 转速(rpm) cnt_diff / 2048 / 0.001 x 60 / 30 float rpm (float)cnt_diff * 60000.0f / 2048.0f / 30.0f;逻辑说明cnt_diff是两个采样时刻的计数器差值除以每转脉冲数得到这 1ms 里转过多少圈乘 60000 是把毫秒换算成分钟。参数说明2048 由编码器线数乘 4 得到30 是减速比这两处必须换成自己电机的实际参数cnt_diff要用int16_t承接16 位计数器回绕后相减仍然能得到正确的带符号差值。PWM 这边容易踩一个坑高级定时器输出比较通道配置好之后还需要使能主输出。以 TIM1 为例__HAL_TIM_MOE_ENABLE(htim1)这行漏掉占空比怎么改电机都没反应。CubeMX 生成代码时这一步通常已经在MX_TIM1_Init里处理了手写工程时特别容易忽略。2.3 驱动芯片怎么选TB6612、AT8236、L298N 的取舍驱动模块不参与 PID 运算但会影响整定结果压降大、响应慢的驱动会引入额外迟滞Kp 加到一定程度就开始振荡整出来的参数窗口很窄。手头常用的三款对比如下。芯片峰值电流压降/内阻适用场景TB66123AMOSFET 内阻小小型直流减速电机AT82366A 以上内阻低带散热焊盘12V/24V 中功率电机L298N2A饱和压降约 1.8V老教材常见新设计不建议TB6612 在 3A 以内性价比较好AT8236 给大扭矩 12V 减速电机留的余量更足。选好驱动后还要定 PWM 频率常见推荐 10kHz 到 20kHz太低能听到转子啸叫太高驱动芯片开关损耗明显变大。用 TIM1 输出 PWM 时72MHz 主频下配 PSC0、ARR3599输出正好 20kHz占空比直接写 CCR 寄存器写 1800 就是 50%。3. 速度环算法落地位置式 PID 与增量式 PID 的 STM32 实现3.1 位置式 PID 的完整实现位置式 PID 的输出是 PWM 占空比的绝对值每一拍都要重新计算三项。把 Kp、Ki、Kd 和内部状态装进结构体代码集中在一个文件里后面做自整定也方便。typedef struct { float kp; float ki; float kd; float err_sum; /* 误差积分累积量 */ float last_err; /* 上一拍误差 */ float output; /* PID 输出 */ float out_min; /* 输出下限 */ float out_max; /* 输出上限 */ } pid_pos_t; void pid_pos_update(pid_pos_t *pid, float target, float actual) { float err target - actual; /* 积分累积超限截断防止积分饱和 */ pid-err_sum err; if (pid-err_sum 200.0f) pid-err_sum 200.0f; if (pid-err_sum -200.0f) pid-err_sum -200.0f; /* PID 合流 */ pid-output pid-kp * err pid-ki * pid-err_sum pid-kd * (err - pid-last_err); /* 输出限幅防止 PWM 跳到非法区间 */ if (pid-output pid-out_max) pid-output pid-out_max; if (pid-output pid-out_min) pid-output pid-out_min; pid-last_err err; }逻辑说明积分项使用截断限幅避免目标长期不达标时 I 项积蓄到一个“拉不回来”的程度D 项用的是误差一阶差分单位是每拍的变化量与 1ms 采样周期绑定。参数说明out_min和out_max对应 PWM 设定值边界比如满量程 1000 就给 -1000 到 1000负值用于反转只做单向转动时把out_min改成 0由驱动芯片 IN1/IN2 负责反向逻辑。位置式 PID 适合把 PWM 绝对值直接交给驱动的场景问题也直观启动瞬间误差大I 项很快顶到上限响应会冲。缓解办法是对目标值做斜坡让目标转速每秒只允许变化固定增量。3.2 增量式 PID输出的是 PWM 变化量增量式计算的是“这次比上次多加多少”输出叠加到上一拍 PWM 上。差分形式是Δu Kp·(e(k) - e(k-1)) Ki·e(k) Kd·(e(k) - 2·e(k-1) e(k-2))typedef struct { float kp; float ki; float kd; float last_err; float last_2_err; float delta; } pid_inc_t; void pid_inc_update(pid_inc_t *pid, float target, float actual) { float err target - actual; pid-delta pid-kp * (err - pid-last_err) pid-ki * err pid-kd * (err - 2.0f * pid-last_err pid-last_2_err); pid-last_2_err pid-last_err; pid-last_err err; }调用处把 delta 加到当前 PWM 上主循环或中断里这样写static float pwm 0.0f; pid_inc_update(pid, target_rpm, cur_rpm); pwm pid.delta; if (pwm out_max) pwm out_max; if (pwm out_min) pwm out_min; TIM_set_duty(htim1, (uint32_t)pwm);逻辑说明增量式的 I 项直接体现在 Ki·e(k) 里相邻两个累加量相减天然带出积分效果不需要单独维护误差累积和。参数说明pwm必须用 float用 int 累加会把每次的小数增量截断掉低速段表现尤其明显限幅逻辑放在累加之后且只在这一个地方做不要在 PID 函数里再重复限幅。位置式和增量式的实际手感差异在于增量式天然不具备“记住大误差”的能力抗积分饱和压力小但面对长时间静差时收敛会慢一点。速度环项目里我通常先用位置式因为测速反馈够准、积分限幅可控自整定也更直观。3.3 采样频率、量化误差与编码器读数方向PID 计算频率和编码器采样频率必须一致用同一个 1ms 定时器中断驱动先读编码器差值再换算出转速再喂 PID最后更新 PWM。这个顺序不能乱否则一个周期内用的 actual 还是上一拍的数据等效于把系统延迟放大了一拍。低速段的量化误差是另一个高发问题。1ms 内只有 1 到 2 个脉冲时转速换算结果会大幅跳变PID 会把量化噪声当成真实偏差去追。处理方式看场景低速重载场景把采样窗口放到 5ms 甚至 10ms 再做差分或者对编码器差值先做一阶低通滤波再进 PID。滤波会引入滞后采样窗口和滤波系数要按实际最高转速重新匹配不是越大越好。编码器读数方向反了是整个系统里症状最隐蔽的错误PID 输出越大转速反而离目标越远积分项再一累积很快顶到限幅。确认方向的方法是开环给一个固定占空比看计数器是增加还是减少再和目标方向对照。方向不对时换 A/B 相接法或者把IC1Polarity、IC2Polarity同时取反两条路任选一条。提示PID 中断里不要做串口打印、Flash 写入这类耗时操作打印放主循环整定状态机在中断里只改标志位。3.4 从速度环到位置环级联 PID 的参数顺序如果目标变成“按住一个位置”单速度环不够用常见做法是再加一层位置环套在速度环外面这就是级联 PID 控制。外环位置 PID 的输出作为内环目标转速内环速度 PID 的输出才是 PWM。两个环的采样频率不一样位置环可以慢一拍比如 10ms 一次内环保持 1ms避免两层在同一周期里互相追赶。级联 PID 的调参顺序一定是先内环再外环内环收不住外环加再大的 Kp 也没用。想给标题里的“自控制”做成级联版本自整定只应该发生在内环速度环上外环参数手动留出裕量即可。4. 参数自整定让 STM32 在线把 Kp、Ki、Kd 试出来4.1 临界比例法手动调参的自动化版本很多上位机里 PID 参数叫 PB比例带、TI积分时间、TD微分时间和 Kp、Ki、Kd 是同一套东西的两种标注方式。无论是三菱还是西门子 PLC 的自整定功能找的都是这三个量在具体对象上的合适取值。STM32 上做同样的事情并不需要复杂的辨识算法临界比例法就足够。手动调参的过程本质是在找系统的临界点先把积分和微分关掉只留 Kp从 0.1 慢慢加看阶跃响应。Kp 太小偏差收得慢Kp 合适响应快且过冲小Kp 偏大开始等幅振荡。临界比例法把“手动找边界”变成确定流程闭环只留 P逐步加大 Kp直到系统出现持久等幅振荡记下 Ku 和振荡周期 Tu再按下表查参数。控制器KpKiKdP0.5·Ku——PI0.45·Ku0.54·Ku/Tu—PID0.6·Ku1.2·Ku/Tu0.075·Ku·Tu这张表默认参数是连续时间形式。代码里用的是离散累加和差分直接把表里的 Ki 塞进位置式 PID 的ki积分项会偏大因为 1ms 中断里 err_sum 一秒钟累积了 1000 个差值。正确做法是把表值 Ki 乘以采样周期1ms 时乘 0.001或者在积分累积那里统一按周期重标定。这个换算不落实自整定出来的参数会比手调的还差。4.2 在 STM32 上执行临界比例法的五个步骤按下面顺序走每一步都有明确的观察对象把 Ki 和 Kd 清零PID 输出限幅设为 PWM 全量程的 ±50%防止试凑阶段输出打满到无法恢复。启动闭环给定一个中速目标值比如输出轴 200 RPM等系统进入平衡状态。以 0.1 为步长增大 Kp每调一次给目标值阶跃200 跳到 250观察转速是衰减还是发散。当转速出现持续等幅振荡记录当前 Kp 为 Ku用调试器或中断计数测出振荡周期 Tu。按 4.1 表格计算 Kp、Ki、Kd修正采样周期后写回结构体再给一次阶跃验证。第 4 步的 Tu 可以在中断里直接测记录相邻两次转速过零之间的毫秒数。转速过零在编码器差分值上最容易判断从正变负或从负变正都算一次过零。提示整定期间目标值要避开 0 转速区间让电机保持在中速工作点摩擦死区的影响才会变小。临界比例法有一个现实前提系统必须能承受一段时间的等幅振荡。如果产品在试凑阶段不允许明显转速波动就用下面这种更温和的继电反馈自整定。4.3 继电反馈自整定不试 Kp直接进入极限环继电反馈法用一个带滞回的比较器替代 PID 输出转速低于目标就输出正幅值 h高于目标就输出负幅值 -h系统会在目标转速附近形成周期振荡也就是极限环。等振荡进入平衡区后量出振荡周期 Tu 和转速振幅 a用近似关系估算临界增益Ku 4h / (π·a)再把 Ku 和 Tu 代入同一张参数表。这个方法的好处是不需要人工逐步试探 Kp适合做成“上电后按一下自动完成整定”的产品逻辑。幅值 h 一般取 PWM 满量程的 10% 到 30%以 ARR3599 为例h 取 360 就是 10% 的占空比摆动量极限环振幅 a 落在 15 到 40 RPM 比较合理a 过小说明滞回带淹没在测速噪声里需要加大 h 或收紧滞回带。一个可以改着用的状态机框架/* 中断周期 1ms整定时让 PID 输出自动让位给继电输出 */ void relay_autotune_step(float actual) { static uint8_t state 0; /* 0正向 1反向 */ static uint16_t cross_cnt 0; static uint32_t last_cross 0; switch (state) { case 0: if (actual at_ref at_hyst) { tim_set_duty(at_h); /* 高出目标切到 h */ state 1; if (cross_cnt 3 ctx-tu_ms 0) { ctx-tu_ms tick_ms - last_cross; } last_cross tick_ms; cross_cnt; } break; case 1: if (actual at_ref - at_hyst) { tim_set_duty(-at_h); /* 低于目标切到 -h */ state 0; } break; } }逻辑说明状态机在正负输出之间来回切cross_cnt从 0 数起只记录第三次穿越之后的周期确保振荡进入平衡区再取数tu_ms取相邻两次穿越的时间差对应极限环的半周期。参数说明at_hyst是滞回带建议取 5 到 10 RPM太小编码器噪声会让状态机抖振太大振荡周期测不准at_ref、at_h、at_hyst要在进入自整定流程前通过串口设置好。真正可用的实现还需要在状态机里同步统计振幅记录正反向半周期的转速峰值和低谷取若干轮的平均值四次穿越之后计算出 a再估算 Ku、Tu写回 PID 结构体并自动切回 PID 输出。整个过程在串口上打印日志这是 stm32 串口调试 PID 时最常看到的流程记录。需要注意的是这两种整定方法都建立在对象近似线性、工作点变化不大的前提下所以自整定要在 20% 到 80% 满速范围内做整定结果在中高速表现可靠低速段还要靠死区处理来补。5. 验证与收尾用串口曲线和阶跃响应确认 PID 参数可用5.1 串口输出格式设计整定完成后先别急着调参数把曲线拉出来看。用 USB 转串口模块115200 波特率每 10ms 输出一拍格式固定成 CSV方便贴进 Excel 或画图工具t_ms,target_rpm,actual_rpm,pwm_duty,kp,ki,kd 0,200,0,0,1.20,0.45,0.01 10,200,187,218,1.20,0.45,0.01 20,200,202,401,1.20,0.45,0.01输出里同时带目标值、实际值、PWM 占空比和当前三参数目的是让数据自查响应曲线不对时能立刻看出是参数问题还是执行机构问题。5.2 阶跃响应诊断表给一次目标值阶跃比如 200 跳到 300 RPM对照下表判断下一步动哪个参数现象参数改动方向超调大、来回振荡几次才收敛先降 Kp再微增 Kd长时间偏差不等于 0曲线平着一条线加 Ki每次加一半再观察目标附近高频小幅抖动检查编码器滤波或换 5ms 采样窗口先别动 Kd启动瞬间 PWM 立刻顶满对目标值做斜坡限制每秒转速增量5.3 参数落盘与低速死区处理自整定不需要每次上电都跑一遍。常见做法是把整定出的 Kp、Ki、Kd 存到 STM32 内部 Flash 末尾扇区上电先读取校验失败再进入自整定流程。Flash 写入要放在主循环里做不要在中断里触发。低速段如果出现周期性一顿一顿通常是测速量化误差和较大 Ki 叠加的结果。轻量处理方式是速度误差小于某个阈值时冻结积分累积维持当前 PWM 输出更精细一点的做法是检测到编码器脉冲间隔拉长时自动从 PID 控制切到固定占空比保持等下一个脉冲再切回。这个思路算是死区补偿的最小实现版本。最后的验证动作用三次目标值阶跃来完成观察超调量是否一致、PWM 是否来回抖动。三次都收敛到相近的静差区间说明自整定结果可靠如果某一次发散问题多半不在整定公式而在编码器方向、输出限幅或驱动芯片占空比上限这几个基础项上把这几个排查干净基于 STM32 的 PID 电机自控制链路才算真正落地。本文还有配套的精品资源点击获取