杰理 SDK 文档中心
首页
首页
  • SDK 概述与快速开始

    • SDK 概览与 AC791N 芯片平台
    • 环境搭建与编译指南
    • 烧录与固件升级
    • 工程结构导览
  • 产品方案应用

    • WiFi 摄像头方案
    • WiFi IPC 可视对讲方案
    • WiFi 故事机方案
    • 扫码枪 HID 方案
    • 开发板示例工程
  • 公共应用组件

    • 语音识别 ASR 引擎
    • LLM 与 AI 语音助手接入
    • 摄像头传感器驱动
    • UI 显示框架与驱动
    • USB 主机与设备栈
    • 文件系统与存储管理
    • 系统服务与外设管理
    • 生产测试与射频工具
  • 蓝牙协议栈

    • 经典蓝牙 BR/EDR
    • BLE 低功耗蓝牙
    • 蓝牙 Mesh 网络
    • 蓝牙扩展协议(RCSP/广播/无线麦克风)
  • WiFi 与网络协议栈

    • WiFi 驱动与网络模式
    • lwIP TCP/IP 协议栈
    • 网络安全与加密库
    • 应用层网络协议
    • 流媒体与音视频传输
    • 云平台接入 SDK
    • P2P 远程访问与设备互联
  • 芯片平台与驱动

    • wl82 平台与硬件加速
    • 外设驱动框架
    • 平台配置与固件打包工具
  • 媒体与音频引擎

    • 音频编解码与音源
    • 音效处理引擎
    • 视频与图像处理
  • 操作系统与运行时

    • 实时操作系统与 POSIX 层
    • C/C++ 运行时库
  • 开发资源与文档

    • 文档与规格书
    • 公共示例工程
    • UI 资源工程与打包
    • SDK 辅助工具与脚本

经典蓝牙 BR/EDR

经典蓝牙 BR/EDR(Basic Rate / Enhanced Data Rate)是 AC79 系列 AIoT SDK 中面向音频设备的传统蓝牙能力集合,覆盖 A2DP 音乐播放、HFP 通话、设备搜索、回连与发射器(Emitter)等完整链路。本文档基于 apps/common/example/bluetooth/edr/bt_music/ 下的 EDR Demo 源码,讲解经典蓝牙在应用层的实现机制、核心数据结构、控制流程与扩展方式。

Purpose and Scope

本文档介绍 SDK 中经典蓝牙 BR/EDR 子系统在**应用层(App Layer)**的实现,具体包括:

  • EDR Demo 的框架入口与编译开关(USE_EDR_DEMO / CONFIG_BT_ENABLE / TCFG_USER_EMITTER_ENABLE)
  • 蓝牙应用状态管理结构 struct app_bt_opr 及其状态字段
  • 可发现/可连接(Discoverable/Connectable)控制逻辑 bt_wait_phone_connect_control()
  • 经典蓝牙发射器(Emitter)的搜索与连接流程
  • A2DP 音乐解码与 HFP 通话在应用层的音频通路
  • 与底层协议栈的交互接口(btstack/avctp_user.h、classic/hci_lmp.h、event/bt_event.h)

以下内容不在本文档范围内,由同目录其他页面负责:

  • BLE(低功耗蓝牙)相关能力,参见 BLE 系列文档(如 apps/common/example/bluetooth/ble/ 下的 GATT Client/Server 示例)
  • 底层控制器(BT Controller)HCI 命令实现与射频链路层细节
  • 第三方协议(如 RCSP 私有协议)及其 bt_trans_data 透传通道

Overview

在 AC79 SDK 中,经典蓝牙 BR/EDR 的完整能力由三层构成:**蓝牙控制器(BT Controller)**负责射频收发与链路管理(ACL/SCO 链路、查询 Inquiry、寻呼 Page);**协议栈(BTStack)**实现 L2CAP、SDP、AVDTP/AVCTP、A2DP、HFP、SPP 等 Profile;应用层(本文档重点)通过 user_send_cmd_prepare() 向协议栈下发控制命令,并通过 bt_event 事件回调驱动状态机。

EDR Demo 位于 apps/common/example/bluetooth/edr/bt_music/,是一个"音乐播放 + 电话"二合一参考实现:

  • main.c:应用主逻辑,持有全局状态 app_bt_hdl,管理连接、回连、音量、通话与音乐切换;
  • bt_decode.c:音频解码线程,对接 A2DP 媒体流(SBC/MPEG 编解码)与 HFP 语音(eSCO)数据;
  • bt_emitter.c:发射器模式(可选,TCFG_USER_EMITTER_ENABLE 开启),使本设备作为音源向其他蓝牙音箱发射音乐。

设计意图:通过"事件驱动 + 命令下发"的架构,把复杂的 BR/EDR 链路管理收敛到底层协议栈,应用层只维护一份精简的状态(app_bt_opr),从而降低多 Profile 并发(A2DP 与 HFP 同时激活)时的耦合复杂度。

Architecture

flowchart TD
    subgraph sg_App["应用层 (EDR Demo)"]
        Main["main.c<br/>app_bt_hdl 状态机"]
        Decode["bt_decode.c<br/>音频解码 (A2DP/HFP)"]
        Emitter["bt_emitter.c<br/>发射器模式"]
    end

    subgraph sg_Stack["协议栈层 (BTStack)"]
        Avctp["avctp_user.h<br/>AVCTP/AVDTP 用户接口"]
        A2dp["a2dp_media_codec.h<br/>A2DP 媒体编解码"]
        HciLmp["classic/hci_lmp.h<br/>HCI/LMP 链路控制"]
        BtEvent["event/bt_event.h<br/>蓝牙事件"]
    end

    subgraph sg_OS["系统服务"]
        Server["server_core.h<br/>音频服务"]
        SysCfg["syscfg/syscfg_id.h<br/>VM 配置存储"]
        Timer["system/timer.h<br/>定时器"]
    end

    Main -->|"user_send_cmd_prepare()"| Avctp
    Main -->|"USER_CTRL_* 命令"| HciLmp
    Decode --> A2dp
    Emitter -->|"USER_CTRL_SEARCH_DEVICE / START_CONNEC_VIA_ADDR"| HciLmp
    Avctp -->|"bt_event 回调"| Main
    Decode --> Server
    Main --> SysCfg
    Main --> Timer
    Emitter --> Timer

架构说明:

  • main.c(状态中枢):static struct app_bt_opr app_bt_hdl 是唯一的应用级状态体,通过 __this 宏访问。所有 Profile 事件(来电、音乐开始/暂停、连接建立/断开)最终都汇聚到这里,由它决定下一步动作(回连、切换音频、更新 UI)。
  • bt_decode.c(音频通路):编译期通过 BT_MUSIC_DECODE 与 BT_PHONE_DECODE 宏分别使能音乐解码与通话解码;解码器对接 earphone_a2dp_audio_codec_open() 等音频服务接口,输出到 server_core 音频服务器。
  • bt_emitter.c(发射器):把本机变成"音源",先发起 Inquiry 搜索周边设备(USER_CTRL_SEARCH_DEVICE),再通过 USER_CTRL_START_CONNEC_VIA_ADDR 按 MAC 地址建链。
  • 协议栈交互:应用层不直接触碰射频,所有动作都转化为 USER_CTRL_* 命令(如 USER_CTRL_WRITE_SCAN_ENABLE、USER_CTRL_PAGE_CANCEL)下发到 avctp_user / hci_lmp,结果以 bt_event 回调返回。这种"命令-事件"模式是 AC79 蓝牙框架的标准交互范式。

核心实现详解

应用状态机:struct app_bt_opr

main.c 定义了整个经典蓝牙应用的核心状态结构,所有运行期状态都浓缩在这一结构体中:

struct app_bt_opr {
    //phone
    u8 phone_ring_flag: 1;
    u8 phone_num_flag: 1;
    u8 phone_income_flag: 1;
    u8 phone_call_dec_begin: 1;
    u8 phone_con_sync_num_ring: 1;
    u8 phone_con_sync_ring: 1;
    u8 emitter_or_receiver: 2;

    u8 media_play_flag : 1;
    u8 call_flag : 1;	// 1-由于蓝牙打电话命令切回蓝牙模式
    u8 exit_flag : 1;	// 1-可以退出蓝牙标志
    u8 enable    : 1;
    u8 emitter   : 1;
    u8 mute      : 1;
    u8 siri_stu;		// ios siri

    u8 call_volume;
    u8 media_volume;
    u8 inband_ringtone;
    u8 last_call_type;
    u8 auto_connection_addr[6];
    u8 income_phone_num[30];
    u8 income_phone_len;
    u16 phone_timer_id;
    u16 poweroff_timer_id;
    int auto_connection_counter;
    int auto_connection_timer;
    int sniff_timer;
    void *dec_server;
};

Source: main.c

设计要点:

  • 位域压缩:电话侧 6 个标志与发射/接收角色共用 1 字节(phone_ring_flag 到 emitter_or_receiver),媒体/通话/退出等标志再占 1 字节。这种紧凑布局说明该结构会在中断/低功耗场景下被频繁读写,用位域节省 RAM 并方便原子性判断。
  • 双角色并存:emitter_or_receiver(2 bit,可表示 4 种状态)与 emitter 标志配合,支持"接收器"(默认音箱)与"发射器"(音源)两种角色切换;call_flag 专门记录"因蓝牙电话命令切回蓝牙模式",用于音频路由回切。
  • 回连状态:auto_connection_addr[6] 保存上次连接的设备地址,auto_connection_counter/timer 驱动超时回连(TIMEOUT_CONN_TIME = 30 秒),poweroff_timer_id 管理关机流程。
  • Siri 支持:siri_stu 字段为 iOS Siri 语音助手交互保留状态,说明该 Demo 覆盖了手机语音助手的经典蓝牙场景。

可发现 / 可连接控制:bt_wait_phone_connect_control()

这是经典蓝牙"设备可见性"的核心逻辑,根据当前连接数动态决定扫描(Inquiry Scan)与寻呼(Page Scan)开关:

static void bt_wait_phone_connect_control(u8 enable)
{
    if (!__this->enable && enable) {
        return;
    }

    if (enable) {
        log_i("is_1t2_connection:%d \t total_conn_dev:%d\n", is_1t2_connection(), get_total_connect_dev());
        if (is_1t2_connection()) {
            /*达到最大连接数,可发现(0)可连接(0)*/
            user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
            user_send_cmd_prepare(USER_CTRL_WRITE_CONN_DISABLE, 0, NULL);
        } else {
            if (get_total_connect_dev() == 1) {
                /*支持连接2台,只连接一台的情况下,可发现(0)可连接(1)*/
                user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
                user_send_cmd_prepare(USER_CTRL_WRITE_CONN_ENABLE, 0, NULL);
            } else {
                /*可发现(1)可连接(1)*/
                user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_ENABLE, 0, NULL);
                user_send_cmd_prepare(USER_CTRL_WRITE_CONN_ENABLE, 0, NULL);
            }
        }
    } else {
        user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
        user_send_cmd_prepare(USER_CTRL_WRITE_CONN_DISABLE, 0, NULL);
    }
}

Source: main.c

设计意图:

  • 多连接(1T2)支持:is_1t2_connection() 表示是否达到最大连接数(本 Demo 支持同时连接 2 台手机)。达到上限后同时关闭可发现与可连接,避免手机反复尝试入链;只连 1 台时仍开放可连接(允许第二台接入)但关闭可发现(不再广播自身),这是功耗与可用性的折中。
  • 统一命令出口:所有开关都通过 user_send_cmd_prepare(USER_CTRL_*, 0, NULL) 异步下发,由协议栈串行处理,避免应用层直接操作寄存器带来的竞态。
  • 使能保护:if (!__this->enable && enable) 防止在蓝牙未使能时误开扫描。

发射器(Emitter)模式

bt_emitter.c 在 TCFG_USER_EMITTER_ENABLE 与 USE_EDR_DEMO 双重宏控制下编译,实现"本机作为音源"的完整搜索-连接流程。

设备搜索

void bt_search_device(void)
{
    if (bt_search_busy) {
        //return;
    }
    user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_DISABLE, 0, NULL);
    user_send_cmd_prepare(USER_CTRL_WRITE_CONN_DISABLE, 0, NULL);

    read_name_start = 0;
    bt_search_busy = 1;
    u8 inquiry_length = 20;   // inquiry_length * 1.28s
    user_send_cmd_prepare(USER_CTRL_SEARCH_DEVICE, 1, &inquiry_length);
    log_i("bt_search_start\n");
}

Source: bt_emitter.c

关键点:

  • 搜索前先关闭本机扫描/可连接,避免自身被其他设备发现(发射器模式只主动连接别人)。
  • inquiry_length = 20,即 Inquiry 持续 20 × 1.28s ≈ 25.6 秒,bt_search_busy 标志防止重复发起搜索。
  • 搜索限制模式通过 SEARCH_LIMITED_MODE 宏选择,可选按 BD_ADDR(SEARCH_BD_ADDR_LIMITED)、按设备名(SEARCH_BD_NAME_LIMITED,默认)、自定义过滤或不过滤,对应 struct remote_name(含 crc/addr[6]/name[32])与 struct inquiry_noname_remote(链表节点,含 rssi、class)两套结果容器。

按地址建链

void emitter_bt_connect(u8 *mac)
{
    if (emitter_or_receiver != BT_EMITTER_EN) {
        return;
    }

    while (hci_standard_connect_check() == 0x80) {
        //wait profile connect ok;
        if (get_curr_channel_state()) {
            break;
        }
        os_time_dly(10);
    }

    ////断开链接
    if (get_curr_channel_state() != 0) {
        user_send_cmd_prepare(USER_CTRL_POWER_OFF, 0, NULL);
    } else {
        if (hci_standard_connect_check()) {
            user_send_cmd_prepare(USER_CTRL_PAGE_CANCEL, 0, NULL);
            user_send_cmd_prepare(USER_CTRL_CONNECTION_CANCEL, 0, NULL);
        }
    }
    /* if there are some connected channel ,then disconnect*/
    while (hci_standard_connect_check() != 0) {
        //wait disconnect;
        os_time_dly(10);
    }

    user_send_cmd_prepare(USER_CTRL_START_CONNEC_VIA_ADDR, 6, mac);
}

Source: bt_emitter.c

设计意图:

  • 先复位再建链:连接新设备前必须确保链路干净——若有 Profile 正在建链(hci_standard_connect_check() == 0x80)先等待完成;已有连接则发 USER_CTRL_POWER_OFF 断开;若处于寻呼中则先 USER_CTRL_PAGE_CANCEL + USER_CTRL_CONNECTION_CANCEL 取消,最后轮询等待链路完全断开(while + os_time_dly(10))才发起 USER_CTRL_START_CONNEC_VIA_ADDR。这种"串行化"策略保证了单射频(BR/EDR 半双工)下不会出现多路寻呼竞争。
  • 命令语义区分:USER_CTRL_POWER_OFF 用于彻底断开已有连接(含 RF 关断),USER_CTRL_PAGE_CANCEL 仅取消正在进行的寻呼,二者针对不同链路状态,体现了协议栈命令的精细粒度。

停止搜索与状态查询

u8 bt_search_status(void)
{
    return bt_search_busy;
}

u8 bt_emitter_role_get(void)
{
    return emitter_or_receiver;
}

void bt_emitter_stop_search_device(void)
{
    user_send_cmd_prepare(USER_CTRL_INQUIRY_CANCEL, 0, NULL);
}

Source: bt_emitter.c

这三个接口分别供 UI/按键层查询搜索忙状态、查询当前角色、以及通过 USER_CTRL_INQUIRY_CANCEL 中止 Inquiry,是应用层与发射器模块的标准门面(Facade)。

音频解码通路:bt_decode.c

bt_decode.c 通过 BT_MUSIC_DECODE(音乐)与 BT_PHONE_DECODE(通话)两个宏分别启用 A2DP 与 HFP 解码路径,并定义 A2DP 编解码器类型:

#define A2DP_CODEC_SBC		0x00
#define A2DP_CODEC_MPEG12	0x01
#define A2DP_CODEC_MPEG24	0x02

Source: bt_decode.c

  • 编码类型与 a2dp_media_codec.h 的媒体流协商结果对应,解码线程依据类型分发到对应解码器。
  • 解码器通过外部接口接入音频服务器:earphone_a2dp_audio_codec_open(int media_type, u8 volume) 打开 A2DP 解码,phone_call_begin(void *priv, u8 volume) / phone_call_end() 管理通话解码,a2dp_media_get_total_buffer_size() 查询缓冲水位。
  • 后台静音检测定义了三个关键时间窗:SBC_FILTER_TIME_MS = 2000(后台音频过滤时间)、SBC_ZERO_TIME_MS = 500(静音多久认为已退出)、NO_SBC_TIME_MS = 100(无音频时间),用于在无有效音频数据时快速关断解码器以省电。
  • 通话路径还检查 get_esco_coder_busy_flag() / get_esco_busy_flag(),避免 eSCO 语音编解码器被音乐播放占用(A2DP 与 HFP 共享音频硬件的互斥保护)。

核心流程

经典蓝牙完整生命周期(接收器模式)

sequenceDiagram
    participant App as 应用层 main.c
    participant Stack as BTStack (avctp_user/hci_lmp)
    participant Phone as 手机
    participant Dec as bt_decode.c
    participant Aud as 音频服务器

    Note over App: 上电/使能
    App->>Stack: user_send_cmd_prepare(USER_CTRL_WRITE_SCAN_ENABLE / CONN_ENABLE)
    Stack-->>App: bt_event (可发现/可连接)
    Phone->>Stack: Inquiry/Page 请求
    Stack-->>App: 连接建立事件
    App->>App: bt_wait_phone_connect_control() 更新扫描/连接开关
    Note over App: A2DP 音乐播放
    Phone->>Stack: AVDTP 媒体流 (SBC)
    Stack->>Dec: 媒体数据回调
    Dec->>Dec: 按 A2DP_CODEC_* 分发解码
    Dec->>Aud: earphone_a2dp_audio_codec_open(media_type, volume)
    Aud-->>App: 解码完成事件
    Note over App: HFP 来电
    Phone->>Stack: eSCO/Ring 事件
    Stack-->>App: phone_ring_flag 置位
    App->>Dec: phone_call_begin(priv, volume)
    Dec->>Aud: 语音编解码 (eSCO)
    App->>Stack: 通话结束 -> phone_call_end()

发射器模式搜索-连接流程

flowchart TD
    Start([触发搜索]) --> Disable["USER_CTRL_WRITE_SCAN_DISABLE<br/>USER_CTRL_WRITE_CONN_DISABLE"]
    Disable --> Inquiry["USER_CTRL_SEARCH_DEVICE<br/>inquiry_length = 20 x 1.28s"]
    Inquiry --> Found{"搜索到设备?"}
    Found -->|"是"| Select["UI 选择目标设备 (按名字/地址过滤)"]
    Found -->|"否/超时"| Stop["USER_CTRL_INQUIRY_CANCEL<br/>bt_search_busy = 0"]
    Select --> Check{"当前链路状态"}
    Check -->|"Profile 建链中 (0x80)"| Wait["等待建链完成<br/>os_time_dly(10) 轮询"]
    Wait --> Check
    Check -->|"已有连接"| Off["USER_CTRL_POWER_OFF 断开"]
    Check -->|"寻呼中"| Cancel["USER_CTRL_PAGE_CANCEL<br/>USER_CTRL_CONNECTION_CANCEL"]
    Off --> Drain["轮询等待链路完全断开"]
    Cancel --> Drain
    Drain --> Connect["USER_CTRL_START_CONNEC_VIA_ADDR(mac, 6)"]
    Connect --> Done([建链成功, 进入发射播放])
    Stop --> Done

流程要点:

  1. 上电回连:开机后应用读取 VM 中保存的 auto_connection_addr[6](通过 get_remote_device_info_from_vm()),在 POWERON_AUTO_CONN_TIME(18 秒,取 6 的倍数以配合协议栈超时粒度)窗口内自动回连上次设备;若回连失败,则在 TIMEOUT_CONN_TIME(30 秒)后再次触发,由 auto_connection_counter 控制尝试次数。
  2. 连接数驱动的可见性:每建立/断开一台设备都会重新调用 bt_wait_phone_connect_control(),在 0/1/2 台连接三个档位间切换 SCAN/CONN 开关,实现"第二台手机可接入、第三台不可见"的 1T2 策略。
  3. 音频路由:音乐与来电通过 call_flag、media_play_flag 协调:来电时暂停音乐(BT_INBAND_RINGTONE 决定是否播放手机彩铃),挂断后依据 last_call_type 恢复音乐或进入静默。
  4. 发射器建链的确定性:emitter_bt_connect() 通过 while 轮询保证任意历史状态下链路都被复位到"无连接",再发起 USER_CTRL_START_CONNEC_VIA_ADDR,使按地址建链的时序完全确定。

使用示例

示例 1:使能/关闭经典蓝牙并控制可见性

/* 使能蓝牙:开启可发现与可连接,等待手机配对 */
__this->enable = 1;
bt_wait_phone_connect_control(1);

/* 关闭蓝牙:同时关闭可发现与可连接 */
bt_wait_phone_connect_control(0);

Source: main.c

示例 2:发射器搜索周边蓝牙设备

/* 发起 20 x 1.28s 的设备搜索 */
bt_search_device();

/* 查询搜索是否进行中 */
u8 busy = bt_search_status();

/* 用户选择设备后按 MAC 建链 */
emitter_bt_connect(selected_mac);

/* 中途放弃搜索 */
bt_emitter_stop_search_device();

Sources:

  • bt_emitter.c
  • bt_emitter.c

示例 3:音频解码器接入(音乐 + 通话)

/* A2DP 音乐解码:打开指定媒体类型解码器 */
extern void *earphone_a2dp_audio_codec_open(int media_type, u8 volume);

/* HFP 通话解码:通话开始/结束 */
extern void *phone_call_begin(void *priv, u8 volume);
extern void phone_call_end(void);

/* eSCO 编解码器占用检测,避免音乐/通话冲突 */
extern u8 get_esco_coder_busy_flag(void);
extern bool get_esco_busy_flag(void);

Sources:

  • main.c
  • bt_decode.c

配置选项

经典蓝牙 EDR Demo 的配置分为编译期宏(在 app_config.h 或工程配置中定义)与运行期常量(main.c / bt_emitter.c 内 #define)两类。

编译期宏开关

宏作用域说明
CONFIG_BT_ENABLE全局总使能蓝牙功能,main.c 中 #ifdef CONFIG_BT_ENABLE 保护整个 EDR 应用
USE_EDR_DEMO应用使能 EDR(经典蓝牙)Demo,main.c、bt_decode.c、bt_emitter.c 均以它作为编译门槛
TCFG_USER_EMITTER_ENABLE应用使能发射器模式,bt_emitter.c 主体逻辑的编译条件
BT_MUSIC_DECODEbt_decode.c使能 A2DP 音乐解码路径
BT_PHONE_DECODEbt_decode.c使能 HFP 通话解码路径

运行期常量

常量类型默认值说明
TIMEOUT_CONN_TIMEint30连接超时断开后的回连时间(秒)
POWERON_AUTO_CONN_TIMEint18开机自动回连时间(秒),需取 6 的倍数
BT_INBAND_RINGTONEint1是否播放手机彩铃(in-band ringtone)
VOLUME_STEPint5音量调节步进
MIN_VOLUME_VALUEint5最小音量值
MAX_VOLUME_VALUEint100最大音量值
INIT_VOLUME_VALUEint100初始化音量值
SBC_FILTER_TIME_MSint2000后台音频过滤时间(毫秒),无有效 SBC 数据时关断解码器
SBC_ZERO_TIME_MSint500静音持续多长时间认为已退出播放(毫秒)
NO_SBC_TIME_MSint100无音频数据判定时间(毫秒)
SEARCH_LIMITED_MODEintSEARCH_BD_NAME_LIMITED (1)发射器搜索过滤策略:0=按地址、1=按名字(默认)、2=自定义、3=不过滤
inquiry_lengthu820发射器 Inquiry 时长,实际时间 = 值 × 1.28 秒

Sources:

  • main.c
  • main.c
  • bt_emitter.c

API 参考

应用层可见性控制(main.c)

bt_wait_phone_connect_control(u8 enable)

控制本设备可发现(Inquiry Scan)与可连接(Page Scan)状态,根据已连接设备数自动选择档位。

参数:

  • enable (u8):1 = 使能扫描/连接,0 = 全部关闭

返回: 无

行为:

  • enable=1 且已达最大连接数(is_1t2_connection() 为真):SCAN_DISABLE + CONN_DISABLE
  • enable=1 且已连 1 台:SCAN_DISABLE + CONN_ENABLE
  • enable=1 且未连接:SCAN_ENABLE + CONN_ENABLE
  • enable=0:SCAN_DISABLE + CONN_DISABLE

发射器接口(bt_emitter.c)

void bt_search_device(void)

发起经典蓝牙 Inquiry 设备搜索。搜索前自动关闭本机扫描/连接,搜索持续 inquiry_length × 1.28 秒,通过 bt_search_busy 标志防止重入。

参数: 无 返回: 无

u8 bt_search_status(void)

返回搜索是否正在进行。

返回: bt_search_busy(1 = 搜索中,0 = 空闲)

u8 bt_emitter_role_get(void)

返回当前设备角色。

返回: emitter_or_receiver(BT_EMITTER_EN 表示发射器模式)

void bt_emitter_stop_search_device(void)

通过 USER_CTRL_INQUIRY_CANCEL 中止正在进行的搜索。

参数: 无 返回: 无

void emitter_bt_connect(u8 *mac)

按 6 字节 MAC 地址发起经典蓝牙连接。内部先复位链路(等待建链完成、断开已有连接、取消寻呼),再发送 USER_CTRL_START_CONNEC_VIA_ADDR。

参数:

  • mac (u8*):目标设备 BD_ADDR(6 字节)

前置条件: 设备必须处于发射器角色(emitter_or_receiver == BT_EMITTER_EN),否则直接返回。

状态查询辅助接口

接口说明
is_1t2_connection()是否已达 1T2 最大连接数
get_total_connect_dev()当前已连接设备总数
hci_standard_connect_check()查询 HCI 标准连接状态(0x80 = Profile 建链中,非 0 = 已有连接)
get_curr_channel_state()当前通道状态
get_remote_device_info_from_vm()从 VM 读取上次配对设备信息
get_esco_coder_busy_flag() / get_esco_busy_flag()eSCO 语音编解码器占用状态

Sources:

  • main.c
  • bt_emitter.c

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

连接管理边界

  • 1T2 满连接:达到最大连接数后 bt_wait_phone_connect_control() 同时关闭 SCAN 与 CONN,此时第三台手机将完全无法发现本设备。若上层仍希望发现,需修改 is_1t2_connection() 判定或直接调用 USER_CTRL_WRITE_SCAN_ENABLE 覆盖。
  • 发射器角色保护:emitter_bt_connect() 与 bt_emitter_start_search_device() 入口都检查 emitter_or_receiver != BT_EMITTER_EN 并直接返回,防止在接收器角色下误发起主动连接。
  • 回连失败:开机回连依赖 VM 中保存的 auto_connection_addr[6];若设备已不在范围内,auto_connection_counter 计数超限后停止重试,需要用户重新配对(bredr_set_dut_enble() 可进入 DUT 测试模式辅助产测)。

音频路径冲突与静音判定

  • eSCO 与 A2DP 互斥:通话(eSCO)与音乐(A2DP)共用音频硬件,get_esco_coder_busy_flag() 用于检测编解码器占用。应用通过 call_flag 记录"因电话切回蓝牙模式",在挂断后依据 last_call_type 恢复音乐,避免路由错乱。
  • 静音退出判定:SBC_ZERO_TIME_MS = 500 表示连续 500ms 静音视为播放退出;SBC_FILTER_TIME_MS = 2000 用于过滤后台零星数据。这两个时间窗是功耗与响应速度的折中——太短会频繁开关解码器,太长则退出不及时。
  • 彩铃开关:BT_INBAND_RINGTONE 为 0 时播放本地铃声,为 1 时透传手机彩铃;若手机不支持 in-band ringtone,phone_con_sync_ring / phone_con_sync_num_ring 标志用于补偿同步逻辑。

并发与竞态防护

  • 轮询等待:emitter_bt_connect() 中用 while (hci_standard_connect_check() == 0x80) { os_time_dly(10); } 忙等 Profile 建链结束,并配合 get_curr_channel_state() 跳出条件,防止死循环。此类轮询发生在应用任务上下文,os_time_dly(10) 主动让出 CPU,不会阻塞系统调度。
  • 互斥锁:bt_emitter.c 定义了 static OS_MUTEX mutex,保护 inquiry_noname_list 等搜索结果的链表操作(generic/list.h 的 list_head),避免搜索回调线程与应用 UI 线程并发遍历。
  • 命令串行化:所有协议栈操作都经 user_send_cmd_prepare() 进入统一命令队列,由 btstack_task 串行消费,从根本上规避了多线程直接下发 HCI 命令的竞态。
  • 标志位重入保护:bt_search_busy 防止重复搜索;__this->enable 保护未使能时的误操作。

性能与运维考虑

  • Inquiry 时长与功耗:发射器搜索 20 × 1.28s ≈ 25.6s 是功耗敏感操作。产品化时建议结合 UI 提供"搜索中/已停止"提示,并通过 bt_emitter_stop_search_device() 让用户可随时中止。
  • 回连时序设计:POWERON_AUTO_CONN_TIME = 18 秒被设计为 6 的倍数,与协议栈超时粒度对齐,避免回连窗口与内部重传定时器错拍;TIMEOUT_CONN_TIME = 30 秒控制断线后的自动重连节奏。
  • 解码器开关频率:NO_SBC_TIME_MS = 100 判定无音频后尽快关闭解码器,配合 SBC_FILTER_TIME_MS 过滤,在"快速响应播放"与"降低静态功耗"之间取得平衡。
  • 产测支持:bredr_set_dut_enble(u8 en, u8 phone) 可进入蓝牙 DUT(Device Under Test)模式,配合 bredr_close_all_scan() 关闭扫描,用于产线射频指标测试。

扩展点

  1. 新增 Profile:main.c 已集成 A2DP/HFP 的事件回调框架;新增 SPP、PBAP 等 Profile 时,在 bt_event 分发处扩展分支,并把新状态字段按位域方式追加到 struct app_bt_opr。
  2. 发射器过滤策略:修改 SEARCH_LIMITED_MODE 可在"按地址/按名字/自定义/不过滤"间切换;自定义模式(SEARCH_CUSTOM_LIMITED)可在 Inquiry 结果回调中实现白名单/黑名单逻辑。
  3. 多连接策略定制:is_1t2_connection() / get_total_connect_dev() 的档位判断集中在 bt_wait_phone_connect_control(),若产品需要 1T3 或 1T1,只需修改该函数的连接数分支。
  4. 音量曲线:VOLUME_STEP / MIN_VOLUME_VALUE / MAX_VOLUME_VALUE / INIT_VOLUME_VALUE 集中在 main.c 顶部,便于按产品调音需求调整。
  5. 音频服务替换:解码结果通过 server_core.h 的音频服务器输出;若使用自定义音频后端,替换 earphone_a2dp_audio_codec_open() 等外部符号即可,无需改动协议栈。

Related Links

  • 示例源码目录:apps/common/example/bluetooth/edr/bt_music/
  • 主入口与状态机:main.c
  • 音频解码通路:bt_decode.c
  • 发射器实现:bt_emitter.c
  • 协议栈接口头文件:btstack/avctp_user.h、btstack/a2dp_media_codec.h、classic/hci_lmp.h、event/bt_event.h
  • 相关主题:BLE 低功耗蓝牙(见 apps/common/example/bluetooth/ble/ 下 GATT Client/Server 示例);RCSP 私有协议透传(见 apps/common/third_party_profile/jieli/rcsp/bt_trans_data/)
Next
BLE 低功耗蓝牙