1. 引言
在之前讲解的直流有刷电机驱动章节中,如果电机的负载没有改变,那么只需要设置固定的占空比,电机速度就会稳定在目标范围。然而,在实际的应用中,负载可能会发生变化,此时如果还是输出固定的电压,电机的速度就偏离目标范围了,为了解决这个问题,引入了PID算法,其作用是当系统受到外界干扰而偏离正常状态时,自动调节回正常范围内。常用于水壶保温系统、大棚温控系统、水位控制系统、电机控制系统等。接下来本章将介绍PID的基础知识,了解什么是PID及PID代码如何编写。
2. 什么是PID
PID就是Proportion(比例)、Integral(积分)、 Differential(微分),其控制流程图如下所示:

先来说比例,其公式为u=kp*e。u为输出、kp为比例系数、e为偏差。根据公式可看出输出u与偏差e成正比。以电机转动为例来说明,假设我期望输出的转速是60,但实际的转速是40,现在偏差是20,那此时kp越大,输出就越大,就能越快达到目标转速。但kp不是越大越好,如果kp越大,此时会出现超调或者振荡,会使得系统的稳定性下降。
然而仅有比例环节还不够,因为会存在静态误差。静态误差指的是假设现在需要调节棚内温度为30℃,而实际温度为25℃,此时偏差e=5,Kp为固定 值,如果此时的输出可以让大棚在半个小时之内升温5℃,而外部的温差可以让大棚在半个小 时之内降温 5℃,也就是说,输出 u 的作用刚好被外部影响抵消了,这就使得偏差会一直存在,这个误差就称为静态误差。
因此,为了消除静态误差,引入了积分环节。积分环节可以对偏差e进行积分,只要存在偏差,积分环节就会不断起作用,主要用于消除静态误差,提高系统的无差度。引入积分环节后,比例+积分环节的公式如下:

加入积分环节后,假设比例环节的作用被抵消,一直存在静态误差;那么此时积分环节就会一直累加误差,从而增大输出u,消除静态误差。从公式可以看出,当偏差e累加值或者是积分系数越大,此时输出u就越大,消除静态误差的时间就越短。
同理,积分系数过大,也会引起超调现象,因此积分系数不是越大越好。在积分环节中,只要存在偏差e,就会不断累加偏差值,直到偏差为0。此时累积偏差不再变化,但积分环节依然再发挥作用,因此易引起超调现象。为了解决上述问题,引入了微分环节提前去抑制输出。加入微分环节后公式如下所示:

从上述公式可以看出,微分环节是根据偏差变化量提前做出相应控制,以减小输出,抑制超调。如第n次的偏差减去n-1次的偏差=-3,代入上述公式可知,微分环节会削弱比例环节和积分环节的作用。
3. PID代码编写
知晓了公式原理之后,接下来进行代码编写。首先采用一个结构体来封装PID的相关参数,如下图所示:
cpp
typedef struct {
float kp; // 比例系数
float ki; // 积分系数
float kd; // 微分系数
float error; // 当前误差
float last_error; // 上次误差
float sum_error; // 误差累积
float output; // 输出值
float target; // 目标值
float actual; // 实际值
} pid_cfg;
PID输出代码如下:
cpp
float pid_caculate_value(pid_cfg *pid, float actual_value,flot target_value)
{
pid->target = target_value; //设置目标值
pid->actual = actual_value; //当前值
pid->error = target_value - actual_value; //偏差值
pid->sum_error += pid->error; //累积偏差值
pid->output= (pid->kp) * (pid->error) + (pid->ki) * (pid->sum_error) \
+(pid->kd) * ((pid->error)-(pid->last_error)); //pid输出值
// 限制积分项,防止积分饱和
if(pid->sum_error > 1000) pid->sum_error = 1000;
if(pid->sum_error < -1000) pid->sum_error = -1000;
// 限制输出范围
if(pid->output > 1000) pid->output = 1000;
if(pid->output < 0) pid->output = 0;
pid->last_error=pid->error; //更新偏差值
return pid->output; //返回输出值
}