Skip to content

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 文件中。

AT-Demo 三层软件架构

1.2 一条 AT 指令的实际路径

各业务共用以下 AT 收发路径。

AT-Demo 基础 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.zip

解压后运行 AT-Demo_V1.7.exe,无需安装。

2.2 AT-Demo 工具使用指南

AT-Demo V1.7 工具主界面

主要功能包括:

  • 自动查找或手动连接模组 AT 串口;
  • 发送 AT 指令并显示应答和 URC;
  • 提供 FS、HTTP、TCP、FTP、MQTT、Audio 等功能入口;
  • 支持多条指令保存、循环发送和 HEX 数据收发;
  • 保存完整的 AT 收发日志。

基本操作顺序如下:

  1. 插入模组 USB 并开机;
  2. 启动 AT-Demo,保持 USB 选项勾选,等待工具自动查找 AT 串口;
  3. 状态栏显示 Connected COMxx @ 115200 bps 后发送 AT;
  4. 收到 OK 后发送 ATI,确认当前固件版本;
  5. 自动连接失败时,取消 USB,点击 Refresh,手动选择 COM 口后点击 Connect;
  6. 测试完成后点击 Save Log 保存日志,使用 Disconnect 释放串口。

2.3 使用注意事项

  1. 同一 COM 口仅允许一个程序占用。
  2. 指令支持范围以当前固件为准。返回 ERROR 时,通过 ATI 核对版本及对应指令手册。
  3. 执行普通 AT 指令前,先退出透传状态。
  4. 排查问题时保存完整的 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 通信模块,右侧 HC32F460JEUA 开发板接线

线色 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 执行链路

普通TCP Demo文件调用路径

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,确认已编入工程。

  1. SDK 中断/回调 .c:文件头包含 bsp_uart_modem.h。
  2. 接收函数:每取得一个有效字节,调用 bsp_modem_uart_rx_byte(rx_byte);FIFO 逐字节传入,SDK 回调已提供字节时不重复读寄存器。
  3. 错误分支:先保存错误标志,再按 SDK 清除,调用 bsp_modem_uart_error(overrun, frame, parity);丢弃错误字节。
  4. 保留 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. 暂存第 1 步的 SDK 头文件和初始化语句,用下方参考实现替换 main.c。
  2. 补回 SDK 头文件,将初始化语句填入 <target>_board_init();不初始化模组 UART。
  3. 保留下方日志初始化、demo_observe() 及 TCP 回调设置;日志驱动在第 8 步适配。
  4. 将 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. 编译联调

完成上述文件、配置和验证代码修改后,按以下顺序联调:

  1. 准备测试端:TCP 启动第 9 步的回显服务器和公网映射;MQTT/MQTTS 用 MQTTX 连接同一服务器、订阅 /hello/test_2,客户端 ID 不要与 MCU 相同;HTTP/HTTPS 确认配置的 URL 可用;Audio 接好对应通路的扬声器或输出设备。
  2. 按第 8 步及上方“加入业务文件”核对所选业务的工程文件和包含路径,执行 Rebuild → Download。
  3. 确认 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 对照