查看: 16896|回复: 23

PID 通用算法,详细例程

[复制链接]
  • TA的每日心情
    奋斗
    2019-3-20 18:16
  • 签到天数: 141 天

    [LV.7]常住居民III

    发表于 2017-3-25 09:53 | 显示全部楼层 |阅读模式
    本帖最后由 希岩 于 2017-3-25 09:56 编辑

           在介绍程序之前,简单介绍一些PID。所谓PID指比例、积分、微分控制的总成。PID控制是一种闭环控制,又是用得最广的控制。一方面,PID控制是一种模糊控制,不需要知道系统的状态空间表达式,仅仅需要调整kp,ki,kd的数值,以达到期望的效果。
           PID中的比例控制,代表了系统的快速性,kp越大,响应越快,但是会导致超调增大,又会导致系统震荡。反之,系统响应太慢。ki指的是积分控制,代表了系统的准确性。ki的加入,大大抑制了稳态误差。自然这个数也不能太大,否则有毛刺。至于kd微分控制,表征系统对未来的预判,在延迟较严重下应用,一般不推荐,会导致系统震荡。
          在本例中,采用PID调节来控制BOOST电路电压。BOOST电路采用经典的LC以及绝缘栅场效应管结构。通过分压电阻引入反馈电压,作为PID控制的反馈量。而控制量,是MOS管的通断时间,也就是占空比。BOOST电压与占空比成正比,因此可以通过调整占空比来调整电压。
          本例中修改宏定义“SETVOLTAGE”来设置BOOST输出电压,最高可设置30V,这是理论上的,没试过。输入电压为开发板上的5V接口。在本例中,将5v电压升至18V。
          MCU为ATMEGA328,采用定时器1产生PWM,此定时器是10位的,因此精度较高。采用定时器2溢出中断作为PID积分的步长参考。
          本例之所以为通用PID算法,是因为在本例中,只需要修改函数PIDCtl_Scan(uint16 feedback)中引用的Set_PWM(tPID->s16PIDCtr)子程序以及 该子程序的参数uint16 feedback来移植到其他闭环控制系统中,非常方便。其中feedback为反馈量,tPID->s16PIDCtr为控制量,为PID算法计算得出的当前控制量。控制量可以是多样的,占空比,电流,电压等。
          算法采用查询方式,需要多次调用。在程序前定义了PID的参数,调整这里的kp,ki以及步长。还有输入量范围,输出量范围即可。
          本例设置BOOST电压为18V ,可以看到,当控制器工作时,如图所示,电压从5v升到18V,并保持稳定。设置其他电压也是如此。PID控制简单介绍到这里,感兴趣的详细看例程。本例程仅供学习参考,不得用于其他用途。
    [kenrobot_code]
    /****************************************************************************   
    //!!本程序只供学习使用,未经作者许可,不得用于其它任何用途。原创程序
    //  原     创  : 凌晨七點半(820329503qq)
    //  设计日期   : 2017-3-25
    //  功能描述   : PID通用控制示例 (Atmega328系列)
                     以通过PWM控制BOOST电路电压为例
    //  说    明:
                   单片机      : ATMEGA328P-PU
                   晶振        : 片外16Mhz石英晶振
                   工作频率    : 16Mhz
                   注          : 外置分压电阻为1/6
    //========================用到的接口===========================
    //              9         PWM端口(PB1)
    //              A0        ADC0   (PD0)        
    //              5V        BOOST电路输入电压   
    ********************************************************************************/

    #include <Arduino.h>
    #include <avr/interrupt.h>

    #define SETVOLTAGE   18         //设置电压为12V     
    // 数据类型
    typedef signed   char sint08;
    typedef unsigned char uint08;
    typedef signed   int  sint16;
    typedef unsigned int  uint16;
    typedef unsigned long uint32;
    typedef signed   long sint32;
    //全局变量
    uint16 u16RTICnt;     //定时器计数
    // ----------------------------------------------------------------------------
    typedef struct {                              // 定时器,单位ms
      uint16 u16TimeStrt;                         // 启动时刻
      uint16 u16TimeNow1;                         // 当前时刻
      uint16 u16Duration;                         // 间隔时间(ms)
    } TMyTimer_ms;

    typedef struct {                                         // PID
      sint08      s08PIDDir;                                 // 1:正向;-1:反向
      sint16      s16InpMax, s16InpMin;                      // 输入量的最大/最小值,用于输入的归一化计算
      sint16      s16OutMax, s16OutMin;                      // 控制量的最大/最小值,用于输出限幅
      sint16      s16PIDCmd;                                 // 归一化的命令值,范围为0 ~ 1023
      sint16      s16PIDAcq;                                 // 归一化的实际值,范围为0 ~ 1023
      sint32      s32PIDCtr;                                 // PID(的放大了256倍)的上一次控制量  
      sint16      s16PIDCtr;                                 // PID的控制量
      sint16      s16PID_E0, s16PID_E1;//, s16PID_E2;        // 本次误差, 上次误差,上上次误差
      uint16      u16PID_Kp, u16PID_Ki;//, u16PID_Kd;        // PID参数
      TMyTimer_ms tTimer_ms;                                 // PID定时器  uint16      u16Timer2;  
    } TParaPID;
    //全局变量设置
    TParaPID tPIDMotor;
    const TParaPID CstParaPID = {
       1,               // sint08  s08PIDDir;                                 // 1:正向;-1:反向
       512, 307,        // sint16  s16InpMax, s16InpMin;                      // 输入量的最大/最小值,用于输入的归一化计算
       1023 ,0 ,        // sint16  s16OutMax, s16OutMin;                      // 控制量的最大/最小值,用于输出限幅
       0 ,               // sint16  s16PIDCmd;                                 // 归一化的命令值,范围为0 ~ 1023
       0  ,              // sint16  s16PIDAcq;                                 // 归一化的实际值,范围为0 ~ 1023
       0   ,             // sint32  s32PIDCtr;                                 // PID(的放大了256倍)的上一次控制量  
       0   ,             // sint16  s16PIDCtr;                                 // PID的控制量
       0   , 0 ,         // sint16  s16PID_E0, s16PID_E1;//, s16PID_E2;        // 本次误差, 上次误差,上上次误差
       4   ,40 ,         // uint16  u16PID_Kp, u16PID_Ki;//, u16PID_Kd;        // PID参数

      {                  // TMyTimer_ms tTimer_ms;                             // PID定时器, PID控制间隔(ms)
         0 ,             //   uint16 u16TimeStrt;                              // 启动时刻
         0 ,             //   uint16 u16TimeNow1;                              // 当前时刻
        10 ,             //   uint08 u16Duration;                              // 间隔时间10ms
      }
    };
    // ----------------------------------------------------------------------------
    void MyTimerStart(TMyTimer_ms *ptMyTime, uint16 u16Duration){             // 初始化定时器
      ptMyTime->u16TimeNow1 = u16RTICnt;                               // 当前时刻
      ptMyTime->u16TimeStrt = ptMyTime->u16TimeNow1;                         // 启动时刻
      ptMyTime->u16Duration = u16Duration;                                   // 间隔时间(ms)
    }
    // ----------------------------------------------------------------------------
    uint08 MyTimerQuery(TMyTimer_ms *ptMyTime){                              // 时间到了返回1,否则返回0
      ptMyTime->u16TimeNow1 = u16RTICnt;
      if(ptMyTime->u16TimeNow1 - ptMyTime->u16TimeStrt < ptMyTime->u16Duration) return 0;
      ptMyTime->u16TimeStrt = ptMyTime->u16TimeNow1;
      return 1;
    }
    // ----------------------------------------------------------------------------
    void InitPID(TParaPID *tpPID01){                                               // 初始化PID参数  
      (void)memcpy(tpPID01, &CstParaPID, sizeof(TParaPID));
      MyTimerStart(&tpPID01->tTimer_ms, tpPID01->tTimer_ms.u16Duration);          // 初始化定时器, 主要用于初始化当前时间

    }
    // ----------------------------------------------------------------------------
    uint16 PID_Sub1(TParaPID *tpPID, sint16 s16V01){                               // 归一化计算,返回佱范围为0 ~ 1023
      uint32 u32Tmp1;
      
      if(tpPID->s16InpMax <= tpPID->s16InpMin) return 0;
      if(s16V01 > tpPID->s16InpMax){
        s16V01 = tpPID->s16InpMax;
      } else if(s16V01 < tpPID->s16InpMin){
        s16V01 = tpPID->s16InpMin;
      }

      u32Tmp1 = s16V01 - tpPID->s16InpMin;
      u32Tmp1 = u32Tmp1 * 1023;
      u32Tmp1 = u32Tmp1 / (tpPID->s16InpMax - tpPID->s16InpMin);
      return (uint16)u32Tmp1;
    }
    // ----------------------------------------------------------------------------
    void PID_Cal(TParaPID *tpPID){                                         // 计算PID控制量,只使用了PI控制
      sint32 s32Tmp1, s32Tmp2;

      //tpPID->s16PID_E2 = tpPID->s16PID_E1;                               // 上上次误差
      tpPID->s16PID_E1 = tpPID->s16PID_E0;                                 // 上次误差
      tpPID->s16PID_E0 = (tpPID->s16PIDCmd - tpPID->s16PIDAcq)             // 本次误差,命令值与实测值的差
                     * tpPID->s08PIDDir;                                   // 计算误差的方向
      s32Tmp1 = ((sint32)tpPID->u16PID_Kp)
                       * ((sint32)(tpPID->s16PID_E0 - tpPID->s16PID_E1));  // 比例增量
      s32Tmp2 = ((sint32)tpPID->u16PID_Ki) * ((sint32)tpPID->s16PID_E0);   // 积分增量

      tpPID->s32PIDCtr = tpPID->s32PIDCtr + s32Tmp1 + s32Tmp2;             // 控制量

      s32Tmp1 = ((sint32)tpPID->s16OutMax) << 16;                          // 控制量的最大值
      s32Tmp2 = ((sint32)tpPID->s16OutMin) << 16;                          // 控制量的最小值
      if       (tpPID->s32PIDCtr > s32Tmp1){  tpPID->s32PIDCtr = s32Tmp1;
      } else if(tpPID->s32PIDCtr < s32Tmp2){  tpPID->s32PIDCtr = s32Tmp2;
      }

      tpPID->s16PIDCtr = (sint16)(tpPID->s32PIDCtr >> 16);                 // PID的控制量,只取整数 return (sint16)(tpPID->s32PIDCtr >> 16);                             // 只取整数
    }
    // ----------------------------------------------------------------------------
    void MyPIDMotorSet(TParaPID *ptPID03, sint16 s16P1){                    // 电机的PID控制, 输入要求的值,不需要反复调用
      ptPID03->s16PIDCmd = PID_Sub1(ptPID03, s16P1);                        // 归一化计算,归一化的命令值,返回范围为0 ~ 1023
    }
    // ----------------------------------------------------------------------------
    uint08 MyPIDMotorCtr(TParaPID *ptPID03, sint16 s16P2){                  // 电机的PID控制, 输入实际的值,需要反复调用,这是控制过程
      if(MyTimerQuery(&ptPID03->tTimer_ms) == 0) return 0;                  // 时间到了返回1,否则返回0
      ptPID03->s16PIDAcq = PID_Sub1(ptPID03, s16P2);                        // 归一化计算,返回范围为0 ~ 1023

      PID_Cal(ptPID03);                                                     // 计算PID控制量,只使用了PI控制

      return 1;
    }

    //--------------------------------------------------------------------------
    //设置PWM占空比
    void Set_PWM(uint16 Duty)
    {
      OCR1A=Duty;
    }
    //--------------------------------------------------------------------------
    //PID控制,查询形式,多次访问
    void  PIDCtl_Scan(uint16 feedback)
    {
    TParaPID *tPID=&tPIDMotor;
      if(MyPIDMotorCtr(tPID, feedback) == 1)
         // PID控制, 输入实际的值,需要反复调用,这是控制过程
              Set_PWM(tPID->s16PIDCtr);                // PID的控制量,此处为占空比
       

    }
    //------------------------------------------------------------
    //初始化定时器2
    void Init_Timer2(void)
    {
      TCCR2A  = 0x00;             //正常模式
      TCCR2B  = 0x04;             //预分频器64分频,每次计数4us
      TIMSK2  |=(1<<TOIE2);       //溢出中断使能,溢出时间1.024ms
    }

    //------------------------------------------------------------
    //初始化PWM,定时器1,16位定时器
    void Init_PWM(void)
    {
      TCCR1A  = (1<<COM1A1)|(1<<WGM11)|(1<<WGM10);
      TCCR1B  = (1<<WGM12)|(1<<CS11);             //快速PWM,预分频器8分频
      TCNT1   = 400;                              //计数top值,5kHz
      OCR1A   = 200 ;                             //计数200,占空比50%
      //OCR1B   = 150 ;                           //计数150,占空比50%
    }

    uint16 u16val;
    //--------------------------------------------------------
    void setup() {
       TParaPID *tPID=&tPIDMotor;
        pinMode(A0, INPUT);                     // A0 为ADC输入
        DDRB|=0x02;                             // 9端口输出

       InitPID(tPID);                           // 初始化PID参数
       Init_PWM();                              // 初始化PWM
       Init_Timer2();                           //初始化定时器
       u16val=SETVOLTAGE*1024/30;              //6分之一分压
       MyPIDMotorSet(tPID, u16val);            // 电机的PID控制, 输入要求的值,不需要反复调用
       SREG   = 0x80;                          //I位全局中断使能,page10
    //  Serial.begin(19200);
    }

    //------------------------------------------------------------
    void loop()
    {
      u16val=analogRead(A0);                     //读取ADC值,作为电压反馈值
      //Serial.println(u16val);
      PIDCtl_Scan(u16val);
    }
    //-----------------------------------
    //中断服务程序
    ISR(TIMER2_OVF_vect)
    {
      TIFR2|=0x01;            //TIFR0_OCF0A=1,A中断标计位清零,page113
      u16RTICnt++;
    }
    //------------------------------------------------------------------
    //This is END.[/kenrobot_code]


    IMG_20170325_090123.jpg
    IMG_20170325_085459.jpg
  • TA的每日心情
    擦汗
    2017-1-10 09:19
  • 签到天数: 1 天

    [LV.1]初来乍到

    发表于 2017-3-25 20:01 | 显示全部楼层
    表示看不懂哦
  • TA的每日心情
    奋斗
    2019-3-20 18:16
  • 签到天数: 141 天

    [LV.7]常住居民III

     楼主| 发表于 2017-3-26 09:30 | 显示全部楼层

    深入学习后能看懂

    该用户从未签到

    发表于 2017-7-18 01:37 | 显示全部楼层
    好贴,明天继续看,太晚了
  • TA的每日心情
    开心
    2019-6-24 00:28
  • 签到天数: 813 天

    [LV.10]以坛为家III

    发表于 2017-7-18 14:43 | 显示全部楼层
    好帖啊,收藏着慢慢看
  • TA的每日心情
    开心
    2017-8-9 21:02
  • 签到天数: 1 天

    [LV.1]初来乍到

    发表于 2017-7-21 21:04 | 显示全部楼层
    您好  请问能不能把PID的库上传一下?谢谢了

    该用户从未签到

    发表于 2017-7-30 15:57 | 显示全部楼层
    TCCR1A  = (1<<COM1A1)|(1<<WGM11)|(1<<WGM10);
      TCCR1B  = (1<<WGM12)|(1<<CS11);
    小白问一下这几句怎么理解啊
  • TA的每日心情
    开心
    2017-9-26 22:20
  • 签到天数: 1 天

    [LV.1]初来乍到

    发表于 2017-9-26 22:48 | 显示全部楼层
    好帖啊,收藏着慢慢看
  • TA的每日心情
    开心
    2018-9-6 14:50
  • 签到天数: 25 天

    [LV.4]偶尔看看III

    发表于 2017-11-24 10:24 | 显示全部楼层
    好帖啊,收藏着慢慢看
  • TA的每日心情
    奋斗
    2019-3-20 18:16
  • 签到天数: 141 天

    [LV.7]常住居民III

     楼主| 发表于 2017-12-20 15:23 | 显示全部楼层

    这个看那个Atmega328P的数据手册上面有寄存器介绍,这个是定时器1的控制寄存器
    您需要登录后才可以回帖 登录 | 立即注册  

    本版积分规则

    热门推荐

    【项目】基于Arduino Nano的多功能桌面感应垃圾桶
    【项目】基于Arduino Nano
    基于Arduino Nano的多功能桌面感应垃圾桶随着大学生活不断往前迈进,宿舍桌面上的杂物
    智能物联网花盆
    智能物联网花盆
    打坏了一个费时5小时 3D打印的花盆,耗时22小时 下载:Kittenblock软件,安装、运
    呼吸灯实验
    呼吸灯实验
    本文节选自《Arduino程序设计基础》 之前的章节已经介绍了多种方法控制LED,但单是开
    为开发板 Generic ESP8266 Module 编译时出错
    为开发板 Generic ESP8266
    第一次尝试Arduino UNO软串口通信,编译一直通不过,错误信息 “开发板 generic (平
    Arduino Uno 无法上传程序
    Arduino Uno 无法上传程序
    原来可以正常使用的Arduino Uno 板子突然不能上传程序了,硬件驱动正常,TX和RX指示灯
    Copyright   ©2015-2016  Arduino中文社区  Powered by©Discuz!   ( 蜀ICP备14017632号-3 )
    快速回复 返回顶部 返回列表