#确定基本思路
我们这里需要的是一个可以按照地上画好的黑线运动的小车。由于都没有经验,所以首先想到的就是去网上找现成的模型确立大致的思路。
差不多就是这个样子吧。
需要的有:可以探测地面上黑线的传感器,一个底盘,两个马达和配套的轮子(选择二驱的小车在之后也是一个坑),一套可以充电的电池,控制电机转动的芯片,一个稳压模块,一个最小系统板,一个单片机(这里都是对应89c52系列的)。接着就是在万能的某宝上物色了。
这个是红外循迹传感模块
这个是稳压及电机驱动模块,但是我买的要比这个小一些,电机驱动模块和稳压模块是分开的,是看起来要比这个小巧很多。
最小系统板(左)和稳压模块(右)特写
电池,马达还有红外探头特写
差不多就是这些东西了,还有一个电机驱动模块太小了又是藏在里面的所以不是很好拍摄。
虽说看起来只是简单地这几样东西,感觉不会花费太多资金,可是在买零件的时候都是要多备货的,当然还有什么连接线,充电器之类的小配件这里不会一一列出来。到头来一算还是快花了快两百元钱。
开始什么都不知道我们的思绪是这样的。
#拼装小车
随着零件陆陆续续的到达,拼装小车的工作也要马上开始了,眼看着离比赛只有不到一个月的时间了但是一个组三个人还是什么都不会,却也不怎么慌,这还真是奇特的现象啊。拼装也花了比较长时间,一个下午,还不算之后对应软件的调整。特别是细小位置需要焊锡的地方,简直是要把人逼疯了。
#投身软件
对于这种最基本的循迹小车来说也不需要什么高端的算法了,网上都是主推PWM实现两轮差速转向。可是都花了这么多资金了怎么都想让自己的车变得厉害一点!所以又在网上看了很长时间又发现一种叫PID的算法也被广泛的应用在循迹小车中,于是就入了PID算法的大坑。也不是说PID算法有多么的难或者多么的高端只有大神才能用之类的,只是要耐心看一下罢了。
##PID
//PID指针初始化
void PID_init(void){
sptr->LastError = 0;
sptr->SumError = 0;
}
//PID计算函数,返回PID计算值
int PIDCalc(void){
float dError,Error;
Error = middle_position - sensor; //偏差
sptr->SumError += Error; //积分
dError = Error - sptr->LastError; //当前微分,应减去前三次的位置差,做近似处理了
sptr->LastError = Error;
return (KP*Error+KI*sptr->LastError+KD*dError);
}
看似一个结构体就能解决的算法在实际使用中还是各种坑。首先我们没有考虑到单片机本身的运行速度以及其内部的存储空间,还有没有没有考虑到传感器的不稳定性(真的是便宜没好货啊)以及各种在实际电路中会出现的奇怪问题。
到后来在图书馆花了一天的时间把整个PID的循迹代码写完之后再回寝室把代码烧录到单片机里面发现整个硬件系统并不能顺利的运转,总是有那么几个传感器不听话,或者说是不稳定,要知道我可是连最难的调整比例参数的步骤都没有坚持到呢。还有直流电机的控制也没有达到我们所需要的程度。到最后实在是没有办法了,这套PID算法只得作罢,又回归到了最原始的PWM算法算来。起先准备安装的8个循迹传感器也就调整成了4个,有时候的确应该遵守前人的规矩啊,这样可以少走很多弯路,节省很多时间。
##PWM
PWM:脉冲宽度调制。从名字上可以看出来,这个算法就是通过调整高电平时间对整个周期的占比从而改变输出电压的有效值以达到调整电机转速的目的。怎么调整高电平时间对整个周期的占比呢?这里又要讲到单片机的中断系统(PWM也可以用硬件直接实现),不过我们这里的就比较低端了,只是用到了定时器中断。使用中断需要注意的地方就是初始值的填充以及定时器工作方式的选择。
//定时器函数用来生成PWM波形,定时为1ms
void timer0() interrupt 1 using 2{
TH0=0XFC;
TL0=0X66;
pwm_value_left++;
pwm_value_right++;
pwm_left();
pwm_right();
}
//左轮PWM函数定义,使左轮的速度变得可以调节从而实现两轮差速转向
void pwm_left(void){
if(push_value_left!=0){
if(pwm_value_left<push_value_left)
leftzh=1; //左电机正极,以此类推
else
leftzh=0;
if(pwm_value_left>20)
pwm_value_left=0;
}
}
//右轮PWM函数定义
void pwm_right(void){
if(push_value_right!=0){
if(pwm_value_right<push_value_right)
rightzh=1;
else
rightzh=0;
if(pwm_value_right>20)
pwm_value_right=0;
}
}
PWM的代码还算是比较简单易懂,定时器函数后面的interrupt用来选择中断模式,using用来指定寄存器。
interrupt 0 指明是外部中断0;
interrupt 1 指明是定时器中断0;
interrupt 2 指明是外部中断1;
interrupt 3 指明是定时器中断1;
interrupt 4 指明是串行口中断;
using 0 是第0组寄存器;
using 1 是第1组寄存器;
using 2 是第2组寄存器;
using 3 是第3组寄存器;
然后程序就这样被不知不觉得写完了。
#调试
调试小车的地方在另一个校区,每天背着个电脑,提着小车过去也算是锻炼身体了。
调试比较坑的地方就是人很多,本来就不大的跑道被一群基佬们围着,偶尔可以看见几个妹子。然后追尾啊什么的都是常事,最气的就是追尾把传感器撞歪了,本来传感器就是便宜货不是很稳定这下一撞就真的不知道会发生什么事情了(室友的小车就是因为传感器不稳定在预赛就跑出轨道被刷了,不然可以稳拿三等奖)。还有调整参数的时候需要不断拿下芯片烧写然后再给装到车上去,真的是要被烦死了(Mac系统跑着win的虚拟机,酸爽可想而知)。
起初push_vlaue还不敢给大,还在探索阶段嘛,小车的速度那真的可以拿出来和乌龟比一下了,然后在预赛中华丽的跑出了49秒的成绩(小组第一19秒),但还是进入了决赛。到了决赛就直接把push_vlaue推满了,用上了各种延时策略让小车在转弯的时候有足够的时间反应(最刺激的还是直角弯,两轮反向转向,整个过程没有用到0.5秒)。总的来说调试就是一个不断探索不断突破的过程吧,仔细想想还是蛮有意思的。
#决赛
就差不多是这个场地了,但是有一个规定的停止区域,如果小车最后没有在规定的停止区域停下来那么会被罚秒,说白了就是有关传感器和一个判断函数的事情了;判断函数是小事,但是这个传感器就得真的把它当大爷供着(结果传感器在预赛和决赛中都表现优异!)。
最后成绩:第七名
因为外观加分输给了第六名零点几秒很是不甘心,所以什么事情都要力求做到完美不要给自己留下遗憾。
#最后说几句
1.比赛之前应该对自己的车要有足够的了解,要几轮驱动;传感器要怎么装;电池电压要给多大;蓄电池怎么选?这些都是值得去考虑的。
2.调整自己的心态,不能说自己只是参加这个比赛玩一下就可以放松。想要拿到成果就要搏一搏,也意味着你要付出比别人更多的努力。你可能会意识到,5V电压给电机供电可能不够,所以单片机和电机要分开供电;现成的代码会让你卡在这个瓶颈,所以你要花更多时间去优化;两轮驱动可能不够,所以你要写更多的代码去控制。
3.元件还是不要买太便宜的,不然到了快要比赛的时候元件出问题了又来不及在淘宝上买真的很着急。
4.选择自己的队友真的很重要,不将就。
我们的车:
全家福:
贴一下本人小车的代码,说了是单片机入门,不喜勿喷。
#include <reg52.h>
//传感器各引脚定义,其实我把middle的传感器拔下来了(对我的车子来说不要中间的这个传感器跑的更好),所以之后middle会默认一直为1
sbit left1 = P0^0;
sbit left2 = P0^1;
sbit middle = P0^2;
sbit right1 = P0^3;
sbit right2 = P1^0;
//电机驱动模块各引脚定义,zh:正,fu:负
sbit leftzh = P3^4;
sbit leftfu = P3^5;
sbit rightzh = P3^3;
sbit rightfu = P3^2;
//电机全速前进定义
#define unint unsigned int
#define Left_moto_go {leftzh=1,leftfu=0;}
#define Right_moto_go {rightzh=1,rightfu=0;}
//PWM和push值以及临时变量定义
unint pwm_value_left = 0;
unint pwm_value_right = 0;
unint push_value_left = 0;
unint push_value_right = 0;
unint num = 0;
//延时函数定义,延时x毫秒
void delay(unint xms){
unint i,j;
for(i=xms;i>0;i--)
for(j=110;j>0;j--)
;
}
//全速前进函数定义,最大速度20
void run(void){
push_value_left=0;
push_value_right=0;
Left_moto_go;
Right_moto_go;
}
//普通前进函数定义,普通速度15
void run_usual(void){
rightfu=0;
leftfu=0;
push_value_left=15;
push_value_right=15;
}
//全部停止函数定义,两边急刹
void stop(void){
push_value_left=0;
push_value_right=0;
leftzh=1;
leftfu=1;
rightzh=1;
rightfu=1;
}
//左直角转弯函数,两轮相对反转,这样转弯急快
void left_90(void){
push_value_left=0;
push_value_right=0;
leftzh=0;
leftfu=1;
rightzh=1;
rightfu=0;
}
//右直角转弯函数
void right_90(void){
push_value_left=0;
push_value_right=0;
leftzh=1;
leftfu=0;
rightzh=0;
rightfu=1;
}
//左转函数定义,一个轮子待机,一个轮子速度10。首先rightfu=0是为了防止电机的奇怪反转。实时证明转弯的时候还是直接停一个电机反应的更加灵敏一些,所以左边的value就给的0.
void turn_left(void){
rightfu=0;
push_value_left=0;
push_value_right=10;
leftzh=0;
leftfu=0;
}
//右转函数定义
void turn_right(void){
leftfu=0;
push_value_right=0;
push_value_left=10;
rightzh=0;
rightfu=0;
}
//左轮PWM函数定义,实现波形调制,原理很简单的
void pwm_left(void){
if(push_value_left!=0){
if(pwm_value_left<push_value_left)
leftzh=1;
else
leftzh=0;
if(pwm_value_left>20)
pwm_value_left=0;
}
}
//右轮PWM函数定义
void pwm_right(void){
if(push_value_right!=0){
if(pwm_value_right<push_value_right)
rightzh=1;
else
rightzh=0;
if(pwm_value_right>20)
pwm_value_right=0;
}
}
//定时器函数用来生成PWM波形,隔1ms就从主函数里面跑出来执行中断里面的函数
void timer0() interrupt 1 using 2{
TH0=0XFC;
TL0=0X66;
pwm_value_left++;
pwm_value_right++;
pwm_left();
pwm_right();
}
//主函数
void main(void){
unint l1,l2,m,r1,r2;
//电机初始化,不然还是保留之前的电平可能会导致电机无故反转或者不工作
leftzh = 0;
leftfu = 0;
rightzh = 0;
rightfu = 0;
//按下单片机开关后延时一秒再开始启动
delay(500);
//全力冲,冲过黑线
run();
//保证在冲过黑线之后再开始下面的循环体,防止提前触发stop()函数导致单片机停止
delay(250);
//为定时器选择工作方式、装初值,实现1ms的定时
TMOD=0X01;
TH0= 0XFC;
TL0= 0X66;
//从这里开始中断函数就要开始工作了,不管怎么样1ms之后就要执行一次中断函数里面的内容
TR0= 1;
ET0= 1;
EA = 1;
//进入死循环,让传感器在这里一直检测
while(1){
//为单片机引脚赋变量
l1=left1;
l2=left2;
m=middle;
r1=right1;
r2=right2;
//中间亮,半速前进
//把middle拔下来之后m一直为1,只是太懒了不想改程序。。。
//传感器检测到黑线就会给单片机返回高电平,就是1了
if(l1==0&&l2==0&&m==1&&r1==0&&r2==0){
run_usual();
num=0;
}
//最左边的传感器检测到黑线
else if(l1==1&&l2==0&&m==1&&r1==0&&r2==0){
turn_left();
delay(20);
num=0;
}
//左边两个传感器检测到黑线
else if(l1==1&&l2==1&&m==1&&r1==0&&r2==0){
left_90();
delay(150);
num=0;
}
else if(l1==0&&l2==1&&m==1&&r1==0&&r2==0){
turn_left();
delay(10);
num=0;
}
else if(l1==0&&l2==0&&m==1&&r1==1&&r2==1){
right_90();
delay(150);
num=0;
}
else if(l1==0&&l2==0&&m==1&&r1==0&&r2==1){
turn_right();
delay(20);
num=0;
}
else if(l1==0&&l2==0&&m==1&&r1==1&&r2==0){
turn_right();
delay(10);
num=0;
}
//因为比赛的黑线比之前练习的黑线粗,所以就把如下的传感器状况当做要向左转90度的条件了
else if(l1==1&&l2==1&&r1==1&&r2==0){
left_90();
delay(150);
num=0;
}
//同上
else if(l1==0&&l2==1&&r1==1&&r2==1){
right_90();
delay(150);
num=0;
}
//最后冲过黑线,但是在检测到黑线的时候不能立刻停,而是要停在两线中间,所以延迟
else if(l1==1&&l2==1&&r1==1&&r2==1){
run();
delay(300);
stop();
break;
}
//要是检测到除以上函数中的传感器状态超过两秒则判定小车跑到了大理石地板上(实验室地板),然后停机
else{
num++;
delay(200);
if(num==10)
stop();
break;
}
}
//从上面的循环跳出来后进入此循环,若没有此循环可能造成从上面循环跳出来后单片机自动复位。
while(1);
}