目录
一、多环PID简介
- 单环PID只能对被控对象的一个物理量进行闭环控制,而当用户需要对被控对象的多个维度物理量(例如:速度、位置、角度等)进行控制时,则需要多个PID控制环路,即多环PID,多个PID串级连接,因此也称作串级PID
- 多环PID相较于单环PID,功能上,可以实现对更多物理量的控制,性能上,可以使系统拥有更高的准确性、稳定性和响应速度
二、单环PID与多环PID工作流程对比
1.单环PID定位置
若使用PD控制器
- 当目标位置大于实际位置时,误差值作为正值输出一个正向PWM驱动电机正转以减小误差
- 当目标位置小于实际位置时,误差值作为负值输出一个反向PWM驱动电机反转以减小误差
- 当误差很小时输出的PWM可能无法驱动电机旋转(可通过加积分项进行改进)
- 当受到外界比较小的干扰时对抗变化的力也会比较小(可通过设置输入死区进行改进)
2.多环PID定位置
若内环定速度使用PI控制器,外环定位置使用PD控制器
仅看内环,由于使用PI控制器,拥有积分项,最终不存在稳态误差
仅看外环,被控部分可看做一个自带积分的速度闭环控制电机。外环的输出即为内环的输入,最终能达到线性控制PWM的效果。
当目标位置大于实际位置时,外环误差值为正值,输出速度为正值,进而内环目标速度也为正值。若目标速度大于实际速度,则内环误差为正值,输出正向PWM驱动电机正向旋转以减小误差
当目标位置小于实际位置时,外环误差值为负值,输出速度为负值,进而内环目标速度也为正值。目标速度一定小于实际速度,则内环误差为负值,输出反向PWM驱动电机反向旋转以减小误差
当目标位置与实际位置误差很小时,外环会输出速度也很小,进而内环输出速度也很小,但由于内环是PI控制器具有积分作用,最终能够控制目标位置和实际位置完全重合
当受到外界比较小的干扰时,实际位置与目标位置产生了偏离,此时外环要产生调控力并进一步让内环做出调控,同时因为干扰带来的位置变化时必然会产生速度,此时内环自身也会产生调控力。即内环会产生两份调控力,比单环的调控力度更大。
三、以双环PID为例,编写伪代码
- 本质是使用两个PID控制器
- 内外环的调控周期可以不同,因此可以在定时器中使用count为其设置不同的调控周期
- 外环的目标值由用户给出,外环的输出作用于内环的输入
- 内环的目标值由外环的输出值得到,内环的输出作用于受控对象
- 由于控制关系是外环->内环->受控对象,所以外环调控周期≥内环调控周期
- 如果想要实现内环以指定速度到达外环的指定位置的话,不能修改Inner_Target,而应修改内环速度的限幅值
/****************************** 定义内环变量 ******************************/
float Inner_Target, Inner_Actual, Inner_Out; //目标值,实际值,输出值
float Inner_Kp = 值;
float Inner_Kd = 值;
float Inner_Ki = 值;
float Inner_Current_Error; //当前误差
float Inner_Last_Error; //上次误差
float Inner_Sum_Error; //误差积分
/****************************** 定义外环变量 ******************************/
float Outer_Target, Outer_Actual, Outer_Out; //目标值,实际值,输出值
float Outer_Kp = 值;
float Outer_Kd = 值;
float Outer_Ki = 值;
float Outer_Current_Error; //当前误差
float Outer_Last_Error; //上次误差
float Outer_Sum_Error; //误差积分
void main()
{
Timer_Init(); //定时器初始化
while()
{
/* 用户在此处根据需求写入外环PID控制器的目标值 */
/* 内环的目标值由外环PID调控后得到 */
Outer_Target = 用户设置的目标值;
}
}
void TIM2_IRQHandle(void)
{
/* 内环和外环可以有不同的调控周期 */
static uint16_t count1, count2;
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
/* 内外环调控周期变化 */
count1++;
count2++;
/************************* 内环PID调控 *************************/
if (count1 >= 内环调控时间)
{
count1 = 0;
/*********** 执行内环PID调控 ***********/
/* 获取内环实际值 */
Inner_Actual = 读取内环传感器获取实际值();
/* 获取上次和本次误差 */
Inner_Last_Error = Inner_Current_Error; //此时的Inner_Current_Error其实是上一次还没有更新的值
Inner_Current_Error = Inner_Target - Inner_Actual; //在这里才更新了Current_Error的值
/* 误差积分(累加) */
Inner_Sum_Error += Inner_Current_Error;
/* PID计算 */
Inner_Out = Inner_Kp * Inner_Current_Error +
Inner_Ki * Inner_Sum_Error +
Inner_Kd * (Inner_Current_Error - Inner_Last_Error);
/* 输出限幅,由受控对象确定范围 */
if (Inner_Out > 上限) {Inner_Out = 上限;}
if (Inner_Out < 下限) {Inner_Out = 下限;}
/* 内环输出值作用于执行器 */
输出至被控制对象函数(Inner_Out);
/********************************/
}
/**************************************************************/
/************************* 外环PID调控 *************************/
if (count2 >= 外环调控时间)
{
count2 = 0;
/*********** 执行外环PID调控 ***********/
/* 获取外环实际值 */
Outer_Actual = 读取外环传感器获取实际值();
/* 获取上次和本次误差 */
Outer_Last_Error = Outer_Current_Error; //此时的Outer_Current_Error其实是上一次还没有更新的值
Outer_Current_Error = Outer_Target - Outer_Actual; //在这里才更新了Current_Error的值
/* 误差积分(累加) */
Outer_Sum_Error += Outer_Current_Error;
/* PID计算 */
Outer_Out = Outer_Kp * Outer_Current_Error +
Outer_Ki * Outer_Sum_Error +
Outer_Kd * (Outer_Current_Error - Outer_Last_Error);
/* 输出限幅,由内环输入范围确定范围 */
if (Outer_Out > 上限) {Outer_Out = 上限;}
if (Outer_Out < 下限) {Outer_Out = 下限;}
/* 外环PID输出值作为内环PID输入值 */
Inner_Target = Outer_Out;
/********************************/
}
/**************************************************************/
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
四、程序优化
- 使用结构体
- 将PID计算封装成函数
1.PID.h
#ifndef __PID_H
#define __PID_H
typedef struct {
float Target;
float Actual;
float Out;
float Kp;
float Ki;
float Kd;
float Current_Error;
float Last_Error;
float Sum_Error;
float OutMax;
float OutMin;
} PID_t;
void PID_Update(PID_t *p);
#endif
2.PID.c
#include "stm32f10x.h" // Device header
#include "PID.h"
/**
* 函 数:PID计算及结构体变量值更新
* 参 数:PID_t * 指定结构体的地址
* 返 回 值:无
*/
void PID_Update(PID_t *p)
{
/*获取本次误差和上次误差*/
p->Last_Error = p->Current_Error; //获取上次误差
p->Current_Error = p->Target - p->Actual; //获取本次误差,目标值减实际值,即为误差值
/*外环误差积分(累加)*/
/*如果Ki不为0,才进行误差积分,这样做的目的是便于调试*/
/*因为在调试时,我们可能先把Ki设置为0,这时积分项无作用,误差消除不了,误差积分会积累到很大的值*/
/*后续一旦Ki不为0,那么因为误差积分已经积累到很大的值了,这就导致积分项疯狂输出,不利于调试*/
if (p->Ki != 0) //如果Ki不为0
{
p->Sum_Error += p->Current_Error; //进行误差积分
}
else //否则
{
p->Sum_Error = 0; //误差积分直接归0
}
/*PID计算*/
/*使用位置式PID公式,计算得到输出值*/
p->Out = p->Kp * p->Current_Error
+ p->Ki * p->Sum_Error
+ p->Kd * (p->Current_Error - p->Last_Error);
/*输出限幅*/
if (p->Out > p->OutMax) {p->Out = p->OutMax;} //限制输出值最大为结构体指定的OutMax
if (p->Out < p->OutMin) {p->Out = p->OutMin;} //限制输出值最小为结构体指定的OutMin
}
3.main.c
/*定义PID结构体变量*/
PID_t Inner = { //内环PID结构体变量,定义的时候同时给部分成员赋初值
.Kp = 0.3, //比例项权重
.Ki = 0.3, //积分项权重
.Kd = 0, //微分项权重
.OutMax = 100, //输出限幅的最大值
.OutMin = -100, //输出限幅的最小值
};
PID_t Outer = { //外环PID结构体变量,定义的时候同时给部分成员赋初值
.Kp = 0.3, //比例项权重
.Ki = 0, //积分项权重
.Kd = 0.4, //微分项权重
.OutMax = 20, //输出限幅的最大值
.OutMin = -20, //输出限幅的最小值
};
void TIM1_UP_IRQHandler(void)
{
/*定义静态变量(默认初值为0,函数退出后保留值和存储空间)*/
static uint16_t Count1, Count2; //分别用于内环和外环的计次分频
if (TIM_GetITStatus(TIM1, TIM_IT_Update) == SET)
{
/*每隔1ms,程序执行到这里一次*/
Key_Tick(); //调用按键的Tick函数
/*内环计次分频*/
Count1 ++; //计次自增
if (Count1 >= 40) //如果计次40次,则if成立,即if每隔40ms进一次
{
Count1 = 0; //计次清零,便于下次计次
/*获取实际速度值和实际位置值*/
/*Encoder_Get函数,可以获取两次读取编码器的计次值增量*/
/*此值正比于速度,所以可以表示速度,但它的单位并不是速度的标准单位*/
/*此处每隔40ms获取一次计次值增量,电机旋转一周的计次值增量约为408*/
/*因此如果想转换为标准单位,比如转/秒*/
/*则可将此句代码改成Speed = Encoder_Get() / 408.0 / 0.04;*/
Speed = Encoder_Get(); //获取编码器增量,得到实际速度
Location += Speed; //实际速度累加,得到实际位置
/*以下进行内环PID控制*/
/*内环获取实际值*/
Inner.Actual = Speed; //内环为速度环,实际值为速度值
/*PID计算及结构体变量值更新*/
PID_Update(&Inner); //调用封装好的函数,一步完成PID计算和更新
/*内环执行控制*/
/*内环输出值给到电机PWM*/
Motor_SetPWM(Inner.Out);
}
/*外环计次分频*/
Count2 ++; //计次自增
if (Count2 >= 40) //如果计次40次,则if成立,即if每隔40ms进一次
{
Count2 = 0; //计次清零,便于下次计次
/*以下进行外环PID控制*/
/*外环获取实际值*/
Outer.Actual = Location; //外环为位置环,实际值为位置值
/*PID计算及结构体变量值更新*/
PID_Update(&Outer); //调用封装好的函数,一步完成PID计算和更新
/*外环执行控制*/
/*外环的输出值作用于内环的目标值,组成串级PID结构*/
Inner.Target = Outer.Out;
}
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
}
}


