FPGA 实现 HDMI 输出完全攻略:从接口原理到 4K 显示全流程
概述
HDMI(High-Definition Multimedia Interface)已成为现代显示设备的标准接口,广泛应用于电视、显示器、投影仪等设备。在 FPGA 应用中,实现 HDMI 输出是高清视频处理系统的核心需求。
为什么需要学习 HDMI 输出?
在 FPGA 项目中,HDMI 输出通常面临以下挑战:
- HDMI 协议复杂,涉及 TMDS 编码、序列化等多个环节
详细介绍 FPGA 实现 HDMI 输出的完整方案,涵盖 HDMI 协议基础、TMDS 编码原理、视频时序标准(1080p/4K)、发送器设计、时钟管理及信号完整性。包含工程化实战案例、仿真验证、上板调试方法及常见问题排查技巧,适合嵌入式硬件开发人员参考。
HDMI(High-Definition Multimedia Interface)已成为现代显示设备的标准接口,广泛应用于电视、显示器、投影仪等设备。在 FPGA 应用中,实现 HDMI 输出是高清视频处理系统的核心需求。
为什么需要学习 HDMI 输出?
在 FPGA 项目中,HDMI 输出通常面临以下挑战:
本文将帮助您:
HDMI 标准由 Philips、Sony、Toshiba、Panasonic、Hitachi、Sanyo、Silicon Image 和 Thomson 等公司联合开发,于 2002 年首次发布。
HDMI 发展历程:
标准 HDMI 接口采用 19 针连接器,引脚定义如下:
HDMI 19 针接口定义: ┌─────────────────────────────┐ │ 1 2 3 4 5 6 7 8 9 │ 第一排(9 针) │ 10 11 12 13 14 15 16 17 18 │ 第二排(9 针) │ 19 │ 第三排(1 针) └─────────────────────────────┘
关键引脚功能表:
| 引脚号 | 信号名称 | 功能说明 | 类型 |
|---|---|---|---|
| 1-3 | TMDS Data 2+ | 蓝色通道正 | 差分 |
| 4 | TMDS Data 2 Shield | 蓝色通道屏蔽 | 地 |
| 5-7 | TMDS Data 1+ | 绿色通道正 | 差分 |
| 8 | TMDS Data 1 Shield | 绿色通道屏蔽 | 地 |
| 9-11 | TMDS Data 0+ | 红色通道正 | 差分 |
| 12 | TMDS Data 0 Shield | 红色通道屏蔽 | 地 |
| 13-15 | TMDS Clock+ | 时钟通道正 | 差分 |
| 16 | TMDS Clock Shield | 时钟通道屏蔽 | 地 |
| 17 | CEC | 消费电子控制 | 单端 |
| 18 | GND | 地线 | 地 |
| 19 | +5V | 电源 | 电源 |
💡 FPGA 实现要点:
对于 FPGA 实现 HDMI 输出,我们主要关注以下信号:
| 特性 | HDMI 1.4 | HDMI 2.0 | HDMI 2.1 |
|---|---|---|---|
| 最大带宽 | 10.2 Gbps | 18 Gbps | 48 Gbps |
| 最高分辨率 | 4K@30Hz | 4K@60Hz | 8K@60Hz |
| 色深 | 8/10/12bit | 8/10/12bit | 8/10/12bit |
| 刷新率 | 24/30/60Hz | 24/30/60Hz | 24/30/60/120Hz |
| 应用场景 | 消费级 | 专业级 | 高端应用 |
TMDS 使用 8b/10b 编码,将 8 位数据编码为 10 位传输数据。
编码目的:
编码过程:
8 位输入数据 → 8b/10b 编码器 → 10 位编码数据 → 序列化 → LVDS 输出
编码表示例:
输入:00000000 → 输出:1011001100 (或反相)
输入:11111111 → 输出:0100110011 (或反相)
8b/10b 编码确保输出数据的 DC 平衡,即 1 和 0 的个数大致相等。
DC 平衡的好处:
// 简化的 TMDS 编码器框架
module tmds_encoder (
input wire [7:0] data_in,
input wire ctrl_in, // 控制信号
output wire [9:0] data_out
);
// 8b/10b 编码逻辑
// 输出 10 位编码数据
endmodule
分辨率参数:
行时序参数:
总像素数 = 2200
├─ 有效像素:1920
├─ 前廊(Front Porch):88
├─ 同步脉冲(Sync):44
└─ 后廊(Back Porch):148
场时序参数:
总行数 = 1125
├─ 有效行:1080
├─ 前廊:4
├─ 同步脉冲:5
└─ 后廊:36
分辨率参数:
行时序参数:
总像素数 = 4400
├─ 有效像素:3840
├─ 前廊:176
├─ 同步脉冲:88
└─ 后廊:296
场时序参数:
总行数 = 2250
├─ 有效行:2160
├─ 前廊:8
├─ 同步脉冲:10
└─ 后廊:72
| 分辨率 | 像素时钟 | 帧率 | 应用场景 |
|---|---|---|---|
| 720p | 74.25MHz | 60Hz | 高清电视 |
| 1080i | 74.25MHz | 60Hz | 隔行扫描 |
| 1080p | 148.5MHz | 60Hz | 全高清 |
| 1440p | 241.5MHz | 60Hz | 高端显示器 |
| 4K@30Hz | 297MHz | 30Hz | 4K 电视 |
| 4K@60Hz | 594MHz | 60Hz | 高端 4K |
支持的音频格式:
音频参数:
HDMI 音频通过音频信息帧(Audio InfoFrame)传输。
音频信息帧结构:
┌─────────────────────────────────┐
│ 帧头(3 字节)
├─────────────────────────────────┤
│ 校验和(1 字节)
├─────────────────────────────────┤
│ 音频信息(27 字节)
│ ├─ 音频编码类型
│ ├─ 声道数
│ ├─ 采样频率
│ └─ 位深
└─────────────────────────────────┘
HDCP(High-bandwidth Digital Content Protection)是 HDMI 的内容保护机制。
HDCP 版本:
HDCP 功能:
简化实现方案:
HDCP 禁用方法:
// 在 EDID 中标记不支持 HDCP
// 或在握手阶段返回不支持信息
TMDS 编码器是 HDMI 发送器的核心模块,负责将 8 位 RGB 数据编码为 10 位 TMDS 数据。
编码流程:
8 位输入 → 8b/10b 编码 → 10 位输出 → 序列化 → LVDS 驱动
编码器的三个主要功能:
TMDS 编码使用两个编码表:
数据编码表特点:
控制编码表:
HSYNC=0, VSYNC=0 → 10'b1101010100
HSYNC=1, VSYNC=0 → 10'b0010101011
HSYNC=0, VSYNC=1 → 10'b0101010100
HSYNC=1, VSYNC=1 → 10'b1010101011
module tmds_encoder (
input wire clk,
input wire [7:0] data_in,
input wire hsync,
input wire vsync,
input wire de, // 数据使能
output reg [9:0] data_out
);
// 编码逻辑
always @(posedge clk) begin
if (de) begin
// 数据编码
data_out <= encode_data(data_in);
end else begin
// 控制编码
data_out <= encode_ctrl({vsync, hsync});
end
end
// 编码函数(伪代码)
function [9:0] encode_data(input [7:0] d);
// 查表或计算编码值
endfunction
function [9:0] encode_ctrl(input [1:0] ctrl);
// 返回控制编码
endfunction
endmodule
面积优化:
速度优化:
功耗优化:
序列化器将 10 位并行数据转换为高速串行数据。
序列化过程:
10 位并行数据 → 移位寄存器 → 高速串行输出
关键参数:
方案一:使用 OSERDES 原语(推荐)
Xilinx FPGA 提供 OSERDES(Output Serializer/Deserializer)原语:
// Xilinx 7 系列 OSERDES
OSERDES2 #(
.DATA_WIDTH(10), // 10 位输入
.DATA_RATE("DDR"), // DDR 模式
.SERDES_MODE("MASTER"), // 主模式
.OUTPUT_MODE("DIFFERENTIAL") // 差分输出
) oserdes_inst (
.CLK(clk_pixel), // 像素时钟
.CLKDIV(clk_pixel), // 分频时钟
.D1(data_in[0]), // 数据输入
.D2(data_in[1]),
.D3(data_in[2]),
.D4(data_in[3]),
.D5(data_in[4]),
.D6(data_in[5]),
.D7(data_in[6]),
.D8(data_in[7]),
.D9(data_in[8]),
.D10(data_in[9]),
.OQ(data_out_p), // 正输出
.OQB(data_out_n) // 负输出
);
方案二:使用 OSERDESE2 原语(7 系列及以上)
OSERDESE2 #(
.DATA_WIDTH(10),
.TRISTATE_WIDTH(1),
.DATA_RATE_OQ("DDR"),
.DATA_RATE_TQ("SDR"),
.SERDES_MODE("MASTER"),
.TBYTE_CTL("FALSE"),
.TBYTE_SRC("FALSE"),
.INIT_OQ(1'b0),
.INIT_TQ(1'b0),
.SRVAL_OQ(1'b0),
.SRVAL_TQ(1'b0),
.IOBDELAY("NONE")
) oserdese2_inst (
.CLK(clk_10x), // 10 倍像素时钟
.CLKDIV(clk_pixel), // 像素时钟
.RST(rst),
.D1(data_in[0]),
.D2(data_in[1]),
.D3(data_in[2]),
.D4(data_in[3]),
.D5(data_in[4]),
.D6(data_in[5]),
.D7(data_in[6]),
.D8(data_in[7]),
.D9(data_in[8]),
.D10(data_in[9]),
.OQ(data_out_p),
.OQB(data_out_n)
);
序列化器需要两个时钟:
时钟生成方法:
// 使用 PLL 生成 10 倍时钟
module clk_gen (
input wire clk_in, // 148.5MHz
output wire clk_pixel, // 148.5MHz
output wire clk_10x // 1.485GHz
);
// PLL 配置
// 输入:148.5MHz
// 输出 1:148.5MHz(1 倍)
// 输出 2:1.485GHz(10 倍)
endmodule
HDMI 发送器需要多个时钟:
参考时钟(100MHz)
↓
PLL/MMCM
├─ 像素时钟(148.5MHz@1080p)
├─ 高速时钟(1.485GHz@1080p)
└─ 其他时钟
# Vivado 约束文件
create_clock -period 6.734 -name clk_pixel [get_ports clk_pixel]
create_generated_clock -name clk_10x \n -source [get_pins pll_inst/CLKIN1] \n -multiply_by 10 \n [get_pins pll_inst/CLKOUT0]
时钟偏斜的影响:
偏斜控制方法:
HDMI 使用 100Ω 差分阻抗。
PCB 设计要求:
计算公式:
Z_diff = 2 × Z_single × (1 - 0.48 × e^(-0.96 × s/h))
其中:
Z_single:单端阻抗
s:线对间距
h:介质厚度
串扰来源:
控制方法:
反射产生的原因:
控制方法:
EMI 来源:
控制方法:
module video_timing_gen (
input wire clk, // 148.5MHz
input wire rst_n,
output wire hsync,
output wire vsync,
output wire de, // 数据使能
output wire [11:0] x, // 水平坐标
output wire [11:0] y // 竖直坐标
);
// 1080p@60Hz 参数
localparam H_TOTAL = 2200;
localparam H_ACTIVE = 1920;
localparam H_FP = 88;
localparam H_SYNC = 44;
localparam V_TOTAL = 1125;
localparam V_ACTIVE = 1080;
localparam V_FP = 4;
localparam V_SYNC = 5;
reg [11:0] h_cnt, v_cnt;
// 行计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) h_cnt <= 0;
else if (h_cnt == H_TOTAL - 1) h_cnt <= 0;
else h_cnt <= h_cnt + 1;
end
// 场计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) v_cnt <= 0;
else if (h_cnt == H_TOTAL - 1) begin
if (v_cnt == V_TOTAL - 1) v_cnt <= 0;
else v_cnt <= v_cnt + 1;
end
end
// 同步信号生成
assign hsync = (h_cnt >= H_ACTIVE + H_FP) && (h_cnt < H_ACTIVE + H_FP + H_SYNC) ? 1'b0 : 1'b1;
assign vsync = (v_cnt >= V_ACTIVE + V_FP) && (v_cnt < V_ACTIVE + V_FP + V_SYNC) ? 1'b0 : 1'b1;
// 数据使能
assign de = (h_cnt < H_ACTIVE) && (v_cnt < V_ACTIVE);
// 坐标输出
assign x = h_cnt;
assign y = v_cnt;
endmodule
module hdmi_tx_1080p (
input wire clk_pixel, // 148.5MHz
input wire clk_10x, // 1.485GHz
input wire rst_n,
input wire [7:0] r_in,
input wire [7:0] g_in,
input wire [7:0] b_in,
output wire hdmi_clk_p,
output wire hdmi_clk_n,
output wire [2:0] hdmi_d_p,
output wire [2:0] hdmi_d_n
);
wire hsync, vsync, de;
wire [11:0] x, y;
// 时序生成
video_timing_gen timing_gen (
.clk(clk_pixel),
.rst_n(rst_n),
.hsync(hsync),
.vsync(vsync),
.de(de),
.x(x),
.y(y)
);
// TMDS 编码
wire [9:0] tmds_r, tmds_g, tmds_b, tmds_clk;
tmds_encoder enc_r (
.clk(clk_pixel),
.data_in(r_in),
.hsync(hsync),
.vsync(vsync),
.de(de),
.data_out(tmds_r)
);
tmds_encoder enc_g (
.clk(clk_pixel),
.data_in(g_in),
.hsync(hsync),
.vsync(vsync),
.de(de),
.data_out(tmds_g)
);
tmds_encoder enc_b (
.clk(clk_pixel),
.data_in(b_in),
.hsync(hsync),
.vsync(vsync),
.de(de),
.data_out(tmds_b)
);
// 时钟编码
assign tmds_clk = 10'b1111100000;
// 序列化
// 使用 OSERDES 原语进行序列化...
endmodule
4K@30Hz 的主要参数:
module video_timing_gen_4k (
input wire clk, // 297MHz
input wire rst_n,
output wire hsync,
output wire vsync,
output wire de,
output wire [12:0] x,
output wire [12:0] y
);
// 4K@30Hz 参数
localparam H_TOTAL = 4400;
localparam H_ACTIVE = 3840;
localparam H_FP = 176;
localparam H_SYNC = 88;
localparam V_TOTAL = 2250;
localparam V_ACTIVE = 2160;
localparam V_FP = 8;
localparam V_SYNC = 10;
// 计数器和同步信号生成逻辑
// 与 1080p 类似,只是参数不同
endmodule
HDMI 音频通过音频样本包(Audio Sample Packet)传输。
音频样本包结构:
┌─────────────────────────────┐
│ 包头(4 字节)
├─────────────────────────────┤
│ 音频样本(28 字节)
│ ├─ 左声道样本(4 字节)
│ ├─ 右声道样本(4 字节)
│ └─ ...
└─────────────────────────────┘
module hdmi_audio_tx (
input wire clk,
input wire [23:0] audio_l, // 左声道
input wire [23:0] audio_r, // 右声道
output wire [55:0] audio_pkt // 音频包
);
// 音频包生成逻辑
// 包含包头、校验和、音频数据
endmodule
EDID(Extended Display Identification Data)是显示器的身份信息。
EDID 内容:
module edid_rom (
input wire [7:0] addr,
output wire [7:0] data
);
// EDID 数据(128 字节)
reg [7:0] edid_mem [0:127];
initial begin
// 初始化 EDID 数据
edid_mem[0] = 8'h00; // 头
edid_mem[1] = 8'hFF; // ...
// 更多 EDID 数据
end
assign data = edid_mem[addr];
endmodule
在实际 FPGA 项目中,HDMI 输出模块需要与其他功能模块协调工作。本节介绍一个完整的工程架构设计。
工程目录结构:
hdmi_project/
├── rtl/
│ ├── hdmi_top.v # 顶层模块
│ ├── video_timing_gen.v # 视频时序生成
│ ├── tmds_encoder.v # TMDS 编码器
│ ├── serializer.v # 序列化器
│ ├── pll_clk_gen.v # PLL 时钟生成
│ ├── edid_rom.v # EDID 存储
│ └── video_source.v # 视频源(测试用)
├── sim/
│ ├── tb_hdmi_top.v # 顶层测试台
│ ├── tb_tmds_encoder.v # TMDS 编码器测试
│ └── sim_run.do # 仿真脚本
├── xdc/
│ ├── hdmi_pins.xdc # HDMI 引脚约束
│ ├── hdmi_timing.xdc # 时序约束
│ └── hdmi_io.xdc # IO 标准约束
├── ip/
│ ├── pll_clk_gen.xci # PLL IP 核
│ └── fifo_video.xci # 视频 FIFO IP 核
└── doc/
├── hdmi_design_spec.md # 设计规范
└── hdmi_debug_guide.md # 调试指南
顶层模块架构:
module hdmi_top (
input clk_100m, // 100MHz 系统时钟
input rst_n, // 复位信号
// 视频输入接口
input [23:0] video_data, // RGB888 视频数据
input video_valid, // 视频有效信号
input video_hsync, // 行同步
input video_vsync, // 场同步
// HDMI 输出接口
output [3:0] hdmi_clk_p, // HDMI 时钟正
output [3:0] hdmi_clk_n, // HDMI 时钟负
output [3:0] hdmi_d_p, // HDMI 数据正
output [3:0] hdmi_d_n, // HDMI 数据负
output hdmi_scl, // I2C 时钟(EDID)
output hdmi_sda, // I2C 数据(EDID)
// 调试接口
output [7:0] debug_state, // 调试状态
output debug_clk_locked // 时钟锁定状态
);
// 内部信号
wire clk_pixel; // 像素时钟
wire clk_pixel_5x; // 5 倍像素时钟
wire pll_locked; // PLL 锁定信号
wire [23:0] rgb_data; // RGB 数据
wire hsync, vsync, de; // 同步和使能信号
wire [9:0] tmds_r, tmds_g, tmds_b, tmds_clk; // TMDS 编码输出
// 1. PLL 时钟生成
pll_clk_gen pll_inst (
.clk_in1(clk_100m),
.clk_out1(clk_pixel),
.clk_out2(clk_pixel_5x),
.locked(pll_locked)
);
// 2. 视频时序生成
video_timing_gen timing_inst (
.clk_pixel(clk_pixel),
.rst_n(rst_n),
.video_data(video_data),
.video_valid(video_valid),
.video_hsync(video_hsync),
.video_vsync(video_vsync),
.rgb_out(rgb_data),
.hsync_out(hsync),
.vsync_out(vsync),
.de_out(de)
);
// 3. TMDS 编码
tmds_encoder enc_r (
.clk(clk_pixel),
.data_in(rgb_data[23:16]),
.c_in(2'b00),
.de_in(de),
.tmds_out(tmds_r)
);
tmds_encoder enc_g (
.clk(clk_pixel),
.data_in(rgb_data[15:8]),
.c_in({vsync, hsync}),
.de_in(de),
.tmds_out(tmds_g)
);
tmds_encoder enc_b (
.clk(clk_pixel),
.data_in(rgb_data[7:0]),
.c_in(2'b00),
.de_in(de),
.tmds_out(tmds_b)
);
// 时钟通道
assign tmds_clk = 10'b1111100000;
// 4. 序列化器
serializer ser_r (
.clk_pixel_5x(clk_pixel_5x),
.tmds_in(tmds_r),
.hdmi_p(hdmi_d_p[0]),
.hdmi_n(hdmi_d_n[0])
);
serializer ser_g (
.clk_pixel_5x(clk_pixel_5x),
.tmds_in(tmds_g),
.hdmi_p(hdmi_d_p[1]),
.hdmi_n(hdmi_d_n[1])
);
serializer ser_b (
.clk_pixel_5x(clk_pixel_5x),
.tmds_in(tmds_b),
.hdmi_p(hdmi_d_p[2]),
.hdmi_n(hdmi_d_n[2])
);
serializer ser_clk (
.clk_pixel_5x(clk_pixel_5x),
.tmds_in(tmds_clk),
.hdmi_p(hdmi_clk_p[0]),
.hdmi_n(hdmi_clk_n[0])
);
// 5. EDID 处理(I2C 从机)
edid_rom edid_inst (
.scl(hdmi_scl),
.sda(hdmi_sda)
);
// 调试输出
assign debug_state = {pll_locked, de, vsync, hsync, 4'b0};
assign debug_clk_locked = pll_locked;
endmodule
关键设计要点:
在上板前进行充分的仿真验证是确保设计正确性的关键步骤。本节介绍 HDMI 输出的仿真方法。
仿真测试台框架:
`timescale 1ns / 1ps
module tb_hdmi_top;
// 时钟和复位
reg clk_100m;
reg rst_n;
// 视频输入
reg [23:0] video_data;
reg video_valid;
reg video_hsync;
reg video_vsync;
// HDMI 输出
wire [3:0] hdmi_clk_p, hdmi_clk_n;
wire [3:0] hdmi_d_p, hdmi_d_n;
wire hdmi_scl, hdmi_sda;
// 调试信号
wire [7:0] debug_state;
wire debug_clk_locked;
// 实例化被测模块
hdmi_top dut (
.clk_100m(clk_100m),
.rst_n(rst_n),
.video_data(video_data),
.video_valid(video_valid),
.video_hsync(video_hsync),
.video_vsync(video_vsync),
.hdmi_clk_p(hdmi_clk_p),
.hdmi_clk_n(hdmi_clk_n),
.hdmi_d_p(hdmi_d_p),
.hdmi_d_n(hdmi_d_n),
.hdmi_scl(hdmi_scl),
.hdmi_sda(hdmi_sda),
.debug_state(debug_state),
.debug_clk_locked(debug_clk_locked)
);
// 时钟生成:100MHz
initial begin
clk_100m = 0;
forever #5 clk_100m = ~clk_100m;
end
// 复位序列
initial begin
rst_n = 0;
#100 rst_n = 1;
end
// 视频数据生成
initial begin
video_data = 24'h000000;
video_valid = 0;
video_hsync = 1;
video_vsync = 1;
// 等待 PLL 锁定
wait(debug_clk_locked);
#1000;
// 生成 1080p@60Hz 视频时序
repeat(10) begin
generate_frame();
end
$finish;
end
// 生成一帧视频
task generate_frame();
integer h, v, x, y;
begin
// 垂直消隐
for (v = 0; v < 45; v = v + 1) begin
generate_hsync_line();
end
// 有效视频区域
for (v = 0; v < 1080; v = v + 1) begin
video_vsync = 0;
// 水平消隐
for (x = 0; x < 88; x = x + 1) begin
video_hsync = 0;
video_valid = 0;
#20;
end
// 有效像素
for (x = 0; x < 1920; x = x + 1) begin
video_hsync = 1;
video_valid = 1;
// 生成彩条测试图案
video_data = generate_test_pattern(x, v);
#20;
end
// 水平消隐
for (x = 0; x < 44; x = x + 1) begin
video_hsync = 1;
video_valid = 0;
#20;
end
end
video_vsync = 1;
end
endtask
// 生成行同步
task generate_hsync_line();
integer x;
begin
video_vsync = 1;
for (x = 0; x < 88; x = x + 1) begin
video_hsync = 0;
video_valid = 0;
#20;
end
for (x = 0; x < 1920; x = x + 1) begin
video_hsync = 1;
video_valid = 1;
#20;
end
for (x = 0; x < 44; x = x + 1) begin
video_hsync = 1;
video_valid = 0;
#20;
end
end
endtask
// 测试图案生成(彩条)
function [23:0] generate_test_pattern(input integer x, input integer y);
integer bar_width;
integer bar_index;
begin
bar_width = 1920 / 8;
bar_index = x / bar_width;
case(bar_index)
0: generate_test_pattern = 24'hFFFFFF; // 白
1: generate_test_pattern = 24'hFFFF00; // 黄
2: generate_test_pattern = 24'h00FFFF; // 青
3: generate_test_pattern = 24'h00FF00; // 绿
4: generate_test_pattern = 24'hFF00FF; // 品红
5: generate_test_pattern = 24'hFF0000; // 红
6: generate_test_pattern = 24'h0000FF; // 蓝
7: generate_test_pattern = 24'h000000; // 黑
default: generate_test_pattern = 24'h000000;
endcase
end
endfunction
// 监测关键信号
initial begin
$monitor("Time=%0t, PLL_Locked=%b, DE=%b, HSYNC=%b, VSYNC=%b, TMDS_R=%h", $time, debug_clk_locked, debug_state[1], debug_state[2], debug_state[3], dut.tmds_r);
end
// 波形记录
initial begin
$dumpfile("hdmi_sim.vcd");
$dumpvars(0, tb_hdmi_top);
end
endmodule
仿真验证要点:
仿真运行脚本(sim_run.do):
# 创建工作库
vlib work
# 编译 Verilog 文件
vlog -sv rtl/hdmi_top.v
vlog -sv rtl/video_timing_gen.v
vlog -sv rtl/tmds_encoder.v
vlog -sv rtl/serializer.v
vlog -sv rtl/pll_clk_gen.v
vlog -sv rtl/edid_rom.v
vlog -sv sim/tb_hdmi_top.v
# 加载仿真
vsim -t 1ps tb_hdmi_top
# 添加波形
add wave -noupdate /tb_hdmi_top/clk_100m
add wave -noupdate /tb_hdmi_top/rst_n
add wave -noupdate /tb_hdmi_top/debug_clk_locked
add wave -noupdate /tb_hdmi_top/video_hsync
add wave -noupdate /tb_hdmi_top/video_vsync
add wave -noupdate /tb_hdmi_top/video_valid
add wave -noupdate /tb_hdmi_top/video_data
# 运行仿真
run -all
仿真通过后,需要在实际硬件上进行调试。本节介绍 HDMI 输出的上板调试方法。
调试前的准备工作:
# HDMI 时钟通道
set_property PACKAGE_PIN A18 [get_ports hdmi_clk_p[0]]
set_property PACKAGE_PIN B18 [get_ports hdmi_clk_n[0]]
set_property IOSTANDARD TMDS_33 [get_ports hdmi_clk_p[0]]
set_property IOSTANDARD TMDS_33 [get_ports hdmi_clk_n[0]]
# HDMI 数据通道 0(红色)
set_property PACKAGE_PIN D19 [get_ports hdmi_d_p[0]]
set_property PACKAGE_PIN D20 [get_ports hdmi_d_n[0]]
set_property IOSTANDARD TMDS_33 [get_ports hdmi_d_p[0]]
set_property IOSTANDARD TMDS_33 [get_ports hdmi_d_n[0]]
# HDMI 数据通道 1(绿色)
set_property PACKAGE_PIN C20 [get_ports hdmi_d_p[1]]
set_property PACKAGE_PIN B20 [get_ports hdmi_d_n[1]]
set_property IOSTANDARD TMDS_33 [get_ports hdmi_d_p[1]]
set_property IOSTANDARD TMDS_33 [get_ports hdmi_d_n[1]]
# HDMI 数据通道 2(蓝色)
set_property PACKAGE_PIN B19 [get_ports hdmi_d_p[2]]
set_property PACKAGE_PIN A20 [get_ports hdmi_d_n[2]]
set_property IOSTANDARD TMDS_33 [get_ports hdmi_d_p[2]]
set_property IOSTANDARD TMDS_33 [get_ports hdmi_d_n[2]]
# I2C 接口(EDID)
set_property PACKAGE_PIN L18 [get_ports hdmi_scl]
set_property PACKAGE_PIN M18 [get_ports hdmi_sda]
set_property IOSTANDARD LVCMOS33 [get_ports hdmi_scl]
set_property IOSTANDARD LVCMOS33 [get_ports hdmi_sda]
set_property PULLUP TRUE [get_ports hdmi_scl]
set_property PULLUP TRUE [get_ports hdmi_sda]
调试步骤:
第一步:验证时钟生成
// 在顶层模块中添加调试接口
output clk_pixel_debug, // 像素时钟输出
output clk_pixel_5x_debug, // 5 倍时钟输出
output pll_locked_debug // PLL 锁定信号
// 连接到 LED 或示波器
assign clk_pixel_debug = clk_pixel;
assign clk_pixel_5x_debug = clk_pixel_5x;
assign pll_locked_debug = pll_locked;
使用示波器测量:
第二步:验证视频时序
// 添加 ILA(集成逻辑分析仪)进行实时监测
ila_hdmi ila_inst (
.clk(clk_pixel),
.probe0(hsync),
.probe1(vsync),
.probe2(de),
.probe3(rgb_data),
.probe4(tmds_r),
.probe5(tmds_g),
.probe6(tmds_b)
);
检查项:
第三步:验证 HDMI 输出
使用示波器或逻辑分析仪测量 HDMI 差分信号:
| 测量项目 | 标准值 | 容差 |
|---|---|---|
| 差分幅度 | 400-600mV | ±10% |
| 共模电压 | 1.2V | ±0.1V |
| 上升时间 | <200ps | - |
| 下降时间 | <200ps | - |
| 抖动 | <100ps | - |
第四步:显示器连接测试
常见调试问题:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| PLL 不锁定 | 时钟输入异常 | 检查 100MHz 时钟源 |
| 显示器无反应 | EDID 读取失败 | 检查 I2C 连接和上拉 |
| 显示闪烁 | 时序不稳定 | 调整时钟约束 |
| 显示异常 | TMDS 编码错误 | 验证编码器逻辑 |
| 部分像素错误 | 信号完整性差 | 检查 PCB 布线 |
正确的时序约束是 HDMI 输出稳定工作的保证。本节介绍关键的时序约束配置。
时序约束文件(hdmi_timing.xdc):
# 定义时钟周期
create_clock -period 6.734 -name clk_pixel [get_ports clk_100m]
# PLL 输出时钟
create_generated_clock -name clk_pixel_pll \n -source [get_pins pll_inst/clk_in1] \n -divide_by 1 -multiply_by 1.4835 \n [get_pins pll_inst/clk_out1]
create_generated_clock -name clk_pixel_5x_pll \n -source [get_pins pll_inst/clk_in1] \n -divide_by 1 -multiply_by 7.4175 \n [get_pins pll_inst/clk_out2]
# 设置时钟不确定性
set_clock_uncertainty 0.2 [get_clocks clk_pixel_pll]
set_clock_uncertainty 0.2 [get_clocks clk_pixel_5x_pll]
# 设置输入延迟(视频输入)
set_input_delay -clock clk_pixel_pll -min 0 [get_ports video_data]
set_input_delay -clock clk_pixel_pll -max 2 [get_ports video_data]
# 设置输出延迟(HDMI 输出)
set_output_delay -clock clk_pixel_5x_pll -min -1 [get_ports hdmi_d_p]
set_output_delay -clock clk_pixel_5x_pll -max 1 [get_ports hdmi_d_p]
# 差分对约束
set_property DIFF_TERM TRUE [get_ports hdmi_d_p]
set_property DIFF_TERM TRUE [get_ports hdmi_clk_p]
# 阻抗约束
set_property DCI_CASCADE {32} [get_iobanks 34]
# 禁用不必要的时序检查
set_false_path -from [get_clocks clk_pixel_pll] \n -to [get_clocks clk_pixel_5x_pll]
关键约束说明:
在 HDMI 输出调试中,经常会遇到各种显示异常。本节介绍系统的排查方法。
问题 1:显示器无信号
症状:连接 HDMI 显示器后,显示器显示'无信号'或'输入不支持'
排查步骤:
1. 检查硬件连接
├─ HDMI 线缆是否正确连接
├─ 显示器是否打开
└─ 是否选择了正确的输入源
2. 检查 FPGA 工作状态
├─ 检查 PLL 是否锁定(debug_clk_locked 信号)
├─ 检查 LED 指示灯
└─ 使用 ILA 观察时序信号
3. 检查 EDID 通信
├─ 使用 I2C 分析仪检查 SCL/SDA 信号
├─ 验证上拉电阻(通常为 4.7kΩ)
└─ 检查 EDID ROM 数据
4. 检查 HDMI 信号质量
├─ 使用示波器测量差分幅度
├─ 检查共模电压
└─ 观察信号眼图
问题 2:显示闪烁或不稳定
症状:显示器显示图像但频繁闪烁或出现雪花
可能原因及解决方案:
// 原因 1:时序参数错误
// 解决:验证视频时序参数
parameter H_ACTIVE = 1920; // 水平有效像素
parameter H_FRONT = 88; // 水平前廊
parameter H_SYNC = 44; // 水平同步宽度
parameter H_BACK = 148; // 水平后廊
parameter V_ACTIVE = 1080; // 垂直有效行
parameter V_FRONT = 4; // 垂直前廊
parameter V_SYNC = 5; // 垂直同步宽度
parameter V_BACK = 36; // 垂直后廊
// 原因 2:时钟抖动过大
// 解决:检查 PLL 配置和 PCB 布线
// 使用示波器测量时钟抖动,应 < 100ps
// 原因 3:TMDS 编码错误
// 解决:验证编码器逻辑
module tmds_encoder_debug (
input clk,
input [7:0] data_in,
input [1:0] c_in,
input de_in,
output [9:0] tmds_out,
output [15:0] debug_info // 调试信息
);
// 添加内部信号监测
wire [3:0] ones_count;
wire [9:0] encoded_data;
// 计算 1 的个数
assign ones_count = data_in[0] + data_in[1] + data_in[2] + data_in[3] + data_in[4] + data_in[5] + data_in[6] + data_in[7];
// 调试输出
assign debug_info = {ones_count, encoded_data[9:4]};
endmodule
问题 3:显示颜色错误
症状:显示的颜色与输入不符(如红变蓝、绿变红等)
排查方法:
1. 检查 RGB 通道连接
├─ 验证 hdmi_d_p[0] 是否连接到红色通道
├─ 验证 hdmi_d_p[1] 是否连接到绿色通道
└─ 验证 hdmi_d_p[2] 是否连接到蓝色通道
2. 检查 TMDS 编码器
├─ 验证每个编码器的输入数据
├─ 检查编码器输出
└─ 使用 ILA 监测 RGB 数据
3. 检查序列化器
├─ 验证序列化器的输入顺序
├─ 检查 OSERDES 配置
└─ 测量 HDMI 差分信号
问题 4:部分像素错误
症状:显示的图像中出现随机错误像素或条纹
解决方案:
// 原因:信号完整性问题
// 解决:改进 PCB 设计
// 1. 阻抗匹配
// 差分对特性阻抗应为 100Ω ±10%
// 2. 串扰控制
// 在差分对之间保持足够的间距
// 建议间距 > 3 倍线宽
// 3. 反射控制
// 使用终端电阻(通常为 100Ω)
// 放置在接收端
// 4. EMI 控制
// 在 HDMI 连接器附近放置去耦电容
// 使用屏蔽线缆
时序问题是 HDMI 输出最常见的问题。本节介绍系统的时序调试方法。
使用 ILA 进行实时监测:
// 创建 ILA 核心
ila_hdmi ila_inst (
.clk(clk_pixel),
.probe0(hsync), // 行同步
.probe1(vsync), // 场同步
.probe2(de), // 数据使能
.probe3(rgb_data[23:16]), // 红色数据
.probe4(rgb_data[15:8]), // 绿色数据
.probe5(rgb_data[7:0]), // 蓝色数据
.probe6(tmds_r), // TMDS 红色编码
.probe7(tmds_g), // TMDS 绿色编码
.probe8(tmds_b), // TMDS 蓝色编码
.probe9(tmds_clk) // TMDS 时钟编码
);
// 触发条件:当 VSYNC 下降沿时触发
// 这样可以捕获一帧的完整时序
时序验证检查表:
□ HSYNC 脉宽验证
- 应为 44 个像素时钟周期(1080p@60Hz)
- 使用 ILA 测量实际脉宽
□ VSYNC 脉宽验证
- 应为 5 行(1080p@60Hz)
- 检查垂直时序参数
□ DE 信号验证
- DE 应与 RGB 数据同步
- 检查 DE 与 HSYNC/VSYNC 的关系
□ 时钟相位验证
- 像素时钟与 5 倍时钟的相位关系
- 使用示波器测量相位差
□ 数据对齐验证
- RGB 数据应在 DE 有效期间输出
- 检查数据延迟
使用示波器进行眼图分析:
1. 连接示波器到 HDMI 差分对
2. 设置示波器为眼图模式
3. 观察眼图特征:
├─ 眼睛开口度:应 > 200mV
├─ 眼睛宽度:应 > 0.4UI(单位间隔)
└─ 眼睛高度:应 > 300mV
4. 如果眼图不理想:
├─ 检查 PCB 布线
├─ 调整终端电阻
└─ 改进信号完整性
时序约束优化:
# 如果出现时序违规,使用以下方法优化
# 1. 增加时钟不确定性裕度
set_clock_uncertainty 0.5 [get_clocks clk_pixel_pll]
# 2. 放松输出延迟约束
set_output_delay -clock clk_pixel_5x_pll -min -2 [get_ports hdmi_d_p]
set_output_delay -clock clk_pixel_5x_pll -max 2 [get_ports hdmi_d_p]
# 3. 使用多周期路径
set_multicycle_path 2 -from [get_clocks clk_pixel_pll] \n -to [get_clocks clk_pixel_5x_pll]
# 4. 禁用不必要的时序检查
set_false_path -from [get_pins pll_inst/LOCKED] \n -to [get_ports debug_clk_locked]
为了获得最佳的 HDMI 输出性能,本节提供优化建议。
1. 时钟优化
// 使用 MMCM 而不是 PLL(更好的抖动性能)
// MMCM 提供更低的相位噪声
// PLL 配置优化
// 增加环路滤波器阶数
// 使用更高的参考时钟频率
// 时钟分布优化
// 使用全局时钟网络
// 避免长的局部布线
2. 序列化器优化
// 使用 OSERDES2 而不是 OSERDES(更高的速率)
// OSERDES2 支持更高的数据速率
// 配置优化
// 使用 DDR 模式
// 配置正确的数据宽度(10 位)
// 布线优化
// 最小化 OSERDES 到 HDMI 连接器的距离
// 使用差分对布线
3. 信号完整性优化
PCB 设计建议:
├─ 差分对特性阻抗:100Ω ±10%
├─ 差分对间距:保持一致
├─ 差分对长度匹配:< 5mil
├─ 避免过孔:使用盲孔或埋孔
├─ 地平面:在差分对下方
├─ 去耦电容:靠近 FPGA 电源引脚
└─ 屏蔽:使用屏蔽线缆和屏蔽罐
布线规则:
├─ 差分对不应跨越分割
├─ 避免与其他高速信号平行
├─ 使用蛇形布线进行长度匹配
└─ 在转角处使用 45°角
4. 功耗优化
// 使用低功耗模式
// 禁用未使用的 OSERDES
// 时钟门控
// 在不需要 HDMI 输出时关闭时钟
// 电源管理
// 使用动态电压调整
// 优化 PLL 功耗配置
5. 可靠性优化
// 添加看门狗定时器
module watchdog_timer (
input clk,
input rst_n,
input pll_locked,
output reg watchdog_reset
);
reg [31:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
counter <= 0;
watchdog_reset <= 0;
end else if (!pll_locked) begin
counter <= counter + 1;
if (counter > 32'd100_000_000) begin
watchdog_reset <= 1;
end
end else begin
counter <= 0;
watchdog_reset <= 0;
end
end
endmodule
// 添加错误检测
// 监测 HDMI 输出的有效性
// 实现自动恢复机制
性能指标参考:
| 指标 | 目标值 | 测量方法 |
|---|---|---|
| 时钟抖动 | < 100ps | 示波器 |
| 眼图开口 | > 200mV | 眼图分析 |
| 差分幅度 | 400-600mV | 示波器 |
| 共模电压 | 1.2V ±0.1V | 示波器 |
| 上升时间 | < 200ps | 示波器 |
| 帧率稳定性 | ±0.1% | 显示器测试 |
本文从基础概念到工程实战,系统介绍了 FPGA 实现 HDMI 输出的完整方案。
✅ 核心知识体系:
第一阶段:理论基础
第二阶段:硬件设计
第三阶段:工程实现
第四阶段:问题解决
❌ 常见误区与解决方案:
| 误区 | 后果 | 解决方案 |
|---|---|---|
| 忽视信号完整性 | 显示闪烁、错误 | 重视 PCB 设计,进行眼图分析 |
| 时钟配置不当 | PLL 不锁定 | 验证 PLL 参数,检查时钟源 |
| TMDS 编码错误 | 颜色错误、显示异常 | 使用 ILA 监测编码输出 |
| 序列化器参数错误 | 数据错误 | 验证 OSERDES 配置 |
| 时序约束不足 | 时序违规 | 使用 create_generated_clock |
| EDID 配置缺失 | 显示器无反应 | 实现 I2C 从机,提供 EDID |
🎯 学习路线建议:
初级阶段(1-2 周)
├─ 理解 HDMI 协议基础
├─ 学习 TMDS 编码原理
└─ 完成 1080p@60Hz 仿真
中级阶段(2-4 周)
├─ 实现完整的 HDMI 发送器
├─ 进行上板调试
└─ 解决常见问题
高级阶段(4 周+)
├─ 支持 4K 分辨率
├─ 集成音频传输
├─ 优化信号完整性
└─ 实现 HDCP 保护
💡 最佳实践总结:
📊 性能指标检查清单:
□ 时钟指标
├─ 像素时钟频率精度:±0.1%
├─ 时钟抖动:< 100ps
└─ PLL 锁定时间:< 1ms
□ 信号指标
├─ 差分幅度:400-600mV
├─ 共模电压:1.2V ±0.1V
├─ 眼图开口:> 200mV
└─ 眼图宽度:> 0.4UI
□ 功能指标
├─ 分辨率支持:1080p、4K
├─ 刷新率:60Hz、30Hz
├─ 色深:8bit、10bit、12bit
└─ 音频:PCM、DTS、Dolby
□ 可靠性指标
├─ 工作温度范围:0-70°C
├─ 长期稳定性:> 1000 小时
└─ 故障恢复:自动重启
HDMI 输出是 FPGA 高清视频处理的关键技术。建议大家:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online