第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 针插针结构,容易受干扰,高分辨率下字体模糊、线长会拖尾,如今已逐渐退出:

图 31.1.1 VGA 接口

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

图 31.1.2 DVI 接口(左)和HDMI 接口(右)实物图

A 型 HDMI 接口引脚定义:

图 31.1.3 HDMI 接口引脚定义

TMDS传输协议(本章灵魂)

DVI 和 HDMI 的物理层都用 TMDS(Transition Minimized Differential Signaling,最小化传输差分信号)传输视频数据:用两根引脚(如"数据2+""数据2-")的电压差正负极性和大小表示 0 或 1。

TMDS 连接通过四个串行通道实现:

图 31.1.4 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):

图 31.1.5 DVI 编码输出示意图

图 31.1.6 DVI 编码器示意图

编码算法的目标有两个:

  • 直流平衡(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 接口:

图 31.2.1 HDMI 接口实物图

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

图 31.2.2 HDMI 接口原理图

31.3 实验任务

驱动开发板 HDMI 接口,在显示器上显示彩条图案。

31.4 程序设计

整体模块设计

与 LCD 彩条实验类似,但 HDMI 只需驱动一款显示器,所以不需要读 ID 模块和时钟分频模块,改用 PLL 时钟 IP 输出像素时钟:

图 31.4.1 系统架构图

  • 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),最后经原语转差分输出。

图 31.4.2 HDMI 系统总体框图

RGB2DVI模块设计(dvi_transmitter_top)

RGB2DVI 设计框图:dvi_encoder 负责编码,serializer_10_to_1 负责并串转换:

图 31.4.3 RGB2DVI 设计框图

两个关键点:

  1. 为什么只要 5 倍时钟:serializer_10_to_1 使用 OSERDESE2 原语,它支持 DDR(双倍数据速率),5 倍时钟 × 2 = 10 倍数据速率,正好满足 10:1 的转换需求。
  2. 时钟通道怎么做:对固定序列 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 编码算法工作,可直接使用。编码算法流程:

图 31.4.6 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

图 31.4.7 编码模块仿真图

仿真中可以对照验证:decision2=1 时对应 decision2 分支;decision2=0 且 decision3=0/1 时对应不同反转分支。注意 cnt 和 dout 是时序逻辑,输出延迟一个时钟周期。

并串转换模块(serializer_10_to_1)

OSERDESE2 是 Xilinx 专用的并-串转换器原语:数据串行化位宽可达 8:1(级联两个原语可达 14:1),数据和三态序列化都支持 SDR/DDR 模式:

图 31.4.9 OSERESE2 框图

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

图 31.4.10 OSERDESE2 位宽扩展

关键属性配置(依据 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

图 31.4.14 并串转换模块仿真波形

仿真验证:并行输入 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

图 31.4.15 HDMI 顶层差分信号

图 31.4.16 HDMI 彩条仿真波形

波形显示差分时钟/数据输出正常,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 文件,显示器出现彩条即成功:

图 31.5.1 开发板连接示意图

图 31.5.2 实验结果

初学者容易踩的坑

  • 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 的时序参数),检验你对时序参数与时钟配套的理解。