四轴飞行器的设计之PID算法的分类与应用实例-STC15单片机实战指南连载
时间:10-02
整理:3721RD
点击:
上一节讲了PID算法概述(见帖子 http://bbs.elecfans.com/jishu_580161_1_1.html),继续讲PID算法的分类与应用实例。
将偏差的比例(Proportion)、积分(Integral)和微分(Differential)通过线性组合构成控制量,用这一控制量对被控对象进行控制,这样的控制器称PID控制器。PID按其控制量可分为:模拟PID控制和数字PID控制,其中数字PID控制算法又分为位置式PID和增量式PID控制算法,但是无论哪种分类,都大致符合如图所示的PID模型。
或许一看这个图,感觉好难懂,因为要涉及高数中的微积分运算,不用慌,接下来,我们以一个实例,并以计算量最小的增量式PID控制算法为例,来介绍PID。此时大家可能觉得PID不可能和C语言、单片机扯上关系,更不可能与温度控制系统扯上关系,要只能否扯上关系,请看接下来的实例应用。
在讲述实例之前,大家需要了解一下增量式PID的运算公式,这个表达式比较多,加之个人运算的习惯,更是层出不穷,这里以较为常用的为例,望读者掌握。
PID = Uk + KP*[E(k)-E(k-1)] + KI*E(k) + KD*[E(k)-2E(k-1)+E(k-2)];
在单片机中运用PID,处于速度和RAM的考虑,一般不用浮点数,这里以整型变量为例来讲述PID在单片机中的应用,等大家以后学了像STM32F4这样的M4核处理器以后,就可以考虑用浮点数来计算了。由于是用整型来做的,所以不是很精确,但是对于一般的场合来说,这个精度也够了,关于系数和温度笔者都统统放大了10倍,所以精度不是很高。但是也不是那么低,大部分的场合也是够了。实在觉得精度不够,可以再放大10倍或者100倍处理,但是要注意不能超出整个数据类型的范围就可以了。本程序包括PID计算和输出两部分,当偏差>10度全速加热,偏差在10度以内为PID计算输出,具体的参考代码参见下面(该实例以飛天一號开发板为硬件平台)。
难理解的加了注释,不难理解的,没必要说。剩下的就是将温度传感器部分的子程序综合进来,之后动手自己做实物,并调试程序,只有多实践,才是“玩”好单片机王道,因此强烈建议读者以FSST15开发板为平台,同时借助LM75A温度传感器,再搭建一个温控设备,亲自进行试验,在实际中不断提高自己的能力。
这里读者需要注意,前面讲述的口诀法和经验值法似乎没用到,那是因为PID算法除了增量式以外,还有两种:位置式和微分先行法,这些知识点,才是后面四轴控制的核心,笔者放后面再来讲述,感兴趣的读者可以先去看看下面链接的博文,写的甚好,通俗易懂。
/* http://blog.sina.com.cn/s/blog_7c7e2d5a01011ta9.html */
老外的位置式PID算法
该PID算法例程摘自网络,作者不详,版权归原创作者所有。为了保持地道,笔者没有做一点点改动,其中中文注释为笔者所加。平心而论,老外的代码确实很独特,思路也很清晰,源码如下,具体含义留读者慢慢研究了,这里不赘。
最后说明下和本文配套的STC15开发板目前正在电子发烧友销售,如果需要请戳这里购买: http://bbs.elecfans.com/product/stc15.html 下一节讲四轴飞行器的硬件模型建立。
将偏差的比例(Proportion)、积分(Integral)和微分(Differential)通过线性组合构成控制量,用这一控制量对被控对象进行控制,这样的控制器称PID控制器。PID按其控制量可分为:模拟PID控制和数字PID控制,其中数字PID控制算法又分为位置式PID和增量式PID控制算法,但是无论哪种分类,都大致符合如图所示的PID模型。

或许一看这个图,感觉好难懂,因为要涉及高数中的微积分运算,不用慌,接下来,我们以一个实例,并以计算量最小的增量式PID控制算法为例,来介绍PID。此时大家可能觉得PID不可能和C语言、单片机扯上关系,更不可能与温度控制系统扯上关系,要只能否扯上关系,请看接下来的实例应用。
在讲述实例之前,大家需要了解一下增量式PID的运算公式,这个表达式比较多,加之个人运算的习惯,更是层出不穷,这里以较为常用的为例,望读者掌握。
PID = Uk + KP*[E(k)-E(k-1)] + KI*E(k) + KD*[E(k)-2E(k-1)+E(k-2)];
在单片机中运用PID,处于速度和RAM的考虑,一般不用浮点数,这里以整型变量为例来讲述PID在单片机中的应用,等大家以后学了像STM32F4这样的M4核处理器以后,就可以考虑用浮点数来计算了。由于是用整型来做的,所以不是很精确,但是对于一般的场合来说,这个精度也够了,关于系数和温度笔者都统统放大了10倍,所以精度不是很高。但是也不是那么低,大部分的场合也是够了。实在觉得精度不够,可以再放大10倍或者100倍处理,但是要注意不能超出整个数据类型的范围就可以了。本程序包括PID计算和输出两部分,当偏差>10度全速加热,偏差在10度以内为PID计算输出,具体的参考代码参见下面(该实例以飛天一號开发板为硬件平台)。
- <font face="宋体" color="Black">#include <reg52.h>
- typedef unsigned char uChar8;
- typedef unsigned int uInt16;
- typedef unsigned long int uInt32;
- sbit ConOut = P1^1; //假如功率电阻接P1.1口
- typedef struct PID_Value
- {
- uInt32 liEkVal[3]; //差值保存,给定和反馈的差值
- uChar8 uEkFlag[3]; //符号,1则对应的为负数,0为对应的为正数
- uChar8 uKP_Coe; //比例系数
- uChar8 uKI_Coe; //积分常数
- uChar8 uKD_Coe; //微分常数
- uInt16 iPriVal; //上一时刻值
- uInt16 iSetVal; //设定值
- uInt16 iCurVal; //实际值
- } PID_ValueStr;
- PID_ValueStr PID; //定义一个结构体
- bit g_bPIDRunFlag = 0; //PID运行标志位
- /* ********************************************************
- /* 函数名称:PID_Operation()
- /* 函数功能:PID运算
- /* 入口参数:无(隐形输入,系数、设定值等)
- /* 出口参数:无(隐形输出,U(k))
- /* 函数说明:U(k)+KP*[E(k)-E(k-1)]+KI*E(k)+KD*[E(k)-2E(k-1)+E(k-2)]
- ******************************************************** */
- void PID_Operation(void)
- {
- uInt32 Temp[3] = {0}; //中间临时变量
- uInt32 PostSum = 0; //正数和
- uInt32 NegSum = 0; //负数和
- if(PID.iSetVal > PID.iCurVal) //设定值大于实际值否?
- {
- if(PID.iSetVal - PID.iCurVal > 10) //偏差大于10否?
- PID.iPriVal = 100; //偏差大于10为上限幅值输出(全速加热)
- else //否则慢慢来
- {
- Temp[0] = PID.iSetVal - PID.iCurVal; //偏差<=10,计算E(k)
- PID.uEkFlag[1] = 0; //E(k)为正数,因为设定值大于实际值
- /* 数值进行移位,注意顺序,否则会覆盖掉前面的数值 */
- PID.liEkVal[2] = PID.liEkVal[1];
- PID.liEkVal[1] = PID.liEkVal[0];
- PID.liEkVal[0] = Temp[0];
- /* ============================================================ */
- if(PID.liEkVal[0] > PID.liEkVal[1]) //E(k)>E(k-1)否?
- {
- Temp[0] = PID.liEkVal[0] - PID.liEkVal[1];
- //E(k)>E(k-1)
- PID.uEkFlag[0] = 0; //E(k)-E(k-1)为正数
- }
- else
- {
- Temp[0] = PID.liEkVal[1] - PID.liEkVal[0];
- //E(k)<E(k-1)
- PID.uEkFlag[0] = 1; //E(k)-E(k-1)为负数
- }
- /* =========================================================== */
- Temp[2] = PID.liEkVal[1] * 2; //2E(k-1)
- if((PID.liEkVal[0] + PID.liEkVal[2]) > Temp[2])
- //E(k-2)+E(k)>2E(k-1)否?
- {
- Temp[2] = (PID.liEkVal[0] + PID.liEkVal[2]) - Temp[2];
- PID.uEkFlag[2]=0; //E(k-2)+E(k)-2E(k-1)为正数
- }
- else //E(k-2)+E(k)<2E(k-1)
- {
- Temp[2] = Temp[2] - (PID.liEkVal[0] + PID.liEkVal[2]);
- PID.uEkFlag[2] = 1; //E(k-2)+E(k)-2E(k-1)为负数
- }
- /* =========================================================== */
- Temp[0] = (uInt32)PID.uKP_Coe * Temp[0];
- //KP*[E(k)-E(k-1)]
- Temp[1] = (uInt32)PID.uKI_Coe * PID.liEkVal[0]; //KI*E(k)
- Temp[2] = (uInt32)PID.uKD_Coe * Temp[2];
- //KD*[E(k-2)+E(k)-2E(k-1)]
- /* 以下部分代码是讲所有的正数项叠加,负数项叠加 */
- /* ========= 计算KP*[E(k)-E(k-1)]的值 ========= */
- if(PID.uEkFlag[0] == 0)
- PostSum += Temp[0]; //正数和
- else
- NegSum += Temp[0]; //负数和
- /* ========= 计算KI*E(k)的值 ========= */
- if(PID.uEkFlag[1] == 0)
- PostSum += Temp[1]; //正数和
- else
- ; /* 空操作
- 就是因为PID.iSetVal > PID.iCurVal(即E(K)>0)才进
- 入if的,那么就没可能为负,所以打个转回去就是了 */
- /* ======== 计算KD*[E(k-2)+E(k)-2E(k-1)]的值 ======== */
- if(PID.uEkFlag[2]==0)
- PostSum += Temp[2]; //正数和
- else
- NegSum += Temp[2]; //负数和
- /* ========= 计算U(k) ========= */
- PostSum += (uInt32)PID.iPriVal;
- if(PostSum > NegSum) //是否控制量为正数
- {
- Temp[0] = PostSum - NegSum;
- if(Temp[0] < 100 ) //小于上限幅值则为计算值输出
- PID.iPriVal = (uInt16)Temp[0];
- else PID.iPriVal = 100; //否则为上限幅值输出
- }
- else //控制量输出为负数,则输出0(下限幅值输出)
- PID.iPriVal = 0;
- }
- }
- else PID.iPriVal = 0; //同上
- }
- /* ********************************************************
- /* 函数名称:PID_Output()
- /* 函数功能:PID输出控制
- /* 入口参数:无(隐形输入,U(k))
- /* 出口参数:无(控制端)
- ******************************************************** */
- void PID_Output(void)
- {
- static uInt16 iTemp;
- static uChar8 uCounter;
- iTemp = PID.iPriVal;
- if(iTemp == 0) ConOut = 1; //不加热
- else ConOut = 0; //加热
- if(g_bPIDRunFlag) //定时中断为100ms(0.1S),加热周期10S(100份*0.1S)
- {
- g_bPIDRunFlag = 0;
- if(iTemp) iTemp--; //只有iTemp>0,才有必要减“1”
- uCounter++;
- if(100 == uCounter)
- {
- PID_Operation(); //每过0.1*100S调用一次PID运算。
- uCounter = 0;
- }
- }
- }
- /* ********************************************************
- /* 函数名称:PID_Output()
- /* 函数功能:PID输出控制
- /* 入口参数:无(隐形输入,U(k))
- /* 出口参数:无(控制端)
- ******************************************************** */
- void Timer0Init(void)
- {
- TMOD |= 0x01; // 设置定时器0工作在模式1下
- TH0 = 0xDC;
- TL0 = 0x00; // 赋初始值
- TR0 = 1; // 开定时器0
- EA = 1; // 打开总中断
- ET0 = 1; // 开定时器中断
- }
- void main(void)
- {
- Timer0Init();
- while(1)
- {
- PID_Output();
- }
- }
- void Timer0_ISR(void) interrupt 1
- {
- static uInt16 uiCounter = 0;
- TH0 = 0xDC;
- TL0 = 0x00;
- uiCounter++;
- if(100 == uiCounter)
- {
- g_bPIDRunFlag = 1;
- }
- }
- </font>
难理解的加了注释,不难理解的,没必要说。剩下的就是将温度传感器部分的子程序综合进来,之后动手自己做实物,并调试程序,只有多实践,才是“玩”好单片机王道,因此强烈建议读者以FSST15开发板为平台,同时借助LM75A温度传感器,再搭建一个温控设备,亲自进行试验,在实际中不断提高自己的能力。
这里读者需要注意,前面讲述的口诀法和经验值法似乎没用到,那是因为PID算法除了增量式以外,还有两种:位置式和微分先行法,这些知识点,才是后面四轴控制的核心,笔者放后面再来讲述,感兴趣的读者可以先去看看下面链接的博文,写的甚好,通俗易懂。
/* http://blog.sina.com.cn/s/blog_7c7e2d5a01011ta9.html */
老外的位置式PID算法
该PID算法例程摘自网络,作者不详,版权归原创作者所有。为了保持地道,笔者没有做一点点改动,其中中文注释为笔者所加。平心而论,老外的代码确实很独特,思路也很清晰,源码如下,具体含义留读者慢慢研究了,这里不赘。
- <font face="宋体" color="Black">#include <stdio.h>
- #include <math.h>
- struct _pid
- {
- int pv; //integer that contains the process value 过程量
- int sp; //integer that contains the set point 设定值
- float integral; //积分值 -- 偏差累计值
- float pgain;
- float igain;
- float dgain;
- int deadband; //死区
- int last_error;
- };
- struct _pid warm,*pid;
- int process_point, set_point,dead_band;
- float p_gain, i_gain, d_gain, integral_val,new_integ;
- //-----------------------------------
- //pid_init DESCRIPTION This function initializes the pointers in the _pid structure to the //process
- //variable and the setpoint.*pv and *sp are integer pointers.
- //-----------------------------------
- void pid_init(struct _pid *warm, int process_point, int set_point)
- {
- struct _pid *pid;
- pid = warm;
- pid->pv = process_point;
- pid->sp = set_point;
- }
- //-----------------------------------
- //pid_tune DESCRIPTION Sets the proportional gain (p_gain), integral gain (i_gain),
- //derivitive gain (d_gain), and the dead band (dead_band) of a pid control structure _pid.
- //设定PID参数 ----P,I,D,死区
- //-----------------------------------
- void pid_tune(struct _pid *pid, float p_gain, float i_gain, float d_gain, int dead_band)
- {
- pid->pgain = p_gain;
- pid->igain = i_gain;
- pid->dgain = d_gain;
- pid->deadband = dead_band;
- pid->integral= integral_val;
- pid->last_error=0;
- }
- //--------—---------------------------
- //pid_setinteg DESCRIPTION Set a new value for the integral term of the pid equation.
- //This is useful for setting the initial output of the pid controller at start up.
- //设定输出初始值
- //---------------------—--------------
- void pid_setinteg(struct _pid *pid,float new_integ)
- {
- pid->integral = new_integ;
- pid->last_error = 0;
- }
- //------------------------------------
- //pid_bumpless DESCRIPTION Bumpless transfer algorithim. When suddenly changing
- //setpoints,or when restarting the PID equation after an extended pause,the derivative of
- //the equation can cause a bump in the controller output. This function ill help smooth out
- //that bump.
- //The process value in *pv should be the updated just before this function is used.
- //pid_bumpless 实现无扰切换
- //当突然改变设定值时,或重新启动后,将引起扰动输出。这个函数将能实现平顺扰
- //动,在调用该函数之前需要先更新PV值
- //-----------------------------------
- void pid_bumpless(struct _pid *pid)
- {
- pid->last_error = (pid->sp)-(pid->pv); //设定值与反馈值偏差
- }
- //------------------------------------
- //pid_calc DESCRIPTION Performs PID calculations for the _pid structure *a.
- //This function uses the positional form of the pid equation, and incorporates an integral
- //windup prevention algorithim.Rectangular integration is used, so this function must be
- //repeated on a consistent time basis for accurate control.
- //RETURN VALUE The new output value for the pid loop. USAGE #include "control.h"
- //本函数使用位置式PID计算方式,并且采取了积分饱和限制运算
- //-----------------------------------
- float pid_calc(struct _pid *pid)
- {
- int err;
- float pterm, dterm, result, ferror;
- err = (pid->sp) - (pid->pv); // 计算偏差
- if (abs(err) > pid->deadband) // 判断是否大于死区
- {
- ferror = (float) err; //do integer to float conversion only
- //once 数据类型转换
- pterm = pid->pgain * ferror; // 比例项
- if (pterm > 100 || pterm < -100)
- {
- pid->integral = 0.0;
- }
- else
- {
- pid->integral += pid->igain * ferror; // 积分项
- if (pid->integral > 100.0) // 输出为0--100%
- {
- pid->integral = 100.0; // 如果结果大于100,则等于100
- }
- else if (pid->integral < 0.0)
- // 如果计算结果小于0.0,则等于0
- pid->integral = 0.0;
- }
- dterm = ((float)(err - pid->last_error)) * pid->dgain;
- // 微分项
- result = pterm + pid->integral + dterm;
- }
- else
- result = pid->integral; // 在死区范围内,保持现有输出
- pid->last_error = err; // 保存上次偏差
- return (result); // 输出PID值(0-100)
- }
- void main(void)
- {
- float display_value;
- int count=0;
- pid = &warm;
- // printf("Enter the values of Process point, Set point, P gain, I gain, D gain \n");
- // scanf("%d%d%f%f%f", &process_point, &set_point, &p_gain, &i_gain, &d_gain);
- // 初始化参数
- process_point = 30;
- set_point = 40;
- p_gain = (float)(5.2);
- i_gain = (float)(0.77);
- d_gain = (float)(0.18);
- dead_band = 2;
- integral_val =(float)(0.01);
- printf("The values of Process point, Set point, P gain, I gain, D gain \n");
- printf(" %6d %6d %4f %4f %4f\n", process_point, set_point, p_gain, i_gain, d_gain);
- printf("Enter the values of Process point\n");
- while(count<=20)
- {
- scanf("%d",&process_point);
- // 设定PV,SP值
- pid_init(&warm, process_point, set_point);
- // 初始化PID参数值
- pid_tune(&warm, p_gain,i_gain,d_gain,dead_band);
- // 初始化PID输出值
- pid_setinteg(&warm,0.0);
- //pid_setinteg(&warm,30.0);
- //Get input value for process point
- pid_bumpless(&warm);
- // how to display output
- display_value = pid_calc(&warm);
- printf("%f\n", display_value);
- //printf("\n%f%f%f%f",warm.pv,warm.sp,warm.igain,warm.dgain);
- count++;
- }
- }
- </font>
最后说明下和本文配套的STC15开发板目前正在电子发烧友销售,如果需要请戳这里购买: http://bbs.elecfans.com/product/stc15.html 下一节讲四轴飞行器的硬件模型建立。
大学学的就是这个PID,熟悉的陌生人
请问一下,老外的PID算法,就是第二个,最后PID输出的result为什么没有限幅?只是把比例和积分限幅了?最终的输出也有可能超过100啊?
看不懂。先收藏
深入浅出,谢谢小编。
学习了, 小编棒棒哒~~
看文字有点模糊,还是看代码来的实在。
看看阿看那看阿奎那看啊看看
学习了,多谢!
