HC32F460 + ESP32 - S3 + NB - IoT 语音聊天系统设计

HC32F460+ESP32-S3+NB-IoT 语音聊天系统设计(附微信小程序)

一、系统整体方案设计

1.1 系统架构

本系统实现 “端 - 网 - 云 - 小程序” 全链路语音消息交互,核心分为 4 层架构:

层级核心设备 / 模块核心功能
终端硬件层HC32F460+ESP32-S3+NB-IoT(BC28)+VS1053语音采集 / 播放、协议透传、NB-IoT 网络接入
网络层NB-IoT 运营商网络 + MQTT 协议语音数据上行 / 下行传输,基于 OneNET 平台实现设备与小程序的消息转发
云端层中国移动 OneNET 物联网平台设备管理、语音数据存储、消息转发(设备 <-> 小程序)
应用层微信小程序语音录制 / 播放、消息展示、与终端的双向语音聊天

1.2 核心业务流程

  1. 终端侧:HC32F460 通过 VS1053 采集语音→编码为 PCM/MP3→UART 发送至 ESP32-S3→ESP32-S3 通过 AT 指令控制 NB-IoT 模块,以 MQTT 协议上传至 OneNET;

  2. 云端侧:OneNET 接收终端语音数据,触发消息推送至微信小程序;

  3. 小程序侧:接收语音数据→解码播放;小程序录制语音→上传至 OneNET→OneNET 下发至终端→ESP32-S3 透传至 HC32F460→VS1053 播放语音;

  4. 双向交互:基于设备 / 小程序的唯一标识(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 引脚功能说明
PB13SCKSPI 时钟
PB14MISOSPI 数据输入
PB15MOSISPI 数据输出
PA8CS片选(低有效)
PA9DREQ数据请求中断
PA10RST复位(低有效)
(2)HC32F460 ↔ ESP32-S3(UART 通信)
HC32F460 引脚ESP32-S3 引脚功能说明
PD0TXD2HC32 发送→ESP32 接收
PD1RXD2HC32 接收→ESP32 发送
共地GND电平匹配
(3)ESP32-S3 ↔ BC28(NB-IoT)(UART 通信)
ESP32-S3 引脚BC28 引脚功能说明
TXD1RXDESP32 发送→BC28 接收
RXD1TXDESP32 接收→BC28 发送
ENPWRKEYBC28 开关机控制
GNDGND共地

三、软件方案设计(分模块实现)

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)

{

&#x20;   stc\_clk\_pll\_cfg\_t stcPllCfg;

&#x20;   // 配置PLL:XTAL(8MHz) → PLL → 180MHz系统时钟

&#x20;   CLK\_PLLStructInit(\&stcPllCfg);

&#x20;   stcPllCfg.pllSrc = CLK\_PLL\_SRC\_XTAL;

&#x20;   stcPllCfg.pllMul = 45; // 8\*45/2=180MHz

&#x20;   CLK\_PLLConfig(\&stcPllCfg);

&#x20;   CLK\_PLLCmd(Enable);

&#x20;   // 等待PLL稳定

&#x20;   while(CLK\_GetFlagStatus(CLK\_FLAG\_PLLSTB) != Set);

&#x20;   // 切换系统时钟至PLL

&#x20;   CLK\_SysClkSwitch(CLK\_SYSCLK\_SRC\_PLL);

&#x20;   // 配置外设时钟:UART2/SPI2时钟使能

&#x20;   CLK\_PeriphClockCmd(CLK\_PERIPH\_UART2, Enable);

&#x20;   CLK\_PeriphClockCmd(CLK\_PERIPH\_SPI2, Enable);

&#x20;   CLK\_PeriphClockCmd(CLK\_PERIPH\_GPIO, Enable);

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* UART初始化函数(与ESP32通信) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void UART\_Init(void)

{

&#x20;   stc\_uart\_init\_t stcUartInit;

&#x20;   stc\_gpio\_init\_t stcGpioInit;

&#x20;   // 配置PD0(TX)、PD1(RX)为UART2功能

&#x20;   GPIO\_StructInit(\&stcGpioInit);

&#x20;   stcGpioInit.u16PinDir = PIN\_DIR\_OUT;

&#x20;   GPIO\_Init(GPIO\_PORT\_D, GPIO\_PIN\_0, \&stcGpioInit);

&#x20;   GPIO\_SetFunc(GPIO\_PORT\_D, GPIO\_PIN\_0, GPIO\_FUNC\_3); // TX

&#x20;   stcGpioInit.u16PinDir = PIN\_DIR\_IN;

&#x20;   GPIO\_Init(GPIO\_PORT\_D, GPIO\_PIN\_1, \&stcGpioInit);

&#x20;   GPIO\_SetFunc(GPIO\_PORT\_D, GPIO\_PIN\_1, GPIO\_FUNC\_3); // RX

&#x20;   // UART参数初始化:115200 8N1

&#x20;   UART\_StructInit(\&stcUartInit);

&#x20;   stcUartInit.u32Baudrate = 115200;

&#x20;   stcUartInit.u32Parity = UART\_PARITY\_NONE;

&#x20;   stcUartInit.u32StopBits = UART\_STOPBIT\_1;

&#x20;   stcUartInit.u32DataBits = UART\_DATA\_BIT\_8;

&#x20;   stcUartInit.u32ClkMode = UART\_CLK\_MODE0;

&#x20;   UART\_Init(UART\_CHN, \&stcUartInit);

&#x20;   // 使能UART接收和发送

&#x20;   UART\_FuncCmd(UART\_CHN, UART\_FUNC\_RX | UART\_FUNC\_TX, Enable);

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* VS1053初始化函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void VS1053\_Init(void)

{

&#x20;   // 1. 复位VS1053

&#x20;   GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_10); // RST置高

&#x20;   delay\_ms(10);

&#x20;   GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_10); // RST置低

&#x20;   delay\_ms(10);

&#x20;   GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_10); // 释放复位

&#x20;   delay\_ms(50);

&#x20;   // 2. SPI初始化(主机模式,CPOL=0, CPHA=0)

&#x20;   stc\_spi\_init\_t stcSpiInit;

&#x20;   SPI\_StructInit(\&stcSpiInit);

&#x20;   stcSpiInit.u32ClockDiv = SPI\_CLK\_DIV4; // 180/4=45MHz

&#x20;   stcSpiInit.u32WireMode = SPI\_WIRE\_MODE\_4;

&#x20;   stcSpiInit.u32MasterSlave = SPI\_MASTER;

&#x20;   stcSpiInit.u32CPOL = SPI\_CPOL\_LOW;

&#x20;   stcSpiInit.u32CPHA = SPI\_CPHA\_FIRST;

&#x20;   SPI\_Init(SPI\_CHN, \&stcSpiInit);

&#x20;   // 3. 配置VS1053寄存器(设置为MP3编码模式,采样率8kHz)

&#x20;   VS1053\_WriteReg(VS1053\_REG\_MODE, 0x0800); // 正常模式,不复位

&#x20;   VS1053\_WriteReg(VS1053\_REG\_BASS, 0x00FF); // 低音增强

&#x20;   VS1053\_WriteReg(VS1053\_REG\_CLOCKF, 0x8000); // 时钟配置

&#x20;   delay\_ms(10);

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 系统初始化入口函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void System\_Init(void)

{

&#x20;   CLK\_Init();

&#x20;   UART\_Init();

&#x20;   VS1053\_Init();

&#x20;   NVIC\_EnableIRQ(UART2\_IRQn); // 使能UART2中断(接收ESP32数据)

}
(2)语音采集函数(VS1053 采集 MP3 数据)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* VS1053写寄存器函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void VS1053\_WriteReg(uint8\_t reg, uint16\_t value)

{

&#x20;   GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置低

&#x20;   // 发送写命令+寄存器地址

&#x20;   SPI\_SendData(SPI\_CHN, 0x02);&#x20;

&#x20;   SPI\_SendData(SPI\_CHN, reg);

&#x20;   // 发送寄存器值(高字节+低字节)

&#x20;   SPI\_SendData(SPI\_CHN, (value >> 8) & 0xFF);

&#x20;   SPI\_SendData(SPI\_CHN, value & 0xFF);

&#x20;   while(SPI\_GetStatus(SPI\_CHN, SPI\_STATUS\_BUSY));

&#x20;   GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置高

&#x20;   delay\_ms(1);

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 语音采集函数(返回采集长度) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

uint16\_t VS1053\_Record\_Voice(uint8\_t \*buf, uint16\_t max\_len)

{

&#x20;   uint16\_t len = 0;

&#x20;   // 1. 设置VS1053为录音模式(MP3,8kHz采样率)

&#x20;   VS1053\_WriteReg(VS1053\_REG\_MODE, 0x1800); // 录音模式

&#x20;   VS1053\_WriteReg(VS1053\_REG\_AICTRL0, 0x0008); // 8kHz采样率

&#x20;   VS1053\_WriteReg(VS1053\_REG\_AICTRL1, 0x0000); // 单声道

&#x20;   VS1053\_WriteReg(VS1053\_REG\_AICTRL2, 0x0000); // 比特率64kbps

&#x20;   delay\_ms(10);

&#x20;   // 2. 等待DREQ中断(数据就绪),读取录音数据

&#x20;   while(len < max\_len)

&#x20;   {

&#x20;       if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_9) == Set) // DREQ置高,数据就绪

&#x20;       {

&#x20;           GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置低

&#x20;           SPI\_SendData(SPI\_CHN, 0x03); // 读数据命令

&#x20;           while(SPI\_GetStatus(SPI\_CHN, SPI\_STATUS\_BUSY));

&#x20;           buf\[len++] = SPI\_ReceiveData(SPI\_CHN); // 读取1字节

&#x20;           GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置高

&#x20;           if(len % 512 == 0) // 每512字节暂停,避免缓存溢出

&#x20;           {

&#x20;               delay\_ms(1);

&#x20;           }

&#x20;       }

&#x20;   }

&#x20;   // 3. 停止录音

&#x20;   VS1053\_WriteReg(VS1053\_REG\_MODE, 0x0800); // 恢复播放模式

&#x20;   return len;

}
(3)串口协议封装与发送函数(向 ESP32 发送语音数据)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 校验和计算函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

uint8\_t Calc\_Checksum(uint8\_t \*data, uint16\_t len)

{

&#x20;   uint8\_t checksum = 0;

&#x20;   for(uint16\_t i=0; i\<len; i++)

&#x20;   {

&#x20;       checksum ^= data\[i];

&#x20;   }

&#x20;   return checksum;

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 语音数据封装并发送至ESP32 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void UART\_Send\_Voice\_Data(uint8\_t \*voice\_data, uint16\_t voice\_len)

{

&#x20;   uint8\_t send\_buf\[4096+6]; // 帧头(2)+长度(2)+类型(1)+数据(N)+校验(1)

&#x20;   uint16\_t total\_len = 2+2+1+voice\_len+1;

&#x20;   // 1. 填充帧头

&#x20;   send\_buf\[0] = 0xAA;

&#x20;   send\_buf\[1] = 0x55;

&#x20;   // 2. 填充数据长度(大端序)

&#x20;   send\_buf\[2] = (voice\_len >> 8) & 0xFF;

&#x20;   send\_buf\[3] = voice\_len & 0xFF;

&#x20;   // 3. 填充数据类型(0x01:语音上传)

&#x20;   send\_buf\[4] = 0x01;

&#x20;   // 4. 填充语音数据

&#x20;   memcpy(\&send\_buf\[5], voice\_data, voice\_len);

&#x20;   // 5. 计算并填充校验和

&#x20;   send\_buf\[5+voice\_len] = Calc\_Checksum(\&send\_buf\[2], 1+voice\_len);

&#x20;   // 6. 串口发送

&#x20;   for(uint16\_t i=0; i\<total\_len; i++)

&#x20;   {

&#x20;       while(UART\_GetStatus(UART\_CHN, UART\_STATUS\_TXBF) == Set); // 等待发送缓冲区空

&#x20;       UART\_SendData(UART\_CHN, send\_buf\[i]);

&#x20;   }

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* UART接收中断函数(接收ESP32下发的语音数据) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void UART2\_IRQHandler(void)

{

&#x20;   static uint8\_t recv\_buf\[4096+6];

&#x20;   static uint16\_t recv\_idx = 0;

&#x20;   uint8\_t recv\_data;

&#x20;   if(UART\_GetIrqStatus(UART\_CHN, UART\_IRQ\_RXDNE) == Set)

&#x20;   {

&#x20;       recv\_data = UART\_ReceiveData(UART\_CHN);

&#x20;       // 帧头检测

&#x20;       if(recv\_idx == 0 && recv\_data != 0xAA) return;

&#x20;       if(recv\_idx == 1 && recv\_data != 0x55) {recv\_idx=0; return;}

&#x20;       recv\_buf\[recv\_idx++] = recv\_data;

&#x20;       // 接收完成(帧头+长度+类型+数据+校验)

&#x20;       if(recv\_idx >= 5)

&#x20;       {

&#x20;           uint16\_t data\_len = (recv\_buf\[2] << 8) | recv\_buf\[3];

&#x20;           if(recv\_idx >= 5 + data\_len + 1)

&#x20;           {

&#x20;               // 校验和验证

&#x20;               uint8\_t checksum = Calc\_Checksum(\&recv\_buf\[2], 1+data\_len);

&#x20;               if(checksum == recv\_buf\[5+data\_len])

&#x20;               {

&#x20;                   // 数据类型为0x02:语音播放

&#x20;                   if(recv\_buf\[4] == 0x02)

&#x20;                   {

&#x20;                       // 调用VS1053播放语音

&#x20;                       VS1053\_Play\_Voice(\&recv\_buf\[5], data\_len);

&#x20;                   }

&#x20;               }

&#x20;               recv\_idx = 0; // 重置接收索引

&#x20;           }

&#x20;       }

&#x20;   }

&#x20;   UART\_ClearIrqStatus(UART\_CHN, UART\_IRQ\_RXDNE);

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* VS1053播放语音函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void VS1053\_Play\_Voice(uint8\_t \*buf, uint16\_t len)

{

&#x20;   uint16\_t i = 0;

&#x20;   // 设置VS1053为播放模式

&#x20;   VS1053\_WriteReg(VS1053\_REG\_MODE, 0x0800);

&#x20;   delay\_ms(10);

&#x20;   // 逐字节发送语音数据至VS1053

&#x20;   while(i < len)

&#x20;   {

&#x20;       if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_9) == Set) // DREQ置高,可写数据

&#x20;       {

&#x20;           GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置低

&#x20;           SPI\_SendData(SPI\_CHN, buf\[i++]);

&#x20;           while(SPI\_GetStatus(SPI\_CHN, SPI\_STATUS\_BUSY));

&#x20;           GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_8); // CS置高

&#x20;       }

&#x20;   }

&#x20;   // 发送播放结束标志

&#x20;   for(uint8\_t j=0; j<10; j++)

&#x20;   {

&#x20;       if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_9) == Set)

&#x20;       {

&#x20;           GPIO\_ResetPins(GPIO\_PORT\_A, GPIO\_PIN\_8);

&#x20;           SPI\_SendData(SPI\_CHN, 0x00);

&#x20;           while(SPI\_GetStatus(SPI\_CHN, SPI\_STATUS\_BUSY));

&#x20;           GPIO\_SetPins(GPIO\_PORT\_A, GPIO\_PIN\_8);

&#x20;       }

&#x20;   }

}
(4)HC32F460 主函数
int main(void)

{

&#x20;   System\_Init(); // 系统初始化

&#x20;   while(1)

&#x20;   {

&#x20;       // 检测录音按键(示例:PA0按键按下开始录音)

&#x20;       if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_0) == Reset)

&#x20;       {

&#x20;           delay\_ms(20); // 消抖

&#x20;           if(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_0) == Reset)

&#x20;           {

&#x20;               // 采集语音(最大4096字节)

&#x20;               g\_voice\_len = VS1053\_Record\_Voice(g\_voice\_buf, 4096);

&#x20;               // 发送至ESP32

&#x20;               UART\_Send\_Voice\_Data(g\_voice\_buf, g\_voice\_len);

&#x20;           }

&#x20;           while(GPIO\_GetInputPin(GPIO\_PORT\_A, GPIO\_PIN\_0) == Reset); // 等待按键释放

&#x20;       }

&#x20;       delay\_ms(10);

&#x20;   }

}

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)

{

&#x20; // 1. 配置PWRKEY引脚,开机BC28

&#x20; pinMode(BC28\_PWRKEY, OUTPUT);

&#x20; digitalWrite(BC28\_PWRKEY, LOW);

&#x20; delay(1000);

&#x20; digitalWrite(BC28\_PWRKEY, HIGH);

&#x20; delay(5000); // 等待BC28开机

&#x20; // 2. 初始化串口

&#x20; NB\_UART.begin(BAUD\_RATE);

&#x20; HC\_UART.begin(BAUD\_RATE);

&#x20; // 3. AT指令配置BC28(关闭回显+设置NB网络+配置MQTT)

&#x20; sendATCmd("AT\r\n", "OK", 1000);        // 测试AT指令

&#x20; sendATCmd("ATE0\r\n", "OK", 1000);      // 关闭回显

&#x20; sendATCmd("AT+CGATT=1\r\n", "OK", 5000);// 附着网络

&#x20; sendATCmd("AT+CGDCONT=1,\\"IP\\",\\"cmnbiot\\"\r\n", "OK", 2000); // 设置APN

&#x20; sendATCmd("AT+QMTCFG=\\"aliauth\\",0,\\"" PRODUCT\_ID "\\",\\"" DEVICE\_ID "\\",\\"" API\_KEY "\\"\r\n", "OK", 2000); // MQTT鉴权

&#x20; sendATCmd("AT+QMTOPEN=0,\\"" MQTT\_SERVER "\\"," String(MQTT\_PORT) "\r\n", "CONNECT OK", 5000); // 连接MQTT服务器

&#x20; sendATCmd("AT+QMTCONN=0,\\"" DEVICE\_ID "\\"\r\n", "CONNACK\_OK", 5000); // 连接MQTT主题

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* AT指令发送与响应解析函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

bool sendATCmd(String cmd, String resp, uint32\_t timeout)

{

&#x20; NB\_UART.flush();

&#x20; NB\_UART.print(cmd); // 发送AT指令

&#x20; uint32\_t start = millis();

&#x20; String recv = "";

&#x20; while(millis() - start < timeout)

&#x20; {

&#x20;   if(NB\_UART.available())

&#x20;   {

&#x20;     recv += char(NB\_UART.read());

&#x20;     if(recv.indexOf(resp) != -1) // 匹配响应

&#x20;     {

&#x20;       Serial.println("AT Cmd Success: " + cmd);

&#x20;       return true;

&#x20;     }

&#x20;   }

&#x20; }

&#x20; Serial.println("AT Cmd Fail: " + cmd + ", Recv: " + recv);

&#x20; return false;

}
(2)MQTT 数据上传 / 接收函数
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 语音数据上传至OneNET \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void uploadVoiceToOneNET(uint8\_t \*data, uint16\_t len)

{

&#x20; // 1. 将二进制数据转为十六进制字符串(NB-IoT传输兼容)

&#x20; String hex\_data = "";

&#x20; for(uint16\_t i=0; i\<len; i++)

&#x20; {

&#x20;   hex\_data += String(data\[i], HEX);

&#x20;   if(i % 2 == 1) hex\_data += " "; // 每2字节加空格,便于解析

&#x20; }

&#x20; // 2. MQTT发布指令

&#x20; String topic = "topic/device/" DEVICE\_ID "/voice/upload";

&#x20; String cmd = "AT+QMTPUB=0,0,0,0,\\"" + topic + "\\",\\"" + hex\_data + "\\"\r\n";

&#x20; sendATCmd(cmd, "PUBACK", 5000);

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* NB-IoT数据接收解析函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void parseNBData(void)

{

&#x20; if(NB\_UART.available())

&#x20; {

&#x20;   recv\_nb\_data += char(NB\_UART.read());

&#x20;   // 检测MQTT下行数据(+QMTRECV: 0,0,"topic/...","data")

&#x20;   if(recv\_nb\_data.indexOf("+QMTRECV:") != -1)

&#x20;   {

&#x20;     // 解析主题和数据

&#x20;     int topic\_start = recv\_nb\_data.indexOf("\\"") + 1;

&#x20;     int topic\_end = recv\_nb\_data.indexOf("\\"", topic\_start);

&#x20;     String topic = recv\_nb\_data.substring(topic\_start, topic\_end);

&#x20;     int data\_start = recv\_nb\_data.indexOf("\\"", topic\_end+1) + 1;

&#x20;     int data\_end = recv\_nb\_data.indexOf("\\"", data\_start);

&#x20;     String hex\_data = recv\_nb\_data.substring(data\_start, data\_end);

&#x20;     // 主题匹配:终端下行语音

&#x20;     if(topic == "topic/device/" DEVICE\_ID "/voice/down")

&#x20;     {

&#x20;       // 十六进制字符串转二进制数据

&#x20;       uint8\_t voice\_data\[4096];

&#x20;       uint16\_t voice\_len = hex2bin(hex\_data, voice\_data);

&#x20;       // 封装串口帧,发送至HC32

&#x20;       sendHC32Data(voice\_data, voice\_len, 0x02);

&#x20;     }

&#x20;     recv\_nb\_data = ""; // 清空缓存

&#x20;   }

&#x20; }

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 十六进制字符串转二进制函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

uint16\_t hex2bin(String hex, uint8\_t \*bin)

{

&#x20; uint16\_t len = 0;

&#x20; for(uint16\_t i=0; i\<hex.length(); i+=3) // 每3字符(2位16进制+空格)

&#x20; {

&#x20;   String hex\_byte = hex.substring(i, i+2);

&#x20;   bin\[len++] = strtol(hex\_byte.c\_str(), NULL, 16);

&#x20; }

&#x20; return len;

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 向HC32发送数据(封装串口帧) \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void sendHC32Data(uint8\_t \*data, uint16\_t len, uint8\_t type)

{

&#x20; uint8\_t send\_buf\[4096+6];

&#x20; // 帧头

&#x20; send\_buf\[0] = 0xAA;

&#x20; send\_buf\[1] = 0x55;

&#x20; // 数据长度(大端)

&#x20; send\_buf\[2] = (len >> 8) & 0xFF;

&#x20; send\_buf\[3] = len & 0xFF;

&#x20; // 数据类型

&#x20; send\_buf\[4] = type;

&#x20; // 数据内容

&#x20; memcpy(\&send\_buf\[5], data, len);

&#x20; // 校验和

&#x20; send\_buf\[5+len] = calcChecksum(\&send\_buf\[2], 1+len);

&#x20; // 串口发送

&#x20; for(uint16\_t i=0; i<5+len+1; i++)

&#x20; {

&#x20;   HC\_UART.write(send\_buf\[i]);

&#x20; }

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 校验和计算函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

uint8\_t calcChecksum(uint8\_t \*data, uint16\_t len)

{

&#x20; uint8\_t checksum = 0;

&#x20; for(uint16\_t i=0; i\<len; i++)

&#x20; {

&#x20;   checksum ^= data\[i];

&#x20; }

&#x20; return checksum;

}
(3)HC32 数据接收与透传函数
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 接收HC32数据并透传至OneNET \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void parseHC32Data(void)

{

&#x20; if(HC\_UART.available())

&#x20; {

&#x20;   recv\_hc\_data += char(HC\_UART.read());

&#x20;   // 检测帧头0xAA55

&#x20;   if(recv\_hc\_data.indexOf(0xAA) != -1 && recv\_hc\_data.indexOf(0x55) != -1)

&#x20;   {

&#x20;     // 解析帧格式:AA 55 \[lenH lenL] \[type] \[data] \[checksum]

&#x20;     uint8\_t \*buf = (uint8\_t \*)recv\_hc\_data.c\_str();

&#x20;     uint16\_t data\_len = (buf\[2] << 8) | buf\[3];

&#x20;     uint8\_t data\_type = buf\[4];

&#x20;     uint8\_t checksum = calcChecksum(\&buf\[2], 1+data\_len);

&#x20;     // 校验和验证

&#x20;     if(checksum == buf\[5+data\_len])

&#x20;     {

&#x20;       // 语音上传(类型0x01)

&#x20;       if(data\_type == 0x01)

&#x20;       {

&#x20;         // 提取语音数据,上传至OneNET

&#x20;         uploadVoiceToOneNET(\&buf\[5], data\_len);

&#x20;       }

&#x20;     }

&#x20;     recv\_hc\_data = ""; // 清空缓存

&#x20;   }

&#x20; }

}

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* ESP32主函数 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

void setup() {

&#x20; Serial.begin(115200); // 调试串口

&#x20; BC28\_Init(); // 初始化BC28

}

void loop() {

&#x20; parseHC32Data(); // 解析HC32数据

&#x20; parseNBData();   // 解析NB-IoT数据

&#x20; delay(10);

}

3.4 微信小程序端代码实现(基于微信原生框架)

(1)小程序页面结构(index.wxml)
\<view class="container">

&#x20; \<!-- 语音消息列表 -->

&#x20; \<scroll-view scroll-y class="msg-list">

&#x20;   \<block wx:for="{{msgList}}" wx:key="index">

&#x20;     \<!-- 终端发送的语音 -->

&#x20;     \<view class="msg-item device-msg" wx:if="{{item.type == 'device'}}">

&#x20;       \<button bindtap="playVoice" data-url="{{item.data}}">播放终端语音\</button>

&#x20;       \<text>{{item.time}}\</text>

&#x20;     \</view>

&#x20;     \<!-- 小程序发送的语音 -->

&#x20;     \<view class="msg-item app-msg" wx:else>

&#x20;       \<button bindtap="playVoice" data-url="{{item.data}}">播放我的语音\</button>

&#x20;       \<text>{{item.time}}\</text>

&#x20;     \</view>

&#x20;   \</block>

&#x20; \</scroll-view>

&#x20; \<!-- 录音按钮 -->

&#x20; \<view class="record-btn">

&#x20;   \<button bindtouchstart="startRecord" bindtouchend="stopRecord">按住说话\</button>

&#x20; \</view>

\</view>
(2)小程序逻辑层(index.js)
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 全局配置 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

const app = getApp();

const oneNETConfig = {

&#x20; host: '183.230.40.39',

&#x20; port: 80,

&#x20; productId: 'your\_product\_id',

&#x20; deviceId: 'your\_device\_id',

&#x20; apiKey: 'your\_api\_key'

};

Page({

&#x20; data: {

&#x20;   msgList: \[], // 消息列表

&#x20;   recorderManager: null, // 录音管理器

&#x20;   voiceTempPath: "", // 临时语音文件路径

&#x20;   websocket: null // WebSocket连接(对接OneNET)

&#x20; },

&#x20; /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 页面加载初始化 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

&#x20; onLoad(options) {

&#x20;   // 1. 初始化录音管理器

&#x20;   this.initRecorder();

&#x20;   // 2. 连接OneNET WebSocket,监听下行消息

&#x20;   this.connectWebSocket();

&#x20; },

&#x20; /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 初始化录音管理器 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

&#x20; initRecorder() {

&#x20;   const recorderManager = wx.getRecorderManager();

&#x20;   const that = this;

&#x20;   // 录音配置

&#x20;   recorderManager.onStart(() => {

&#x20;     console.log('开始录音');

&#x20;   });

&#x20;   // 录音停止回调

&#x20;   recorderManager.onStop((res) => {

&#x20;     that.setData({ voiceTempPath: res.tempFilePath });

&#x20;     // 上传语音至OneNET

&#x20;     that.uploadVoiceToOneNET(res.tempFilePath);

&#x20;   });

&#x20;   this.setData({ recorderManager });

&#x20; },

&#x20; /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 连接OneNET WebSocket \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

&#x20; connectWebSocket() {

&#x20;   const that = this;

&#x20;   // OneNET WebSocket地址(需提前配置设备订阅)

&#x20;   const wsUrl = \`ws://\${oneNETConfig.host}:\${oneNETConfig.port}/mqtt/websocket\`;

&#x20;   const websocket = wx.connectSocket({

&#x20;     url: wsUrl,

&#x20;     header: {

&#x20;       'content-type': 'application/json'

&#x20;     }

&#x20;   });

&#x20;   // 连接成功

&#x20;   websocket.onOpen(() => {

&#x20;     console.log('WebSocket连接成功');

&#x20;     // 订阅终端下行语音主题

&#x20;     const subscribeMsg = {

&#x20;       cmd: "subscribe",

&#x20;       params: {

&#x20;         topic: \`topic/device/\${oneNETConfig.deviceId}/voice/upload\`

&#x20;       }

&#x20;     };

&#x20;     websocket.send({ data: JSON.stringify(subscribeMsg) });

&#x20;   });

&#x20;   // 接收消息

&#x20;   websocket.onMessage((res) => {

&#x20;     const msg = JSON.parse(res.data);

&#x20;     // 终端上传的语音数据

&#x20;     if(msg.topic === \`topic/device/\${oneNETConfig.deviceId}/voice/upload\`)

&#x20;     {

&#x20;       // 添加至消息列表

&#x20;       that.setData({

&#x20;         msgList: \[

&#x20;           ...that.data.msgList,

&#x20;           {

&#x20;             type: 'device',

&#x20;             data: msg.data,

&#x20;             time: new Date().toLocaleTimeString()

&#x20;           }

&#x20;         ]

&#x20;       });

&#x20;     }

&#x20;   });

&#x20;   // 连接关闭

&#x20;   websocket.onClose(() => {

&#x20;     console.log('WebSocket连接关闭,重连中...');

&#x20;     setTimeout(() => { that.connectWebSocket(); }, 3000);

&#x20;   });

&#x20;   this.setData({ websocket });

&#x20; },

&#x20; /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 开始录音 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

&#x20; startRecord() {

&#x20;   const { recorderManager } = this.data;

&#x20;   // 录音配置(MP3格式,8kHz采样率)

&#x20;   recorderManager.start({

&#x20;     format: 'mp3',

&#x20;     sampleRate: 8000,

&#x20;     numberOfChannels: 1,

&#x20;     bitRate: 64000

&#x20;   });

&#x20; },

&#x20; /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 停止录音 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

&#x20; stopRecord() {

&#x20;   this.data.recorderManager.stop();

&#x20; },

&#x20; /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 上传语音至OneNET \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

&#x20; uploadVoiceToOneNET(tempPath) {

&#x20;   const that = this;

&#x20;   // 1. 读取语音文件为二进制

&#x20;   wx.getFileSystemManager().readFile({

&#x20;     filePath: tempPath,

&#x20;     encoding: 'hex', // 转为十六进制字符串(与终端兼容)

&#x20;     success(res) {

&#x20;       // 2. 构造MQTT发布请求

&#x20;       const publishData = {

&#x20;         cmd: "publish",

&#x20;         params: {

&#x20;           topic: \`topic/device/\${oneNETConfig.deviceId}/voice/down\`,

&#x20;           data: res.data,

&#x20;           qos: 0

&#x20;         }

&#x20;       };

&#x20;       // 3. 发送至OneNET

&#x20;       that.data.websocket.send({ data: JSON.stringify(publishData) });

&#x20;       // 4. 添加至消息列表

&#x20;       that.setData({

&#x20;         msgList: \[

&#x20;           ...that.data.msgList,

&#x20;           {

&#x20;             type: 'app',

&#x20;             data: tempPath,

&#x20;             time: new Date().toLocaleTimeString()

&#x20;           }

&#x20;         ]

&#x20;       });

&#x20;     }

&#x20;   });

&#x20; },

&#x20; /\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 播放语音 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/

&#x20; playVoice(e) {

&#x20;   const voiceUrl = e.currentTarget.dataset.url;

&#x20;   // 播放本地临时文件(小程序语音)或OneNET下载的语音(终端语音)

&#x20;   wx.playVoice({

&#x20;     filePath: voiceUrl,

&#x20;     complete() {

&#x20;       console.log('播放完成');

&#x20;     }

&#x20;   });

&#x20; }

});

四、OneNET 平台配置步骤

  1. 登录 OneNET 物联网平台(https://open.iot.10086.cn/);

  2. 创建产品:选择 “NB-IoT” 接入方式,数据格式选择 “自定义 JSON”;

  3. 添加设备:录入设备 IMEI(BC28 模块的 IMEI),生成 DeviceID 和 API Key;

  4. 配置 MQTT 主题:添加上行主题topic/device/{DeviceID}/voice/upload和下行主题topic/device/{DeviceID}/voice/down

  5. 配置 WebSocket 推送:开启设备消息推送,关联小程序 OpenID,实现消息转发。

五、系统调试与注意事项

  1. 硬件调试:
  • 确保 HC32F460 与 ESP32-S3 的串口电平匹配(均为 3.3V);

  • BC28 模块需插入 NB-IoT 卡,且所在区域有 NB-IoT 网络覆盖;

  • VS1053 的 DREQ 引脚需配置为中断,避免数据读写溢出。

  1. 软件调试:
  • ESP32 端需先调试 AT 指令是否能正常连接 OneNET;

  • 小程序需配置合法域名(OneNET 的 IP / 域名),在微信开发者工具中开启 “不校验合法域名”;

  • 语音编码格式需统一(建议 MP3 8kHz 单声道),避免播放异常。

  1. 网络调试:
  • NB-IoT 模块需先执行AT+CSQ查看信号强度(≥10 为正常);

  • MQTT 连接失败时,检查 ProductID/DeviceID/API Key 是否正确。

六、扩展功能建议

  1. 语音降噪:在 HC32F460 端增加 DSP 算法,对采集的语音进行降噪处理;

  2. 消息加密:采用 AES 加密语音数据,提升传输安全性;

  3. 多设备交互:支持多个终端设备与小程序的群组语音聊天;

  4. 语音转文字:对接微信同声传译 API,实现语音转文字展示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值