理论 链接到标题

针对情形 链接到标题

  • 需要对被控对象的多个物理量进行控制(比如同时控制位置和速度),将多个PID串级连接。(如果是并行的话可能会发生输出冲突)
  • 对于单个变量的控制,在功能和性能上多环PID都会比单环控制器好。

alt text

注释
感性的理解就是我们,如果我们将速度环PID与电机看成一个整体,那么这个整体相当于一个自带定速控制的电机,其效果肯定单纯的开环电机要好。

实际控制流程 链接到标题

下面对实际的控制流程做一个分析。

  • 初始时刻 目标位置、实际位置、目标速度、实际速度都为零
  • 现在(用户)设置一个目标位置(非零的值,不妨设大于零),
  • 位置环PID检测到差异,从而使得输出速度开始增大
  • 目标速度=输出速度
  • ∴速度环PID也检测到差异,输出PWM开始增大
  • 并且由于速度环PID与电机闭环,因此可以使得电机输出一个趋向于目标速度的速度。
  • 电机的转动使得实际位置开始与目标位置靠近。
  • 因此输出速度也在逐渐的减小;
  • 进一步的导致输出速度也开始减小;
  • 理想状态时在目标速度趋向于实际位置的时候,输出速度(即目标速度)也趋向于零,形成的效果就是电机丝滑泊入目标位置,并不存在超调现象。(妙啊!)
提示
这也就是为什么我们刚刚说双环具有更高的响应速度和性能

代码实现:双环定速控制 链接到标题

初步移植 链接到标题

  • 以02的定速控制代码为基础进行修改,因此也是准备将其变成内环的,因此对所有设计的变量都加前缀Inner。
  • copy一份代码作为外环
  • 首先改分频始终信号,将count改为count1(内环)和count2(外环)
  • 并且也对外环的改成前缀Outer(这里视频中说的是用Keil的功能,不过笔者更倾向于用vscode的功能,)

修改外环代码 链接到标题

Act的获取 链接到标题

首先,由于Encoder_Get()获取的事每次调用函数的位置增量,所以如果其在一个工程中被调用了两次,就不符合我们想要的差分效果了,因此,我们需要用两个中间变量先储存测量量。在一开始调用一次Encoder_Get(),存储好每一周期测得的速度和位置:

/*(这段代码应该在count1计数的代码中)*/
Speed = Encoder_GetSpeed();
Location+=Speed;
/*然后在下面将其分别赋值给InneActr和OuterAct*/

外环的输出作为内环的输入 链接到标题

将外环的最后一句的输出改一下

InnerTarget = OuterOut;

于是限幅的范围也要做一点调整了。将外环的输出限制在内环的输入范围,之前这个也控制过,是150。于是限幅……

if (OuterOut > 100) {OuterOut = 100;}
if (OuterOut < -100) {OuterOut = -100;}

双环PID调参 链接到标题

警告
同时调节双环的六个参数是不可行的,内外环的参数会互相影响,很那看出结果找到最优解
所以我们需要分开调节,

注意到内外环虽然会相互影响,但是是相对独立的,并且内环脱离外环能独立工作

因此我们先调内环!

调内环 链接到标题

将外环的代码注释掉,(或者最重要的注释掉InnerTarget = OuterOut;),烧录工程。

  • 先调$K_p$,调整好的标准是开始出现一点点抖动
  • 再调$K_i$,这部分可能需要结合着来调整$K_p$,反正最后反应迅速且抖动在能容忍范围内就是好调。
  • 最后调$K_d$,可要可不要,如果前面调得好可以置0.

我的调节结果如下(我认为已经比较好切不出现明显抖动)

alt text

参数为

  • $InnerK_p=0.42$
  • $InnerK_i=0.41$
  • $InnerK_d=0.00$

于是我们将我们的结果写出程序。

调外环 链接到标题

程序改动部分 链接到标题

  • 将刚刚的外环代码取消注释
  • 将量纲设置改一下,外环控的是位置,因此改成下面的程序
/*外环的量纲设置以及显示*/
OuterKp = RP_GetValue(1) / 4095.0 * 2;
OuterKi = RP_GetValue(2) / 4095.0 * 2;
OuterKd = RP_GetValue(3) / 4095.0 * 2;
OuterTarget = RP_GetValue(4) / 4095.0 * 816-408;//这是主要要修改的部分,将速度的量纲变成位置的量纲(或者说是数量级)

OLED_Printf(0, 16, OLED_8X16, "Kp:%4.2f", OuterKp);
OLED_Printf(0, 32, OLED_8X16, "Ki:%4.2f", OuterKi);
OLED_Printf(0, 48, OLED_8X16, "Kd:%4.2f", OuterKd);

OLED_Printf(64, 16, OLED_8X16, "Tar:%+04.0f", OuterTarget);
OLED_Printf(64, 32, OLED_8X16, "Act:%+04.0f", OuterActual);
OLED_Printf(64, 48, OLED_8X16, "Out:%+04.0f", OuterOut);

OLED_Update();

Serial_Printf("%f,%f,%f\r\n", OuterTarget, OuterActual, OuterOut);
  • 烧入程序

实测调节 链接到标题

alt text

  • 可以看到,在$K_p$、$K_i$和$K_d$都调节到0.00的情况下(也就是没有PID的情况下),电机能做到惊人的定速(图中的抖动是我用手稍稍施力扭动的结果),这是内环的功劳,此时即使有偏差,但是速度是稳住零的,不会抖动。
提示
观察图可以发现在前段有向目标值靠近的趋势,这是因为电位器不能完全保证$K_p$、$K_i$和$K_d$都为0.00的,因此不能做到严格的开环控制,外环PID还是会一点点的将实际位置往目标位置拽。
  • 外环一般使用PD控制
  • 先调$K_p$,调到电机能在目标位置附近抖动为止
    注释
    不用慌,因为后面$K_d$会在一定程度上消除超调
  • $K_i$最好不要给,可以发现一点点的$K_i$就能造成很大的超调和震荡,因为在这里本身也没有稳态误差
    错误
    为什么没有稳态误差的证明还没有找到
  • $K_d$可以较少超调,一直调,直至出现不可忍受的延迟。

提示
还是那句话,参数越大,响应越快,但是不可避免的抖动也就越快。
如果说我不太介意抖动但是介意响应速度(比如无人机飞控),那么高参数会好($K_p$给大一点,$K_d$也相应的给大一点), 不过我觉得我们的自瞄系统应该倾向于丝滑过度,因此我以丝滑为目标,下面是我的调参结果:

alt text

参数为

  • $OuterK_p=0.44$
  • $OuterK_i=0.00$
  • $OuterK_d=0.40$

不过无论是追求快速型还是丝滑型,在转盘停下来后

  • 误差都能被消除(或者说相对来说消除的比较干净),
  • 且不会出现抖动。

这还是内环的功劳

代码实现:双控控制定速与定位 链接到标题

实际上这个表示并不严谨,应该是限速定位控制,方法是改刚刚代码中的限幅部分,

if (OuterOut > 20) {OuterOut = 20;}
if (OuterOut < -20) {OuterOut = -20;}

这里将100改成了20,

  • 由于在调节的时候除了在结束时速度PID控制器基本都是满速控制的,因此可以认为我们实现了“定速”20。
  • 另一方面,这个速度仍然是闭环控制的,因此如果手动加阻力,其out会增大使得速度仍然在20出。

图像如下,

alt text

  • 图中红框部分是我用手施加阻力的情况,如果是单环定位控制这部分的抖动一定是剧烈的,但是在双环控制下可以看到仍然是很稳定的(当然,肯定是与无阻力情况有所不同的)。
  • 另一方面,限速相当于主动降低延迟,因此过冲的情况几乎没有,如果追求丝滑的可以考虑(嘿嘿)……

PID代码的封装 链接到标题

实际上,这是在处理多级问题必须要考虑的,通过函数或者结构体的封装可以使得功能模块化,有助于提高代码的可读性,方便日后的维护和调用。

用结构体封装变量 链接到标题

提示

.h文件的写法

在之前学C语言时候并没有系统学习.h文件以及调用的写法,下面补充一些知识, 依据ai,对于PID.h文件的基础写法是这样

#ifndef __PID_H   // 如果没有定义这个宏
#define __PID_H   // 那么定义这个宏

// 这里放你的头文件内容(函数声明、类定义等)

#endif  // __PID_H   // 结束宏定义

代码解释也已经在里面了,