基于FPGA的HDMI编码模块设计(包含工程源文件)

前文已经通过FPGA实现了TMDS视频编码的算法,也对单沿数据采样转双沿数据采样的ODDR原语做了详细讲解和仿真验证,本文将这些模块结合,设计出HDMI编码模块,在HDMI接口的显示器上显示一张图片。

1、整体思路

如图1所示,是本文的整体框图,video_display用于产生图片的像素数据,video_driver用于生成指定分辨率,刷新率的像素有效指示信号,行、场同步信号,dvi_transmitter模块将行、场同步信号,像素信号,像素有效指示信号通过TMDS编码,然后将并行的单沿信号转换为串行双沿采样信号,最后通过OBUFDS将串行数据转换为差分信号输出,驱动HDMI接口的显示器显示图片。

图1 整体框图

本次使用的显示器分辨率为1024*768,如果刷新率达到60Hz,根据数据手册要求HDMI并行数据时钟为65MHz,串行数据的时钟频率为65MHz*5 = 325MHz。开发板外部晶振提供100MHz时钟,所以利用MMCM时钟单元通过输入的100MHz时钟,输出65MHz和325MHz时钟作为其余模块时钟信号。

2、video_driver模块

该模块实现的功能与VGA、LCD的RGB接口的功能一致,没有找到HDMI相关时序图,就放一个LCD的RGB接口时序图,如图2所示。Vsync为场同步信号,Hsync为行同步信号,DE为像素有效指示信号,Dn0~Dn7为像素数据。Hsync信号为高电平时表示在刷新一帧数据,Vsync表示刷新一行数据,在tvd和thd重合的时间内DE为高电平,此时的像素数据用于显示器显示。


图2 LCD的RGB接口时序

如图3 中V_DSIP和H_DISP围成蓝色区域就是人眼看到的显示器区域,控制芯片刷新显示器时,除了产生有效区域的时序外,还要产生显示前沿和显示后沿等时序,这些时序只有行场同步信号产生对应时序,DE信号和像素信号只在蓝色区域有效。比如一块1024*600分辨率的频率,其实就是蓝色区域有1024个纵坐,有600个横坐标,即H_DISP=1024,V_DISP=600。根据数据手册知 H_SYNC=20,H_BACK=36,H_FRONT=4,即刷新一行数据,其实要发送1024+20+36+4=1084个场同步信号,相当于刷新1084列的时间。行刷新也是类似的道理。

图3 显示器的显示区域示意图

显示器一般是一行一行进行刷新数据的,从左往右,从上往下刷新,当行、场同步信号生成到有效显示区域时,输出对应位置的像素数据,并且把像素有效指示信号拉高。

该模块需要根据显示器分辨率产生符合要求的行、场同步信号,当刷新到有效显示区域时,向上游模块请求像素数据,并且把刷新位置的像素坐标输出给上游模块。向下游模块或者显示器接口输出行、场信号,像素数据,像素有效指示信号。信号端口如下表1所示:
表1 video_driver模块的端口信号

信号名 I/O 位宽 含义
clk I 1 系统时钟
rst I 1 系统复位,高电平有效
data_req O 1 像素请求信号,高电平有效
pixel_xpos O 11 请求像素横坐标
pixel_ypos O 11 请求像素纵坐标
pixel_data I 24 上游模块输入的像素数据
video_hs O 1 场同步信号
video_vs O 1 行同步信号
video_de O 1 像素有效指示信号
video_rgb O 24 输出的像素数据

通过视频电子标准协会的显示器标准手册(在公众号后台回复"HDMI"就可获取)就可以获得不同分辨率显示器的各个参数信息,如图4是1024*768分辨率显示器的一些参数。当刷新率为60Hz时,需要65MHz的时钟信号,同时可以获取图3中8个参数,H_DISP=1024,H_SYNC=136,H_BACK=160,H_FRONT=24,V_DISP=768,V_SYNC=6,V_BACK=29,V_FRONT=3,这些数值的单位是时钟个数。


图4 1024*768分辨率的各个参数

该手册有几乎所有分辨率显示器的参数,可以查取各自的所需要的分辨率,可知1080P 60Hz需要148.5MHz的时钟信号。

该模块的代码整体比较简单,如下所示,需要注意的是像素请求信号和像素坐标需要提前几个时钟周期发送给上游模块。、

`define HDMI_768P
module video_driver (
    input           	        clk	        ,//系统时钟信号;
    input           	        rst  	    ,//系统复位信号,高电平有效;
	
    output  reg     	        video_hs	,//行同步信号;
    output  reg     	        video_vs	,//场同步信号;
    output  reg     	        video_de	,//数据使能;
    output  reg [23:0]          video_rgb	,//RGB888颜色数据;

    output	reg			        data_req 	,//像素申请信号;
    input   	[23:0]          pixel_data	,//像素点数据;
    output  reg	[10:0]          pixel_xpos	,//像素点横坐标;
    output  reg	[10:0]          pixel_ypos   //像素点纵坐标;
);
    `ifdef HDMI_720P//1280*720 分辨率时序参数;60HZ刷新率对应时钟频率74.25MHZ
        localparam  H_SYNC      =   11'd40      ;//行同步;
        localparam  H_BACK      =   11'd220     ;//行显示后沿;
        localparam  H_DISP      =   11'd1280    ;//行有效数据;
        localparam  H_FRONT     =   11'd110     ;//行显示前沿;

        localparam  V_SYNC      =   11'd5       ;//场同步;
        localparam  V_BACK      =   11'd20      ;//场显示后沿;
        localparam  V_DISP      =   11'd720     ;//场有效数据;
        localparam  V_FRONT     =   11'd5       ;//场显示前沿;
    `elsif HDMI_768P//1024*768,60HZ分辨率时序参数;对应时钟频率65MHz;
        localparam  H_SYNC      =  12'd136      ;//行同步;
        localparam  H_BACK      =  12'd160      ;//行显示后沿;
        localparam  H_DISP      =  12'd1024     ;//行有效数据;
        localparam  H_FRONT     =  12'd24       ;//行显示前沿;

        localparam  V_SYNC      =  12'd6        ;//场同步;
        localparam  V_BACK      =  12'd29       ;//场显示后沿;
        localparam  V_DISP      =  12'd768      ;//场有效数据;
        localparam  V_FRONT     =  12'd3        ;//场显示前沿;
    `elsif HDMI1080P//1920*1080分辨率时序参数,60HZ刷新率对应时钟频率148.5MHZ
        localparam  H_SYNC      =  12'd44       ;//行同步;
        localparam  H_BACK      =  12'd148      ;//行显示后沿;
        localparam  H_DISP      =  12'd1920     ;//行有效数据;
        localparam  H_FRONT     =  12'd88       ;//行显示前沿;

        localparam  V_SYNC      =  12'd5        ;//场同步;
        localparam  V_BACK      =  12'd36       ;//场显示后沿;
        localparam  V_DISP      =  12'd1080     ;//场有效数据;
        localparam  V_FRONT     =  12'd4        ;//场显示前沿;
    `else//1024*600分辨率时序参数;
        localparam  H_SYNC      =  12'd20       ;//行同步;
        localparam  H_BACK      =  12'd140      ;//行显示后沿;
        localparam  H_DISP      =  12'd1024     ;//行有效数据;
        localparam  H_FRONT     =  12'd160      ;//行显示前沿;

        localparam  V_SYNC      =  12'd3        ;//场同步;
        localparam  V_BACK      =  12'd20       ;//场显示后沿;
        localparam  V_DISP      =  12'd600      ;//场有效数据;
        localparam  V_FRONT     =  12'd12       ;//场显示前沿;
    `endif

    localparam  SHOW_H_B    =   H_SYNC + H_BACK;//LCD图像行起点;
    localparam  SHOW_V_B    =   V_SYNC + V_BACK;//LCD图像场起点;
    localparam  SHOW_H_E    =   H_SYNC + H_BACK + H_DISP;//LCD图像行结束;
    localparam  SHOW_V_E    =   V_SYNC + V_BACK + V_DISP;//LCD图像场结束;
    localparam  H_TOTAL     =   H_SYNC + H_BACK + H_DISP + H_FRONT ;//行扫描周期;
    localparam  V_TOTAL     =   V_SYNC + V_BACK + V_DISP + V_FRONT;//场扫描周期;
    localparam  H_TOTAL_W   =   clogb2(H_TOTAL - 1);
    localparam  V_TOTAL_W   =   clogb2(V_TOTAL - 1);

    reg       	                video_en    ;
    reg  [H_TOTAL_W - 1 : 0]    cnt_h       ;
    reg  [V_TOTAL_W - 1 : 0]    cnt_v       ;
    
    //自动计算位宽函数
    function integer clogb2(input integer depth);begin
        if(depth == 0)
            clogb2 = 1;
        else if(depth != 0)
            for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)
                depth=depth >> 1;
        end
    endfunction

    //行计数器对像素时钟计数;
    always@(posedge clk)begin
        if(rst)
            cnt_h <= {{H_TOTAL_W}{1'b0}};
        else if(cnt_h >= H_TOTAL - 1)
            cnt_h <= {{H_TOTAL_W}{1'b0}};
        else
            cnt_h <= cnt_h + 1'b1;
    end

    //场计数器对行计数;
    always@(posedge clk)begin
        if(rst)
            cnt_v <= {{V_TOTAL_W}{1'b0}};
        else if(cnt_h == H_TOTAL - 1'b1) begin
            if(cnt_v >= V_TOTAL - 1'b1)
                cnt_v <= {{V_TOTAL_W}{1'b0}};
            else
                cnt_v <= cnt_v + 1'b1;
        end
    end

    //请求像素点颜色数据输入,在产生杭长同步信号前两个时钟向上游产生请求信号;
    always@(posedge clk)begin
        if(rst)//初始值为0;
            data_req <= 1'b0;
        else if((cnt_h >= SHOW_H_B - 4) && (cnt_h < SHOW_H_E - 4) && (cnt_v >= SHOW_V_B - 1) && (cnt_v < SHOW_V_E - 1))
            data_req <= 1'b1;
        else
            data_req <= 1'b0;
    end

    //生产X轴坐标值,与req信号对齐;
    always@(posedge clk)begin
        if(rst)//初始值为0;
            pixel_xpos <= 11'd0;
        else if((cnt_h >= SHOW_H_B - 4) && (cnt_h < SHOW_H_E - 4))
            pixel_xpos <= cnt_h + 4 - SHOW_H_B;
        else 
            pixel_xpos <= 11'd0;
    end

    //生产y轴坐标值,与req信号对齐;
    always@(posedge clk)begin
        if(rst)//初始值为0;
            pixel_ypos <= 11'd0;
        else if((cnt_v >= SHOW_V_B - 1) && (cnt_v < SHOW_V_E - 1))
            pixel_ypos <= cnt_v + 1 - SHOW_V_B;
        else 
            pixel_ypos <= 11'd0;
    end

    //video_hs相对data_req滞后两个时钟周期,video_de和video_rgb要与video_hs对齐;
    //则video_de和video_rgb也要滞后data_req两个时钟;
    //要求像素数据pixel_data在data_req拉高后的下一个时钟输入;
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            video_hs <= 1'b0;
            video_vs <= 1'b0;
            video_en <= 1'b0;
            video_de <= 1'b0;
            video_rgb <= 24'd0;
        end
        else begin
            video_en <= data_req;
            video_de <= video_en;
            video_hs <= (cnt_h >= H_SYNC);//行同步信号赋值;
            video_vs <= (cnt_v >= V_SYNC);//场同步信号赋值;
            video_rgb <= video_en ? pixel_data : 24'd0;//RGB888数据输出;
        end
    end

endmodule

在本次设计中,上游模块输出的像素数据会滞后像素坐标两个时钟(坐标经过2级触发器后输出像素数据),由于生成请求信号又是时序电路,所以需要提前三个时钟周期(即提前三列,即场计数器提前4个时钟周期,计数器从0开始计数)产生像素请求信号和对应坐标。保证所有信号严格对齐传输到下一模块即可。

该模块仿真如下面几张图所示。


图5 整体仿真


图6 开始刷新一行时序


图7 刷新一行图像的中间时序


图8 刷新一行尾部时序

3、video_display模块

video_display模块是video_driver模块的上游模块,需要根据video_driver模块的像素请求信号和坐标数据输出对应的像素数据。对应的端口信号如下表2所示。
表2 video_display模块端口信号

信号名 I/O 位宽 定义
clk I 1 系统时钟
rst I 1 系统复位,高电平有效
pixel_req I 1 像素请求信号,高电平有效
pixel_xpos I 11 像素点横坐标
pixel_ypos I 11 像素点纵坐标
pixel_data O 24 像素数据

本文通过LCD显示图片,通过Image2Lcd 2.9软件获取生成图片数据,将数据存储在rom中,然后通过坐标值读取对应数据显示在显示器上即可。

如图9所示,解压软件安装包,双击Img2Lcd.exe文件即可运行。


图9 Image2Lcd 2.9软件

如图10所示,加载一张图片,输出数据类型选择数组格式,水平扫描,生成300*200分辨率的16位rgb565格式数据。由于本文没有使用DDR存储器,只使用了内部的ROM IP(一个ROM最多存储65536个数据)对数据进行存储,所以数据量不能过大,在显示时,将图片放大三倍即可占满大半个屏幕。


图10 Image2Lcd 2.9软件生成数据

由于ROM IP存储数据的格式与输出数据的格式有点出入,需要把数组的前后括号去掉,利用vscode的替换功能,将文件中的0x删除,将逗号替换为空格,需要将前后两个8位数据合并为16位数据,将回车全部去掉,操作方式如下视频所示。

图片数据处理

利用ROM IP存储上述生成的数据,生成ROM IP的方法比较简单,如下所示,深度等于分辨率,即300*200,数据宽度为16位。


图11 ROM IP位宽深度设置

Port coning页面直接默认即可,此处不对输入、输出信号加寄存器延时,最后配置图12界面即可,首先点击1处,在一个路径下存储一个.coe文件。点击2处为这个文件添加内容(ROM初始化数据),在弹出的界面下,点击3处填写"memory_initialization_radix",在4处填写16,表示存储在.coe文件中是16进制数据。然后就是在5处填写"memory_initialization_vector",6处就是ROM IP存储的数据,即前文视频中利用vscode处理的60000个16进制图像数据,将该文件的数据复制到图6处,然后点击图7生产ROM初始化文件,依次点击8、9处,最后10处表示没有被初始化的内存部分被赋值为16进制的0。


图12 ROM初始化数据生成

ROM存储300200分辨率的像素数据,显示时将图片放大三倍,分辨率变为900 600,那么将图片放到显示器中间,如下图13所示。当下游模块输出的横坐标位于62~962,纵坐标位于84~684之间时,从ROM中读取对应位置的数据进行显示。

图13 LCD显示策略

具体处理方式为,当横坐标位于62~962,纵坐标位于84~684之间时,横坐标减去62,纵坐标减去84生成新的坐标(x,y)来取ROM中的数据。由于需要将图片放大三倍,即显示器连续三行显示一行图像,显示器连续三列显示同一列的像素数据。因此x/3+y/3*200作为ROM地址即可实现图片放大三倍,乘以200时因为存储的一行数据有200个。

注意此模块,输入坐标到对应坐标输出像素数据只延迟两个时钟,与上一个模块要求的延时必须一致。需要将ROM存储的16位像素数据转换为24位像素数据输出给下游模块进行显示,rgb565转rgb888很简单,补零即可,不再赘述。参考代码如下:

module  video_display(
    input                   clk         ,
    input                   rst         ,
    
    input                   pixel_req   ,//请求输入像素数据;
    input        [10 : 0]   pixel_xpos  ,//像素点横坐标
    input        [10 : 0]   pixel_ypos  ,//像素点纵坐标
    output  reg  [23 : 0]   pixel_data   //像素点数据
);
    localparam WHITE  = 24'b11111111_11111111_11111111;  //RGB888 白色
    localparam RED    = 24'b11111111_00001100_00000000;  //RGB888 红色
    localparam GREEN  = 24'b00000000_11111111_00000000;  //RGB888 绿色
    localparam BLUE   = 24'b00000000_00000000_11111111;  //RGB888 蓝色

    reg  [1 : 0]    pixel_req_r;
    always@(posedge clk)begin
        pixel_req_r <= {pixel_req_r[0],pixel_req};
    end

    reg [9 : 0] x,y;//
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            x <= 10'd0;
        end
        else if(pixel_xpos >= 62 && pixel_xpos < 962)begin
            x <= pixel_xpos - 62;
        end
        else begin
            x <= 0;
        end
    end

    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            y <= 10'd0;
        end
        else if(pixel_ypos >= 84 && pixel_ypos < 684)begin
            y <= pixel_ypos - 84;
        end
        else begin
            y <= 0;
        end
    end

    reg [9 : 0] pixel_xpos_r,pixel_ypos_r;//
    always@(posedge clk)begin
        pixel_xpos_r <= pixel_xpos;
        pixel_ypos_r <= pixel_ypos;
    end

    reg [15 : 0] addr;//
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            addr <= 16'd0;
        end
        else begin
            addr <= x/3 + (y/3)*300;
        end
    end

    wire  [15:0]    rom_out;
    video_rom u_video_rom (
        .a      ( addr      ),// input wire [15 : 0] a
        .spo    ( rom_out   ) // output wire [23 : 0] spo
    );

    //根据当前像素点坐标指定当前像素点颜色数据,在屏幕上显示图片;
    always@(posedge clk)begin
        if(rst)
            pixel_data <= WHITE;
        else if(pixel_req_r[1])begin
            if((pixel_xpos_r >= 62) && (pixel_xpos_r < 962) && (pixel_ypos_r >= 84) && (pixel_ypos_r < 684))
                pixel_data <= {rom_out[15:11],3'd0,rom_out[10:5],2'd0,rom_out[4:0],3'd0};
            else 
                pixel_data <= WHITE;
        end
        else begin
            pixel_data <= WHITE;
        end
    end
   
endmodule

对该模块进行仿真,仿真结果如下面几张图所示,pixel_req信号每拉高一次表示请求输入一行数据,同时pixel_xpos和pixel_ypos分别表示屏幕上像素点的横坐标和纵坐标,x和y表示图像显示区域的坐标,图14是一张宏观的仿真图,下游模块请求输入几行数据。


图14 整体仿真


图15 输出显示第一行数据

由图15可知,当下游刷新到84行62列时,开始输出第一行数据,并且连续三列输出的数据时相等的,ROM IP的地址addr信号没经过三个x坐标变化一次,ROM输出的第一数据为16'h29e4,第二个数据为16'h10e2,与ROM IP初始化的前两个数据也能对应上,如图16所示;观察信号可知输出像素数据滞后输入坐标两个时钟周期,满足前文的时序要求,该模块仿真通过。


图16 ROM IP初始化数据

由图17可知,当列地址大于962时,不在从ROM IP中输出像素数据,与前文的显示区域规划一致。


图17 仿真第一行数据结束阶段

4、dvi_transmitter模块

该模块的接口信号如下表3所示:
表3 dvi_transmitter模块端口信号列表

信号名 I/O 位宽 定义
clk I 1 像素时钟信号
Clk_5x I 1 5倍clk频率的时钟信号
rst I 1 复位信号,高电平有效
video_hsync I 1 场同步信号
video_vsync I 1 行同步信号
video_de I 1 像素有效指示信号
video_din I 24 像素输入数据
tmds_clk_p O 1 HDMI差分时钟P端
tmds_clk_n O 1 HDMI差分时钟n端
tmds_data_p O 3 HDMI三路差分数据线P端
tmds_data_n O 3 HDMI三路差分数据线n端
tmds_oen O 1 HDMI接口方向信号,高电平设置为输出

HDMI驱动模块,该模块内部框图如下所示,利用前面文章编写的TMDS算法模块对输入的像素数据video_din、行、场同步信号video_vsync和video_hsync,像素有效指示信号video_de进行编码,输出三路10位并行编码数据tms_out数据。

接下来就是把10位单沿采样的并行数据转换成串行的双沿采样数据,并行转串行采用计数器加时序逻辑实现,而单沿采样转双沿采样使用ODDR原语实现。首先通过拼接和计数器将10位并行数据转换成2路串行数据输入给ODDR原语,为什么转换成两路串行数据与ODDR原语的工作方式有关,可以查看前文对ODDR原语的讲解部分。

图18 HDMI编码接口模块框图

此处ODDR使用SAME_EDGE模式,将时钟上升沿采集的2路串行数据,在时钟的上升沿和下降沿分别输出,生成oddr_out双沿信号。将该信号输入obufds原语,转换成差分信号输出FPGA芯片,通过HDMI接口直接传输。

TMDS连接的时钟通道采用与数据通道相同的并转串逻辑来实现,通过对10位二进制序列10'b11111_00000在10倍像素时钟频率下进行并串转换,得到像素时钟频率下的TMDS参考时钟。

注意并串转换和ODDR原语使用时钟信号的频率是TMDS算法模块时钟的5倍,原因是将10位并行数据转为双沿采样的串行数据。

本文只使用HDMI作为输出接口,所以只需要将tmds_open信号赋值为高电平把HDMI接口设置为输出即可。

该模块为了简化书写,使用了一些for循环模块,如果对for循环使用不是很了解,可以点击查看前文对for循环的讲解和使用。

对应代码比较简单,参考代码如下所示:

module dvi_transmitter(
    input                   clk         ,//系统时钟信号,
    input                   clk_5x      ,//频率为系统时钟5倍的时钟信号;
    input                   rst         ,//系统复位,高电平有效;
    
    input   [23 : 0]        video_din   ,//RGB888视频输入信号;
    input                   video_hsync ,//行同步信号;
    input                   video_vsync ,//场同步信号;
    input                   video_de    ,//像素使能信号;
    
    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 [9 : 0] tms_out    [3 : 0]         ;

    assign tmds_oen = 1'b1;//将双向的HDMI接口设置为输出。
    
    //对三个颜色通道进行编码
    dvi_tmds_encoder u_dvi_tmds_b (
        .clk    (clk            ),//系统时钟信号;
        .rst    (rst            ),//系统复位信号,高电平有效;
        .din    (video_din[7:0] ),//输入待编码数据;
        .c0	    (video_hsync    ),//控制信号C0;
        .c1	    (video_vsync    ),//控制信号c1;
        .de	    (video_de       ),//输入数据有效指示信号;;
        .q_out  (tms_out[0][9:0]) //编码输出数据;
    );

    dvi_tmds_encoder u_dvi_tmds_g (
        .clk    (clk            ),
        .rst    (rst            ),
        .din    (video_din[15:8]),
        .c0     (1'b0           ),
        .c1     (1'b0           ),
        .de     (video_de       ),
        .q_out  (tms_out[1][9:0])
    );

    dvi_tmds_encoder u_dvi_tmds_r (
        .clk    (clk            ),
        .rst    (rst            ),
        .din    (video_din[23:16]),
        .c0	    (1'b0           ),
        .c1	    (1'b0           ),
        .de	    (video_de       ),
        .q_out  (tms_out[2][9:0])
    );

    assign tms_out[3][9 : 0] = 10'b11_1110_0000;//时钟信号编码后的数据为10'b11_1110_0000;

    wire [4 : 0]    tms_out_l    [3 : 0];
    wire [4 : 0]    tms_out_h    [3 : 0];
    wire  [3 : 0]   oddr_out ;

    //将编码数据进行拼接,拼接成IDDR两路输入信号的数据格式。
    generate
        genvar i;
        for(i=0 ; i<4 ; i=i+1)begin : JOINT
            assign tms_out_l[i][4:0] = {tms_out[i][8],tms_out[i][6],tms_out[i][4],tms_out[i][2],tms_out[i][0]};
            assign tms_out_h[i][4:0] = {tms_out[i][9],tms_out[i][7],tms_out[i][5],tms_out[i][3],tms_out[i][1]};
        end
    endgenerate

    reg [2 : 0] cnt;//
    //5进制计数器,用于将5位并行数据转换为串行数据;
    always@(posedge clk_5x)begin
        if(rst)begin//初始值为0;
            cnt <= 3'd0;
        end
        else if(cnt == 3'd4)begin
            cnt <= 3'd0;
        end
        else begin
            cnt <= cnt + 3'd1;
        end
    end

    reg  [3 : 0]    iddr_l      ;
    reg  [3 : 0]    iddr_h      ;
    wire [3 : 0]    obufds_out_p;
    wire [3 : 0]    obufds_out_n;
    generate
        for(i=0 ; i<4 ; i=i+1)begin : ODDR
            //将编码拼接后的5位并行数据转换为串行数据;
            always@(posedge clk_5x)begin
                if(cnt > 3'd0)begin
                    iddr_l[i] <= tms_out_l[i][cnt-1];
                    iddr_h[i] <= tms_out_h[i][cnt-1];
                end
                else begin
                    iddr_l[i] <= tms_out_l[i][4];
                    iddr_h[i] <= tms_out_h[i][4];
                end
            end
            //调用ODDR原语完成单沿转双沿;
            ODDR #(
                .DDR_CLK_EDGE   ("SAME_EDGE"),// "OPPOSITE_EDGE" or "SAME_EDGE" 
                .INIT           (1'b0       ),// Initial value of Q: 1'b0 or 1'b1
                .SRTYPE         ("SYNC"     ) // Set/Reset type: "SYNC" or "ASYNC" 
            ) u_ODDR (
                .Q  (oddr_out[i]),// 1-bit DDR output
                .C  (clk_5x     ),// 1-bit clock input
                .CE (1'b1       ),// 1-bit clock enable input
                .D1 (iddr_l[i]  ),// 1-bit data input (positive edge)
                .D2 (iddr_h[i]  ),// 1-bit data input (negative edge)
                .R  (rst        ),// 1-bit reset
                .S  (1'b0       ) // 1-bit set
            );
            //调用OBUFDS原语,将ODDR输出的双沿信号转换为差分信号;
            OBUFDS #(
                .IOSTANDARD ("TMDS_33"  )//I/O电平标准为TMDS
            )
            u_obufds (
                .I  (oddr_out[i]    ),//ODDR输出的双沿信号;
                .O  (obufds_out_p[i]),
                .OB (obufds_out_n[i]) 
            );
        end
    endgenerate
    
    assign tmds_clk_p = obufds_out_p[3];
    assign tmds_clk_n = obufds_out_n[3];
    assign tmds_data_p = obufds_out_p[2 : 0];
    assign tmds_data_n = obufds_out_n[2 : 0];
    
endmodule

该模块仿真截图如图19所示,经过验证没有问题,涉及差分信号之类的比较多,仿真不好分析,有兴趣的打开工程文件对照波形和代码自行分析。


图19 HDMI驱动模块仿真

5、锁相环模块

本文使用显示器分辨率为1024*768,如果刷新率为60Hz,需要像素时钟65MHz,同时ODDR原语使用时钟为像素时钟5倍,即325MHz。所以需要利用锁相环将开发板输入的100MHz时钟信号转换为65MHz和325MHz的时钟信号给其余模块使用。

锁相环模块设置如下所示:


图20 锁相环模块配置

6、上板测试

顶层模块的参考代码如下所示:

module  top(
    input                   sys_clk     ,
    input                   sys_rst_n   ,
    
    output                  tmds_clk_p  ,// TMDS 时钟通道
    output                  tmds_clk_n  ,
    output                  tmds_oen    ,
    output [2 : 0]          tmds_data_p ,// TMDS 数据通道
    output [2 : 0]          tmds_data_n
);
    wire                    clk         ;
    wire                    rst         ;
    wire                    clk_5x      ;
    wire                    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   ;

    assign rst = ~sys_rst_n;

    //例化锁相环IP;
    clk_wiz_0  U_clk_wiz_0(
        .clk_in1    ( sys_clk   ),//输入系统时钟;
        .clk_out1   ( clk       ),//像素时钟;
        .clk_out2   ( clk_5x    ),//5倍像素时钟;
        .resetn     ( sys_rst_n ),//系统复位,低电平有效;
        .locked     ( locked    )
    );

    //例化视频显示驱动模块
    video_driver  u_video_driver(
        .clk            ( clk           ),//系统时钟信号;
        .rst            ( rst           ),//系统复位信号,高电平有效;
        .video_hs       ( video_hs      ),//行同步信号;
        .video_vs       ( video_vs      ),//场同步信号;
        .video_de       ( video_de      ),//数据使能;
        .video_rgb      ( video_rgb     ),//RGB888颜色数据;
        .data_req		( data_req      ),//像素申请信号;
        .pixel_xpos     ( pixel_xpos_w  ),//像素点数据;
        .pixel_ypos     ( pixel_ypos_w  ),//像素点横坐标;
        .pixel_data     ( pixel_data_w  ) //像素点纵坐标;
    );

    //例化视频显示模块
    video_display  u_video_display(
        .clk            ( clk           ),//系统时钟信号;
        .rst            ( rst           ),//系统复位信号,高电平有效;
        .pixel_req      ( data_req      ),//请求输入像素数据;
        .pixel_xpos     ( pixel_xpos_w  ),//像素点横坐标
        .pixel_ypos     ( pixel_ypos_w  ),//像素点纵坐标
        .pixel_data     ( pixel_data_w  ) //像素点数据
    );

    //例化HDMI驱动模块
    dvi_transmitter u_dvi_transmitter(
        .clk           ( clk        ),//系统时钟信号,
        .clk_5x        ( clk_5x     ),//频率为系统时钟5倍的时钟信号;
        .rst           ( rst        ),//系统复位,高电平有效;
        .video_din     ( video_rgb  ),//RGB888视频输入信号;
        .video_hsync   ( video_hs   ),//行同步信号;
        .video_vsync   ( video_vs   ),//场同步信号;
        .video_de      ( video_de   ),//像素使能信号;
        .tmds_clk_p    ( tmds_clk_p ),//TMDS时钟通道;
        .tmds_clk_n    ( tmds_clk_n ),
        .tmds_data_p   ( tmds_data_p),//TMDS数据通道;
        .tmds_data_n   ( tmds_data_n), 
        .tmds_oen      ( tmds_oen   ) //TMDS输出使能;
    );

endmodule 

对应的TestBench比较简单,参考代码如下所示:

`timescale 1 ns/1 ns
module test();
    parameter	CYCLE		=   10          ;//系统时钟周期,单位ns,默认10ns;
    parameter	RST_TIME	=   10          ;//系统复位持续时间,默认10个系统时钟周期;
    parameter	STOP_TIME	=   1000        ;//仿真运行时间,复位完成后运行1000个系统时钟后停止;

    reg			                sys_clk     ;//系统时钟,默认100MHz;
    reg			                sys_rst_n   ;//系统复位,默认低电平有效;
    
    wire                        tmds_clk_p  ;
    wire                        tmds_clk_n  ;
    wire                        tmds_oen    ;
    wire  [2 : 0]               tmds_data_p ;
    wire  [2 : 0]               tmds_data_n ;

    top  u_top (
        .sys_clk        ( sys_clk       ),
        .sys_rst_n      ( sys_rst_n     ),
        .tmds_clk_p     ( tmds_clk_p    ),
        .tmds_clk_n     ( tmds_clk_n    ),
        .tmds_oen       ( tmds_oen      ),
        .tmds_data_p    ( tmds_data_p   ),
        .tmds_data_n    ( tmds_data_n   )
    );

    //生成周期为CYCLE数值的系统时钟;
    initial begin
        sys_clk = 0;
        forever #(CYCLE/2) sys_clk = ~sys_clk;
    end

    //生成复位信号;
    initial begin
        sys_rst_n = 1;
        #2;
        sys_rst_n = 0;//开始时复位10个时钟;
        #(RST_TIME*CYCLE);
        sys_rst_n = 1;
        #(STOP_TIME*CYCLE);
        $stop;//停止仿真;
    end

endmodule

上板测试结果:

HDMI显示结果

由于ROM大小限制,生产图片的时候像素的采样率第,加上图片被放大三倍,导致显示300*200像素的图片相比电脑文件夹里图片模糊,是正常现象,后续采用DDR将整张图片存储即可清晰显示。

整个工程就完成了,通过HDMI接口主要是学习HDMI接口的硬件原理,TMDS算法实现,以及xilinx的一些原语,通过阅读手册,至少知道如何将单端信号和差分信号相互转换,高速的并行串行数据如何转换,怎么把触发器放在ILOGIC和OLOGIC中,如何对输入输出的信号进行延时的设计。

其实该接口还可以进行简化,本文使用代码完成并行转串行,前文还学过一个原语,可以直接将并行单沿采集的数据直接转换为串行双沿采集的数据,代码比ODDR更简单,也更可靠,下一节将使用该原语实现HDMI接口。

本文值讲述了HDMI的编码设计,别忘了开发板上的HDMI接口可是双向的,既然Verilog HDL能够实现HDMI编码,那也肯定可以实现HDMI解码的,到时候就会使用差分转单端,双沿转单沿,串行转并行。所以前文所讲解到的原语几乎可以全部使用在HDMI接口的设计中。但是此开发板上只有一个HDMI接口,解码后的数据无法视觉验证正确性,所以先放一放,在采购一个HDMI模块后进行HDMI解码。

需要本文工程在后台回复"基于FPGA的HDMI接口设计"(不包括引号),选择ODDR实现的文件即可。

相关推荐
IM_DALLA5 小时前
【Verilog学习日常】—牛客网刷题—Verilog快速入门—VL21
学习·fpga开发
皇华ameya9 小时前
AMEYA360:村田电子更适合薄型设计应用场景的3.3V输入、12A输出的DCDC转换IC
fpga开发
千穹凌帝12 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
一口一口吃成大V18 小时前
FPGA随记——FPGA时序优化小经验
fpga开发
贾saisai19 小时前
Xilinx系FPGA学习笔记(九)DDR3学习
笔记·学习·fpga开发
redcocal1 天前
地平线秋招
python·嵌入式硬件·算法·fpga开发·求职招聘
思尔芯S2C2 天前
高密原型验证系统解决方案(下篇)
fpga开发·soc设计·debugging·fpga原型验证·prototyping·深度调试·多fpga 调试
坚持每天写程序2 天前
xilinx vivado PULLMODE 设置思路
fpga开发
redcocal2 天前
地平线内推码 kbrfck
c++·嵌入式硬件·mcu·算法·fpga开发·求职招聘
邹莉斯3 天前
FPGA基本结构和简单原理
fpga开发·硬件工程