目录

一.功能介绍及硬件准备
二.电机控制及调速
三.小车循迹方案
四.跟随功能实现
五.测速功能实现
六.OLED显示车速
七.摇头避障功能实现
八.SU-03T语音模块介绍
九.语音切换小车模式+OLED显示模式


一.功能介绍及硬件准备

这是一款基于51单片机开发的智能小车,通过这篇文章我会记录下来开发这款小车的全部过程。这款小车集成了循迹,避障,跟随,语音切换模式选择,并且将可以将车速显示到OLED屏幕上,也可以通过手机app蓝牙操控小车。(注:全文的代码采取分文件编程的写法)

硬件准备

小车底盘一个(两驱),5号4节电池盒一个,51单片机最小系统一个,HC04超声波模块一个,SG90舵机一个,红外避障模块传感器两个,红外光电反射传感器两个,L9110S电机驱动模块(L298n也可以使用),测速传感器一个,SU-03T离线语音模块一个,HC-08蓝牙模块一个,DC-DC电压转换模块两个,0.96寸OLED屏幕一个,杜邦线若干,热熔胶枪一个,也可以再准备一个面包板。


二.电机控制及调速

关于电机控制选用的是L9110s电机驱动模块,在淘宝里也很容易买到,也才不到2块钱,比l298n偏移很,但缺点就是容易发烫。

接线说明:

我们以L9110s电机驱动模块为新手小白讲一下这些模块怎么接线,后面就不多赘述模块如何接线了。模块通常的引脚就是VCC,GND,以及其他的控制或信号引脚。VCC就是模块的电源正极(大多数的模块都是5V供电,具体的参考模块说明书),接到单片机最小系统的VCC引脚上。GND就是模块的负极,接到单片机最小系统的GND引脚上。VCC或GND也可接到通过面包板引出的正负极上。剩下的引脚就接到单片机的IO口上即可。


控制小车前后左右:

控制小车的前后左右运动说白了就是控制两个电机的正反转,两个电机同时正转小车前进,同时反转小车后退,电机一个转一个不转小车实现转向。逻辑非常简单,下面就是L9110s电机驱动模块的真值表,并且该模块可同时控制两个电机。将两个电机的两根线

接入端子中就可以写代码控制了。

IA1输入高电平,IA1输入低电平,【OA1 OB1】电机正转;

IA1输入低电平,IA1输入高电平,【OA1 OB1】电机反转;

IA2输入高电平,IA2输入低电平,【OA2 OB2】电机正转;

IA2输入低电平,IA2输入高电平,【OA2 OB2】电机反转;

motor.c


#include "reg52.h"
 
sbit RightCon1A = P3^2; //电机A由P3.2,P3.3控制
sbit RightCon1B = P3^3;
 
sbit LeftCon1A = P3^4;  //电机B由P3.4,P3.5控制
sbit LeftCon1B = P3^5;
 
void goBack() //后退
{
    LeftCon1A = 0;
    LeftCon1B = 1;
     
    RightCon1A = 0;
    RightCon1B = 1;
}
 
void goRight() //右转
{
    LeftCon1A = 0;
    LeftCon1B = 1;
     
    RightCon1A = 0;
    RightCon1B = 0;
}
 
 
void goLeft() //左转
{
    LeftCon1A = 0;
    LeftCon1B = 0;
     
    RightCon1A = 0;
    RightCon1B = 1;
}
  
void goForward() //前进
{
    LeftCon1A = 1;
    LeftCon1B = 0;
     
    RightCon1A = 1;
    RightCon1B = 0;
}
 
void stop()  //停车
{
	LeftCon1A = 0;
    LeftCon1B = 0;
     
    RightCon1A = 0;
    RightCon1B = 0;	
}


蓝牙控制小车

蓝牙控制小车的核心思想就是采用串口中断,用手机app给蓝牙模块发送不同的字符串,单片机接收到字符串后进入串口中断,通过判断字符串内容来控制小车的前后左右。换句话说,蓝牙控制小车不需要配置任何蓝牙模块的相关代码,只需写好串口中断的控制即可实现蓝牙控制。

usart.c

#include "reg52.h"
#include "intrins.h"
#include <string.h>
#include "motor.h"
 
#define SIZE 12
sfr AUXR = 0x8E;
char buffer[SIZE];
 
void UartInit(void)	//9600bps@11.0592MHz
{
	AUXR=0X01;
	SCON = 0x50;    //配置串口工作方式1,REN使能(REN:串行使能接收位)
	TMOD &= 0xF0;		
	TMOD |=0X20;    //设定定时器1工作方式位,8位自动重装载
	TL1 = 0xFD;		//设定定时器初值
	TH1 = 0xFD;		//设定定时器初值(波特率9600初值)
	ET1 = 0;		//紧止定时器1中断
	TR1 = 1;		//启动定时器1
	
	EA=1;       //开启总中断
	ES=1;       //开始串口中断
}
 
//发送M1-前进,发送M2-后退,发送M3-左转,发送M4-右转
 
void Usart_Handler() interrupt 4
{
	static int i=0;
	char tmp;
	
	if(RI)//接收中断处理
	{
		RI=0;//清除中断标志位
		tmp=SBUF;
		
		if(tmp=='M'){
			i=0;
		}
		buffer[i++]=tmp;
		if(buffer[0]=='M'){
			switch(buffer[1]){
				case'1':
				goForward();	
				break;
				case'2':
				goBack();	
				break;
				case'3':
				goLeft();	
				break;			
				case'4':
				goRight();	
				break;			
			}
		}		
		if(i==12){
		memset(buffer,'\0',SIZE);  //清空串口接收区
		i=0;
		}	
	}
}


小车调速

前面的代码实现的小车的前进都是让小车全速前进,电池的功率有多大小车前进的速度就有多快,6节干电池供电肯定会比四节干电池快的多。那么我们用单片机如何给小车调速,我们用PWM给小车进行调速。 

调速原理:全速前进是LeftCon1A = 0; LeftCon1B = 1;完全停止是LeftCon1A = 0;LeftCon1B = 0;那么单位时间内,比如20ms, 有15ms是全速前进,5ms是完全停止, 速度就会比5ms全速前进,15ms完全停止获得的功率多,相应的速度更快!这就是PWM通过改变占空比调速的原理。

为了更好控制两个电机的不同状态打开两个定时器中断,定时器0控制左边电机,定时器2控制右边电机。使用两组定时器中断调速,这样就可以通过差速的方式控制小车的转向。左轮定时器0调速,右轮定时器1调速,那么左转就是右轮速度大于左轮!

time.c

#include "reg52.h"
#include "motor.h"
 
char leftspeed;
char cntLeft=0;
 
char rightspeed;
char cntRight=0;
 
void Time0Init()
{
	//1. 配置定时器0工作模式位16位计时
	TMOD = 0x01;
	//2. 给初值,定一个0.5出来
	TL0=0x33;
	TH0=0xFE;
	//3. 开始计时
	TR0 = 1;
	TF0 = 0;
	//4. 打开定时器0中断
	ET0 = 1;
	//5. 打开总中断EA
	EA = 1;
}
 
void Time1Init()
{
	//1. 配置定时器1工作模式位16位计时
	TMOD &= 0x0F;
	TMOD |= 0X1 <<4;
	//2. 给初值,定一个0.5出来
	TL1=0x33;
	TH1=0xFE;
	//3. 开始计时
	TR1 = 1;
	TF1 = 0;
	//4. 打开定时器0中断
	ET1 = 1;
	//5. 打开总中断EA
	EA = 1;
}
 
void Time0Handler() interrupt 1
{
	cntLeft++;  //统计爆表的次数. cnt=1的时候,报表了1
	//重新给初值
	TL0=0x33;
	TH0=0xFE;
	
	//控制PWM波
	if(cntLeft < leftspeed){
    goForwardLeft();
	}else{
		   stopLeft();
	}
	
	if(cntLeft == 40){//爆表40次,经过了20ms
		 cntLeft = 0;  //当100次表示1s,重新让cnt从0开始,计算下一次的1s
	}		
}
 
void Time1Handler() interrupt 3
{
	cntRight++;  //统计爆表的次数. cnt=1的时候,报表了1
	//重新给初值
	TL1=0x33;
	TH1=0xFE;
	
	//控制PWM波
	if(cntRight < rightspeed){
		//右前进
		goForwardRight();
	}else{
		//停止
		stopRight();
	}
	
	if(cntRight == 40){//爆表40次,经过了20ms
		cntRight = 0;  //当100次表示1s,重新让cnt从0开始,计算下一次的1s
	}		
}



三.小车循迹方案

循迹模块介绍:

我们选用的是TCRT5000传感器,传感器的红外发射二极管不断发射红外线,当发射出的红外线没有被反射回来或被反射回来但强度不够大时, 红外接收管一直处于关断状态,此时模块的输出端为高电平,指示二极管一直处于熄灭状态 被检测物体出现在检测范围内时,红外线被反射回来且强度足够大,红外接收管饱和, 此时模块的输出端为低电平,指示二极管被点亮。

(注:该模块有一个数字信号输出DO和一个模拟信号输出AO,我们只使用了数字信号DO引脚,AO悬空即可)

总结就是一句话,没反射回来,D0输出高电平,灭灯!

循迹原理:

小车循迹是沿着黑色的线走,由于黑色具有较强的吸收能力,当循迹模块发射的红外线照射到黑线时,红外线将会被黑线吸收,导致循迹模块上光敏三极管处于关闭状态,此时模块上一个LED熄灭。在没有检测到黑线时,模块上两个LED 常亮。

总结就是一句话,有感应到黑线,D0输出高电平 ,灭灯!

小车行驶在直线赛道的时候,两个循迹模块分别是在黑线的两侧,不会吸收发射出的红外线。行驶在圆形赛道的时候,某一侧的循迹模块必然会接触到黑线部分,因此会给单片机一个高电平信号,单片机通过判断是那一侧的循迹模块发出的高电平从而控制小车往那个方向转向。

总结:

走直线时:两个循迹模块都是低电平。

左转时:左模块输出高电平,右模块输出低电平。

右转时:右模块输出高电平,左模块输出低电平。

还需注意的就是这个循迹模块的电压输入是3v-5v,而我们的电池盒提供的电压是6v,虽然不会烧坏模块,但实测的效果会大打折扣,所以使用DC-DC电压模块给循迹模块提供5v的电压。

根据上面讲的原理,开始写一个测试代码。


#include <reg52.h>
#include "motor.h"
 
sbit leftSensor = P2^7;  //左循迹模块
sbit rightSensor = P2^6; //右循迹模块
 
void main()
{		
	while(1){
		if(leftSensor == 0 && rightSensor == 0){
			goForward();
		}
		if(leftSensor == 1 && rightSensor == 0){
			goLeft();
		}
		if(leftSensor == 0 && rightSensor == 1){
			goRight();
		}
		if(leftSensor == 1 && rightSensor == 1){
			stop();
		}		
    }
}


循迹模块电位器调节:

经过测试这段代码就可以实现循迹的功能,但是把代码烧录进去之后还需要根据实际情况调节循迹模块上的电位器改变循迹模块的灵敏度。要是发现小车一放下就转圈圈,或者不按照黑线循迹,那么很有可能就是电位器的灵敏度的问题。比如家里地板颜色偏灰,这个时候就要把灵敏度调高。

PWM调速加入实现小车丝滑转弯:

上面的代码虽然已经可以实现循迹的功能,但是在实际测试中发现在转弯的时候一抽一抽。现在就改进一下小车“抽抽” 的这个问题。

上面的代码实现小车转弯的时候,相当于是一种急刹车式的转弯,小车在转弯前丝毫不减速。那么想要让小车丝滑转弯,那么就必须两个轮子都要有速度,而不是通过一个轮子转另一个轮子不转这种方式实现转弯。我们把之前写过的PWM调速的代码加入到循迹的代码中即可实现丝滑转弯。

#include "motor.h"
#include "usart.h"
#include "time.h"
#include <reg52.h>
 
/*
	leftspeed,rightspeed这两个参数具体给多大
	根据小车跑动情况来随时修改
*/
sbit leftSensor = P2^7; //左循迹模块
sbit rightSensor = P2^6;//右循迹模块
 
extern char leftspeed;
extern char rightspeed;
 
void main()
{
		Time0Init();
		Time1Init();		
 
		while(1){
		if(leftSensor == 0 && rightSensor == 0){
			leftspeed = 40; 
			rightspeed = 40;	
		}
		if(leftSensor == 1 && rightSensor == 0){
			leftspeed = 15;
			rightspeed = 40; //右轮速度大于左轮,右转
		}
		if(leftSensor == 0 && rightSensor == 1){
			leftspeed = 40;  //左轮速度大于右轮,右转
			rightspeed = 15;
		}
		if(leftSensor == 1 && rightSensor == 1){
			leftspeed = 0; 
			rightspeed = 0;	
		}		
     }
}


实测可能会遇到的问题:

1.直线跑不直:在跑直线的时候可能跑着跑着就越来越斜的这种情况,这种情况的原因就是两个电机的速度不一致导致的。解决方法就是把转的快的那一边的电机速度一点一点给它调慢,直到小车彻底跑直为止。

2.转弯时跑出赛道:这个情况的出现就是转弯的时候电机转速不够,提高相应的电机转速即可。


四.跟随功能实现

原理和寻线是一样的,寻线红外观朝下,跟随朝前。用到的也是两个红外模块只不过发射管的位置不一样而已。

跟随小车的原理:

左边跟随模块能返回红外,输出低电平,右边不能返回,输出高电平,说明物体在左边,需要左转 右边跟随模块能返回红外,输出低电平,左边不能返回,输出高电平,说明物体在右边,需要右转

跟随代码如下:

#include "motor.h"
#include "reg52.h"
 
sbit leftSensor = P2^5;
sbit rightSensor = P2^4;
 
void main()
{
	while(1){
		if(leftSensor == 0 && rightSensor == 0){
			goForward();
		}
		if(leftSensor == 1 && rightSensor == 0){
			goRight();
		}
		
		if(leftSensor == 0 && rightSensor == 1){
			
			goLeft();
		}
		
		if(leftSensor == 1 && rightSensor == 1){
			stop();
		}
	}
}


五.测速功能实现

模块介绍:

用途:广泛用于电机转速检测,脉冲计数,位置限位等。

有遮挡,输出高电平;无遮挡,输出低电平

接线 VCC 接电源正极3.3-5V

GND 接电源负极

DO TTL开关信号输出

AO 此模块不起作用

安装位置如图所示:

  

测速原理:

轮子走一圈,经过一个周长,C = 2x3.14x半径= 3.14 x 轮子直径(6.5cm),对应的码盘也转一圈,码盘有20个格子,每经过一个格子,会遮挡(高电平)和不遮挡(低电平),那么码盘一小格就是对应走了 3.14 * 6.5 cm /20 = 1.0205CM。换句话说就是一个脉冲就是走了1.0205CM。定时器可以设计成一秒,统计脉冲数,假设一秒有80脉冲,那么就是80cm/s。

代码逻辑:

接下来我们编程实现将车速通过串口发送给串口助手,也可以使用蓝牙模块发送到手机app上。我们先发送到串口助手上看看效果。

time.c


#include <REGX52.H>
 
unsigned int cnt = 0;
extern unsigned int rightspeedCnt;
extern unsigned int leftspeedCnt; 
unsigned int speedleft;
unsigned int speedright;
char singal;
 
void Time0Init()
{
	//1. 配置定时器0工作模式位16位计时
	TMOD = 0x01;
	//2. 给初值,定一个0.5ms出来
	TL0=0x33;
	TH0=0xFE;
	//3. 开始计时
	TR0 = 1;
	TF0 = 0;
	//4. 打开定时器0中断
	ET0 = 1;
	//5. 打开总中断EA
	EA = 1;
}
 
void Time0Handler() interrupt 1
{
	cnt++;  //统计爆表的次数. cnt=1的时候,报表了1
	//重新给初值
	TL0=0x33;
	TH0=0xFE;
	
	if(cnt == 2000){//爆表2000次,经过了1s
	{
		cnt=0;
		singal=1;
		speedright = rightspeedCnt;	//计算小车的速度,也就是拿到speedCnt的值
		speedleft = leftspeedCnt;
		rightspeedCnt=0;
		leftspeedCnt=0;//1秒后拿到speedCnt个格子,就能算出这1s的速度,格子清零
	}	
  }
}


usart.c

#include <REGX52.H>
 
unsigned int cnt = 0;
extern unsigned int rightspeedCnt;
extern unsigned int leftspeedCnt; 
unsigned int speedleft;
unsigned int speedright;
char singal;
 
void Time0Init()
{
	//1. 配置定时器0工作模式位16位计时
	TMOD = 0x01;
	//2. 给初值,定一个0.5ms出来
	TL0=0x33;
	TH0=0xFE;
	//3. 开始计时
	TR0 = 1;
	TF0 = 0;
	//4. 打开定时器0中断
	ET0 = 1;
	//5. 打开总中断EA
	EA = 1;
}
 
void Time0Handler() interrupt 1
{
	cnt++;  //统计爆表的次数. cnt=1的时候,报表了1
	//重新给初值
	TL0=0x33;
	TH0=0xFE;
	
	if(cnt == 2000){//爆表2000次,经过了1s
	{
		cnt=0;
		singal=1;
		speedright = rightspeedCnt;	//计算小车的速度,也就是拿到speedCnt的值
		speedleft = leftspeedCnt;
		rightspeedCnt=0;
		leftspeedCnt=0;//1秒后拿到speedCnt个格子,就能算出这1s的速度,格子清零
	}	
  }
}


main.c

#include "reg52.h"
#include "intrins.h"
#include <string.h>
 
 
void UartInit(void)		//9600bps@11.0592MHz
{
	SCON = 0x50; //配置串口工作方式1,REN使能接收
	TMOD &= 0x0F;
	TMOD |= 0x20;//定时器1工作方式位8位自动重装
	
	TH1 = 0xFD;
	TL1 = 0xFD;//9600波特率的初值
	TR1 = 1;//启动定时器
	
	EA = 1;//开启总中断
}
 
void SendByte(char mydata)//发送字符
{
	SBUF = mydata;
	while(!TI);
	TI=0;
}
 
void SendString(char *str)//发送字符串
{
	while(*str != '\0'){
		SendByte(*str);
		str++;
	}
}



六.OLED显示车速

车速可以再上位机中显示了,接下来我们将车速显示到OLED屏幕上。使用OLED屏幕需要先了解IIC或者SPI的协议,我使用的0.96寸IIC协议的OLED屏幕。这里就不多赘述OLED屏幕的使用和IIC协议了,我之前也写过有关IIC和OLED屏幕相关的文章,感兴趣的小伙伴可以去看一下。不想深究OLED原理的也可以直接拿厂家提供的代码直接使用。

由于OLED相关的代码过于冗长,就不在文章里展示了,我主页里的资源有这个小车的完整代码,我们主要展示主函数代码以及汉字取模软件的使用。

取模软件使用:

输入显示的文字

 

 按下Ctrl+Enter,选择C51格式

文字的代码随即生成

本来是想用汉字显示到屏幕中,但是16*16的汉字显示的话屏幕太小了,后面的车速的内容就放不下了,所以最后决定用英文显示。这样的话就用不上取模软件了,直接在程序里面包含厂家提供的英文字模库即可。

main.c

#include "motor.h"
#include "usart.h"
#include "reg52.h"
#include "time.h"
#include "stdio.h"
#include "OLED.h"
 
sbit speedIO1 = P3^2;//外部中断0
sbit speedIO2 = P3^3;//外部中断1
unsigned int leftspeedCnt = 0;  //统计左轮格子,脉冲次数
unsigned int rightspeedCnt = 0; //统计右轮格子,脉冲次数
extern unsigned int speedleft;  //左轮速度
extern unsigned int speedright; //右轮速度
extern char singal;    //发送速度的信号
char SpeedMes_R[24];  //主程序发送右轮速度数据的字符串缓冲区
char SpeedMes_L[24];  //主程序发送左轮速度数据的字符串缓冲区
 
void Ex0Init()
{
	EX0 = 1;//允外部中断
	IT0 = 1;//外部中断的下降沿触发
}
 
void Ex1Init()
{
	EX1 = 1;//允外部中断
	IT1 = 1;//外部中断的下降沿触发
}
 
void main()
{
	Time0Init();//定时器0初始化
	UartInit();//串口相关初始化
	Ex0Init();//外部中断0初始化
	Ex1Init();//外部中断1初始化
	Oled_Init();//OLED初始化
	Oled_Clear();//清屏
	
    while(1){
      if(singal){
		sprintf(SpeedMes_R,"R-speed:%d cm/s",speedright);//串口数据的字符串拼装,speed是格子,每个格子1cm
		SendString(SpeedMes_R);//速度发出去
		SendString("\r\n");	
				
		sprintf(SpeedMes_L,"L-speed:%d cm/s",speedleft);//串口数据的字符串拼装,speed是格子,每个格子1cm
		SendString(SpeedMes_L);//速度发出去
		SendString("\r\n");	
			
		singal = 0;//清0speed,下次由定时器1s后的中断处理中再置一 	  
		}
		Oled_Show_Str(1,1,SpeedMes_L); //显示左轮速度
		Oled_Show_Str(2,1,SpeedMes_R); //显示右轮速度
    }
}
 
void rightspeedHandler() interrupt 0 //外部中断处理函数
{
	rightspeedCnt++; //每经过一共格子,加一
}
	
void leftspeedHandler() interrupt 2 //外部中断处理函数
{
	leftspeedCnt++; //每经过一共格子,加一
}	



七.摇头避障功能实现

避障功能是实现用的是超声波模块,其原理是通过发送和收超声波,利用时间差和声音传播速度, 计算出模块到前方障碍物的距离。当检测到小于指定距离时,小车停止。

怎么让它发送波:Trig给Trig端口至少10us的高电平

怎么知道它开始发了 Echo信号:由低电平跳转到高电平,表示开始发送波

怎么知道接收了返回波 Echo:由高电平跳转回低电平,表示波回来了

怎么算时间:Echo引脚维持高电平的时间! 波发出去的那一下,开始启动定时器 波回来的拿一下,我们开始停止定时器,计算出中间经过多少时间

怎么算距离:距离 = 速度 (340m/s)* 时间/2

Hc04.c

#include "reg52.h"
#include "delay.h"
 
sbit Trig = P2^3;
sbit Echo = P2^2;
 
void Time1Init()
{	
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x10;
	TH1 = 0;
	TL1 = 0;
	//设置定时器0工作模式1,初始值设定0开始数数,不着急启动定时器
}
 
void startHC()   //发送超声波
{
	Trig = 0;
	Trig = 1;
	Delay10us();
	Trig = 0;
}
 
double get_distance()  //获取距离
{
		double time;
		//定时器数据清零,以便下一次测距
		TH1 = 0;
		TL1 = 0;
	//1. Trig ,给Trig端口至少10us的高电平
		startHC();
		//2. echo由低电平跳转到高电平,表示开始发送波
		while(Echo == 0);
		//波发出去的那一下,开始启动定时器
		TR1 = 1;
		//3. 由高电平跳转回低电平,表示波回来了
		while(Echo == 1);
		//波回来的那一下,我们开始停止定时器
		TR1 = 0;
		//4. 计算出中间经过多少时间
		time = (TH1 * 256 + TL1)*1.085;//us为单位
		//5. 距离 = 速度 (340m/s)* 时间/2
		return  (time * 0.017);
}


摇头功能使用舵机实现

怎么控制舵机

向黄色信号线“灌入”PWM信号,PWM波的频率不能太高,大约50HZ,即周期=1/频率=1/50=0.02s,20ms左右

0.5ms-------------0度; 2.5% 对应函数中占空比为250

1.0ms------------45度; 5.0% 对应函数中占空比为500

1.5ms------------90度; 7.5% 对应函数中占空比为750

2.0ms-----------135度; 10.0% 对应函数中占空比为1000

2.5ms-----------180度; 12.5% 对应函数中占空比为125

SG90.c

#include "reg52.h"
#include "delay.h"
 
sbit sg90_con = P1^1;
 
int jd;  //定义角度
int cnt = 0;
 
void Time0Init()
{
	//1. 配置定时器0工作模式位16位计时
	TMOD &= 0xF0;		//设置定时器模式
	TMOD |= 0x01;
	//2. 给初值,定一个0.5出来
	TL0=0x33;
	TH0=0xFE;
	//3. 开始计时
	TR0 = 1;
	TF0 = 0;
	//4. 打开定时器0中断
	ET0 = 1;
	//5. 打开总中断EA
	EA = 1;
}
 
 
void SG90_Middle()
{
	//中间位置
	jd = 3; //90度 1.5ms高电平
	cnt = 0;
}
 
 
void SG90_Right()
{
	//右边位置
	jd = 1; //0度
	cnt = 0;
}
 
void SG90_Left()
{
	//左边位置
	jd = 5; //135度
	cnt = 0;
}
 
void Time0Handler() interrupt 1
{
	cnt++;  //统计爆表的次数. cnt=1的时候,报表了1
	//重新给初值
	TL0=0x33;
	TH0=0xFE;
	
	//控制PWM波
	if(cnt < jd){
		sg90_con = 1;
	}else{
		sg90_con = 0;
	}
	
	if(cnt == 40){//爆表40次,经过了20ms
		cnt = 0;  //当100次表示1s,重新让cnt从0开始,计算下一次的1s
		sg90_con = 1;
	}		
}


注意:如果舵机电压低于额定电压时,舵机可能会疯狂地不受控制的摇头,供电正常后这个问题就可以解决。(一开始我还以为是舵机坏了)

八.SU-03T语音模块介绍

接下来进入小车的最后一个阶段,语音控制。选用的是SU-03T这款语音模块,这款模块对小白特别友好,无需编程,不需要二次开发,通过厂家给的网站配置后即可使用,傻瓜式操作。而且这款模块的识别还是非常灵敏的,前端的界面设计的也非常好用。

 SU-03T语音模块配置:
智能公元/AIOT快速产品化平台 ​​​​​​http://www.smartpi.cn/#/

登录厂家所提供的开发平台,点击创建产品->其它产品

选择纯离线 方案

 

 选择我们使用的SU-03T

 填写好产品名称,语言选择中文,如何点击保存。

接下来就进入了我们的配置界面,我们选择三个IO口分别切换我们的循迹模式,跟随模式,避障模式。把语音模块的三个IO口都设置为高电平

接着配置语音模块的唤醒词,这里可以多配置几条,并且可以设置灵敏度。

接着再定义应答语,根据自己的功能定义。

接着再设置每种词条的命令,我设置的是当说出某种词条的时候指定的IO口输出低电平。

然后其余的设置都比较简单,根据自己的爱好选择音调,语速之类的。

点击生成后就等待生成,大约半个小时左右。

生成完之后点击下载SDK,后续烧录的过程参考厂家提供的资料即可,我都会跟这个项目的源代码放在一起。

九.语音切换小车模式+OLED显示模式

语音模块的加入是我们实现的最后一个功能,也是我们之前所有功能的一个大汇总,所有的功能都是基于我们前面写过的代码。但是在实现这个功能之前我有一点需要强调。由于51单片机只有两组定时器,而我们的许多功能都用到了定时器,比如测速,电机调速,舵机,超声波避障。因此我们没有多余的定时器去分配给这么多功能,因此最后这个“大杂烩”小车我们选择抛弃测速,电机调速这两个功能。倘若选用更强大的MCU比如STM32就不存在这种取舍问题。

小车总体功能:当说出“进入循迹模式”,小车会进入循迹模式。说出“进入跟随模式”,小车会进入跟随模式。说出“进入避障模式”,小车会进入避障模式。并且OLED屏幕上会显示小车的模式。

OLED.c

#include "reg52.h"
#include "intrins.h"
#include "Oledfont.h"
 
sbit scl = P1^2;
sbit sda = P1^3;
 
void IIC_Start()
{
	scl = 0;
	sda = 1;
	scl = 1;
	_nop_();
	sda = 0;
	_nop_();
}
 
void IIC_Stop()
{
	scl = 0;
	sda = 0;
	scl = 1;
	_nop_();
	sda = 1;
	_nop_();
}
 
char IIC_ACK()
{
	char flag;
	sda = 1;//就在时钟脉冲9期间释放数据线
	_nop_();
	scl = 1;
	_nop_();
	flag = sda;
	_nop_();
	scl = 0;
	_nop_();
	
	return flag;
}
 
void IIC_Send_Byte(char dataSend)
{
	int i;
	
	for(i = 0;i<8;i++){
		scl = 0;//scl拉低,让sda做好数据准备
		sda = dataSend & 0x80;//1000 0000获得dataSend的最高位,给sda
		_nop_();//发送数据建立时间
		scl = 1;//scl拉高开始发送
		_nop_();//数据发送时间
		scl = 0;//发送完毕拉低
		_nop_();//
		dataSend = dataSend << 1;
	}
}
 
void Oled_Write_Cmd(char dataCmd)
{
	//	1. start()
	IIC_Start();
	//		
	//	2. 写入从机地址  b0111 1000 0x78
	IIC_Send_Byte(0x78);
	//	3. ACK
	IIC_ACK();
	//	4. cotrol byte: (0)(0)000000 写入命令   (0)(1)000000写入数据
	IIC_Send_Byte(0x00);
	//	5. ACK
	IIC_ACK();
	//6. 写入指令/数据
	IIC_Send_Byte(dataCmd);
	//7. ACK
	IIC_ACK();
	//8. STOP
	IIC_Stop();
}
 
void Oled_Write_Data(char dataData)
{
	//	1. start()
	IIC_Start();
	//		
	//	2. 写入从机地址  b0111 1000 0x78
	IIC_Send_Byte(0x78);
	//	3. ACK
	IIC_ACK();
	//	4. cotrol byte: (0)(0)000000 写入命令   (0)(1)000000写入数据
	IIC_Send_Byte(0x40);
	//	5. ACK
	IIC_ACK();
	///6. 写入指令/数据
	IIC_Send_Byte(dataData);
	//7. ACK
	IIC_ACK();
	//8. STOP
	IIC_Stop();
}
 
 
void Oled_Init(void){
	Oled_Write_Cmd(0xAE);//--display off
	Oled_Write_Cmd(0x00);//---set low column address
	Oled_Write_Cmd(0x10);//---set high column address
	Oled_Write_Cmd(0x40);//--set start line address  
	Oled_Write_Cmd(0xB0);//--set page address
	Oled_Write_Cmd(0x81); // contract control
	Oled_Write_Cmd(0xFF);//--128   
	Oled_Write_Cmd(0xA1);//set segment remap 
	Oled_Write_Cmd(0xA6);//--normal / reverse
	Oled_Write_Cmd(0xA8);//--set multiplex ratio(1 to 64)
	Oled_Write_Cmd(0x3F);//--1/32 duty
	Oled_Write_Cmd(0xC8);//Com scan direction
	Oled_Write_Cmd(0xD3);//-set display offset
	Oled_Write_Cmd(0x00);//
	
	Oled_Write_Cmd(0xD5);//set osc division
	Oled_Write_Cmd(0x80);//
	
	Oled_Write_Cmd(0xD8);//set area color mode off
	Oled_Write_Cmd(0x05);//
	
	Oled_Write_Cmd(0xD9);//Set Pre-Charge Period
	Oled_Write_Cmd(0xF1);//
	
	Oled_Write_Cmd(0xDA);//set com pin configuartion
	Oled_Write_Cmd(0x12);//
	
	Oled_Write_Cmd(0xDB);//set Vcomh
	Oled_Write_Cmd(0x30);//
	
	Oled_Write_Cmd(0x8D);//set charge pump enable
	Oled_Write_Cmd(0x14);//
	
	Oled_Write_Cmd(0xAF);//--turn on oled panel		
}
 
void Oled_Clear()
{
	unsigned char i,j; //-128 --- 127
	
	for(i=0;i<8;i++){
		Oled_Write_Cmd(0xB0 + i);//page0--page7
		//每个page从0列
		Oled_Write_Cmd(0x00);
		Oled_Write_Cmd(0x10);
		//0到127列,依次写入0,每写入数据,列地址自动偏移
		for(j = 0;j<128;j++){
			Oled_Write_Data(0);
		}
	}
}
 
void Oled_Show_Char(char row,char col,char oledChar){ //row*2-2
	unsigned int  i;
	Oled_Write_Cmd(0xb0+(row*2-2));                           //page 0
	Oled_Write_Cmd(0x00+(col&0x0f));                          //low
	Oled_Write_Cmd(0x10+(col>>4));                            //high	
	for(i=((oledChar-32)*16);i<((oledChar-32)*16+8);i++){
		Oled_Write_Data(F8X16[i]);                            //写数据oledTable1
	}
 
	Oled_Write_Cmd(0xb0+(row*2-1));                           //page 1
	Oled_Write_Cmd(0x00+(col&0x0f));                          //low
	Oled_Write_Cmd(0x10+(col>>4));                            //high
	for(i=((oledChar-32)*16+8);i<((oledChar-32)*16+8+8);i++){
		Oled_Write_Data(F8X16[i]);                            //写数据oledTable1
	}		
}
 
 
/******************************************************************************/
// 函数名称:Oled_Show_Char 
// 输入参数:oledChar 
// 输出参数:无 
// 函数功能:OLED显示单个字符
/******************************************************************************/
void Oled_Show_Str(char row,char col,char *str){
	while(*str!=0){
		Oled_Show_Char(row,col,*str);
		str++;
		col += 8;	
	}		
}


main.c


#include "reg52.h"
#include "hc04.h"
#include "delay.h"
#include "sg90.h"
#include "motor.h"
#include "oled.h"
 
#define Middle 0 //定义舵机状态标志位
#define Left   1
#define Right  2
 
#define Following 1
#define Tracking  2
#define Avioding  3 //定义模式状态标志位
 
//语音模块引脚定义
sbit A25 = P1^5;   //跟随模式
sbit A26 = P1^6;   //避障模式
sbit A27 = P1^7;   //循迹模式
 
//跟随红外模块引脚定义
sbit Fol_leftSensor  = P2^5;
sbit Fol_rightSensor = P2^4;
 
//循迹模块引脚定义
sbit Tra_leftSensor  = P0^1;
sbit Tra_rightSensor = P0^2;
 
char dir;
double M_distance; //正前方距离
double L_distance; //左侧距离
double R_distance; //右侧距离
 
//跟随模式
void Following_Mode()
{
		if(Fol_leftSensor == 0 && Fol_rightSensor == 0){
				goForward();	
		}
		if(Fol_leftSensor == 1 && Fol_rightSensor == 0){
				goRight();
		}
		if(Fol_leftSensor == 0 && Fol_rightSensor == 1){
				goLeft();
		}
		if(Fol_leftSensor == 1 && Fol_rightSensor == 1){
				stop();
		}	
}
 
//循迹模式
void Tracking_Mode()
{
		if(Tra_leftSensor == 0 && Tra_rightSensor == 0){
			goForward();
		}
		if(Tra_leftSensor == 1 && Tra_rightSensor == 0){
			goLeft();
		}
		
		if(Tra_leftSensor == 0 && Tra_rightSensor == 1){
			goRight();
		}
		
		if(Tra_leftSensor == 1 && Tra_rightSensor == 1){
			stop();
		}		
}
 
//避障模式
void Avioding_Mode()
{
			if(dir != Middle){
			SG90_Middle();		
			dir = Middle;
			Delay300ms();	
		}
		
		M_distance = get_distance();
		if(M_distance > 25){
			goForward();//前进
		}else if(M_distance < 10){
			goBack();//距离过小时后退
		}
		else{
			stop();
			SG90_Left();
			Delay300ms();
			L_distance = get_distance();
						
			SG90_Middle();
			Delay300ms();
			
			SG90_Right();
			Delay300ms();	
			R_distance = get_distance();
			dir = Right;
			
			if(L_distance < R_distance){
				goRight();
			}
			
			if(L_distance > R_distance){
				goLeft();
			}			
		}
}
 
void main()
{
	int mark = 0;
	
	Time0Init();
	Time1Init();
	//舵机的初始位置
	SG90_Middle();
	Delay300ms();		
	Oled_Init();//OLED初始化
	Oled_Clear();//清屏
	Oled_Show_Str(2,2,"-----Ready----");
	
	while(1){
		//满足避障模式的条件
		if(A26 == 0 && A25 == 1 && A27 == 1){
				if(mark!=Avioding){
				Oled_Clear();
				Oled_Show_Str(2,2,"Avioding_Mode");
				}
				mark = Avioding;  
				Avioding_Mode();
		}
		//满足跟随模式的条件
		if(A26 == 1 && A25 == 0 && A27 == 1){
				if(mark!=Following){
				Oled_Clear();
				Oled_Show_Str(2,2,"Following_Mode");
				}
				mark = Following;
				Following_Mode();
		}
		//满足循迹模式的条件
		if(A26 == 1 && A25 == 1 && A27 == 0){
				if(mark!=Tracking){
				Oled_Clear();
				Oled_Show_Str(2,2,"Tracking_Mode");
				}
				mark = Tracking;
				Tracking_Mode();		
		}		
	}
}