开源汇总写在下面
第18届全国大学生智能汽车竞赛四轮车开源讲解_joshua.x的博客-csdn博客
开源链接写在下面
https://gitee.com/joshua_xu/the-18th-smartcar
https://gitee.com/joshua_xu/the-18th-smartcar
注:文章中所有参数,角点范围之类的东西仅作为参考,实际参数,请根据需要实际调整!!!!!!!!!
实际上,智能车所有参数都需要根据你的实际情况进行调整,万万不可照搬不误!!!!!
一、控制方法
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脉宽。
那么我们得到了这样一个比例式:

解得x=750时,单片机将输出50hz,脉宽时间为1.5ms的pwm波。对应着舵机理论归中。
舵机初始化代码如下:
#define steer_pin tim2_pwm_map3_ch1_a15//舵机控制信号输出口
#define steer_right 705 //舵机右打死,-
#define steer_mid 782 //舵机归中
#define steer_left 857 //舵机左打死,+
//舵机初始化函数
pwm_init(steer_pin, 50, steer_mid);
我的中值steer_mid是782,由于安装位置,拉杆机械误差,车模机械误差,实际中值一般很难是750,但是也在750附近,符合理论估计。
拉杆长度,机械偏差,轮子磨损,都会影响舵机中值
实际上,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算法,大家的车子其他的流程都是一样的,所谓国赛代码,普通代码的差距一般也就在这里。
当然,大家也不要指望单纯换一个误差获取,或者换一个模糊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,没丢线置0
int 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届车赛采用的做法,具体讲述就是固定某个范围(此处是最下面valid_height行),将这范围内的误差积累求和,将这个和作为误差返回。
#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.误差平均
这个算法是上面误差积累的改进,也是固定某些行(此处是51~55行),计算他们的误差,但是这里取了平均值,对于一些异常数据会有一定的滤波抗干扰作用。
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.加权平均
这个是我本届比赛使用的方法,本人使用比较稳定。
先将赛道的每一行都给予相应的权重。
建议先使用平均误差找到比较合适的前瞻范围。
这里权重并没有什么特别要求,在主要控制行(控制前瞻)位置将权重放大一点就好,后面还要取平均所以数据并不会有太大改变。
这里权重仅作为参考,实际参数,需要实际调整,实际上,智能车所有参数都需要根据你的实际情况进行调整!!!!!
当然,大家也不要指望单纯换一个误差获取,或者换一个模糊pd就让车子直接三米,拥有质的飞跃。这显然是不现实的。想要车子跑的快,需要从机械到图像,控制,参数甚至赛道,这些要素做到十足的搭配,磨合。每一条都需要调整很久,代码也需要精心斟酌推敲。
即使车子控制可以直接从一米八飞升到三米,图像也需要做对应的调整,因为三米车他的图像和一米车完全不一样,元素关键帧变少,留给你的容错空间更小。
饭要一口一口吃,路要一步一步走,希望大家一步一步学习,一点一点进步。
//加权控制
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_encoeder
encoder_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。
注:有些同学看到商家提供的demo中是在main函数中使用delay_ms(10)这样的函数,就在车子的代码中写了delay,我说明一下,不用!!!
demo中是使用delay来模拟定时读取的效果,我们直接放在定时器中断中,做到了真实的定时读取,完全不需要使用delay。事实上,智能车中很少很少使用delay,也就是在按键处消抖处使用一下,其他地方几乎不用,因为智能车处于毫秒级控制,直接delay太浪费资源了。
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波的期望占空比,我的期望值有可能在随时改变,直道要加速,弯道要减速,还要差速,那就要做到输出与输入跟随的紧密,不超调,不震荡。中间计算过程无需关心,我只关系输入与输出。
有些同学关心pid的计算输出,我的建议是无需关心中间计算过程,不用关心每次算出来的占空比是多少。
就像我们在开车的时候,我想保持60km/h的车速,我不用关心我油门需要踩下几厘米,也无需关心我车子载了几个人,从而需要调整我的油门深度,我只要关心我当前速度到没到60,没到我就踩油门,到了我就踩刹车,差的多我就猜深一点,超速我就刹车踩一点。如此操作,若干次后,我就可以保持车速在任意我想要的区间,pid就是基于不断进行误差比较实现的控制,你只需要调用就好。当然想要实现响应快准狠,需要大量的调整参数。
这里pi参数仅作为参考,实际参数,需要实际调整!!!!!!!!!
实际上,智能车所有参数都需要根据你的实际情况进行调整!!!!!
再次注意,pid会算出负数,这表示电机需要反转,强行减速刹车,所以我pid输出使用的有符号int,这里反转需要再电机驱动处进行处理,我在驱动方式那里有写。
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之类的进行特别处理。我只在元素处进行了必要的补线。当然这也根据不同人的想法,我也看到有大佬在技术报告中提到对中线进行滤波处理。这都可以的,只要效果好,没什么不行的。
我在一些视频评论区中看到,有些人说pid可以通过数学建模,然后计算传递函数,从而计算出理论最优值。
但是根据我的与一些智能车强校,国一车手,甚至是我上班公司的985硕士工程师,十年工作经验老工程师,甚至是教我自控原理课的老师交流。实际使用的时候,没有人会去建模,大家都是凭着感觉在调参,而且pid参数没有最优,只要调到合适就好。
以上均为个人建议,仅供参考。
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%就好。
希望能够帮助到一些人。
本人菜鸡一只,各位大佬发现问题欢迎留言指出
发表评论