从0编写一份PID控制代码

从0编写一份PID控制代码

一、前言

上一章节我分享了控制算法PID的基本概念,以及调参方式,相信大家对PID有了一个基本的了解,这一章我分享一下现在我使用的PID算法代码(代码是大疆工程师写的PID代码模板,写的非常棒),结合原理分析,让大家对其有一个更加深刻的理解,并且知道如何写PID算法
上一节文章链接:你和PID调参大神之间,就差这篇文章!

二、PID初始化代码

工程或者比赛中我们用到的PID一般不止一个,这些PID只是参数的值不一样,参数类型,参数运算函数基本相同,所以定义一个结构体,将这些有关参数作为结构体的成员,定义一个新的PID参数时,就是建立一个新的结构体,运算和初始化时直接调用对应的成员变量就行,十分方便简洁,具体定义的结构体如下:

typedef struct
{
    //PID运算模式
    uint8_t mode;
    //PID 三个基本参数
    fp32 Kp;
    fp32 Ki;
    fp32 Kd;

    fp32 max_out;  //PID最大输出
    fp32 max_iout; //PID最大积分输出

    fp32 set;	  //PID目标值
    fp32 fdb;	  //PID当前值

    fp32 out;		//三项叠加输出
    fp32 Pout;		//比例项输出
    fp32 Iout;		//积分项输出
    fp32 Dout;		//微分项输出
    //微分项最近三个值 0最新 1上一次 2上上次
    fp32 Dbuf[3];  
    //误差项最近三个值 0最新 1上一次 2上上次
    fp32 error[3];  

} pid_type_def;

定义的结构体成员变量的功能已经备注在代码中,有了结构的体之后,我们使用结构体定义一个PID结构体,编写一个初始化代码,初始运行时调用一次,初始化各个参数,PID_init函数主体如下:

void PID_init(pid_type_def *pid, uint8_t mode, const fp32 PID[3], fp32 max_out, fp32 max_iout)
{
    if (pid == NULL || PID == NULL)
    {
        return;
    }
    pid->mode = mode;
    pid->Kp = PID[0];
    pid->Ki = PID[1];
    pid->Kd = PID[2];
    pid->max_out = max_out;
    pid->max_iout = max_iout;
    pid->Dbuf[0] = pid->Dbuf[1] = pid->Dbuf[2] = 0.0f;
    pid->error[0] = pid->error[1] = pid->error[2] = pid->Pout = pid->Iout = pid->Dout = pid->out = 0.0f;
}
参数功能
*pid传入要初始化的PID结构体指针
modePID运行的模式,增量式还是位置式PID,此处我们定义一个枚举变量用于设置模式
PID[3]传入一个数组,用于作为三个基本参数P、I、D的初始值
max_outPID总输出的限幅,防止整体输出过大,传入一个正数,限制范围为[-max_out,+max_out]
max_iout积分项输出的限幅,因为系统刚启动时与目标误差较大,累计误差计算输出会很大,影响系统稳定性,所以对累计误差进行限幅,传入一个正数,限制范围为[-max_iout,+max_iout]

模式枚举

enum PID_MODE
{
    PID_POSITION = 0,
    PID_DELTA
};

三、PID运算代码

PID初始化完成之后,就是编写具体的运算代码了,代码内容如下,具体含义注释在代码中:

fp32 PID_calc(pid_type_def *pid, fp32 ref, fp32 set)
{
    //判断传入的PID指针不为空
    if (pid == NULL)
    {
        return 0.0f;
    }
    //存放过去两次计算的误差值
    pid->error[2] = pid->error[1];
    pid->error[1] = pid->error[0];
    //设定目标值和当前值到结构体成员
    pid->set = set;
    pid->fdb = ref;
    //计算最新的误差值
    pid->error[0] = set - ref;
    //判断PID设置的模式
    if (pid->mode == PID_POSITION)
    {
        //位置式PID
        //比例项计算输出
        pid->Pout = pid->Kp * pid->error[0];
        //积分项计算输出
        pid->Iout += pid->Ki * pid->error[0];
        //存放过去两次计算的微分误差值
        pid->Dbuf[2] = pid->Dbuf[1];
        pid->Dbuf[1] = pid->Dbuf[0];
        //当前误差的微分用本次误差减去上一次误差来计算
        pid->Dbuf[0] = (pid->error[0] - pid->error[1]);
        //微分项输出
        pid->Dout = pid->Kd * pid->Dbuf[0];
        //对积分项进行限幅
        LimitMax(pid->Iout, pid->max_iout);
        //叠加三个输出到总输出
        pid->out = pid->Pout + pid->Iout + pid->Dout;
        //对总输出进行限幅
        LimitMax(pid->out, pid->max_out);
    }
    else if (pid->mode == PID_DELTA)
    {
        //增量式PID
        //以本次误差与上次误差的差值作为比例项的输入带入计算
        pid->Pout = pid->Kp * (pid->error[0] - pid->error[1]);
        //以本次误差作为积分项带入计算
        pid->Iout = pid->Ki * pid->error[0];
        //迭代微分项的数组
        pid->Dbuf[2] = pid->Dbuf[1];
        pid->Dbuf[1] = pid->Dbuf[0];
        //以本次误差与上次误差的差值减去上次误差与上上次误差的差值作为微分项的输入带入计算
        pid->Dbuf[0] = (pid->error[0] - 2.0f * pid->error[1] + pid->error[2]);
        pid->Dout = pid->Kd * pid->Dbuf[0];
        //叠加三个项的输出作为总输出
        pid->out += pid->Pout + pid->Iout + pid->Dout;
        //对总输出做一个先限幅
        LimitMax(pid->out, pid->max_out);
	}
	return pid->out;
}

限幅代码为预编译代码

#define LimitMax(input, max)   \
    {                          \
        if (input > max)       \
        {                      \
            input = max;       \
        }                      \
        else if (input < -max) \
        {                      \
            input = -max;      \
        }                      \
    }

以上就是PID运行时的处理代码了,这里PID有两种处理模式,一个增量式一个是位置式,两个算法具体说起来还比较复杂,这里有一篇很棒的参考文章,链接挂在这里,想要了解原理可以参考这篇文章:

位置式PID与增量式PID区别浅析

四、PID输出清空代码

有时候我们需要停止PID,需要清除中间变量,主要就是目标值和中间变量清零,具体代码如下

void PID_clear(pid_type_def *pid)
{
    if (pid == NULL)
    {
        return;
    }
	//当前误差清零
    pid->error[0] = pid->error[1] = pid->error[2] = 0.0f;
    //微分项清零
    pid->Dbuf[0] = pid->Dbuf[1] = pid->Dbuf[2] = 0.0f;
    //输出清零
    pid->out = pid->Pout = pid->Iout = pid->Dout = 0.0f;
    //目标值和当前值清零
    pid->fdb = pid->set = 0.0f;
}

以上代码我放到CSDN内,需要自取:链接

下面是一个使用Python结合PID控制来控制树莓派五路灰度小车进行循迹转弯的示例代码: ```python import RPi.GPIO as GPIO import time # 设置引脚 pin_left = 11 pin_middle_left = 13 pin_middle = 15 pin_middle_right = 16 pin_right = 18 # 设置PID参数 kp = 0.5 # 比例系数 ki = 0.1 # 积分系数 kd = 0.2 # 微分系数 # 初始化GPIO GPIO.setmode(GPIO.BOARD) GPIO.setup(pin_left, GPIO.IN) GPIO.setup(pin_middle_left, GPIO.IN) GPIO.setup(pin_middle, GPIO.IN) GPIO.setup(pin_middle_right, GPIO.IN) GPIO.setup(pin_right, GPIO.IN) # 初始化小车控制 def init_car(): pass # 获取灰度传感器数据 def get_sensor_data(): sensor_data = { 'left': GPIO.input(pin_left), 'middle_left': GPIO.input(pin_middle_left), 'middle': GPIO.input(pin_middle), 'middle_right': GPIO.input(pin_middle_right), 'right': GPIO.input(pin_right) } return sensor_data # PID控制 def pid_control(sensor_data): error = sensor_data['middle'] * 1 + sensor_data['middle_left'] * 0.5 - sensor_data['middle_right'] * 0.5 - sensor_data['left'] * 2 - sensor_data['right'] * (-2) output = kp * error return output # 循迹转弯控制 def track_turn(): init_car() while True: sensor_data = get_sensor_data() output = pid_control(sensor_data) # 根据PID输出控制小车转弯 if output > 0: # 向左转 pass elif output < 0: # 向右转 pass else: # 直行 pass time.sleep(0.01) # 主函数 if __name__ == "__main__": try: track_turn() except KeyboardInterrupt: GPIO.cleanup() ``` 请注意,这只是一个示例代码,具体的转弯控制逻辑需要根据你的具体硬件连接和传感器输出进行调整。你需要根据实际情况设置引脚和PID参数,并编写具体的小车控制逻辑。此外,还需要根据你的控制方(如PWM控制电机速度等)对代码进行相应的修改。
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Top嵌入式

投喂个鸡腿

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值