菜单
首页/博客/PID 控制器 - 1

PID 控制器 - 1

PID,比例-微分-积分控制,是大部分控制人的第一课。

从 PID 开始,“电控” 不再只是电子电路、寄存器和代码,而是建立在此基础上的控制,成为真正的电控。

通过 PID 控制算法,你可以很容易的控制电机旋转、平衡车等许多以前难以控制的系统。

控制是什么

控制就是通过某种方法,让系统达到目标状态。

系统和状态

如果我们控制一个平衡车,就可以把它叫做一个系统,它是我们控制的对象。

系统本质就是一个函数:输入一个量,然后得到下一刻状态

$$ \displaylines{ u = \begin{pmatrix} 电机力矩 \end{pmatrix} \\ x = \begin{pmatrix} 倾角 \\ 倾角角速度 \\ 位移 \\ 速度 \end{pmatrix} \\ x_t, u_t \overset{f}{\rightarrow}x_{t+1} } $$

实际状态转移方程,应该写为$\dot x = f(x,u)$,不过为了方便可以理解为这种时间的递推关系

u 叫控制量,x 叫状态变量,f 就是这个平衡车系统。

通过力学和运动学建模,函数 f 是可以写出来的,但大多数时候它都相当复杂。

开环和闭环

通过某种规律直接控制,而不去检查到底有没有达到目标,这是开环控制:

graph LR
input((输入)) --目标值(target)--> controller[控制器]
controller --控制量(u)--> actuator[执行器]
actuator --力、力矩等--> system[被控制的系统]
system --> y((系统状态))

检查了当前状态和目标的差距,再根据这个差距去控制,这是闭环控制:

graph LR
input((输入)) --目标值(target)--> controller[控制器]
controller --控制量(u)--> actuator[执行器]
actuator --力、力矩等--> system[被控制的系统]
system --> y((系统状态))
system --> sensor[传感器]
sensor --测量值(measrue)--> controller

开环大部分情况都不一定能达到目标,而闭环通过了解还差多少,可以更逼近目标。

PID 是闭环控制。

graph LR
A[设定值] -->|输入| E(误差 e)
E --> P[比例 P]
E --> I[积分 I]
E --> D[微分 D]
P --> U[输出 u]
I --> U
D --> U
U --> C[被控对象]
C --> F[传感器]
F -->|反馈| E

PID 公式

PID 最原始的公式长这样:

目标值和测量值的误差为 e (error):

$$ \displaylines{ e = x_{target} - x_{measure} } $$

对于控制量 u,设 $T_i$ 是积分时间,$T_d$ 是微分时间,有

$$ \displaylines{ u = K_p ( e + \frac{1}{T_i} \int^t_0 e \ dt + T_d \frac{de}{dt} ) } $$

这个公式实在反人类,所以简化:

$$ \displaylines{ u = K_p \cdot e + K_i \cdot \int e + K_d \cdot \dot e } $$

$\dot e$ 即 e 的微分,这是一种常用的表示。

我们发现它分三项:

比例(Proportion),积分(Integration),微分(Differentiation),取首字母故曰 PID。

这三个比例就是我们需要手动调的参数。

为了理解 PID 我们一项一项的理解。

感性地理解 PID

假设光滑平面有一个小木块,目标是让它到某个位置(状态量),手段是给它施加一个力(控制量)。

令位移是 x,目标是 $x_0$,力 F = u,则 $e = x_0 - x$。

只有比例项(P 控制)

根据常识,我们会反方向拉木块,并且离 $x_0$ 越远越用力,即:

$$ \displaylines{ u = K_p \cdot e } $$

这时 $K_p$ 越大,反应越大。

但我们不幸的发现,这就是是胡克定律:控制器相当于弹簧,木块做简谐运动!

考虑阻力时,确实会逐渐停止,但不一定停得快,正常我们也不希望有摩擦。

弹簧阻尼:比例 + 微分(PD 控制)

于是手动加阻力,让输出减速.

微分(导数)就是速度,让阻力和速度成正比,于是:

$$ \displaylines{ u = K_p \cdot e + K_d \cdot \dot e } $$

微分项可以理解为根据误差变化率提前“刹车”,抑制超调与震荡(目标值恒定时,$\dot e = \dot x_0 - \dot x = -\dot x$,此时微分项与木块速度负相关)。这时的 PD 控制器就是有阻力的弹簧:震幅度减小,逐渐停在 $x_0$。

其实微分项也可以理解为:

$$ \displaylines{ K_d \cdot \dot e = -K_d \cdot \dot x } $$

即通过误差的变化率,间接对木块速度进行调节:x 偏离目标远时,位移项发力;接近目标时,误差变化率主导,让木块速度逐渐趋近于 0。

PD 控制器,在没有持续外力干扰时,大部分情况够用了。但如果加个外力呢?

静态误差:加积分(PID 控制器)

如果存在一个给物块的外力。

前面说 PD 控制即带阻力的弹簧,据高中物理,弹簧受恒外力,最终会保持一个形变:

这有点像小学水池一边加水、一边放水的问题?

为了衡量误差持续时间,其造成的影响,于是把 e 对时间积分:

$$ \displaylines{ u = K_p \cdot e + K_i \cdot \int e + K_d \cdot \dot e } $$

一直偏移,就一直积分,积分项就不断增大,最终抵消误差。

我们最终得到了完整的 PID 控制器。

PID 控制器的代码部署

下面给出一个简单的 PID 控制器,基于 C 语言。

首先实现微分和积分。上面的数学公式都是时间连续形式的,但我们的程序不是,一般会有一个循环周期(如1ms或2ms),也称作 PID 的控制周期,这称之为离散形式。这个过程叫做离散化。

积分的离散形式就是累加(或说求和),微分的离散形式就是差分(逐个相减):

$$ \displaylines{ \int_0^t e dt= \sum^t e_i \Delta T \\ \dot e_t = \frac{e_t - e_{t-1}}{\Delta T} } $$

其中 $\Delta T$ 就是循环周期,$e_t$ 这样的变量就是指 e 在第 t 时刻的值。

我们写成 c 代码:

int main(){
    
float int_e=0,  // 积分
      diff_e=0; // 微分
float e,      // 误差
      prev_e; // 上次的误差(previous error)
const float dt = 0.001; // 控制周期为 1 ms = 0.001 s

// 进入控制循环
while(1){
    // 首先更新误差
    prev_e = e;                // 保存上一次循环时,计算的误差
    e      = target - measure; // 设目标值、测量值,他们可能来自其他地方
    
    // 然后计算积分和微分
    int_e  +=  e           * dt;
    diff_e  = (e - prev_e) / dt;
    
    // 循环结束,等待一个dt后进入下一次循环
    delay_ms(1);
}
    
}

这样我们就可以近似得到误差的积分、微分了。于是我们就可以计算PID了:

int main(){

float kp = 1, // 假设 pid 的三个参数  **新增**
      ki = 1,
      kd = 1;
float int_e=0,  // 积分
      diff_e=0; // 微分
float e,      // 误差
      prev_e; // 上次的误差(previous error)
float output; // 输出 **新增**
const float dt = 0.001; // 控制周期为 1 ms = 0.001 s

// 进入控制循环
while(1){
    // 首先更新误差
    prev_e = e;                // 保存上一次循环时,计算的误差
    e      = target - measure; // 假设目标值(可能来自遥控)、测量值(可能来自传感器)
    
    // 然后计算积分和微分
    int_e  +=  e           * dt;
    diff_e  = (e - prev_e) / dt;
    
    // 计算pid **新增**
    output =   kp * e
             + ki * int_e
             + kd * diff_e;
    
    // output 可能会发送给电机
    
    // 循环结束,等待一个dt后进入下一次循环
    delay_ms(1);
}
    
}

但是这样写太难看了,而且可能有多个pid,我们的代码没办法复用,所以我们写成一个函数来储存计算过程代码,用一个结构体来储存这些变量:

// ----------------------------- PID 定义 -------------------------------

struct PID_Data{
    float kp, ki, kd;
    float int_e, diff_e;
    float e, prev_e;
    float output;
    float dt; 
};

// 用来给结构体初始化,init是initial的缩写。
void PID_Init(struct PID_Data *self, float kp, float ki, float kd, float dt){
    self->kp = kp;
    self->ki = ki;
    self->kd = kd;
    self->int_e = 0;
    self->diff_e = 0;
    self->e = 0;
    self->prev_e = 0;
    self->output = 0;
    self->dt = dt;
    
    // Q:为什么要给其他值清零?
    // A:如果你把结构体变量,定义在全局,默认都会赋0;但如果你
    //    在局部定义变量,结构体中各个值不会清零。这会造成严重
    //    后果。例如int_e积分初始若为999999,输出值会爆炸的!
}

float PID_Updata(struct PID_Data *self, float target, float measure){
    // 更新误差
    self->prev_e = self->e;
    self->e      = target - measure;
    
    // 计算积分、微分
    self->int_e  +=  self->e                 * self->dt;
    self->diff_e  = (self->e - self->prev_e) / self->dt;
    
    // 计算pid
    self->output =   self->kp * self->e
                   + self->ki * self->int_e
                   + self->kd * self->diff_e;
    
    return self->output;
}

// ----------------------------- 使用 -------------------------------
// 只是demo代码,展示怎么使用上面的PID定义

struct PID_Data yaw_motor_pid;
struct PID_Data pitch_motor_pid;

int main(){
    // 初始化pid
    PID_Init(&pitch_motor_pid, 10, 0.1, 2, 0.001);
    PID_Init(&yaw_motor_pid,   20, 0  , 5, 0.001);
    
    // 控制循环
    while(1){
        float pitch_output;
        float yaw_output;
        
        // 控制
        pitch_output = PID_Updata(&pitch_motor_pid, 0, imu.pitch_angle);
        yaw_output   = PID_Updata(&yaw_motor_pid,   0, imu.yaw_angle);
        
        // 输出
        Motor_Send_Message(&pitch_motor, pitch_output);
        Motor_Send_Message(&yaw_motor,   yaw_output);
        
        // 周期1ms
        delay_ms(1);
    }
}

PID 的拓展

PID的局限性:

  1. 在我以上的代码中,我没有对输出值限幅,这可能使电机超出额定工作能力,导致损毁。同时,我也没有对积分限幅,这可能导致超调。
  2. PID本身是对线性系统的控制,例如我例子中的物块。如果是平衡车,他的运动就没那么简单了,这可能导致控制效果变差。

这样看来,PID控制器本身局限性很大。但因为他简单易调可操作,依旧是运用最广泛的控制器。而为了弥补局限性,让PID在不同的地方依旧能够有较好的效果,人们已经在PID上有了大量的拓展:

在PID外,我们也常常需要用一些补偿,让PID效果更好,即前馈,例如对阻力、重力补偿等等。他们可以让PID更接近线性系统,减少稳态误差。(如果能够用前馈,就最好不要用PID的积分,积分必然导致延迟)。