经典三问
Q: HC-05 和 HC-06 是什么?
A: 它们是把 UART 字节流换成经典蓝牙无线链路的模块,两边各接一段,中间那段看不见。
Q: 为什么要把 CH340 换成蓝牙?
A: CH340 让小车连在电脑上,蓝牙让小车离开桌面------手里的手机就是那个上位机。
Q: 串口蓝牙常见使用场景?
A: 手机遥控、无线调参、现场看日志,以及任何"主机和小车之间不该有根线"的场合。
桌面上调串口的时候,CH340 很好用:一根 USB 线插进电脑,VOFA+ 打开,PA9 和 PA10 接好,发一串十六进制,单片机马上回包。
小车放到地上以后,这根 USB 线就变成了拖车绳。车一动,电脑、线和电池一起被拽着走,手还得防着线缠进轮子。要做一台真的会跑的小车,这一根线迟早得去掉。
去掉它的代价比想象中小。上一回那个 AA 55 ... SUM8 0D 的帧一个字节都不用改,手机那边发的还是同样的一串十六进制------换掉的只是"谁把字节送到 USART1"。
串口不是一个东西,是一种约定
这句话得先想清楚,因为后面几乎没有代码可写。
USART1 并不是一条通向电脑的路。它只是 PA9 和 PA10 两个引脚上的一套约定:高低电平怎么表示一位、一帧有多少位、每秒多少位。真正"通向哪里",取决于这两个引脚外面接了什么。
- 接 CH340:这两个引脚通向电脑的 USB 口;
- 接 HC-05 或 HC-06:这两个引脚通向空气,另一头是手机。
于是同一个串口有两种接法,程序看到的却是同一段东西:
| 手里接上的东西 | 实际上它是什么 |
|---|---|
| CH340 | USB ↔ 串口的转换器 |
| HC-05 / HC-06 | 蓝牙(SPP)↔ 串口的转换器 |
蓝牙的 profile 里,SPP 这个名字其实很直白:Serial Port Profile,就是"假装自己是一根串口线"。模块收下手机发来的无线数据,转身从 TXD 送到 PA10;STM32 从 PA9 发出去的字节,被它变成无线数据送回手机。中间飞在天上的那一段,程序完全看不见。
所以 CH340 和 HC-05 在程序眼里是同一个东西:一条 UART 字节流。协议还是上一回那套帧头、长度、校验和状态机,业务层还是那六个命令,一行都不用改。
接线:四根线,和三个必须先想清楚的问题
text
蓝牙模块 VCC -> 5V
蓝牙模块 GND -> GND
蓝牙模块 TXD -> STM32 PA10(USART1_RX)
蓝牙模块 RXD -> STM32 PA9 (USART1_TX)
TX 和 RX 要交叉。模块发出的 TX,应该落到单片机的 RX;两边都接 TX,就是两个人都在讲话,谁也不听。
第一个问题是供电。 小车上的电是这条链:12V 电池 → 电源开关 → 降压/电源分配模块 → 5V、3.3V 这些输出,全线共地。蓝牙模块的 VCC 就从这条线上取。
底板一般自带稳压,VCC 接 5V 是常见做法;也有只吃 3.3V 的模块。按你手上那块板的说明来,别把别人"接 5V 能用"当成所有模块都行。
要注意的是电流:蓝牙模块平时只有几毫安,但发射那一下可以到几十毫安。它和电机共用一路 5V 时,电机启动的瞬间可能把这一路电压拽下去,模块先复位------这正是后面第四个实验要抓的现场(电流电压这东西有时候很"玄学",不一定会出现这种情况)。
第二个问题是电平。 STM32 是 3.3V 逻辑,模块的 TXD 输出一般也是 3.3V,这个方向安全。反过来 RXD 这一端能不能接受更高的电平,取决于底板有没有做分压或电平转换:常见底板直接接 3.3V 就行,有些带分压的可以接 5V 串口,裸封装的 RXD 看手册再决定是否直接喂 5V。同样是查说明,不靠猜。(虽然我直接买的模块好像没遇到过不能接的)
第三个问题是共地。 蓝牙模块的高低电平和 STM32 必须是同一个参照;只接 VCC、TX、RX 不接 GND,模块的灯可能照常亮,通信却像在水里喊话。
HC-05 和 HC-06 的差别
先记一句话:HC-05 通常可以配置成主机或从机,HC-06 通常只做从机,等着被连。
| HC-05 | HC-06 | |
|---|---|---|
| 角色 | 可设主/从 | 一般只做从机 |
| 配置方式 | AT 指令,功能齐全 | AT 指令,项少;不同固件写法差别大 |
| 进入配置 | 一般靠 KEY/EN 按键在上电时按住 |
一般没有 KEY,未被连接时直接用 AT |
| 谁连谁 | 既能连手机,也能连另一个 HC-05 | 等手机或一个主机来连 |
| 适合谁 | 想研究主从、想模块对模块 | 只想让手机连上小车 |
不同厂家底板的固件改过名字、回显和参数格式,所以AT 指令不能只背一张网上的表 。先做三件事:记下模块上电时的灯光节奏、查商品页面写的默认波特率/名称/密码、用当前串口参数发一句 AT 看它回不回 OK。
手机端也有一个平台差别要先说清楚:iPhone 用不了 HC-05/HC-06。 iOS 不开放经典蓝牙的 SPP,只开放 BLE;这两个模块走的是 SPP。所以这次的实验请用安卓手机配一个"蓝牙串口"类 App,苹果设备要另想办法(MFi 认证模块,或者换 BLE 方案)。这一步没有绕过去的办法,先确认手里的手机能用,再往下做。
AT 模式:让模块先学会我们的参数
AT 模式不是"信号更强的模式"。它是模块暂时不去连手机,把收到的 UART 字节当成配置命令来读。
进入方式各板不同,但可复现的流程是这样的:
text
断电
-> 按说明按住 KEY/板载按键(HC-05 常见做法)
-> 上电,观察指示灯节奏
-> 用 CH340 接模块的 RXD/TXD(仍然是交叉接)
-> 发 AT,确认回 OK
-> 一次只改一个参数
-> 退出 AT,重新上电
-> 再用手机搜索和连接
配置时 CH340 仍然有用,只不过它这时的身份不是小车的链路,而是模块的配置工具。配置完再把 HC-05/HC-06 接回 STM32。
常用指令大致是这些,具体写法以手上固件的说明为准:
| 指令 | 想做什么 | 注意 |
|---|---|---|
AT |
确认模块在听 | 回 OK 才算进了 AT 模式 |
AT+NAME=ZW-CAR |
改蓝牙名称 | 有些固件是 AT+NAMEZW-CAR |
AT+PSWD=1234 |
改配对密码 | 默认常见 1234 或 0000 |
AT+UART=115200,0,0 |
改通信波特率等参数 | HC-06 上常见的是 AT+BAUD8(8 对应 115200) |
AT+ROLE=1 |
设成主机(HC-05) | 设完要重新上电 |
AT+RESET |
让模块重启 | 参数生效的常用办法 |
命令没回 OK 的时候,别连着换十个命令试。把三件事分开确认:
- 是不是真在 AT 模式------灯光的节奏和正常等待连接时不一样;
- 当前波特率是多少------AT 模式下的波特率常常和通信波特率不同,很多固件在 AT 模式用 38400,通信时另设一个;CH340 那头的参数要跟着改;
- 要不要回车换行 ------有的固件要求每条命令后面跟
\r\n,有的不要求。
还有一件事必须对齐:模块的通信波特率要和 STM32 的一致。 这条链路我们从一开始就定在 115200,而模块出厂常见的通信波特率是 9600------网上很多蓝牙例程的串口参数写成 9600,就是在对模块的默认值。你有两个选择:把模块改成 115200,或者把那份串口初始化 Uart_Init() 里的 115200 改成 9600。两边一致就行,但别让它们各自为政,那会表现成一连上就收到满屏乱码。
代码:一行都不用改
这里最省事。现在的主循环长这样(上一回为了把搬字节的活交给 DMA,把环形缓冲区换成了两块缓冲;Key_Tick() 还在 1ms 中断里,Mode_Loop() 还在主循环里):
c
static uint8_t ready_index;
static uint16_t ready_length;
static uint16_t i;
while (1)
{
Mode_Loop();
if (rx_ready != 0U)
{
ready_index = rx_ready_index;
ready_length = rx_block_length;
rx_ready = 0U;
for (i = 0U; i < ready_length; i++)
{
Protocol_Service(rx_block[ready_index][i]);
}
}
}
不管这些字节是 DMA 从 USART1->DR 搬过来的,还是从中断里一个个人工搬进来的,Protocol_Service() 收到的永远只是"一个字节"。它照着上一回那套状态机找 AA 55、读长度、算 SUM8,既不知道也不需要知道这些字节刚才坐的是 USB 还是无线电。
业务层同样如此。上一回那个分发函数的前两个分支(speed_percent 是上一回那个文件作用域的默认速度,static int16_t speed_percent = 30;):
c
static uint8_t CarCommand_Execute(uint8_t command, const uint8_t *payload, uint8_t length)
{
switch (command)
{
case CAR_CMD_STOP:
Motor_Stop();
LED_WriteOne(0, 0U);
return 0U;
case CAR_CMD_FORWARD:
Motor_SetTargetPercent(speed_percent, speed_percent);
LED_WriteOne(0, 1U);
return 0U;
...
Motor_SetTargetPercent() 给的是左右轮的百分比,负数是反转;Motor_Stop() 把两路都停掉;LED_WriteOne(0, 1U) 是把第 0 颗外接灯写成亮。它们都不知道命令是从哪来的。它们只知道"有人让我停"或者"有人让我往前走"。
换链路不改协议,改协议不动业务------前面几篇一层一层分开,为的就是现在这一次不折腾。
手机 App 里发什么
手机上的蓝牙串口 App 必须有一个本事:发送十六进制 。只能发文字的 App 做不了这件事------把 AA 55 ... 当文字复制过去,发出去的是字符 'A'、'A'、空格和数字,单片机收到的字节完全是另一回事。
能发十六进制的话,直接用手上的帧。这几条最常用:
text
停止 AA 55 01 01 01 00 01 04 0D
前进 AA 55 01 01 02 00 01 05 0D
设速度 100 AA 55 01 01 06 01 01 64 6E 0D
发出去以后,板子会回一条应答,例如设速度那条对应:
text
AA 55 01 81 06 01 01 00 8A 0D
校验和的算法没变:从设备 ID 一直加到最后一个内容字节,只留低 8 位。想改速度,就把内容那个字节换成你要的百分比,再把校验和重算一遍。
先让手机找到它
手机蓝牙设置里搜不到设备,是最容易把人带进死胡同的一关。先把问题拆成几段:
text
灯不亮 -> 先查 VCC/GND、底板稳压和供电电流
灯快闪但搜不到 -> 模块没在可发现状态,或已经被别的设备连着
能搜到但配对失败 -> 密码不对、手机缓存了旧配对记录、模块角色不对
刚连上马上断开 -> 供电掉压、模块复位、手机 App 主动关掉了连接
连上但数据乱码 -> 两边波特率、校验位、停止位不一致
能连上、发帧没反应 -> App 发的不是十六进制,或者模块没接到 PA9/PA10
"自动断开"不一定怪蓝牙协议。电机启动的电流变化可能让 5V 这一路瞬间下陷,蓝牙模块先重启,手机看到的就是断开。把电机供电和蓝牙逻辑供电分开观察,或者先拆掉电机只做桌面收发,很快就能把供电问题和协议问题分开。
五个现场实验
实验一:CH340 和蓝牙交替接入
同一份程序,只换链路:
- CH340 接
PA9/PA10,电脑发一条速度帧; - 断电,拔掉 CH340;
- HC-05/HC-06 按 TX/RX 交叉接回,手机发同一条帧。
屏幕上的正确帧数、错误帧数和最后命令,应该以同样的方式变化。变的只有"谁把字节送到 USART1"。
实验二:电脑端蓝牙配对,VOFA+ 照旧看曲线
这一条绕开了手机 App 的限制:让电脑来当那个主机。
在 Windows 里和 HC-05 配对(密码就是模块的配对密码)。配对完成后,设备管理器里通常会出现两个"标准串行 over 蓝牙链接"的 COM 口------一个用来连接,一个不是,选那个能连上的。然后在 VOFA+ 里选这个 COM 口,参数和模块的波特率对齐。
曲线会照旧出来。这件事说明的是:VOFA+ 只认 COM 口,它不关心这个 COM 口背后是 USB 线还是蓝牙。 上一回把数据送上曲线的那些函数,此处一个字都没变。
实验三:故意改 STM32 波特率
模块设成 115200,把工程临时改成 9600。手机仍然显示"已连接",但收到的会是乱码,二进制帧也过不了校验。改回 115200、重新烧录,再发一条停止帧。
蓝牙连接状态和 UART 数据状态是两件事。 手机显示连接成功,只说明无线链路建立了,不代表两边的串口参数已经一致。
实验四:电机启动造成的断开
先只接蓝牙模块和 STM32,连续发停止、设速度、停止,看连接稳不稳。再接上电机驱动,让电机在架空状态下启动,继续发同样的帧。
如果出现"电机一启动手机就断",用万用表量蓝牙供电那一点,先把电机供电、降压模块容量、地线和电源电容查清楚。不要先去改协议状态机------它根本没有机会收到下一个字节。
实验五:距离、遮挡和墙
把小车放在桌上,手机慢慢远离,同时每隔一段距离发一条停止帧,看屏幕上的错误帧数什么时候开始涨。再试试隔着墙、或者把手机放到身体另一侧。
经典蓝牙的标称距离是"理想条件"下的数字,实际取决于天线摆放、遮挡、2.4G 频段的拥挤程度,以及模块本身的发射功率。这个实验不给你一个精确的距离,它给你的是一件事:你在多远的范围内还能确定车会停下来。 遥控小车时,这个范围才是真正要记住的数字。
落地时的命令顺序
真正让车跑起来时,我会把停止帧放在手边。先架空,发前进帧看轮子方向对不对;再放到地面,速度从很小的值开始。手机 App 里把停止帧设成一个明显的快捷按钮,别让停止命令藏在十几行历史记录里。
命令和两个轮的对应关系还是上一回那六个:停止、前进、后退、左转、右转、设速度。转向靠"一轮慢、一轮快",实际占空比要按驱动板、电机、轮胎和地面重新试------低于某个占空比,电机在地面上根本克服不了静摩擦,同一个命令在架空和落地时不会给出同一种结果。
最后橘猫说
把这几层分开看,换模块这件事才不吓人:
text
配对、名称、密码、AT 配置 -> 模块自己管
波特率、校验位、停止位 -> UART 这一层管
帧头、长度、SUM8、重同步 -> 协议这一层管
前进、停止、差速、PID -> 小车业务这一层管
HC-05/HC-06 没有让串口消失:它们只替换了"CH340 到电脑"这一段物理链路。所以把 SUM8 升级成 CRC16 不用重学 AT 指令,换一块蓝牙模块也不用重写电机代码。
同一条链路上还有别的无线方案,但它们接的不是同一层:有的是 SPI 接口的点对点模块,有自己的包格式,不经过串口;有的是 Wi-Fi 或 LoRa,各自解决的是距离、带宽和功耗的不同组合。蓝牙在这里的位置很清楚------它负责把一段串口线变短成空气,其余的都不归它管。
最后是这类问题里最常见的两个,先答在这里:
- "我有两个模块,能不能不用手机,让它们自己对连?" 可以,但要配对角色:两个 HC-05 里得有一个设成主机(
AT+ROLE=1)并绑定对方的地址;一个 HC-06 只能当从机,等一个主机来连它。绑定地址的具体写法各固件不同,按说明来。 - "连上了,但单片机好像什么都没收到?" 按顺序查三件事:App 发的是不是十六进制、模块的
TXD是不是真的接到了PA10、两边的波特率是不是一致。这三件事占掉这类问题的大多数。