单片机/MCU论坛
直播中

xymbmcu

12年用户 1025经验值
擅长:可编程逻辑
私信 关注
[文章]

四轴飞行器的设计之PID算法的分类与应用实例-STC15单片机实战指南连载

上一节讲了PID算法概述(见帖子 https://bbs.elecfans.com/jishu_580161_1_1.html),继续讲PID算法的分类与应用实例。

将偏差的比例(Proportion)、积分(Integral)和微分(Differential)通过线性组合构成控制量,用这一控制量对被控对象进行控制,这样的控制器称PID控制器。PID按其控制量可分为:模拟PID控制和数字PID控制,其中数字PID控制算法又分为位置式PID和增量式PID控制算法,但是无论哪种分类,都大致符合如图所示的PID模型。

001.png

  或许一看这个图,感觉好难懂,因为要涉及高数中的微积分运算,不用慌,接下来,我们以一个实例,并以计算量最小的增量式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计算输出,具体的参考代码参见下面(该实例以飛天一號开发板为硬件平台)。
  1. #include
  2. typedef  unsigned char  uChar8;
  3. typedef  unsigned int   uInt16;
  4. typedef  unsigned long int   uInt32;
  5. ***it ConOut = P1^1;                    //假如功率电阻接P1.1口
  6. typedef struct PID_Value
  7. {
  8.         uInt32 liEkVal[3];                     //差值保存,给定和反馈的差值
  9.         uChar8 uEkFlag[3];                        //符号,1则对应的为负数,0为对应的为正数
  10.         uChar8 uKP_Coe;                                //比例系数
  11.         uChar8 uKI_Coe;                                //积分常数
  12.         uChar8 uKD_Coe;                                //微分常数
  13.         uInt16 iPriVal;                     //上一时刻值
  14.         uInt16 iSetVal;                    //设定值
  15.         uInt16 iCurVal;                    //实际值
  16. } PID_ValueStr;
  17. PID_ValueStr PID;                                //定义一个结构体
  18. bit g_bPIDRunFlag = 0;                 //PID运行标志位
  19. /* ********************************************************
  20. /* 函数名称:PID_Operation()
  21. /* 函数功能:PID运算
  22. /* 入口参数:无(隐形输入,系数、设定值等)
  23. /* 出口参数:无(隐形输出,U(k))
  24. /* 函数说明:U(k)+KP*[E(k)-E(k-1)]+KI*E(k)+KD*[E(k)-2E(k-1)+E(k-2)]
  25. ******************************************************** */
  26. void PID_Operation(void)
  27. {
  28.         uInt32 Temp[3] = {0};         //中间临时变量
  29.         uInt32 PostSum = 0;         //正数和
  30.         uInt32 NegSum = 0;          //负数和
  31.         if(PID.iSetVal > PID.iCurVal)                        //设定值大于实际值否?
  32.         {
  33.                 if(PID.iSetVal - PID.iCurVal > 10)   //偏差大于10否?
  34.                         PID.iPriVal = 100;              //偏差大于10为上限幅值输出(全速加热)
  35.                 else                                                        //否则慢慢来
  36.                 {
  37.                         Temp[0] = PID.iSetVal - PID.iCurVal;        //偏差<=10,计算E(k)
  38.                            PID.uEkFlag[1] = 0;     //E(k)为正数,因为设定值大于实际值
  39.                            /* 数值进行移位,注意顺序,否则会覆盖掉前面的数值 */
  40.                     PID.liEkVal[2] = PID.liEkVal[1];
  41.                     PID.liEkVal[1] = PID.liEkVal[0];
  42.                     PID.liEkVal[0] = Temp[0];
  43. /* ============================================================ */
  44.                     if(PID.liEkVal[0] > PID.liEkVal[1])  //E(k)>E(k-1)否?
  45.                     {
  46.                                 Temp[0] = PID.liEkVal[0] - PID.liEkVal[1];        
  47.                                 //E(k)>E(k-1)
  48.                         PID.uEkFlag[0] = 0;                                         //E(k)-E(k-1)为正数
  49.                         }                                       
  50.                            else
  51.                         {
  52.                                 Temp[0] = PID.liEkVal[1] - PID.liEkVal[0];         
  53.                                 //E(k)
  54.                         PID.uEkFlag[0] = 1;                                         //E(k)-E(k-1)为负数
  55.                         }
  56. /* =========================================================== */
  57.                     Temp[2] = PID.liEkVal[1] * 2;              //2E(k-1)
  58.                         if((PID.liEkVal[0] + PID.liEkVal[2]) > Temp[2])         
  59.                         //E(k-2)+E(k)>2E(k-1)否?
  60.                     {
  61.                                 Temp[2] = (PID.liEkVal[0] + PID.liEkVal[2]) - Temp[2];
  62.                         PID.uEkFlag[2]=0;                 //E(k-2)+E(k)-2E(k-1)为正数
  63.                         }                                                                 
  64.                            else                                                        //E(k-2)+E(k)<2E(k-1)
  65.                         {
  66.                                 Temp[2] = Temp[2] - (PID.liEkVal[0] + PID.liEkVal[2]);
  67.                         PID.uEkFlag[2] = 1;                 //E(k-2)+E(k)-2E(k-1)为负数
  68.                         }  
  69. /* =========================================================== */
  70.                     Temp[0] = (uInt32)PID.uKP_Coe * Temp[0];
  71.                         //KP*[E(k)-E(k-1)]
  72.                     Temp[1] = (uInt32)PID.uKI_Coe * PID.liEkVal[0]; //KI*E(k)
  73.                     Temp[2] = (uInt32)PID.uKD_Coe * Temp[2];   
  74.                         //KD*[E(k-2)+E(k)-2E(k-1)]
  75.                         /* 以下部分代码是讲所有的正数项叠加,负数项叠加 */
  76.                         /* ========= 计算KP*[E(k)-E(k-1)]的值 ========= */
  77.                         if(PID.uEkFlag[0] == 0)
  78.                                 PostSum += Temp[0];                              //正数和
  79.                         else                                             
  80.                                 NegSum += Temp[0];                               //负数和
  81.                         /* ========= 计算KI*E(k)的值 ========= */
  82.                         if(PID.uEkFlag[1] == 0)     
  83.                                 PostSum += Temp[1];                       //正数和
  84.                         else
  85.                                    ;         /* 空操作
  86.                                         就是因为PID.iSetVal > PID.iCurVal(即E(K)>0)才进
  87.                     入if的,那么就没可能为负,所以打个转回去就是了 */
  88.                         /* ======== 计算KD*[E(k-2)+E(k)-2E(k-1)]的值 ======== */
  89.                         if(PID.uEkFlag[2]==0)
  90.                                 PostSum += Temp[2];                 //正数和
  91.                         else
  92.                                 NegSum += Temp[2];                 //负数和
  93.                         /* ========= 计算U(k) ========= */                        
  94.                         PostSum += (uInt32)PID.iPriVal;         
  95.                         if(PostSum > NegSum)             //是否控制量为正数
  96.                         {
  97.                                 Temp[0] = PostSum - NegSum;
  98.                                 if(Temp[0] < 100 )             //小于上限幅值则为计算值输出
  99.                                         PID.iPriVal = (uInt16)Temp[0];
  100.                                 else PID.iPriVal = 100;       //否则为上限幅值输出
  101.                         }
  102.                         else                         //控制量输出为负数,则输出0(下限幅值输出)
  103.                                    PID.iPriVal = 0;
  104.                 }
  105.         }
  106.         else PID.iPriVal = 0;                                //同上
  107. }
  108. /* ********************************************************
  109. /* 函数名称:PID_Output()                                                                        
  110. /* 函数功能:PID输出控制                                         
  111. /* 入口参数:无(隐形输入,U(k))                                                
  112. /* 出口参数:无(控制端)
  113. ******************************************************** */
  114. void PID_Output(void)
  115. {
  116.         static uInt16 iTemp;
  117.         static uChar8 uCounter;
  118.         iTemp = PID.iPriVal;
  119.         if(iTemp == 0)  ConOut = 1;        //不加热
  120.         else  ConOut = 0;                    //加热
  121.         if(g_bPIDRunFlag)                    //定时中断为100ms(0.1S),加热周期10S(100份*0.1S)
  122.         {
  123.                 g_bPIDRunFlag = 0;
  124.                 if(iTemp) iTemp--;                //只有iTemp>0,才有必要减“1”
  125.                 uCounter++;
  126.                 if(100 == uCounter)
  127.                 {
  128.                         PID_Operation();                //每过0.1*100S调用一次PID运算。
  129.                         uCounter = 0;        
  130.                 }
  131.         }
  132. }
  133. /* ********************************************************
  134. /* 函数名称:PID_Output()         
  135. /* 函数功能:PID输出控制
  136. /* 入口参数:无(隐形输入,U(k))
  137. /* 出口参数:无(控制端)
  138. ******************************************************** */
  139. void Timer0Init(void)
  140. {
  141.         TMOD |= 0x01;        // 设置定时器0工作在模式1下
  142.         TH0 = 0xDC;
  143.         TL0 = 0x00;                // 赋初始值
  144.         TR0 = 1;                        // 开定时器0
  145.         EA = 1;                        // 打开总中断
  146.         ET0 = 1;                        // 开定时器中断
  147. }
  148. void main(void)
  149. {
  150.         Timer0Init();
  151.         while(1)
  152.         {
  153.                 PID_Output();
  154.         }
  155. }
  156. void Timer0_ISR(void) interrupt 1
  157. {
  158.         static uInt16 uiCounter = 0;
  159.         TH0 = 0xDC;
  160.         TL0 = 0x00;
  161.         uiCounter++;
  162.         if(100 == uiCounter)
  163.         {
  164.                 g_bPIDRunFlag = 1;
  165.         }
  166. }

  难理解的加了注释,不难理解的,没必要说。剩下的就是将温度传感器部分的子程序综合进来,之后动手自己做实物,并调试程序,只有多实践,才是“玩”好单片机王道,因此强烈建议读者以FSST15开发板为平台,同时借助LM75A温度传感器,再搭建一个温控设备,亲自进行试验,在实际中不断提高自己的能力。
  这里读者需要注意,前面讲述的口诀法和经验值法似乎没用到,那是因为PID算法除了增量式以外,还有两种:位置式和微分先行法,这些知识点,才是后面四轴控制的核心,笔者放后面再来讲述,感兴趣的读者可以先去看看下面链接的博文,写的甚好,通俗易懂。
  /* http://blog.sina.com.cn/s/blog_7c7e2d5a01011ta9.html */

  老外的位置式PID算法

  该PID算法例程摘自网络,作者不详,版权归原创作者所有。为了保持地道,笔者没有做一点点改动,其中中文注释为笔者所加。平心而论,老外的代码确实很独特,思路也很清晰,源码如下,具体含义留读者慢慢研究了,这里不赘。
  1. #include
  2. #include
  3. struct _pid
  4. {
  5.         int pv;                //integer that contains the process value 过程量
  6.         int sp;                 //integer that contains the set point   设定值
  7.         float integral;                 //积分值 -- 偏差累计值
  8.         float pgain;
  9.         float igain;
  10.         float dgain;
  11.         int deadband;                //死区
  12.         int last_error;
  13. };
  14. struct _pid warm,*pid;
  15. int process_point, set_point,dead_band;
  16. float p_gain, i_gain, d_gain, integral_val,new_integ;
  17. //-----------------------------------
  18. //pid_init DESCRIPTION This function initializes the pointers in the _pid structure to the //process
  19. //variable and the setpoint.*pv and *sp are integer pointers.
  20. //-----------------------------------
  21. void pid_init(struct _pid *warm, int process_point, int set_point)
  22. {
  23.         struct _pid *pid;
  24.         pid = warm;
  25.         pid->pv = process_point;
  26.         pid->sp = set_point;
  27. }
  28. //-----------------------------------
  29. //pid_tune DESCRIPTION Sets the proportional gain (p_gain), integral gain (i_gain),
  30. //derivitive gain (d_gain), and the dead band (dead_band) of a pid control structure _pid.
  31. //设定PID参数 ----P,I,D,死区
  32. //-----------------------------------
  33. void pid_tune(struct _pid *pid, float p_gain, float i_gain, float d_gain, int dead_band)
  34. {
  35.         pid->pgain = p_gain;
  36.         pid->igain = i_gain;
  37.         pid->dgain = d_gain;
  38.         pid->deadband = dead_band;
  39.         pid->integral= integral_val;
  40.         pid->last_error=0;
  41. }
  42. //--------—---------------------------
  43. //pid_setinteg DESCRIPTION Set a new value for the integral term of the pid equation.
  44. //This is useful for setting the initial output of the pid controller at start up.
  45. //设定输出初始值
  46. //---------------------—--------------
  47. void pid_setinteg(struct _pid *pid,float new_integ)
  48. {
  49.         pid->integral = new_integ;
  50.         pid->last_error = 0;
  51. }
  52. //------------------------------------
  53. //pid_bumpless DESCRIPTION Bumpless transfer algorithim. When suddenly changing
  54. //setpoints,or when restarting the PID equation after an extended pause,the derivative of
  55. //the equation can cause a bump in the controller output. This function ill help smooth out
  56. //that bump.
  57. //The process value in *pv should be the updated just before this function is used.
  58. //pid_bumpless 实现无扰切换
  59. //当突然改变设定值时,或重新启动后,将引起扰动输出。这个函数将能实现平顺扰
  60. //动,在调用该函数之前需要先更新PV值
  61. //-----------------------------------
  62. void pid_bumpless(struct _pid *pid)
  63. {
  64.         pid->last_error = (pid->sp)-(pid->pv);  //设定值与反馈值偏差
  65. }
  66. //------------------------------------
  67. //pid_calc DESCRIPTION Performs PID calculations for the _pid structure *a.
  68. //This function uses the positional form of the pid equation, and incorporates an integral
  69. //windup prevention algorithim.Rectangular integration is used, so this function must be
  70. //repeated on a consistent time basis for accurate control.
  71. //RETURN VALUE The new output value for the pid loop. USAGE #include "control.h"
  72. //本函数使用位置式PID计算方式,并且采取了积分饱和限制运算
  73. //-----------------------------------
  74. float pid_calc(struct _pid *pid)
  75. {
  76.         int err;
  77.         float pterm, dterm, result, ferror;
  78.         err = (pid->sp) - (pid->pv);                        // 计算偏差
  79.         if (abs(err) > pid->deadband)                        // 判断是否大于死区
  80.         {
  81.                 ferror = (float) err;   //do integer to float conversion only
  82.                 //once 数据类型转换
  83.                 pterm = pid->pgain * ferror;                // 比例项
  84.                 if (pterm > 100 || pterm < -100)
  85.                 {
  86.                         pid->integral = 0.0;
  87.                 }
  88.                 else
  89.                 {
  90.                         pid->integral += pid->igain * ferror;        // 积分项
  91.                         if (pid->integral > 100.0)        // 输出为0--100%
  92.                         {
  93.                                 pid->integral = 100.0;        // 如果结果大于100,则等于100
  94.                         }
  95.                         else if (pid->integral < 0.0)        
  96.                         // 如果计算结果小于0.0,则等于0
  97.                                 pid->integral = 0.0;
  98.                 }
  99.                 dterm = ((float)(err - pid->last_error)) * pid->dgain;        
  100.                 // 微分项
  101.                 result = pterm + pid->integral + dterm;
  102.         }
  103.         else
  104.         result = pid->integral;                                 // 在死区范围内,保持现有输出
  105.         pid->last_error = err;                                        // 保存上次偏差
  106.         return (result);                                                        // 输出PID值(0-100)
  107. }
  108. void main(void)
  109. {
  110.         float display_value;
  111.         int count=0;
  112.         pid = &warm;
  113.         // printf("Enter the values of Process point, Set point, P gain, I gain, D gain n");
  114.         // scanf("%d%d%f%f%f", &process_point, &set_point, &p_gain, &i_gain, &d_gain);
  115.         // 初始化参数
  116.         process_point = 30;
  117.         set_point = 40;
  118.         p_gain = (float)(5.2);
  119.         i_gain = (float)(0.77);
  120.         d_gain = (float)(0.18);
  121.         dead_band = 2;
  122.         integral_val =(float)(0.01);
  123.         printf("The values of Process point, Set point, P gain, I gain, D gain n");
  124.         printf(" %6d %6d %4f %4f %4fn", process_point, set_point, p_gain, i_gain, d_gain);
  125.         printf("Enter the values of Process pointn");
  126.         while(count<=20)
  127.         {
  128.                 scanf("%d",&process_point);
  129.                 // 设定PV,SP值
  130.                 pid_init(&warm, process_point, set_point);
  131.                 // 初始化PID参数值
  132.                 pid_tune(&warm, p_gain,i_gain,d_gain,dead_band);
  133.                 // 初始化PID输出值
  134.                 pid_setinteg(&warm,0.0);
  135.                 //pid_setinteg(&warm,30.0);
  136.                 //Get input value for process point
  137.                 pid_bumpless(&warm);
  138.                 // how to display output
  139.                 display_value = pid_calc(&warm);
  140.                 printf("%fn", display_value);
  141.                 //printf("n%f%f%f%f",warm.pv,warm.sp,warm.igain,warm.dgain);
  142.                 count++;
  143.         }
  144. }

最后说明下
和本文配套的STC15开发板目前正在电子发烧友销售,如果需要请戳这里购买: https://bbs.elecfans.com/product/stc15.html 下一节讲四轴飞行器的硬件模型建立

回帖(9)

YiSuan210

2016-4-13 18:30:52
大学学的就是这个PID,熟悉的陌生人
举报

奋斗小青年1314

2016-4-26 09:54:35
请问一下,老外的PID算法,就是第二个,最后PID输出的result为什么没有限幅?只是把比例和积分限幅了?最终的输出也有可能超过100啊?
举报

摘星揽月111

2016-5-18 09:34:43
看不懂。。。先收藏
举报

zhushw

2016-5-18 09:46:41
深入浅出,谢谢楼主。
举报

gengyuchao

2016-6-10 19:27:28
学习了, 楼主棒棒哒~~
举报

海洋

2016-12-21 19:58:35
看文字有点模糊,还是看代码来的实在。。。
举报

情牵一线123

2016-12-23 11:23:26
看看阿看那看阿奎那看啊看看
举报

奕凡321

2017-1-7 08:11:27
学习了,多谢!!!!!
举报

胡先生

2018-6-2 19:40:07
厉害1,牛逼,学习学习
举报

更多回帖

发帖
×
20
完善资料,
赚取积分