AT 指令与调试模块
本文档介绍 AC82N MCU SDK 中的 AT_CHAR 指令与调试模块——MCU 通过 UART 串口与蓝牙从机芯片通信的 AT 指令协议栈,涵盖驱动层(at_char_drv.c)、应用层(at_char_app.c)、协议帧格式、异步事件上报与典型使用流程。
Purpose and Scope
本页覆盖 sdk/apps/common/at_char/ 目录下的 AT_CHAR 协议模块:
- 驱动层
at_char_drv.c/.h:UART 底层收发、AT 数据包组帧/解析、命令表、异步事件回调(连接/断开/校准/体脂数据); - 应用层
at_char_app.c/.h:面向业务的封装接口,例如蓝牙广播配置、低功耗模式配置、从机关机配置; - 使用示例
sdk/cpu/demo/at_char_demo.c:模块集成与调用方式的参考 demo。
以下内容属于其他目录页的边界,本页仅引用不展开:底层 UART 驱动(uart_v2.h)、系统消息框架(sys_msg_event_notify / MSG_EVENT_*)、以及 BLE 从机芯片侧的 AT 协议实现(本模块只是该协议的主机端客户端)。
Overview
AT_CHAR 模块解决的核心问题是:MCU 主控(AC82N)如何与独立的蓝牙从机芯片进行稳定、可扩展的命令交互。其设计思路是复用经典的 Hayes AT 指令风格,以纯 ASCII 文本通过 UART 串口通信:
- 主机(本模块)发送形如
AT+CMD=param1,param2...\r的指令帧; - 从机(蓝牙芯片)回复
OK/ERR等结果码,或主动上报IM_CONN:、CALIBRATE:、BODY_INFO:等异步事件。
典型应用场景包括:
- 广播配置:批量下发广播使能、设备名、广播数据、扫描响应数据、广播参数(见
at_char_ble_slave_broadcast_config); - 低功耗控制:使能/查询从机低功耗模式(
at_char_slave_lowpower_mode_config/at_char_get_slave_lowpower_mode); - 从机关机:先断开连接再下电(
at_char_slave_poweroff_config); - 体脂秤场景:从机主动上报称重校准(
CALIBRATE:)、校准结束(CALIBRATE_END)、校准复位(CALIBRATION_RESET)、体脂数据(BODY_INFO:),MCU 通过系统消息事件转发给应用; - 透传模式:连接成功后可直接使用
at_char_uart_send_direct发送裸数据。
该模块是"命令-应答 + 异步上报"的混合模型:同步命令走 at_char_send_and_recv 的阻塞收发路径;异步事件走 UART 中断回调 + 系统消息通知路径,二者通过 run_state 标志互斥,避免中断处理与命令收发互相干扰。
Architecture
flowchart TD
subgraph sg_App["应用层 (Application)"]
AppAPI["at_char_app.c<br/>广播/低功耗/关机配置接口"]
Demo["at_char_demo.c<br/>集成示例"]
end
subgraph sg_Drv["驱动层 (Driver)"]
SendRecv["at_char_send_and_recv()<br/>组帧 + 阻塞收发"]
Direct["at_char_uart_send_direct()<br/>透传发送"]
Tables["命令表/头部表/特殊字符表"]
IRQ["at_char_uart_irq_callback()<br/>异步事件解析"]
Info["struct at_char_info<br/>全局上下文"]
end
subgraph sg_Sys["系统服务"]
UART["uart_v2 串口驱动"]
SysMsg["sys_msg_event_notify()<br/>MSG_EVENT_AT_CHAR_* 事件"]
end
subgraph sg_Slave["外部设备"]
BLE["蓝牙从机芯片<br/>(AT 协议从端)"]
end
AppAPI --> SendRecv
Demo --> AppAPI
SendRecv --> Tables
SendRecv --> Info
SendRecv --> UART
Direct --> UART
IRQ --> UART
IRQ --> Info
IRQ --> SysMsg
Info --> SysMsg
UART <-->|"ASCII AT 帧"| BLE
架构说明:
- 应用层(
at_char_app.c)只面向业务语义,通过_at_char_send_and_recv包装函数把"命令枚举 + 参数表"翻译成标准AT_CHAR_SEND数据包,不直接接触串口细节; - 驱动层(
at_char_drv.c)持有全局单例上下文_at_char_info(静态结构体,通过__this宏访问),负责组帧、长度计算、阻塞收发、结果码判定以及中断态异步事件解析; - 系统服务:串口操作全部委托给
uart_v2驱动(uart_send_blocking/uart_recv_blocking/uart_recv_bytes),事件通知走sys_msg_event_notify; - 从机是协议对端,运行在独立蓝牙芯片上,本模块不关心其内部实现,只保证帧格式与应答约定一致。
设计意图:分层后应用代码只依赖语义接口(如"配置广播""查询低功耗"),驱动层集中处理协议细节(帧拼接、超时、OK/ERR 判定),换串口或换从机协议时只需修改驱动层,这正是模块拆分为 app/drv 两个文件的原因。
协议与实现详解
模块文件与角色
| 文件 | 角色 | 关键职责 |
|---|---|---|
sdk/apps/common/at_char/at_char_drv.c | 驱动层 | 组帧/解帧、UART 收发、命令表、异步事件解析、资源管理 |
sdk/apps/common/at_char/at_char_drv.h | 驱动头文件 | 定义 AT_CHAR_SEND 数据包、at_char_head/cmd/spec 枚举、at_char_res 结果枚举、AT_CHAR_CH 通道、MSG_EVENT_AT_CHAR_* 事件 |
sdk/apps/common/at_char/at_char_app.c | 应用层 | 面向业务的命令封装(广播/低功耗/关机) |
sdk/apps/common/at_char/at_char_app.h | 应用头文件 | 声明应用层 API |
sdk/cpu/demo/at_char_demo.c | 示例 | 模块初始化与调用流程的集成 demo |
全局上下文与内存布局
驱动层用一个静态单例结构体保存全部运行时状态:
struct at_char_info {
char *at_char_send_buf; //发送数据buf
void *uart_rx_cbuf; //uart接收缓冲区
void *rx_buf; //uart数据读取内存
s32 uart_num; //uart序号
u8 init_ok; //初始化标志
volatile u8 run_state; //at_char工作状态
u8 slave_ch; //从机工作通道
u32 e_value; //发布消息携带的数据
u8 conn_state; //蓝牙芯片连接状态
};
static struct at_char_info _at_char_info = {
.at_char_send_buf = NULL,
.uart_rx_cbuf = NULL,
.rx_buf = NULL,
.init_ok = 0,
.slave_ch = AT_CHAR_CH,
};
#define __this (&_at_char_info)
设计要点:run_state 被声明为 volatile,因为它既在任务上下文(发送命令时)被置位,也在 UART 中断上下文(at_char_uart_irq_callback)被读取,用于阻止中断在命令收发期间误解析半截数据。slave_ch 记录从机当前工作的蓝牙通道,默认取宏 AT_CHAR_CH(定义于 at_char_drv.h)。发送缓冲区 at_char_send_buf 每次发送时按需 malloc,发送完毕立即释放,属于典型的"短生命周期动态分配"策略,避免常驻大缓冲占用 RAM。
收发缓冲尺寸由宏控制:
#define DMA_RX_CBUF_SIZE (64)
#define RX_BUF_SIZE (32)
DMA_RX_CBUF_SIZE(64 字节)是 UART DMA 环形接收缓冲;RX_BUF_SIZE(32 字节)是命令应答的读取缓冲。32 字节意味着本模块假定从机对单条命令的应答不超过该长度——应答中只保留 OK/ERR 与关键字段(如 LOWPOWER:x、IM_CONN:x),这解释了为什么查询类接口要用 strstr 在缓冲内定位关键字而不是直接整帧解析。
协议常量表:头部、命令、特殊字符
驱动层用三张常量表集中描述协议词法:
//头部信息列表
const char *const at_char_head_table[] = {AT_HEAD_AT_CMD, AT_HEAD_AT_CHL};
//命令列表
const char *const at_char_cmd_table[] = {
AT_STR_ENTER, AT_STR_OK, AT_STR_ERR, AT_STR_GVER,
AT_STR_GCFGVER, AT_STR_NAME, AT_STR_LBDADDR, AT_STR_BAUD,
AT_STR_ADV, AT_STR_ADVPARAM, AT_STR_ADVDATA, AT_STR_SRDATA,
AT_STR_CONNPARAM, AT_STR_SCAN, AT_STR_TARGETUUID, AT_STR_CONN,
AT_STR_DISC, AT_STR_OTA, AT_STR_CONN_CANNEL, AT_STR_POWEROFF,
AT_STR_LOWPOWER,
};
//特殊字符列表
const char *const at_spec_char_table[] = {
SPEC_CHAR_PLUS_SIGN, SPEC_CHAR_GREATER_THAN_SIGN, SPEC_CHAR_EQUAL_SIGN,
SPEC_CHAR_QUESTION_SIGN, SPEC_CHAR_ENTER_SIGN, SPEC_CHAR_COMMA_SIGN,
};
- 头部表:两种帧头
AT_HEAD_AT_CMD(标准 AT 前缀)与AT_HEAD_AT_CHL(通道类前缀,如切换从机通道时发送的AT>9\r即属于此类帧); - 命令表:21 条命令,涵盖测试帧(
ENTER/OK/ERR)、版本查询(GVER/GCFGVER)、蓝牙名称/地址/波特率(NAME/LBDADDR/BAUD)、广播(ADV/ADVPARAM/ADVDATA/SRDATA)、连接(CONNPARAM/SCAN/TARGETUUID/CONN/DISC/CONN_CANNEL)、OTA、关机(POWEROFF)与低功耗(LOWPOWER); - 特殊字符表:协议分隔符
+、>、=、?、\r、,,分别对应设置/查询/结束等语法角色。
命令在表中按下标顺序与枚举 enum at_char_cmd 一一对应,AT_STR_* 宏与 AT_STR_* 枚举值共同保证"字符串 ↔ 枚举"映射不出错——这是嵌入式 C 中常见的"表驱动"做法:新增命令只需在表尾追加一项并同步扩展枚举,组帧逻辑(at_char_send_and_recv)无需改动。
帧格式与组帧算法
at_char_send_and_recv 是驱动层的核心函数,完整实现了"组帧 → 发送 → 等待应答 → 判定结果"的同步闭环:
char *at_char_send_and_recv(const AT_CHAR_SEND *packet)
{
u8 str_len;
u8 offset = 0;
u8 param_len = 0;
ASSERT(__this->init_ok, "at char module not init !!!")
ASSERT(packet, "at char send empty packet!!!")
if (__this->slave_ch != AT_CHAR_CH) {
__this->run_state = 1;
char *at_ch_set[1] = {"AT>9\r"};
at_char_uart_send_direct(at_ch_set[0], 5);
memset(__this->rx_buf, 0, RX_BUF_SIZE);
uart_recv_blocking(__this->uart_num, __this->rx_buf, RX_BUF_SIZE, 50);
__this->run_state = 0;
if (strstr(__this->rx_buf, at_char_cmd_table[OK])) {
__this->slave_ch = AT_CHAR_CH;
} else {
return NULL;
}
}
...
通道切换(前置步骤):发送正式命令前先检查 slave_ch。若从机不在默认通道 AT_CHAR_CH,则先发送通道切换帧 AT>9\r(5 字节),等待从机回 OK 后把 slave_ch 更新为默认通道;超时未收到 OK 则直接返回 NULL。这保证了无论从机此前被切到哪个通道(例如 IM_CONN: 事件携带的通道号),后续命令总能回到默认通道执行。
长度计算与组帧:无参数帧长度 = 头部 + 命令 + 特殊字符(1) + 回车(1);带参数帧额外累加所有参数字符串长度、参数间的逗号 (param_size-1) 与结尾回车:
if (!packet->param_size) {
str_len = strlen(at_char_head_table[packet->head]) + strlen(at_char_cmd_table[packet->cmd]) + 1 + 1;
} else {
for (u8 i = 0; i < packet->param_size; i++) {
param_len += strlen(packet->param_buf[i]);
}
str_len = strlen(at_char_head_table[packet->head]) + strlen(at_char_cmd_table[packet->cmd]) + 1 + param_len + (packet->param_size - 1) + 1;
}
__this->at_char_send_buf = (char *)malloc(str_len);
if (__this->at_char_send_buf == NULL) {
log_error("at char send malloc err!!!\n");
return NULL;
}
memset(__this->at_char_send_buf, 0, str_len);
memcpy(__this->at_char_send_buf, at_char_head_table[packet->head], strlen(at_char_head_table[packet->head]));
offset += strlen(at_char_head_table[packet->head]);
memcpy(__this->at_char_send_buf + offset, at_char_cmd_table[packet->cmd], strlen(at_char_cmd_table[packet->cmd]));
offset += strlen(at_char_cmd_table[packet->cmd]);
memcpy(__this->at_char_send_buf + offset, at_spec_char_table[packet->spec], 1);
offset += 1;
if (!packet->param_size) {
memcpy(__this->at_char_send_buf + offset, at_spec_char_table[ENTER_SIGN], 1);
offset += 1;
} else {
for (u8 i = 0; i < packet->param_size; i++) {
memcpy(__this->at_char_send_buf + offset, packet->param_buf[i], strlen(packet->param_buf[i]));
offset += strlen(packet->param_buf[i]);
if (i == (packet->param_size - 1)) {
memcpy(__this->at_char_send_buf + offset, at_spec_char_table[ENTER_SIGN], 1);
} else {
memcpy(__this->at_char_send_buf + offset, at_spec_char_table[COMMA_SIGN], 1);
}
offset += 1;
}
}
组帧结果形如 AT+CMD=param1,param2\r(=/?/\r 由 packet->spec 决定)。逐字节 memcpy + offset 游标的写法避免了 sprintf 的格式串解析开销与栈开销,在 MCU 上更省资源;同时每个参数用 strlen 独立计算,天然支持不同长度的参数。
阻塞收发与应答判定:
__this->run_state = 1;
uart_send_blocking(__this->uart_num, __this->at_char_send_buf, str_len, 0);
/* log_info("send: %s\n", __this->at_char_send_buf); */
memset(__this->at_char_send_buf, 0, str_len);
free(__this->at_char_send_buf);
__this->at_char_send_buf = NULL;
memset(__this->rx_buf, 0, RX_BUF_SIZE);
uart_recv_blocking(__this->uart_num, __this->rx_buf, RX_BUF_SIZE, 50);
log_info("%s\n", __this->rx_buf);
__this->run_state = 0;
if (strstr(__this->rx_buf, at_char_cmd_table[OK])) {
return __this->rx_buf;
} else if (strstr(__this->rx_buf, at_char_cmd_table[ERR])) {
log_error("comm succ, but control err!!!\n");
return NULL;
} else {
log_error("comm err, please sure slave exist!!!\n");
return NULL;
}
}
- 发送前将
run_state置 1,发送并释放缓冲后以 50ms 超时阻塞等待从机应答,最后复位run_state; - 应答判定采用三级逻辑:命中
OK→ 成功并返回接收缓冲指针(调用方可在缓冲内继续strstr提取数据,例如LOWPOWER:后面的值);命中ERR→ 通信成功但命令执行失败;两者都未命中 → 判定为通信失败(从机可能不存在或超时); - 返回的指针指向静态
rx_buf,因此下一次调用会覆盖上次结果,调用方应在返回后尽快消费数据。
透传发送接口
连接建立后,业务层可以直接把裸数据发给从机(透传模式),驱动层对此提供旁路接口:
void at_char_uart_send_direct(void *send_buf, u8 send_len)
{
ASSERT(send_buf, "at char send empty packet!!!")
uart_send_blocking(__this->uart_num, send_buf, send_len, 0);
}
该接口不组帧、不等待应答、不触碰 run_state,直接透传给 UART。典型用途包括:通道切换帧 AT>9\r 的发送,以及连接成功后业务层自定义数据的下发。
UART 中断回调:异步事件通道
从机除了被动应答命令,还会主动上报事件。驱动层通过注册到 UART 驱动的中断回调解析这些事件:
static void at_char_uart_irq_callback(uart_dev uart_num, enum uart_event event)
{
u8 calibrate_w[3];
if (event & UART_EVENT_RX_DATA) {
if (!__this->run_state) {
memset(__this->rx_buf, 0, RX_BUF_SIZE);
uart_recv_bytes(uart_num, __this->rx_buf, RX_BUF_SIZE);
if (strstr(__this->rx_buf, "IM_CONN:")) {
__this->conn_state = 1;
__this->slave_ch = *(strstr(__this->rx_buf, "IM_CONN:") + strlen("IM_CONN:")) - '0';
sys_msg_event_notify(MSG_EVENT_AT_CHAR_CONN, __this->e_value);
} else if (strstr(__this->rx_buf, "IM_DISC:")) {
__this->conn_state = 0;
__this->slave_ch = *(strstr(__this->rx_buf, "IM_DISC:") + strlen("IM_DISC:")) - '0';
sys_msg_event_notify(MSG_EVENT_AT_CHAR_DISCONN, __this->e_value);
} else if (strstr(__this->rx_buf, "CALIBRATE:")) {
for (u8 i = 0; i < 3; i++) {
calibrate_w[i] = *(strstr(__this->rx_buf, "CALIBRATE:") + strlen("CALIBRATE:") + i) - '0';
}
__this->e_value = calibrate_w[0] * 100 + calibrate_w[1] * 10 + calibrate_w[2];
sys_msg_event_notify(MSG_EVENT_AT_CHAR_CALIBRATE_WEIGHT, __this->e_value);
} else if (strstr(__this->rx_buf, "CALIBRATE_END")) {
sys_msg_event_notify(MSG_EVENT_AT_CHAR_CALIBRATE_END, __this->e_value);
} else if (strstr(__this->rx_buf, "CALIBRATION_RESET")) {
sys_msg_event_notify(MSG_EVENT_AT_CHAR_CALIBRATION_RESET, __this->e_value);
} else if (strstr(__this->rx_buf, "BODY_INFO:")) {
if (update_body_info(__this->rx_buf)) {
sys_msg_event_notify(MSG_EVENT_AT_CHAR_BODY_FAT_SCALE_GO, __this->e_value);
}
}
}
}
...
}
回调解析的事件及语义:
| 上报前缀 | 含义 | 驱动动作 | 通知事件 |
|---|---|---|---|
IM_CONN: | 从机建立连接,后随通道号数字 | conn_state=1,更新 slave_ch | MSG_EVENT_AT_CHAR_CONN |
IM_DISC: | 从机断开连接,后随通道号数字 | conn_state=0,更新 slave_ch | MSG_EVENT_AT_CHAR_DISCONN |
CALIBRATE: | 称重校准数据,后随 3 位数字 | 解析为百/十/个位存入 e_value | MSG_EVENT_AT_CHAR_CALIBRATE_WEIGHT |
CALIBRATE_END | 校准流程结束 | 直接转发 | MSG_EVENT_AT_CHAR_CALIBRATE_END |
CALIBRATION_RESET | 校准复位 | 直接转发 | MSG_EVENT_AT_CHAR_CALIBRATION_RESET |
BODY_INFO: | 体脂秤数据帧 | 调用弱函数 update_body_info,返回非 0 才转发 | MSG_EVENT_AT_CHAR_BODY_FAT_SCALE_GO |
run_state 互斥是这套异步机制的关键:只有在 run_state == 0(即没有命令收发在进行)时,中断回调才解析串口数据;命令收发期间(run_state == 1)收到的事件字节会被忽略,避免把命令应答与异步事件混在一起。代价是:若命令阻塞收发与事件到达时间重叠,事件可能丢失——因此事件驱动的应用(如体脂秤)应避免长时间占用命令通道。
弱函数扩展点:体脂数据解析
__attribute__((weak))
u8 update_body_info(const char *info)
{
return 0;
}
update_body_info 被声明为 weak 弱符号,默认实现返回 0(不触发 BODY_INFO: 事件转发)。业务层若需要体脂秤数据,可在自己的代码中定义同名的强符号覆盖它,解析 info 指向的 BODY_INFO: 帧并返回非 0,驱动随即上报 MSG_EVENT_AT_CHAR_BODY_FAT_SCALE_GO。这是 SDK 典型的"默认关闭、按需开启"扩展模式:不解析体脂数据的项目零成本,需要时只需提供同名函数。
应用层封装接口
应用层把常用业务组合成语义化 API,全部基于私有包装函数 _at_char_send_and_recv:
static char *_at_char_send_and_recv(enum at_char_head _head, enum at_char_cmd _cmd, enum at_spec_char _spec, char **_param_buf, u8 _param_size)
{
AT_CHAR_SEND at_char = {
.head = _head,
.cmd = _cmd,
.spec = _spec,
.param_buf = _param_buf,
.param_size = _param_size,
};
return at_char_send_and_recv(&at_char);
}
蓝牙广播配置——按序下发 6 条命令,任一条失败即整体失败(原子化语义):
enum at_char_res at_char_ble_slave_broadcast_config(void)
{
char *param_table[] = {"0", "JL_BLE_CD09", "020106", "07094A4C5F424C45", "160", "1"};
enum at_char_cmd cmd_table[] = {ADV, NAME, ADVDATA, SRDATA, ADVPARAM, ADV};
for (u8 i = 0; i < ARRAY_SIZE(cmd_table); i++) {
if (_at_char_send_and_recv(AT_CMD, cmd_table[i], EQUAL_SIGN, ¶m_table[i], 1) == NULL) {
return AT_CHAR_ERR;
}
}
return AT_CHAR_SUCC;
}
配置顺序本身有讲究:先 ADV=0 停播,再依次设置设备名 NAME=JL_BLE_CD09、广播数据 ADVDATA=020106(即 flag 0x06)、扫描响应数据 SRDATA=07094A4C5F424C45(完整服务 UUID)、广播参数 ADVPARAM=160,最后 ADV=1 重新开播——先停后改再启,避免广播参数在播报中被修改导致不一致。
低功耗使能与查询:
enum at_char_res at_char_slave_lowpower_mode_config(u8 en)
{
char *param_table[] = {"0", "1"};
en = !!en;
if (_at_char_send_and_recv(AT_CMD, LOWPOWER, EQUAL_SIGN, ¶m_table[en], 1) == NULL) {
return AT_CHAR_ERR;
}
return AT_CHAR_SUCC;
}
en = !!en 把任意非 0 入参归一化为 1,再作数组下标取参数 "0"/"1"——用查表代替 if/else 分支。查询接口则用 ? 特殊字符组帧,并在应答中定位关键字:
s8 at_char_get_slave_lowpower_mode(void)
{
char *rx = NULL;
char mode;
rx = _at_char_send_and_recv(AT_CMD, LOWPOWER, QUESTION_SIGN, NULL, 0);
if (rx) {
char *mode_str = strstr(rx, "LOWPOWER:");
if (mode_str != NULL) {
mode = *(mode_str + strlen("LOWPOWER:"));
if (mode == '0') {
return 0;
} else if (mode == '1') {
return 1;
}
}
}
return -1;
}
返回 -1 表示获取失败(通信失败或应答格式异常),与合法值 0/1 区分开——这是嵌入式接口中"错误码与数据共用返回类型"的常见手法,调用方必须先判 -1。
从机关机配置——先断开(DISC=8,参数 8 表示断开原因/通道)再下电:
enum at_char_res at_char_slave_poweroff_config(void)
{
char *param_table[] = {"8"};
if (_at_char_send_and_recv(AT_CMD, DISC, EQUAL_SIGN, ¶m_table[0], 1) == NULL) {
return AT_CHAR_ERR;
}
if (_at_char_send_and_recv(AT_CMD, POWEROFF, ENTER_SIGN, NULL, 0) == NULL) {
return AT_CHAR_ERR;
}
return AT_CHAR_SUCC;
}
函数注释特别说明"at_char_init 会唤醒蓝牙模块"——即关机后再次初始化模块会通过 UART 唤醒从机,因此调用时序由业务层控制。
Core Flow
同步命令收发流程
at_char_send_and_recv 是模块的主同步路径。一次完整命令交互如下:
sequenceDiagram
participant App as 应用层(at_char_app)
participant Drv as 驱动层(at_char_send_and_recv)
participant Uart as uart_v2
participant BLE as 蓝牙从机芯片
App->>Drv: AT_CHAR_SEND 数据包(头/命令/特殊符/参数)
Drv->>Drv: ASSERT init_ok 与 packet
alt slave_ch != AT_CHAR_CH
Drv->>Uart: 发送 "AT>9\r" 通道切换帧
Uart-->>Drv: uart_recv_blocking(50ms)
alt 应答含 OK
Drv->>Drv: slave_ch = AT_CHAR_CH
else
Drv-->>App: 返回 NULL
end
end
Drv->>Drv: 计算 str_len 并 malloc 发送缓冲
Drv->>Drv: 拼接 头+命令+特殊符+参数(逗号分隔)+回车
Drv->>Uart: uart_send_blocking 发送帧
Uart->>BLE: AT+CMD=param1,param2\r
BLE-->>Uart: OK / ERR / 查询结果
Drv->>Uart: uart_recv_blocking(32B, 50ms 超时)
alt 应答含 "OK"
Drv-->>App: 返回 rx_buf 指针(含应答内容)
else 应答含 "ERR"
Drv-->>App: 返回 NULL(通信成功但控制失败)
else 超时/无应答
Drv-->>App: 返回 NULL(通信失败)
end
关键时序说明:整个过程是阻塞式的——发送完成后线程停在 uart_recv_blocking 上等待最多 50ms。这意味着命令通道是串行化的,调用方不应在中断/高实时性上下文调用本接口;而 run_state=1 期间 UART 中断回调跳过解析,保证应答与事件互不污染。
异步事件上报流程
从机主动上报的事件走完全不同的路径(中断上下文 → 系统消息 → 应用):
flowchart TD
BLE["蓝牙从机芯片"] -->|"主动上报<br/>IM_CONN:/IM_DISC:/CALIBRATE:/BODY_INFO:"| UART["uart_v2 中断"]
UART -->|"UART_EVENT_RX_DATA"| IRQ["at_char_uart_irq_callback()"]
IRQ --> Check{"run_state == 0 ?"}
Check -->|"否(命令收发中)"| Drop["丢弃本次数据"]
Check -->|"是"| Parse["uart_recv_bytes 读取并逐前缀匹配"]
Parse -->|"IM_CONN:"| C1["conn_state=1, 更新 slave_ch"]
Parse -->|"IM_DISC:"| C2["conn_state=0, 更新 slave_ch"]
Parse -->|"CALIBRATE:"| C3["解析 3 位数字 → e_value"]
Parse -->|"CALIBRATE_END"| C4["e_value 透传"]
Parse -->|"CALIBRATION_RESET"| C5["e_value 透传"]
Parse -->|"BODY_INFO:"| C6{"update_body_info()<br/>返回非 0 ?"}
C6 -->|"是"| C7["上报体脂事件"]
C6 -->|"否"| Drop
C1 --> Notify["sys_msg_event_notify<br/>(MSG_EVENT_AT_CHAR_*)"]
C2 --> Notify
C3 --> Notify
C4 --> Notify
C5 --> Notify
C7 --> Notify
Notify --> App["应用层事件处理"]
为什么拆成两条路径:同步命令需要"一问一答"的确定性,适合阻塞收发;而连接/断开/校准等事件是突发、无请求关联的,若也走命令通道会造成应答串扰。驱动用 run_state 作为两条路径的仲裁者——命令通道占用时事件被让路,空闲时事件优先解析。
应用层组合流程示例
广播配置、低功耗控制、关机三个应用 API 的组合时序:
flowchart LR
A["at_char_init()<br/>(唤醒从机)"] --> B["at_char_ble_slave_broadcast_config()<br/>ADV=0 → NAME → ADVDATA → SRDATA → ADVPARAM → ADV=1"]
B -->|"全部 OK"| C{"是否需要低功耗?"}
B -->|"任一失败"| E["返回 AT_CHAR_ERR"]
C -->|"是"| D["at_char_slave_lowpower_mode_config(1)<br/>LOWPOWER=1"]
C -->|"否"| F["正常运行<br/>(透传/事件监听)"]
D --> F
F -->|"关机场景"| G["at_char_slave_poweroff_config()<br/>DISC=8 → POWEROFF"]
E --> H["业务层重试/告警"]
G --> H
Usage Examples
示例 1:BLE 广播配置(应用层 API 的典型调用)
at_char_ble_slave_broadcast_config 展示了"参数表 + 命令表 + 循环下发"的批量命令模式,6 条命令按"停播 → 配置 → 开播"顺序执行:
enum at_char_res at_char_ble_slave_broadcast_config(void)
{
char *param_table[] = {"0", "JL_BLE_CD09", "020106", "07094A4C5F424C45", "160", "1"};
enum at_char_cmd cmd_table[] = {ADV, NAME, ADVDATA, SRDATA, ADVPARAM, ADV};
for (u8 i = 0; i < ARRAY_SIZE(cmd_table); i++) {
if (_at_char_send_and_recv(AT_CMD, cmd_table[i], EQUAL_SIGN, ¶m_table[i], 1) == NULL) {
return AT_CHAR_ERR;
}
}
return AT_CHAR_SUCC;
}
示例 2:查询从机低功耗状态(应答关键字解析)
查询类接口用 QUESTION_SIGN 组帧,返回后通过 strstr 定位 LOWPOWER: 前缀并读取其后的字符,三态返回(-1 失败 / 0 正常 / 1 低功耗):
s8 at_char_get_slave_lowpower_mode(void)
{
char *rx = NULL;
char mode;
rx = _at_char_send_and_recv(AT_CMD, LOWPOWER, QUESTION_SIGN, NULL, 0);
if (rx) {
char *mode_str = strstr(rx, "LOWPOWER:");
if (mode_str != NULL) {
mode = *(mode_str + strlen("LOWPOWER:"));
if (mode == '0') {
return 0;
} else if (mode == '1') {
return 1;
}
}
}
return -1;
}
示例 3:透传发送(连接成功后的数据通路)
连接建立后,业务可直接发送裸数据;通道切换帧 AT>9\r 也是经由该接口发出的:
void at_char_uart_send_direct(void *send_buf, u8 send_len)
{
ASSERT(send_buf, "at char send empty packet!!!")
uart_send_blocking(__this->uart_num, send_buf, send_len, 0);
}
示例 4:驱动层直接组帧(扩展新命令的模板)
业务需要自定义命令时,直接构造 AT_CHAR_SEND 结构体调用 at_char_send_and_recv(该结构体定义于 at_char_drv.h),例如从机关机接口先发 DISC=8 再发无参 POWEROFF\r:
enum at_char_res at_char_slave_poweroff_config(void)
{
char *param_table[] = {"8"};
if (_at_char_send_and_recv(AT_CMD, DISC, EQUAL_SIGN, ¶m_table[0], 1) == NULL) {
return AT_CHAR_ERR;
}
if (_at_char_send_and_recv(AT_CMD, POWEROFF, ENTER_SIGN, NULL, 0) == NULL) {
return AT_CHAR_ERR;
}
return AT_CHAR_SUCC;
}
示例 5:弱函数覆盖(体脂数据接入)
默认 update_body_info 返回 0 不触发事件;业务实现同名强符号覆盖后,驱动在收到 BODY_INFO: 帧时调用它,返回非 0 即上报 MSG_EVENT_AT_CHAR_BODY_FAT_SCALE_GO:
__attribute__((weak))
u8 update_body_info(const char *info)
{
return 0;
}
完整的模块初始化与调用编排可参考 sdk/cpu/demo/at_char_demo.c(at_char_demo.c),该 demo 展示了本模块在具体 CPU 工程中的接入方式。
Configuration Options
模块配置分散在驱动头文件宏与 at_char_drv.c 的静态初始化中:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
AT_CHAR_CH | 宏 | 定义于 at_char_drv.h | 从机默认工作通道,_at_char_info.slave_ch 初始值;命令发送前若通道不符会先发 AT>9\r 切回 |
DMA_RX_CBUF_SIZE | 宏 | 64 | UART DMA 环形接收缓冲大小(字节),需容纳从机最大事件帧 |
RX_BUF_SIZE | 宏 | 32 | 命令应答读取缓冲大小(字节),超过部分被截断,应答解析依赖关键字段定位 |
uart_num | 结构体字段 | 由 at_char_init 传入 | 使用的 UART 序号,uart_send_blocking / uart_recv_blocking / uart_deinit 均基于它 |
| 应答等待超时 | 硬编码 | 50(ms) | uart_recv_blocking 的超时参数,组帧收发路径中两处使用(通道切换确认与命令应答) |
update_body_info | 弱符号 | 返回 0 | 体脂数据解析钩子,业务覆盖后返回非 0 触发 BODY_INFO: 事件上报 |
注:
AT_CHAR_CH与AT_STR_*/SPEC_CHAR_*/MSG_EVENT_AT_CHAR_*等宏的具体值定义于at_char_drv.h,命令表/头部表/特殊字符表的字符串内容见 at_char_drv.c。
API Reference
驱动层(at_char_drv.h / at_char_drv.c)
char *at_char_send_and_recv(const AT_CHAR_SEND *packet)
AT_CHAR 数据包发送与接收(同步命令核心接口)。
- 参数:
packet— 数据包结构体,含head(帧头枚举)、cmd(命令枚举)、spec(特殊字符枚举)、param_buf(参数字符串指针数组,无参填 NULL)、param_size(参数个数,无参填 0)。 - 返回:成功返回指向静态
rx_buf的指针(内含从机应答,可继续strstr提取数据);失败返回NULL(通道切换失败 / malloc 失败 / 应答含ERR/ 超时无应答)。 - 断言:模块未初始化(
init_ok==0)或packet为 NULL 时触发ASSERT。
void at_char_uart_send_direct(void *send_buf, u8 send_len)
串口透传发送裸包数据。
- 参数:
send_buf— 待发送数据;send_len— 数据长度。 - 返回:无。
- 说明:不组帧不等待应答;连接成功后透传模式直接使用;
send_buf为 NULL 触发ASSERT。
u8 get_at_char_conn_state(void)
获取蓝牙芯片连接状态(IM_CONN:/IM_DISC: 事件维护的 conn_state)。
- 返回:
1已连接,0未连接。
void at_char_uninit(void)
释放 AT_CHAR 资源:释放发送缓冲、UART 接收环形缓冲、读取缓冲,调用 uart_deinit,复位 init_ok。未初始化时直接返回。
应用层(at_char_app.h / at_char_app.c)
enum at_char_res at_char_ble_slave_broadcast_config(void)
配置 BLE 从机广播:ADV=0 → NAME=JL_BLE_CD09 → ADVDATA=020106 → SRDATA=07094A4C5F424C45 → ADVPARAM=160 → ADV=1。
- 返回:
AT_CHAR_SUCC全部成功;AT_CHAR_ERR任一命令失败(失败即中止后续命令)。
enum at_char_res at_char_slave_lowpower_mode_config(u8 en)
使能/关闭从机低功耗模式(LOWPOWER=1 / LOWPOWER=0)。
- 参数:
en— 任意非 0 视为使能,0 为关闭。 - 返回:
AT_CHAR_SUCC/AT_CHAR_ERR。
s8 at_char_get_slave_lowpower_mode(void)
查询从机低功耗状态(发送 LOWPOWER? 并解析应答 LOWPOWER:x)。
- 返回:
0正常状态;1低功耗状态;-1获取失败(通信失败或应答格式异常)。
enum at_char_res at_char_slave_poweroff_config(void)
从机关机:先 DISC=8 断开连接,再 POWEROFF\r 下电。
- 返回:
AT_CHAR_SUCC/AT_CHAR_ERR。 - 注意:模块重新初始化(
at_char_init)会唤醒蓝牙模块。
u8 update_body_info(const char *info)(弱符号)
体脂数据解析钩子。默认实现返回 0;业务覆盖后解析 BODY_INFO: 帧,返回非 0 时驱动上报 MSG_EVENT_AT_CHAR_BODY_FAT_SCALE_GO。
- 参数:
info— 指向接收缓冲的BODY_INFO:数据帧。 - 返回:
0忽略;非0触发事件上报。
事件通知(系统消息侧)
驱动通过 sys_msg_event_notify 发布以下事件(枚举定义于 at_char_drv.h,消息携带 e_value):
| 事件 | 触发条件 | e_value 含义 |
|---|---|---|
MSG_EVENT_AT_CHAR_CONN | 收到 IM_CONN: | 透传上次值;slave_ch/conn_state 已同步更新 |
MSG_EVENT_AT_CHAR_DISCONN | 收到 IM_DISC: | 同上 |
MSG_EVENT_AT_CHAR_CALIBRATE_WEIGHT | 收到 CALIBRATE:xxx | 解析出的三位数字(如 123) |
MSG_EVENT_AT_CHAR_CALIBRATE_END | 收到 CALIBRATE_END | 透传上次值 |
MSG_EVENT_AT_CHAR_CALIBRATION_RESET | 收到 CALIBRATION_RESET | 透传上次值 |
MSG_EVENT_AT_CHAR_BODY_FAT_SCALE_GO | BODY_INFO: 且 update_body_info 返回非 0 | 透传上次值 |
Failure Modes, Edge Cases & Concurrency
失败模式与错误处理
| 场景 | 检测方式 | 处理行为 |
|---|---|---|
| 模块未初始化即调用 | ASSERT(__this->init_ok, ...) | 断言触发,进入调试器(debug.h) |
| 空数据包/空发送缓冲 | ASSERT(packet/send_buf, ...) | 断言触发 |
| 通道切换失败 | strstr(rx_buf, "OK") 未命中 | 返回 NULL,调用方按失败处理 |
| 发送缓冲 malloc 失败 | at_char_send_buf == NULL | log_error("at char send malloc err!!!") 并返回 NULL |
从机应答 ERR | strstr(rx_buf, "ERR") 命中 | log_error("comm succ, but control err!!!"),返回 NULL——通信链路正常但命令被拒绝,通常是参数非法 |
| 从机无应答/超时 | uart_recv_blocking 50ms 超时且无 OK/ERR | log_error("comm err, please sure slave exist!!!"),返回 NULL——从机掉线、未上电或帧格式不匹配 |
| 应答超长截断 | RX_BUF_SIZE=32 | 超出部分丢失;设计上依赖关键字段定位,故业务解析须用 strstr 而非按偏移硬编码 |
调试信息设计:模块所有日志带 LOG_TAG_CONST AT_CHAR / LOG_TAG "[AT_CHAR]" 前缀(at_char_drv.c、at_char_app.c),配合 debug.h 的日志框架可按 TAG 过滤;应答内容通过 log_info("%s\n", __this->rx_buf) 打印(发送帧的打印被注释保留,便于排查时放开),这是定位"命令没生效"类问题的主要手段。
边界情况
- 通道状态耦合:
IM_CONN:事件携带的通道号会覆盖slave_ch,因此下次命令前必须执行通道切换确认;若从机频繁切换通道,每条命令都会多一次AT>9\r往返,命令延迟增加。 - 应答缓冲复用:
at_char_send_and_recv返回的指针指向静态rx_buf,调用方必须在下一次调用前消费完数据,否则结果被覆盖。查询类 API(如at_char_get_slave_lowpower_mode)在函数内部就完成了提取,返回的是拷贝后的值,不受此限制。 e_value语义:e_value是发布消息携带的"透传值",只有CALIBRATE_WEIGHT会写入新解析的数字,其余事件(CONN/DISC/END/RESET)沿用上次值——依赖e_value区分事件细节的应用需注意此语义。
并发与可重入性
run_state是volatile标志,在任务上下文(命令收发)置 1/清零,在 UART 中断上下文(at_char_uart_irq_callback)读取。它提供了命令路径与事件路径的互斥:命令进行中到达的事件数据被丢弃,避免半帧污染。- 整个命令路径是阻塞且串行的:
uart_send_blocking+uart_recv_blocking(50ms)期间当前任务被挂起。因此:- 不要在多线程/多任务中并发调用
at_char_send_and_recv——没有互斥锁保护,会产生帧交织; - 不要在中断上下文中调用(阻塞会卡死中断处理);
- 长命令序列(如 6 条广播配置)会占用通道最多约 6×50ms。
- 不要在多线程/多任务中并发调用
- 事件丢失风险:若从机在命令收发窗口内上报事件,该事件被
run_state==1拦截丢弃,且无重传机制。对可靠性要求高的场景(如体脂秤数据),业务应避免在数据上报期间执行命令,或在应用层补偿查询。
Performance & Operational Considerations
- 内存占用:模块无常驻发送缓冲,发送帧按需
malloc/free;常驻内存为 DMA 环形缓冲 64B + 读取缓冲 32B,整体开销很小,适合 MCU 资源受限环境。 - 最坏命令延迟:单命令 = 发送 + 最长 50ms 等待 + 解析,约几 ms 到 50ms;含通道切换时为两段等待。批量配置接口(广播 6 条)最坏约 300ms,掉线从机场景每条命令都吃满 50ms 超时。
- 日志开销:每次命令都
log_info打印应答;正式产品若对时序敏感可关闭LOG_TAG AT_CHAR的日志输出。 - 初始化时机:
at_char_init会唤醒从机,需在从机上电稳定的时机调用;at_char_uninit会释放 UART,系统休眠/关蓝牙流程中应先 uninit 再下电。
Extension Points
- 新增命令:在
at_char_cmd_table表尾追加AT_STR_*字符串(at_char_drv.c),同步扩展at_char_drv.h中的enum at_char_cmd,组帧/收发逻辑零改动——表驱动设计的直接收益。 - 体脂数据接入:覆盖弱函数
update_body_info(at_char_drv.c),解析BODY_INFO:帧并返回非 0 触发MSG_EVENT_AT_CHAR_BODY_FAT_SCALE_GO。 - 业务语义封装:仿照
at_char_app.c的_at_char_send_and_recv包装模式,把多条命令组合成高内聚业务接口(如广播配置的"停播→配置→开播"),并保持"任一失败即返回AT_CHAR_ERR"的原子语义。 - 异步事件消费:在应用层注册
MSG_EVENT_AT_CHAR_*系统消息监听,处理连接状态变化、称重校准与体脂数据事件。 - 透传通道:连接成功后经
at_char_uart_send_direct自定义协议数据,无需改动 AT 协议本身。
Related Links
- at_char_app.c(应用层封装)
- at_char_drv.c(驱动层核心)
- at_char_app.h / at_char_drv.h(枚举与宏定义)
- at_char_demo.c(集成示例)
- 底层串口驱动见
uart_v2.h(uart_send_blocking/uart_recv_blocking/uart_recv_bytes/uart_deinit) - 系统消息与日志框架见
debug.h(LOG_TAG/log_info/log_error)与系统事件通知sys_msg_event_notify