前言
本人是一名大一学生,刚学会使用51单片机并在上个学期参加校内电子设计大赛,经过一个寒假的学习与一个多月的制作。在比赛过程中与队友制作这台51循迹小车,想趁着还没忘记写篇文章梳理一下,如果有不足的地方,还请见谅并欢迎指出。
一、作品展示
二、使用的组件
2x单片机最小系统
2xSTC89C52RC
1xLCd1602
1x12V锂电池
2x智能车底板
4x电机与轮胎
2x码盘与码盘计数传感器
4xTCRT5000循迹模块3PIN
1xL298N四路驱动模块
1xHC-SR04超声波模块
杜邦线若干
三、制作过程与程序设计思路
1、循迹功能实现
首先是要能够让小车走起来,先将单片机上的I/O口与驱动电路连接起来,包括使能(通过使能端做到后面的PWM控制),具体连接到哪个I/O口可以根据实际情况去设计。
typedef unsigned int u16;//定义类型16位
#define left_go P1_0=1,P1_1=0,P1_6=1,P1_7=0
#define left_back P1_0=0,P1_1=1,P1_6=0,P1_7=1
#define left_stop P1_0=0,P1_1=0,P1_6=0,P1_7=0
#define right_go P1_2=0,P1_3=1,P1_4=0,P1_5=1
#define right_back P1_2=1,P1_3=0,P1_4=1,P1_5=0
#define right_stop P1_2=0,P1_3=0,P1_4=0,P1_5=0
u16 speed_left,speed_right;//定义左右两边的速度
void run(u16 s_L,u16 s_R)
{
speed_left = s_L,speed_right = s_R;
left_go; //左电机前进
right_go; //右电机前进
}
void left_run(u16 s_L,u16 s_R)
{
speed_left = s_L,speed_right = s_R;
left_back; //左电机前进
right_go; //右电机前进
}
void right_run(u16 s_L,u16 s_R)
{
speed_left = s_L,speed_right = s_R;
left_go; //左电机前进
right_back; //右电机前进
}
void stop(u16 s_L,u16 s_R)
{
speed_left = s_L,speed_right = s_R;
left_back;
right_back;
}
void E_stop()
{
left_stop;
right_stop;
}
这便是让小车动起来的基础程序了,speed_ left和speed_right是PWM控制使用的参数,作为小车速度。接下来是PWM的程序,PWM的原理实际上通过控制流过电机电流的占空比来操控速度,比如我设定speed_left=50,在0<=PWM_time<50的时候向电机输出电流,50<=PWM_time<=100不向电机输出电流,这样左电机的速度就控制在全速的一半。
#define EN_left_on P3_4=1,P3_7=1 //左电机使能
#define EN_left_off P3_4=0,P3_7=0 //左电机关闭
#define EN_right_on P3_5=1 ,P3_6=1 //右电机使能
#define EN_right_off P3_5=0,P3_6=0 //右电机关闭
u16 PWM_time_max=100,PWM_time=0;//定义PWM周期与PWM运行阶段
void timer0_on() //定时器0启动 设置200us 8位自动重装
{
TMOD|=0x02;
TH0=72;
TL0=72;
ET0=1;
TR0=1;
}
void PWM_on() interrupt 1 //每次定时器0溢出就执行一次PWM程序
{
if(PWM_time<PWM_time_max)
{
if(PWM_time<speed_left)
EN_left_on;
if(PWM_time>=speed_left)EN_left_off;
if(PWM_time<speed_right)
EN_right_on;
if(PWM_time>=speed_right)EN_right_off;
PWM_time++;
}
else if(PWM_time>=PWM_time_max)//当运行周期达到一百的时候归0
PWM_time=0;
}
循迹程序方面,一开始我们的思路是用中间两个红外保证小车在直线行走,外侧两个在弯道的时候使小车保持在黑线上,所以一开始基于这个思路一个比较复杂的循迹程序就设计出来了。(一开始我们是想通过两轮的速度差来转向,让轮胎转动方向一致,以保证测距尽可能精准。但是后面实际测试的时候发现小车的质量太大了,只能把两侧的轮胎方向调反,不过实际上这样的效果更多是让轮胎停转或变慢,前面设置两种stop也是这个原因,一个给一定反向电流是小车快速慢下来,另一种是关闭电机)
sbit left=P0^5;//内部左侧红外
sbit right=P2^7;//内部右侧红外
sbit left_0=P0^4;//外侧左红外
sbit right_0=P0^7;//外侧右红外
u16 key=0;
void track()
{
if(left==1 && right==0)
{left_run(20,30);key=0;}
else if(left==0 && right==1)
{right_run(30,20);key=0;}
else if(left_0==1 && right_0==0)
{left_run(30,30);key=1;}
else if(left_0==0 && right_0==1)
{right_run(30,30);key=2;}
else if(key==1)
left_run(30,30);
else if(key==2)
right_run(30,30);
else if(key==0)
run(30,30);
}
这个程序写下来,想法很丰满,实际上出现很多问题,首先是程序太复杂了,不方便后期数据调试,并且运行效率低。后面学习过寒假里买来完整品送的代码后意识到还可以用赋值的方法,比如外侧左红外检测到黑线返回1后,可以与其他传感器的值加起来,变成1000,内部左侧红外检测到就是100即0100(程序里不用0100主要是数字前加0就是八进制了),if语句也可以换成switch语句。不过他给的例程是两路的,我们后想到把key改成while,一直到黑线回到中间。改进后的程序:
u16 data1,data2 = left,data3 = right,data4=left_0,data5=right_0;
data1 =data4*1000+data2*100+data3*10+data5;
switch(data1)
{
case 0:run(30,30);break;
case 100:left_run(20,30);break;
case 10:right_run(30,20);break;
case 1000:left_run(10,38);while(left==0&&right==0);break;
case 1:right_run(38,10);while(left==0&&right==0);break;
case 1111:E_stop();break;
}
2、避障功能实现
先说我们采用的超声波避障模块的使用原理,首先在Trig端发射一段10us以上的高电平,然后在超声波模块的Echo端会返回一段与超声波发射到返回相同时长的高电平,这期间用一个计时器计时,用while函数等待Echo返回高电平和等待高电平结束,具体是实现函数如下:
sbit Trig=P3^1;//超声波信号发出端
sbit Echo=P3^0;//超声波信号接收端
u16 M_sensor=1;//超声波返回值
void timer1_on()//定时器1初始化
{
TMOD|=0x10;
TH1=0;
TL1=0;
ET1=1;
TR1=1;
}
void U_wave_distance()
{
u16 measure_time=0;
float S=0;
measure_time=TH1*256+TL1;
TH1=0;
TL1=0;
S=measure_time*0.17*1.085;
if(S>400||S<2)//规定小车何时进行避障操作
{
M_sensor=1;
}
else
{
M_sensor=0;
}
}
void U_wave_control()
{
Trig=0;
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
Trig=1;//发射10us以上的高电平
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
Trig=0;
while(!Echo);//等待返回高电平
timer1_on();//开启定时器
while(Echo); //等待变成低电平
TR1=0;//关闭定时器
U_wave_distance();
if(M_sensor==0)//避障操作,向左规避
{
u16 i=500;
stop(40,40);
delayms(1);
left_run(10,38);
delayms(200);
run(40,40);
delayms(280);
right_run(50,20);
delayms(280);
E_stop();
}
}
到现在为止,设计的程序都是为了在一个单片机上使用,而后面我们改成了两个单片机,是因为在测试的时候我们发现,速度调得比较低的话,如果开启了超声波,小车会有很明显的卡顿,就好像PWM的周期被拉长了,即使我们觉得可能是两次发射超声波间隔太小,用for循环增加循迹时间也是一样,还让反应速度变慢(不排除惯性太大的原因,我们后期意识到小车质量较大的时候已经在使用两个单片机了)。为了保证小车能够尽可能快的检测到障碍物并且执行避障程序不影响其他功能,所以我们最后使用了两个单片机。一个是用来作为主控单片机,发射超声波下达避障命令和测距,LCD显示。另一个是执行比避障命令和循迹功能。两个单片机之间我们用了两根线连接,一根是发出避障命令,另一根确定执行完避障命令。
主控单片机
void U_wave_control()
{
Trig=0;
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
Trig=1;
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
Trig=0;
while(!Echo);
timer1_on();
while(Echo);
TR1=0;
U_wave_distance();
if(M_sensor==0)
{
if(P1_1==1)//P1_0连接另一个单片机的recive,P1_1连接的是confirm
{
P1_0=0;
while(P1_1==1);
while(P1_1==0);
P1_0=1;
}
}
}
电机控制单片机
void main()
{
EA_on;
timer0_on();
while(1)
{
u16 data1,data2 = left,data3 = right,data4=left_0,data5=right_0,i=500;
data1 =data4*1000+data2*100+data3*10+data5;
switch(data1)
{
case 0:run(30,30);break;
case 100:left_run(20,30);break;
case 10:right_run(30,20);break;
case 1000:left_run(10,38);while(left==0&&right==0);break;
case 1:right_run(38,10);while(left==0&&right==0);break;
case 1111:E_stop();break;
}
if(recive==0)U_control();
}
}
void U_control()
{
u16 i=500;
confirm=0;
stop(40,40);
delayms(1);
left_run(10,38);
delayms(200);
run(40,40);
delayms(280);
right_run(50,20);
delayms(280);
E_stop();
confirm=1;
}
3、LCD1602的使用与时钟和测距的实现
测距的实现,测距我们是使用了码盘和计数传感器(类似于光电门的设备),原理就是通过外部中断,记录通过码盘上孔的次数,然后再根据转一周孔的数量和轮胎的直径计算行驶距离(所以在设计转弯的时候我们一开始是很不情愿使用两侧轮胎反向的,对精度有影响),为了更加准确我们使用了两个这样的装置,给两个后轮装上,然后计算平均值。
u16 num=0,number=0;//码盘计数
void exit0_init()//外部中断0
{
IT0=1;
PX0=1;
EX0=1;
}
void exit0() interrupt 0
{
num++;
}
void exit1_init()//外部中断1
{
IT1=1;
PX1=1;
EX1=1;
}
void exit1() interrupt 2
{
number++;
}
u16 count()
{
u16 Distance=0;//LCD显示实数太麻烦了,并且精度应该也没那么高,所以就干脆用整型
float k=0;
k=(num+number)/2.0;//求平均
Distance=k/20.0*6.6*3.1415926;
return Distance;
}
运行时间的测算就比较简单了,计时器的简单应用就不多做介绍了。
void timer0_on() //定时器0启动 设置200us 8位自动重装
{
TMOD|=0x02;
TH0=72;
TL0=72;
ET0=1;
TR0=1;
}
void time() interrupt 1
{
times++;
if(times==5000)
{
if(min==99&&s==59){min=0;s=0;}
else if(s==59){min++;s=0;}
else s++;
times=0;
}
}
LCD1602的使用我们是学习了b站上江科大自化协的视频,我51单片机业主要是看他的视频学习的,大佬的讲解真的十分细致啊,再次感谢大佬的分享。(这里有一点注意,自己买的51单片机最小系统上的I/O口和大佬提供程序里的定义可能不一样,要自己对照原理图修改一下)LCD1602的程序是直接写在主函数的。
void main()
{
u16 k=0;
EA_on;
LCD_Init();
timer0_on();
exit1_init();
exit0_init();
LCD_ShowString(1,1,"Time: min s");
LCD_ShowString(2,1,"Distance: cm");
while(1)
{
U_wave_control();delay1ms(30);//建议是超声波发射之间的时间达到六十毫秒,考虑到计时器用的时间和LCD用的时间,就只取了一半,不过现在来看还挺正常的(有时确实有点问题)
LCD_ShowNum(1,6,min,2);
LCD_ShowNum(1,11,s,2);
LCD_ShowNum(2,10,count(),5);
}
}
四、最终程序
主控单片机程序
#include <REGX52.H>
#include <intrins.h>
#include "LCD1602.H"
typedef unsigned int u16;
#define EA_on EA=1 //允许总中断
#define EA_off EA=0 //停止总中断(大概是仪式感吧)
sbit Trig=P3^1;//超声波信号发出端
sbit Echo=P3^0;//超声波信号接收端
u16 M_sensor=1;//超声波返回值
u16 min=0,s=0,times=0;//定义时钟时间
u16 num=0,number=0;//码盘计数
void delay1ms(u16 k);
//超声波模块
void U_wave_distance();
void U_wave_control();
//时钟控制模块
void timer0_on();//定时器0启动
void time();
//行驶距离计算
u16 count();
void exit0_init(void);
void exit1_init(void);
void main()
{
u16 k=0;
EA_on;
LCD_Init();
timer0_on();
exit1_init();
exit0_init();
LCD_ShowString(1,1,"Time: min s");
LCD_ShowString(2,1,"Distance: cm");
while(1)
{
U_wave_control();delay1ms(30);
LCD_ShowNum(1,6,min,2);
LCD_ShowNum(1,11,s,2);
LCD_ShowNum(2,10,count(),5);
}
}
void delay1ms(u16 k) //@11.0592MHz
{
unsigned char i, j;
while(k--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
//时钟模块
void timer0_on() //定时器0启动 设置200us 8位自动重装
{
TMOD|=0x02;
TH0=72;
TL0=72;
ET0=1;
TR0=1;
}
void time() interrupt 1
{
times++;
if(times==5000)
{
if(min==99&&s==59){min=0;s=0;}
else if(s==59){min++;s=0;}
else s++;
times=0;
}
}
void timer1_on()//定时器1初始化
{
TMOD|=0x10;
TH1=0;
TL1=0;
ET1=1;
TR1=1;
}
//超声波模块
void U_wave_distance()
{
u16 measure_time=0;
float S=0;
measure_time=TH1*256+TL1;
TH1=0;
TL1=0;
S=measure_time*0.17*1.085;
if(S>400||S<2)//规定小车何时进行避障操作(单位毫米)
{
M_sensor=1;
}
else
{
M_sensor=0;
}
}
void U_wave_control()
{
Trig=0;
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
Trig=1;
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
Trig=0;
while(!Echo);
timer1_on();
while(Echo);
TR1=0;
U_wave_distance();
if(M_sensor==0)
{
if(P1_1==1)
{
P1_0=0;
while(P1_1==1);
while(P1_1==0);
P1_0=1;
}
}
}
//行驶距离计算
void exit0_init()
{
IT0=1;
PX0=1;
EX0=1;
}
void exit0() interrupt 0
{
num++;
}
void exit1_init()
{
IT1=1;
PX1=1;
EX1=1;
}
void exit1() interrupt 2
{
number++;
}
u16 count()
{
u16 Distance=0;
float k=0;
k=(num+number)/2.0;
Distance=k/20.0*6.6*3.1415926;
return Distance;
}
电机驱动单片机程序
#include <REGX52.H>
#include <intrins.h>
#define left_go P1_0=1,P1_1=0,P1_6=1,P1_7=0
#define left_back P1_0=0,P1_1=1,P1_6=0,P1_7=1
#define left_stop P1_0=0,P1_1=0,P1_6=0,P1_7=0
#define right_go P1_2=0,P1_3=1,P1_4=0,P1_5=1
#define right_back P1_2=1,P1_3=0,P1_4=1,P1_5=0
#define right_stop P1_2=0,P1_3=0,P1_4=0,P1_5=0
#define EN_left_on P3_4=1,P3_7=1 //左电机使能
#define EN_left_off P3_4=0,P3_7=0 //左电机关闭
#define EN_right_on P3_5=1 ,P3_6=1 //右电机使能
#define EN_right_off P3_5=0,P3_6=0//右电机关闭
#define EA_on EA=1 //允许中断
#define EA_off EA=0 //停止中断
typedef unsigned int u16;//定义类型16位
u16 speed_left,speed_right;//定义左右两边的速度
u16 PWM_time_max=100,PWM_time=0;//定义PWM周期与PWM运行阶段
sbit left=P0^5;//内部左侧红外
sbit right=P2^7;//内部右侧红外
sbit left_0=P0^4;
sbit right_0=P0^7;
sbit confirm=P0^1;//避障命令确认
sbit recive=P0^0;
//软件演示
void delayms(u16 ms);
//PWM模块
void timer0_on();//定时器0启动
void PWM_on();//PWM运行
void run(u16 s_L,u16 s_R);
void left_run(u16 s_L,u16 s_R);
void right_run(u16 s_L,u16 s_R);
void stop(u16 s_L,u16 s_R);
void E_stop();
//超声波接收与动作
void U_control();
void main()
{
EA_on;
timer0_on();
while(1)
{
u16 data1,data2 = left,data3 = right,data4=left_0,data5=right_0,i=500;
data1 =data4*1000+data2*100+data3*10+data5;
switch(data1)
{
case 0:run(30,30);break;
case 100:left_run(20,30);break;
case 10:right_run(30,20);break;
case 1000:left_run(10,38);while(left==0&&right==0);break;
case 1:right_run(38,10);while(left==0&&right==0);break;
case 1111:E_stop();break;
}
if(recive==0)U_control();
}
}
//软件延时
void delayms(u16 ms) //@11.0592MHz
{
unsigned char i, j;
while(ms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void run(u16 s_L,u16 s_R)
{
speed_left = s_L,speed_right = s_R;
left_go; //左电机前进
right_go; //右电机前进
}
void left_run(u16 s_L,u16 s_R)
{
speed_left = s_L,speed_right = s_R;
left_back; //左电机前进
right_go; //右电机前进
}
void right_run(u16 s_L,u16 s_R)
{
speed_left = s_L,speed_right = s_R;
left_go; //左电机前进
right_back; //右电机前进
}
void stop(u16 s_L,u16 s_R)
{
speed_left = s_L,speed_right = s_R;
left_back;
right_back;
}
void E_stop()
{
left_stop;
right_stop;
}
//PWM
void timer0_on() //定时器0启动 设置200us 8位自动重装
{
TMOD|=0x02;
TH0=72;
TL0=72;
ET0=1;
TR0=1;
}
void PWM_on() interrupt 1
{
if(PWM_time<PWM_time_max)
{
if(PWM_time<speed_left)
EN_left_on;
if(PWM_time>=speed_left)EN_left_off;
if(PWM_time<speed_right)
EN_right_on;
if(PWM_time>=speed_right)EN_right_off;
PWM_time++;
}
else if(PWM_time>=PWM_time_max)
PWM_time=0;
}
//超声波接收与动作
void U_control()
{
u16 i=500;
confirm=0;
stop(40,40);
delayms(1);
left_run(10,38);
delayms(200);
run(40,40);
delayms(280);
right_run(50,20);
delayms(280);
E_stop();
confirm=1;
}
五、总结
三百多行代码,一个多月的时间,一边要忙于学业又要抽时间去给小车设计程序和测试,尤其是某些模块坏掉的时候还要等待快递,本来一个礼拜只能去测试两三次,又一次次在调试时面对小车奇奇怪怪的问题和一次次修改。不过这都过去了,在所有功能都实现的时候感觉一切都是值得的。希望各位都能在自己喜欢的事业上取得成功。