news 2026/9/30 3:36:08

TMDS编码算法解析:FPGA实现RGB转HDMI的关键技术

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TMDS编码算法解析:FPGA实现RGB转HDMI的关键技术

搞过视频显示的朋友,对TMDS这三个字母应该不陌生。DVI和HDMI的物理层,全线都是这套数据编码算法。我第一次正面和它打交道,是在用FPGA做RGB信号转HDMI输出的时候,原本以为不就是把24位像素并转串发出去嘛,结果实际走一遍流程才发现,里面编码的那几步大有讲究。当时照着参考设计抄了一遍没多想,直到后来自己从零写编码器,被花屏和偏色折磨了两天,才老老实实把TMDS的算法原理啃完。

这篇就围绕TMDS数据编码算法展开,把8b/10b编码的两个阶段讲透,顺便给出我在FPGA上实现RGB转TMDS的完整思路和调试记录。适合刚入手视频链路设计、或者想搞懂DVI/HDMI物理层编码的朋友。

1. TMDS是什么,为什么显示接口离不开它

1.1 直接送并行RGB行不行

先从最简单的直觉说起。传统的RGB并行接口,比如VGA或者MCU并口屏,R、G、B各8位,加上时钟和同步信号,线缆动辄十几根线。低频低分辨率下这没问题,但到了1080p@60Hz,像素时钟是148.5MHz,并行总线的数据率已经到了3.5Gbps以上,线间串扰、EMI、阻抗匹配全都不好做。HDMI/DVI的解法,是把RGB三路加上控制信号打包成三条串行差分链路,每条链路速率是像素时钟的10倍,再单独送一路像素时钟作为参考。

这时候问题就来了:串行链路上发送的是连续比特流,如果是裸数据,那数据里0和1的分布完全不可控。万一连续几百个bit都是同一个电平,接收端没法恢复时钟;万一数据里跳变太多,高频分量压不住,EMI超标,信号还会被传输线衰减得面目全非。所以必须把原始8位数据先编码成10位,让数据流在统计上具备两个特性:跳变次数可控、直流分量稳定。这就是TMDS编码要解决的根问题。

1.2 两个核心指标:最小化跳变和直流平衡

TMDS把8bit转成10bit分成了两级。第一级做“最小化跳变”,第二级做“直流平衡”。理解这两级分别干了什么,就理解了整套算法。

最小化跳变的目标,是让编码后的码字内部以及相邻码字之间,0和1的切换次数尽可能少。信号线上电平跳变少,一方面降低了EMI辐射,另一方面接收端采样更容易,抖动裕量更大。差分信号虽然抗干扰强,但转换太频繁一样会带来功耗和发热问题。TMDS通过统计当前数据中1的个数,选一个合适的反转策略,把9位中间码字里1的数量压到很小的范围。1少就意味着连续0多,连续0在串行流里不会产生跳变,整体跳变密度自然降下来。

直流平衡解决的是另一个问题。差分信号接收端只关心两线之间的压差,但链路里如果有交流耦合、隔直电容或者变压器,长时间发送同一电平会让信号摆幅漂移,判别阈值不稳。TMDS的做法是维护一个运行偏差计数器cnt,统计到目前为止发送的1比0多了多少。每次编码新码字时,根据cnt的正负决定当前码字是否取反,让1的个数分布始终围绕0均值附近抖动。这两点做到位,高速串行链路上才能稳定传输。

2. TMDS 8b/10b编码算法逐级拆解

2.1 第一级编码,把8bit压成9bit

第一级编码把8bit输入D[7:0]变换为9bit中间值q_m[8:0]。标准过程如下。

先数一下D中有几个1,记为N1,范围是0到8。然后做判断:

  • 如果N1大于4,或者N1等于4且D[0]为0,就把D逐位取反得到q_m[7:0],同时q_m[8]置0;
  • 否则,q_m[7:0]保持D不变,同时q_m[8]置1。

写成伪代码就是下面这样:

N1 = count_ones(D); if (N1 > 4 || (N1 == 4 && D[0] == 0)) begin q_m[7:0] = ~D; q_m[8] = 0; end else begin q_m[7:0] = D; q_m[8] = 1; end

这里的q_m[8]其实是一个“是否取了反”的标志位,接收端看到它就知道要不要把q_m[7:0]再反转回去,从而还原出原始D。至于为什么在N1等于4且D[0]等于0这个边界条件下也要反转,是为了把q_m里1的数量分布压低。经过这一级后,q_m[8:0]中绝大多数情况下1的数量在1到5之间,只有全0或全1输入这种极端情况才会越界。

这段代码是TMDS编码的核心骨架,很多参考设计里都能看到完全一样的逻辑。但我要提醒一句:这里N1的统计必须在组合逻辑里完成,不要在always块里用时钟驱动纯计数,否则时序上会没命地加路径延迟。工程上一般用assign加循环,或者直接查表。

2.2 第二级编码,把9bit扩成10bit

第二级编码把9bit的q_m扩展成10bit的q_out,同时维护一个运行偏差值cnt。先算出q_m中1的个数N1_qm,0的个数就是9减N1_qm。然后看cnt的情况:

  • 如果N1_qm大于4,或者N1_qm等于4且cnt等于0,则输出q_out[9]为0,q_out[8:0]为~q_m;
  • 否则,输出q_out[9]为1,q_out[8:0]为q_m。

cnt的更新规则跟着输出走:

  • 取反输出时,cnt加上本次码字里0的数量减去1的数量,也就是cnt = cnt + N0_qm - N1_qm;
  • 不取反输出时,cnt加上本次码字里1的数量减去0的数量,也就是cnt = cnt + N1_qm - N0_qm。

初始时cnt为0。长期来看,cnt会在零附近来回摆动。如果你的工程只跑标准视频分辨率,4bit计数器基本够用,但严谨一点建议用5bit或8bit。原因后面调试部分会提到。

这里有个容易忽略的细节:N1_qm等于4且cnt等于0这个分支。当1和0数量相等时,无论取不取反,直流贡献都一样,但规范里明确规定此时要取反。你不需要纠结为什么,FPGA实现时照做就行,但分支不能丢,否则在某些固定图案下会出现信道失锁,表现就是偶发花屏,想破头都猜不到是这里出的问题。

2.3 亲手推一遍编码全过程

用具体数据过一遍流程,比背公式清楚得多。

假设输入D = 8'b10110010。

先数1的个数:第7位是1、第5位是1、第4位是1、第1位是1,总共4个。N1等于4,且D[0]等于0,所以第一级取反,得到q_m[7:0] = ~D = 8'b01001101,q_m[8] = 0。数一下q_m[8:0]里1的个数,是4个。

假设当前cnt = 0。N1_qm = 4,满足N1_qm等于4且cnt等于0,执行取反输出。q_out[9] = 0,q_out[8:0] = ~q_m = 9'b110110010,所以完整的q_out[9:0] = 10'b0110110010。这个码字里1的个数是5,0的个数也是5,直流贡献为零。cnt更新为0加5减4,等于1。

再看一个极端输入D = 8'b11111111。N1 = 8,大于4,第一级取反,q_m[8:0] = 9'b000000000,一个1都没有。N1_qm = 0,假设此时cnt = 1,不满足N1_qm大于4,也不满足N1_qm等于4且cnt等于0,所以不取反。q_out[9] = 1,q_out[8:0] = 000000000,输出10'b1000000000。cnt更新为1加0减9,等于负8。

全1输入对直流平衡是很大的考验,但视频数据里很少连续出现这种图案,TMDS的反馈机制会在后续码字里逐步把cnt拉回来。这两个例子虽然极端,却足够把算法的两个阶段走通了。

3. FPGA实现RGB转TMDS的完整方案

3.1 顶层架构和时钟分配

FPGA做视频输出,典型的数据通路是这样的:上游模块按像素时钟给出24位RGB和行场同步、DE信号,FPGA内部把24位RGB拆成R、G、B三路,各进一个TMDS编码器,得到三路10位并行编码数据;编码器在DE拉低时还要插入特定的控制字符;最后,三路10位数据分别通过高速串行器,以像素时钟10倍的比特率逐位发出,再经过差分缓冲器转成差分对送到HDMI/DVI连接器。时钟通道不需要编码,直接把像素时钟送出去就可以。

顶层模块的端口大致如下:

module rgb2tmds ( input wire pix_clk, // 像素时钟,也是TMDS时钟通道频率 input wire bit_clk, // 5倍像素时钟,用于DDR串行化 input wire [23:0] rgb_data, input wire hsync, input wire vsync, input wire de, output wire [2:0] tmds_p, output wire [2:0] tmds_n, output wire tmds_clk_p, output wire tmds_clk_n );

很多初学者会误解成需要一个10倍像素时钟的串行时钟。实际上,10:1的串行化在FPGA里通常用DDR方式实现,也就是上下沿各输出一个bit,所以bit_clk只需要5倍像素时钟。比如1080p@60Hz,pix_clk为148.5MHz,bit_clk为742.5MHz。这个频率在很多中端FPGA上是可行的。如果老老实实用10倍时钟做单沿移位,1485MHz的全局时钟布线会非常困难,普通LUT根本跑不动。

时钟源一般用一个PLL或者MMCM同时生成pix_clk和bit_clk,保证严格的5倍关系。在Xilinx里就是MMCM/PLL的两个输出,在Intel/Altera里是ALTPLL的两个输出。相位关系也要注意,bit_clk和pix_clk的上升沿最好对齐,否则OSERDES内部采并行数据时可能在边沿附近抖动。

3.2 三通道TMDS编码器实现

TMDS编码器是整套系统的核心。下面这个Verilog模块是我实际调试用的精简版本,去掉了复位逻辑和跨时钟处理。三个通道各例化一份,DE同时用于控制字符切换。

module tmds_encoder ( input wire clk, input wire de, input wire [7:0] data, input wire [1:0] ctrl, output reg [9:0] tmds ); reg [3:0] n0d; reg [3:0] n0q_enc; reg [4:0] dc_cnt; reg [8:0] q_m; reg [9:0] q_out; integer i; // 统计输入数据中1的个数 always @(*) begin n0d = 0; for (i = 0; i < 8; i = i + 1) begin n0d = n0d + data[i]; end end // 第一级编码 always @(*) begin if (n0d > 4 || (n0d == 4 && data[0] == 0)) begin q_m[7:0] = ~data; q_m[8] = 1'b0; end else begin q_m[7:0] = data; q_m[8] = 1'b1; end end // 统计 q_m 中1的个数 always @(*) begin n0q_enc = 0; for (i = 0; i < 9; i = i + 1) begin n0q_enc = n0q_enc + q_m[i]; end end // 第二级编码与输出寄存器 always @(posedge clk) begin if (de) begin if (n0q_enc > 4 || (n0q_enc == 4 && dc_cnt == 0)) begin q_out[9] = 1'b0; q_out[8:0] = ~q_m; dc_cnt <= dc_cnt + (5'd9 - n0q_enc) - n0q_enc; end else begin q_out[9] = 1'b1; q_out[8:0] = q_m; dc_cnt <= dc_cnt + n0q_enc - (5'd9 - n0q_enc); end end else begin case (ctrl) 2'b00: q_out <= 10'b1101010100; 2'b01: q_out <= 10'b0010101011; 2'b10: q_out <= 10'b0101010100; 2'b11: q_out <= 10'b1010101011; default: q_out <= 10'b1101010100; endcase dc_cnt <= 5'd0; end tmds <= q_out; end endmodule

这里有几个要点。

第一,dc_cnt我声明成了5bit。别看这个计数器小,在高分辨率纯色画面下,如果位宽不够,cnt可能上下溢出,导致编码结果突变,画面出现周期性的条纹。仿真可能看不出来,上板才会暴露。

第二,DE无效时期,通道0的ctrl一般接hsync和vsync的组合。DVI规范规定DE为低时通道0必须发送特定的控制字符,不能随便填0。这里case里写的四个10bit常量就是标准控制码型,顺序对应ctrl的00、01、10、11。如果做纯DVI输出,通道1和通道2在DE无效时也建议发送固定控制字符,最简单的做法是让它们的ctrl接2'b00。

第三,tmds输出打了一拍寄存器。这一拍不是必须的,但能改善时序,把编码器内部的组合逻辑和OSERDES输入寄存器隔开。代价是数据整体延迟一拍,对视频设备来说无所谓,因为三通道一起延迟。

3.3 串行器与差分输出

编码器出来的是10bit并行数据,工作在像素时钟域。要真正发到差分线上,必须用高速时钟把这10bit逐个移出去。在Xilinx 7系列器件上,首选OSERDESE2原语,一个OSERDESE2在DDR模式下支持1:10串行化。代码示例如下:

OSERDESE2 #( .DATA_RATE_OQ ("DDR"), .DATA_RATE_TQ ("SDR"), .DATA_WIDTH (10), .SERDES_MODE ("MASTER"), .TRISTATE_WIDTH (1) ) oserdes_inst ( .CLK (bit_clk), .CLKDIV (pix_clk), .D1 (parallel_data[0]), .D2 (parallel_data[1]), .D3 (parallel_data[2]), .D4 (parallel_data[3]), .D5 (parallel_data[4]), .D6 (parallel_data[5]), .D7 (parallel_data[6]), .D8 (parallel_data[7]), .D9 (parallel_data[8]), .D10 (parallel_data[9]), .OCE (1'b1), .OQ (serial_out), .T1(1'b0), .T2(1'b0), .T3(1'b0), .T4(1'b0), .TCE(1'b0), .TFB(), .TQ(), .SHIFTIN1(), .SHIFTIN2(), .SHIFTOUT1(), .SHIFTOUT2(), .OFB(), .RST(1'b0), .CLKDIVP(1'b0) );

这里CLK接bit_clk,也就是5倍像素时钟,CLKDIV接像素时钟。DATA_WIDTH设为10,DDR模式下OSERDES会在bit_clk的上下沿各输出一个bit,五个bit_clk周期刚好发完一个10bit码字。D1到D10的映射关系要跟你的并行数据位顺序一致,这里我按parallel_data[0]对应D1来写,也就是先发出低位。实际使用时要仔细核对器件手册和编码器输出位的先后约定。

如果你的FPGA没有OSERDES这类专用串行器,退而求其次可以用逻辑阵列搭移位寄存器,在10倍像素时钟下移位输出。低速分辨率勉强能跑,一旦到720p以上基本无法收敛时序。这也是为什么视频发射IP核大多绑定特定器件系列的原因。

串行数据出来之后,还要经过OBUFDS转成差分对:

OBUFDS #(.IOSTANDARD("TMDS_33")) obufds_r ( .I (serial_r), .O (tmds_p[0]), .OB (tmds_n[0]) );

在Xilinx的约束文件里,需要把差分引脚对应的IO标准设为TMDS_33或LVDS。不同开发板上电平和端接电阻方案有差异,配置IO标准时务必对照板子的原理图,别照抄参考设计。

3.4 带宽估算与器件选型

做FPGA选型时,不光要看逻辑资源,更要看高速串行逻辑能不能跑到目标速率。这里给一个快速估算方法。

TMDS单通道物理速率等于像素时钟乘10。1080p@60Hz像素时钟148.5MHz,单通道速率就是1.485Gbps,三通道总数据率4.455Gbps。去掉8b/10b编码的20%开销后,实际视频数据率是148.5MHz乘24bit,也就是3.564Gbps。

选器件时,要查两件事:OSERDES支持的串行时钟上限是多少,FPGA bank的IO标准支不支持TMDS。有些老款CPLD或者低端FPGA到1.0Gbps就上不去了,硬要做1080p就会在综合报告里看到一连串时序违例。我见过有人非要用老款器件跑1080p60,最后只能忍痛降到720p。提前算清楚带宽,能少走很多弯路。

另外,PCB布局也很关键。三条TMDS数据对和一条时钟对之间要保持等长,差分阻抗控制在100欧姆左右。如果PCB已经做死了,线长差太多,可以在FPGA内部给OSERDES加延迟校准,但那种办法只适合微调,线长差太远一样救不回来。

4. 调试实录:常见问题与排查手段

4.1 花屏和噪点先查这三处

花屏是TMDS调试里最常见的现象。经验上,优先排查三个方向。

一是DE时序。DE信号必须在有效像素数据到来时拉高,消隐期拉低。如果DE比数据早半拍或者晚半拍,画面上会出现固定位置的锯齿或者一行颜色错乱。这种问题用示波器抓DE和数据信号的相对时序,或者用ILA在FPGA内部观测,很快就能定位。

二是串行数据错位。10bit数据进入OSERDES时,D1到D10的顺序如果反了或者整体移了一位,画面不会全黑,而是出现细密的雪花噪点。因为TMDS是两级编码,错位后接收端解码拿到的码字完全对不上,噪点会遍布整个画面。这种问题需要逐位核对OSERDES的数据映射和编码器输出的对应关系。

三是时钟相位异常。bit_clk和pix_clk的相位关系如果没在PLL里约束好,OSERDES采样并行数据时可能落在跳变沿附近,表现就是画面时而正常时而花。处理办法是把PLL输出相位调到0,并做跨时钟路径约束。

我在一次MCU接口转HDMI的项目里,遇到的现象是画面左侧四分之一有波纹,查了两天发现是DE信号在行消隐期间没有拉低,导致编码器把消隐期的无效数据当成有效像素送了出去。所以拿到一个花屏问题,第一件事永远是确认数据使能DE的时序和范围,而不是怀疑编码算法有问题。

4.2 偏色缺色问题怎么定位

三个通道各自独立编码,如果某个通道的编码器或串行器工作不正常,就会出现明显的偏色。比如红色通道的TMDS链路没接通,画面上所有物体都偏青。

定位方法很简单:降低分辨率跑纯色测试图案,把R、G、B分别置成255,看显示器上对应颜色是否正常。红色全屏应该是纯红,如果发黑或偏青,说明红色通道有问题;绿色全屏发紫,说明绿色通道异常;蓝色通道同理。

另一个隐蔽的坑是控制字符编码错误。前面代码里控制期的四种码型如果写错一个bit,DE拉低期间接收端会解出错误的控制字符,可能导致显示器误判分辨率,甚至直接黑屏。DVI规范要求控制字符必须是特定码型,不是任意10bit都能用。从参考设计里拷贝代码时,这四个常量不要顺手改。

4.3 跨时钟时序约束怎么做

TMDS设计里最容易被忽略的是时序约束。OSERDES的输入是像素时钟域的并行数据,输出是bit_clk域的串行数据,两个时钟虽然频率不同,但来自同一个PLL,是同步生成时钟。正确做法是在XDC/SDC里给bit_clk创建生成时钟关系,并约束跨域路径。

Xilinx下的示意约束:

create_generated_clock -name bit_clk -source [get_pins mmcm/CLKOUT0] \ -multiply_by 5 [get_nets bit_clk] set_clock_groups -asynchronous \ -group [get_clocks pix_clk] \ -group [get_clocks bit_clk]

很多人第一次上手发现,仿真完全正常,综合实现后时序报告却一堆violation,原因就是没做时序例外,工具默认把这两个时钟当成异步时钟处理,导致OSERDES内部的hold时间检查出错。具体到不同厂家工具链,设置方法略有差异,但思路一致:bit_clk和pix_clk必须被识别为同源衍生时钟,布局布线时才能正确对齐相位。

4.4 常见问题速查表

现象可能原因优先排查手段
黑屏时钟差分对接反或未输出示波器量TMDS时钟差分电平
花屏带固定错位DE时序超前或滞后ILA观察DE与rgb数据对齐关系
满屏雪花噪点10bit串行顺序错检查OSERDES D1-D10映射
偏色某通道断链或编码器故障纯色测试图案逐通道确认
画面微闪时钟相位未对齐调整PLL phase或增加约束
高分辨率无法锁定单通道速率超出器件上限降低分辨率测试并核对时序报告

这张表基本囊括了我调试中遇到的大多数问题。其中“高分辨率无法锁定”有时候也跟HDMI连接器、线缆质量有关,但FPGA设计里最常踩的还是速率上限和时序约束这两个坑。

最后说一点个人体会。TMDS这套算法在如今的高速接口里看起来朴素,甚至有点古老,但它把“编码要兼顾传输可靠性和直流平衡”这个思路讲得非常清楚。后来接触SerDes、PCIe、JESD204B这些更高速接口的人,回过来看TMDS都会觉得概念上是相通的。FPGA初学者想深入高速接口,拿一个RGB转TMDS的小工程练手再合适不过。我第一次点亮HDMI画面时,花了大半天在调色彩空间和同步信号上,最后发现是vsync、hsync极性接反了,跟TMDS编码本身半毛钱关系都没有。所以说,做视频接口调试,先怀疑时序和控制信号,再怀疑算法,能帮你省下大量时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:36:05

Python代码质量守门员:Pylint与Flake8静态检查实战指南

1. 为什么需要静态检查&#xff1a;两个工具帮我守住了代码底线1.1 先看一个让人头大的代码评审现场我参与过不少Python项目的评审&#xff0c;最怕的就是那种“变量乱起名、函数几百行、import堆在文件中间”的代码。改起来要命&#xff0c;review起来更是一肚子火。可问题在于…

作者头像 李华
网站建设 2026/9/30 3:35:25

G.709标准详解:OTN帧结构、开销字节与排障实战

简介&#xff1a;这是一份G.709标准中文版与OTN光传送网络技术的系统梳理文档&#xff0c;主要面向光传输领域工程师、通信专业学生以及网络运维人员&#xff0c;用于快速建立OTN分层结构、帧格式与映射机制等核心概念。内容以ITU-T G.872/G.709规范为主线&#xff0c;先后讲解…

作者头像 李华
网站建设 2026/9/30 3:35:14

二次上界引理:从Lipschitz光滑性到梯度下降收敛性

刚开始推梯度下降收敛性那几天&#xff0c;我一直觉得证明里的那个二次函数像是“凭空蹦出来”的。明明面前是一个任意凸光滑函数&#xff0c;怎么一到推导时&#xff0c;它就被一个带 (L/2) 系数的二次函数从上方压住&#xff0c;还要刚好压在切平面上方一点点&#xff1f;后来…

作者头像 李华
网站建设 2026/9/30 3:35:00

K-Means聚类在校园美食推荐系统中的实践:从协同过滤困境到高效方案

1. 校园美食场景下&#xff0c;为什么K-Means比协同过滤更友好1.1 经典推荐算法在课设中的现实困境每年到了课程设计和毕业设计选题的时候&#xff0c;"推荐系统"都是最热门的方向之一。你去看知网上一堆本科论文&#xff0c;十篇里有三篇是某某推荐系统的设计与实现…

作者头像 李华
网站建设 2026/9/30 3:34:28

Flutter 鸿蒙化实战:capp 终端库适配 OpenHarmony 的完整方案

1. 先说清楚 capp 是什么&#xff0c;为什么要做鸿蒙化做移动端和跨平台这行的朋友应该都有感受&#xff1a;Flutter 不再只是做 App UI 的框架了。这两年&#xff0c;用 Flutter 写工具类应用、内部运维控制台、乃至命令行工具的团队越来越多。capp这个三方库&#xff0c;正是…

作者头像 李华
网站建设 2026/9/30 3:34:02

LeetCode 1934确认率:SQL聚合与LEFT JOIN实战详解

LeetCode第1934题《确认率》&#xff0c;属于那种一读题干感觉是白送分、一提交就发现被暗坑放倒的SQL题。我第一次做的时候信心满满写完LEFT JOIN和GROUP BY&#xff0c;结果用例直接红一片。排查了半天&#xff0c;问题出在“把没有确认记录的用户排除掉了”“除数为零时没兜…

作者头像 李华