第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 的输入、输出、控制通路都标清楚了。

16b/18b 编码与 FRL 字符分类 输入 16bit 数据 / 控制码 16b/18b 编码器 DC平衡 + 控制位插入 输出 18bit 码流 FRL 字符类型 数据字符(Data Char) 控制字符(Ctrl Char) 特殊字符(Special) 控制符号子类 LC(锁码) FS(帧起始) TS(流结束) RFU(保留) 注意:控制字符在码流中不可被误判为数据字符 编码器通过监控运行不一致性(RD)来维持 DC 平衡

这张图把编码流程和字符分类都画在一起了。左边进来 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 搭建一个简化的编码器,跑几个典型测试向量试试看。