ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA直出HDMI:RGB转TMDS串行化全流程与调试实战

FPGA直出HDMI:RGB转TMDS串行化全流程与调试实战

第一次在FPGA上把RGB信号转成HDMI输出时,我以为这就像UART发送一样简单:把像素字节按顺序丢出去,显示器就亮了。结果板子一上电,示波器上波形明明在跑,屏幕就是无信号,或者奇奇怪怪的花屏、偏色、滚动条。后来把整套链路里里外外捋了一遍,才发现核心不是“编码”,而是“把并行像素流变成高速串行比特流”这个物理过程,以及一堆容易被人忽略的握手时序和板级细节。这篇文章就把我实际做FPGA RGB转HDMI显示系统的完整思路、参数计算和踩坑过程写出来,适合正在做视频输出、图像采集显示,或者想用FPGA直连HDMI接口的开发者参考。

1. 为什么自己用FPGA做RGB转HDMI,而不是直接买一颗转换芯片

1.1 HDMI发送器本质是“并行转串行”,不是显示协议

很多第一次接触的人会下意识把HDMI当成一种高级的“显示通信协议”,觉得要先学懂各种帧结构、数据岛、音频包才能动手。实际上,从FPGA的角度看,HDMI/DVI发送链路的核心动作非常朴素:

  • 把RGB三个通道的并行像素数据,按TMDS算法编码成10bit符号
  • 三个通道每个通道串行输出,物理线路上看到的是高速差分信号
  • 接收端(显示器)会自动完成时钟恢复、解码和像素重建

也就是说,发送端要解决的问题,并不是“怎么把画面内容组织成协议包”,而是“怎么在指定像素时钟下,稳定地把三路10bit数据变成一路高速串行流”。这个理解一旦转过弯来,整个项目的主线就清楚了。

这也是为什么我看到有人纠结“HDMI 1.4和2.0的编码格式有什么不同”就卡住。对入门级别的RGB输入、8bit色深、1080p60来说,你压根用不到深色、HDR、音频回传这些扩展功能,核心就是三路TMDS数据和一路像素时钟。

1.2 桥接芯片与纯逻辑直出的取舍

市面上有专门的HDMI发送桥接芯片,FPGA这边只需要输出RGB+DE+时钟,剩下的TMDS编码和串行化全部由芯片完成。优点是一天就能点亮屏幕,缺点也明显:带宽被芯片型号锁死,比如1080p通常没问题,但4K60就要换更贵的型号;而且这类芯片的寄存器初始化、EDID管理、音频配置非常繁琐,有时候调I2C配置的时间比调FPGA逻辑还长。

纯逻辑直出的思路是:FPGA内部完成TMDS编码,把串行数据从普通IO引脚输出。它的上限取决于FPGA本身的serdes能力,低端FPGA也能做到720p、1080p60;好处是不依赖外部芯片,物料成本低,带宽潜力更大,还可以把帧率、分辨率、测试图生成全部集成到同一颗FPGA里。代价是板级设计、输出约束、时序收敛都得自己处理。

我做这个项目时选的是纯逻辑方案,因为目标只是把FPGA内部生成的彩色测试图输出到普通显示器上,同时验证一套可复用的RGB转HDMI发送通路,后续还能在此基础上扩展分辨率或者叠加图像处理。如果你手上有现成的桥接芯片,那当然可以快速出活;但如果想彻底掌握这条链路的细节、方便以后做更高分辨率或者定制输出格式,建议至少自己写一次FPGA直出方案。

2. 时钟树设计:1080p60的像素时钟和高速位时钟是怎么算出来的

2.1 从“一行多少个像素”推导出像素时钟

做显示输出,第一件事就是把时序参数的来龙去脉算清楚,不能只看别人给的表格。以最常见的1080p60为例,接收端期望的水平和垂直时序是一组固定的数值,其中水平总像素是2200,垂直总行数是1125。也就是说,一帧实际传输的像素数不是1920×1080,而是2200×1125。

所以像素时钟就是:

像素时钟 = 2200 × 1125 × 60Hz = 148.5MHz

这个148.5MHz是整个系统的基准,HDMI发送链路的每一个环节都跟它挂钩。如果你做的是720p60,水平总像素是3300的话,像素时钟就是74.25MHz;如果做的是自定义分辨率,算法一模一样,先定下blanking参数再算时钟,不要反过来拍脑袋定时钟。

这里有个新手常犯的错误:直接用1920×1080×60去算,得到124.4MHz,然后发现显示画面偏左、或者右边有一条黑边。原因就是没把blanking算进去。显示器在水平消隐期也需要时间来完成行同步,所以总像素一定比有效像素多。

2.2 五倍频还是十倍频:串行数据率的两种实现路径

TMDS物理层规定,每通道10bit要在1个像素时钟周期内全部发完。以1080p60为例,每通道数据率就是:

串行比特率 = 148.5MHz × 10 = 1.485Gbps

问题来了:FPGA内部逻辑时钟通常跑不到1.485GHz,怎么把这个数据搬出去?业界有两种常见路径:

  1. 使用五倍像素时钟(742.5MHz),配合DDR输出。742.5MHz时钟的上升沿和下降沿各送1bit,一个像素时钟内正好送出10bit。很多FPGA的高速IO模块都能在这个频率下工作。

  2. 直接使用十倍像素时钟(1.485GHz)单沿输出。这种方案需要FPGA的IO支持极高的单沿翻转率,低端芯片基本不用想。

所以绝大多数FPGA直出方案都走第一条路:内部逻辑时钟用148.5MHz,IO时钟用742.5MHz,用DDR双沿的方式把10bit串出去。这也是接下来配置serdes模块时最重要的时钟关系。

2.3 跨时钟域与FIFO设计

如果你的RGB数据来自DDR3读出的图像数据,或者来自摄像头接口,那它的时钟大概率不是干净的148.5MHz,而是某个异步时钟域。在这种情况下,直接拿异步数据去驱动TMDS编码器会出现亚稳态和偶发花屏。

标准做法是在进入TMDS链路之前加一个异步FIFO,写入端是源时钟域,读出端是148.5MHz的像素时钟域。FIFO深度不需要很大,一般16到32就足够,因为显示链路本质是流式的,只要平均速率匹配,瞬时缓冲量不会太大。关键是FIFO的读侧要用像素时钟连续读,不要让读使能有间隙,否则blanking期间后端就会出现像素空洞。

如果你生成图像数据本身就发生在148.5MHz域内,那可以不用FIFO,但要保证生成逻辑的时序收敛,特别是跨组合逻辑的路径不能太深。我实测下来,像测试图生成、多边形绘制这类简单逻辑,直接在像素时钟域做完全没有问题。

3. TMDS编码与通道映射:数据流在链路里到底怎么走

3.1 从24bit像素到三路10bit符号

TMDS编码不是简单地把8bit RGB值搬过去,而是有一套保证直流平衡的编码算法。简单理解就是:把8bit数据通过异或或同或逻辑展开成9bit,再根据数据中0和1的数量决定是否翻转第10bit,从而让线路上的直流分量尽可能平衡。

这个编码器可以用FPGA的组合逻辑完成,不需要查表。以红通道为例,输入是8bit的R,输出是10bit的TMDS符号,绿蓝通道同理。编码完成后,三路各10bit分别进入三个串行器。

需要特别注意的是,TMDS编码的输入不只有像素数据,还有控制信号。在水平和垂直消隐期,DE为低电平,这时候三路通道要发送固定的控制符号来表示行场同步。也就是说,发送器必须能在“像素数据”和“控制符号”之间无缝切换,切换时机完全由DE信号决定。

3.2 DE与control token的优先级

很多人写代码时只关注DE有效期间的像素编码,把DE无效期间随便塞个0,结果显示器能识别出画面,但画面上下翻滚,或者行场不同步。原因就是消隐期的control token必须是特定值。

TMDS标准里,消隐期发送的control符号是有讲究的:

  • 通道0在消隐期携带HSYNC和VSYNC相关的控制位
  • 通道1和通道2在消隐期发送固定的控制符号

各通道的control输入有两根线(比如c0、c1),编码器会根据这两根线的值生成对应的10bit符号。如果你在DE无效期间直接把控制符号置为0,而行同步、场同步的极性又配置不对,就会导致显示器完全无法锁定。

我在编码模块里是这么处理的:一个周期内如果DE为高,就按像素编码输出;DE为低,就按内部维护的HSYNC和VSYNC信号编码control输出。同时对外提供配置寄存器,可以翻转行场同步的极性,因为不同显示器对极性的要求并不一致,做成可配置能省很多调试时间。

3.3 通道顺序与引脚极性

R、G、B三路通道和HDMI物理引脚的对应关系,是这个项目里最容易被忽略又最容易翻车的地方。有些参考设计把通道0对应R,通道1对应G,通道2对应B;但HDMI插座上的差分对顺序在不同板卡上可能有差异。

我的建议是:在FPGA的引脚约束阶段就明确标出三组差分对分别属于哪个通道,然后在编码输出端加一个可控的通道交换。这样即使PCB上丝印标错了,也不需要改板子,直接在寄存器里把R和B对调一下就行。

另外,TMDS时钟通道的正负极性也要注意。有些FPGA的DPHY或者OBUFDS会对输出取反,如果时钟极性反了,接收端采到的所有数据都会错位,表现就是画面完全花掉或者无信号。排查时可以用示波器看时钟通道的差分波形是否和像素时钟频率一致,再确认相对数据通道的相位。

4. 输出级实现:从直接IO翻转到OSERDESE2的关键路径

4.1 三种输出方案的演进

把10bit符号变成物理差分信号,有三种做法,难度和性能依次递增:

方案一:直接用Vivado里的DDR输出原语,把bit0~bit9拼成两个5bit组,分别在时钟上升沿和下降沿送出。这个方案逻辑简单,但要求你手动处理双沿逻辑,时序比较紧张。

方案二:使用厂商的串行器原语,比如某7系列FPGA的OSERDESE2,配置成10:1模式,硬件自己完成并行到串行的转换。这是最推荐的方案,因为原语在布局布线时会被放到靠近IO的位置,时序天然更可控。

方案三:如果分辨率超过1080p,或者需要更大位宽,可以走专用高速收发器通道。但这类资源不是每个芯片都有,而且配置复杂,入门阶段先不做。

我当时用的就是第二种,OSERDESE2加上OBUFDS输出差分信号。核心配置是:DATA_RATE_OQ选DDR,DATA_WIDTH配10,CLK接5倍像素时钟,CLKDIV接像素时钟。并行侧用10bit输入,注意原语的输入顺序和移位方向,不同厂商定义不完全一样,最好对照原语手册把每个bit的实际含义过一遍,不要想当然。

4.2 一个可用的10bit串行器配置片段

这里给一段基于某7系列FPGA的示意配置,重点是展示时钟关系,不是完整工程代码:

OSERDESE2 #( .DATA_RATE_OQ ("DDR"), .DATA_RATE_TQ ("DDR"), .DATA_WIDTH (10), .TRISTATE_WIDTH (1), .SERDES_MODE ("MASTER") ) oserdes_ch0 ( .OQ (tmds_p[0]), .OCE (1'b1), .CLK (clk_5x), // 5倍像素时钟,742.5MHz .CLKDIV (clk_pix), // 像素时钟,148.5MHz .RST (serdes_rst), .D1 (tmds_10bit[0]), .D2 (tmds_10bit[1]), // ... D3~D8 按原语顺序填入 .T1 (1'b0), .T2 (1'b0), .T3 (1'b0), .T4 (1'b0), .TBYTEIN (1'b1), .TBYTEOUT (), .TQ () );

配套的差分输出是这样的:

OBUFDS #( .IOSTANDARD ("TMDS_33") ) obufds_ch0 ( .I (tmds_p[0]), .O (hdmi_tx_p[0]), .OB (hdmi_tx_n[0]) );

注意这里有个关键点:如果你用的是五倍像素时钟+DDR模式,那么5倍时钟本身是742.5MHz。这个时钟的来源一般是PLL或者MMCM,配置时要注意PLL的VCO频率范围,有些低端芯片的VCO上限不够直接生成742.5MHz,需要先倍频再分频,或者采用其他组合方式。千万不能让PLL的输出超出规格,否则即使约束通过,实际抖动也会很大,显示器会出现随机闪屏。

4.3 输出约束与引脚分配

这一节是我最想强调的,因为我第一次画板子时,认为只要能综合通过就行,结果上板后从720p到1080p都无法稳定显示。

约束要注意三件事:

第一,TMDS输出引脚必须用差分约束,让布线工具知道这是一对差分信号,并尽量走同一层、保持等长。对内不等长会直接恶化时序裕量。

第二,时钟引脚和重时钟引脚的分配要靠近对应的输出管脚,不要跨巨大的时钟区域。跨区布线会让高频率信号的时钟偏斜变大,影响发送时序。

第三,如果FPGA支持TMDS输出电平标准,就显式指定;不支持的话,可能需要用LVDS或DIFF_SSTL等兼容电平,并注意后端是否要加电平转换电路。直接在普通LVCMOS IO上跑1.485Gbps,除非信号路径极短,否则大概率过不了信号完整性测试。

5. 上电时序与DDC握手:为什么有波形但显示器黑屏

5.1 HPD与EDID:显示器不认你的灵魂原因

很多人做到“VS波形正常”之后,卡在显示器死活不亮。这时候多半不是TMDS数据的问题,而是HDMI握手没做好。HDMI源端(你的FPGA板)需要让显示器感知到“有设备接入”,并允许它读取EDID。

最核心的信号是HPD(Hot Plug Detect)。在HDMI连接器上,HPD由显示器输出,源端应该监控它并做对应处理。但作为FPGA开发板,如果你的发送器没有主动处理HPD,显示器会认为根本没有设备插入,自然不启用输出。

简单可靠的模拟方法是:板卡上一上电就通过一个电阻把HPD引脚拉高到3.3V。这样显示器会认为源端已经就绪,然后去试图通过DDC通道(I2C)读取EDID。如果你的设计不打算支持EDID读取,就让I2C引脚保持空闲或接上拉,显示器读取失败后通常会进入“无信号”状态,但至少不会因为你完全没处理握手而黑屏。

如果显示器屏幕上出现“无信号,进入省电模式”,那说明HPD至少被正确处理了,问题多半在TMDS时钟或者数据通路上。

5.2 先跑blank,再让DE信号有效

上电瞬间,PLL还没锁定,FPGA内部的像素时钟是抖动的。如果这个时候就输出有效DE和像素数据,显示器可能在错误的时钟边沿上建立同步,然后陷入反复追踪的循环。

我采用的顺序是:

  1. FPGA上电,复位所有模块
  2. 等待PLL lock信号拉高
  3. 在DE保持无效的情况下,连续输出若干个完整帧的blank像素
  4. 等显示器稳定后,再拉高DE使能,输出有效图像

这个“先blank后图像”的顺序在很多参考设计里看不到,但它真的能避免很大一部分“上电黑屏,重启一下又好了”的诡异现象。我在实际调试中遇到过:直接上电就出图,第一次大概率花屏或者闪一下,但热复位之后完全正常。原因就是PLL尚未稳定时就开始发送有效图像数据,接收端的时钟恢复完全被带乱。改成先blank 200ms以上,问题直接消失。

6. 板级调试实录:示波器、驱动强度与EMI问题

6.1 看波形时到底在看什么

调试TMDS链路时,示波器不能只看“有没有波形”,要看三个关键点:

第一,时钟通道的频率是否精确等于像素时钟频率。148.5MHz不能变成147MHz或者149.5MHz,频率偏差过大会导致显示器无法锁定或者出现行场不同步。

第二,数据通道的差分幅度和摆率是否正常。如果幅度太低,可能是输出驱动强度配置过弱,或者线路损耗太大;如果波形边缘有过冲,说明驱动强度可能过高,可以尝试调低电流或者增加串联电阻。

第三,时钟和数据之间的相对抖动。用示波器余辉模式看时钟眼图,如果眼图闭合严重,优先检查PLL配置和电源噪声。

我调试时最喜欢用一张黑白竖条测试图:竖条图案对TMDS串行数据中的高频成分和低频成分都很有压力,任何信号完整性问题都会在竖条边缘表现为拖影、噪点或者微小色偏。

6.2 电源与地弹:最容易忽略的隐性杀手

FPGA在输出1.485Gbps串行流时,瞬时功耗变化非常剧烈。如果板卡的电源网络阻抗过高,或者去耦电容不足,输出驱动翻转时会在电源上产生周期性纹波,这些纹波会直接耦合到PLL和输出缓冲,造成随机抖动。

实际遇到过一次:单独跑core逻辑一切正常,但只要开启TMDS输出,图像偶尔出现水平方向的随机噪点。排查了所有逻辑和时序都没有问题,最后发现是给FPGA IO bank供电的LDO纹波偏大,输出负载一上去就掉电压。换了一颗纹波更小的电源模块之后,问题彻底消失。

所以不管是做评估板还是集成到自己的PCB,一定要保证高速输出IO bank的供电余量充足,至少留30%以上的电流裕度,并且靠近FPGA电源引脚放置足够容值的去耦电容。

6.3 用一张测试图排查通道映射与极性

讲一个很实用的调试招数:生成一张带“R”、“G”、“B”三个色块,同时左上角画一个特定角标的测试图。如果显示器显示出来色块位置交换,那就直接确认通道映射关系需要调整;如果角标位置偏了半个字符,说明DE相位和像素数据没有对齐;如果画面整体偏右或偏左,需要调整同步位置参数。

我还遇过一个“红绿蓝都正常但整体画面发紫”的情况,最后发现是蓝色通道少了最低位的数据线没连接,导致蓝色分量在低灰度时丢失。这种问题用纯色测试图一眼就能看出来,但用彩色自然图像反而很难判断。

7. 从1080p60到更高分辨率:带宽升级路径

7.1 2像素并行架构:提高输出能力的第一步

如果目标分辨率超过1080p60,比如1440p60或者1080p120,单个像素时钟可能达到220MHz甚至300MHz以上。这个时候,内部逻辑在单像素时钟域里做复杂处理通常会遇到时序收敛困难。

常见做法是把数据通路改成2像素并行:一个像素时钟周期内同时处理2个像素,内部逻辑时钟可以降到像素时钟的一半,但输出串行器仍然按2倍并行位宽工作。比如1440p60的像素时钟约241.5MHz,采用2像素并行后内部逻辑时钟只要约120MHz,配合每个通道20bit并行输入给更高位宽的串行器,FPGA的压力会小很多。

不过要注意:改成并行像素后,生成测试图和做图像处理的部分也要同步改成2像素流水线,代码复杂度会明显上升。如果没有强需求,建议先把1080p60的链路做稳,再扩展并行宽度。

7.2 什么时候该换高速收发器或专用桥接芯片

从技术趋势看,HDMI 2.0以上引入了更高的数据率,TMDS也逐步演进到FRL模式。很多通用FPGA的普通IO并不支持FRL所需的极高速度,所以到这一步,通常有两条路:

一是选用集成高速收发器的FPGA,用收发器实现HDMI 2.0的物理层,逻辑侧仍然可以保留RGB输入和TMDS编码思想,但编码后的数据会通过并行接口送给收发器,再由收发器负责串行化。

二是回到第一章说的桥接芯片思路。当你评估完纯逻辑方案的带宽成本、PCB难度、开发时间之后,发现专用芯片更划算,那就果断切换。桥接芯片负责HDMI协议的最高速部分,FPGA专心做图像生成和简单处理,两边各干各擅长的事。

我的建议是:如果你的产品三年内只做1080p,纯逻辑方案性价比很高;如果明确要做4K60甚至更高,直接在一开始就选带高速收发器的FPGA或者桥接芯片方案,不要在普通IO上硬扛,否则板级和时序成本会成倍增加。

8. 那几个让我印象深刻的坑

最后把我实际踩过的、值得单独说一遍的坑总结一下,给准备动手的人提个醒。

第一个坑是OSERDESE2的bit顺序。我一开始严格按照原语手册写的并行输入映射,但输出波形完全不对,后来才发现不同系列、不同封装下,原语的串行输出顺序有差异。解决方法是写一个简单的循环测试模式,比如并行输入0b1100000000,看输出波形高电平出现在哪个位置,以此校准接线。

第二个坑是DE信号不能有毛刺。TMDS编码器对DE非常敏感,如果DE信号上有组合逻辑产生的毛刺,哪怕只有一个周期,接收端就可能在消隐期和有效显示期之间误切换,造成画面边缘出现一条噪声带。我用的是在像素时钟域里从寄存器直接输出的DE,不在外部组合逻辑里做DE。

第三个坑是热插拔场景下的复位。如果用户在使用中拔掉HDMI线再插回去,FPGA端如果没有检测HPD变化并重新执行一遍“先blank后出图”的流程,显示器很可能会一直黑屏直到重启。我后来在系统里加了一个简单的HPD检测逻辑,一旦发现HPD从低变高,就自动把输出状态机重置到blank状态,再平滑地恢复图像输出。

第四个坑是TTL电平的3.3V对HDMI的5V引脚。HDMI规范里源端要提供5V电源,但如果你的FPGA板没有专门的5V HDMI电源引脚,千万不要直接把FPGA的3.3V接到HDMI连接器的5V脚。正确的做法是单独从板卡电源生成5V,再通过合适的限流和保护电路接到连接器。否则一旦插上某些带反向保护的显示器接口,板卡可能直接烧掉。

做这套系统最舒服的时刻,是终于看到测试图上红绿蓝三色块按预期位置出现的那一次。前面所有的时序计算、约束调整、信号质量排查,在这一刻都值了。如果你也在做类似的东西,希望这篇文章能帮你少走我走过的那些弯路。核心就一句话:协议可以慢慢学,但时钟树、IO约束、上电时序和板级信号完整性,是决定成败的四根柱子。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进