单片机/MCU论坛
登录
直播中
xymbmcu
12年用户
1025经验值
擅长:可编程逻辑
私信
关注
[文章]
四轴飞行器的设计之PID算法的分类与应用实例-STC15单片机实战指南连载
单片机
飞行器
四轴飞行器
上一节讲了PID算法概述(见帖子
https://bbs.elecfans.com/jishu_580161_1_1.html
),继续讲PID算法的分类与应用实例。
将偏差的比例(Propor
ti
on)、积分(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在单片机中的应用,等大家以后学了像
STM32
F4这样的M4核处理器以后,就可以考虑用浮点数来计算了。由于是用整型来做的,所以不是很精确,但是对于一般的场合来说,这个精度也够了,关于系数和温度笔者都统统放大了10倍,所以精度不是很高。但是也不是那么低,大部分的场合也是够了。实在觉得精度不够,可以再放大10倍或者100倍处理,但是要注意不能超出整个数据类型的范围就可以了。本程序包括PID计算和输出两部分,当偏差>10度全速加热,偏差在10度以内为PID计算输出,具体的参考代码参见下面(该实例以飛天一號
开发板
为硬件平台)。
#include
typedef unsigned char uChar8;
typedef unsigned int uInt16;
typedef unsigned long int uInt32;
***it 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)
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;
}
}
难理解的加了注释,不难理解的,没必要说。剩下的就是将温度传感器部分的子程序综合进来,之后动手自己做实物,并调试程序,只有多实践,才是“玩”好单片机王道,因此强烈建议读者以FSST15开发板为平台,同时借助LM75A温度传感器,再搭建一个温控设备,亲自进行试验,在实际中不断提高自己的能力。
这里读者需要注意,前面讲述的口诀法和经验值法似乎没用到,那是因为PID算法除了增量式以外,还有两种:位置式和微分先行法,这些知识点,才是后面四轴控制的核心,笔者放后面再来讲述,感兴趣的读者可以先去看看下面链接的博文,写的甚好,通俗易懂。
/*
http://blog.sina.com.cn/s/blog_7c7e2d5a01011ta9.html
*/
老外的位置式PID算法
该PID算法例程摘自网络,作者不详,版权归原创作者所有。为了保持地道,笔者没有做一点点改动,其中中文注释为笔者所加。平心而论,老外的代码确实很独特,思路也很清晰,源码如下,具体含义留读者慢慢研究了,这里不赘。
#include
#include
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 %4fn", process_point, set_point, p_gain, i_gain, d_gain);
printf("Enter the values of Process pointn");
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("%fn", display_value);
//printf("n%f%f%f%f",warm.pv,warm.sp,warm.igain,warm.dgain);
count++;
}
}
最后说明下
和本文配套的STC15
开发板
目前正在
电子
发烧友销售,如果需要请戳这里购买:
https://bbs.elecfans.com/product/stc15.html
下一节讲
四轴飞行器的硬件模型建立
。
回帖
(9)
YiSuan210
2016-4-13 18:30:52
大学学的就是这个PID,熟悉的陌生人
大学学的就是这个PID,熟悉的陌生人
举报
奋斗小青年1314
2016-4-26 09:54:35
请问一下,老外的PID算法,就是第二个,最后PID输出的result为什么没有限幅?只是把比例和积分限幅了?最终的输出也有可能超过100啊?
请问一下,老外的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,牛逼,学习学习
厉害1,牛逼,学习学习
举报
更多回帖
rotate(-90deg);
回复
相关帖子
单片机
飞行器
四轴飞行器
四
轴
飞行器
的设计
之
PID
算法
概述-
STC15
单片机
实战
指南
连载
8091
四
轴
飞行器
的设计
之
四
元数与滤波
算法
——
STC15
单片机
实战
指南
连载
6693
四
轴
飞行器
的设计
之
四
轴
主板的综合程序——
STC15
单片机
实战
指南
连载
11462
四
轴
飞行器
的设计
之
搭建
四
轴
飞行器
的遥控
器
-
STC15
单片机
实战
指南
连载
5974
四
轴
飞行器
的设计之主板硬件模型-
STC15
单片机
实战
指南
连载
13206
四
轴
飞行器
的设计
之
PID
控制电机的参数整定——
STC15
单片机
实战
指南
连载
9824
四
轴
飞行器
的设计
之
四
轴
的运行状况与电机转动的关系-
STC15
单片机
实战
指南
连载
7225
基于
STC15
单片机
的
四
轴
飞行器
系统设计
45124
四
轴
飞行器
的
PID
算法
4828
STC15
四
轴
飞行器
的遥控
器
电路原理图免费下载
13
发帖
登录/注册
20万+
工程师都在用,
免费
PCB检查工具
无需安装、支持浏览器和手机在线查看、实时共享
查看
点击登录
登录更多精彩功能!
首页
论坛版块
小组
免费开发板试用
ebook
直播
搜索
登录
×
20
完善资料,
赚取积分