为什么 PID 需要手动/自动模式
在调试 PID 算法时,我们偶尔需要强制系统输出某个特定值。最直接的做法是忽略计算结果,直接覆盖输出。虽然这看似简单,但实际工程中是个隐患。
想象一下,你让 PID 控制器辛苦计算,算出的结果却被你强行置零。这种行为会让控制器产生'积分饱和'现象:它以为自己在努力调节,实际上输出没变,于是它会不断增大误差积分项(ITerm),试图推动输出。当你停止覆写并切回 PID 控制时,巨大的积分项会导致输出瞬间飙升,造成系统震荡甚至损坏设备。

解决方案:模式切换
解决这个问题的标准做法是引入状态标志,区分'手动'和'自动'模式。手动模式下,由外部指定输出;自动模式下,由 PID 算法接管。通过这种方式,我们既能保留 PID 的内部逻辑,又能安全地切换控制权。
下面是一个基于 C++ 的实现示例,核心在于 Compute 函数中的模式判断:
/* 工作变量 */
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; // 采样周期 1 秒
double outMin, outMax;
bool inAuto = false; // 模式标志:false=手动,true=自动
#define MANUAL 0
#define AUTOMATIC 1
void Compute() {
// 关键:如果不在自动模式,直接返回,不更新任何内部变量
if(!inAuto) return;
unsigned long now = millis();
int timeChange = (now - lastTime);
if(timeChange >= SampleTime) {
/* 计算所有误差变量 */
double error = Setpoint - Input;
ITerm += (ki * error);
/* 积分限幅 */
if(ITerm > outMax) ITerm = outMax;
else if(ITerm < outMin) ITerm = outMin;
double dInput = (Input - lastInput);
/* 计算 PID 输出 */
Output = kp * error + ITerm - kd * dInput;
/* 输出限幅 */
if(Output > outMax) Output = outMax;
else if(Output < outMin) Output = outMin;
/* 保存变量供下次使用 */
lastInput = Input;
lastTime = now;
}
}
void SetTunings(double Kp, double Ki, double Kd) {
double SampleTimeInSec = ((double)SampleTime)/1000;
kp = Kp;
ki = Ki * SampleTimeInSec;
kd = Kd / SampleTimeInSec;
}
void SetSampleTime(int NewSampleTime) {
if (NewSampleTime > 0) {
double ratio = (double)NewSampleTime / (double)SampleTime;
ki *= ratio;
kd /= ratio;
SampleTime = (unsigned long)NewSampleTime;
}
}
void SetOutputLimits(double Min, double Max) {
if(Min > Max) return;
outMin = Min;
outMax = Max;
if(Output > outMax) Output = outMax;
else if(Output < outMin) Output = outMin;
if(ITerm > outMax) ITerm = outMax;
else if(ITerm < outMin) ITerm = outMin;
}
void SetMode(int Mode) {
inAuto = (Mode == AUTOMATIC);
}
这个方案的核心在于 if(!inAuto) return;。当处于手动模式时,Compute 函数会立即退出,不会更新 ITerm 或 Output。这意味着即使你在手动调节,PID 的积分项也不会继续累积,从而避免了切换回自动模式时的冲击。
你可能会问,为什么不直接在调用 Compute 的地方加个判断?这样做虽然也能达到效果,但将逻辑封装在 Compute 内部更稳健。这样无论上层如何调用,只要模式未变,内部状态就是安全的。更重要的是,它让我们清楚地知道何时更改了模式,便于后续的状态追踪和调试。


