
简介面向 FPGA/ASIC 视频接口开发和数字 IC 设计学习者该压缩包提供一套基于 VHDL 编写的 HDMI 收发 IP 核。这套资源对有一定 VHDL 基础、希望深入理解视频接口的读者尤其友好。IP 覆盖 HDMI 信号从接收解码到发送编码的完整链路包含 TMDS 通道串并转换、通道绑定、字节对齐、编解码模块、FIFO 缓冲和时钟管理并附带测试 pattern可结合仿真工具检查时序与输出数据是否符合 HDMI 规范。压缩包共 18 个文件整体约 49KB内容以 htm 格式封装 RTL 源代码按接收、发送和公共模块组织接收方向涉及 HDMI 解码、通道绑定、TMDS 位到字节转换、相位对齐及时钟初始化发送方向实现串化 FIFO、编码器和 HDMI 编码器公共部分提供时序同步、去抖、移位延迟和双口 RAM 等基础电路。目前已有 248 人学习下载。通过阅读这份 RTL 级 VHDL 代码可以快速理解 HDMI 链路的编解码原理、SERDES 时序设计方式以及测试激励的构造方法适合作为自研 IP 或集成到 FPGA 工程的重要参考也便于后续做协议扩展与故障定位。1. 打开一个 HDMI.rar 时你其实打开的是整个数字视频链路解压一份标题里带 “HDMI”“VHDL”“ip核” 的 .rar 文件通常意味着你想在 FPGA 上跑通一个 HDMI 源端接口。很多人以为这是“改改引脚就能点亮屏幕”的活直到上板后发现黑屏、闪屏、颜色错乱才意识到问题根本不在引脚而在像素时钟、TMDS 编码和 IP 核的整合方式。HDMI 源端这条链路至少包含三块时序发生器把 DE/HSYNC/VSYNC 排布成帧、TMDS 编码器把 8 位像素变成 DC 平衡的 10 位符号、串行器再以 10 倍像素时钟把符号逐位送出。对 5 年以上经验的工程师来说真正拉开差距的不是“能不能点亮”而是音频辅助数据包怎么塞、N/CTS 怎么算、MMCM 抖动怎么压。这篇文章就按这条链路从理论到上板调试完整讲一遍方向是 VHDL但思路可以平移到你常用的一整套 FPGA 工具链上。2. 在 VHDL 里把像素流变成 TMDS从三段时序到 10 位符号2.1 DE、HSYNC、VSYNC 三段时序先排布成标准视频帧HDMI 源端核心不是颜色数据本身而是和像素同步的“门控信号” DEData Enable。DE 为 1 期间三个 TMDS 数据通道上传输 RGB 像素DE 为 0 期间数据通道进入控制期此时携带的是 HSYNC 和 VSYNC 的电平状态。换句话说HSYNC/VSYNC 不是“和 pixel 一起传出去的 sideband”而是被编码进 DE0 的控制期符号里。任何 VHDL 实现的第一步都是把时序参数变成可综合的计数器。以 1280x720p60 为例VESA 给出的时序参数如下参数数值说明水平有效像素1280每行可见像素水平前肩 HFP110DE 拉低后的等待周期水平同步脉宽 HSync40HSYNC 拉低保持周期水平后肩 HBP220HSYNC 结束到 DE 拉高的间隔垂直有效行720每帧可见行数垂直前肩 VFP5帧结束到 VSYNC 周期垂直同步脉宽 VSync5VSYNC 拉低保持周期垂直后肩 VBP20VSYNC 结束到下一帧 DE 拉高用 VHDL 写出来就是两个计数器加一个有限状态机。水平计数器的范围是 0 到 1649垂直计数器范围 0 到 749DE 的位置完全由这两个计数器比较得出。下面是从实际工程里剥离出来的骨架代码省略复位分支以便聚焦计数结构constant H_ACTIVE : integer : 1280; constant H_FP : integer : 110; constant H_SYNC : integer : 40; constant H_BP : integer : 220; constant V_ACTIVE : integer : 720; constant V_FP : integer : 5; constant V_SYNC : integer : 5; constant V_BP : integer : 20; signal h_cnt : integer range 0 to H_ACTIVE H_FP H_SYNC H_BP - 1 : 0; signal v_cnt : integer range 0 to V_ACTIVE V_FP V_SYNC V_BP - 1 : 0; process(pix_clk) begin if rising_edge(pix_clk) then if h_cnt H_ACTIVE H_FP H_SYNC H_BP - 1 then h_cnt 0; if v_cnt V_ACTIVE V_FP V_SYNC V_BP - 1 then v_cnt 0; else v_cnt v_cnt 1; end if; else h_cnt h_cnt 1; end if; end if; end process; de 1 when h_cnt H_ACTIVE and v_cnt V_ACTIVE else 0; hs 0 when h_cnt H_ACTIVE H_FP and h_cnt H_ACTIVE H_FP H_SYNC else 1; vs 0 when v_cnt V_ACTIVE V_FP and v_cnt V_ACTIVE V_FP V_SYNC else 1;计数器进位、DE 和同步信号的生成逻辑核心是“边界值比较”。HSYNC 是一个周期级别的电平不是事件。写这类代码时最常见的坑是同步信号极性搞反——HDMI 端默认要求负极性也就是低电平有效。如果你的 IP 核参考工程里时序极性是从 VGA 时代继承过来的正极性上板就会直接黑屏。这属于“看起来代码完全正确”但显示不出来的第一类原因。提示把时序参数定义成 VHDL 常量而不是散落魔法数字换分辨率时只需要改这一处。constraint 文件里还要核对 blanking 总长度是否和代码一致。2.2 TMDS 编码器在 VHDL 里的最小可综合实现TIMDS 编码器的任务是把 8 位像素数据、或 2 位控制信号变换成 10 位符号。DVI 1.0 规范定义了一套“尽量让符号中 0/1 数量均衡”的算法分两个阶段第一阶段根据输入中 1 的个数决定用 XOR 还是 XNOR 递推让中间 9 位 q_m 的跳变尽量少第二阶段根据计数器 cnt 追踪 DC 偏差决定是否把 q_m 整体反转后输出。注意第二阶段还约定输出的最高位是 q_m(0)这是 10 位符号的第 9 位用于表示是否反转。下面的 VHDL 是 HDMI 源端里最常看到的 TMDS 编码器写法DE1 时编码像素DE0 时直接输出控制期映射library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity tmds_encoder is port ( clk : in std_logic; data : in std_logic_vector(7 downto 0); de : in std_logic; ctrl : in std_logic_vector(1 downto 0); tmds : out std_logic_vector(9 downto 0) ); end entity tmds_encoder; architecture rtl of tmds_encoder is signal cnt : integer range -4 to 4 : 0; begin process(clk) variable n1_d : integer range 0 to 8 : 0; variable n1_q : integer range 0 to 8 : 0; variable q_m : std_logic_vector(7 downto 0); variable q_out : std_logic_vector(9 downto 0); begin if rising_edge(clk) then if de 1 then n1_d : 0; for i in datarange loop if data(i) 1 then n1_d : n1_d 1; end if; end loop; q_m(0) : data(0); if (n1_d 4) or (n1_d 4 and data(0) 0) then for i in 1 to 7 loop q_m(i) : q_m(i - 1) xnor data(i); end loop; else for i in 1 to 7 loop q_m(i) : q_m(i - 1) xor data(i); end loop; end if; n1_q : 0; for i in q_mrange loop if q_m(i) 1 then n1_q : n1_q 1; end if; end loop; if cnt 0 or n1_q 4 then q_out : q_m(0) q_m; if n1_q 4 then cnt cnt; else cnt cnt n1_q - 4; -- 保持均值 end if; elsif (cnt 0 and n1_q 4) or (cnt 0 and n1_q 4) then q_out : not q_m(0) not q_m; cnt cnt 4 - n1_q; else q_out : q_m(0) q_m; cnt cnt n1_q - 4; end if; tmds q_out; else -- control period case ctrl is when 00 tmds 0010101011; when 01 tmds 0010101010; when 10 tmds 1101010100; when others tmds 1101010101; end case; cnt 0; end if; end if; end process; end architecture rtl;这个编码器的逻辑要点有三个。第一阶段一的递推必须串行执行因为每个 q_m(i) 依赖前一位在 148.5MHz 像素时钟下8 级组合逻辑加上后面 n1_q 的统计约 20 个 LUT 的延迟时序收敛的压力不小如果跑 4K30 的 297MHz建议直接在 Vivado 里给这条路径打 multicycle 约束或换用流水线拆两拍。第二cnt 计数器的范围被限制在 -4 到 4防止 DC 偏差漂移这个限制来自规范对游程长度的要求。第三DE0 时输出控制期映射并清零 cnt是为了保证每个 blanking 区间都重新开始累积平衡避免上一帧遗留的直流偏差影响后续数据期。2.3 控制期与数据期为什么必须是 10 位而不是 8 位直接传 8 位像素会有两个问题一是接收端难以恢复时钟二是长串连续的 0 或 1 会让差分对上的直流分量漂移导致接收端阈值判断错误。TMDS 把 8 位扩展成 10 位编码后的符号最多连续 5 个同值电平且符号整体维持直流平衡接收端因此能用一个简单的 PLL 从数据流里恢复出位时钟。这是“DC 平衡”这个词在 HDMI 里的本质意义。控制期编码没有复杂的递推逻辑它本身就是一组预定义映射DE0 时三个数据通道各自根据 ctrl[1:0] 输出固定 10 位符号。上面代码里的四个常量映射是业界实现里的通行做法符号具体值以 DVI 规范的 Table 3-1 为准。实际工程里还有一个容易被忽略的细节蓝通道通道 0在 DE0 时不仅编码 ctrl还要在数据岛期间输出包头同步前缀和辅助数据。也就是说三个通道的控制期工作方式不完全一样蓝通道会比红绿通道多一个“data island”状态。所以在整合 VHDL 工程时一定要确认你拿到的编码器实例化了几份、每份分别挂在哪个通道上。如果三个通道共用一个编码器状态机那它只适用于不传辅助数据的最小 HDMI 发送端音频和 InfoFrame 都传不出去。3. 选 IP 核还是手写 RTLVivado 与开源 VHDL 方案的整合落地3.1 三条路径官方 IP、开源 VHDL、纯 RTL 的取舍“hdmi ip核”这个诉求在不同场景下有完全不同的答案。如果你用 Vivado厂商自带 HDMI 1.4/2.0 TX Subsystem IP如果你打开的是 .rar 里的 VHDL 源码那大概率是某个开源实现或老工程师的手写编码器。三条路径的差异如下路径音频支持时序收敛成本可控性适用场景Vivado HDMI TX IP完整含音频时钟再生低厂商已做约束低黑盒配置产品化、快速出图开源 VHDL IP 核通常只有视频音频需自己补中需要自己约束高可读可改学习、定制协议纯手写 RTL 编码器完全自己实现高编码器路径要自己调最高研究 TMDS 原理、超低延迟工程上我的判断标准是项目周期小于两周直接上官方 IP需要学习协议、做低延迟或特殊封装就花时间研究开源 VHDL只是为了验证算法写个逻辑分析仪能看到 10 位符号变化就够了。三者不冲突很多量产方案其实是“官方 IP 做视频 自研寄存器配置音频”因为厂商音频部分对不同采样率的 N/CTS 表更新并不及时。3.2 用 Vivado 的 Clocking Wizard IP 核把时钟树理顺HDMI 源端需要三路相关时钟像素时钟 pix_clk、串行器位时钟 serial_clk10 倍像素时钟、以及给 MMCM 反馈用的参考时钟。Vivado 里这块固定用 Clocking Wizard 生成配置要点是 MMCM 的输出CLKOUT0 出 pix_clkCLKOUT1 出 10 倍频的 serdes_clk两者必须来自同一个 MMCM 才能保证相位关系可控。这里最常翻车的是没有勾选 “MMCM 抖动优化” 选项导致 TMDS 时钟上的抖动超过 HDMI 规范要求高端显示器会触发信号质量检测然后直接黑屏。配置 Clocking Wizard 时输入时钟频率要按实际板载晶振填输出频率按目标分辨率填。以下是一组典型配置示意输入 50MHz 晶振目标 1080p60# 在 Vivado Tcl Console 中生成 Clocking Wizard 并设置参数 create_ip -name clk_wiz -vendor xilinx.com -library ip -module_name hdmi_clk set_property -dict [list \ CONFIG.PRIM_IN_FREQ {50.000} \ CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {148.5} \ CONFIG.CLKOUT2_REQUESTED_OUT_FREQ {1485} \ CONFIG.MMCM_DIVCLK_DIVIDE {1} \ CONFIG.MMCM_CLKFBOUT_MULT_F {29.7} \ CONFIG.USE_JITTER_OPTIMIZATION {true} \ ] [get_ips hdmi_clk]第一个输出频率是像素时钟第二个是串行器时钟。PRIM_IN_FREQ 必须和实际晶振一致否则 MMCM 的 VCO 频率计算会偏。USE_JITTER_OPTIMIZATION 这个选项默认是 false很多参考工程不会帮你勾这是 1080p 以上容易出现的隐性坑。生成成功后在约束文件里给 pix_clk 和 serdes_clk 分别指定时钟周期约束并确保 MMCM 的 locked 信号参与复位释放。提示不要试图用一个简单的 PLL 同时输出 148.5MHz 和 1485MHz很多 PLL 的 VCO 上限根本到不了 1.5GHz。serdes_clk 通常通过 OSERDESE2 内部的高速时钟路径直接驱动等价于 10 倍像素时钟。3.3 拿到 .rar 里的 VHDL 工程后先看顶层再做约束从网上下载的 HDMI VHDL 工程解压后第一步不是编译而是先读顶层文件。你需要确认三件事顶层有几个时钟输入、复位是高有效还是低有效、有没有把 TMDS 输出做成差分对。很多开源工程的顶层端口会写成 dout_p[2:0] 和 dout_n[2:0] 的形式这就是已经规划好 OBUFDS 的差分引脚如果只有一个普通 10 位输出向量说明它把串行化放在更底层了。按这个顺序集成是比较稳的找到顶层实体列出所有端口和板卡原理图对照引脚编号。检查 reset 极性VHDL 里常见 active-low reset 配一个reset_n如果你板子的按键是高有效记得在顶层反转一次。暂时注释掉音频和 I2C 相关模块先只跑视频通路。用 Vivado 跑综合实现重点看 timing summary 里有没有关于 OSERDESE2 或 TMDS 路径的违规。上板用 ILA 抓信号而不是直接看显示器。在 Vivado 里对源码工程执行编译的命令也值得固定下来便于反复修改后快速回归# 刷新 IP 并重新综合实现 update_ip_catalog generate_target all [get_ips hdmi_clk] reset_run synth_1 launch_runs synth_1 -jobs 8 wait_on_run synth_1 launch_runs impl_1 -to_step write_bitstream -jobs 8 wait_on_run impl_1compile 阶段过了不等于能点亮90% 的问题在实现阶段暴露。看到 “Timing constraint not met” 时不要急着改代码先看是数据路径还是时钟路径违规。TMDS 编码器路径违规通常是组合逻辑过深解法是打一拍或拆两个编码级时钟路径违规通常是 MMCM 配置错误重新检查 CLKOUT2 的频率是否与像素时钟成精确倍数。4. 从视频到音频辅助数据包、N/CTS 与 10:1 串行器4.1 HDMI 输出音频原理音频数据是怎么进到 data island 的HDMI 输出音频原理与传统 I2S 完全不同。HDMI 上每条数据通道在 DE0 的间隔里既可能处于控制期也可能处于“数据岛”Data Island期。数据岛传输的是辅助数据包包括 InfoFrame 和音频采样。蓝色通道在进入数据岛之前必须发出一段 8 个周期的 preamble前导码取值固定的 2 位组合10并配合同步码q_out[9:8]00。接收端侦测到 preamble 和同步码后才知道后面跟着的是数据岛内容。音频辅助数据包Audio Data Packet类型 0x02的 VHDL 构造很简单本质就是把音频采样和元数据按字节排列后在 byte counter 控制的节拍里丢进蓝色通道的数据期。包头和包体的排布遵循 CEA-861 定义-- 音频辅助数据包Packet Type 0x02 constant AUDIO_PKT_TYPE : std_logic_vector(7 downto 0) : x02; -- HB1: bit3-0 为通道数减 1bit7 为布局 -- HB2: bit3-0 为采样频率编码bit7-4 为样本字长编码 -- HB3: 保留通常填 0 type audio_pkt_t is record header : std_logic_vector(23 downto 0); -- 字节 0-2 body : std_logic_vector(27*8-1 downto 0); -- 最多 28 字节 end record;这个包体的实际发送时机由状态机控制蓝通道在 DE0 时切换三段状态控制期、前导期、数据岛。很多开源 VHDL 工程为了简化直接省略数据岛只发控制期和视频数据期这样显示器依然能出画面因为纯视频不依赖 InfoFrame但音频肯定没有。如果你下载的 .rar 里没有audio_packet相关的 case 分支就说明它不支持声音别浪费时间找 bug。音频时钟恢复的核心是 N/CTS 参数。发送端在音频采样时钟域取 N 值在 TMDS 时钟域取 CTS 值接收端用它恢复出和源端完全同步的音频时钟。计算公式是CTS 128 * Fs_kHz * TMDS_Clock_Hz / (N * 1000)以 48kHz 采样、1080p60 的 148.5MHz TMDS 时钟为例N 取 HDMI 规范推荐的 6144计算结果刚好是 CTS148500对应像素时钟。这组整数关系不是巧合而是标准设计时特意保证的。工程实现上不推荐实时计算而是针对每个采样率建一张查找表因为 44.1kHz 系列在不同 TMDS 时钟下 CTS 不是整数需要按规范取最接近的整数值。4.2 OSERDESE2 串行器用两个原语拼出 10:1 DDR 输出TMDS 编码器输出的是 10 位并行符号但物理线上实际是串行位流速率是像素时钟的 10 倍。在 7 系列及以后的 Xilinx FPGA 上这步用 OSERDESE2 原语完成。单个 OSERDESE2 在 DDR 模式下最大只能做 4:1 串行化因此要做 10:1 必须把 master 和 slave 两个原语级联D5 接口把 slave 的移位输出拼进 master。-- 单个数据通道的 10:1 串行化原语例化 OSERDESE2_inst : OSERDESE2 generic map ( DATA_RATE DDR, DATA_WIDTH 10, SERDES_MODE MASTER, TRISTATE_WIDTH 1 ) port map ( D1 tmds_parallel(0), D2 tmds_parallel(1), D3 tmds_parallel(2), D4 tmds_parallel(3), D5 slv_shift_out, D6 0, OQ tmds_serial, CLK serial_clk, CLKDIV pix_clk, RST rst, ... );这里参数说明要仔细DATA_RATE 必须是 “DDR”因为 10 位数据在半个位时钟周期内只能装载 5 位要靠时钟双边沿输出DATA_WIDTH10 告诉原语这次串行化包含 10 个 bitSERDES_MODE“MASTER” 表示本原语输出最终串行流slave 原语的 D5 负责把第 6 到第 9 位在内部 Q 端拼给 master。CLK 接 10 倍像素时钟CLKDIV 接像素时钟。最关键的是CLK 和 CLKDIV 必须来自同一个 MMCM 的同步输出否则相位关系无法保证。串行器的输出还不能直接上引脚要通过 OBUFDS 转成差分对连接到 HDMI 连接器的 TMDS 数据通道。HDMI 连接器上四个差分对分别是数据通道 0/1/2 和时钟通道。很多工程只写了 OBUFDS 却忘了输入引脚约束里要配IOOSTANDARD LVDS_25结果综合能过、上板没信号就是约束没写全。4.3 常用分辨率的 TMDS 时钟参数表不同的分辨率对应不同的像素时钟音频 N/CTS 也随之变化。这张表是综合多个标准后整理的常用值精确计时以对应的 CEA-861 或 VESA 时序为准分辨率刷新率像素时钟是否 /1.001串行器时钟640x4806025.175 MHz是251.75 MHz1280x7206074.25 MHz否742.5 MHz1280x72059.9474.175 MHz是741.75 MHz1920x108060148.5 MHz否1.485 GHz1920x108059.94148.35 MHz是1.4835 GHz3840x216030297 MHz否2.97 GHz注意最后一行 4K30 的串行器时钟达到 2.97GHz超出了 7 系列普通 IO Bank 的能力实际工程会用两个 OSERDESE3 拼接或用 GT 高速收发器实现。在这张表里能看到一个规律带 59.94Hz 刷新率的分辨率像素时钟都必须除以 1.001这是从 NTSC 彩色副载波继承下来的历史包袱。如果你的 Clocking Wizard 只配了 148.5MHz 而显示器期望 148.35MHz多数显示器能容忍但音频的 N/CTS 参数必须跟着改否则会听到爆音或周期性杂音。5. 收尾但不过时ILA 验证与 HDMI 接口调试图5.1 用 ILA IP 核抓 DE、TMDS 和 HPD 三个信号上板之后的第一件事不是看屏幕而是用 Vivado 的 ILA IP 核抓信号。ILA 在调试中要抓三个关键对象DE 是否按帧周期翻转、TMDS 编码器输出的数据期符号是否为预期像素值、以及 HDMI 连接器 19 脚的 HPD 热插拔检测是否拉高。19 脚是 Hot Plug Detect源端要靠它判断显示器是否连接HPD 为低时不要启动 TMDS 输出否则可能损伤接口电路。抓 DE 信号的触发设置可以这样写进 ILA 的触发端口attribute mark_debug : string; attribute mark_debug of de : signal is true; attribute mark_debug of tmds_blue : signal is true; attribute mark_debug of hpd : signal is true;综合后在硬件管理器里把触发条件设为de 0抓一段上升沿。正常情况 DE 应该呈周期性高低变化频率等于行频乘以有效行占比如果 DE 永远为 0说明时序生成器根本没有启动回头查复位释放和 MMCM 的 locked 信号如果 DE 正常但显示器黑屏把触发条件改成de 1抓 TMDS 数据看看数据期符号是否在两个固定值之间跳变。现象原因方向排查手段HPD 为低无画面5V 电源没给到 18 脚或接口虚焊万用表量 18 脚电平有 HPD无 TMDS 时钟MMCM 未 locked或串行时钟未输出ILA 抓 locked检查 clk_wiz 配置画面闪屏串行时钟抖动过大或差分对不等长检查 PCB 差分走线调 MMCM jitter 优化颜色偏绿或偏紫通道映射错位或数据位序颠倒换测试彩条对比 RGB 通道顺序间歇性黑屏HPD 接触不良或 EDID 读取中断加长 HPD 去抖检查连接器5.2 排错时最容易忽略的几个细节调试 HDMI 源端时最容易被忽视的是引脚约束和复位释放顺序。差分引脚在 XDC 里要成对出现而且命名必须与 OBUFDS 例化的_p/_n对应复位释放不能和 MMCM locked 信号同一拍需要至少延迟 100us否则 OSERDESE2 可能在未锁定的时钟下启动输出乱码。另一个细节是 I2C 通道。HDMI 源端需要通过 I2C 读取显示器的 EDID获取显示器支持的分辨率和音频格式。很多最小系统直接跳过 EDID 硬输出 1080p60这在电脑显示器上通常可行但部分电视机不接受这种方式表现为黑屏或提示“不支持的信号”。这时用 VIO IP 核手动改写 I2C 地址或加一段简单的 EDID 读取状态机是标准解法。最后一招是验证法把编码器输出和 OSERDESE2 输入之间接一个固定的测试序列生成器让它在 DE 有效期间输出纯红或彩条。如果显示器能正常显示彩条说明从编码器到串行器到物理连接都没问题问题出在你的像素数据源如果彩条都不出直接抓差分对的波形和时钟看电平是否符合 TMDS 标准。这一层验证做完HDMI 通路的基本问题就不会再有漏网之鱼。本文还有配套的精品资源点击获取