第18届全国大学生智能汽车竞赛四轮车开源讲解【4】--控制

开源 0

开源汇总写在下面

第18届全国大学生智能汽车竞赛四轮车开源讲解_Joshua.X的博客-CSDN博客

一、控制方法

1.控制对象基本介绍

1.1舵机

舵机是控制车模方向的重要组成部分,一般放在车头,实物见下图。

C车模允许使用的有S3010,U400这两款舵机

如果比赛限制车模类型为C车模,那么只能使用S3010(已停产)或者U400舵机进行比赛,控制舵机主要是控制PWM波的脉宽。控制规律如下图。

舵机占PWM脉宽与转动角度之间关系

关于PWM波这里不再介绍,CSDN上有很多的介绍。

S3010,U400这两款舵机是的控制频率为50Hz(官方推荐参数),想要控制这个舵机,就需要单片机产生50HZ的PWM波,改变占空比,使脉冲宽度在1ms~2ms,使之实现左右转动,控制车模方向。

首先我们选取一个能够产生PWM波的IO口(并非所有IO口都可以产生PWM波),使他的PWM频率是50Hz。在配置的库中,所有PWM波的占空比的细分都被映射到了0~10000。(有些是0~50000,需要查看对应工程的实际情况)

PWM初始化函数

占空比0是纯低电平,占空比10000是满占空比,也就是纯高电平。

我们一起来推导下如何才能实现脉冲在1~2ms之间。

已知我们配置是PWM频率为50Hz,那么周期就是20ms。

按照比例映射,当占空比为10000时输出是20ms脉宽,我们求占空比为x时,输出1.5ms脉宽。

那么我们得到了这样一个比例式:

/frac{20ms/frac{}{}}{10000}=/frac{1.5ms/frac{}{}}{x}

%20解得x=750时,单片机将输出50HZ,脉宽时间为1.5ms的PWM波。对应着舵机理论归中。

%20舵机初始化代码如下:

%20
#define%20STEER_PIN%20%20%20%20TIM2_PWM_MAP3_CH1_A15//舵机控制信号输出口#define%20STEER_RIGHT%20%20705%20%20//舵机右打死,-#define%20STEER_MID%20%20%20%20782%20%20//舵机归中#define%20STEER_LEFT%20%20%20857%20%20//舵机左打死,+//舵机初始化函数pwm_init(STEER_PIN,%2050,%20STEER_MID);
%20我的中值STEER_MID是782,由于安装位置,拉杆机械误差,车模机械误差,实际中值一般很难是750,但是也在750附近,符合理论估计。

%20%20%20%20%20%20
拉杆长度,机械偏差,轮子磨损,都会影响舵机中值

实际上,STEER_MID这个值需要机械归零校准的,具体流程就是将舵机打角在视觉中值附近(也就是看起来车轮是正的)。把车放在地砖线附近,用手推车。(我一般推2m左右,当然越远越准)参考地砖线,如果车子走的是直线,那就找到了这个中值;如果往左/右歪,就适当加减STEER_MID这个值,直到推出来是直线为止。

车子放正,左边地砖缝作为参考线,舵机归正推着走,看误差

同理调整这个数字,找到轮胎左右打角的极限,再往回收一点点,让轮子打角幅度比较大,但又能流畅转动。这样就找到了舵机的左、中、右值。这样基本的控制准备工作就做好了。

详细机械调整方法,会在后文的机械篇提到,敬请期待。

找到了中值,左右极值后,就可以自由的让车模左右转动了。改变的就是这个变量angle的值。

//舵机占空比设置函数pwm_set_duty(STEER_PIN, STEER_MID+angle);//舵机调节

目前我们知道舵机在占空为STEER_MID时舵机打角为正,且增大/减小该值会向左/右打角。

那么我在他的基础上+angle这个变量,当angle为正的时候,PWM脉宽大于1.5ms,舵机向左打,当angle为负,PWM脉宽小于1.5ms,舵机向右打,那么我想办法将赛道的拐弯情况,和这个变量做个映射,这个映射就是PD控制,这样就实现了车模的自主巡线。

1.1.1 舵机魔改

舵机的推荐电压在7v左右,具体情况查看参数手册。

其实在官方推荐的参数上可以适当再增加一点电压,电压越高,舵机相应速度越快。但是同时也越容易损坏,这方面需要自行权衡。

50Hz的频率也是可以进行魔改,据群内消息,u400舵机用300Hz没问题,实测什么样我就不知道。各位准备好钱,自行尝试。

具体数值关系上文有提到公式,自行换算。

1.2.电机

这是我们智能车使用的普通直流有刷电机。

 直流有刷电机主要靠驱动板进行供电,驱动。

一个电机需要两路信号来控制方向和转速,共有两种方法,详情见下图。

 驱动方式不同是因为驱动芯片的不同,导致代码编写会有不同,这一点需要和硬件队友进行确认。

电机驱动的两种方式
驱动方式1

利用两路pwm对一个电机进行控制。如果A路是PWM,B路0,则是正转,且PWM占空比越大,转速越快,想要反转也只需要将AB两路信号调转过来即可,A路输出0,B路输出PWM。控制代码如下:(0为低电平,占空比为0的PWM波就是低电平)

void Motor_Right(int pwm_R){    if(pwm_R>=SPEED_MAX)//限幅处理        pwm_R=SPEED_MAX;    else if(pwm_R<=SPEED_MIN)            pwm_R=SPEED_MIN;    if(pwm_R>=0)   {    pwm_duty(MOTOR01_CH1, pwm_R);//右电机正转    pwm_duty(MOTOR01_CH2, 0);   }  else   {    pwm_duty(MOTOR01_CH1, 0);     //右电机反转    pwm_duty(MOTOR01_CH2, -pwm_R);   }}

这里我是做了一个函数,输入的参数是我期望的占空比。如果是正,那么就是正转,输入到A通道里面,B给0;如果是负数,那我认为需要反转,将占空比加个负号(负负得正,占空比只能有正数)输入到B通道中,A给0,这样就实现了正反转控制。

驱动方式2

这种方式比较好理解,一路1或0确定控制方向,另一路PWM用来控制转速。代码如下:

void Motor_Right(int pwm_R){    if(pwm_R>=SPEED_MAX)//限幅处理        pwm_R=SPEED_MAX;    else if(pwm_R<=SPEED_MIN)            pwm_R=SPEED_MIN;    if(pwm_R>=0)//正转    {        pwm_set_duty(MOTOR02_SPEED_PIN, pwm_R);//右电机正转        gpio_set_level(MOTOR02_DIR_PIN, 0);//02左电机,D14,1反转,0正转    }//这里给1给0正反转需要实测,一切以实际为准    else   {        pwm_set_duty(MOTOR02_SPEED_PIN, -pwm_R);//右电机反转        gpio_set_level(MOTOR02_DIR_PIN, 1);//02左电机,D14,1反转,0正转   }}

还是先判断正转反转,正转就给方向脚0,反转就给方向角1,然后选取绝对值输入到占空比设置函数中去。

这两种方式,无论哪种方式都不是绝对的。实际情况和电机接线方式,电池正负极接入方式,都有关系。有可能我是代码A给pwm,B给0是正转,但实际你的车是反转,这种情况就把电机接线,或者代码改一个就好(一般建议改代码,电机接线不好改)。

二、方向控制

方向控制的核心问题就是车模知道自己在赛道什么位置,他需要知道自己需要向哪边调整。

还是那句话,没有绝对优秀的算法,每一种算法都有自己的优劣,这里仅展示一些方法来给各位提供参考。

方向控制流程图

 寻找边界我们在上一章边线提取已经讲过了,元素判断我们会在后面进行讲解。

本文将讲解计算视野内中线,误差获取/计算偏差,PD算法,将PD算出的打角值放入占空比调整函数。

在方向控制中最重要的就是:误差获取/计算偏差,PD算法,大家的车子其他的流程都是一样的,所谓国赛代码,普通代码的差距一般也就在这里。

注:以下方案有可能会用到一个或者多个下述变量,以下变量均在开源【3】边线提取一章有讲。

const uint8  Standard_Road_Wide[MT9V03X_H];//标准赛宽数组volatile int Left_Line[MT9V03X_H]; //左边线数组volatile int Right_Line[MT9V03X_H];//右边线数组volatile int Mid_Line[MT9V03X_H];  //中线数组volatile int Road_Wide[MT9V03X_H]; //实际赛宽数组volatile int White_Column[MT9V03X_W];//每列白列长度volatile int Search_Stop_Line;     //搜索截止行,只记录长度,想要坐标需要用视野高度减去该值volatile int Boundry_Start_Left;   //左右边界起始点volatile int Boundry_Start_Right;  //第一个非丢线点,常规边界起始点volatile int Left_Lost_Time;       //边界丢线数volatile int Right_Lost_Time;volatile int Both_Lost_Time;//两边同时丢线数int Longest_White_Column_Left[2]; //最长白列,[0]是最长白列的长度,也就是Search_Stop_Line搜索截止行,[1】是第某列int Longest_White_Column_Right[2];//最长白列,[0]是最长白列的长度,也就是Search_Stop_Line搜索截止行,[1】是第某列int Left_Lost_Flag[MT9V03X_H] ; //左丢线数组,丢线置1,没丢线置0int Right_Lost_Flag[MT9V03X_H]; //右丢线数组,丢线置1,没丢线置0

1.误差获取

中线计算我都是和偏差放在一起,因为中线的意义在于计算偏差,只要我能得到偏差,中线就没那么重要,我就没有单独计算中线。

误差获取就是去计算某行,或者某些行的中线与理论中线之间存在的偏差。用理论中线的值减去赛道中线的值得到(赛道中线减去理论中线的值也可以,只是正负号的含义相反罢了)。用这个偏差来表征车模偏离赛道中心的距离。这个值的正负号代表着方向,数值大小代表着偏离程度。

误差获取有很多种办法。

  1. 单行控制/领跑行控制
  2. 误差积累
  3. 误差平均
  4. 动态前瞻
  5. 加权平均

上述方案我们都简单的介绍一下

1.单行控制/领跑行控制

他的本质就是电磁式跑法,将摄像头视野中固定的某行作为舵机打角行,计算该行的中线与理论中线的偏差,前瞻的位置是死的。他的特点在于只采用一行作为控制行,而且这一行是固定不动的,所以一但这一行的图像出现了问题,对于舵机打角的判断就会有很大的影响,而且方向判断只用一行,对于摄像头收集到的多行数据有些浪费。

建议与后文提到的的动态前瞻进行组合使用,可以取得较好的效果。

float Err_Sum(int err_get_line){    float err=0;    err=(MT9V03X_W/2-((Left_Line[err_get_line]+Right_Line[err_get_line])>>1));//右移1位,等效除2    return err;}

2.误差积累

这是我17届车赛采用的做法,具体讲述就是固定某个范围,将这范围内的误差积累求和,将这个和作为误差返回。

#define VALID_HEIGHT  15  //只扫描从下往上数15行,根据情况,可以修改该值int Err_Sum(){    int i=0,sum=0;    for(i=MT9V03X_H-1;i>MT9V03X_H-VALID_HEIGHT-1;i--)    {        sum+=(MT9V03X_W/2)-Mid_Line[i];    }    return sum;}

当时是直接选取了屏幕最下方的15行误差积累作为舵方向控制,给舵机使用。

算法属于能用,但不是很好用的范围。最直接的结果就是这个误差值很大,需要将PD参数调整的很小。

3.误差平均

这个算法是上面误差积累的改进,也是固定某些行,计算他们的误差,但是这里取了平均值,对于一些异常数据会有一定的滤波抗干扰作用。

float Err_Sum(void){    int i;    float err=0;    //常规误差     for(i=51;i<=55;i++)     {        err+=(MT9V03X_W/2-((Left_Line[i]+Right_Line[i])>>1));//右移1位,等效除2     }    err=err/5.0;return err;}

4.动态前瞻

这个是比较高端有用的算法。理论上来说,速度越快,前面越是直道,我们的前瞻越应该看到越远,从而及时的反应过来前方路况;速度越慢,弯道越多,前瞻应该稍微近一点。

这就需要我们以速度,和视野范围为输入量,前瞻位置为输出量,去拟合一个函数。去合理的计算前瞻位置,很遗憾,本人并没有完成这项工作,这里就交各位自行发挥。

5.加权平均

这个是我本届比赛使用的方法,本人使用比较稳定。

先将赛道的每一行都给予相应的权重。

//加权控制const uint8 Weight[MT9V03X_H]={        1, 1, 1, 1, 1, 1, 1, 1, 1, 1,              //图像最远端00 ——09 行权重        1, 1, 1, 1, 1, 1, 1, 1, 1, 1,              //图像最远端10 ——19 行权重        1, 1, 1, 1, 1, 1, 1, 3, 4, 5,              //图像最远端20 ——29 行权重        6, 7, 9,11,13,15,17,19,20,20,              //图像最远端30 ——39 行权重       19,17,15,13,11, 9, 7, 5, 3, 1,              //图像最远端40 ——49 行权重        1, 1, 1, 1, 1, 1, 1, 1, 1, 1,              //图像最远端50 ——59 行权重        1, 1, 1, 1, 1, 1, 1, 1, 1, 1,              //图像最远端60 ——69 行权重};

接着从赛道最下面到搜索截止行,将所有误差进行加权求平均,权重数组就是上面的数组。

float Err_Sum(void){    int i;    float err=0;    float weight_count=0;    //常规误差    for(i=MT9V03X_H-1;i>=MT9V03X_H-Search_Stop_Line-1;i--)//常规误差计算    {        err+=(MT9V03X_W/2-((Left_Line[i]+Right_Line[i])>>1))*Weight[i];//右移1位,等效除2        weight_count+=Weight[i];    }    err=err/weight_count;    return err;}

这样利用到了赛道每一行的数据,增强了抗干扰性,又可以通过权重比例来确定车模切内还是切外,本人实际效果较好,推荐大家使用。

注:误差的获取的一个原则就是在其他情况都相同的情况下,误差选取的范围距离车越近,车模反应越慢,从而越切外;选取的范围越远,反应越提前,车模越切内。所有的这些需要各位车手根据自己的实际情况合理的选取误差。

其实个人观看国赛车视频,大部分国赛车还是比较偏向切内一点的。

至此,我们就完成了误差获取,都可以调用

Err=Err_Sum();      //误差计算

这个函数,获取到摄像头收到数据的误差,然后就可以将误差放入PD,进行下一步控制。

2.PD控制

所谓PD控制就是PID算法的改进版,PID算法也不在这里赘述。

我使用的是位置式的PD,去掉了积分环节I。增量式与位置式PID的区别也不再阐述,智能车方向环大多是位置式PD算法。

PD的控制的误差传入就是上面我们计算的摄像头误差,由于我们期望车模永远贴着中线跑,所以设定值(期望值)不会变是0。

#define STEER_RIGHT  705  //舵机右打死,-#define STEER_MID    782  //舵机归中#define STEER_LEFT   857  //舵机左打死,+#define LEFT_MAX     (STEER_LEFT- STEER_MID)//+#define RIGHT_MAX    (STEER_RIGHT-STEER_MID)//-//函数本体int PD_Camera(float expect_val,float err)//舵机PD调节{   float  u;   float  P=1.98; //参数需自行整定,这里仅作为参考   float  D=1.632;   volatile static float error_current,error_last;   float ek,ek1;   error_current=err-expect_val;   ek=error_current;   ek1=error_current-error_last;   u=P*ek+D*ek1;   error_last=error_current;   if(u>=LEFT_MAX)//限幅处理       u=LEFT_MAX;   else if(u<=RIGHT_MAX)//限幅处理       u=RIGHT_MAX;   return (int)u;}//实际使用Steer_Angle=PD_Camera(0,Err);//摄像头舵机PD调Steer(Steer_Angle);

这是最传统的PD,没有什么修改,原汁原味。

江湖传言,纯P控制,跑到2m/s没问题,PD控制跑到2.5m/s没有问题,再往上提速就得看看分段PD,模糊PD了。

根据我自己实测,的确是这样。单独一套PD参数,图像做好上2.5m/s的确没问题的。

我实际使用的模糊PD,将会在后面的在电磁一章进行讲述。

得出PD算出的打角值后,我们将数据送到舵机占空比调整函数中即可。

流程如下

//while(1)中Err=Err_Sum();               //误差计算//中断中Steer_Angle=PD_Camera(0,Err);//摄像头舵机PD调Steer(Steer_Angle);          //实际占空比控制

舵机输出函数本体如下:

/*-------------------------------------------------------------------------------------------------------------------  @brief     舵机电机输出函数  @param     angle 输入舵机方向信号,+-LEFT_MAX,RIGHT_MAX之间  @return    null  Sample     Steer(angle);  @note      舵机最终输出函数,由其他函数调用                                           在给舵机前有限幅处理的#define STEER_RIGHT  771  //舵机右打死,-#define STEER_MID    851  //舵机归中#define STEER_LEFT   931  //舵机左打死,+-------------------------------------------------------------------------------------------------------------------*/void Steer(int angle){    if(angle>=LEFT_MAX)//限幅处理       angle=LEFT_MAX;   else if(angle<=RIGHT_MAX)        angle=RIGHT_MAX;    pwm_set_duty(STEER_PIN, STEER_MID+angle);//舵机调节}

需要注意的是,我的输出值是基于归中值的相对值,范围是左右极限减去中值,因为我在最终的调整舵机占空比函数中用的是STEER_MID+angle,这里是angle就是基于中间值的正负偏差。

如果摄像头偏差是Err是0,那么PD算出来的结果也基本是0附近,舵机最终输出的就是STEER_MID+0,就是STEER_MID,轮子效果就是归中打直。

还有一个问题,就是正负号的问题。舵机控制中,有很多处理需要考虑正负号。

  1. 首先是误差获取,是用(屏幕中线-赛道中线),还是(赛道中线-屏幕中线)。
  2. PD里面的期望值,正常来说我的期望值是0,那么我的error_current是err-0,还是0-err。
  3. 舵机输出函数,输出的值是STEER_MID+angle,还是STEER_MID-angle。

这几个数值的正负号最好都要统一,不然的话误差会直接反过来。车子往右偏,结果轮子还在往右打。

或者误差是正数,结果舵机打的是负数的角,虽然不影响运行,但是看起来仍有些不舒服。

最后一个问题,控制周期的问题。我在国赛代码中有看到过两种控制策略:

  1. 整套流程放在while(1)中,摄像头每处理完一帧图片,接下来就计算偏差,计算PD,进行舵机控制。
  2. 舵机控制放在20ms中断中,因为舵机50Hz的频率,保证舵机准时输出。中线误差计算放在while(1)中计算。

这两种策略都是有道理的。

第一种策略保证了使用一帧图片,做一次控制,保证每一帧图像结束后就进行控制,精细度更高。

第二种控制策略是我在比赛中所使用的。因为我调车的时候,有时会接上图传或者打开屏幕,这样每一圈while(1)时间会有所不同。在打开屏幕时,一圈while(1)需要40ms;关闭屏幕一圈while(1)只需要10ms左右;只打开图传,一圈while(1)需要15ms左右。这就导致我在打开图传情况下调整的参数,不适配我关闭图传时的参数,导致控制出现问题。所以我将舵机控制放在定时器中,误差更新在while(1)中,只要一圈while(1)速度小于20ms,那么控制都是来得及的。

三、速度控制

1.开环控制

所谓开环控制,个人认为就是没有反馈的控制,这里特指电机的速度控制。

我们在将期望的占空比输入到电机的控制函数中,电机的速度会变快。但是具体多快,到没到我们的预期,阻力对他影响如何,这都无法得知,只能知道一件事,我期望他变快,我给电机的电压挺高的。

比如我直道给了3000,也就是3000/10000=30%的占空比,我期望他跑快一点。弯道给2000,20%的占空比,期望他慢一点。

然而车模实际运行需要抵抗阻力,而且加速减速需要反应时间,车模跑起来还会有惯性。很容易就出现加不起速,刹不住车的情况。在弯道还没减速到20%占空比,就已经出界了,这都是开环控制的弊端,所以一般建议初期使用开环,等到控制稍微成熟一点,使用闭环增强稳定性,也便于提速。

2.闭环控制

闭环就是有反馈的控制,我现在给出一个期望速度,经过计算得出一个占空比,然后就去测量车轮的速度情况如何,如果没到我的期望,那我就再给高一点的占空比。如果到了,那我就维持当前占空比;如果超了,那我给小一点,如果超的多了,那我甚至可以让他反转,让他强制减速。这个控制周期一般很短,我个人使用的10ms进行一次控制,队友用的5ms,都是可以的。

这个测量转速的装置是编码器。

2.1编码器

编码器把角位移或直线位移转换成电信号,前者称为码盘,后者称为码尺。按照读出方式编码器可以分为接触式和非接触式两种;按照工作原理编码器可分为增量式和绝对式两类。增量式编码器是将位移转换成周期性的电信号,再把这个电信号转变成计数脉冲,用脉冲的个数表示位移的大小。

方向编码器、正交编码器、角度编码器

不同的主控芯片有不同的适用范围,参考原则见下图。

根据主控芯片不同选择不同的编码器

实际使用时他就是测量车轮转速的一个设备,通过齿轮啮合,与车轮一同转动。

编码器通过齿轮与车轮啮合

这里简单提一句,这三者齿轮啮合不要太紧,也不能太松,基本原则是每两个齿轮之间,能够通过一张A4纸,A4纸不会破,且用手转起来两边阻力基本一致即可。具体情况将会在机械篇里面讲解,敬请期待。

初始化编码器后,将它放到定时器中断中,定时读取数据,读完清零。

读到的数据越大,说明车轮转速越快。

初始化,读编码器数据代码如下:

#define ENCODER_L_CH1   TIM1_ENCOEDER_MAP3_CH1_E9#define ENCODER_L_CH2   TIM1_ENCOEDER_MAP3_CH2_E11#define ENCODER_L_TIM   TIM1_ENCOEDER#define ENCODER_R_CH1   TIM9_ENCOEDER_MAP3_CH1_D9#define ENCODER_R_CH2   TIM9_ENCOEDER_MAP3_CH2_D11#define ENCODER_R_TIM   TIM9_ENCOEDERencoder_dir_init(ENCODER_L_TIM,ENCODER_L_CH1,ENCODER_L_CH2);      //编码器初始化  左后轮encoder_dir_init(ENCODER_R_TIM,ENCODER_R_CH1,ENCODER_R_CH2);      //编码器初始化  右后轮//下述代码放在定时器中断中Speed_Left_Real=(-encoder_get_count(ENCODER_L_TIM));//加个负号,保证向前走编码器是正数,倒退是负数,更符合直觉Speed_Right_Real=(encoder_get_count(ENCODER_R_TIM));encoder_clear_count(ENCODER_R_TIM);encoder_clear_count(ENCODER_L_TIM);Velocity_Control(Speed_Left_Real,Speed_Right_Real);//读取转速,闭环控制
2.1.1 沁恒使用方向编码器的bug

我之前使用过调车使用的是英飞凌的芯片,使用起来编码器一切正常。

后来根据规则使用了沁恒的CH32V307主控,编码器使用的是某邱家的1024线方向编码器,会出现bug。

就是有一个轮子跑着跑着会疯转。原因是有一个编码器在正转的时候会莫名其妙读到负数,然后PID认为误差过大,开始拉满占空比调整,轮子就疯转起来。

这个问题不止我一个人遇到,我也咨询过技术人员,得到的答复是直接读寄存器的值。

关于编码器bug在群中的讨论
//-------------------------------------------------------------------------------------------------------------------// 函数简介     定时器编码器解码取值// 参数说明     timer_ch      定时器枚举体// 返回参数     void// 备注信息// 使用示例    encoder_get_count(TIM2_ENCOEDER)  // 获取定时器2的采集到的编码器数据//-------------------------------------------------------------------------------------------------------------------int16 encoder_get_count(encoder_index_enum encoder_n){    int16 result = 0;    int16 return_value = 0;    switch(encoder_n)    {        case TIM1_ENCOEDER:  result = TIM1->CNT;   break;        case TIM2_ENCOEDER:  result = TIM2->CNT;   break;        case TIM3_ENCOEDER:  result = TIM3->CNT;   break;        case TIM4_ENCOEDER:  result = TIM4->CNT;   break;        case TIM5_ENCOEDER:  result = TIM5->CNT;   break;        case TIM8_ENCOEDER:  result = TIM8->CNT;   break;        case TIM9_ENCOEDER:  result = TIM9->CNT;   break;        case TIM10_ENCOEDER: result = TIM10->CNT;  break;        default:             result = 0;                 break;    }    if(0xFF == encoder_dir_pin[encoder_n])    {        return_value = result;    }    else    {        if(!gpio_get_level((gpio_pin_enum)encoder_dir_pin[encoder_n]))        {            return_value = -result;        }		else		{			return_value = result;		}    }    return return_value;}

上面是方向编码器的读取代码,在读取寄存器数值后,又用IO口进行了方向的判断。

如果直接读寄存器的值,那么就失去了方向这一个数据,

当然,可以用角度编码器,或者正交编码器试试,不清楚有没有这个bug。

我个人也遇到了这个bug,我是左轮有这个问题,右轮一切正常。

我对左轮编码器数据直接取了绝对值,因为正常跑车过程中不会有负数出现。

在刹车时,用PID刹车1s,然后PWM给0。用PID是保证迅速刹车,用PWM给0是为了防止疯转,切断电机输出。车子一般在PID刹车时就停下来了,PWM给0就不会因为惯性再往前跑了。

当然,也有可能是硬件坏了,大家可以换块板子试试,或者换个编码器试试。

其他主控平台暂未发现这个bug,希望代码库尽快更新,修复这个bug。

2.2 PID

正常情况下,我们希望车模在直道跑到的快一点,弯道稍微慢一点。当速度足够高时,我们还会希望后轮进行差速,弥补舵机打角的不足。

想要控制车轮转速快速到达预期值,就需要闭环控制,闭环控制使用最大多的就是PID算法。

我使用的是增量式PI算法,大部分智能车也都是增量式PI算法,算法不再重复介绍,直接上代码。

/*-------------------------------------------------------------------------------------------------------------------  @brief     PID控制  @param     int set_speed ,int speed,期望值,实际值  @return    电机占空比SPEED_MIN~SPEED_MAX  Sample     pwm_R= PID_R(set_speed_right,right_wheel);//pid控制电机转速             pwm_L= PID_L(set_speed_left,left_wheel ); //pid控制电机转速  @note      调参是门玄学-------------------------------------------------------------------------------------------------------------------*/int PID_L(int set_speed ,int speed)//pid控制电机转速{    volatile static int out;    volatile static int out_increment;    volatile static int ek,ek1;    float kp=1.46,ki=2.3;      if(Go==7)//正常运行状态使用的参数    {        //float P_L=30;        //float I_L=1.6;        kp = P_L;//一套pi足矣,速度拨动不会太大        ki = I_L;    }    else//发车阶段速度环要硬,不能超调晃动    {        kp = 20.0;//一套pi足矣,速度拨动不会太大        ki = 0.9;    }    ek1 = ek;    ek = set_speed - speed;    out_increment= (int)(kp*(ek-ek1) + ki*ek);    out+= out_increment;    if(out>=SPEED_MAX)//限幅处理        out=SPEED_MAX;    else if(out<=SPEED_MIN)        out=SPEED_MIN;    return (int) out;}

我理解的PID就是跟随,我在当前期望一个速度,输入进去,同时输入进一个当前转速,输出的就是PWM波的期望占空比,我的期望值有可能在随时改变,直道要加速,弯道要减速,还要差速,那就要做到输出与输入跟随的紧密,不超调,不震荡。中间计算过程无需关心,我只关系输入与输出。

调车过程中的速度线

黄线是期望,绿线是实际

蓝线是期望,红线是实际

 上面两张图是我实际调出来的速度曲线图,通过调整合适的PI参数,是完全可以做到速度响应好的效果,提速快,刹车稳。

(这里打个广告,上面图是我的车使用蓝牙向上位机发送的数据,上位机可以根据收到的数据绘制出曲线图,相应的软件和通讯协议规范我之前的文章中有,传送门在这里:VOFA+上位机三种协议(FireWater,JustFloat,RawData)C语言参考代码_justfloat协议_Joshua.X的博客-CSDN博客)

这里提醒一下,调PID时候要让车在赛道上跑,收集实际数据。车模悬空时候的参数基本不用调就可以特别好看,但没什么用。

空载速度线

负载速度线

 这是我在群里看到的两张图,很有代表性。显然空载和负载曲线完全不同。

2.3棒棒算法

棒棒算法是补充PID的算法,他正常情况下不会作用。只在极限情况下使用,与PID配合。

具体作用是当当前转速与期望转速过大,直接拉满输出。这里的拉满是有正有负的。

保证了瞬间可以将速度拉上去,瞬间将速度降下来。

代码如下

#define SPEED_MAX   4000  //电机速度限幅,正#define SPEED_MIN  -4000  //电机速度限幅,负void Velocity_Control(int speed_left_real,int speed_right_real)//赛道类型判别,来选定速度{    int pwm_R=0,pwm_L=0;    if(Go==7)//当标志位被置7后,正常进行速度控制    {        //速度决策        //速度决策        pwm_L= PID_L(Speed_Left_Set ,speed_left_real );//pid控制电机转速        pwm_R= PID_R(Speed_Right_Set,speed_right_real);//pid控制电机转速        //棒棒作为[PID的辅助        if(Speed_Left_Set - speed_left_real>150)        {            pwm_L=SPEED_MAX;        }        else if(Speed_Left_Set - speed_left_real<-150)        {            pwm_L=SPEED_MIN;        }        if(Speed_Right_Set- speed_right_real>150)        {            pwm_L=SPEED_MAX;        }        else if(Speed_Right_Set- speed_right_real<-150)        {            pwm_L=SPEED_MIN;        }    }    Motor_Left (pwm_L);    Motor_Right(pwm_R);}

当然,这样对PID的冲击非常大,会影响PID的稳定性。就像你骑自行车,刚起步的时候有人突然推你一把,你的速度能够立刻提起来,但是你不见得接受的住这突然的提速,很容易摔倒。

所以棒棒的阈值需要调整的比较高。我最后比赛没有使用。

2.4 ADRC

ADRC也是一种控制方式,作用和PID一样。

也是通过期望输出,实际转速之间通过计算,得到期望的占空比,不过由于我只听说过,完全没有接触过,这里只让大家知道这种算法,详细资料请自行获取。

3.速度决策

我的速度决策其实做的并不好,参数太少了,导致调整比较生硬。

化简代码如下:

int Base_Speed=330;     //基准速度int Straight_Speed=400; //直道高速float Shift_Ratio=1.5;  //变速系数float Err_Diff=1.1;     //常规差速系数,差速过大容易导致侧翻void Velocity_Control(int speed_left_real,int speed_right_real)//赛道类型判别,来选定速度{    int pwm_R=0,pwm_L=0;    if(Go==7)//当标志位被置7后,正常进行速度控制    {        if(Electromagnet_Flag==0)//默认摄像头跑        {            Speed_Left_Set =Base_Speed;            Speed_Right_Set=Base_Speed;            if(Straight_Flag==1)//直道高速冲刺            {                Speed_Left_Set=Straight_Speed;                Speed_Right_Set=Straight_Speed;            }            Speed_Left_Set =Speed_Left_Set -(MT9V03X_H-Search_Stop_Line)*Shift_Ratio;//变速            Speed_Right_Set=Speed_Right_Set-(MT9V03X_H-Search_Stop_Line)*Shift_Ratio;//            Speed_Left_Set =Speed_Left_Set -Err*Err_Diff;//差速            Speed_Right_Set=Speed_Right_Set+Err*Err_Diff;        }        pwm_L= PID_L(Speed_Left_Set ,speed_left_real );//pid控制电机转速        pwm_R= PID_R(Speed_Right_Set,speed_right_real);//pid控制电机转速    }    Motor_Left (pwm_L);    Motor_Right(pwm_R);}

我实际的控速测量要比这个复杂一些,主要是有出入库,坡道,横断,断路等元素,需要对速度做出把控。

只要能看懂我上述的代码,我开源的代码也是一样的,只是多了各种元素状态,不同速度而已。

  1. Base_Speed:车模在运行过程中我设置了一个基本速度,车模大部分时间是使用这个速度在跑;
  2. Straight_Speed:直道高速,车模前方是长直道,那么直接高速冲就完了。直道的判断会在元素文章里面讲。
  3. Shift_Ratio:变速系数,有效视野越短,说明是弯道,我的速度就需要在基准速度的基础上慢一点,视野比较长,就快一点。
  4. Err_Diff:差速系数,车模高速转弯时,仅靠舵机是会比较吃力,需要后轮进行差速,来辅助车模转弯,我的差速就是摄像头误差*差速系数,一边加,一边减,叠加到期望值中。误差越大,差速越大。但是不要太大,会导致车模侧。

基本的参数就是这四个,实际代码中有电磁速度,环岛速度,坡道速度,横断速度。还有不同元素对应不同的的差速系数,原理都是上面那一套,大家自行体会。

参数越多,调节起来越精确、就像山地车有那么多轮盘可以变速,因为不同的场地适应不同的轮盘,可以获得更好的效果。你用共享单车一套轮盘骑着走也不是不行,只是适应性差一些。

一个重点,参数越多,调节越精确,但是调节起来难度越大;参数少,各种参数适配会好做一点,但是效果会稍微差一点。

四、调参经验

1.方向

说实话,个人感觉对方向控制你影响最大的是机械。

每一套机械都会存在一套参数区间,不同的PD系数,不同的前瞻行,不同的误差权重都会带来不一样的效果。每套参数区间大小不一样,范围未知,只能凭借运气去试。所以参数越多,越难调。

机械就是摄像头高度,俯仰角度,摄像头位置这些,当你凭运气调整到一个比较好的位置,你的PD参数,误差权重,前瞻位置,都不用太大的调整,就可以跑的很流畅。

注:个人认为摄像头高度高一些会比较有利于图像,有利于调参。

我曾经就有一次,在调整完摄像头后随便给了个参数,车子跑的又快又稳,我还以为是运气好,参数对了。后期又调整了参数,发现其实参数对车模影响不大,机械影响要更大。

当然,你不能凭运气保证你的机械是合适的,该调参还是得调参。个人建议如下:

  1. 先将前瞻行,权重行放的位置低一点。
  2. 低速行驶下先用纯P控制速度开环处理。
  3. 等到开环控制到1.8m/s左右可以闭环,这时会瞬间感觉车灵活了,可以稍微提速。
  4. 到2.3/ms可以加上D,D可以增加车模的灵活性,属于锦上添花。
  5. 个人感觉P反应打角力度,D是灵敏度,刚开始调车小P大D效果好一点。
  6. 等到速度快了,2.5m/s就可以把前瞻行,权重往前放,有利于车模提速
  7. 行驶中,听到车模轮胎在地面发出“吱吱”漂移声,不是好事。说明前轮打角了,但是车子没有转弯,后轮在推着打角的前轮往前冲,车模前轮胎和赛道进行滑动摩擦,不是车轮滚动,建议直接换轮胎,这点会在后续的机械一章中讲解。
  8. 我个人的控制理念是一套控制算法控制全程,所以我基本没有对中线,边线,pd之类的进行特别处理。我只在元素处进行了必要的补线。当然这也根据不同人的想法,我也看到有大佬在技术报告中提到对中线进行滤波处理。这都可以的,只要效果好,没什么不行的。

以上均为个人建议,仅供参考。

2.速度

其实我只在前期调了速度曲线,想着让曲线更加丝滑,反应更快,更加贴着我的期望速度在跑。

但是后期我发现了一件事,我给的期望速度真的合理吗?

未必,我的期望速度不合理,那我实际速度贴的好又有什么用呢?

所以我将速度调整到反应迅速,提速,刹车都很灵敏的时候,我就不再看速度线了。因为有可能速度线中的超调,震荡有可能是适合车模差速过弯的。车模沿着速度线跑未必能跑出最好效果。

所以在不同场地时候,我更多的是调整期望速度,差速系数这些东西,没有动PI参数。

只在出库时候,有特殊要求,我调整了PI系数。

这里简单讲一下实际跑车中PI参数含义,以刹车为例。

  1. 在车模刹车中,纯P控制,就是车子慢慢刹车,直到刹停为止,P越大刹车阻力越大。
  2. I的作用:有一个微微的回弹,也就是超调。I越大,回弹次数越多,回弹越激烈。过大的I会导致控制发散,造成车子刹不住,还会“发疯”。
  3. 所以一般不建议I给太大,P可以适当给大一点。
  4. 不同的车子PI系数没有什么参考价值,我的队友的PI都给到好几万才有一点效果,我只给了几十。所以说效果好就行了,不必纠结参数。

大家也可以将期望车速给0。

  1. P越大,车子越难推,松手后车模只会慢慢回到原点,基本不会回弹。
  2. I越大,车子松手后,会像弹簧一样,在震动中逐渐停止。

最后就是我三年调车经验,在速度环中:P大能抑制I的超调。

粉线期望,紫线实际

 在上图中,前三个框明显出现了超调现象。在后面的调车中,我增大P,超调变小了。

以上均为个人建议,仅供参考。

3.限幅

方向限幅是为了防止舵机打角过大,轮胎卡底盘,找到轮胎左右打角极限就好。

速度限幅是为了防止占空比输出异常,电机疯转。

速度限幅和供电有关系,380电机的额定电压是7.2v。

如果用正常的2S电池,满电8.4v,那么将占空比拉满了输出给到电机大概就是8v左右,满占空比10000,限幅给到95%~100%都是可以的。

如果用3S电池满电12.6v,拉满占空比给到电机,电机也能抗住,注意散热即可。

这里就劝告各位,调车过一会就让车歇一会,用个风扇吹一吹,保证电机不烧。我同学调车,调起来就根本停不下来,电机都烧了十好几个了,换一个电机参数也都是需要调整的,大家注意。

我比赛需要充电,电压高效率会高一些,所以我选择的是6s电池,满电25.2v。前面也看到我的限幅值在4000,也就是40%。在限幅情况下拉满占空比25.2*0.4=10.8v。

以后比赛应该用不到6s电池,大家使用2s或者3s电池,限幅给到95%~100%就好。

希望能够帮助到一些人。

本人菜鸡一只,各位大佬发现问题欢迎留言指出

也许您对下面的内容还感兴趣: