本文由 Claude Code 编写。

在做三相DC-AC逆变器时碰到一个奇怪的现象, 简单记录一下原理。

1. 现象

上电时目标电压target还是0, 但用增量式PID时, 开头几个周期PID总会输出几个较小的正数(0.6左右), 要过3~4个周期这个输出才会被压回0。

debug时观察到此时的电压有效值now_voltage_mV是正数, error也是负的。按直觉误差一直是负的, 输出就该一直被限幅在0才对, 为什么反而冒出来一段正输出?

换成位置式PID后这个现象就消失了。

2. 前提

先明确几个事实:

  • now_voltage_mV是用方差法算的RMS, RMS恒为非负。所以哪怕真实输出为0, 测量噪声也会让有效值是个小正数。
  • 上电时target = 0, 于是error = 0 - 小正数 < 0, 而且持续为负
  • 增量式初始状态previous_err1 = previous_err2 = 0, last_output = 0。但真实的首个误差是负的-E, 初值与现实不一致
  • 输出下限min_output = 0, 即负输出会被截断到0。

3. 原理分析

离散PID的增量式公式:

关键在于微分项看的是误差的二阶差分, 而不是误差本身的符号

设各周期误差都约等于-E, 逐拍推演(参数取Kp=0.0001, Ki=0.000012, Kd=0.00005):

周期e(k), e(k-1), e(k-2)Δu(k)u(k) = u(k-1)+Δu截断后 / last_output
k=1-E, 0, 0-E(Kp+Ki+Kd)0 + 负 = 负截断到0, last_output存0
k=2-E, -E, 00·Kp + (-E)Ki + (+E)Kd = E(Kd-Ki) 0 + 正 = 正的小量不截断, 输出≈E·Kd ← 虚假小输出
k=3-E, -E, -E0 + (-E)Ki + 0·Kd = -E·Kiu(2) - E·Ki慢慢往下掉
k≥3稳态每拍-E·Ki逐步递减若干拍后归0

三个环节凑在一起:

  1. 微分项的启动冲量是正的。误差从初值0「阶跃」到-E, 这个人为阶跃在第2拍产生二阶差分(-E) - 2(-E) + 0 = +E, 是正的。也就是说误差本身是负的, 但误差的”变化”在那一拍是正的, 微分项就贡献了一个正冲量, 这就是经典的 derivative kick, 在增量式里藏得更深。

  2. 截断污染了内部状态(真正的病根)。第1拍本该得到负的u(1), 但被限幅到0, 而且把截断后的0存回了last_output。于是”系统本来有多负”这个信息被抹掉了。若状态自洽, 第2拍应是u(2) = u(1)(负) + 小正冲量 = 仍为负→输出0, 尖峰会被吃掉; 但现在变成u(2) = 0 + 小正冲量 = 正, 尖峰就冒出来了。这本质上是增量式PID的积分饱和(windup)——限幅丢掉的量没有反馈回算法内部状态, 内部的”账”和实际输出对不上。

  3. 归零耗时约Kd/Ki。尖峰之后每拍靠-E·Ki往下磨, 0.00005 / 0.000012 ≈ 4.2, 即3~4拍后归0, 和现象吻合。

Note

反直觉的点在于: 位置式每拍拿Kp·e + Ki·∑e + Kd·Δe重新算绝对值, 负误差→负结果→干净地截断到0; 而增量式算的是变化量并累加, 看的不是误差符号而是误差怎么变, 再叠加last_output这种跨拍记忆, 就给了虚假冲量泄漏的机会。

4. 解决方式

最省事的做法是换成位置式PID, 它不带last_output跨拍记忆, 天然免疫这个问题, 我最后就是这么处理的。

如果坚持要用增量式, 治本的改法是修抗饱和: 只对返回值做限幅, 但把未截断的原始值存进last_output, 让内部状态和真实历史保持一致; 或者上电首拍用当前真实error初始化previous_err1/err2, 消掉那个人为阶跃。