文章目录
-
- 一、为什么必须"分时复用"
-
- [1.1 硬件只有一组引脚](#1.1 硬件只有一组引脚)
- [1.2 三条铁律](#1.2 三条铁律)
- [1.3 软件状态怎么表示](#1.3 软件状态怎么表示)
- 二、ESP-12F:先把硬件搞对
-
- [2.1 它不是"插上就能用"的模块](#2.1 它不是"插上就能用"的模块)
- [2.2 供电:最容易"看起来正常"的坑](#2.2 供电:最容易"看起来正常"的坑)
- [2.3 三个启动引脚](#2.3 三个启动引脚)
- [2.4 上电日志是 74880,不是 115200](#2.4 上电日志是 74880,不是 115200)
- [2.5 一条必须记住的时序](#2.5 一条必须记住的时序)
- [三、进入 MQTT 的完整路径](#三、进入 MQTT 的完整路径)
-
- [3.1 按键链路](#3.1 按键链路)
- [3.2 主循环里的切换代码](#3.2 主循环里的切换代码)
- [3.3 四处"顺序不能反"](#3.3 四处"顺序不能反")
- [3.4 别被日志里的波特率误导](#3.4 别被日志里的波特率误导)
- [四、初始化全景:11 步,一步都不能少](#四、初始化全景:11 步,一步都不能少)
-
- [4.1 总表](#4.1 总表)
- [4.2 代码骨架](#4.2 代码骨架)
- [4.3 为什么必须"试发一条"](#4.3 为什么必须"试发一条")
- 五、`Esp_Probe`:先探活,再谈别的
-
- [5.1 四步策略](#5.1 四步策略)
- [5.2 结论怎么读](#5.2 结论怎么读)
- [5.3 探测失败时为啥还要挂中断](#5.3 探测失败时为啥还要挂中断)
- [六、`Esp_Cmd_Expect`:把"收满 64 字节"换成"逐字节匹配"](#六、
Esp_Cmd_Expect:把"收满 64 字节"换成"逐字节匹配") -
- [6.1 错误写法与它的三个死法](#6.1 错误写法与它的三个死法)
- [6.2 正确写法:边收边匹配](#6.2 正确写法:边收边匹配)
- [6.3 四个必须记住的细节](#6.3 四个必须记住的细节)
- [6.4 超时时间表:每个数字都有来历](#6.4 超时时间表:每个数字都有来历)
- [七、`Esp_Rx_Poll`:绕过 HAL 直接读寄存器](#七、
Esp_Rx_Poll:绕过 HAL 直接读寄存器) -
- [7.1 为什么不用 `HAL_UART_Receive`](#7.1 为什么不用
HAL_UART_Receive) - [7.2 `Esp_Flush` 为什么要"先读 SR 再读 DR"](#7.2
Esp_Flush为什么要"先读 SR 再读 DR") - [7.3 打印应答时要把 `\r\n` 转义](#7.3 打印应答时要把
\r\n转义)
- [7.1 为什么不用 `HAL_UART_Receive`](#7.1 为什么不用
- 八、`AT+MQTTCLEAN`:本章最隐蔽的一个坑
-
- [8.1 症状](#8.1 症状)
- [8.2 原因:ESP 把配置存进了 flash](#8.2 原因:ESP 把配置存进了 flash)
- [8.3 为什么 `AT+MQTTDISCONN` 不够](#8.3 为什么
AT+MQTTDISCONN不够) - [8.4 万一还是被拒:`skip_conn` 兜底](#8.4 万一还是被拒:
skip_conn兜底)
- [九、MQTT 参数与主题:每个数字都别乱改](#九、MQTT 参数与主题:每个数字都别乱改)
-
- [9.1 `AT+MQTTUSERCFG` 八个参数](#9.1
AT+MQTTUSERCFG八个参数) - [9.2 `AT+MQTTCONN` 与自动重连](#9.2
AT+MQTTCONN与自动重连) - [9.3 两个主题:为什么必须分开](#9.3 两个主题:为什么必须分开)
- [9.4 ClientID 冲突:两台设备互踢](#9.4 ClientID 冲突:两台设备互踢)
- [9.5 订阅](#9.5 订阅)
- [9.1 `AT+MQTTUSERCFG` 八个参数](#9.1
- [十、发布:为什么必须用 `AT+MQTTPUBRAW`](#十、发布:为什么必须用
AT+MQTTPUBRAW) -
- [10.1 直接 `AT+MQTTPUB` 会失败](#10.1 直接
AT+MQTTPUB会失败) - [10.2 `PUBRAW` 的三段式握手](#10.2
PUBRAW的三段式握手) - [10.3 四个关键点](#10.3 四个关键点)
- [10.4 关中断的副作用(要知道)](#10.4 关中断的副作用(要知道))
- [10.1 直接 `AT+MQTTPUB` 会失败](#10.1 直接
- [十一、接收:环形缓冲 + 按行解析](#十一、接收:环形缓冲 + 按行解析)
-
- [11.1 中断只做一件事](#11.1 中断只做一件事)
- [11.2 主循环按行解析](#11.2 主循环按行解析)
- [11.3 订阅消息怎么从行里抠出来](#11.3 订阅消息怎么从行里抠出来)
- [11.4 ESP 重启检测:一条最容易被忽略的逻辑](#11.4 ESP 重启检测:一条最容易被忽略的逻辑)
- 十二、和主循环怎么接
-
- [12.1 命令执行](#12.1 命令执行)
- [12.2 两条通道的对照](#12.2 两条通道的对照)
- 十三、一次完整的联调日志(照着对)
-
- [13.1 切到 MQTT](#13.1 切到 MQTT)
- [13.2 运行中](#13.2 运行中)
- [13.3 H5 端看到的](#13.3 H5 端看到的)
- 十四、调不通:按这个顺序查
-
- [14.1 初始化阶段](#14.1 初始化阶段)
- [14.2 发布阶段](#14.2 发布阶段)
- [14.3 收不到 H5 命令](#14.3 收不到 H5 命令)
- 十五、本章易错点总结
- 十六、小结与下一篇
- [附录 A:本章完整代码](#附录 A:本章完整代码)
-
- [A.1 环形缓冲与取字节](#A.1 环形缓冲与取字节)
- [A.2 直接读寄存器的轮询](#A.2 直接读寄存器的轮询)
- [A.3 逐字节增量匹配](#A.3 逐字节增量匹配)
- [A.4 探活](#A.4 探活)
- [A.5 MQTT 初始化主流程](#A.5 MQTT 初始化主流程)
- [A.6 发布(运行期)](#A.6 发布(运行期))
- [A.7 最小验证:五步确认 MQTT 通](#A.7 最小验证:五步确认 MQTT 通)
系列第十二篇。前面十一章铺的所有地基,都是为这一章服务:一组串口引脚、两个模块、两种波特率、一套"一问一答"的 AT 协议。
这一章我卡了整整三天。最难受的不是"连不上",而是每一步都回 OK,最后发布却永远 ERROR:
textsetEchoOff : OK!!! setWifiMode:OK!!! connectWifi : OK!!! setMqttPara : OK!!! MqttConn : OK!!! MqttSub : OK!!! [PUB] FAILED后来才知道:ESP 上电后自己就已经把 broker 连上了 ,LinkID 0 被它占着,我再发
AT+MQTTUSERCFG=0,...只能回 ERROR;而"回 OK"的那几条,是它沿用旧会话在敷衍我。这一篇会把整条链路的每一步、每个超时数字、每个"看起来成功其实没成功"的地方都拆开讲。
一、为什么必须"分时复用"
1.1 硬件只有一组引脚
STM32F411 的 USART2 固定在 PA2(TX)/ PA3(RX),不能重映射到别处。而工程里有两个串口模块:
| 模块 | 用途 | 波特率 |
|---|---|---|
| HC-06 蓝牙 | 手机近场控制(第 11 章) | 9600 |
| ESP-12F(ESP8266 + AT 固件) | WiFi → MQTT → H5 远程控制 | 115200 |
解决办法只有两条,缺一不可:
- 硬件:双刀双掷拨码开关,把 PA2/PA3 整体切到某一个模块(第 3 章 5.2 节);
- 软件:进 MQTT 时把 USART2 改成 115200,回蓝牙时改回 9600。
1.2 三条铁律
- 软件读不到拨码开关。 没有任何一根 GPIO 接在开关上,所以"硬件在哪一侧"必须靠人记住,并且和
g_net保持一致。 - 切换必须成对。 只切 PA2 不切 PA3,两个模块的 TX 会同时驱动同一根线,现象是随机乱码------比完全不通更难查。
- 切换顺序固定:先断电 / 先停收发 → 拨开关 → 再上电。 带电热插拔容易在切换瞬间产生电平对打。
后面所有"MQTT 初始化全 FAIL",第一个要查的就是拨码开关。这不是套路话,是命中率最高的一条。
1.3 软件状态怎么表示
c
#define NET_BT 0 // 蓝牙:9600
#define NET_MQTT 1 // WiFi/ESP-MQTT:115200
volatile uint8_t g_net = NET_BT; // 当前协议(中断和主循环都会读)
volatile uint8_t g_net_req = NET_REQ_NONE; // 中断置的切换请求,主循环执行
volatile uint8_t g_mqtt_inited = 0; // 1=已完整初始化过一次
volatile uint8_t g_net_busy = 0; // 1=正在连 WiFi/broker,KEY4 请求丢弃
volatile uint8_t mqtt_ready = 0; // 1=会话真正可用(mqtt.c 内部)
四个标志各管一件事,别混:
| 标志 | 谁写 | 含义 |
|---|---|---|
g_net |
主循环 | 当前走哪个协议(决定中断把字节送进哪个缓冲) |
g_net_req |
中断写、主循环清 | "请切换"的请求,避免在中断里做耗时操作 |
g_mqtt_inited |
主循环 | 是否已经成功走过一遍初始化(失败要能重来) |
mqtt_ready |
mqtt.c | 会话现在是否可用(ESP 重启后要清 0) |
二、ESP-12F:先把硬件搞对
2.1 它不是"插上就能用"的模块
ESP-12F 是裸 ESP8266 模组,买到手大致有三种情况,你只能上电试:
| 情况 | 表现 |
|---|---|
| 出厂带较新的 AT 固件 | 115200 发 AT 回 OK,能直接做本章实验 |
| 带很老的 AT 固件 | 回 OK,但 AT+MQTTUSERCFG 等指令不支持(回 ERROR) |
| 空片 / 烧的是别的固件 | 发 AT 一个字都不回 |
本工程是在 ESP-AT v2.2.0 上验证的。指令集不同的版本,本章的 AT 指令会大面积 FAIL。
三条路:
- 买标注 "ESP-AT 固件" 且版本够新的模组;
- 自己用 USB 转 TTL 烧 AT 固件(本章不展开);
- 先发
AT+GMR看版本号,低于 v2.x 就别硬试了。
怎么判断固件在不在 :上电后 115200 发 AT,回 OK 就是有;一个字都不回,先怀疑固件或波特率。
2.2 供电:最容易"看起来正常"的坑
ESP8266 发射瞬间电流能冲到 300mA 以上,平均也有 70~80mA。
| 现象 | 原因 |
|---|---|
一联网就复位 / 串口打印 rst cause:2 |
供电电流不够,电压跌到 3.0V 以下 |
| 平时能通、一发数据就掉线 | 同上,用 USB 转 TTL 的 3.3V 直接带不动 |
所以:ESP-12F 的 VCC 必须用独立、能出 500mA 的 3.3V ,并且和 STM32 共地。别用 STM32 板上的 3.3V 排针硬带,也别用 USB 转 TTL 的 3.3V 输出。
2.3 三个启动引脚
| 引脚 | 正常工作时 | 说明 |
|---|---|---|
| EN(CH_PD) | 高(3.3V) | 拉低就停在上电复位状态,模块"死"了 |
| GPIO15 | 低(GND) | 拉高会进别的启动模式,AT 固件跑不起来 |
| GPIO0 | 高(悬空/上拉) | 拉低是"下载模式",不是运行模式 |
裸模块必须自己加:EN 上拉到 3.3V(10k)、GPIO15 下拉到 GND(10k)。买带底板的模组通常已经处理好。
2.4 上电日志是 74880,不是 115200
ESP8266 的 ROM 启动日志固定 74880 波特率输出,内容大致是:
text
ets Jan 8 2013,rst cause:2, boot mode:(3,6)
...
ready
你用 115200 去收,看到的就是一串乱码(日志里那些 NIIT、K 就是这么来的)。
这条日志非常有用:
- 看到乱码 → 说明线是通的,只是波特率不对(或被别的波特率占着);
- 一个字都没有 → 线没通(拨码、供电、交叉接线);
- 能看懂
ready→ ESP 已经跑起来了,AT 固件在等指令。
2.5 一条必须记住的时序
text
STM32 上电 ──────────────► 立刻跑 main()
ESP 上电 ── 1~2 秒启动 ──► 才开始响应 AT
ESP 启动比你慢 1~2 秒。上电后马上发 AT,它不会理你 。所以 Mqtt_Server_Init() 第一句就是:
c
HAL_Delay(3000); // 等 ESP 从上电/复位里完全出来
Esp_Flush(); // 把这 3 秒里它喷的启动日志清干净
少这一句,后面的探测就会把"还没启动完"误判成"没接线"。
三、进入 MQTT 的完整路径
3.1 按键链路
text
上电:MODE_MANUAL(手动,KEY4=加热)
│
├─ KEY2 ──► MODE_AUTO
│
├─ KEY4 ──► MODE_REMOTE(默认 NET_BT,USART2=9600)
│
└─ KEY4 ──► NET_MQTT(USART2=115200,首次要初始化十几秒)
│
└─ KEY4 ──► 回 NET_BT(9600)
所以从冷启动进 MQTT,需要按 KEY2 → KEY4 → KEY4 三次。
3.2 主循环里的切换代码
c
if(g_net_req != NET_REQ_NONE)
{
uint8_t req = g_net_req;
g_net_req = NET_REQ_NONE; // 先清,避免重复执行
if(req == NET_REQ_MQTT)
{
g_net = NET_MQTT; // ★ 先置协议标志
Uart2_SetBaudrate(115200); // 再切波特率(此时不能挂中断)
if(!g_mqtt_inited)
{
g_net_busy = 1; // 初始化期间丢弃 KEY4
printf("[NET] Switch HW dial to WiFi(W_TXD/W_RXD)! MQTT init ~10s...\r\n");
if(Mqtt_Server_Init() == HAL_OK)
g_mqtt_inited = 1; // 只有全部成功才算"已初始化"
else
printf("[NET] MQTT init failed (see FAIL line above). press KEY4 twice to retry\r\n");
g_net_busy = 0;
}
else
{
HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1); // 连过,只重挂接收
}
printf("[NET] WiFi/MQTT @%lu\r\n", (unsigned long)huart2.Init.BaudRate);
}
else
{
g_net = NET_BT;
Uart2_SetBaudrate(9600);
HAL_UART_Receive_IT(&huart2, bt_buf, 1);
printf("[NET] Bluetooth @9600\r\n");
}
}
3.3 四处"顺序不能反"
| 顺序 | 反了会怎样 |
|---|---|
先 g_net 再切波特率 |
切换瞬间到达的字节会走错分支(进蓝牙缓冲,或反之) |
| 切波特率时不能挂中断 | Mqtt_Server_Init() 内部用阻塞收发,挂着 IT 会让 HAL_UART_Receive 返回 HAL_BUSY,recv 永远为空 |
先 g_net_busy=1 再初始化 |
初始化十几秒里按 KEY4,"切回蓝牙"的请求会插进 AT 指令流,整轮初始化报废 |
| 初始化失败也要挂接收 | 否则 ESP 的报错打印全丢,你连失败原因都看不到 |
3.4 别被日志里的波特率误导
c
printf("[NET] WiFi/MQTT @%lu\r\n", (unsigned long)huart2.Init.BaudRate);
这里打印的是实际生效的波特率,不是写死的 115200。如果探测阶段发现 ESP 只认 9600,它会自动降速,日志就会是:
text
[ESP] online @9600 (not 115200), keep 9600
[NET] WiFi/MQTT @9600
这是正常的 ,不是配置错了。第 11 章的蓝牙分支反过来,是写死的 @9600 字面量。
四、初始化全景:11 步,一步都不能少
4.1 总表
| 步 | 动作 | 函数 | 超时 | 失败后果 |
|---|---|---|---|---|
| ① | 等 ESP 启动 + 清缓冲 | HAL_Delay(3000) / Esp_Flush() |
--- | 后面全部无应答 |
| ② | 探测 ESP 是否在线 | Esp_Probe() |
见第五节 | 直接返回失败,不白等 |
| ③ | 关回显 | setEchoOff() → ATE0 |
1000ms ×3 | 回显占满缓冲,OK 被挤后 |
| ④ | Station 模式 | setWifiMode(1) → AT+CWMODE=1 |
1000ms | 连不上路由器 |
| ⑤ | 连路由器 | connectWifi() → AT+CWJAP |
20000ms | 后续全失败 |
| ⑥ | 复位 MQTT 链路 | AT+MQTTDISCONN=0 + AT+MQTTCLEAN=0 |
1500/2000ms | USERCFG 必回 ERROR |
| ⑦ | 配置 MQTT 参数 | setMqttPara() → AT+MQTTUSERCFG |
1500ms | 连不上 broker |
| ⑧ | 连 broker | MqttConn() → AT+MQTTCONN |
15000ms ×2 | 无会话 |
| ⑨ | 订阅命令主题 | MqttSub() → AT+MQTTSUB |
1500ms | 收不到 H5 命令 |
| ⑩ | 试发一条 | Mqtt_Pub_Raw_Blocking() |
1500 + 2000ms | 判失败,可重试 |
| ⑪ | 切中断接收 | HAL_UART_Receive_IT(...,1) |
--- | 运行时收不到任何东西 |
为什么"任何一步失败就停" :后面的步骤依赖前面的结果。WiFi 都没连上,还硬跑 CONN/SUB,只会白等几十秒然后满屏 ERROR。代码用 do{...}while(0) 只是为了能 break,它不是循环。
4.2 代码骨架
c
uint8_t Mqtt_Server_Init(void)
{
uint8_t ok = 1;
uint8_t skip_conn = 0; // 1=ESP 已自己连上 broker,跳过 USERCFG/CONN
printf("[NET] MQTT init start...\r\n");
HAL_Delay(3000);
Esp_Flush();
if(!Esp_Probe()) // ② 探活失败 → 立刻返回,不浪费后面的时间
{
printf("[NET] ESP probe failed -> skip WiFi/broker init (cause printed above)\r\n");
mqtt_ready = 0;
HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1); // 仍挂上,方便看 ESP 打印
return HAL_ERROR;
}
do
{
if(setEchoOff() != HAL_OK) { ok = 0; break; }
if(setWifiMode(1) != HAL_OK) { ok = 0; break; }
if(connectWifi("7", "66668888") != HAL_OK) { ok = 0; break; }
/* ⑥ 关键:先彻底擦掉 LinkID 0 的旧配置 */
Esp_Cmd_Expect("AT+MQTTDISCONN=0\r\n", 1500);
HAL_Delay(500);
Esp_Cmd_Expect("AT+MQTTCLEAN=0\r\n", 2000);
HAL_Delay(500);
if(setMqttPara() != HAL_OK)
{
if(Mqtt_Link_Connected("after cfg fail")) // 配置被拒,但会话其实已存在
skip_conn = 1;
else { ok = 0; break; }
}
if(!skip_conn)
{
HAL_Delay(500); // 给 ESP 一点时间装载配置
if(MqttConn() != HAL_OK) { ok = 0; break; }
}
if(MqttSub() != HAL_OK) { ok = 0; break; }
} while(0);
if(ok) /* ⑩ 试发一条,真收到 OK 才算通 */
{
char json[64] = {0};
uint16_t jlen = Mqtt_Build_Json(json, sizeof(json));
printf("[NET] test publish: %s\r\n", json);
if(Mqtt_Pub_Raw_Blocking(json, jlen) != HAL_OK)
{
printf("MqttPub(test) : FAIL,recv"); Esp_Print_Resp();
ok = 0;
}
else printf("MqttPub(test) : OK!!!\r\n");
}
mqtt_ready = ok;
if(ok) printf("[NET] MQTT ready (WiFi + broker + sub all OK)\r\n");
else printf("[NET] MQTT init FAILED at step above. press KEY4 twice to retry\r\n");
/* ⑪ 无论成败都挂中断接收 */
HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1);
last_pub_tick = HAL_GetTick(); // 试发刚结束,2 秒上报计时从现在开始
return ok ? HAL_OK : HAL_ERROR;
}
4.3 为什么必须"试发一条"
AT+MQTTCONN 回 OK、AT+MQTTSUB 也回 OK,但随后 PUB 一直回 ERROR 的情况真实发生过(就是文章开头那段日志)。
光看前两步会误判成"已经成功",然后进主循环每 2 秒发一次、每 2 秒报一次 ERROR,把串口刷满。
所以初始化末尾一定要真发一条:broker 收下了,才算 MQTT 可用。
五、Esp_Probe:先探活,再谈别的
5.1 四步策略
text
① 115200 发 AT,3 次(每次间隔 300ms)
② 不行 → 切 9600,再 3 次
③ 还不行 → 切回 115200 再补 1 次(可能刚启动完)
④ 全失败 → 看"有没有收到过任何字节",给出结论
为什么还要试 9600:如果上一个实验把 ESP 的 AT 波特率改成了 9600,你只看 115200 就会误判成"没接线",然后把线拆了重接,白折腾一小时。
为什么第 ③ 步要补一次:ESP 启动日志是 74880,这段时间收什么都是乱码;等几秒它就绪了。不补这一次,就会把"还没启动完"当成"坏了"。
5.2 结论怎么读
| 日志 | 含义 | 处理 |
|---|---|---|
[ESP] online @115200 |
正常 | 继续 |
[ESP] online @9600 (not 115200) |
波特率被人改过 | 可以继续,全程按 9600 走 |
[ESP] online @115200 (was still booting) |
刚才只是没启动完 | 继续 |
zero bytes received -> USART2 RX not connected to ESP (dial on Bluetooth side? ESP unpowered?) |
一个字节都没收到 | 拨码、供电、TX/RX 交叉,三选一 |
no valid answer, but bytes did arrive -> baud mismatch (or ESP still booting) |
收到了,但是乱码 | 波特率不对 / 还在启动 |
这两句话是整个工程里最有价值的诊断信息,一定要会读:
- 0 字节 = 物理层不通;
- 有字节但乱码 = 物理层通了,参数不对。
5.3 探测失败时为啥还要挂中断
c
HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1);
return HAL_ERROR;
失败也要挂接收,因为 ESP 之后可能还在往外喷启动日志/报错。挂着中断,这些内容就会进环形缓冲,由主循环打印出来------失败原因往往就在这些被忽略的字节里。
六、Esp_Cmd_Expect:把"收满 64 字节"换成"逐字节匹配"
6.1 错误写法与它的三个死法
很多人(包括我)第一版是这么写的:
c
HAL_UART_Receive(&huart2, buf, 64, 5000);
if(strstr((char*)buf, "OK")) ...
死法一:缓冲区被事件流占满。ESP 连 WiFi 时会先刷一串:
text
WIFI DISCONNECT
+CWJAP:1
WIFI CONNECTED
WIFI GOT IP
OK
64 字节正好被前几行塞满,真正的 OK 还在后面 → 被误判成 FAIL。
死法二:RxState 不是 READY 。上一步的接收没收干净,HAL_UART_Receive 直接返回 HAL_BUSY,buf 里啥也没有。
死法三:ORE 溢出。字节来太快没取走,硬件置 ORE,HAL 中止本次接收并返回错误。
6.2 正确写法:边收边匹配
c
static uint8_t Esp_Cmd_Expect(const char *cmd, uint32_t timeout)
{
uint16_t n = 0;
uint32_t t0, t_close;
esp_last_resp[0] = '\0';
Esp_Flush(); // ★ 每条指令前都清残留
if(HAL_UART_Transmit(&huart2, (uint8_t*)cmd, strlen(cmd), 200) != HAL_OK)
return HAL_ERROR; // 发送失败要当场报,别白等
t0 = HAL_GetTick();
while(HAL_GetTick() - t0 < timeout)
{
char b;
if(!Esp_Rx_Poll(&b, 20)) continue; // 20ms 一轮,外层还有总超时
if(n < sizeof(esp_last_resp) - 1)
{
esp_last_resp[n++] = b;
esp_last_resp[n] = '\0'; // 边收边补结束符,随时能 strstr
}
if(strstr(esp_last_resp, "ERROR") || strstr(esp_last_resp, "FAIL"))
return HAL_ERROR;
if(strstr(esp_last_resp, "OK") || strstr(esp_last_resp, "+MQTTCONNECTED"))
return HAL_OK;
}
/* 超时后再多收 300ms:把迟到的应答留下,否则下一条的 Flush 会丢掉它 */
t_close = HAL_GetTick() + 300;
...
return HAL_ERROR;
}
6.3 四个必须记住的细节
① FAIL 也要当失败。
连不上路由器时,ESP 回的是 +CWJAP:1 / FAIL,不是 ERROR。只认 ERROR 的话,你会一直等到 20 秒超时,日志上还看不出原因。
② +MQTTCONNECTED 也算成功。
AT+MQTTCONN 有时不急着回 OK,而是先推 +MQTTCONNECTED:0,1,"broker.emqx.io",1883。你要是只认 OK,就会误判失败。
③ 每条指令前必须 Esp_Flush()。
上一条的 ERROR 如果留在缓冲里,下一条指令刚发出去就被判失败------日志表现为 recv:K → ERROR 交错,非常难查。
④ 超时后多收 300ms。
这条只影响排错,不影响功能:没有它,迟到的应答会被下一条的 Flush 清掉,日志只剩一个 (len=0)",你根本不知道 ESP 说了什么。
6.4 超时时间表:每个数字都有来历
| 指令 | 超时 | 为什么是这个数 |
|---|---|---|
AT(探活) |
500ms | 正常几毫秒就回,500ms 足够 |
ATE0 / AT+CWMODE |
1000ms | 简单配置,回得快 |
AT+CWJAP |
20000ms | 找不到 AP 时 ESP 会静默扫描十几秒,一个字节都不回;给 10 秒会误判失败 |
AT+MQTTDISCONN |
1500ms | 没连时回 ERROR,正常现象 |
AT+MQTTCLEAN |
2000ms | 要擦 flash,稍慢 |
AT+MQTTUSERCFG |
1500ms | 写配置 |
AT+MQTTCONN |
15000ms | 要先 DNS 解析再 TCP 握手,网络差时十几秒;给 5 秒必误判 |
AT+MQTTSUB |
1500ms | 本地操作,快 |
AT+PING |
5000ms | 用来区分"网络不通"和"broker 拒绝" |
AT+MQTTCONN? |
1000ms | 查询,快 |
这张表怎么用:所有"FAIL, recv(len=0)"基本都是超时给短了。
七、Esp_Rx_Poll:绕过 HAL 直接读寄存器
c
static uint8_t Esp_Rx_Poll(char *pc, uint32_t wait_ms)
{
uint32_t t0 = HAL_GetTick();
do
{
uint32_t sr = huart2.Instance->SR;
if(sr & USART_SR_RXNE)
{
*pc = (char)(huart2.Instance->DR & 0xFF); // 读 DR 自动清 RXNE
return 1;
}
if(sr & USART_SR_ORE)
{
(void)huart2.Instance->DR; // 溢出:读一下 DR 清标志
}
} while(HAL_GetTick() - t0 < wait_ms);
return 0;
}
7.1 为什么不用 HAL_UART_Receive
三个理由:RxState 检查、ORE 中止、按固定长度返回。第 10 章 7.2 节详细讲过,这里再补一条工程经验:
初始化阶段用阻塞收发(直接读寄存器),运行阶段用中断 + 环形缓冲。 两套机制不要同时开:
- 初始化时如果已经挂了
Receive_IT,HAL_UART_Receive会因为RxState=BUSY直接返回HAL_BUSY; - 反过来,
Esp_Rx_Poll读走字节后,中断的计数也会乱。
所以本工程的顺序是:先阻塞跑完初始化,最后一行才挂中断。
7.2 Esp_Flush 为什么要"先读 SR 再读 DR"
c
static void Esp_Flush(void)
{
esp_rx_head = 0;
esp_rx_tail = 0;
(void)huart2.Instance->SR; // 先读 SR
(void)huart2.Instance->DR; // 再读 DR
}
读 SR 再读 DR 这一对动作,能同时清掉 RXNE 和 ORE/NE/FE/PE。
踩过的坑:原来用
__HAL_UART_CLEAR_FLAG(&huart2, UART_FLAG_RXNE),它是往 SR 写位,对 RXNE 其实不起作用。残留字节会串到下一条指令的应答里,日志看起来像"ESP 回了两个 OK"。
7.3 打印应答时要把 \r\n 转义
c
static void Esp_Print_Resp(void)
{
const char *s = esp_last_resp;
printf("(len=%u)\"", (unsigned)strlen(esp_last_resp));
while(*s)
{
if(*s == '\r') printf("\\r");
else if(*s == '\n') printf("\\n");
else printf("%c", *s);
s++;
}
printf("\"\r\n");
}
ESP 的应答几乎都以 \r\n 开头。直接 printf("%s", resp),终端上就是一行空白 ------日志里那个孤零零的 ERROR 就是这么来的。转义之后你会看到:
text
recv(len=7)"\r\nERROR"
一眼就知道:收到了,内容是 ERROR,不是没收到。
八、AT+MQTTCLEAN:本章最隐蔽的一个坑
8.1 症状
text
setMqttPara : FAIL,recv(len=7)"\r\nERROR"
前面 CWJAP 明明 OK,为什么配置参数会被拒?
8.2 原因:ESP 把配置存进了 flash
ESP-AT 会把 MQTT 配置持久化。上电、WiFi 一恢复,它就自己把 broker 连上了,日志里会出现:
text
[ESP] +MQTTCONNECTED:0,1,"broker.emqx.io",1883
这时 LinkID 0 已经被占用,你再发:
text
AT+MQTTUSERCFG=0,1,"ESP12F_2604_HH","","",0,0,""
它只能回 ERROR------因为"0 号连接"正在用着。
8.3 为什么 AT+MQTTDISCONN 不够
DISCONN 只是断开连接,配置还在 flash 里。ESP 拿着旧配置马上又连回去(实测断开三次都失败,因为它比你想的快)。
真正管用的是:
text
AT+MQTTCLEAN=0 // 把 LinkID 0 的配置一起擦掉,回到"未配置"状态
所以顺序必须是:
c
Esp_Cmd_Expect("AT+MQTTDISCONN=0\r\n", 1500); // 没连时回 ERROR,正常,忽略
HAL_Delay(500);
Esp_Cmd_Expect("AT+MQTTCLEAN=0\r\n", 2000); // ★ 真正复位
HAL_Delay(500);
两个 HAL_Delay(500) 不是凑数:ESP 内部要时间真正释放连接,紧接着发下一条容易被回 busy p...。
8.4 万一还是被拒:skip_conn 兜底
c
if(setMqttPara() != HAL_OK)
{
if(Mqtt_Link_Connected("after cfg fail")) // 查 AT+MQTTCONN?
{
skip_conn = 1; // 会话其实是好的,直接沿用
}
else { ok = 0; break; }
}
AT+MQTTCONN? 回 +MQTTCONN:0,1,... 就说明链路已连。这时跳过 USERCFG 和 CONN,只重新订阅一次------重连之后原来的订阅一定丢了,不重订就收不到 H5 命令。
查询本身失败(固件不支持/超时)按"未连接"处理,不阻断流程。
九、MQTT 参数与主题:每个数字都别乱改
9.1 AT+MQTTUSERCFG 八个参数
text
AT+MQTTUSERCFG=0,1,"ESP12F_2604_HH","","",0,0,""
| 位置 | 值 | 含义 |
|---|---|---|
| ① | 0 |
连接 ID(LinkID),全工程固定用 0 |
| ② | 1 |
MQTT 版本 3.1.1 |
| ③ | "ESP12F_2604_HH" |
ClientID,见 9.4 节的坑 |
| ④ | "" |
用户名(公共 broker 留空) |
| ⑤ | "" |
密码(留空) |
| ⑥ | 0 |
不清除会话 |
| ⑦ | 0 |
不使用 SSL(1883 是明文端口) |
| ⑧ | "" |
遗嘱主题(留空 = 不用) |
9.2 AT+MQTTCONN 与自动重连
text
AT+MQTTCONN=0,"broker.emqx.io",1883,1
最后一个 1 = 使能自动重连:连接断了 ESP 会自己重试。
这就是为什么"WiFi 恢复后 ESP 自己又连上了 broker"------自动重连 + flash 配置,两个特性叠加,才有了 8.2 节那个坑。
代码里重试 2 次,每次 15 秒;失败后还会:
- 查
AT+MQTTCONN?(可能只是 OK 迟到); - 发
AT+PING="broker.emqx.io",把"网络不通"和"broker 拒绝"分开:
| PING 结果 | 含义 |
|---|---|
+PING:45 |
通,延迟 45ms |
+PING:0 / ERROR |
域名解析不到,或没有外网 |
9.3 两个主题:为什么必须分开
c
#define TOPIC_PUB "2604/topic-HH" // 设备 → broker → H5(温湿度)
#define TOPIC_SUB "2604/cmd-HH" // H5 → broker → 设备(命令)
如果只用一个主题,会出两个问题:
- 自己收到自己发的消息:设备每 2 秒发一次 JSON,自己也会收到一次,如果解析不当就会把 JSON 当成命令;
- 命令和数据混在一条流里:H5 要过滤掉温湿度、设备要过滤掉命令,两边都变复杂。
分开之后,订阅的永远是命令,发布的永远是数据,各自只看自己关心的。
班级号要改成自己的。 2604 是班级编号,公共 broker 上同班的设备共用这套主题。你如果直接照抄,会收到同学设备的命令------表现是"没人操作,风扇自己转了"。
9.4 ClientID 冲突:两台设备互踢
MQTT 协议规定:同一个 ClientID 只能有一个在线连接。后连的会把先连的踢掉。
如果你两台板子都用 ESP12F_2604_HH:
- 现象 A:板子 A 正常,板子 B 一上电,A 就掉线;
- 现象 B:日志里
[NET] ESP rebooted -> MQTT session lost反复出现; - 现象 C:
PUB时好时坏。
处理 :ClientID 加后缀区分,比如 ESP12F_2604_HH_01、_02。H5 那边不用动,主题不变就行。
9.5 订阅
c
Esp_Cmd_Expect("AT+MQTTSUB=0,\"" TOPIC_SUB "\",1\r\n", 1500);
最后一个 1 是 QoS1 (至少送达一次)。发布端 PUBRAW 的第 4 个参数也是 1,两边一致。
十、发布:为什么必须用 AT+MQTTPUBRAW
10.1 直接 AT+MQTTPUB 会失败
要发的 JSON:
json
{"t":26,"h":55,"a":1,"c":412,"v":88}
里面有双引号 。用 AT+MQTTPUB 时,引号必须转义成 \":
text
AT+MQTTPUB=0,"2604/topic-HH","{\"t\":26,...}",1,0
而 ESP-AT v2.2.0 不认这种转义 ,直接回 ERROR。
我当时做的对照实验:
| 测试 | 结果 |
|---|---|
MqttPub(test)(带引号的 JSON) |
FAIL |
MqttPub(test,plain)(纯文本,无引号) |
OK |
纯文本能过、JSON 不过,就是转义问题。结论:JSON 一律走 AT+MQTTPUBRAW。
10.2 PUBRAW 的三段式握手
text
① 发命令:AT+MQTTPUBRAW=0,"2604/topic-HH",<长度>,1,0\r\n
② 等 ESP 回 '>' 提示符 ← 见到它才允许发数据
③ 发原始 JSON(不带 \r\n!)
④ 等 OK
对应代码:
c
static uint8_t Mqtt_Pub_Raw_Blocking(const char *json, uint16_t len)
{
char cmd[96] = {0};
char b;
uint16_t n = 0;
uint32_t t0;
uint8_t got_prompt = 0;
snprintf(cmd, sizeof(cmd), "AT+MQTTPUBRAW=0,\"" TOPIC_PUB "\",%u,1,0\r\n", (unsigned)len);
esp_last_resp[0] = '\0';
Esp_Flush();
if(HAL_UART_Transmit(&huart2, (uint8_t*)cmd, strlen(cmd), 200) != HAL_OK)
return HAL_ERROR;
/* ① 等 '>' */
t0 = HAL_GetTick();
while(HAL_GetTick() - t0 < 1500)
{
if(!Esp_Rx_Poll(&b, 20)) continue;
if(b == '>') { got_prompt = 1; break; }
if(n < sizeof(esp_last_resp) - 1)
{
esp_last_resp[n++] = b;
esp_last_resp[n] = '\0';
}
if(strstr(esp_last_resp, "ERROR")) return HAL_ERROR;
}
if(!got_prompt) return HAL_ERROR;
/* ② 原样发数据:绝对不能带 \r\n */
if(HAL_UART_Transmit(&huart2, (uint8_t*)json, len, 200) != HAL_OK)
return HAL_ERROR;
/* ③ 等 OK */
esp_last_resp[0] = '\0';
n = 0;
t0 = HAL_GetTick();
while(HAL_GetTick() - t0 < 2000)
{
if(!Esp_Rx_Poll(&b, 20)) continue;
if(n < sizeof(esp_last_resp) - 1)
{
esp_last_resp[n++] = b;
esp_last_resp[n] = '\0';
}
if(strstr(esp_last_resp, "ERROR")) return HAL_ERROR;
if(strstr(esp_last_resp, "OK")) return HAL_OK;
}
return HAL_ERROR;
}
10.3 四个关键点
① 长度必须精确。
第 3 个参数是"后面要发的字节数"。多算一个,ESP 会多等一个字节,卡住;少算一个,JSON 被截断,H5 JSON.parse 直接抛异常。
② 数据后面绝对不能带 \r\n。
这段是消息内容 ,不是 AT 指令。多一个 \r\n,H5 收到的 JSON 末尾就有两个不可见字符,JSON.parse 在某些写法下会失败。
③ '>' 提示符可能出现得很晚。
1500ms 是实测值。网络差的时候会慢,别给 100ms。
④ 发布必须"整段同步做完"。
c
uint8_t MqttPub()
{
...
__HAL_UART_DISABLE_IT(&huart2, UART_IT_RXNE); // 握手期间不让中断抢字节
ret = Mqtt_Pub_Raw_Blocking(json, jlen);
__HAL_UART_ENABLE_IT(&huart2, UART_IT_RXNE); // 结束再开
...
}
原因是我遇到过一个非常难受的现象:成功一次、失败一次,交替出现。
拆开看才发现:'>' 提示符被中断存进了环形缓冲,主循环轮询时抢不到;于是下一条命令被 ESP 当成"上一次的数据"吃掉了。
握手是一个原子过程,中间不能有别人碰串口。 关中断接收这段时间大约是几十毫秒。
10.4 关中断的副作用(要知道)
关 RXNE 中断的这几十毫秒里,如果 H5 正好下发命令,那一条会丢。
- 概率:2 秒一次、每次几十毫秒,大概 1%~3%;
- 表现:点了按钮没反应,再点一次就好;
- 处理:H5 端做"无回显超时重发",或者接受这个概率(本工程选择接受)。
没有两全其美:要么握手可能被抢字节,要么极少数命令会丢。选一个能接受的。
十一、接收:环形缓冲 + 按行解析
11.1 中断只做一件事
c
void Esp_Uart_RxByte(uint8_t b)
{
uint16_t next = (esp_rx_head + 1) % ESP_RXBUF_SIZE;
if(next == esp_rx_tail) return; // 满:丢弃新字节
esp_rxbuf[esp_rx_head] = b;
esp_rx_head = next;
}
ESP_RXBUF_SIZE = 512,head 只由中断写、tail 只由主循环写(第 10 章 8.2 节)。
115200 下一个字节约 86.8µs,512 字节约 44ms 的缓冲量。只要主循环不在某处卡 44ms 以上,就不会溢出。
11.2 主循环按行解析
c
void Mqtt_Poll(void)
{
static char line[256] = {0};
static uint16_t len = 0;
char c;
while(Esp_Rx_GetByte(&c)) // 没有 while(1),取完就返回
{
if(c == '\n') // 换行 = 一整行结束
{
line[len] = '\0';
Esp_Handle_Line(line);
len = 0;
}
else if(c != '\r') // 回车丢掉
{
if(len < sizeof(line) - 1) line[len++] = c;
else len = 0; // 太长(乱码)→ 丢弃重来
}
}
if(mqtt_ready && (HAL_GetTick() - last_pub_tick >= MQTT_PUB_PERIOD))
{
last_pub_tick = HAL_GetTick();
MqttPub(); // 每 2 秒上报一次
}
}
三个细节:
\r丢、\n断行 :ESP 的应答是\r\n,保留\r会让字符串末尾多一个字符,strstr比较时容易出幺蛾子;- 行太长直接清零 :防止
line[len++]越界; - 上报必须
mqtt_ready:broker 没连上时发PUBRAW只会一直回 ERROR,既刷屏又把串口塞满。
11.3 订阅消息怎么从行里抠出来
ESP 推送订阅消息的格式:
text
+MQTTSUBRECV:0,"2604/cmd-HH",6,FAN_ON
| 段 | 含义 |
|---|---|
+MQTTSUBRECV |
事件名 |
0 |
LinkID |
"2604/cmd-HH" |
主题 |
6 |
数据长度 |
FAN_ON |
真正的消息内容 |
代码数第 3 个逗号之后的内容:
c
p = strstr(line, "+MQTTSUBRECV");
if(p)
{
uint8_t commas = 0;
char *q = p;
while(*q != '\0' && commas < 3)
{
if(*q == ',') commas++;
q++;
}
if(commas == 3)
{
strncpy(esp_sub_data, q, sizeof(esp_sub_data) - 1);
esp_sub_data[sizeof(esp_sub_data) - 1] = '\0';
esp_sub_ready = 1; // 置标志,交给 main.c 取用
return;
}
}
注意:这里不是"按逗号 split",只是数到第 3 个逗号然后把后面整段拷走------命令里不会有逗号,所以够用。如果以后要传 JSON 命令,就得改成按长度截取(第 4 段的长度值)。
11.4 ESP 重启检测:一条最容易被忽略的逻辑
c
if(strstr(line, "ready"))
{
if(mqtt_ready)
{
mqtt_ready = 0;
printf("\r\n[NET] ESP rebooted -> MQTT session lost, stop publishing. press KEY4 twice to re-init\r\n");
}
return;
}
ESP 掉电/复位后一定打印 ready。这时:
- WiFi 会自动重连(自动重连 + flash 配置);
- 但 MQTT 会话已经丢了 ,此后
PUB只会一直回 ERROR。
所以看到 ready 就把 mqtt_ready 清 0,停止上报。主循环再同步:
c
if(g_mqtt_inited && !Mqtt_Is_Ready())
g_mqtt_inited = 0; // 下次切 MQTT 会重新走一遍初始化
没有这段逻辑,你会看到"设备一直发、H5 一直没数据",而且日志满屏 ERROR。 这是我看过的最多同学卡住的地方。
十二、和主循环怎么接
12.1 命令执行
c
if(g_mode == MODE_REMOTE && g_net == NET_MQTT)
{
Mqtt_Poll();
if(esp_sub_ready)
{
esp_sub_ready = 0; // 先清,防止同一条命令被执行两次
printf("[MQTT] cmd: %s\r\n", esp_sub_data);
Remote_Cmd_Exec(esp_sub_data, 0); // ★ echo_back=0:绝不回显
}
if(g_mqtt_inited && !Mqtt_Is_Ready())
g_mqtt_inited = 0;
}
echo_back=0 是硬性要求 :MQTT 通道下往 USART2 发任何东西,都会被 ESP 当成 AT 指令,回你 ERROR,甚至打断正在进行的握手。
12.2 两条通道的对照
| 项目 | 蓝牙(第 11 章) | MQTT(本章) |
|---|---|---|
| 波特率 | 9600 | 115200(可能被探测降到 9600) |
| 命令来源 | 手机 APP 直连 | H5 → broker → ESP |
| 命令格式 | 字符串 FAN_ON |
同样字符串,从 +MQTTSUBRECV 里抠出 |
| 回显 | 必须回(echo_back=1) |
必须不回(echo_back=0) |
| 上报 | {"t":26,"h":55} 两项 |
{"t":26,"h":55,"a":1,"c":412,"v":88} 五项 |
| 接收方式 | 单字节 + 主循环拼 | 中断环形缓冲 + 按行解析 |
| 初始化 | 无需(模块透传) | 11 步 AT 指令,十几秒 |
同一个 Remote_Cmd_Exec() 服务两条通道,所以 H5 和手机 APP 发的命令字面量必须完全一样。
十三、一次完整的联调日志(照着对)
13.1 切到 MQTT
text
[MODE] Remote (KEY4) | Bluetooth ON
[NET] Bluetooth @9600
[KEY4] request MQTT
[NET] Switch HW dial to WiFi(W_TXD/W_RXD)! MQTT init ~10s...
[NET] MQTT init start...
[ESP] online @115200
setEchoOff : OK!!!
setWifiMode:OK!!!
connectWifi : OK!!!
[NET] reset MQTT link (DISCONN + CLEAN)...
setMqttPara : OK!!!
MqttConn : OK!!!
MqttSub : OK!!!
[NET] test publish: {"t":26,"h":55,"a":1,"c":412,"v":88}
MqttPub(test) : OK!!!
[NET] MQTT ready (WiFi + broker + sub all OK)
[NET] WiFi/MQTT @115200
13.2 运行中
text
[PUB] OK ← 每 2 秒一条
[ESP] +MQTTSUBRECV:0,"2604/cmd-HH",6,FAN_ON
[MQTT] cmd: FAN_ON
[CMD] FAN ON
13.3 H5 端看到的
在 H5 页面上,温湿度每 2 秒刷新一次,空气质量三项也会跟着变。如果温湿度在动、但 H5 的空气质量一直不更新 ,多半是 Mqtt_Set_Env() 没被调用(第 6、7 章的传感器读取没跑),不是 MQTT 的问题。
十四、调不通:按这个顺序查
14.1 初始化阶段
| 日志 | 原因 | 处理 |
|---|---|---|
[ESP] zero bytes received |
物理层不通 | 拨码切到 WiFi 侧 / 查 ESP 供电 / TX-RX 交叉 / 共地 |
[ESP] no valid answer, but bytes did arrive |
收到乱码 | 波特率不对(ESP 可能被改过);或还在启动,等 3 秒再试 |
probe recv: "NIIT..." |
74880 启动日志 | 正常,等它跑完 |
setEchoOff : FAIL |
模块没有 AT 固件 / 波特率错 | 换固件或降速 |
connectWifi : FAIL,recv(len=0) |
超时太短 / AP 找不到 | 确认 SSID 密码,超时给 20s |
connectWifi : FAIL 后 +CWJAP:1 |
密码错 | 重新确认密码 |
setMqttPara : FAIL |
LinkID 0 被占用 | 补 AT+MQTTCLEAN=0 |
MqttConn : FAIL(try 1) 但 link ... connected=1 |
OK 迟到 | 代码已兜底,继续 |
MqttConn : FAIL + PING ERROR |
没外网 / 域名解析失败 | 换网络,或改 broker 地址 |
MqttSub : FAIL |
会话没建立 | 先看 CONN 是否真成功 |
MqttPub(test) : FAIL |
详见 14.2 | --- |
14.2 发布阶段
| 现象 | 原因 | 处理 |
|---|---|---|
| PUB 一直 ERROR | 会话已丢 / 用了 AT+MQTTPUB 发 JSON |
查 mqtt_ready;改用 PUBRAW |
| PUB 成功一次失败一次 | '>' 被中断抢走 |
握手期间关 RXNE 中断 |
| PUB 回 ERROR 且 payload 有乱码 | 长度算错 / 数据后多了 \r\n |
用 strlen 算长度,别手写 |
| 满屏 ERROR 刷屏 | mqtt_ready 为 0 还在发 |
发布前判 mqtt_ready |
14.3 收不到 H5 命令
- H5 发到的是不是
2604/cmd-HH? 主题不一致,设备永远收不到; - 设备订阅成功了吗? 日志里要有
MqttSub : OK!!!; - 有没有
+MQTTSUBRECV行? 没有 → 消息没到设备;有但没执行 → 解析问题; - 班级号对不对? 公共 broker 上主题写错一位就串到别人那儿;
- ESP 是不是重启过? 看有没有
[NET] ESP rebooted; - H5 和设备的 ClientID 是不是撞了? 撞了会互踢,表现是"时好时坏"。
十五、本章易错点总结
| 现象 | 真正原因 | 处理 |
|---|---|---|
| 所有 AT 指令全 FAIL,recv 为空 | 拨码还在蓝牙侧 | 切到 WiFi 侧,断电重上 |
| ESP 一点回应都没有 | 没有可用的 AT 固件(空片或版本太老) | 先发 AT+GMR 看版本,低于 v2.x 就烧固件 |
| 一发数据就复位 | 供电电流不够 | 独立 3.3V/500mA,别用板载排针 |
| 完全收不到字节 | EN 没拉高 / GPIO15 没拉低 | EN→3.3V,GPIO15→GND |
| 收到的都是乱码 | ESP 启动日志是 74880 | 等 3 秒再发 AT |
AT+CWJAP 白等 20 秒 |
找不到 AP 时 ESP 静默扫描 | 确认 SSID/密码;超时给足 20s |
setMqttPara : FAIL |
LinkID 0 被 flash 里的旧配置占用 | AT+MQTTCLEAN=0 |
DISCONN 之后还是 ERROR |
只断连不擦配置,ESP 会自己连回去 | 必须 CLEAN |
| CONN 明明成功却回 FAIL | OK 迟到,ESP 只推了 +MQTTCONNECTED |
把 +MQTTCONNECTED 也算成功 |
PUB 一直 ERROR |
用了 AT+MQTTPUB 发带引号 JSON |
改 AT+MQTTPUBRAW |
PUB 成功/失败交替 |
'>' 被中断接收抢进环形缓冲 |
握手期间关 RXNE 中断 |
| 订阅收到但 JSON 被截断 | PUBRAW 长度参数算错 | 用 strlen,别手写数字 |
| H5 收到 JSON 解析报错 | 数据后面多发了 \r\n |
原始数据不带换行 |
| 没人操作设备自己动 | 主题没改班级号,收到同学的设备命令 | 改成自己的班号 |
| 两台板子轮流掉线 | ClientID 相同,互相踢线 | ClientID 加唯一后缀 |
| ESP 重启后一直 ERROR | 会话已丢,mqtt_ready 还是 1 |
检测 ready 行并清 0 |
| 初始化十几秒像卡死 | 正常行为(阻塞式 AT 往返) | 期间别按键,等它跑完 |
| 初始化期间按键把流程搞乱 | KEY4 请求插进 AT 指令流 | g_net_busy 丢弃请求 |
| 切协议后第一个字节丢失 | g_net 和挂接收的顺序反了 |
先置协议标志,再挂中断 |
日志里 [NET] WiFi/MQTT @9600 以为是配错 |
探测发现 ESP 只认 9600,主动降速 | 正常,不用改 |
十六、小结与下一篇
这一章记住八件事:
- 拨码开关软件读不到 ,硬件和
g_net必须人工一致; - ESP 比 STM32 慢启动 1~2 秒,上电先等 3 秒;
- 探活的结论只有两种:0 字节 = 物理层不通;有字节但乱码 = 参数不对;
- AT 应答必须逐字节增量匹配 ,
HAL_UART_Receive(64)会被事件流塞满; FAIL也是失败 ,+MQTTCONNECTED也是成功;AT+MQTTCLEAN必须有,否则 flash 里的旧配置会占住 LinkID 0;- JSON 必须走
PUBRAW,且握手要整段独占串口; - ESP 重启 = 会话丢失 ,看到
ready就要清mqtt_ready。
下一篇是前端:H5 页面怎么连 broker、怎么发命令、怎么显示数据。你会发现,页面上那几个按钮背后,就是这一章订阅/发布的两条主题。
如果这篇文章对你有帮助,欢迎点赞收藏。系列更新中,下一篇见。
附录 A:本章完整代码
A.1 环形缓冲与取字节
c
#define ESP_RXBUF_SIZE 512
uint8_t esp_rx_byte = 0; // 中断单字节落点
static uint8_t esp_rxbuf[ESP_RXBUF_SIZE];
static volatile uint16_t esp_rx_head = 0; // 中断写
static volatile uint16_t esp_rx_tail = 0; // 主循环读
void Esp_Uart_RxByte(uint8_t b)
{
uint16_t next = (esp_rx_head + 1) % ESP_RXBUF_SIZE;
if(next == esp_rx_tail) return; // 满:丢新字节
esp_rxbuf[esp_rx_head] = b;
esp_rx_head = next;
}
static uint8_t Esp_Rx_GetByte(char *pc)
{
if(esp_rx_head == esp_rx_tail) return 0; // 空
*pc = (char)esp_rxbuf[esp_rx_tail];
esp_rx_tail = (esp_rx_tail + 1) % ESP_RXBUF_SIZE;
return 1;
}
static void Esp_Flush(void)
{
esp_rx_head = 0;
esp_rx_tail = 0;
(void)huart2.Instance->SR; // 先读 SR
(void)huart2.Instance->DR; // 再读 DR:一起清 RXNE + ORE/NE/FE/PE
}
A.2 直接读寄存器的轮询
c
static uint8_t Esp_Rx_Poll(char *pc, uint32_t wait_ms)
{
uint32_t t0 = HAL_GetTick();
do
{
uint32_t sr = huart2.Instance->SR;
if(sr & USART_SR_RXNE) { *pc = (char)(huart2.Instance->DR & 0xFF); return 1; }
if(sr & USART_SR_ORE) { (void)huart2.Instance->DR; }
} while(HAL_GetTick() - t0 < wait_ms);
return 0;
}
A.3 逐字节增量匹配
见 6.2 节完整代码。调用约定:
c
// 返回 HAL_OK:见到 OK 或 +MQTTCONNECTED
// 返回 HAL_ERROR:见到 ERROR / FAIL,或超时
// 无论成败,esp_last_resp 里都留有最近一次应答(用 Esp_Print_Resp 打印)
A.4 探活
c
static uint8_t Esp_Probe(void)
{
uint8_t i;
probe_got_bytes = 0;
for(i = 0; i < 3; i++) // ① 115200 ×3
{
if(Esp_At_Ping()) { printf("[ESP] online @115200\r\n"); return 1; }
HAL_Delay(300);
}
Uart2_SetBaudrate(9600); // ② 9600 ×3
for(i = 0; i < 3; i++)
{
if(Esp_At_Ping()) { printf("[ESP] online @9600 (not 115200), keep 9600\r\n"); return 1; }
HAL_Delay(300);
}
Uart2_SetBaudrate(115200); // ③ 回 115200 补 1 次
if(Esp_At_Ping()) { printf("[ESP] online @115200 (was still booting)\r\n"); return 1; }
if(probe_got_bytes)
printf("[ESP] no valid answer, but bytes did arrive -> baud mismatch (or ESP still booting)\r\n");
else
printf("[ESP] zero bytes received -> USART2 RX not connected to ESP (dial on Bluetooth side? ESP unpowered?)\r\n");
return 0;
}
A.5 MQTT 初始化主流程
见 4.2 节。要点回顾:
| 顺序 | 动作 |
|---|---|
| 1 | HAL_Delay(3000) + Esp_Flush() |
| 2 | Esp_Probe(),失败直接返回 |
| 3 | ATE0(3 次重试) |
| 4 | AT+CWMODE=1 |
| 5 | AT+CWJAP="SSID","PWD"(20s) |
| 6 | AT+MQTTDISCONN=0 → 500ms → AT+MQTTCLEAN=0 → 500ms |
| 7 | AT+MQTTUSERCFG=0,1,"ClientID","","",0,0,"" |
| 8 | AT+MQTTCONN=0,"broker.emqx.io",1883,1(15s ×2) |
| 9 | AT+MQTTSUB=0,"2604/cmd-HH",1 |
| 10 | 试发一条(PUBRAW),失败判整体失败 |
| 11 | HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1) |
A.6 发布(运行期)
见 10.2 节与 10.3 节。调用点:
c
// Mqtt_Poll() 内部,每 2 秒一次
if(mqtt_ready && (HAL_GetTick() - last_pub_tick >= MQTT_PUB_PERIOD))
{
last_pub_tick = HAL_GetTick();
MqttPub();
}
A.7 最小验证:五步确认 MQTT 通
第一步:确认硬件
- 拨码 → WiFi 侧;
- ESP:VCC(3.3V/≥500mA)、GND、EN→3.3V、GPIO15→GND;
- ESP TXD → PA3,RXD → PA2,共地;
- 上电后串口能看到一大串 74880 乱码,然后出现
ready(用 115200 看是乱码,正常)。
第二步:确认探活
按 KEY2 → KEY4 → KEY4,日志应出现:
text
[ESP] online @115200
zero bytes received→ 回第一步;bytes did arrive→ 波特率不对,等 3 秒再试。
第三步:确认会话
日志里依次出现 setEchoOff : OK!!! ... MqttSub : OK!!! ... MqttPub(test) : OK!!! ... [NET] MQTT ready。
任何一步 FAIL,对照第十四节的表。
第四步:确认上报
主循环运行时,日志每 2 秒一条 [PUB] OK。H5 上温湿度开始跳动。
第五步:确认命令
H5 点一下风扇,日志应出现:
text
[ESP] +MQTTSUBRECV:0,"2604/cmd-HH",6,FAN_ON
[MQTT] cmd: FAN_ON
[CMD] FAN ON
五步全过,整条链路就通了。接下来第 13 章,我们站到 H5 那一侧看。