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控制模块。