
1. 项目思路与硬件选型为什么先做“环路输出”做FPGA图像处理的同学早晚会碰上一件事怎么把外部视频源接进FPGA处理完再送出去。市面上很多开发板带HDMI输入输出接口但真正能把一根HDMI线从摄像头或电脑接进来、再通过另一根线送到显示器的教程并不多。黑金这套“FPGA基础HDMI视频输入与环路输出实验”表面上是一个入门实验实际上把视频采集链路里最关键的几个环节全部串起来了。所谓环路输出Loop-out就是把输入端接收到的视频信号经过FPGA处理后或者不处理直接通过发送端送出去。它和“采集存储回放”最大的区别是环路输出对实时性要求极高端到端的延迟通常要控制在几毫秒以内因此不能把视频数据放进DDR3/4里转一大圈再出来必须走“边收边发”的流水线结构。我先说结论这个实验适合谁适合刚学完Verilog基础语法、想接触真实视频协议的人也适合那些已经会用FPGA做LED流水灯、UART通信但对“像素时钟”“行场同步”“TMDS编码”完全没有概念的同学。做完这个实验你会真正理解视频数据流在FPGA内部是怎么流动的而不是停留在“写写代码、仿真一下”的层面。从硬件选型上黑金这块开发板用的是Xilinx Artix-7系列FPGA板载两路HDMI接口——一路输入、一路输出。选择Artix-7的原因很简单内置GTP高速收发器但HDMI这种协议其实用不到GTP直接用LVDS差分引脚就能跑。Artix-7的普通IO引脚支持的最高速度可以覆盖HDMI 1080p60Hz所需的3.4Gbps数据率所以不需要额外的高端器件成本控制得住学习门槛也低。这里补充一个关键背景HDMI协议在物理层走的是TMDSTransition Minimized Differential Signaling最小化传输差分信号通道一共4对差分线——3对数据线蓝色、绿色、红色通道加1对时钟线。时钟频率由视频分辨率决定比如720p60Hz的像素时钟是74.25MHz1080p60Hz则是148.5MHz。每对数据线上每个时钟周期传输10bit数据其中8bit是有效像素数据另外2bit是编码控制位。换句话说FPGA要处理的不是简单的“像素字节流”而是经过8b/10b编码后的串行数据。2. TMDS协议拆解从像素到差分信号的完整旅程要想顺利实现HDMI输入解析先得把TMDS的编码机制彻底吃透。很多新手拿到官方文档就懵了一堆专业术语看着头疼。我换个方式讲。2.1 像素时钟与数据通道的关系先说最核心的带宽计算逻辑。HDMI 1.4规定单通道最高数据率为3.4Gbps3个数据通道合计10.2Gbps。而1080p60Hz的像素时钟是148.5MHz每个像素3个颜色分量各8bit理论上需要的裸数据率是148.5MHz × 24bit 3.564Gbps。但TMDS做了8b/10b编码实际传输率为148.5MHz × 30bit3通道 × 10bit 4.455Gbps。这刚好在HDMI 1.4的带宽范围内。这里有个初学者容易忽略的细节每个数据通道的10bit不完全是像素数据。TMDS在传输有效像素时8bit像素数据经过编码变成10bit但在行消隐和场消隐期间则传输控制信号HSYNC、VSYNC、DE等和控制数据包。如果把这些通通算进去实际的有效数据吞吐会比理论值低所以做FPGA工程时留给时钟余量很重要。这也是为什么我在做时序约束时会把像素时钟的余量留到10%以上。2.2 编码器内部到底做了什么TMDS编码器做的事可以简单概括为两个步骤首先把8bit像素数据转换成10bit符号然后通过差分线发送出去。接收端解码器反向操作先从差分信号中恢复出10bit符号再解码成8bit像素数据和控制信号。编码器的核心算法分两步走“异或运算”和“异或非运算”。具体选哪种取决于输入数据中“1”的数量。目的只有一个把传输线上的电平跳变次数降到最低从而减少电磁干扰。这个机制和USB、PCIe这类高速串行协议用8b/10b编码的初衷是一样的只是TMDS的查表规则更特殊一些不展开写数学推导但你得知道它存在。2.3 接收端必须处理的三个同步问题FPGA接收HDMI信号时有三个同步问题必须正确处理否则画面必然花屏或黑屏时钟恢复与对齐HDMI把像素时钟单独用一对差分线传输所以接收端不需要CDR时钟数据恢复电路直接用PLL锁定外部时钟就行。这是一个巨大的简化也是为什么很多FPGA开发板敢用普通IO口接HDMI的原因。通道去偏移Deskew3对数据线在PCB走线上长度会有差异导致同一时刻的像素数据到达FPGA的时间不一致。FPGA内部需要通过可调延迟单元IODELAY逐通道校准把3个通道的数据对齐到同一个时钟沿。这个校准过程通常用“训练序列”完成但HDMI不像PCIe那样有专门的训练序列所以很多方案的做法是在采集端先检测DE数据使能信号的上升沿然后以此为基准对齐。像素重组3个通道串行传回的数据每10bit对应一个颜色分量。颜色分量按照B、G、R顺序排列需要正确拼接成32bit或48bit的像素数据总线。如果通道接反或字节序搞错画面会出现颜色通道互换——典型症状就是红蓝交换。2.4 环路输出实验的关键意义这个实验最妙的地方在于它不要求你对视频信号做任何深度处理只是为了验证“输入解析→FPGA裸透传→输出编码”这条链路是否畅通。环路输出一旦通了就证明你的接收端、发送端、时钟架构、数据对齐全部正确。后续不管你是做边缘检测、色彩空间转换还是做多路视频拼接都只是在这条链路上加处理模块的问题。很多工程师在实际项目中绕过环路输出这个步骤直接上手写图像处理算法结果最后排查下来发现数据源本身就是错的——那种挫败感我相信不少人都经历过。3. 硬件电路与FPGA管脚约束实战前面讲了协议层面的原理现在落到具体工程上。你不能光知道“HDMI有4对差分线”还得搞清楚它们接到FPGA哪个BANK、用哪些引脚、怎么约束。3.1 黑金开发板的HDMI硬件设计要点黑金这块板子的HDMI输入接口用的是标准的19脚Type-A母座板载ESD保护电路差分对直接连接到FPGA的HRHigh Range或HPHigh PerformanceBANK。建议在工程里优先查看原理图确认输入差分对接的BANK因为不同BANK的IO标准支持能力不一样。HDMI在硬件设计上有一个关键点差分对的等长走线。在PCB布线时同一对差分线的长度差必须控制在5mil以内三对数据线之间的长度差最好控制在20mil以内。你买到的开发板是原厂设计好的走线质量通常可靠但如果你打算自己画板子做HDMI接口这个等长原则必须严格遵守经验不足的人很容易在这里翻车。3.2 XDC约束文件的写法与技巧在Vivado工程里HDMI接口的约束分三部分引脚位置约束、IO标准约束、以及时钟约束。引脚位置约束直接照搬原理图比如set_property -dict {PACKAGE_PIN AF17 IOSTANDARD LVDS} [get_ports hdmi_clk_p] set_property -dict {PACKAGE_PIN AG17 IOSTANDARD LVDS} [get_ports hdmi_clk_n]这里要特别提醒HDMI差分对的参考电压VCCO必须正确配置。很多开发板HDMI接口用的BANK的VCCO是1.8V或2.5V使用LVDS标准时需要保证VCCO与IO标准匹配。如果不匹配轻则信号质量差重则完全无法锁定。时钟约束更讲究。HDMI输入的像素时钟是外部时钟频率取决于上游设备输出什么分辨率。你不能在XDC里写死一个148.5MHz因为对方可能输出720p的74.25MHz。常用的处理方式是create_clock -name hdmi_clk -period 13.468 [get_ports hdmi_clk_p] set_input_delay -clock hdmi_clk -max 2.0 [get_ports hdmi_data_p*] set_input_delay -clock hdmi_clk -min 0.5 [get_ports hdmi_data_p*]-period 13.468对应74.25MHz1秒/74.25MHz 约等于13.468ns但你心里要清楚这只是初始约束。实际跑起来如果上游设备切到1080p这个约束就失效了。考虑到这个实验主要固定用某个分辨率把关键时序参数固定下来没问题如果要做自适应需要配合MMCM/PLL的动态重配置功能那是进阶玩法。3.3 LVDS与TMDS信号电平的适配问题HDMI的TMDS电气标准和LVDS不完全一样直接拿LVDS的IP核接收HDMI信号是不行的。HDMI的信号摆幅更大、共模电压不同FPGA的LVDS接收器虽然能“容忍”HDMI信号的电压范围但正规做法是外接转换芯片比如德州仪器的TMDS141HDMI 1.4转LVDS桥接芯片或者用板载的HDMI转并行RGB方案。黑金这块板子上的做法是HDMI输入接口经过板载的转换芯片通常是TI的TFP401或ADV7611系列把TMDS串行信号转成并行RGBHSYNCVSYNCDE信号再送进FPGA。这样FPGA端就不必处理高速串行数据而是直接处理并行像素总线——接口频率只有几十到一百多MHz任何FPGA都轻松搞定。这一点非常重要它直接决定了代码复杂度。很多初学者一上来就要在FPGA里做8b/10b解码结果被串行数据折腾得痛不欲生。实际上大多数开发板已经帮你把最难的物理层解调做完了你只要负责并行数据通路。4. 代码实现RX并行接收、像素流水与TX编码发送现在进入真正的代码环节。我先把整个工程的模块结构画出来再逐个模块讲解。这个项目的源码结构大致如下hdmi_loopback_top.v ├── hdmi_rx_decode.v // 接收并行数据解析DE/HS/VS ├── sync_process.v // 同步时序处理像素对齐 ├── video_pipeline.v // 图像处理管道本实验为直通/灰阶变换 ├── hdmi_tx_encode.v // 发送端编码与控制信号生成 └── clock_gen.v // PLL时钟管理4.1 接收端模块从板上芯片到像素总线由于板上转换芯片已经输出了并行数据接收模块的核心任务变得非常轻量正确采样并行数据解析出行场同步信号和DE并把像素数据打包成统一的内部格式。转换芯片送出的数据格式一般是24位RGB数据8位R、8位G、8位B加上HSYNC、VSYNC、DE三个控制信号以及像素时钟PCLK。由于并行数据位宽较大而FPGA内部频率通常没有必要跑到200MHz以上这里直接采用“像素时钟域同步设计”——也就是说整个数据通路都在PCLK比如148.5MHz下运行。下面的代码展示了典型的接收端采样逻辑always (posedge pclk or posedge rst) begin if (rst) begin rgb_data 24d0; de_dly 1b0; hs_dly 1b0; vs_dly 1b0; end else begin // 打拍同步消除亚稳态风险 {de_dly, hs_dly, vs_dly} {de_in, hs_in, vs_in}; rgb_data {rgb_r, rgb_g, rgb_b}; end end代码本身非常简单但有两个陷阱必须提示第一转换芯片输出的PCLK相位可能与数据有一定偏差建议先用IDELAY调整输入延迟或者在芯片配置端开启时钟相位调整功能。否则在高速分辨率下容易采到建立时间不够的数据。第二DE信号在行场消隐期间为低电平这段时间送往HDMI发送端的像素数据必须填充成黑色或者也可以填充成自定义的背景色。否则发送端会把消隐期的“垃圾数据”当有效像素发送导致画面边缘出现花屏。4.2 环路输出的核心像素直通与同步信号再生环路输出最朴素的实现就是把接收端解析出来的RGB数据直接接到发送端的RGB数据总线。但是有一个细节不能偷懒同步信号必须经过时序重整之后再输出。为什么不能直接透传HSYNC/VSYNC因为发送端芯片往往需要精确的同步脉冲宽度尤其是对于某些显示器脉宽太窄或者不对称会导致画面偏移甚至黑屏。所以稳妥的做法是接收端把HSYNC/VSYNC的极性、前后肩计算出来然后在发送端重新生成。在直通模式下发送端模块的核心逻辑是wire [23:0] out_rgb bypass_mode ? rgb_in : process_rgb(rgb_in); always (posedge clk_pix_out) begin if (vde) tmds_pixel out_rgb; else tmds_pixel {hsync_reg, vsync_reg, control_bits}; end这里的vde是视频数据使能信号在有效区域为高。发送端规定在vde为高时传输像素在vde为低时传输控制信号。控制信号不仅包括HSYNC和VSYNC还包括HDMI规定的数据包如音频数据包、辅助数据包但在这个基础实验中可以不发数据包先把控制信号正确传出去即可。4.3 环路输出的三种模式直通、OSD叠加与简单图像处理既然代码具备了“接收-处理-发送”的完整链路这个实验就可以顺势扩展出三个有梯度的小功能第一种是纯直通模式。这是基础功能——输入什么就输出什么中间不加任何处理。这一模式最常用来自检链路把笔记本的HDMI输出接到FPGA输入再从FPGA输出接一台显示器。如果屏幕能正常显示电脑画面说明RX和TX链路都通了没有任何花屏和偏色。我强烈建议新手在这一步多花时间观察画面细节——字符边缘是否锐利颜色是否准确动态画面上有没有撕裂感。第二种是OSD叠加模式。在直通的基础上叠加一个固定的图片或文字。最简单的方法是在像素数据进入发送端模块前加一个MUX逻辑assign out_rgb (osd_enable in_osd_area) ? osd_color : rgb_in;这个功能看着简单其实是后面做字符菜单、十字线叠加、测试图案生成的基础。你会发现处理视频数据流和处理普通存储器数据完全不一样——视频数据是连续的、实时的、不能随便停顿任何组合逻辑的延迟差异都会导致像素错位。第三种是灰度化处理模式。把彩色RGB转换为灰度图这是图像处理领域最经典的算法在FPGA里实现也最简单。公式是Gray 0.299R 0.587G 0.114B在FPGA里通常用整数近似wire [15:0] gray_sum; assign gray_sum (rgb_r * 77) (rgb_g * 150) (rgb_b * 29); assign gray_out gray_sum[15:8];注意这里用移位代替浮点乘法把0.299变成77/256≈0.301把0.587变成150/256≈0.586把0.114变成29/256≈0.113。有人会问这样精度会不会不够对于8bit灰度输出已经完全够用人的眼睛几乎分辨不出误差。这种“定点化近似”的思想在FPGA图像处理项目里非常常用一定要掌握。4.4 发送端编码并行到串行的转换发送端最关键的部分是把24位并行像素数据编码成TMDS串行信号。如果板上用的是HDMI发送芯片比如SIL9013或TFP410FPGA端其实只需要输出并行RGB和同步信号芯片内部会自动完成8b/10b编码和串行化。但如果你想在FPGA内部直接实现TMDS编码用原语ISERDES/OSERDES实现也行。Xilinx提供OSERDESE2原语可以把10位并行数据串行化成一对差分信号OSERDESE2 #( .DATA_RATE_OQ (DDR), .DATA_WIDTH (10), .SERDES_MODE (MASTER) ) oserdes_inst ( .CLK (pix_clk_x5), // 5倍像素时钟 .CLKDIV (pix_clk), .D1 (encoded[0]), .D2 (encoded[1]), // ... .D8 (encoded[8]), .T1 (1b0), .OQ (tmds_out) );这里有个容易踩的坑5倍像素时钟的产生。1080p60Hz的像素时钟是148.5MHz5倍就是742.5MHz。Artix-7的OSERDES确实能跑这个频率但MMCM/PLL的输出要仔细检查742.5MHz已经比较接近器件的上限。我见过有人直接用PLL生成5倍频失败的情况后来改成2.5倍双沿DDR模式才通过时序收敛。所以如果你的开发板没有HDMI发送芯片做FPGA内置TMDS编码前先确认器件速度等级和PLL输出能力。5. 时序分析与调试实录从黑屏到稳定1080p输出这个环节是整个实验最磨人也最值钱的部分。我在做这个项目时前前后后踩了四五个坑每个坑排查过程都很有代表性。下面按时间顺序记录完整的调试过程。5.1 首次上板屏直接黑屏没有任何反应第一版代码写完烧进去显示器提示“无信号”。这是一个典型症状但也让人无从下手。我的排查顺序从易到难先检查HDMI线是否插好换线、换接口排除接触不良。用示波器看转换芯片输出端有没有像素时钟信号确认芯片是否正常初始化。用Vivado内部逻辑分析仪ILA抓FPGA引脚上的PCLK、DE、HS、VS信号看看数据到底有没有进入FPGA。结果发现PCLK有信号DE也有脉冲但HSYNC看起来怪怪的——频率只有一半脉宽不一致。最后查到原因某个早期版本的分频器在复位释放时没有正确同步导致分频输出有毛刺半途丢了一个沿。修复后HSYNC恢复正常。排查这类问题我有一个固定流程不管前面是否正常先在接收端加上计数器统计用帧计数器、每帧有效像素计数等一旦计数数值异常就能立刻定位异常的方向不用靠猜。5.2 画面出来了但颜色明显偏绿修复同步问题之后屏幕终于有画面了但颜色整体偏绿红色几乎看不到。这个症状在我看来就是“通道错位”。HDMI的3对数据线分别对应R、G、B三个通道。如果板上转换芯片或者PCB布线把通道顺序打乱了实际开发板不会打乱但你的代码可能拼错FPGA收到的像素数据里“本来该是R的位置放的是G”画面当然偏色。我检查了一下代码里的像素总线拼接顺序果然发现我把{R, G, B}写成了{B, G, R}。原因是我参考了一份网上的例程那套代码用的是BGR顺序。修正之后颜色就正常了。另一个可能的通道错位是通道间延迟不匹配。三个通道到达接收端的时刻有细微差异导致RGB数据发生错位。这个在并行数据上表现为“颜色镶边”在串行TMDS信号上则表现为严重雪花。解决办法是用IO延迟原语IDELAY逐通道调整。我调试时通过扫描IDELAY的tap值找到了最优窗口大概有20多个tap的余量说明信号质量还是不错的。5.3 画面稳定但边缘有锯齿/毛刺颜色正常之后我又发现一个更微妙的问题画面整体稳定但文字边缘、物体轮廓有明显的锯齿。这个现象在FPGA视频项目里非常常见本质是时钟抖动或采样相位不佳。解决方案有两个方向一是调整输入时钟的相位。Vivado里可以直接用PLL的CLKOUT_PHASE_CTRL调整或者在MMCM的输入路径上加可编程延迟。对比效果后通常能找到一个锯齿最小的相位点。二是检查电源质量。HDMI接口的供电如果纹波过大会导致转换芯片输出时钟抖动剧增。我实测黑金板用USB供电时画面边缘锯齿明显变多换成适配器供电后锯齿立刻好转。这一点很少有人提但经验丰富的老工程师都会优先检查供电。5.4 环路输出时偶发丢帧、画面卡顿这个问题出现在我把OSD叠加模块接入数据通路之后。运行几分钟画面偶尔会冻结一下然后又恢复。我首先怀疑是代码里某个计数器溢出或者状态机跳飞出问题。用ILA抓了几次发现确实有些像素行的数据在消隐期被误当成有效像素发送了。查代码后确认OSD坐标判断模块用了组合逻辑逻辑层级太深在148.5MHz时钟下时序违例导致某些时刻判断结果不稳定。解决办法很朴素——把“是否在OSD区域”的判断逻辑改成寄存器输出在输入侧提前一拍计算好输出侧直接使用。always (posedge pclk) begin osd_active (hcount osd_x_start hcount osd_x_end vcount osd_y_start vcount osd_y_end); end assign out_rgb osd_active_dly ? osd_color : rgb_in;这里还引出一个重要经验在像素流水线上插入组合逻辑时一定要给逻辑留出足够的时间余量。如果某一级组合逻辑延误超过一个像素时钟周期就会影响后续所有的像素处理。工程上的惯例是像素数据路径上每插入一个功能模块就至少增加一级流水寄存器。5.5 1080p死活上不去只能稳定跑720p这是实验室里最容易遇到的问题之一。Vivado时序报告中发送端OSERDES路径的WNS最差负时序裕量为负说明742.5MHz的串行时钟没有收敛。我当时的处理步骤是第一步检查RTL代码的时钟域划分。之前的代码把像素时钟域和串行时钟域的逻辑写得比较乱导致布线工具无法优化。后来强制分成两个独立时钟域中间用FIFO或寄存器隔离。第二步优化关键路径。TMDS编码器的10位查表逻辑如果直接写成always块里的case综合后会生成很长的组合链路。我改用预计算表LUTROM把关键路径长度降了一半。第三步调整流水线结构。在编码器输出端插入两级dff让时序工具在布局布线时有更多余地。做完这三步时序报告就完全收敛了。1080p60Hz画面稳定输出长时间运行无异常。5.6 分辨率切换后花屏笔记本在不知情的情况下把输出分辨率从1080p切到720p比如接投影仪时系统会自动调整结果画面花成一团。这个问题的本质是我的PLL锁定的输入像素时钟还是1080p的148.5MHz但上游设备已经切成720p的74.25MHz。PLL失锁后没有自动重锁定机制输出端还按原来的时序发送数据。解决思路有三个层次第一个层次项目里固定只支持一个分辨率不考虑动态切换。这是最简单也最省事的方案适合教学实验。第二个层次在硬件上配置成“时钟丢失自动复位”模式当输入时钟变化导致PLL失锁时PLL输出Locked信号拉低触发整个发送端模块复位并重新初始化。第三个层次也是工程中真正靠谱的方案用动态时钟切换。FPGA内部放两个PLL分别锁定在常用分辨率对应的频率上然后根据输入时钟频率实时选择正确的PLL输出。这需要用到MMCM/PLL的动态重配置端口DRP实现起来有一定复杂度但在做多分辨率自适应项目时是必须掌握的。6. 常见问题速查表与调试经验总结下面这张表是我在实际调试中整理的常见问题速查表每一项都是踩过坑验证过的不是从文档里抄来的。现象可能原因排查方向完全黑屏、显示无信号TX时钟未工作PLL失锁用ILA抓PLL locked、像素时钟信号偏色红蓝交换/偏绿RGB通道拼接错误、通道延迟不匹配检查代码拼接顺序扫描IDELAY值上下滚动的黑条或画面偏移同步信号脉宽不对、前后肩数值错误检查HSYNC/VSYNC再生逻辑对照规范文字边缘锯齿明显时钟抖动大、采样相位不佳调PLL相位换更稳定电源偶发丢帧、画面卡顿组合逻辑时序违例、FIFO溢出查时序报告加流水寄存器切换分辨率后花屏PLL未自动重锁定增加PLL失锁检测和复位逻辑长时间运行死机代码有状态机跳飞出或FIFO读空写满未处理加状态机保护FIFO水印标志上报这套实验做完我最大的体会是FPGA做视频处理代码难度反而不高难的是时序意识和排查思路。HDMI环路输出看起来只是一个直通实验实际上把时钟架构、信号完整性、数据同步、跨时钟域、时序收敛这些基本功全部涵盖在内。如果你现在刚开始学FPGA建议按照“先做HDMI直通→再做OSD叠加→再做灰度/边缘处理→最后做多分辨率自适应”的顺序推进。每前进一步你对视频数据的理解都会扎实一分。黑金这套实验的价值不在于代码本身而在于它把一条真实、完整、可复现的视频处理链路摊开给你看——这里面每一环单独拎出来都不难但串在一起就是你能拿得出手的FPGA图像处理项目基础。最后分享一个小技巧调试HDMI视频项目时不要用笔记本屏幕做信号源信号源分辨率建议固定在1080p60Hz不要开HDR、不要开可变刷新率。用带HDMI输入的台式机显示器接收FPGA输出配合一张合适的测试图比如色彩条、网格、文字混排的图片你会更快发现画面中的细微异常。等整套链路稳定了再慢慢增加分辨率和功能的复杂度也不迟。