STM32 从 0 到 1 做智能家居(十二):USART2 分时复用 —— ESP-12F + MQTT

本文详解STM32F411通过USART2单串口分时复用驱动ESP-12F模块实现MQTT通信的关键技术:硬件拨码切换、AT指令波特率动态适配(74880/9600/115200)、逐字节增量匹配应答、环形缓冲+按行解析、LinkID 0占用规避、flash持久化配置清理、MQTT初始化11步流程、JSON发布三段式握手及关中断同步机制,覆盖供电、启动时序、超时设定、ClientID防冲突等易错点。

文章目录

系列第十二篇。前面十一章铺的所有地基,都是为这一章服务:一组串口引脚、两个模块、两种波特率、一套"一问一答"的 AT 协议。

这一章我卡了整整三天。最难受的不是"连不上",而是每一步都回 OK,最后发布却永远 ERROR:

setEchoOff : 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 三条铁律

  1. 软件读不到拨码开关。 没有任何一根 GPIO 接在开关上,所以"硬件在哪一侧"必须靠人记住,并且和 g_net 保持一致。
  2. 切换必须成对。 只切 PA2 不切 PA3,两个模块的 TX 会同时驱动同一根线,现象是随机乱码——比完全不通更难查。
  3. 切换顺序固定:先断电 / 先停收发 → 拨开关 → 再上电。 带电热插拔容易在切换瞬间产生电平对打。

后面所有"MQTT 初始化全 FAIL",第一个要查的就是拨码开关。这不是套路话,是命中率最高的一条。

1.3 软件状态怎么表示

#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_readymqtt.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 波特率输出,内容大致是:

ets Jan  8 2013,rst cause:2, boot mode:(3,6)
...
ready

你用 115200 去收,看到的就是一串乱码(日志里那些 NIIT、K 就是这么来的)。

这条日志非常有用:

  • 看到乱码 → 说明线是通的,只是波特率不对(或被别的波特率占着);
  • 一个字都没有 → 线没通(拨码、供电、交叉接线);
  • 能看懂 ready → ESP 已经跑起来了,AT 固件在等指令。

2.5 一条必须记住的时序

STM32 上电 ──────────────► 立刻跑 main()
ESP 上电   ── 1~2 秒启动 ──► 才开始响应 AT

ESP 启动比你慢 1~2 秒。上电后马上发 AT,它不会理你。所以 Mqtt_Server_Init() 第一句就是:

HAL_Delay(3000);   // 等 ESP 从上电/复位里完全出来
Esp_Flush();       // 把这 3 秒里它喷的启动日志清干净

少这一句,后面的探测就会把"还没启动完"误判成"没接线"。

三、进入 MQTT 的完整路径

3.1 按键链路

上电: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 主循环里的切换代码

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 别被日志里的波特率误导

printf("[NET] WiFi/MQTT @%lu\r\n", (unsigned long)huart2.Init.BaudRate);

这里打印的是实际生效的波特率,不是写死的 115200。如果探测阶段发现 ESP 只认 9600,它会自动降速,日志就会是:

[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() → ATE01000ms ×3回显占满缓冲,OK 被挤后
④Station 模式setWifiMode(1) → AT+CWMODE=11000ms连不上路由器
⑤连路由器connectWifi() → AT+CWJAP20000ms后续全失败
⑥复位 MQTT 链路AT+MQTTDISCONN=0 + AT+MQTTCLEAN=01500/2000msUSERCFG 必回 ERROR
⑦配置 MQTT 参数setMqttPara() → AT+MQTTUSERCFG1500ms连不上 broker
⑧连 brokerMqttConn() → AT+MQTTCONN15000ms ×2无会话
⑨订阅命令主题MqttSub() → AT+MQTTSUB1500ms收不到 H5 命令
⑩试发一条Mqtt_Pub_Raw_Blocking()1500 + 2000ms判失败,可重试
⑪切中断接收HAL_UART_Receive_IT(...,1)—运行时收不到任何东西

为什么"任何一步失败就停":后面的步骤依赖前面的结果。WiFi 都没连上,还硬跑 CONN/SUB,只会白等几十秒然后满屏 ERROR。代码用 do{...}while(0) 只是为了能 break,它不是循环。

4.2 代码骨架

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 四步策略

① 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 探测失败时为啥还要挂中断

HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1);
return HAL_ERROR;

失败也要挂接收,因为 ESP 之后可能还在往外喷启动日志/报错。挂着中断,这些内容就会进环形缓冲,由主循环打印出来——失败原因往往就在这些被忽略的字节里。

六、Esp_Cmd_Expect:把"收满 64 字节"换成"逐字节匹配"

6.1 错误写法与它的三个死法

很多人(包括我)第一版是这么写的:

HAL_UART_Receive(&huart2, buf, 64, 5000);
if(strstr((char*)buf, "OK")) ...

死法一:缓冲区被事件流占满。ESP 连 WiFi 时会先刷一串:

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 正确写法:边收边匹配

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+CWMODE1000ms简单配置,回得快
AT+CWJAP20000ms找不到 AP 时 ESP 会静默扫描十几秒,一个字节都不回;给 10 秒会误判失败
AT+MQTTDISCONN1500ms没连时回 ERROR,正常现象
AT+MQTTCLEAN2000ms要擦 flash,稍慢
AT+MQTTUSERCFG1500ms写配置
AT+MQTTCONN15000ms要先 DNS 解析再 TCP 握手,网络差时十几秒;给 5 秒必误判
AT+MQTTSUB1500ms本地操作,快
AT+PING5000ms用来区分"网络不通"和"broker 拒绝"
AT+MQTTCONN?1000ms查询,快

这张表怎么用:所有"FAIL, recv(len=0)"基本都是超时给短了。

七、Esp_Rx_Poll:绕过 HAL 直接读寄存器

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"

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 转义

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 就是这么来的。转义之后你会看到:

recv(len=7)"\r\nERROR"

一眼就知道:收到了,内容是 ERROR,不是没收到。

八、AT+MQTTCLEAN:本章最隐蔽的一个坑

8.1 症状

setMqttPara : FAIL,recv(len=7)"\r\nERROR"

前面 CWJAP 明明 OK,为什么配置参数会被拒?

8.2 原因:ESP 把配置存进了 flash

ESP-AT 会把 MQTT 配置持久化。上电、WiFi 一恢复,它就自己把 broker 连上了,日志里会出现:

[ESP] +MQTTCONNECTED:0,1,"broker.emqx.io",1883

这时 LinkID 0 已经被占用,你再发:

AT+MQTTUSERCFG=0,1,"ESP12F_2604_HH","","",0,0,""

它只能回 ERROR——因为"0 号连接"正在用着。

8.3 为什么 AT+MQTTDISCONN 不够

DISCONN 只是断开连接,配置还在 flash 里。ESP 拿着旧配置马上又连回去(实测断开三次都失败,因为它比你想的快)。

真正管用的是:

AT+MQTTCLEAN=0      // 把 LinkID 0 的配置一起擦掉,回到"未配置"状态

所以顺序必须是:

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 兜底

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 八个参数

AT+MQTTUSERCFG=0,1,"ESP12F_2604_HH","","",0,0,""
位置值含义
①0连接 ID(LinkID),全工程固定用 0
②1MQTT 版本 3.1.1
③"ESP12F_2604_HH"ClientID,见 9.4 节的坑
④""用户名(公共 broker 留空)
⑤""密码(留空)
⑥0不清除会话
⑦0不使用 SSL(1883 是明文端口)
⑧""遗嘱主题(留空 = 不用)

9.2 AT+MQTTCONN 与自动重连

AT+MQTTCONN=0,"broker.emqx.io",1883,1

最后一个 1 = 使能自动重连:连接断了 ESP 会自己重试。

这就是为什么"WiFi 恢复后 ESP 自己又连上了 broker"——自动重连 + flash 配置,两个特性叠加,才有了 8.2 节那个坑。

代码里重试 2 次,每次 15 秒;失败后还会:

  1. 查 AT+MQTTCONN?(可能只是 OK 迟到);
  2. 发 AT+PING="broker.emqx.io",把"网络不通"和"broker 拒绝"分开:
PING 结果含义
+PING:45通,延迟 45ms
+PING:0 / ERROR域名解析不到,或没有外网

9.3 两个主题:为什么必须分开

#define TOPIC_PUB   "2604/topic-HH"   // 设备 → broker → H5(温湿度)
#define TOPIC_SUB   "2604/cmd-HH"     // H5 → broker → 设备(命令)

如果只用一个主题,会出两个问题:

  1. 自己收到自己发的消息:设备每 2 秒发一次 JSON,自己也会收到一次,如果解析不当就会把 JSON 当成命令;
  2. 命令和数据混在一条流里: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 订阅

Esp_Cmd_Expect("AT+MQTTSUB=0,\"" TOPIC_SUB "\",1\r\n", 1500);

最后一个 1 是 QoS1(至少送达一次)。发布端 PUBRAW 的第 4 个参数也是 1,两边一致。

十、发布:为什么必须用 AT+MQTTPUBRAW

10.1 直接 AT+MQTTPUB 会失败

要发的 JSON:

{"t":26,"h":55,"a":1,"c":412,"v":88}

里面有双引号。用 AT+MQTTPUB 时,引号必须转义成 \":

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 的三段式握手

① 发命令:AT+MQTTPUBRAW=0,"2604/topic-HH",<长度>,1,0\r\n
② 等 ESP 回 '>' 提示符     ← 见到它才允许发数据
③ 发原始 JSON(不带 \r\n!)
④ 等 OK

对应代码:

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。

④ 发布必须"整段同步做完"。

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 中断只做一件事

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 主循环按行解析

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 秒上报一次
  }
}

三个细节:

  1. \r 丢、\n 断行:ESP 的应答是 \r\n,保留 \r 会让字符串末尾多一个字符,strstr 比较时容易出幺蛾子;
  2. 行太长直接清零:防止 line[len++] 越界;
  3. 上报必须 mqtt_ready:broker 没连上时发 PUBRAW 只会一直回 ERROR,既刷屏又把串口塞满。

11.3 订阅消息怎么从行里抠出来

ESP 推送订阅消息的格式:

+MQTTSUBRECV:0,"2604/cmd-HH",6,FAN_ON
段含义
+MQTTSUBRECV事件名
0LinkID
"2604/cmd-HH"主题
6数据长度
FAN_ON真正的消息内容

代码数第 3 个逗号之后的内容:

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 重启检测:一条最容易被忽略的逻辑

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,停止上报。主循环再同步:

if(g_mqtt_inited && !Mqtt_Is_Ready())
  g_mqtt_inited = 0;      // 下次切 MQTT 会重新走一遍初始化

没有这段逻辑,你会看到"设备一直发、H5 一直没数据",而且日志满屏 ERROR。 这是我看过的最多同学卡住的地方。

十二、和主循环怎么接

12.1 命令执行

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(本章)
波特率9600115200(可能被探测降到 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

[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 运行中

[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 : FAILLinkID 0 被占用补 AT+MQTTCLEAN=0
MqttConn : FAIL(try 1) 但 link ... connected=1OK 迟到代码已兜底,继续
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 命令

  1. H5 发到的是不是 2604/cmd-HH? 主题不一致,设备永远收不到;
  2. 设备订阅成功了吗? 日志里要有 MqttSub : OK!!!;
  3. 有没有 +MQTTSUBRECV 行? 没有 → 消息没到设备;有但没执行 → 解析问题;
  4. 班级号对不对? 公共 broker 上主题写错一位就串到别人那儿;
  5. ESP 是不是重启过? 看有没有 [NET] ESP rebooted;
  6. 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 : FAILLinkID 0 被 flash 里的旧配置占用AT+MQTTCLEAN=0
DISCONN 之后还是 ERROR只断连不擦配置,ESP 会自己连回去必须 CLEAN
CONN 明明成功却回 FAILOK 迟到,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 环形缓冲与取字节

#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 直接读寄存器的轮询

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 节完整代码。调用约定:

// 返回 HAL_OK:见到 OK 或 +MQTTCONNECTED
// 返回 HAL_ERROR:见到 ERROR / FAIL,或超时
// 无论成败,esp_last_resp 里都留有最近一次应答(用 Esp_Print_Resp 打印)

A.4 探活

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 节。要点回顾:

顺序动作
1HAL_Delay(3000) + Esp_Flush()
2Esp_Probe(),失败直接返回
3ATE0(3 次重试)
4AT+CWMODE=1
5AT+CWJAP="SSID","PWD"(20s)
6AT+MQTTDISCONN=0 → 500ms → AT+MQTTCLEAN=0 → 500ms
7AT+MQTTUSERCFG=0,1,"ClientID","","",0,0,""
8AT+MQTTCONN=0,"broker.emqx.io",1883,1(15s ×2)
9AT+MQTTSUB=0,"2604/cmd-HH",1
10试发一条(PUBRAW),失败判整体失败
11HAL_UART_Receive_IT(&huart2, &esp_rx_byte, 1)

A.6 发布(运行期)

见 10.2 节与 10.3 节。调用点:

// 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,日志应出现:

[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 点一下风扇,日志应出现:

[ESP] +MQTTSUBRECV:0,"2604/cmd-HH",6,FAN_ON
[MQTT] cmd: FAN_ON
[CMD] FAN ON

五步全过,整条链路就通了。接下来第 13 章,我们站到 H5 那一侧看。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值