先交代两句背景,方便第一次进系列的朋友跟上。蓝牙经典音频设备里,控制器固件住着一个叫链路管理器的模块,两台设备连上之后,两边各自的链路管理器就开始隔空对话:你是谁、支持什么特性、要不要加密、语音链路怎么排时隙、怎么省电。这套对话用的语言就是链路管理协议,消息叫 PDU。这些 PDU 不走普通数据通道,而是塞进控制专用的逻辑链路里随数据包过江。前面十一篇把加密握手、嗅探协商、语音建链、测试模式这些流程挨个过了一遍,这篇换个姿势:不追流程,改读花名册------规范最后压轴的 PDU 总表,88 条指令,每条叫什么、多长、坐什么车、谁能发、带什么参数,全在上面。这张表平时没人正眼看,其实它是链路管理层的账本,前面所有流程的指令都能在这里查到户口。
目录
[二、操作码的空间经济学:127 是一扇逃生门](#二、操作码的空间经济学:127 是一扇逃生门)
[三、坐什么车:DM1 与 DV 的捎带学问](#三、坐什么车:DM1 与 DV 的捎带学问)
[五、全家福点名:88 条指令按家族走一遍](#五、全家福点名:88 条指令按家族走一遍)
[六、Note 1 深潜:eSCO 的还价艺术](#六、Note 1 深潜:eSCO 的还价艺术)
一、账本怎么读:五列各管一摊
每条 PDU 在表里占一行,五个维度。Length 是整条消息的字节数,注意包含操作码自己------LMP_ACCEPTED 长 2 字节,是一个操作码加一个回显的操作码;LMP_DETACH 也是 2 字节,操作码加错误码。Opcode 是身份证号。Packet type 规定这条消息允许坐哪种包。Possible direction 约束谁有资格发起,C 是中央、P 是外设,也就是微微网里的主从两端。最后的 Contents 列逐字节给出参数在载荷里的位置,写固件的人照着切就行。

表头只有短短一句话,但信息量不小,原文如下:
Where the "Possible direction" column in Table 5.1 shows "B", the PDU is sent on the APB-C logical link. All other PDUs are sent on the ACL-C logical link and only in the direction(s) indicated.
方向列标了 B 的,走的是广播物理链路上的控制逻辑链路;其余全部走常规的 ACL-C 控制链路,且只按标注的方向收发。整张表里方向为 B 的只有一条------LMP_CLK_ADJ,时钟调整指令。为什么它要广播?因为时钟调整带着生效时刻参数,微微网里所有设备必须在同一个瞬间步调一致地动时钟,点对点发一遍根本保证不了原子性。这条孤零零的 B,背后是广播等时业务对全网同步的硬需求。
对着截图核一遍开头几行,账本的格式就熟了:LMP_ACCEPTED 两字节、操作码 3、DM1/DV 双向可捎带;LMP_AU_RAND 十七字节随机数、只能专车;往下到 LMP_CLK_ADJ,十五字节、127 转义、方向一栏写着孤零零的 B。
二、 操作码 的空间经济学:127 是一扇逃生门
操作码只有一个字节,理论容量 256。表里实际用的是 1 到 126 加上一个特殊的 127。设计这套编码的年代大概觉得一百多个够用了,结果生态爆发,嗅探子分级、安全简单配对、增强速率协商、信道分类、槽可用性修改......新需求一波接一波,直码很快见底。规范没有推倒重来,而是把 127 做成了转义符:第一个字节写 127,第二个字节写扩展操作码,从第三字节起才是真正的指令编号。表里的写法如 127/01 表示转义加扩展码 1,实际就是 LMP_ACCEPTED_EXT。
这套设计的妙处在于兼容。老设备不认识 127 打头的新指令,链路管理规范早就规定了对未知指令的标准动作------回一条带错误码的拒绝。新设备跟新设备之间用新指令,新跟旧之间老设备礼貌拒绝、新设备降级,谁都不崩。一个字节的转义机制,撑起了后来二十年的扩展空间,这是分层协议留后门的教科书案例。
账本末尾还有一行小字,值得抄下来:
The following opcodes were previously used: 22, 25 to 30. All other opcodes not listed in Table 5.1 are reserved for future use.
操作码 22 和 25 到 30 曾被使用,如今退役;表里没列出的全部保留给未来。翻旧账很有意思:22 是嗅探的旧版命令,后来并入了请求式的流程;25 到 30 那一片属于驻留模式家族------驻留机制在 5.0 里被整体移除,这些操作码随之作废。所以别看 88 条 PDU 88 个坑,有几个坑是空着的墓碑,记录着协议进化路上被淘汰的机制。留着编号不复用,是为了防止新旧设备把同一条指令解读成两个意思。

三、坐什么车:DM1 与 DV 的捎带学问
先补两个包名词。DM1 是经典蓝牙的单时隙中等速率数据包,载荷最多 17 字节,带纠错,控制消息的默认座驾。DV 是语音数据混载包:语音字段装 80 比特实时话音,数据字段最多装 9 字节数据,专门为通话中捎带控制信息设计。
总表的 Packet type 列几乎只有两种值:DM1 和 DM1/DV。翻译一下:只能坐专车,或者可以搭语音的顺风车。这个区分的完整规则写在基带规范里,链路管理这边只给结论。原文值得整段读:
The ACL-C logical link shall use DM1 or DV packets. DV packets shall only be used on the ACL-C link if the ACL-C message is less than or equal to 9 bytes and an HV1 synchronous logical transport is in use. The APB-C logical link shall use DM1 packets.
ACL-C 控制链路用 DM1 或 DV 包承载;DV 包仅在消息不超过 9 字节且 HV1 同步逻辑传输在用时才允许;广播控制链路 APB-C 只许用 DM1。HV1 又是什么?它是三种 SCO 语音包型里时隙负担最重的一种,每两个时隙就要占一个发包。

对着总表核对这条规则,会发现严丝合缝。凡是标 DM1/DV 的指令,长度全部不超 9 字节:特性交换正好 9 字节(操作码加 8 字节特性掩码)、槽偏移 9 字节、保持 7 字节、鉴权响应 5 字节;凡是超过 9 字节的,比如 17 字节的随机数家族(配对的初始随机数、鉴权挑战、组合密钥)、16 字节的信道图设置、10 字节的嗅探请求,统统只标 DM1。转义指令基本全是 DM1 专车,唯二例外是功率控制的请求与响应,恰好 3 字节。
为什么 DV 捎带非要等 HV1 在用?基带规范把这件事讲得很直白,SCO 时隙上的控制信息有最高优先级:
If the SCO slot has to be used to send control information with high priority concerning a control packet between the Central and the SCO Peripheral, or a control packet between the Central and any other Peripheral, the packet composer will discard the SCO information and use the control information instead. This control information shall be sent in a DM1 packet. Data or link control information may also be exchanged between the Central and the SCO Peripheral by using the DV or DM1 packets.
如果某个 SCO 时隙必须用来发高优先级控制信息,包组装器会丢弃语音信息改用控制信息,且该控制信息必须用 DM1 发送;中央与 SCO 外设之间的数据或链路控制信息也可以用 DV 或 DM1 交换。通话进行中,每个语音时隙本来就要发包,控制消息塞进 DV 的数据字段,语音一比特不少、控制消息免费过江------不用为了一条功率指令单独抢一个时隙。

还有个更细的补丁,写在 SCO 链路流程的末尾:
If the SCO packet type is HV1 the LMP_ACCEPTED shall be sent using the DM1 packet.
如果 SCO 包型协商的是 HV1,那么对 SCO 链路请求的确认必须用 DM1 发。HV1 语音负担最重,确认消息若再走 DV 捎带会和语音字段争位,干脆强制专车。这些边边角角的约束,全是实现者踩坑之后沉淀下来的。
拿一条超长指令看它的户口,直觉就更具体了。嗅探请求 10 字节、只能 DM1:

四、方向列的门道:谁发起是设计过的
Possible direction 有三种常规值:C→P 只许中央发、C←P 只许外设发、C↔P 双向。粗看像废话,细看全是权力分配。
只许中央发的,都是牵一发动全网的东西:信道图设置(改跳频序列必须全网一致)、链路监控超时、启动与停止加密、测试激活与测试控制、服务质量。临时密钥两条也是中央专属------密钥分发的信任方向决定了话语权。

只许外设发的最有意思。信道分类上报是外设的义务------外设身处干扰现场,只有它知道哪些信道被 Wi-Fi 占了,中央汇总后画信道图。恢复加密请求更反直觉,方向是 C←P:加密可以由中央暂停(为了重新鉴权或换密钥),但恢复的主动权交给外设。外设完成了鉴权流程、确认自己这端密钥就绪,才发起恢复------把关键的确认权交给流程里最后完成的那一方,避免中央单方面恢复时外设还在旧密钥状态里裸奔。


五、全家福点名:88 条指令按家族走一遍
把 88 条 PDU 按功能分家,账本立刻清爽了。一家一家过,每家配一张户口页。
身份能力组。名称请求与响应是一对分片协议:请求带 14 比特的名称偏移,响应回偏移、总长和 14 字节名称片段------设备名可以到 248 字节,靠这对指令一截一截拼出来。版本交换回三样:蓝牙版本号、厂商识别码、子版本号。特性交换是四条:基础版一页 8 字节掩码,扩展版带页号和最大支持页号,特性多了就翻页。



配对与鉴权组,见证三代人。第一代是传统配对:初始随机数撒种子,组合密钥或单元密钥二选一交换,鉴权挑战发随机数、应答回四字节摘要------安全强度今天看已经很弱,但指令都还躺在表里。

第二代是安全简单配对:IO 能力交换开场,封装头加封装载荷两条指令把 192 或 256 位椭圆曲线公钥拆成 16 字节一块搬运,确认值与随机数两条完成承诺-披露握手,DH 密钥校验收尾,配上数字比较失败、通行码失败、带外失败的三个失败信号。


第三代是加密管理:模式与密钥长度协商,启停加密,暂停与恢复,还有操作码 66 的 AES 暂停加密请求------密钥刷新期间临时挂起加密而不拆链路。新版本又添了密钥长度掩码一问一答两条新面孔,让双方一次性摸清对方支持的密钥长度集合。

功率与速率组。功率方面是四条步进指令(增、减、顶格、归零)加两条带响应的新版功率控制请求;速率侧是首选速率宣告、自动速率允许、包型表协商,外加服务质量里带轮询间隔和广播计数。

时序与信道组,最庞大。时钟偏移一问一答,时序精度一问一答(漂移加抖动,嗅探和保持模式要用它算守时窗口),槽偏移带地址,最大槽位与请求,角色切换带生效时刻,监控超时,AFH 三兄弟(信道图设置、分类上报、分类请求),时钟调整三件套,加上槽可用性修改三条------为广播等时业务腾时隙的新机制,定义映射、设置类型零、切换三步走。

省电组四条:保持与保持请求、嗅探请求、退出嗅探、嗅探子分级一问一答。
语音组四条:SCO 链路请求、eSCO 链路请求、各自对应的拆除请求。

连接生命周期组三条:连接意图宣告表示上层有业务要跑,建链完成宣布链路管理层收工,分离带错误码分手。
测试组两条,上一篇的主角。心跳一问一答,用来探测链路控制层还活着没。

六、Note 1 深潜:eSCO 的还价艺术
表尾的 Note 1 专门为 eSCO 链路请求写了六条注释,先看这条指令本身------全表参数最多的 16 字节,链路句柄、逻辑传输地址、时序控制标志、偏移、间隔、重传窗、双向包型、双向长度、空口编码、协商状态,一样不缺:


注释的核心是协商状态参数的四个取值,原文如下:
When responding to an eSCO link request with a new suggestion for these parameters, this flag may be set to 1 to indicate that the last received negotiable parameters are possible, but the new parameters specified in the response eSCO link request would be preferable, to 2 to indicate that the last received negotiable parameters are not possible as they cause a reserved slot violation or to 3 to indicate that the last received negotiable parameters would cause a latency violation. The flag is set to zero in the initiating LMP_eSCO_LINK_REQ.
翻译成谈判语言:发起方首次出价,协商状态写 0。收到报价的一方如果全盘接受,回确认完事;如果对方开的条件自己勉强能干但有更优方案,回一条新的 eSCO 链路请求、状态写 1,意思是按你说的也行,但我写的这个更好,你看;如果对方的条件直接踩了保留时隙红线,状态写 2,这是硬伤,没得谈;如果对方的条件会造成时延超限------对语音这种等时业务是致命的------状态写 3。一来一回之间,间隔、重传窗、包型、双向长度这些可谈参数被逐步磨到双方都能接受。

这是整个链路管理协议里罕见的还价式设计。绝大多数指令是一锤子买卖:要么接受要么拒绝,拒绝方在错误码里说明理由、发起方换个参数重来。eSCO 却把理由和反建议打包在同一条指令里,把多轮重试压缩成少轮协商------语音链路建立对时延敏感,省一个来回就少几十毫秒。注释里其余几条也值得记:eSCO 的逻辑传输地址不许中途改绑;eSCO 的包型和长度可以上下行不对称;包长不再像 HV 包那样锁死。
七、把账本装进代码:一个最小解析器
对着表写个解析器,比背表快。核心逻辑就两层:直码查表,127 再查一层转义表,顺手校验长度。
# 操作码字典(节选):{opcode: (名称, 总长, 可否DV捎带)}
LMP_PDUS = {
1: ("LMP_NAME_REQ", 2, True), 2: ("LMP_NAME_RES", 17, False),
3: ("LMP_ACCEPTED", 2, True), 4: ("LMP_NOT_ACCEPTED", 3, True),
7: ("LMP_DETACH", 2, True), 19: ("LMP_SWITCH_REQ", 5, True),
23: ("LMP_SNIFF_REQ", 10, False), 39: ("LMP_FEATURES_REQ", 9, True),
43: ("LMP_SCO_LINK_REQ", 7, True), 60: ("LMP_SET_AFH", 16, False),
57: ("LMP_TEST_CONTROL", 10, False),
}
ESC_PDUS = { # 127 转义表:{扩展码: (名称, 总长)}
0x01: ("LMP_ACCEPTED_EXT", 4),
0x0C: ("LMP_eSCO_LINK_REQ", 16), # 协商状态在载荷第16字节
0x1F: ("LMP_POWER_CONTROL_REQ", 3),
0x15: ("LMP_SNIFF_SUBRATING_REQ", 9),
}
def parse(pdu: bytes):
if pdu[0] != 127:
name, ln, dv = LMP_PDUS.get(pdu[0], ("未知指令", None, False))
return name, ln, dv
name, ln = ESC_PDUS.get(pdu[1], ("未知转义指令", None))
return name, ln, pdu[1] in (0x1F, 0x20) # 仅功率控制两条可捎带
# 例:一条嗅探请求(opcode 23)
name, ln, dv = parse(bytes([23, 0, 0x10, 0x03, 0x20, 0x00, 4, 0, 1, 0]))
# => LMP_SNIFF_REQ, 10字节, 不可捎带(DV) ------ 超过9字节,只能专车DM1
解析器里那个 DV 布尔值就是第三节规则的落地:超过 9 字节的指令,抓包软件里看到它出现在 DV 包的数据段,不用犹豫,直接判实现违规。
八、拿抓包分析:常客、过客与三出戏
规范读完后换个场景验证。第一份日志来自一段音乐播放连接的空口日志,建链头两秒,总表里的家族按次序集体亮相。

对着log逐条看:身份组(特性、扩展特性、名称、版本)最先登场,且特性交换做了两页;随后功率控制请求对端顶格发射;连接管理层一声建链完成收工;接着信道组发车道------信道图、信道分类使能、最大槽位、包型表、首选速率、时钟偏移、监控超时,一气呵成;最后安全组压轴:IO 能力、公钥搬运、确认与披露、DH 校验、鉴权、开加密。值得一提的是第四行那条分离指令------这是上一次连接的分手信,紧挨着新连接的建链流程,新旧交替在同一屏里完成。这条时间线基本就是总表的出场顺序表:先认人,再修路,最后上锁。
第二份log来自通话链路,三出戏都跟方向和状态约束有关。

第一出:角色切换。外设主动发起请求,两阶段完成------先谈妥再执行,第二阶段的指令里带着槽偏移 814 微秒,这是新旧中央交接时钟基准用的。
第二出:协商冲突。双方几乎同时发起包型表协商,各自都回确认自己那条,但后到的一方被对方以事务冲突拒绝------链路管理不允许两条同类事务交叉,冲突的后到者出局,这正是拒绝指令家族日常最常见的工作场景。
第三出:状态禁入。语音链路活跃期间,中央尝试发起嗅探,被外设以 LMP PDU Not Allowed(错误码 0x24)拒绝------语音等时业务期间禁止进嗅探,这是流程规则里写死的约束,抓包里见到的拒绝理由跟规范条文一字不差。
第三份log最有意思:把整段连接的链路管理消息按类型数个数。

一百七十条消息里,信道图更新一家占三十一条,功率控制十四条,两者合计超过四成------它们是链路的日常维护队,环境一变就要出动。特性、版本、名称这些身份消息集中在建链初期,配对完成后就消失。这印证了总表的一个隐含分层:有的指令是一生一次的引路人,有的是终身值守的维护工。读协议时最该花力气的是后者,因为量产设备的日常问题九成出在维护型流程上。
九、收个尾
88 条 PDU,串起来其实是三个问题:怎么编(一个字节操作码加转义逃生门)、怎么运(DM1 专车与 DV 顺风车的九字节分界)、谁有权(方向列里的权力与义务分配)。表本身没有一行流程描述,但每条流程规则都凝结在表的列里------HV1 时确认必须走 DM1,是时序冲突的解法;恢复加密只许外设发起,是把确认权交给流程最后完成的一方;eSCO 的协商状态字段,是把多轮还价压进单条指令的巧思。能把总表读出这些味道,链路管理层就算真正毕业了。
十、测验
问题一:蓝牙链路管理协议的操作码只有一个字节,后来指令数量超出规划是怎么解决的?老设备遇到新指令如何处理?
参考答案:
规范把 127 定义为转义操作码:第一字节 127 表示转义,第二字节为扩展操作码,从第三字节起才是真正的指令编号,等效获得第二字节操作码空间。新增的指令如嗅探子分级、安全简单配对全套、槽可用性修改、心跳等都走这条路。兼容性上,不认识 127 转义指令的老设备按未知指令处理,回 LMP_NOT_ACCEPTED_EXT 并带错误码,新设备据此降级到旧流程,双方不会失步。另外历史上操作码 22 与 25 到 30 已退役(旧版嗅探指令与驻留模式家族),规范保留这些编号不复用,防止新旧设备对同一操作码产生不同解读。
问题二:LMP 控制消息什么条件下可以放在 DV 包里发送?为什么超过 9 字节的指令没有这个待遇?
参考答案:
基带规范规定 ACL-C 控制链路用 DM1 或 DV 包承载,但 DV 捎带有两个前提:控制消息总长不超过 9 字节,且 HV1 同步逻辑传输在用。DV 包是语音数据混载包,语音字段 80 比特实时话音,数据字段最多 9 字节,控制消息塞进数据字段可以搭语音时隙的便车,不为一条控制消息单独占用时隙。超过 9 字节装不下自然只能走 DM1,这就是总表里 DM1/DV 与 DM1 两种标注的分界依据------特性交换、槽偏移等 9 字节指令可捎带,随机数家族 17 字节、信道图 16 字节、嗅探请求 10 字节全部专车。另有补充规则:HV1 包型下对 SCO 链路请求的确认必须用 DM1,广播控制链路 APB-C 上的时钟调整指令也只许 DM1。
问题三:eSCO 链路请求里的协商状态参数有哪几个取值,各自代表什么?这种设计相比 SCO 有什么优势?
参考答案:
协商状态四个取值:0 表示这是发起方的首次出价;1 表示收到的参数可行,但响应方在回程指令里给出了更优的参数建议,属于还价;2 表示对方参数不可行,原因是造成保留时隙冲突------这是硬性约束;3 表示对方参数会造成时延超限,对语音等时业务不可接受。相比 SCO 链路要么接受要么拒绝、拒绝后发起方只能盲目换参数重试的模式,eSCO 把拒绝理由和反建议打包在同一条指令里返回,把多轮盲试压缩成少轮定向协商,显著缩短语音链路建立时延。配套的灵活性还有:上下行包型与长度可以不对称、包长不再锁死于 HV 包型、增加重传窗参数支持有限重传。