0 序言
制作小车前,我们需要先明确自己的需求:
- 通信:BT-04 蓝牙模块
- 电机驱动:L298N电机驱动模块
- 底盘架构:三个轮子,呈三角形排布。其中两个轮子与电机相连,另一个为万向轮。
1 小车安装
2 蓝牙通信
2.1 数据包结构
小车计划采用“蓝牙调试器”软件的专业调试模式进行控制:软件链接。我们需要先熟悉该软件发送的数据包结构。
包头 | 原数据 | 校验 | 包尾 |
---|---|---|---|
1字节 | 自定义 | 1字节 | 1字节 |
0xA5 | 自定义 | “原数据”所有字节之和的低8位 | 0x5A |
传输的数据类型及位数我们可以在软件中自行设置。
2.2 Arduino代码
基础配置信息
// 头文件
#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; // 包尾
数据包结构体
typedef struct {
int go; // 前进 -128 ~ 127
int turn; // 转向 -128 ~ 127
} rxTypedef;
初始化函数
BT-04蓝牙模块的波特率为9600 bit/s
,串口的波特率应与蓝牙模块相同,设置为9600 bit/s
。
void bluetoothInit(rxTypedef* remoteInfo) {
// 初始化数据
remoteInfo->go = 0;
remoteInfo->turn = 0;
// 启动串口 波特率设置为9600 bit/s
Serial.begin(9600);
}
串口数据接收函数
每当Arduino接收到数据时,会调用 serialEvent
函数,我们在该函数中对接收到的数据进行处理。
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;
}
}
}
数据包解包函数
从缓存数组中取出每个变量的数据。此处未进行数据校验。
void remoteInfoUpdate() {
if (length != rxDataLenth) { // 数据包长度检测
return;
} else {
remoteInfo.go = rxBuffer[1];
remoteInfo.turn = rxBuffer[2];
}
}
至此,蓝牙通信的有关代码全部完成。
3 电机驱动模块
3.1 L298N模块的使用
这是一个L298N电机驱动模块的图片。你可能会觉得接口有一点点多。但其实搞清楚它的控制原理后就能很快完成接线。
输出A、输出B两个接口分别与小车的两个电机相连(不用区分正负);12V供电接口与12V锂电池正极相连;GND接口同时与电池负极和Arduino的GND接口相连;5V供电接口与Arduino的Vin接口相连。
对于通道A使能、逻辑输入这几个接口,网络上的文章描述的都较为复杂。此处我会给出一个更简单的理解方式。注:由于通道AB的功能相同,下文只对一个通道进行描述。
图中的两个5V针脚会恒定输出5V电压;两个使能针脚可以理解为开关,当接到5V电压时,对应通道打开;1、2针脚用于控制电机的转动方向,1、2针脚分别为高电平时电机会转动且转动方向相反,其他情况电机不转动。
因此,如果我们想利用PWM来对电机进行调速,我们有两种接线方式:
3.2 接线方式一(不推荐)
我们不拔除通道使能与5V之间的跳帽。这种情况下,通道会一直保持打开状态。我们分别将1、2两个针脚接到Arduino的两个PWM接口,通过分别控制1、2两个针脚对应的输出来控制电机旋转方向和速度。显然,这种接线方式会占用较多的PWM引脚(对于每个通道需要占用两个PWM引脚),但不需要占用普通引脚。在PWM引脚较少的情况下不推荐使用这种方案。
3.3 接线方式二(推荐)
拔除通道使能与5V之间的跳帽,将使能针脚与Arduino的PWM引脚相连、1、2两个针脚与Arduino的两个普通引脚相连。这是,我们可以通过PWM引脚来控制通道的开关,用于调速;控制1、2两个针脚的电平来控制电机的转动方向。对于每个通道,这种接线方式只需要占用一个PWM引脚,但是会占用两个普通引脚。我们需要根据实际的引脚数量在两种接线方式中进行权衡。本文接下来的代码部分以第二种接线方式为例。
3.4 Arduino代码
电机基础信息配置
// 电机引脚设置
#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
电机初始化函数
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);
}
电机速度设置函数
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
}
电机控制函数
void motorCtrl(int go, int turn) {
if (turn < 0) {
motorSetSpeed(go * 2 + turn, go * 2);
} else {
motorSetSpeed(go * 2, go * 2 - turn);
}
}
至此,电机的驱动代码全部完成。
4 setup函数和loop函数
setup
函数只会运行一次。在此函数中,我们进行各类初始化操作。
void setup() {
// 初始化
motorInit();
bluetoothInit(&remoteInfo);
}
loop
函数会循环运行。在此函数中,我们执行需要循环的操作。
void loop() {
// 电机控制
motorCtrl(remoteInfo.go, remoteInfo.turn);
delay(100);
}