VGA 通信协议
VGA 是一种视频信号传输方式,包括红色、绿色、蓝色的模拟信号以及位置同步信号(水平和垂直信号)。如果我们的 FPGA 能够按照 VGA 协议产生这些信号,那么我们就可以通过 VGA 显示器显示我们想要的图像了。
基于此,我们可以做出许多更加有意思的东西,比如各类小游戏等。下面是一些往届的游戏作品展示:
怎么样?心动了吗?心动不如行动,就让我们立刻开始吧!
★ 3.1.1 VGA 显示原理
VGA 显示器通过对屏幕进行逐行扫描来显示图像。电子束从屏幕的左上角开始,沿着从左到右、从上到下的方向逐行扫描屏幕。
在每一行扫描过程中,电子束会被激活以显示相应像素点的颜色(HEN 段)。每扫描完一行,电子束回到屏幕的左边下一行的起始位置,在此期间,电子束被消隐(HSW、HBP、HFP 段),其中,在 HSW 段还会使用行同步信号进行行同步;扫描完所有的行,即形成一帧(VEN 段),接着电子束回到屏幕左上角,在此期间消隐电子束(VSW、VBP、VFP 段),并在 VSW 段用场同步信号进行场同步。

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 显示屏规格对应的各定时参数的值如下表:

定时参数表
学校的显示屏是 800x600@72Hz 的规格,根据以上表格,可以得到各定时参数的值如下:
| 像素频率(MHZ) |
HSW |
BP |
HEN |
HFP |
VSW |
VBP |
VEN |
VFP |
| 50 |
120 |
64 |
800 |
56 |
6 |
23 |
600 |
37 |
对应的时序图如下:

时序信号图
3.1.2 VGA 显示模块

VGA 显示模块逻辑结构
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 位。

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

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

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 |
行 |
则各定时信号如图:

定时信号示例
下面是相关的代码框架:
| 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( // 每个时钟周期计数器增加1,表示扫描一个像素
.clk (pclk),
.rstn (rstn),
.d (d_h),
.ce (1'b1),
.q (q_h)
);
CntS #(16,VSW_t) vcnt( // 每行扫描完计数器增加1,表示扫描一行像素点
// 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 将画布与显示屏适配,从而产生色彩信息。它是让我们的显示屏正确显示色彩的关键。

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
61
62
63
64
65
66
67 | // 实现DDP功能,将画布与显示屏适配,从而产生色彩信息。
// DDP和DST共同称为DU即显示单元
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, // 图像对应像素点的rgb值
output reg [DW-1:0] raddr // 计算得到图像某个像素点的地址
);
// 放大四倍
reg [1:0] sx; // 提示:sx与sy的作用是作为计数器,逢4进1。将800×600
reg [1:0] sy; // 的有效显示区域用200×150表示,也就是实际上的每4×4的
reg [1:0] nsx; // 像素点简化成1×1,这样能大大减小IP核声明调用的开销。
reg [1:0] nsy;
// 过于模糊?有需要的同学可以考虑只放大两倍。思考:不放大为什么不行?
// 开销太大?同学也可以自行探索放大八倍、十六倍的写法。
always @(*) begin
sx=nsx;
sy=nsy;
end
wire p;
// 取ven下降沿
PS #(1) ps( // 提示:取下降沿是因为扫描信号从有效区进入了消隐区
.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 // 提示:此处地址计算逢4换行,否则继
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中助教已经为你们实现了。)
| PS.v |
|---|
1
2
3
4
5
6
7
8
9
10
11
12 | module PS#(
parameter WIDTH = 1
)
(
input s,
input clk,
output p
);
// TODO:
endmodule
|
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.jpeg")
# 预处理
# 获取图片尺寸的大小__预期(600,400)
print (img_raw.size)
# 获取图片的格式 png
print (img_raw.format)
# 获取图片的图像类型 RGBA
print (img_raw.mode)
# 生成缩略图(若要修改图片尺寸请自行修改此处)
img = img_raw.resize((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 何如?
请根据以上内容,按要求完成本项练习。