编者注:本项目来自Instructables,作者为newtonis。这是一个用于比赛的循迹跟踪小车项目,他是怎么在兼顾尺寸和速度性的同时满足小车高性能的要求的呢?
循迹小车在创客圈里面并不是什么新鲜事,但普通的循迹小车要么比较大,要么则比较重,并不适合用来比赛。在我们当地的创客圈子里面有一个循迹小车的比赛项目,要求上场的小车的尺寸不能超过12x20cm,那我们该怎样来满足这样的需求呢?
本项目的主要开发过程分为三个部分:电子构建,结构设计和3D打印,程序配置和调试。下面我们就来完成它吧。
在开发此项目的过程中,要切记的一点是我们是在开发一个比赛用的小车。此处我选用了Pololu提供的电机,他家的电机分为四种:低功率、中等功率、高功率和长寿命高功率,这四种电机的工作电压都为6V,而且物理尺寸也基本相同。低功率的电机功耗较低,但是要知道我们这是为了比赛,所以应该选择那两种高功率的电机。其中长寿命高功率的电机的使用寿命更长,但是并不能承受高于6V的电压。综合考虑之后,我选择了高功率的电机,它们能够不中断地连续使用好几个小时,这已经足够了。
接下来我们应该选择多少的传动比呢?通常电机的传动比越高,电机的速度就越低,但是电机提供的扭矩却更高,就能提供更高的加速度。对比较重的机器人来说这确实非常重要。我的建议是在30:1到10:1的传动比之间做出选择,最后我选择的四个传动比为10:1的电机,毕竟是为了比赛,我会尽量降低小车的质量,从而提供更高的速度。当然,你也可以选择使用2个30:1的传动比的电机,这样为了获得更高的速度,你需要为电机提供高于6V的电压。
因为电机的速度是以每分钟的转速衡量的,所以在转速固定的情况下,轮子直径越大,理论上就跑得越快,但与此同时轮子也会越重。综合考虑之后我选择了直径32毫米的窄边车轮,其单个重量为3.2克。
电池方面我推荐使用储能密度大、可充电的LiPo电池。我使用的是850mAh 7.4V双芯LiPo电池。
为了能让小车的电机获得足够的反应时间,需要将小车的循迹探测器尽量前置,那么这就需要用到2个前置的球脚轮.当然这两个球脚轮并不是必需的,如果你的小车的设计能够在行进过程中维持平衡,或者只是采用后轮驱动的方式运动(将探测器安装在车体前部),那么就不需要这两个球脚轮。
注意两只球脚轮的相对位置,不要让其对小车的行进方向产生影响。
为了在保证小车基本结构上尽量降低小车的质量,我选择自己设计小车的结构件。点击这里下载我用Google SketchUp设计的3D结构模型,然后用3D打印机打印出来即可。注意,打印之前需要导出适合3D打印机的STL文件。
这一步我需要搭建能够用来探测地面标线的探测器设备。这里我用到了9个CNY70光电传感器(5个或7个应该也可以),9个220Ω电阻和9个56kΩ下拉电阻。另外我还设计了一个简易的PCB板用来承载我的电路结构。
这套探测器的原理很简单,每个CNY70传感器都会产生0-5V的电压,其中0V左右表示探测到黑色,5V左右则是白色。CNY70提供了0-1024的精度范围。
电路板上一共引出了11个接口,其中两个分别是接地和5V电源,其它9个是传感器的模拟输出。电路本身并不复杂,复制以下电路9次即可。
探测器的电路不复杂,主电路却相对较为复杂一点。这里我们没有使用常见的Arduino,而是使用了PIC18F4550单片机,因为其体积更小,而且在某些比赛中是禁止使用Arduino的。
电路设计如下:
相关文件:
主电路的组件清单如下:
PIC18F4550单片机
20MHz晶振
3到6个LED(PCB上只使用了3个)
3个220欧姆电阻(用于LED)
2个10nF陶瓷电容
2个18nF陶瓷电容
L293D或SN7544(电机驱动模块)
开关
三个按钮
7805三端稳压器(为单片机提供5V电压)
线材等
接下来就是制作对应的PCB板。
然后将各个组件焊接到PCB上。
接下来就要对我们机器人的大脑做文章了。这里我使用的是MPLAB XC8编译器和Sublime Text作为开发环境。具体的安装过程这里不提,这里提供了Sublime Text用来构建和运行XC8的命令行代码,你需要用它来配置XC8的路径和安装PK2CMD。
rayito.c是我编写的代码,下一个步骤我会对其中一些代码进行一些说明。
代码的工作原理示意图:
各个传感器的工作模式示意图:
传感器的精度为0-1024,但事实上我们的测量基本上不可能达到两边的顶点(纯黑和纯全反射)。那么我们就可以对传感器得到的数值进行定义,低于A值都定义为黑色,高于B值都定义为白色,则有0<=A<=B<=1024。
对传感器的读数进行调试和校准:
void initLED(){ ///初始化读数变量,在程序启动时调用一次
int x;
for (x = 0;x < 8;x++){
amax[x] = 0; ///Amax是上面提到的B值
amin[x] = 1024; ///Amin是上面提到的A值
}
}
void CalRead(){ ///读取校准数值
L_ROJO = T1000 < 500*6; ///红色LED闪烁
int x;
for (x = 0;x < 7;x++){ //跟踪传感器
/***注意: 使用T(x)是因为其在程序的第一部分就进行了定义,让排成列的传感器能够以正确的顺序排列。***/
amax[x] = max(amax[x],T(x)); ///如果得到的值高于上一次值则替代
amin[x] = min(amin[x],T(x)); ///如果得到的值低于上一个值则替代
}
///不能使用T(7)加入第7个传感器的数值,该传感器不用于读路线。.
///我们用其来读取某些赛道特定的曲线路径标记。
amax[7] = max(amax[7],J(7));
amin[7] = min(amin[7],J(7));
}
经过了此段之后,我们会得到7个由传感器产生的数值,并将其存储在C数列中,每一个传感器都有amin (A)和amax (B)值。那么在程序进行标准取值时,我们将小于A的值都当成A,将大于B的值都当成B。中间值忽略。
///这是伪代码,不能直接工作,只用于了解原理
///在实际代码中,amax[x]是B而amin[x]是A
int w = W[x]; ///W[x] 取值0~1024
w = min(w,B); //如果w大于B,则w=B
w = max(w,A); //如果w小于A,则w= A
w -= A; ///让w逻辑上取值在0到B-A
w *= 1024; ///w取值为(0到B-A)*1024
w /= (B-A); ///现在w取值为0 到1024
这一部分算法是我们实现高性能的关键部分之一。在本算法中,用于取平均数的数字的价值越高,其对最终平均数的影响就越大。这样我们的算法就不只是以下的算法那么简单了:
if (S1 see black){
Left();
}else if (S2 see black){
Right();
}else{
Front();
}
为了真正造出一个高性能的机器小车,我们需要尽可能准确地确定路线的中心线。使用传感器进行测量时,每一个程序循环中我们都可以得到5组不同的中心线位置,而我们需要通过加权平均的方式将这5个数值转变成一个数值。
void Ponderado(){ sum = 0;
division = 0;
nove = 0;
char center;
for (x = 0;x <= 6;x++){
if(T(x)>amax[x]){
w=amax[x];
}else if(T(x) < amin[x]){
w=amin[x];
}else{
w=T(x);
}***/
w = ran(T(x),amin[x],amax[x]);
w -= amin[x];
w *= (ll)1000;
w /= (amax[x]-amin[x]);
if (w > TH){
nove = 1;
}
if (x == 3){
if (w > TH){ center = 1; }else{ center = 0; } ///We store here if the center sensor reads 0 or 1000
}
///A is the weight
///B is the position
if (x == 0 or x == 6){ continue; } ///We ignore the sensors 0 and 6
v = (1000) * (x-3);///This variable will be -2000,-1000,0,1000,2000 in each cycle
sum += (w*v);
division += (w);
}
if (nove == 0){ ///if we don't see the line POSICION = POSICION > 0 ? 200 : - 200; ///Now we set the value accordingly the last value set. if posicion is > to 0 then posicion equals 200, if not it equals -200
}else{
POSICION = (ll)(sum) / (ll)(division); ///We get the weighted average result
POSICION /= 10; ///We change the scale from -2000,2000 to -200,200
}
}
确定了中心线之后就需要控制电机对线进行跟踪了。由于我们采用的电机并不具有直接转向的功能,我们需要用到另一种转向方式:速度差转向——通过为小车的轮子设定不同的速度从而让小车整体产生转向效果。控制速度则用到了常用的脉冲宽度调制,这一部分整合到了小车的代码中。此处我们需要用到一个函数MotorsSpeed(a,b),其将定义电机的速度,并将其设置到区间(-1000,1000)之间,包括前后速度。
void MotorsSpeed(int A,int B){
///If mode equals alfa then we invert the motors voltage
MotorASpeed(MODE == ALFA ? A : B);
MotorBSpeed(MODE == BETA ? A : B);
}
void MotorASpeed(int S){
S = min(S,1000);
S = max(S,-1000);
ADIR = S > 0 ? 0 : 1;
S = S > 0 ? S : 1000 + S;
CCP1CONbits.DC1B1 = S % 4;
CCPR1L = S / 4;
}
void MotorBSpeed(int S){
S = min(S,1000);
S = max(S,-1000);
BDIR = S > 0 ? 0 : 1;
S = S > 0 ? S : 1000 + S;
CCP2CONbits.DC2B = S % 4;
CCPR2L = S / 4;
}
实现了对电机的速度控制,就可以实现转向算法了。
void LineFollow(){
double kp,kd,kr,speed;
///POSICION has the value of the center of the line previously calculated
kp = KP[speedMode];
kd = KD[speedMode];
kr = KR[speedMode];
DER = POSICION - LP; ///We calculate how much the line has moved from the last iteration
PIDf = (POSICION* kp + DER * kd);
if (PIDf > 0){
MotorsSpeed(Mr(speed-PIDf,kr) , speed); ///Mr makes the value to multiply by KR if it is negative. You can delete it
}else{
MotorsSpeed(speed , Mr(speed+PIDf,kr) ); ///Mr makes the value to multiply by KR if it is negative. You can delete it
}
LP = POSICION; ///We store the last line position
}
部署好了算法也就大功告成了,接下来就该拿到赛场上比试比试。哦,看起来高手还是挺多的嘛……
2015-2016赛季全球创客马拉松深圳大学站持续报名中!关注“硬创邦”(微信号:leiphone_bang),回复“深大”即可参与报名!而且,雷锋网在未来三个月内选一个合适的时间,在北上深选择一个地点,举办创马“火星救援”专场!详情可点击此处了解。
此外还可加入全球创客马拉松主群(群号:259592983),参与我们的互动讨论~