结合代码理解PID控制器

什么是PID控制器

PID控制器(比例-积分-微分控制器),由比例单元(P)、积分单元(I)和微分单元(D)组成。可以通过调整这三个单元的增益$k_p$, $k_i$和$k_d$来调整其特性。PID控制器适用于基本上线性,且动态特性不随时间变化的系统。

PID控制器是一个在工业控制应用中常见的反馈回路部件。这个控制器把收集到的数据和一个参考值进行比较,然后把这个差别用于计算新的输入值,这个新的输入值的目的是可以让系统的数据达到或者保持在参考值。PID控制器可以根据历史数据和差别的出现率来调整输入值,使系统更加准确而稳定。

PID控制器的比例单元(P)、积分单元(I)和微分单元(D)分别对应目前误差、过去累计误差及未来误差。若是不知道受控系统的特性,一般认为PID控制器是最适用的控制器。借由调整PID控制器的三个参数,可以调整控制系统,设法满足设计需求。控制器的响应可以用控制器对误差的反应快慢、控制器过冲的程度及系统震荡的程度来表示。不过使用PID控制器不一定保证可达到系统的最佳控制,也不保证系统稳定性。

有些应用只需要PID控制器的部分单元,可以将不需要单元的参数设为零即可。因此PID控制器可以变成PI控制器、PD控制器、P控制器或I控制器。其中又以PI控制器比较常用,因为D控制器对回授噪声十分敏感,而若没有I控制器的话,系统不会回到参考值,会存在一个误差量。

水箱模型1.0 - P

受控模型

以一个简单的水箱模型为例,假设有一个水箱,要从空箱开始注水到某个液位,可以通过控制水龙头的大小来控制注水的速度,用下面的代码来模拟这个过程。

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
"""model.py: 定义了PID控制的对象模型类 - Model"""

class Model(object):
"""接受PID控制的模型类(水箱)。

Attributes:
target: 经过控制,最终期望达到的目标值(目标液位)。
curr: 当前达到的值(当前液位)
"""

def __init__(self, target=100):
"""初始化"""

self.target = target
self.curr = 0

def get_leavings(self):
"""获取偏差值(剩余容量)

Return:
目标值与当前值之差
"""

return self.target - self.curr

def click(self, val):
"""模型进行一次跳动(单位时间注水)

Paraments:
val: 本次跳动中变化的值(注水量)
"""

self.curr += val
pass

控制器

对于这样的模型,我们只需要通过比例单元来进行控制就可以快速达到期望的液位。数学公式描述如下:
$$
u = k_pe
$$
下面的代码实现了一个控制器的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""controller.py: 定义了一个PID控制器 - Controller"""

class Controller(object):
"""PID控制器类

Attributes:
kp: 比例参数
"""

def __init__(self, kp = 0.5):
"""初始化"""
self.kp = kp
pass

def click(self, m):
"""跳动一次,通过PID计算本次应该变化的量

Paraments:
m: 接受控制的模型对象
"""
e = m.get_e()
u = self.kp * e
m.click(u)
pass

控制效果

使用下面的代码可以显示$k_p\in(0, 1)$上几个特定值下的控制效果。

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
from model import Model
from controller import Controller
import matplotlib.pyplot as plt

DATA_SIZE = 10 # 绘制曲线的数量
END_TIME = 50 # 时间轴终点

STATIC_KP = False

if __name__ == "__main__":
plt.figure(1)

for i in range(1, DATA_SIZE):
m = Model(100)
kp = 0.5 if(STATIC_KP) else 1 / DATA_SIZE * i
con = Controller(kp)
lst = []
for t in range(0, END_TIME+1):
con.click(m)
lst.append(m.curr)

label_str = 'kp=%.1f' % (kp)
plt.plot(range(0, END_TIME+1), lst, label=label_str)

plt.legend(loc='best')
plt.show()

不同比例参数$k_p$下的控制曲线如下:

可以看出,随着比例系数$k_p$的增加,达到期望液位的速度就越快。

水箱模型2.0 - PI

在上面的水箱模型中,我们只用了一个比例单元就完成了对液位的控制,现在假设水箱模型有了新的变化,现在,在往水箱注水的同时,水箱也会作为水源向外供水,这里设定水箱向外供水的速度恒定为每个单位时间减5,水箱模型的代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 省略前面的代码

def click(self, u):
"""模型进行一次跳动

Paraments:
val: 本次跳动中变化的值
"""

self.curr += u - 5
pass

# 省略后面的代码