第4章 FRL编码机制:16b/18b编码、FRL字符与控制符号
HDMI 2.1 的 FRL 模式,说白了就是用一种新的方式把数据塞进通道里。老一代 TMDS 用 8b/10b,FRL 改用 16b/18b。你可能会问,为什么非要搞个新的?原因很简单——带宽需求上去了,旧编码效率不够。16b/18b 把开销从 25% 降到了 12.5%,多出来的带宽刚好给 48Gbps 用。
我刚开始接触这个编码时,总觉得它跟 8b/10b 差不多,不就是多了一倍的数据位嘛?后来在调试一个 4K144 的项目时,发现链路训练总是卡在第 2 步,折腾了两天才意识到——控制符号的判定逻辑跟 TMDS 完全不一样。嗯,咱们今天就把这块彻底讲清楚。
16b/18b 编码核心原理
先看个框架图,帮你把整体逻辑串起来。这张图是我自己画的,把 16b/18b 的输入、输出、控制通路都标清楚了。
这张图把编码流程和字符分类都画在一起了。左边进来 16bit 数据或控制码,经过编码器变成 18bit。右边三个分支表示编码后的字符类型。你注意看,控制字符下面还有子类——LC、FS、TS、RFU,这些在链路训练时特别重要。
16b/18b 编码怎么干活的?
编码器每周期吃进去 16bit,吐出来 18bit。多出来的 2bit 干嘛用?一个是 DC 平衡位,一个是控制标志位。DC 平衡位用来保证码流中 0 和 1 的数量尽量相等,避免信号漂移。控制标志位告诉接收端:这 16bit 到底是数据,还是控制命令。
18bit 输出 = [16bit 原始数据] XOR [DC 平衡位] + [控制标志位]
实际算法还要查表,但思路就是:先根据当前运行不一致性决定是否反转数据,再插入标志。
我在项目中遇到过一个问题:某颗芯片的编码器在 DC 平衡位计算时用了有符号溢出逻辑,导致某些 16bit 值(比如 0xAAAA)编码后长时间不翻转,接收端的 CDR 锁不住。后来查了三天,发现是 RTL 里一个符号扩展写错了。嗯,这种边缘 case 仿真很难覆盖,所以建议你在做 FPGA 原型验证时,专门跑一组“全 0 交替”和“全 1 交替”的测试向量。
FRL 字符与控制符号详解
FRL 定义了 4 种控制符号。它们不是随便编码的——每个控制符号都有一个唯一 16bit 模式,接收端用硬件就能识别,不需要查表。你想想看,如果识别一个控制符号需要 10 个时钟周期,那 48Gbps 的链路怎么顶得住?
| 符号名称 | 缩写 | 16bit 模式(十六进制) | 用途 |
|---|---|---|---|
| 锁码字符 | LC | 0xFF00 | 链路训练期间用于建立比特锁 |
| 帧起始 | FS | 0xFC03 | 标记一个 FRL 包的开始 |
| 流结束 | TS | 0xF00F | 标记一个 FRL 包的结束 |
| 保留 | RFU | 0x0FF0 | 预留,目前未使用 |
你可能注意到了,LC 是 0xFF00,FS 是 0xFC03,这些值都不是随机选的。它们有两大特点:第一,每个模式都有明确的游程长度限制(最大连续相同位不超过多少);第二,这些模式经过 16b/18b 编码后,在 18bit 空间里不会与任何数据字符冲突。我记得有一版草案里 FS 用的是 0xFFFF,结果发现它编码后跟一个长 0 数据冲突,后来才改成了 0xFC03。
在抓 FRL 码流时,可以先找 LC 字符。链路训练阶段,发送端会连续发送 1024 个 LC。接收端只要在 4 个时钟内检测到连续 3 个 LC,就可以认为链路已经对齐。我习惯把这个检测窗口设到 8 个时钟,防止噪声干扰。
控制符号插入与检测
发送端在每 128 个数据字符后,必须插入一个控制符号(通常是 FS 或 TS)。为什么是 128?这是 HDMI 2.1 规范定的,用来保证接收端不会因为长时间看不到控制符号而失锁。你想想看,如果接收端连续收到 1000 个数据字符,它怎么知道自己还在正确的 FRL 包里面?控制符号就是心跳。
接收端的检测逻辑更直接:每当收到一个 18bit 数据,先检查控制标志位是否为 1。如果为 1,则将这 18bit 解码成 16bit 控制模式,然后查表匹配。匹配成功就触发相应的状态机事件。
我曾经在一个项目中遇到接收端误将数据字符当成 LC 的情况。原因是在某些特殊数据模式下,16b/18b 编码后的 18bit 值偶然与控制符号的 18bit 编码相同。后来我们用了一个“两次确认”机制:接收端必须连续收到两个相同的控制符号才认为有效。这个技巧后来被我写进了公司的设计规范里。
编码与解码的实战代码片段
下面给一段简化的 16b/18b 编码器伪代码。注意,这是为了说明原理,实际产品里会用查表法加速。
// 简化的 16b/18b 编码器核心逻辑
// Input: data_in[15:0], ctrl_flag (0=data, 1=control)
// Output: encoded[17:0]
// Internal: rd (running disparity, -1 or +1)
if (ctrl_flag) {
// 控制字符直接映射到预定义 16bit 模式
case (control_code):
LC: raw_16 = 0xFF00;
FS: raw_16 = 0xFC03;
TS: raw_16 = 0xF00F;
RFU: raw_16 = 0x0FF0;
default: raw_16 = 0xFFFF; // 错误处理
} else {
// 数据字符:先计算是否需要反转以保持 DC 平衡
ones_count = count_ones(data_in);
// 如果当前 1 的数量 > 8,或者=8 但 RD 为正,则反转
if (ones_count > 8 || (ones_count == 8 && rd == +1)) {
raw_16 = ~data_in;
inv_flag = 1;
} else {
raw_16 = data_in;
inv_flag = 0;
}
}
// 拼接编码结果:inv_flag 作为第 17 位,ctrl_flag 作为第 16 位
encoded[15:0] = raw_16;
encoded[16] = ctrl_flag;
encoded[17] = inv_flag;
解码器就是反过程:先看第 16 位是数据还是控制,如果是控制就直接把低 16bit 用查表映射回控制符号;如果是数据,则根据第 17 位判断是否要取反。
调试中的常见坑
- DC 平衡位极性错误:发送端和接收端对运行不一致性的初始值约定不一致。规范建议初始值为 -1,但有些芯片厂商默认初始值为 +1。解决办法——在链路训练阶段让双方交换一个 0x0000 的校准字符,归一化 RD 状态。
- 控制符号检测窗口过窄:我见过一个设计把检测窗口只设了 2 个时钟,结果由于信号抖动,偶尔漏检 FS,导致解包失败。建议窗口至少设到 4 个时钟,并允许有一次亚稳态采样。
- 保留字符 RFU 的处理:虽然目前没用,但一定要做到能正确识别并忽略。我曾经遇到一个芯片把 RFU 当成无效字符直接丢弃,结果后续 RFU 被正式定义后,固件升级困难。建议把 RFU 解析结果送给上层软件,不要硬件忽略。
好,这一节的内容就这么多。16b/18b 编码虽然看起来比 8b/10b 复杂,但只要把 DC 平衡和控制符号的逻辑理清,剩下的就是查表。你回去可以用 verilog 搭建一个简化的编码器,跑几个典型测试向量试试看。