
做 LE Audio 广播类产品这几年有一件事我感受特别深大家讨论最多的永远是 LC3 编码、BIS 同步这些偏底层的技术但真正让“广播音频”从一个概念变成可用体验的反而是协议栈上层那个不起眼的服务——BASSBroadcast Audio Scan Service广播音频扫描服务。没有它你身边那些没有屏幕、没有操作界面的耳机和助听器根本不知道附近有一条广播流存在更别说主动接入进去了。BASS 的核心作用简单说就是让“有能力交互的设备”去帮“没有能力交互的设备”完成广播音频的发现、选择、同步和状态管理。手机当遥控器耳机去执行这是 LE Audio 广播场景里最常见也最实用的形态。这篇文章基于我在实际项目里的协议梳理与开发经验把 BASS 的角色关系、GATT 服务结构、状态流转、握手细节和踩坑记录都摊开来讲适合蓝牙协议栈开发、音频固件开发、产品测试以及刚接触 LE Audio 生态的同行参考。不需要你有很深的蓝牙基础但至少要知道 GATT 特征、广播包这类基本概念。1. 定位复盘BASS 到底解决了广播音频里的什么问题1.1 广播音频的“发现-接入”链路传统蓝牙音频是连接式音视频分发手机跟耳机建立 ACL 连接再走 A2DP 把音频数据传过去。这套模式稳定但天然是一对一。LE Audio 引入的广播音频不一样它把音频打散成一个个等时数据包用 BISBroadcast Isochronous Stream广播等时流向周围撒任何支持 LE Audio 的接收器都可以在广播覆盖范围内独立收流。这就是一对多、多对多的共享音频基础。但广播音频最大也最隐蔽的问题在于它没有“握手”。广播源根本不知道谁在听也不关心接收器加入或离开。这时候会产生一个现实需求接收器得先知道“附近有没有想要的广播”“广播在哪一个物理信道上发”“数据包间隔和重传参数是什么”才能去做同步。放在传统蓝牙里这些信息可以通过连接交互一问一答拿到但在广播音频里没有这种对话通道。唯一的信息来源是广播包本身以及周期性广播同步信号接收器得靠扫描去解析这些内容。问题是很多接收器形态的尴尬由此暴露。耳机、助听器这类设备为了把功耗压到极低往往不会持续做全频段扫描同时它们没有屏幕也没有合适的交互入口用户不可能在上面完成“选择某一路广播并订阅”这种动作。于是协议栈上就需要一个中间层让手机/平板/手表这些有计算能力和交互界面的设备代替接收器去完成广播扫描辅助工作。BASS 就是为这件事定义的。1.2 为什么“广播接收器”自己无法完成这件事把问题再往下拆一层你会发现真正的瓶颈不在同步算法而在“谁来决定并下发同步意图”。接收器如果自己扫描能拿到广播源地址和同步信息但拿不到用户的收听意图。它不知道自己该收哪一路广播也不知道该同步哪几个 BIS 索引。这些决策信息对纯音频播放设备来说属于没必要常驻的附加功能。让一个只负责解码、播放的声学设备去管理“频道切换”的复杂逻辑无论从产品定义还是系统资源上都不划算。于是协议里做了一个很巧妙的分工接收器仍然保底保留基础扫描能力能自己发现并接入公开广播但是对于更复杂的覆盖、切换、多流订阅场景它可以把状态上抛给一个“广播助手Broadcast Assistant”。这个助手角色通常由手机承担它发现接收器身边的广播流并通过 BASS 服务把同步指令写进接收器。接收器收到指令后再自己完成对目标广播源的同步。也就是说BASS 把“决策”和“执行”彻底分离了。这种分离带来的直接好处是协议分层足够干净。接收器固件只要实现一个标准服务和配套状态机不需要为每种交互场景定制逻辑。而手机、平板、甚至带屏幕的遥控器因为它们天然有网络、显示和用户操作能力可以统一实现 BASS 客户端去操控任何支持该服务的接收设备。这样一来整套体验的上限由“屏幕设备”决定而不是由那个只带一两个按键的耳机决定。2. BASS 服务体系拆解角色、特征与状态机2.1 角色边界与实现载体BASS 本身没有定义“广播源”侧的协议行为它管的是接收器和助手之间的交互。协议里明确了几类角色顺着应用场景去理解会非常清楚。广播源Broadcast Source通过 BIG 建立广播等时流周期性广播同步信号和音频包的设备。一般是无屏的媒体网关、音频路由器、电视伴侣之类的设备。广播接收器Broadcast Receiver实际接收并解码 BIS 音频流的设备同时也是 BASS 服务端GATT Server。典型代表是耳机、助听器、音箱。广播助手Broadcast Assistant具备用户交互能力或网络能力的设备在同一个 LE Audio 场景里负责为接收器“安排”广播同步任务它是 BASS 客户端GATT Client。典型代表是手机 App、系统设置里的音频切换面板、专用遥控器。注意一个物理设备可以同时承担多个角色。手机既可以作为广播源把本地音乐广播给附近的耳机也可以作为普通接收器直接听广播可以作为 BASS 客户端去控制手表上的音频流。所以在实际实现里不能把角色跟设备型号绑定要按逻辑实例去划分。在手机充当广播助手的场景手机和耳机之间的连接通常是一条 LE ACL 连接BASS 就跑在这条连接上。但音频数据不在这条连接上传它依然通过耳机和广播源之间的 BIS 收到。所以你会看到一种“控制面归控制面数据面归数据面”的分流设计两者互不干扰。这种架构的价值在做低功耗产品时尤其突出接收器不需要长驻高速连接只要在需要交互时才跟 BASS 客户端通信。2.2 GATT 服务结构与三个核心特征BASS 服务在 GATT 层暴露了三个关键数据入口分别是广播接收状态、广播接收状态表以及已修改的广播接收状态向量。这三个特征各司其职理解了它们BASS 的使用方式就懂了一大半。广播接收状态特征用于告诉 BASS 客户端“接收器目前正在接收哪些广播”。它的数据结构是一组列表项每个列表项对应一条广播流包含广播源地址、地址类型、广播同步状态、PA 同步状态、BIS 同步状态、当前正在同步的 BIS 索引列表等信息。这个特征通常还要带 Notification 配置广播源状态发生切换时主动通知客户端省去客户端反复轮询的功耗开销。广播接收状态表特征是 BASS 客户端下达指令的入口。客户端需要在这个表里添加条目把希望接收器同步的广播源地址、期望同步的 BIS 索引或者将广播标识符解析后的元数据写进去。接收器收到这个写入请求后会解析条目内容并执行同步。不难理解这个特征需要支持 Write/Write Long而且写入的数据内容有严格的格式约束填错一个状态位都可能让同步中止。已修改的广播接收状态向量特征是一个精简的通知通道。接收器用它向客户端报告“状态表里哪些条目在上次读取后发生了变化”。它不携带完整状态数据只带标志位客户端收到后如果有需要再去主动读完整的广播接收状态特征。这种设计在广播源较多、列表项较长的场景里非常省流量也是做多接收器并行调试时的重要依据。此外GATT 层还要配合客户端特征配置描述符CCCD和服务器特征配置描述符SCPD的配置。很多初做 BASS 的同事会在这一步卡壳只实现了特征本身忘掉把通知描述符放开结果设备端状态无论怎么变化客户端始终收不到任何事件。这类坑会在第四章专门列出来。2.3 典型状态机与数据传输路径BASS 的交互流程不像点对点音频那样有复杂的连接握手它的状态流转集中围绕“同步意图”的写入与回报。以一个手机助手控制耳机接入广播的完整过程为例状态机会经过这么几步。第一步手机通过通用发现流程找到要控制的耳机并建立 LE 连接。第二步手机在连接上发现 BASS 服务通过读“广播接收状态”特征拿到耳机当前已接收的广播列表如果列表为空说明耳机处于空闲状态。第三步手机通过扫描周边环境发现了一个公开广播源并从广播包解析出同步信息。第四步手机向“广播接收状态表”写入一个新条目把这个广播源的地址、期望同步的 BIS 索引等参数塞进去。第五步耳机收到写入请求后启动与广播源的同步流程包括同步周期性广播同步信号再建立 BIG 同步拿到 BIS。第六步耳机完成同步后更新自身的“广播接收状态”特征并通过 Notification 告诉手机“状态变化了”。第七步手机收到通知后读取“已修改的广播接收状态向量”发现变化条目的索引再反查完整状态确认耳机已经进入了广播音频接收态。这个状态机的最关键一点是接收器自己不会主动发起“接收决策”一切同步行为的触发源都来自状态表里出现的新条目。反向地如果你想让耳机停止接收某一路广播也是通过删除或更新状态表对应条目来完成。可以说BASS 的整个状态机就是“客户端写出意图服务端执行并回报状态”的闭环。理解了这个闭环后面所有协议细节都能对号入座。3. 实操推演一套广播接收系统从配置到出声的完整过程3.1 广播源侧参数与 BIG 建立先看广播源这一侧。假设你有一台音频路由器想把一段音频通过 LE Audio 广播给周围耳机。设备首先要通过 HCI 指令创建一个 BIGBroadcast Isochronous Group并指定 BIG 事件间隔、重传次数、BIS 的映射、编解码配置、同步延迟等参数。这些参数会直接影响到接收端的同步开销和抗干扰能力实际选择时非常讲究。比如 BIG 事件间隔它决定广播源多久发送一组等时数据。间隔越小音频延迟越低但接收器要频繁唤醒收包功耗明显上升。间隔越大功耗越友好但接收器要做较长时间的定时同步才能跟上事件边界稍有漂移就会丢包。重传数也很关键广播环境里射频冲突不可避免BIS 默认支持网络级重传BIG Retransmission来降低丢包率。设置重传时别忘了它同样占用射频资源重传次数加得过高会拉长每个事件持续时间反而挤占了下一个广播事件的发射窗口。从广播包的结构看接收器可以发现一个主广播承载的指向信息拿到“广播音频”服务的数据然后根据辅助广播特征去同步周期性广播同步信号。周期性广播同步信号里包含了 BIG 广播的绝对时基映射接收器据此计算出第一个 BIS 事件在本地时间轴上的位置。实际开发里我们为了兼容低端芯片通常会把广播包里的 AdvHandle、SID、SyncInfo 整理成参数模板方便在 BASS 状态表写入之后直接交给底层同步服务去复用。否则每次都要临时解析广播包流程会变得既慢又容易出错。3.2 广播助手如何帮接收器接入 BIS现在重点推演 BASS 的实际搬砖过程。场景很简单某用户戴着一副耳机BASS 服务端手里拿着一台已经配对过的手机BASS 客户端附近有一台广播源正在循环输出公共广播音频。手机先与耳机建立 LE 连接。这个连接可以走普通 LE 流程关键是连接参数不能太激进。因为后续要在这个连接上做状态表和通知的读写如果连接间隔太长手机发一条 BASS 指令到耳机要等一个半连接事件才能收到响应体验会非常迟钝。我们通常建议把连接间隔控制在 30ms 到 50ms 之间既不过度占用射频又能保证交互时延可控。连接建立后手机做 GATT 发现找到 BASS 服务的三个特征。接着读“广播接收状态”特征确认当前耳机没有接收任何广播。然后手机开始扫描附近的广播源。这一步并不是 BASS 协议强制规定的但在产品逻辑上缺不了因为手机总得先知道附近有哪些流可选。看到目标广播后手机解析广播包拿到广播源地址、同步信息也拿到该广播包含的 BIS 数量和每个 BIS 的配置。之后手机把目标广播源的信息整理成一个状态表条目写入耳机的“广播接收状态表”特征。耳机收到这条写入后会先解析状态的地址类型和地址字段如果地址是公开广播能够直接解析的就尝试与该广播源的周期性广播同步信号做同步。同步成功后耳机继续按 BIG 信息建立等时同步把指定的 BIS 拉起来开始解码音频。最后耳机更新本地“广播接收状态”特征并通过 Notification 推送给手机手机端界面从“未接收”变成“正在广播收听中”。这里有个容易被忽略的次序问题手机必须在写入状态表前确保自己解析到了正确的同步信息尤其是 BIS 索引列表。如果 BASS 客户端只把广播源地址写进去而没写 BIS 索引接收器不知道自己该拉哪些流可能只同步了一个广播事件却不送音频给解码器。我们项目里因此走了一段时间的弯路后来统一要求所有通过 BASS 接入的广播都必须显式带出 BIS 索引集合就算只带一个索引也要写明确。3.3 公共广播与私有广播的差异处理广播音频有两种形态一种是完全公开的比如商场广播或校园广播接收器通过扫描就可以直接接入不需要任何凭证。另一种是私有广播广播源会对音频流做加密接收器必须拿到广播码或密钥才能解密并同步。BASS 在处理这两种广播时侧重点完全不同。公开广播场景BASS 客户端的职责基本就是“发现、选项、写入、回报”整个过程没有秘密信息交换安全性压力很小。手机只要能解析广播包里的同步参数就能指挥任何一台支持 BASS 的耳机接入。这种场景对建立 BIG 时的重传参数、信源同步稳定性要求高但协议逻辑最简单。私有广播场景难点转移到密钥传输。广播码不能直接放在普通广播包里被任何人读取必须通过已经建立的安全连接由一个可信的 BASS 客户端下发给接收器。BASS 状态表条目里要携带广播码信息接收器拿着这个码去做解密同步。这个过程要求手机和耳机之间的 LE 连接加密且经过配对认证否则广播码存在被中间人截获的风险。不少产品把“允许手机控制耳机接入私有广播”功能做成安全敏感操作接入前强制弹窗让用户确认道理就在这。公共广播和私有广播还存在一个混合形态同一路 BIS 流部分流公开部分流加密。例如某个混合广播把伴奏作为公开 BIS把主唱人声作为私有 BIS听者可以选择只收伴奏或者同时订阅两个流。BASS 状态表要为每一条 BIS 分别标记是否需要解密信息这也意味着接收器内部要对同一个 BIG 事件里的不同 BIS 做差异化处理。这种场景在实现芯片驱动时最容易出问题建议先在 PC 上做原型验证熟悉了混合订阅的状态流转之后再移到嵌入式平台。4. 开发排雷BASS 实践经验、常见问题与测量手段4.1 实现踩坑实录做 BASS 实现我踩过最深刻的一个坑是“特征写了但没彻底实现属性”。GATT 层上广播接收状态表如果要支持客户端写入特征属性里必须包含 Write 或 Write Long。部分协议栈封装库里默认只开放了 Read 和 Notify导致手机端明明看到了这个特征一写入就返回“请求不支持”。排查这类问题最简单的方法是先抓取 GATT 层的 ATT Write 响应的错误码如果返回 0x06Attribute Not Found或者 0x80Application Error先别怀疑手机端代码回头检查服务端固件的特征配置矩阵。第二个坑出现在长特征写入上。BASS 状态表在管理多路广播、多 BIS 的情况下单条 Attribute Value 可能超过默认 MTU 大小。GATT 客户端需要执行 Prepare Write 和 Execute Write 的“长写”流程服务端则必须支持偏移写入并按顺序缓存分段数据。很多实现把 MTU 协商到了 247 或更高就以为不会触发长写但实际中广播源数量一多单条目照样会撑爆一个 ATT 包。正确姿势是同时处理“短写”和“长写”两条路径在协议栈回调里对长写做重新组装并在组装完成后触发状态表更新逻辑。第三个坑和通知有关。已修改的广播接收状态向量以及广播接收状态本身的 Notification都需要客户端先在 CCCD 里把通知开关打开。如果手机端没有写 0x0100 到描述符无论服务端怎么变化手机都收不到任何事件。这个坑在联调阶段特别阴险因为特征读出来是正常的命令写下去也是成功的但界面始终不刷新。排查方法很简单找一台逻辑分析仪抓 LE 连接上的 ATT Handle Value Notification如果抓不到包就先看 CCCD 状态。第四个坑集中在并发同步能力。有些低端耳机的蓝牙芯片同时只能维护一个 BIG 同步如果状态表里被写入了两个或更多广播流条目固件要么直接拒绝后续条目要么在切换时出现数秒静默。产品设计上一定要在实现前明确“最大同步流数”并在 BASS 状态表写入前做校验接收器对自己的能力边界要有硬性回合而不是默默失败。第五个坑和时间同步有关。BASS 客户端写入同步参数后接收器立刻去同步周期性广播同步信号。但广播源的事件时钟和接收器本地时钟本来就是异步的如果接收器底层没有做好绝对参考时基换算就会出现看似同步成功但听不到声音的问题。调试这种场景时最直观的办法是打印 HCI 层同步事件里的“同步偏移量”以及每次同步建立的时间戳对比接收器侧维护的“广播事件本地计数”有没有跳变。一旦发现大量跳变基本可以断定是底层时钟漂移补偿没有接上。4.2 常见问题速查表聊了这么多把我在联调里遇到的典型问题整理成了一张速查表按“现象-可能原因-处置方式”的格式列出来方便你遇到同类问题时直接对照。现象可能原因处置方式手机发现不了 BASS 服务固件没有在广播/连接后公开服务GATT 数据库未包含 BASS检查服务 UUID 注册确认特征属性有 Read/Write/Notify写入广播接收状态表返回失败特征不支持 Write LongMTU 协商值过小状态表条目格式错位抓取 ATT 错误码适配长写流程按规范字段逐位校验状态表写入成功但没有音频BIS 索引集合没写全同步参数解析错误广播源加密但未传广播码核对 BIS 索引确认广播码字段打印 HCI 同步事件状态码耳机已经出声手机端界面未更新广播接收状态特征的通知开关没打开服务端状态变化后没有发送 Notification确认 CCCD 写 0x0100检查服务端是否调用状态通知接口多广播源切换时延迟大接收器需要先断开当前同步再建立新同步提前释放旧 BIG 同步优化状态表写入顺序私有广播始终无法解密广播码下发时机不对连接未加密就尝试同步确认连接已通过配对和加密广播码在状态表中按正确字段放置这张表只能覆盖高频问题实际项目里还会有芯片底层的奇偶性问题但大方向都离不开特征属性、写流程、状态通知、同步参数和加密处理这五个维度。排查的时候优先抓 HCI 层事件比在应用层猜来猜去高效得多。4.3 可复用的验证手段与调试建议BASS 的验证不能只靠“听响不响”。推荐一套我一直在用的验证路径从功能覆盖到异常注入能有效减少现场回归问题。第一步准备两台支持 LE Audio 的接收器设备一台作为纯接收器一台同时开启 BASS 服务端和广播源能力让接收器能同时扮演“控制端”与“被控端”覆盖角色切换。第二步用手机端抓包工具观察 GATT 发现、状态表写入、Notification 上报这三段关键行为确认时序符合协议预期。第三步做异常输入测试比如清空状态表后让耳机进入广播接收状态再随机删除状态表条目看设备是否还能正常回到空闲态或报出正确错误码。抓包是必须掌握的技能。我习惯用支持蓝牙 HCI 日志的手机做全量抓取再把日志导入到抓包分析工具里解析 ATT、L2CAP 层面的交互。分析时重点看三条命令序列GATT DiscoverATT Write Request/ResponseATT Handle Value Notification。如果这三条序列在日志里都完整出现且顺序正确BASS 基本就通了。如果发现在 Write 之后一直没有 Notification多半卡在服务端状态机的同步流程内部这时候需要回到固件侧打印同步日志。最后给个很实用的小建议在实际调试过程中不要直接在“广播接收状态”特征里同时塞过多广播流。规范虽然允许列表项比较多但每增加一个条目接收器内部就要多维护一条同步上下文内存和 CPU 开销非线性上升。产品功能上宁可通过“列表覆盖”的方式管理也就是客户端在写入新广播源前先删除不再需要的旧条目再添加新条目。这样做既符合接收器侧能力模型也能显著降低并发同步导致的时钟漂移概率。我在实际项目里体会到BASS 的复杂不在于它的协议定义有多长而在于它把广播音频的“决策面”和“执行面”切开之后两端都需要严格对齐状态认知。作为广播助手那一端你得确保 BASS 客户端写入的参数足够准作为接收器那一端你得确保状态变化能及时同步并上报。很多“听不到声音”“切换卡顿”的问题追到最后往往不是编解码问题而是状态表维护和 Notification 时序没有做好。把这套机制吃透再去设计共享音频、助听辅听、公共广播类的产品你会发现原本看起来最麻烦的“一拖多”音频接入问题其实早就有了一条清晰可靠的解决路径。