教程 · 入门 · 1 小时

用 HC-05 模块通过蓝牙控制机器人

把 HC-05 接到 Arduino 上并与手机配对,再设计一套串口指令协议,让机器人在蓝牙链路断开时依然能及时响应、保持安全。

引言

HC-05 是让机器人变成无线最便宜的办法。它是一个蓝牙串口桥:手机发来的任何东西,都会以普通字节的形式出现在 Arduino 的串口上;Arduino 打印的任何东西,也会回到手机。你的程序里完全不需要知道蓝牙的存在。

这种简单正是它的吸引力,也正是它的陷阱。无线电链路不是一根线——它会掉、会延迟,而且会在车还在跑的时候断掉。本文大部分内容讲的就是这个差别。

接线,以及几乎人人都会犯的那个错

HC-05 模块的逻辑供电是 3.3 V,尽管转接板上的 VCC 接受 5 V。RX 引脚不耐 5 V。

HC-05 Arduino Uno / Nano 说明
VCC 5V 板载稳压器负责转换
GND GND 与其他一切共地
TXD D10(软串口 RX) 3.3 V 输出被 5 V 输入读作逻辑高没问题
RXD D11(软串口 TX)经分压器 直接灌 5 V 时间久了会损坏
EN / KEY D9(可选) 上电时拉高,进入 AT 命令模式

用两只电阻的分压器把 Arduino 的 5 V 发送线降到 3.3 V——从 Arduino 引脚到 HC-05 的 RXD 串 2 kΩ,从这个节点到地再接 3.3 kΩ。成本是两个元件,却能避免一种诊断起来极其恼人的故障,因为这样损坏的模块往往还能半工作:它能配对、能发送,然后默默地无视或损坏你发给它的内容。

接线图。Arduino Uno 或 Nano 的 TXD 线直接接到 HC-05 模块的 D10 脚,标注为安全,因为 3.3 V 输出在 5 V 输入上会被读作高电平。从 D11 回到模块 RXD 的回程路径经过一只 2 千欧和一只 3.3 千欧的分压器,并附警告:5 V 直接进 RXD 会部分损坏模块,导致它仍能配对但会损坏你发出的内容。
四根线里有三根是顺手的。第四根需要分压器,因为 5 V 进模块的 RXD 是缓慢地损坏它,而不是立刻炸给你看。 下载 SVG

**用 SoftwareSerial,不要用 0、1 脚。**硬件串口是与 USB 共用的,模块接在那里会跟串口监视器抢线,还会阻塞上传。

#include <SoftwareSerial.h>
SoftwareSerial bt(10, 11);   // RX, TX

void setup() {
  Serial.begin(115200);      // USB, for debugging
  bt.begin(9600);            // HC-05 default baud
}

配对

给机器人上电,模块的 LED 快速闪烁——那表示“正在等待配对”。在手机上,用配对码 1234 或 0000 与 HC-05 配对。

配对不等于连接。只有当手机上的某个应用真的打开了串口,链路才会建立。起步阶段随便一个蓝牙串口终端都行;连接建立后,模块的 LED 会放慢成双闪。

设计指令协议

最直白的第一个协议是每条指令一个字符,它大约能撑十分钟:

if (bt.available()) {
  char c = bt.read();
  if (c == 'F') forward();
  else if (c == 'B') back();
  else if (c == 'S') stop();
}

它有两个问题。

**它没有速度。**每条指令都是全油门,所以车不是停着就是猛冲。改成随指令一起发送数值——一个字母加一个数字,用换行结尾,解析起来很简单,手工输入调试时也很简单:

// "F180\n" — forward at 180 of 255
char buffer[16];
if (bt.available()) {
  int n = bt.readBytesUntil('\n', buffer, sizeof(buffer) - 1);
  buffer[n] = '\0';
  char cmd = buffer[0];
  int value = atoi(buffer + 1);
  ...
}

它假定消息完整到达。bt.available() 非零只意味着有字节到了,不是全部都到了。每轮循环读一个字符并立即动作,就会在半条指令上动作。一直读到换行符,才是让解析可靠的关键。

真正要紧的故障:链路丢失

有线的车,你一松手它就停。无线车会继续做你最后告诉它的事——如果那件事是“全速前进”,而你刚刚走出了覆盖范围,那么结果就是一台全速钻到沙发底下的车。

时序图。上面一排指令脉冲在按住按键期间每 100 毫秒到来一次,随后在一条标注“信号丢失”的虚线处停止。下面一条表示没有超时的情形,信号丢失后保持红色并无限期失控运行。第二条带 400 毫秒松手超时的条,在最后一条指令之后不久转为停止。
指令流是在链路断掉时停止的,而不是在用户松手时停止的——这正是车必须自己超时的原因。 下载 SVG

**每一台无线车都需要一个松手超时。**规则很简单:最近没有指令到达,就停车。

unsigned long lastCommandAt = 0;
const unsigned long TIMEOUT_MS = 500;

void loop() {
  if (readCommand()) lastCommandAt = millis();

  // Nothing heard recently? Assume the link is gone.
  if (millis() - lastCommandAt > TIMEOUT_MS) stop();
}

要让它真的起作用,控制器必须持续说话。让手机重复发送当前指令——按住按键时大约每 100 ms 一次——而不是按下时一次、松开时一次。这样“按键松开”和“手机走出范围”就会得到同一个安全结果,而你也永远不必依赖那条可能永远不会到达的松开消息。

这里要用 millis(),永远不要用 delay()。循环里一个 delay(200) 就是 200 ms 期间不读任何指令、不检查超时,车是完全聋的。

AT 命令模式

要改模块名或改波特率,就在上电时把 EN/KEY 拉高。LED 会缓慢闪烁——大约每两秒一次——这就是你处在命令模式而不是数据模式的标志。

AT              → OK
AT+NAME=MyRobot → OK
AT+PSWD="4821"  → OK
AT+UART=38400,0,0

AT 模式下的指令必须以 38400 波特率发送,与数据模式的波特率无关,而且大多数固件要求回车和换行都发。如果 AT 什么都没返回,几乎总是这个不匹配造成的。

如果你要回传遥测数据,把数据波特率提上去是值得的;但在协议还没调通之前先留在 9600——链路更快主要只会让分帧错误更难看清。

开跑前先验证

三项检查,按这个顺序做:

  1. **回声测试。**让 Arduino 把收到的东西原样打印回来。如果手打的一行原封不动地回来,接线和波特率就是对的。
  2. **解析测试。**把解析出的指令字母和数值打印出来。确认 F180 得到的是 F 和 180,不是 F 和 1。
  3. **超时测试。**发一条前进指令,然后关掉应用或走开。车必须在你设定的超时内停下。如果不停,在轮子上地面之前先把它修好。

链路可靠之后,同一套指令协议可以直接搬进一台带自主功能的机器人:无线层可以看手机遥控车项目,或者升级到 ESP32,它通过 Wi-Fi 提供一个控制页面,完全不需要手机应用。

探索关联图谱

本教程所属的搭建

包含本教程的项目和学习路线。

延伸阅读

参考资料