跳转至

VGA 通信协议

VGA 是一种视频信号传输方式,包括红色、绿色、蓝色的模拟信号以及位置同步信号(水平和垂直信号)。如果我们的 FPGA 能够按照 VGA 协议产生这些信号,那么我们就可以通过 VGA 显示器显示我们想要的图像了。

基于此,我们可以做出许多更加有意思的东西,比如各类小游戏等。下面是一些往届的游戏作品展示:

怎么样?心动了吗?心动不如行动,就让我们立刻开始吧!


★ 3.1.1 VGA 显示原理

VGA 显示器通过对屏幕进行逐行扫描来显示图像。电子束从屏幕的左上角开始,沿着从左到右、从上到下的方向逐行扫描屏幕。

在每一行扫描过程中,电子束会被激活以显示相应像素点的颜色(HEN 段)。每扫描完一行,电子束回到屏幕的左边下一行的起始位置,在此期间,电子束被消隐(HSW、HBP、HFP 段),其中,在 HSW 段还会使用行同步信号进行行同步;扫描完所有的行,即形成一帧(VEN 段),接着电子束回到屏幕左上角,在此期间消隐电子束(VSW、VBP、VFP 段),并在 VSW 段用场同步信号进行场同步。

Image title

VGA 时序信号图

下面是各定时显示参数的具体含义:

  • HBP (horizontal back porch):表示从水平同步信号结束开始到一行的有效数据开始之间的 pclk 个数,对应驱动中的 left_margin;
  • HEN:水平显示有效区域,对应水平像素分辨率;
  • HFP (horizontal front porch):表示一行的有效数据结束到下一个水平同步信号开始之间的 pclk 个数,对应驱动中的 right_margin;
  • HSW (horizontal sync width):表示水平同步信号的宽度,以 pclk 为单位计算,对应驱动中的 hsync_len;
  • VBP (vertical back porch):表示在一帧图像开始时,垂直同步信号以后的无效的行数,对应驱动中的 upper_margin;
  • VEN:垂直显示有效区域,对应垂直像素分辨率;
  • VFB (vertical front porch):表示在一帧图像结束后,垂直同步信号以前的无效的行数,对应驱动中的 lower_margin;
  • VSW (vertical sync width):表示垂直同步脉冲的宽度,以行数为单位计算,对应驱动中的 vsync_len。

pclk 是一个特殊的时钟信号,我们将在后文进行介绍。

VGA 显示屏规格对应的各定时参数的值如下表:

Image title

定时参数表

学校的显示屏是 800x600@72Hz 的规格,根据以上表格,可以得到各定时参数的值如下:

像素频率(MHZ) HSW BP HEN HFP VSW VBP VEN VFP
50 120 64 800 56 6 23 600 37

对应的时序图如下:

Image title

时序信号图

3.1.2 VGA 显示模块

Image title

VGA 显示模块逻辑结构
  • VRAM:Video RAM,视频存储器(也称画布)

  • DU:Display Unit,显示单元

    • DDP:Display Data Processing,显示数据处理

    • DST:Display Scan Timing,显示扫描定时

3.1.2.1 画布 VRAM

画布用于存储绘画信息,用双端口 BRAM 实现。由于 FPGA 的容量限制,我们这里使用的存储容量为 32K\(\times\)12。

Tips

这个数据是怎么来的呢?在显示时,屏幕上每 \(4\times4=16\) 个实际像素点缩聚成一个点,用一个存储单元存储,而屏幕的分辨率为 \(800\times600\),因此我们存储的分辨率为 \((800/4)\times(600/4)=200\times150\)。对于每个存储单元(也就是屏幕上的 \(4\times4\) 正方形像素),其需要存储的色彩信息共需 12 位,其中红、绿、蓝三色各 4 位。

因此,需要的存储空间总数应当不少于 \(200\times150\approx29K\),每个存储单元的大小为 12 位。

Image title

VRAM

你可以用 IP 核生成 BRAM,其大小可以根据需要及 FPGA 的容量自行调整。

Image title

VRAM_addr

3.1.2.2 显示扫描定时 DST

DST 按标准的显示规格(800x600),产生刷新显示器的定时信号。如前所述,只有有了这些信号,才能让 VGA 有所显示。

Image title

DST

各输入输出接口的含义如下:

  • hs, vs:行同步,场同步。传递给 VGA 显示器用于同步。
  • hen, ven:水平显示有效,垂直显示有效。传递给 DDP 用于产生坐标。
  • pclk:像素 (pixel) 时钟。注意,它不是普通的时钟 clk,由显示屏规格决定。对于 800x600@72Hz 的规格,应当选用 50MHz。你应当使用时钟 IP 核 (Clocking Wizard)进行定制。

作为示例,假设参数如下表所示:

SW 同步信号宽度 BP 同步信号结束到有效数据的间隔 EN 显示有效区域 FP 有效数据结束到下一同步信号的间隔 单位
HS 1 2 4 3 像素
VS 1 1 2 1

则各定时信号如图:

Image title

定时信号示例

下面是相关的代码框架:

DST.v
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//任意数循环计数
module CntS #(
    parameter               WIDTH               = 16,
    parameter               RST_VLU             = 0
)(
    input                   [ 0 : 0]            clk,
    input                   [ 0 : 0]            rstn,   //复位使能
    input                   [WIDTH-1:0]         d,      //置数
    input                   [ 0 : 0]            ce,

    output      reg         [WIDTH-1:0]         q
);
always @(posedge clk) begin
    if (!rstn)
        q <= RST_VLU;
    else if (ce) begin
        if (q == 0)    
            q <= d;
        else
            q <= q - 1;
    end
    else
        q <= q;
end
endmodule

module DST (
    input                   [ 0 : 0]            rstn,
    input                   [ 0 : 0]            pclk,

    output      reg         [ 0 : 0]            hen,
    output      reg         [ 0 : 0]            ven,
    output      reg         [ 0 : 0]            hs,
    output      reg         [ 0 : 0]            vs
);

localparam HSW_t    = 119;
localparam HBP_t    = 63;
localparam HEN_t    = 799;
localparam HFP_t    = 55;

localparam VSW_t    = //TODO
localparam VBP_t    = //TODO
localparam VEN_t    = //TODO
localparam VFP_t    = //TODO

localparam SW       = 2'b00;
localparam BP       = 2'b01;
localparam EN       = 2'b10;
localparam FP       = 2'b11;

reg     [ 0 : 0]    ce_v;

reg     [ 1 : 0]    h_state;
reg     [ 1 : 0]    v_state;

reg     [15 : 0]    d_h;
reg     [15 : 0]    d_v;

wire    [15 : 0]    q_h;
wire    [15 : 0]    q_v;

CntS #(16,HSW_t) hcnt(
    .clk        (pclk),
    .rstn       (rstn),
    .d          (d_h),
    .ce         (1'b1),

    .q          (q_h)
);

CntS #(16,VSW_t) vcnt(
    //TODO
);

always @(*) begin
    case (h_state)
        SW: begin
            d_h = HBP_t;  hs = 1; hen = 0;
        end
        BP: begin
            d_h = HEN_t;  hs = 0; hen = 0;
        end
        EN: begin
            d_h = HFP_t;  hs = 0; hen = 1;
        end
        FP: begin
            d_h = HSW_t;  hs = 0; hen = 0;
        end
    endcase
    case (v_state)
        // TODO
    endcase
end

always @(posedge pclk) begin
    if (!rstn) begin
        h_state <= SW; v_state <= SW; ce_v <= 1'b0;
    end
    else begin
        if(q_h == 0) begin
            h_state <= h_state + 2'b01;
            if (h_state == FP) begin
                ce_v <= 0;
                if (q_v == 0)
                    v_state <= v_state + 2'b01;
            end
            else
                ce_v <= 0;
        end
        else if (q_h == 1) begin
            if(h_state == FP)
                ce_v <= 1;
            else
                ce_v <= 0;
        end
        else ce_v <= 0;
    end
end
endmodule

至此,你已经可以点亮你的屏幕了,如果你实现得正确,你就可以看见白色的屏幕。

题目 3-1:如果我不曾见过光明

请根据以上内容,按要求完成本项练习

注意

在消隐区,rgb应当为12'h000(黑色),否则,显示屏将不能正常显示。

3.1.2.3 显示数据处理 DDP

DDP 将画布与显示屏适配,从而产生色彩信息。它是让我们的显示屏正确显示色彩的关键。

Image title

DDP

各输入输出接口的含义如下:

  • hen, ven:水平显示有效,垂直显示有效。由 DST 产生,根据这两个信号,我们可以得到当前的像素坐标。

  • raddr: 当前的像素在画布 VRAM 中的查询地址。

  • rdata:上一个像素对应的rgb数据。由查询画布VRAM得到。

DDP.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
module DDP#(
        parameter DW = 15,
        parameter H_LEN = 200,
        parameter V_LEN = 150
    )(
    input               hen,
    input               ven,
    input               rstn,
    input               pclk,
    input  [11:0]       rdata,

    output reg [11:0]   rgb,
    output reg [DW-1:0] raddr
    );

    reg [1:0] sx;       //放大四倍
    reg [1:0] sy;
    reg [1:0] nsx;
    reg [1:0] nsy;


    always @(*) begin
        sx=nsx;
        sy=nsy;
    end

    wire p;

    PS #(1) ps(                     //取ven下降沿
        .s      (~(hen&ven)),
        .clk    (pclk),
        .p      (p)
    );

    always @(posedge pclk) begin           //可能慢一个周期,改hen,ven即可
        if(!rstn) begin
            nsx<=0; nsy<=3;
            rgb<=0;
            raddr<=0;
        end
        else if(hen&&ven) begin
            rgb<=rdata;
            if(sx==2'b11) begin
                raddr<=raddr+1;
            end
            nsx<=sx+1;
        end                                      //无效区域
        else if(p) begin                        //ven下降沿
            rgb<=0;
            if(sy!=2'b11) begin
                raddr<=raddr-H_LEN;
            end
            else if(raddr==H_LEN*V_LEN) begin
                raddr<=0;
            end
            nsy<=sy+1;
        end
        else rgb<=0;
    end
endmodule
注意

代码中的取边沿模块 PS 你应当自行实现。

此外,你仍应该注意,在消隐区,rgb应当为12'h000(黑色)。否则,显示屏将不能正常显示。(这一部分在DDP中助教已经为你们实现了。)

3.1.3 图片转coe文件

作为 VGA 通信协议的一个简单应用,我们可以通过 VGA 显示器显示图片。我们需要将图片的 rgb 信息以 coe 文件的格式存入 ROM 中。

这里我们提供了一个 python 脚本,可以将图片转换为 coe 文件:

TransToCoe.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# Auther: Xhy
# 将图片自动转换为指定大小的.coe文件
# 最终的文件默认200*150大小,请尽量使用200k*150*k大小的图片
# 图片格式任意,路径可自行修改,默认为"test.jpeg"
# 输出image_thumb.jpg为缩略图,并输出result.coe

from PIL import Image

img_raw = Image.open("./test.jpg")

#预处理
# 获取图片尺寸的大小__预期(600,400)
print (img_raw.size)
# 获取图片的格式 png
print (img_raw.format)
# 获取图片的图像类型 RGBA
print (img_raw.mode)
# 生成缩略图
img_raw.thumbnail((200, 150))
# 把图片强制转成RGB
img = img_raw.convert("RGB")
# 把图片调整为16色
img_w=img.size[0]
img_h=img.size[1]
for i in range(0,img_w):
 for j in range(0,img_h):
  data=img.getpixel((i,j))
  re=(16*int(data[0]/16),16*int(data[1]/16),16*int(data[2]/16))
  img.putpixel((i,j),re)
# 保存图片
img.save('./image_thumb.jpg', 'JPEG')
# 显示图片
# img.show()

# 转换为.coe文件
width=200
height=150
file = open("./result.coe","w")
file.write(";32k*12\nmemory_initialization_radix=16;\nmemory_initialization_vector=\n")

for j in range(0,height):
 for i in range(0,width):
  data=img.getpixel((i, j))
  re=['%01X' %int(s/16) for s in data]
  result=""
  for item in re:
    result+=item
  file.write(result)
  file.write(" ")
 file.write("\n")
for i in range(0,32*1024-width*height):
 file.write("000 ")
file.write("\n;")
print("Finish")

你可以根据需要自行修改该脚本。

至此,你已经能够将你喜欢的图片显示在VGA上了,快去试试吧!

题目 3-2:让世界热闹起来!

请根据以上内容,按要求完成本项练习

题目 3-3:比 bilibili 何如?

请根据以上内容,按要求完成本项练习


最后更新: December 25, 2023

评论