PID控制器的数字实现及C语法讲解
为方便学习与交流,根据自己的理解与经验写了这份教程,有错误之处请各位读者予以指出,具体包含以下三部分内容:
(1) PID数字化的推导过程(实质:微积分的近似计算);
(2) 程序风格介绍(程序风格来源于TI官方案例);
(3) C有关语法简述(语法会结合实例进行讲解)。
PID控制器是工业过程控制中广泛采用的一种控制器,其中,P、I、D分别为比例(Proportion)、积分(Integral)、微分(Differential)的简写;将偏差的比例、积分和微分通过线性组合构成控制量,用该控制量对受控对象进行控制,称为PID算法。
为了用软件实现PID算法,需将PID控制器离散化。
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;
分析:
第一步:整体分析
该语句表示定义了一个数据类型Myint(这里Myint为数据类型标识符);
(至于其具体表示什么类型,请看下步分析)
第二步:正常变量声明(去掉typedef)
int Myint;
其所表示的类型为变量Myint(这里Myint为变量标识符)的数据类型,即整型类型。
应用:
Myint a; //声明整型变量a
[例2]
typedef struct { //省略成员 }PID;
分析:
第一步:整体分析
该语句表示定义了一个数据类型PID(这里PID为类型标识符)
(至于其具体表示什么类型,请看下步分析)
第二步:正常变量声明(去掉typedef)
struct { //省略成员 }PID;
其所表示的类型为结构体变量PID(这里PID为变量标识符)的数据类型,即结构体类型,且其具有的成员同结构体变量PID。
应用:
PID ASR; //定义结构体变量ASR
[例3]
typedef void (*PFun)(int );
分析:
第一步:整体分析
该语句表示定义了一个数据类型PFun(这里PFun为类型标识符)
(至于其具体表示什么类型,请看下步分析)
第二步:正常变量声明(去掉typedef)
void (*PFun)(int );
其所表示的类型为函数指针PFun(这里PFun为变量标识符)的数据类型,即函数指针类型,且指针所指向的函数类型:形参为整型,无返回值的一类函数。
应用:
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`(函数名代表对应函数的入口地址)使用强制类型转换(转换为函数指针类型),也可不用,我都调试验证过;现把程序截取出来,方便大家理解:
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,0,0, \ 0.0002, \ 0,0,0, \ 0,0,0, \ 0,0,0,0, \ (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,0,0, \ 0.0002, \ 0,0,0, \ 0,0,0, \ 0,0,0,0} //条件编译的判别条件 #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+2*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=2,SpeedKi=1,SpeedLimit=10; //速度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=2; //速度给定 ASR.Fdb=1; //速度反馈 ASR.Err=ASR.Ref-ASR.Fdb; //偏差 pid_calc(&ASR); //PI计算(直接函数调用) } //=================================================== //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,0,0, \ 0.0002, \ 0,0,0, \ 0,0,0, \ 0,0,0,0, \ (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+2*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=2,SpeedKi=1,SpeedLimit=10; //速度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=2; //速度给定 ASR.Fdb=1; //速度反馈 ASR.Err=ASR.Ref-ASR.Fdb; //偏差 ASR.calc(&ASR); //PI计算_调用PID计算函数 } //=================================================== //End of File //===================================================