开源汇总写在下面
第18届全国大学生智能汽车竞赛四轮车开源讲解_Joshua.X的博客-CSDN博客
一、控制方法
1.控制对象基本介绍
1.1舵机
舵机是控制车模方向的重要组成部分,一般放在车头,实物见下图。
如果比赛限制车模类型为C车模,那么只能使用S3010(已停产)或者U400舵机进行比赛,控制舵机主要是控制PWM波的脉宽。控制规律如下图。
关于PWM波这里不再介绍,CSDN上有很多的介绍。
S3010,U400这两款舵机是的控制频率为50Hz(官方推荐参数),想要控制这个舵机,就需要单片机产生50HZ的PWM波,改变占空比,使脉冲宽度在1ms~2ms,使之实现左右转动,控制车模方向。
首先我们选取一个能够产生PWM波的IO口(并非所有IO口都可以产生PWM波),使他的PWM频率是50Hz。在配置的库中,所有PWM波的占空比的细分都被映射到了0~10000。(有些是0~50000,需要查看对应工程的实际情况)
占空比0是纯低电平,占空比10000是满占空比,也就是纯高电平。
我们一起来推导下如何才能实现脉冲在1~2ms之间。
已知我们配置是PWM频率为50Hz,那么周期就是20ms。
按照比例映射,当占空比为10000时输出是20ms脉宽,我们求占空比为x时,输出1.5ms脉宽。
那么我们得到了这样一个比例式:
%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实际上,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.单行控制/领跑行控制
他的本质就是电磁式跑法,将摄像头视野中固定的某行作为舵机打角行,计算该行的中线与理论中线的偏差,前瞻的位置是死的。他的特点在于只采用一行作为控制行,而且这一行是固定不动的,所以一但这一行的图像出现了问题,对于舵机打角的判断就会有很大的影响,而且方向判断只用一行,对于摄像头收集到的多行数据有些浪费。
建议与后文提到的的动态前瞻进行组合使用,可以取得较好的效果。
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,轮子效果就是归中打直。
还有一个问题,就是正负号的问题。舵机控制中,有很多处理需要考虑正负号。
- 首先是误差获取,是用(屏幕中线-赛道中线),还是(赛道中线-屏幕中线)。
- PD里面的期望值,正常来说我的期望值是0,那么我的error_current是err-0,还是0-err。
- 舵机输出函数,输出的值是STEER_MID+angle,还是STEER_MID-angle。
这几个数值的正负号最好都要统一,不然的话误差会直接反过来。车子往右偏,结果轮子还在往右打。
或者误差是正数,结果舵机打的是负数的角,虽然不影响运行,但是看起来仍有些不舒服。
最后一个问题,控制周期的问题。我在国赛代码中有看到过两种控制策略:
- 整套流程放在while(1)中,摄像头每处理完一帧图片,接下来就计算偏差,计算PD,进行舵机控制。
- 舵机控制放在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认为误差过大,开始拉满占空比调整,轮子就疯转起来。
这个问题不止我一个人遇到,我也咨询过技术人员,得到的答复是直接读寄存器的值。
//-------------------------------------------------------------------------------------------------------------------// 函数简介 定时器编码器解码取值// 参数说明 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);}
我实际的控速测量要比这个复杂一些,主要是有出入库,坡道,横断,断路等元素,需要对速度做出把控。
只要能看懂我上述的代码,我开源的代码也是一样的,只是多了各种元素状态,不同速度而已。
- Base_Speed:车模在运行过程中我设置了一个基本速度,车模大部分时间是使用这个速度在跑;
- Straight_Speed:直道高速,车模前方是长直道,那么直接高速冲就完了。直道的判断会在元素文章里面讲。
- Shift_Ratio:变速系数,有效视野越短,说明是弯道,我的速度就需要在基准速度的基础上慢一点,视野比较长,就快一点。
- Err_Diff:差速系数,车模高速转弯时,仅靠舵机是会比较吃力,需要后轮进行差速,来辅助车模转弯,我的差速就是摄像头误差*差速系数,一边加,一边减,叠加到期望值中。误差越大,差速越大。但是不要太大,会导致车模侧。
基本的参数就是这四个,实际代码中有电磁速度,环岛速度,坡道速度,横断速度。还有不同元素对应不同的的差速系数,原理都是上面那一套,大家自行体会。
参数越多,调节起来越精确、就像山地车有那么多轮盘可以变速,因为不同的场地适应不同的轮盘,可以获得更好的效果。你用共享单车一套轮盘骑着走也不是不行,只是适应性差一些。
一个重点,参数越多,调节越精确,但是调节起来难度越大;参数少,各种参数适配会好做一点,但是效果会稍微差一点。
四、调参经验
1.方向
说实话,个人感觉对方向控制你影响最大的是机械。
每一套机械都会存在一套参数区间,不同的PD系数,不同的前瞻行,不同的误差权重都会带来不一样的效果。每套参数区间大小不一样,范围未知,只能凭借运气去试。所以参数越多,越难调。
机械就是摄像头高度,俯仰角度,摄像头位置这些,当你凭运气调整到一个比较好的位置,你的PD参数,误差权重,前瞻位置,都不用太大的调整,就可以跑的很流畅。
注:个人认为摄像头高度高一些会比较有利于图像,有利于调参。
我曾经就有一次,在调整完摄像头后随便给了个参数,车子跑的又快又稳,我还以为是运气好,参数对了。后期又调整了参数,发现其实参数对车模影响不大,机械影响要更大。
当然,你不能凭运气保证你的机械是合适的,该调参还是得调参。个人建议如下:
- 先将前瞻行,权重行放的位置低一点。
- 低速行驶下先用纯P控制速度开环处理。
- 等到开环控制到1.8m/s左右可以闭环,这时会瞬间感觉车灵活了,可以稍微提速。
- 到2.3/ms可以加上D,D可以增加车模的灵活性,属于锦上添花。
- 个人感觉P反应打角力度,D是灵敏度,刚开始调车小P大D效果好一点。
- 等到速度快了,2.5m/s就可以把前瞻行,权重往前放,有利于车模提速
- 行驶中,听到车模轮胎在地面发出“吱吱”漂移声,不是好事。说明前轮打角了,但是车子没有转弯,后轮在推着打角的前轮往前冲,车模前轮胎和赛道进行滑动摩擦,不是车轮滚动,建议直接换轮胎,这点会在后续的机械一章中讲解。
- 我个人的控制理念是一套控制算法控制全程,所以我基本没有对中线,边线,pd之类的进行特别处理。我只在元素处进行了必要的补线。当然这也根据不同人的想法,我也看到有大佬在技术报告中提到对中线进行滤波处理。这都可以的,只要效果好,没什么不行的。
以上均为个人建议,仅供参考。
2.速度
其实我只在前期调了速度曲线,想着让曲线更加丝滑,反应更快,更加贴着我的期望速度在跑。
但是后期我发现了一件事,我给的期望速度真的合理吗?
未必,我的期望速度不合理,那我实际速度贴的好又有什么用呢?
所以我将速度调整到反应迅速,提速,刹车都很灵敏的时候,我就不再看速度线了。因为有可能速度线中的超调,震荡有可能是适合车模差速过弯的。车模沿着速度线跑未必能跑出最好效果。
所以在不同场地时候,我更多的是调整期望速度,差速系数这些东西,没有动PI参数。
只在出库时候,有特殊要求,我调整了PI系数。
这里简单讲一下实际跑车中PI参数含义,以刹车为例。
- 在车模刹车中,纯P控制,就是车子慢慢刹车,直到刹停为止,P越大刹车阻力越大。
- I的作用:有一个微微的回弹,也就是超调。I越大,回弹次数越多,回弹越激烈。过大的I会导致控制发散,造成车子刹不住,还会“发疯”。
- 所以一般不建议I给太大,P可以适当给大一点。
- 不同的车子PI系数没有什么参考价值,我的队友的PI都给到好几万才有一点效果,我只给了几十。所以说效果好就行了,不必纠结参数。
大家也可以将期望车速给0。
- P越大,车子越难推,松手后车模只会慢慢回到原点,基本不会回弹。
- 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%就好。
希望能够帮助到一些人。
本人菜鸡一只,各位大佬发现问题欢迎留言指出