ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PBP协议解析:LE Audio公共广播的发现机制与公告级别优化

PBP协议解析:LE Audio公共广播的发现机制与公告级别优化

1. 先搞清楚一件事:广播音频流凭什么被发现

1.1 音频通道与发现通道是两条路

如果你抓过 LE Audio 广播场景的空中包,应该会发现一个很有意思的现象:真正承载声音的 BIS(Broadcast Isochronous Stream,广播等时流)和用来让别人找到你的周期广播(Periodic Advertising,PA)是两条完全独立的链路。BIS 干的是"运输音频"的活,LC3 编码后的数据按固定的等时间隔(ISO Interval)从源端发出来;而 PA 干的是"宣告存在"的活,它周期性地告诉周围设备:这里有一个广播,它的参数是什么。

接收端要做的第一步不是直接听音频,而是先在广播信道里扫描到 PA 事件。PA 事件里除了带一段公告数据,还携带 BIGInfo。BIGInfo 才是真正的"同步钥匙",里面包含 BIS 索引、PHY、信道映射、加密标志和时序偏移。拿生活中的场景类比,BIS 是已经铺好的高速公路,PA 就是路牌和地图:接收端必须先看懂路牌,才知道要去哪条车道、在哪个时隙接车。

这里有个容易混淆的点:LE Audio 广播用的是广播扩展(Advertising Extensions)机制,而不是老的经典广播。设备先在主广播信道(37/38/39)上收到 ADV_EXT_IND,再去辅助信道接收 AUX_SYNC_IND,随后才能进入周期广播的"锁定"状态。很多新接触 LE Audio 的工程师会把主信道上的普通广播当成完整公告,结果抓包时看到一堆零散广播包,却始终找不到 BIGInfo,就是这个链路没理清。

1.2 没有统一公告协议时的四种乱象

在 PBP 出现之前,一个广播源如果想让普适的接收端发现自己,基本只能靠各家私有方案,工程上会碰到四类很现实的问题。

第一是发现时延不可控。广播源可以自己选 PA 间隔,有的栈默认 400ms,有的为了省电放到 1s 以上,而接收端扫描占空比也是各自为政。两者叠加后,最坏情况是从用户进入覆盖区到接收端锁上广播要好几秒。在公共广播场景下,这个时间意味着用户看着手机屏幕干等,体验非常糟糕。

第二是功耗双向浪费。想让别人快点发现自己,广播源就得把 PA 间隔压短,射频发射占空比直线上升;接收端为了快一点发现别人,就得拉长扫描窗口甚至连续扫描。双方都没有一个共同的"速度档位"可以协商,只能互相过度配置。

第三是公告内容不统一。有的把频道名塞在厂商自定义数据结构里,有的把语言信息放在服务数据后段,接收端应用要解析就得写一堆针对性的分支代码,而且一旦广播源换了协议栈版本就解析失败。最终的后果是:一个通用的广播接收 App 无法做到"列出附近所有公共广播频道"这种最基本的功能。

第四是高密度场景没有过滤依据。场馆里同时有几十路广播时,接收端必须把所有 PA 都解析一遍,再把结果全部抛给 UI。没有稳定的短标识(比如 16bit 的广播 ID)做分组和过滤,用户看到的就是一大串以 MAC 地址结尾的条目,既难认也难选。

PBP(Public Broadcast Profile)就是冲着这四类问题来的。它把"公告内容长什么样""公告频率有多快""接收端扫描行为应该满足什么下限"这些散落的问题,统一成一份可对照、可验收的协议要求。

2. PBP 到底管什么:协议边界与角色模型

2.1 PBP 只标准化"公告与发现"这一小段

做协议实现的人最怕一句话带过的 Profile,因为搞不清边界就会把不该自己管的事也揽进来。PBP 的全称是 Public Broadcast Profile,它不定义编解码器,LC3 是独立编解码规范;它不定义音频数据的组包方式,那是 ISOAL 和 BAP 的范围;它更不涉及单播连接里的音频流建立流程,那是 ASCS/PACS 的职责。

PBP 真正关心的就两件事:第一,公共广播源应该用什么格式、多高的频率对外发公告;第二,支持 PBP 的接收端应当以什么样的扫描行为去发现这些公告,从而得到一个有上限的发现时延。你可以把它理解为:给广播音频的"广告牌"统一了尺寸、内容和亮度等级,但不负责广告牌后面那家店卖什么商品。

这个边界设计是有意为之。LE Audio 协议族采用分层思路,BAP 解决"流能不能通",PBP 解决"公共流能不能被高效找到",两者解耦后,厂商可以只实现 BAP 的私有广播,也可以两者都实现。实际开发中,判断一个需求该不该放在 PBP 里做,就看它是否影响"发现效率"这四个字。

2.2 公共广播 vs 私有广播:PBP 管哪一半

PBP 名字里的 Public 不是随便加的。LE Audio 广播分两类:一类是不加密的公共广播,任何实现了对应接收能力的设备都可以直接同步收听,典型场景就是场馆助听、车站广播这类开放音频;另一类是加密的私有广播,需要广播码(Broadcast Code)才能解开 BIS 负载,典型场景是会议室投屏音频、需要访问控制的付费内容。

PBP 明确只管前者。公共广播的最大特点是"发现即得":接收端不需要配对、不需要扫码、不需要任何带外信息交换,只要扫到公告就能同步、出声。这既是 PBP 的产品价值,也是它的协议约束来源——因为目标受众是任意设备,公告格式和级别就必须标准化,不能指望每个厂商都提前约定。

私有广播的发现和密钥分享走的是另一套机制(广播码通常通过 BASS 的 Set Broadcast Code 写入,或者通过其他带外通道传递),PBP 只是负责把"这是私有广播、没有广播码就别试了"这类信息在公告里表达清楚,避免接收端盲目尝试同步浪费时间。

2.3 Source / Sink / Controller 三种角色

PBP 定义了三种角色,实际设备通常身兼数职。

Public Broadcast Source(广播源)负责配置 BIS、启动 PA、把 BAA 公告放进周期广播数据里,并对外承诺某个公告级别。Public Broadcast Sink(接收端)负责扫描 PA、解析公告、按需同步 BIS、解码 LC3,最后把声音交给上层播放。Public Broadcast Controller(控制器)则是一个管理角色,典型形态是手机:它自己不一定听音频,但它通过 BASS 服务去控制旁边的耳机或助听设备,告诉对方"去扫这个广播源、去同步那个频道",同时读回接收状态。

把 Controller 单独拎出来,是因为"发现决策"和"音频渲染"在真实产品里经常不在同一个设备上。耳机功耗预算小、没有屏幕,不适合承担繁重的扫描和 UI 逻辑;手机算力强、有界面,适合做发现决策。这种角色拆分直接决定了 BASS 在 PBP 里的地位,后面拆时序的时候会再回到这一点。

3. 公告级别(Announcement Level)——发现效率的主旋钮

3.1 PBAL 字段与三档语义

PBP 最核心的机制是公告级别(Announcement Level)。广播源在公告数据里放一个单字节字段,表示自己当前采用哪一档公告强度。这个字段对应的是 Public Broadcast Announcement Level,我印象里特性 UUID 是 0x2BDB,具体编码可以在蓝牙标准组织维护的官方 UUID 对照表里查到。

级别通常分三档:低(Low)、中(Medium)、高(High)。语义上,低级别意味着"可以被发现,但对发现速度不做强承诺",高级别意味着"接收端应当在极短时间内就能锁上我"。不同协议栈对枚举值的具体编码习惯不太一样,有的用 0x00/0x01/0x02 表示低/中/高,有的用 0x01/0x02/0x03,还保留 0x00 表示"未指定"。拿到 SDK 后第一件事就是把 PBAL 的枚举映射确认清楚,否则后面写配置工具时会对着数值发呆。

为了说清楚三档的量级差别,我整理了一张参考表。需要特别说明:规范对每一档的精确上限要求以官方 PBP 文档为准,下表数值是我在工程实践中看到的主流实现常用参考值,用来理解数量级和做验收预判是够用的。

公告级别典型语义PA 间隔参考上限适用场景
Level 1 低可发现即可,时延不敏感约 1s常驻场馆、电池供电的长时间广播
Level 2 中几秒内有结果即可约 250ms大多数公共广播的默认档
Level 3 高尽快锁定,越快越好约 100ms 或更短临时广播、助听场景、需要快速重连的场景

3.2 级别为什么能换算成"最坏发现时延"

理解公告级别的关键,在于意识到它本质上是一个"最坏情况契约"。接收端不知道广播源确切的 PA 间隔是多少,它只需要知道对方承诺的级别所对应的间隔上限,就能规划自己的扫描策略。

举个简单的例子:如果广播源承诺 Level 3,PA 间隔上限约 100ms,那么接收端只要保证自己在一个 PA 间隔内至少有一次有效的扫描窗口能与 PA 事件重叠,最坏情况下最多等一个多周期就能收到公告。反过来,如果广播源只有 Level 1,PA 间隔可能到 1s,接收端还按 100ms 的预算去设计扫描占空比,就会白白浪费功耗;如果接收端扫描太稀疏,又可能连 1s 一次的公告都错过。

所以 PBP 的"优化发现效率"并不是简单地把 PA 间隔压到最小,而是给双方一个可计算、可对齐的档位。这跟网络里的 QoS 类似:与其约定一个实际上做不到的固定值,不如约定一个上限和一个等级语义,让两端在功耗与速度之间找到适合自己的平衡点。

另外要记住一个单位细节:周期广播间隔的单位是 1.25ms,所以 100ms 的 PA 间隔在协议字段里写作 80。很多同学第一次对着协议栈参数调 Level 3 时,把 80 当成了 80ms,结果发现广播源实际公告间隔比预期慢得多。

3.3 不同场景的档位选择建议

级别选择本质上是"射频预算换发现速度"。

如果广播源是插电设备,比如场馆里的信息屏、会议室的共享音频盒子,功耗压力不大,直接用 Level 3 换最好的用户体验即可。如果广播源是电池设备,比如临时导游机、便携翻译器,就要算一下射频开销:PA 间隔从 250ms 缩到 100ms,发射占空比翻了不止一倍,续航会肉眼可见地下降,这时候 Level 2 是更稳妥的默认档。

还有一种工程上很实用的做法是"分级启动":设备进入待广播状态时先用 Level 3 高频公告一段时间,让附近的接收端快速发现并完成同步,等确认至少有一个接收端锁上之后,再自动降到 Level 2 甚至 Level 1 维持存在。很多产品把这种模式包装成"配对模式"或"发现模式",PBP 协议上完全支持,因为它要求的是"上限"而不是"固定值",源设备可以在同一级别约束内动态调整实际间隔。

我个人的建议是:把公告级别做成运行时可配置项,不要写死在固件里。上线前验收测试时用 Level 3,日常运行用 Level 2,既能保证认证测试一次过,又能兼顾无线卫生。

4. BAA 公告数据结构与 Broadcast ID 的过滤价值

4.1 BAA 的 AD 结构与字段说明

PBP 定义的公告叫 Broadcast Audio Announcement(BAA),它放在周期广播事件的 AD Structure 里。从协议实现角度看,BAA 对应的服务叫 Broadcast Audio Announcement Service(BAAS),服务 UUID 是 0x185D。有意思的是,这个服务在多数实现里是"虚拟服务",主要存在于广播数据中,不一定在 GATT 数据库里开一个真正可连接的 Server;如果广播源本身是可连接的,规范也允许通过 GATT 暴露相同服务,让管理端主动读取公告级别。

BAA 的数据结构非常紧凑,主要包含三部分:

字段长度说明
Service UUID(BAAS)2 字节固定 0x185D
Public Broadcast ID2 字节广播源分配的短标识,用于分组与过滤
PBAL(公告级别)1 字节当前公告档位

用 16 位 UUID 的 Service Data 类型(AD Type 0x16)表达,一个完整的服务数据段看起来是这样:

06 16 5D 18 12 34 02

其中 0x06 是长度,0x16 是 Service Data(16 位 UUID)类型,0x5D18 是 BAAS 的 UUID(小端),0x1234 是 Public Broadcast ID,0x02 是 PBAL 公告级别。实际抓包里这段数据前面通常还会跟着 Flags、完整设备名等常规 AD 结构,但核心就是这 7 个字节。

从抓包工具里看,Wireshark 或蓝牙分析仪一般会把这段解析成"Service Data - 0x185D",并尝试把后面的字段按 PBP 语义解出来。如果解析结果里看不到 Broadcast ID 或 PBAL,而只有一串 Raw Data,多半是广播源的协议栈没有按标准放字段,或者字节序处理错了。

4.2 Public Broadcast ID:16bit 怎么用才不浪费

Public Broadcast ID 设计成 16bit 是有讲究的。它不适合当作全局唯一标识,因为空间太小,同一场馆里很容易撞 ID;它的价值在于"分组"和"过滤"。

举个例子,一个商场里可能有 5 个不同楼层的广播源,它们同属一个运营方,约定都用同一个 Broadcast ID 0x1234,接收端解析到公告后就可以按 ID 把这些频道归为一组,UI 上显示成"商场广播"而不是五条孤立的 MAC 地址。又比如一个博物馆里同时有中英文两路导览,两路可以用两个不同 ID 区分,用户选择语言时,接收端只需要过滤对应 ID,不用把另外几路的 BIS 全部同步一遍。

工程上两个容易忽略的点:第一,Broadcast ID 一定要跟源设备地址联合使用。协议上真正唯一的标识是(源 BD_ADDR + Broadcast ID)组合,单纯按 ID 过滤会撞车。两个互不相关的广播源碰巧选了同一个 ID 时,接收端靠 ID 过滤会把错误频道推给用户。我处理过的方案是 App 端用 ID 分组展示、用地址做最终判定,两套信息都保留。第二,场馆运营方最好做一次 ID 规划,把同一区域的广播源分到固定 ID 段里,避免现场广播源各自随机选 ID 导致一片混乱。

4.3 BASS 协同:让接收端之外的人做发现决策

公告级别的效率优化如果没有 Controller 角色配合,发挥空间会打折扣,因为很多接收端(耳机、助听器)根本没有 UI,发现了广播也不知道该不该连。PBP 在这个场景下依赖 Broadcast Audio Scan Service(BASS,服务 UUID 0x185F)。

接收端实现 BASS 后,手机作为 GATT Client 连接它,通过 Broadcast Audio Scan Control Point 写入"添加广播源"指令,把目标源的地址和 Broadcast ID 告诉耳机;耳机随后按指令去扫描和同步,并把结果通过 Broadcast Receive State 特性回传给手机。对公共广播来说整个过程不需要广播码,Controller 只需要知道"哪个源、哪个 ID"。这比耳机自己盲扫然后在两个耳塞间同步状态要省电得多,也让"发现"这个动作从音频渲染链路里彻底解耦出来。

5. 从 PA 到 BIS:一次完整的 PBP 发现时序

5.1 源端启动广播的四步动作

站在广播源视角,一次规范的 PBP 广播启动要依次做四件事:先定音频参数,再建 BIG,再配 PA,最后把公告挂上去。这里的顺序不是随便排的,PA 必须先于 BIG 启动,因为接收端需要先通过 PA 获知 BIG 的存在,BIG 才能被找到。

第一步,确定 LC3 编码参数。公共广播常用的配置是 48kHz 采样率、10ms 帧长,单声道码率约 64~128kbps,可以根据音质目标和射频余量调整。第二步,创建广播等时组(BIG),每个频道对应一个 BIS。ISO 间隔一般取与帧长一致的 10ms,这样编码器每出一个帧,BIS 事件正好发一次,时延和缓冲都最好算。第三步,配置周期广播。PA 间隔按公告级别设置,比如 Level 3 用 100ms,PHY 用 1M 还是 Coded 根据覆盖距离来定,Coded PHY 能传更远但每次发射时间更长。第四步,把 BAA 服务数据填进周期广播的 AD 段,然后启动 PA,再启动 BIG,BIGInfo 就会跟着 PA 事件周期性发出来。

很多商业化协议栈把这几步封装成一个"Start Broadcast"接口,参数一填就完事。但从调试角度,我强烈建议把 PA 启动和 BIG 启动做成两个可观察的独立阶段,至少在日志里能看到各自的开始时间,不然后面排查"抓包只有 BIG 没有 PA"这类问题时,根本不知道是哪个环节挂了。

5.2 接收端逐步锁定广播的过程

接收端的发现过程可以用一条状态链概括:初始扫描 -> PA 同步 -> 公告解析 -> BIGInfo 获取 -> BIS 同步 -> 出声。

初始扫描阶段,接收端在主广播信道扫描,扫描窗口建议覆盖到至少一个完整 PA 周期。这个阶段是功耗大头,也是级别发挥作用的地方:如果接收端知道目标源承诺 Level 3,可以把扫描窗口设置得比 Level 1 场景更激进。扫到 ADV_EXT_IND 后,接收端转向辅助信道,捕获 AUX_SYNC_IND,这时才算真正进入"PA 同步"状态。

PA 同步后,接收端在 PA 事件里解析 BAA:拿到 Broadcast ID 和 PBAL,在应用层决定这个频道要不要继续。这一步是关键的分流点,不感兴趣的公告在这里就被丢弃,不会触发后续任何 BIS 同步动作,功耗和 CPU 开销都省在这里。决定要继续后,接收端等待下一个 PA 事件里的 BIGInfo,解析出 BIS 索引、PHY、信道映射、加密标志,然后在下一个 BIS 事件时把射频切到对应信道,接收等时数据包,通过 ISOAL 重组还原成完整的 LC3 帧,送入解码器,再经过一段去抖缓冲后出声。

整个过程的端到端耗时可以粗算:初始扫描检测约一个扫描周期,PA 同步约一两个 PA 间隔,BIGInfo 和第一个 BIS 事件最多再等两个周期,最后还有接收端的渲染缓冲。按 Level 3 配置(PA 间隔 100ms、扫描窗口 60ms 左右),从接收端上电到耳朵里听到声音,实测通常在 300~800ms 这个量级。如果超过这个量级很多,优先怀疑扫描占空比设置和去抖缓冲。

5.3 一组可用来验收的示例参数

协议开发最怕的就是没有一组可复现的基准参数。我这里给出一组我在测试环境里常用的示例值,可以直接拿来当验收基线:

参数示例值说明
LC3 帧长10ms48kHz 采样下的标准帧长
ISO 间隔10ms协议字段按 1.25ms 单位编码,写 8
PA 间隔(Level 3)100ms协议字段写 80
PA 间隔(Level 2)250ms协议字段写 200
接收端扫描窗口60ms需覆盖一个 PA 周期
PA 同步超时2~10s连续收不到 PA 事件就判定失步
去抖缓冲20~50ms吸收射频抖动和突发干扰

最后再强调一次:发现效率不等于音频时延。接收端锁上广播之后,音频时延主要由 ISO 间隔和去抖缓冲决定,和 PA 间隔没有关系。也就是说,把公告级别从 Level 2 调到 Level 3,只会让"发现变快",不会让"已有连接的声音变快",别拿它去优化音频延迟。

6. 落地测试中的五个典型坑与对策

6.1 公告级别的工程权衡:快不是免费的

第一类坑是把公告级别当成一个可以无限调大的参数。Level 3 确实发现快,但代价是射频发射占空比上升。在一个多源密集的环境里,十几个 Level 3 广播源同时高频发公告,2.4GHz 频段的信道占用会明显恶化,甚至影响同区域 Wi-Fi。

我的对策是区分"发现期"和"稳定期"。发现期用 Level 3,接收端锁上后广播源降级到 Level 2 甚至 Level 1。这个切换逻辑最好放在广播源端,因为它掌握全局状态;接收端不需要知道源什么时候降级,它按之前的级别规划扫描即可。

6.2 高密度环境下的过滤与信道卫生

第二类坑是忽略多源场景的过滤效率。PBP 标准给了 Broadcast ID,但如果接收端把每个 ID 都当成"要同步的频道",资源仍然会浪费。正确的做法是:在 BAA 解析完成后、BIS 同步之前,由应用层决定过滤名单;固件只负责把过滤命中率高的广播保留下来。

大场馆部署上,我也建议运营方把广播源的发射功率做一次统一约束。公告覆盖范围应该与音频可收听范围一致,而不是盲目开满功率。最典型的故障是:PA 用低功率发射、BIS 用高功率发射,接收端站在中间地带"听得到音频却永远发现不了广播",这在抓包里表现得很诡异。

6.3 用协议分析仪验证的检查点

第三类坑是"看起来在广播,实际没按 PBP 广播"。用蓝牙协议分析仪抓包时,我建议至少核对五个检查点:BAA 服务数据是否正确出现在 AUX_SYNC_IND 里;Broadcast ID 和 PBAL 字段是否按预期编码;PA 间隔是否稳定且不超过对应级别上限;BIGInfo 是否随 PA 周期性出现,BIS 索引和 PHY 是否匹配;加密标志是否为"未加密"。

公共广播最隐蔽的问题是加密位不一致:广播源在 BIGInfo 里标了加密,但在 BAA 里没有同步说明,接收端发现后尝试同步失败,表现就是 UI 上能看到频道,但永远连不上。这类问题不抓包很难定位,因为连接阶段的失败日志在各家栈里五花八门。

6.4 协议栈实现差异与固件版本的坑

第四类坑是不同协议栈对 PBP 的支持深度参差不齐。有些芯片宣称支持 LE Audio 广播,但固件只实现了 BAP 层面的 BIS 收发,BAA 公告结构要么没有、要么字段顺序不符合标准。碰到这种情况,不能靠猜,必须用协议分析仪抓包验证,以抓包结果为准,而不是以 SDK 文档的"支持 PBP"字样为准。

第五类坑是 BASS 指令兼容性。Controller 通过 BASCP 添加广播源时,不同实现支持的指令参数格式有差异,比如源地址携带方式、Broadcast ID 的字节序。我的经验是先拿一个已认证的参考接收端做互操作测试,确认 Controller 端的指令封装没问题后,再接入自家接收端,否则两边同时排查会非常耗时。

7. PBP 与相邻 Profile 的协作边界

7.1 与 BAP:PBP 是上层"公告层"

BAP(Basic Audio Profile)定义了广播音频的角色、数据通路和同步基础,PBP 则在这些基础之上增加了一套面向公共发现的公告与行为约束。你可以把 BAP 理解成"管道规范",PBP 是"管道上贴的标签规范"。实现上,PBP 源一定实现了 BAP 的广播角色,但反过来不成立——只实现 BAP 的广播源可以通过带外方式(比如扫码)让接收端获得同步参数,只是没法被通用设备自动发现。

7.2 与 BASS:被管理接收端的必经之路

PBP 没有重新发明一套远程发现机制,而是明确复用 BASS。接收端如果要支持 Controller 管理,就必须实现 BASS 服务;Controller 则作为 GATT Client 使用它。这两个 Profile 的分工是:BASS 管"指令通道",PBP 管"公告语义"。手机上看到"附近公共广播"列表的体验,是 PBP 的公告让列表内容变得可解析、可分类,是 BASS 让耳机真正按列表去执行同步。

7.3 PBP 明确不负责的事

最后把"不归 PBP 管"的清单列一下,方便你排查问题时少走弯路。私有广播的密钥交换与广播码管理,归 BAP 私密广播和带外机制管。单播场景的音频流建立与编解码协商,归 ASCS/PACS 管。左右耳同步成一套立体声设备,归 CSIP 管。助听器特有的精细化调参,归 HAS 管。音量同步和媒体控制链路,则在单播场景里归对应的媒体控制 Profile 管。

把这些边界记住,遇到"PBP 怎么还管不了这个"的疑问时,先翻一下问题到底出在哪一层,再去对应规范里找答案,比在 PBP 文档里反复找不存在的内容高效得多。

最后再说一个实测里很有用的习惯:把接收端从"扫描开始"到"解码器出第一帧"的时间戳打出来,写成一个自动化打点脚本,每轮改完参数跑一遍。我有几次调 PBP 参数,靠抓包看一切正常,就是用户感知"入耳慢",最后都是靠这个端到端时间戳脚本定位到扫描占空比和去抖缓冲的问题。验收时用 Level 3、日常运行用 Level 2、上线前用多源并发场景压一遍,这套组合拳打下来,广播音频的发现效率问题基本就不会再找上门了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进