杰理 SDK 文档中心
首页
首页
  • 项目概览

    • 项目概述与能力地图
    • 构建系统与编译流程
    • 芯片系列与规格
  • 应用示例

    • SPP 与 BLE 双模透传
    • AT 指令串口协议
    • HID 设备应用
    • 蓝牙 Mesh 应用
    • 公共组件与第三方协议
  • 芯片平台支持

    • 外设驱动
    • 电源与充电管理
    • 启动与链接脚本
    • 配置工具与 OTA 资源
  • 协议栈与系统库

    • 蓝牙控制器
    • BTStack 协议栈接口
    • 系统内核与服务
    • OTA 升级机制
  • 文档与参考

    • 蓝牙 AT 协议参考
    • 开发文档与认证信息

AT 指令串口协议

本文档介绍 fw-AC630N_BT_SDK 中基于 AT 指令的串口控制协议实现:经典蓝牙 SPP(串口仿真 Profile)与 BLE GATT 两条数据通道如何承载 AT 指令流,以及上层如何通过事件回调与 edr_at_* 系列 API 控制蓝牙状态。

Purpose and Scope

本页覆盖 AT 指令协议在 SDK 中的完整实现机制,包括:

  • AT 指令传输载体:经典蓝牙 SPP 通道(spp_at_com.c)与 BLE GATT 通道(le_at_com.c);
  • AT 指令框架:两个模块共同依赖的 at.h 解析框架;
  • 事件回调机制:edr_at_register_event_cbk / edr_at_send_event 如何把蓝牙状态变化与数据接收上报给应用层;
  • 上层控制 API:spp_at_com.h 暴露的地址、名称、可见性、配对、断开、SPP 透传等接口;
  • 编译开关与配置项:TRANS_AT_COM、USER_SUPPORT_PROFILE_SPP、DEF_BLE_DEMO_AT_COM 等。

以下内容属于其他页面,不在本页展开:RCSP(JL 私有双端控制协议)、sig_mesh 组网、音频链路与编解码、升级(OTA)流程。若需了解这些能力,请参考对应目录(apps/common/third_party_profile/jieli/JL_rcsp/、apps/common/third_party_profile/sig_mesh/、apps/common/update/)的独立文档。

Overview

协议设计意图

在嵌入式蓝牙耳机/音箱等设备上,外部主机(如手机 APP、测试工装、上位机)需要一种简单、稳定、与具体 Profile 解耦的方式来控制设备行为。AT 指令协议正是为此设计:主机通过蓝牙虚拟串口(SPP,经典蓝牙)或 GATT 特性(BLE)发送形如 AT+XXX 的文本指令,设备端解析后执行,并把结果/事件以文本或二进制形式回传。

这种设计带来三个关键收益:

  1. 协议可读性强:AT 指令是行业通用格式(源自 Modem 控制),调试、抓包、人工验证都方便;
  2. 传输通道可替换:指令语义与承载通道解耦,同一套 at.h 解析框架既可跑在 SPP 上,也可跑在 BLE GATT 上;
  3. 事件上报统一:设备主动上报(连接建立、断开、数据到达)通过单一事件回调出口 at_send_event_callbak 汇聚,应用层只需注册一次回调。

两个传输载体

载体实现文件使能条件说明
经典蓝牙 SPPapps/common/third_party_profile/jieli/spp_at_com.cTRANS_AT_COM 且 USER_SUPPORT_PROFILE_SPP==1蓝牙串口仿真 Profile,主机侧表现为虚拟 COM 口
BLE GATTapps/common/third_party_profile/jieli/le_at_com.cTCFG_BLE_DEMO_SELECT == DEF_BLE_DEMO_AT_COM通过自定义 GATT 服务特性收发 AT 数据

关键术语

  • AT 指令:以 AT 开头、\r\n 结尾的文本命令,如 AT+NAME=abc;
  • 事件(Event):设备主动上报的异步消息,如 AT_EVT_BT_CONNECTED、AT_EVT_SPP_DATA_RECEIVED;
  • edr_at_*:EDR(Enhanced Data Rate,经典蓝牙)侧的 AT 控制 API 前缀;
  • TRANS_AT_COM:SDK 中"AT 指令透传模式"的总开关宏。

Architecture

下图展示 AT 指令协议的整体架构:主机通过两条蓝牙通道进入 SDK,AT 模块解析后驱动应用逻辑,并把事件回传给上层。

flowchart TD
    subgraph sg_Host["上位机 / 手机 APP"]
        Host["AT 指令发送端"]
    end

    subgraph sg_BT["蓝牙协议栈 (BTStack)"]
        SPP["SPP Profile<br/>经典蓝牙串口"]
        GATT["BLE GATT<br/>DEF_BLE_DEMO_AT_COM"]
    end

    subgraph sg_AT["AT 指令层"]
        SPP_AT["spp_at_com.c<br/>EDR AT 服务"]
        LE_AT["le_at_com.c<br/>LE AT 服务"]
        Parser["at.h 解析框架"]
    end

    subgraph sg_App["应用层"]
        EventCbk["事件回调<br/>at_send_event_callbak"]
        AppLogic["应用逻辑<br/>连接 / 配对 / 名称 / 透传"]
    end

    Host -->|"AT 指令流 (SPP)"| SPP
    Host -->|"AT 指令流 (BLE)"| GATT
    SPP -->|"at_spp_recieve_cbk"| SPP_AT
    GATT -->|"GATT RX"| LE_AT
    SPP_AT --> Parser
    LE_AT --> Parser
    SPP_AT -->|"AT_EVT_* 事件"| EventCbk
    LE_AT -->|"AT_EVT_* 事件"| EventCbk
    EventCbk --> AppLogic
    AppLogic -->|"edr_at_set_name / at_spp_send_data 等"| SPP_AT

架构说明

  • **spp_at_com.c(EDR AT 服务)**是经典蓝牙侧的核心:持有 spp_operation_t *spp_api 句柄(spp_at_com.c L37),通过它完成数据收发与状态感知;SPP 连接状态与数据到达都会转换为 AT 事件上报。
  • **le_at_com.c(LE AT 服务)**是 BLE 侧的等价物:仅当 BLE demo 被配置为 DEF_BLE_DEMO_AT_COM 时编译(le_at_com.c L48),其 GATT 服务通过固定 handle(如 ATT_CHARACTERISTIC_ae02_01_VALUE_HANDLE)收发 AT 数据。
  • 事件回调汇聚:两个模块共用同一个静态函数指针 at_send_event_callbak(spp_at_com.c L40),由 edr_at_register_event_cbk 注册,应用层据此统一接收 AT_EVT_* 事件。
  • 应用层反向控制:上层执行 AT 指令后,通过 spp_at_com.h 暴露的 edr_at_set_name、edr_at_set_address、at_spp_send_data 等 API 写回蓝牙栈,形成闭环。

核心实现解析

1. 编译开关与模块边界

spp_at_com.c 的整体编译受双重宏控制,缺一不可:

#if TRANS_AT_COM
#if (USER_SUPPORT_PROFILE_SPP==1)

来源:spp_at_com.c L24-L25

  • TRANS_AT_COM 是 AT 指令透传模式的总开关,需要在 app_config.h 中全局使能;
  • USER_SUPPORT_PROFILE_SPP==1 保证经典蓝牙的 SPP Profile 已注册。

这种双层守卫的设计意图是:即使 SDK 集成了 AT 代码,只要产品不需要该功能,编译器会直接裁掉整段实现,不占用任何 RAM/Flash。对应的 BLE 侧则通过 TCFG_BLE_DEMO_SELECT == DEF_BLE_DEMO_AT_COM(le_at_com.c L48)选择 BLE demo 类型,使 GATT 服务按 AT 透传模式初始化。

2. EDR AT 服务:全局状态与依赖注入

模块顶部维护三份核心状态(spp_at_com.c L37-L40):

static struct spp_operation_t *spp_api = NULL;   // SPP 操作句柄(依赖注入)
static u8 spp_state;                             // SPP 内部状态
static u8 bt_edr_status;                         // EDR 蓝牙状态位图(ST_BIT_*)
static void (*at_send_event_callbak)(u8 event_type, const u8 *packet, int size);
  • spp_api 由 SPP Profile 初始化时注入,模块内部不直接调用 btstack 底层 API,而是统一走 spp_operation_t 接口——这是典型的依赖倒置:便于在无 SPP 环境(例如纯 BLE 产品)下裁剪,也便于替换为其他传输层做单元测试;
  • bt_edr_status 是位图式状态机,ST_BIT_SPP_CONN 等位标记连接状态,edr_at_get_staus() 直接返回它,供上层轮询;
  • at_send_event_callbak 是唯一的事件出口,通过 edr_at_register_event_cbk 注册,类型为裸函数指针,事件用 (event_type, packet, size) 三元组传递。

3. 数据发送路径:at_spp_send_data

int at_spp_send_data(u8 *data, u16 len)
{
    if (spp_api) {
        bt_sniff_ready_clean();
        log_info("spp_api_tx(%d) \n", len);
        /* log_info_hexdump(data, len); */
        return spp_api->send_data(NULL, data, len);
    }
    return SPP_USER_ERR_SEND_FAIL;
}

来源:spp_at_com.c L53-L62

要点:

  • 发送前清理 sniff 状态:调用 bt_sniff_ready_clean(),避免设备处于低功耗 sniff 模式时数据发送延迟或丢失——这是低功耗蓝牙音频设备上常见的坑,发送路径主动退出低功耗窗口,保证 AT 响应实时性;
  • 句柄为空时返回错误码 SPP_USER_ERR_SEND_FAIL,而不是空指针崩溃,属于防御式编程;
  • 配套的 at_spp_send_data_check(u16 len)(spp_at_com.c L64-L72)在发送前查询 spp_api->busy_state(),供上层决定是否排队/丢弃,防止协议栈缓冲溢出。

4. 数据接收路径:at_spp_recieve_cbk

static void at_spp_recieve_cbk(void *priv, u8 *buf, u16 len)
{
    log_info("spp_api_rx(%d) \n", len);
    log_info_hexdump(buf, len);

#if TEST_SPP_DATA_RATE
    if ((buf[0] == 'A') && (buf[1] == 'F')) {
        spp_test_start = 1;//start
    } else if ((buf[0] == 'A') && (buf[1] == 'A')) {
        spp_test_start = 0;//stop
    }
#endif

    //loop send data for test
    /* if (at_spp_send_data_check(len)) { */
    /* at_spp_send_data(buf, len); */
    /* } */
    edr_at_send_event(AT_EVT_SPP_DATA_RECEIVED, buf, len);
}

来源:spp_at_com.c L79-L98

要点:

  • 收到 SPP 数据后原样转发为 AT_EVT_SPP_DATA_RECEIVED 事件,不做任何字节级解析——真正的 AT 指令解析由应用层回调里的 at.h 框架完成。这种"传输层与协议层分离"的设计让 AT 指令集可以自由增删而不动传输代码;
  • TEST_SPP_DATA_RATE 数据率测试分支用 AF/AA 两个魔术前缀控制测试启停,属于内嵌的自测能力,默认关闭;
  • 被注释掉的回环测试代码揭示了作者曾经的调试手段(收到即回发),保留注释便于回归。

5. 状态回调与事件映射

static void at_spp_state_cbk(u8 state)
{
    switch (state) {
    case SPP_USER_ST_CONNECT:
        log_info("SPP_USER_ST_CONNECT ~~~\n");
        bt_edr_status |= BIT(ST_BIT_SPP_CONN);
        edr_at_send_event(AT_EVT_BT_CONNECTED, NULL, 0);
        break;

    case SPP_USER_ST_DISCONN:
        log_info("SPP_USER_ST_DISCONN ~~~\n");
        bt_edr_status &= ~BIT(ST_BIT_SPP_CONN);
        edr_at_send_event(AT_EVT_BT_DISCONNECTED, NULL, 0);
        break;

    default:
        break;
    }
}

来源:spp_at_com.c L100-L118

设计要点:

  • SPP 连接状态 → 位图 + 事件双路通知:bt_edr_status 供同步轮询(edr_at_get_staus()),AT_EVT_BT_CONNECTED/DISCONNECTED 供异步事件驱动,两种消费方式并存;
  • 状态回调在 btstack 的协议栈上下文中执行,事件通过回调同步抛出,应用层回调实现必须保持轻量,避免阻塞蓝牙协议栈线程。

6. LE AT 服务(BLE GATT 通道)

le_at_com.c 是 BLE 侧的对应实现,其 GATT 服务按固定 handle 定义收发端点:

#define TEST_SEND_HANDLE_VAL         ATT_CHARACTERISTIC_ae02_01_VALUE_HANDLE
/* #define TEST_SEND_HANDLE_VAL         ATT_CHARACTERISTIC_ae05_01_VALUE_HANDLE */
#define EXT_ADV_MODE_EN              0
#define TEST_AUDIO_DATA_UPLOAD       0 //测试文件上传

来源:le_at_com.c L51-L55

GATT 收发缓冲配置(le_at_com.c L78-L80):

#define ATT_LOCAL_PAYLOAD_SIZE    (200)                   //note: need >= 20
#define ATT_SEND_CBUF_SIZE        (512)                   //note: need >= 20,缓存大小,可修改
#define ATT_RAM_BUFSIZE           (ATT_CTRL_BLOCK_SIZE + ATT_LOCAL_PAYLOAD_SIZE + ATT_SEND_CBUF_SIZE)
  • ATT_LOCAL_PAYLOAD_SIZE=200:单包 GATT 载荷上限(≥20 是 BLE 4.x 默认 MTU 的最低要求,200 表明该 SDK 支持扩展 MTU/长包);
  • ATT_SEND_CBUF_SIZE=512:发送环形缓冲,吸收对端读取速度波动,注释明确提示"可修改"——这是典型的资源-吞吐权衡点;
  • 整个 RAM 占用由 ATT_CTRL_BLOCK_SIZE + 载荷 + 发送缓存三部分叠加计算,便于预估内存。

7. 事件类型与状态机

状态迁移与事件映射如下:

stateDiagram-v2
    [*] --> Idle
    Idle --> Connected: SPP_USER_ST_CONNECT<br/>bt_edr_status \|= BIT(ST_BIT_SPP_CONN)<br/>AT_EVT_BT_CONNECTED
    Connected --> Idle: SPP_USER_ST_DISCONN<br/>bt_edr_status &= ~BIT(ST_BIT_SPP_CONN)<br/>AT_EVT_BT_DISCONNECTED
    Connected --> Connected: 数据到达 AT_EVT_SPP_DATA_RECEIVED
    Connected --> Connected: 数据发送 at_spp_send_data

事件常量(AT_EVT_*)与 ST_BIT_* 位定义由 at.h / bt_common.h 提供,事件在 edr_at_send_event 中统一转发给已注册的应用回调(spp_at_com.c L50)。

核心流程

端到端指令交互时序

以下时序图展示一条典型 AT 指令(例如"查询/设置设备名称")从主机发出到响应回传的完整路径:

sequenceDiagram
    participant H as 上位机 (Host)
    participant S as SPP 协议栈
    participant A as spp_at_com.c
    participant C as 事件回调 at_send_event_callbak
    participant P as 应用层 (at.h 解析)

    Note over H,S: 1. 建立连接
    H->>S: 经典蓝牙 SPP 连接
    S->>A: at_spp_state_cbk(SPP_USER_ST_CONNECT)
    A->>A: bt_edr_status \|= BIT(ST_BIT_SPP_CONN)
    A->>C: edr_at_send_event(AT_EVT_BT_CONNECTED, NULL, 0)
    C->>P: 通知上层"已连接"

    Note over H,P: 2. 发送 AT 指令
    H->>S: 发送 "AT+NAME=MyDevice\r\n"
    S->>A: at_spp_recieve_cbk(buf, len)
    A->>C: edr_at_send_event(AT_EVT_SPP_DATA_RECEIVED, buf, len)
    C->>P: at.h 解析指令并执行

    Note over P,S: 3. 执行与响应
    P->>A: edr_at_set_name(name, len) / at_spp_send_data(resp, len)
    A->>A: bt_sniff_ready_clean()
    A->>S: spp_api->send_data(NULL, resp, len)
    S-->>H: 响应数据

    Note over H,S: 4. 断开连接
    H->>S: 断开 SPP 连接
    S->>A: at_spp_state_cbk(SPP_USER_ST_DISCONN)
    A->>A: bt_edr_status &= ~BIT(ST_BIT_SPP_CONN)
    A->>C: edr_at_send_event(AT_EVT_BT_DISCONNECTED, NULL, 0)
    C->>P: 通知上层"已断开"

流程要点

  1. 连接阶段:协议栈把连接/断开事件通过 at_spp_state_cbk 汇入 AT 模块,模块同步更新 bt_edr_status 位图并抛事件。应用层若关心连接状态,可注册回调或轮询 edr_at_get_staus();
  2. 指令阶段:SPP 收到的原始字节流不做任何解析地包装成 AT_EVT_SPP_DATA_RECEIVED 事件。解析责任完全落在应用层回调中的 at.h 框架——这是"薄传输层 + 厚协议层"的典型分层;
  3. 响应阶段:响应文本通过 at_spp_send_data 回发。发送前调用 bt_sniff_ready_clean() 确保链路处于活跃状态;若协议栈忙(at_spp_send_data_check 返回 0),上层需自行决定重试策略;
  4. 断开阶段:与连接阶段对称,位图清除并抛 AT_EVT_BT_DISCONNECTED。应用层应在此事件中清理会话状态(例如丢弃未完成的指令队列)。

使用示例

示例 1:SPP 数据发送(AT 响应回传)

int at_spp_send_data(u8 *data, u16 len)
{
    if (spp_api) {
        bt_sniff_ready_clean();
        log_info("spp_api_tx(%d) \n", len);
        /* log_info_hexdump(data, len); */
        return spp_api->send_data(NULL, data, len);
    }
    return SPP_USER_ERR_SEND_FAIL;
}

来源:spp_at_com.c L53-L62

应用层解析完一条 AT 指令后,把响应文本(如 OK\r\n)以 u8* 缓冲区传入此函数即可回传给主机。注意返回值为 int:非零为成功,SPP_USER_ERR_SEND_FAIL 表示 spp_api 尚未初始化(SPP 未注册或未连接)。

示例 2:接收数据并上报事件

static void at_spp_recieve_cbk(void *priv, u8 *buf, u16 len)
{
    log_info("spp_api_rx(%d) \n", len);
    log_info_hexdump(buf, len);
    ...
    edr_at_send_event(AT_EVT_SPP_DATA_RECEIVED, buf, len);
}

来源:spp_at_com.c L79-L98

buf/len 是协议栈回调传入的原始数据,模块不拷贝、不解析,直接以事件形式抛给应用回调。应用回调中的 AT 解析框架据此识别 AT+... 命令字并分发。

示例 3:SPP 连接状态变化映射为 AT 事件

static void at_spp_state_cbk(u8 state)
{
    switch (state) {
    case SPP_USER_ST_CONNECT:
        bt_edr_status |= BIT(ST_BIT_SPP_CONN);
        edr_at_send_event(AT_EVT_BT_CONNECTED, NULL, 0);
        break;
    case SPP_USER_ST_DISCONN:
        bt_edr_status &= ~BIT(ST_BIT_SPP_CONN);
        edr_at_send_event(AT_EVT_BT_DISCONNECTED, NULL, 0);
        break;
    default:
        break;
    }
}

来源:spp_at_com.c L100-L118

应用层可在回调中根据 AT_EVT_BT_CONNECTED 主动下发设备信息(如固件版本、设备名称),实现"连接即握手"的协议风格。

示例 4:向主机发送唤醒字符

static void at_spp_send_wakeup(void)
{
    putchar('W');
}

来源:spp_at_com.c L74-L77

当设备侧的 UART 调试/控制口需要被唤醒时,发送单字符 'W'。该函数体现了 AT 协议与物理串口的衔接点:BT 数据与 UART 调试口共用同一套指令语义。

配置选项

配置项类型默认值作用域说明
TRANS_AT_COM宏开关0/1(由产品配置)全局AT 指令透传模式总开关,为 0 时 spp_at_com.c 整段代码被裁剪
USER_SUPPORT_PROFILE_SPP宏开关1(SPP 产品)全局是否注册经典蓝牙 SPP Profile,必须为 1 才编译 EDR AT 模块
TCFG_BLE_DEMO_SELECT枚举DEF_BLE_DEMO_AT_COM(AT 产品)BLE demo选择 BLE demo 类型,等于 DEF_BLE_DEMO_AT_COM 时编译 LE AT 服务
ATT_LOCAL_PAYLOAD_SIZE宏200(需 ≥20)le_at_com.cGATT 单包载荷上限,影响单条 AT 指令的最大分片长度
ATT_SEND_CBUF_SIZE宏512(需 ≥20)le_at_com.cGATT 发送环形缓冲大小,注释标注"可修改",按吞吐需求调整
TEST_SPP_DATA_RATE宏0spp_at_com.c数据率自测开关,开启后启用 SPP_TIMER_MS 定时统计
SPP_TIMER_MS宏100spp_at_com.c仅测试模式:数据率统计定时器周期(ms)

配置入口以各产品 app_config.h 为准;TRANS_AT_COM 与 TCFG_BLE_DEMO_SELECT 的具体取值定义于 SDK 的公共配置模板中,修改后需重新编译并确认裁剪结果符合预期(可查看编译日志中 spp_api_rx 等日志是否出现)。

API 参考

以下接口全部声明于 spp_at_com.h L7-L19,供应用层(AT 指令解析回调)调用。

int edr_at_set_address(u8 *addr)

设置经典蓝牙设备地址(BD_ADDR)。

参数: addr(u8*)— 6 字节蓝牙地址。 返回: int — 0 表示成功;非 0 表示失败(地址非法或底层写入失败)。

int edr_at_get_address(u8 *addr)

读取当前蓝牙地址。

参数: addr(u8*)— 输出缓冲区,至少 6 字节。 返回: int — 0 成功;非 0 失败。底层最终映射到 bt_get_mac_addr()(spp_at_com.c L44)。

int edr_at_set_name(u8 *name, u8 len)

设置蓝牙广播/查询名称。

参数:

  • name(u8*):新名称字符串;
  • len(u8):名称长度。 返回: int — 0 成功;非 0 失败。底层调用 bt_set_local_name() 与 lmp_hci_write_local_name()(spp_at_com.c L43/L47)。

int edr_at_get_name(u8 *name)

读取本地名称。

参数: name(u8*)— 输出缓冲区。 返回: int — 0 成功;非 0 失败。底层来自 bt_get_local_name()(spp_at_com.c L42)。

int edr_at_set_visibility(u8 inquiry_en, u8 page_scan_en)

设置设备可被发现(inquiry scan)与可连接(page scan)属性。

参数:

  • inquiry_en(u8):0=不可发现,非 0=可发现;
  • page_scan_en(u8):0=不可连接,非 0=可连接。 返回: int — 0 成功;非 0 失败。

int edr_at_set_pair_mode(u8 mode)

设置配对模式(可配对/不可配对等)。

参数: mode(u8)— 配对模式枚举值,取值定义见 bt_common.h。 返回: int — 0 成功;非 0 失败。

int edr_at_set_pincode(u8 *pincode)

设置经典蓝牙 PIN 码(传统配对)。

参数: pincode(u8*)— PIN 码字符串。 返回: int — 0 成功;非 0 失败。

int edr_at_disconnect(void)

主动断开当前经典蓝牙(ACL/SPP)连接。

返回: int — 0 成功;非 0 表示当前无连接或断开失败。

int edr_at_set_cod(u8 *cod_data)

设置 Class of Device(设备类别,决定设备图标/服务类型)。

参数: cod_data(u8*)— COD 数据。底层调用 lmp_hci_write_class_of_device()(spp_at_com.c L48)。 返回: int — 0 成功;非 0 失败。

int edr_at_send_spp_data(u8 *data, u8 len)

通过 SPP 通道向主机发送数据(AT 响应)。

参数:

  • data(u8*):待发送数据;
  • len(u8):数据长度。 返回: int — 0 成功;非 0 失败(如 SPP_USER_ERR_SEND_FAIL)。

void edr_at_register_event_cbk(void *cbk)

注册 AT 事件回调。回调原型为 void (*)(u8 event_type, const u8 *packet, int size),事件类型包括 AT_EVT_BT_CONNECTED、AT_EVT_BT_DISCONNECTED、AT_EVT_SPP_DATA_RECEIVED 等。

参数: cbk(void*)— 回调函数指针,传 NULL 可注销。

void at_spp_init(void)

初始化 EDR AT 服务:注册 SPP 收包/状态回调、初始化内部状态位图。应在系统启动、SPP Profile 就绪后调用一次。

u8 edr_at_get_staus(void)

返回当前 EDR 蓝牙状态位图(ST_BIT_* 组合),供上层同步轮询。注意函数名为 get_staus(拼写沿用了 SDK 原始命名),非 get_status。

返回: u8 — 状态位图;BIT(ST_BIT_SPP_CONN) 置位表示 SPP 已连接。

失败模式、边界与并发

发送失败与忙碌处理

  • spp_api == NULL(SPP 未初始化/未注册)时 at_spp_send_data 直接返回 SPP_USER_ERR_SEND_FAIL。上层 AT 响应逻辑必须处理该返回值,避免"指令已执行但无响应"的假死状态;
  • 协议栈发送缓冲忙时 at_spp_send_data_check() 返回 0。此时继续发送会丢包或阻塞,推荐上层实现简单的重试/队列机制;代码中保留的回环测试注释即展示了"先 check 再 send"的用法。

低功耗竞态

发送路径调用 bt_sniff_ready_clean() 以退出 sniff 低功耗窗口。若在窗口外发送(或该函数未生效),可能出现首包延迟。竞态风险:edr_at_send_event 在协议栈上下文同步调用回调,若回调内部再触发 at_spp_send_data,属于同线程重入,一般安全;但回调若做阻塞操作(如 flash 擦写、长延时),会直接拖慢蓝牙协议栈。

数据边界

  • SPP 侧对单帧长度没有额外拆分逻辑,超过底层 MTU 的数据由 spp_operation_t->send_data 自行处理——AT 指令文本较短(通常 <64B)因此风险低,但二进制透传大数据块时需自行分帧;
  • BLE 侧由 ATT_LOCAL_PAYLOAD_SIZE(200B)限定单包载荷,超过需走发送缓冲(512B)排队。

并发模型

AT 模块没有独立线程:所有回调(at_spp_recieve_cbk、at_spp_state_cbk)均在 btstack 协议栈上下文执行,edr_at_* API 亦多在应用任务中调用。跨任务访问 bt_edr_status 位图属于读多写少,未加锁——若产品在多任务间同时读写,建议用原子读或临界区保护。

边界案例

  • 未注册回调:edr_at_send_event 在 at_send_event_callbak == NULL 时应有空指针保护(实现见 spp_at_com.c L50 下方定义),接入方务必先 edr_at_register_event_cbk 再使能连接;
  • 重复连接/断连风暴:状态回调幂等性依赖 bt_edr_status 位操作(|= / &=),重复事件不会破坏位图一致性;
  • at_spp_send_wakeup 的 putchar('W'):在未接 UART 调试口或 putchar 被重定向的平台上可能产生副作用,裁剪时应一并移除。

性能与运维

  • 吞吐:SPP 侧数据率由协议栈决定,模块内仅做透传,CPU 开销集中在 log_info_hexdump(调试打印,发布版建议关闭——spp_at_com.c 顶部 #if 1 即日志总开关,spp_at_com.c L15-L22);
  • 内存:BLE 侧 AT 服务 RAM 占用为 ATT_CTRL_BLOCK_SIZE + ATT_LOCAL_PAYLOAD_SIZE + ATT_SEND_CBUF_SIZE(默认约 200+512+控制块)。调大 ATT_SEND_CBUF_SIZE 可提升大指令吞吐,但会增大 RAM 占用;
  • 日志:模块用 log_info/log_info_hexdump 输出收发字节,配合 printf_buf 可抓取完整 AT 报文,是联调主机侧指令集的第一手段;
  • 自测:TEST_SPP_DATA_RATE + AF/AA 魔术指令可做回环吞吐测试,量产前建议跑一遍确认链路质量。

扩展点

  1. 新增 AT 指令:无需改动 spp_at_com.c——在应用层回调(edr_at_register_event_cbk 注册的函数)中通过 at.h 框架注册新命令字,收到 AT_EVT_SPP_DATA_RECEIVED 后由框架分发执行,响应走 at_spp_send_data/edr_at_send_spp_data 回传;
  2. 替换传输层:spp_operation_t 句柄抽象了收发与状态,理论上可注入其他面向连接的传输(如 UART、USB CDC)复用同一套 AT 语义;le_at_com.c 已证明同一协议可跑在 GATT 上;
  3. 扩展事件类型:edr_at_send_event 的 event_type 参数为 u8,可在 at.h 的 AT_EVT_* 枚举基础上追加自定义事件,回调签名无需变更;
  4. 发送策略定制:在 at_spp_send_data 之上封装排队/重试层,配合 at_spp_send_data_check 实现背压(backpressure),可应对高速透传场景。

相关链接

  • spp_at_com.h(EDR AT 接口声明)
  • spp_at_com.c(EDR AT 服务实现)
  • le_at_com.h(LE AT 接口声明)
  • le_at_com.c(LE AT 服务实现)
  • 相关源码:at.h(AT 解析框架)、spp_trans_data.h / le_trans_data.h(透传数据 demo)、spp_user.h(SPP Profile 用户接口)
  • 相关主题:BLE GATT 服务与特性(见 BLE 相关页面)、RCSP 控制协议(apps/common/third_party_profile/jieli/JL_rcsp/)
Prev
SPP 与 BLE 双模透传
Next
HID 设备应用