[Arduino]从零制作蓝牙小车

序言

制作小车前,我们需要先明确自己的需求:

通信:BT-04 蓝牙模块
电机驱动:L298N电机驱动模块
底盘架构:三个轮子,呈三角形排布。其中两个轮子与电机相连,另一个为万向轮。

蓝牙通信

小车计划采用“蓝牙调试器”软件的专业调试模式进行控制。我们需要先熟悉该软件发送的数据包结构。

包头 原数据 校验 包尾
1字节 自定义 1字节 1字节
0xA5 自定义 “原数据”所有字节之和的低8位 0x5A

传输的数据类型及位数我们可以在软件中自行设置。

Arduino代码

基础配置信息

1
2
3
4
5
6
7
8
9
10
11
12
// 头文件
#include "HardwareSerial.h"

// 数据长度设置
#define rxDataLenth 5 // 包头(1) + 前进(1) + 转向(1) + 校验(1) + 包尾(1)
#define bufferSize 10 // 缓存区长度

bool isReading = false;
int length = 0;
char rxHead = 0xA5; // 包头
char rxTail = 0x5A; // 包尾

数据包结构体

1
2
3
4
typedef struct {
int go; // 前进 -128 ~ 127
int turn; // 转向 -128 ~ 127
} rxTypedef;

初始化函数

BT-04蓝牙模块的波特率为9600 bit/s,串口的波特率应与蓝牙模块相同,设置为9600 bit/s。

1
2
3
4
5
6
7
8

void bluetoothInit(rxTypedef* remoteInfo) {
// 初始化数据
remoteInfo->go = 0;
remoteInfo->turn = 0;
// 启动串口 波特率设置为9600 bit/s
Serial.begin(9600);
}

串口数据接收函数

每当Arduino接收到数据时,会调用 serialEvent函数,我们在该函数中对接收到的数据进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
char rxBuffer[bufferSize]; // 缓存接收数据

void serialEvent() {
// 调用serialEvent函数时可能已经读入多个数据,我们需要对当前已接收的所有数据进行处理
while (Serial.available()) {
char tmp = Serial.read(); // 从串口中读入一帧数据
if (tmp == rxHead) { // 如果是包头,则开始记录数据
length = 0;
isReading = true;
} else if (isReading == false) {
// 如果既不是包头,也不在读数据,则忽略这一帧数据
continue;
}

// 将数据存入缓存数组
rxBuffer[length++] = tmp;

if (tmp == rxTail) { // 如果是包尾,则停止记录数据,并开始解包操作
isReading = false;
remoteInfoUpdate(); // 解包函数
} else if (length >= bufferSize) { // 长度超出缓存区长度接收异常,停止接收并清空缓存
length = 0;
isReading = false;
}
}
}

数据包解包函数

从缓存数组中取出每个变量的数据。此处未进行数据校验。

1
2
3
4
5
6
7
8
void remoteInfoUpdate() {
if (length != rxDataLenth) { // 数据包长度检测
return;
} else {
remoteInfo.go = rxBuffer[1];
remoteInfo.turn = rxBuffer[2];
}
}

至此,蓝牙通信的有关代码全部完成。

电机驱动模块

电机基础信息配置

1
2
3
4
5
6
7
8
// 电机引脚设置
#define Motor1_M1_Port 12
#define Motor1_M2_Port 13
#define Motor1_PWM_Port 3

#define Motor2_M1_Port A4
#define Motor2_M2_Port A5
#define Motor2_PWM_Port 11

电机初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
void motorInit() {
// 设置引脚模式
pinMode(Motor1_M1_Port, OUTPUT);
pinMode(Motor1_M2_Port, OUTPUT);
pinMode(Motor2_M1_Port, OUTPUT);
pinMode(Motor2_M2_Port, OUTPUT);

// 将引脚置于低电平
digitalWrite(Motor1_M1_Port, LOW);
digitalWrite(Motor1_M2_Port, LOW);
digitalWrite(Motor2_M1_Port, LOW);
digitalWrite(Motor2_M2_Port, LOW);
}

电机速度设置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void motorSetSpeed(int v1, int v2) {
// 设置转动方向
if (v1 >= 0) {
digitalWrite(Motor1_M1_Port, LOW);
digitalWrite(Motor1_M2_Port, HIGH);
} else {
digitalWrite(Motor1_M1_Port, HIGH);
digitalWrite(Motor1_M2_Port, LOW);
}
if (v2 >= 0) {
digitalWrite(Motor2_M1_Port, LOW);
digitalWrite(Motor2_M2_Port, HIGH);
} else {
digitalWrite(Motor2_M1_Port, HIGH);
digitalWrite(Motor2_M2_Port, LOW);
}

// 设置速度 通过PWM控制
analogWrite(Motor1_PWM_Port, abs(v1)); // Value = 0 ~ 255
analogWrite(Motor2_PWM_Port, abs(v2)); // Value = 0 ~ 255
}

电机控制函数

1
2
3
4
5
6
7
void motorCtrl(int go, int turn) {
if (turn < 0) {
motorSetSpeed(go * 2 + turn, go * 2);
} else {
motorSetSpeed(go * 2, go * 2 - turn);
}
}

至此,电机的驱动代码全部完成。

setup函数和loop函数

setup函数只会运行一次。在此函数中,我们进行各类初始化操作。

1
2
3
4
5
6
void setup() {

// 初始化
motorInit();
bluetoothInit(&remoteInfo);
}

loop函数会循环运行。在此函数中,我们执行需要循环的操作。

1
2
3
4
5
void loop() {
// 电机控制
motorCtrl(remoteInfo.go, remoteInfo.turn);
delay(100);
}