第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;
    }
};
个人经验: 积分限幅的取值我一般设为执行器最大输出的80%。比如你的电机最大能输出100N·m,那积分限幅就设80。这样即使积分饱和,也不至于瞬间输出过大。

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
核心区别: 位置式PID输出的是绝对控制量,增量式输出的是相对变化量。在自动驾驶中,我建议转向控制用增量式(避免积分饱和导致方向盘打死),速度控制用位置式(需要精确的绝对速度值)。

1.4 两种PID的对比与选型建议

对比项 位置式PID 增量式PID
输出形式 绝对控制量 控制量增量
积分饱和 容易发生 天然避免
计算量 稍大(需积分累加) 稍小
适用场景 速度控制、温度控制 转向控制、步进电机
手动/自动切换 有冲击 无冲击

我个人习惯是:能用增量式就用增量式。为什么?因为增量式天然抗积分饱和,而且手动自动切换时没有冲击。我记得有一次在测试车上,从手动模式切到自动模式,位置式PID直接跳了一个大输出,车猛地一窜——还好系了安全带。

选型建议: 如果你的执行器是比例阀、电机驱动器这类需要绝对电压/电流信号的,用位置式。如果是步进电机、伺服驱动器这类接受脉冲增量的,用增量式。别搞反了,搞反了车会跟你闹脾气。

1.5 采样时间的选择——一个容易被忽视的问题

离散化PID里有个关键参数——采样时间Δt。选大了控制精度不够,选小了计算负担重。我一般遵循两个原则:

  • 原则一: 采样频率至少是系统带宽的10倍。比如你的车辆横摆响应带宽是5Hz,那采样频率至少50Hz(Δt=20ms)。
  • 原则二: 采样时间要稳定。不要这次10ms,下次15ms,再下次8ms。这样微分项会剧烈波动。

我曾经在一个项目中,因为用了非实时操作系统,采样时间抖动很大。结果PID控制效果时好时坏,查了两天才发现是采样时间不均匀的问题。后来加了定时器中断,问题就解决了。

好了,这一章的内容就到这里。下一章我们会聊PID参数整定的实战技巧——怎么用Ziegler-Nichols法快速找到一组能用的参数,以及怎么在实车上微调。