news 2026/9/8 11:33:59

FPGA入门必做:HDMI环路输出实验详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA入门必做:HDMI环路输出实验详解

做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每个像素一个时钟周期
HActive1920每行有效像素数
HFrontPorch88行前肩
HSync44行同步脉冲宽度
HBackPorch148行后肩
HTotal2200整行总像素数
VActive1080有效像素行数
VFrontPorch4场前肩
VSync5场同步脉冲宽度
VBackPorch36场后肩
VTotal1125总行数

验证一下: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环路输出这种“够得着”的题目啃到能讲给别人听,后面的路会顺很多。下一步建议可以做:把透传改成缓存一行的行缓冲实现简单去隔行,或者用按键切换多种图像处理效果。继续做下去吧,这条路上每往前走一步,你都会发现之前学的东西全都在派上用场。

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

2027研究生开题报告一键生成工具完整度与价格横评

2027研究生开题报告一键生成工具完整度与价格横评 在电气工程与智能电网多能互补综合能源系统&#xff08;IES&#xff09;低碳优化调度与能量管理策略方向的研究生开题阶段&#xff0c;很多硕博同学都在寻找高效且严谨的开题辅助工具&#xff1a;2027研究生开题报告一键生成工…

作者头像 李华
网站建设 2026/9/8 11:31:45

从分布式系统视角解析组织架构与决策机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:31:34

硬件工程师应届生技能清单:从真实工作流反推学习优先级

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:30:32

MODBUS RTU串口调试实战:帧结构、CRC校验与寄存器映射全解析

1. 写在前面&#xff1a;为什么这条老协议至今仍是调试台上的主角 做嵌入式这些年&#xff0c;串口调试助手一直是我电脑上打开率最高的工具之一&#xff0c;而MODBUS协议又几乎是串口调试里绕不开的坎。这次的项目笔记其实源于一个很常见的场景&#xff1a;设备端采集板换了一…

作者头像 李华
网站建设 2026/9/8 11:30:05

UE5山湖环境制作全流程:地形、水体、材质与光照实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华