【制作】基于51单片机的蓝牙遥控小车方案

基于51单片机的蓝牙遥控小车方案

最近开学,我们电子实验室也来了一些20级的学弟学妹,当初去宣传的时候是说来这里能自己做遥控车、软件的,现在提供方案,以作指导,第一次写这种方案,有不妥之处我们再议,哈哈~

系统原理

51单片机蓝牙遥控小车的系统框图大致如下:
系统原理
这是经典的自制蓝牙遥控车系统方案,整个系统分为手机跟小车两部分。
手机端可以自己写蓝牙软件,也可以直接用应用商店现成的,新手建议直接在应用商店下载“蓝牙串口”相关的应用即可。
小车这部分是我们重点关注的,主要由51单片机,电机驱动,蓝牙通信模块和电机等组成。小车的架子可以自己选择,选择两个电机或者四个电机的都可以,这边建议新手选择两个电机的快速入门。
蓝牙模块负责通过无线 电与手机进行通信,接收手机下发的指令,并传达给51单片机。51单片机作为小车的主要控制器,根据蓝牙模块接收到的手机指令操作电机等外围器件执行例如启动、关闭电机,打开、关闭LED等动作。因为单片机的IO口驱动能力有限(电流被限制)所以要外加电机驱动来使电机转动,因而电机驱动的作用就是接收51单片机的指令来控制电机的动作。电机的作用就显而易见了,用来使小车运动。
基本的系统架构就是这样,接下来接收各个部分的技术要点。

各个模块介绍

蓝牙模块

蓝牙模块这边以型号“HC-05”为例介绍。

蓝牙模块的价格从十几到二十几不等,我们这个方案十几的也能用,看自己需要。
HC-05有两种模式,主模式和从模式,主模式能主动配对其他蓝牙设备并连接,从设备只能等待其他蓝牙设备的连接,根据系统框图,我们这个系统就只需要有从模式的就可以了,因为我们只接受手机下发的指令。
蓝牙模块是通过串口通信协议跟单片机进行交流的(什么是通信协议,就类似于我们的中文,英语和方言等,是一种交流的规则,比如说中文用汉字“好”表示好的可以的意思,而英语用英文字母“OK”表示好的意思,需要支持共同协议的双方才能通信),所以需要事先确定好通信协议,比如蓝牙发送2是前进的意思,8是后退的意思等。蓝牙模块的操作一般看商家提供的资料就知道怎么使用了,而且拿回来可以直接用,这边就不过多介绍了。

我所使用的是HC-05蓝牙模块作为本方案的通信模块

电机及其驱动模块

众所周知,单片机的驱动能力非常有限,甚至LED都需要驱动,更别说功率更大的电机了,而电机驱动模块就是增强单片机的负载能力,使单片机能控制电机,所以使用电机驱动模块非常有必要。

电机驱动的选择需要根据电机的种类选择,电机一般有直流电机和步进电机,各自的优缺点大家可以到网上查资料,它们所需要的的驱动模块类型也不一样,购买时需要确认好,避免买错了。这边最近淘宝搞活动,刚刚好花2块钱买了两个28byj48步进电机和配套的驱动板(这里是个坑,速度巨慢,建议买其他的,比如25GA370直流电机等,买电机最好买减速电机,不然电机本身的扭力可能不足以驱动小车),于是就准备用这个来做。

我所使用的是两个28byj48步进电机和配套的驱动板作为本方案的电机(后面发现是坑,巨慢)

电源及电源模块

电源大家可以选择电池盒或者是航模锂电池等,其提供的电压必须是满足小车里所有器件对电压的最低要求的,例如单片机需要5v电压,而电机需要12v,那么电池应该提供的电压是大于等于12v的。

这里我比较推荐航模锂电池,容量大,输出功率高,适合小车这种有大功率用电(电机)的设备。那么如果使用12v的电池,单片机的5v怎么搞呢?这里就需要用到稳压模块,需要准备一些稳压模块,把12v转换成5v提供给单片机,当然如果出现了问题一般是没有共地(所有GND没有相连)。

稳压模块一般注意的就是其提供的功率应该要满足5v电路的需求,那么稳压模块一般推荐LM2596,这边我这次使用的也是这个。

我所使用的是12V锂电池和LM2596s稳压模块作为本方案的电源

单片机最小系统

任何51单片机的最小系统都可以,可以自己搭,也可以直接买,买的话,推荐以下几种:

我所使用的是图左边这个(注意晶振要安装好)

其他模块

像上面系统原理图上提到的小灯和屏幕或者是没提到的超声波测距和摄像头等都可以为小车增加新的功能特性,由于我们的目的就只想做个遥控车,其他模块就大家自己去了解了,了解的时候可以重点关注器件的数据手册以及单片机的数据手册。

我在本方案中为了简单没有使用其他传感器模块

小车结构

小车架子

众所周知,车一般都有一个车架子,作为车的支撑和承载结构。车架很大方面影响到车的最终效果,大家可以去选购合适的车架。其中有一点需要说明的是,我们选择的车架前轮转弯的调整方式应该是通过电机来调整比较简单适合入门,通过舵机调整的效果更好,但是需要自己学习,这边就不讲舵机的相关知识了。电机实现的比较优雅的转弯方式大家可以搜索“万向轮”和“转向前桥”,这边我使用结构比较简单的万向轮来实现转弯。除了转弯实现方式,电机是否与小车架子匹配也是一方面,不要到时候电机和架子都买了,发现电机无法安装就尴尬了,哈哈。

小车架子除了以上要注意的,其他的大家可以按喜好选择。

我所使用的是类似于图片中的架子(万向轮),不过电机是28byj48步进电机

通信协议

上面讲蓝牙模块的时候提到了通信协议,这边不多再提,主要讲通信协议的设计要点。
下面引自百度百科对通信协议的要素说明:

通信协议主要由以下三个要素组成:
语法:即如何通信,包括数据的格式、编码和信号等级(电平的高低)等。
语义:即通信内容,包括数据内容、含义以及控制信息等。
定时规则(时序):即何时通信,明确通信的顺序、速率匹配和排序。

建立通信协议的目的是准确、完整、快速的传达消息,因此,消息的指令应该简短明确且有完整性校验。
这边简单讲下我们这个方案通信协议设计的基本思路和过程(以万向轮架子为例)。

这边我想通过控制电机来控制小车的动作。
我们需要控制两个电机,电机包括转向(正转、反转、停止)、速度这两个参数。经过试验,发现28byj48步进电机能调步进速度的范围在1ms-30ms之间比较合适,30ms已经很慢了。

  • S代表设置转向(S:sta),后面跟1位数字作为状态表达。0:停止,2:逆(后退),1:顺(前进)。
  • 想继续用s代表设置速度的(s:speed),但想到两个‘S’不利于区分,而且状态只有3种的话就直接用一位数字就能完成所有的状态表达了,而速度的范围是1-30,需要两位数字,因此就使用数字代表设置速度了,同时也表示速度的十位。

基本就以上两种类型的信息就能实现对电机的状态和速度控制。
考虑到小车上有两个电机,因此我们再设计一个描述符,表示我们要控制的是哪一个电机。

  • 调整的电机,L代表要控制电机1,R代表要控制电机2。

消息基本设计完毕,最后面还需要加完整性校验或者是说触发消息。
以后系统越做越复杂的话可能不止我们这一种协议,因此应该在消息头表明消息类型,这样也便于程序处理。

  • 我们的控制电机的消息都以C开头(C:car)

消息传输完毕后应该告诉单片机消息传输完毕了,就像我们跟别人说再见一样。以便单片机及时处理消息。

  • 我们的消息都以\r\n(换行回车)结尾

最后,我们设计的通信可以做如下总结:

/**********************
串口通信协议说明:
有以下4种指令:

左电机:
1.CLS{$模式代码}\r\n,模式代码为0,1,2,分别代表0:停止,2:逆(后退),1:顺(前进),成功设置返回'1'
2.CL{$两位速度值}\r\n,速度值为01-30,必须是两位,例如:01,30 ,成功设置返回'2'

右电机:(同左电机,不过指令的L改为R)
1.CRS{$模式代码}\r\n,模式代码为0,1,2,分别代表0:停止,2:逆(后退),1:顺(前进),成功设置返回'3'
2.CR{$两位速度值}\r\n,速度值为01-30,必须是两位,例如:01,30,成功设置返回'4'

设置失败返回'0'
**********************/

具体怎么实现我们要在代码部分完成。

程序代码

单片机部分

硬件

硬件连接如下:

很简单~
电池输出的12V电压通过稳压器稳压到5V之后提供给整个系统电源。
蓝牙模块连接至单片机的串口(RX接TX,TX接RX)。
两个电机驱动依次接至单片机的P1口。
电机直接分别接到电机驱动模块上。

程序

新建工程,写入如下代码:

/**************************************************
时间:2020-11-06 11:45:40
作者:Minuye
版本:v1
功能:步进电机遥控车代码
**************************************************/
#include <reg52.h>
#define UART_MAX_COUNT 6

unsigned char code BeatCode[8] = {	//电机节拍
	0x0E, 0x0C, 0x0D, 0x09,
	0x0B, 0x03, 0x07, 0x06};

void ConfigTimer0();//配置定时器函数申明
void InitUART();//配置串口函数申明
void SendOneByte(unsigned char c);//串口发送函数申明

unsigned char beatstaL = 0;	//左边电机状态,0:停止,2:逆(后退),1:顺(前进)
unsigned char revL = 1;		//左边默认电机速度为7(1 - 30)

unsigned char beatstaR = 0;	//右边电机状态,0:停止,2:逆(后退),1:顺(前进)
unsigned char revR = 1;		//右边默认电机速度为7(1 - 30)

bit flagUART = 0;						//串口收到数据标志位
unsigned char uartBuff[UART_MAX_COUNT];	//串口数据接收缓存

void main()
{
	unsigned char cIndex;	//处理缓存标记
	InitUART();				//配置串口	
	ConfigTimer0();			//配置T0
	PT0 = 1;				//配置T0中断为高优先级,启用这行可以防止在其他中断产生的时候电机卡 
	EA = 1;					//开总中断

	//初始化缓存
	for(cIndex=0;cIndex<UART_MAX_COUNT;cIndex++)
	{
		uartBuff[cIndex] = ' ';	
	}	

	while(1)
	{
		if(flagUART)//如果串口缓存好了
		{
			bit isSuccess = 0;	//处理结果标记
			flagUART = 0;	//标记已经处理

			//以下是通信协议处理
			if(uartBuff[0] == 'C')	//如果是自己的数据
			{
				switch(uartBuff[1])	//判断是哪个电机的
				{
					case 'R':
						if(uartBuff[2] == 'S')	//如果是设置状态的
						{
							switch(uartBuff[3])
							{
								case '0':beatstaR = 0;isSuccess = 1;break;	//停止
								case '1':beatstaR = 1;isSuccess = 1;break;	//前进
								case '2':beatstaR = 2;isSuccess = 1;break;	//后退
							}

							//返回成功设置状态码
							if(isSuccess)
							{
								SendOneByte('1');
							}
						}
						else	//否则是设置速度的
						{
							unsigned char shi,ge;	//定义十位和个位

							//先把字符转成数字
							shi = uartBuff[2] - '0';
							ge = uartBuff[3] - '0';
							
							if(shi<4 && shi>=0)	//十位合法性校验
							{
								if(ge<10 && ge>=0)	//个位合法性校验
								{
									revR = shi*10+ge;	//计算指令要设置的速度是多少

									//返回成功设置状态码
									isSuccess = 1;
									SendOneByte('2');
								}
							}	
						}
					break;

					case 'L': 
						if(uartBuff[2] == 'S')	//如果是设置状态的
						{
							switch(uartBuff[3])
							{
								case '0':beatstaL = 0;isSuccess = 1;break;	//停止
								case '1':beatstaL = 1;isSuccess = 1;break;	//前进
								case '2':beatstaL = 2;isSuccess = 1;break;	//后退	
							}

							//返回成功设置状态码
							if(isSuccess)
							{
								SendOneByte('3');
							}
						}
						else	//否则是设置速度的
						{
							unsigned char shi,ge;	//定义十位和个位

							//先把字符转成数字
							shi = uartBuff[2] - '0';
							ge = uartBuff[3] - '0';
							
							if(shi<4 && shi>=0)	//十位合法性校验
							{
								if(ge<10 && ge>=0)	//个位合法性校验
								{
									revL = shi*10+ge;	//计算指令要设置的速度是多少

									//返回成功设置状态码
									isSuccess = 1;
									SendOneByte('4');
								}
							}	
						}
					break;
				}	
			}
			
			//清空一下缓存(不必要)
			for(cIndex=0;cIndex<UART_MAX_COUNT;cIndex++)
			{
				SendOneByte(uartBuff[cIndex]);
				uartBuff[cIndex] = ' ';	
			}
			
			if(!isSuccess)
			{
				SendOneByte('0');	
			}
		}

		//右电机速度阈值控制
		if(revR<1)
		{
			revR = 1;
		}else{
			if(revR>30)
			{
				revR = 30;
			}
		}
		
		//左电机速度阈值控制
		if(revL<1)
		{
			revL = 1;
		}else{
			if(revL>30)
			{
				revL = 30;
			}
		}
	}
}
/* -----------------------配置并启动T0,ms-T0定时时间------------------- */
void ConfigTimer0()
{
    TMOD &= 0xF0;	//清零T0的控制位
    TMOD |= 0x01;	//配置T0为模式1
    TH0 = 0xF8; 	//加载T0重载值
    TL0 = 0xCD;
    ET0 = 1;		//使能T0中断
    TR0 = 1;		//启动T0
}

/*--------------------------电机动作-----------------------*/
void beatR()//右边电机
{
	unsigned char tmp;
	static unsigned char i = 1;	//当前节拍
	tmp = P1;
	tmp = tmp & 0xF0;	//清空P1低四位
	tmp = tmp | BeatCode[(i-1)];	//写入节拍
	P1 = tmp;
	switch(beatstaR)		//电机状态
	{
		case 0:break;		//停止
		case 1:i++;break;	//逆时针
		case 2:i--;break;	//顺时针
		default:break;
	}
	if(i>8)i = 1;	//循环
	if(i<1)i = 8;
}

void beatL()//左边电机
{
	unsigned char tmp;
	static unsigned char i = 1;	//当前节拍
	tmp = P1;
	tmp = tmp & 0x0F;	//清空P1高四位
	tmp = tmp | (BeatCode[(i-1)]<<4);	//写入节拍
	P1 = tmp;
	switch(beatstaL)		//电机状态
	{
		case 0:break;		//停止
		case 1:i--;break;	//顺时针
		case 2:i++;break;	//逆时针
		default:break;
	}
	if(i>8)i = 1;	//循环
	if(i<1)i = 8;
}


/* ---------------------------------定时器中断----------------------------- */
void InterruptTimer0() interrupt 1
{
	static unsigned char timer = 0;	//软件计时
    TH0 = 0xF8;	//加载T0重载值
    TL0 = 0xCD;

	timer++;	//时间累加
	if(timer > 30)	//超时
	{
		timer = 0;
	}

	if(timer%revL == 0)	//左电机刷新时间
	{
		beatL();		//电机刷新	
	}

	if(timer%revR == 0)	//右电机刷新时间
	{
		beatR();		//电机刷新	
	}

}

/* ---------------------------------串口相关----------------------------- */
void InitUART()
{
    TMOD = 0x20;
    SCON = 0x50;
    TH1 = 0xFD;
    TL1 = TH1;
    PCON = 0x00;
    ES = 1;
    TR1 = 1;
}

//发送一个字符到串口
void SendOneByte(unsigned char c)
{
    SBUF = c;
    while(!TI);
    TI = 0;
}

//串口中断
void UARTInterrupt(void) interrupt 4
{
	unsigned char temp;
	static unsigned char index = 0;
    if(RI)//接收中断
    {
		temp = SBUF;			//取出数据
		uartBuff[index] = temp;	//保存数据

		index++;

		if(temp == '\n'||index == UART_MAX_COUNT)//如果是指令结尾或者缓存满了
		{
			flagUART = 1;	//提醒主循环处理
			index = 0;		//下次覆盖缓存
		} 
		
        RI = 0;//标志位置0
    } 
}

安卓部分

单片机部分我们已经知道如何使用蓝牙的数据了,但是作为遥控器的手机如何正确发数据给单片机呢?
这边有两个选择,一个是使用现有应用市场上的蓝牙串口软件,设置好协议后可以直接用,还有就是自己编写安卓代码,实现高度定制的安卓蓝牙遥控器。
市场上的可以使用蓝牙串口或者是蓝牙调试器直接设置好协议和布局即可。

我这边是选择的是自己编写安卓代码,开源地址:https://gitee.com/minuy/bluetooth-car-remote-control,可以直接拿到代码自己改,或者在这里下载APK直接用。

我选择的是自己制作遥控软件。

软件截图:

自己改也不难,代码一共就1000+行。

作品效果

成功实现遥控小车的基本功能,视频链接:
Bilibili:https://b23.tv/mVO5Hp

总结

做小车是一个很锻炼自己动手能力的事,尤其对新手来说。挺开心的,能做一个自己的遥控小车。哈哈

posted @ 2020-11-19 23:37  清风来叙  阅读(3594)  评论(0编辑  收藏  举报