
简介面向STM32嵌入式开发者的PID控制实践资源包以光照控制为场景讲解比例-积分-微分算法在Cortex-M3内核上的落地方法。压缩包共727个文件以C源文件、头文件、汇编文件及工程调试中间文件为主另含2份PDF参考文档和1份电路原理图大小51.75MB便于从代码、文档、硬件三个维度对照学习。已有4352人学习下载。资源基于STM32F1库函数版本包含定时器采样、ADC采集、PWM输出等关键模块配置并给出了完整工程文件与编译产物可帮助读者快速搭建光照调节闭环。通过阅读开发指南、分析原理图并逐步调试PID参数能深入理解比例、积分、微分项的实际作用适合正在学习STM32或自动控制原理的开发者作为实战案例参考。 前几天整理工程文件翻到一个编号为 023 的文件夹里面躺着一份 STM32 PID 算法程序。这种代码在电机调速、恒温控制、平衡车、巡线小车里几乎天天见但也是很多朋友最容易搞成一锅粥的东西——公式从网上抄下来能跑一换到自己的板子上就开始震荡、超调、爬不到目标值。这段时间我正好用它调了三个不同类型的对象绕了不少弯路把思路和坑都记下来希望能帮你从“调参随缘”变成“有章可循”。这篇内容适合两类人一类是刚接触 STM32 控制项目的学生另一类是做毕设或小产品时需要快速落地闭环控制的工程师。我会先把 PID 三个环节各自解决什么问题讲清楚再对比位置式和增量式两种写法的取舍然后给出一套可以直接用在 STM32 工程里的程序结构和调参方法。1. 开环控制为什么不行PID 解决的是“不确定”1.1 固定的 PWM 不代表固定的转速做电机调速时最直觉的方案是直接输出一个固定的 PWM 占空比。实际跑起来就会发现转速是飘的电池电压下降转速跟着掉负载稍微变重转速又掉一截。开环控制假定了“输入和输出之间存在固定不变的关系”但真实物理系统里电压波动、摩擦变化、负载扰动随时在打破这个关系。拿开车举例开环控制就像蒙上眼睛踩油门你觉得踩住某个角度就能维持速度可实际还有上坡下坡和风速变化。闭环控制则像正常开车时盯着速度表低了就多踩一点高了就松一点。PID 就是替你做“盯着速度表调整油门”这件事的算法——它不看精确模型只看目标值和反馈值之间的误差根据误差动态调整输出。这一步想通了后面学公式就不晕。1.2 PID 在 STM32 项目里的典型位置直流电机测速闭环编码器读速度PID 输出 PWM 占空比常见于小车、传送带、云台电机。温度控制NTC 或 DS18B20 测温PID 控制加热功率恒温箱、热床、热水器都是这一类。平衡车和倒立摆MPU6050 读角度角度环加速度环做串级 PID是控制类毕设的热门选题。四轴飞行器和云台姿态环本质也是 PID只是多了坐标变换和串级结构。这些场景跨度很大但控制结构完全一致传感器采集反馈控制器计算误差执行器输出纠正量。把这个通用框架想明白写哪个项目的 PID 都只是参数和接口的差别。1.3 为什么不是更“高级”的控制算法现代控制理论里的 LQR、滑模、模型预测控制确实更精致但它们普遍需要被控对象的数学模型或者需要比 STM32 强得多的计算资源。在大多数嵌入式小项目里对象模型是未知的、非线性的、还随时在变。PID 不依赖精确模型参数整定好了效果就很能打这才是它能几十年经久不衰的根本原因。对刚入门的项目来说把 PID 吃透的性价比远高于去套一个看似高级但根本调试不出来的控制器。2. 先把原理吃透比例、积分、微分各自的活2.1 比例项把当前错误换算成输出比例项最简单输出等于 Kp 乘以当前误差。误差大输出就大系统朝目标走误差变小输出跟着变小。它相当于根据“现在的偏差”踩油门。但只有比例项的系统通常会有稳态误差电机带负载后当 Kp·e 刚好等于维持负载所需的占空比时系统就平衡了而这个误差通常不是 0。要彻底消除这股“差一口气”的劲需要积分项出手。2.2 积分项把历史欠账补回来积分项对误差做累加只要误差不为零积分值就会一直增长输出也持续变化直到误差被压到零附近。可以把它理解成“把以前没还清的债一起算上”。积分项的副作用是容易过冲系统已经到达目标了但积分项里还囤着过去的一大笔债要还输出停不下来于是冲过头。还有更麻烦的积分饱和问题我放到下一节专门说。2.3 微分项提前刹车微分项关注的是误差变化趋势。误差快速增大时微分项会输出反向的抑制力误差快速减小时它又会拉住输出防止一下子冲过目标。它相当于“预判未来”的刹车。但微分对测量噪声极其敏感温度传感器哪怕只有 0.1℃ 的量化跳变经过微分放大后也可能变成明显的输出抖动所以用不用微分、微分放多大要结合传感器质量来判断。2.4 离散化之后我们写在代码里的公式连续 PID 的数学形式是u(t) Kp·e(t) Ki·∫e(τ)dτ Kd·de(t)/dt单片机只能按固定周期采样所以要离散化。把采样周期记为 T位置式写成u(k) Kp·e(k) Ki·T·Σe(i) Kd·(e(k) - e(k-1))/T实际工程里通常直接令 Ki Ki·TKd Kd/T代码里就变成u(k) Kp·e(k) Ki·Σe(i) Kd·(e(k) - e(k-1))增量式的公式是Δu(k) Kp·(e(k) - e(k-1)) Ki·e(k) Kd·(e(k) - 2e(k-1) e(k-2))增量式输出的是相对上一次的变化量不是全量。后面换采样周期时这两个公式里的 Ki 和 Kd 都要按周期重新折算否则参数全乱这一点很多人会忽略。3. 位置式与增量式两种写法的选型逻辑3.1 一张表看清两者差异对比项位置式 PID增量式 PID输出含义当前完整的控制量 u(k)控制量的增量 Δu(k)历史状态PID 内部累加积分项输出在外部累加积分饱和需要专门处理增量本身不会积分饱和但仍要输出限幅单次故障影响可能直接输出巨大控制量只会多走一小步典型场景温控、舵机、模拟量输出步进电机、数字 PWM 累加输出位置式直观输出直接对应“加热器功率 60%”或者“舵机角度 90°”出了问题也好排查增量式更稳输出是一步一步加出来的不会因为一个错误采样就跳变到危险值。两种写法在 STM32 上的开销几乎一样真正的选择依据是执行器和系统结构。3.2 实际项目怎么选温度控制优先用位置式。加热功率天然是一个绝对值输出 0 到 100% 的占空比非常直观限幅也一目了然。电机速度环我通常用增量式。电机 PWM 占空比本身就是需要不断维护的累加值增量式输出自然贴合这种结构正反转切换、急停时对执行器的冲击也更小。如果执行器是步进电机增量式几乎是唯一选择因为步进电机接收的就是“每一步走多少”。如果执行器是舵机这类位置型机构位置式直接给出目标脉宽即可。3.3 位置式的抗积分饱和位置式最常见的坑就是积分饱和。过程是这样的系统还没到达目标时误差大积分项一路累加等到达目标时积分项里已经囤了大量“债务”导致严重过冲。标准处理方法是输出限幅加积分限幅输出到达上限且误差还在同一个方向时停止积分累加。伪代码是pid-integral pid-err; if (pid-integral pid-integral_max) pid-integral pid-integral_max; if (pid-integral -pid-integral_max) pid-integral -pid-integral_max;还有一种做法叫积分分离误差偏大时积分项清零误差进入一定范围再恢复积分。这个方法在温控里很好用能明显减少启动阶段的过冲。增量式不太需要这套东西因为它的积分效果体现在输出累加里输出限幅本身就限制了积分效果的上限。这正好说明没有银弹选型要看对象。4. 在 STM32 里落地的完整代码结构4.1 先定义 PID 结构体我习惯用结构体把一个控制回路的全部状态装起来这样多路电机、多路温控时只需要定义多个实例代码可读性也好很多。typedef struct { float Kp; // 比例系数 float Ki; // 积分系数已合并采样周期 float Kd; // 微分系数已合并采样周期 float target; // 目标值 float feedback; // 反馈值 float err; // 当前误差 float err_last; // 上一次误差 float err_prev; // 上上次误差增量式用 float integral; // 积分累加值 float out; // 输出值 float out_max; // 输出限幅 float integral_max; // 积分限幅位置式用 } PID_TypeDef;初始化函数要做的事情就是清零结构体并写入初始参数。特别提醒不同控制回路的 out_max 往往不同电机可能限 0~999对应 TIM 重装值温控可能限 0~100所以在初始化时把限幅一起设好避免在中断里反复做业务判断。4.2 位置式实现float PID_Calc(PID_TypeDef *pid, float target, float feedback) { pid-target target; pid-feedback feedback; pid-err target - feedback; // 积分累加并限幅 pid-integral pid-err; if (pid-integral pid-integral_max) pid-integral pid-integral_max; if (pid-integral -pid-integral_max) pid-integral -pid-integral_max; // 三路合成 pid-out pid-Kp * pid-err pid-Ki * pid-integral pid-Kd * (pid-err - pid-err_last); // 输出限幅 if (pid-out pid-out_max) pid-out pid-out_max; if (pid-out -pid-out_max) pid-out -pid-out_max; pid-err_last pid-err; return pid-out; }4.3 增量式实现float PID_IncCalc(PID_TypeDef *pid, float target, float feedback) { float dOut; pid-target target; pid-feedback feedback; pid-err target - feedback; // 计算增量 dOut pid-Kp * (pid-err - pid-err_last) pid-Ki * pid-err pid-Kd * (pid-err - 2.0f * pid-err_last pid-err_prev); // 增量限幅防止大步跳变 if (dOut pid-out_max) dOut pid-out_max; if (dOut -pid-out_max) dOut -pid-out_max; // 累加输出并做终端限幅 pid-out dOut; if (pid-out pid-out_max) pid-out pid-out_max; if (pid-out -pid-out_max) pid-out -pid-out_max; // 更新历史误差 pid-err_prev pid-err_last; pid-err_last pid-err; return pid-out; }注意增量式里 out_max 承担了两个职责第一次限制每一步的变化量第二次限制最终输出范围。如果两个场景需要不同限制建议拆成 inc_max 和 out_max 两个字段不然增量限幅设小了响应太慢设大了又起不到保护作用。4.4 定时器中断里的控制周期PID 计算必须放在固定周期的中断里而不是在主循环里用 delay 凑。主循环里任何一句代码耗时波动都会让采样周期不稳定直接导致参数失效。速度环我常用 1kHz也就是 TIM 定时器 1ms 中断一次温控 10Hz 左右就够因为热惯性大采样太频繁只会把传感器噪声喂给微分。中断服务程序的结构大概是void TIMx_IRQHandler(void) { if (TIM_GetITStatus(TIMx, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIMx, TIM_IT_Update); float speed Encoder_GetSpeed(); // 读反馈 float duty PID_IncCalc(pid, set, speed); // 算 PID duty clamp(duty, 0.0f, 999.0f); // 再次限幅 TIM_SetCompare1(PWM_TIM, (uint32_t)duty); // 输出到 PWM } }设置目标值可以从串口或者按键来主循环里只管改 pid.target。控制算得准不准很大程度上取决于反馈读得准不准。编码器滤波、传感器一阶低通滤波这些事如果采样前不处理好后面参数再怎么调都是白费。5. 调参是门手艺从现象反推参数问题5.1 两条最常用的调参路径路径一是纯经验试凑。先把 Ki 和 Kd 设成 0只用 Kp 从很小开始往上加加到系统出现轻微持续震荡记下此时的 Kp再把 Kp 退回到它的 50% 到 60% 作为工作点然后缓慢加 Ki 消除稳态误差最后加一点 Kd 压过冲。这个方法不需要任何理论但每一步都要等系统稳定下来再观察急不得。路径二是临界比例法。同样先把 Ki、Kd 设为零增大 Kp 直到系统出现等幅振荡记录临界增益 Kp_crit 和振荡周期 T_crit然后用经验表估算初始参数。比如 Ziegler-Nichols 表里常见的取法Kp 取 0.6 倍临界增益Ki 取 1.2 倍临界增益除以振荡周期Kd 取 0.075 倍临界增益乘以振荡周期。这个方法给的是起点不是终点最终仍需手工微调但它能快速把你带到正确的参数区域比盲目乱调高效得多。5.2 现象与原因的对照表现象最可能的原因处理建议稳态误差始终存在比例太小或积分太小加大 Kp必要时加大 Ki持续等幅振荡嗡嗡响Kp 过大减小 Kp启动剧烈过冲回落慢积分过大或微分不足减小 Ki增大 Kd高频小幅抖动反馈噪声被微分放大降低 Kd或对反馈滤波响应慢半天爬不到目标Kp 和 Ki 都偏小同步加大但别一步加太多正反馈越调越偏输出方向接反检查执行器极性和反馈方向5.3 我踩过的几个坑第一个坑是换采样周期。有个项目把参考代码里的 1ms 定时器改成 5ms参数没重新折算系统当场陷入疯狂震荡。原因就是前面说的 Ki 和 Kd 里已经包含采样周期频率变了等效的积分时间和微分时间全变了。所以每次改中断周期都要按比例重新折算参数不能直接复制粘贴。第二个坑是执行器方向。PID 输出给的是正数结果电机接反了反馈值越调越远。一开始我还以为是 Kp 太大折腾很久才发现是方向问题。后来养成习惯调试前先开环给一个小占空比确认电机转向和编码器读数方向一致再做闭环。这一步能省掉大量冤枉时间。第三个坑是温控里的微分。DS18B20 量化精度有限温度在目标值附近跳动时微分项把这些量化跳变放大输出就跟着锯齿状抖动。后来对温度反馈做了滑动平均滤波再调小 Kd才稳定下来。传感器噪声不解决PID 参数再好也白搭。第四个坑和数据类型有关。把 PID 输出直接强转成 uint16_t 之前如果没做限幅负值会变成 65535PWM 占空比瞬间拉满执行器直接冲到最大。规范做法是先限幅再转换类型最后才写入定时器比较寄存器顺序不能反。最后说我个人的一个习惯调参时一定用串口把目标值、反馈值和 PID 输出按控制周期打出来在电脑上画成曲线看而不是只盯着板子上的现象。很多时候肉眼觉得“挺稳”曲线上一看其实在持续低频震荡肉眼觉得“响应还行”曲线显示已经过冲了 30%。PID 调试的本质就是观察曲线和参数的关系把这一步做好调参就从玄学变成了工程。本文还有配套的精品资源点击获取