一句话:BLE 是"我主动喊,你主动听";经典蓝牙是"你喊 inquiry,我应答"——名字一个要自己打包,一个 controller 代劳,设计差异背后是两种完全不同的产品哲学。
面试或者聊天时问一句:"经典蓝牙的广播是怎么工作的?"十个做嵌入式的有八个会下意识套用 BLE 的模型:设备持续往外发广播包,扫描方收。
这个模型对经典蓝牙是错的。经典蓝牙压根没有"主动广播"这个接口,它的"可被发现"是被动应答式的。而且顺着这条差异往下挖,能挖出一串反直觉的事实——比如 Zephyr 协议栈里,经典蓝牙的名字是 controller 自动填进应答包的,host 一行代码都不用管;BLE 的名字却要应用层自己一个字节一个字节地打包。
这篇文章基于 Zephyr v3.3.0 源码(subsys/bluetooth/host/adv.c和classic/br.c),把两套机制摆在一起对一遍。
一、机制本质:主动喊 vs 被动应答
先把两边的骨架摆出来:
| 维度 | BLE 广播 | 经典蓝牙"广播"(Inquiry) |
|---|---|---|
| 链路 | LE 广播物理信道(37/38/39),单向广播 | BR/EDR Inquiry Scan 信道,一问一答 |
| 谁发起 | 广播方主动持续发 ADV_IND | 发现方主动发 Inquiry,被发现方应答 |
| 数据载体 | AdvData ≤31 字节 + ScanRsp ≤31 字节 | EIR 最多 240 字节,随应答直接携带 |
| 名字怎么拿 | 放 ad 随广播直达;放 sd 则扫描方发 SCAN_REQ 二次请求才回 | EIR 直接带;没带则发现方单独补问 |
记成一句:BLE 是"我主动喊,你主动听";经典蓝牙是"你喊 inquiry,我应答",名字直接随应答带上。
BLE 里那个"扫描方要二次请求才有 ScanRsp"的设计,很多人以为经典蓝牙也有对应物。确实有对应的角色——EIR——但方向相反:EIR 是应答时直接携带的,不需要二次请求。这个差别后面还会反复出现。
二、BLE 侧:应用层全手工
Zephyr 里启动一次 BLE 广播,入口是bt_le_adv_start(param, ad, ad_len, sd, sd_len)(adv.c:1315),legacy 路径连续下发四条 HCI 命令:
bt_le_adv_start(param, ad, sd) ├─ LE Set Adv Param ← interval / 类型 / 自身地址 ├─ LE Set Adv Data ← 业务数据(≤31B) ├─ LE Set Scan Rsp Data ← 名字常在这里(≤31B) └─ LE Set Adv Enable ← 开始广播注意第三条:扫描应答数据(sd)是和广播数据(ad)并列的独立参数,由应用代码显式传入。名字(BT_DATA_NAME_COMPLETE)放 ad 还是 sd 没有强制规定,两种都合法。常见做法是业务数据塞进 ad、名字塞进 sd——31 字节的广播包太金贵,留给业务;但直接把名字放 ad 里主动广播出去的设备也不少。
set_data_add_complete()(adv.c:483)打包时还有个贴心的细节:如果名字放不下,会自动降级成BT_DATA_NAME_SHORTENED截断塞进去(adv.c:500)。
所以 BLE 的名字获取路径要看名字放哪:名字在 ad 里,随 ADV_IND 第一次广播就到了扫描方手里;名字在 sd 里,就要走三段式——扫描方收 ADV_IND → 扫描方觉得有意思,发 SCAN_REQ → 广播方回 SCAN_RSP,名字才到手(下图)。取不到就取不到,协议栈没有补问机制。
三、经典蓝牙侧:host 只写一次名字,剩下的 controller 全代劳
再看经典蓝牙。设备侧初始化bt_br_init()(br.c:841)时按顺序下发一串命令,其中三条是关键:
WRITE_INQUIRY_MODE = 0x02(br.c:892)——指定 Inquiry 应答用 Extended Inquiry Result 格式,能带 240 字节 EIR;WRITE_LOCAL_NAME(br.c:905)——写入本地名字;WRITE_CLASS_OF_DEVICE = CONFIG_BT_COD(br.c:920)——写入 CoD(设备类别)。
CoD 是一个 24 位的设备类别码,扫描方靠它在列表里区分"这是手机、这是耳机、这是电脑"——手机扫描列表里那个小小的设备图标,就是照着这个码画出来的。常见取值(参考 SIG Assigned Numbers):
| 设备 | CoD |
|---|---|
| 手机(Smart Phone) | 0x5A020C |
| 耳机 | 0x240404 |
| A2DP 音箱/sink | 0x200404 |
| 免提 HF | 0x200408 |
| 车机 | 0x240328 |
| 台式电脑 | 0x000104 |
| 笔记本 | 0x00030C |
应用侧把它当配置项写进CONFIG_BT_COD就行,具体位域怎么解析这里不展开。还有一个细节:bit13(limited discoverable)由协议栈在bt_br_set_discoverable(limited)里自动置位/清零,配置值里不要预置它。
"开始广播"对应bt_br_set_discoverable()(br.c:1275):
bt_br_set_discoverable(enable, limited) ├─ Write Current IAC LAP(GIAC 0x9e8b33 / LIAC) ├─ 更新 CoD 的 discoverable 位 └─ Write Scan Enable(Inquiry Scan 开)流程里第一条Write Current IAC LAP值得多看一眼:IAC(Inquiry Access Code)是"我应答哪些询问"的暗号。被发现方不是谁来问都答,只应答访问码对得上的 Inquiry。GIAC(0x9e8b33)是通用码,几乎所有设备都用它,所以手机能扫到身边所有可被发现设备;LIAC 是受限码,只用于特定会话(如配对时临时可见)。这条命令就是在告诉 controller 当前监听哪个码。一句话区分:CoD 回答"我是什么",IAC 回答"我应答谁"。
到这里,注意一个反直觉的事实:Zephyr host 从头到尾没有发过Write_Extended_Inquiry_Response——整个hci_types.h里甚至没定义这个 opcode。那 EIR 里的名字是哪来的?
controller 自动生成的。controller 会把Write_Local_Name写入的名字自动填进 EIR 的 0x09(Complete Local Name)TLV,再附上Write_Class_of_Device写入的 CoD 等信息,Inquiry 应答时一起发出。名字和 CoD 都一样:host 各写一次 HCI 命令,剩下的组装、发送全由 controller 代劳。
对比两套设计哲学,差异就在这几条: -BLE:名字由应用代码自己打进 ad 或 sd,放哪没有强制规定——一切在应用手里; -经典蓝牙:名字只写一次WRITE_LOCAL_NAME,EIR 由 controller 代劳,随应答直接带出——host 几乎不用操心。
经典蓝牙的 EIR 里要是没名字(老设备、没设名字的设备),发现方还有一个兜底:Inquiry 结束后逐个发 Remote Name Request(LMP 层)单独去问,大多数都能问回来。不过这条兜底链有个狠角色——补查"失败一次即放弃",实测会导致约 5% 的设备名字为空。这些细节,上一篇《蓝牙扫描列表里,为什么有些设备连名字都没有?》已经拆解过,这里不展开。
四、为什么两套机制长成了两个样子?
机制差异看完了,更有意思的问题是:为什么会长成两个样子?答案在使用场景里。
经典蓝牙是"人选设备"。发起搜索的手机、车机、平板都带屏幕,扫描结果列表是给人看的——用户看到正确的名字才知道"这是我要连的设备",选中了才发起连接。名字是经典蓝牙发现的第一交互要素:拿不到名字 ≈ 这条设备不可选 ≈ 连不上。所以协议栈为名字建了专门的补查机制(Remote Name Request),宁可逐个发请求也要把名字凑齐。
BLE 是"程序选设备"。BLE 的常见形态是固定配对/自动重连——APP 记住第一次绑定的 MAC 地址,之后直接按地址连接,广播数据只用于过滤识别。尤其厂商自定义协议的 BLE 设备(各类 IoT 外设),对端 APP 是定向开发的,过滤直接用自定义 AD 字段甚至固定 MAC 前缀,根本不解析名字。名字在 BLE 里更多是锦上添花——手机系统蓝牙设置页展示用,31 字节的广播数据里放不下就不放了,不少厂商索性不放。
所以两边对"名字拿不到"的容忍度天差地别:经典蓝牙有补查机制兜底;BLE 没有对等机制,ad 或 sd 里没放名字、或没被对方收到,就结束了。
最后收个尾:经典蓝牙的 name 是给用户看的门面,拿不到 ≈ 不可连接;BLE 的 name 是锦上添花,MAC 和自定义字段才是识别主体。两套"广播"机制的所有差异,都是这两句产品哲学的投影。
五、小结
- 经典蓝牙没有"主动广播","可被发现"是应答 inquiry,一问一答;
- BLE 名字由应用打进 ad 或 sd,放哪没有强制规定;经典蓝牙名字只写一次
Write_Local_Name,controller 自动填进 EIR 随应答带出; - 差异的根源是场景:经典蓝牙"人选设备",名字是第一交互要素;BLE"程序选设备",地址和自定义字段才是识别主体。
区分清楚这两套机制,排查蓝牙问题时就不会把 BLE 的模型错套到经典蓝牙头上——比如以为经典蓝牙的"广播"会"把名字发丢",可它根本不广播,名字是应答时 controller 代发的。
你之前有没有把经典蓝牙的 inquiry 错当成"广播"过?评论区聊聊你踩过的蓝牙概念坑。觉得这篇把机制讲清楚了,点个"在看"。
系列衔接:本系列上一篇《经典蓝牙扫描设备名称为空的机制分析与解决方法》,讲清扫描列表里 5% 空名字的机制根源。
嵌入式 蓝牙 BLE 经典蓝牙 Zephyr 源码分析