即便了解了PID的理论, 但是在实际运用时还是不会调?
这篇文章以控制直流电机的速度为例, 一步一步带你了解如何借助vofa+来进行调参.

1. 准备工作

对于减速直流电机, 基础的驱动部分我就不多赘述, 这里就简单介绍一下下面需要用到的函数分别是什么意思.

PWM赋值函数, 用于控制电机的速度.
参数: speed-->速度值(0~100), which-->哪一个电机

void set_speed(int16_t speed,uint8_t which);//速度单独设置

编码器读取函数

void get_speed();

此函数执行后可以直接从变量读取编码器的值

motor[i].en_actual

2. PID是电机平稳到达目标速度

整体框架

这里我先来贴一下代码的具体框架:

/*
    config.pwm_out_l 和 config.pwm_out_r 分别代表左右电机的PID输出值并且赋值给set_speed函数
    M1, M2 是电机编码, 这里我驱动了两个电机(并不复杂, 实际还是调整一个电机, 这两个是完全对称的)(主要还是懒得改代码qwq)
*/
void set_angle_pid(){
    config.pwm_out_l=0,config.pwm_out_r=0;

    get_speed();//读取编码器值

    //同速环设置
    motor_speed_pid[M1].target_val = config.speed_begin;//设定目标值
    motor_speed_pid[M2].target_val = config.speed_begin;
    config.pwm_out_r+=speed_pid(&motor_speed_pid[M1],motor[M1].en_actual);//目标值带入取输出值
    config.pwm_out_l+=speed_pid(&motor_speed_pid[M2],motor[M2].en_actual);


    //KalmanFilter的滤波效果很好, 这里观察PID参数现象还是不滤波现象明显点, 调完参数后我是一定会加上去的
    //别的不说,直行环我们还是希望能尽可能稳定的
    //config.pwm_out_l=KalmanFilter(&kfpVar,config.pwm_out_l);
    //config.pwm_out_r=KalmanFilter(&kfpVar,config.pwm_out_r);

    //输出限幅,再怎么样速度也只有这么多,同时也可以判断PID的参数是否合理,如果输出一直都是这里的最大值,其意味着PID的参数一定大了,不论是什么波形
    if(config.pwm_out_l>=40)config.pwm_out_l = 40;
    else if(config.pwm_out_l<=-40) config.pwm_out_l = -40;
    if(config.pwm_out_r>=40)config.pwm_out_r = 40;
    else if(config.pwm_out_r<=-40)config.pwm_out_r=-40;

    //速度赋值
    set_speed((int16_t)config.pwm_out_r,M1);
    set_speed((int16_t)config.pwm_out_l,M2);
}

先来看我的框架:

  1. 读取编码器值(获取当前实际测量的速度)
  2. 设置目标速度(我希望能让电机达到怎样的速度)
  3. 将目标速度带入PID控制器, 获取输出值(通过speed_pid函数获取当前这个时间段的PWM输出值)
  4. 滤波(观察现象时我没有使用, 参数调完后我是一定会调用的)
  5. 限幅(这一步很重要, 我曾经在调参时就烧掉了两对直流电机)
  6. 赋值(经过了一大段计算可别忘了这个)
  7. 本函数每隔一定时间调用(计算)一次, 我是每10ms调用一次.

宏观上来看并不复杂, 这里我们着重来康康PID控制器speed_pid函数的实现以及各个参数对结果的影响.

PID控制器的具体实现

PID有个特别方便的地方就是我们其实可以忽略物理单位的影响, 我在写直流电机驱动的时候编码器的值和速度单位我并没有建立数学上的关联, 只需要调节PID这几个参数即可达到效果, 但是有一点需要的就是两者一定是成正比的.
贴一下代码:

//速度pid实现
float speed_pid(_pid *pid,float actual_val)
{
    /*计算目标值与实际值的误差*/
    float err=pid->target_val-actual_val;

    //一阶低通滤波
    static float last_err = 0;
    float a =0.7f;
    pid->err = (1-a)*last_err + a*err;
    last_err = err;

    pid->integral += pid->err;    // 误差累积

    //积分限幅这个需要根据具体情况给,过大则I就很难调
    if (pid->integral >= 1000) {pid->integral =1000;}
    else if (pid->integral < -1000)  {pid->integral = -1000;}

    /*PID算法实现*/
    pid->actual_val = pid->Kp*pid->err
                      +pid->Ki*pid->integral
                      +pid->Kd*(pid->err-pid->err_last);

    /*误差传递*/
    pid->err_last=pid->err;

    /*返回当前实际值*/
    return pid->actual_val;
}

以及在初始的时候我的参数的初始化

void PID_param_init(){
    motor_speed_pid[M1].Kp =motor_speed_pid[M2].Kp = 0.0f;//后面简称Kp
    motor_speed_pid[M1].Ki = motor_speed_pid[M2].Ki= 0.0f;//后面简称Ki
    motor_speed_pid[M1].Kd=motor_speed_pid[M2].Kd = 0.0f;//后面简称Kd
}

十分常规的位置式PID框架
在正式调参前, 我们将所有的参数初始化为0.0f

PID参数的调整

调P

调参一定是先从Kp开始的, Kp的含义是比例系数, 其数值的大小决定的系统的响应快慢, 先说结论:

  • Kp较大-->系统能更快到达目标值
  • Kp较小-->系统响应较慢, 但是相对稳定
  • Kp过大-->系统超调大(体现在电机最终会在目标值附近产生较大的来回振荡), 如果比这个还要大,那电机就会直接保持在最大的速度(因为你输出的就已经超过了量程)
  • Kp过小-->系统响应特别慢, 如果特别小, 那么系统可能根本就不能达到目标值(即实际值停留在一个较小值当中)

这里我画了一张图, 是我心中Kp大小对系统的影响

我使用了vofa+观察波形, 在我调参的时候, 我会不断改变Kp的值, 然后观察波形的变化, 然后根据波形的变化来调整Kp的值, 直到波形稳定下来.
下面我来分别贴一下这几种情况的波形大概是什么情况的(这里我设定目标值为30(编码器测得为30的速度,而非PWM)):


这里推荐一个快速得到目标值的技巧, 相信很多人都知道, 那就是二分查找, 对于任意的量程, 我只需要O(logn)的复杂度既可以找到对应的值, 如果说有4096个测试值我们只需要试12次, 有64个测试值, 我们只需要试6次, 当然如果Kp有小数位也同样快速. 所以别再一开始就那样 10 20 30...那样一个个试了, 那样太慢了.而是100 50 25 12.5这样可以帮我快速找到合适的区间.

  • Kp过小

  • Kp小

  • Kp相对合适

可以看到稳定前毛刺不是那么尖锐,而且响应也稍微快了一些

  • Kp大

这里其实很明显出现了抖动

其实如果是仅仅调P的话你可能会发现我们有时候确实能看到系统稳定后的波形如何, 但是实际稳定下来的值和目标值依然有一定的偏差, 这个其实不用管, 我们在调P的时候最主要关注两点响应速度是否满足我们的要求, 振荡是不是可以接收, 是否存在离谱的稳定偏差即可, 至于上述的小稳定偏差, 那是I的事

调D

把P调合适之后在后续就不要大幅度改变这个值了, 下一步我们来调D的值, D项的含义是变化趋势(导数), 其数值主要影响系统在趋于稳定的时候的振荡幅度
在上面的Kp合适的一栏, 看起来并没有那么的合适, 因为在初期接近目标值的时候依然有大幅度振荡, 虽然最终是趋于稳定的, 但是我们希望的是在接近目标值的时候不要有振荡, 所以我们引入D项, 来减小振荡幅度. 也就是说D项的作用其实是在阻碍着系统的变化的.

当D项越大, 这个阻碍的作用就越大, 可以来帮助我们减小振荡的幅度
如果D项过大, 这个阻碍作用也过大, 体现出来的和Kp大一样也就是在目标值附近反复振荡这肯定不是我们想要的.

下面来看看D项对应的几种情况.

  • D项合适

  • D项过大

也是抖动以及响应变慢,和Kp整大是一个效果,只不过Kp正向增大,Kd则是反向(阻碍)增大.

调I

I是比较复杂的, 这里我放在了最后一个调整, 在调整P, D参数之后反映过来的结果还是和目标值的差了一点, 这时候就需要我们去调整I的大小了.
I积分项, 当和目标值偏差持续存在的时候, I项就会作用起来填补这个误差使最终趋于目标值

当然如果持续有误差(比如说一直检测不到反馈值)的时候 累计项可能会变得特别大, 这样导致的结果就是在下一次检测到作用值的时候, 作用值会突然跳变, 这肯定不是我们所希望的. 其实这种情况有一个专业术语叫积分饱和.

为了防止这种情况的发生, 我们需要限制积分项的值, 在代码中我限制了积分项的值在[-1000, 1000]之间, 如果超过这个范围, 那么就取这个范围的最大值或者最小值.

可以看到I是比较复杂的, 我们既要考虑I的大小也要考虑限幅的大小, 不管限幅大小是不用特别担心的, 只要给的值不是特别离谱, 通过前面两个系统其实是可以将其拉回来的.我们主要还是看I项的大小.

整体的调整

在系统稳定之后可能还会有一定的误差, 你比如说这里我稳定下来的参数总是在+-1的区间里面振荡,频率还不小, 这个其实就很难避免了, 后面我看了一下减小这种情况主要有两点:

  1. 增大调参的量程:这里我的量程是0-100, 这个着实不好调整, 我们可以适当增大Pwm赋值的范围比如说0-1000,或者0~5000, 这样稳定下来的即便有小范围的振荡但是实际效果可能十分微小, 况且这样也方便我们调整参数.我的Kp, Ki, Kd都精确到了小数后几位了, 如果量程大一些或许就不会这样.
  2. 对结果值滤波, 虽然只有+-1但是加上滤波函数之后效果有所改良, 小范围的振荡频率减小了, 变成了偶尔振荡.
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐