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

    • SDK 概览与产品定位
    • 支持芯片平台与蓝牙认证
    • SDK 架构与目录分层
  • 快速开始

    • 环境搭建与编译工具链
    • 编译构建系统
    • 板级工程与配置
    • 烧录与固件升级工具
  • 应用工程

    • 应用选择与工程总览
    • SPP + BLE 数传应用框架
    • 透传与 AT 指令示例
    • BLE 广播/中心与定位示例
    • 2.4G 私有协议与 Dongle 示例
    • 云平台接入示例
    • HID 人机交互应用框架
    • HID 示例工程(键盘/鼠标/遥控器/手柄)
    • Bluetooth Mesh 应用框架
    • Mesh 模型与 Mesh DFU 固件升级
    • Mesh 音频编解码演示
  • 芯片平台与硬件抽象

    • 芯片平台总览与差异
    • 音频编解码与时钟管理
    • 外设驱动接口(ADC/IIC/SPI/PWM/LED/充电)
    • 芯片配置工具与下载支持
  • 蓝牙协议栈

    • 蓝牙控制器层(btctrler)
    • 蓝牙协议栈与 Profile(btstack)
    • 蓝牙模块选择与配置
  • 媒体与音频框架

    • 音频流框架
    • 音频编解码与 A2DP 媒体
    • 音频效果处理(EQ/频谱/变调/环绕/超低音)
    • 本地 TWS 与音频同步
  • 系统服务与运行时

    • 实时操作系统与任务调度
    • 消息事件机制
    • 电源管理与低功耗
    • 存储与配置系统
    • 设备驱动框架(USB/RTC)
  • 应用公共组件

    • 音频应用组件
    • 设备外设抽象(按键/触摸/传感器/存储)
    • 蓝牙公共模块与消息联动
    • 调试与配置组件
    • 杰理关键词唤醒(jl_kws)
  • 第三方协议与云平台接入

    • 杰理 RCSP 私有协议
    • 低功耗蓝牙 Mesh 方案(llsync_mesh)
    • Sig Mesh 方案
    • 涂鸦协议接入
    • 腾讯连连接入
    • 华为 HiLink 接入
  • 固件升级与维护

    • OTA 升级机制
    • 升级补丁与版本维护
    • 升级工具链(BLE OTA / USB Dongle OTA)
  • 文档与开发资源

    • 数据手册与架构文档
    • 协议与云平台开发文档
    • 常见问题与技术支持

SPP + BLE 数传应用框架

SPP + BLE 数传应用框架是 AC63 系列蓝牙 SoC SDK 中一个同时启用经典蓝牙 SPP(串口透传 Profile)与低功耗蓝牙 BLE 的数传示例应用,覆盖从应用状态机、协议栈启动、SPP 数据收发、UART 透传到 RFCOMM 流控的完整数据通路。

Purpose and Scope

本页面围绕 apps/spp_and_le 应用目录,系统讲解 SPP + BLE 双模数传框架的:

  • 应用入口与生命周期(app_spp_and_le.c 状态机);
  • BLE 初始化参数(trans_data_ble_config);
  • SPP 数据传输模块(spp_trans.c)的初始化、发送、接收、状态回调与 UART 透传;
  • RFCOMM 信用流控(flow control)机制;
  • 基于 AT 命令的数传变体(spp_at_trans.c)。

以下主题不在本页范围,请参阅对应页面:BT 协议栈底层任务与 HCI 事件分发见「BTStack 协议栈」相关页面;spp_online_db 在线数据库属于在线升级配套能力;板级外设配置(ADC/按键/LED)见各 board 页面。

Overview

该框架解决的核心问题是:让蓝牙芯片像一根"无线串口线"一样工作。手机或上位机通过经典蓝牙 SPP 连接芯片,芯片侧再把 SPP 收到的数据(或从 UART 收到的数据)原样转发,形成"UART ⇄ SPP ⇄ 手机"的透明数据通道;同时芯片还维护一条 BLE 链路,用于低功耗场景下的连接与后续扩展(如 GATT 服务、在线升级、RCSP 协议等)。

框架由三层组成:

  1. 应用层:app_spp_and_le.c 定义应用状态机(APP_STA_*)、启动流程(EDR + BLE 双栈初始化)、软关机时序;
  2. 传输层:spp_trans.c 封装 SPP Profile 操作表(struct spp_operation_t *spp_api),提供 transport_spp_* 系列接口,屏蔽底层 RFCOMM/HCI 细节;
  3. 变体层:spp_at_trans.c 在 SPP 之上叠加 AT 命令通道,把连接/断开/收数事件转换成 AT_EVT_* 事件上抛给 AT 框架。

框架的编译开关为 CONFIG_APP_SPP_LE 与 CONFIG_APP_AT_COM(二选一),通过 TCFG_USER_EDR_ENABLE、USER_SUPPORT_PROFILE_SPP、TCFG_USER_BLE_ENABLE 控制功能裁剪,SDK 为 AC632N/AC631N/AC635N/AC636N/AC637N/AC638N 等多颗芯片提供 board 工程。

Architecture

下图展示框架的组件关系与数据通路(基于实际源码结构绘制):

flowchart TD
    subgraph sg_App["应用层 apps/spp_and_le"]
        AppMain["app_spp_and_le.c<br/>spple_state_machine / spple_app_start"]
        SppTrans["spp_trans.c<br/>transport_spp_* 接口"]
        AtTrans["spp_at_trans.c<br/>AT 命令数传变体"]
    end

    subgraph sg_Profile["SPP Profile 层 include_lib/btstack"]
        SppOp["struct spp_operation_t<br/>spp_get_operation_table"]
        SppUser["spp_user.h<br/>SPP_USER_ST_* 状态"]
        Rfcomm["rfcomm 信用流控<br/>rfcomm_change_credits_setting"]
    end

    subgraph sg_BtStack["协议栈与硬件"]
        BtStack["btstack_init<br/>EDR + BLE 双栈"]
        Uart["UART 外设<br/>uart_db_regiest_recieve_callback"]
        Hw["蓝牙射频/基带"]
    end

    subgraph sg_Peer["对端设备"]
        Phone["手机/上位机 SPP 客户端"]
    end

    AppMain -->|"ACTION_SPPLE_MAIN 启动"| SppTrans
    AppMain -->|"transport_spp_init"| SppTrans
    SppTrans -->|"spp_get_operation_table"| SppOp
    SppTrans -->|"regist_recieve/state/wakeup"| SppUser
    SppTrans -->|"rfcomm_send_cretits_by_profile"| Rfcomm
    SppTrans -->|"注册 UART 收数回调"| Uart
    SppOp --> BtStack
    BtStack --> Hw
    Hw -->|"RFCOMM/SPP 连接"| Phone
    AtTrans -->|"AT_EVT_* 事件上抛"| AppMain
    AtTrans --> SppOp

组件职责说明:

  • app_spp_and_le.c 是整个应用的主控:它决定何时进入蓝牙任务(enter_btstack_num 防重入)、配置双栈启动参数、处理 HCI 事件与电源事件,并在软关机前先断开蓝牙链路;
  • spp_trans.c 是 SPP 数传的"门面":应用只依赖 transport_spp_init/send_data/... 接口,内部通过 spp_get_operation_table() 拿到协议栈提供的操作表,注册三个回调(收数、状态、唤醒)即可工作;
  • spp_at_trans.c 复用同一套操作表机制,但把数据/事件转为 AT 事件,供 AT 命令解析器使用,与透传模式互斥(#if (!CONFIG_APP_AT_COM) 编译隔离);
  • SPP Profile 层(include_lib/btstack)提供操作表、连接状态常量和 RFCOMM 信用流控原语,是框架与协议栈的契约边界。

应用入口与生命周期

app_spp_and_le.c 是整个框架的入口,整体被 #if CONFIG_APP_SPP_LE 包裹,只有在使能 SPP+LE 应用时才编译。文件顶部定义日志标签 [SPP_AND_LE],并引用 app_core.h / server_core.h 等系统核心头文件。

应用状态机

应用通过 spple_state_machine() 挂接系统应用框架的状态机,处理 APP_STA_CREATE / START / PAUSE / RESUME / STOP / DESTROY 六种状态。真正有业务逻辑的是 APP_STA_START 下的 ACTION_SPPLE_MAIN 动作,触发 spple_app_start() 完成协议栈初始化:

static int spple_state_machine(struct application *app, enum app_state state, struct intent *it)
{
    switch (state) {
    case APP_STA_CREATE:
        break;
    case APP_STA_START:
        if (!it) {
            break;
        }
        switch (it->action) {
        case ACTION_SPPLE_MAIN:
            spple_app_start();
            break;
        }
        break;
    ...
    return 0;
}

Source: app_spp_and_le.c

设计意图:把"应用启动"与"系统框架调度"解耦——系统应用框架只负责状态流转,具体启动动作通过 intent->action 分发,这样同一状态机可以服务于不同动作(如 AT 模式、透传模式),为板级复用提供统一入口。

双栈启动流程

spple_app_start() 是框架的心脏,按序完成:系统时钟切换 → PLL 参数配置 → EDR 预启动 → BLE 预启动 → 双栈统一初始化 → 按键/外设使能:

static void spple_app_start()
{
    if (enter_btstack_num == 0) {
        enter_btstack_num = 1;
        clk_set("sys", BT_NORMAL_HZ);
#if (TCFG_USER_EDR_ENABLE || TCFG_USER_BLE_ENABLE)
        u32 sys_clk =  clk_get("sys");
        bt_pll_para(TCFG_CLOCK_OSC_HZ, sys_clk, 0, 0);
#if TCFG_USER_EDR_ENABLE
        btstack_edr_start_before_init(NULL, 0);
#if USER_SUPPORT_PROFILE_HCRP
        __change_hci_class_type(BD_CLASS_PRINTING);
#endif
#if DOUBLE_BT_SAME_MAC
        __change_hci_class_type(BD_CLASS_TRANSFER_HEALTH);
#endif
#endif
#if TCFG_USER_BLE_ENABLE
        btstack_ble_start_before_init(&trans_data_ble_config, 0);
#endif
        btstack_init();
#else
        sys_timer_add(NULL, spple_timer_handle_test, 1000);
#endif
    }
    sys_key_event_enable();
    ...
}

Source: app_spp_and_le.c

要点分析:

  • enter_btstack_num 静态计数保证 btstack_init() 只执行一次,防止重复初始化导致协议栈资源泄漏;
  • clk_set("sys", BT_NORMAL_HZ) + bt_pll_para(...) 把系统时钟切到蓝牙正常工作频率并校准 PLL——蓝牙对 RF 时钟精度敏感,必须在协议栈启动前完成;
  • btstack_edr_start_before_init / btstack_ble_start_before_init 是"预启动"接口,允许在统一 btstack_init() 之前分别注入 EDR/BLE 的启动参数;DOUBLE_BT_SAME_MAC 时把 HCI class 改为 BD_CLASS_TRANSFER_HEALTH,让手机默认搜索到 EDR 设备;
  • 无蓝牙配置时降级为 1 秒定时器自检(spple_timer_handle_test),便于无射频环境下的功能调试。

BLE 初始化配置

BLE 预启动需要传入 ble_init_cfg_t 结构,框架在文件顶部以静态常量方式定义:

static const ble_init_cfg_t trans_data_ble_config = {
#if DOUBLE_BT_SAME_MAC
    .same_address = 1,
#else
    .same_address = 0,
#endif
    .appearance = 0,
};

Source: app_spp_and_le.c

same_address 决定 EDR 与 BLE 是否共用同一 MAC 地址(DOUBLE_BT_SAME_MAC 编译开关控制);appearance = 0 表示不声明 GAP 外观特征。该配置说明框架的 BLE 侧定位是"伴随链路"——主要用于后续 GATT 服务(如 RCSP 在线升级、online_db)的承载,而非本页核心。

软关机时序

spple_set_soft_poweroff() 体现了"先断链、再关机"的健壮性设计:直接软关机会导致对端等链路超时才感知断开(数秒级),用户体验差;因此先主动调用 btstack_ble_exit(0) / btstack_edr_exit(0) 断开双栈链路,再延时 WAIT_DISCONN_TIME_MS(300ms)后真正关机:

static void spple_set_soft_poweroff(void)
{
    log_info("set_soft_poweroff\n");
    is_app_spple_active = 1;
    //必须先主动断开蓝牙链路,否则要等链路超时断开
#if TCFG_USER_BLE_ENABLE
    btstack_ble_exit(0);
#endif
#if TCFG_USER_EDR_ENABLE
    btstack_edr_exit(0);
#endif
#if (TCFG_USER_EDR_ENABLE || TCFG_USER_BLE_ENABLE)
    //延时300ms,确保BT退出链路断开
    sys_timeout_add(NULL, power_set_soft_poweroff, WAIT_DISCONN_TIME_MS);
#else
    power_set_soft_poweroff();
#endif
}

Source: app_spp_and_le.c

电源事件通过 spple_power_event_to_user() 封装成 SYS_DEVICE_EVENT(来源 DEVICE_EVENT_FROM_POWER)投递给系统事件队列,保持与系统电源管理框架的接口一致。

SPP 数据传输模块核心实现

apps/spp_and_le/modules/bt/spp_trans.c 是框架的数据面核心,编译条件为 TCFG_USER_EDR_ENABLE && USER_SUPPORT_PROFILE_SPP && !CONFIG_APP_AT_COM(与 AT 变体互斥)。模块维护两个全局状态:spp_api(协议栈操作表指针)与 spp_state(SPP 连接状态),spp_channel(当前 RFCOMM 通道号,由收数回调写入)。

初始化与回调注册

transport_spp_init() 是模块唯一对外初始化入口,头文件声明为 void transport_spp_init(void):

void transport_spp_init(void)
{
    log_info("trans_spp_init\n");
#if (USER_SUPPORT_PROFILE_SPP==1)
    spp_state = 0;
    spp_get_operation_table(&spp_api);
    spp_api->regist_recieve_cbk(0, transport_spp_recieve_cbk);
    spp_api->regist_state_cbk(0, transport_spp_state_cbk);
    spp_api->regist_wakeup_send(NULL, transport_spp_send_wakeup);
#endif
    ...
}

Sources:

  • spp_trans.c
  • spp_trans.h

设计要点:

  • 通过 spp_get_operation_table(&spp_api) 拉取协议栈暴露的 struct spp_operation_t 操作表,而不是直接 include 协议栈内部符号——这是典型的"接口隔离 + 依赖注入"模式,让应用层与 btstack 内部实现解耦,协议栈升级不影响应用代码;
  • 三个回调各司其职:regist_recieve_cbk 收数据、regist_state_cbk 收连接状态、regist_wakeup_send 通知"可以发数据了"(低功耗模式下唤醒发送);
  • 回调的 priv 参数(此处为 0)会被协议栈原样回传,transport_spp_recieve_cbk 里用它恢复 spp_channel,实现单模块复用多通道。

数据发送

发送路径的核心是"先查忙、再发送",并顺带清除 sniff 低功耗节流:

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

int transport_spp_send_data_check(u16 len)
{
    if (spp_api) {
        if (spp_api->busy_state()) {
            return 0;
        }
    }
    return 1;
}

Source: spp_trans.c

spp_api->busy_state() 用于查询协议栈发送缓冲是否忙碌;返回 0 表示忙,上层应丢弃或缓存数据(示例中直接丢弃并打日志)。bt_comm_edr_sniff_clean() 在每次收发前调用,作用是打断 EDR sniff 低功耗模式,保证数据发送不被节流延迟——这是数传应用保证吞吐的关键细节。

收数与 UART 透传

收到对端数据后进入 transport_spp_recieve_cbk:记录通道号、hexdump 打印、清 sniff,然后回环(loopback)发给对端,用于链路自测:

static void transport_spp_recieve_cbk(void *priv, u8 *buf, u16 len)
{
    spp_channel = (u16)priv;
    log_info("spp_api_rx(%d) \n", len);
    log_info_hexdump(buf, len);
    bt_comm_edr_sniff_clean();
    ...
    //loop send data for test
    if (transport_spp_send_data_check(len)) {
        log_info("-loop send\n");
        transport_spp_send_data(buf, len);
    }
}

Source: spp_trans.c

UART 侧由状态回调在连接建立时动态注册收数回调,把串口数据直接搬进蓝牙:

static void transport_spp_state_cbk(u8 state)
{
    spp_state = state;
    switch (state) {
    case SPP_USER_ST_CONNECT:
        log_info("SPP_USER_ST_CONNECT ~~~\n");
#if TCFG_UART0_RX_PORT != NO_CONFIG_PORT
        //for test 串口数据直通到蓝牙
        uart_db_regiest_recieve_callback(transport_uart_rx_to_spp);
#endif
        break;
    case SPP_USER_ST_DISCONN:
        log_info("SPP_USER_ST_DISCONN ~~~\n");
        spp_channel = 0;
        break;
    default:
        break;
    }
}

void transport_uart_rx_to_spp(u8 *packet, u32 size)
{
    if (SPP_USER_ST_CONNECT == spp_state && transport_spp_send_data_check(size)) {
        transport_spp_send_data(packet, size);
    } else {
        log_info("drop uart data!!!\n");
    }
}

Source: spp_trans.c 与 spp_trans.c

状态机驱动的透传设计意图:只有 SPP 已连接(SPP_USER_ST_CONNECT)时才允许 UART→蓝牙转发,未连接时丢弃数据并打日志,避免数据在无链路时悄悄丢失造成上位机困惑;断开时清零 spp_channel,为流控逻辑提供"无通道"的判定条件。

RFCOMM 信用流控

框架内置基于 RFCOMM credit 的接收流控开关,防止对端发送过快导致芯片接收缓冲溢出:

#define SPP_DATA_RECIEVT_FLOW              0//流控功能使能
#define FLOW_SEND_CREDITS_NUM              1 //控制命令中 控制可接收的数据包个数,发送给对方的,range(1~32)
#define FLOW_SEND_CREDITS_TRIGGER_NUM      1 //触发更新控制命令的阈值,range(1 to <= FLOW_SEND_CREDITS_NUM)

//配置流控制的参数,蓝牙协议栈初始化前调用配置
void transport_spp_flow_cfg(void)
{
#if SPP_DATA_RECIEVT_FLOW
    rfcomm_change_credits_setting(FLOW_SEND_CREDITS_NUM, FLOW_SEND_CREDITS_TRIGGER_NUM);
#endif
}

//流控使能 EN: 1-停止收数 or 0-继续收数
int transport_spp_flow_enable(u8 en)
{
    int ret = -1;
    u16 credits = FLOW_SEND_CREDITS_NUM;
#if SPP_DATA_RECIEVT_FLOW
    if (spp_channel) {
        if (en) {
            credits = 0;
        }
        ret = rfcomm_send_cretits_by_profile(spp_channel, credits, !en);
    }
#endif
    log_info("trans_spp_flow_enable:%02x,%d,%d\n", spp_channel, en, ret);
    return ret;
}

Source: spp_trans.c 与 spp_trans.c

机制说明:RFCOMM 协议用 credit 数告诉对端"你还能发多少个包"。transport_spp_flow_enable(1) 把 credit 置 0(停止收数),transport_spp_flow_enable(0) 恢复 FLOW_SEND_CREDITS_NUM 并带 auto_flag(末参数 !en)。spp_channel 为 0(未连接)时直接返回 -1,保证流控命令只在有效链路上发送。默认 SPP_DATA_RECIEVT_FLOW=0,即流控关闭,需要时先在协议栈初始化前调用 transport_spp_flow_cfg() 配置 credit 参数。

断开控制

void transport_spp_disconnect(void)
{
    if (SPP_USER_ST_CONNECT == spp_state) {
        log_info("trans_spp_disconnect\n");
        user_send_cmd_prepare(USER_CTRL_SPP_DISCONNECT, 0, NULL);
    }
}

Source: spp_trans.c

断开通过 user_send_cmd_prepare(USER_CTRL_SPP_DISCONNECT, ...) 投递到 BT 任务队列异步执行,避免在回调上下文直接操作协议栈造成重入问题。

核心数据通路

下图展示一次完整的 UART → SPP → 手机的数据流(基于 transport_uart_rx_to_spp / transport_spp_send_data 实际调用链):

sequenceDiagram
    participant U as UART 外设
    participant T as spp_trans.c
    participant A as spp_operation_t
    participant S as BTStack EDR
    participant P as 手机 SPP 客户端

    Note over T,S: transport_spp_init 注册三个回调
    P->>S: RFCOMM 连接建立
    S-->>T: transport_spp_state_cbk(CONNECT)
    T->>U: uart_db_regiest_recieve_callback 注册
    U->>T: transport_uart_rx_to_spp(packet, size)
    T->>T: 检查 spp_state == CONNECT<br/>且 busy_state() 非忙
    T->>A: send_data(NULL, data, len)
    A->>S: RFCOMM 发送
    S-->>P: 空口数据包
    P-->>S: 对端回包
    S-->>T: transport_spp_recieve_cbk(priv, buf, len)
    T->>T: 记录 spp_channel、hexdump、清 sniff
    T->>A: 回环 send_data(buf, len)
    A->>S: 回发给手机
    S-->>P: 回环数据

Source: spp_trans.c

值得注意的工程细节:数据收发路径上每次都会调用 bt_comm_edr_sniff_clean(),这是因为 EDR 进入 sniff 模式后空口时隙被压缩,若不清除会显著增加传输延迟;该调用在发送、接收两个方向都做,保证双向低时延。

AT 命令数传变体

apps/spp_and_le/examples/at_com/spp_at_trans.c 是同一框架在 CONFIG_APP_AT_COM 编译开关下的变体:不直接透传,而是把 SPP 收数、连接事件包装成 AT 事件(AT_EVT_SPP_DATA_RECEIVED、AT_EVT_BT_CONNECTED、AT_EVT_BT_DISCONNECTED)上抛给 AT 解析框架:

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);
    bt_comm_edr_sniff_clean();
    ...
    edr_at_send_event(AT_EVT_SPP_DATA_RECEIVED, buf, len);
}

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

Source: spp_at_trans.c

变体与透传模式的关键差异:

  • 收数回调不回环数据,而是通过 edr_at_send_event 上抛,由 AT 框架决定如何应答(例如拼装 +SPP: <data> 指示);
  • 连接状态用位图 bt_edr_status 的 ST_BIT_SPP_CONN 记录,可与 A2DP/HFP 等其他 Profile 状态位共存,便于 AT 查询指令一次性返回整体状态;
  • 发送接口同样带 at_spp_send_data_check 忙检查与 bt_comm_edr_sniff_clean(),保持与透传模式一致的底层时序约束。

这种"同一操作表、不同上抛策略"的设计说明框架把"协议栈对接"与"业务策略"分离:对接代码(操作表获取、回调注册、忙检查、sniff 清理)两模式完全一致,只有数据去向不同,新增变体时只需复制回调骨架即可。

使用示例

以下代码全部取自仓库实际源码,展示框架的关键用法。

示例 1:初始化 SPP 数传模块并注册回调

在应用启动流程中(btstack_init() 之后)调用 transport_spp_init(),模块内部自动获取操作表并注册收数/状态/唤醒回调:

void transport_spp_init(void)
{
    log_info("trans_spp_init\n");
    log_info("spp_file: %s", __FILE__);
#if (USER_SUPPORT_PROFILE_SPP==1)
    spp_state = 0;
    spp_get_operation_table(&spp_api);
    spp_api->regist_recieve_cbk(0, transport_spp_recieve_cbk);
    spp_api->regist_state_cbk(0, transport_spp_state_cbk);
    spp_api->regist_wakeup_send(NULL, transport_spp_send_wakeup);
#endif
}

Source: spp_trans.c

示例 2:连接建立后把 UART 数据直通蓝牙

状态回调在 SPP_USER_ST_CONNECT 时注册 UART 收数回调,串口来的每一包数据经 transport_uart_rx_to_spp 检查连接与忙状态后发送:

void transport_uart_rx_to_spp(u8 *packet, u32 size)
{
    if (SPP_USER_ST_CONNECT == spp_state && transport_spp_send_data_check(size)) {
        transport_spp_send_data(packet, size);
    } else {
        log_info("drop uart data!!!\n");
    }
}

Source: spp_trans.c

示例 3:AT 模式下上报 SPP 数据与连接事件

AT 变体不直接回环数据,而是把收数与状态变化转为 AT_EVT_* 事件,供 AT 解析器统一处理:

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);
    bt_comm_edr_sniff_clean();
    edr_at_send_event(AT_EVT_SPP_DATA_RECEIVED, buf, len);
}

Source: spp_at_trans.c

示例 4:BLE 双模启动参数注入

BLE 预启动前传入 ble_init_cfg_t 配置(MAC 复用策略 + GAP 外观),与 EDR 预启动一起在 btstack_init() 之前完成:

static const ble_init_cfg_t trans_data_ble_config = {
#if DOUBLE_BT_SAME_MAC
    .same_address = 1,
#else
    .same_address = 0,
#endif
    .appearance = 0,
};
...
btstack_ble_start_before_init(&trans_data_ble_config, 0);

Source: app_spp_and_le.c 与 app_spp_and_le.c

配置选项

框架的裁剪全部通过编译期宏完成,汇总如下:

宏/配置项类型默认值说明
CONFIG_APP_SPP_LEbool由工程定义总开关:启用 SPP+LE 数传应用(包裹整个 app_spp_and_le.c)
CONFIG_APP_AT_COMbool与透传互斥选择 AT 命令变体;spp_trans.c 在 !CONFIG_APP_AT_COM 时编译
TCFG_USER_EDR_ENABLEbool1使能经典蓝牙 EDR 协议栈
TCFG_USER_BLE_ENABLEbool1使能低功耗蓝牙 BLE 协议栈
USER_SUPPORT_PROFILE_SPPbool1使能 SPP Profile(spp_trans.c/spp_at_trans.c 的编译前提)
DOUBLE_BT_SAME_MACbool由工程定义EDR 与 BLE 是否共用同一 MAC;同时影响 HCI class 类型
TCFG_UART0_RX_PORTportNO_CONFIG_PORTUART0 RX 端口;非 NO_CONFIG_PORT 时注册 UART→SPP 透传
SPP_DATA_RECIEVT_FLOWbool0RFCOMM 接收流控总开关(spp_trans.c 内)
FLOW_SEND_CREDITS_NUMint1发给对端的可收包数 credit(1~32)
FLOW_SEND_CREDITS_TRIGGER_NUMint1触发 credit 更新的阈值(≤ credit 数)
TEST_SPP_DATA_RATEbool0使能 100ms 定时器吞吐测试(SPP_TIMER_MS)
WAIT_DISCONN_TIME_MSint300软关机前等待蓝牙断链的延时
TCFG_USB_SLAVE_CDC_ENABLEbool由工程定义使能 USB CDC,启动时调用 usb_start() 并支持 cdc_write_data 测试
TCFG_SOFTOFF_WAKEUP_KEY_DRIVER_ENABLEbool由工程定义软关机唤醒按键驱动使能

配置注意事项:

  • CONFIG_APP_AT_COM 与透传模式互斥由源码显式保证(spp_trans.c 顶部 #if (!CONFIG_APP_AT_COM)),两者不能同时使能;
  • 流控开关 SPP_DATA_RECIEVT_FLOW 默认关闭;开启时须在协议栈初始化前调用 transport_spp_flow_cfg() 完成 credit 参数配置,否则 transport_spp_flow_enable 不会生效(#if 内代码不编译);
  • TCFG_UART0_RX_PORT != NO_CONFIG_PORT 是 UART 透传注册的条件,配置了 RX 端口才会在连接时挂接 UART 收数回调。

API 参考

以下为框架对外暴露的核心接口(均位于 apps/spp_and_le 目录,签名取自实际源码):

void transport_spp_init(void)

初始化 SPP 传输模块:获取操作表、注册收数/状态/唤醒回调、清零状态。须在 btstack_init() 之后、使用任何 transport_spp_* 接口之前调用。

声明来源: spp_trans.h

int transport_spp_send_data(u8 *data, u16 len)

通过 SPP 发送一帧数据。发送前清除 EDR sniff,保证低时延。

参数: data 数据指针;len 数据长度。 返回: 操作表 send_data 的返回值;若 spp_api 为空返回 SPP_USER_ERR_SEND_FAIL。

int transport_spp_send_data_check(u16 len)

查询协议栈发送缓冲是否可用(忙检查)。

参数: len 待发送长度(当前实现未使用,保留给后续缓存策略)。 返回: 1 表示可发送,0 表示协议栈忙(busy_state() 为真)。

void transport_uart_rx_to_spp(u8 *packet, u32 size)

UART 收数回调:仅当 spp_state == SPP_USER_ST_CONNECT 且发送不忙时转发到蓝牙,否则丢弃并打日志。

void transport_spp_disconnect(void)

主动断开 SPP 连接。仅当处于 SPP_USER_ST_CONNECT 时向 BT 任务投递 USER_CTRL_SPP_DISCONNECT 命令异步执行。

void transport_spp_flow_cfg(void)

配置 RFCOMM credit 参数(FLOW_SEND_CREDITS_NUM / FLOW_SEND_CREDITS_TRIGGER_NUM),须在协议栈初始化前调用。

int transport_spp_flow_enable(u8 en)

使能/停止接收流控。en=1 时 credit 置 0(停止收数),en=0 时恢复默认 credit 并以 auto_flag=1 发送。

返回: 0 或协议栈返回值;未连接(spp_channel==0)或流控未使能时返回 -1。

变体接口(spp_at_trans.c)

  • int at_spp_send_data(u8 *data, u16 len):AT 模式发送,语义同 transport_spp_send_data;
  • int at_spp_send_data_check(u16 len):AT 模式忙检查;
  • static void edr_at_send_event(u8 event_type, const u8 *packet, int size):向 AT 框架上报事件(AT_EVT_SPP_DATA_RECEIVED / AT_EVT_BT_CONNECTED / AT_EVT_BT_DISCONNECTED)。

回调契约(由 struct spp_operation_t 定义)

回调触发时机框架内实现
regist_recieve_cbk收到对端数据transport_spp_recieve_cbk(透传)/ at_spp_recieve_cbk(AT)
regist_state_cbkSPP 连接/断开transport_spp_state_cbk / at_spp_state_cbk
regist_wakeup_send协议栈可发送唤醒transport_spp_send_wakeup(输出字符 W)

失败模式、边界情况与并发

未连接时的数据行为

  • UART→蓝牙:transport_uart_rx_to_spp 在 spp_state != SPP_USER_ST_CONNECT 时丢弃数据并打印 drop uart data!!!。设计上选择"丢弃+日志"而非缓存,避免无界缓存导致内存耗尽;上位机应通过连接事件感知链路状态。
  • 蓝牙→UART:透传模式接收回调仅在连接态被注册,断开后 UART 回调不再挂接(SPP_USER_ST_DISCONN 分支只清 spp_channel,UART 回调注册在 CONNECT 分支),因此不存在断开后的悬空转发。

发送忙时的丢包

transport_spp_send_data_check 依赖 spp_api->busy_state()。协议栈忙时上层数据被丢弃(示例默认行为)。对于对吞吐有硬性要求的应用,需要扩展为发送队列(见"扩展点")。

协议栈未初始化

transport_spp_send_data 在 spp_api == NULL(未调用 transport_spp_init)时返回 SPP_USER_ERR_SEND_FAIL,调用方需自行处理该错误码。这也说明初始化顺序是硬约束:必须先 init 再 send。

并发与重入

  • 所有 transport_spp_* 接口设计为在 BT 协议栈线程/回调上下文调用;transport_spp_disconnect 特意用 user_send_cmd_prepare 把断开命令投递到任务队列,避免在回调里同步操作协议栈造成重入死锁;
  • spp_channel 由收数回调写入、流控接口读取,属于单线程(BT 任务)数据,无锁即可;若应用在非 BT 线程调用 transport_spp_flow_enable,需要自行加锁或用消息队列串行化;
  • spple_app_start 用 enter_btstack_num 保证 btstack_init() 只执行一次,防止重复启动导致协议栈资源泄漏(如任务重复创建)。

软关机竞态

spple_set_soft_poweroff 先 btstack_ble_exit / btstack_edr_exit 再延时 300ms 关机,防止对端因链路超时(数秒级)才感知断开;若 300ms 内断链未完成,系统仍会强制关机——这是时延与确定性的权衡。

性能与运维考量

  • sniff 清理是吞吐关键:收发路径上每次调用 bt_comm_edr_sniff_clean() 打断低功耗节流。数传场景吞吐优先,牺牲一点功耗换低时延;非数传场景应避免持续收发以省电。
  • 吞吐自测:TEST_SPP_DATA_RATE=1 时启动 100ms 定时器(SPP_TIMER_MS),连接后收到 AF 前缀启动测速、AA 停止,每周期发送 250 字节并打印 spp_data_rate: xxx bps。可用于产线或实验室吞吐评估。
  • 日志分级:spp_trans.c 使用 LOG_ERROR/DEBUG/INFO_ENABLE + LOG_TAG "[SPP_TRNS]",关闭 LOG_DUMP_ENABLE 可减少 log_info_hexdump 的开销;量产固件建议只保留 LOG_ERROR_ENABLE。
  • 看门狗/低功耗:transport_spp_send_wakeup 输出字符 W 作为协议栈唤醒调试信号;UART 透传场景建议评估低功耗模式下 UART 唤醒与蓝牙唤醒的配合(源码中通过回调机制预留,未做深度电源策略)。

扩展点

  1. 新增 SPP 业务变体:复制 spp_trans.c 的回调骨架(获取操作表 → 注册三回调 → 忙检查 → sniff 清理),替换数据去向即可;spp_at_trans.c 已示范如何把数据/事件转成自定义事件上抛。
  2. 发送队列:在 transport_spp_send_data_check 返回 0 时缓存数据,并在 transport_spp_send_wakeup 回调里补发,可实现零丢包高吞吐传输。
  3. 多通道支持:回调 priv 参数携带通道号(transport_spp_recieve_cbk 用它恢复 spp_channel),可以按通道维护独立收发上下文。
  4. BLE 侧承载业务:trans_data_ble_config 预留了 appearance 等 GAP 参数,BLE 链路可与 RCSP/online_db 等 GATT 服务组合(参考 apps/common/third_party_profile/jieli/ 下的配套实现)。
  5. 流控联动:上层应用可在接收处理慢时调用 transport_spp_flow_enable(1) 暂停对端,处理完再 transport_spp_flow_enable(0) 恢复——适合大块数据处理场景。

测试情况

  • 仓库内 apps/spp_and_le 目录为工程级示例而非单元测试;其自测手段为回环测试:transport_spp_recieve_cbk 收到数据后原样发回,配合手机端 SPP 调试工具可验证链路收发完整性;
  • 吞吐测试:TEST_SPP_DATA_RATE 定时器测速(AF/AA 命令字控制起停);
  • 透传测试:TCFG_UART0_RX_PORT 配置后,串口工具发送数据经 UART→SPP→手机验证;
  • AT 测试:CONFIG_APP_AT_COM 工程通过 AT 指令触发连接并观察 AT_EVT_* 事件(spp_at_trans.c 中 bt_edr_status 位图可被 AT 查询指令读取)。

相关链接

  • app_spp_and_le.c(应用入口与状态机)
  • spp_trans.c(SPP 数据传输模块)
  • spp_trans.h(传输模块接口声明)
  • spp_at_trans.c(AT 命令数传变体)
  • spp_config.h(SPP Profile 配置)
  • spp_user.h(SPP 用户接口与状态常量)
  • trans_data_demo 配套头文件
  • spp_online_db.c(SPP 在线数据库,配套能力)
Prev
应用选择与工程总览
Next
透传与 AT 指令示例