杰理 SDK 文档中心
首页
首页
  • 概述与入门

    • 项目概述与芯片平台
    • 环境搭建与工具链安装
    • 编译与烧录指南
    • 工程结构总览
  • 应用层与公共模块

    • GP MCU 主应用入口
    • AT 指令与调试模块
    • 电池检测与电源管理
    • EEPROM 与参数存储
    • 按键与 USB 设备驱动
    • 音频解码与 APA 语音播报
  • 外设驱动与示例

    • 高精度 ADC(HADC)
    • 通用 ADC 与定时器
    • UART / SPI / IIC 通信外设
    • MCPWM 与电机控制
    • RTC 与低功耗唤醒
    • 段码 LCD 驱动
    • NOR Flash 与红外编解码组件
  • 显示与 UI 系统

    • LCD 驱动与字库引擎
    • UI 平台与控件绘制
    • UI 工程与资源生成工具
  • 系统底层与芯片平台

    • cd09 芯片平台与预编译库
    • GPIO 与 IIC 底层驱动
    • 系统文件系统与设备模型
  • 启动引导与固件升级

    • UBOOT 引导工程
    • 固件升级机制
  • 开发工具与资源

    • 编译脚本与命令行工具
    • 音频文件转换工具
    • 硬件资料与文档资源

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)

来源:at_char_drv.c

设计要点: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)

来源:at_char_drv.c

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_char_drv.c

  • 头部表:两种帧头 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;
        }
    }
    ...

来源:at_char_drv.c

通道切换(前置步骤):发送正式命令前先检查 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_char_drv.c

组帧结果形如 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;
    }
}

来源:at_char_drv.c

  • 发送前将 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);
}

来源:at_char_drv.c

该接口不组帧、不等待应答、不触碰 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);
                }
            }
        }
    }
    ...
}

来源:at_char_drv.c

回调解析的事件及语义:

上报前缀含义驱动动作通知事件
IM_CONN:从机建立连接,后随通道号数字conn_state=1,更新 slave_chMSG_EVENT_AT_CHAR_CONN
IM_DISC:从机断开连接,后随通道号数字conn_state=0,更新 slave_chMSG_EVENT_AT_CHAR_DISCONN
CALIBRATE:称重校准数据,后随 3 位数字解析为百/十/个位存入 e_valueMSG_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;
}

来源:at_char_drv.c

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);
}

来源:at_char_app.c

蓝牙广播配置——按序下发 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, &param_table[i], 1) == NULL) {
            return AT_CHAR_ERR;
        }
    }
    return AT_CHAR_SUCC;
}

来源:at_char_app.c

配置顺序本身有讲究:先 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, &param_table[en], 1) == NULL) {
        return AT_CHAR_ERR;
    }
    return AT_CHAR_SUCC;
}

来源:at_char_app.c

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;
}

来源:at_char_app.c

返回 -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, &param_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_app.c

函数注释特别说明"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, &param_table[i], 1) == NULL) {
            return AT_CHAR_ERR;
        }
    }
    return AT_CHAR_SUCC;
}

来源:at_char_app.c

示例 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;
}

来源:at_char_app.c

示例 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);
}

来源:at_char_drv.c

示例 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, &param_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_app.c

示例 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;
}

来源:at_char_drv.c

完整的模块初始化与调用编排可参考 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宏64UART 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_GOBODY_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 == NULLlog_error("at char send malloc err!!!") 并返回 NULL
从机应答 ERRstrstr(rx_buf, "ERR") 命中log_error("comm succ, but control err!!!"),返回 NULL——通信链路正常但命令被拒绝,通常是参数非法
从机无应答/超时uart_recv_blocking 50ms 超时且无 OK/ERRlog_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

  1. 新增命令:在 at_char_cmd_table 表尾追加 AT_STR_* 字符串(at_char_drv.c),同步扩展 at_char_drv.h 中的 enum at_char_cmd,组帧/收发逻辑零改动——表驱动设计的直接收益。
  2. 体脂数据接入:覆盖弱函数 update_body_info(at_char_drv.c),解析 BODY_INFO: 帧并返回非 0 触发 MSG_EVENT_AT_CHAR_BODY_FAT_SCALE_GO。
  3. 业务语义封装:仿照 at_char_app.c 的 _at_char_send_and_recv 包装模式,把多条命令组合成高内聚业务接口(如广播配置的"停播→配置→开播"),并保持"任一失败即返回 AT_CHAR_ERR"的原子语义。
  4. 异步事件消费:在应用层注册 MSG_EVENT_AT_CHAR_* 系统消息监听,处理连接状态变化、称重校准与体脂数据事件。
  5. 透传通道:连接成功后经 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
Prev
GP MCU 主应用入口
Next
电池检测与电源管理