1. 项目概述与核心需求解析
1.1 这个板卡到底是干什么的
拿到一块HDMI-FMC子卡,第一时间要搞清楚它解决的是什么问题。FMC(FPGA Mezzanine Card)是FPGA标准扩展接口,VC707这块板子本身不带HDMI接口,只有FMC HPC接口、GTX高速串行口和各种通用IO。想要在FPGA上输出视频信号,常见路径有三条:用开发板自带的HDMI座子(如果板上有的话)、通过FMC转HDMI子卡、或者用GTX配合外部PHY芯片。VC707上没有直接焊HDMI座,所以最省事的办法就是插一块HDMI-FMC子卡。
这块子卡的本质是:把FPGA的并行视频数据(RGB + 行场同步 + 时钟)通过TMDS编码变成高速差分信号送出去。FMC接口提供物理连接通道,子卡上完成电平转换和接口物理层适配。
我在实际项目中遇到过不少人一上来就盯着代码看,这其实是方向性错误。HDMI-FMC子卡这件事,硬件连接、引脚约束、时钟规划、编码逻辑四者是绑定关系,任何一个环节出问题,结果都是黑屏或者画面撕裂。所以这篇先讲清楚整体架构和硬件选型思路,再讲FPGA侧怎么实现时序和编码,最后列排查清单。
1.2 适合谁参考,能解决什么问题
如果你手头有VC707(或者其他带FMC HPC接口的板子),想通过HDMI输出视频信号,无论是做图像处理演示、视频采集回显还是算法验证,这篇文章可以让你在半天内跑通整个链路,而不是在一堆英文手册和论坛帖子里踩坑。
内容定位是:已经会基本Vivado操作的用户,但不一定写过视频时序生成器的人。会讲清楚TMDS编码怎么来的、DDC通道是怎么通过I2C走的、像素时钟和差分时钟的比例关系,这些才是HDMI-FMC子卡能不能稳定工作的关键。
2. 硬件平台与接口信号梳理
2.1 FMC接口的引脚分配逻辑
VC707上的FMC HPC接口有400个引脚,其中差分对数量非常可观。但HDMI子卡并不会把所有引脚都用完,一般只用其中一小部分:数据差分对(3对)、时钟差分对(1对)、I2C引脚、热插拔检测引脚、电源和地。
关键点在于,不同厂家的HDMI-FMC子卡Pin Mapping不完全一样,有的用的是K系列引脚,有的用的是N系列引脚,还有的干脆把I2C复用到了别的Bank上。所以拿到子卡第一件事不是看原理图,而是找子卡的FMC Pin Mapping表格,确认你要压制的引脚号和VC707上的物理引脚对应关系。
以常见的一款HDMI-FMC子卡为例,它一般做以下信号映射:
- HDMI_TX_CLK_P/N:一对差分时钟,承载像素时钟
- HDMI_TX_D0_P/N、D1_P/N、D2_P/N:三对数据线
- HDMI_TX_CEC:控制通道(做Demo一般不用)
- HDMI_DDC_SCL/SDA:I2C通道,用于和显示器握手
我用过的某款子卡(某公司FX型号)就用的是HPC接口Bank上的某些引脚,同时要求VC707侧的DIP开关将IO电压设置为2.5V。如果这部分没跳对,子卡的工作电平不对,I2C大概率握手失败,显示器直接提示No Signal。
提醒:FMC接口的信号电平通常需要按照子卡需求配置,在VC707上常见的是Bank VCCO配置为2.5V或1.8V,务必对应子卡手册,不能盲目使用默认设置。
2.2 HDMI信号链与其他视频标准的差异
要理解HDMI-FMC子卡为什么这样设计,得先知道HDMI在物理层用了什么手法。HDMI视频传输采用TMDS(Transition Minimized Differential Signaling)差分信号,数据线是三条通道,每条通道将8bit信号编码成10bit,这10bit里包含了控制信号、数据信号和同步信号,时钟通道单独传输像素时钟。
换句话说,1080p60的画面需要像素时钟148.5MHz,那TMDS差分时钟也是148.5MHz,三对数据线每一对在信号跳变时携带的其实是编码后的串行数据流,有效数据速率达到1.485Gbps。这套机制从DVI时代延续下来,兼容性极强,也是为什么HDMI输出依然是FPGA视频处理项目最常用的出口。
对比一下其他常见接口就能明白选型差异:
| 接口 | 有效数据带宽 | 是否需要额外PHY | 在VC707上的实现难度 |
|---|---|---|---|
| HDMI(TMDS) | 像素时钟×3倍,eq 445.5Mbps@1080p60 | 子卡上面有电平转换,FPGA内用OBUFDS | 中等 |
| DisplayPort | 高速串行链路,需GTX | 需要专门PHY | 较高 |
| LVDS输出 | 并行差分,带宽低 | 直接可用,但需要接收端芯片 | 简单 |
FPGA厂商之所以支持HDMI直出,是因为TMDS电平虽然电流小但不需要额外的物理层PHY片,FPGA的普通IO通过OBUFDS原语就能直接输出差分信号。大部分HDMI-FMC子卡的BOM很简单:连接器、ESD保护芯片、电平转换或直接接输出保护,逻辑都在FPGA里。
3. 这一次从时序生成器开始
3.1 像素时钟、行同步、场同步三者怎么对齐
想要输出HDMI信号,光有HDMI编码器是不够的。编码器的输入端需要一份标准的视频时序:Pixel Clock、VGA_HS、VGA_VS、VGA_DE、VGA_RGB。如果RGB和同步信号没有对齐,出来的画面要么左移右移,要么上下滚动,甚至完全花屏。
这部分的底层逻辑和VGA完全一样:每个时钟周期送一个像素,行同步拉高/拉低一个固定周期数,每行结束输出水平消隐区,场同步用若干行来表示垂直消隐区。DE信号有效时,表示当前处于有效数据区域,RGB数据必须有效。
我自己常用的做法是写一个可参数化的Video Timing Generator,模态参数直接用查找表定义。VC707上的时钟是200MHz,先经过MMCM生成像素时钟,再送给时序生成器。举例,1080p60的参数是:1920×1080@60Hz,PCLK 148.5M,H_Active 1920,H_FP 88,H_SYNC 44,H_BP 148,V_Active 1080,V_FP 4,V_SYNC 5,V_BP 36,总计每行2200像素、每帧1125行。这些数字是VESA标准定的,照搬即可,不用自己算。
如果把VGA时序生成器写好了,HDMI本质上就是给它换了一层编码外衣。很多教程把HDMI神秘化,其实先跑通VGA时序再介入TMDS编码,调试难度直线下降。
3.2 通过MMCM/PLL产生正确的像素时钟
在VC707上面,200MHz系统时钟进到MMCM,可以产生一个变频时钟。这里有一个容易忽略的点:如果使用默认的自动引脚规划,Vivado可能会选择一条时钟走线比较绕的路径,导致时钟抖动偏大。建议在创建时钟约束时,指定MMCM输出直接驱动像素时钟逻辑,让工具尽量用邻近的BUFG资源。
再一个细节:1080p30和1080p60的像素时钟不同,一个是74.25MHz,一个是148.5MHz,对应的MMCM配置参数也不一样。建议把像素时钟做成模块参数,通过上层的IP配置或define切换。我早期图省事,直接在代码里硬编码MMCM倍频参数,后来换分辨率要重新综合跑实现,折腾了四十分钟,后来全部改成从参数表读取,一次生成,后续只要改generic就可以。
建议:不要把M/N参数写死在代码里,尽量在工程顶层通过约束文件或IP的配置重写,至少留出参数化入口。
3.3 层级划分:时序、编码、输出
硬件逻辑分成三个模块时,边界清晰也方便定位问题:
- timing_gen:负责产生VGA级别的行场同步、DE、RGB数据
- tmds_encoder:把像素数据和同步信号编码成TMDS 10位序列
- tmds_serializer:把10位并行转换成串行差分输出,同时输出时钟差分对
这种划分的好处是:如果屏幕全黑,先看timing有没有输出,再看encoder有没有在正确数据段内编码,最后看serializer的时钟和ODDR有没有配置对。反过来如果数据错乱但同步正常,问题基本集中在encoder的编码流程或serializer位序。
用Verilog写这套逻辑时,需要注意所有跨时钟域数据都只在单一像素时钟域操作。TMDS串行化在更高频率的位时钟域完成,但位时钟可以从像素时钟DCM倍频得到。串行器一般用7系列FPGA的OSERDESE2实现,一个10:1的并转串需要三个OSERDESE2配合。
4. 核心细节解析与实操要点
4.1 TMDS编码流程逐位拆解
TMDS编码器是HDMI视频传输的核心,它的存在是为了让差分信号跳变次数可控,减少电磁干扰,同时保证DC平衡。在数据通道上,编码流程分两步:第一步做异或/异或非编码,减少跳变;第二步根据已编码位流中的1和0数量做反相控制,维持直流平衡。
以每个数据通道为例,输入是8bit像素数据(R、G或B通道)加上2bit控制信号(HSYNC和VSYNC),也可能是DE信号。编码器首先判断DE有效:DE有效时,数据按数据编码;DE无效时,输入被当成控制信号,编码器直接输出固定的控制序列(用10bit的特定码型区分不同的控制组合)。
我建议初学者别死记编码表,而是先把编码器的行为模型在脑海里过一遍:8bit进,10bit出,每一位都是组合逻辑+时序逻辑的混合体。实际用Verilog写的时候,用行为级描述,一个always块按状态跳变更新10bit数据,另一个always块完成DC平衡补偿,这样综合工具能推断出比较理想的实现。T无关紧要,最重要的是理解数据编码通道只在DE为高时对RGB数据编码,DE为低时输出的控制码是商量好的一组固定序列。
这里给一个常见行为级编码表达,方便理解:
- 当DE=1时,对输入像素数据进行TMDS 8B10B编码
- 当DE=0时,根据HS/VS状态,直接输出C0、C1、C2等预定义控制码型
10bit序列在串行化输出时必须注意位序。TMDS在线上是小端输出,即LSB先走。我在第一次做点屏实验时,串行器位序接反了,屏幕显示的是一条条细碎横纹,不是完整黑屏。这种问题用逻辑分析仪抓引脚时很难发现,必须把位序检查列进排查清单。
4.2 OSERDESE2在原语层面如何配置
7系列FPGA不支持直接用IOB寄存器和普通DDR原语拼出10:1串行器,必须用OSERDESE2。一个OSERDESE2最高支持8:1,所以10:1需要两个模块级联:主模块配置成8:1,从模块配置成10:1(以扩展宽度模式配合)。
配置细节要注意:
- OSERDESE2的DATA_WIDTH必须显式设置成10
- 主从模块之间的连接需要把从模块的Q输出连到主模块的SHIFTOUT接口
- 时钟方面,CLK接位时钟,CLKDIV接像素时钟
- SERDES模式选DDR,保证上下沿都能加载数据
用原语写代码时,直接例化比较冗长,我一般会在工程里封装一个tmds_serializer_10to1模块,内部例化两个OSERDESE2。这个模块复用性很强,三个数据通道和一个时钟通道各例化一份即可。
如果只想先跑通简单Demo,也可以直接用输出端的ODDR近似处理某些低速模式,比如将10bit数据截成2个5bit拼凑输出,但画质会受一些影响。建议正经项目还是老老实实用OSERDESE2。
4.3 HDMI输出时哪些信号必须约束
先把综合、实现跑通,不代表输出稳定。HDMI是高速差分信号,时序收敛非常依赖XDC约束的完整程度。最少要写以下几类约束:
- 像素时钟约束:create_clock,指定频率
- MMCM输出约束:如果从MMCM生成位时钟,要创建生成时钟,工具会自动传播
- 差分引脚约束:P端口和N端口必须同时约束,P端口指定物理位置,N端口用create_clock/SET_PROPERTY关联
- 所有HDMI输出IO加上IOBUF或OBUFDS原语,确保芯片引脚上差分驱动
- 对位时钟对应的BUFG指定延迟或位置,减少走线偏斜
我见过不少人只把引脚约束写了,时钟约束没写,Vivado会提示缺少主时钟定义甚至产生大量时序违约,但工程还能跑完。结果下载之后,显示器时而同步时而丢失,这种人最难受。建议第一步就把时钟约束写好,让工具在布局布线时就兼顾时序收敛。
工程里一个陷阱是:像素时钟名字和位时钟名字搞混。MMCM产生的时钟,如果只约束了像素时钟部分而把位时钟当自动生成,有可能会出现工具错误推断导致布线紧张。建议在XDC中把位时钟也用generated_clock显式声明。
5. 实操过程与核心环节实现
5.1 Vivado工程搭建与IP配置步骤
在VC707上新建一个空工程,器件型号选xc7vx485tffg1761-2,这是VC707的核心FPGA型号。创建RTL工程后,先把顶层模块按如下结构组织好:
- 顶层board_top:管脚约束、差分输出例化
- hdmi_tx:组合三个子模块,完成数据流聚合
- timing_gen:生成VGA/HDMI时序
- tmds_encoder:颜色通道编码
- tmds_serializer_10to1:并转串差分输出
新建工程后,先添加约束文件,把FMC_HDP、FMC_HDN等引脚映射到XDC里。如果你的子卡型号有差异,引脚映射要自行改,这一步跳不过。然后例化MMCM IP,名字随意,比如clk_wiz_hdmi。
在Vivado IP Catalog里搜索Clocking Wizard,配置输入时钟200MHz(VC707板载时钟),输出时钟设为像素时钟。MMCM输出的频率如果低至几十MHz,注意选择BUFG驱动方式,避免输出缓冲区数量不足导致时钟高扇出冲突。
5.2 引脚约束的完整写法参考
对于一个参考设计,XDC至少要包含如下的核心内容(以某款HDMI-FMC子卡为参考,实际引脚号需按自己子卡修改):
set_property PACKAGE_PIN AK18 [get_ports hdmi_tx_clk_p] set_property IOSTANDARD LVCMOS25 [get_ports hdmi_tx_clk_p] set_property PACKAGE_PIN AK19 [get_ports hdmi_tx_clk_n] set_property IOSTANDARD LVCMOS25 [get_ports hdmi_tx_clk_n] set_property PACKAGE_PIN AK20 [get_ports hdmi_tx_d0_p] set_property IOSTANDARD LVCMOS25 [get_ports hdmi_tx_d0_p] ...同时加上板的差分终端约束:
set_property DIFF_TERM TRUE [get_ports hdmi_tx_clk_p]DIFF_TERM开启的目的是让FPGA片内端接完配,保证差分信号不因阻抗失配而反射。这在高频信号上很重要,不开启的时候屏幕偶尔闪烁一下,开启之后稳定很多。
这里点名一个常见错误:VC707有些FMC引脚默认复用了GTX或其它Bank,如果约束脚位和FMC的其他功能冲突,工具会在DRC阶段报告错误。所以引脚约束写完要先跑一次Implementation,通过物理位置检查后再编写逻辑,避免一上来就怒刷编码器。
5.3 编码器与串行器的综合实现示意
用一个行为级例子演示数据通道编码器的大致写法,便于理解工作流程:
module tmds_encoder ( input clk, input reset, input [7:0] data_in, input c0, c1, input de, output reg [9:0] encoded ); reg [8:0] q_m; reg [3:0] ones_count; reg [3:0] data_ones; reg decision_xnor; ... endmodule上述代码里有两个关键状态:第一个是q_m的计算(异或编码),第二个是DC平衡决策(根据已输出序列中1的数量决定是否反相)。这个编码器并不是一蹴而就的,我建议先在仿真器里做各种数据输入对比,确认输出序列满足“跳变次数<=4且连续0不超过5”的基本要求。如果仿真不过,先别上板。
在example里加入一个简单图片生成器(比如彩条),可从顶层开始调。彩条的好处是行场同步时间不用精确到帧级别也能看出图像是否正常,因为它结构简单,花屏特征非常明显。
最后才是把彩条替换成真实视频源(如DDR读数据或图像缩放模块)。这时HDMI通道已经稳定,剩下问题基本是视频数据的时序等待问题。
6. 常见问题与排查技巧实录
6.1 显示器完全没有信号,该从哪里查
第一优先级检查目标:DDC通道。HDMI显示器上电后会通过DDC(Display Data Channel)去读显卡的EDID,这个通信走I2C。FPGA虽然是主动输出方,但显示器没有读到EDID时,可能直接拒绝识别信号。
排查方法:用I2C主机控制器(或一段逻辑分析仪)挂在DDC总线上,模拟读0x50地址的EDID字节,确认能否正常回应。如果I2C信号电平和时序有问题,先查电平转换芯片的使能脚有没有被设置正确。某次我曾经用了子卡上默认的I2C上拉电阻,但没给DDC供电电源打开,导致I2C总线上只有时钟没有数据,显示器完全黑屏。
第二优先级:检查是否有像素时钟从FPGA出来。用示波器量TMDS时钟差分对,看看频率有没有出现。如果什么都没有,通常是MMCM配置不对或者时钟约束没写对导致BUFG没有驱动。
第三优先级:检查同步信号是否有TTL输出。如果能在VC707的IO上抓到行场同步脉冲,说明timing_gen在工作,问题大概率在TMDS链路或引脚映射。
6.2 画面花屏或者颜色错乱怎么办
花屏通常分两类。第一类是整屏乱码,但行场同步稳定,这是因为串行器的位序错了或者TMDS通道映射弄反。比如把D0通道的数据接到了D2通道,屏幕上就会出现颜色通道互换的明显现象,R和B的色差最容易看出来。
解决方法是做纯色测试:先让三个通道分别输出R=255/G=0/B=0,看看显示器是不是纯红。如果输出纯蓝,说明R和B通道互换,改引脚映射即可。
第二类花屏是局部竖直条纹,这通常不是逻辑问题,而是时序裕量不够。位时钟和像素时钟的相位没对齐,或者等长布线做不到,导致某一对数据线采样点偏移。这时优先优化OSERDESE2的IOB的位置约束,或者调整MMCM的输出阶段的phase shift,逐步找到最佳窗口。
同时建议把CLK和数据的长度匹配保持在PCB允许的范围内,如果是自己画的子卡,要按差分对等长的标准来布线,差分对内误差控制在5mil以内。
6.3 随机闪屏:热插拔与ESD防护
HDMI热插拔会面临瞬间电流冲击和信号毛刺,显示器和FPGA都可能出现短暂异常。解决方案是在子卡设计时保留热插拔检测引脚(HPD),并且FPGA侧在检测到HPD变化后做去抖和重新初始化。
如果项目中的画面随机闪一下,且频率与触摸或插拔时一致,那大概率是ESD或地弹影响了拟电平。在子卡上应该有ESD防护器件,如果没有,可以在FMC和HDMI座之间加共模电感。这一点很多现成的子卡已经内置,但如果是手工改版就得留个位置。
我遇到过一次诡异现象:显示器预设的输入源是HDMI 2,但实际插在HDMI 1上,每次开机显示器都会花屏大概两秒后自动恢复。看起来像信号问题,其实就是信号在握手阶段的时序不够标准。这个说明,输入源的EDID读取和HDCP协商如果失败,显示器会反复重试,表现就是带内闪屏,不是FPGA逻辑问题。
6.4 独门经验:用彩条和纯色图做链路核对
最后分享一个我屡试不爽的调试顺序:先在timing_gen里接上彩条生成器,让每条颜色通道的数值固定分布,然后逐步替换为真实图像。
如果彩条能正常输出,说明HDMI数据链路基本通畅。然后改输出纯红色检测通道映射。最后接真实数据源,看帧存读是否有下溢,即读指针追上了写指针,造成画面跳帧,这些和HDMI本身无关,是后端数据流的问题,要单独加FIFO去隔离。
整个调试过程,逻辑综合时间往往只占一小部分,大头全在时序约束和硬件连接确认上。这就要求拿到板卡时先拿一份Pin Map表格和原理图,和XDC逐一核对,不要轻易相信网上流传的默认约束,不同批次的板卡有可能做了小改版却没说。
7. 基于VC707的其它能力扩展与遗留问题
7.1 把HDMI当输出口只是第一步
跑通HDMI输出后,实际上你已经具备了FPGA视频处理的基础框架。可以往里面加图像缩放、OSD叠加、边缘检测、多路视频源切换等。VC707资源丰沛,7系V系列FPGA有足够的DSP和BRAM来做实时图像处理,而HDMI-FMC子卡就是把处理结果展示出来的唯一窗口。
做实时视频处理时要特别留意数据流带宽:1080p60每个像素按RGB888计算是24bit,带宽约3.56Gbps,如果用三个DMA通道并行搬运,DDR带宽预算是够的,但AXI总线上同时有其它主设备时,就要做优先级仲裁。很多人在这里才发现,FPGA逻辑跑得很快,瓶颈反而是存储带宽。
这种情况下我习惯把输出端加上行缓冲和帧缓冲,通过FIFO隔离处理器和视频输出,同时用AXI性能计数器实测带宽占用量,避免漏检。
7.2 后续分辨率怎么扩展
HDMI-FMC子卡逻辑框架支持1080p以外的分辨率。只要修改timing_gen参数表和MMCM输出频率。4K60需要594MHz像素时钟,这对7系FPGA的普通IO和OSERDESE2来说压力很大,所以FMC子卡方案通常只做到1080p60。如果需要4K输出,建议换用带GTX的HDMI 2.0方案,或者使用专用发送芯片。
在文档开头说了这是“上篇”,那自然有“下篇”的规划。下篇会讲如何在HDMI通道上叠加AXI视频流、如何让EDID和HDCP走通、以及把整条链路接入MicroBlaze软核做寄存器配置管理的细节。那些内容相对会更高阶,但HDMI-FMC子卡上篇把物理链路和基础时序摸清楚之后,后面的东西就有抓手了。
如果你手头正好有VC707和一块HDMI子卡,建议今晚就把工程搭起来,先跑彩条试试。成功点亮的那一刻,印在屏幕上的颜色,就是你视频项目真正跑起来的入场券。