HC32F460/ESP32-S3 对接自带 MQTT 的 NB-IoT 语音聊天系统(私有云 + 微信小程序)
一、核心调整说明:选用自带 MQTT 协议栈的 NB-IoT 模组
1.1 模组选型:移远 BC35-G(核心优势)
移远 BC35-G 是专为物联网设计的 NB-IoT 模组,内置 MQTT 3.1.1 协议栈,无需主控(HC32F460/ESP32-S3)封装 MQTT 帧,仅需通过 UART 发送 AT 指令即可完成 MQTT 连接、订阅、发布,大幅降低主控开发难度,减少资源占用,提升通信稳定性。
1.2 方案对比(基于自带 MQTT 的 NB-IoT 模组)
| 维度 | 方案 1:HC32F460+BC35-G | 方案 2:ESP32-S3+BC35-G |
|---|---|---|
| 核心架构 | HC32F460 + BC35-G(自带 MQTT) + VS1053 | ESP32-S3 + BC35-G(自带 MQTT) + VS1053 |
| MQTT 处理方式 | 主控发 AT 指令,模组内置协议栈完成 MQTT 通信 | 主控发 AT 指令,模组内置协议栈完成 MQTT 通信 |
| 开发难度 | 中(裸机 + 简化 AT 指令,无需封装 MQTT) | 低(框架 + 简化 AT 指令,无需封装 MQTT) |
| 主控资源占用 | 极低(仅处理 AT 指令 / 语音,无 MQTT 协议栈开销) | 低(仅处理 AT 指令 / 语音,无 MQTT 协议栈开销) |
| 功耗 | 极致低功耗(HC32 休眠 + BC35-G PSM 模式,μA 级) | 中等(ESP32 休眠 + BC35-G PSM 模式) |
| 通信稳定性 | 高(模组原生 MQTT,适配 NB-IoT 低带宽) | 高(模组原生 MQTT,适配 NB-IoT 低带宽) |
| 适用场景 | 工业低功耗终端(矿用 / 物联网传感器) | 民用 / 快速迭代场景(智能家居 / 便携设备) |
1.3 硬件连接调整(BC35-G 与主控的 UART 连接)
BC35-G 的 UART 通信引脚与 BC28 兼容,硬件连接无需大幅调整,核心引脚如下:
| 主控引脚 | BC35-G 引脚 | 功能说明 |
|---|---|---|
| HC32F460 PD0/ESP32-S3 IO16 | TXD | 主控发送→模组接收 |
| HC32F460 PD1/ESP32-S3 IO17 | RXD | 主控接收→模组发送 |
| 主控 IO 口 | PWRKEY | 模组开关机(低电平触发) |
| GND | GND | 共地(3.3V 电平匹配) |
二、系统整体设计(适配自带 MQTT 的 NB-IoT 模组)
2.1 核心架构(简化版)
终端侧(HC32F460/ESP32-S3)   ├── VS1053:采集/播放MP3语音   ├── BC35-G(自带MQTT):通过AT指令配置MQTT,接入私有云   ↓ NB-IoT运营商网络   ↓ 私有云服务器   ├── EMQ X MQTT Broker:接收模组MQTT消息   ├── Python服务:MQTT<->WebSocket转发,对接微信小程序   ↓ 微信小程序:录音/播放,WebSocket接收/发送语音2.2 通信协议优化(模组自带 MQTT 适配)
(1)终端 <->BC35-G AT 指令简化
主控仅需发送标准化 AT 指令配置 BC35-G 的 MQTT 参数,无需拼接 MQTT 帧,示例核心指令:
AT+QMTCFG="broker",0,"120.78.xxx.xxx",1883 // 配置私有云MQTT Broker AT+QMTCONN=0,"device\_001","admin","123456" // MQTT登录(模组自动封装报文) AT+QMTPUB=0,0,0,0,"iot/voice/device\_001/upload","语音数据" // 发布消息(模组自动处理MQTT帧)(2)MQTT 主题 / 数据格式(与原方案兼容)
终端上行:
iot/voice/device/{dev_id}/upload(模组直接发布)终端下行:
iot/voice/device/{dev_id}/down(模组订阅后推送至主控)语音数据帧:保留原帧格式(
0xAA55+设备ID+长度+语音数据+校验和),模组仅透传二进制数据,不修改内容。
三、方案 1:HC32F460+BC35-G(自带 MQTT)实现
3.1 核心代码调整(简化 MQTT AT 指令)
(1)头文件与宏定义(新增 BC35-G 适配)
\#include "hc32f460\_clk.h" \#include "hc32f460\_uart.h" \#include "hc32f460\_spi.h" \#include "hc32f460\_gpio.h" \#include "delay.h" \#include \<string.h> // 设备配置 \#define DEV\_ID "device\_001" // 终端唯一ID \#define NB\_UART HC32F460\_UART2 // 与BC35-G通信的UART \#define BAUD\_RATE 9600 // BC35-G默认波特率9600 \#define VOICE\_BUF\_LEN 4096 // 语音缓存长度 // 私有云MQTT配置(BC35-G直接对接) \#define MQTT\_BROKER "120.78.xxx.xxx" // 私有云公网IP \#define MQTT\_PORT 1883 // MQTT端口 \#define MQTT\_USER "admin" // MQTT账号 \#define MQTT\_PWD "123456" // MQTT密码 // 引脚定义(与原方案一致) \#define VS1053\_CS\_PIN GPIO\_PORT\_A, GPIO\_PIN\_8 \#define VS1053\_DREQ\_PIN GPIO\_PORT\_A, GPIO\_PIN\_9 \#define VS1053\_RST\_PIN GPIO\_PORT\_A, GPIO\_PIN\_10 \#define RECORD\_KEY\_PIN GPIO\_PORT\_A, GPIO\_PIN\_0 \#define BC35\_PWRKEY\_PIN GPIO\_PORT\_A, GPIO\_PIN\_2 // BC35-G开关机引脚 // 全局变量 uint8\_t g\_voice\_buf\[VOICE\_BUF\_LEN]; // 语音数据缓存 uint16\_t g\_voice\_len = 0; // 语音数据长度 uint8\_t g\_recv\_buf\[2048]; // BC35-G接收缓存 uint16\_t g\_recv\_idx = 0; // 接收索引(2)BC35-G 初始化函数(自带 MQTT 适配)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* BC35-G开关机函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ void BC35\_PowerOn(void) {   stc\_gpio\_init\_t stcGpioInit;   GPIO\_StructInit(\&stcGpioInit);   stcGpioInit.u16PinDir = PIN\_DIR\_OUT;   GPIO\_Init(BC35\_PWRKEY\_PIN, \&stcGpioInit);       // 低电平触发开机(持续1.2秒)   GPIO\_ResetPins(BC35\_PWRKEY\_PIN);   delay\_ms(1200);   GPIO\_SetPins(BC35\_PWRKEY\_PIN);   delay\_ms(5000); // 等待模组入网 } /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 发送AT指令并等待响应(BC35-G适配) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ bool BC35\_SendATCmd(char \*cmd, char \*resp, uint32\_t timeout) {   uint32\_t start = millis();   g\_recv\_idx = 0;   memset(g\_recv\_buf, 0, sizeof(g\_recv\_buf));   // 发送AT指令(BC35-G需加\r\n)   UART\_SendString(NB\_UART, cmd);   delay\_ms(10);   // 等待响应(BC35-G响应格式:OK/ERROR/+QMTCONN: 0,0)   while(millis() - start < timeout)   {   if(g\_recv\_idx > 0 && strstr((char\*)g\_recv\_buf, resp) != NULL)   {   printf("AT Success: %s\r\n", cmd);   return true;   }   delay\_ms(10);   }   printf("AT Fail: %s, Recv: %s\r\n", cmd, g\_recv\_buf);   return false; } /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* BC35-G MQTT初始化(核心:自带协议栈) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ bool BC35\_MQTT\_Init(void) {   // 1. 开机并测试AT指令   BC35\_PowerOn();   if(!BC35\_SendATCmd("AT\r\n", "OK", 1000)) return false;   // 2. 关闭回显、附着NB-IoT网络   if(!BC35\_SendATCmd("ATE0\r\n", "OK", 1000)) return false;   if(!BC35\_SendATCmd("AT+CGATT=1\r\n", "OK", 8000)) return false; // 附着网络(最长8秒)   // 3. 配置NB-IoT APN(移动:cmnbiot,联通:unilink)   if(!BC35\_SendATCmd("AT+CGDCONT=1,\\"IP\\",\\"cmnbiot\\"\r\n", "OK", 2000)) return false;   // 4. 配置MQTT Broker(BC35-G自带MQTT协议栈)   char mqtt\_cfg\_broker\[128];   sprintf(mqtt\_cfg\_broker, "AT+QMTCFG=\\"broker\\",0,\\"%s\\",%d\r\n", MQTT\_BROKER, MQTT\_PORT);   if(!BC35\_SendATCmd(mqtt\_cfg\_broker, "OK", 2000)) return false;   // 5. 配置MQTT登录参数(账号密码)   char mqtt\_cfg\_auth\[128];   sprintf(mqtt\_cfg\_auth, "AT+QMTCFG=\\"auth\\",0,\\"%s\\",\\"%s\\"\r\n", MQTT\_USER, MQTT\_PWD);   if(!BC35\_SendATCmd(mqtt\_cfg\_auth, "OK", 2000)) return false;   // 6. 建立MQTT连接(模组自动封装MQTT CONNECT报文)   char mqtt\_conn\[128];   sprintf(mqtt\_conn, "AT+QMTCONN=0,\\"%s\\"\r\n", DEV\_ID); // 0=客户端ID,DEV\_ID=设备ID   if(!BC35\_SendATCmd(mqtt\_conn, "+QMTCONN: 0,0", 10000)) return false; // 0,0=连接成功   // 7. 订阅下行语音主题(模组自动处理MQTT SUBSCRIBE)   char mqtt\_sub\[128];   sprintf(mqtt\_sub, "AT+QMTSUB=0,1,\\"iot/voice/device/%s/down\\",0\r\n", DEV\_ID);   if(!BC35\_SendATCmd(mqtt\_sub, "+QMTSUB: 0,1,0", 5000)) return false; // 0=订阅成功   return true; }(3)BC35-G 语音发布函数(简化版,模组自带 MQTT)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 封装语音帧并通过BC35-G发布(自带MQTT) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ bool BC35\_Publish\_Voice(uint8\_t \*voice\_data, uint16\_t voice\_len) {   // 1. 构造语音帧(保持原格式:AA55+设备ID+长度+数据+校验和)   uint8\_t frame\_buf\[VOICE\_BUF\_LEN + 12];   uint16\_t frame\_len = 2 + 8 + 2 + voice\_len + 1;   frame\_buf\[0] = 0xAA;   frame\_buf\[1] = 0x55;   memcpy(\&frame\_buf\[2], DEV\_ID, 8); // 设备ID(8字节)   frame\_buf\[10] = (voice\_len >> 8) & 0xFF;   frame\_buf\[11] = voice\_len & 0xFF;   memcpy(\&frame\_buf\[12], voice\_data, voice\_len);   // 计算校验和   uint8\_t checksum = 0;   for(uint16\_t i=2; i<12+voice\_len; i++) checksum ^= frame\_buf\[i];   frame\_buf\[12+voice\_len] = checksum;   // 2. 转换为十六进制字符串(BC35-G仅支持字符串发布,适配NB-IoT低带宽)   char hex\_buf\[VOICE\_BUF\_LEN\*2 + 24];   memset(hex\_buf, 0, sizeof(hex\_buf));   for(uint16\_t i=0; i\<frame\_len; i++)   {   sprintf(\&hex\_buf\[i\*2], "%02X", frame\_buf\[i]);   }   // 3. MQTT发布(BC35-G自动封装MQTT PUBLISH报文)   char mqtt\_pub\[2048];   sprintf(mqtt\_pub, "AT+QMTPUB=0,0,0,0,\\"iot/voice/device/%s/upload\\",\\"%s\\"\r\n", DEV\_ID, hex\_buf);   return BC35\_SendATCmd(mqtt\_pub, "+QMTPUB: 0,0", 10000); // +QMTPUB:0,0=发布成功 }(4)HC32F460 主函数(适配 BC35-G)
int main(void) {   System\_Init(); // 时钟/UART/SPI/VS1053初始化(与原方案一致)   printf("HC32F460 + BC35-G Voice System Start\r\n");   // 初始化BC35-G(自带MQTT)   if(!BC35\_MQTT\_Init())   {   printf("BC35-G MQTT Init Fail\r\n");   while(1);   }   while(1)   {   // 录音按键触发(PA0按下)   if(GPIO\_GetInputPin(RECORD\_KEY\_PIN) == Reset)   {   delay\_ms(20); // 消抖   if(GPIO\_GetInputPin(RECORD\_KEY\_PIN) == Reset)   {   printf("Start Record...\r\n");   // 采集语音(VS1053函数与原方案一致)   g\_voice\_len = VS1053\_Record\_Voice(g\_voice\_buf, VOICE\_BUF\_LEN);   // 通过BC35-G发布至私有云(自带MQTT)   if(BC35\_Publish\_Voice(g\_voice\_buf, g\_voice\_len))   {   printf("Voice Publish Success, Len: %d\r\n", g\_voice\_len);   }   else   {   printf("Voice Publish Fail\r\n");   }   while(GPIO\_GetInputPin(RECORD\_KEY\_PIN) == Reset); // 等待按键释放   }   }   // 处理BC35-G下行数据(订阅的语音消息)   if(g\_recv\_idx > 0 && strstr((char\*)g\_recv\_buf, "+QMTRECV:") != NULL)   {   // 解析模组推送的下行语音数据(与原方案一致)   Parse\_BC35\_DownData(g\_recv\_buf);   g\_recv\_idx = 0;   memset(g\_recv\_buf, 0, sizeof(g\_recv\_buf));   }   delay\_ms(10);   } }四、方案 2:ESP32-S3+BC35-G(自带 MQTT)实现
4.1 核心代码调整(Arduino 框架)
(1)头文件与宏定义(BC35-G 适配)
\#include \<Arduino.h> \#include \<SPI.h> \#include \<HardwareSerial.h> // 设备配置 \#define DEV\_ID "device\_001" \#define VOICE\_BUF\_LEN 4096 // 私有云MQTT配置(BC35-G直接对接) \#define MQTT\_BROKER "120.78.xxx.xxx" \#define MQTT\_PORT 1883 \#define MQTT\_USER "admin" \#define MQTT\_PWD "123456" // 引脚定义 \#define VS1053\_CS 5 \#define VS1053\_DREQ 6 \#define VS1053\_RST 7 \#define RECORD\_KEY 0 \#define BC35\_PWRKEY 8 // BC35-G开关机引脚 \#define NB\_UART Serial2 // 与BC35-G通信 \#define BAUD\_RATE 9600 // BC35-G默认波特率 // 全局变量 HardwareSerial \&bc35Serial = NB\_UART; uint8\_t g\_voice\_buf\[VOICE\_BUF\_LEN]; uint16\_t g\_voice\_len = 0; String bc35\_recv\_buf = "";(2)BC35-G MQTT 初始化函数(简化版)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* BC35-G开关机 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ void BC35\_PowerOn() {   pinMode(BC35\_PWRKEY, OUTPUT);   digitalWrite(BC35\_PWRKEY, LOW);   delay(1200); // 触发开机   digitalWrite(BC35\_PWRKEY, HIGH);   delay(5000); // 等待入网 } /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* BC35-G AT指令发送 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ bool BC35\_SendATCmd(String cmd, String resp, uint32\_t timeout) {   bc35\_recv\_buf = "";   bc35Serial.flush();   bc35Serial.print(cmd);   uint32\_t start = millis();   while(millis() - start < timeout)   {   while(bc35Serial.available())   {   bc35\_recv\_buf += char(bc35Serial.read());   }   if(bc35\_recv\_buf.indexOf(resp) != -1)   {   Serial.printf("AT OK: %s\r\n", cmd.c\_str());   return true;   }   delay(10);   }   Serial.printf("AT Fail: %s, Recv: %s\r\n", cmd.c\_str(), bc35\_recv\_buf.c\_str());   return false; } /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* BC35-G MQTT初始化(自带协议栈) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ bool BC35\_MQTT\_Init() {   bc35Serial.begin(BAUD\_RATE, SERIAL\_8N1, 17, 16); // RX, TX   BC35\_PowerOn();   // 基础配置(与HC32逻辑一致,简化为Arduino语法)   if(!BC35\_SendATCmd("AT\r\n", "OK", 1000)) return false;   if(!BC35\_SendATCmd("ATE0\r\n", "OK", 1000)) return false;   if(!BC35\_SendATCmd("AT+CGATT=1\r\n", "OK", 8000)) return false;   if(!BC35\_SendATCmd("AT+CGDCONT=1,\\"IP\\",\\"cmnbiot\\"\r\n", "OK", 2000)) return false;   // 配置MQTT Broker(自带协议栈)   String mqtt\_cfg = String("AT+QMTCFG=\\"broker\\",0,\\"") + MQTT\_BROKER + "\\"," + String(MQTT\_PORT) + "\r\n";   if(!BC35\_SendATCmd(mqtt\_cfg, "OK", 2000)) return false;   // MQTT登录   String mqtt\_auth = String("AT+QMTCFG=\\"auth\\",0,\\"") + MQTT\_USER + "\\",\\"" + MQTT\_PWD + "\\"\r\n";   if(!BC35\_SendATCmd(mqtt\_auth, "OK", 2000)) return false;   // 建立MQTT连接   String mqtt\_conn = String("AT+QMTCONN=0,\\"") + DEV\_ID + "\\"\r\n";   if(!BC35\_SendATCmd(mqtt\_conn, "+QMTCONN: 0,0", 10000)) return false;   // 订阅下行主题   String mqtt\_sub = String("AT+QMTSUB=0,1,\\"iot/voice/device/") + DEV\_ID + "/down\\",0\r\n";   if(!BC35\_SendATCmd(mqtt\_sub, "+QMTSUB: 0,1,0", 5000)) return false;   return true; }(3)语音发布函数(BC35-G 自带 MQTT)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 发布语音至私有云(BC35-G适配) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/ bool BC35\_Publish\_Voice(uint8\_t \*voice\_data, uint16\_t voice\_len) {   // 构造语音帧(与HC32一致)   uint8\_t frame\[VOICE\_BUF\_LEN + 12];   frame\[0] = 0xAA;   frame\[1] = 0x55;   memcpy(\&frame\[2], DEV\_ID, 8);   frame\[10] = (voice\_len >> 8) & 0xFF;   frame\[11] = voice\_len & 0xFF;   memcpy(\&frame\[12], voice\_data, voice\_len);   // 计算校验和   uint8\_t checksum = 0;   for(uint16\_t i=2; i<12+voice\_len; i++) checksum ^= frame\[i];   frame\[12+voice\_len] = checksum;   // 转十六进制字符串(BC35-G发布要求)   String hex\_str = "";   for(int i=0; i<12+voice\_len+1; i++)   {   hex\_str += String(frame\[i], HEX);   }   // MQTT发布(模组自动封装报文)   String mqtt\_pub = String("AT+QMTPUB=0,0,0,0,\\"iot/voice/device/") + DEV\_ID + "/upload\\",\\"" + hex\_str + "\\"\r\n";   return BC35\_SendATCmd(mqtt\_pub, "+QMTPUB: 0,0", 10000); }(4)ESP32-S3 主函数
void setup() {   Serial.begin(115200);   pinMode(RECORD\_KEY, INPUT\_PULLUP);   VS1053\_Init(); // 语音模块初始化(与原方案一致)   Serial.println("ESP32-S3 + BC35-G Voice System Start");   if(!BC35\_MQTT\_Init())   {   Serial.println("BC35-G MQTT Init Fail");   while(1);   } } void loop() {   // 录音按键触发   if(digitalRead(RECORD\_KEY) == LOW)   {   delay(20);   if(digitalRead(RECORD\_KEY) == LOW)   {   Serial.println("Start Record...");   g\_voice\_len = VS1053\_Record\_Voice(g\_voice\_buf, VOICE\_BUF\_LEN);   if(BC35\_Publish\_Voice(g\_voice\_buf, g\_voice\_len))   {   Serial.printf("Publish Success, Len: %d\r\n", g\_voice\_len);   }   else   {   Serial.println("Publish Fail");   }   while(digitalRead(RECORD\_KEY) == LOW);   }   }   // 处理BC35-G下行语音数据   Parse\_BC35\_DownData();   delay(10); }五、私有云服务器(Python)与微信小程序(无调整)
5.1 服务器端代码(完全兼容)
BC35-G 仅作为 MQTT 客户端,发布 / 订阅的主题和数据格式与原方案一致,因此 Python 服务代码无需修改,核心逻辑:
EMQ X 接收 BC35-G 发布的语音数据;
Python 服务解析语音帧,通过 WebSocket 推送给微信小程序;
接收小程序上传的语音,转发至 BC35-G 订阅的下行主题。
5.2 微信小程序(完全兼容)
小程序仅与私有云 WebSocket 通信,无需感知终端侧的 NB-IoT 模组类型,因此页面结构、逻辑层、样式层代码均无需修改。
六、BC35-G(自带 MQTT)调试关键指令
| 指令 | 功能说明 | 正常响应 |
|---|---|---|
| AT | 测试模组通信 | OK |
| AT+CGATT? | 查看网络附着状态 | +CGATT: 1(附着成功) |
| AT+CSQ | 查看 NB-IoT 信号强度 | +CSQ: 15,99(≥10 正常) |
| AT+QMTCONN? | 查看 MQTT 连接状态 | +QMTCONN: 0,0(已连接) |
| AT+QMTPUB=… | 发布 MQTT 消息 | +QMTPUB: 0,0(发布成功) |
| AT+QMTSUB=… | 订阅 MQTT 主题 | +QMTSUB: 0,1,0(订阅成功) |
七、核心优势总结(自带 MQTT 的 NB-IoT 模组)
开发难度大幅降低:主控无需封装 MQTT 协议栈(无需处理固定头、可变头、报文标识符等),仅需发送标准化 AT 指令;
主控资源占用极低:HC32F460/ESP32-S3 仅需处理语音采集 / 播放和 AT 指令,无需分配资源给 MQTT 协议栈;
通信稳定性提升:模组原生适配 NB-IoT 低带宽、高延迟特性,MQTT 报文重传、心跳由模组自动处理;
功耗优化:BC35-G 支持 PSM(省电模式),配合主控休眠,终端整体功耗可降至 μA 级,适配电池供电场景。
八、扩展建议
MQTT 心跳优化:通过
AT+QMTCFG="keepalive",0,300配置 BC35-G 的 MQTT 心跳(300 秒),降低功耗;数据加密:通过
AT+QMTCFG="ssl",0,1开启 BC35-G 的 SSL/TLS 加密,保护语音数据传输;多模组适配:自带 MQTT 的 NB-IoT 模组(如有人 NB73、中移物联 M5310)均可复用本方案,仅需调整 AT 指令前缀;
离线缓存:利用 BC35-G 的本地缓存功能,在网络中断时暂存语音数据,恢复后自动补发。