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 协议等)。
框架由三层组成:
- 应用层:
app_spp_and_le.c定义应用状态机(APP_STA_*)、启动流程(EDR + BLE 双栈初始化)、软关机时序; - 传输层:
spp_trans.c封装 SPP Profile 操作表(struct spp_operation_t *spp_api),提供transport_spp_*系列接口,屏蔽底层 RFCOMM/HCI 细节; - 变体层:
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_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_LE | bool | 由工程定义 | 总开关:启用 SPP+LE 数传应用(包裹整个 app_spp_and_le.c) |
CONFIG_APP_AT_COM | bool | 与透传互斥 | 选择 AT 命令变体;spp_trans.c 在 !CONFIG_APP_AT_COM 时编译 |
TCFG_USER_EDR_ENABLE | bool | 1 | 使能经典蓝牙 EDR 协议栈 |
TCFG_USER_BLE_ENABLE | bool | 1 | 使能低功耗蓝牙 BLE 协议栈 |
USER_SUPPORT_PROFILE_SPP | bool | 1 | 使能 SPP Profile(spp_trans.c/spp_at_trans.c 的编译前提) |
DOUBLE_BT_SAME_MAC | bool | 由工程定义 | EDR 与 BLE 是否共用同一 MAC;同时影响 HCI class 类型 |
TCFG_UART0_RX_PORT | port | NO_CONFIG_PORT | UART0 RX 端口;非 NO_CONFIG_PORT 时注册 UART→SPP 透传 |
SPP_DATA_RECIEVT_FLOW | bool | 0 | RFCOMM 接收流控总开关(spp_trans.c 内) |
FLOW_SEND_CREDITS_NUM | int | 1 | 发给对端的可收包数 credit(1~32) |
FLOW_SEND_CREDITS_TRIGGER_NUM | int | 1 | 触发 credit 更新的阈值(≤ credit 数) |
TEST_SPP_DATA_RATE | bool | 0 | 使能 100ms 定时器吞吐测试(SPP_TIMER_MS) |
WAIT_DISCONN_TIME_MS | int | 300 | 软关机前等待蓝牙断链的延时 |
TCFG_USB_SLAVE_CDC_ENABLE | bool | 由工程定义 | 使能 USB CDC,启动时调用 usb_start() 并支持 cdc_write_data 测试 |
TCFG_SOFTOFF_WAKEUP_KEY_DRIVER_ENABLE | bool | 由工程定义 | 软关机唤醒按键驱动使能 |
配置注意事项:
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_cbk | SPP 连接/断开 | 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 唤醒与蓝牙唤醒的配合(源码中通过回调机制预留,未做深度电源策略)。
扩展点
- 新增 SPP 业务变体:复制
spp_trans.c的回调骨架(获取操作表 → 注册三回调 → 忙检查 → sniff 清理),替换数据去向即可;spp_at_trans.c已示范如何把数据/事件转成自定义事件上抛。 - 发送队列:在
transport_spp_send_data_check返回 0 时缓存数据,并在transport_spp_send_wakeup回调里补发,可实现零丢包高吞吐传输。 - 多通道支持:回调
priv参数携带通道号(transport_spp_recieve_cbk用它恢复spp_channel),可以按通道维护独立收发上下文。 - BLE 侧承载业务:
trans_data_ble_config预留了appearance等 GAP 参数,BLE 链路可与 RCSP/online_db 等 GATT 服务组合(参考apps/common/third_party_profile/jieli/下的配套实现)。 - 流控联动:上层应用可在接收处理慢时调用
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 查询指令读取)。