【PID系列】PID代码设计

1、PID对象设计

   观察公式(21),发现我们控制PID,需要的变量主要有:

(1)我们要求解的参数有:

PID输出值output

(2)系统正常运行必须输入的参数有

比例常数Kp,

积分常数Ki,

微分常数Kd,

实时反馈值feedbackVal,

设定值setpoint,

采样时间samplingTime

(3)运行构成中需要求解的过程变量有:

当前偏差值error,

比例值proportion,

积分值integral,

求积分时,都是在上一次的积分值加上这次的偏差,所以也要记录上次的积分值lastIntegral,

微分值differential,

微分值等于本次的偏差减去上次的偏差,所以也要记录上次的偏差值lastError

   以上是PID中使用到的所有变量,我们将上述所有变量放入一个结构体中,定义该结构体为PID控制对象。上述参数中,某些参数可以以局部参数代替,我们暂时全部放入结构体,后期创建函数的时候,我们再优化。

定义结构体如下:

    typedef  struct PID_OBJ_TAG     //PID结构体对象
    {
        float Kp;           //比例常数
        float Ki;           //积分常数
        float Kd;           //微分常数
        float feedbackVal;  //实时反馈值
        float setpoint;     //设定值
        float samplingTime; //采样时间
        float error;        //当前偏差
        float lastError;    //上次偏差
        float proportion;   //比例值
        float integral;     //积分值
        float lastIntegral; //上一次积分
        float differential;//微分值
    }PID_OBJ_t;

PID对象我们已经创建完成。

我们在PID头文件内做一个宏定义,用来定义需要用到几个PID

    #define PID_MAX_NUM     3   //定义需要用到3个PID    

在PID c文件内定义静态变量PID对象。

static  PID_OBJ_t PID_Object[PID_MAX_NUM];//定义PID参数对象

   我们定义了使用的PID对象的最大数量,那我们需要在PID对象还要再添加一个变量,来表示该对象有没有被使用,在使用之前我们将其设置为true,表示正在使用,使用结束后,将其设置为false,表示不再使用。该变量类型为bool,我们命名为used。这样PID对象就变为如下所示:

    typedef  struct PID_OBJ_TAG     //PID结构体对象
    {
        float Kp;           //比例常数
        float Ki;           //积分常数
        float Kd;           //微分常数
        float feedbackVal;  //实时反馈值
        float setpoint;     //设定值
        float samplingTime; //采样时间
        float error;        //当前偏差
        float lastError;    //上次偏差

        float integral;     //积分值
        float lastIntegral; //上一次积分
        float differential;//微分值
        bool  used;         //表示该对象是否被使用
    }PID_OBJ_t;

2、PID接口参数

   我们从上述描述中看到,PID要正常运行,某些参数需要在开始之前进行传值,比如比例常数Kp,积分常数Ki, 微分常数Kd等等。

方案一 将PID控制对象结构体开放给调用者。

   将PID控制对象结构体开放给调用者,调用者在调用PID相关函数之前,自己对相关变量进行初始化。该方案对调用者来说,操作相当灵活。这是优点也是缺点。就像权力需要关进制度的笼子里面,才能发挥它该有的积极作用,否则可能就是消极作用甚至会产生毁灭。这种方案的缺点是,不仅把需要赋值的参数开放给了调用者,其他的比如过程参数等也开放给了调用者,这样,其他不需要调用者控制的参数调用者也有权控制,导致这些参数就会有被莫名更改的风险。另外,给调用者开放的参数过多,调用者就想要搞明白每一个参数的意义,但是对于调用者来说,无需知道其他参数的意义,这样不利于调用者快速学会使用该PID模块。一句话概括就是,这种方案不满足最少知识原则(迪米特法则)。

方案二 将需要赋值的参数单独封装起来,作为接口参数,通过函数对PID对象进行赋值。

   该方案值对用户开放有限的变量,用户只需要明白几个需要赋值的变量的意义,无需知道其他变量的意义就可以进行PID控制,对调用者来说比较友好,我们使用该方案。

   我们来定义一个PID对象配置结构体,作为接口参数,通过该结构体,对PID对象中的某些参数赋值 配置结构体定义如下:

typedef struct PID_CFG_TAG
{
    float Kp;           //比例常数
    float Ki;           //积分常数
    float Kd;           //微分常数
    float samplingTime; //采样时间
}PID_CGF_t;

   有些人可能会问,为什么不把设定值和反馈值也加进去,它也是需要赋值才能正常工作的呀。主要是因为设定值和反馈值是实时变化的,放在这种开始前配置一次的的变量里面不合适,这两个变量我们在后面通过函数形参的形式直接提供更加合适一点。

3、PID接口函数

   接下来,我们需要创建一个传递这个配置参数的函数。函数原型如下: ```c PID_Id PID_New(PID_CGF_t * pParam); ```

   该函数传递配置参数进去,返回PID对象的指针,但是对于调用者来说,没必要知道PID对象的细节,所以我们对PID对象的指针进行二次封装,命名为PID的id号(PID_Id),表示当前PID的识别号,这样调用者就不会去追究PID对象的细节问题,有利于PID的使用。

我们在头文件中定义PID的ID号的类型如下:

typedef void * PID_Id;   //定义空指针为PID的ID号

函数PID_New的具体内容如下所示:

/**---------------------------------------------------------------------------------------
 函数原型:  PID_Id PID_New(PID_CGF_t * pParam)
 功    能:  分配PID对象,配置参数并返回PID id号
 输入参数:	pParam:PID配置参数
 输出参数:	如果PID对象创建成功,则返回ID号,如果创建不成功,则返回NULL
 返 回 值:	NA
 注意事项:	
---------------------------------------------------------------------------------------*/
PID_Id PID_New(PID_CGF_t * pParam)
{
    if(pParam == NULL)//配置参数为空,则创建失败
    {
        return NULL;
    }

    for(int i = 0; i < PID_MAX_NUM; i++)
    {
        PID_OBJ_t *pThis;

        pThis = &PID_Object[i];

        if(!pThis->used)//寻找没有被使用的PID对象
        {
            pThis->Kp = pParam->Kp;
            pThis->Ki = pParam->Ki;
            pThis->Kd = pParam->Kd;
            pThis->samplingTime = pParam->samplingTime;
            pThis->used = true;
            return pThis;
        }
    }
    return NULL;
}

   到此,我们配置好PID对象了,获得了PID对象的id号,接下来我们通过id号来进行PID控制工作。

4、PID控制

   我们来创建一个函数,通过该函数完成PID的控制工作。 函数原型如下: ```c float PID_Work(PID_Id id, float setpoint, float feedbackVal); ```

   该函数带有3个形参,分别为PID对象Id号,设定值setpoint和实时反馈值feedbackVal。 该函数返回值为浮点型,代表计算出来的输出值output。计算出的输出值output可以直接送给执行机构去执行。

下面我们来写出函数体:

/*---------------------------------------------------------------------------------------
 函数原型:  float PID_Work(PID_Id id, float setpoint, float feedbackVal)
 功    能:  PID控制
 输入参数:	id:PID Id号
            setpoint:设定值
            feedbackVal:反馈值
 输出参数:	NA
 返 回 值:	NA
 注意事项:	1、该函数需要以采样周期时间轮询调用
---------------------------------------------------------------------------------------*/
float PID_Work(PID_Id id, float setpoint, float feedbackVal)
{
    PID_OBJ_t pThis;
    float IntegralVal;

    //判断输入参数是否合法,保证程序的健壮性
    if(id == NULL)
    {
        return 0;
    }

    //将id降至类型转化为PID对象
    pThis = (PID_OBJ_t)id;

    //判断对象是否已经初始化
    if(!pThis->used)
    {
        return 0;
    }

    //求出偏差,偏差 = 当前设定值 - 当前反馈值
    pThis->error = setpoint - feedbackVal;

    //求比例项,比例项 = 比例常数Kp*偏差
    pThis->proportion = pThis->Kp * pThis->error;

    //求积分值,积分值 = 上一次计算得出的积分值 + 本次的偏差
    IntegralVal = pThis->lastIntegral +  pThis->error * pThis->samplingTime;
    //求积分项,积分项 =  积分常数 * 积分值 
    pThis->integral = pThis->Ki * IntegralVal;

    //保存本次的积分值,作为下一次积分中上一次计算得出的积分值
    This->lastIntegral = IntegralVal;

    //求微分项,微分项 = 微分常数 * (本次偏差 - 上一次偏差)
    pThis->differential = pThis->Kd * (pThis->error  - pThis->lastError ) / pThis->samplingTime;

    //保存本次偏差值作为下一次计算的上次偏差
    pThis->lastError = pThis->error;

    //求PID最终输出值
    pThis->output = pThis->proportion + pThis->integral + pThis->differential;

    return pThis->output;
}

   上面就是PID控制函数,明白了原理,再来码代码其实就非常简单。不过有些眼尖的朋友可能已经发现了,我们定义的PID对象中有些变量并没有用到,所以接下来我们就需要对整个PID进行优化了,其中有些过程参数完全可以用局部变量代替。优化过程我就不详细描述了。在这里贴出优化完成的PID头文件和c文件代码。

PID头文件如下:

#ifndef __PID_H
#define __PID_H
#include <stdint.h>

#define PID_MAX_NUM     3   //定义需要用到3个PID   

typedef struct PID_CFG_TAG
{
    float Kp;           //比例常数
    float Ki;           //积分常数
    float Kd;           //微分常数
    float samplingTime; //采样时间
}PID_CGF_t;

typedef void * PID_Id;   //定义空指针为PID的ID号

/**---------------------------------------------------------------------------------------
 函数原型:  PID_Id PID_New(PID_CGF_t * pParam)
 功    能:  分配PID对象,配置参数并返回PID id号
 输入参数:	pParam:PID配置参数
 输出参数:	如果PID对象创建成功,则返回ID号,如果创建不成功,则返回NULL
 返 回 值:	NA
 注意事项:	
---------------------------------------------------------------------------------------*/
extern PID_Id PID_New(PID_CGF_t * pParam);

/*---------------------------------------------------------------------------------------
 函数原型:  float PID_Work(PID_Id id, float setpoint, float feedbackVal)
 功    能:  PID控制
 输入参数:	id:PID Id号
            setpoint:设定值
            feedbackVal:反馈值
 输出参数:	NA
 返 回 值:	NA
 注意事项:	1、该函数需要以采样周期时间轮询调用
---------------------------------------------------------------------------------------*/
extern float PID_Work(PID_Id id, float setpoint, float feedbackVal);

#endif /*__PID_H*/


PID c文件如下:

#include <stdint.h>
#include <stdbool.h>
#include "PID.h"

/*--------------------------------------------------------------------------------------
 							内部数据结构定义
--------------------------------------------------------------------------------------*/

typedef  struct PID_OBJ_TAG     //PID结构体对象
{
    float Kp;           //比例常数
    float Ki;           //积分常数
    float Kd;           //微分常数
    float samplingTime; //采样时间
    float lastError;    //上次偏差
    float lastIntegral; //上一次积分
    bool  used;         //表示该对象是否被使用
}PID_OBJ_t;



static  PID_OBJ_t PID_Object[PID_MAX_NUM];//定义PID参数对象


/**---------------------------------------------------------------------------------------
 函数原型:  PID_Id PID_New(PID_CGF_t * pParam)
 功    能:  分配PID对象,配置参数并返回PID id号
 输入参数:	pParam:PID配置参数
 输出参数:	如果PID对象创建成功,则返回ID号,如果创建不成功,则返回NULL
 返 回 值:	NA
 注意事项:	
---------------------------------------------------------------------------------------*/
PID_Id PID_New(PID_CGF_t * pParam)
{
    if(pParam == NULL)//配置参数为空,则创建失败
    {
        return NULL;
    }

    for(int i = 0; i < PID_MAX_NUM; i++)
    {
        PID_OBJ_t *pThis;

        pThis = &PID_Object[i];

        if(!pThis->used)//寻找没有被使用的PID对象
        {
            pThis->Kp = pParam->Kp;
            pThis->Ki = pParam->Ki;
            pThis->Kd = pParam->Kd;
            pThis->samplingTime = pParam->samplingTime;
            pThis->used = true;
            return pThis;
        }
    }
    return NULL;
}


/*---------------------------------------------------------------------------------------
 函数原型:  float PID_Work(PID_Id id, float setpoint, float feedbackVal)
 功    能:  PID控制
 输入参数:	id:PID Id号
            setpoint:设定值
            feedbackVal:反馈值
 输出参数:	NA
 返 回 值:	NA
 注意事项:	1、该函数需要以采样周期时间轮询调用
---------------------------------------------------------------------------------------*/
float PID_Work(PID_Id id, float setpoint, float feedbackVal)
{
    PID_OBJ_t *pThis;
    float integral;
    float error;
    float differential;

    //判断输入参数是否合法,保证程序的健壮性
    if(id == NULL)
    {
        return 0;
    }

    //将id降至类型转化为PID对象
    pThis = (PID_OBJ_t*)id;

    //判断对象是否已经初始化
    if(!pThis->used)
    {
        return 0;
    }

    //求出偏差,偏差 = 当前设定值 - 当前反馈值
    error = setpoint - feedbackVal;

    //求积分值,积分值 = 上一次计算得出的积分值 + 本次的偏差
    integral = pThis->lastIntegral +  error * pThis->samplingTime;
    //保存本次的积分值,作为下一次积分中上一次计算得出的积分值
    pThis->lastIntegral = integral;

    //求微分项,微分项 = 微分常数 * (本次偏差 - 上一次偏差)
    differential = pThis->Kd * (error  - pThis->lastError ) / pThis->samplingTime;

    //保存本次偏差值作为下一次计算的上次偏差
    pThis->lastError = error;

    //求PID最终输出值
    return pThis->Kp * error + pThis->Ki * integral + differential;
}

   这样我们写出了PID控制代码。但是这个控制代码是根据公式写出的理论性控制代码,可要在实际工程中使用,我们还需要考虑很多其他因素,后续几篇文章我们会不断的优化该控制代码,形成一个实际工程中拿来就用的PID控制模块。

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值