基於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
總結
做小車是一個很鍛煉自己動手能力的事,尤其對新手來說。挺開心的,能做一個自己的遙控小車。哈哈
