做FPGA视频方向,绕不开的第一个实战题目就是HDMI。我带过的同学里,十个有八个点完灯之后就不知道该干嘛了——其实最该做的,就是“HDMI视频输入与环路输出实验”。这个题目听起来有点专业,拆开看就是:把一路HDMI信号源接进FPGA,FPGA把视频数据原样(或者加一点处理)再从另一路HDMI口送出去,让显示器正常出图。整个过程涉及HDMI接口、TMDS编码、I2C配置、像素时钟、视频时序和数据通路,刚好把FPGA视频应用最核心的基础知识串成了一条线。这篇文章我会把方案怎么选、代码怎么写、上板怎么调、坑怎么避,按我实际带人做实验的顺序完整讲一遍,适合刚学完语法、想做点真实项目的FPGA入门者,也适合想搞懂视频数据流的人当参考。
1. 环路输出实验到底在做什么
1.1 环路输出不是“一根线的事”
很多同学听说环路输出,第一反应是:这跟拿一根HDMI线把播放器和显示器直接连起来有什么区别?区别大了。直接连线,时钟、数据、同步信号都在物理线路上完成传输,中间没有一个环节需要你干预。而环路输出实验里,HDMI信号要先经过接收芯片转成并行RGB数据,送入FPGA,FPGA要么直接把数据转发出去,要么对像素做点处理再发给发送芯片,最后才是显示器。也就是说,真正“跑数据”的是FPGA内部逻辑,而不是那根线。
这个实验最大的价值就在这里:它让你把视频当作普通数据流来理解。视频不是一个“神秘的黑盒子”,而是一连串按时钟逐像素排放的数据包,配套的行场同步信号和DE信号负责告诉你“哪里是有效像素,哪里是消隐期”。一旦你理解了这个,后续做图像处理、多路拼接、OSD叠加、甚至MIPI接口移植,都只是在这条数据流上做文章而已。
我在实际带实验时的目标是:先把输入通路打通,确认接收芯片能出图;再走输出通路,确认显示器能正常显示;最后在中间加一个“肉眼可见”的像素处理,用结果证明FPGA确实参与了对视频数据的运算。三步走完,才算完整吃透这个实验。
1.2 环路输出和视频采集回显有本质区别
明白环路输出还得知道它跟“采集回显”不一样。采集回显是:HDMI输入,帧数据写入DDR,处理器(比如ARM核或CPU)读出来处理,再送显示。这条路天然有几十甚至几百毫秒的延迟,因为数据经过了存储和调度。环路输出则不然,它强调的是低延迟透传,FPGA基本只做同步采样和组合逻辑转发,延迟典型值在几个像素时钟周期到一两行之间,可以说是“所见即所得”。
这种低延迟特性让环路输出天然适合做需要即时反馈的场景:视频会议切台、摄像头预览、KVM切换器、去隔行前级处理。反过来,环路输出也是FPGA视频系统里最基础的功能模块,很多板卡对外宣称的“多种视频接口互转”,内部第一级基本都是它。所以你把这个实验搞明白了,等于拿到了几乎所有视频开发板方案的通用“地基”。
2. HDMI的底层是TMDS
2.1 一条HDMI线里到底在传什么
先说物理层。HDMI有4对差分信号线,其中3对是数据通道,1对是时钟通道。时钟通道传输的是像素时钟,数据通道则负责把像素的R、G、B三个颜色分量(或者经过编码后的数据)串行地发出去。这意味着,一个1080P60的画面,像素时钟是148.5MHz,每个像素的RGB共24bit,除以3个通道后每通道8bit,再经过8b/10b编码变成10bit,所以每通道的串行数据速率是148.5MHz × 10 = 1.485Gbps。
这里“8b/10b编码”是关键。HDMI芯片不会直接把8bit像素数据裸传出去,而是先编成10bit码字。为什么?因为裸传的话,接收端很难判断哪一位是数据起始位,长串的0或1还会导致直流不平衡,接收电路没法正确恢复时钟。8b/10b编码保证了足够多的跳变沿,同时控制直流分量,接收端才能稳定采样。你可以把它理解成:寄快递前先给箱子打好统一的包装和标签,快递员才知道怎么搬运和投递。
另外,行场同步信号和音频、辅助数据并没有单独占用引脚。行场同步信号被编码进数据通道的前导码区域(也叫控制区),音频和辅助数据则嵌在行消隐期的“数据岛”里。所以接收芯片解出来的不光是像素,还有一堆控制信号,只是它们被封装得比较隐蔽。
2.2 像素格式、时钟与1080P时序参数
FPGA端最容易处理的像素格式是RGB 4:4:4,每分量8bit,不加任何压缩。接收芯片像ADV7611输出来的,一般就是这种格式的并行总线:24bit RGB加数据有效信号DE、行同步HSYNC、场同步VSYNC,还有一个像素时钟LLC。发送芯片(如ADV7513)接受的也是这套信号,只是方向反过来。
视频时序的一堆参数,什么HActive、HBackPorch、VTotal,很多人一上来就被绕晕。我给你个记忆方法:视频就像一张表格,HActive和VActive是真正的表格内容,HBlanking和VBlanking是表格周围的留白。留白里又分成前肩、同步、后肩,它们的作用是让显示器有足够时间换行、换帧。
以我们实验最常见的1080P60为例,一帧的完整参数如下:
| 参数 | 数值 | 说明 |
|---|---|---|
| 像素时钟 | 148.5MHz | 每个像素一个时钟周期 |
| HActive | 1920 | 每行有效像素数 |
| HFrontPorch | 88 | 行前肩 |
| HSync | 44 | 行同步脉冲宽度 |
| HBackPorch | 148 | 行后肩 |
| HTotal | 2200 | 整行总像素数 |
| VActive | 1080 | 有效像素行数 |
| VFrontPorch | 4 | 场前肩 |
| VSync | 5 | 场同步脉冲宽度 |
| VBackPorch | 36 | 场后肩 |
| VTotal | 1125 | 总行数 |
验证一下:148.5MHz ÷ 2200 × 1125 ≈ 60Hz,正好是每秒60帧。以后遇到任何分辨率、刷新率,只要按这个办法算一遍,心里就有数了。输出芯片能不能跑得动、像素时钟够不够,用这个表一算就明白。
3. 硬件平台与数据流架构
3.1 一套成熟方案:接收芯片+FPGA+发送芯片
把HDMI信号直接接到FPGA引脚上、用FPGA逻辑做TMDS解码,理论上可行,但入门阶段非常不推荐。一来TMDS解码涉及高速串行输入和时钟恢复,需要Xilinx里的专用IP和GTP/GTX收发器,配置复杂;二来给普通教学板增加成本和难度。大多数厂商的视频开发板,比如黑金AX7350这类板子,走的是带专用编解码芯片的方案:输入端用ADI的ADV7611把HDMI信号转成并行RGB,输出端用ADI的ADV7513把并行RGB重新编码成HDMI信号。
为什么选ADV7611这类的芯片?因为它把最难啃的物理层全包了:TMDS解串、时钟恢复、HDCP解密、EDID管理都做好了,FPGA侧只需要面对24bit并行数据和几个同步信号。这等于你把一个复杂的接口问题降低成了简单的“数据读取”问题。用成熟视频板卡的同学,拿到手的原理图、引脚约束和寄存器手册都是现成的,跟着本文思路能少走很多弯路。
这里插一个很多新手完全注意不到的细节:HDMI接口上有5V电源和热插拔检测(HPD)引脚,供电电流一般几十毫安。设计板卡时,5V电源处理和HPD上拉电阻经常是故障高发区。我自己就见过因为HPD引脚漏接导致电脑死活不输出信号的板子。你在排查硬件问题时,不妨先看看是不是这里出的岔子。
3.2 并行RGB数据进入FPGA之后做什么
串行信号再复杂,进了FPGA就变简单了:ADV7611输出的是24bit像素数据D[23:0]、DE、HSYNC、VSYNC、像素时钟LLC。FPGA要做的事情,按优先级排列就三件:
第一是同步。输入信号和像素时钟是天然同步的,直接用LLC做时钟、把RGB和DE当普通数据采进来就行,不需要跨时钟域处理。第二是解析。如果只是透传,你不必去数行列;但要做叠加、切边、改色,就必须把当前像素的位置算出来,这就是上一节讲的时序参数在逻辑里的落地。第三是转发。把数据原样送到输出芯片,或者先做处理再送,关键是保证输出端用的时钟和同步信号满足芯片要求。
输出端的时钟设计值得多说一句。环路输出最简单可靠的做法是直接把ADV7611的LLC像素时钟接到输出芯片的像素时钟上,数据也直接并联,整条链路一个时钟。但如果你想在FPGA里做异步处理,比如缓存一行或做跨时钟域,就必须用异步FIFO或者DDR缓冲。入门阶段我建议先走“单时钟直连”,把通路跑通,再考虑复杂架构;一上来就跨时钟域,很可能把问题绕晕。
4. 核心逻辑实现:从输入时序解析到环路输出
4.1 I2C配置输入芯片,先让芯片“说话”
拿到一块新板子,哪个模块最先动?不是逻辑,是I2C。AV7611和ADV7513都有I2C从机接口,上电后必须通过I2C写寄存器,把它们配成需要的模式,才能正常出图。ADV7611的写地址常见是0x98,ADV7513是0x72,但不同板卡可能有差异,务必以原理图和手册为准。
配置哪些寄存器有规律:输入端口选择(HDMI的A/B/C/D口选哪个)、输入格式(是HDMI还是DVI)、输出像素格式(RGB 4:4:4还是YCbCr)、输出同步模式(外部同步还是芯片自带同步)、以及一些使能位。我一般把配置流程写成类似下面的C风格伪代码:
// 伪代码:通过FPGA内部的I2C控制器写ADV7611寄存器 void adv7611_write(uint8_t reg_addr, uint8_t reg_val) { iic_start(); iic_write_byte(0x98); // ADV7611 写地址 iic_write_byte(reg_addr); iic_write_byte(reg_val); iic_stop(); } // 示例:配置为RGB输出、内嵌同步、使能主链路 adv7611_write(0x00, 0x07); // 根据数据手册填写 adv7611_write(0x0D, 0x02); adv7611_write(0x0F, 0x40);注意,我故意不把每个寄存器的具体值写满,因为不同芯片版本和板卡参考设计差异不小。真正的做法是翻开对应芯片的数据手册,找到输入格式和输出格式章节,照着“推荐配置表”抄一遍,再根据实测修正。抄寄存器表不丢人,丢人的是明知道手册就在桌上却凭感觉乱填。
写配置时有个小技巧:先读芯片的ID寄存器,能读到正确ID,说明I2C物理链路是通的。如果这一步都不通过,后面全是白搭。我习惯在Vivado里用VIO挂一个手动触发,随时读ID、随时写配置,比改一遍综合一遍高效得多。
4.2 像素时钟和同步信号怎么处理
接收芯片配好后,FPGA侧要面对的信号就这些:pixel_clk(LLC)、hsync、vsync、de、rgb[23:0]。如果是纯透传,逻辑极简,直接把信号打两拍同步后转发即可。但加处理的场景需要知道“当前是第几行第几列”,这种场景要恢复出像素位置计数器。
基于上一节的1080P60参数,可以用下面的Verilog在有效区域里维护行列计数:
module video_timing_decode #( parameter H_ACTIVE = 1920, parameter V_ACTIVE = 1080 )( input wire pixel_clk, input wire rst_n, input wire de, output reg [11:0] h_cnt, output reg [11:0] v_cnt ); always @(posedge pixel_clk or negedge rst_n) begin if (!rst_n) begin h_cnt <= 0; v_cnt <= 0; end else if (de) begin if (h_cnt == H_ACTIVE - 1) begin h_cnt <= 0; if (v_cnt == V_ACTIVE - 1) v_cnt <= 0; else v_cnt <= v_cnt + 1; end else begin h_cnt <= h_cnt + 1; end end end endmodule这个计数器的h_cnt、v_cnt就是当前像素的图像坐标,灵活得很:想画边框、做分屏、定位文字区域,只要比较坐标就行。我这里用de作门控,没有数前肩后肩,因为对图像处理来说通常只关心有效区域。如果你要生成完整的输出时序,那就得把HTotal、VTotal也数进去,逻辑再加一层。
4.3 环路输出:外置发送芯片和FPGA原生TMDS怎么选
环路输出的出口有两条路,取决于你的板卡设计。第一条路是走ADV7513这类外置发送芯片,FPGA只需输出RGB+DE+HSYNC+VSYNC+像素时钟,芯片负责8b/10b编码和串行输出,稳定省事。第二条路是板子上没有发送芯片,HDMI输出座直接连到FPGA IO上,那就要在FPGA里自己实现TMDS编码器和OSERDESE2串化器。两条路我都跑过,说下区别:
| 方案 | 外置发送芯片(ADV7513) | FPGA原生TMDS输出 |
|---|---|---|
| 开发难度 | 低,重点在寄存器配置 | 高,要写编码器+高速串化 |
| 对FPGA引脚要求 | 并行IO即可 | 需要支持IO串化的引脚,且要加约束 |
| 时序收敛 | 轻松,像素时钟域为主 | 难,要处理5倍时钟甚至10倍数据率 |
| 适合阶段 | 入门、产品级方案 | 深入理解HDMI协议、极限优化时用 |
如果你用的是带ADV7513的板卡,环路输出只需按手册配好寄存器、把输入数据打给输出芯片就行。但如果你手上只有一块最简开发板,或者想彻底搞懂HDMI编码,那就必须走原生方案。原生方案的核心是:软件8b/10b编码 + OSERDESE2把10bit并行转成高速串行比特流。
// OSERDESE2 例化要点:10:1 DDR模式,串行速率 = 像素时钟 × 5 OSERDESE2 #( .DATA_RATE_OQ("DDR"), .DATA_WIDTH(10), .SERDES_MODE("MASTER") ) oserdes_tmds_ch0 ( .CLK(clk_pixel_x5), // 5×像素时钟 .CLKDIV(pixel_clk), // 像素时钟 .D({tmds_data[9:0]}), // 10bit编码数据 .OQ(tmds_p), // 差分正极输出 .OB(tmds_n) // 差分负极输出 );我自己第一次跑原生TMDS,在时序上撞了好几天墙:OSERDESE2的CLK和CLKDIV相位关系必须正确,引脚约束里还得显式声明串行数据速率,否则Vivado里综合能过、上板就是花屏。所以很诚恳的建议是:入门先走外置芯片方案,原生TMDS放到第二或者第三阶段再自己啃,收获会大很多,也不会被劝退。
4.4 给环路数据加点“看得见的处理”
纯透传验证成功之后,建议立刻加图像处理来证明自己“真的控制了像素”。最实用的是改颜色。比如把红色通道和蓝色通道交换,屏幕上原本是彩条图案的话,红蓝会瞬间对调,肉眼一目了然。这段逻辑只有一行assign语句:
assign out_rgb[23:16] = in_rgb[7:0]; // 输出红色 = 输入蓝色 assign out_rgb[15:8] = in_rgb[15:8]; // 绿色不变 assign out_rgb[7:0] = in_rgb[23:16]; // 输出蓝色 = 输入红色进阶一点可以加边框叠加,在图像四周画一个亮色框,这非常考验行列坐标是否正确。做法是利用前面计数器的值,把靠近图像边界8个像素的区域强制赋成目标色:
wire is_border = (h_cnt < 8) | (h_cnt >= H_ACTIVE - 8) | (v_cnt < 8) | (v_cnt >= V_ACTIVE - 8); assign out_r = is_border ? 12'hFFF : in_r; assign out_g = is_border ? 8'h00 : in_g; assign out_b = is_border ? 8'h00 : in_b;如果边框位置正确、边缘清晰不抖动,说明整套像素通路已经完全打通,你对视频数据的控制是真的,不是碰运气。
5. 上板调试实录:从黑屏到正常显示
5.1 上板前先检查这四件事
每次带人做这个实验,我发现至少有一半“诡异故障”,都是上板前没做好检查。建议按这个清单过一遍:
- 引脚约束有没有核对过原理图?HDMI输入输出芯片的I2C引脚、数据引脚、时钟引脚,一个都不能错。
- 时钟约束做了没有?给像素时钟引脚加上约束,用
create_clock声明148.5MHz,否则时序分析没有依据。 - HDMI线连对接口没有?很多板卡有多个HDMI口,必须分清输入还是输出,插错口是最低级的黑屏原因。
- EDID和5V供电是否正常?信号源要能读到显示端的EDID才肯输出,HPD引脚如果电平不对,信号源会被认定“没有显示器”。
这四条我踩过的概率排序:约束漏加 > 线插错 > EDID/HPD异常 > 引脚笔误。“管线时序”这句话说一万遍都不多。
5.2 用ILA和VIO定位输入通路
上板后如果屏幕没画面,别急着怀疑输出,先用调试工具看输入。Vivado的ILA(片上逻辑分析仪)和VIO(虚拟IO)在这里是神器。我的排查顺序是:
第一步,VIO读ADV7611的ID寄存器。ID读得出来,说明I2C通、芯片活。第二步,看输入端有没有像素时钟,ILA抓pixel_clk上升沿,看RGB和DE波形。如果DE完全没有高电平,说明芯片没有锁定输入信号;如果DE有高电平但没变化,说明可能锁了一个无效分辨率。第三步,检查hsync和vsync极性和频率是否和信号源匹配,看计数器算出来的HTotal、VTotal是不是和你预期一致。
ILA抓波形时有个容易犯的错:采样时钟设错了。比如你用了ILA自己的100MHz采样去抓148.5MHz的像素时钟信号,采样率不够,看到的DE和RBG完全不是真实状态。正确的做法是直接用输入像素时钟作为ILA的采样时钟,或者至少满足奈奎斯特条件,否则你会被假波形带偏很久。
5.3 验证环路输出和延迟情况
输入通路的信号正常了,再确认输出。先用纯透传模式,不做什么处理,直接转发。如果显示器正常显示,说明输入->FPGA->输出整条链路没有问题。此时可以打开秒表,用拍摄设备同时录信号源和显示器,数一下画面切换的帧差:透传模式下通常从源画面变化到屏幕变化,肉眼完全看不出延迟,也就在几毫秒量级,因为根本没有帧缓存。
接下来把4.4的颜色交换打开,你会看到屏幕上的红蓝互换,这不是故障,而是“处理生效”的标志。我每次带人做实验都会强调:看到异常颜色不要慌,先想想是不是自己刚才开了某种处理。反过来,如果开了颜色交换屏幕却没变化,那说明数据根本没从FPGA里走,可能被旁路了,要回头检查引脚和使能信号。
6. 常见问题速查:无画面、花屏、错位的根因定位
6.1 按症状快速定位问题
做这个实验会遇到的故障,翻来覆去就那么几类,我整理成了一张表,排查时按图索骥效率很高:
| 现象 | 可能原因 | 优先排查方向 |
|---|---|---|
| 完全无画面 | HDMI线插错、HPD异常、I2C未配置成功 | 检查输入输出口、HPD电平、VIO读ID |
| 雪花/满屏花点 | 信号源未锁定、时钟约束缺失、TMDS链路不稳 | ILA抓像素时钟和DE,确认芯片锁定状态 |
| 图像整体偏移/错位 | 同步信号极性反了、时序参数配错 | 检查HSYNC/VSYNC极性,核对HTotal/VTotal |
| 颜色发紫/发绿 | RGB通道错位、像素格式配置错 | 检查ADV7611/ADV7513的像素格式寄存器 |
| 图像有残影/拖尾 | 数据同步没打好拍、输出时钟相位不对 | 检查和输出芯片的数据建立保持时间 |
| 信号源检测不到设备 | HPD引脚异常、EDID没读到 | 确认HPD上拉、EDID ROM和I2C EEPROM通路 |
6.2 我遇到过的三个最坑细节
第一坑是复位时序。ADV7611上电后需要一小段时间稳定,I2C配置如果来得太早就写不进去。我的解决办法是在逻辑里做一个计数器,上电后延时100ms再开始配置,或者在VIO里手动触发配置,等芯片完全就绪。
第二坑是像素时钟约束。Vivado默认不知道你的pixel_clk是多少频率,不写约束也能综合,但布局布线时工具会乱猜,结果就是时序收敛得稀烂,上板自然花屏。加一行create_clock -period 6.734 -name pixel_clk [get_ports pixel_clk],工具才会认真去优化,这条看着无关紧要,实际作用极大。
第三坑是HDMI线材和接口的质量。实验桌上经常混着各种来历不明的线,有些线芯径损失太大,1.485Gbps信号跑上去就崩,表现出来就是“换个线就好了”。遇到诡异问题,先换一根线排除物理层,别在逻辑里死磕。
7. 从环路输出开始继续往下走
环路输出实验打通之后,你再看视频相关的FPGA项目,思路会清晰很多。市面上的视频处理板卡,本质都是这条通路的扩展:加DDR帧缓存就能做缩放和帧率转换,加OSD模块就能在画面上叠字叠图,加多路输入和交叉开关就能做无缝切换。这些进阶功能,底子还是你在这个实验里理解的像素、时序、时钟和同步。
我始终觉得,FPGA学视频处理,最忌讳一上来就碰MIPI、PCIE这种高速复杂协议。先把HDMI环路输出这种“够得着”的题目啃到能讲给别人听,后面的路会顺很多。下一步建议可以做:把透传改成缓存一行的行缓冲实现简单去隔行,或者用按键切换多种图像处理效果。继续做下去吧,这条路上每往前走一步,你都会发现之前学的东西全都在派上用场。