HC32F460+ESP32-S3+NB-IoT 语音聊天系统设计(附微信小程序)
一、系统整体方案设计
1.1 系统架构
本系统实现 “端 - 网 - 云 - 小程序” 全链路语音消息交互,核心分为 4 层架构:
| 层级 | 核心设备 / 模块 | 核心功能 |
|---|---|---|
| 终端硬件层 | HC32F460+ESP32-S3+NB-IoT(BC28)+VS1053 | 语音采集 / 播放、协议透传、NB-IoT 网络接入 |
| 网络层 | NB-IoT 运营商网络 + MQTT 协议 | 语音数据上行 / 下行传输,基于 OneNET 平台实现设备与小程序的消息转发 |
| 云端层 | 中国移动 OneNET 物联网平台 | 设备管理、语音数据存储、消息转发(设备 <-> 小程序) |
| 应用层 | 微信小程序 | 语音录制 / 播放、消息展示、与终端的双向语音聊天 |
1.2 核心业务流程
-
终端侧:HC32F460 通过 VS1053 采集语音→编码为 PCM/MP3→UART 发送至 ESP32-S3→ESP32-S3 通过 AT 指令控制 NB-IoT 模块,以 MQTT 协议上传至 OneNET;
-
云端侧:OneNET 接收终端语音数据,触发消息推送至微信小程序;
-
小程序侧:接收语音数据→解码播放;小程序录制语音→上传至 OneNET→OneNET 下发至终端→ESP32-S3 透传至 HC32F460→VS1053 播放语音;
-
双向交互:基于设备 / 小程序的唯一标识(DeviceID/OpenID)实现点对点语音聊天。
二、硬件方案设计
2.1 硬件选型
| 模块 / 芯片 | 型号 | 核心作用 |
|---|---|---|
| 主控 1(语音处理) | HC32F460 | 控制 VS1053 完成语音采集 / 播放,与 ESP32-S3 串口通信 |
| 主控 2(网络透传) | ESP32-S3 | 串口透传、NB-IoT 模块 AT 指令控制、MQTT 协议封装 |
| NB-IoT 模块 | 移远 BC28 | 接入 NB-IoT 网络,实现与 OneNET 的 MQTT 通信 |
| 语音编解码模块 | VS1053 | 语音采集(ADC)、播放(DAC),支持 MP3/PCM 编码解码 |
| 音频输入输出 | 麦克风 + 扬声器 | 语音采集与播放 |
2.2 硬件连接原理图(核心引脚)
(1)HC32F460 ↔ VS1053(SPI 通信)
| HC32F460 引脚 | VS1053 引脚 | 功能说明 |
|---|---|---|
| PB13 | SCK | SPI 时钟 |
| PB14 | MISO | SPI 数据输入 |
| PB15 | MOSI | SPI 数据输出 |
| PA8 | CS | 片选(低有效) |
| PA9 | DREQ | 数据请求中断 |
| PA10 | RST | 复位(低有效) |
(2)HC32F460 ↔ ESP32-S3(UART 通信)
| HC32F460 引脚 | ESP32-S3 引脚 | 功能说明 |
|---|---|---|
| PD0 | TXD2 | HC32 发送→ESP32 接收 |
| PD1 | RXD2 | HC32 接收→ESP32 发送 |
| 共地 | GND | 电平匹配 |
(3)ESP32-S3 ↔ BC28(NB-IoT)(UART 通信)
| ESP32-S3 引脚 | BC28 引脚 | 功能说明 |
|---|---|---|
| TXD1 | RXD | ESP32 发送→BC28 接收 |
| RXD1 | TXD | ESP32 接收→BC28 发送 |
| EN | PWRKEY | BC28 开关机控制 |
| GND | GND | 共地 |
三、软件方案设计(分模块实现)
3.1 核心通信协议定义
(1)HC32F460 ↔ ESP32-S3 串口帧格式
帧头(2字节) | 数据长度(2字节) | 数据类型(1字节) | 语音数据(N字节) | 校验和(1字节)
0xAA55 | 0000\~FFFF | 0x01:语音上传 0x02:语音播放 | PCM/MP3数据 | 所有字节异或
(2)MQTT 通信主题(OneNET 平台)
-
终端上行:
topic/device/{DeviceID}/voice/upload(终端→云端) -
终端下行:
topic/device/{DeviceID}/voice/down(云端→终端) -
小程序上行:
topic/app/{OpenID}/voice/upload(小程序→云端) -
小程序下行:
topic/app/{OpenID}/voice/down(云端→小程序)
3.2 HC32F460 端代码实现(基于 Keil MDK)
(1)工程初始化(时钟 + UART+SPI+VS1053)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 头文件包含 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
\#include "hc32f460\_clk.h"
\#include "hc32f460\_uart.h"
\#include "hc32f460\_spi.h"
\#include "vs1053.h"
\#include "uart\_protocol.h"
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 全局变量 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
\#define UART\_CHN HC32F460\_UART2 // 与ESP32通信的串口
\#define SPI\_CHN HC32F460\_SPI2 // 与VS1053通信的SPI
uint8\_t g\_voice\_buf\[4096]; // 语音数据缓存
uint16\_t g\_voice\_len = 0; // 语音数据长度
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 时钟初始化函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void CLK\_Init(void)
{
  stc\_clk\_pll\_cfg\_t stcPllCfg;
  // 配置PLL:XTAL(8MHz) → PLL → 180MHz系统时钟
  CLK\_PLLStructInit(\&stcPllCfg);
  stcPllCfg.pllSrc = CLK\_PLL\_SRC\_XTAL;
  stcPllCfg.pllMul = 45; // 8\*45/2=180MHz
  CLK\_PLLConfig(\&stcPllCfg);
  CLK\_PLLCmd(Enable);
  // 等待PLL稳定
  while(CLK\_GetFlagStatus(CLK\_FLAG\_PLLSTB) != Set);
  // 切换系统时钟至PLL
  CLK\_SysClkSwitch(CLK\_SYSCLK\_SRC\_PLL);
  // 配置外设时钟:UART2/SPI2时钟使能
  CLK\_PeriphClockCmd(CLK\_PERIPH\_UART2, Enable);
  CLK\_PeriphClockCmd(CLK\_PERIPH\_SPI2, Enable);
  CLK\_PeriphClockCmd(CLK\_PERIPH\_GPIO, Enable);
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* UART初始化函数(与ESP32通信) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void UART\_Init(void)
{
  stc\_uart\_init\_t stcUartInit;
  stc\_gpio\_init\_t stcGpioInit;
  // 配置PD0(TX)、PD1(RX)为UART2功能
  GPIO\_StructInit(\&stcGpioInit);
  stcGpioInit.u16PinDir = PIN\_DIR\_OUT;
  GPIO\_Init(GPIO\_PORT\_D, GPIO\_PIN\_0, \&stcGpioInit);
  GPIO\_SetFunc(GPIO\_PORT\_D, GPIO\_PIN\_0, GPIO\_FUNC\_3); // TX
  stcGpioInit.u16PinDir = PIN\_DIR\_IN;
  GPIO\_Init(GPIO\_PORT\_D, GPIO\_PIN\_1, \&stcGpioInit);
  GPIO\_SetFunc(GPIO\_PORT\_D, GPIO\_PIN\_1, GPIO\_FUNC\_3); // RX
  // UART参数初始化:115200 8N1
  UART\_StructInit(\&stcUartInit);
  stcUartInit.u32Baudrate = 115200;
  stcUartInit.u32Parity = UART\_PARITY\_NONE;
  stcUartInit.u32StopBits = UART\_STOPBIT\_1;
  stcUartInit.u32DataBits = UART\_DATA\_BIT\_8;
  stcUartInit.u32ClkMode = UART\_CLK\_MODE0;
  UART\_Init(UART\_CHN, \&stcUartInit);
  // 使能UART接收和发送
  UART\_FuncCmd(UART\_CHN, UART\_FUNC\_RX | UART\_FUNC\_TX, Enable);
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* VS1053初始化函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void VS1053\_Init(void)
{
  // 1. 复位VS1053
  GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_10); // RST置高
  delay\_ms(10);
  GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_10); // RST置低
  delay\_ms(10);
  GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_10); // 释放复位
  delay\_ms(50);
  // 2. SPI初始化(主机模式,CPOL=0, CPHA=0)
  stc\_spi\_init\_t stcSpiInit;
  SPI\_StructInit(\&stcSpiInit);
  stcSpiInit.u32ClockDiv = SPI\_CLK\_DIV4; // 180/4=45MHz
  stcSpiInit.u32WireMode = SPI\_WIRE\_MODE\_4;
  stcSpiInit.u32MasterSlave = SPI\_MASTER;
  stcSpiInit.u32CPOL = SPI\_CPOL\_LOW;
  stcSpiInit.u32CPHA = SPI\_CPHA\_FIRST;
  SPI\_Init(SPI\_CHN, \&stcSpiInit);
  // 3. 配置VS1053寄存器(设置为MP3编码模式,采样率8kHz)
  VS1053\_WriteReg(VS1053\_REG\_MODE, 0x0800); // 正常模式,不复位
  VS1053\_WriteReg(VS1053\_REG\_BASS, 0x00FF); // 低音增强
  VS1053\_WriteReg(VS1053\_REG\_CLOCKF, 0x8000); // 时钟配置
  delay\_ms(10);
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 系统初始化入口函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void System\_Init(void)
{
  CLK\_Init();
  UART\_Init();
  VS1053\_Init();
  NVIC\_EnableIRQ(UART2\_IRQn); // 使能UART2中断(接收ESP32数据)
}
(2)语音采集函数(VS1053 采集 MP3 数据)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* VS1053写寄存器函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void VS1053\_WriteReg(uint8\_t reg, uint16\_t value)
{
  GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置低
  // 发送写命令+寄存器地址
  SPI\_SendData(SPI\_CHN, 0x02); 
  SPI\_SendData(SPI\_CHN, reg);
  // 发送寄存器值(高字节+低字节)
  SPI\_SendData(SPI\_CHN, (value >> 8) & 0xFF);
  SPI\_SendData(SPI\_CHN, value & 0xFF);
  while(SPI\_GetStatus(SPI\_CHN, SPI\_STATUS\_BUSY));
  GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置高
  delay\_ms(1);
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 语音采集函数(返回采集长度) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
uint16\_t VS1053\_Record\_Voice(uint8\_t \*buf, uint16\_t max\_len)
{
  uint16\_t len = 0;
  // 1. 设置VS1053为录音模式(MP3,8kHz采样率)
  VS1053\_WriteReg(VS1053\_REG\_MODE, 0x1800); // 录音模式
  VS1053\_WriteReg(VS1053\_REG\_AICTRL0, 0x0008); // 8kHz采样率
  VS1053\_WriteReg(VS1053\_REG\_AICTRL1, 0x0000); // 单声道
  VS1053\_WriteReg(VS1053\_REG\_AICTRL2, 0x0000); // 比特率64kbps
  delay\_ms(10);
  // 2. 等待DREQ中断(数据就绪),读取录音数据
  while(len < max\_len)
  {
  if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_9) == Set) // DREQ置高,数据就绪
  {
  GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置低
  SPI\_SendData(SPI\_CHN, 0x03); // 读数据命令
  while(SPI\_GetStatus(SPI\_CHN, SPI\_STATUS\_BUSY));
  buf\[len++] = SPI\_ReceiveData(SPI\_CHN); // 读取1字节
  GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置高
  if(len % 512 == 0) // 每512字节暂停,避免缓存溢出
  {
  delay\_ms(1);
  }
  }
  }
  // 3. 停止录音
  VS1053\_WriteReg(VS1053\_REG\_MODE, 0x0800); // 恢复播放模式
  return len;
}
(3)串口协议封装与发送函数(向 ESP32 发送语音数据)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 校验和计算函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
uint8\_t Calc\_Checksum(uint8\_t \*data, uint16\_t len)
{
  uint8\_t checksum = 0;
  for(uint16\_t i=0; i\<len; i++)
  {
  checksum ^= data\[i];
  }
  return checksum;
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 语音数据封装并发送至ESP32 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void UART\_Send\_Voice\_Data(uint8\_t \*voice\_data, uint16\_t voice\_len)
{
  uint8\_t send\_buf\[4096+6]; // 帧头(2)+长度(2)+类型(1)+数据(N)+校验(1)
  uint16\_t total\_len = 2+2+1+voice\_len+1;
  // 1. 填充帧头
  send\_buf\[0] = 0xAA;
  send\_buf\[1] = 0x55;
  // 2. 填充数据长度(大端序)
  send\_buf\[2] = (voice\_len >> 8) & 0xFF;
  send\_buf\[3] = voice\_len & 0xFF;
  // 3. 填充数据类型(0x01:语音上传)
  send\_buf\[4] = 0x01;
  // 4. 填充语音数据
  memcpy(\&send\_buf\[5], voice\_data, voice\_len);
  // 5. 计算并填充校验和
  send\_buf\[5+voice\_len] = Calc\_Checksum(\&send\_buf\[2], 1+voice\_len);
  // 6. 串口发送
  for(uint16\_t i=0; i\<total\_len; i++)
  {
  while(UART\_GetStatus(UART\_CHN, UART\_STATUS\_TXBF) == Set); // 等待发送缓冲区空
  UART\_SendData(UART\_CHN, send\_buf\[i]);
  }
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* UART接收中断函数(接收ESP32下发的语音数据) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void UART2\_IRQHandler(void)
{
  static uint8\_t recv\_buf\[4096+6];
  static uint16\_t recv\_idx = 0;
  uint8\_t recv\_data;
  if(UART\_GetIrqStatus(UART\_CHN, UART\_IRQ\_RXDNE) == Set)
  {
  recv\_data = UART\_ReceiveData(UART\_CHN);
  // 帧头检测
  if(recv\_idx == 0 && recv\_data != 0xAA) return;
  if(recv\_idx == 1 && recv\_data != 0x55) {recv\_idx=0; return;}
  recv\_buf\[recv\_idx++] = recv\_data;
  // 接收完成(帧头+长度+类型+数据+校验)
  if(recv\_idx >= 5)
  {
  uint16\_t data\_len = (recv\_buf\[2] << 8) | recv\_buf\[3];
  if(recv\_idx >= 5 + data\_len + 1)
  {
  // 校验和验证
  uint8\_t checksum = Calc\_Checksum(\&recv\_buf\[2], 1+data\_len);
  if(checksum == recv\_buf\[5+data\_len])
  {
  // 数据类型为0x02:语音播放
  if(recv\_buf\[4] == 0x02)
  {
  // 调用VS1053播放语音
  VS1053\_Play\_Voice(\&recv\_buf\[5], data\_len);
  }
  }
  recv\_idx = 0; // 重置接收索引
  }
  }
  }
  UART\_ClearIrqStatus(UART\_CHN, UART\_IRQ\_RXDNE);
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* VS1053播放语音函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void VS1053\_Play\_Voice(uint8\_t \*buf, uint16\_t len)
{
  uint16\_t i = 0;
  // 设置VS1053为播放模式
  VS1053\_WriteReg(VS1053\_REG\_MODE, 0x0800);
  delay\_ms(10);
  // 逐字节发送语音数据至VS1053
  while(i < len)
  {
  if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_9) == Set) // DREQ置高,可写数据
  {
  GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置低
  SPI\_SendData(SPI\_CHN, buf\[i++]);
  while(SPI\_GetStatus(SPI\_CHN, SPI\_STATUS\_BUSY));
  GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置高
  }
  }
  // 发送播放结束标志
  for(uint8\_t j=0; j<10; j++)
  {
  if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_9) == Set)
  {
  GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_8);
  SPI\_SendData(SPI\_CHN, 0x00);
  while(SPI\_GetStatus(SPI\_CHN, SPI\_STATUS\_BUSY));
  GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_8);
  }
  }
}
(4)HC32F460 主函数
int main(void)
{
  System\_Init(); // 系统初始化
  while(1)
  {
  // 检测录音按键(示例:PA0按键按下开始录音)
  if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_0) == Reset)
  {
  delay\_ms(20); // 消抖
  if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_0) == Reset)
  {
  // 采集语音(最大4096字节)
  g\_voice\_len = VS1053\_Record\_Voice(g\_voice\_buf, 4096);
  // 发送至ESP32
  UART\_Send\_Voice\_Data(g\_voice\_buf, g\_voice\_len);
  }
  while(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_0) == Reset); // 等待按键释放
  }
  delay\_ms(10);
  }
}
3.3 ESP32-S3 端代码实现(基于 Arduino 框架)
(1)串口初始化 + NB-IoT AT 指令封装
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 宏定义 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
\#define NB\_UART Serial1 // ESP32与BC28通信的串口
\#define HC\_UART Serial2 // ESP32与HC32通信的串口
\#define BC28\_PWRKEY 2 // BC28开关机引脚
\#define BAUD\_RATE 115200 // 串口波特率
// OneNET平台配置
\#define MQTT\_SERVER "183.230.40.39" // OneNET MQTT服务器
\#define MQTT\_PORT 6002 // MQTT端口
\#define PRODUCT\_ID "your\_product\_id"// 产品ID
\#define DEVICE\_ID "your\_device\_id" // 设备ID
\#define API\_KEY "your\_api\_key" // 设备API Key
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 全局变量 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
String recv\_hc\_data = ""; // 接收HC32的数据缓存
String recv\_nb\_data = ""; // 接收BC28的数据缓存
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* BC28初始化函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void BC28\_Init(void)
{
  // 1. 配置PWRKEY引脚,开机BC28
  pinMode(BC28\_PWRKEY, OUTPUT);
  digitalWrite(BC28\_PWRKEY, LOW);
  delay(1000);
  digitalWrite(BC28\_PWRKEY, HIGH);
  delay(5000); // 等待BC28开机
  // 2. 初始化串口
  NB\_UART.begin(BAUD\_RATE);
  HC\_UART.begin(BAUD\_RATE);
  // 3. AT指令配置BC28(关闭回显+设置NB网络+配置MQTT)
  sendATCmd("AT\r\n", "OK", 1000); // 测试AT指令
  sendATCmd("ATE0\r\n", "OK", 1000); // 关闭回显
  sendATCmd("AT+CGATT=1\r\n", "OK", 5000);// 附着网络
  sendATCmd("AT+CGDCONT=1,\\"IP\\",\\"cmnbiot\\"\r\n", "OK", 2000); // 设置APN
  sendATCmd("AT+QMTCFG=\\"aliauth\\",0,\\"" PRODUCT\_ID "\\",\\"" DEVICE\_ID "\\",\\"" API\_KEY "\\"\r\n", "OK", 2000); // MQTT鉴权
  sendATCmd("AT+QMTOPEN=0,\\"" MQTT\_SERVER "\\"," String(MQTT\_PORT) "\r\n", "CONNECT OK", 5000); // 连接MQTT服务器
  sendATCmd("AT+QMTCONN=0,\\"" DEVICE\_ID "\\"\r\n", "CONNACK\_OK", 5000); // 连接MQTT主题
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* AT指令发送与响应解析函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
bool sendATCmd(String cmd, String resp, uint32\_t timeout)
{
  NB\_UART.flush();
  NB\_UART.print(cmd); // 发送AT指令
  uint32\_t start = millis();
  String recv = "";
  while(millis() - start < timeout)
  {
  if(NB\_UART.available())
  {
  recv += char(NB\_UART.read());
  if(recv.indexOf(resp) != -1) // 匹配响应
  {
  Serial.println("AT Cmd Success: " + cmd);
  return true;
  }
  }
  }
  Serial.println("AT Cmd Fail: " + cmd + ", Recv: " + recv);
  return false;
}
(2)MQTT 数据上传 / 接收函数
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 语音数据上传至OneNET \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void uploadVoiceToOneNET(uint8\_t \*data, uint16\_t len)
{
  // 1. 将二进制数据转为十六进制字符串(NB-IoT传输兼容)
  String hex\_data = "";
  for(uint16\_t i=0; i\<len; i++)
  {
  hex\_data += String(data\[i], HEX);
  if(i % 2 == 1) hex\_data += " "; // 每2字节加空格,便于解析
  }
  // 2. MQTT发布指令
  String topic = "topic/device/" DEVICE\_ID "/voice/upload";
  String cmd = "AT+QMTPUB=0,0,0,0,\\"" + topic + "\\",\\"" + hex\_data + "\\"\r\n";
  sendATCmd(cmd, "PUBACK", 5000);
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* NB-IoT数据接收解析函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void parseNBData(void)
{
  if(NB\_UART.available())
  {
  recv\_nb\_data += char(NB\_UART.read());
  // 检测MQTT下行数据(+QMTRECV: 0,0,"topic/...","data")
  if(recv\_nb\_data.indexOf("+QMTRECV:") != -1)
  {
  // 解析主题和数据
  int topic\_start = recv\_nb\_data.indexOf("\\"") + 1;
  int topic\_end = recv\_nb\_data.indexOf("\\"", topic\_start);
  String topic = recv\_nb\_data.substring(topic\_start, topic\_end);
  int data\_start = recv\_nb\_data.indexOf("\\"", topic\_end+1) + 1;
  int data\_end = recv\_nb\_data.indexOf("\\"", data\_start);
  String hex\_data = recv\_nb\_data.substring(data\_start, data\_end);
  // 主题匹配:终端下行语音
  if(topic == "topic/device/" DEVICE\_ID "/voice/down")
  {
  // 十六进制字符串转二进制数据
  uint8\_t voice\_data\[4096];
  uint16\_t voice\_len = hex2bin(hex\_data, voice\_data);
  // 封装串口帧,发送至HC32
  sendHC32Data(voice\_data, voice\_len, 0x02);
  }
  recv\_nb\_data = ""; // 清空缓存
  }
  }
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 十六进制字符串转二进制函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
uint16\_t hex2bin(String hex, uint8\_t \*bin)
{
  uint16\_t len = 0;
  for(uint16\_t i=0; i\<hex.length(); i+=3) // 每3字符(2位16进制+空格)
  {
  String hex\_byte = hex.substring(i, i+2);
  bin\[len++] = strtol(hex\_byte.c\_str(), NULL, 16);
  }
  return len;
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 向HC32发送数据(封装串口帧) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void sendHC32Data(uint8\_t \*data, uint16\_t len, uint8\_t type)
{
  uint8\_t send\_buf\[4096+6];
  // 帧头
  send\_buf\[0] = 0xAA;
  send\_buf\[1] = 0x55;
  // 数据长度(大端)
  send\_buf\[2] = (len >> 8) & 0xFF;
  send\_buf\[3] = len & 0xFF;
  // 数据类型
  send\_buf\[4] = type;
  // 数据内容
  memcpy(\&send\_buf\[5], data, len);
  // 校验和
  send\_buf\[5+len] = calcChecksum(\&send\_buf\[2], 1+len);
  // 串口发送
  for(uint16\_t i=0; i<5+len+1; i++)
  {
  HC\_UART.write(send\_buf\[i]);
  }
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 校验和计算函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
uint8\_t calcChecksum(uint8\_t \*data, uint16\_t len)
{
  uint8\_t checksum = 0;
  for(uint16\_t i=0; i\<len; i++)
  {
  checksum ^= data\[i];
  }
  return checksum;
}
(3)HC32 数据接收与透传函数
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 接收HC32数据并透传至OneNET \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void parseHC32Data(void)
{
  if(HC\_UART.available())
  {
  recv\_hc\_data += char(HC\_UART.read());
  // 检测帧头0xAA55
  if(recv\_hc\_data.indexOf(0xAA) != -1 && recv\_hc\_data.indexOf(0x55) != -1)
  {
  // 解析帧格式:AA 55 \[lenH lenL] \[type] \[data] \[checksum]
  uint8\_t \*buf = (uint8\_t \*)recv\_hc\_data.c\_str();
  uint16\_t data\_len = (buf\[2] << 8) | buf\[3];
  uint8\_t data\_type = buf\[4];
  uint8\_t checksum = calcChecksum(\&buf\[2], 1+data\_len);
  // 校验和验证
  if(checksum == buf\[5+data\_len])
  {
  // 语音上传(类型0x01)
  if(data\_type == 0x01)
  {
  // 提取语音数据,上传至OneNET
  uploadVoiceToOneNET(\&buf\[5], data\_len);
  }
  }
  recv\_hc\_data = ""; // 清空缓存
  }
  }
}
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* ESP32主函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
void setup() {
  Serial.begin(115200); // 调试串口
  BC28\_Init(); // 初始化BC28
}
void loop() {
  parseHC32Data(); // 解析HC32数据
  parseNBData(); // 解析NB-IoT数据
  delay(10);
}
3.4 微信小程序端代码实现(基于微信原生框架)
(1)小程序页面结构(index.wxml)
\<view class="container">
  \<!-- 语音消息列表 -->
  \<scroll-view scroll-y class="msg-list">
  \<block wx:for="{{msgList}}" wx:key="index">
  \<!-- 终端发送的语音 -->
  \<view class="msg-item device-msg" wx:if="{{item.type == 'device'}}">
  \<button bindtap="playVoice" data-url="{{item.data}}">播放终端语音\</button>
  \<text>{{item.time}}\</text>
  \</view>
  \<!-- 小程序发送的语音 -->
  \<view class="msg-item app-msg" wx:else>
  \<button bindtap="playVoice" data-url="{{item.data}}">播放我的语音\</button>
  \<text>{{item.time}}\</text>
  \</view>
  \</block>
  \</scroll-view>
  \<!-- 录音按钮 -->
  \<view class="record-btn">
  \<button bindtouchstart="startRecord" bindtouchend="stopRecord">按住说话\</button>
  \</view>
\</view>
(2)小程序逻辑层(index.js)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 全局配置 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
const app = getApp();
const oneNETConfig = {
  host: '183.230.40.39',
  port: 80,
  productId: 'your\_product\_id',
  deviceId: 'your\_device\_id',
  apiKey: 'your\_api\_key'
};
Page({
  data: {
  msgList: \[], // 消息列表
  recorderManager: null, // 录音管理器
  voiceTempPath: "", // 临时语音文件路径
  websocket: null // WebSocket连接(对接OneNET)
  },
  /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 页面加载初始化 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
  onLoad(options) {
  // 1. 初始化录音管理器
  this.initRecorder();
  // 2. 连接OneNET WebSocket,监听下行消息
  this.connectWebSocket();
  },
  /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 初始化录音管理器 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
  initRecorder() {
  const recorderManager = wx.getRecorderManager();
  const that = this;
  // 录音配置
  recorderManager.onStart(() => {
  console.log('开始录音');
  });
  // 录音停止回调
  recorderManager.onStop((res) => {
  that.setData({ voiceTempPath: res.tempFilePath });
  // 上传语音至OneNET
  that.uploadVoiceToOneNET(res.tempFilePath);
  });
  this.setData({ recorderManager });
  },
  /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 连接OneNET WebSocket \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
  connectWebSocket() {
  const that = this;
  // OneNET WebSocket地址(需提前配置设备订阅)
  const wsUrl = \`ws://\${oneNETConfig.host}:\${oneNETConfig.port}/mqtt/websocket\`;
  const websocket = wx.connectSocket({
  url: wsUrl,
  header: {
  'content-type': 'application/json'
  }
  });
  // 连接成功
  websocket.onOpen(() => {
  console.log('WebSocket连接成功');
  // 订阅终端下行语音主题
  const subscribeMsg = {
  cmd: "subscribe",
  params: {
  topic: \`topic/device/\${oneNETConfig.deviceId}/voice/upload\`
  }
  };
  websocket.send({ data: JSON.stringify(subscribeMsg) });
  });
  // 接收消息
  websocket.onMessage((res) => {
  const msg = JSON.parse(res.data);
  // 终端上传的语音数据
  if(msg.topic === \`topic/device/\${oneNETConfig.deviceId}/voice/upload\`)
  {
  // 添加至消息列表
  that.setData({
  msgList: \[
  ...that.data.msgList,
  {
  type: 'device',
  data: msg.data,
  time: new Date().toLocaleTimeString()
  }
  ]
  });
  }
  });
  // 连接关闭
  websocket.onClose(() => {
  console.log('WebSocket连接关闭,重连中...');
  setTimeout(() => { that.connectWebSocket(); }, 3000);
  });
  this.setData({ websocket });
  },
  /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 开始录音 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
  startRecord() {
  const { recorderManager } = this.data;
  // 录音配置(MP3格式,8kHz采样率)
  recorderManager.start({
  format: 'mp3',
  sampleRate: 8000,
  numberOfChannels: 1,
  bitRate: 64000
  });
  },
  /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 停止录音 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
  stopRecord() {
  this.data.recorderManager.stop();
  },
  /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 上传语音至OneNET \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
  uploadVoiceToOneNET(tempPath) {
  const that = this;
  // 1. 读取语音文件为二进制
  wx.getFileSystemManager().readFile({
  filePath: tempPath,
  encoding: 'hex', // 转为十六进制字符串(与终端兼容)
  success(res) {
  // 2. 构造MQTT发布请求
  const publishData = {
  cmd: "publish",
  params: {
  topic: \`topic/device/\${oneNETConfig.deviceId}/voice/down\`,
  data: res.data,
  qos: 0
  }
  };
  // 3. 发送至OneNET
  that.data.websocket.send({ data: JSON.stringify(publishData) });
  // 4. 添加至消息列表
  that.setData({
  msgList: \[
  ...that.data.msgList,
  {
  type: 'app',
  data: tempPath,
  time: new Date().toLocaleTimeString()
  }
  ]
  });
  }
  });
  },
  /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 播放语音 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
  playVoice(e) {
  const voiceUrl = e.currentTarget.dataset.url;
  // 播放本地临时文件(小程序语音)或OneNET下载的语音(终端语音)
  wx.playVoice({
  filePath: voiceUrl,
  complete() {
  console.log('播放完成');
  }
  });
  }
});
四、OneNET 平台配置步骤
-
登录 OneNET 物联网平台(https://open.iot.10086.cn/);
-
创建产品:选择 “NB-IoT” 接入方式,数据格式选择 “自定义 JSON”;
-
添加设备:录入设备 IMEI(BC28 模块的 IMEI),生成 DeviceID 和 API Key;
-
配置 MQTT 主题:添加上行主题
topic/device/{DeviceID}/voice/upload和下行主题topic/device/{DeviceID}/voice/down; -
配置 WebSocket 推送:开启设备消息推送,关联小程序 OpenID,实现消息转发。
五、系统调试与注意事项
- 硬件调试:
-
确保 HC32F460 与 ESP32-S3 的串口电平匹配(均为 3.3V);
-
BC28 模块需插入 NB-IoT 卡,且所在区域有 NB-IoT 网络覆盖;
-
VS1053 的 DREQ 引脚需配置为中断,避免数据读写溢出。
- 软件调试:
-
ESP32 端需先调试 AT 指令是否能正常连接 OneNET;
-
小程序需配置合法域名(OneNET 的 IP / 域名),在微信开发者工具中开启 “不校验合法域名”;
-
语音编码格式需统一(建议 MP3 8kHz 单声道),避免播放异常。
- 网络调试:
-
NB-IoT 模块需先执行
AT+CSQ查看信号强度(≥10 为正常); -
MQTT 连接失败时,检查 ProductID/DeviceID/API Key 是否正确。
六、扩展功能建议
-
语音降噪:在 HC32F460 端增加 DSP 算法,对采集的语音进行降噪处理;
-
消息加密:采用 AES 加密语音数据,提升传输安全性;
-
多设备交互:支持多个终端设备与小程序的群组语音聊天;
-
语音转文字:对接微信同声传译 API,实现语音转文字展示。
2288

被折叠的 条评论
为什么被折叠?



