PID控制器的数字实现及C语法讲解

  • 概述  

  为方便学习与交流,根据自己的理解与经验写了这份教程,有错误之处请各位读者予以指出,具体包含以下三部分内容:

  (1)  PID数字化的推导过程(实质:微积分的近似计算);

  (2)  程序风格介绍(程序风格来源于TI官方案例);

  (3)  C有关语法简述(语法会结合实例进行讲解)。

==========================================================================================================================================

  • PID控制器的数字化

  PID控制器是工业过程控制中广泛采用的一种控制器,其中,P、I、D分别为比例(Proportion)、积分(Integral)、微分(Differential)的简写;将偏差的比例、积分和微分通过线性组合构成控制量,用该控制量对受控对象进行控制,称为PID算法

  为了用软件实现PID算法,需将PID控制器离散化。

  1. 整体思路

        

  2. 方框图

  PID控制器的方框图如图所示:

          

  3. 拉氏域的表达式

  根据方框图,可写出PID控制器对应的传递函数:

        

         其中,Kp为比例系数,ki为积分系数,Kd为微分系数

  4. 时域的表达式

  在分析时,通常借助于拉氏空间,例如判断系统的稳定性与相对稳定性;而现在我们关心的是时域里的问题,因此对上式进行拉普拉斯逆变换,得到时域里的表达式:

      

其对应的结构框图如图所示:

              

  5. 差分方程

  该时域里的表达式不便于编程处理,因此需对该式进行离散化处理,从而得到可编程实现的差分方程,分析过程如下:

  (说明:PID离散化的实质为微积分的离散化(数值化处理),由于这个推导过程很多教材上都有介绍,因而略去推导过程,只给出最终表达式,程序的算法就是基于此表达式而写的)

  数字PID控制器的增量式算法:

      

      其中,T为步长,即采样周期(由微控制器的定时器确定

  u(kT)=u(k),便得到PID控制器增量式算法的差分方程:

      

  这样就可编程实现了或许有人会问,为什么差分方程就可编程实现呢?这是因为解差分方程的一般解法就是迭代法,而迭代法只需初值跟通项公式,这在计算机编程中很容易实现 

  为使编程方便,可引入中间变量,定义如下:

             

  则,PID控制器增量式算法的差分方程变为:

      

  说明:

   (1)在PID增量式算法中只需对输出u(t)作限幅处理

   (2)当微分系数 Kd=0 时,PID控制器就成了PI控制器(在编写PID程序时默认使其为PI调节器

       当积分系数 Ki=0 时,PID控制器就成了PD控制器。

=======================================================================================

  • 基于微控制器的算法实现

  我写的数字PID程序如图所示(在最后的附件部分),有两套代码,一套是直接函数调用(C/C++通用),另一套是使用函数指针进行函数调用仅适用于C,现从两个方面对该程序做讲解:

(一)程序风格

  程序采用了模块化编程的思想,这样做的目的增强代码的可移植性及程序的可读性

  程序被拆分成三个模块:

  一个是PID的头文件’PID.h’:主要是定义算法实现有关的数据类型;

  一个是PID的源文件’PID.c’:主要是定义算法实现的函数;

  一个是主函数文件’amain.c’:PID程序的使用方法,即在主程序中做相应的初始化工作,在中断服务程序中进行PID的计算。

说明:读这个程序时可能有点困难,不过这属情理之中的事,毕竟刚接触这种风格的童鞋不太能理解这种风格的产生(为什么这么做)及用意(这么做的好处);我的建议是:在理解算法的原理后,根据自己的编程风格尝试着写一下,然后再跟这套程序对比着来理解,推敲一下别人为什么要这么做;当熟悉了整个流程后,你才能体会这种程序风格的优势,再将这种编程风格慢慢转化为自己的编程风格。

(二)程序中涉及的C语法讲解

  这里,我只讲述为什么要采用这些语法以及采用这些语法所带来的好处,至于细枝末节的问题,就请各位童鞋自行查阅有关资料,顺带给大家推荐一本不错的C语言教材:C Primer Plus,毕竟学习的兴趣浓度跟书籍的编排也有关。

  1. 条件编译指令

  第一处:#ifndef PID_H语句

  使用该语句的目的是避免造成把重复定义语句(如,结构体类型定义)添加到工程中,而使得编译出错

  说明:其实也可不用#ifndef语句,因为每个定义的变量都具有特定的物理含义,不会造成重复定义现象。

  第二处:#if (PID_DEBUG) 语句

  使用该语句的目的实现功能切换注意了:是在校正PID参数后手动切换,通过改变宏定义语句#define PID_DEBUG 1中的宏体实现),具体请看程序清单。

  2. 结构体及结构体指针

  使用结构体类型的好处:可为实现某一功能的各变量进行“打包”处理

  使用结构体指针的好处:通过传址调用,对方便对结构体变量本身进行操作

  3. typedef数据类型定义

  使用typedef数据类型定义的好处是方便跨平台进行代码移植操作;但由于教材的缘故,造成很多童鞋都停留在表面层次上的理解(typedef 数据类型 别名),因而此处作重点讲解。

我的理解:任何一个typedef声明中的标识符不再是一个变量,而是代表一个数据类型,其表示的数据类型为正常变量声明(去掉typedef)的那个标识符的数据类型。


  理解起来可能有点困难,现结合实例来讲解:

[例1]

  typedef int Myint;

  分析:

  第一步:正常变量声明(去掉typedef

    int Myint; 

  该语句表示定义一个int型变量Myint这里,Myint为变量名

  第二步:整体分析

    typedef int Myint;

  该语句表示定义一个Myint类型此时,Myint为数据类型标识符,其具体所表示的类型:int型;

  应用:

    Myint a; //声明整型变量a

[例2]    

  typedef struct
  {
   //省略成员
  }PID;

  分析:

  第一步:正常变量声明(去掉typedef)

    struct
    {
     //省略成员
    }PID;

  该语句表示定义一个结构体变量PID这里,PID为变量名

  第二步:整体分析

    typedef struct
    {
    //省略成员
    }PID;

  该语句表示定义一个PID类型此时,PID为数据类型标识符,其具体所表示的类型:结构体类型,且其具有的成员同结构体变量PID这里,PID为变量名

  应用:

      PID ASR; //定义结构体变量ASR

[例3]

  typedef void (*PFun)(int );

  分析:

  第一步:正常变量声明(去掉typedef)

    void (*PFun)(int );

  该语句表示定义一个函数指针PFun这里,PFun为变量名

  第二步:整体分析

    typedef void (*PFun)(int );

  该语句表示定义一个PFun类型此时,PFun为数据类型标识符,其具体所表示的类型:函数指针类型,且其指向形参为int型,无返回值的一类函数;

  应用:

    PFun  pf; //定义函数指针pf

  说明:typedef的用法与宏定义#define的用法类似,但又有区别,体现在以下两点:

  (a)  typedef是对数据类型的定义,而#define是对数值的定义;

  (b)  typedef由编译器解释,而#define由预处理器执行。

  4. 空形参函数和形参带(void)函数

  这是在C/C++中相当容易混淆的地方,因此这里重点介绍一下,若是这个知识点没搞懂,那么这个程序你就无法看懂为什么会如此定义函数指针及利用函数指针来进行函数调用。

  void本身就是一种数据类型(空类型),把void作为形参时,表示这个函数不需要参数。

  在C++中,空形参表与新参为void是等价的,这是C++中明确规定的;但在C中则是两回事:C中的空形参表仅表示函数的形参个数和类型不确定,并非没有参数,这会暂时挂起编译器的类型检查机制,从而造成类型安全隐患,所以在C中欲表示函数无形参时,最好用void,此时编译器将进行函数参数类型验证。

  [例]

void pid_calc(int); //函数声明
void (*calc_1)(int); //函数指针声明
void (*calc_2)(); //函数指针声明 void main()
{
//将函数的入口地址赋给函数指针
calc_1=pid_calc; //C编译通过;C++编译通过
calc_2=pid_calc; //C编译通过;C++编译失败
}

  5. 函数指针及其函数调用

  函数调用,除了直接调用”函数名(实参)”这种语法外,还可通过函数指针来实现,两者并无区别,但为了代码的紧凑性及美观性,建议大家使用函数指针来进行函数调用。

  在我放出的两套代码中,一套是直接函数调用C/C++通用),另一套是使用函数指针进行函数调用仅适用于C),大家可体会这两种用法的区别。

  6. 数据类型转换

  C语言中的数据类型分为自动类型转换与强制类型转换

  (1) 自动类型转换(由编译器完成)

  自动转换的适用场合及其转换规则,请读者查阅有关资料

  (2) 强制类型转换通过类型转换运算实现

  在本程序中,即可以将自定义函数的函数名pid_calc函数名代表对应函数的入口地址直接赋值给函数指针calc,也可将自定义函数的函数名pid_calc先强制类型转换转换为函数指针后,再赋值给函数指针calc;这两种方式虽说能达到同样的效果,但其所反映的思想却有所不同。

  现把代码截取出来,方便大家对比:

void pid_calc(PID *p); //函数声明
void (*calc)(); //函数指针:指向PID计算函数 void main()
{
//将函数的入口地址赋给指针变量
calc=(void (*)(unsigned long))pid_calc; //编译通过(强制类型转换)
calc=pid_calc; //编译通过
}

  7. 代码换行问题

  为了代码的美观调试方便,需涉及到代码换行问题

  在本程序的宏定义语句中使用了”\”,这是宏定义中连接上下行的连接符,表示该宏定义还未结束。

//定义PID控制器的初始值
#define PID_DEFAULTS {0,0, \
,,, \
0.0002, \
,,, \
,,, \
,,,, \
(void (*)(unsigned long))pid_calc}

=======================================================================================

附件一:直接函数调用(C/C++通用)

PID.h文件

//===================================================
//PID.h
//===================================================
#ifndef PID_H
#define PID_H //定义PID计算用到的结构体类型
typedef struct
{
float Ref;         //输入:系统待调节量的给定值
float Fdb;         //输入:系统待调节量的反馈值 //PID控制器部分
float Kp;          //参数:比例系数
float Ki;          //参数:积分系数
float Kd;          //参数:微分系数 float T;          //参数:离散化系统的采样周期 float a0;          //变量:a0
float a1;          //变量: a1
float a2;          //变量: a2 float Err;         //变量:当前的偏差e(k)
float Err_1;     //历史:前一步的偏差e(k-1)
float Err_2;      //历史:前前一步的偏差e(k-2) float Out;        //输出:PID控制器的输出u(k)
float Out_1;    //历史:PID控制器前一步的输出u(k-1)
float OutMax;      //参数:PID控制器的最大输出
float OutMin;      //参数:PID控制器的最小输出 }PID; //定义PID控制器的初始值
#define PID_DEFAULTS {0,0, \
,,, \
0.0002, \
,,, \
,,, \
,,,} //条件编译的判别条件
#define PID_DEBUG 1 //函数声明
void pid_calc(PID *p); #endif //===================================================
//End of file.
//===================================================

PID.c文件

//===================================================
//PID.c
//===================================================
#include "PID.h"
//===================函数定义========================
/****************************************************
*说 明:
* (1)PID控制器默认为PI调节器
* (2)使用了条件编译进行功能切换:节省计算时间
* 在校正PID参数时,使用宏定义将PID_DEBUG设为1;
* 当参数校正完成后,使用宏定义将PID_DEBUG设为0,同时,在初始化时
* 直接为p->a0、p->a1、p->a2赋值
****************************************************/
void pid_calc(PID *p)
{
//使用条件编译进行功能切换
#if (PID_DEBUG)
float a0,a1,a2;
//计算中间变量a0、a1、a2
a0=p->Kp+p->Ki*p->T+p->Kd/p->T;
a1=p->Kp+*p->Kd/p->T;
a2=p->Kd/p->T;
//计算PID控制器的输出
p->Out=p->Out_1+a0*p->Err-a1*p->Err_1+a2*p->Err_2;
#else
//计算PID控制器的输出
p->Out=p->Out_1+p->a0*p->Err-p->a1*p->Err_1+p->a2*p->Err_2;
#endif //输出限幅
if(p->Out>p->OutMax)
p->Out=p->OutMax;
if(p->Out<p->OutMin)
p->Out=p->OutMin; //为下步计算做准备
p->Out_1=p->Out;
p->Err_2=p->Err_1;
p->Err_1=p->Err; } //===================================================
//End of file.
//===================================================

amain.c主函数文件

//===================================================
//amain.c
//=================================================== //将用户定义的头文件包含进来
#include "PID.h" //=============宏定义=====================
#define T0   0.0002       //离散化采样周期,单位s //============全局变量========================
//定义PID控制器对应的结构体变量
PID ASR=PID_DEFAULTS;       //速度PI调节器ASR //定义PID控制器的参数及输出限幅值
float SpeedKp=,SpeedKi=,SpeedLimit=;  //速度PI调节器ASR //===============主程序=======================
void main()
{
//初始化PID控制器
ASR.Kp=SpeedKp;
ASR.Ki=SpeedKi;
ASR.T=T0;
ASR.OutMax=SpeedLimit;
ASR.OutMin=-SpeedLimit; } //============中断服务程序====================
interrupt void T1UFINT_ISR(void)
{
//转速调节ASR
ASR.Ref=input1;         //速度给定
ASR.Fdb=input2;         //速度反馈
ASR.Err=ASR.Ref-ASR.Fdb;    //偏差
pid_calc(&ASR);         //函数调用:启动PID计算
output=ASR.Out;         //读取PID控制器的输出 } //===================================================
//End of file.
//===================================================

=======================================================================================

附件二:使用函数指针进行函数调用(仅适用于C

PID.h文件

//===================================================
//PID.h
//===================================================
#ifndef PID_H
#define PID_H //定义PID计算用到的结构体类型
typedef struct
{
float Ref;       //输入:系统待调节量的给定值
float Fdb;       //输入:系统待调节量的反馈值 //PID控制器部分
float Kp;        //参数:比例系数
float Ki;        //参数:积分系数
float Kd;        //参数:微分系数 float T;        //参数:离散化系统的采样周期 float a0;        //变量:a0
float a1;        //变量: a1
float a2;        //变量: a2 float Err;       //变量:当前的偏差e(k)
float Err_1;      //历史:前一步的偏差e(k-1)
float Err_2;    //历史:前前一步的偏差e(k-2) float Out;       //输出:PID控制器的输出u(k)
float Out_1;    //历史:PID控制器前一步的输出u(k-1)
float OutMax;     //参数:PID控制器的最大输出
float OutMin;     //参数:PID控制器的最小输出 void (*calc)();    //函数指针:指向PID计算函数 }PID; //定义PID控制器的初始值
#define PID_DEFAULTS {0,0, \
,,, \
0.0002, \
,,, \
,,, \
,,,, \
(void (*)(unsigned long))pid_calc} //加与不加强制类型转换都没影响 //条件编译的判别条件
#define PID_DEBUG 1 //函数声明
void pid_calc(PID *p); #endif //===================================================
//End of file.
//===================================================

PID.c文件

//===================================================
//PID.c
//===================================================
#include "PID.h"
//===================函数定义========================
/****************************************************
*说 明:
* (1)PID控制器默认为PI调节器
* (2)使用了条件编译进行功能切换:节省计算时间
* 在校正PID参数时,使用宏定义将PID_DEBUG设为1;
* 当参数校正完成后,使用宏定义将PID_DEBUG设为0,同时,在初始化时
* 直接为p->a0、p->a1、p->a2赋值
****************************************************/
void pid_calc(PID *p)
{
//使用条件编译进行功能切换
#if (PID_DEBUG)
float a0,a1,a2;
//计算中间变量a0、a1、a2
a0=p->Kp+p->Ki*p->T+p->Kd/p->T;
a1=p->Kp+*p->Kd/p->T;
a2=p->Kd/p->T;
//计算PID控制器的输出
p->Out=p->Out_1+a0*p->Err-a1*p->Err_1+a2*p->Err_2;
#else
//计算PID控制器的输出
p->Out=p->Out_1+p->a0*p->Err-p->a1*p->Err_1+p->a2*p->Err_2;
#endif //输出限幅
if(p->Out>p->OutMax)
p->Out=p->OutMax;
if(p->Out<p->OutMin)
p->Out=p->OutMin; //为下步计算做准备
p->Out_1=p->Out;
p->Err_2=p->Err_1;
p->Err_1=p->Err; } //===================================================
//End of file.
//===================================================

amain.c主函数文件

//===================================================
//amain.c
//=================================================== //将用户定义的头文件包含进来
#include "PID.h" //=============宏定义=====================
#define T0   0.0002     //离散化采样周期,单位s //============全局变量========================
//定义PID控制器对应的结构体变量
PID ASR=PID_DEFAULTS;     //速度PI调节器ASR //定义PID控制器的参数及输出限幅值
float SpeedKp=,SpeedKi=,SpeedLimit=;   //速度PI调节器ASR //===============主程序=======================
void main()
{
//初始化PID控制器
ASR.Kp=SpeedKp;
ASR.Ki=SpeedKi;
ASR.T=T0;
ASR.OutMax=SpeedLimit;
ASR.OutMin=-SpeedLimit; } //============中断服务程序====================
interrupt void T1UFINT_ISR(void)
{
//转速调节ASR
ASR.Ref=input1;         //速度给定
ASR.Fdb=input2;         //速度反馈
ASR.Err=ASR.Ref-ASR.Fdb;    //偏差
ASR.calc(&ASR);         //函数调用:启动PID计算
output=ASR.Out;         //读取PID控制器的输出 } //===================================================
//End of file.
//===================================================

PID控制器的数字实现及C语法讲解的更多相关文章

  1. PID控制器开发笔记之十二:模糊PID控制器的实现

    在现实控制中,被控系统并非是线性时不变的,往往需要动态调整PID的参数,而模糊控制正好能够满足这一需求,所以在接下来的这一节我们将讨论模糊PID控制器的相关问题.模糊PID控制器是将模糊算法与PID控 ...

  2. PID控制器开发笔记之十一:专家PID控制器的实现

    前面我们讨论了经典的数字PID控制算法及其常见的改进与补偿算法,基本已经覆盖了无模型和简单模型PID控制经典算法的大部.再接下来的我们将讨论智能PID控制,智能PID控制不同于常规意义下的智能控制,是 ...

  3. PID控制器开发笔记之十三:单神经元PID控制器的实现

    神经网络是模拟人脑思维方式的数学模型.神经网络是智能控制的一个重要分支,人们针对控制过程提供了各种实现方式,在本节我们主要讨论一下采用单神经元实现PID控制器的方式. 1.单神经元的基本原理 单神经元 ...

  4. PID控制器开发笔记之十:步进式PID控制器的实现

    对于一般的PID控制系统来说,当设定值发生较大的突变时,很容易产生超调而使系统不稳定.为了解决这种阶跃变化造成的不利影响,人们发明了步进式PID控制算法. 1.步进式PID的基本思想 所谓步进式PID ...

  5. PID控制器开发笔记之九:基于前馈补偿的PID控制器的实现

    对于一般的时滞系统来说,设定值的变动会产生较大的滞后才能反映在被控变量上,从而产生合理的调节.而前馈控制系统是根据扰动或给定值的变化按补偿原理来工作的控制系统,其特点是当扰动产生后,被控变量还未变化以 ...

  6. PID控制器开发笔记之八:带死区的PID控制器的实现

    在计算机控制系统中,由于系统特性和计算精度等问题,致使系统偏差总是存在,系统总是频繁动作不能稳定.为了解决这种情况,我们可以引入带死区的PID算法. 1.带死区PID的基本思想 带死区的PID控制算法 ...

  7. PID控制器开发笔记之七:微分先行PID控制器的实现

    前面已经实现了各种的PID算法,然而在某些给定值频繁且大幅变化的场合,微分项常常会引起系统的振荡.为了适应这种给定值频繁变化的场合,人们设计了微分先行算法. 1.微分先行算法的思想 微分先行PID控制 ...

  8. PID控制器开发笔记之六:不完全微分PID控制器的实现

    从PID控制的基本原理我们知道,微分信号的引入可改善系统的动态特性,但也存在一个问题,那就是容易引进高频干扰,在偏差扰动突变时尤其显出微分项的不足.为了解决这个问题人们引入低通滤波方式来解决这一问题. ...

  9. PID控制器开发笔记之五:变积分PID控制器的实现

    在普通的PID控制算法中,由于积分系数Ki是常数,所以在整个控制过程中,积分增量是不变的.然而,系统对于积分项的要求是,系统偏差大时,积分作用应该减弱甚至是全无,而在偏差小时,则应该加强.积分系数取大 ...

随机推荐

  1. Python 替换字符串

    string类型是不可变的,因此不能采用直接赋值的方式.比如一个字符串 helloworld,想把o替换成z,那么只有先替换,然后再迭代. strings="helloworld" ...

  2. 引用自定义的framework

    关于静态库引用文件 如果希望你的工程能在未来能导出成静态库,那么在你编写的时候要遵循静态库引用原则,使用这种方式. 注意:这种引用方式必须在你的Products下静态库成黑色时候,才能编译通过. 使用 ...

  3. 查看Oracle执行计划的几种方法

    查看Oracle执行计划的几种方法 一.通过PL/SQL Dev工具 1.直接File->New->Explain Plan Window,在窗口中执行sql可以查看计划结果.其中,Cos ...

  4. linux - 文本处理 及 正则表达式

    先新建一个文件,并写入一些东西,方便测试, 从passwd里复制几行吧 $ /etc/passwd > passwd t$ ll 总用量 drwxrwxr-x huanghao huanghao ...

  5. Objective-C ,ios,iphone开发基础:使用GDataXML解析XML文档,(libxml/tree.h not found 错误解决方案)

    使用GDataXML解析XML文档 在IOS平台上进行XML文档的解析有很多种方法,在SDK里面有自带的解析方法,但是大多情况下都倾向于用第三方的库,原因是解析效率更高.使用上更方便 这里主要介绍一下 ...

  6. javaweb学习总结十六(tomcat服务器的使用、虚拟目录的映射)

    一:tomcat服务器的目录 1:目录结构 2:web应用程序介绍 二:虚拟目录的映射 假如我在E盘建立一个目录web,里面存放1.html文件,我想让外界访问 1:直接在conf目录下的server ...

  7. CF Covered Path (贪心)

    Covered Path time limit per test 1 second memory limit per test 256 megabytes input standard input o ...

  8. ClassLoader类加载器

    总的来说,当动态加载一个资源时,至少有三种类加载器可供选择: 系统类加载器(也被称为应用类加载器)(system classloader) 当前类加载器(current classloader) 当前 ...

  9. 微信 redirect_uri参数错误 正确的处理

    如果您若成功将微信搭建了到自己的服务器中的情况下,进行网页授权时出现如下图 解决方案: 开发->接口权限->找到类目为"网页服务->网页账号" 点击修改,注意,此 ...

  10. Apache Commons 简述

    Apache Commons 是一个关注于可复用的 Java 组件的 Apache 项目.Apache Commons 由三部分构成: Commons Proper - 一个可复用的 Java 组件库 ...