经典算法:第二篇 自动控制理论之PID控制算法

本文最后更新于:1 个月前

在自动控制理论中,最负盛名的莫过于PID控制算法了。原理简单,应用广泛,受到很多的工程师们的喜爱。在稚晖君最新的视频BV1jE41137eu中,他设计的’自行车’XUAN的自行车车头无刷电机的控制算法就是PID控制算法。

前言

在过程控制中,按偏差的比例(P)、积分(I)和微分(D)进行控制的PID控制器(亦称PID调节器)是应用最为广泛的一种自动控制器。它具有原理简单,易于实现,适用面广,控制参数相互独立,参数的选定比较简单等优点;而且在理论上可以证明,对于过程控制的典型对象──“一阶滞后+纯滞后”与“二阶滞后+纯滞后”的控制对象,PID控制器是一种最优控制。PID调节规律是连续系统动态品质校正的一种有效方法,它的参数整定方式简便,结构改变灵活(PI、PD、…)。

在工业过程中,连续控制系统的理想PID控制规律为:

$$u(t)=K_p(e(t)+\frac{1}{T_t}\int_0^te(t)dt+T_D\frac{de(t)}{dt})$$

式中,Kp——比例增益,Kp与比例度成倒数关系;
Tt——积分时间常数;
TD——微分时间常数;
u(t)——PID控制器的输出信号;
e(t)——给定值r(t)与测量值之差。

反馈调节

在自动控制原理中,引入反馈调节的主要目的就是为了使得系统更加稳定,增加系统的抗干扰能力,减小稳态误差。

为了能够更好地描述整个系统的各个量之间的关系,像神经网络一样,我们将PID系统看做是一个黑盒模型,将当前输入设为rin(t),输出为rout(t),那么系统误差就是err(t)=rin(t)-rout(t)。

像在稚晖君的”自行车”项目中,把自行车的水平偏移量作为输出rout(t),而无刷电机的输出电流设为rin(t),根据算法竟可能的减小err(t)的值就可以了,这其实并不是一个表面上看起来那么简单的过程。我们来模拟一下这个看似简单的车头控制过程。

比例增益调节(P)

要实现车头转动一定的角度,那么我们首先想到的就是将目标角度与输出角度的差值err(t)按照一定的比例系数$K_p$进行直接调节。

缺一张流图,下次再画。

根据上图,我们可以知道系统的传递函数为

$$G(s)=\frac{1}{Ts+1}$$

那么该系统的阶跃响应就是

$$e_{ss}=\lim_{s\to0}s\frac{\frac{1}{s}}{1+KG(s)}=\lim_{s\to0}\frac{1}{1+\frac{K}{Ts+1}}=\frac{1}{1+K}$$

由上式可知误差值并不会随着$K_p$的取值而变成0,始终会存在一个小的误差,没有办法达到理想的角度。理论上只有当K足够大时,才能够接近0。当然,如果$K_p$的值过大,导致车头的摆动幅度太大的话,会引入一个惯性的动作,使得车头的位置偏移过大,超过我们所需要的目标角度,这个时候就需要一个反向的输出来掰回车头,如此往复,最终我们看到的结果就是车头在需要的目标角度附近来回摆动。

积分控制(I)

$$G(s)=\frac{G_0(s)}{s^k}$$
$$e_{ss}=\lim_{s\to0}\frac{1}{s^k+s^kK\frac{G_0(s)}{s^k}}=\lim_{s\to0}\frac{1}{KG(0)}=c$$

通过观察上式可知,如果我们要得到一个稳定的0误差的输出,只需要引入一个$\frac{1}{s^k}$的积分器,就可以使得系统消除自身产生的稳态误差。

微分控制(D)

前面我们说到,当我们的$K_p$过大的时候,就会出现摆动幅度过大,使得系统产生自激振荡,为了消除这种现象,我们就需要判断,现在的转动角度的速度是否过快,或者说,角度对于时间t的差分值是不是太大了,如果我们输出渐渐接近输入,误差会不断减小,为一个正数,误差的导数为负数 。转动角度任然以$K_p$比例在接近目标角度,但是此时的微分控制器会产生一个相反的输出来抑制$K_p$的作用,相当于一种“制动”作用,防止输出变化过快而超过目标值,即防止超调量过大。如果输出超过了参考信号,那么误差就会变号,比例控制的组成部分是负数,控制量反向以再次接近目标,此时误差的导数变为正数,微分控制使得控制量能够在绝对值上得到削减。

代码实现

虽然从PID为什么能够稳定的控制目标的输出上看起来长篇大论,但是,只要用代码实现前面那个PID的公式就可以了。

位置式PID算法来自CSDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import time

class PID:
def __init__(self, P=0.2, I=0.0, D=0.0):
self.Kp = P
self.Ki = I
self.Kd = D
self.sample_time = 0.00
self.current_time = time.time()
self.last_time = self.current_time
self.clear()

def clear(self):
self.SetPoint = 0.0
self.PTerm = 0.0
self.ITerm = 0.0
self.DTerm = 0.0
self.last_error = 0.0
self.int_error = 0.0
self.windup_guard = 20.0
self.output = 0.0

def update(self, feedback_value):
error = self.SetPoint - feedback_value
self.current_time = time.time()
delta_time = self.current_time - self.last_time
delta_error = error - self.last_error
if (delta_time >= self.sample_time):
self.PTerm = self.Kp * error#比例
self.ITerm += error * delta_time#积分
if (self.ITerm < -self.windup_guard):
self.ITerm = -self.windup_guard
elif (self.ITerm > self.windup_guard):
self.ITerm = self.windup_guard
self.DTerm = 0.0
if delta_time > 0:
self.DTerm = delta_error / delta_time
self.last_time = self.current_time
self.last_error = error
self.output = self.PTerm + (self.Ki * self.ITerm) + (self.Kd * self.DTerm)

def setKp(self, proportional_gain):
self.Kp = proportional_gain

def setKi(self, integral_gain):
self.Ki = integral_gain

def setKd(self, derivative_gain):
self.Kd = derivative_gain

def setWindup(self, windup):
self.windup_guard = windup

def setSampleTime(self, sample_time):
self.sample_time = sample_time

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import PID
import time
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import make_interp_spline
#这个程序的实质就是在前九秒保持零输出,在后面的操作中在传递函数为某某的系统中输出1

def test_pid(P = 0.2, I = 0.0, D= 0.0, L=100):

pid = PID.PID(P, I, D)

pid.SetPoint=0.0
pid.setSampleTime(0.01)

END = L
feedback = 0

feedback_list = []
time_list = []
setpoint_list = []

for i in range(1, END):
pid.update(feedback)
output = pid.output
if pid.SetPoint > 0:
feedback +=output# (output - (1/i))控制系统的函数
if i>9:
pid.SetPoint = 1
time.sleep(0.01)

feedback_list.append(feedback)
setpoint_list.append(pid.SetPoint)
time_list.append(i)

time_sm = np.array(time_list)
time_smooth = np.linspace(time_sm.min(), time_sm.max(), 300)
feedback_smooth = make_interp_spline(time_list, feedback_list)(time_smooth)
plt.figure(0)
plt.plot(time_smooth, feedback_smooth)
plt.plot(time_list, setpoint_list)
plt.xlim((0, L))
plt.ylim((min(feedback_list)-0.5, max(feedback_list)+0.5))
plt.xlabel('time (s)')
plt.ylabel('PID (PV)')
plt.title('TEST PID')

plt.ylim((1-0.5, 1+0.5))

plt.grid(True)
plt.show()

if __name__ == "__main__":
test_pid(1.2, 1, 0.001, L=80)
# test_pid(0.8, L=50)

输出结果:
Figure_0-2021-06-20

总结

上面的PID运行原理我理解的或者描述可能不太准确,但是公式和代码是没问题的。如果想要更详细的视频解析,可以在哔哩哔哩搜一下PID控制。

这上面的一大段只是稚晖君的自行车算法中的一小部分,太强了,菜鸡默默地滚去学习。