【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);
	}
}
相关推荐
西岸行者2 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意2 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码2 天前
嵌入式学习路线
学习
毛小茛2 天前
计算机系统概论——校验码
学习
babe小鑫2 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms2 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下2 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。2 天前
2026.2.25监控学习
学习
im_AMBER2 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J2 天前
从“Hello World“ 开始 C++
c语言·c++·学习