☰
FPGA实战:RGB转TMDS编码原理与HDMI接口实现全解析
2026/9/30 1:05:35 网站建设 项目流程

第一次在FPGA上调HDMI输出,我对着黑屏显示器坐了三个小时。逻辑分析仪显示RGB数据和同步信号全对,仿真波形也看不出问题,直到我把TMDS编码器的最终输出抓下来,才意识到自己对“数据编码”的理解有多粗糙——我直接把8位像素按原样送了进去,根本没有做8b到10b的变换。TMDS(Transition Minimized Differential Signaling,过渡最小化差分信号)这套数据编码算法,是HDMI和DVI物理层的底层基础,RGB转TMDS则是FPGA上做视频输出的必经之路。这篇文章把我踩过的坑和整套算法的细节一次说清楚,适合正在调FPGA视频接口、或者想彻底搞明白HDMI底层原理的开发者。

1. 为什么视频传输偏偏要用TMDS这套编码

1.1 从DVI到HDMI:TMDS的身世

TMDS来自Silicon Image公司,1999年随DVI 1.0标准发布,2002年HDMI 1.0直接继承了这套物理层编码。直到HDMI 2.0时代,TMDS都还是绝对主力——1080p@60Hz下单通道数据率1.485Gbps,4K@60Hz需要跑到6Gbps,底层编码算法本质上没变过。只有到了HDMI 2.1才全面换用FRL(Fixed Rate Link),TMDS逐步退居兼容模式。

所以现在市面上能买到的绝大多数HDMI设备,里面跑的仍然是TMDS。你在FPGA上做一个RGB转TMDS的发送器,就相当于手工实现了一个简化版HDMI发送端的核心。

1.2 三个绕不开的工程问题

任何高速串行传输都要面对三个基础问题,TMDS的编码设计完全是围绕它们展开的。

第一个是直流平衡。视频数据不是均匀的随机信号,一张纯白画面的像素全是0xFF,一张纯黑画面全是0x00。如果直接把这些原始数据串行发出去,线路上的平均电平会长时间偏向高或低,接收端没法做交流耦合,也无法稳定恢复时钟。TMDS解决方式是引入“运行差异”计数器,保证任意一段码流中1和0的数量长期趋于相等。

第二个是电磁干扰和信号完整性问题。如果数据流中0→1和1→0的翻转非常频繁且随机,高速差分线上的辐射会变大,串扰也会变严重。TMDS的阶段一编码专门做了“过渡最小化”,让输出码流中相邻位的翻转次数尽量少。

第三个是时钟恢复。HDMI链路上除了专门传像素时钟的通道之外,三路数据通道本身不带独立时钟,接收端需要从数据边沿恢复出位时钟。这就要求数据流中必须有足够多的跳变沿,不能出现长时间不变的电平。TMDS的编码规则保证了即使数据全0或全1,输出码流也始终有节奏地翻转。

1.3 一条TMDS链路长什么样

标准TMDS链路有4对差分线:3对数据通道加1对时钟通道。RGB三个颜色分量各占一条数据通道,每个像素时钟周期传输一个10位的TMDS符号。时钟通道传输的就是像素时钟本身,接收端以它为参考重建整个时序。

数据通道里传输的内容分成两类:DE有效时传视频像素,DE无效时传控制信号(比如HSYNC和VSYNC)。视频像素经过完整的两阶段8b/10b编码,控制信号则直接映射成固定10位模式。DE信号本身不会单独占用一根线,它是通过码流中标志位的不同来区分的——这一点在FPGA实现时尤其容易混淆。

2. 编码算法逐拍拆解:8位数据如何变成10位符号

2.1 阶段一:过渡最小化(Transition Minimization)

对每个8位像素数据D[7:0],先计算一组“前缀异或”序列N[7:0]:

N[0] = D[0] N[i] = N[i-1] ^ D[i] (i = 1..7)

然后统计N[0]到N[6]这7位中1的个数。如果1的个数大于4,或者等于4且D[0]为0,就判定为“需要反转”,否则不反转。这个决策条件看起来有点绕,实际上是统计数据中的“位分布倾向”,决定q_m采用正常序还是反相序,目的是让后续输出的相邻位翻转尽量少。

根据决策结果生成q_m[7:0]:

if (decision == 0) q_m[i] = q_m[i-1] ^ N[i] else q_m[i] = q_m[i-1] ^ ~N[i]

注意这里q_m[0]始终等于D[0],逐位递推而不是整体取反。这个细节非常关键,网上很多简化代码直接写q_m = ~N或q_m = N,看起来能用,实际上生成的码字不符合标准推导,接收端按标准反解时可能还原出错位。

我拿一个真实数据走一遍。假设D = 0xB1,即二进制10110001(bit7到bit0依次为1、0、1、1、0、0、0、1):

N[0]=1 N[1]=1^0=1 N[2]=1^0=1 N[3]=1^0=1 N[4]=1^1=0 N[5]=0^1=1 N[6]=1^0=1 N[7]=1^1=0

N[7:0] = 01110111(0x77),统计N[0]到N[6]得6个1,大于4,决策为1。然后递推:

q_m[0]=1 q_m[1]=1 ^ ~1 = 0 q_m[2]=0 ^ ~1 = 0 q_m[3]=0 ^ ~1 = 0 q_m[4]=0 ^ ~0 = 1 q_m[5]=1 ^ ~1 = 0 q_m[6]=0 ^ ~1 = 0 q_m[7]=0 ^ ~0 = 1

得到q_m = 10000111(0x87)。整个过程必须按位递推,不能用简单取反替代,这是我对这个算法最深的体会之一。

2.2 阶段二:直流平衡(DC Balancing)

q_m只有8位,要变成10位,还差2位。这2位不只是补位,而是承载了直流平衡的核心逻辑。

设q_m中1的个数为k,则k的范围是0到8。编码器内部维护一个运行差异计数器cnt,记录“我已经累计发送了多少个多余的1”。每次编码后,cnt会更新,目标是让cnt尽量回到0附近。

规则分三种情况:

  • 如果cnt为0,或者k等于4(本次本身平衡),输出q_m或~q_m,具体看cnt的符号(cnt为正说明之前1多,本次选~q_m来抵消;cnt为负则选q_m),然后cnt按本次实际差异更新,如果k为4则cnt清零。
  • 如果cnt为正且k大于4,或者cnt为负且k小于4,说明本次的方向和累计偏差方向相同,需要反转q_m输出来对冲,cnt减去本次正差异。
  • 其余情况,直接输出q_m,cnt加上本次差异。

这里的“差异”等于2k-8(1的个数减去0的个数),每次更新后cnt理论上会向0回归,长期看线路上的直流电平稳定。标准中cnt一般限制在±4范围内,实际实现时建议用5位有符号数避免极端情况下溢出。

接收端拿到10位符号后,先看bit8判断是不是数据符号,再看bit9决定是否把后8位取反,恢复出q_m,再反推原始数据。整个链路是双向可逆的,这正是TMDS编码设计严谨的地方。

2.3 控制字符:DE为0时发什么

DE为0时,不传像素,传控制信号。HDMI的每条数据通道在控制期有2位控制输入,映射成固定的10位编码。常用映射如下:

控制输入ctrl[1:0]10位输出
001101010100
010010101011
100101010100
111010101011

这些控制码本身大多接近DC平衡,接收端看到这类模式就知道当前处于消隐区,同步信号的边沿就通过这些码字的切换来传递。实际工程中,HSYNC和VSYNC通常映射到蓝色通道的ctrl[1:0]上,RGB三通道分配哪一路由设计者决定,不同HDMI发送芯片的映射可能有差异。

提示:控制字符的具体位序和极性在不同资料里存在差异。我做项目时习惯直接以HDMI规范中的CTLx编码表为准,HDL代码里用常量表形式给出,调试时优先比对示波器上的波形而不是凭记忆核对码值。

2.4 解码端视角:怎样才能完整还原

把编码算法理解透,最好的检验方式是站在接收端想一遍。接收端收到10位符号后:

  • 检查bit8。如果为1,表示这是视频数据;如果为0,说明是控制符号,查表还原出2位控制信号。
  • 对于视频符号,根据bit9判断信号极性,恢复出8位q_m。
  • 从q_m反推原始数据D:q_m[0]就是D[0],后面每一位根据阶段一的递推关系进行逆运算。

接收端的反推不需要知道发送端当时decision的值,因为q_m本身就携带了全部信息。这也是为什么发送端必须严格按标准递推生成q_m,任何简化取反都会让接收端还原得牛头不对马嘴。

3. FPGA落地:RGB转TMDS的完整实现路径

3.1 顶层架构

FPGA上做RGB转TMDS发送器,整体分四块:像素源、TMDS编码器、并行转串行、时钟生成。

像素源输出24位RGB、HSYNC、VSYNC、DE和像素时钟。三个颜色通道各接一个TMDS编码器,每个编码器输出10位符号。然后三个10位符号同步进入串行化模块,变成3对高速差分数据。时钟通道单独处理,把像素时钟直接通过DDR输出原语变成一对差分时钟。

我画过不少版本,最核心的教训是:编码器工作在像素时钟域,串行化模块工作在5倍像素时钟域,这两个时钟域之间的数据交接必须安排得明明白白。很多黑屏问题不是编码错,而是时序跨域没处理好。

3.2 核心编码器的Verilog实现

下面是一个可直接综合的TMDS编码器模块。这个模块我至少用了三年,从720p到1080p都跑过,逻辑本身没问题:

module tmds_encoder ( input wire clk, input wire [7:0] data_in, input wire [1:0] ctrl_in, input wire de_in, output reg [9:0] tmds_out ); reg [7:0] n; // 前缀异或序列 reg [7:0] q_m; // 过渡最小化后的8位 reg [3:0] ones_n; // N[0]..N[6]中1的个数 reg decision; integer i; // 阶段一:计算N序列 always @(*) begin n[0] = data_in[0]; for (i = 1; i < 8; i = i + 1) n[i] = n[i-1] ^ data_in[i]; end // 统计N[0]..N[6]中1的个数,注意是7位 always @(*) begin ones_n = 4'd0; for (i = 0; i < 7; i = i + 1) ones_n = ones_n + n[i]; end // 决策:1的个数>4,或==4且D[0]==0时反转 always @(*) begin decision = (ones_n > 4'd4) || ((ones_n == 4'd4) && ~data_in[0]); end // 递推生成q_m always @(*) begin q_m[0] = data_in[0]; for (i = 1; i < 8; i = i + 1) q_m[i] = q_m[i-1] ^ (decision ? ~n[i] : n[i]); end // 阶段二:直流平衡 reg signed [4:0] cnt; // 运行差异计数器 reg [3:0] ones_q_m; // q_m中1的个数 wire [4:0] diff_qm; // 本次q_m对应差异 2k-8 always @(*) begin ones_q_m = 4'd0; for (i = 0; i < 8; i = i + 1) ones_q_m = ones_q_m + q_m[i]; end assign diff_qm = {1'b0, ones_q_m, 1'b0} - 5'd8; // 等价于2*k-8 always @(posedge clk) begin if (de_in) begin if (cnt == 0 || ones_q_m == 4) begin // cnt非0且本次平衡时,按cnt符号选择极性 if (cnt <= 0) begin tmds_out[9] <= 1'b1; tmds_out[8] <= 1'b1; tmds_out[7:0] <= q_m; end else begin tmds_out[9] <= 1'b0; tmds_out[8] <= 1'b1; tmds_out[7:0] <= ~q_m; end if (cnt == 0) cnt <= diff_qm; else cnt <= 0; end else if ((cnt > 0 && ones_q_m > 4) || (cnt < 0 && ones_q_m < 4)) begin tmds_out[9] <= 1'b0; tmds_out[8] <= 1'b1; tmds_out[7:0] <= ~q_m; cnt <= cnt - diff_qm; end else begin tmds_out[9] <= 1'b1; tmds_out[8] <= 1'b1; tmds_out[7:0] <= q_m; cnt <= cnt + diff_qm; end end else begin case (ctrl_in) 2'b00: tmds_out <= 10'b1101010100; 2'b01: tmds_out <= 10'b0010101011; 2'b10: tmds_out <= 10'b0101010100; 2'b11: tmds_out <= 10'b1010101011; default: tmds_out <= 10'b1101010100; endcase cnt <= 0; end end endmodule

代码里有几个点必须单独说。diff_qm的计算我用移位减法的写法是为了避免综合器把它优化出问题,实际等价于2*k - 8。统计ones_n时标准只统计7位,不是8位,这个细节曾经让我花了一晚上对比标准文档。控制字符的case里,我把默认分支也写上了,防止综合后产生锁存器。

3.3 并行转串行:DDR与OSERDES的取舍

10位并行数据要变成1对高速差分信号,关键是串行化怎么做。我整理过三种常见方案:

方案时钟需求资源占用适用场景
OSERDESE2 10:1 SDR10倍像素时钟少,需级联主从高分辨率,速率要求高
OSERDESE2 5:1 DDR5倍像素时钟少1080p附近,最常用
移位寄存器+ODDR5倍像素时钟中等,逻辑实现低端FPGA,教学验证

以1080p@60Hz(像素时钟148.5MHz)为例,数据率1.485Gbps。7系列FPGA的OSERDESE2在DDR模式下,用742.5MHz时钟即可驱动,这个频率在器件规格内。如果用10倍时钟做SDR,1.485GHz的全局时钟布线压力会非常大,一般不推荐。

DDR模式的实现逻辑很直观:10位符号拆成高5位和低5位,每个5倍时钟周期内,上升沿输出一位、下降沿输出一位,5个时钟周期刚好把10位发完。OSERDESE2原语本身支持这种用法,Xilinx的SelectIO IP也封装好了。

Altera平台对应的是ALTDDIO_OUT,Lattice平台用DDR输出寄存器加逻辑移位。不管什么平台,核心思想一致:5倍时钟做DDR传输,数据通道和时钟通道之间做相位对齐。

3.4 时钟方案:5倍像素时钟与相位对齐

时钟是整个发送链路的心脏。我的经验是先确定像素时钟,再用MMCM/PLL生成5倍频时钟:

  • 720p@60Hz:像素时钟74.25MHz,5倍时钟371.25MHz
  • 1080p@60Hz:像素时钟148.5MHz,5倍时钟742.5MHz
  • 1080p@50Hz:像素时钟148.5MHz(实际是148.5/1.001,做开发板功能验证时不纠结)

时钟通道输出的像素时钟本身,通过ODDR原语固定输出模式实现差分对:一个DDR寄存器在上升沿输出1、下降沿输出0,这样每两个TMDS位周期产生一个完整时钟周期,正好一个像素时钟周期内有10个位周期。

数据通道和时钟通道之间的相位关系是重点。我遇到过画面能点亮但偶尔闪一下的情况,最后定位到是数据通道的串行边界和时钟通道边沿偏离了理想位置。解决方法是利用MMCM的fine phase shift微调串行时钟相位,或者在数据通道上插入可调的IDELAY。具体调整值要靠扫相位实测,没有一次到位的捷径。

3.5 硬件连接:电平标准与PCB注意事项

FPGA的IO标准直接决定信号能不能被显示器正确识别。TMDS是电流型差分信号,规范要求共模电平约3.3V、差分摆幅约±10mA电流在50欧端接上产生约500mV摆幅。

老一点的FPGA方案里,直接支持TMDS_33 IO标准的器件不少。Vivado里给差分端口分配TMDS_33即可,这时驱动能力最接近规范。如果器件只支持LVDS_25,也可以点起来,但驱动幅度偏低,短距离(几十厘米)没问题,线长了就掉链子。

我在实际开发板上经常碰到的情况是:FPGA差分IO直接连HDMI座子,IO标准配LVDS_25。这种做法做验证完全够用,但不适合做产品——要么加TMDS电平转换芯片(比如TFP410、SIL171),要么选用原生支持TMDS电平的FPGA bank。

PCB布局走线要注意三件事:四对差分线尽量等长,对内P/N偏差控制在50密耳以内;差分阻抗控制在100欧;电源去耦要做好,高速切换时IO电源纹波会影响信号质量。我在一块手工焊接的板上踩过坑,VCCIO波纹稍微大一点,屏幕就开始出横纹。

4. 调试实录:从黑屏到点亮的排查链路

4.1 先仿真:编码器输出对不对

上板之前一定先仿真,这是我能给出的最诚恳的建议。仿真用例不要用随机数据,直接用彩条测试图样,这样可以直观判断输出码流是否合理。

仿真重点看三件事。第一,DE为0时,输出是否是固定的控制编码表里的值,且每两个像素时钟周期切换一次。第二,DE为1时,10位输出的bit8必须是1。第三,跑几百个像素周期后看cnt是否保持在合理范围,如果cnt一直往一个方向跑偏,说明直流平衡逻辑有bug。

我习惯在testbench里写一个简单的解码模块,把TMDS输出解回RGB,再和原始像素比对。只要解码比对通过,编码器的问题基本就排除了。

4.2 硬件层面:从黑屏到点亮的排查链条

上板后黑屏,不要慌着改代码。按顺序查,一个问题一个问题排除:

第一查PLL锁定。用ILA抓MMCM的locked信号,没锁定说明时钟输入有问题,或者配置参数不对。

第二查时钟通道。用示波器看差分时钟对是否有输出,频率是否符合像素时钟。没有时钟输出,后面一切免谈。

第三查数据通道波形。示波器看三对差分数据是否有对应频率的跳变,如果有一对完全没波形,检查对应的编码器输出是否卡在固定值。

第四查DE和同步时序。DE的极性、HSYNC/VSYNC的极性如果和显示器要求不一致,也会黑屏。有的显示器对同步信号极性和DE的时序容忍度很低。

第五查串行顺序。10位并行转串行时,发送顺序是从bit0开始还是从bit9开始,不同规范和不同IP的约定可能不同。这个坑最隐蔽,症状是画面能出来但颜色和位置错乱。

4.3 常见故障对照表

现象可能原因检查方向
完全黑屏时钟通道没输出、PLL未锁定查MMCM配置、时钟输入
黑屏但时钟通道正常编码器卡在控制字符、DE没识别查DE极性、编码器状态
花屏、雪花数据通道时序对齐差、串行时钟相位偏扫相位、加IDELAY
颜色错乱通道映射错误、位序不对查串行顺序、RGB通道连接
颜色偏色某通道直流平衡异常查cnt复位、编码器输出
间歇性黑屏/闪屏电源纹波、差分走线不等长查供电、调整I/O强度

4.4 我踩过的一个坑:10位符号的发送顺序

有一次画面能显示,但是整体颜色像是RGB通道互换后又错位了一位,我排查了很久才发现是串行化模块的位序问题。TMDS规范中符号的发送顺序是固定的,但FPGA的OSERDESE2原语和移位寄存器实现方式不同,对“哪一位先出去”的约定可能不一样。当时我把10位符号的最终输出顺序反过来,一切就正常了。

这个坑几乎每个人都会踩一次。建议在串行化模块的接口处加一个位序反转的配置参数,调试时可以先试默认顺序,不行就翻过来,能省大量时间。

5. 扩展思考:从8b/10b家族看TMDS的定位

5.1 TMDS与8b/10b编码的异同

TMDS看起来像是8b/10b编码的变体,但实际上有本质区别。两者都是8位数据扩成10位、效率都是80%、都带直流平衡,但设计侧重点不同:

维度TMDS8b/10b
编码目标最小化翻转、DC平衡保证翻转密度、DC平衡
控制字符固定编码,2位控制K码,5位控制
解码方式递推逆运算查表
典型场景HDMI/DVIPCIe、USB3.0、千兆以太网

8b/10b的著名劣势之一是实现复杂、需要查表;TMDS把控制信息压缩成更少的组合,电路规模更小,这也是当年DVI选择它的原因之一。

5.2 Deep Color、HDR对编码算法的挑战

像素位宽超过24位后,TMDS编码算法本身不用改,变的是数据的吞吐方式。30位或36位深色模式下,要么用更高的TMDS时钟,要么分时复用多路通道。HDMI 1.4的深色模式就是用提高TMDS时钟频率实现的——像素时钟变成原来的1.25倍、1.5倍或2倍。

对FPGA实现的影响是:编码器逻辑不变,但串行化部分的速率压力变大。30位深色1080p需要1.85Gbps单通道速率,7系列FPGA的普通IO已经逼近极限,这时就得考虑更高级的GTX/GTH收发器或者换用HDMI 2.0的FRL方案。

HDR本身不改变TMDS编码,它只是改变了像素数据的格式(比如宽色域、高位深)。如果你在FPGA上做HDR视频输出,编码器部分完全不用动,处理好像素格式转换就行。

5.3 什么情况下可以绕开TMDS

做产品时没必要把所有场景都硬往TMDS上套。板内短距离RGB并口传输,根本不需要编码;传感器接口用MIPI或LVDS更合适;工业相机走Camera Link或CoaXPress。只有需要接HDMI/DVI显示器的场景,TMDS才是必须品。

我自己的体会是:把TMDS编码算法彻底搞懂,最大的收获不是背下了那套递推公式,而是理解了“为什么高速接口需要编码层”这个通用问题。之后再看PCIe、以太网、USB,它们那些复杂的加扰、编码机制,底层逻辑都是一脉相通的。

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

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

立即咨询