HDMI Transmitter Subsystem IP 这个方向,我在过去几年里陆陆续续做过几个项目,从纯 FPGA 原型验证到最终 SoC 流片都有涉及。说实话,第一次接触这套 IP 的时候,我以为无非就是把并行视频数据串行化然后按差分对发出去,能有多复杂?结果真正啃下来才发现,从 TMDS 编码到音频采样包插入、从 CEC 仲裁到 HDCP 握手、从 1.4 的 340MHz 到 2.0 的 600MHz 时序收敛,每一个环节都有足够多的细节能把人卡住。这篇文章我打算把整个 Transmitter Subsystem 的设计思路从头到尾拆一遍,包括架构划分、关键模块的 RTL 实现要点、FPGA 验证阶段容易踩的坑,以及 1.4 和 2.0 两个版本在实现上的核心差异。不管你是刚接触 HDMI 协议的 FPGA 工程师,还是正在做 SoC 显示子系统集成的设计者,应该都能从中找到对自己有用的东西。
1. 先搞清楚 HDMI Transmitter Subsystem 到底包含哪些东西
1.1 从系统视角看信号链路
很多人一上来就盯着 TMDS 编码器看,这没错,但不够。一个完整的 HDMI Transmitter Subsystem 远不止一个编码器加几个串行器那么简单。从系统层面看,它至少包含以下几个功能域:
- 视频处理通路:接收来自显示控制器或视频管线的像素数据,完成色彩空间转换(如果需要)、像素打包、时序缓冲。
- TMDS 编码与串行化:对每个色彩通道进行 8b/10b 风格的 TMDS 编码,然后通过 OSERDES 或专用 SerDes 以 10 倍像素时钟速率串行输出。
- 音频子系统:采集音频采样数据,按照 HDMI 规范打包成 Audio Sample Packet,在视频消隐期间插入到数据流中。
- 辅助数据通道:包括 InfoFrame 的生成与发送、EDID 读取(通过 DDC)、CEC 通信、以及 HDCP 加密引擎的集成。
- 时钟管理:像素时钟、TMDS 位时钟、音频主时钟之间的域交叉和相位关系管理。
这五个功能域不是孤立的,它们之间有明确的耦合关系。比如音频包的插入时机必须和视频时序严格对齐,InfoFrame 的更新必须和 VSYNC 同步,HDCP 的加密必须在 TMDS 编码之前完成。理解这些耦合关系,是做好架构划分的前提。
1.2 为什么 Subsystem 的边界划分很关键
在实际项目中,HDMI Transmitter 很少作为一个孤立的 IP 存在,它通常要挂到 AXI 或 AHB 总线上,由处理器配置寄存器,同时和显示控制器、音频控制器、DMA 等模块协同工作。所以 Subsystem 的边界划分直接影响到集成的难易程度和后续的复用性。
我的经验是,把 Subsystem 划分为三个层次比较合理:
| 层次 | 职责 | 典型接口 |
|---|---|---|
| 寄存器与配置层 | 寄存器读写、中断管理、时钟复位控制 | APB/AXI-Lite |
| 数据处理层 | 视频采集、音频采集、InfoFrame 组装、HDCP 加密 | AXI-Stream / Native Video |
| 物理编码层 | TMDS 编码、串行化、差分输出 | 并行 DDR 接口到 PHY |
这样划分的好处是,物理编码层可以针对不同的工艺节点或 FPGA 平台做替换,而数据处理层和寄存器层保持稳定。我在一个 28nm SoC 项目里就是这么做的,FPGA 验证阶段用的是 Xilinx 的 OSERDES,流片时换成了自研的 SerDes PHY,上层 RTL 几乎没动。
1.3 HDMI 1.4 和 2.0 在 Subsystem 层面的核心差异
这是很多人容易混淆的地方。HDMI 1.4 和 2.0 在协议层面的差异看起来只是带宽从 10.2Gbps 提升到 18Gbps,但在 Subsystem 设计上,影响是全方位的:
- TMDS 位时钟:1.4 最高 340MHz(每通道 3.4Gbps),2.0 最高 600MHz(每通道 6Gbps)。这意味着 2.0 对时序收敛的要求高了一个档次。
- 色彩深度与格式:1.4 支持 8/10/12-bit 色深,2.0 增加了 YCbCr 4:2:0 的支持,这对视频通路的像素打包逻辑有影响。
- Scrambling:2.0 引入了 TMDS Scrambling(仅针对 340MHz 以上),这是 1.4 完全没有的机制,需要在编码器之前增加扰码模块。
- 通道速率与时钟比:1.4 通常用 10:1 的串行化比,2.0 在 600MHz 时可能需要 20:1 或更高的串行化比,取决于 PHY 的能力。
所以如果你要设计一个同时兼容 1.4 和 2.0 的 Subsystem,Scrambling 模块和更宽的串行化通路是必须提前规划的。
2. TMDS 编码器的 RTL 实现:从算法到可综合代码
2.1 TMDS 编码算法回顾与硬件映射
TMDS 编码的本质是把 8-bit 像素数据转换成 10-bit 的直流平衡码流。算法分两个阶段:第一阶段根据输入数据的 1 的个数决定是否做异或/异或非变换,第二阶段根据运行中的直流平衡状态决定是否翻转。这个算法在软件里实现很简单,但在硬件里要做成流水线,需要仔细考虑。
我见过不少实现是把两个阶段放在一个时钟周期里完成,结果关键路径太长,在 600MHz 下根本跑不动。正确的做法是至少拆成两级流水:
// 第一级:计算变换后的数据 always @(posedge clk) begin ones_count <= count_ones(din); if (ones_count > 4 || (ones_count == 4 && ~din[0])) dout_stage1 <= din ^ 8'hFF; // 异或非变换 else dout_stage1 <= din; // 异或变换 // 同时计算变换后数据的 1 的个数和 0 的个数 ones_stage1 <= count_ones(dout_stage1); zeros_stage1 <= 8 - count_ones(dout_stage1); end // 第二级:直流平衡调整 always @(posedge clk) begin if (balance_counter == 0 || ones_stage1 == zeros_stage1) begin // 根据上一轮的状态决定是否翻转 dout_stage2 <= (ones_stage1 > 4) ? {~dout_stage1[9], dout_stage1[8:0]} : {1'b0, dout_stage1}; // 更新 balance_counter end else begin // 根据 balance_counter 的正负和当前数据的不平衡度做调整 end end这里的关键点是count_ones的实现。在 FPGA 里可以用 LUT 直接查表,在 ASIC 里可以用加法树。但要注意,8-bit 的 popcount 在 600MHz 下可能需要再打一拍,否则组合逻辑延迟会成为瓶颈。
2.2 控制周期与数据周期的切换逻辑
TMDS 不是一直传数据的。在视频消隐期间,通道 0 要传控制信号(HSYNC、VSYNC、CTL0-3),通道 1 和 2 要传前导码或保护带。这个切换逻辑看起来简单,但实际做的时候有几个坑:
第一个坑是前导码的生成。在数据周期开始之前,通道 1 和 2 需要发送 2-bit 的前导码,通道 0 需要发送 4-bit 的前导码。这些前导码的值取决于即将发送的数据类型(视频数据、音频包、InfoFrame 等)。如果前导码错了,接收端可能无法正确识别数据类型。
第二个坑是保护带的插入。在控制周期和数据周期之间,需要插入保护带,确保接收端的时钟恢复电路不会失锁。保护带的长度和内容在规范里有明确定义,但实际实现时容易和消隐期的其他信号混淆。
我的做法是单独做一个period_fsm模块,用状态机明确区分 Video Data Period、Data Island Period、Control Period 三种状态,每种状态下的通道输出用多路选择器切换。这样逻辑清晰,也方便后续调试。
2.3 编码器流水线深度与 600MHz 时序收敛
到了 HDMI 2.0 的 600MHz,时序收敛是最大的挑战。TMDS 编码器的组合逻辑路径包括 popcount、比较、异或、多路选择,如果不做流水线,在 28nm 工艺下大概只能跑到 300-400MHz。
我的经验是至少做三级流水:
- 第一级:输入寄存 + 初步的 1 计数
- 第二级:变换逻辑 + 精确的 1/0 计数
- 第三级:直流平衡调整 + 输出寄存
这样每级的组合逻辑深度控制在 6-8 个 LUT 以内,在 28nm 下跑到 600MHz 是比较稳妥的。在 FPGA 里,Xilinx 的 UltraScale+ 系列因为 LUT 结构不同,可能需要调整流水线划分,但总体思路一样。
还有一个技巧是把 popcount 拆成两级:先用 LUT 算 4-bit 的 popcount,再用加法器合并。这样比直接用一个 8-input 的 LUT 查表要快,因为 8-input LUT 在大多数 FPGA 架构里是不存在的。
3. 音频与辅助数据的插入机制
3.1 Audio Sample Packet 的组装时机
HDMI 的音频不是独立通道传输的,而是打包成 Audio Sample Packet,在视频的 Data Island Period 插入到 TMDS 数据流中。这就带来一个问题:音频采样率(比如 48kHz)和视频帧率(比如 60Hz)是异步的,怎么保证音频包在正确的时机插入?
答案是使用音频 FIFO + 帧同步的机制。音频采样先写入一个异步 FIFO,FIFO 的读侧由视频时序控制。每一帧的 Data Island Period 到来时,根据当前 FIFO 里的采样数和音频包的容量,决定这一帧插入多少个音频包。
这里的关键参数是N 值(每帧的音频采样数)和CTS 值(音频时钟与像素时钟的比值)。这两个值需要通过 ACR(Audio Clock Regeneration)包发送给接收端,接收端用它们来恢复音频时钟。如果 N 和 CTS 算错了,接收端恢复出来的音频时钟就会有偏差,表现为音频断续或变调。
我踩过的一个坑是:在 48kHz/60Hz 的场景下,N=6144、CTS=148500 是标准值,但如果像素时钟不是标准的 148.5MHz,而是 148.35MHz(比如 1080p59.94),CTS 需要相应调整。这个细节在规范里有说明,但很容易被忽略。
3.2 InfoFrame 的生成与更新策略
InfoFrame 是 HDMI 用来传递辅助信息的包,包括 AVI InfoFrame(视频格式信息)、Audio InfoFrame(音频格式信息)、Vendor Specific InfoFrame(厂商自定义信息)等。这些 InfoFrame 不是每帧都变,但一旦视频格式或音频格式发生变化,就需要及时更新。
我的实现方式是:为每种 InfoFrame 维护一个双缓冲(Double Buffer),处理器通过 APB 总线写入新的 InfoFrame 内容,硬件在 VSYNC 到来时自动切换缓冲区。这样避免了在帧中间更新 InfoFrame 导致的撕裂问题。
还有一个细节是InfoFrame 的校验和。HDMI 规范要求 InfoFrame 包的最后一个字节是校验和,接收端会验证。如果校验和错了,接收端可能直接丢弃这个包。所以硬件在组装 InfoFrame 时,必须自动计算校验和,而不是让软件去算。
3.3 DDC 通道与 EDID 读取的硬件实现
DDC(Display Data Channel)是 HDMI 用来读取接收端 EDID 的 I2C 通道。虽然它本质上就是一个 I2C 接口,但在 HDMI Transmitter Subsystem 里,它的实现有几个特殊之处:
- 时钟拉伸:HDMI 接收端可能会拉伸 DDC 时钟,硬件必须支持。
- EDID 分段读取:当 EDID 超过 256 字节时,需要通过 Segment Pointer 切换分段。
- 热插拔检测:HPD 信号的变化需要触发中断,通知软件重新读取 EDID。
我通常会在 Subsystem 里集成一个简单的 I2C Master,专门用于 DDC 通信,而不是复用系统里的通用 I2C 控制器。这样做的好处是时序可控,而且不会和其他 I2C 设备产生仲裁冲突。
4. HDCP 加密引擎的集成要点
4.1 HDCP 在数据通路中的位置
HDCP 加密是在 TMDS 编码之前进行的。也就是说,视频数据先经过 HDCP 加密(异或运算),然后再做 TMDS 编码。这个顺序不能反,否则接收端解密后会得到错误的数据。
在 Subsystem 里,HDCP 引擎通常是一个独立的模块,通过 AXI-Stream 接口插入到视频通路中。它需要和处理器交互来完成认证握手,但认证完成后,加密运算是逐像素实时进行的,不能有停顿。
4.2 认证状态机与密钥更新
HDCP 的认证过程比较复杂,涉及多个步骤:发送 Aksv、读取 Bksv、验证、计算 Km、生成 Ks、启动加密。这个过程由软件驱动,但硬件需要提供寄存器接口和中断支持。
一个容易忽略的点是密钥更新。HDCP 规范要求每 2 秒至少更新一次密钥(或者每 128 帧),如果更新失败,必须重新认证。硬件需要提供一个帧计数器,在达到阈值时触发中断,通知软件执行密钥更新。
我在一个项目里遇到过因为密钥更新不及时导致接收端每隔几秒黑屏一次的问题。排查了很久才发现是软件的中断响应延迟太大,后来把密钥更新的优先级提高,问题就解决了。
4.3 HDCP 2.2 与 1.4 的兼容性设计
如果你的 Subsystem 需要同时支持 HDCP 2.2 和 1.4,设计复杂度会显著增加。HDCP 2.2 使用 AES-128 加密,和 1.4 的流加密完全不同。通常的做法是集成两个独立的加密引擎,通过多路选择器切换。
但这里有一个坑:HDCP 2.2 的认证过程需要和接收端协商版本,如果接收端只支持 1.4,Transmitter 必须回退到 1.4。这个回退逻辑需要在软件和硬件之间做好协调,否则可能出现认证死锁。
5. FPGA 验证阶段的实战经验
5.1 用 OSERDES 实现 TMDS 串行化
在 FPGA 上验证 HDMI Transmitter,最常用的方案是用 Xilinx 的 OSERDESE2 或 Intel 的 SERDES。以 Xilinx 7 系列为例,OSERDESE2 支持最高 10:1 的串行化比,配合 5 倍或 10 倍的位时钟,可以实现 1.4 的 3.4Gbps 每通道。
具体配置时需要注意:
- DDR 模式:OSERDESE2 必须配置为 DDR 模式,否则串行化比要翻倍。
- 时钟关系:像素时钟和位时钟必须是 1:10 的关系(对于 10:1 串行化),且相位要调好。
- Output Buffer:TMDS 是电流驱动模式,FPGA 的普通 LVDS 输出可能不兼容,需要外接电平转换或使用专用的 HDMI 发送芯片。
我在一个 Artix-7 项目里直接用 OSERDESE2 驱动 HDMI 接口,发现信号质量不太理想,后来加了一级 TMDS 驱动器芯片(比如 TI 的 SN75DP159)才稳定下来。所以如果你的 FPGA 板子上没有专用的 HDMI 发送芯片,这一点要提前考虑。
5.2 眼图测试与预加重配置
HDMI 2.0 的 6Gbps 速率对信号完整性要求很高。在 FPGA 上,通常可以通过调整 Output Buffer 的预加重(Pre-emphasis)和摆幅(Swing)来优化眼图。
我的经验是:先用默认配置跑一遍,用示波器看眼图。如果眼图闭合,逐步增加预加重等级,直到眼图张开。但预加重也不是越大越好,过大的预加重会导致 EMI 超标。一般在实际项目中,我会把预加重设在中间档位,然后通过 PCB 走线优化来补偿。
5.3 用 ILA 抓取 TMDS 编码前后的数据
调试 HDMI Transmitter 时,最有用的工具是 ILA(Integrated Logic Analyzer)。我会在 TMDS 编码器的输入和输出各挂一个 ILA,抓取几帧数据,然后和预期值对比。
这里有一个技巧:用已知的测试图案。比如发送纯红色(R=0xFF, G=0x00, B=0x00),然后看编码后的 10-bit 数据是否符合 TMDS 规范。如果不符合,说明编码器有问题;如果符合但接收端显示不对,说明问题在串行化或物理层。
我还遇到过因为 ILA 采样时钟和像素时钟不同源导致的抓取数据错位问题。后来改成用像素时钟作为 ILA 的采样时钟,问题就解决了。
6. SoC 集成中的时钟与复位设计
6.1 像素时钟、TMDS 位时钟与音频时钟的域交叉
在 SoC 集成时,HDMI Transmitter Subsystem 通常涉及三个时钟域:像素时钟域、TMDS 位时钟域、音频时钟域。这三个时钟来自不同的 PLL,相位和频率关系复杂。
我的做法是:
- 视频通路:像素时钟域到 TMDS 位时钟域的跨接,用异步 FIFO 或握手信号。
- 音频通路:音频时钟域到像素时钟域的跨接,用异步 FIFO,深度要足够容纳一帧的音频采样。
- 寄存器接口:APB 时钟域到像素时钟域的跨接,用同步器或异步桥。
这里的关键是FIFO 深度的计算。以音频 FIFO 为例,如果音频采样率是 192kHz,视频帧率是 60Hz,每帧的音频采样数是 3200。FIFO 深度至少要能容纳两帧的采样数,即 6400 个采样,才能保证不会溢出或下溢。
6.2 复位序列与亚稳态防护
HDMI Transmitter 的复位序列很重要。如果复位顺序不对,可能导致 TMDS 输出在复位期间出现毛刺,接收端可能误判为有效信号。
我通常的复位顺序是:
- 先复位寄存器接口和配置逻辑
- 再复位视频和音频通路
- 最后复位 TMDS 编码器和串行器
释放复位的顺序相反。同时,所有跨时钟域的信号都要加两级同步器,防止亚稳态传播。
还有一个细节是热插拔检测(HPD)的处理。当 HPD 信号变化时,需要复位整个 Subsystem 并重新读取 EDID。这个逻辑要在硬件里做好,否则软件可能来不及响应。
6.3 低功耗设计考虑
在移动 SoC 里,HDMI Transmitter 的功耗是一个敏感指标。几个常用的低功耗手段:
- 时钟门控:在没有视频输出时,关闭 TMDS 位时钟和像素时钟。
- 电源域隔离:把 HDMI PHY 放在独立的电源域,不用时完全断电。
- 动态频率调整:根据视频分辨率动态调整像素时钟频率。
我在一个移动项目里,通过时钟门控把 HDMI Transmitter 的待机功耗从 15mW 降到了 2mW 以下。关键是要确保门控逻辑不会导致状态机丢失状态,所以门控前要先把状态机复位到一个已知状态。
7. 常见问题排查与调试技巧
7.1 接收端无显示或显示花屏
这是最常见的问题。排查思路如下:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无显示 | HPD 未拉高、EDID 读取失败 | 检查 DDC 通信、测量 HPD 电平 |
| 显示花屏 | TMDS 编码错误、时序不对 | 用 ILA 抓取编码前后数据 |
| 颜色不对 | 色彩空间转换错误 | 检查 AVI InfoFrame 的配置 |
| 间歇性黑屏 | HDCP 认证失败、密钥更新超时 | 检查 HDCP 状态寄存器 |
我的经验是,先确认 HPD 和 EDID 没问题,再看 TMDS 编码,最后查 HDCP。这个顺序能覆盖 90% 以上的问题。
7.2 音频断续或变调
音频问题通常和 ACR 包的 N/CTS 值有关。如果 N/CTS 算错了,接收端恢复的音频时钟就会有偏差。排查方法是:
- 用音频分析仪测量接收端恢复的音频时钟频率
- 和发送端的音频时钟频率对比
- 如果偏差超过 100ppm,检查 N/CTS 的计算
还有一个可能是音频 FIFO 溢出或下溢。可以在硬件里加一个 FIFO 水位计数器,通过寄存器读出来,看看是否在正常范围内。
7.3 时序不收敛的典型场景
HDMI 2.0 的 600MHz 时序收敛是老大难问题。除了前面说的流水线划分,还有几个技巧:
- 使用寄存器输出:所有跨模块的信号都打一拍再输出,减少组合逻辑路径。
- 避免宽多路选择器:宽多路选择器在 FPGA 里会消耗大量 LUT,而且延迟大。可以用分级多路选择器代替。
- 手动例化 DSP 或 BRAM:某些计数或存储逻辑用 DSP48 或 BRAM 实现,比用 LUT 更快。
我在一个 Kintex UltraScale 项目里,通过把 TMDS 编码器的 popcount 改用 DSP48 实现,把时序余量从 -0.3ns 提升到了 +0.2ns。
8. 从 1.4 升级到 2.0 的改造路径
8.1 Scrambling 模块的插入位置
HDMI 2.0 在 340MHz 以上引入了 Scrambling,目的是降低 EMI。Scrambling 是在 TMDS 编码之前进行的,用一个 LFSR 生成的伪随机序列和视频数据异或。
插入 Scrambling 模块时要注意:
- LFSR 的初始种子:规范里有定义,不能随便改。
- Scrambling 的使能条件:只在 340MHz 以上使能,以下不使能。
- 和 HDCP 的顺序:Scrambling 在 HDCP 加密之后,TMDS 编码之前。
8.2 更高串行化比的实现
从 1.4 的 10:1 串行化升级到 2.0 的 20:1,需要更宽的 OSERDES 或级联多个 OSERDES。在 Xilinx 的架构里,可以用两个 OSERDESE2 级联实现 20:1。但要注意时钟的分频比和相位关系。
8.3 向后兼容的设计策略
如果你的 Subsystem 要同时支持 1.4 和 2.0,最好的策略是参数化设计。把串行化比、Scrambling 使能、时钟频率等做成参数,在例化时根据配置选择。这样一套 RTL 可以覆盖两种模式,减少维护成本。
我在一个项目里就是这么做的,通过一个HDMI_VERSION参数控制,综合出两个不同的网表,分别用于 1.4 和 2.0 的场景。实测下来,资源开销只增加了 15% 左右,但省去了维护两套代码的麻烦。
9. 写在最后的一些个人体会
做 HDMI Transmitter Subsystem 这些年,最大的感受是:协议规范要读透,但不能只读规范。规范告诉你应该怎么做,但不会告诉你实际做的时候哪里会出问题。比如 TMDS 编码的直流平衡,规范里只给了算法,但实际实现时流水线怎么切、popcount 怎么优化,全靠经验积累。
另一个体会是验证要趁早。HDMI 的问题往往在系统联调时才暴露,这时候改硬件的成本很高。所以我在项目里会尽早搭建 FPGA 验证平台,哪怕是一个简化版的,也能提前发现大部分问题。
最后,不要忽视软件的作用。HDMI Transmitter 的很多功能,比如 EDID 解析、HDCP 认证、InfoFrame 更新,都需要软件配合。硬件设计时要考虑软件的易用性,比如寄存器布局要清晰、中断要明确、状态要可读。这样软件同事调试起来也轻松,整个项目的进度也会更顺。