PID算法浅析


首先帮大家解决一下什么是PID调节,为什么就要这样的疑惑。

PID是比例,积分,微分的英文单词的首字母的简称。

P: Proportion(比例),就是输入偏差乘以一个常数

I :  Integral    (积分),就是对输入偏差进行积分运算

D: Derivative(微分),对输入偏差进行微分运算

(输入偏差 = 读出的被控制对象的值-设定值。比如说我要把温度控制在30度,但是现在我从温度传感器上读出温度为28度。则这个30度就是“设定值”,28度就是“读出的被控制对象的值”。)

P,打个比方,如果现在的输出是1,目标输出是100,那么P的作用是以最快的速度达到100,把P理解为一个系数即可;而I呢?大家学过高数的,0的积分才能是一个常数,I就是使误差为0而起调和作用;D呢?大家都知道微分是求导数,导数代表切线是吧,切线的方向就是最快到至高点的方向。这样理解,最快获得最优解,那么微分就是加快调节过程的作用了。

 

(现今,计算机已然进入了控制领域,用数字计算机代替模拟计算机调节器组成计算机控制系统,可以用软件实现PID控制算法,而且可以利用计算机的逻辑功能,使PID控制更加灵活,在模拟控制系统中,控制器最常用的控制规律是PID控制。模拟PID控制系统原理框图如下:系统由PID控制器和被控对象组成)



下面举个例子说明一下PID,让大家有个感官的认识,。

一个人闭眼走路,假设他知道自己离目的地有100米远,那么他就可以以每秒一米一步这样的速度走向目的地,100米刚刚好是100步,这是一个非常理想化的现象。假设他不知道目的地有多远,目的地可能是1000米也有可能是10000米,他用每秒每步3米得速度向前,很不巧的是这个目的地在80米处,他走了26步时刚刚好差2米,走27步有刚刚好又多出1米,这就是所谓的稳态误差,如果这个人知道目的地在大概15米处得地方,开始这个人以每秒一米一步的速度,走完一步然后目测一下离目的地还有多远,结果发现还剩下大概14米,显然一米一步太慢,因此这个人决定每秒大于一米一步走,得出一条式子,

y=Kp*e(t)

其中y为下一次要每步要走的距离,e(t) 为目测距离,也就是偏差,换句话说就是自己走了的距离跟要走的距离也就是目的地的误差,Kp就是一个常数,假设我们把Kp设置为0.5,             

y=Kp*e(t)可以得出y=7;也就是说那个人下一步要以每秒7米得速度走,重复上述的过程,,7+1共走了8米,然后目测一下距离15米处还有多远,还有7米得误差,所以下一步要走3.5米,然后在重复,发现最后会出现一个稳态的误差,也就是多走一步会超出目的地,少走一步又没到目的地。当然这个上述的例子情况非常特殊,大家可能觉得最后那些误差可以忽略,但是实际应用中,肯定没有人走路的那么特殊,按照这种线性比例下去最后得到的误差会非常大,所以就引入了一个积分的概念,积分的数学几何定义是在区间[a, b]里连续的非负曲线与直线x=a,x=b围成的图形的面积。从积分的定义可以得到一个函数

其中Ti为积分时间,e(t)就是误差了。Y就是输出,它是个不定积分,事实上把它融入到上述人走路的例子它是个定积分,从0 到t时刻的误差的对时间的积分,也就是说误差曲线et)与时间轴围成的面积,积分时间Ti是一个常量,也就是说是自己规定大小,很明显,由上式得y为e(t)与t所围成的图形的面积的除以Ti的值,Ti越大y越小,Ti越小y越大,大了系统会动荡,所以要慢慢调节系数。

下面是关于积分跟比例的专业阐述:

 

       比例(P)控制

比例控制是一种最简单的控制方式。其控制器的输出与输入误差信号成比例关系。当仅有比例控制时系统输出存在稳态误差(Steady-state error)。

 

  积分(I)控制

在积分控制中,控制器的输出与输入误差信号的积分成正比关系。对一个自动控制系统,如果在进入稳态后存在稳态误差,则称这个控制系统是有稳态误差的或简称有差系统(System with Steady-state Error)。

为了消除稳态误差,在控制器中必须引入“积分项”。积分项对误差取决于时间的积分,随着时间的增加,积分项会增大。这样,即便误差很小,积分项也会随着时间的增加而加大,它推动控制器的输出增大使稳态误差进一步减小,直到等于零。

因此,比例+积分(PI)控制器,可以使系统在进入稳态后无稳态误差。

       微分调节就是偏差值的变化率。例如,如果输入偏差值线性变化,则在调节器输出侧叠加一个恒定的调节量。大部分控制系统不需要调节微分时间。因为只有时间滞后的系统才需要附加这个参数。如果画蛇添足加上这个参数反而会使系统的控制受到影响。

      

PID控制器是一种线性控制器,它根据给定值Yd(t)与实际输出值Y(t)构成的控制偏差

PID的控制规律为:

其中:

Kp:比例系数(成比例的控制系统的偏差信号error(t),偏差一旦产生,控制器立即产生控制作用,以减少偏差)

Ti  :积分时间常数(积分环节主要用于消除静差,提高系统的无差度,积分作用的强弱取决于积分时间常数Ti,Ti越大,积分作用越弱,反之则越强)

Td:(微分时间常数)(反映偏差信号的变化趋势(变化速率),并能在偏差信号变得太大之前,在系统中引入一个有效的早期修正信号,从而加快系统的动作速度,减少调节时间)

 




举个例子,人去调节窝炉的温度,慢慢调节旋钮,使得温度慢慢变大,要使得温度达到某个固定值,人可以慢慢调节,边看温度边调节,如果开始离这个这目标温度远就快速旋旋钮(比例效果),到最后要使得温度误差小就微调(积分效果),然后实际上温度是有一个惯性在那里,开始你以很快速度调节旋钮的时候温度不会突变,不会一下子就达到稳定值,它慢慢增加到最后,但是不是每个人都是这么有经验,当他看到温度值离目标温度还差这么远,又加快旋转旋钮,最终结果导致实际温度跟目标温度差别非常远,微调也跟本没法调整,最后导致系统的不稳定,但是如果这个人很有经验,他事先知道这个温度是有惯性的,开始它快速旋转旋钮看温度上升率非常高,也就是温度变化非常快,他就放慢旋转速度了,最后结果是准确的把温度调整到最佳(微分效果)。

人可以是这样子,但是计算机可不会这样调节,那么就要通过一个PID得到一个输出值来调节了。

下面是一段关于微分的专业阐述:

       控制器的输出与输入误差信号的微分(即误差的变化率)成正比关系。 自动控制系统在克服误差的调节过程中可能会出现振荡甚至失稳。其原因是由于存在有较大惯性组件(环节)或有滞后(delay)组件,具有抑制误差的作用,其变化总是落后于误差的变化。解决的办法是使抑制误差的作用的变化“超前”,即在误差接近零时,抑制误差的作用就应该是零。这就是说,在控制器中仅引入 “比例”项往往是不够的,比例项的作用仅是放大误差的幅值,而目前需要增加的是“微分项”,它能预测误差变化的趋势,这样,具有比例+微分的控制器,就能 够提前使抑制误差的控制作用等于零,甚至为负值,从而避免了被控量的严重超调。所以对有较大惯性或滞后的被控对象,比例+微分(PD)控制器能改善系统在 调节过程中的动态特性。

       综上所述得到一个一条公式,这个就是模拟PID

下面是关于应用,增量式PID算法。其实PID的算法可以做很深,但没必要,一般入门级的算法已经在很多场合够用了,这里之所以选用增量式PID算法(另外还有位置式PID等等),因为增量式PID算法运算量少,非常适合单片机的应用。

       显然要想给单片机运算,就必须是数字量,而上述的PID是模拟PID,我们要将他数字化,离散化。

       其中积分在上面说到的,他的几何意义就是求e(t)与时间轴t围成的图形的面积,将这个面积分成T等分  ,T=0到T=1跟e(t)围成的面积加上T=1到T=2跟e(t)围成的面积一直累加……直到T-1到T跟e(t)围成的面积刚好就是整个e(t)与t时间轴围成的面积,刚刚好是e(t)对t的积分,如果T无限大,那么就可以分割成无限个小图形那么这个图形的面积就可以用T[e(1)+e(2)+………+e(T-1)+e(T)]来代替积分效果,而这个T等分就是AD在整个时间轴t中采样的点,显然越快的AD在相同的时间t里面采样的点越多,换句话说就是T更接近无限大。因此积分可以用累和代替

 

下面为积分的专业的解释

定义

  设函数f(x)在[a,b]上有界,在[a,b]中任意插入若干个分点

  a=x0

  把区间[a,b]分成n个小区间

  [x0,x1],...[xn-1,xn]。

在每个小区间[xi-1,xi]上任取一点ξi(xi-1≤ξi≤xi),作函数值f(ξi)与小区间长度的乘积f(ξi)△xi,并作出和

 如果不论对[a,b]怎样分法,也不论在小区间上的点ξi怎样取法,只要当区间的长度趋于零时,和S总趋于确定的极限I,

这时我们称这个极限I为函数f(x)在区间[a,b]上的定积分, 记作

微分用差分代替,先说明一下微分的几何意义

我们可以想象把上图中的f(x)换成e(t),x轴换成t轴,把△x换成△t,当△t非常小的时候曲线MN等价于直线MN,△y就等于dy,所以

可以用Td*[e(t)-e(t-1)]/ △t,同样△t就是采样时间~越小越好。

 

因此模拟PID离散化得到在k-1时刻的输出

因此得到一个增量

其中的T为采样时间

   ,如果计算机控制系统采用恒定的采样周期T,一旦确定A、B、C(系数的选取是PID的关键这里不做讨论)

 


然后要知道PID算法具体分两种:一种是位置式的 ,一种是增量式的。在小车里一般用增量式,为什么呢?位置式PID的输出与过去的所有状态有关,计算时要对e(每一次的控制误差)进行累加,这个计算量非常大,而明显没有必要。而且小车的PID控制器的输出并不是绝对数值,而是一个△,代表增多少,减多少。换句话说,通过增量PID算法,每次输出是PWM要增加多少或者减小多少,而不是PWM的实际值。所以明白增量式PID就行了。



增量式PID控制算法与位置式PID算法相比,计算量小得多,因此在实际中得到广泛的应用。

位置式PID控制算法也可以通过增量式控制算法推出递推计算公式:

就是目前在计算机控制中广泛应用的数字递推PID控制算法。


接着说PID参数的整定,也就是PID公式中,那几个常数系数Kp,Ti,Td等是怎么被确定下来然后带入PID算法中的。如果要运用PID,则PID参数是必须由自己调出来适合自己的项目的。通常四旋翼,自平衡车的参数都是由自己一个调节出来的,这是一个繁琐的过程。本次我们可以不管,关于PID参数怎么确定的,网上有很多经验可以借鉴。比如那个经典的经验试凑口诀:
                         参数整定找最佳, 从小到大顺序查。
                         先是比例后积分, 最后再把微分加。
                         曲线振荡很频繁, 比例度盘要放大。
                         曲线漂浮绕大弯, 比例度盘往小扳。
                         曲线偏离回复慢, 积分时间往下降。
                         曲线波动周期长, 积分时间再加长。
                         曲线振荡频率快, 先把微分降下来。
                         动差大来波动慢, 微分时间应加长。
                         理想曲线两个波, 前高后低四比一。
                         一看二调多分析, 调节质量不会低。

 


接下来我们用例子来辅助我们把常用的PID模型讲解了。(PID控制并不一定要三者都出现,也可以只是PI、PD控制,关键决定于控制的对象。)(下面的内容只是介绍一下PID模型,可以不看,对理解PID没什么用)
例子:我们要控制一个人,让他一PID的控制方式来行走110步后停下来。
1)P比例控制,就是让他按照一定的比例走,然后停下。比如比例系数为108,则走一次就走了108步,然后就不走了。
说明:P比例控制是一种最简单的控制方式,控制器的输出与输入误差信号成比例关系。但是仅有比例控制时系统输出存在稳态误差。比如上面的只能走到108,无论怎样都走不到110。
2)PI积分控制,就是按照一定的步伐走到112步然后回头接着走,走到108步位置时,然后又回头向110步位置走。在110位置处来回晃荡几次,最后停在110步的位置。
说明:在积分I控制中,控制器的输出与输入误差信号的积分成正比关系。对一个自动控制系统来说,如果在进入稳态后存在稳态误差,则称这个控制系统是有稳态误差的或简称有差系统。为了消除稳态误差,在控制器中必须引入“积分项”。积分项对误差的影响取决于时间的积分,随着时间的增加,积分项会增大。这样,即便误差很小,积分项也会随着时间的增加而加大,它推动控制器的输出增大,从而使稳态误差进一步减小,直到等于0。因此,比例+积分(PI)控制器可以使系统在进入稳态后无稳态误差。
3)PD微分控制,就是按照一定的步伐走到一百零几步后,再慢慢地走向110步的位置靠近,如果最后能精确停在110步的位置,就是无静差控制;如果停在110步附近(如109步或111步位置),就是有静差控制。
说明:在微分控制D中,控制器的输出与输入误差信号的微分(即误差的变化率)成正比关系。
      自动控制系统在克服误差的调节过程中可能会出现振荡甚至失稳,原因是存在较大惯性组件(环节)或滞后组件,具有抑制误差的作用,其变化总是落后于误差的变化。解决的办法是使抑制误差作用的变化“超前”,即在误差接近于零时,抑制误差的作用就应该是零。这就是说,在控制器中仅引入“比例P”项往往是不够的,比例项的作用仅是放大误差的幅值,而目前需要增加的是“微分项”,它能预测误差变化的趋势。这样,具有比例+微分的控制器就能够提前使抑制误差的控制作用等于零,甚至为负值,从而避免了被控量的严重超调。所以对有较大惯性或滞后的被控对象,比例P+微分D(PD)控制器能改善系统在调节过程中的动态特性。



五、用小明来说明PID:
       小明接到这样一个任务:有一个水缸有点漏水(而且漏水的速度还不一定固定不变),要求水面高度维持在某个位置,一旦发现水面高度低于要求位置,就要往水缸里加水。 小明接到任务后就一直守在水缸旁边,时间长就觉得无聊,就跑到房里看小说了,每30分钟来检查一次水面高度。水漏得太快,每次小明来检查时,水都快漏完了,离要求的高度相差很远,小明改为每3分钟来检查一次,结果每次来水都没怎么漏,不需要加水,来得太频繁做的是无用功。几次试验后,确定每10分钟来检查一次。这个检查时间就称为采样周期。 开始小明用瓢加水,水龙头离水缸有十几米的距离,经常要跑好几趟才加够水,于是小明又改为用桶加,一加就是一桶,跑的次数少了,加水的速度也快了,但好几次将缸给加溢出了,不小心弄湿了几次鞋,小明又动脑筋,我不用瓢也不用桶,老子用盆,几次下来,发现刚刚好,不用跑太多次,也不会让水溢出。这个加水工具的大小就称为比例系数。
       小明又发现水虽然不会加过量溢出了,有时会高过要求位置比较多,还是有打湿鞋的危险。他又想了个办法,在水缸上装一个漏斗,每次加水不直接倒进水缸,而是倒进漏斗让它慢慢加。这样溢出的问题解决了,但加水的速度又慢了,有时还赶不上漏水的速度。于是他试着变换不同大小口径的漏斗来控制加水的速度,最后终于找到了满意的漏斗。漏斗的时间就称为积分时间。
       小明终于喘了一口,但任务的要求突然严了,水位控制的及时性要求大大提高,一旦水位过低,必须立即将水加到要求位置,而且不能高出太多,否则不给工钱。小明又为难了!于是他又开努脑筋,终于让它想到一个办法,常放一盆备用水在旁边,一发现水位低了,不经过漏斗就是一盆水下去,这样及时性是保证了,但水位有时会高多了。他又在要求水面位置上面一点将水缸要求的水平面处凿一孔,再接一根管子到下面的备用桶里这样多出的水会从上面的孔里漏出来。这个水漏出的快慢就称为微分时间。



六、在代码中理解PID:(好好看注释,很好理解的。注意结合下面PID的公式)
首先看PID的增量型公式:
PID=Uk+KP*【E(k)-E(k-1)】+KI*E(k)+KD*【E(k)-2E(k-1)+E(k-2)】
在单片机中运用PID,出于速度和RAM的考虑,一般不用浮点数,这里以整型变量为例来讲述PID在单片机中的运用。由于是用整型来做的,所以不是很精确。但是对于一般的场合来说,这个精度也够了,关于系数和温度在程序中都放大了10倍,所以精度不是很高,但是大部分的场合都够了,若不够,可以再放大10倍或者100倍处理,不超出整个数据类型的范围就可以了。一下程序包括PID计算和输出两部分。当偏差>10度时全速加热,偏差在10度以内时为PID计算输出。
程序说明:下面的程序,先看main函数。可知在对定时器0初始化后就一直在执行PID_Output()函数。在PID_Output()函数中先用iTemp变量来得到PID运算的结果,来决定是启动加热丝加热还是不启动加热丝。下面的if语句结合定时器来决定PID算法多久执行一次。PID_Operation()函数看似很复杂,其实就一直在做一件事:根据提供的数据,用PID公式把最终的PID值算出来。



#include 
typedef unsigned char      uChar8;      
typedef unsigned int       uInt16;
typedef unsigned long int  uInt32;
 
sbit 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算法不是一直在运算。而是每隔一定时间,算一次。
/* ********************************************************
/* 函数名称: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) 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;
    }
}


本文参考与(谢谢啦!!!):http://blog.csdn.net/chenbang110/article/details/9302519

                                               :http://www.arduino.cn/thread-12813-1-1.html


你可能感兴趣的:(算法,STM32-ARM嵌入式)