从 CH340 到 HC-05 和 HC-06:让小车脱离电脑

经典三问

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 字节流。协议还是上一回那套帧头、长度、校验和状态机,业务层还是那六个命令,一行都不用改。

接线:四根线,和三个必须先想清楚的问题

蓝牙模块 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 字节当成配置命令来读。

进入方式各板不同,但可复现的流程是这样的:

断电
 -> 按说明按住 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 的时候,别连着换十个命令试。把三件事分开确认:

  1. 是不是真在 AT 模式——灯光的节奏和正常等待连接时不一样;
  2. 当前波特率是多少——AT 模式下的波特率常常和通信波特率不同,很多固件在 AT 模式用 38400,通信时另设一个;CH340 那头的参数要跟着改;
  3. 要不要回车换行——有的固件要求每条命令后面跟 \r\n,有的不要求。

还有一件事必须对齐:模块的通信波特率要和 STM32 的一致。 这条链路我们从一开始就定在 115200,而模块出厂常见的通信波特率是 9600——网上很多蓝牙例程的串口参数写成 9600,就是在对模块的默认值。你有两个选择:把模块改成 115200,或者把那份串口初始化 Uart_Init() 里的 115200 改成 9600。两边一致就行,但别让它们各自为政,那会表现成一连上就收到满屏乱码。

代码:一行都不用改

这里最省事。现在的主循环长这样(上一回为了把搬字节的活交给 DMA,把环形缓冲区换成了两块缓冲;Key_Tick() 还在 1ms 中断里,Mode_Loop() 还在主循环里):

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;):

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'、空格和数字,单片机收到的字节完全是另一回事。

能发十六进制的话,直接用手上的帧。这几条最常用:

停止        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

发出去以后,板子会回一条应答,例如设速度那条对应:

AA 55 01 81 06 01 01 00 8A 0D

校验和的算法没变:从设备 ID 一直加到最后一个内容字节,只留低 8 位。想改速度,就把内容那个字节换成你要的百分比,再把校验和重算一遍。

先让手机找到它

手机蓝牙设置里搜不到设备,是最容易把人带进死胡同的一关。先把问题拆成几段:

灯不亮              -> 先查 VCC/GND、底板稳压和供电电流
灯快闪但搜不到      -> 模块没在可发现状态,或已经被别的设备连着
能搜到但配对失败    -> 密码不对、手机缓存了旧配对记录、模块角色不对
刚连上马上断开      -> 供电掉压、模块复位、手机 App 主动关掉了连接
连上但数据乱码      -> 两边波特率、校验位、停止位不一致
能连上、发帧没反应  -> App 发的不是十六进制,或者模块没接到 PA9/PA10

“自动断开”不一定怪蓝牙协议。电机启动的电流变化可能让 5V 这一路瞬间下陷,蓝牙模块先重启,手机看到的就是断开。把电机供电和蓝牙逻辑供电分开观察,或者先拆掉电机只做桌面收发,很快就能把供电问题和协议问题分开。

五个现场实验

实验一:CH340 和蓝牙交替接入

同一份程序,只换链路:

  1. CH340 接 PA9/PA10,电脑发一条速度帧;
  2. 断电,拔掉 CH340;
  3. 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 里把停止帧设成一个明显的快捷按钮,别让停止命令藏在十几行历史记录里。

命令和两个轮的对应关系还是上一回那六个:停止、前进、后退、左转、右转、设速度。转向靠“一轮慢、一轮快”,实际占空比要按驱动板、电机、轮胎和地面重新试——低于某个占空比,电机在地面上根本克服不了静摩擦,同一个命令在架空和落地时不会给出同一种结果。

最后橘猫说

把这几层分开看,换模块这件事才不吓人:

配对、名称、密码、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、两边的波特率是不是一致。这三件事占掉这类问题的大多数。
posted @ 2026-11-04 13:23  Zw-awa  阅读(113)  评论(0)    收藏  举报