第1章:PID离散化实现——从连续域到离散域的转换
各位同学,今天我们来聊聊PID控制器的离散化实现。说实话,很多初学者一上来就对着连续域的PID公式猛调参数,结果在实车上跑得一塌糊涂。为什么?因为你的控制器是跑在数字芯片上的,不是模拟电路啊。
我个人习惯先把连续域的东西搞清楚,再谈离散化。你想想看,连续域的PID公式长这样:
u(t) = Kp * e(t) + Ki * ∫e(τ)dτ + Kd * de(t)/dt
但在嵌入式系统里,我们只能每隔一个采样周期Δt去计算一次。这就引出了一个问题——怎么把积分和微分用离散的方式表达出来?
1.1 离散化的两种主流方法
我在项目中用过不少离散化方法,但最常用的就两种:前向欧拉法和后向欧拉法。说白了,就是用差分代替微分,用累加代替积分。
| 方法 | 积分近似 | 微分近似 | 特点 |
|---|---|---|---|
| 前向欧拉 | ∫e dt ≈ Σ e(k) * Δt | de/dt ≈ (e(k+1) - e(k)) / Δt | 实现简单,但可能不稳定 |
| 后向欧拉 | ∫e dt ≈ Σ e(k) * Δt | de/dt ≈ (e(k) - e(k-1)) / Δt | 更稳定,我推荐用这个 |
嗯,这里要注意:前向欧拉法在采样频率不够高时,微分项容易产生振荡。我曾经在调试一个转向控制器时,就因为用了前向欧拉,结果方向盘高频抖动,差点把测试车开进花坛里。从那以后,我基本只用后向欧拉法。
1.2 位置式PID的离散化实现
位置式PID,说白了就是直接计算当前时刻的控制量u(k)。它的离散化公式是:
u(k) = Kp * e(k) + Ki * Σe(i) * Δt + Kd * (e(k) - e(k-1)) / Δt
这里有个坑——积分项会无限累加。如果执行器饱和了,积分还在继续增加,就会出现"积分饱和"现象。我刚开始做自动驾驶时,就因为这个现象导致车辆在弯道里越转越猛,最后不得不紧急制动。
来看C++实现:
class PositionalPID {
private:
float Kp, Ki, Kd;
float dt;
float integral;
float prev_error;
float integral_limit; // 积分限幅
public:
PositionalPID(float p, float i, float d, float sample_time)
: Kp(p), Ki(i), Kd(d), dt(sample_time),
integral(0), prev_error(0), integral_limit(100) {}
float calculate(float setpoint, float measurement) {
float error = setpoint - measurement;
// 积分项累加
integral += error * dt;
// 积分限幅——血的教训换来的
if (integral > integral_limit) integral = integral_limit;
if (integral < -integral_limit) integral = -integral_limit;
// 微分项(后向欧拉)
float derivative = (error - prev_error) / dt;
// 计算输出
float output = Kp * error + Ki * integral + Kd * derivative;
prev_error = error;
return output;
}
};
1.3 增量式PID的离散化实现
增量式PID不直接计算控制量,而是计算控制量的增量Δu(k)。公式推导起来很简单:
Δu(k) = u(k) - u(k-1)
= Kp * (e(k) - e(k-1)) + Ki * e(k) * Δt + Kd * (e(k) - 2*e(k-1) + e(k-2)) / Δt
你想想看,增量式PID有什么好处?它天然不带积分累加,所以不会出现积分饱和。而且当执行器是步进电机这类增量型设备时,直接输出增量更自然。
Python实现如下:
class IncrementalPID:
def __init__(self, Kp, Ki, Kd, dt):
self.Kp = Kp
self.Ki = Ki
self.Kd = Kd
self.dt = dt
self.error_prev = 0
self.error_prev2 = 0
self.output_prev = 0
def calculate(self, setpoint, measurement):
error = setpoint - measurement
# 计算增量
delta_u = (self.Kp * (error - self.error_prev)
+ self.Ki * error * self.dt
+ self.Kd * (error - 2*self.error_prev + self.error_prev2) / self.dt)
# 累加得到当前输出
output = self.output_prev + delta_u
# 更新状态
self.error_prev2 = self.error_prev
self.error_prev = error
self.output_prev = output
return output
1.4 两种PID的对比与选型建议
| 对比项 | 位置式PID | 增量式PID |
|---|---|---|
| 输出形式 | 绝对控制量 | 控制量增量 |
| 积分饱和 | 容易发生 | 天然避免 |
| 计算量 | 稍大(需积分累加) | 稍小 |
| 适用场景 | 速度控制、温度控制 | 转向控制、步进电机 |
| 手动/自动切换 | 有冲击 | 无冲击 |
我个人习惯是:能用增量式就用增量式。为什么?因为增量式天然抗积分饱和,而且手动自动切换时没有冲击。我记得有一次在测试车上,从手动模式切到自动模式,位置式PID直接跳了一个大输出,车猛地一窜——还好系了安全带。
1.5 采样时间的选择——一个容易被忽视的问题
离散化PID里有个关键参数——采样时间Δt。选大了控制精度不够,选小了计算负担重。我一般遵循两个原则:
- 原则一: 采样频率至少是系统带宽的10倍。比如你的车辆横摆响应带宽是5Hz,那采样频率至少50Hz(Δt=20ms)。
- 原则二: 采样时间要稳定。不要这次10ms,下次15ms,再下次8ms。这样微分项会剧烈波动。
我曾经在一个项目中,因为用了非实时操作系统,采样时间抖动很大。结果PID控制效果时好时坏,查了两天才发现是采样时间不均匀的问题。后来加了定时器中断,问题就解决了。
好了,这一章的内容就到这里。下一章我们会聊PID参数整定的实战技巧——怎么用Ziegler-Nichols法快速找到一组能用的参数,以及怎么在实车上微调。