☰
【LMP】规范精讲[13]: 参数的户口本、公钥的集装箱与出厂的兜底值
2026/11/6 5:16:27 网站建设 项目流程

一条链路上飞过的每个数字,都有名有姓有单位;64 字节的公钥,得拆成四节 16 字节的车厢才能上路;而所有谈判开始之前,设备兜里都揣着五个出厂默认值。这篇把 LMP 收尾的三大件一次讲透。


目录

一、户口本:八十多个参数的登记制度

1.1 五栏登记表和一条总则

1.2 按家族逛字典

1.3 两条贯穿全表的位规则

二、集装箱:16 字节的封装货运

三、出厂设置:五个兜底值

四、罚则字典:错误码的展开

五、一段小彩蛋

六、收个尾

七、测验


前面十几篇把 LMP 的过程性内容都过完了:建链、认证、加密、功控、AFH、嗅探、多时隙、角色切换。今天换一个视角,不讲流程,讲流程里流动的东西本身——参数怎么定义、大块头数据怎么封装、谈判之前大家的共识是什么。这三块内容在规范里属于那种平时不显眼、出问题时必回头查的硬底子。我把它们分别叫作户口本、集装箱和出厂设置。

老规矩,全程用两份手头的空口抓包做伴读:一份是音乐场景的链路全过程,一份是语音通话场景(中间夹着 eSCO 建立与拆除)。


一、户口本:八十多个参数的登记制度

1.1 五栏登记表和一条总则

参数定义部分是一张巨大的表,八十多个参数挨个登记,每个条目五栏:参数名、长度(字节)、类型、单位、详细说明。像户口本一样,姓名、籍贯、出生日期、身份信息,一个都不能少。

表的开头有一句总则,先把没有明确强制范围(mandatory range)的参数统一打了包:

Where no mandatory range is given, the mandatory range consists of all valid values that are not reserved for future use.

翻译过来就是:凡是我没画线的,除了标着保留给未来使用的值,其余全都在合法范围内。这句话看着不起眼,实际是给实现者的定心丸——你收到的值只要不是保留值,就不能说人家非法。保留值在表里大量出现(各种 2-255: reserved for future use),它们就是各参数的地雷区。

1.2 按家族逛字典

八十多个条目顺着字母排,读起来枯燥,但按功能分家就清爽了。下面挑每家的核心讲。

AFH 家族:跳频地图的两套账本。这家有信道分类(AFH_Channel_Classification)和信道地图(AFH_Channel_Map)两个 10 字节的参数,容易混。分类是情报账本,地图是执行账本。分类参数是 40 个 2 比特元素,第 n 个元素描述信道 2n 和 2n+1 的好坏(0 未知、1 好、3 坏),唯独第 39 个元素只管 78 号信道一个——因为 79 个信道两两配对,最后一对只剩一个。地图参数是 80 个 1 比特元素,第 n 个直接决定信道 n 用不用,第 79 号元素保留。从设备靠分类账本上报情报,Central 汇总后改写地图账本,跳频序列随之改道。

地图不是发出去立刻生效的,要等一个切换时刻(AFH_Instant)。这个参数 4 字节,存的是 Central 时钟的 27:1 位:

Bits 27:1 of the Central's clock value at the time of switching hop sequences. Only even values are valid.

只取 27:1 位、只允许偶数——这两个细节后面统一讲,先记住它不是随便填的数。同家的最大/最小间隔参数限定了 0x0640 到 0xBB80 槽,也就是 1 到 30 秒,同样只收偶数。

时钟家族:对表专用。时钟偏移(Clock_Offset)是 15 位无符号数,单位 1.25 毫秒,计算式是 (从设备 CLKN16-2 减 Central 的 CLKN16-2) 再模 2 的 15 次方。这个值量出来存着,下次 page 同一个设备时能直接拿去预测跳频相位,省下几百毫秒的搜索时间。我的音乐抓包里建链后就有一笔 0x5644 的记录,通话抓包里是 0x206A——不同链路、不同晶振,偏移自然不同,但都规规矩矩落在 15 位容器里。

同一家族还有粗调时钟五件套(Clk_Adj_前缀)和槽偏移(Slot_Offset,0 到 1249 微秒)。粗调那组是为角色切换场景准备的:新 Central 要把自己的表针拨到和 piconet 对齐,调整量以微秒计(Clk_Adj_Offset 是有符号数,正负 624 微秒以内),调整时刻照例只收偶数。

声码器一家之主:Air_Mode。语音链路的载荷格式就这一个字节定终身:0 是 µ-law 对数律、1 是 A-law 对数律、2 是 CVSD、3 是透明数据。欧洲电话用 A-law,北美用 µ-law,蓝牙默认 CVSD,透明模式则把编解码完全交给上层——我的通话抓包里 eSCO 协商的结果就是透明数据,说明语音帧原样穿透,编解码在应用层做。

速率位域:一个字节管两件事。数据速率(Data_Rate)参数是位域设计的教科书案例。基础速率模式下,bit0 决定用不用 FEC(0 用 1 不用),bit1-2 是包长偏好(1/3/5 槽);增强速率模式下,bit3-4 选速率(1 是 2 Mb/s、2 是 3 Mb/s),bit5-6 再来一组包长偏好,bit7 保留。两套业务塞进一个字节,互不干扰。

空口上它是这样的:音乐抓包里有一条速率偏好,基础速率选了带 FEC 的包(DM 系列),增强速率选 3 Mb/s 加 5 槽包。对照位域拆开就是 bit0=0、bit3-4=2、bit5-6=3,一个字节表达了三种倾向。

链路质量双指标:Drift 和 Jitter。漂移是时钟长期偏离的百万分比,抖动是短期波动的微秒数,各占一个字节。谁测谁报——接收方实测自己这端的时钟质量,随定时精度上报给对端,让对端决定时钟补偿策略和轮询提前量。我的抓包里两份日志都报了 250 ppm 的漂移和 10 微秒的抖动,其中一个场景还先后出现 0 微秒和 10 微秒两次上报——同一条链路,测量时刻不同,结果就不同,这不是异常,恰恰说明这两个值是活的测量值而非写死的配置。

eSCO 家族:语音链路的谈判筹码。这家人口最多:句柄、逻辑传输地址(1 到 7)、包类型(0x07 是 EV3、0x26 是 2-EV3、0x37 是 3-EV3,EV4/EV5 及 2-EV5/3-EV5 各有编号,还有一个方向敏感的 0x00——Central 发是 POLL、从设备发是 NULL)。

真正的重头是三个数:TeSCO 是语音帧间隔(4 到 254 槽,只收偶数),W eSCO 是重传窗口宽度(0 到 254 槽,只收偶数),Packet_Length 是载荷长度。

谈判不是一次成交的。还价状态(Negotiation_State)字段记录了卖家的四种态度:0 发起谈判、1 你给的参数能用但我有更好的、2 会造成保留槽冲突、3 会造成时延冲突、4 压根不支持。注意 2 和 3——这两个不是笼统的拒绝,而是精确的拒绝理由,直接对应后面要讲的错误码 0x34 和时延违规场景。

为了保底互通,规范还专门列了一张强制参数范围表:接收方必须接受表内任何一个组合,可以额外接受更大范围的值。单槽包 EV3 的 TeSCO 固定 6,2-EV3 放宽到 6-12 的偶数,3-EV3 到 6-18;三槽包 EV4/EV5/2-EV5/3-EV5 统一 16;W eSCO 单槽包只许 0、2、4,三槽包只许 0 和 6;载荷长度两个方向都是 10×TeSCO÷2;声码器模式单槽包至少支持 A-law/µ-law/CVSD/透明中的一种,三槽包只强制透明模式。这张表就是谈判桌上的法律底线——你可以说不,但不能对表内的组合说不。

嗅探家族:低功耗三兄弟。TSniff 是嗅探周期(偶数,且不得超过监督超时乘 0.999,强制范围 0x0006 到 0x0540),Sniff_Attempt 是每个周期开头必须醒着收的槽数(1 到 TSniff÷2),Sniff_Timeout 是听到自己的地址后继续守候的槽数(0 到 0x0028)。三者的关系像闹钟:周期多久响一次、醒了先听多长、有动静再守多久。我的音乐抓包里协商出 T=800 槽(500 毫秒)、Att=4、超时 1,从设备每半秒醒 4 个槽,绝大多数时间在睡觉。

监督超时与切换时刻:链路的生死簿。监督超时(Supervision_Timeout)只许 0(无限等待)或 0x0190 到 0xFFFF。抓包里两次都是 8000 槽,换算 0.625 毫秒一槽正好 5 秒。切换时刻(Switch_Instant)和 AFH 的即时值同构:Central 时钟的 27:1 位,只收偶数。

安全与身份:钥匙的尺寸学问。密钥长度(Key_Size)范围 7 到 16 字节,抓包里顶格 16。广播加密的密钥长度掩码(Key_Size_Mask)是 16 个比特位图,第 n 位代表 n 字节长度是否支持,0 到 6 位必须为零——因为 7 字节以下压根不许存在。设备名相关的三个参数(Name_Length、Name_Offset、Name_Fragment)把最长 248 字节的名字切成 14 字节的分片运输,Name_Fragment 的类型标注 utf8s{14z},14 个 UTF-8 字符加零结尾。IO 能力四选一:仅显示、显示加确认、仅键盘、无输入无输出——简单配对时靠它决定走数字比较还是万能钥匙流程。

功率状态字:三种调制三张脸。功率控制响应(Power_Adj_Rsp)是 4 个 2 比特元素组成的字节,分别汇报 GFSK、π/4-DQPSK、8DPSK 三种调制下的功率状态:0 不支持、1 变了一档(未到边界)、2 已到最大、3 已到最小。我的音乐抓包里建链第一件事就是功率顶格,状态字三种调制全是 max power;随后几个来回的升降,状态字如实变成 changed one step。一个字节回答三个问题,位域的效率体现得淋漓尽致。

SAM 子图家族和杂项。子图索引(SAM_Index)强制支持 0、1、2 和 0xFF;子图类型参数是 56 个 2 比特字段排进 14 字节,每个字段描述一个槽的收发可用性(只发、只收、全开、全关);TSAM_SM 是子图长度(2 到 56,偶数);保持时长(Hold_Time)限 0x0014 到 0x8000 且不得超过监督超时的 0.999 倍;轮询间隔(Poll_Interval)强制范围 0x0006 到 0x1000;时控标志(Timing_Control_Flags)里只有 bit1 有意义,区分初始化方式 1 和 2。这一圈逛下来,字典的主体就都在手里了。

1.3 两条贯穿全表的位规则

上面反复出现两个短语:only even values are valid,和 bits N:M。这不是装饰性注释,全表有十几处参数受它们管辖,规范特意在表后面用两段话把规则说死。

第一段,偶数条款:

If a parameter is described as only even values are valid and a device receives an LMP PDU with an odd value for this parameter field, the PDU should be rejected with an error code of Invalid LMP Parameters (0x1E).

为什么这么多参数要偶数?因为蓝牙的槽成对出现(625 微秒一槽,收发交替),所有涉及槽边界的时刻量——切换时刻、各类即时值、各种周期——都天然落在偶数槽号上。发奇数等于宣称自己要在半个槽的中间做事,物理上就不成立。规范甚至贴心地把位域编码方式都设计好了:即时值类参数存的是时钟的 27:1 位,最低位在源头就被丢掉,想发出奇数都难。

第二段,位提取规则:

Where a parameter is described as bits N:M of some other value V, the parameter value in the PDU shall be V shifted right by M bits. Where the space available for the parameter is greater than that needed (N-M+1 bits), the parameter value still occupies the entire space and the remaining bits shall be zero.

规则本身三句话:右移 M 位、装满整个容器、多余位补零。规范还给了个例子,值得原样看:

For example, if a 16 bit parameter is equal to 0xDEAD and bits 10:6 are stored in a uint8, the resulting parameter will be 0x1A.

0xDEAD 是 1101 1110 1010 1101,取第 10 到第 6 位是 11010,装进 8 位容器左补三个零,得 0001 1010 即 0x1A。我把这两条规则连同这个例子画成了一张图:

用 C 代码把这两条规则落地,就十几行的事:

/* 位提取:bits N:M,右移 M 位,高位补零 */ static inline uint32_t lmp_extract_bits(uint32_t v, uint8_t n, uint8_t m) { return (v >> m) & ((1u << (n - m + 1)) - 1u); } /* 偶数校验:违反即整条 PDU 拒绝,错误码 0x1E */ int lmp_check_even(uint32_t v, uint8_t *err) { if (v & 1u) { /* 奇数 = 非法 */ *err = 0x1E; /* Invalid LMP Parameters */ return -1; } return 0; } /* 角色切换时刻的装载:CLK 取 27:1,天然偶数 */ uint32_t lmp_make_switch_instant(uint32_t clk27_0) { return clk27_0 >> 1; /* 最低位丢弃,奇数不可能出现 */ }

对照真实空口看一眼最后那行代码的效果:通话抓包里那次角色切换,28 位本地时钟是 0x00E7EF0,二进制末位是 0,右移一位得 0x0073F78——抓包工具显示的切换时刻正是这个数。发出去的值天然是偶数,接收方左移一位还原,两边对齐在同一个槽边界上。

二、集装箱:16 字节的封装货运

LMP 单条 PDU 的载荷上限是 16 字节,这在大多数场景够用——参数表里最长的 Name_Fragment 也才 14 字节。但安全流程里有大块头:椭圆曲线公钥。P-192 的公钥 48 字节,P-256 的公钥 64 字节,塞不进一条 PDU,怎么办?

规范的答案是封装(encapsulation)机制,货运集装箱思路:先发一条头单报报名号——什么货、多长,然后分批把 16 字节一节的车厢发过去,接收方拼箱验收。

头单就是表里那三个参数:主类型(Encap_Major_Type)、次类型(Encap_Minor_Type)、载荷长度(Encap_Payload_Length)。目前启用的组合只有两组:

主类型 1 加次类型 1 是 P-192 公钥,48 字节,字节 23-0 装 X 坐标、47-24 装 Y 坐标,小端序;主类型 1 加次类型 2 是 P-256 公钥,64 字节,字节 31-0 装 X 坐标、63-32 装 Y 坐标,同样小端。注意坐标系是先低后高、每个坐标内部又按小端排——两层字节序嵌套,实现时最容易在这里翻车。其余所有主次类型组合全部保留,也就是说这套集装箱目前只运公钥这一种货。

有个细节值得单独说:Encap_Data 参数的定义注明 MSBs zero padded when data is less than 16 bytes——最后一节车厢如果装不满,高位补零,不许短箱发运。接收方按头单里登记的总长度取数,多的零位自然被截掉。

分块运输的代码也简单,两段函数管进出两 directions:

#define ENCAP_P192 0x01 #define ENCAP_P256 0x02 #define ENCAP_FRAG 16 /* 发送侧:64 字节公钥拆四节 */ void encap_send_pubkey(const uint8_t key[64], uint8_t minor_type) { lmp_send_encap_header(1, minor_type, 64); /* 先报名号 */ for (int off = 0; off < 64; off += ENCAP_FRAG) lmp_send_encap_data(&key[off]); /* 4 节 16 字节车厢 */ } /* 接收侧:按长度拼箱,末节高位零填充已被长度自然截断 */ int encap_recv(uint8_t minor_type, const uint8_t *frags, size_t nfrags, uint8_t *out) { size_t total = (minor_type == ENCAP_P192) ? 48 : 64; for (size_t i = 0; i < nfrags; i++) memcpy(&out[i * ENCAP_FRAG], &frags[i * ENCAP_FRAG], ENCAP_FRAG); return (nfrags * ENCAP_FRAG >= total) ? 0 : -1; }

在空口上,这套机制表现为一条头单加连续四条 16 字节载荷,一气呵成。简单配对走安全建立流程时,公钥交换全靠它扛。

三、出厂设置:五个兜底值

谈判开始之前,大家的共识是什么?规范用一句祈使句开篇:

Devices shall use these values before anything else has been negotiated.

在一切协商发生之前,设备必须按默认值行事。默认值表一共五个:

AFH_Mode 关闭——建链初期老老实实全 79 信道跳频,信道地图还不存在,谈不上裁剪;AFH_Reporting_Mode 关闭——从设备的情报上报通道默认关闭,等 Central 想要情报再打开;Drift 250 ppm——一个偏保守的时钟质量假设,绝大多数消费级晶振都比它好,用保守值意味着轮询提前量留足,不会错过包;Jitter 10 微秒——同理,宁可多等不可错过;Max_Slots 1——最保守的包长,建链第一毫秒谁都不知道对端能不能收 3 槽 5 槽大包,先按单槽起步。

这五个默认值挑得极有讲究:两个 AFH 相关的管跳频,两个时钟相关的管同步,一个包长管传输。全是建链初期最容易踩空的领域。默认值的本质是互操作兜底——两个从未谋面的设备,在说第一句话之前就知道对方会以什么姿态行事。

四、罚则字典:错误码的展开

参数表里 Error_Code 一栏只有一句 See [Vol 1] Part F,真正的错误码字典在规范第一卷的控制器错误码附录里。跟 LMP 参数直接相关的一共七个,值得逐个过一遍。

0x19 Unknown LMP PDU:控制器收到不认识的 LMP 操作码。版本错配的典型症状——老设备收到新版本才有的 PDU,只能这么回答。

0x1E Invalid LMP Parameters:参数级违法,法定触发条件有四条,原文列举得很清楚——PDU 长度非法;该偶不偶;超出规定范围;两个以上参数互相矛盾。前文偶数条款的罚则就是它。这四条几乎覆盖了参数表的所有违规形态,是七个错误码里出场率最高的一个。

0x22 LMP Response Timeout:事务在超时窗口内没有等到响应。前面讲事务模型时提过的计时器到点,就报这个。

0x23 LMP Error Transaction Collision:两个设备同时发起同一个事务,撞车了。规范对碰撞的处理是各退一步重来,这个错误码就是撞车时的鸣笛。

0x24 LMP PDU Not Allowed:操作码本身没毛病、参数也没毛病,但当前状态机不允许这个 PDU 出现。状态敏感型拒绝。

0x30 Parameter Out of Mandatory Range:请求的参数值超出强制范围且接收方不接受。注意它和 0x1E 的分工——0x1E 罚的是无争议的违法(奇数、越界、矛盾),0x30 罚的是表内合法但超出强制范围、接收方又不愿意迁就的值。eSCO 谈判里对方还价状态 4(不支持)就可能伴随它出现。

0x34 Reserved Slot Violation:本次同步链路谈判被终止,还价状态正是保留槽冲突。当 eSCO 的参数组合会侵犯已存在的保留槽时,谈判以这个错误码收场。

这七个码不是抽象概念,我在通话抓包里就逮到一个现行。eSCO 链路建立期间,Central 尝试发起嗅探协商(T=144 槽即 90 毫秒、Att=4),从设备直接拒绝,理由正是 LMP PDU Not Allowed(0x24)——参数本身挑不出毛病,但 eSCO 占着保留槽,这个嗅探组合此刻不被状态机允许。时间线如下:

七秒后 eSCO 拆除,但那次嗅探协商没有重试——发起方显然判断这个场景下不值得再试。一个错误码背后的完整故事:参数合法性与链路状态是两回事,前者由参数表管辖,后者由状态机管辖,0x24 就是这两套管辖权的分界线。

五、一段小彩蛋

规范的附录 A 有一张参数更名对照表,把旧版小写名称(air mode、data rate、clock offset)和新版驼峰名称(Air_Mode、Data_Rate、Clock_Offset)一一对应。如果你读的是十年前的代码或旧版规范,参数名对不上号时可以来这里考古。名字改了,语义没变——户口本换了封面,人还是那些人。

六、收个尾

三件套讲完了,串起来回味一下。户口本(参数定义)回答了链路上每个数字的身份问题:多长、什么类型、什么单位、边界在哪;集装箱(封装机制)解决了 16 字节天花板与大块头数据之间的矛盾,头单加车厢的货运模式简洁而完备;出厂设置(默认值)则是在一切谈判之前铺设的互操作底线,五个参数覆盖跳频、同步、包长三个最敏感的领域。

如果只能记住三句话,我建议是这三句:偶数条款管住所有槽边界参数,违反即 0x1E;位提取规则(右移 M 位、补零装满)管住所有位域参数,0xDEAD 取 10:6 位得 0x1A;默认值管住谈判开始前的一切行为,Drift 250、Jitter 10、Max_Slots 1、Poll_Interval 40、AFH 全关。这三句话撑起了 LMP 参数世界的半壁江山。

至此 LMP 主体内容全部讲完。回头看这十几篇:从 PDU 格式和事务模型起笔,经特性协商、安全握手、连接控制,到今天的三件套收尾,一条 ACL 链路从生到死的所有管理动作都在射程之内了。

七、测验

Q1:LMP 参数表中大量参数标注 only even values are valid,背后的原因是什么?接收方收到奇数值应如何处理?这套约束在编码层面是怎么保证的?

答:

原因在于蓝牙的时隙成对出现,625 微秒一槽、收发交替,所有涉及槽边界的量(各类即时值、周期、间隔)在物理上都只能落在偶数槽号上,发送奇数值等于宣称在半个槽中间做事,物理上不成立。处理方式规范写得明确:整条 PDU 以 0x1E Invalid LMP Parameters 拒绝,而不是只忽略该字段——参数级违法污染的是整条报文的可信度。编码层面的保证体现在位域设计:即时值类参数存的是时钟的 27:1 位(如 Switch_Instant、AFH_Instant),装载时右移一位把最低位丢掉,发出的值天然是偶数,接收方左移一位还原。源头设计消灭了非法值,比收端校验更可靠,两者叠加构成双保险。

Q2:LMP 单条 PDU 载荷上限 16 字节,P-256 公钥 64 字节是怎么传过去的?封装头三个字段各起什么作用?字节序有什么讲究?

答:

靠 LMP 封装机制,货运集装箱模式:先发一条头单(封装头),声明主类型、次类型和载荷总长度,然后把 64 字节公钥拆成四节、每节 16 字节逐条发送,最后一节装不满则高位补零,接收方按头单登记的总长度拼箱截断。主类型目前只有 1(公钥类),次类型 1 是 P-192、2 是 P-256,其余组合全部保留;载荷长度向接收方预告车厢总数和拼箱长度。字节序有双层结构:整体布局是 X 坐标在前(P-256 为字节 31-0)、Y 坐标在后(字节 63-32),每个坐标内部又按小端序排布,实现时两层都不能搞错。这套机制目前只运公钥一种货,简单配对的 DH 密钥交换全靠它完成。

Q3:错误码 0x1E、0x30、0x24 都能拒绝一条 LMP PDU,三者的区别是什么?eSCO 谈判中对方报还价状态为保留槽冲突时,对应哪个错误码?

答:

三者管辖权不同。0x1E Invalid LMP Parameters 罚参数本身违法,法定触发条件四条:PDU 长度非法、该偶不偶、超出规定范围、参数互相矛盾——是参数级的执法。0x30 Parameter Out of Mandatory Range 罚的是值在理论上合法但超出强制范围且接收方不接受的情况,本质是谈判破裂而非报文违法,多见于 eSCO 参数还价。0x24 LMP PDU Not Allowed 罚的是时机不对——操作码合法、参数合法,但当前链路状态不允许这个 PDU,是状态机级的执法,比如 eSCO 占用保留槽期间的某些嗅探协商就会被它拒绝。还价状态 2(保留槽冲突)对应的终止错误码是 0x34 Reserved Slot Violation,谈判以它收场;还价状态 4(不支持)则更接近 0x30 的语义。区分这三个码的实战意义:收到 0x1E 该查自己的参数打包,收到 0x30 该调整请求值,收到 0x24 该等状态变化再重试。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询