蓝牙遥控小车:Arduino + HC-05 手机 App 驱动 2WD 底盘

Cartoon of a dark navy smartphone showing a ring of teal arrow buttons, sending teal radio arcs to a two-wheel robot car with yellow wheels, a blue-and-green Bluetooth module and a red motor driver board on its clear acrylic deck

一台蓝牙遥控小车,就是一个 2WD 底盘、一块 L298N 驱动板,再加上一颗把手机按键交给 Arduino 的 HC-05。免费的遥控 App 里每按一个键,只发出一个 ASCII 字符 —— 「F」是前进,「S」是停 —— 而整份程序就是一个 switch-case,把每一个字符翻译成一种 H 桥状态。想通这一件事,任何一款蓝牙 App 都能开动你的车 —— 前提是用 Android 手机,因为 HC-05 讲的是 Bluetooth Classic,而 iOS 并不把它开放给普通 App。

做一台蓝牙遥控小车需要哪些零件?

2WD 底盘套件提供的是机械部分 —— 亚克力底板、两颗 TT 减速马达、轮子、一个万向轮、螺丝,以及一个船型开关。HC-05 是这台车和我们那篇避障小车之间唯一的区别:同一副底盘、同一块驱动板、同一组电池,只是换了一样东西来告诉它往哪走。

Parts list

ItemPriceQty
2WD 4WD Smart Car Chassis Single Layer Acrylic Arduino Smart Car Chassis Kit With Motor - 4WD CAR KIT2WD 4WD Smart Car Chassis Single Layer Acrylic Arduino Smart Car Chassis Kit With Motor - 4WD CAR KIT4WDSCARRM29.95
TP4056 18650 Battery Charger Module Lithium Ion Battery Li-ion Battery Charger Type C Micro USB - TP4056 CHARGER (TYPEC)TP4056 18650 Battery Charger Module Lithium Ion Battery Li-ion Battery Charger Type C Micro USB - TP4056 CHARGER (TYPEC)TP4056CRM2.50
Soldering Service for Circuit Boards & Electronics - 5 Pins Soldering ServiceSoldering Service for Circuit Boards & Electronics - 5 Pins Soldering ServiceSOLSE05RM0.80
400 pcs 1/4W Resistor Pack Resistor Kit 20 Common Value with 20 each400 pcs 1/4W Resistor Pack Resistor Kit 20 Common Value with 20 each400RPACRM7.95缺货

4WD 底盘是另一条路线,跑的是同一份程序,把每一侧的两颗马达并联接进 L298N 的同一个通道即可。TP4056 是单节充电板,所以两节 18650 要从电池盒里取出来、一节一节地充,绝对不能整组接在 7.4 V 的电池组两端。5 针焊接服务覆盖的正是四个马达焊点,如果你不想自己动烙铁;电阻包则是留给下文那条回传线的分压电阻,任何 1:2 的比例都成立:1k 配 2k,或者 10k 配 20k。

第一次下单能不能用得上,取决于杜邦线的公母。HC-05 那排六支脚是弯针头,L298N 的 IN1–IN4 也是公针,而 Uno 板边是母座 —— 所以每一条信号线都是公对母。电源线是唯一的例外:一端是压在 L298N 螺丝端子下的裸露公针,另一端要插进 Uno 的母座,这两条得从公对公那一包里拿。电池盒搪过锡的引线直接拧进螺丝端子,所以这台车全程用不到面包板 —— 面包板硬跳线留着做桌面实验就好。这块 Uno 是用全尺寸 USB-B 烧录的,所以还需要那条 A 转 B 数据线。动手接第一根线之前先养成一个习惯:HC-05 的引脚名印在板子的背面而不是正面,所以请把模块翻过来看清楚。VCC 和 GND 就挨在那一排上,把这两支接反,是能直接送走一颗模块的错误。

唯一躲不掉的焊接活是 TT 马达:后端盖上是两片裸露的金属焊片,另附一对导线,一共四个焊点。手法请看我们的焊接教程;如果你宁可把零件寄回来给我们,5 针焊接服务覆盖的正好就是这四个焊点。

接线之前先把底盘装好。亚克力底板和那几片小小的马达固定片,出厂时两面都贴着牛皮纸保护膜,看上去很像纸板 —— 请先掀起一角、把两面都撕干净,否则装完的车怎么看都像半成品。用固定片和长螺丝把两颗马达分别装在底板两侧,轮子压到轴上,万向轮装在另一端的下方,甲板上方留出位置给 Uno、驱动板和电池盒。

手机 App 到底是怎么控制这台车的?

蓝牙遥控 App 其实一点都不神秘。配对之后,HC-05 对手机来说就是一个普通串口,而 App 不过是一个把按键画成图标的键盘:按下向上箭头,它发出单个字节 F;松手,它发出 S。这条链路上再没有别的东西 —— 没有握手,没有报文格式,也不需要装任何协议库。

这也是我们让你自己去搜索、而不是指定某一款 App 的原因。在 Play Store 上搜 Arduino Bluetooth RC carBluetooth RC controller,都会出来一大排免费遥控器,而你要的那一款会写明支持 HC-05 或 HC-06 —— 为 Bluetooth Low Energy 写的 App 根本看不见 Classic 模块,这正是「线全接对了,车却毫无反应」最常见的原因。名字也不等于规格:好几款毫无关系的 App 用着几乎一样的标题,同一个箭头发出的字符却各不相同。所以先挑一款,再去弄清楚它实际发的是什么。这里没有任何一步需要从下载站拿 APK,而一款要求的权限超出蓝牙范围的遥控器,要的东西已经多过这份工作所需。

所以,任何一张命令对照表都不要照单全收,包括下面这一张 —— App 版本不同,字符就可能不同,而你自己的手机一分钟内就能给出答案。做这个测试时,只把 HC-05 的 VCC、GND 和 TXD 接到 Uno 的 5 V、GND 和 D2,电池开关保持关闭。Uno 的 5V 引脚就是那条已经稳压好的电源轨本身,而不是稳压器前面的输入口,所以你灌进去的电压是直接架在 USB 口正在供的电上的 —— USB 和 L298N 的 5 V 输出绝对不能同时接着。上传下面这份程序,把串口监视器开在 9600 baud,连上 App,然后逐个按键:

// 第一步:先搞清楚「你那一款」App 实际发的是什么。
// 上传这份程序,串口监视器开 9600,连上 App,逐个按键。
#include <SoftwareSerial.h>
SoftwareSerial bt(2, 3);   // 2 = Uno 的 RX,接 HC-05 的 TXD。3 保持不接。

void setup() {
  Serial.begin(9600);
  bt.begin(9600);
  Serial.println(F("Press a button in the app..."));
}

void loop() {
  if (bt.available()) {
    char c = bt.read();
    Serial.print(c);
    Serial.print(F("  (byte "));
    Serial.print((int)c);   // 顺便把数值也打出来,方便看清不可见字符
    Serial.println(F(")"));
  }
}

大多数免费遥控 App 用的都是下面这一套,也正是后面那份主程序所预期的:

App 上的按键 发出的字符 程序让车做什么
前进 / 后退 F / B 两颗马达同方向
左 / 右 L / R 两颗马达反方向 —— 原地打转
四个斜向 G I H J 一边驱动、一边空转 —— 画一道大弧线
按键松开 S 两个使能端都归零
调速滑杆 09q 设定下一个动作要用的 PWM 值
灯光、喇叭以及其他 别的字母 无法识别,所以程序会把车停住 —— 如果这挡了你的路,就给喇叭或灯光按键各配一个空的 case

请留意这背后的含义:在收到新指令之前,车会一直执行上一条命令,所以油门按着的时候手机一走出通信范围,车就会一路开下去。HC-05 的链路在空旷处大约十米,隔一堵墙或者隔着人体会明显更短,比大多数人动手前预想的近。套件那个船型开关串在电池线上 —— 那就是你的急停开关,它必须放在伸手就够得到的地方。

卡通链条图:深藏青色手机上一个青色箭头按键被按下,沿着青色无线电波发出一枚空白令牌,进入蓝绿色蓝牙模块,再传到红色马达驱动板,最后驱动两颗旋转的黄色马达
一次按键、一个字节、一种马达状态。整台车的原理,就是这条链条每秒重复很多遍。

HC-05 要怎么和 Android 手机配对?

HC-05 是使用串口协议(SPP)的 Bluetooth Classic 设备,所以它出现在 Android 系统的普通蓝牙设置里,而不是在 App 内部 —— 也因为 iOS 不把 Classic 串口开放给普通 App,这条路线只能走 Android。给车上电,打开设置,和名为 HC-05 的设备配对,输入配对码 1234 —— 少数批次出厂用的是 0000。配对完成之后,才打开遥控 App,从它自己的设备列表里选中这个模块。

配对和连接是两种不同的状态,把它们混为一谈,是装好的车一动不动最常见的原因。Android 会永久记住配对;而数据连接是 App 每次重新打开的。模块自己会告诉你它处在哪一种状态:等待时 LED 大约每秒闪两下,一旦有 App 占住了这个串口,就变成慢速的双闪。改名和 AT 模式的完整做法,请看我们的 HC-05 手机控制教程

HC-06 可以原样替换,什么都不用改:在一台从不主动发起连接的车上,「只能做从机」并不会有任何损失。把配对和第一次试跑完整看一遍会很有帮助:

这台车的 HC-05 需要分压电阻吗?

不需要 —— 一台只负责接收指令的车,HC-05 身上一颗电阻都不用加。不过原因比结论更重要,因为网上有一半的接线图都画了两颗这台车根本用不上的电阻。

HC-05 的射频核心工作在 3.3 V,所以它的 TXD 输出在 0 到 3.3 V 之间摆动,而它的 RXD 输入并不耐 5 V。这是两根不同的线、两个不同的问题。在这台车真正用到的那个方向上,问题只剩一个:3.3 V 能不能被 Uno 上的 ATmega328P 可靠地读成逻辑高电平。它的规格书保证高于 0.6 × VCC 即为高电平;在 5 V 的板子上这个门限是 3.0 V,而 3.3 V 高过了它 —— 而且余量足够,所以这是写在规格里的保证,不是运气好。

一台只驱动、不回话的车,永远不会往手机发东西,所以 Uno 的发送脚根本没有任务。让它空着,5 V 打进 3.3 V 的问题就从头到尾不存在:电源、地,加一根信号线。真正需要分压的时刻,是你希望车能回话的时候 —— 在 App 的终端页面上显示电池电压、回传传感器读数,或者进 AT 会话给模块改名。那时把 Uno 的 D3 经过一颗 1 kΩ 电阻接到 RXD,再从 RXD 用一颗 2 kΩ 接到 GND:这一对电阻把 5 V 按 2/(1+2) 的比例分掉,正好是 3.3 V,而程序里预留的 BT_TX_PIN 也就开始派上用场了。

千万不要图省事把模块改接到 0 号和 1 号引脚上。那是 Uno 的硬件串口,和 USB 芯片共用,所以接着 HC-05 时每一次上传都会被它抢线 —— 那个经典的 stk500_recv(): programmer is not responding 报错,害得不少好板子被退回来。把 SoftwareSerial 放在 D2 上,两场对话就互不干扰。

卡通图:蓝绿色 HC-05 蓝牙模块通过一根带箭头指向开发板的青色跳线,连到深藏青色 Arduino 开发板上,旁边还有一根灰色导线没有接,以及两颗闲置的蓝色电阻
一根线就承载了全部对话。回传线和那两颗电阻,可以一直留在袋子里,直到你想让车开口回话为止。

驱动板、模块和电池要怎么接?

两节 18650 电芯组成一个回路、引出两处取电点。它们在双槽电池盒里头尾相接,也就是串联:标称 7.4 V,充满 8.4 V。这组电压喂进 L298N 的 +12V 端子,桥会先留下大约 1.8 V 给自己,把约 5.6 V 交给额定 3–6 V 的马达;同一路输入经过板载稳压器,又变成给 Uno 和 HC-05 用的干净 5 V。套件自带的 4×AA 电池盒做不到这一点 —— 6 V 减掉桥的那一份只剩 4.2 V,车只会爬;而四槽 18650 电池盒更糟,因为多槽电池盒同样是把电芯串起来,四节会给桥送上 14.8 V,其中大约 13 V 会落到一颗 6 V 的马达上。这笔账在我们那篇避障小车里算得更细。

板上那颗小小的 5V-EN 跳线帽请留着,它的作用正是把稳压器接到输入端;同时请把 ENAENB 上的两个黑色跳线帽拔掉 —— 出厂状态下它们把两个使能端拉高,于是无论 App 的滑杆怎么推,车都只有一档速度。

接到 为什么
电池盒红线,经过套件那个船型开关 L298N +12V 螺丝端子 这组 7.4 V 同时喂桥和板载的 5 V 稳压器
电池盒黑线 L298N GND(中间那颗螺丝) 马达电流的回流路径
L298N +5V 螺丝端子 Uno 5V 引脚 供 Uno 和模块用电 —— 不必再加第二组电池
L298N GND Uno GND 没有共地,IN 引脚就没有参考电平
L298N OUT1 / OUT2 左边马达的两根线 那个轮子转反了就把这两根对调 —— 代码不用改
L298N OUT3 / OUT4 右边马达的两根线 另一侧同理
L298N IN1 / IN2 Uno D9 / D8 一高一低,决定左边马达的转向
L298N IN3 / IN4 Uno D7 / D6 右边马达的同一组
L298N ENA / ENB(跳线帽已拔) Uno D10 / D11 各自一侧的 PWM 转速
HC-05 VCC / GND Uno 5V / GND 模块自带的稳压器接受 3.6–6 V,5 V 是它常规的供电电压
HC-05 TXD Uno D2 唯一那根信号线:模块说,Uno 听
HC-05 RXDENSTATE 不接 在一台只驱动的车上用不到

Arduino 代码:一个字符进来,一种 H 桥状态出去

只需要 SoftwareSerial,而它本来就随 Arduino IDE 一起装好了。开发板选 Arduino Uno,用 A 转 B 数据线上传,然后先拔掉 USB 再打开电池开关 —— 反过来也一样,USB 插回去之前先把电池关掉,理由相同。这份程序编译出来 3,884 字节,大约是 Uno 闪存的 12%。它会把收到的每一个字符持续打印到串口监视器上,所以一台不听话的车可以把轮子架空,在桌面上诊断。

// 蓝牙遥控小车:Arduino Uno + HC-05 + L298N + 2WD 底盘。
// 手机 App 就是一个键盘。每按一个键只发出一个 ASCII 字符,
// 而这份程序就是那个把字符翻译成 H 桥状态的 switch-case。
// 换了 App,就把这些字母改成它实际发出的那些。

#include <SoftwareSerial.h>

// ---- 引脚 ----
const uint8_t BT_RX_PIN = 2;   // Uno 在这里接收 <- HC-05 的 TXD
const uint8_t BT_TX_PIN = 3;   // 库要求填这一支,但故意不接线

const uint8_t ENA_PIN = 10;    // 左侧转速      (PWM)
const uint8_t IN1_PIN = 9;     // 左侧方向
const uint8_t IN2_PIN = 8;
const uint8_t IN3_PIN = 7;     // 右侧方向
const uint8_t IN4_PIN = 6;
const uint8_t ENB_PIN = 11;    // 右侧转速      (PWM)

SoftwareSerial bt(BT_RX_PIN, BT_TX_PIN);

// ---- 你要调的几个数 ----
// 255 分之 140,大约就是在 7.4 V 电池组上、扣掉 L298N 自己那一份之后,
// 一颗 TT 马达能起步所需要的占空比。低于这个值,齿轮箱只会嗡嗡响而不转。
const uint8_t SPEED_FLOOR = 140;
const uint8_t SPEED_TOP   = 250;

// 没有两颗 TT 马达是一样的。把落后那一侧的微调值
// 每次抬高 5 个计数,直到车走得笔直。
const uint8_t TRIM_LEFT  = 0;
const uint8_t TRIM_RIGHT = 0;

uint8_t speedNow = 200;        // 由 App 的调速按键改写

// 一次调用同时设定两侧。速度给 0 表示那个轮子放开自由滚动,
// 这正是「画一道缓弧」和「原地打转」的区别所在。
void motors(bool leftFwd, uint8_t leftPwm, bool rightFwd, uint8_t rightPwm) {
  digitalWrite(IN1_PIN, leftFwd  ? HIGH : LOW);
  digitalWrite(IN2_PIN, leftFwd  ? LOW  : HIGH);
  digitalWrite(IN3_PIN, rightFwd ? HIGH : LOW);
  digitalWrite(IN4_PIN, rightFwd ? LOW  : HIGH);

  uint8_t l = (leftPwm  == 0) ? 0 : min(255, leftPwm  + TRIM_LEFT);
  uint8_t r = (rightPwm == 0) ? 0 : min(255, rightPwm + TRIM_RIGHT);
  analogWrite(ENA_PIN, l);
  analogWrite(ENB_PIN, r);
}

void halt() {
  analogWrite(ENA_PIN, 0);
  analogWrite(ENB_PIN, 0);
}

void setup() {
  pinMode(ENA_PIN, OUTPUT);
  pinMode(IN1_PIN, OUTPUT);
  pinMode(IN2_PIN, OUTPUT);
  pinMode(IN3_PIN, OUTPUT);
  pinMode(IN4_PIN, OUTPUT);
  pinMode(ENB_PIN, OUTPUT);
  halt();

  Serial.begin(9600);          // USB 这扇窗口,用来看清到底收到了什么
  bt.begin(9600);              // HC-05 出厂时的波特率
  Serial.println(F("Bluetooth RC car ready."));
}

void loop() {
  if (!bt.available()) {
    return;                    // 没有新东西:上一条命令继续生效
  }

  char c = bt.read();
  Serial.println(c);           // 这样你就能看到 App 究竟发了什么

  // 有些 App 会在每个按键后面附一个换行。忽略掉它,
  // 否则每发一条命令,一毫秒之后车就会停下。
  if (c == '\n' || c == '\r') {
    return;
  }

  // 调速按键:'0' 是还能用的最慢一档,'9' 最快,
  // 而 'q' 是 App 上的全速键。
  if (c >= '0' && c <= '9') {
    speedNow = map(c - '0', 0, 9, SPEED_FLOOR, SPEED_TOP);
    return;                    // 改速度不会重新启动马达
  }
  if (c == 'q') {
    speedNow = 255;
    return;
  }

  switch (c) {
    case 'F': motors(true,  speedNow, true,  speedNow); break;  // 前进
    case 'B': motors(false, speedNow, false, speedNow); break;  // 后退

    // 两侧反向,车就在原地打转。
    case 'L': motors(false, speedNow, true,  speedNow); break;  // 左转
    case 'R': motors(true,  speedNow, false, speedNow); break;  // 右转

    // 一边驱动、一边空转:画的是大弧线,而不是原地转身。
    case 'G': motors(true,  0,        true,  speedNow); break;  // 左前
    case 'I': motors(true,  speedNow, true,  0);        break;  // 右前
    case 'H': motors(false, 0,        false, speedNow); break;  // 左后
    case 'J': motors(false, speedNow, false, 0);        break;  // 右后

    case 'S': halt(); break;                                    // 停车

    // 任何我们不认识的东西 —— 一个灯光按键、线路噪声、
    // 另一款 App 的字符 —— 都表示「意图不明」,所以停车。
    default:  halt(); break;
  }
}

调速滑杆的低端为什么像是失灵了?

因为一个很低的 PWM 值不是「慢的 TT 马达」,而是一颗堵转的马达。PWM 把电源快速地通断,马达响应的是平均值:7.4 V 减掉桥拿走的 1.8 V,满占空比时绕组上大约是 5.6 V,那么半占空比就只剩大约 2.8 V。而一副 TT 齿轮箱要从静止挣脱自身的静摩擦,大约需要 3 V,也就是约 54% 的占空比 —— SPEED_FLOOR = 140 就是这么来的。车一旦已经在滚动,更低的值也能维持它跑;出问题的永远是起步,所以滑杆看起来一直好好的,直到你停下来为止。

走弧线而不是直线,是机械问题而不是程序 bug:两副注塑齿轮组的摩擦从来不会一样,所以同一个 PWM 值下两个轮子的转速略有差别。这正是 TRIM_LEFTTRIM_RIGHT 存在的意义 —— 把落后那一侧每次抬高 5 个计数,直到它走得笔直。关于这块驱动板上的 PWM,我们那篇 L298N 调速教程讲得更深。

四格卡通俯视图:同一台两轮机器人小车分别演示两轮同时向前、两轮同时向后、两轮反向原地打转,以及只有一个轮子驱动而画出一道大弧线
四次按键,四种轮子状态。同向前进,反向打转,一轮空转则画弧。

我们从真实客户那里常看到的错误

在 Android 设置里配对好了,却什么都没发生。配对只是交换了配对码;连接还得由 App 来打开。请在遥控 App 自己的设备列表里选中 HC-05,然后看着模块的 LED 从快闪变成慢速双闪。

串口监视器上字母都对,轮子却不动。信号和电源是两类不同的故障。检查 L298N 的 GND 是不是真的接到了 Uno 的 GND 上,以及 ENA 和 ENB 要么由 D10 和 D11 驱动,要么还戴着原来的跳线帽 —— 拔了帽子又不接线,那个使能端就是一个悬空的逻辑输入,既不是可靠的开,也不是可靠的关。

收到的是一堆乱码。模块和程序在波特率上没谈拢。一颗做过 AT 会话的模块可能被留在 38400,而 bt.begin(9600) 仍按 9600 去读,于是每个字节都在错误的时刻被采样。要么把程序改成一致,要么在 AT 模式下发一条 AT+UART=9600,0,0 把模块调回来。

跑了二十分钟都好好的,之后就不停掉线。L298N 那路 5 V 是线性稳压器,而线性稳压器需要输入比输出高出大约两伏。刚充满的电池组给它 8.4 V,余量很宽裕;随着电芯耗到 6 V 左右,这点余量就没了,5 V 电源轨跟着往下掉,于是马达还在转,Uno 和 HC-05 却在半路重启。怪 App 之前先把电芯充满,并且不要把它们放到每节 3.2 V 以下 —— 具体怎么安全地做,请看我们的 18650 充电教程

常见问题

HC-05 蓝牙模块的默认配对码是多少?

1234,少数批次是 0000。Android 只在配对时问一次,而且是在手机自己的蓝牙设置里问,不是在遥控 App 里。

遥控小车上的 HC-05 需要分压电阻吗?

只驱动的车不需要。接的只有 HC-05 的 TXD 到 Uno 的 D2,而 3.3 V 高过 ATmega328P 3.0 V 的高电平门限。等你为了回传数据或发 AT 指令而把 Uno 的 D3 接到 RXD 时,再加那对 1 kΩ/2 kΩ 分压电阻。

可以用 iPhone 控制 HC-05 小车吗?

不行。HC-05 用的是 Bluetooth Classic 串口,而 iOS 不把它开放给普通 App。要用 iPhone 控制,得换成 BLE 模块,再配一款为 BLE 写的 App。

我的车为什么只有全速一档?

ENA 和 ENB 上的跳线帽还没拔,两个使能端被永久拉高。把它们拔掉,再把那两支针接到 D10 和 D11,程序里的 PWM 才够得着它们。

做蓝牙遥控小车该用哪一款 Android App?

任何一款免费的 Bluetooth Classic 遥控器都行。在 Play Store 搜 Arduino Bluetooth RC car,挑一款说明里写明支持 HC-05 或 HC-06 的,因为为 Bluetooth Low Energy 写的 App 根本看不见这颗模块。标题在毫无关系的 App 之间会重复,而它们的字符表并不相同,所以请跑一遍上面那份监听程序,让 App 自己告诉你它发的是什么。

最后更新:2026年8月。有问题?WhatsApp 联系我们

Leave a Reply

Your email address will not be published. Required fields are marked *