AT-Demo介绍¶
1. AT-Demo 总体介绍¶
AT-Demo 将繁琐的 AT 指令流程封装成简单的应用接口,让用户只要关注业务流程开发。它具有移植简单、样例丰富等特点,可以让用户快速进行 MCU+通信模块的场景开发。仓库提供 Win32 和单片机两种运行环境,两者共用同一套 AT 指令处理代码:
- Win32 AT 助手 Demo:直接通过电脑串口连接模组,用于查看指令、参数、应答和主动上报;
- 单片机 Demo:以 HC32F460JEUA 为参考平台,说明如何将公共 AT 代码接到其他 MCU。
单片机 Demo 使用裸机主循环。 UART 接收中断将字节写入环形缓冲,主循环处理 AT 应答、联网和各功能流程。网络协议、TLS 和音频由 YM310 处理,主循环不要停下来等待指令完成。
移植前在 Win32 Demo 中执行 AT 和 ATI,确认通信及固件版本,再进行 MCU 端适配。
AT 指令格式、参数和返回值请参考:
1.1 三层结构¶
代码分为 App、AT、Port 三层。
App 层负责程序入口、联网、收发和断线恢复。platform/ 目录放当前 MCU 的 UART、毫秒计时和 SDK 驱动。更换 MCU 时复用功能文件,修改入口和硬件相关文件。
AT 层包含 Core 和 Common。Core 处理指令排队、发送、超时、重试及应答分流;Common 提供 Socket、MQTT、HTTP、Audio 等接口。更换 MCU 时原样使用,不修改该层文件。
Port 层实现 Port/at_port.h 中的内存、毫秒时间和 UART 接口。UART 和计时调用 App 的平台驱动,本例内存使用 C 库 malloc/free;寄存器和 SDK 调用不放在 Port 文件中。

1.2 一条 AT 指令的实际路径¶
各业务共用以下 AT 收发路径。

发送:App 提交 AT,Core 排队处理,再经 at_uart_write() 和 UART 驱动发出 AT\r\n。
接收:UART 驱动保存 YM310 返回的 OK\r\n,Core 通过 at_uart_read() 取出数据,区分当前指令应答和主动上报,再调用对应回调。
发送异常时,依次检查指令提交和 UART 输出;接收异常时,依次检查接收中断、环形缓冲、at_uart_read() 和回调。
2. Win32 AT 助手 Demo¶
Win32 Demo 通过 COM 口连接 YM310,用于验证固件、指令及业务参数。
2.1 开始前准备¶
2.1.1 运行环境¶
- Windows 10 或 Windows 11 64 位系统;
- YM310 模组和 USB 数据线;
- 已开通数据业务的 SIM 卡,用于 TCP、MQTT/MQTTS、HTTP/HTTPS 等联网测试;仅测试
AT、ATI等基础指令时可不插卡; - 模组 USB 驱动,设备管理器中能够看到 AT 串口;
- 从源码编译时,需要 Visual Studio 2019 或更高版本,并安装“使用 C++ 的桌面开发”组件。
运行前释放被 SSCOM 或其他程序占用的 AT 串口。
2.1.2 从源码启动¶
解决方案位于:
App/Win32/at_ui.sln
双击 at_ui.sln,在 Visual Studio 中选择 Debug | x64 或 Release | x64,然后执行“生成解决方案”。输出文件位于 App/Win32/output/。
2.1.3 直接运行 EXE¶
Win64 工具包:
解压后运行 AT-Demo_V1.7.exe,无需安装。
2.2 AT-Demo 工具使用指南¶

主要功能包括:
- 自动查找或手动连接模组 AT 串口;
- 发送 AT 指令并显示应答和 URC;
- 提供 FS、HTTP、TCP、FTP、MQTT、Audio 等功能入口;
- 支持多条指令保存、循环发送和 HEX 数据收发;
- 保存完整的 AT 收发日志。
基本操作顺序如下:
- 插入模组 USB 并开机;
- 启动 AT-Demo,保持 USB 选项勾选,等待工具自动查找 AT 串口;
- 状态栏显示
Connected COMxx @ 115200 bps后发送AT; - 收到
OK后发送ATI,确认当前固件版本; - 自动连接失败时,取消 USB,点击 Refresh,手动选择 COM 口后点击 Connect;
- 测试完成后点击 Save Log 保存日志,使用 Disconnect 释放串口。
2.3 使用注意事项¶
- 同一 COM 口仅允许一个程序占用。
- 指令支持范围以当前固件为准。返回
ERROR时,通过ATI核对版本及对应指令手册。 - 执行普通 AT 指令前,先退出透传状态。
- 排查问题时保存完整的 TX、RX 和 URC 日志。
3. 单片机 Demo¶
AT-Demo win32&华大单片机正式版 v1.7 源码
根目录 文件功能说明.md 说明各层文件、SDK、独立工程和测试脚本的用途。
3.1 Demo 介绍¶
HC32 参考工程位于 App/Hc32f460/mdk/。
参考工程使用 HC32F460JEUA,UART 中断接收数据,主循环处理 AT 和各功能流程。以下先移植普通 TCP,再按 3.3.3 切换 MQTT/MQTTS、HTTP/HTTPS 或四种 Audio 通路。各功能共用串口、时间和内存接口。
3.2 Demo 执行流程¶
3.2.1 开始前准备¶
- 硬件:目标 MCU 开发板、YM310 及底板、下载器、USB-TTL;网络测试还需要已开通流量的 SIM 卡。
- 软件:Keil、对应芯片的 Device Pack。HC32 示例使用 HC32F460JEUA 和 J-Link。
- 串口:一个连接模组,另一个输出日志。
TCP、MQTT、HTTP 测试前,确认 AT+CPIN? 返回 READY、AT+CEREG? 状态为 1 或 5。AT+CSQ 只用于查看信号,不能代替驻网检查。Audio 不需要 SIM 卡或驻网。
先按 2.1.3 直接运行 EXE 下载并运行 Win32 工具,连接模组验证 AT、ATI 和准备使用的功能。验证后点击 Disconnect,关闭工具,再接 MCU。
固件检查: Connect 成功并完成指令支持查询后,若 Quick Commands 下的功能按钮仍为灰色,当前固件不支持该功能。基础指令正常、功能初始化却持续返回 ERROR 时,也先核对固件。按 AT软件介绍—AT版本列表 选择与模组型号及所需功能匹配的固件。
| 线色 | YM310 端 | HC32 参考板端 |
|---|---|---|
| 红 | MAIN_TX |
PA5 / USART1_RX |
| 蓝 | MAIN_RX |
PA4 / USART1_TX |
| 黑 | GND | GND |
图中 PA4/PA5 为 HC32 参考引脚;新平台改用已配置的 UART TX/RX,并与模组交叉连接、共地。接线前按两块板的原理图核对供电及 UART 电平,不将 USB-TTL 电源脚直接接到未知电压的引脚。
日志接线:目标 MCU 的独立日志 UART TX → USB-TTL RX,GND 共地,115200、8N1。HC32 参考接线为白线 PA15 / USART2_TX 和黑线 GND;仅打印日志时无需连接绿线 PA10 / USART2_RX。日志口与模组通信口分开。
3.2.2 TCP 执行链路¶
main.c 创建 AT 对象并轮询 app_net.c、app_tcp.c。app_net.c 确认驻网后通知 TCP 启动;AT/common/sock/at_sock.c 提交连接、收发指令,AT/core/src/at_chat.c 经 Port 调用 UART。下行数据按 +IPD 声明的长度交给回调,连接断开后由 app_tcp.c 安排重连。
3.3 Demo 移植流程¶
使用本页提供的 v1.7 源码,先移植普通 TCP,再按 3.3.3 接入其他业务。将 <target> 替换为实际平台名,只修改目标副本;AT/、Port/at_port.h 和原 HC32 工程保持不变。
本例使用 C 库 malloc/free,无需复制 App/Hc32f460/platform/runtime/hc32_runtime.c/.h(静态内存池、占用统计及字符串兼容实现)。时基和 UART 分别在第 2、3 步适配;字符串兼容见第 8 步。
本章用独立日志口打印回调结果:第 7 步设置回调,第 8 步修改日志驱动。按键、LED、看门狗等按需要添加。测试完成后可以关闭日志,主循环中的功能调用仍需保留。
TCP 普通、TCP 透传、MQTT、MQTTS、HTTP、HTTPS、ES8311、TM8211、PWM、SPI 共 10 种组合分别编译。每个工程只运行一种功能,共用 Port 和平台驱动;使用本章的 main.c,不加入原示例的 profile_main.c。
3.3.1 具体流程¶
第 1~9 步完成普通 TCP;其余九种组合按 3.3.3 切换功能并添加测试调用,沿用本节的平台接口和日志驱动。
第 1 步:复制目标芯片最小工程¶
新建文件: 复制目标 SDK 裸机工程至 App/<target>/mdk/<工程名>/,入口放入 App/<target>/main.c;保留 SDK 启动、System、驱动及链接文件。
修改位置: App/<target>/main.c;App/<target>/mdk/<工程名>/<工程名>.uvprojx。
具体修改:
| 文件或工程位置 | 函数或配置 | 操作 |
|---|---|---|
App/<target>/main.c |
main() |
保留 SDK 原工程的系统、时钟及必要中断初始化;删除演示循环及模组 UART 初始化调用,循环体留空 |
.uvprojx → Project 窗口 |
入口文件 | 将入口指向上述 main.c,移除其他定义 main() 的文件;已指向该文件时不重复加入 |
.uvprojx → Options for Target → C/C++ |
语言及 GNU 扩展 | AC5 勾选 C99 Mode,并在 Misc Controls 追加 --gnu;AC6 在 Misc Controls 设置 -std=gnu99,替换已有的 -std=c99(如有) |
.uvprojx → Options for Target → Target / Linker |
芯片及链接配置 | 沿用目标 SDK 配置;工程移动后修正源码、启动文件及链接文件的相对路径 |
| Keil 调试窗口 | main() 的 for (;;) |
Rebuild、Download,在循环处设断点并运行,确认能进入循环 |
第 7 步将保留的 SDK 初始化移入 main.c::<target>_board_init()。
参考实现:最小工程及入口
需修改:将 <target> 和 <工程名> 替换为实际名称。
可直接使用:保留目标 SDK 原有启动、System、HAL 和链接文件。
App/<target>/
├─ main.c
├─ mdk/<工程名>/<工程名>.uvprojx
└─ 目标 SDK 的启动、System、HAL 和链接文件
App/<target>/main.c:保留 SDK 初始化。
只整理 SDK 原入口,保留原有头文件和初始化语句;下例仅示意 main() 结构,不替换整个文件。
/* 文件:App/<target>/main.c
* 位置:文件头及 main()。
*/
/* 需修改:main.c 文件头包含 SDK 初始化所需头文件。 */
/* main.c::main()
* 需修改:保留目标 SDK 的 HAL、时钟及必要中断初始化,主循环保留。
*/
int main(void)
{
/* 可直接使用:第 7 步添加 TCP 代码。 */
for (;;) {
}
}
第 2 步:接入毫秒时间并配置内存¶
修改位置: 将 App/Hc32f460/platform/bsp/bsp_time.c/.h 复制到 App/<target>/platform/bsp/;中断及堆配置使用目标 SDK 的原文件。
具体修改: 用参考实现第 1 项分别替换目标 bsp_time.c、bsp_time.h 的完整内容,再按下表接入 SDK。表中 .c/.h 均指目标副本。
| 文件 | 函数或配置 | 操作 |
|---|---|---|
bsp_time.c、bsp_time.h |
文件内容 | 使用参考实现第 1 项,包含 bsp_time_tick_1ms() 及其声明;不保留 HC32 专用中断入口 |
目标 SDK 中断 .c |
SysTick_Handler() |
文件头包含 bsp_time.h,每 1 ms 调用一次 bsp_time_tick_1ms();;保留原有计时处理,默认死循环则替换。文件定位见参考实现第 2 项 |
bsp_time.c |
bsp_time_init() |
SDK 已有 1 ms 节拍:清空函数体;没有:填入参考实现第 3 项的 SysTick 配置,文件头包含目标 SDK 的系统/CMSIS 头文件 |
Keil Startup 分组的 startup_*.s |
已有 Heap_Size |
按 RAM 容量设置 C 库堆;16 KB 配置见参考实现第 4 项。SDK 从其他文件配置堆时,修改其实际配置 |
用 Keil Find in Files 搜索 SysTick_Handler,修改其函数体所在 .c;HC32 参考:App/Hc32f460/platform/vendor/driver/src/hc32f460_interrupts.c。其他定时器改其 1 ms 中断/回调,每毫秒只计数一次。
bsp_time_get_ms()、bsp_delay_ms() 保留参考实现;上述源码及头文件目录在第 8 步加入 Keil。
参考实现:时间函数、中断调用及 C 库堆配置
1. 提供时间接口
App/<target>/platform/bsp/bsp_time.h:以下为完整头文件。
/* 文件:App/<target>/platform/bsp/bsp_time.h */
/* 可直接使用:头文件保护及标准类型引用。 */
#ifndef BSP_TIME_H
#define BSP_TIME_H
#include <stdint.h>
/* 可直接使用:保持声明不变。 */
void bsp_time_init(void);
/* 可直接使用:保持声明不变。 */
void bsp_time_tick_1ms(void);
/* 可直接使用:保持声明不变。 */
uint32_t bsp_time_get_ms(void);
/* 可直接使用:保持声明不变。 */
void bsp_delay_ms(uint32_t ms);
#endif
App/<target>/platform/bsp/bsp_time.c:以下为完整文件;bsp_time_init() 按第 3 项填写。
/* 文件:App/<target>/platform/bsp/bsp_time.c */
/* 沿用 bsp_time.h 文件名的情况下可直接使用。
* 头文件改名的情况下需要修改:将下方 include 改为实际头文件名。
*/
#include "bsp_time.h"
/* 可直接使用:毫秒计数变量,由中断递增。 */
static volatile uint32_t m_ms;
/* SDK 已有 1 ms 节拍并已接入计数的情况下可直接使用。
* SDK 尚未启动 1 ms 节拍的情况下需要修改:在 bsp_time_init() 中填入本步第 3 项的节拍初始化
* 代码。
*/
void bsp_time_init(void)
{
}
/* 可直接使用;SDK 的 1 ms 中断/回调每次调用一次。 */
void bsp_time_tick_1ms(void)
{
++m_ms;
}
/* 可直接使用。 */
uint32_t bsp_time_get_ms(void)
{
return m_ms;
}
/* 可直接使用;要求 1 ms 中断运行。 */
void bsp_delay_ms(uint32_t ms)
{
uint32_t start = bsp_time_get_ms();
while ((uint32_t)(bsp_time_get_ms() - start) < ms) { }
}
2. 接入 1 ms 中断
在 SDK 的 SysTick_Handler() 所在 .c 包含 bsp_time.h,在原函数内追加下方调用,保留原有处理:
/* 文件:目标 SDK 中定义 SysTick_Handler() 的中断 .c
* 位置:SysTick_Handler();按本项正文定位文件。
*/
/* 需修改:仅追加计数调用,不覆盖 SDK 原处理或重复定义入口。 */
void SysTick_Handler(void)
{
/* 可直接使用:SDK 原有语句保留。 */
/* 可直接使用:每 1 ms 仅调用一次。 */
bsp_time_tick_1ms();
}
若启动文件只提供弱定义的默认入口,在目标 bsp_time.c 新增上述函数;已有 C 函数时直接修改,不重复定义。默认入口中的死循环应移除,SDK 原有计时或回调转发应保留。
其他定时器:从 SDK 注册处或向量表找到中断/回调,在其 1 ms 分支调用 bsp_time_tick_1ms(),保留清标志语句。
3. 确定节拍初始化
App/<target>/platform/bsp/bsp_time.c 的 bsp_time_init():SDK 已启动 1 ms 节拍时留空;否则,采用 Cortex-M SysTick 时用下方代码替换原函数体,并包含目标 SDK 的系统/CMSIS 头文件。不新增同名函数。
/* 文件:App/<target>/platform/bsp/bsp_time.c
* 位置:bsp_time_init() 的函数体;不是文件级语句。
*/
/* SDK 未启动节拍且使用 Cortex-M SysTick 的情况下可直接使用。
* 使用其他定时器的情况下需要修改:将下方代码换为目标 SDK 的 1 ms 定时初始化,填入
* bsp_time_init()。
* SDK 已启动 1 ms 节拍的情况下需要修改:不添加本段代码,bsp_time_init() 保持为空。
*/
SystemCoreClockUpdate();
if (SysTick_Config(SystemCoreClock / 1000u) != 0u) {
/* 可直接使用:配置失败时停止初始化。 */
for (;;) { }
}
节拍只启动一次,SDK 初始化和 bsp_time_init() 不重复配置。
4. 配置内存容量
修改 Keil Startup 分组当前编入的 startup_*.s 中已有的 Heap_Size:
; 文件:Keil Startup 分组实际编入的 startup_*.s;位置:已有 Heap_Size 定义(不在函数内)。
; 需修改:按 RAM 预算设置,16 KB 仅为示例。
Heap_Size EQU 0x00004000
按可用 RAM 设置堆,预留栈和静态数据。无 Heap_Size 时,从 Options for Target → Linker 定位堆配置;无需另写初始化函数。
第 3 步:复用 UART 驱动并替换硬件调用¶
修改位置: 将 App/Hc32f460/platform/bsp/bsp_uart_modem.c/.h 复制到 App/<target>/platform/bsp/。
具体修改: 用参考实现第 1 项替换目标 bsp_uart_modem.c 的完整内容;bsp_uart_modem.h 保留原内容,只增加第 2 项的两条声明。再按下表适配硬件调用,表中 .c/.h 均指目标副本。
| 文件 | 函数或位置 | 操作 |
|---|---|---|
bsp_uart_modem.c |
文件头 | 包含目标 SDK 的 UART、GPIO、中断头文件 |
bsp_uart_modem.c |
modem_hw_init() |
设置实际 UART、TX/RX 引脚、115200/8N1,启用收发;成功返回 0,失败返回非 0;删除该处 #error |
bsp_uart_modem.c |
modem_irq_init() |
配置同一 UART 的接收、错误中断和优先级,接入下述 SDK 处理函数;删除该处 #error |
bsp_uart_modem.c |
modem_write_byte() |
等待 UART 可写并发送 byte;成功返回 0,失败或超时返回非 0;删除该处 #error |
bsp_uart_modem.h |
最后一个 #endif 前 |
加入 bsp_modem_uart_rx_byte()、bsp_modem_uart_error() 的声明,见参考实现第 2 项 |
目标 SDK 中断/回调 .c |
UART 接收函数 | 文件头包含 bsp_uart_modem.h;每取出一个字节,调用 bsp_modem_uart_rx_byte(rx_byte) |
同一 SDK 中断/回调 .c,或 SDK 单独的错误处理 .c |
UART 错误函数或错误分支 | 先保存错误标志,再按 SDK 要求清除,最后调用 bsp_modem_uart_error(overrun, frame, parity),传入保存的标志 |
从 SDK UART 示例的注册处定位接收、错误回调所在 .c。HC32 参考:bsp_uart_modem.c 的 modem_rx_irq_callback()、modem_err_irq_callback()。
其余缓冲、收发及统计函数保留参考实现;源码及头文件目录在第 8 步加入 Keil。
参考实现:bsp_uart_modem.c/.h 及 SDK 中断接入
1. 替换目标 App/<target>/platform/bsp/bsp_uart_modem.c
填写上述三个硬件函数的 SDK 调用,再删除各自的 #error;其余代码保留。
/* 文件:App/<target>/platform/bsp/bsp_uart_modem.c */
/* 可直接使用:下方三个 #include 保持不变。
* 需修改:在这三个 #include 之前新增目标 SDK 的 UART、GPIO、中断头文件引用。
*/
#include <stddef.h>
#include "bsp_uart_modem.h"
#include "bsp_time.h"
/* RAM 足够且 rb_overflow 不增加的情况下可直接使用。
* 接收缓冲溢出的情况下需要修改:增大 bsp_uart_modem.c 的 MODEM_RB_SIZE,保持为 2 的幂,复测
* 无溢出。
* 需要节省 RAM 的情况下需要修改:减小 MODEM_RB_SIZE,保持为 2 的幂,并确认 rb_overflow 不增
* 加。
*/
#define MODEM_RB_SIZE (1024u)
/* 可直接使用:随 MODEM_RB_SIZE 自动计算。 */
#define MODEM_RB_MASK (MODEM_RB_SIZE - 1u)
/* 可直接使用。 */
static uint8_t m_rb[MODEM_RB_SIZE];
static volatile uint16_t m_head;
static volatile uint16_t m_tail;
static volatile bsp_modem_stat_t m_stat;
/* bsp_uart_modem.c::modem_hw_init()
* 需修改:填入 UART 时钟、引脚、115200/8N1 及收发使能。
*/
static int modem_hw_init(void)
{
#error "bsp_uart_modem.c: implement modem_hw_init() with target SDK UART initialization"
/* 需修改:按 SDK 初始化结果返回,成功为 0,失败为非 0。 */
return -1;
}
/* bsp_uart_modem.c::modem_irq_init()
* 需修改:配置接收/错误中断及优先级。
*/
static void modem_irq_init(void)
{
#error "bsp_uart_modem.c: implement modem_irq_init() with target SDK interrupt setup"
}
/* bsp_uart_modem.c::modem_write_byte()
* 需修改:SDK 等待可写并发送 byte,须有超时;成功返回 0,否则返回非 0。
*/
static int modem_write_byte(uint8_t byte)
{
(void)byte;
#error "bsp_uart_modem.c: implement modem_write_byte() with target SDK byte transmission"
return -1;
}
/* 可直接使用;先适配上述三个硬件函数。 */
void bsp_modem_uart_init(void)
{
if (modem_hw_init() != 0) {
/* 可直接使用:UART 初始化失败时停止。 */
for (;;) { }
}
modem_irq_init();
}
/* 可直接使用;由 SDK 接收函数传入字节。 */
void bsp_modem_uart_rx_byte(uint8_t byte)
{
uint16_t next = (uint16_t)((m_head + 1u) & MODEM_RB_MASK);
uint16_t pending;
++m_stat.rx_bytes;
m_stat.last_rx_ms = bsp_time_get_ms();
if (next == m_tail) {
++m_stat.rb_overflow;
return;
}
m_rb[m_head] = byte;
m_head = next;
pending = (uint16_t)((m_head - m_tail) & MODEM_RB_MASK);
if (pending > m_stat.rx_peak)
m_stat.rx_peak = pending;
}
/* 可直接使用;SDK 清除错误后传入标志。 */
void bsp_modem_uart_error(int overrun, int frame, int parity)
{
if (overrun) ++m_stat.overrun;
if (frame) ++m_stat.frame_error;
if (parity) ++m_stat.parity_error;
}
/* 可直接使用;字节发送由 modem_write_byte() 完成。 */
uint32_t bsp_modem_write(const void *data, uint32_t len)
{
const uint8_t *p = (const uint8_t *)data;
uint32_t n = 0u;
if (p == NULL)
return 0u;
while (n < len) {
if (modem_write_byte(p[n]) != 0)
break;
++n;
}
m_stat.tx_bytes += n;
if (n != 0u)
m_stat.last_tx_ms = bsp_time_get_ms();
return n;
}
/* 可直接使用;无数据返回 0。 */
uint32_t bsp_modem_read(void *buf, uint32_t len)
{
uint8_t *p = (uint8_t *)buf;
uint32_t n = 0u;
if (p == NULL)
return 0u;
while (n < len && m_tail != m_head) {
p[n++] = m_rb[m_tail];
m_tail = (uint16_t)((m_tail + 1u) & MODEM_RB_MASK);
}
return n;
}
/* 可直接使用。 */
void bsp_modem_get_stat(bsp_modem_stat_t *stat)
{
if (stat == NULL)
return;
stat->rx_bytes = m_stat.rx_bytes;
stat->tx_bytes = m_stat.tx_bytes;
stat->rx_pending = (uint16_t)((m_head - m_tail) & MODEM_RB_MASK);
stat->rx_peak = m_stat.rx_peak;
stat->rb_overflow = m_stat.rb_overflow;
stat->overrun = m_stat.overrun;
stat->frame_error = m_stat.frame_error;
stat->parity_error = m_stat.parity_error;
stat->last_rx_ms = m_stat.last_rx_ms;
stat->last_tx_ms = m_stat.last_tx_ms;
}
2. 修改目标 bsp_uart_modem.h 和 SDK 的实际中断文件
App/<target>/platform/bsp/bsp_uart_modem.h 保留头文件保护、stdint.h、bsp_modem_stat_t 和原有函数声明,仅在最后一个 #endif 前增加以下两条声明:
/* 文件:App/<target>/platform/bsp/bsp_uart_modem.h
* 位置:最后一个 #endif 之前。
*/
/* 可直接使用:此声明不改;SDK 接收入口每字节调用一次。 */
void bsp_modem_uart_rx_byte(uint8_t byte);
/* 可直接使用:此声明不改;错误入口清标志后调用,参数依次为溢出、帧错、校验错。 */
void bsp_modem_uart_error(int overrun, int frame, int parity);
从 SDK UART 注册处或向量表找到实际中断 .c,确认已编入工程。
- SDK 中断/回调
.c:文件头包含bsp_uart_modem.h。 - 接收函数:每取得一个有效字节,调用
bsp_modem_uart_rx_byte(rx_byte);FIFO 逐字节传入,SDK 回调已提供字节时不重复读寄存器。 - 错误分支:先保存错误标志,再按 SDK 清除,调用
bsp_modem_uart_error(overrun, frame, parity);丢弃错误字节。 - 保留 SDK 的清标志和接收重启语句;删除原回显发送、另一套读取缓冲的代码及阻塞接收等待。
3. 保持收发要求
App/<target>/platform/bsp/bsp_uart_modem.c:MODEM_RB_SIZE 保持为 2 的幂;bsp_modem_read() 无数据返回 0;bsp_modem_write() 按 len 发送,等待期间保持接收中断可响应。
第 4 步:实现 Port 接口¶
新建文件: Port/<target>/at_<target>_port.c。
修改位置: 新文件中的八个函数;Port/at_port.h 原样引用。
具体修改: 将下方完整实现写入新建的 .c,引用已有 Port/at_port.h,不另建平台头文件;按下表核对调用。
at_<target>_port.c 中的函数 |
操作 |
|---|---|
at_malloc()、at_free() |
分别调用 C 库 malloc()、free();零长度申请返回 NULL |
at_get_ms() |
返回 bsp_time.c 的 bsp_time_get_ms() 结果 |
at_uart_open() |
调用 bsp_uart_modem.c 的 bsp_modem_uart_init();本例固定 UART、115200/8N1 |
at_uart_close() |
清除本文件的 s_uart_opened,不关闭物理 UART |
at_uart_write() |
调用 bsp_uart_modem.c 的 bsp_modem_write(buf, len),返回实际发送长度 |
at_uart_read() |
调用 bsp_uart_modem.c 的 bsp_modem_read(buf, len),无数据返回 0 |
at_uart_event_wait() |
调用 bsp_time.c 的 bsp_delay_ms(timeout_ms);本例主循环不调用 |
本例不另建内存文件。运行时修改波特率或关闭 UART 硬件的可选修改见参考实现末尾。
参考实现:Port/<target>/at_<target>_port.c
/* 文件:Port/<target>/at_<target>_port.c */
/* 沿用 bsp_time.h、bsp_uart_modem.h 文件名及接口名的情况下可直接使用。
* BSP 头文件或接口改名的情况下需要修改:将下方 include 和对应函数调用改为实际名称。
*/
#include <stdlib.h>
#include "at_port.h"
#include "bsp_time.h"
#include "bsp_uart_modem.h"
/* 可直接使用:串口打开状态变量。 */
static int s_uart_opened;
/* 使用第 2 步 C 库堆的情况下可直接使用。
* 改用内存池的情况下需要修改:将 at_malloc()、at_free() 中的 malloc/free 换为内存池配套接口,
* 分配失败返回 NULL。
*/
void *at_malloc(unsigned int nbytes)
{
return nbytes == 0u ? NULL : malloc(nbytes);
}
/* at_malloc() 使用 C 库 malloc() 的情况下可直接使用。
* at_malloc() 使用其他分配器的情况下需要修改:将 at_free() 中的 free(ptr) 换为配套释放接口,
* 允许传入 NULL。
*/
void at_free(void *ptr)
{
free(ptr);
}
/* 可直接使用。 */
unsigned int at_get_ms(void)
{
return (unsigned int)bsp_time_get_ms();
}
/* 目标工程固定使用一个 UART、115200/8N1 的情况下可直接使用。
* 需要运行时修改波特率的情况下需要修改:按本步末尾说明修改 bsp_modem_uart_init() 和
* at_uart_open(),传入 baud,初始化成功后才设置 s_uart_opened。
*/
int at_uart_open(const char *port, unsigned int baud)
{
(void)port;
(void)baud;
if (!s_uart_opened) {
bsp_modem_uart_init();
s_uart_opened = 1;
}
return 0;
}
/* 只需标记 UART 已关闭的情况下可直接使用。
* 需要关闭 UART 硬件的情况下需要修改:按本步末尾说明新增 BSP 关闭接口,在 at_uart_close()
* 中调用,并保留 s_uart_opened=0。
*/
void at_uart_close(void)
{
s_uart_opened = 0;
}
/* 可直接使用。 */
unsigned int at_uart_write(const void *buf, unsigned int len)
{
return (unsigned int)bsp_modem_write(buf, len);
}
/* 可直接使用。 */
unsigned int at_uart_read(void *buf, unsigned int len)
{
return (unsigned int)bsp_modem_read(buf, len);
}
/* 可直接使用;本例主循环不调用。 */
void at_uart_event_wait(unsigned int timeout_ms)
{
bsp_delay_ms(timeout_ms);
}
仅需运行时修改波特率或关闭 UART 硬件时修改:
App/<target>/platform/bsp/bsp_uart_modem.c/.h:将bsp_modem_uart_init()改为接收波特率并返回结果;Port 的at_uart_open()传入baud,成功后才设置s_uart_opened。- 同一 BSP
.c/.h:新增并声明bsp_modem_uart_close(),关闭 UART 及接收中断;Port 的at_uart_close()调用该函数。默认实现不需要此接口。
第 5 步:复用模组控制接口¶
修改位置:
- 将
App/Hc32f460/platform/bsp/bsp_modem.c/.h复制到App/<target>/platform/bsp/。 - 将
App/Hc32f460/config/at_config.h完整复制到App/<target>/config/at_config.h。
具体修改: 默认不接模组复位线,执行下表操作。
| 目标文件 | 函数或宏 | 操作 |
|---|---|---|
App/<target>/platform/bsp/bsp_modem.c |
整个文件 | 替换为参考实现第 2 项,保留全部控制接口和状态机 |
App/<target>/platform/bsp/bsp_modem.h |
类型及公开函数声明 | 原样保留 |
App/<target>/config/at_config.h |
AT_CFG_MODEM_RESET_GPIO_ENABLE |
设为 0;不填写参考实现中受该宏控制的 GPIO 代码 |
需要 GPIO 复位时,按参考实现第 3 项适配 reset_pin_write()、bsp_modem_init(),再启用该宏;未接线也保留 bsp_modem.c/.h。
参考实现:bsp_modem.c 及可选 GPIO 复位
1. 修改目标 App/<target>/config/at_config.h
修改已有宏,不重复定义:
/* 文件:App/<target>/config/at_config.h
* 位置:已有 AT_CFG_MODEM_RESET_GPIO_ENABLE 宏定义。
*/
/* 不使用 GPIO 复位的情况下可直接使用。
* 使用 GPIO 复位的情况下需要修改:按本步第 3 项接线并适配 GPIO,将
* AT_CFG_MODEM_RESET_GPIO_ENABLE 改为 1。
*/
#define AT_CFG_MODEM_RESET_GPIO_ENABLE (0u)
2. 替换目标 App/<target>/platform/bsp/bsp_modem.c
bsp_modem.h 原样保留;未接复位线时,以下代码无需填写 GPIO 调用。
/* 文件:App/<target>/platform/bsp/bsp_modem.c */
/* AT_CFG_MODEM_RESET_GPIO_ENABLE 为 0 的情况下可直接使用。
* AT_CFG_MODEM_RESET_GPIO_ENABLE 为 1 的情况下需要修改:保留下方五个 include,另加目标 SDK
* 的 GPIO 头文件。
*/
#include <stddef.h>
#include <string.h>
#include "at_config.h"
#include "bsp_time.h"
#include "bsp_modem.h"
/* 可直接使用。 */
static bsp_modem_control_stat_t s_stat;
static uint32_t s_state_ms;
static uint8_t s_complete_event;
#if AT_CFG_MODEM_RESET_GPIO_ENABLE
/* bsp_modem.c::reset_pin_write()
* 需修改:启用 GPIO 复位时,将 high 写入复位引脚。
*/
static void reset_pin_write(int active)
{
int high = AT_CFG_MODEM_RESET_ACTIVE_LOW ? !active : active;
(void)high;
#error "bsp_modem.c: implement reset_pin_write() with target SDK GPIO write"
}
#endif
/* AT_CFG_MODEM_RESET_GPIO_ENABLE 为 0 的情况下可直接使用。
* AT_CFG_MODEM_RESET_GPIO_ENABLE 为 1 的情况下需要修改:在 bsp_modem_init() 的 #error 处初
* 始化 GPIO,失败返回 -1;成功后保留下方复位释放及状态设置。
*/
int bsp_modem_init(void)
{
/* 可直接使用。 */
memset(&s_stat, 0, sizeof(s_stat));
s_stat.state = BSP_MODEM_RESET_IDLE;
s_complete_event = 0u;
s_state_ms = 0u;
#if AT_CFG_MODEM_RESET_GPIO_ENABLE
/* 需修改:初始化复位 GPIO,失败返回 -1。 */
#error "bsp_modem.c: implement bsp_modem_init() GPIO setup with target SDK"
reset_pin_write(0);
s_stat.supported = 1u;
#endif
return 0;
}
/* 可直接使用。 */
int bsp_modem_reset_supported(void)
{
return s_stat.supported ? 1 : 0;
}
/* 可直接使用。 */
int bsp_modem_request_reset(void)
{
if (!s_stat.supported) {
++s_stat.unsupported_count;
return -2;
}
if (s_stat.state != BSP_MODEM_RESET_IDLE) {
++s_stat.busy_reject_count;
return -1;
}
#if AT_CFG_MODEM_RESET_GPIO_ENABLE
reset_pin_write(1);
#endif
++s_stat.request_count;
s_stat.state = BSP_MODEM_RESET_ASSERTED;
s_state_ms = bsp_time_get_ms();
s_complete_event = 0u;
return 0;
}
/* 可直接使用;时序按 at_config.h 设置。 */
void bsp_modem_process(void)
{
uint32_t now;
if (!s_stat.supported || s_stat.state == BSP_MODEM_RESET_IDLE)
return;
now = bsp_time_get_ms();
if (s_stat.state == BSP_MODEM_RESET_ASSERTED &&
(uint32_t)(now - s_state_ms) >= AT_CFG_MODEM_RESET_PULSE_MS) {
#if AT_CFG_MODEM_RESET_GPIO_ENABLE
reset_pin_write(0);
#endif
s_stat.state = BSP_MODEM_RESET_SETTLING;
s_state_ms = now;
} else if (s_stat.state == BSP_MODEM_RESET_SETTLING &&
(uint32_t)(now - s_state_ms) >= AT_CFG_MODEM_RESET_SETTLE_MS) {
s_stat.state = BSP_MODEM_RESET_IDLE;
++s_stat.complete_count;
s_complete_event = 1u;
}
}
/* 可直接使用。 */
int bsp_modem_busy(void)
{
return s_stat.state != BSP_MODEM_RESET_IDLE ? 1 : 0;
}
/* 可直接使用。 */
int bsp_modem_take_reset_complete(void)
{
int complete = s_complete_event ? 1 : 0;
s_complete_event = 0u;
return complete;
}
/* 可直接使用。 */
void bsp_modem_get_control_stat(bsp_modem_control_stat_t *stat)
{
if (stat != NULL)
*stat = s_stat;
}
3. 需要 GPIO 硬复位时的修改位置
| 目标文件 | 函数或宏 | 操作 |
|---|---|---|
App/<target>/platform/bsp/bsp_modem.c |
文件头 | 包含目标 SDK 的 GPIO 头文件 |
| 同上 | reset_pin_write() |
将 high 写入实际复位引脚,删除该处 #error |
| 同上 | bsp_modem_init() |
初始化复位 GPIO,失败返回 -1;删除该处 #error,保留 reset_pin_write(0) 和 supported=1 |
App/<target>/config/at_config.h |
AT_CFG_MODEM_RESET_ACTIVE_LOW、AT_CFG_MODEM_RESET_PULSE_MS、AT_CFG_MODEM_RESET_SETTLE_MS |
按实际复位电平、脉冲和稳定时间设置 |
| 同上 | AT_CFG_MODEM_RESET_GPIO_ENABLE |
接线及上述适配完成后改为 1 |
只连接模组规定的复位输入,不接 PWRKEY 或电源开关;保留 bsp_modem_process() 的非阻塞时序。
第 6 步:复制 TCP 业务文件并修改服务器配置¶
修改位置: 将 App/Hc32f460/ 的 app_modem_prepare.c/.h、app_net.c/.h、app_tcp.c/.h、app_observer.h 复制至 App/<target>/;沿用第 5 步的 config/at_config.h。app_observer.h 不增加 .c 编译项。
具体修改: 三组功能文件原样保留,只修改目标 at_config.h 中下列已有宏。
| 宏 | 设置 |
|---|---|
AT_CFG_TCP_HOST |
实际公网域名或 IP,不带 tcp:// 和端口 |
AT_CFG_TCP_PORT |
服务器端口;使用公网映射时填公网端口 |
AT_CFG_TCP_ENABLE |
1u |
AT_CFG_TCP_BOOT_PAYLOAD |
"4321" |
AT_CFG_TCP_HEARTBEAT_ENABLE |
1u,普通模式启用模组侧心跳 |
AT_CFG_TCP_HEARTBEAT_CONTENT、AT_CFG_TCP_HEARTBEAT_INTERVAL_S |
"HELLO"、10 |
AT_CFG_TCP_SEND_PERIOD_MS |
10000ul,透传模式由 App 发送同一心跳 |
AT_CFG_RECV_BUF_SIZE、AT_CFG_URC_BUF_SIZE |
首次联调分别用 1024u、512u,按 RAM 和实际响应长度调整 |
其他宏保留;启动数据由 app_tcp_init() 记录。
参考实现:业务文件复用及 TCP 配置
App/<target>/config/at_config.h:修改已有宏,服务器示例值须替换;保留第 5 步配置。
/* 文件:App/<target>/config/at_config.h
* 位置:以下已有宏的定义处;不放入函数。
*/
/* 最长 URC 含结束空间不超过 512 字节且 RAM 足够的情况下可直接使用。
* 最长 URC 放不下的情况下需要修改:增大 at_config.h 的 AT_CFG_URC_BUF_SIZE,复测 URC 完整、
* 分配成功。
* 需要节省 RAM 的情况下需要修改:按最长 URC 含结束空间缩小该宏,复测 URC 完整、分配成功。
*/
#define AT_CFG_URC_BUF_SIZE (512u)
/* 最长累计响应含结束符不超过 1024 字节且 RAM 足够的情况下可直接使用。
* 最长响应放不下的情况下需要修改:增大 at_config.h 的 AT_CFG_RECV_BUF_SIZE,复测响应完整、
* 分配成功。
* 需要节省 RAM 的情况下需要修改:按最长响应含结束符缩小该宏,复测响应完整、分配成功。
*/
#define AT_CFG_RECV_BUF_SIZE (1024u)
/* 可直接使用。 */
#define AT_CFG_TCP_ENABLE (1u)
/* 需修改:填写实际公网域名或 IP,不带协议和端口。 */
#define AT_CFG_TCP_HOST "tcp.example.com"
/* 需修改:填写实际公网端口。 */
#define AT_CFG_TCP_PORT (9000)
/* 首次连接发送 4321 的情况下可直接使用。
* 首次连接发送其他数据的情况下需要修改:将 at_config.h 的 AT_CFG_TCP_BOOT_PAYLOAD 改为实际
* 启动字符串。
*/
#define AT_CFG_TCP_BOOT_PAYLOAD "4321"
/* 可直接使用:普通模式由模组发送心跳,透传模式由App发送。 */
#define AT_CFG_TCP_HEARTBEAT_ENABLE (1u)
#define AT_CFG_TCP_HEARTBEAT_CONTENT "HELLO"
#define AT_CFG_TCP_HEARTBEAT_INTERVAL_S (10)
#define AT_CFG_TCP_SEND_PERIOD_MS (10000ul)
第 7 步:接入应用入口¶
修改位置: App/<target>/main.c。
具体修改:
- 暂存第 1 步的 SDK 头文件和初始化语句,用下方参考实现替换
main.c。 - 补回 SDK 头文件,将初始化语句填入
<target>_board_init();不初始化模组 UART。 - 保留下方日志初始化、
demo_observe()及 TCP 回调设置;日志驱动在第 8 步适配。 - 将
DEMO_UART_TEST设为1;第 9 步基础测试通过后切换 TCP。
将参考代码中的 target_board_init() 定义和调用统一改为 <target>_board_init() 的实际名称,如 stm32_board_init();不编入 profile_main.c。
参考实现:App/<target>/main.c
/* 文件:App/<target>/main.c
* 位置:完整入口;SDK 初始化填写位置见 target_board_init()。
*/
/* 可直接使用:下方已有 #include 保持不变。
* 需修改:在这些行之前新增第 1 步保留的 SDK 头文件引用。
*/
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "bsp_log.h"
#include "app_observer.h"
#include "at_port.h"
#include "at_chat.h"
#include "at_urc.h"
#include "at_urc_baseline.h"
#include "at_config.h"
#include "bsp_uart_modem.h"
#include "bsp_time.h"
#include "bsp_modem.h"
#include "app_modem_prepare.h"
#include "app_net.h"
#include "app_tcp.h"
/* 验证内存、时基和基础 AT 收发的情况下可直接使用。
* 基础测试通过并开始 TCP 联调的情况下需要修改:将 main.c 的 DEMO_UART_TEST 改为 0,重新编译
* 下载。
*/
#define DEMO_UART_TEST 1
/* 可直接使用:初始化错误码变量。 */
static volatile unsigned int s_demo_error;
/* main.c::target_board_init()
* SDK 已完成板级初始化的情况下可直接使用空函数体,并删除 #error。
* main() 仍需初始化的情况下需要修改:填入第 1 步的 SDK 调用,再删除 #error。
* 不初始化模组 UART,不重复启动节拍。
*/
static void target_board_init(void)
{
#error "Adapt target_board_init() before building"
}
/* 可直接使用:打印初始化错误并停止。 */
static void error_handler(unsigned int code)
{
s_demo_error = code;
printf("[DEMO] init error=%u\r\n", code);
for (;;) { }
}
#if DEMO_UART_TEST
/* 可直接使用:以下基础测试变量。 */
static unsigned char s_test_rx[64];
static volatile unsigned int s_test_rx_len;
static volatile unsigned int s_test_ms;
static bsp_modem_stat_t s_test_uart_stat;
/* 可直接使用。 */
static void demo_basic_test(void)
{
void *p32 = at_malloc(32u);
void *p512 = at_malloc(512u);
void *p1024 = at_malloc(1024u);
int ok = (p32 != NULL && p512 != NULL && p1024 != NULL);
unsigned int start;
at_free(p32);
at_free(p512);
at_free(p1024);
if (!ok)
error_handler(6u);
printf("[BASIC] memory OK\r\n");
if (at_uart_write("AT\r\n", 4u) != 4u)
error_handler(7u);
start = at_get_ms();
s_test_rx_len = 0u;
while ((unsigned int)(at_get_ms() - start) < 1000u &&
s_test_rx_len < sizeof(s_test_rx)) {
s_test_rx_len += at_uart_read(&s_test_rx[s_test_rx_len],
(unsigned int)sizeof(s_test_rx) - s_test_rx_len);
}
bsp_modem_get_stat(&s_test_uart_stat);
printf("[BASIC RX] bytes=%u\r\n", s_test_rx_len);
for (unsigned int i = 0u; i < s_test_rx_len; ++i)
putchar(s_test_rx[i]);
printf("\r\n[BASIC UART] rb_ovf=%lu ovr=%lu fe=%lu pe=%lu\r\n",
(unsigned long)s_test_uart_stat.rb_overflow,
(unsigned long)s_test_uart_stat.overrun,
(unsigned long)s_test_uart_stat.frame_error,
(unsigned long)s_test_uart_stat.parity_error);
}
#else
/* 独立串口联调短数据和 HTTP 响应的情况下可直接使用。
* 使用已有日志接口的情况下需要修改:替换 printf/putchar,保留 data/len 处理。
* 连续大数据的情况下需要修改:改为非阻塞分块处理,避免打印长时间占用主循环。 */
static void demo_observe(const app_observer_event_t *e, void *user)
{
static const char *const names[] = {
"status", "rx", "header", "queued", "done", "error"
};
const unsigned char *p;
unsigned int i, n;
(void)user;
if (e == NULL || (unsigned int)e->kind >= 6u)
return;
if (e->kind == APP_OBSERVER_STATUS &&
(e->operation == NULL || strcmp(e->operation, "connection") != 0))
return;
printf("[%s] %s op=%s code_type=%d code=%d source=%d bytes=%u\r\n",
e->module, names[e->kind], e->operation ? e->operation : "",
(int)e->code_type, e->code, e->source_event, e->len);
if (e->message != NULL)
printf("%s\r\n", e->message);
p = (const unsigned char *)e->data;
n = p != NULL ? e->len : 0u;
if (strncmp(e->module, "HTTP", 4u) != 0 && n > 64u)
n = 64u;
for (i = 0u; i < n; ++i) {
unsigned int ch = p[i];
if (ch == '\r' || ch == '\n' || ch == '\t' ||
(ch >= 0x20u && ch != 0x7Fu))
putchar((int)ch);
else
printf("\\x%02X", ch);
}
if (n != 0u)
printf("\r\n");
if (n < e->len)
printf("preview=%u/%u\r\n", n, e->len);
}
/* 使用 TCP、MQTT/MQTTS 或 Audio 的情况下可直接使用。
* 使用 HTTP/HTTPS 的情况下需要修改:按 3.3.3 第 2 项替换 s_adapter.read,并在创建 AT 对象前
* 初始化读取保护。
*/
static const at_adapter_t s_adapter = {
.lock = NULL,
.unlock = NULL,
.write = at_uart_write,
.read = at_uart_read,
.error = NULL,
.debug = NULL,
.urc_bufsize = AT_CFG_URC_BUF_SIZE,
.recv_bufsize = AT_CFG_RECV_BUF_SIZE
};
/* 可直接使用:以下 AT 对象及 TCP 启用状态变量。 */
static at_obj_t *s_at;
static int s_tcp_started;
/* 使用 TCP 的情况下可直接使用。
* 使用 MQTT/MQTTS、HTTP/HTTPS 或 Audio 的情况下需要修改:按 3.3.3 第 3 项替换
* net_event_cb() 和启用标志。
*/
static void net_event_cb(app_net_event_t event, int value, void *user)
{
(void)value;
(void)user;
if (event == APP_NET_EVENT_REGISTERED) {
app_tcp_set_network(1);
if (!s_tcp_started) {
app_tcp_set_enabled(1);
s_tcp_started = 1;
}
} else if (event == APP_NET_EVENT_LOST) {
app_tcp_set_network(0);
} else if (event == APP_NET_EVENT_MODEM_RESET) {
app_tcp_on_modem_reset();
}
}
#endif
/* 完成 target_board_init()、BSP 适配并运行 TCP 的情况下可直接使用。
* 运行 MQTT/MQTTS、HTTP/HTTPS 或 Audio 的情况下需要修改:按 3.3.3 替换 main.c 的初始化、回
* 调及轮询调用。
*/
int main(void)
{
target_board_init();
bsp_time_init();
bsp_log_init();
printf("[DEMO] start\r\n");
if (bsp_modem_init() != 0)
error_handler(1u);
if (at_uart_open(NULL, 115200u) != 0)
error_handler(2u);
#if DEMO_UART_TEST
demo_basic_test();
unsigned int last_report = at_get_ms();
for (;;) {
s_test_ms = at_get_ms();
if ((unsigned int)(s_test_ms - last_report) >= 1000u) {
last_report = s_test_ms;
printf("[BASIC] ms=%u\r\n", s_test_ms);
}
}
#else
/* 可直接使用:TCP 初始化及 URC 注册。 */
s_at = at_obj_create(&s_adapter);
if (s_at == NULL)
error_handler(3u);
app_modem_prepare_init(s_at);
if (app_net_init(s_at, net_event_cb, NULL,
app_modem_prepare_command_mode, NULL) != 0)
error_handler(4u);
if (app_tcp_init(s_at) != 0)
error_handler(5u);
app_tcp_set_initial_transparent(0);
app_tcp_set_observer(demo_observe, NULL);
if (at_urc_add_baseline() != 0)
error_handler(8u);
at_urc_attach(s_at);
s_tcp_started = 0;
app_net_start();
for (;;) {
/* 使用 TCP 的情况下可直接使用。
* 使用 MQTT/MQTTS、HTTP/HTTPS 或 Audio 的情况下需要修改:按 3.3.3 第 3 项替换
* app_net_process() 参数和 app_tcp_process(),保留 at_obj_process()。
*/
at_obj_process(s_at);
app_net_process(app_tcp_stream_active() || app_tcp_transparent());
app_tcp_process();
}
#endif
}
第 8 步:配置 Keil 工程¶
修改位置:
- 将
App/Hc32f460/platform/bsp/bsp_log.c/.h复制到App/<target>/platform/bsp/,保留bsp_log_init()声明,修改.c中的 UART 初始化和发送调用。SDK 已有日志驱动时直接调用,不再写一套输出函数。 - 修改
App/<target>/main.c和App/<target>/mdk/<工程名>/<工程名>.uvprojx,按下表加入文件和配置。 - 目标工具链的 C 库未提供
strcasecmp/strncasecmp时,新建App/<target>/platform/compat/at_libc_compat.c/.h,参考实现见下方“字符串兼容”。
具体修改: 先完成参考实现第 2、3 项的文件适配,再按下表配置工程;第 9 步统一编译烧录。
| 位置 | 操作 |
|---|---|
.uvprojx → Project |
按下表创建 Group,用 Add Existing Files to Group 加入 .c;保留 SDK 分组,main.c 仅一份 |
.uvprojx → C/C++ → Include Paths |
加入参考实现第 1 项的目录,保留 SDK 路径;目标 config 排在其他含同名 at_config.h 的目录之前 |
目标 at_libc_compat.c/.h、C/C++ → Misc Controls |
按参考实现第 2 项选择兼容文件和声明头文件 |
目标 bsp_log.c/.h 或 SDK 日志文件 |
按参考实现第 3、4 项接入独立日志口;fputc() 或 SDK 输出入口仅保留一份 |
.uvprojx → Options for Target → Target → Use MicroLIB |
采用下方 fputc() 参考实现时勾选;复用 SDK 日志函数时保留原运行库配置 |
| Keil Group | 加入的 .c 文件 |
|---|---|
Port |
Port/<target>/at_<target>_port.c |
AT-Core |
AT/core/src/at_chat.c |
AT-Common |
AT/common/urc/at_urc.c、at_urc_baseline.c;AT/common/net/at_net.c;AT/common/sock/at_sock.c |
App |
App/<target>/main.c、app_modem_prepare.c、app_net.c、app_tcp.c |
Platform-BSP |
App/<target>/platform/bsp/bsp_time.c、bsp_uart_modem.c、bsp_modem.c |
Platform-Compat(工具链需要兼容实现时) |
App/<target>/platform/compat/at_libc_compat.c |
Platform-Log |
App/<target>/platform/bsp/bsp_log.c;复用 SDK 日志驱动时同时加入其依赖 |
只编入目标副本,不同时加入原目录的 HC32 BSP、Port、profile_main.c 或其他平台 Port。启动及中断文件沿用第 1 步。
参考实现:包含路径、字符串兼容及输出配置
1. 包含路径
在当前 Target 的 C/C++ → Include Paths 追加下表目录。
- 工程采用本文目录的情况下可直接使用。
- 工程位置不同的情况下需要修改:以
.uvprojx所在目录重新计算相对路径。
| 代码目录 | 相对路径 |
|---|---|
Port |
..\..\..\..\Port |
AT/core/include |
..\..\..\..\AT\core\include |
AT/common/urc |
..\..\..\..\AT\common\urc |
AT/common/net |
..\..\..\..\AT\common\net |
AT/common/sock |
..\..\..\..\AT\common\sock |
App/<target> |
..\.. |
App/<target>/config |
..\..\config |
App/<target>/platform/bsp |
..\..\platform\bsp |
App/<target>/platform/compat(新增兼容文件时) |
..\..\platform\compat |
2. 字符串兼容
按目标工具链的 C 库和已有工程是否提供 strcasecmp()、strncasecmp(),选择下表操作。兼容实现已列在下方。
| 当前工程情况 | 操作 |
|---|---|
| 已提供函数,调用处已包含声明 | 不添加兼容文件或预包含选项 |
| 已提供函数,调用处未包含声明 | 预包含声明头文件,不重复定义函数 |
| C 库和已有工程均未提供函数 | 使用下方 .c/.h,仅保留未提供的函数;编入 .c 并预包含 .h |
下方分别给出新建 .h、.c 的完整内容。两个函数分别判断,已有函数不再添加同名实现;按下表配置后在第 9 步编译链接。
App/<target>/platform/compat/at_libc_compat.h:
/* 文件:App/<target>/platform/compat/at_libc_compat.h */
/* C 库和目标工程均未提供这两个函数的情况下可直接使用。
* 仅有一个函数未提供的情况下需要修改:只保留该函数声明,另一个引用原声明头文件。
* 两个函数均已提供的情况下需要修改:不添加兼容文件;调用处未包含声明时预包含原声明头文件。
*/
#ifndef AT_LIBC_COMPAT_H
#define AT_LIBC_COMPAT_H
int strcasecmp(const char *s1, const char *s2);
int strncasecmp(const char *s1, const char *s2, unsigned int n);
#endif
App/<target>/platform/compat/at_libc_compat.c:
/* 文件:App/<target>/platform/compat/at_libc_compat.c */
/* 可直接使用。 */
#include <ctype.h>
/* 沿用文件名和目录的情况下可直接使用。
* 改名或移动的情况下需要修改:更新下方 include 和 Keil 包含路径。
*/
#include "at_libc_compat.h"
/* C 库和目标工程均未提供 strcasecmp() 的情况下可直接使用。
* 已有实现的情况下需要修改:删除本函数,在 at_libc_compat.h 引用原声明。
*/
int strcasecmp(const char *s1, const char *s2)
{
for (;;) {
int a = tolower((unsigned char)*s1++);
int b = tolower((unsigned char)*s2++);
if (a != b || a == 0)
return a - b;
}
}
/* C 库和目标工程均未提供 strncasecmp() 的情况下可直接使用。
* 已有实现的情况下需要修改:删除本函数,在 at_libc_compat.h 引用原声明。
*/
int strncasecmp(const char *s1, const char *s2, unsigned int n)
{
while (n != 0u) {
int a = tolower((unsigned char)*s1++);
int b = tolower((unsigned char)*s2++);
if (a != b || a == 0)
return a - b;
--n;
}
return 0;
}
使用上述兼容文件时,在当前 Target 的 C/C++ → Misc Controls 追加下表选项;保留第 1 步的 C99/GNU 和 SDK 选项。
| 编译器 | 追加选项 |
|---|---|
| AC5 | --preinclude=at_libc_compat.h |
| AC6 | -include at_libc_compat.h |
C 库已提供函数、调用处未包含声明时,将选项中的 at_libc_compat.h 换为原声明头文件;已包含声明时不添加该选项。
3. 日志输出
以下修改复制后的 App/<target>/platform/bsp/bsp_log.c/.h。PA15/PA10 只是 HC32 参考引脚,换板后按实际接线设置。
| 文件及位置 | 修改内容 |
|---|---|
bsp_log.h → bsp_log_init() |
保留声明,更新串口和引脚注释 |
bsp_log.c → 文件头 |
替换 hc32_ddl.h,使用目标 SDK 头文件 |
bsp_log.c → LOG_*、m_uart_cfg |
改为实际 UART、引脚及 115200/8N1 |
bsp_log.c → bsp_log_init() |
配置时钟、引脚和串口;失败时停止 |
bsp_log.c → fputc() |
从日志 UART 发字符,等待须有超时 |
下方为局部修改示例:.h 保留原头文件保护和声明;.c 在原位置修改头文件、配置及函数,不重复追加。填入目标 SDK 调用后删除对应的 #error。
/* 文件:App/<target>/platform/bsp/bsp_log.h
* 需修改:原文件头的串口、引脚注释改为实际接线。 */
/* 可直接使用:保留原头文件及以下声明。 */
void bsp_log_init(void);
/* 文件:App/<target>/platform/bsp/bsp_log.c */
/* 需修改:将原 hc32_ddl.h 换成目标 SDK 的 UART、GPIO、时钟头文件。 */
/* 可直接使用:保留以下两个引用。 */
#include <stdio.h>
#include "bsp_log.h"
/* 需修改:在原定义位置重写 LOG_USART、LOG_TX_*、LOG_RX_*、LOG_BAUDRATE、m_uart_cfg,
* 设置实际 UART、引脚复用及 115200/8N1;不再使用的定义删除。只输出日志时可不配置 RX。 */
/* SDK 已完成日志 UART 时钟、引脚和串口配置的情况下可直接使用空函数体,并删除 #error。
* SDK 尚未配置的情况下需要修改:在此函数中配置时钟、TX 引脚复用、115200/8N1 和发送使能,
* 失败时停止;删除原 HC32 调试引脚释放调用。 */
void bsp_log_init(void)
{
#error "Adapt bsp_log_init() before building"
}
/* 使用 MicroLIB 的情况下需要修改:用目标 SDK 的日志 UART 发送函数替换原函数体,
* 设置等待超时;成功返回 ch,失败或超时返回 EOF。
* SDK 已有 fputc() 或其他输出函数的情况下需要修改:删除此函数,保留 SDK 输出函数,
* 确认它发送到实际接线的日志 UART。 */
int fputc(int ch, FILE *file)
{
(void)file;
#error "Adapt fputc() before building"
return EOF;
}
4. 日志初始化
第 7 步已在 App/<target>/main.c 中包含 bsp_log.h,并按以下顺序调用,无需重复添加。
/* 位置:App/<target>/main.c::main(),模组初始化及创建 AT 对象之前。
* 保留上述接口名的情况下可直接使用。
* 日志接口改名的情况下需要修改:替换 bsp_log.h 的引用和下方初始化调用。 */
target_board_init();
bsp_time_init();
bsp_log_init();
printf("[DEMO] start\r\n");
将日志 TX 接 USB-TTL RX,GND 共地。串口助手选实际 COM 口,设为 115200、8N1;先打开串口,再按 MCU 实体 RST,应先看到 [DEMO] start。
第 9 步:编译烧录与联调¶
修改位置: App/<target>/main.c 文件头的 DEMO_UART_TEST;服务器地址在 App/<target>/config/at_config.h 的 AT_CFG_TCP_HOST、AT_CFG_TCP_PORT。
具体修改: 分两轮编译烧录,第一轮通过后再进行第二轮;修改宏不会自动更新 MCU 中的固件。
两轮共用操作: 每轮配置完成后,执行 Rebuild → Download,确认 Verify OK 后退出调试;等待 YM310 完成开机,打开独立日志口,再按 MCU 实体 RST。沿用第 1~8 步的文件和日志配置,测试时不暂停 MCU,不选中 PowerShell 文字。
1. 基础接口测试(第一次烧录)
| 顺序 | 操作 | 核对结果 |
|---|---|---|
| 1 | 将 main.c 中已有的 DEMO_UART_TEST 设为 1 |
选择基础测试,不启动 TCP |
| 2 | 在 MCU 实体复位前,确保 YM310 已完成开机并处于 AT 命令模式;本轮不需要 TCP 服务器 | 基础测试直接收发 AT,不执行模组等待和退出透传流程 |
| 3 | 执行上方共用操作,完成第一次烧录并实体复位 | 日志显示 [DEMO] start |
| 4 | 查看内存、AT 应答、UART 错误计数和毫秒计数 | memory OK,应答含 OK,错误计数为 0;ms 每秒增加约 1000 |
参考结果:基础测试日志
[DEMO] start
[BASIC] memory OK
[BASIC RX] bytes=... 应答中应包含 OK
[BASIC UART] rb_ovf=0 ovr=0 fe=0 pe=0
[BASIC] ms=... 每秒输出一次,数值增加约 1000
出现 [DEMO] init error=N 时,在第 7 步的 main.c 中查找 error_handler(N),定位失败的初始化调用。若只显示启动行、不再推进,检查第 2 步的毫秒中断及第 3 步的 UART 初始化。以上结果均符合后继续第二轮;仅显示 memory OK 不表示 AT 收发通过。
2. TCP 功能测试(第二次烧录)
| 顺序 | 操作 | 核对结果 |
|---|---|---|
| 1 | 启动下方回显服务器并配置公网映射;将公网域名、端口填入 at_config.h 的 AT_CFG_TCP_HOST、AT_CFG_TCP_PORT |
服务器持续监听 |
| 2 | 将 main.c 中同一处 DEMO_UART_TEST 改为 0;保留 app_tcp_set_initial_transparent(0) 和第 7 步的 TCP 观测回调 |
选择普通 TCP 功能 |
| 3 | 再次执行上方共用操作,完成第二次烧录并实体复位 | TCP 固件替换基础测试固件,开始联网和 TCP 连接 |
| 4 | 核对服务器与 MCU 日志 | 服务器先收到 4321,随后每约 10 秒收到 HELLO;MCU 回调收到对应回显 |
| 5 | 等待服务器关闭会话 | 新连接建立,恢复 HELLO 收发 |
后续功能测试保持 DEMO_UART_TEST=0。4321 每次 MCU 启动只发一次,不要求重连后重发。
参考实现:TCP 回显服务器与结果核对
电脑服务器:在源码包根目录执行。
准备 Python 3 和 TCP 公网映射;已有可公网访问、支持回显和主动断线的 TCP 服务器时可直接使用。
# 从源码包根目录运行且监听 8001 端口的情况下可直接使用。
# 监听其他端口的情况下需要修改:将下方 --port 8001 改为实际本地监听端口,
# 并将公网映射的本地端口设为同一值。
python .\App\Hc32f460\tools\tcp_reconnect_server.py --host 0.0.0.0 --port 8001 --close-after 25
将公网映射地址及端口填入 App/<target>/config/at_config.h 的 AT_CFG_TCP_HOST、AT_CFG_TCP_PORT。服务器回显数据,每条连接 25 秒后关闭并继续监听。
TCP 回显核对
第 7 步 main.c::demo_observe() 输出连接状态、动作结果及回显。rx 数据应包含 4321、HELLO,允许分块到达;服务器关闭会话后应重新连接并恢复心跳。测试时不暂停 MCU,无需进入调试器。
3.3.2 验证与排错¶
本节检查第 9 步的基础接口和普通 TCP:核对 4321、周期 HELLO 的回显,以及断线后的心跳恢复。服务器显示 ACCEPT 只说明已连接,还要核对收发数据。TCP 透传、MQTT、HTTP、Audio 的检查表见 3.3.3“编译联调”。
| 现象 | 检查位置 |
|---|---|
Core 编译报零长度数组、匿名结构/联合或 typeof 错误 |
第 1 步:AC5 保留 C99 Mode 并追加 --gnu;AC6 使用 -std=gnu99;添加第 8 步的预包含选项时,不覆盖这些设置 |
strcasecmp/strncasecmp 未声明或重复定义 |
第 8 步:C 库已提供函数时引用原声明;工具链未提供时使用 at_libc_compat.c/.h,只保留一套实现 |
s_demo_error=0,但程序停在 UART 初始化 |
在 App/<target>/platform/bsp/bsp_uart_modem.c 的 bsp_modem_uart_init() 中检查 modem_hw_init() 返回值;默认实现失败时停在该函数的 for (;;),不会设置错误码 2 |
| 毫秒计数不增加或超时速度不正确 | 第 2 步:目标 SDK 中断 .c 的 SysTick_Handler() 是否每 1 ms 调用一次 bsp_time_tick_1ms();App/<target>/platform/bsp/bsp_time.c 的 bsp_time_init() 是否重复配置节拍 |
| 创建 AT 对象失败或内存不足 | 第 2、4、6 步:目标 SDK 的 C 库堆配置、at_malloc/free 实现及响应/URC 缓冲大小。确认 malloc 返回值,结合工作队列峰值调整堆容量;分配和释放使用同一分配器 |
| TCP 不启动或不重连 | 第 6、7 步:App/<target>/config/at_config.h 的 AT_CFG_TCP_HOST、AT_CFG_TCP_PORT;App/<target>/main.c 的 net_event_cb() 事件转发,以及 main() 的 at_obj_process()、app_net_process()、app_tcp_process() 调用 |
| 已连接但无启动数据 | 先让模组完成开机,再复位 MCU;4321 每次 MCU 启动只发送一次,重连后恢复 HELLO 心跳 |
3.3.3 平台依赖与初始化约束¶
TCP 验证通过后,保留 Port、BSP、app_modem_prepare.c/.h、app_net.c/.h、app_observer.h 和 AT 基础文件。按下面四项更换功能文件、配置、初始化和主循环。
1. 加入业务文件
下表 App 文件复制到 App/<target>/,AT 文件从原路径编入。Keil 添加方法见第 8 步;切换为非 TCP 业务时,从当前 Target 移出 app_tcp.c、at_sock.c。各工程只编入一个 main.c,目标 config 目录排在公共配置目录之前。
| 业务 | App 文件(复制后的路径) | AT 文件(原路径编入) |
|---|---|---|
| TCP / 透传 | App/<target>/app_tcp.c/.h |
AT/common/sock/at_sock.c/.h |
| MQTT / MQTTS | App/<target>/app_mqtt.c/.h |
AT/common/mqtt/at_mqtt.c/.h |
| HTTP / HTTPS | App/<target>/app_http.c/.h |
AT/common/http/at_http.c/.h |
| Audio | App/<target>/app_audio.c/.h |
AT/common/audio/at_audio.c/.h |
下文接口在上表对应的 .c/.h 中实现和声明。
App .c 加入 Keil App 分组,AT .c 加入 AT-Common;在 C/C++ → Include Paths 追加下表目录。保留第 8 步公共路径,TCP 的 AT/common/sock 不重复添加。
| 所选业务 | 追加的头文件目录 | .uvprojx 位于 App/<target>/mdk/<工程名>/ 时的相对路径 |
|---|---|---|
| MQTT / MQTTS | AT/common/mqtt |
..\..\..\..\AT\common\mqtt |
| HTTP / HTTPS | AT/common/http |
..\..\..\..\AT\common\http |
| Audio | AT/common/audio |
..\..\..\..\AT\common\audio |
工程目录不同,按第 8 步的方法以 .uvprojx 所在目录换算相对路径。
2. 修改配置与初始化
以下参考实现仅替换 App/<target>/main.c 和配置文件中的指定部分,不覆盖整个文件;复制的业务 .c/.h 保持不变。选用一份业务参考实现,再按下方“事件与主循环”替换回调和轮询调用。
App/<target>/config/at_config.h:替换已有同名宏,不在文件末尾重复追加;未列出的配置保留。服务器及音频参数按实际环境修改。App/<target>/main.c文件头:替换原#include "app_tcp.h"。App/<target>/main.c::main():找到以下原代码,用所选业务的初始化片段整体替换;前面的app_net_init()和后面的at_urc_add_baseline()保留。
if (app_tcp_init(s_at) != 0)
error_handler(5u);
app_tcp_set_initial_transparent(0);
参考实现:TCP / 透传配置与初始化
/* 文件:App/<target>/config/at_config.h
* 需修改:替换已有同名宏,填入实际服务器域名及端口。 */
#define AT_CFG_TCP_ENABLE (1u)
#define AT_CFG_TCP_HOST "tcp.example.com"
#define AT_CFG_TCP_PORT (9000)
/* 文件:App/<target>/main.c 文件头。
* 可直接使用:保留 TCP 头文件。 */
#include "app_tcp.h"
/* 位置:App/<target>/main.c::main() 的原 TCP 初始化块。
* 普通 TCP 可直接使用;使用透传时,将最后一行参数 0 改为 1。 */
if (app_tcp_init(s_at) != 0)
error_handler(5u);
app_tcp_set_initial_transparent(0);
参考实现:MQTT / MQTTS 配置与初始化
/* 文件:App/<target>/config/at_config.h
* 需修改:替换已有同名宏,按服务器设置地址、端口、账号和主题。
* CLIENT_ID 必须与同一服务器上的其他客户端区分。 */
#define AT_CFG_MQTT_ENABLE (1u)
#define AT_CFG_MQTT_HOST "tests.yuge-info.com"
#define AT_CFG_MQTT_PORT (1883)
#define AT_CFG_MQTTS_PORT (8883)
#define AT_CFG_MQTT_CLIENT_ID "at_demo_01"
#define AT_CFG_MQTT_USERNAME ""
#define AT_CFG_MQTT_PASSWORD ""
#define AT_CFG_MQTT_SUB_TOPIC "/hello/test_1"
#define AT_CFG_MQTT_PUB_TOPIC "/hello/test_2"
/* 文件:App/<target>/main.c 文件头。
* 可直接使用:用下行替换原 #include "app_tcp.h"。 */
#include "app_mqtt.h"
/* 位置:App/<target>/main.c::main() 的原 TCP 初始化块。
* MQTT 可直接使用;使用 MQTTS 时,将 set_secure() 参数 0 改为 1。 */
if (app_mqtt_init(s_at) != 0)
error_handler(5u);
app_mqtt_set_secure(0);
app_mqtt_set_demo_publish(0);
参考实现:HTTP / HTTPS 配置与初始化
/* 文件:App/<target>/config/at_config.h
* 需修改:替换已有同名宏,填入实际请求地址和 POST 正文。
* HEAD 使用对应的 GET 地址。 */
#define AT_CFG_HTTP_POST_URL "http://tests.yuge-info.com:9000/httpbin/post"
#define AT_CFG_HTTP_GET_URL "http://tests.yuge-info.com:9000/httpbin/get"
#define AT_CFG_HTTPS_POST_URL "https://tests.yuge-info.com:9443/httpbin/post"
#define AT_CFG_HTTPS_GET_URL "https://tests.yuge-info.com:9443/httpbin/get"
#define AT_CFG_HTTP_POST_BODY "4231"
#define AT_CFG_HTTPS_POST_BODY "433"
/* 文件:App/<target>/main.c 文件头。
* 可直接使用:用下行替换原 #include "app_tcp.h"。 */
#include "app_http.h"
/* 位置:App/<target>/main.c,main() 外的 s_adapter 定义。
* 可直接使用:用以下定义替换第 7 步的 s_adapter;仅 .read 不同。 */
static const at_adapter_t s_adapter = {
.lock = NULL,
.unlock = NULL,
.write = at_uart_write,
.read = app_http_uart_guard_read,
.error = NULL,
.debug = NULL,
.urc_bufsize = AT_CFG_URC_BUF_SIZE,
.recv_bufsize = AT_CFG_RECV_BUF_SIZE
};
/* 位置:App/<target>/main.c::main()。
* 可直接使用:用以下四行替换原 AT 对象创建及 NULL 检查,勿重复创建对象。
* 后面的 app_modem_prepare_init()、app_net_init() 保留。 */
app_http_uart_guard_init(at_uart_read);
s_at = at_obj_create(&s_adapter);
if (s_at == NULL)
error_handler(3u);
/* 位置:App/<target>/main.c::main() 的原 TCP 初始化块。
* HTTP 默认 POST 可直接使用;使用 HTTPS 时,将 configure() 首参改为 1。 */
if (app_http_init(s_at) != 0)
error_handler(5u);
app_http_configure(0, AT_HTTP_POST);
这两处设置用于收齐分片的 +HTTPACTION 长度字段后再解析,避免把长度 398 误读成 3。两个函数位于 App/<target>/app_http.c/.h,底层 at_uart_read() 沿用第 4 步。
参考实现:Audio 配置与初始化
/* 文件:App/<target>/config/at_config.h
* ES8311 且板上有录音输入时,以下值可直接使用。
* 使用 TM8211、PWM、SPI 时,将 CODEC 分别改为 1、2、3,HAS_INPUT 改为 0。
* 若 Keil C/C++ -> Define 已定义同名宏,同步修改工程定义。 */
#define AT_CFG_AUDIO_CODEC (0)
#define AT_CFG_AUDIO_HAS_INPUT (1)
/* ES8311、TM8211、SPI 以 3 档联调的情况下可直接使用。
* PWM 对照测试的情况下需要修改:将下方音量改为 2,再按实际输出调整。 */
#define AT_CFG_AUDIO_VOLUME (3)
/* 可直接使用:四类统一播报;CTTS mode=2 使用 GBK 字节。 */
#define AT_CFG_AUDIO_TTS_MODE (2)
#define AT_CFG_AUDIO_TTS_TEXT "\xC9\xCF\xBA\xA3\xD3\xF2\xB8\xF1\xD0\xC5\xCF\xA2\xBC\xBC\xCA\xF5\xD3\xD0\xCF\xDE\xB9\xAB\xCB\xBE\x54\x54\x53\xD3\xEF\xD2\xF4\xB2\xA5\xB7\xC5\xB2\xE2\xCA\xD4"
四类使用相同的 Audio API,由 YM310 选择不同的音频通路。本章不需要按键,烧录后自动执行下表中的测试流程。
| 类型 | AT_CFG_AUDIO_CODEC / AT_CFG_AUDIO_HAS_INPUT |
模组侧音频通路 | 接入验证代码后的执行结果 |
|---|---|---|---|
| ES8311 | 0 / 1 |
I²S 音频、I²C 配置,连接 ES8311、麦克风和输出电路 | TTS → 等待 1 秒 → 录音 5 秒 → 回放同一录音 |
| TM8211 | 1 / 0 |
I²S 输出到 TM8211 及配套输出电路 | 该通路播报一次后待机 |
| PWM | 2 / 0 |
YM310 的 PWM 音频输出及配套电路 | 该通路播报一次后待机 |
| SPI | 3 / 0 |
YM310 的 SPI 音频输出及配套电路 | 该通路播报一次后待机 |
按模组板上的电路和固件选择通路,不能只改宏就测试全部四类。MCU 只通过 UART 发 AT 指令,不需要另写 I²S、I²C、PWM 或 SPI 音频驱动。
ES8311 没接麦克风时,将 AT_CFG_AUDIO_HAS_INPUT 改为 0,只测试播报。保留 AT_CFG_AUDIO_RECORD_FILE,录音和回放使用同一个文件;每次测试会覆盖它。
音量按扬声器和输出电路调整,播放时听完整句。
四类都播报“上海域格信息技术有限公司TTS语音播放测试”。通过 AT+CAUDPARAM="CODECTYPE",0/1/2/3 的配置结果和对应电路的声音区分通路。
/* 文件:App/<target>/main.c 文件头。
* 可直接使用:用下行替换原 #include "app_tcp.h"。 */
#include "app_audio.h"
/* 位置:App/<target>/main.c::main() 的原 TCP 初始化块。
* 可直接使用:Audio 初始化失败时沿用第 7 步的错误处理。
* 本段仅初始化;启用后配置音频通路和音量,不自动播报、录音或播放。
* 验证触发代码见下方“事件与主循环”中的 Audio 参考实现,烧录后按“编译联调”检查。 */
if (app_audio_init(s_at) != 0)
error_handler(5u);
3. 事件与主循环
替换事件回调和标志声明
在 App/<target>/main.c 中,用所选业务的一份参考实现替换 static int s_tcp_started; 及完整 net_event_cb();TCP / 透传沿用第 7 步。事件定义见 app_net.h。
参考实现:MQTT / MQTTS
/* App/<target>/main.c:替换原 static int s_tcp_started; */
static int s_mqtt_started;
/* App/<target>/main.c:整体替换原 net_event_cb()。 */
static void net_event_cb(app_net_event_t event, int value, void *user)
{
(void)value;
(void)user;
if (event == APP_NET_EVENT_REGISTERED) {
app_mqtt_set_network(1);
if (!s_mqtt_started) {
app_mqtt_set_enabled(1);
s_mqtt_started = 1;
}
} else if (event == APP_NET_EVENT_LOST) {
app_mqtt_set_network(0);
} else if (event == APP_NET_EVENT_MODEM_RESET) {
app_mqtt_on_modem_reset();
}
}
参考实现:HTTP / HTTPS
/* App/<target>/main.c:替换原 static int s_tcp_started; */
static int s_http_started;
/* App/<target>/main.c:整体替换原 net_event_cb()。 */
static void net_event_cb(app_net_event_t event, int value, void *user)
{
(void)value;
(void)user;
if (event == APP_NET_EVENT_REGISTERED) {
app_http_set_network(1);
if (!s_http_started) {
app_http_set_enabled(1);
s_http_started = 1;
}
} else if (event == APP_NET_EVENT_LOST) {
app_http_set_network(0);
} else if (event == APP_NET_EVENT_MODEM_RESET) {
app_http_on_modem_reset();
}
}
参考实现:Audio
/* App/<target>/main.c:替换原 static int s_tcp_started; */
static int s_audio_started;
/* App/<target>/main.c:整体替换原 net_event_cb()。 */
static void net_event_cb(app_net_event_t event, int value, void *user)
{
(void)value;
(void)user;
if (event == APP_NET_EVENT_MODEM_READY) {
if (!s_audio_started) {
app_audio_set_enabled(1);
s_audio_started = 1;
}
} else if (event == APP_NET_EVENT_MODEM_RESET) {
/* 先通知未完成的操作失败,再清空启用状态。 */
app_audio_on_modem_reset();
app_audio_set_enabled(0);
s_audio_started = 0;
}
}
替换 main() 中的标志初始化
在 App/<target>/main.c::main() 中找到以下原代码,只替换中间的 s_tcp_started = 0;,前后两行保留。
at_urc_attach(s_at);
s_tcp_started = 0; /* 仅替换此行,替换内容见下表。 */
app_net_start();
| 所选业务 | 中间一行替换为 |
|---|---|
| TCP / 透传 | s_tcp_started = 0;(保持原样) |
| MQTT / MQTTS | s_mqtt_started = 0; |
| HTTP / HTTPS | s_http_started = 0; |
| Audio | s_audio_started = 0; |
上面的参考实现已包含标志判断和赋值,不再另加启用或清零代码。
替换主循环调用
App/<target>/main.c → main() 的 for (;;):保留 at_obj_process(s_at),其后按下表顺序调用。
| 业务 | 网络轮询 | 业务轮询 |
|---|---|---|
| TCP / 透传 | 沿用第 7 步 | app_tcp_process() |
| MQTT / MQTTS | app_net_process(app_mqtt_busy()) |
app_mqtt_process() |
| HTTP / HTTPS | app_net_process(app_http_busy()) |
app_http_process() |
| Audio | app_net_process(app_audio_busy()) |
app_audio_process() |
主循环负责处理网络和功能状态;要发消息、请求 GET/HEAD 或播放声音,还要加入下面对应的测试代码。
替换结果回调注册
在 App/<target>/main.c::main() 中,将 app_tcp_set_observer(demo_observe, NULL); 替换为下表对应调用,位置仍在功能初始化成功后、at_urc_add_baseline() 之前。保留第 7 步的 demo_observe() 和日志初始化,不修改功能 .c。
| 业务 | main() 中保留或替换的调用 |
|---|---|
| TCP / 透传 | app_tcp_set_observer(demo_observe, NULL); |
| MQTT / MQTTS | app_mqtt_set_observer(demo_observe, NULL); |
| HTTP / HTTPS | app_http_set_observer(demo_observe, NULL); |
| 四种 Audio | app_audio_set_observer(demo_observe, NULL); |
这些接口位于对应的 app_*.c/.h,事件类型见 app_observer.h。Audio 还要保留下方的 app_audio_set_event_cb():一个打印结果,一个控制测试顺序。不要覆盖模块内部的 Common 回调。
日志中可能同时出现模块输出和回调输出,不代表重复收发。HTTP/HTTPS 打印完整响应头和正文;TCP/MQTT 每块最多打印 64 字节,len 是实际收到的长度。
回调中的数据需要保存时先复制,不保存原指针。后续发送或播放在主循环中调用。queued 表示已接收请求,done/error 表示对应操作完成或失败;字段说明见 app_observer.h。
接入验证动作
将对应测试代码加到 App/<target>/main.c:状态变量和函数放在 main() 外,测试调用放在主循环的功能轮询之后。沿用原来的 for (;;),不再增加主循环。烧录后的运行结果见下方“编译联调”;测试通过后再按需要调整触发时机。
参考实现:MQTT / MQTTS 发布验证
以下代码在 MCU 连接、订阅成功后发布一次 8765。主题沿用上方“修改配置与初始化”中的配置,修改主题时同步修改下方调用和测试端配置;MQTTX 操作见下方“编译联调”。
/* 文件:App/<target>/main.c,放在 static int s_mqtt_started; 后。
* 可直接使用:记录本次 MCU 运行期间是否已受理测试发布。 */
static int s_mqtt_test_submitted;
/* 位置:App/<target>/main.c::main() 的 for (;;) 内,紧接 app_mqtt_process(); 后。
* 使用上述测试主题和数据的情况下可直接使用。
* 使用其他主题或数据的情况下需要修改:替换下方主题、数据及对应字节长度。 */
if (!s_mqtt_test_submitted && app_mqtt_ready() && !app_mqtt_busy()) {
if (app_mqtt_publish_to("/hello/test_2", "8765", 4u, 0) == 0)
s_mqtt_test_submitted = 1;
}
app_mqtt_publish_to() 在 App/<target>/app_mqtt.c/.h 中实现和声明;订阅数据通过上述观测回调的 APP_OBSERVER_DATA 事件传入。数据可能含 +MSUB 头,不要求原始回调长度等于纯载荷长度。
参考实现:HTTP / HTTPS GET、HEAD 验证
上方“修改配置与初始化”已配置默认 POST。以下代码在 POST 成功后追加 GET,GET 成功后追加 HEAD;三次请求成功后不再提交。
/* 文件:App/<target>/main.c,放在 static int s_http_started; 后。
* 可直接使用:0=尚未提交 GET,1=已提交 GET,2=已提交 HEAD;不代表请求已完成。 */
static unsigned int s_http_test_step;
/* 位置:App/<target>/main.c::main() 的 for (;;) 内,紧接 app_http_process(); 后。
* 按 POST -> GET -> HEAD 验证的情况下可直接使用。
* 需要自己决定请求时机的情况下需要修改:移除此测试块,在发起请求的位置检查
* app_http_ready() 后调用 app_http_next_method()。 */
if (s_http_test_step < 2u && app_http_ready()) {
if (app_http_next_method() == 0)
++s_http_test_step;
}
app_http_next_method() 位于 App/<target>/app_http.c/.h;请求失败时由模块退避重试,当前请求成功后才会推进。观测回调应分别收到三次请求的完成通知及 2xx 状态,不能只凭 s_http_test_step=2 判定 HEAD 成功。
参考实现:Audio 分通路验证
Audio 初始化完成后不会自动发声,还需在 main.c 加入下面三段代码:测试函数、事件回调设置和主循环调用。模组就绪后即可测试,不等待驻网。ES8311 有录音输入时执行录音回放;TM8211、PWM、SPI 只播报,不录音。
/* 文件:App/<target>/main.c,放在 static int s_audio_started; 后。
* 可直接使用:记录完成事件,主循环再提交下一项操作。 */
typedef enum {
AUDIO_CHECK_START_TTS = 0,
AUDIO_CHECK_WAIT_TTS,
AUDIO_CHECK_RECORD_DELAY,
AUDIO_CHECK_START_RECORD,
AUDIO_CHECK_WAIT_RECORD,
AUDIO_CHECK_START_PLAY,
AUDIO_CHECK_WAIT_PLAY,
AUDIO_CHECK_DONE,
AUDIO_CHECK_FAILED
} audio_check_step_t;
static volatile audio_check_step_t s_audio_check_step;
#if AT_CFG_AUDIO_CODEC == 0 && AT_CFG_AUDIO_HAS_INPUT
static unsigned int s_audio_record_delay_ms;
#endif
/* 可直接使用:只记录事件,不在回调内提交下一条音频命令。 */
static void audio_check_event(app_audio_event_t event, void *user)
{
(void)user;
if (event == APP_AUDIO_EVENT_OUTPUT_FAIL ||
event == APP_AUDIO_EVENT_INPUT_FAIL) {
s_audio_check_step = AUDIO_CHECK_FAILED;
} else if (event == APP_AUDIO_EVENT_OUTPUT_DONE &&
s_audio_check_step == AUDIO_CHECK_WAIT_TTS) {
#if AT_CFG_AUDIO_CODEC == 0 && AT_CFG_AUDIO_HAS_INPUT
s_audio_record_delay_ms = at_get_ms();
s_audio_check_step = AUDIO_CHECK_RECORD_DELAY;
#else
s_audio_check_step = AUDIO_CHECK_DONE;
#endif
} else if (event == APP_AUDIO_EVENT_INPUT_DONE &&
s_audio_check_step == AUDIO_CHECK_WAIT_RECORD) {
s_audio_check_step = AUDIO_CHECK_START_PLAY;
} else if (event == APP_AUDIO_EVENT_OUTPUT_DONE &&
s_audio_check_step == AUDIO_CHECK_WAIT_PLAY) {
s_audio_check_step = AUDIO_CHECK_DONE;
}
}
/* 按本节执行一次验证的情况下可直接使用。
* 需要自己触发音频操作的情况下需要修改:调整本函数的动作顺序,保留完成事件判断。 */
static void audio_check_process(void)
{
if (!s_audio_started || app_audio_busy())
return;
if (s_audio_check_step == AUDIO_CHECK_START_TTS) {
if (app_audio_tts_start(AT_CFG_AUDIO_TTS_TEXT) == 0)
s_audio_check_step = AUDIO_CHECK_WAIT_TTS;
}
#if AT_CFG_AUDIO_CODEC == 0 && AT_CFG_AUDIO_HAS_INPUT
else if (s_audio_check_step == AUDIO_CHECK_RECORD_DELAY) {
if ((unsigned int)(at_get_ms() - s_audio_record_delay_ms) >= 1000u)
s_audio_check_step = AUDIO_CHECK_START_RECORD;
} else if (s_audio_check_step == AUDIO_CHECK_START_RECORD) {
if (app_audio_record_start(AT_CFG_AUDIO_RECORD_FILE, 5) == 0)
s_audio_check_step = AUDIO_CHECK_WAIT_RECORD;
} else if (s_audio_check_step == AUDIO_CHECK_START_PLAY) {
if (app_audio_play_start(AT_CFG_AUDIO_RECORD_FILE) == 0)
s_audio_check_step = AUDIO_CHECK_WAIT_PLAY;
}
#endif
}
/* 位置:App/<target>/main.c::main(),紧接 app_audio_init(s_at) 的失败检查之后。
* 未注册上层 Audio 事件回调的情况下可直接使用。
* 已有回调的情况下需要修改:在原回调中调用 audio_check_event(event, user),保留原回调注册。 */
app_audio_set_event_cb(audio_check_event, NULL);
/* 位置:App/<target>/main.c::main() 的 for (;;) 内,紧接 app_audio_process(); 后。
* 可直接使用:每轮推进验证步骤,完成或失败后不再提交。 */
audio_check_process();
- ES8311:播报结束 1 秒后录音。对麦克风说一段不同于 TTS 的话,5 秒后自动回放,应听到刚才说的内容。
- TM8211、PWM、SPI:各自对应的电路播报一次测试文本,不录音。
s_audio_check_step为AUDIO_CHECK_DONE时流程结束;为AUDIO_CHECK_FAILED时停止,不回放旧录音。
app_audio_set_event_cb()、app_audio_tts_start()、app_audio_record_start()、app_audio_play_start() 位于 App/<target>/app_audio.c/.h。保留模块内部的 Common 回调,只设置这里的测试回调。app_audio_busy()==0 表示空闲;TTS 接口返回 0 表示已接收请求,下一步仍要等待完成事件。
完成上述修改后,将 App/<target>/main.c 的 DEMO_UART_TEST 保持为 0。按键、LED 等功能按实际需求接入,可参考 HC32 案例。第 7 步其余公共初始化和 URC 挂接保持原样,相关接口的实现文件无需修改。
MQTT 的测试标志记录请求是否已接收,Audio 的 s_audio_check_step 记录测试进度。重新测试时按 MCU 实体 RST;只重启模组不会从头执行这两个测试。HTTP 请求失败后会等待再重试。
4. 编译联调
完成上述文件、配置和验证代码修改后,按以下顺序联调:
- 准备测试端:TCP 启动第 9 步的回显服务器和公网映射;MQTT/MQTTS 用 MQTTX 连接同一服务器、订阅
/hello/test_2,客户端 ID 不要与 MCU 相同;HTTP/HTTPS 确认配置的 URL 可用;Audio 接好对应通路的扬声器或输出设备。 - 按第 8 步及上方“加入业务文件”核对所选业务的工程文件和包含路径,执行 Rebuild → Download。
- 确认
Verify OK后退出调试;等待 YM310 开机,打开日志串口,再按 MCU 实体 RST。网络业务等待驻网及连接完成,Audio 等待音频配置完成,按下表核对。无需进入调试或按 F5;COM 号以实际设备为准。
烧录后的运行与验证
加入“事件与主循环”中的测试代码后,MCU 每次启动自动测试一轮,不需要按键,TCP 心跳会继续发送。按下表核对日志、对端数据或声音。
TCP/透传、MQTT/MQTTS、HTTP/HTTPS 启动后的模组初始化、驻网和连接建立可能需要几十秒,待初始化及连接完成后再核对功能日志。
| 工程组合 | 烧录后的执行过程 | 核对结果 |
|---|---|---|
| TCP 普通 | 连接 → 发送一次 4321 → 模组每 10 秒发送 HELLO |
服务器收到数据并回显;关闭会话后重连,恢复 HELLO 收发 |
| TCP 透传 | 进入透传 → 发送一次 4321 → App 每 10 秒发送 HELLO |
核对服务器及回调数据;断线探测、退出透传后重新连接并恢复心跳 |
| MQTT | 1883 连接 → 订阅 /hello/test_1 → 发布一次 8765 |
MQTTX 在 /hello/test_2 收到 8765;向订阅主题发 111111,MCU 回调收到对应数据 |
| MQTTS | TLS、8883 连接,其余顺序同 MQTT | MQTTX 使用 TLS 连接;同样核对发布和订阅两个方向 |
| HTTP | POST 4231 → GET → HEAD → 待机 |
三次状态 200;POST 正文回显 4231,GET 有正文,HEAD 只有响应头 |
| HTTPS | HTTPS POST 433 → GET → HEAD → 待机 |
三次状态 200;POST 正文回显 433,GET 有正文,HEAD 只有响应头 |
| ES8311 | TTS → 等待 1 秒 → 录音 5 秒 → 播放同一录音 | 听到 TTS 及自己刚才说的话;对应三个动作均完成 |
| TM8211 | 选择 CODECTYPE=1 → TTS 一次 → 待机 |
对应电路正常发声,TTS 完成,无录音动作 |
| PWM | 选择 CODECTYPE=2 → TTS 一次 → 待机 |
对应电路正常发声,TTS 完成,无录音动作 |
| SPI | 选择 CODECTYPE=3 → TTS 一次 → 待机 |
对应电路正常发声,TTS 完成,无录音动作 |
TCP 透传验证与排错
以下路径均指 App/<target>/ 下的文件;基础接口和编译问题沿用 3.3.2。
| 现象 | 检查位置及操作 |
|---|---|
| 服务器收到数据,日志无回显 | 确认服务器已回显;main.c::main() 登记 app_tcp_set_observer(demo_observe, NULL) |
| 断线后恢复较慢 | config/at_config.h 的 AT_CFG_TCP_TRANS_RX_IDLE_MS 控制无下行探测时间;随后退出透传、查询状态并重连,比普通模式慢 |
MQTT / MQTTS 验证与排错
| 现象 | 检查位置及操作 |
|---|---|
| 已驻网但未连接或订阅 | main.c::net_event_cb() 核对驻网启用调用;main() 保留 app_net_process(app_mqtt_busy())、app_mqtt_process() |
| 显示发布完成,MQTTX 没收到 | MQTTX 与 MCU 连接同一服务器并订阅 /hello/test_2;核对 main.c::main() 中 app_mqtt_publish_to() 的主题 |
| 订阅数据未显示 | main.c::main() 登记 app_mqtt_set_observer(demo_observe, NULL);向已订阅的 /hello/test_1 发消息 |
HTTP / HTTPS 验证与排错
| 现象 | 检查位置及操作 |
|---|---|
| 响应长度异常 | main.c 的 s_adapter.read 使用 app_http_uart_guard_read;main() 在 at_obj_create() 前调用 app_http_uart_guard_init(at_uart_read) |
| 正文只显示前 64 字节 | main.c::demo_observe() 对 APP_OBSERVER_HEADER/DATA 按完整 data/len 输出,不限制预览长度 |
| POST 后没有 GET、HEAD | main.c::main() 在 app_http_process() 后加入本节 GET、HEAD 测试代码,并确认前一请求已成功 |
Audio 验证与排错
| 现象 | 检查位置及操作 |
|---|---|
| 未开始播报 | main.c::net_event_cb() 在 APP_NET_EVENT_MODEM_READY 启用 Audio;main() 保留 app_audio_process() 和 audio_check_process() |
| ES8311 不录音或不回放 | 有麦克风时将 config/at_config.h 的 AT_CFG_AUDIO_HAS_INPUT 设为 1;main() 登记 app_audio_set_event_cb(audio_check_event, NULL) |
| 已显示完成,但无声、破音或断音 | 核对 config/at_config.h 的 AT_CFG_AUDIO_CODEC、AT_CFG_AUDIO_VOLUME 及模组输出电路;PWM 可先用音量 2 对照 |