ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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位加上时钟和同步信号线缆动辄十几根线。低频低分辨率下这没问题但到了1080p60Hz像素时钟是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_qm0的个数就是9减N1_qm。然后看cnt的情况如果N1_qm大于4或者N1_qm等于4且cnt等于0则输出q_out[9]为0q_out[8:0]为~q_m否则输出q_out[9]为1q_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 8b10110010。先数1的个数第7位是1、第5位是1、第4位是1、第1位是1总共4个。N1等于4且D[0]等于0所以第一级取反得到q_m[7:0] ~D 8b01001101q_m[8] 0。数一下q_m[8:0]里1的个数是4个。假设当前cnt 0。N1_qm 4满足N1_qm等于4且cnt等于0执行取反输出。q_out[9] 0q_out[8:0] ~q_m 9b110110010所以完整的q_out[9:0] 10b0110110010。这个码字里1的个数是50的个数也是5直流贡献为零。cnt更新为0加5减4等于1。再看一个极端输入D 8b11111111。N1 8大于4第一级取反q_m[8:0] 9b000000000一个1都没有。N1_qm 0假设此时cnt 1不满足N1_qm大于4也不满足N1_qm等于4且cnt等于0所以不取反。q_out[9] 1q_out[8:0] 000000000输出10b1000000000。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倍像素时钟。比如1080p60Hzpix_clk为148.5MHzbit_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] 1b0; end else begin q_m[7:0] data; q_m[8] 1b1; 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] 1b0; q_out[8:0] ~q_m; dc_cnt dc_cnt (5d9 - n0q_enc) - n0q_enc; end else begin q_out[9] 1b1; q_out[8:0] q_m; dc_cnt dc_cnt n0q_enc - (5d9 - n0q_enc); end end else begin case (ctrl) 2b00: q_out 10b1101010100; 2b01: q_out 10b0010101011; 2b10: q_out 10b0101010100; 2b11: q_out 10b1010101011; default: q_out 10b1101010100; endcase dc_cnt 5d0; 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接2b00。第三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 (1b1), .OQ (serial_out), .T1(1b0), .T2(1b0), .T3(1b0), .T4(1b0), .TCE(1b0), .TFB(), .TQ(), .SHIFTIN1(), .SHIFTIN2(), .SHIFTOUT1(), .SHIFTOUT2(), .OFB(), .RST(1b0), .CLKDIVP(1b0) );这里CLK接bit_clk也就是5倍像素时钟CLKDIV接像素时钟。DATA_WIDTH设为10DDR模式下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。1080p60Hz像素时钟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看显示器上对应颜色是否正常。红色全屏应该是纯红如果发黑或偏青说明红色通道有问题绿色全屏发紫说明绿色通道异常蓝色通道同理。另一个隐蔽的坑是控制字符编码错误。前面代码里控制期的四种码型如果写错一个bitDE拉低期间接收端会解出错误的控制字符可能导致显示器误判分辨率甚至直接黑屏。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编码本身半毛钱关系都没有。所以说做视频接口调试先怀疑时序和控制信号再怀疑算法能帮你省下大量时间。
返回列表