1. 实验整体设计与硬件架构拆解
1.1 环路输出到底是什么
先把这个实验名字拆开看。HDMI视频输入,指的是通过HDMI接口把外部视频源(比如电脑、机顶盒、另外一块开发板)送过来的画面信号,让FPGA成功接收到;环路输出,英文叫Loop Out,是指FPGA把接收到的视频信号原封不动地再送出去,接到显示器上能看到和输入完全一样的画面。
很多刚接触FPGA视频处理的人第一反应是:环路输出不就是一根线分成两路吗?实际上在FPGA开发板里,事情没那么简单。这个实验本质上是让你彻底搞清楚一条完整的视频数据链路:HDMI接口物理层信号进来之后怎么被解码,变成FPGA能处理的并行数据,经过FPGA内部之后又如何被重新编码成HDMI信号送出去。环路输出只是验证这条链路通不通的最直观方式——输入什么,输出什么,画面一致性一目了然。
这个实验在整个FPGA视频开发的学习路径里属于承上启下的关键节点。往上接HDMI接口协议、TMDS编码、DDC通道这些基础概念,往下接帧缓存、图像缩放、色彩空间转换、OSD叠加等真正的图像处理算法。如果你打算以后做图像采集、视频拼接、AI推理前处理这类项目,这个实验就是绕不开的第一课。
1.2 硬件链路与芯片选型
做这个实验,你得先清楚开发板上视频信号实际走的物理路径。以常见的黑金FPGA开发板为例,整条链路通常是这样的:
HDMI输入接口进来,信号先到HDMI接收芯片,比如ADI的ADV7611或者Silicon Image的SiI9134,这类芯片负责把HDMI串行差分信号(TMDS)解码成并行视频数据,包括24位RGB数据、行场同步信号、数据有效信号DE和像素时钟,然后这些并行信号送入FPGA的GPIO引脚。FPGA内部可以做各种处理,处理完之后把并行数据再交给HDMI发送芯片,比如ADV7513,由它重新编码成TMDS差分信号送给显示器。
这里要强调一点:FPGA本身并不直接处理HDMI的TMDS高速串行信号,而是通过外接的PHY芯片(物理层收发器)完成串并转换。FPGA只处理并行视频总线。这就好比网线接口和网卡芯片的关系,FPGA更像是主板上的CPU,它拿到的是已经转换好的数字信号,而不是网线上那种模拟差分信号。
我对ADV7611和ADV7513这对组合比较熟悉,因为它们在国产FPGA开发板上用得特别多。ADV7611支持最高1080p60的输入分辨率,内置EDID RAM,可以通过I2C接口配置;ADV7513支持最高4K30的输出,同样通过I2C配置。两个芯片都是3.3V供电,I2C接口电平也是3.3V,跟FPGA引脚直接对接没问题,但要注意有些型号是1.8V接口的,选型时一定要看数据手册。
1.3 两种环路实现方式对比:模拟直通与数字环路
环路输出在硬件实现上有两种思路。第一种是模拟直通,把输入的TMDS差分信号通过PCB走线直接分出一路到输出接口,信号完全不经过FPGA处理,纯粹是物理层的fan-out。这种方式延迟最低、电路最简单,但缺点是没法做任何图像处理,而且对PCB布线要求很高——HDMI属于高速差分信号,阻抗控制、等长匹配、串扰抑制稍微不到位,画面就会出现雪花点或者直接黑屏。
第二种是数字环路,也就是本实验采用的方式。HDMI接收芯片把信号解码成并行RGB数据送进FPGA,FPGA再把这份数据原封不动地交给发送芯片编码输出。虽然多了一次串并转换和重新编码,信号延迟会多出几十个微秒,但好处非常明显:数据在FPGA内部是可见、可控的,你可以随时插入图像处理逻辑,也可以在画面的任意位置叠加OSD信息,甚至可以做帧率转换和分辨率缩放。这就是为什么做视频处理项目时几乎都选数字环路而不是模拟直通。
两个方案的核心区别我列个表格做个对比:
| 对比项 | 模拟直通 | 数字环路 |
|---|---|---|
| 信号处理方式 | 物理层直接分发差分信号 | 接收芯片解码后FPGA处理再编码 |
| 是否可插入图像算法 | 不可以 | 可以 |
| 延迟 | 纳秒级 | 微秒级 |
| PCB设计难度 | 高,需严格阻抗匹配 | 相对低,只需要保证并行信号质量 |
| 灵活性 | 无 | 高,可扩展图像处理 |
做实验阶段,数字环路明显是更有价值的选择,因为你后面所有视频处理实验都是在这条数字链路上扩展出来的。
2. 关键电路细节与工程搭建
2.1 HDMI接口电路与供电电流的几个细节
很多人第一次画HDMI电路时只关心那几对差分线,结果做出来的板子问题一堆。我整理一些实测中的关键点,供你画板或者排查问题时参考。
先说HDMI接口的5V供电。HDMI规范里要求源端设备通过19脚输出5V电源,用于给显示器的DDC(显示数据通道)和HPD(热插拔检测)电路供电。这个5V需要多少电流?规范规定最小值是50mA,但实际设计中我一般留到200mA以上的余量。原因很简单,显示器端的EDID EEPROM上电瞬间会有比较大的浪涌电流,如果供电能力不足会拉低5V电压,导致显示器检测不到源端设备,表现为“无信号输入”。
HPD引脚的处理也是一个典型坑。HPD是HDMI接口的第18脚,由接收端(显示器)拉高表示“我已连接”。源端主机通过检测这个引脚电平来判断是否要发起HDMI通信。开发板上通常会让HPD经过一个电阻连接到FPGA或者接收芯片的检测引脚,但有些板子为了省事直接把HPD接死在高电平,这样会导致一个问题:主机无法感知显示器的插拔事件,也就不会重新发送视频信号。做实验时如果你发现显示器睡着了叫不醒,先量一下HPD有没有正常拉高。
再说TMDS差分线的ESD保护。HDMI接口经常被热插拔,如果没有ESD防护器件,静电很容易打坏接收芯片的输入级。正规设计会在每组差分线上加TVS管或者ESD阵列,比如USBLC6-2SC6这类器件,摆放位置要尽量靠近连接器。开发板上一般已经加了这些保护,但如果你是自己画的底板,这个钱不要省。
最后是HDMI输出侧的电流驱动能力。HDMI源端输出需要驱动15米甚至更长的线缆,TMDS对的驱动电流通常在10mA左右,由发送芯片内部的可编程电流源控制。ADV7513之类的芯片通过寄存器可以调节输出电流幅度和预加重参数,线缆较长或者画面有重影时,可以试着调大驱动电流和预加重。
2.2 FPGA内部数据通路设计
搞清楚了硬件链路,接下来就要设计FPGA内部的数据通路。这个实验的核心通路非常清晰:接收芯片过来的并行视频数据,经过一个数据对齐模块,然后进入一个可选的图像处理模块(实验阶段直接透传),最后送到发送芯片。
并行视频数据长什么样?以24位RGB模式为例,每个像素时钟周期传输一个像素,数据线包括RGB各8位、VS(垂直同步)、HS(水平同步)、DE(数据有效)以及像素时钟PCLK。DE有效的时候,RGB总线上是有效像素数据;DE无效的时候,总线上的数据无效,对应的是行消隐和场消隐区间。
FPGA内部代码上,我习惯的第一步是给所有输入信号加一级寄存器打拍,做时钟域同步和信号对齐。不要小看这一步,虽然看起来只是延迟了一个时钟周期,但能有效避免亚稳态问题,特别是在PCLK与FPGA内部时钟不是同源的情况下。环路输出实验里FPGA不涉及跨时钟域处理,接收芯片输出的PCLK直接作为整个视频通路的时钟,这样最简单也最稳定。
数据通路的基本代码思路可以概括为:输入信号打拍对齐,然后根据DE信号把像素数据分流,有效数据进入处理逻辑,处理完再和VS、HS、DE、PCLK一起打包送给发送芯片。实验阶段的“处理逻辑”就是直接透传,但模块接口上要留好位置,方便以后插入真算法。
2.3 I2C配置与寄存器初始化要点
HDMI接收和发送芯片都是需要初始化配置的,配置手段就是I2C总线。这一步是实验能否成功的核心,很多新手在这里卡了一整天,板子检查了半天没毛病,结果代码一跑还是黑屏,最后发现I2C根本没通。
为什么需要配置?因为接收芯片和发送芯片上电后不是默认就能工作的,必须通过I2C写寄存器告诉它们:输入信号是什么格式、输出分辨率是多少、色彩空间怎么处理、是否启用内部均衡器等等。ADV7611在上电后有一整套初始化序列,要依次写入几十个寄存器值才能进入正常工作状态。
I2C配置这块我建议你不要自己凭感觉写寄存器,直接参考开发板厂商提供的初始化代码或者芯片数据手册里的推荐配置。ADV7611的驱动中有一段关键序列,包括配置输入端口选择、视频格式检测使能、输出格式选择(24位RGB还是YCbCr 4:2:2)等。其中输出格式的选择要特别注意,必须和发送芯片的输入格式匹配,否则画面颜色会错乱,或者干脆黑屏。
调试I2C时有个非常实用的技巧:先把接收芯片的I2C地址读出来确认总线畅通。ADV7611的8位I2C地址默认是0x98(7位地址0x4C),如果读不到ACK,说明硬件连接有问题或者芯片没有正常上电,不要继续往下调。我遇到过I2C上拉电阻虚焊导致总线时通时断的情况,用示波器量SCL和SDA波形就能看出来——正常的总线空闲状态应该都是高电平,如果波形有毛刺或者幅度不够,优先查上拉电阻和电源噪声。
2.4 时钟方案:像素时钟是怎么来的
视频处理和普通逻辑设计最大的区别就是有时钟连续性要求。FPGA里的视频数据通路,时钟就是接收芯片输出的PCLK,这个时钟的频率取决于输入视频的分辨率和刷新率。1080p60的像素时钟是148.5MHz,720p60是74.25MHz,1080p30也是74.25MHz。
这个时钟频率和芯片的I2C配置是配套的关系。接收芯片通过检测输入信号的时序参数,会自动识别出当前分辨率和刷新率,然后输出的PCLK频率会对应变化。FPGA这边不需要做任何动态配置,只要保证时序约束里PCLK的时钟约束正确就行。
但是这里有一个容易被忽略的问题:PCLK是一个来自外部芯片的时钟,它和FPGA内部主时钟(比如50M或者200M晶振)是异步的。如果你的设计里除了视频通路还有别的逻辑(比如按键控制、串口打印),千万不能把PCLK域的信号直接和主时钟域信号混用,跨时钟域要加同步器或者FIFO。环路输出实验里可以不做跨时钟域,但养成这个意识非常重要,后面做图像缓存时一定会用到。
3. 工程实现与Verilog核心代码
3.1 工程整体模块划分
下面进入实操部分。我用Vivado工具来演示,黑金开发板大多是Xilinx的FPGA芯片,Vivado是标配。整个工程我建议按照下面的模块划分来组织:
top.v:顶层模块,例化所有子模块,完成引脚分配iic_drv.v:I2C主机控制器,用于配置ADV7611和ADV7513hdmi_rx_cfg.v:接收芯片初始化状态机,控制I2C写入ADV7611寄存器hdmi_tx_cfg.v:发送芯片初始化状态机,控制I2C写入ADV7513寄存器video_align.v:输入视频数据对齐模块,打拍同步loop_back.v:视频数据透传模块,实验阶段用,后面可扩展
这个模块划分的思路是让每个模块职责单一,方便调试和复用。特别说一下video_align.v,很多初学者会忽略它,直接拿输入信号用,结果时序分析报一堆FAIL。HDMI接收芯片送过来的数据和PCLK之间是有固定相位关系的,但FPGA内部走线会让信号产生延迟差异,所以必须在PCLK上升沿统一打拍一次,保证所有信号对齐到同一个时钟沿。
3.2 接收端数据同步与对齐代码
先说接收端。ADV7611输出的信号有:PCLK、DE、VS、HS、RGB数据(24位)。如果想要后续做图像处理,还可能有FID字段等,实验阶段不用管。
对齐模块的代码很简洁,但作用关键:
module video_align #( parameter DATA_WIDTH = 24 )( input wire pclk, input wire rst_n, input wire de_i, input wire vs_i, input wire hs_i, input wire [DATA_WIDTH-1:0] data_i, output reg de_o, output reg vs_o, output reg hs_o, output reg [DATA_WIDTH-1:0] data_o ); // 第一级同步:直接用PCLK上升沿打拍 // 这里不需要异步处理,因为所有信号本来就和PCLK同步 always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin de_o <= 1'b0; vs_o <= 1'b0; hs_o <= 1'b0; data_o <= {DATA_WIDTH{1'b0}}; end else begin de_o <= de_i; vs_o <= vs_i; hs_o <= hs_i; data_o <= data_i; end end endmodule你可能觉得这段代码没什么技术含量,但它解决了一个真实的问题:输入信号之间的相位偏差。FPGA引脚上的信号经过IBUF、布线之后到达触发器的延迟不完全一致,如果DE比数据早到半个时钟周期,你抓到的数据就会错位,画面出现杂色条纹。打拍之后所有信号统一延迟一个时钟周期,相位自然就对齐了。
3.3 发送端配置与数据打包
发送端ADV7513的初始化是另一个关键点。它上电后默认处于待机模式,必须配置正确才能开始编码输出。核心配置包括:视频输入格式(24位RGB还是16位YCbCr)、输出分辨率对应的时钟分频参数、同步信号极性、色彩空间转换开关等。
ADV7513初始化的代码和接收端类似,也是I2C写寄存器序列。我建议在hdmi_tx_cfg.v里写成参数化的数组形式,方便修改:
// 以ADV7513为例,写入寄存器的参数列表 // 每个元素表示 {寄存器地址, 寄存器值} localparam integer REG_NUM = 47; reg [7:0] reg_addr [0:REG_NUM-1]; reg [7:0] reg_data [0:REG_NUM-1]; // 初始化阶段利用I2C驱动依次写入 // 核心配置项包括: // 0x41: 输出格式为RGB,24位 // 0x48: 使能视频输入 // 0x55: 设置像素时钟分频,根据输入分辨率选择 // 0x9A: 设置同步信号极性,适应输入信号特征这里有一个很重要的思想:发送芯片的输出并不是凭空产生的,它只是把你给它的并行数据编码成TMDS差分信号。所以输入给它的VS、HS、DE、数据和PCLK必须严格符合你期望输出分辨率的时序要求。环路输出实验里,这些信号直接来自接收芯片,相当于时序天然匹配;但如果以后你做分辨率转换,就必须自己生成一套符合目标分辨率的同步时序了。
3.4 透传模式与画面验证
所有初始化配置完成后,环路通路的核心代码其实就一句话:
// 透传模块:接收端的数据和同步信号完全原样传递给发送端 wire [23:0] rx_data_aligned; wire rx_de_aligned; wire rx_vs_aligned; wire rx_hs_aligned; loop_back u_loop_back ( .pclk (pclk), .de_in (de_aligned), .vs_in (vs_aligned), .hs_in (hs_aligned), .data_in (data_aligned), .de_out (tx_de), .vs_out (tx_vs), .hs_out (tx_hs), .data_out (tx_data) );下载到板子上之后,接上HDMI信号源,显示器应该能立刻看到画面。如果没画面,先别急,大概率不是代码问题,而是初始化卡住了。这时先确认I2C配置是否完成,比如在状态机里加一个计数器,配置完成信号拉高后再让数据通路使能,避免数据在芯片没初始化好的时候就送过去。
实测中我建议在发送端数据通路上加一个使能信号,初始化完成前输出DE拉低,初始化完成后再放行数据。这个做法虽然简单,但能避免很多莫名其妙的黑屏问题——芯片还没准备好就喂数据,它可能输出乱码甚至直接锁死。
4. 常见问题与排查技巧实录
4.1 新手最容易踩的坑:写在前面
做这个实验时,新手最容易出现几个扎心的情况:显示器显示“无信号”、画面满是雪花、颜色明显偏色、图像有噪点或花屏、画面能显示但是会间歇性闪屏。下面整理一张速查表,都是我实际调试中踩过的坑和对应的处理方法:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全无信号 | HPD没有被拉高 | 检查HDMI座子18脚电平,确认接收芯片HPD输出配置寄存器正确 |
| 完全无信号 | I2C配置失败 | 用逻辑分析仪抓I2C波形,确认地址和ACK信号正常 |
| 蓝屏/黑屏但有菜单 | 接收芯片没有检测到输入 | 检查输入信号源分辨率是否被芯片支持,检查输入端口选择寄存器 |
| 雪花点或杂色条纹 | 数据位序错位或相位未对齐 | 检查软件上数据线序配置(ADV7611的引脚交换寄存器) |
| 偏色严重 | 色彩空间不匹配 | 确认接收芯片输出RGB还是YCbCr,确认发送芯片输入格式配置一致 |
| 间歇性闪屏 | 像素时钟不稳定或电源纹波大 | 示波器测PCLK频率稳定度,检查板卡3.3V和1.8V电源纹波 |
| 画面有重影 | TMDS输出驱动电流不够或线缆过长 | 调大ADV7513输出电流幅度,换短线缆验证 |
| 只有HDMI输出能显示,VGA等接口不行 | 接口电路与接收芯片通道映射不匹配 | 核对通道选择寄存器和实际硬件接口走线对应关系 |
4.2 I2C读不回来的排查实战
I2C问题是最多的,单独拎出来详细说。I2C总线只有两根线SCL和SDA,看起来简单,但排查起来很容易让人心态崩溃。如果I2C读不回来数据,按照下面的顺序逐步排查:
第一,量电压。SCL和SDA线在总线空闲时都应该被上拉到3.3V,如果哪根线是低电平,说明有器件把它拉死了,逐个断开I2C从设备排查。第二,看波形。用示波器抓SCL的时钟信号,看频率是否正确,高电平幅度是否足够。ADV7611的I2C最高支持400kHz,但我的经验是第一版驱动放在100kHz最稳妥,信号质量差也不容易出错。第三,确认地址。7位地址和8位地址容易搞混,ADV7611的7位地址是0x4C,但写寄存器的8位地址是0x98,读是从0x99开始。很多人的代码里地址写错了,一直读不到ACK。
还有一个很容易忽略的细节:I2C总线上多个器件时,上拉电阻的阻值要合理。如果上拉电阻过大,总线上升沿太慢,高速模式就容易通信失败。2.2k到4.7k之间通常合适,具体要看总线上挂了多少个器件。
4.3 环回画面正常但有小瑕疵的处理思路
有时候环路已经通了,画面也能显示,但仔细看会发现某些颜色过渡区域有轻微的噪点,或者竖直细线边缘有彩边。这个问题多半出在色彩采样格式上。
ADV7611可以输出RGB格式也可以输出YCbCr 4:2:2格式。如果接收芯片配置为输出YCbCr 4:2:2,而发送芯片仍然希望输入RGB,中间就需要做色彩空间转换和色度插值,跳过这一步就会导致颜色偏色和边缘劣化。最简单的解决办法是让两个芯片都用RGB 4:4:4格式,中间保持透传,这样就能避免格式转换带来的画质损失。
环路输出的实验场景中我建议直接用RGB 4:4:4 24位模式,这是最简单、画质最好、中间不需要任何额外处理的工作方式。等你后面要做图像算法了,再根据算法的需要决定是否切换到YCbCr处理——那时候会专门设计色彩空间转换模块,而不是像现在这样直接透传。
4.4 我在实际调试中积累的几点小经验
最后分享几条没法写进代码注释里的经验,是用时间堆出来的。
其一,保持I2C初始化状态机的可观测性。在状态机里加一个心跳计数器,每完成一步配置就翻转一次调试引脚,用示波器或者LED就能直观看到初始化进度。哪一步卡住了,LED就停在哪个状态,比翻代码快得多。我习惯用4位拨码开关显示状态编号,调试效率提升非常明显。
其二,视频调试的黄金法则是一次只改一个变量。很多人遇到黑屏,立刻开始同时改I2C配置、改数据通路、换线缆,结果问题没解决,连之前能用的状态都回不去了。正确做法是:第一步验证I2C通信,第二步单独测试接收芯片输出(写一个简单的计数器代替真实数据),第三步固定一个已知良好的输入源,第四步再动发送芯片配置。每一步都确认无误才进行下一步。
其三,信号源的选择也有讲究。不要上来就用4K电脑输出测试1080p的接收芯片,分辨率超出芯片能力会导致检测混乱。最好的调试信号源是一台可以手动设置输出分辨率的设备,从720p60开始调起,逐级往上测,基本能覆盖所有问题。
其四,也是很多文档不会写的:HDMI输出对线缆质量非常敏感。开发板和显示器之间的HDMI线如果太长或者质量太差,即使信号链路本身没问题也可能显示异常。手头至少备一根1米以内的短线和一根质量好的HDMI线,用于排除线缆因素。别问我为什么强调这一点,我被一根劣质线坑过整整一个下午。
做这个实验时我心里一直记着一句话:视频链路是流水线,任何一级出问题都会导致最终画面异常,但调试时一定要逆向逐级定位,从最靠近显示器的环节开始反推,而不是从信号源开始一路查下去。先确认发送芯片配置无误,再确认数据到达了发送芯片,再确认接收芯片输出了数据,最后才去怀疑输入信号本身。这个思路看起来反直觉,实际效率却是最高的——因为“显示器没画面”这个问题,源头大概率不在输入,而在输出和中间链路。