尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

用电子表格仿真离散PID控制:不学微积分也能调参

用电子表格仿真离散PID控制:不学微积分也能调参 PID比例-积分-微分控制在工业现场几乎无处不在从温控仪表到电机调速从无人机姿态到电源管理到处都有它的影子。但很多非控制专业的开发者看到∫e(t)dt和de(t)/dt两个数学符号就被劝退了——微积分没学好是不是就学不会 PID 了结论是完全不需要。真正跑在产品里的 PID 控制器几乎全部是离散化后的差分和累加运算说穿了就是加减乘除。这篇文章要做的就是一件事把 PID 从微积分公式翻译成一张电子表格里的递推计算让你在 Excel、WPS 或 Google Sheets 里亲手把闭环调通亲眼看到比例、积分、微分三个分量分别做了什么。读完这篇文章你会得到三层收获第一彻底理解离散 PID 为什么不需要微积分第二获得一个可复制的电子表格仿真模板用来观察超调、振荡和稳态误差第三掌握从表格迁移到 Python 或嵌入式 C 代码的方法为真实项目调试建立直觉。1. 这篇文章真正要解决的问题先聊一下普遍痛点。很多嵌入式开发者第一次接触 PID是在“电机调速”或“温控系统”这类项目里。查资料时看到的几乎都是连续域公式u(t) Kp * e(t) Ki * ∫e(t)dt Kd * de(t)/dt微分方程符号一出现学习曲线立刻变得陡峭。很多人因此陷入“看公式似懂非懂写代码无从下手”的尴尬局面。另一个常见误区是一说到 PID 仿真第一反应就是上 MATLAB/Simulink。但 MATLAB 对个人开发者来说 license 成本高、环境重、上手慢拿来做一次简单的控制验证有点“杀鸡用牛刀”。而几乎每台电脑上都会有的电子表格软件恰恰被很多人忽略了。电子表格处理递推计算有一种天然优势每一行就是一个采样时刻上一行的计算结果可以随时被下一行引用变量变化过程一览无余。这对理解 PID 内部工作状态非常有帮助。这篇文章真正要解决的是“控制算法的认知门槛”问题适合以下几类读者做嵌入式开发准备在 MCU 上用 PID 控制电机、舵机或加热器的开发者刚接触自动控制原理被微积分公式拦住想找一个直观理解路径的学生做上位机或测试工具需要快速验证某个 PID 参数组合是否合理的调试工程师。说白了这篇文章是帮你“用最便宜的实验环境建立控制直觉”再把直觉翻译成实际代码。2. 离散化为什么“不用微积分”也能做 PID要理解 PID 的离散化先得搞清楚连续公式里那三个数学符号在工程上到底代表什么。2.1 比例项当前偏差的即时反应e(t)是“目标值和当前值之差”也就是当前偏差。比例项Kp * e(t)的含义非常直白偏差越大输出越大让它尽快把偏差压下去。这是一个纯瞬时运算离散化之后就是P(k) Kp * e(k)没有任何微积分成分。2.2 积分项用累加替代积分积分项∫e(t)dt真正在做的事是把历史上所有偏差加起来。为什么需要它因为纯比例控制一定会留下稳态误差——比如加热器输出 50% 时刚好能维持 40 度但你的目标温度是 60 度那系统就永远停在某个低于 60 度的位置。积分项的功能是“记账”过去的偏差一直在累积累积到一定程度就能产生额外输出把这个稳态误差顶掉。连续域的积分在离散域里就是累加求和I(k) Ki * Σ(e(i) * dt)其中dt是采样周期Σ是对所有历史采样点的偏差求和。这个计算在单片机里就是一个变量不断累加在电子表格里就是“上一行累加值加上本行偏差乘以dt”。2.3 微分项用差分替代微分微分项de(t)/dt描述的是“偏差变化得有多快”。如果偏差正在快速减小说明系统已经在接近目标此时微分项会提前“踩刹车”减少输出抑制超调。差分的想法更简单用“当前偏差减去上一次偏差再除以采样时间”来近似变化率。D(k) Kd * (e(k) - e(k-1)) / dt2.4 连续域和离散域对比数学概念连续域表达式离散域实现计算成本比例项Kp * e(t)Kp * e(k)一次乘法积分项Ki * ∫e(t)dtKi * Σ(e(i)*dt)一次加法一次乘法微分项Kd * de(t)/dtKd * (e(k)-e(k-1))/dt一次减法一次除法看到这个表格就明白了离散 PID 就是“乘、加、减、除”的组合和微积分没有直接关系。你用不上求导法则也用不上积分技巧只需要按照采样周期一步步递推。这也是为什么我在文章标题里强调“Calculus-free PID”——控制算法真正需要的不是数学技巧而是更新、判断、调整的工程思维。电子表格恰恰是展示这种递推思维最好的工具。3. 在电子表格里搭建 PID 仿真实验台下面进入实操环节。我们用电子表格做一个闭环控制的仿真被控对象是一个一阶惯性系统类似加热器或者简单的电机转速模型设定值是目标值表格逐行计算 PID 输出再让对象模型更新当前值形成闭环。3.1 整体思路表格中每一行代表一个采样时刻一行接一行地递推。闭环逻辑是计算当前误差e SP - PV计算 PID 三项输出并求和得到控制量u把控制量u输入到被控对象模型得到下一时刻的过程值PV下一行重复这个过程。为了让公式更清晰我们需要一个固定的参数区域用绝对引用锁定参数。这里假设把参数放在K1:K6单元格区域。3.2 参数区设置先在工作表顶部填写参数单元格参数示例值说明K1Kp2.0比例系数K2Ki0.5积分系数K3Kd0.1微分系数K4dt0.1采样周期单位秒K5K_obj1.0对象增益K6T_obj2.0对象时间常数单位秒说明一下这些参数的物理含义。dt 0.1表示每秒钟采样 10 次K_obj 1.0表示稳态时控制量和过程值是 1:1 的关系T_obj 2.0表示对象响应速度时间常数越小响应越快。这些数字不是固定的你可以随时改来观察效果。3.3 数据区列定义从第 2 行开始放数据。用 A 到 J 列各列含义如下A: 时间 t B: 设定值 SP C: 过程值 PV D: 误差 Err SP - PV E: 比例项 P F: 积分累加值 G: 积分项 I H: 误差变化率 I: 微分项 D J: 控制输出 u第一行数据第 2 行需要手工填入初始状态因为递推公式需要引用上一行的值。假设初始温度为 25 度设定值为 50 度。3.4 第 2 行初始值单元格值说明A20初始时间 0B250设定值 50C225过程值初始为 25D2B2-C2误差 25E2$K$1*D2P 项F20积分累加从 0 开始G2$K$2*F2I 项H20第一行无历史变化率设为 0I2$K$3*H2D 项J2E2G2I2控制输出这里有一个新手容易踩的坑第一行不能直接写“用上一行”的公式因为它是表格边界没有上一行。所以第 2 行的积分累加值和误差变化率必须手工设为 0。3.5 第 3 行开始递推从第 3 行开始所有公式都可以引用上一行了。关键公式如下A3 A2 $K$4 B3 50 C3 C2 ($K$4 / $K$6) * ($K$5 * J2 - C2) D3 B3 - C3 E3 $K$1 * D3 F3 F2 D3 * $K$4 G3 $K$2 * F3 H3 (D3 - D2) / $K$4 I3 $K$3 * H3 J3 E3 G3 I3逐一解释这些公式的作用。A3 A2 $K$4是时间轴递增每行比上一行多一个采样周期dt。C3 C2 (dt / T_obj) * (K_obj * J2 - C2)是被控对象模型。它的含义是对象在当前值C2的基础上以时间常数T_obj的速度向目标值K_obj * J2靠近。控制量J2是上一行算出来的也就是当前行的控制输出作用于下一行——这符合真实系统的因果性。D3是误差等于设定值减去过程值。控制的目标就是让这个值趋近于 0。F3是积分累加在上一行累加值基础上加上D3 * dt。注意这里用的是当前行误差而不是上一行误差因为积分项描述的是“到目前为止的偏差积累”。H3是误差变化率用“当前误差减上一行误差”除以采样周期近似微分。J3是总输出等于 P I D 三项之和。设置好第 3 行的公式后选中第 3 整行往下拖动填充到比如第 1002 行这样就能观察 100 秒内的仿真曲线。建议给 D 列误差、C 列过程值、J 列控制输出分别插入折线图。选中插入图表后你会看到过程值从 25 度爬升到 50 度附近的过程这是非常直观的闭环响应曲线。4. 完整示例一阶温度对象的闭环仿真现在我们把第 3 节的模板完整跑起来看看不同参数下的系统表现。下面用一个实际算例验证表格逻辑。4.1 基准参数下的响应假设对象是一个加热器模型采样周期dt 0.1秒对象时间常数T_obj 2.0秒目标是让温度从 25 度升到 50 度并稳定。先取一组温和参数Kp 2.0 Ki 0.5 Kd 0.1运行表格后你会看到这样的过程开始阶段误差为 25P 项输出 50控制量较大温度快速上升接近设定值误差逐渐减小P 项输出下降越过设定值后出现一个小幅超调然后回落最终稳定积分项把稳态误差抹掉温度稳定在 50 度附近。这个“先上升、再越过、然后回落稳定”的过程就是经典的二阶响应特性。通过折线图观察这个相位变化比直接读公式要直观得多。4.2 只保留 P 控制稳态误差把表格中的 Ki 和 Kd 都设为 0即Kp 2.0, Ki 0, Kd 0。此时再看曲线你会发现温度稳定在 45 度左右——离目标 50 度还差一段距离。原因很简单当输出为 100% 时对象稳态温度是 100 度但输出高于某个值后温度会继续上升直到系统输出刚好维持某个低于目标的温度。此时误差存在但 P 项输出产生的维持力和对象散热/负载达到平衡系统就停在稳态误差上。这就是积分项存在的意义。4.3 加入积分项消除稳态误差把 Ki 调回 0.5曲线会慢慢向 50 度靠拢稳态误差被消除。这正是比例积分PI控制的核心价值。在实际工程中绝大多数温控系统只用 PI 就够了微分项往往不启用。4.4 加入微分项抑制超调把 Kd 从 0 调到 0.1 再调到 0.5观察超调幅度。Kd 的作用是“预测误差趋势”当偏差快速减小时微分项输出负值相当于提前刹车让系统减速靠近目标从而降低超调。但注意Kd 调得过大也会带来副作用这会在常见问题部分详细说明。为了让示例更贴近真实我建议你把表格参数区改成这样再跑一组Kp 3.0 Ki 0.1 Kd 0.05对比上一组参数你会发现这组参数可能振荡更剧烈稳定时间更长。这就是“参数整定”最朴素的起点通过修改三个数观察响应曲线找到平衡点。5. 增量式 PID另一种更稳的实现方式上面用的是位置式 PID也就是每次计算一个绝对输出值u(k)。这种实现思路简单但在工程上有一个麻烦积分累加项可能变得非常大导致输出饱和手动切换控制策略时输出会出现跳变。增量式 PID 是位置式的升级版本。它不直接计算输出值而是计算输出增量Δu(k)。5.1 增量式的公式拆解由位置式离散 PIDu(k) Kp * e(k) Ki * Σ(e(i)*dt) Kd * (e(k) - e(k-1)) / dt用u(k) - u(k-1)做差中间的积分累加项被消掉了得到Δu(k) Kp * (e(k) - e(k-1)) Ki * dt * e(k) Kd * (e(k) - 2*e(k-1) e(k-2)) / dt其中二阶差分项e(k) - 2*e(k-1) e(k-2)是对微分的精细处理。实际输出为u(k) u(k-1) Δu(k)增量式的优势很明显积分的累积效应变成了逐步叠加系统天然具有抗积分饱和能力输出增量不会出现突变对执行机构更友好从手动切换到自动控制时不需要重新初始化积分值。5.2 在表格里实现增量式 PID在原有表格基础上需要增加两列来保存历史误差单元格解释K 列e(k-1)上一行误差L 列e(k-2)上两行误差第 3 行开始增量式和位置式的差别体现在输出列J3 J2 $K$1 * (D3 - D2) $K$2 * $K$4 * D3 $K$3 * (D3 - 2*D2 之前某行误差) / $K$4但这里有个细节需要提醒在电子表格里保存“上两行误差”最直接的方法是加两列专门存放历史值。比如 K3 存D2L3 存K2然后输出列引用 K3 和 L3。这样公式更清晰不容易出错。增量式 PID 在实际工程中常用于步进电机控制、阀门调节这类对输出平稳性要求较高的场景。如果你之后要写嵌入式代码增量式也容易实现只需要维护两个历史误差变量。6. 在表格里理解参数整定Kp、Ki、Kd 分别干什么很多人背下了“调参口诀”——先调 P、再调 I、最后调 D——但不知道为什么要按这个顺序。在表格里亲手跑几组参数这个顺序的自然性就体现出来了。6.1 调大 Kp响应变快但容易振荡把 Kp 从 1 调到 3 再调到 5。你会观察到系统响应更快初始上升段更陡峭超调变大如果 Kp 超过某个临界值系统开始持续振荡。这是因为比例项是“照妖镜”它直接放大误差。误差越大它越用力也越容易用力过猛冲过头。6.2 调大 Ki消除稳态误差但可能震荡加剧持续增大 Ki你会看到稳态误差被消除得更快但系统可能变成等幅振荡。原因是积分项在累积历史偏差累积速度过快就会“过冲”。在表格里观察 F 列积分累加值你会发现它一直在增大即使过程值已经接近设定值。这个现象对应工程中的“积分饱和”。如果控制输出达到上限但积分还在累积系统恢复响应就会变慢。6.3 调大 Kd抑制超调但会被噪声放大增大 Kd超调会明显下降但过程值曲线可能出现毛刺。因为微分项对误差变化率敏感而在真实系统中测量值总有噪声。表格仿真里没有噪声所以看上去没问题换到真实传感器后过大的 Kd 会让输出抖动得厉害。6.4 一个实用的整定顺序建议你在表格里按下面的流程做一次完整的整定练习先把 Ki 和 Kd 设为 0增大 Kp直到系统出现等幅振荡记录临界 Kp 值把 Kp 降到临界值的 50%~60%逐步增加 Ki直到稳态误差消除如果超调过大再小步增加 Kd同时观察输出是否抖动。这套流程对应工程上经典的“临界比例度法”本质并没有多高深就是通过实验建立“参数-响应”的对应关系。在电子表格上先做一遍心理就有底了。7. 从表格迁移到真实代码Python 和嵌入式参考电子表格的价值在于快速验证但最终算法要跑在代码里。这里给出一个与表格递推逻辑完全一致的 Python 实现以及嵌入式 C 的代码骨架。7.1 Python 最小实现下面这段代码模拟的是一阶对象加 PID 控制器逻辑和表格完全对应import time class PID: def __init__(self, kp, ki, kd, dt, limit_min-100.0, limit_max100.0): self.kp kp self.ki ki self.kd kd self.dt dt self.limit_min limit_min self.limit_max limit_max self.integral 0.0 self.prev_error 0.0 self.output 0.0 def update(self, setpoint, pv): error setpoint - pv self.integral error * self.dt derivative (error - self.prev_error) / self.dt self.output self.kp * error self.ki * self.integral self.kd * derivative self.prev_error error # 输出限幅 if self.output self.limit_max: self.output self.limit_max elif self.output self.limit_min: self.output self.limit_min return self.output def simulate(): dt 0.1 kp, ki, kd 2.0, 0.5, 0.1 pid PID(kp, ki, kd, dt) T_obj 2.0 K_obj 1.0 pv 25.0 sp 50.0 for step in range(1000): u pid.update(sp, pv) pv pv (dt / T_obj) * (K_obj * u - pv) if step % 20 0: print(ft{step * dt:.1f}s SP{sp:.1f} PV{pv:.2f} u{u:.2f}) if __name__ __main__: simulate()运行后你会看到 PV 从 25 逐步逼近 50输出u先大后小并最终稳定。这个结果和表格仿真是完全一致的。代码里的integral对应表格中的 F 列prev_error对应表格中保存上一行误差的变量。如果输出需要增量式只需要改成这样def update_incremental(self, setpoint, pv): error setpoint - pv delta_u (self.kp * (error - self.prev_error) self.ki * self.dt * error self.kd * (error - 2 * self.prev_error self.prev_prev_error) / self.dt) self.output delta_u self.prev_prev_error self.prev_error self.prev_error error return self.output需要维护prev_error和prev_prev_error两个历史变量。7.2 嵌入式 C 代码骨架嵌入式代码不需要在 MCU 上跑对象模型而是把传感器采集到的实际值送给 PID 函数再把输出用于 PWM 占空比、阀门开度或电流指令。这里给一个最精简的位置式 PID 骨架typedef struct { float kp; float ki; float kd; float dt; float integral; float prev_error; float out_min; float out_max; } pid_t; float pid_update(pid_t *pid, float setpoint, float pv) { float error setpoint - pv; pid-integral error * pid-dt; float derivative (error - pid-prev_error) / pid-dt; float output pid-kp * error pid-ki * pid-integral pid-kd * derivative; pid-prev_error error; if (output pid-out_max) { output pid-out_max; } else if (output pid-out_min) { output pid-out_min; } return output; }使用这个函数时要注意三点。第一dt必须与 PID 函数的实际调用间隔保持一致不能想多久调用一次就多久调用一次第二输出限幅要加上否则积分饱和会非常严重第三从手动输出切换到 PID 自动输出时需要把integral初始化为当前手动输出避免输出跳变。8. 常见问题与排查思路这里整理一些在表格仿真和实际部署中最常遇到的问题。问题现象可能原因排查方式解决方案表格公式显示错误或结果不对公式引用了错误单元格或第一行初始值未设置检查第 2 行是否手工填入了初始值把初始值单元格重新赋值避免公式引用空行图表只显示一条直线对象模型公式没有正确引入上一行输出检查 C 列公式是否引用了 J 列上一行确认 C3 公式中的J2引用方向正确过程值振荡越来越大Kp 过大或 Ki 过大在表格中观察误差极值是否递增减小 Kp或把 Ki 调小后重新仿真稳定后与设定值存在偏差积分项未开启或 Ki 太小检查 G 列 I 项是否为 0增大 Ki 并观察稳态误差输出总是停在边界上积分累积过多发生积分饱和查看 F 列积分累加值是否异常增大增加输出限幅和抗积分饱和逻辑Kd 很大时输出出现毛刺微分对噪声敏感在真实系统中查看信号波形增加滤波或关闭 D 项把表格参数移植到真实设备后效果完全不同对象模型与实际系统差异大对比实际相应时间常数先在真实系统小范围试凑不能直接照搬这里重点说两个最容易忽视的问题。第一个是采样周期。表格里dt统一为 0.1 秒但真实系统里定时器可能不准或者某个函数被阻塞了长时间没调用。dt不一致会导致积分项和微分项的计算结果都偏离预期。在嵌入式代码里推荐用get_tick_ms()算出两次调用的真实时间差再传给 PID 函数而不是假设固定时间间隔。第二个是正作用和反作用。不是所有系统都是“输出越大过程值越大”。比如制冷设备输出增大时温度反而下降此时误差计算需要考虑方向。如果方向搞反了控制系统会变成正反馈输出往反方向使劲系统立刻发散。调参前先确认执行机构的作用方向。9. 工程化建议与安全提醒电子表格仿真跑通只是第一步要拿到真实项目里用还需要做几层加固。9.1 先仿真后实物的标准流程建议的落地路径是先用电子表格或 Python 搭建对象模型确认 PID 算法和参数方向都正确再借助真实采集数据建模或者直接在设备上小步试凑最后才进入正式的闭环运行。流程上必须包括备份、回滚和观察窗口防止参数不当导致设备损坏。9.2 输出限幅是底线任何 PID 输出都应做限幅。在代码示例里已经加了out_min和out_max。对于电机或加热器输出限幅不光是保护执行机构也是防止积分项无限膨胀的手段。更严谨的做法是抗积分饱和当输出达到上限且误差仍为正时暂停积分累加避免下一轮积分过大造成严重超调。9.3 参数可追溯、可回滚调参是经验活但要养成工程习惯。每次修改参数前记录修改时间、修改人、参数值和现场效果。生产环境改动前先确认当前运行版本和参数值改完之后留出观察期。保存 CSV 日志方便事后用表格或 Python 分析响应曲线。这条建议在团队协作时尤其重要因为“调参调挂了”是控制项目里最常见的事故。9.4 表格仿真和真实系统的差异一定要清醒认识到表格里的被控对象是理想化的一阶模型真实系统往往有非线性、时滞、摩擦、噪声和负载扰动。所以表格里调出来的一组好参数顶多是“参考起点”不能直接作为生产参数。建议你在表格仿真时故意加入随机噪声和输出死区看看 PID 还能不能稳定这样更接近真实情况。10. 总结与后续学习路径这篇文章核心讲清楚了一件事离散 PID 不需要微积分它是一种递推算法用累加替代积分用差分替代微分可以完整地在一张电子表格上演示闭环控制过程。通过表格你能直观看到 P 项如何响应误差、I 项如何消除稳态误差、D 项如何抑制超调也能亲手完成一次参数整定实验形成对控制系统的直觉。理解这个递推逻辑之后建议你先把自己的表格模板保存下来尝试修改对象模型比如加入延迟环节观察 PID 参数稳定范围的变化然后按照文章里的 Python 代码把表格逻辑翻译成可执行脚本最后再在开发板上尝试真实的电机转速控制加上输出限幅和抗积分饱和就构成了一条从理论到工程落地的完整路径。如果下一步想深入可以继续学习串级 PID、前馈补偿、模糊 PID 和基于模型的开环前馈叠加双环结构。但无论多高级的控制算法底层的起点都是“偏差怎么算、输出怎么更新”这两个问题。把电子表格里的递推逻辑吃透了后面的路会顺畅很多。
返回列表