SPP 与 BLE 双模透传
SPP 与 BLE 双模透传(apps/spp_and_le)是杰理 AC631x(bd29)平台上的一个蓝牙透传参考应用:设备同时使能经典蓝牙(EDR/BR)的 SPP 串口协议与低功耗蓝牙(BLE)GATT 透传服务,实现手机 APP 通过任意一种蓝牙通道与设备侧 MCU/串口进行双向数据透传,并支持 AT 指令模式切换。
Purpose and Scope
本页面向开发者完整讲解 apps/spp_and_le 应用的双模透传能力,包括:
- 应用层的初始化流程、状态机与蓝牙功能配置(
app_spp_and_le.c); - SPP 透传数据通路:协议栈操作表注册、收发回调、流控与回环测试;
- BLE 透传数据通路:GATT Profile 数据表、ATT 读写回调、Notify/Indicate 发送;
- 模块使能宏与构建配置(
Makefile、app_config.h); - 低功耗(sniff 模式)、错误处理与并发注意点。
不包含(属于其他页面的内容):RCSP 杰理私有协议、OTA 升级(rcsp_user_update)、充电管理(app_charge.c)、按键驱动(apps/common/key/)的细节,仅在涉及本应用联动时提及。AT 指令解析(at_cmds.c / at_uart.c)是 TRANS_AT_COM=1 编译路径下的兄弟能力,本页仅说明其与本应用的关系。
概述
透传(Transparent Transmission)是蓝牙芯片最常见的应用形态之一:手机 APP 与设备之间不需要理解蓝牙协议细节,只需把串口/自定义数据原封不动地搬到对端。本应用在两个蓝牙通道上同时提供该能力:
| 通道 | 协议 | 数据形态 | 典型场景 |
|---|---|---|---|
| 经典蓝牙(EDR) | SPP(Serial Port Profile,基于 RFCOMM) | 流式字节,类似串口 | 配对简单、兼容老式蓝牙设备、大批量数据 |
| 低功耗蓝牙(BLE) | GATT 自定义服务(UUID ae30,特征 ae01) | ATT 包(Notify/Write-Without-Response) | 低功耗、现代手机/穿戴设备 |
应用通过编译期宏选择能力组合(TRANS_DATA_EN、TCFG_USER_EDR_ENABLE、TCFG_USER_BLE_ENABLE),并可通过 TRANS_AT_COM 在"纯透传"与"AT 指令控制"两种运行模式间切换。README 明确说明:代码已经实现收到手机的 SPP 数据后主动回送(loopback),用于收发测试,开发者可以此为基础扩展为自己的业务协议。
关键设计意图:
- 双模共存:BLE 地址由 EDR 地址派生(
lib_make_ble_address),保证双模设备地址一致且唯一;EDR 连接后主动关闭 BLE 广播(bt_ble_adv_enable(0)),避免同设备双通道同时占用。 - 流控分离:SPP 用
busy_state()+ 发送完成唤醒回调做背压;BLE 用协议栈发送缓冲区有效长度 +ATT_EVENT_CAN_SEND_NOW事件做背压,二者都避免在回调上下文里阻塞协议栈。 - 低功耗优先:空闲 5 秒后自动进入 sniff 模式(
SNIFF_CNT_TIME 5),收发数据时通过clear_sniff_cnt()退出。
架构
flowchart TD
subgraph sg_App["应用层 apps/spp_and_le"]
AppMain["app_main.c"]
SppAndLe["app_spp_and_le.c<br/>(状态机/初始化/事件)"]
AppIdle["app_idle.c"]
AtCom["app_at_com.c / at_cmds.c / at_uart.c<br/>(TRANS_AT_COM=1 时启用)"]
UserCfg["user_cfg.c / log_config.c / misc.c"]
end
subgraph sg_Trans["透传协议封装 apps/common/third_party_profile/jieli"]
SppTrans["spp_trans_data.c + spp_user.c<br/>(SPP 收发封装)"]
LeTrans["le_trans_data.c<br/>(BLE GATT 透传)"]
LeAt["le_at_com.c / spp_at_com.c<br/>(AT 模式桥接)"]
end
subgraph sg_Stack["协议栈层 include_lib/btstack"]
Btstack["btstack_init / btstack_task"]
SppApi["SPP Operation Table<br/>(spp_get_operation_table)"]
BleApi["BLE 控制器 + ATT 层<br/>(ble_user_cmd_prepare)"]
end
subgraph sg_Hw["硬件层"]
Uart["UART (AT 指令口)"]
Radio["蓝牙射频 (EDR + BLE 双模)"]
end
AppMain --> SppAndLe
AppMain --> AppIdle
SppAndLe -->|"user_spp_data_handler"| SppTrans
SppAndLe -->|"TRANS_AT_COM 切换"| AtCom
AtCom --> LeAt
SppTrans -->|"spp_api 回调/发送"| SppApi
LeTrans -->|"att_read/write 回调 + 发送"| BleApi
Btstack --> SppApi
Btstack --> BleApi
LeAt --> Uart
SppApi --> Radio
BleApi --> Radio
架构说明:app_main.c 拉起应用后由 app_spp_and_le.c 完成蓝牙功能初始化(EDR/BLE 地址、连接参数、协议栈启动);透传逻辑封装在 apps/common/third_party_profile/jieli/trans_data_demo/ 的 spp_trans_data.c 与 le_trans_data.c 中(见 Makefile 的编译对象清单);二者分别通过 SPP 操作表与 BLE 命令封装与协议栈交互,最终数据流到射频。TRANS_AT_COM=1 时,app_spp_and_le.c 整个文件被 #if (!TRANS_AT_COM) 排除编译,改由 AT 指令路径接管(app_at_com.c、at_cmds.c、at_uart.c 与 spp_at_com.o、le_at_com.o)。
应用启动流程(来自 app_spp_and_le.c):
static void app_start()
{
clk_set("sys", BT_NORMAL_HZ);
u32 sys_clk = clk_get("sys");
bt_pll_para(TCFG_CLOCK_OSC_HZ, sys_clk, 0, 0);
bt_function_select_init();
bredr_handle_register();
__change_hci_class_type(0);//default icon
btstack_init();
#if TCFG_USER_EDR_ENABLE
sys_auto_sniff_controle(1, NULL);
#endif
sys_key_event_enable();
}
Source: app_spp_and_le.c
启动顺序的设计意图:先设置系统时钟并配置 PLL 参数(保证射频与系统时钟匹配),再初始化蓝牙功能选择(连接数、超时、配对参数、BLE 地址),注册 EDR 业务回调(SPP 数据、音量、电量),最后才 btstack_init() 拉起协议栈;sniff 自动低功耗在协议栈就绪后开启。
SPP 透传实现详解
初始化与回调注册
SPP 数据通路的核心封装在 spp_trans_data.c(与 spp_user.c 一起编译,见 Makefile)。初始化时通过 spp_get_operation_table() 拿到协议栈的 SPP 操作表,并注册三类回调:
void transport_spp_init(void)
{
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);
}
Source: README.md
regist_recieve_cbk:手机 → 设备的数据入口,收到 SPP 数据后在此回调中处理;regist_state_cbk:连接/断开状态通知,回调参数为SPP_USER_ST_CONNECT/SPP_USER_ST_DISCONN;regist_wakeup_send:协议栈发送缓冲空闲后的"可继续发送"通知,用于驱动流控。
应用层侧,app_spp_and_le.c 在 bredr_handle_register() 中把 SPP 数据处理器注册给协议栈数据分发:
static void bredr_handle_register()
{
#if (USER_SUPPORT_PROFILE_SPP==1)
spp_data_deal_handle_register(user_spp_data_handler);
#endif
/* bt_fast_test_handle_register(bt_fast_test_api);//测试盒快速测试接口 */
#if BT_SUPPORT_MUSIC_VOL_SYNC
music_vol_change_handle_register(set_music_device_volume, phone_get_device_vol);
#endif
#if BT_SUPPORT_DISPLAY_BAT
get_battery_value_register(bt_get_battery_value); /*电量显示获取电量的接口*/
#endif
}
Source: app_spp_and_le.c
数据收发与流控
发送侧采用"先检查、后发送、回调唤醒"三段式流控,防止在协议栈忙时盲目发包:
int transport_spp_send_data(u8 *data, u16 len)
{
if (spp_api) {
log_info("spp_api_tx(%d) \n", len);
clear_sniff_cnt(); /* 有数据交互,退出/延迟进入 sniff */
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: README.md
transport_spp_send_wakeup 是发送完成的唤醒钩子,示意可继续发送(示例实现仅 putchar('W'),实际产品中应触发上层发送队列继续发送)。接收侧 transport_spp_recieve_cbk 打印长度与十六进制内容,并调用 clear_sniff_cnt() 防止低功耗模式中断传输。
回环测试(loopback)
README 给出的收发测试逻辑:收到手机数据后立即原样回送,验证 SPP 链路双向可用:
static void transport_spp_recieve_cbk(void *priv, u8 *buf, u16 len)
{
log_info("spp_api_rx(%d) \n", len);
log_info_hexdump(buf, len);
clear_sniff_cnt();
//loop send data for test
if (transport_spp_send_data_check(len)) {
transport_spp_send_data(buf, len);
}
}
SPP 数据通路时序
sequenceDiagram
participant Phone as 手机 SPP 客户端
participant Stack as btstack 协议栈 (SPP/RFCOMM)
participant Spp as spp_trans_data.c
participant App as app_spp_and_le.c (user_spp_data_handler)
participant Uart as 设备侧业务/串口
Note over Phone,App: 连接建立
Stack-->>Spp: state_cbk(SPP_USER_ST_CONNECT)
Spp-->>App: 状态更新
Note over Phone,App: 手机 → 设备 数据下行
Phone->>Stack: SPP 数据包
Stack->>Spp: recieve_cbk(buf, len)
Spp->>Spp: clear_sniff_cnt()
Spp->>App: user_spp_data_handler(packet_type, ch, packet, size)
App->>Uart: 业务处理/转发
Note over Phone,App: 设备 → 手机 数据上行
Uart->>App: 待发送数据
App->>Spp: send_data_check(len)
Spp-->>App: 返回 1 (协议栈空闲)
App->>Spp: send_data(data, len)
Spp->>Stack: spp_api->send_data(NULL, data, len)
Stack-->>Spp: send_wakeup() (可继续发送)
关键点:所有回调都运行在 btstack 协议栈任务上下文,因此回调内不能做耗时操作,数据应尽快拷贝/转发;send_data_check 与 busy_state() 的存在就是为了让应用层自行实现发送队列背压,而不是在回调里阻塞等待。
BLE 透传实现详解
GATT Profile 数据表
BLE 透传封装在 le_trans_data.c,其 GATT 服务表由工具 make_gatt_services 生成后以字节数组形式放在 le_trans_data.h,开发者可通过该工具重新配置服务与属性。Profile 包含两个 Primary Service(见 README.md):
static const uint8_t profile_data[] = {
// 0x0001 PRIMARY_SERVICE 1800 (GAP)
0x0a, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x28, 0x00, 0x18,
// 0x0002 CHARACTERISTIC 2a00 READ | WRITE | DYNAMIC
0x0d, 0x00, 0x02, 0x00, 0x02, 0x00, 0x03, 0x28, 0x0a, 0x03, 0x00, 0x00, 0x2a,
// 0x0003 VALUE 2a00 READ | WRITE | DYNAMIC (设备名)
0x08, 0x00, 0x0a, 0x01, 0x03, 0x00, 0x00, 0x2a,
// 0x0004 PRIMARY_SERVICE ae30 (透传自定义服务)
0x0a, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x28, 0x30, 0xae,
// 0x0005 CHARACTERISTIC ae01 WRITE_WITHOUT_RESPONSE | DYNAMIC
0x0d, 0x00, 0x02, 0x00, 0x05, 0x00, 0x03, 0x28, 0x04, 0x06, 0x00, 0x01, 0xae,
// 0x0006 VALUE ae01 WRITE_WITHOUT_RESPONSE | DYNAMIC
0x08, 0x00, 0x04, 0x01, 0x06, 0x00, 0x01, 0xae,
};
Source: README.md
属性规划要点:
- 0x1800 GAP 服务:标准服务,
0x2a00为设备名特征(ATT_CHARACTERISTIC_2a00_01_VALUE_HANDLE),读回调返回gap_device_name; - 0xae30 自定义服务:
0xae01为透传特征,属性为 WRITE_WITHOUT_RESPONSE | DYNAMIC —— 手机下行采用"无响应写"(低延迟、无需 ATT 应答),DYNAMIC 表示值由应用动态管理而非静态表; - 透传特征没有配 Notify 属性位,但示例中
app_send_user_data仍可对任意 handle 发 Notify/Indicate,前提是对端已使能对应 CCC(att_get_ccc_config(handle + 1))。
ATT 读写回调
协议栈通过两个回调把 ATT 层的读/写请求送到应用:
att_read_callback(connection_handle, att_handle, offset, buffer, buffer_size):对ATT_CHARACTERISTIC_2a00_01_VALUE_HANDLE返回设备名。注意其offset + buffer_size > att_value_len时的越界保护逻辑 —— 协议栈可能以分片方式多次调用,应用必须按 offset 正确返回子串(见 README.md);att_write_callback(connection_handle, att_handle, transaction_mode, offset, buffer, buffer_size):处理手机写入,handle命中透传特征时即收到下行数据(见 README.md)。
事件回调(cbk_packet_handler)
HCI 事件层回调统一分发协议栈事件,与透传相关的主要是:
static void cbk_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size)
{
int mtu;
switch (packet_type) {
case HCI_EVENT_PACKET:
switch (hci_event_packet_get_type(packet)) {
case ATT_EVENT_HANDLE_VALUE_INDICATION_COMPLETE:
log_info("ATT_EVENT_HANDLE_VALUE_INDICATION_COMPLETE\n");
case ATT_EVENT_CAN_SEND_NOW:
can_send_now_wakeup(); /* 协议栈允许继续发数,触发发送队列 */
break;
case HCI_EVENT_LE_META:
switch (hci_event_le_meta_get_subevent_code(packet)) {
case HCI_SUBEVENT_LE_ENHANCED_CONNECTION_COMPLETE:
/* ... 连接建立,记录 con_handle ... */
}
}
}
}
Source: README.md
ATT_EVENT_CAN_SEND_NOW 是 BLE 侧的发送唤醒信号,语义与 SPP 的 send_wakeup 完全对应:应用在回调里把待发队列继续送入协议栈。
Notify/Indicate 发送与检查
上行发送统一走 app_send_user_data,发送前必须先通过 app_send_user_data_check 确认协议栈缓冲足够:
static int app_send_user_data(u16 handle, u8 *data, u16 len, u8 handle_type)
{
u32 ret = APP_BLE_NO_ERROR;
if (!con_handle) { /* 无连接句柄,直接失败 */
return APP_BLE_OPERATION_ERROR;
}
if (!att_get_ccc_config(handle + 1)) { /* 对端未使能 CCC,通知无意义 */
log_info("fail,no write ccc!!!,%04x\n", handle + 1);
return APP_BLE_NO_WRITE_CCC;
}
ret = ble_user_cmd_prepare(BLE_CMD_ATT_SEND_DATA, 4, handle, data, len, handle_type);
return ret;
}
static int app_send_user_data_check(u16 len)
{
u32 buf_space = get_buffer_vaild_len(0);
if (len <= buf_space) {
return 1;
}
return 0;
}
Source: README.md
设计意图:ble_user_cmd_prepare(BLE_CMD_ATT_SEND_DATA, ...) 把发送请求打包进协议栈命令队列,在协议栈任务中异步执行,从而保证应用回调上下文不会被 ATT 发送阻塞;handle_type 用于区分 Notify 与 Indicate 两种发送模式。CCC 检查(att_get_ccc_config(handle + 1))防止对端未订阅时发送浪费空包。
BLE 数据通路时序
sequenceDiagram
participant Phone as 手机 BLE 客户端
participant Stack as btstack (ATT/HCI)
participant Le as le_trans_data.c
participant App as 应用层 (业务)
participant Buf as 协议栈发送缓冲
Note over Phone,Le: 下行: 手机写透传特征
Phone->>Stack: ATT Write (0xae01, WRITE_WITHOUT_RESPONSE)
Stack->>Le: att_write_callback(handle, buffer, size)
Le->>App: 业务处理/转发到串口
Note over Phone,Le: 上行: 应用发送 Notify
App->>Le: app_send_user_data_check(len)
Le->>Buf: get_buffer_vaild_len(0) 检查
Buf-->>Le: 空间足够
App->>Le: app_send_user_data(handle, data, len, type)
Le->>Stack: ble_user_cmd_prepare(BLE_CMD_ATT_SEND_DATA,...)
Stack-->>Le: ATT_EVENT_CAN_SEND_NOW (可继续发送)
Le-->>App: can_send_now_wakeup() 驱动队列
BLE 与 SPP 两条通路共享同一套"检查 → 入队 → 唤醒"的异步流控模型,这是双模透传稳定性的关键:无论在哪个通道,都不会在协议栈回调上下文中同步阻塞。
低功耗(sniff)与连接管理
自动 sniff 控制
app_spp_and_le.c 实现了"空闲进 sniff、有数据退出"的经典低功耗策略(见 app_spp_and_le.c):
#define SNIFF_CNT_TIME 5 // 空闲5S之后进入sniff模式
#define SNIFF_MAX_INTERVALSLOT 800
#define SNIFF_MIN_INTERVALSLOT 100
#define SNIFF_ATTEMPT_SLOT 4
#define SNIFF_TIMEOUT_SLOT 1
void bt_check_enter_sniff()
{
struct sniff_ctrl_config_t sniff_ctrl_config;
u8 addr[12];
u8 conn_cnt = 0;
if (sniff_ready_status) {
sniff_ready_status = 0; /* 刚发过数据,本次检查跳过 */
return;
}
conn_cnt = bt_api_enter_sniff_status_check(SNIFF_CNT_TIME, addr);
ASSERT(conn_cnt <= 2);
for (i = 0; i < conn_cnt; i++) {
sniff_ctrl_config.sniff_max_interval = SNIFF_MAX_INTERVALSLOT;
sniff_ctrl_config.sniff_mix_interval = SNIFF_MIN_INTERVALSLOT;
sniff_ctrl_config.sniff_attemp = SNIFF_ATTEMPT_SLOT;
sniff_ctrl_config.sniff_timeout = SNIFF_TIMEOUT_SLOT;
memcpy(sniff_ctrl_config.sniff_addr, addr + i * 6, 6);
user_send_cmd_prepare(USER_CTRL_SNIFF_IN, sizeof(struct sniff_ctrl_config_t), (u8 *)&sniff_ctrl_config);
}
}
- 启动时
sys_auto_sniff_controle(1, NULL)注册 1 秒周期定时器(sys_timer_add(NULL, bt_check_enter_sniff, 1000))持续检查; bt_api_enter_sniff_status_check(SNIFF_CNT_TIME, addr)统计连接的空闲时长,支持最多 2 台设备(ASSERT(conn_cnt <= 2),与TCFG_BD_NUM对应);- 数据收发路径(
clear_sniff_cnt())把sniff_ready_status置位,让下一次检查跳过,等效于"数据活跃时暂缓进入 sniff"; - 退出 sniff 通过
USER_CTRL_ALL_SNIFF_EXIT命令(bt_check_exit_sniff())。
连接/断开事件处理
static void bt_hci_event_connection(struct bt_event *bt) { bt_wait_phone_connect_control_ext(0, 0); }
static void bt_hci_event_disconnect(struct bt_event *bt) { bt_wait_phone_connect_control_ext(1, 1); }
static void bt_hci_event_page_timeout(struct bt_event *bt) { bt_wait_phone_connect_control_ext(1, 1); }
static void bt_hci_event_connection_timeout(struct bt_event *bt){ bt_wait_phone_connect_control_ext(1, 1); }
void bt_wait_phone_connect_control_ext(u8 inquiry_en, u8 page_scan_en)
{
#if TCFG_USER_EDR_ENABLE
if (inquiry_en) user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_ENABLE, 0, NULL);
else user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
if (page_scan_en) user_send_cmd_prepare(USER_CTRL_WRITE_CONN_ENABLE, 0, NULL);
else user_send_cmd_prepare(USER_CTRL_WRITE_CONN_DISABLE, 0, NULL);
#endif
}
Sources: app_spp_and_le.c、app_spp_and_le.c
设计意图:连接成功后关闭可发现/可连接,防止被其他手机反复骚扰打断透传;断开/超时后恢复可发现与可连接,等待回连。bt_hci_event_handler 中还处理了测试盒事件(HCI_EVENT_VENDOR_REMOTE_TEST):EDR 连接建立时主动 bt_ble_adv_enable(0) 关闭 BLE 广播,避免同一设备双通道并存(见 app_spp_and_le.c)。
配置选项
| 宏/配置 | 类型 | 默认/取值 | 说明 |
|---|---|---|---|
TRANS_DATA_EN | int | 1 | 蓝牙双模透传总开关(README 模块使能) |
TCFG_USER_BLE_ENABLE | int | 1 | BLE 功能使能;关掉后 lib_make_ble_address 与 ble_module_enable 分支不编译 |
TCFG_USER_EDR_ENABLE | int | 1 | EDR(经典蓝牙)功能使能;关掉后 sniff 控制、可发现/可连接控制分支不编译 |
TRANS_AT_COM | int | 0 | 1 时编译 AT 指令模式(app_at_com.c/at_cmds.c/at_uart.c),app_spp_and_le.c 整体被 #if (!TRANS_AT_COM) 排除 |
USER_SUPPORT_PROFILE_SPP | int | 1 | 是否注册 SPP 用户数据处理器(spp_data_deal_handle_register) |
TCFG_BD_NUM | int | 1~2 | 同时连接设备数;为 2 时 __set_auto_conn_device_num(2),sniff 检查 ASSERT(conn_cnt <= 2) |
BT_SUPPORT_DISPLAY_BAT | int | 0 | 电量显示;开启时 __bt_set_update_battery_time(60) 并注册电量回调 |
BT_SUPPORT_MUSIC_VOL_SYNC | int | 0 | 音乐音量同步;开启时注册音量变化处理器 |
TCFG_CLOCK_OSC_HZ | int | 振荡器频率 | 传给 bt_pll_para 的 OSC 参数 |
BT_NORMAL_HZ | int | 系统主频 | 应用启动时 clk_set("sys", BT_NORMAL_HZ) 设定的系统时钟 |
WAIT_DISCONN_TIME_MS | int | 300 | 软关机前等待蓝牙链路断开的延时(ms) |
SNIFF_CNT_TIME | int | 5 | 空闲多少秒后进入 sniff 模式 |
SNIFF_MAX_INTERVALSLOT | int | 800 | sniff 最大间隔(slot) |
SNIFF_MIN_INTERVALSLOT | int | 100 | sniff 最小间隔(slot) |
编译对象与依赖路径见 Makefile:spp_trans_data.o、spp_user.o、le_trans_data.o、le_client_demo.o、spp_at_com.o、le_at_com.o 均来自 apps/common/third_party_profile/jieli/;配置项定义于 app_config.h 及 board/$(CPU) 板级目录,具体默认值以各板级 app_config.h 为准。
API 参考
SPP 侧(spp_trans_data.c)
| 函数 | 签名要点 | 说明 |
|---|---|---|
transport_spp_init | void transport_spp_init(void) | 获取 SPP 操作表并注册接收/状态/唤醒回调 |
transport_spp_recieve_cbk | static void (void *priv, u8 *buf, u16 len) | 接收回调:打印、clear_sniff_cnt()、回环测试 |
transport_spp_state_cbk | static void (u8 state) | 连接状态回调(SPP_USER_ST_CONNECT/SPP_USER_ST_DISCONN) |
transport_spp_send_data | int transport_spp_send_data(u8 *data, u16 len) | 发送数据;spp_api 为空返回 SPP_USER_ERR_SEND_FAIL |
transport_spp_send_data_check | int transport_spp_send_data_check(u16 len) | 发送前检查:busy_state() 忙则返回 0 |
transport_spp_send_wakeup | static void transport_spp_send_wakeup(void) | 发送完成唤醒钩子,驱动继续发送 |
BLE 侧(le_trans_data.c)
| 函数 | 签名要点 | 说明 |
|---|---|---|
cbk_packet_handler | static void (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) | HCI 事件分发:ATT_EVENT_CAN_SEND_NOW → can_send_now_wakeup();LE_ENHANCED_CONNECTION_COMPLETE → 记录连接 |
att_read_callback | static uint16_t (hci_con_handle_t, uint16_t att_handle, uint16_t offset, uint8_t *buffer, uint16_t buffer_size) | 读请求;0x2a00 返回设备名,含 offset 越界保护 |
att_write_callback | static int (hci_con_handle_t, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size) | 写请求;透传特征命中即收到下行数据 |
app_send_user_data | static int app_send_user_data(u16 handle, u8 *data, u16 len, u8 handle_type) | Notify/Indicate 发送;无连接返回 APP_BLE_OPERATION_ERROR,未使能 CCC 返回 APP_BLE_NO_WRITE_CCC |
app_send_user_data_check | static int app_send_user_data_check(u16 len) | 发送前检查 get_buffer_vaild_len(0) 是否足够 |
应用层辅助接口(app_spp_and_le.c)
| 函数 | 签名要点 | 说明 |
|---|---|---|
app_start | static void app_start(void) | 时钟/PLL 配置 → bt_function_select_init → bredr_handle_register → btstack_init → sniff 控制 |
bt_function_select_init | static void bt_function_select_init(void) | 连接数、超时、简单配对参数(__set_simple_pair_param(3, 0, 2))、BLE 地址派生 |
bredr_handle_register | static void bredr_handle_register(void) | 注册 SPP 数据、音量、电量回调 |
bt_send_pair | void bt_send_pair(u8 en) | 发送配对命令(USER_CTRL_PAIR) |
sys_auto_sniff_controle | void sys_auto_sniff_controle(u8 enable, u8 *addr) | 启停自动 sniff 检查定时器 |
bt_check_enter_sniff / bt_check_exit_sniff | void (void) | 空闲进入 / 数据退出 sniff |
app_set_soft_poweroff | static void app_set_soft_poweroff(void) | 先 ble_module_enable(0) 关 BLE,再延时 300ms 软关机 |
失败模式、边界情况与并发
回调上下文与并发约束
SPP 的接收/状态/唤醒回调与 BLE 的 cbk_packet_handler、att_read_callback、att_write_callback 均运行在 btstack 协议栈任务上下文中。由此得出两条硬约束:
- 回调内禁止阻塞:不能等待互斥量、不能做慢速外设访问,数据应拷贝到应用缓冲后返回;
- 发送必须异步化:SPP 用
spp_api->send_data(协议栈内部排队),BLE 用ble_user_cmd_prepare(BLE_CMD_ATT_SEND_DATA, ...)把命令投递到协议栈任务队列,天然串行化,避免多任务并发写 ATT 通道。
典型失败场景与处理
| 场景 | 触发条件 | 代码行为 |
|---|---|---|
| SPP 协议栈忙 | busy_state() 为真 | transport_spp_send_data_check 返回 0,上层应暂存数据,等待 transport_spp_send_wakeup |
| SPP 未初始化 | spp_api 为 NULL | transport_spp_send_data 返回 SPP_USER_ERR_SEND_FAIL |
| BLE 未连接 | con_handle 为 0 | app_send_user_data 返回 APP_BLE_OPERATION_ERROR,不发空包 |
| 对端未订阅通知 | att_get_ccc_config(handle + 1) 为 0 | 返回 APP_BLE_NO_WRITE_CCC,日志打印 fail,no write ccc!!! |
| BLE 发送缓冲不足 | len > get_buffer_vaild_len(0) | app_send_user_data_check 返回 0,等待 ATT_EVENT_CAN_SEND_NOW |
| ATT 读越界 | offset + buffer_size > att_value_len | att_read_callback 直接 break,不拷贝数据(分片读取保护) |
| 双设备连接 | conn_cnt > 2 | ASSERT(conn_cnt <= 2) 触发断言,与 TCFG_BD_NUM 配置联动 |
| 软关机时链路未断 | BLE 仍连接 | 先 ble_module_enable(0) 主动断开,再延时 300ms 执行 power_set_soft_poweroff,避免链路超时等待 |
双模共存边界
- 地址派生:BLE 地址由 EDR MAC 经
lib_make_ble_address派生并le_controller_set_mac设置,保证双模设备地址关联(见 app_spp_and_le.c); - 通道互斥:EDR 连接建立时关闭 BLE 广播(
bt_ble_adv_enable(0)),断开后由事件处理恢复可发现/可连接;同一时刻手机优先走其中一个通道,避免数据在两个通道间串扰; - sniff 与透传互斥:收发路径调用
clear_sniff_cnt()延迟进入 sniff,保证连续数据传输不被低功耗打断。
性能与运维注意事项
- 流控节奏:SPP 单包发送受
busy_state约束,BLE 受发送缓冲约束;上层发送队列应设计为"唤醒驱动"模型(send_wakeup/CAN_SEND_NOW),而不是轮询,以降低协议栈负载。 - sniff 参数:
SNIFF_MAX_INTERVALSLOT 800(约 1s)适合低吞吐透传;高吞吐场景需调小间隔或延长SNIFF_CNT_TIME,否则数据延迟增大。 - 日志开销:
log_info_hexdump(buf, len)在接收回调中逐字节打印,高速透传时建议注释掉(README 中已有/* log_info_hexdump(data, len); */的先例)。 - 系统时钟:
app_start固定clk_set("sys", BT_NORMAL_HZ)并配置 PLL,改动时钟前需确认射频参数匹配(bt_pll_para(TCFG_CLOCK_OSC_HZ, sys_clk, 0, 0))。
扩展点
- 替换回环测试为业务处理:在
transport_spp_recieve_cbk/att_write_callback中把数据转发到 UART 或业务模块,实现真正的"透传";上行数据分别调用transport_spp_send_data与app_send_user_data。 - 修改 GATT 服务:用
make_gatt_services工具重新生成profile_data表(le_trans_data.h),可增删特征、调整属性(如给透传特征加 Notify、增加 RX/TX 双特征)。 - AT 指令模式:将
TRANS_AT_COM置 1 后编译app_at_com.c/at_cmds.c/at_uart.c,通过 UART 下发 AT 指令控制连接、查询状态,app_spp_and_le.c自动退出编译。 - 多连接扩展:
TCFG_BD_NUM=2时自动使能双设备连接(__set_auto_conn_device_num(2)),sniff 控制已支持双连接(ASSERT(conn_cnt <= 2))。 - 配对策略:
__set_simple_pair_param(3, 0, 2)(io_capabilities=3 NoInputNoOutput、authentication=0)为免输入配对,可按产品安全需求调整。
相关链接
- app_spp_and_le.c(应用主逻辑)
- README.md(SPP/LE 透传接口说明)
- Makefile(编译对象与依赖)
spp_trans_data.c/spp_user.c:apps/common/third_party_profile/jieli/trans_data_demo/(SPP 透传封装)le_trans_data.c/le_at_com.c/le_client_demo.c:apps/common/third_party_profile/jieli/trans_data_demo/(BLE 透传封装)- AT 指令模式:
app_at_com.c/at_cmds.c/at_uart.c(apps/spp_and_le/,TRANS_AT_COM=1时启用)