拓冰建站拓冰建站
首页 / 资讯中心 / 正文

PID算法从公式到代码:位置式与增量式实现及整定要点

简介一个面向PID算法入门者的C语言测试程序适合刚接触自动控制、想通过实际代码理解比例-积分-微分原理的初学者。程序把PID控制器拆解为初始化、误差计算、比例项、积分项、微分项及输出执行等步骤逻辑清晰便于对照理论逐行阅读。压缩包仅包含1个.c源文件体积约3KB轻量易用下载后可直接在常见C环境中编译运行。已有212人学习下载。通过修改Kp、Ki、Kd参数可以直观观察系统响应的变化体会比例、积分、微分各自的作用也能初步练习积分限幅、微分平滑等工程处理。对于后续学习Ziegler-Nichols整定或更复杂的控制算法是一份不错的起步素材。1. 从一段C语言代码看懂PID算法的真实边界你在调试一个温度控制系统时发现比例系数调到临界值温度还是离目标值差两度把积分项加进去之后超调又开始压不住等微分项生效系统反而抖得像筛子。这不是操作者的手感问题而是PID的参数和代码实现之间隔着好几个必须亲手踩一遍的坑。这个压缩包里的“菜鸟级PID算法学习专用测试程序.c”把一个完整的位置式PID控制流程压缩进单个源文件目标是让刚学完C语言的嵌入式初学者能在30分钟内看到偏差、积分、微分的计算过程是怎么从公式变成变量的。下面就用这份源码做解剖对象从结构体定义、误差累加到输出限幅把PID算法程序代码实现的完整路径走一遍。2. PID算法的数学原理与测试程序中的位置式实现2.1 比例、积分、微分三项的离散化语义PID控制器的连续表达式是 u(t) Kp·e(t) Ki·∫e(t)dt Kd·de(t)/dt。在单片机里没有连续的积分和微分只能按固定的采样周期T做离散化最常用的近似方式是矩形法累加积分、后向差分代替微分。于是得到位置式PID的离散计算公式u(k) Kp·e(k) Ki·T·Σe(i) Kd·(e(k) − e(k−1))/T其中 e(k) r(k) − y(k)r(k)是第k个采样周期的期望值y(k)是实际反馈值u(k)是控制输出。这里有一个初学者最容易忽略的细节积分项的写法有些教材把Ki定义为积分时间的倒数公式写成 Ki/T·Σe(i)而工程代码经常把Ki预乘了采样周期T于是直接写成 Ki·Σe(i)。两种写法作用等价但参数数值范围完全不同拿到一份现成代码先确认它的Ki维度再动手改参数。三个参数的调节方向正好相反。Kp管响应速度太小反应迟钝过大会导致震荡甚至发散Ki负责消除静差但会显著加剧超调Kd抑制超调却会放大高频噪声。下面的表列出参数增大对系统四个维度的典型影响注意这是方向性的经验结论具体数值和对象模型强相关。参数增大响应速度超调量稳态误差稳定性趋势Kp加快增大减小变差Ki加快明显增大消除明显变差Kd基本不变减小无显著影响先改善后恶化这三项之间是相互耦合的。典型表现是加大Kp之后超调变大于是再加大Kd去压制超调结果系统对外部扰动的抵抗力又变弱最后只能回头把Ki降下来牺牲静态精度换稳定。所以调参不是一次到位的而是在方向性结论的基础上来回迭代。测试程序里把三个系数直接暴露成可改的全局变量目的就是方便做这种多次试探。2.2 测试程序中的PID结构体与缓存变量设计打开这个.c文件你会看到典型的面向控制任务的代码组织方式开头通常是用结构体把所有PID状态封装在一起。为什么不用全局散装变量因为嵌入式项目里往往同时存在多个控制回路比如四轴飞行器有多个电机温度控制可能不止一个加热区把状态装进结构体之后每个控制回路各声明一个实例即可互不干扰。typedef struct { float Kp; /* 比例增益 */ float Ki; /* 积分增益 */ float Kd; /* 微分增益 */ float setpoint; /* 目标值 */ float integral; /* 积分累计值 */ float last_error; /* 上一次偏差用于微分计算 */ float output; /* 控制器输出 */ } PID_t; void PID_Init(PID_t *pid, float kp, float ki, float kd) { pid-Kp kp; pid-Ki ki; pid-Kd kd; pid-setpoint 0.0f; pid-integral 0.0f; pid-last_error 0.0f; pid-output 0.0f; }逻辑说明PID_Init只负责把状态变量清零不涉及采样和输出这样初始化在任何时刻调用都不会引发执行机构跳动。参数为什么用float而不用double在Cortex-M系列这类主流嵌入式单片机上float运算指令是单周期或双周期double虽然精度高但计算开销明显更大对控制周期为110ms的应用来说float的7位有效数字已经足够描述执行器精度。初始化之后进入控制主循环这里要关注一个顺序问题必须先取反馈、再算偏差、最后更新输出。如果颠倒了采样和计算的顺序第一次进循环时读到的就是上一次的旧反馈相位滞后会引入额外误差尤其在电机闭环等低延迟场景下这个顺序错误会导致启动瞬间的冲击电流。2.3 误差计算、积分累加与输出限幅的实现路径测试程序里真正的控制核心是PID更新函数输入是当前反馈值输出是控制量。函数内部做四件事求偏差、累加积分、做微分、限幅输出。代码看起来很简单但每一行都有工程上的讲究。float PID_Update(PID_t *pid, float measurement) { /* 第1步计算当前偏差 */ float error pid-setpoint - measurement; /* 第2步积分累加注意这里没有显式乘以采样周期 */ pid-integral error; /* 第3步微分采用一阶后向差分 */ float derivative error - pid-last_error; /* 第4步比例、积分、微分三项加权求和 */ float out pid-Kp * error pid-Ki * pid-integral pid-Kd * derivative; /* 第5步输出限幅防止执行器饱和 */ if (out 100.0f) out 100.0f; if (out -100.0f) out -100.0f; pid-output out; pid-last_error error; return out; }逻辑说明第二步没有除以采样周期是因为前面提到的Ki已经包含了T的折算如果你从仿真工具导出的公式是Ki·T·Σe(i)代码里就应该用未折算的Ki再乘以实际dt。第三步的差分项也没再除以T同理Kd里隐含了1/T。这两处最容易让初学者拿书上的公式对不上代码。输出限幅的作用被多数入门文章低估。真实执行器有物理约束比如PWM占空比最大是100%阀门开度不可能超过全行程。如果不做限幅积分项会一路累加等到偏差方向反转后需要很长时间才能“消化”掉多余的积分值这就是积分饱和现象的来源。测试程序里把限幅值写死为±100实际项目里应该把上限设为执行器的饱和输出值。关于积分饱和还有一点值得注意上面的代码里积分项是无限累加的直到输出限幅才把最终的out截断。这种“先算后截断”的方式在偏差长期存在时积分仍会突破边界所以更稳妥的做法是把积分项本身也做限幅后面第5章会展开去饱和处理。3. 增量式PID算法与位置式PID的取舍3.1 位置式实现为什么不敢直接用在执行机构上位置式PID输出的是控制量的绝对值。考虑一个用PWM控制加热棒的温度回路位置式每次算出的out直接决定占空比。如果系统运行中出现了两个采样周期的数据抖动比如传感器读到了两次错误的高温积分项立刻追加上两个大幅度的正值占空比被推向100%等反馈恢复正常积分的惯性还要好几秒才能消除。这种过去误差的“永久记忆”是位置式的先天特征。增量式PID只输出Δu(k)也就是本次相对上次的输出变化量不携带历史积分总和天然规避了错误累积导致的长期输出偏移。从公式推导上看位置式输出 u(k) 与上一次输出 u(k−1) 相减合并同类项后得到Δu(k) Kp·(e(k) − e(k−1)) Ki·e(k) Kd·(e(k) − 2·e(k−1) e(k−2))推导过程不复杂主要依靠移项和合并。注意这个结果里不再出现Σe(i)意味着控制器内部无需保存长期积分值只需要保存最近三拍偏差。对于执行器本身有记忆属性的系统比如步进电机走一步就停一步增量式更匹配因为它输出的正是“走多少步”。工程上常用下面的表格判断该选哪种实现形式。对比维度位置式PID增量式PID输出形式控制量绝对值控制量增量历史数据依赖需要积分累加值只需最近三拍偏差积分饱和风险高低手动/自动切换需要做输出跟踪否则跳变切换影响小适合场景阀门、加热等位置保持型步进电机、伺服驱动等增量型这里要纠正一个常见误区增量式不等于更高级。很多工程教材力推增量式理由是抗积分饱和能力强但位置式配合积分分离或限幅之后效果并不比增量式差。选哪种首先看执行机构是“吃绝对值还是吃增量”再考虑系统故障时的安全性。例如位置式在输出通道断线时会直接输出一个固定值执行器停在对应位置增量式一旦输出清零执行器可能保持上一拍的位置也可能反向自由滑动取决于步进电机还是比例阀。3.2 在测试工程上把位置式改造成增量式的三个步骤第一步把结构体中的 integral 和 last_error 替换为长度为3的偏差环形缓存typedef struct { float Kp, Ki, Kd; float error[3]; /* error[0]当前偏差error[1]上一拍error[2]上上一拍 */ float delta_out; /* 增量输出 */ } IncPID_t; void IncPID_Init(IncPID_t *pid, float kp, float ki, float kd) { pid-Kp kp; pid-Ki ki; pid-Kd kd; pid-error[0] pid-error[1] pid-error[2] 0.0f; pid-delta_out 0.0f; }第二步改写计算函数每次调用先把误差数组整体后移再写入新的当前偏差float IncPID_Update(IncPID_t *pid, float setpoint, float measurement) { /* 历史偏差顺序后移腾出当前位置 */ pid-error[2] pid-error[1]; pid-error[1] pid-error[0]; pid-error[0] setpoint - measurement; /* 增量式核心公式 */ pid-delta_out pid-Kp * (pid-error[0] - pid-error[1]) pid-Ki * pid-error[0] pid-Kd * (pid-error[0] - 2.0f * pid-error[1] pid-error[2]); return pid-delta_out; }逻辑说明整个计算过程没有累加器所有浮点运算都是同数量级的加减法数值溢出风险远低于位置式。误差数组的移位必须放在计算偏差之前否则会用到两次同一个偏差导致微分项形同虚设。第三步在主程序里保存一个累计输出变量把增量叠加到它上面同时做限幅static float current_out 0.0f; /* 每个控制周期调用一次 */ float actuator_out current_out IncPID_Update(pid, 50.0f, current_temp); if (actuator_out 100.0f) actuator_out 100.0f; if (actuator_out 0.0f) actuator_out 0.0f; current_out actuator_out;这里的限幅与位置式的限幅作用相同区别在于增量式即使出现限幅误差数组仍在持续更新偏差一旦反向恢复输出立即就会沿正确方向回到目标附近不需要像位置式那样先把积分“消化”完。3.3 积分分离位置式抗饱和的另一种改造方式并不是所有场景都适合改成增量式。加热炉、液位控制这类执行机构需要绝对开度的项目位置式更直观。但如果直接保留2.3节那段代码积分饱和迟早出现。社区里最常用的变通方案是积分分离偏差超过阈值时暂停积分累加偏差回落后再恢复。这样做抵消了位置式最大的短板同时保留绝对输出特性。float error pid-setpoint - measurement; if (fabsf(error) INTEGRAL_LIMIT) { /* 偏差过大积分停止增长 */ } else { pid-integral error; }INTEGRAL_LIMIT的值不是随便给的太小会让积分在大偏差区间完全失效系统很难跨越死区太大又失去抗饱和意义。常见做法是先看执行器满量程对应的最大偏差范围取目标值的10%左右作为起点再根据阶跃响应是否出现长时间超调来回调。这个参数和设备控制精度直接相关只能在设备上用实测曲线来标定。积分分离配合输出限幅之后位置式PID的抗饱和表现已经接近增量式实现成本只是多一个if判断。测试程序里的原始代码虽然只有几行但理解到这一步才算真正读懂这份源码。4. 用Ziegler-Nichols整定法驱动测试程序找参数4.1 临界比例度法的流程与工程约束参数整定是PID学习中最难有标准答案的环节因为传递函数不同最优参数也完全不同。测试程序给出的默认参数应对具体设备基本是飘的。最经典的无模型整定法是Ziegler-Nichols临界比例度法不需要知道被控对象的精确数学模型只需要一组试验数据就能落定初始参数。操作流程分四步。第一步把测试程序里的Ki、Kd改为0只保留比例控制第二步把Kp从一个很小的值开始逐步增大每次增加后等系统进入稳定状态再观察输出波形第三步当输出曲线恰好出现等幅、持续的振荡时记录此时的增益值Ku和振荡周期Tu第四步将Ku和Tu代入经验公式得到三组PID参数。注意等幅振荡和发散振荡的区别前者的波形幅度是稳定的后者一个周期比一个周期大出现发散必须立刻减小Kp否则设备或执行器可能损坏。控制器KpKiKdP0.50·Ku——PI0.45·Ku1.2·Kp/Tu—PID0.60·Ku2.0·Kp/TuKp·Tu/8把测试程序改造成支持临界比例度法的环境很容易只需要把Kp、Ki、Kd开放成宏定义或外部可改变量再在main函数里提供一键清零的重置接口。实际应用中这个方法的两个软肋也要知道一是系统不允许出现持续等幅振荡比如温度过冲会烧坏设备那就改用衰减曲线法找衰减比41的波形二是惯性大的系统达到等幅振荡可能要几分钟甚至更久试验期间如果没有安全保护机制建议预留报警阈值。4.2 改造测试程序加入一阶惯性仿真对象如果没有真实硬件单纯看PID输出值变化很难建立直观感受。一种常见的做法是改造这个测试程序在代码里内置一个用一阶惯性近似的仿真对象把PID的输出先喂给仿真对象再把仿真结果作为反馈送回PID输入端这样组成了一个软件在环的闭环系统。static float plant_value 0.0f; /* 一阶惯性对象时间常数tau采样周期dt */ float simulate_plant(float ctrl_out, float dt, float tau) { plant_value (ctrl_out - plant_value) * dt / tau; return plant_value; } /* 主循环里的调用顺序计算PID输出 - 喂给仿真对象 - 读取反馈 */ float pid_out PID_Update(pid, 50.0f, plant_value); plant_value simulate_plant(pid_out, 0.1f, 1.0f);逻辑说明这个仿真模型本质是递推式差分方程把温控、小车油门这类系统近似成一阶惯性虽然不算精确模型但已经能体现超调量和稳态误差的基本形态。参数说明dt和tau的取值会直接改变响应速度测试环境里建议先保持dt0.1、tau1再按4.1的方法整定一组参数观察恢复时间。这样做还有个额外的好处可以在仿真对象上叠加一个高频干扰项比如每隔一段时间给plant_value直接加一个幅值有限的噪声然后再回到测试程序跑一遍能直观看到微分项为什么会产生抖动也为下一章的滤波处理提供了实验载体。4.3 从响应曲线形态反推参数调整方向整定完成后还有一个容易被忽略的验证动作手动观察阶跃响应曲线。把目标值从50改成60记录输出到达目标的时间、第一次越过目标的超调峰值、以及稳定后与目标值之间的残差。这三个指标对应三组参数方向上升时间太长就加大Kp超调过大就加大Kd稳态残差不消除就加大Ki。但每次只改一个参数改完之后重跑整套阶跃才是稳妥的迭代方式。如果上升时间和超调同时不满足多半是Kp、Kd都需要动。一个工程经验是先用临界比例度法确定Kp的大致范围再固定Kp微调Ki消除稳态残差最后用Kd收敛超调。注意Kd过大的表现很特殊系统会出现周期很短的高频振荡而Kp过大时振荡周期较长观察振荡周期就能判断是哪一项出了问题。这套判断逻辑放在测试程序里就是几轮循环的功夫但对真机来说一次错误参数的后果可能就是设备报警。5. 微分项低通滤波与采样周期补偿的落地点5.1 给微分项加一阶低通滤波位置式PID的D项计算误差的差分也就是相邻两次偏差的差值。理论上这个差值越小越接近真实导数但在真实传感器上哪怕只有±1个LSB的量化噪声也会让相邻两次偏差的差值出现与真实趋势无关的随机跳动Kd一旦放大这些跳动直接叠加到输出上系统就表现为频率较高的抖动。有经验的工程师不会直接去掉微分项而是给它加一阶惯性滤波把突变削平后再参与运算。float raw_d error - pid-last_error; pid-d_filtered 0.8f * raw_d 0.2f * pid-d_filtered; float out pid-Kp * error pid-Ki * pid-integral pid-Kd * pid-d_filtered;逻辑说明0.8是滤波系数的经验试探值。alpha越接近1滤波越弱跟随性越好越接近0滤波越强相位滞后越大。alpha0.8对多数电机和温控系统是相对安全的起始值实际项目中可以做成运行时可调边试边改。5.2 用实际时间差补偿积分残差多数入门代码把采样周期T写死成常数实际单片机的定时器虽然稳定但中断响应、浮点运算耗时会让T产生微小波动。积分项的错误积累到一定程度稳态残差就出现了。常见的做法是每次进入控制循环读取当前系统计数器的时刻值用实际时间差代替固定dt。uint32_t now TIMER_ReadTick(); float actual_dt (float)(now - last_tick) / 1000.0f; pid-integral error * actual_dt; float derivative (error - pid-last_error) / actual_dt; last_tick now;这种做法引入两处除法对控制周期影响有限但对积分精度的改善是可见的尤其是定时器抢占频繁的裸机调度场景。把采样周期补偿写进测试程序后你会发现不同负载下输出的稳态残差明显缩小。把滤波系数alpha和实际dt这两处补丁同时打进测试程序再用第4章的仿真对象跑一次阶跃响应注意观察输出曲线的高频分量是否消失以及目标值附近的残差是否被压到量化噪声以下。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门