pid
2025-09-22 22:25:25

pid通俗解释

单级PID

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
57
//首先定义PID结构体用于存放一个PID的数据
typedef struct
{
float kp, ki, kd; //三个系数
float error, lastError; //误差、上次误差
float integral, maxIntegral; //积分、积分限幅
float output, maxOutput; //输出、输出限幅
}PID;

//用于初始化pid参数的函数
void PID_Init(PID *pid, float p, float i, float d, float maxI, float maxOut)
{
pid->kp = p;
pid->ki = i;
pid->kd = d;
pid->maxIntegral = maxI;
pid->maxOutput = maxOut;
}

//进行一次pid计算
//参数为(pid结构体,目标值,反馈值),计算结果放在pid结构体的output成员中
void PID_Calc(PID *pid, float reference, float feedback)
{
//更新数据
pid->lastError = pid->error; //将旧error存起来
pid->error = reference - feedback; //计算新error
//计算微分
float dout = (pid->error - pid->lastError) * pid->kd;
//计算比例
float pout = pid->error * pid->kp;
//计算积分
pid->integral += pid->error * pid->ki;
//积分限幅
if(pid->integral > pid->maxIntegral) pid->integral = pid->maxIntegral;
else if(pid->integral < -pid->maxIntegral) pid->integral = -pid->maxIntegral;
//计算输出
pid->output = pout+dout + pid->integral;
//输出限幅
if(pid->output > pid->maxOutput) pid->output = pid->maxOutput;
else if(pid->output < -pid->maxOutput) pid->output = -pid->maxOutput;
}

PID mypid = {0}; //创建一个PID结构体变量

int main()
{
//...这里有些其他初始化代码
PID_Init(&mypid, 10, 1, 5, 800, 1000); //初始化PID参数
while(1)//进入循环运行
{
float feedbackValue = ...; //这里获取到被控对象的反馈值
float targetValue = ...; //这里获取到目标值
PID_Calc(&mypid, targetValue, feedbackValue); //进行PID计算,结果在output成员变量中
设定执行器输出大小(mypid.output);
delay(10); //等待一定时间再开始下一次循环
}
}

单环PID

PID三个环节作用

  • 比例:起主要控制作用,使反馈量向目标值靠拢,但可能导致振荡
  • 积分:消除稳态误差,但会增加超调量
  • 微分:产生阻尼效果,抑制振荡和超调,但会降低响应速度

例子

对电机转速进行控制
可用条件:已知电机的实时转速,并且可控制电机中流过的电流大小
PID目标值:需要电机达到的转速
PID反馈值:电机的实时转速
PID输出值:电机中流过的电流大小
分析:电机中流过的电流大小近似正比于电机的扭矩,也就近似正比于电机角加速度的大小,是转速的低阶物理量,因此可以用电流大小作为输出值

串级PID

单级PID目标值和反馈值经过一次PID计算就得到输出值并直接作为控制量,但如果目标物理量和输出物理量之间不止差了一阶的话,中间阶次的物理量我们是无法控制的。比如:目标物理量是位置,输出物理量是加速度,则小球的速度是无法控制的。

而串级PID就可以改善这一点。串级PID其实就是两个单级PID“串”在一起组成的,它的信号框图如下:

串级PID

外环和内环就分别是一个单级PID,每个单级PID就如我们之前所说,需要获取一个目标值和一个反馈值,然后产生一个输出值。串级PID中两个环相“串”的方式就是将外环的输出作为内环的目标值

案例

可用条件:小球实时位置、小球实时速度、施加在小球上的控制力
目标值:小球目标位置
外环反馈:小球实时位置
内环反馈:小球实时速度
输出值:施加在小球上的控制力

小球

内环与小球构成了一个恒速系统,PID内环负责小球的速度控制;而如果把内环和小球看作一个整体被控对象,外环又与这个对象一起构成了一个位置控制系统,外环负责位置控制;总体来说,外环负责根据小球位置误差计算出小球需要达到的速度,而内环负责计算出控制力使小球达到这个目标速度,两个环协同工作,就可以完成任务了。

串级PID的内环一般负责低阶物理量(通常是变化更快、更基础的物理量)的调节,而外环负责高阶物理量(通常是变化较慢、更关注的最终目标量、高阶量的导数或相关量)的调节并计算出低阶物理量的目标值。

任务:对电机进行串级角度控制
可用条件:电机实时角度、电机实时转速、可以控制电机电流大小
外环目标值:需要电机达到的角度
外环反馈值:电机的实时角度
内环反馈值:电机的实时速度
输出值:电机电流大小
分析:外环负责电机角度控制,根据电机目标角度和反馈角度计算出目标转速;内环负责转速控制,根据速度反馈和目标转速计算出电流

代码实现

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
//此处需要插入上面的单级PID相关代码

//串级PID的结构体,包含两个单级PID
typedef struct
{
PID inner; //内环
PID outer; //外环
float output; //串级输出,等于inner.output
}CascadePID;

//串级PID的计算函数
//参数(PID结构体,外环目标值,外环反馈值,内环反馈值)
void PID_CascadeCalc(CascadePID *pid, float outerRef, float outerFdb, float innerFdb)
{
PID_Calc(&pid->outer, outerRef, outerFdb); //计算外环
PID_Calc(&pid->inner, pid->outer.output, innerFdb); //计算内环
pid->output = pid->inner.output; //内环输出就是串级PID的输出
}

CascadePID mypid = {0}; //创建串级PID结构体变量

int main()
{
//...其他初始化代码
PID_Init(&mypid.inner, 10, 0, 0, 0, 1000); //初始化内环参数
PID_Init(&mypid.outer, 5, 0, 5, 0, 100); //初始化外环参数
while(1) //进入循环运行
{
float outerTarget = ...; //获取外环目标值
float outerFeedback = ...; //获取外环反馈值
float innerFeedback = ...; //获取内环反馈值
PID_CascadeCalc(&mypid, outerTarget, outerFeedback, innerFeedback); //进行PID计算
设定执行机构输出大小(mypid.output);
delay(10); //延时一段时间
}
}

调参

需要先断开两环的连接,手动指定内环目标值,进行内环调参,当内环控制效果较好后再接上外环进行外环调参,具体的调参方法与单级PID相同。