【PID学习】多环PID

目录

一、多环PID简介

二、单环PID与多环PID工作流程对比

1.单环PID定位置

2.多环PID定位置

三、以双环PID为例,编写伪代码

四、程序优化

1.PID.h

2.PID.c

3.main.c


一、多环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);
	}
}
相关推荐
testpassportcn2 小时前
CompTIA A+ 220-1201 認證介紹|CompTIA A+ Core 1 考試內容、題型與高效備考指南
网络·学习·改行学it
2501_944934732 小时前
数据洞察力:职业转型的核心竞争力
学习
AI视觉网奇2 小时前
ue5 默认相机设置
笔记·学习·ue5
山土成旧客2 小时前
【Python学习打卡-Day44】站在巨人的肩膀上:玩转PyTorch预训练模型与迁移学习
pytorch·python·学习
星河天欲瞩2 小时前
【深度学习Day1】环境配置(CUDA、PyTorch)
人工智能·pytorch·python·深度学习·学习·机器学习·conda
木木木一2 小时前
Rust学习记录--C12 实例:写一个命令行程序
学习·算法·rust
DBBH2 小时前
DBBH的AI学习笔记
人工智能·笔记·学习
青衫码上行2 小时前
如何构建maven项目
java·学习·maven
June bug2 小时前
【实习笔记】正交实验法设计测试用例
笔记·学习·测试用例