第31章:HDMI彩条显示实验
本章对应原书第 740~770 页,属于「第五篇 实战篇」。
本章在讲什么?(先看这个)
LCD 屏是"并排递数据",而 HDMI 是"串行高速发数据"。这一章要弄懂 HDMI/DVI 背后的 TMDS 传输协议:8 位像素数据怎么编码成 10 位字符、10 位并行数据怎么高速串行化成差分信号。你还会第一次接触 Xilinx 的硬件原语 OSERDESE2——它是 FPGA 内部专用的并串转换器。
实验任务:驱动开发板 HDMI 接口,在普通显示器上显示彩条。显示时序部分与 LCD 彩条实验高度相似(扫描逻辑复用),新增的是 DVI 编码和并串转换两个模块。
核心概念拆解
31.1 简介:从VGA到HDMI
HDMI(High-Definition Multimedia Interface,高清多媒体接口)能同时传输视频和音频,带宽高、可传无压缩数字音视频。各版本速率一览:
- HDMI 1.0(2002 年):最高 5Gbps;
- HDMI 1.4(2006 年):理论 10.2Gbps,实际 8.16Gbps;
- HDMI 2.0(2013 年):理论 18Gbps,实际 14.4Gbps;
- HDMI 2.1(2017 年):理论 48Gbps,实际 42.6Gbps。
开发板板载 HDMI 接口符合 1.4 标准,受实现方案和速度等级限制,最大支持 1080P@60。
在 HDMI 之前主流的是 VGA(Video Graphics Array)——模拟信号传输,15 针插针结构,容易受干扰,高分辨率下字体模糊、线长会拖尾,如今已逐渐退出:

HDMI 向下兼容 DVI(数字视频接口),两者的主要差别:DVI 只能传视频不能传音频,且体积更大:

A 型 HDMI 接口引脚定义:

TMDS传输协议(本章灵魂)
DVI 和 HDMI 的物理层都用 TMDS(Transition Minimized Differential Signaling,最小化传输差分信号)传输视频数据:用两根引脚(如"数据2+""数据2-")的电压差正负极性和大小表示 0 或 1。
TMDS 连接通过四个串行通道实现:

- 三个数据通道:DVI 传输像素点的 R、G、B 三个颜色分量(RGB 4:4:4);HDMI 默认也用 RGB,还可选 YCrCb 格式;
- 一个时钟通道:传输像素时钟,为接收端提供参考频率。
数据通路分两个阶段:8 位颜色数据先经 8B/10B 编码器变成 10 位像素字符,再经并串转换器(Serializer)转成串行数据发出。这个 10:1 的并转串产生的串行速率是像素时钟的 10 倍。
消隐期传什么?控制字符。每个通道有 2 位控制信号输入,对应四种控制字符,提供 HSYNC/VSYNC 信息和数据边界(用于同步)。DVI 的整个消隐期都传控制字符;HDMI 的消隐期还可以传音频或附加数据(如字幕)——这正是两者最主要的差别(图中 "Auxiliary Data" 标注 "HDMI Only")。
本实验只显示图像不传音频,所以只需实现 DVI 接口的驱动逻辑。
TMDS编码算法
DVI 编码器示意图(RGB 三通道逻辑完全相同;HSYNC/VSYNC 在蓝色通道编码;绿、红通道的控制信号 DVI 规范中预留未用,置 2'b00):


编码算法的目标有两个:
- 直流平衡(DC balance):一段时间内传输的 1 和 0 个数大致相等,减小电磁干扰,可用低成本电缆实现长距离高质量传输;
- 限制跳变次数:每个编码后 10-bit 字符中状态跳变(1↔0)不超过 5 次,减小上冲和下冲。
每个 8-bit 数据被转换成 460 个特定 10-bit 字符之一。2-bit 控制信号则对应四个固定的 10-bit 控制字符:10'b1101010100、10'b0010101011、10'b0101010100、10'b1010101011——每个控制字符有七次及以上跳变,接收端正是靠跳变次数差异来区分视频字符和控制字符、实现同步。
31.2 硬件设计
开发板右侧板载一个 HDMI 接口:

原理图中,三个数据通道 HDMI_D[2:0] 和时钟通道 HDMI_CLK 直连 TMDS 差分引脚。HDMI_HPD 是热拔插检测(Hot Plug Detect)通道:接收设备置 HPD 为高通知发送端"连接已建立",检测到低电平表示断开:

31.3 实验任务
驱动开发板 HDMI 接口,在显示器上显示彩条图案。
31.4 程序设计
整体模块设计
与 LCD 彩条实验类似,但 HDMI 只需驱动一款显示器,所以不需要读 ID 模块和时钟分频模块,改用 PLL 时钟 IP 输出像素时钟:

- PLL时钟模块(clk_wiz_0):输出像素时钟 pixel_clk 和 5 倍像素时钟 pixel_clk_x5(理论上要 10 倍,为何 5 倍就够?见下文 OSERDESE2 的 DDR 特性);
- 视频显示模块(video_display):按像素坐标输出彩条数据(同 LCD 彩条实验);
- 视频驱动模块(video_driver):输出 video_hs/video_vs/video_de 时序和 video_rgb 数据(同 LCD 驱动模块);
- DVI传输模块(dvi_transmitter_top):编码(dvi_encoder)+ 并串转换(serializer_10_to_1),最后经原语转差分输出。

RGB2DVI模块设计(dvi_transmitter_top)
RGB2DVI 设计框图:dvi_encoder 负责编码,serializer_10_to_1 负责并串转换:

两个关键点:
- 为什么只要 5 倍时钟:serializer_10_to_1 使用 OSERDESE2 原语,它支持 DDR(双倍数据速率),5 倍时钟 × 2 = 10 倍数据速率,正好满足 10:1 的转换需求。
- 时钟通道怎么做:对固定序列 10'b11111_00000 做同样的并串转换,就能得到与像素时钟一致的 TMDS 参考时钟。
顶层代码(注意编码模块要求复位高电平有效,所以用 asyn_rst_syn 做异步复位、同步释放,并转成高有效):
module dvi_transmitter_top(
input pclk, // pixel clock
input pclk_x5, // pixel clock x5
input reset_n, // reset
input [23:0] video_din, // RGB888 video in
input video_hsync, // hsync data
input video_vsync, // vsync data
input video_de, // data enable
output tmds_clk_p, // TMDS 时钟通道
output tmds_clk_n,
output [2:0] tmds_data_p, // TMDS 数据通道
output [2:0] tmds_data_n,
output tmds_oen // TMDS 输出使能
);
//wire define
wire reset;
//并行数据
wire [9:0] red_10bit;
wire [9:0] green_10bit;
wire [9:0] blue_10bit;
wire [9:0] clk_10bit;
//串行数据
wire [2:0] tmds_data_serial;
wire tmds_clk_serial;
assign tmds_oen = 1'b1;
assign clk_10bit = 10'b1111100000;
//异步复位,同步释放
asyn_rst_syn reset_syn(
.reset_n (reset_n),
.clk (pclk),
.syn_reset (reset) //高有效
);
//对三个颜色通道进行编码
dvi_encoder encoder_b (
.clkin (pclk),
.rstin (reset),
.din (video_din[7:0]),
.c0 (video_hsync), // 行场同步在蓝色通道编码
.c1 (video_vsync),
.de (video_de),
.dout (blue_10bit)
) ;
dvi_encoder encoder_g (
.clkin (pclk),
.rstin (reset),
.din (video_din[15:8]),
.c0 (1'b0), // 绿、红通道控制信号预留,置0
.c1 (1'b0),
.de (video_de),
.dout (green_10bit)
) ;
dvi_encoder encoder_r (
.clkin (pclk),
.rstin (reset),
.din (video_din[23:16]),
.c0 (1'b0),
.c1 (1'b0),
.de (video_de),
.dout (red_10bit)
) ;
//对编码后的数据进行并串转换
serializer_10_to_1 serializer_b(
.reset (reset),
.paralell_clk (pclk),
.serial_clk_5x (pclk_x5),
.paralell_data (blue_10bit),
.serial_data_out (tmds_data_serial[0])
);
serializer_10_to_1 serializer_g(
.reset (reset),
.paralell_clk (pclk),
.serial_clk_5x (pclk_x5),
.paralell_data (green_10bit),
.serial_data_out (tmds_data_serial[1])
);
serializer_10_to_1 serializer_r(
.reset (reset),
.paralell_clk (pclk),
.serial_clk_5x (pclk_x5),
.paralell_data (red_10bit),
.serial_data_out (tmds_data_serial[2])
);
// 时钟通道:固定序列10'b1111100000转串行,得到像素时钟
serializer_10_to_1 serializer_clk(
.reset (reset),
.paralell_clk (pclk),
.serial_clk_5x (pclk_x5),
.paralell_data (clk_10bit),
.serial_data_out (tmds_clk_serial)
);
//转换差分信号
OBUFDS #(
.IOSTANDARD ("TMDS_33") // I/O 电平标准为TMDS
) TMDS0 (
.I (tmds_data_serial[0]),
.O (tmds_data_p[0]),
.OB (tmds_data_n[0])
);
OBUFDS #(
.IOSTANDARD ("TMDS_33")
) TMDS1 (
.I (tmds_data_serial[1]),
.O (tmds_data_p[1]),
.OB (tmds_data_n[1])
);
OBUFDS #(
.IOSTANDARD ("TMDS_33")
) TMDS2 (
.I (tmds_data_serial[2]),
.O (tmds_data_p[2]),
.OB (tmds_data_n[2])
);
OBUFDS #(
.IOSTANDARD ("TMDS_33")
) TMDS3 (
.I (tmds_clk_serial),
.O (tmds_clk_p),
.OB (tmds_clk_n)
);
endmodule
编码模块(dvi_encoder)
该模块是 Xilinx 应用笔记官方提供的,按 DVI 规范的 TMDS 编码算法工作,可直接使用。编码算法流程:

算法思路:10 位输出中,前 8 位由原始数据运算而来,第 9 位表示运算方式(1 为异或 XOR,0 为异或非 XNOR),第 10 位是反转标志(1 表示反转),以此达到 DC 平衡。
module dvi_encoder (
input clkin, // pixel clock input
input rstin, // async. reset input (active high)
input [7:0] din, // data inputs: expect registered
input c0, // c0 input
input c1, // c1 input
input de, // de input
output reg [9:0] dout // data outputs
);
//统计输入数据中"1"的个数
reg [3:0] n1d; //number of 1s in din
reg [7:0] din_q;
always @ (posedge clkin) begin
n1d <=#1 din[0] + din[1] + din[2] + din[3] + din[4] + din[5] + din[6] + din[7];
din_q <=#1 din;
end
///////////////////////////////////////////////////////
// Stage 1: 8 bit -> 9 bit
// decision1: "1"的个数多于"0"时用XNOR,否则用XOR
///////////////////////////////////////////////////////
wire decision1;
assign decision1 = (n1d > 4'h4) | ((n1d == 4'h4) & (din_q[0] == 1'b0));
wire [8:0] q_m;
assign q_m[0] = din_q[0];
assign q_m[1] = (decision1) ? (q_m[0] ^~ din_q[1]) : (q_m[0] ^ din_q[1]);
assign q_m[2] = (decision1) ? (q_m[1] ^~ din_q[2]) : (q_m[1] ^ din_q[2]);
assign q_m[3] = (decision1) ? (q_m[2] ^~ din_q[3]) : (q_m[2] ^ din_q[3]);
assign q_m[4] = (decision1) ? (q_m[3] ^~ din_q[4]) : (q_m[3] ^ din_q[4]);
assign q_m[5] = (decision1) ? (q_m[4] ^~ din_q[5]) : (q_m[4] ^ din_q[5]);
assign q_m[6] = (decision1) ? (q_m[5] ^~ din_q[6]) : (q_m[5] ^ din_q[6]);
assign q_m[7] = (decision1) ? (q_m[6] ^~ din_q[7]) : (q_m[6] ^ din_q[7]);
assign q_m[8] = (decision1) ? 1'b0 : 1'b1;
///////////////////////////////////////////////////////
// Stage 2: 9 bit -> 10 bit(用disparity计数器调整直流平衡)
///////////////////////////////////////////////////////
reg [3:0] n1q_m, n0q_m; // q_m 中1和0的个数
always @ (posedge clkin) begin
n1q_m <=#1 q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7];
n0q_m <=#1 4'h8 - (q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7]);
end
parameter CTRLTOKEN0 = 10'b1101010100;
parameter CTRLTOKEN1 = 10'b0010101011;
parameter CTRLTOKEN2 = 10'b0101010100;
parameter CTRLTOKEN3 = 10'b1010101011;
reg [4:0] cnt; //disparity counter, MSB is the sign bit
wire decision2, decision3;
assign decision2 = (cnt == 5'h0) | (n1q_m == n0q_m);
assign decision3 = (~cnt[4] & (n1q_m > n0q_m)) | (cnt[4] & (n0q_m > n1q_m));
//流水线对齐
reg de_q, de_reg;
reg c0_q, c1_q;
reg c0_reg, c1_reg;
reg [8:0] q_m_reg;
always @ (posedge clkin) begin
de_q <=#1 de;
de_reg <=#1 de_q;
c0_q <=#1 c0;
c0_reg <=#1 c0_q;
c1_q <=#1 c1;
c1_reg <=#1 c1_q;
q_m_reg <=#1 q_m;
end
//10-bit 输出
always @ (posedge clkin or posedge rstin) begin
if(rstin) begin
dout <= 10'h0;
cnt <= 5'h0;
end else begin
if (de_reg) begin // 有效数据期:输出编码像素
if(decision2) begin
dout[9] <=#1 ~q_m_reg[8];
dout[8] <=#1 q_m_reg[8];
dout[7:0] <=#1 (q_m_reg[8]) ? q_m_reg[7:0] : ~q_m_reg[7:0];
cnt <=#1 (~q_m_reg[8]) ? (cnt + n0q_m - n1q_m) : (cnt + n1q_m - n0q_m);
end else begin
if(decision3) begin
dout[9] <=#1 1'b1;
dout[8] <=#1 q_m_reg[8];
dout[7:0] <=#1 ~q_m_reg[7:0];
cnt <=#1 cnt + {q_m_reg[8], 1'b0} + (n0q_m - n1q_m);
end else begin
dout[9] <=#1 1'b0;
dout[8] <=#1 q_m_reg[8];
dout[7:0] <=#1 q_m_reg[7:0];
cnt <=#1 cnt - {~q_m_reg[8], 1'b0} + (n1q_m - n0q_m);
end
end
end else begin // 消隐期:输出控制字符
case ({c1_reg, c0_reg})
2'b00: dout <=#1 CTRLTOKEN0;
2'b01: dout <=#1 CTRLTOKEN1;
2'b10: dout <=#1 CTRLTOKEN2;
default: dout <=#1 CTRLTOKEN3;
endcase
cnt <=#1 5'h0;
end
end
end
endmodule

仿真中可以对照验证:decision2=1 时对应 decision2 分支;decision2=0 且 decision3=0/1 时对应不同反转分支。注意 cnt 和 dout 是时序逻辑,输出延迟一个时钟周期。
并串转换模块(serializer_10_to_1)
OSERDESE2 是 Xilinx 专用的并-串转换器原语:数据串行化位宽可达 8:1(级联两个原语可达 14:1),数据和三态序列化都支持 SDR/DDR 模式:

由于单个 OSERDESE2 最多 8:1,实现 10:1 需要两个原语级联(一个 MASTER 一个 SLAVE,Slave 的数据输入只能用 D3~D8):

关键属性配置(依据 UG471 手册):10 转 1 的并串转换要求 DATA_RATE_OQ = "DDR"、DATA_RATE_TQ = "SDR"、DATA_WIDTH = 10。原语模板可在 Vivado 的 Tools → Language Templates 中搜索 "OSERDESE2"(选 A7 系列)获得。
module serializer_10_to_1(
input reset, // 复位,高有效
input paralell_clk, // 输入并行数据时钟
input serial_clk_5x, // 输入串行数据时钟
input [9:0] paralell_data, // 输入并行数据
output serial_data_out // 输出串行数据
);
//wire define
wire cascade1; //用于两个OSERDESE2 级联的信号
wire cascade2;
//例化OSERDESE2 原语,实现并串转换,Master 模式
OSERDESE2 #(
.DATA_RATE_OQ ("DDR"), // 设置双倍数据速率
.DATA_RATE_TQ ("SDR"), // DDR, BUF, SDR
.DATA_WIDTH (10), // 输入的并行数据宽度为10bit
.SERDES_MODE ("MASTER"), // 设置为Master,用于10bit 宽度扩展
.TBYTE_CTL ("FALSE"),
.TBYTE_SRC ("FALSE"),
.TRISTATE_WIDTH (1)
)
OSERDESE2_Master (
.CLK (serial_clk_5x), // 串行数据时钟,5 倍时钟频率
.CLKDIV (paralell_clk), // 并行数据时钟
.RST (reset), // 1-bit input: Reset
.OCE (1'b1), // 1-bit input: Output data clock enable
.OQ (serial_data_out), // 串行输出数据
.D1 (paralell_data[0]), // D1 - D8: 并行数据输入
.D2 (paralell_data[1]),
.D3 (paralell_data[2]),
.D4 (paralell_data[3]),
.D5 (paralell_data[4]),
.D6 (paralell_data[5]),
.D7 (paralell_data[6]),
.D8 (paralell_data[7]),
.SHIFTIN1 (cascade1), // SHIFTIN1 用于位宽扩展
.SHIFTIN2 (cascade2),
.SHIFTOUT1 (),
.SHIFTOUT2 (),
.OFB (),
.T1 (1'b0),
.T2 (1'b0),
.T3 (1'b0),
.T4 (1'b0),
.TBYTEIN (1'b0),
.TCE (1'b0),
.TBYTEOUT (),
.TFB (),
.TQ ()
);
//例化OSERDESE2 原语,实现并串转换,Slave 模式
OSERDESE2 #(
.DATA_RATE_OQ ("DDR"),
.DATA_RATE_TQ ("SDR"),
.DATA_WIDTH (10),
.SERDES_MODE ("SLAVE"), // 设置为Slave,用于10bit 宽度扩展
.TBYTE_CTL ("FALSE"),
.TBYTE_SRC ("FALSE"),
.TRISTATE_WIDTH (1)
)
OSERDESE2_Slave (
.CLK (serial_clk_5x),
.CLKDIV (paralell_clk),
.RST (reset),
.OCE (1'b1),
.OQ (), // Slave不直接输出
.D1 (1'b0), // 注意:Slave的数据输入只能用D3~D8
.D2 (1'b0),
.D3 (paralell_data[8]),
.D4 (paralell_data[9]),
.D5 (1'b0),
.D6 (1'b0),
.D7 (1'b0),
.D8 (1'b0),
.SHIFTIN1 (),
.SHIFTIN2 (),
.SHIFTOUT1 (cascade1), // 级联到Master的SHIFTIN
.SHIFTOUT2 (cascade2),
.OFB (),
.T1 (1'b0),
.T2 (1'b0),
.T3 (1'b0),
.T4 (1'b0),
.TBYTEIN (1'b0),
.TCE (1'b0),
.TBYTEOUT (),
.TFB (),
.TQ ()
);
endmodule

仿真验证:并行输入 10'b1101010100,经过 4 个串行时钟的延迟后,串行输出依次为 0-0-1-0-1-0-1-0-1-1——正是并行数据从低位到高位依次串行输出的结果。
顶层模块
顶层例化时钟 IP(输出 pixel_clk 和 5 倍时钟)、视频驱动/显示模块和 RGB2DVI 模块:
module hdmi_colorbar_top(
input sys_clk,
input sys_rst_n,
output tmds_clk_p, // TMDS 时钟通道
output tmds_clk_n,
output [2:0] tmds_data_p, // TMDS 数据通道
output [2:0] tmds_data_n
);
//wire define
wire pixel_clk;
wire pixel_clk_5x;
wire clk_locked;
wire [10:0] pixel_xpos_w;
wire [10:0] pixel_ypos_w;
wire [23:0] pixel_data_w;
wire video_hs;
wire video_vs;
wire video_de;
wire [23:0] video_rgb;
//例化MMCM/PLL IP 核
clk_wiz_0 clk_wiz_0(
.clk_in1 (sys_clk),
.clk_out1 (pixel_clk), //像素时钟
.clk_out2 (pixel_clk_5x), //5 倍像素时钟
.reset (~sys_rst_n),
.locked (clk_locked)
);
//例化视频显示驱动模块
video_driver u_video_driver(
.pixel_clk (pixel_clk ),
.sys_rst_n (sys_rst_n ),
.video_hs (video_hs ),
.video_vs (video_vs ),
.video_de (video_de ),
.video_rgb (video_rgb ),
.data_req (),
.pixel_xpos (pixel_xpos_w ),
.pixel_ypos (pixel_ypos_w ),
.pixel_data (pixel_data_w )
);
//例化视频显示模块
video_display u_video_display(
.pixel_clk (pixel_clk),
.sys_rst_n (sys_rst_n),
.pixel_xpos (pixel_xpos_w),
.pixel_ypos (pixel_ypos_w),
.pixel_data (pixel_data_w)
);
//例化RGB2DVI 模块
dvi_transmitter_top u_rgb2dvi_0(
.pclk (pixel_clk),
.pclk_x5 (pixel_clk_5x),
.reset_n (sys_rst_n & clk_locked), // 等PLL锁定后再释放复位
.video_din (video_rgb),
.video_hsync (video_hs),
.video_vsync (video_vs),
.video_de (video_de),
.tmds_clk_p (tmds_clk_p),
.tmds_clk_n (tmds_clk_n),
.tmds_data_p (tmds_data_p),
.tmds_data_n (tmds_data_n),
.tmds_oen ()
);
endmodule
仿真验证
TB 很简单,只有时钟和复位两个激励:
`timescale 1ns/1ns
module tb_hdmi_colorbar_top();
reg sys_clk ;
reg sys_rst_n ;
wire tmds_clk_p ;
wire tmds_clk_n ;
wire [2:0] tmds_data_p ;
wire [2:0] tmds_data_n ;
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#201
sys_rst_n <= 1'b1;
end
always #10 sys_clk <= ~sys_clk;
hdmi_colorbar_top hdmi_colorbar_top_inst(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.tmds_clk_p (tmds_clk_p ),
.tmds_clk_n (tmds_clk_n ),
.tmds_data_p (tmds_data_p),
.tmds_data_n (tmds_data_n)
);
endmodule


波形显示差分时钟/数据输出正常,video_rgb 在 video_de 有效区内依次输出白、黑、红、绿、蓝五种颜色,与设计一致。
31.5 下载验证
管脚分配(注意 TMDS 通道的电平标准是 TMDS_33,不是 LVCMOS33):
| 信号名 | 方向 | 管脚 | 说明 | 电平标准 |
|---|---|---|---|---|
| sys_clk | input | U18 | 系统时钟 50M | LVCMOS33 |
| sys_rst_n | input | N16 | 复位 | LVCMOS33 |
| tmds_data_p[2:0] | output | J20/K19/G19 | TMDS 数据通道正极 | TMDS_33 |
| tmds_data_n[2:0] | output | H20/J19/G20 | TMDS 数据通道负极 | TMDS_33 |
| tmds_clk_p | output | J18 | TMDS 时钟通道正极 | TMDS_33 |
| tmds_clk_n | output | H18 | TMDS 时钟通道负极 | TMDS_33 |
create_clock -period 20.000 -name sys_clk -waveform {0.000 10.000} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
set_property -dict {PACKAGE_PIN J20 IOSTANDARD TMDS_33 } [get_ports {tmds_data_p[2]}]
set_property -dict {PACKAGE_PIN K19 IOSTANDARD TMDS_33 } [get_ports {tmds_data_p[1]}]
set_property -dict {PACKAGE_PIN G19 IOSTANDARD TMDS_33 } [get_ports {tmds_data_p[0]}]
set_property -dict {PACKAGE_PIN J18 IOSTANDARD TMDS_33 } [get_ports tmds_clk_p]
set_property -dict {PACKAGE_PIN H20 IOSTANDARD TMDS_33 } [get_ports {tmds_data_n[2]}]
set_property -dict {PACKAGE_PIN J19 IOSTANDARD TMDS_33 } [get_ports {tmds_data_n[1]}]
set_property -dict {PACKAGE_PIN G20 IOSTANDARD TMDS_33 } [get_ports {tmds_data_n[0]}]
set_property -dict {PACKAGE_PIN H18 IOSTANDARD TMDS_33 } [get_ports tmds_clk_n]
上板:JTAG 连接下载器,HDMI 线连接显示器与开发板,上电下载 BIT 文件,显示器出现彩条即成功:


初学者容易踩的坑
- 5 倍时钟别理解错:串行速率确实是像素时钟的 10 倍,但 OSERDESE2 工作在 DDR 模式(双边沿采样),所以只需 5 倍时钟;PLL 必须同时输出 pixel_clk 和 pixel_clk_x5 两个时钟,缺一不可。
- 编码模块复位是高有效:dvi_encoder 的 rstin 高电平有效,与平时低有效的 sys_rst_n 相反,要用 asyn_rst_syn 模块做"异步复位、同步释放"并取反。
- OSERDESE2 级联规则:Slave 的数据只能接 D3~D8(D1/D2 必须置 0),级联靠 SHIFTOUT/SHIFTIN 连接;属性 DATA_RATE_OQ="DDR"、DATA_RATE_TQ="SDR"、DATA_WIDTH=10 按 UG471 配置,配错会综合报错或输出错乱。
- TMDS 引脚电平标准:HDMI 差分引脚必须约束为 TMDS_33,写成 LVCMOS33 会导致实现失败或信号异常。
- 复位释放要等 PLL 锁定:顶层把
sys_rst_n & clk_locked传给 DVI 模块,PLL 未锁定就放行数据会输出乱码。 - 串行输出有固定延迟:仿真中并行数据到串行输出有约 4 个串行时钟的延迟,这是 OSERDESE2 内部流水线,属正常现象。
小结 & 下一步
本章打通了"数字视频走串行差分线"的完整链路:视频时序(复用 LCD 扫描框架)→ TMDS 8B/10B 编码(DC 平衡 + 控制字符)→ OSERDESE2 级联实现 10:1 并串转换 → OBUFDS 输出 TMDS 差分信号。这是 FPGA 视频输出领域最经典的 RGB2DVI 方案,学会了它,HDMI 方块移动、更高分辨率输出都是在它之上做增量。
动手建议:完成拓展训练——把 720p 彩条改成 1080p 彩条显示(更换 PLL 输出的像素时钟为 148.5MHz 并修改 video_driver 的时序参数),检验你对时序参数与时钟配套的理解。