HDMI协议实现彩条静态显示&方块移动

文章目录

  • 前言
  • 一、硬件原理
    • 1、硬件设计原理图
    • 2、引脚图
  • 二、系统设计
    • 1、系统模块框图
    • 2、RTL视图
    • 3、RGB2DVI 模块框图
    • 4、ALTDDIO_OUT IP 核的信号框图
  • 三、HDMI的行&场时序
    • 1、时序图
    • 2、常见显示器时序参数
  • 四、源码
    • 1、video_driver(显示驱动模块)
    • 2、serializer_10_to_1(并转串模块)
    • 3、dvi_encoder(编码模块)
    • 4、video_display(视频显示模块)
  • 五、效果
  • 六、总结
  • 七、参考资料&完整工程


前言

环境:
1、Quartus18.0
2、vscode
3、板子型号:原子哥开拓者2(EP4CE10F17C8)
4、显示屏
要求:
是驱动开拓者开发板上的 HDMI 接口,在显示器上显示彩条图案。HDMI 接口输出的分辨率为 1280*720,刷新速率为 60hz。


一、硬件原理

1、硬件设计原理图

HDMI协议实现彩条静态显示&方块移动_第1张图片
HDMI协议实现彩条静态显示&方块移动_第2张图片

  • 分析:

1、在开拓者2的HDMI中,我们只是支持输出,把输入限制死了。
2、HDMI_HPD 指的是热拔插检测(Hot Plug Detect): 当视频设备与接收设备通过 HDMI 连接时,接收设备将 HPD 置为高电平,通知发送设备。当发送设备检测到 HPD 为低电平时,表明断开连接。加了一个抑制二极管做电压敏感保护。
3、SCL、SDA使用CMOS管用于电平的转换作用: 用于 HDMI 发送端和接收端之间交换一些配置信息,通过 I2C 协议通信。

2、引脚图

1、4、7和3、6、9组成了三对差分信号,用于发送颜色数据。

  • CEC: 工业上常用的一个保留的信号引脚。
  • SCL和SDA: 组成了一组I2C的信号线
  • DDC/CED: 用于告知发送端,我们接收端具体可以接收什么样的数据。
  • 插拔检测: 连接好过后接收端会发送一个拉高的信号,发送端在接收到信号过后会向接收端开始发送数据

二、系统设计

1、系统模块框图

HDMI协议实现彩条静态显示&方块移动_第3张图片

2、RTL视图

HDMI协议实现彩条静态显示&方块移动_第4张图片

3、RGB2DVI 模块框图

HDMI协议实现彩条静态显示&方块移动_第5张图片

这里实现了RGB转DVI模块,主要想提一下这里的ddio实现的并转串。这里我们需要一下子将10bit数字转成串行的,这就需要10倍的像素时钟。这里我们只用了一个 5 倍的时钟频率,这是因为 ALTDDIO_OUT IP核可以实现 DDR 的功能,即它在五倍时钟频率的基础上又实现了双倍数据速率。

4、ALTDDIO_OUT IP 核的信号框图

HDMI协议实现彩条静态显示&方块移动_第6张图片

这里我们选择的数据为为1比特,但是,其有上升沿以及下降沿都进行相应的发送处理。

  • 端口描述:

datain_h: 在 outclock 时钟上升沿输入的数据,输入数据的位宽为WIDTH;
datain_l: 在 outclock 时钟下降沿输入的数据,输入数据的位宽为 WIDTH;
outclock: 用于寄存数据输出的时钟信号,在 outclock 时钟信号的每个电平输出 DDR 数据到 dataout 端口;
dataout: DDR 输出数据端口,位宽为 WIDTH。dataout 端口应该直接提供顶层设计的输出引脚。

  • 下面是其输出时序图:
    HDMI协议实现彩条静态显示&方块移动_第7张图片

观察发现,在一个时钟·周期内,其输出了两个比特位,那么我们只需要5倍的时钟速率即可。

三、HDMI的行&场时序

1、时序图

HDMI协议实现彩条静态显示&方块移动_第8张图片

在行&场同步信号有效信号过来时我们的DE(像素有效信号)还不能立刻拉高,我们需要等到显示后沿结束过后才拉高,开始输出有效数据。然后DE拉低进入显示前沿,待显示前沿过后我们的行场同步信号又开始下一行或帧的传输。

2、常见显示器时序参数

HDMI协议实现彩条静态显示&方块移动_第9张图片

这里没有完整的,我们实验用的是1024x720。其中的e等于a、b、c、d的总和。同样s等于前四个相加。这个对我们后面写代码是必要的,完整文件在结尾。

四、源码

这里文件比较多,我就放三个主要的就行,结尾我会放完整工程。

1、video_driver(显示驱动模块)

这里主要参照上面的行场时序图进行编程理解。

//在video_driver 模块的驱动下按照工业标准的VGA 显示时序
//输出视频信号、行场同步信号以及视频有效信号。这些信号作为RGB2DVI模块的输入
module video_driver (
    input               pixel_clk             ,
    input               sys_rst_n             ,
    input [23:0]        pixel_data            ,//像素点数据

    //RGB接口
    output              video_hs              ,//行同步信号
    output              video_vs              ,//场同步信号
    output              video_de              ,//数据使能
    output [23:0]       video_rgb             ,//RGB888颜色数据

    output [10:0]       pixel_xpos            ,//像素点横坐标
    output [10:0]       pixel_ypos             //像素点纵坐标
);

//1280*720 分辨率时序参数
parameter H_SYNC = 11'd40,      //行同步
          H_BACK = 11'd220,     //行显示后沿
          H_DISP = 11'd1280,    //行有效数据
          H_FRONT = 11'd110,    //行显示前沿
          H_TOTAL = 11'd1650;   //行扫描周期

parameter V_SYNC = 11'd5,      //场同步
          V_BACK = 11'd20,     //场显示后沿
          V_DISP = 11'd720,    //场有效数据
          V_FRONT = 11'd5,    //场显示前沿
          V_TOTAL = 11'd750;   //场扫描周期

reg [10:0] cnt_h;
reg [10:0] cnt_v;

wire video_en;
wire data_req;

assign video_de = video_en;

assign video_hs = (cnt_h < H_SYNC ) ? 1'b0 : 1'b1;
assign video_vs = (cnt_v < V_SYNC ) ? 1'b0 : 1'b1;

//有效信号输出时序
assign video_en = (((cnt_h >= H_SYNC + H_BACK) && (cnt_h < H_SYNC + H_BACK + H_DISP)) 
                && ((cnt_v >= V_SYNC + V_BACK) && (cnt_v < V_SYNC + V_BACK + V_DISP)))
                ? 1'b1 : 1'b0;
//RGB888数据输出
assign video_rgb = video_en ? pixel_data : 24'd0;

//请求像素点颜色数据输入
assign data_req = (((cnt_h >= H_SYNC + H_BACK - 1'b1) && (cnt_h < H_SYNC + H_BACK + H_DISP - 1'b1)) 
                && ((cnt_v >= V_SYNC + V_BACK) && (cnt_v < V_SYNC + V_BACK + V_DISP)))
                ? 1'b1 : 1'b0;

//像素点坐标
assign pixel_xpos = data_req ? (cnt_h - (H_SYNC + H_BACK - 1'b1)) : 11'd0;
assign pixel_ypos = data_req ? (cnt_v - (V_SYNC + V_BACK - 1'b1)) : 11'd0;

//行计数器对像素时钟计数
//使用同步复位信号:抗干扰能力强、消除毛刺,同步消耗资源较多、
//需要保持一个时钟周期以上才可以复位、可能带来时钟偏移等问题

//异步复位信号则相反消耗资源少、抗干扰弱、有亚稳态问题

//行计数器对像素时钟进行计数
always @(posedge pixel_clk) begin
    if(!sys_rst_n)
        cnt_h <= 11'd0;
    else if(cnt_h == H_TOTAL - 1'b1)
        cnt_h <= 11'd0;
    else 
        cnt_h <= cnt_h + 1'b1;
end

//场计数器对行计数,一个场内的行计完切换下一帧
always @(posedge pixel_clk) begin
    if(!sys_rst_n)
        cnt_v <= 11'd0;
    else if(cnt_h == H_TOTAL - 1'b1) begin
        if(cnt_v < V_TOTAL - 1'b1)
            cnt_v <= cnt_v + 1'b1;
        else
            cnt_v <= 11'd0;
    end
end

endmodule

2、serializer_10_to_1(并转串模块)

这里通过例化一个PLL实现一个五倍的像素时钟和一个ALTDDIO_OUT IP核,用于并转串的操作。

module serializer_10_to_1(
    input               serial_clk_5x,//输入串行数据时钟
    input    [9:0]      paralell_data,//输入并行数据

    output              serial_data_p,//输出串行差分数据p
    output              serial_data_n //输出串行差分数据N
);

reg [2:0] bit_cnt = 0;
reg [4:0] datain_rise_shift = 0;
reg [4:0] datain_fall_shift = 0;

wire [4:0] datain_rise;
wire [4:0] datain_fall;

//拆分数据
assign datain_rise = {paralell_data[8],paralell_data[6],paralell_data[4],
                      paralell_data[2],paralell_data[0]};

assign datain_fall = {paralell_data[9],paralell_data[7],paralell_data[5],
                      paralell_data[3],paralell_data[1]};

//位计数器
always @(posedge serial_clk_5x) begin
    if(bit_cnt == 3'd4)
        bit_cnt <= 1'b0;
    else
        bit_cnt <= bit_cnt + 1'b1;
end

//移位寄存器,用于并行数据的每一位
always @(posedge serial_clk_5x) begin
    if(bit_cnt == 3'd4)begin
        datain_rise_shift <= datain_rise;
        datain_fall_shift <= datain_fall;
    end
    else begin
        datain_rise_shift <= datain_rise_shift[4:1];
        datain_fall_shift <= datain_fall_shift[4:1];
    end
end

//例化DDIO_OUT IP核,因为我们我们是差分传输,实际上是两条线,需要例化两个IP
ddio_out ddio_out_inst_1(
    .datain_h    (datain_rise_shift[0]),
    .datain_l    (datain_fall_shift[0]),
    .outclock    (serial_clk_5x),
    .dataout     (serial_data_p)
);
ddio_out ddio_out_inst_2(
    .datain_h    (~datain_rise_shift[0]),
    .datain_l    (~datain_fall_shift[0]),
    .outclock    (serial_clk_5x),
    .dataout     (serial_data_n)
);
endmodule

3、dvi_encoder(编码模块)

这里参照那张编码流程图更容易理解。

/***********
下面会用到一个#1,在1ns过后才开始进行赋值
作用:该延迟有效地实现了 1ns clk-to-q 或 rst_n-to-q 延迟,使用波形查看器查看时可以轻松解释。
比如:波形查看器中的小延迟还可以很容易地看到时序逻辑输出的值在时钟边沿之前是什么,
通过将波形查看器光标放在时钟边沿本身,大多数波形查看工具将显示相应的靠近波形显示左侧的信号名称旁边的二进制、十进制或十六进制值。
***********/
`timescale 1ps/1ps

module dvi_encoder(
    input               clkin   ,//像素时钟
    input               rst_n   ,//异步复位同步释放的复位信号、高有效
    input   [7:0]       din     ,//8位数据
    input               c0      ,//控制信号1
    input               c1      ,//控制信号2
    input               de      ,//发送时序

    output reg [9:0]    dout    //编码后的数据
);

reg [3:0] n1d;//1的个数
reg [7:0] din_q;//

//计算1的个数
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

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;

//计算q_m的1的个数用于下一个decision条件
reg [3:0] n1q_m, n0q_m;

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,
          CTRLTOKEN1 = 10'b0010101011,
          CTRLTOKEN2 = 10'b0101010100,
          CTRLTOKEN3 = 10'b1010101011;

reg [4:0] cnt ;
wire decision2,decision3;

assign decision2 = (cnt == 5'h0) || (n0q_m == n1q_m);
assign decision3 = ((~cnt[4]) && (n1q_m > n0q_m)) || ((cnt[4]) && (n1q_m < n0q_m));

reg de_q, de_reg;
reg c0_q,c1_q;
reg c0_reg,c1_reg;
reg [8:0] q_m_reg;
//应该是延后一个时钟周期用于与前面寄存din_q同步
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;
    c0_reg <=#1 c1_q;

    q_m_reg <=#1 q_m;
end

always @(posedge clkin or posedge rst_n) begin
    if(rst_n)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

4、video_display(视频显示模块)

//这个模块是用于生成显示图案的
//这里我们只需要实现彩条的显示,
//其实就是通过x、y的坐标对不同范围的频幕给不同的颜色即可
module video_display(
    input                   pixel_clk       ,//75MHZ
    input                   sys_rst_n       ,
    input [10:0]            pixel_xpos      ,//像素点横坐标
    input [10:0]            pixel_ypos      ,//像素点纵坐标

    output reg [23:0]       pixel_data//像素点数据,传回驱动模块用于在有效数据发送期间进行发送
    
);
/**************
这里的方块动态显示模块实际上就是依据左上角的一个坐标确定整个方块的位置
并给小方块颜色,同样根据左上角坐标点的位置判定是否到达边界。
**************/ 

parameter H_DISP = 11'd1280;//分辨率--行
parameter V_DISP = 11'd720;//分辨率--列

localparam SIDE_W  = 11'd40,//屏幕边框
           BLOCK_W = 11'd40,//方块宽度
           BLUE    = 24'b00000000_00000000_11111111,  //屏幕颜色,蓝色
           WHITE   = 24'b11111111_11111111_11111111,  //背景颜色,白色
           BLACK   = 24'b00000000_00000000_00000000;  //方块颜色, 黑色
//左上角方块初始位置
reg [10:0] block_x = SIDE_W;
reg [10:0] block_y = SIDE_W;

reg [21:0] div_cnt;//分频计数器
reg h_direct;//指示水平移动方向 1:右移,0:左移
reg v_direct;//指示垂直移动方向 1:向下,0:向上

wire move_en;//方块移动使能,100HZ

assign move_en = (div_cnt == 22'd742500) ? 1'b1 : 1'b0;

//计数实现时钟分频
always @(posedge pixel_clk) begin
    if(!sys_rst_n)begin
        div_cnt <= 22'd0;
    end
    else begin
        if(div_cnt < 22'd742500)
            div_cnt <= div_cnt + 1'b1;
        else
            div_cnt <= 22'd0;
    end
end

//判断方块移动到边界,改变移动方向
always @(posedge pixel_clk) begin
    if(!sys_rst_n) begin
        h_direct <= 1'b1;               //初始向右移动
        v_direct <= 1'b1;               //初始竖直向下
    end
    else begin
        //判断水平位置
        if(block_x == SIDE_W - 1'b1) //到达左边界,水平向右
            h_direct <= 1'b1;
        else if(block_x == H_DISP - SIDE_W - BLOCK_W)
            h_direct <= 1'b0;
        else
            h_direct <= h_direct;
        //判断垂直位置
        if(block_y == SIDE_W - 1'b1)
            v_direct <= 1'b1;
        else if(block_y == V_DISP - SIDE_W - BLOCK_W)
            v_direct <= 1'b0;
        else
            v_direct <= v_direct;
    end
end

//根据方块移动方向,改变坐标
always @(posedge pixel_clk) begin
    if(!sys_rst_n)begin
        block_x <= SIDE_W;           //初始化方块坐标
        block_y <= SIDE_W;
    end
    else if(move_en) begin//10ms改变坐标,实现动态效果
        if(h_direct)
           block_x <= block_x + 1'b1;//向右
        else
            block_x <= block_x - 1'b1;

        if(v_direct)
            block_y <= block_y + 1'b1;//向下
        else
            block_y <= block_y - 1'b1;
    end
    else begin
        block_x <= block_x;
        block_y <= block_y;
    end
end

//不同区域绘制不同颜色

always @(posedge pixel_clk) begin
    if(!sys_rst_n)
        pixel_data <= BLACK;
    else begin
         if(  (pixel_xpos < SIDE_W) || (pixel_xpos >= H_DISP - SIDE_W)
           || (pixel_ypos < SIDE_W) || (pixel_ypos >= V_DISP - SIDE_W))
             pixel_data <= BLUE;                 //绘制屏幕边框为蓝色
         else
         if(  (pixel_xpos >= block_x) && (pixel_xpos < block_x + BLOCK_W)
           && (pixel_ypos >= block_y) && (pixel_ypos < block_y + BLOCK_W))
             pixel_data <= BLACK;                //绘制方块为黑色
         else
             pixel_data <= WHITE;                //绘制背景为白色
    end
end
endmodule

五、效果

由于博主离校在外,没有支持HDMI的显示屏,无法对效果进行演示,在经过多方寻找有心无力,还冲动买了一块,后面考虑到用不了多少次就退款了。等我返校有时间了再给大伙试试效果,上面的代码就当理解学习先吧,有条件的可以自己试试效果。


六、总结

说实话这个相对前面的学习好像又上了一个坡,还是有一定的理解难度的。这个项目我拢共看了四便,手敲了一遍代码过后理解更加的深刻,之前只用过VGA做过屏幕的显示,这次又是不一样的体验。这个项目里涉及到有PLL、DDIO、HDMI协议等,我觉得里面我觉得比较巧妙的就是并转串那里,真的太棒了。可惜的就是没有能够进一步去了解到DDIO这个IP,这是我第一次使用,仅仅是配置了下。下一步准备看看如何显示动态的画面。

七、参考资料&完整工程

1、以上资料均来自正点原子的教学视频或开拓者2开发教程:原子官方
2、完整的显示器时序参数文件:显示器时序参数文件 提取码:hzjj
2、源码:https://github.com/no1jiangjiang/HDMI

你可能感兴趣的:(FPGA,fpga开发)