简介:这是一份面向 STM32 学习者的 PID 控制算法 C 语言示例,适合初学嵌入式控制或正在做电机调速、温控等项目的开发者参考。压缩包体积小巧,仅 864B,共 1 个 C 文件,所有代码集中在一个文件中,便于快速阅读、拷贝和移植到自己的工程。虽然文件不多,但源码中涵盖了 PID 控制器的结构体定义、参数初始化、控制量计算等核心环节,能够帮助读者对照理解比例、积分、微分三项的作用,以及采样周期和限幅处理等实现细节,这些细节对后续实际调参很有帮助。资源已有 328 人学习,说明该示例对不少入门者具有参考价值。对于想快速掌握 STM32 上 PID 实现思路的开发者来说,这份小巧的代码包可以当作现成的模板,避免从零搭建的重复劳动;在此基础上针对具体被控对象调整参数即可完成初步验证。 上个月帮朋友调一个基于STM32的直流电机转速控制板,折腾了一晚上,最后问题出在编码器A相和B相接反了。PID参数再准,反馈方向错了全都白搭。这个经历让我想把最近总结的STM32实现PID控制算法的完整思路和踩坑记录整理出来,给正在做电机调速、温控、平衡小车、智能车等项目的朋友做个参考。
这篇文章主体适合两类人:一是已经会用STM32输出PWM、会用定时器,但还没接通过闭环控制的初学者;二是写过PID但总觉得“调不稳”的朋友。我会从算法选型、代码框架、参数整定到常见故障排查,尽量把关键细节讲透。
1. 整体思路与PID选型
1.1 为什么我偏爱增量式PID而非位置式PID
PID控制算法本身不复杂,核心就是一个误差的比例、积分、微分加权计算。但在STM32上落地时,写位置式还是增量式,直接影响整个代码结构和现场调试体验。
位置式PID的输出是执行机构的绝对值,比如直接输出50%占空比,它需要把历史误差全部累加起来。增量式PID输出的只是“在当前输出基础上的变化量”,比如“占空比再增加2%”,它不需要关心之前输出过多少。在电机转速控制这种场景下,我几乎都优先用增量式,原因有几个:
- 无积分累积误差问题,不会因为历史误差累积导致输出饱和失控。
- 输出是增量形式,配合PWM限幅非常自然,不会在启动时直接给满占空比。
- 即使某次控制周期被干扰漏执行,下一次输出依然基于当前实际状态,不会漂移太远。
但位置式PID也有它的舞台。比如温控系统,执行器是加热丝,控制量是加热功率绝对值,断电重启后应该恢复到一个确定的输出,这时用位置式更直观。另外带保持特性的执行器(阀门、舵机)也常用位置式。我在项目选择时有一条铁律:先想清楚执行器是“绝对值型”还是“增量型”,再决定算法形态。
1.2 三个参数的“脾气”必须摸清
KP、KI、KD这三个参数,网上公式满天飞,但最容易忽略的是它们各自的物理含义。我自己总结了一套理解方式:
比例项KP负责“现在”。误差大就输出大,误差小就输出小。KP越大反应越猛,但过了头就会震荡。积分项KI负责“过去”,专门消除稳态误差。比如电机带负载时转速差一点点,P项输出已经不足以抵消负载,这时候积分慢慢积累,把输出顶上。但积分太强会超调、会振荡。微分项KD负责“预测未来”,看误差的变化趋势。误差正在快速缩小时,微分项会提前“收油”,抑制超调。但微分对噪声极其敏感,编码器反馈毛刺大时,微分项会把毛刺放大成剧烈抖动。
所以整定顺序永远是先P,再I,最后D。KPI没稳定之前加积分和微分都是添乱。
1.3 双环/级联控制在什么情况下才需要
热搜词里频繁出现“级联PID”“PID双环控制”,很多新手一上来就想做双环,但双环不是炫技,是做单环做不到的事才上。
我做过一个位置跟随项目,要求小车准确停在某个距离。单环直接控制轮子PWM,位置永远有稳态误差,因为PWM和速度之间不是线性关系。后来改成双环:外环是位置环,输出是目标速度;内环是速度环,输出才是PWM。外环每秒跑100Hz,内环跑1000Hz。效果立刻不一样。
双环的核心思想是“外环定目标,内环去执行”。如果你做的是电磁循迹车、云台稳定、四轴飞控这种对动态响应有明确要求的项目,双环基本是标配。但如果只是单纯的温度加热、简单风扇调速,单环完全够用,硬上双环反而加大调参难度。
2. STM32代码框架与核心实现
2.1 数据结构和初始化该写清楚
PID代码网上版本很多,但我强烈建议自己定义一个结构体,把所有状态变量放一起,别用裸浮点变量堆。这样多路PID复用、调试打印都方便。
typedef struct { float kp; float ki; float kd; float target; // 目标值 float feedback; // 反馈值 float error; // 当前误差 float last_error; // 上一次误差 float prev_error; // 上上次误差 float output; // 当前输出 float out_limit; // 输出限幅(正值,正负都限) float integral; // 积分累积(位置式用) float int_limit; // 积分限幅(位置式用) } PID_TypeDef;初始化时把所有浮点成员清零,KP/KI/KD按整定结果赋初值,limit字段按你PWM映射范围设定。我习惯把out_limit设为PWM最大值的百分比形式,比如100.0f,方便映射时理解。
2.2 反馈采样:编码器模式还是外部中断
STM32做速度闭环,反馈来源最常见的是增量式编码器。推荐使用定时器的编码器模式,而不是外部中断数脉冲。编码器模式硬件自动判向、自动计数,不需要CPU干预,而且不会因为中断频繁丢失脉冲。
配置时核心几个点:定时器时钟使能、GPIO复用模式、时基结构体、编码器模式结构体。重点在TIM_EncoderInterfaceConfig这一步,让CH1、CH2同时输入两路编码器信号。ARR建议设大一点,比如0xFFFF,避免计数溢出方向判断错误。
如果你只是做灰度传感器、霍尔传感器这类离散反馈,那用外部中断或定时器输入捕获都行。但必须保证采样周期稳定,PID计算最好放在定时器更新中断里,不要放在主循环的while里裸跑,否则控制周期会乱。
2.3 核心PID计算函数写法
增量式PID的离散公式很多资料写得很抽象,我给出一个可以直接用的版本。增量式输出的是“增量”,所以每次计算完要在原输出上累加,同时做输出限幅。
void PID_Incremental_Calc(PID_TypeDef *pid) { float delta_out; float kp = pid->kp; float ki = pid->ki; float kd = pid->kd; pid->error = pid->target - pid->feedback; delta_out = kp * (pid->error - pid->last_error) + ki * pid->error + kd * (pid->error - 2.0f * pid->last_error + pid->prev_error); // 更新历史误差,注意顺序 pid->prev_error = pid->last_error; pid->last_error = pid->error; // 累加输出并限幅 pid->output += delta_out; if (pid->output > pid->out_limit) pid->output = pid->out_limit; else if (pid->output < -pid->out_limit) pid->output = -pid->out_limit; }注意两点:误差历史必须保存最近两拍,prev_error和last_error的更新顺序不能反,否则微分项就废了;输出限幅前一定要用累加后的output,而不是直接把delta_out限幅,很多新手在这里翻车。
2.4 输出限幅与PWM映射的细节
STM32的PWM输出范围是0到ARR,而PID输出很可能出现负值。对有刷电机驱动,我用的是H桥/双路PWM控制器,所以把PID输出拆成正反转两路:输出大于0时正向PWM有效,输出小于0时反向PWM有效。占空比取PID输出的绝对值,超过ARR就截断。
还有一个容易忽略的项:死区。让电机在目标附近微调时,PID输出很小,占空比太小电机根本转不动,造成高频启停。建议在PWM映射时加一个最小占空比阈值,比如大于电机启动占空比才输出,否则给0。但这会让速度环出现死区振荡,解决思路是把阈值做得很小,或根据实测启动占空比做线性补偿。具体数值跟电机减速比、供电电压强相关,实测为准。
3. 参数整定实操记录与调试手法
3.1 试凑法完整流程:从纯P开始一步步加
我做转速环整定,通常用无负载的小型直流减速电机做实验。先把积分和微分项全部置零,KI和KD都设为0,只留KP。
第一步先给一个很小的KP,比如0.05,目标转速设100r/min左右,观察反馈会不会跟上。如果电机一点反应都没有,说明KP太小,输出增量被占空比阈值吃掉了,加大KP直到转速有明显响应。
第二步逐渐加大KP,观察曲线。目标转速100,反馈在90到110之间波动且持续震荡,说明KP偏大。回调到震荡刚好消失的位置,大概就是KP的“临界值”,我通常取这个临界值的0.6到0.8倍作为最终KP。
第三步加KI。KI从KP的十分之一开始试,比如KP=0.6,KI先给0.06。观察稳态误差是否消除,响应是否变慢。如果出现低频振荡,KI减小。如果转速始终差一截,KI加大。这里要用表格记录每一轮参数和现象,否则很容易调着调着就乱了。
3.2 用串口和上位机看曲线,别靠眼睛
转速稳不稳,光靠眼睛看轮子转是不行的。我强烈建议把PID调试信息通过串口发出来:目标值、反馈值、PID输出三个变量,用虚拟示波器软件观察曲线。常用的有SerialPlot、匿名上位机、VOFA+这类工具,原理都差不多,按行解析文本或二进制数据。
我一般打印成这样:
printf("T:%d F:%d O:%d\r\n", (int)target, (int)feedback, (int)output);每10个控制周期打印一次,避免占用太多串口带宽。用SerialPlot的浮点解析功能就能直接画三条曲线。调参时眼睛盯的是曲线形状,而不是电机转得快慢。反馈曲线能看到超调、振荡频率、稳态误差这些信息,是判断参数改向哪边调的最可靠依据。
3.3 一组可复用的整定快照
下面是我在一个典型的小型直流减速电机(供电12V、减速比30、500线编码器)上实测的一组参数快照,没有加负载。直接抄不一定完全适配你的硬件,但你可以把它当起点:
| 阶段 | KP | KI | KD | 现象 |
|---|---|---|---|---|
| 纯P | 0.3 | 0 | 0 | 响应偏慢,稳态差5% |
| 纯P | 0.8 | 0 | 0 | 临界震荡,超调8% |
| P+I | 0.6 | 0.06 | 0 | 稳态误差消除,超调12% |
| P+I+D | 0.6 | 0.06 | 0.02 | 超调降到4%,响应可接受 |
注意KI值是KP的十分之一起步,D值是I的三分之一左右起步。实际项目里我会根据曲线再微调,但总的原则是:先让系统“稳”,再谈“准”,最后追求“快”。
4. 常见问题与排错速查
4.1 电机不转、飞车、震荡的排查思路
PID项目里最容易出问题的其实不是算法,而是硬件反馈和PWM通道配置。我把这几类高频问题整理成速查表,每一条都是我实际踩过的坑:
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 电机完全不转 | PID输出太小被占空比阈值吃掉 | 串口打印PID输出值,看是否过小 | 降低最小占空比阈值或增大KP |
| 电机转向错误 | H桥PWM映射方向反了 | 开环写固定占空比试方向 | 交换正反向映射,或调整编码器方向 |
| 转速一直震荡不收敛 | KP过大或反馈采样抖动 | 看串口曲线判断震荡频率 | 降低KP,加大输出滤波 |
| 一启动就冲满速 | 反馈信号丢失,误差巨大 | 打印编码器计数值,确认反馈是否变化 | 检查编码器接线和定时器配置 |
| 有静差消除不了 | KI太小或输出饱和 | 观察稳态时积分项的变化 | 增大KI,检查输出是否顶到限幅 |
反馈方向反了是“隐形杀手”。现象是目标给100,反馈在10到-10之间乱跳,甚至飞车。解决办法很简单:给定一个固定正占空比,观察编码器计数值是增还是减,如果减了,交换编码器A/B接线,或者在代码里把计数结果取反。
4.2 积分饱和、微分噪声和死区问题
积分饱和是位置式PID的老毛病,现象是系统出现大偏差一段时间后,即使反馈已经回到目标,输出还卡在限幅上迟迟不回落。解决手段是给积分项单独做限幅,我把它称为“积分保险丝”,这在前面结构体里已经预留了int_limit字段。每次积分累加后检查这个上限,超了就截断,保证积分项不会比输出限幅更离谱。
微分噪声则是增量式PID的坑。编码器在低速时脉冲间隔大,读数跳变得厉害,微分项会把这种量化噪声放大成抖动。我常用的手段是给反馈值做滑动平均滤波,或者直接限制微分项的输出变化率。还有一种做法是“测量微分”改成“目标值微分”,即只对目标信号求导,不对反馈求导,因为目标一般很平滑,能显著降低噪声影响。
4.3 STM32调试器连不上时先别慌
热搜词里有一条“error: no stm32 target found”,这我遇到过很多次。现象是Keil或STM32CubeIDE点下载时报找不到目标芯片,但昨天还好好的。常见原因有:SWD引脚被程序复用掉了、芯片供电不稳、目标板在低功耗模式下、或者SWD时钟频率太高。
我的排查顺序是:先按住目标板复位键的同时点下载,等连接建立瞬间再松复位——这一招能解决大量“假死”情况。如果不行,降低SWD频率到1MHz以下再试。还不行就检查BOOT0是否拉高进入ISP模式,用串口工具擦除整个Flash,芯片救回来之后再重新烧录。
这跟PID有什么关系?关系大了。很多调参调到一半,程序跑飞导致SWD口被改配置,连不上调试器,以为板子坏了。其实只要保持SWD引脚不随便复用,或者下载前预留一段延时再执行GPIO配置,这种问题基本可以避免。
5. 进阶方向与我的工程体会
5.1 级联双环怎么搭:外环定目标,内环去做事
如果你做的是位置随动、云台稳定这类项目,单环做不到的时候就要上双环。双环实现并不复杂,核心是把两个PID结构体独立实例化,然后让外环的输出作为内环的目标输入。
我一个位置跟随项目里,外环是位置环,周期10ms,输出是目标速度,传给内环速度环。内环周期1ms,输出是PWM。注意两个环的周期不能一样,一般内环比外环快5到10倍。为什么?因为外环需要等内环稳定后再做下一步决策,如果两个环同频,整个系统的相位裕度会很差,容易振荡。
代码上其实就多一行赋值:把外环PID的output,作为内环PID的target传入内环计算函数。但调参顺序反过来了,先调内环,再调外环。内环不稳,外环永远调不好,这个顺序千万别反。
5.2 模糊PID和自适应控制在什么情况下有必要
有些项目PID整定到后面发现,负载变化范围太大,固定参数不够用。网上搜“模糊PID”,其实核心还是PID,只是让KP、KI、KD根据当前误差大小在线调整。
我的经验是:只有在负载剧烈变化、且传统PID无论怎么调都压不住超调或响应太慢时,才考虑模糊PID。比如一个机械臂在不同姿态下负载惯量差好几倍,固定PID很难兼顾。模糊PID的工程难点不在算法,而在模糊规则的制定和输入输出的隶属度函数标定,这部分需要大量实测数据支撑。
更实用的替代方案是增益调度:把负载分成几个区间,每个区间存一组PID参数,根据负载状态切换。实现简单、性能可控,而且现场调参直观。很多伺服驱动器内部也是这么干的。
5.3 我调PID项目踩过的几个教训
第一,先确认反馈正确再谈算法。我那次编码器A/B接反导致飞车,不是PID写错了,是反馈符号反了。凡是算法表现异常,先怀疑硬件和接线,再怀疑公式和代码。
第二,控制周期一定要固定。PID计算放在定时器中断里而不是主循环,因为主循环的执行时间不稳定,控制周期漂移会让参数整定完全失真。把控制周期写在定时器配置里,一劳永逸。
第三,不建议用死记硬背的参数。不同电机、不同减速比、不同供电电压,参数可能差一个数量级。我见过有人把网上抄的PID参数直接粘进STM32工程里,结果电机啸叫得像报警器。参数必须跟硬件匹配,整定过程躲不掉。
最后再说一个小技巧:调PID之前,先开环测一下执行器的响应特性。比如给个固定PWM,看转速从0到稳定需要多长时间、有没有明显死区、正反转是否对称。这些数据心里有数之后,再上闭环,参数试凑会快很多。这套流程我用了很多年,希望也能帮你少走点弯路。
本文还有配套的精品资源,点击获取