一台蓝牙遥控小车,就是一个 2WD 底盘、一块 L298N 驱动板,再加上一颗把手机按键交给 Arduino 的 HC-05。免费的遥控 App 里每按一个键,只发出一个 ASCII 字符 —— 「F」是前进,「S」是停 —— 而整份程序就是一个 switch-case,把每一个字符翻译成一种 H 桥状态。想通这一件事,任何一款蓝牙 App 都能开动你的车 —— 前提是用 Android 手机,因为 HC-05 讲的是 Bluetooth Classic,而 iOS 并不把它开放给普通 App。
做一台蓝牙遥控小车需要哪些零件?
2WD 底盘套件提供的是机械部分 —— 亚克力底板、两颗 TT 减速马达、轮子、一个万向轮、螺丝,以及一个船型开关。HC-05 是这台车和我们那篇避障小车之间唯一的区别:同一副底盘、同一块驱动板、同一组电池,只是换了一样东西来告诉它往哪走。
材料清单 —— Arduino + HC-05 蓝牙遥控小车
Parts list
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 car 或 Bluetooth 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 |
两个使能端都归零 |
| 调速滑杆 | 0–9、q |
设定下一个动作要用的 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 上,两场对话就互不干扰。

驱动板、模块和电池要怎么接?
两节 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 跳线帽请留着,它的作用正是把稳压器接到输入端;同时请把 ENA 和 ENB 上的两个黑色跳线帽拔掉 —— 出厂状态下它们把两个使能端拉高,于是无论 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 RXD、EN、STATE | 不接 | 在一台只驱动的车上用不到 |
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_LEFT 和 TRIM_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 联系我们。



Wireless Bluetooth Module HC-05 HC-06 Master Slave Bluetooth Module HC05 HC06 - HC-05 BLUETOOTH MODULE
L298N Motor Driver Module Arduino Compatible Dual H Bridge Dual DC Motor Control Arduino Robotic Car
2WD 4WD Smart Car Chassis Single Layer Acrylic Arduino Smart Car Chassis Kit With Motor - 2WD CAR KIT
Arduino Compatible UNO R3 FT232 Development Board Arduino Compatible CH340 Alternative
Data Cable Type-A Type-C MicroUSB Type-B 0.5m 1m 30cm 0.3m 100cm Data Transfer Upload Code - TYPE-A TO TYPE-B (1.0M)
18650 Battery Holder 1/2/3/4 Slot 3.7V Battery Cell Housing Battery Case Battery Casing - 18650 BATTERY HOLDER (2)
3.7V 18650 Battery Rechargeable Li-Ion Battery 1200mAh 2200mAh 3200mAh Flat Top - 18650 Li-Ion Battery (2200mAh)
40pcs Dupont Wire 10cm 20cm 30cm for Breadboard DIY Experiment Jumper Wire Breadboard wire - DUPONT WIRE M-F 20CM
40pcs Dupont Wire 10cm 20cm 30cm for Breadboard DIY Experiment Jumper Wire Breadboard wire - DUPONT WIRE M-M 20CM
TP4056 18650 Battery Charger Module Lithium Ion Battery Li-ion Battery Charger Type C Micro USB - TP4056 CHARGER (TYPEC)
Soldering Service for Circuit Boards & Electronics - 5 Pins Soldering Service
400 pcs 1/4W Resistor Pack Resistor Kit 20 Common Value with 20 each