基于FPGA的VGA图形教程一二三
介绍
本教程系列介绍了使用FPGA进行视频图形编程,从创建VGA驱动程序开始,转向更高级的功能,包括位图,精灵和效果。FPGA在高速I / O和自定义逻辑方面表现出色:您会惊讶地发现,使用几行Verilog可以实现多少。
该系列是围绕Digilent Arty,Basys 3和Nexys Video板设计的。如果你正在使用Arty或Nexys Video,你还需要Pmod VGA(Basys 3内置VGA输出)。详细要求如下。
我假设您对Verilog有基本的了解,并且习惯使用Xilinx的Vivado软件。如果您是FPGA开发的新手,请先试用Verilog&Vivado入门。
完成本教程后,转到第2部分,我们将介绍位图显示。
在github.com/WillGreen/timetoexplore上查找此教程和其他FPGA教程的代码和资源。
欢迎收到@WillFlux的反馈。2019年1月更新。
要求
1. Digilent Arty A7-35T,Arty S7-50T,Basys 3或Nexys Video板(其他板卡参见下文)
2. Pmod VGA如果使用Arty或Nexys Video(Basys 3内置VGA)
3. 支持VGA的显示器和电缆
4. Micro USB线缆可为电路板编程和供电
5. 安装Xilinx Vivado(包括Digilent板文件)
其他FPGA板
如果您使用的是不同的FPGA板,您仍然可以通过对top.v模块进行一些更改来学习本教程:
1. 硬件I / O:更新硬件端口,例如CLK和VGA_R,以匹配您的主板。
2. 时钟:如果您的电路板时钟不是100 MHz,则需要更新像素时钟码。
3. VGA输出:如果您的VGA输出不是每种颜色4位,请调整VGA分配语句。
VGA如何工作
VGA是使用15针D-sub连接器的模拟视频标准。它不需要高时钟速度或复杂的编码,因此在学习FPGA图形时是一个很好的起点。VGA有五个主要信号引脚:红色,绿色和蓝色各一个,同步两个。水平同步划分一条线。垂直同步划分屏幕,也称为帧。
VGA信号有两个阶段:绘制像素和消隐间隔。同步信号出现在消隐间隔内; 通过显示前沿和显示后沿与像素绘画分开。当开发VGA时,监视器基于阴极射线管(CRT):消隐间隔给出电压电平稳定的时间,并使电子枪返回到线或屏幕的开始。
在本文中,我们将创建一个经典的640x480 60 Hz VGA显示器。所需的像素时钟为25 MHz 1,这是我们电路板100 MHz时钟的一小部分。产生有效VGA信号的关键是获得正确的时序。为简单起见,我们将按像素和线条进行计时。每个像素是25 MHz像素时钟(40 ns)的刻度。一条线是一组完整的水平像素。
水平像素时序
· 显示前沿Front Porch:16
· 同步脉冲Sync Pulse:96
· 显示后沿Back Porch:48
· 有效视频Active Pixels:640
· 总像素Total pixels:800
垂直线时序(2019年1月更新)
· 有效像素Active Pixels:480
· 显示前沿Front Porch:10
· 同步脉冲Sync Pulse:2
· 显示后沿Back Porch:33
· Total lines总线数:525
要了解有关显示时序的更多信息,包括高清,请参阅视频时序:VGA,SVGA,720P,1080P。
VGA模块
在Vivado创建一个新的RTL项目,调用vga01Arty,Basys 3或Nexys Video板作为目标。如果您需要有关项目创建的建议,请参阅我的入门教程系列的第1部分。
在新项目中创建一个名为vga640x480.v[ view source ] 的设计源:
module vga640x480(
input wire i_clk, // base clock
input wire i_pix_stb, // pixel clock strobe
input wire i_rst, // reset: restarts frame
output wire o_hs, // horizontal sync
output wire o_vs, // vertical sync
output wire o_blanking, // high during blanking interval
output wire o_active, // high during active pixel drawing
output wire o_screenend, // high for one tick at the end of screen
output wire o_animate, // high for one tick at end of active drawing
output wire [9:0] o_x, // current pixel x position
output wire [8:0] o_y // current pixel y position
);
// VGA timings https://timetoexplore.net/blog/video-timings-vga-720p-1080p
localparam HS_STA = 16; // horizontal sync start
localparam HS_END = 16 + 96; // horizontal sync end
localparam HA_STA = 16 + 96 + 48; // horizontal active pixel start
localparam VS_STA = 480 + 10; // vertical sync start
localparam VS_END = 480 + 10 + 2; // vertical sync end
localparam VA_END = 480; // vertical active pixel end
localparam LINE = 800; // complete line (pixels)
localparam SCREEN = 525; // complete screen (lines)
reg [9:0] h_count; // line position
reg [9:0] v_count; // screen position
// generate sync signals (active low for 640x480)
assign o_hs = ~((h_count >= HS_STA) & (h_count < HS_END));
assign o_vs = ~((v_count >= VS_STA) & (v_count < VS_END));
// keep x and y bound within the active pixels
assign o_x = (h_count < HA_STA) ? 0 : (h_count - HA_STA);
assign o_y = (v_count >= VA_END) ? (VA_END - 1) : (v_count);
// blanking: high within the blanking period
assign o_blanking = ((h_count < HA_STA) | (v_count > VA_END - 1));
// active: high during active pixel drawing
assign o_active = ~((h_count < HA_STA) | (v_count > VA_END - 1));
// screenend: high for one tick at the end of the screen
assign o_screenend = ((v_count == SCREEN - 1) & (h_count == LINE));
// animate: high for one tick at the end of the final active pixel line
assign o_animate = ((v_count == VA_END - 1) & (h_count == LINE));
always @ (posedge i_clk)
begin
if (i_rst) // reset to start of frame
begin
h_count <= 0;
v_count <= 0;
end
if (i_pix_stb) // once per pixel
begin
if (h_count == LINE) // end of line
begin
h_count <= 0;
v_count <= v_count + 1;
end
else
h_count <= h_count + 1;
if (v_count == SCREEN) // end of screen
v_count <= 0;
end
end
endmodule
o_x并o_y表示可见640x480显示屏内的水平和垂直位置:这是您用于定位和绘制图形的位置。h_count并v_count表示自行或屏幕开始以来发生的像素和行数(包括消隐间隔):这些用于同步信号。对于640x480同步脉冲是低电平有效,因此我们~在分配它们时使用。现在你可以忽略其他输出; 它们将在以后使用。
在我们开始绘制图形之前,我们需要了解我们的模块如何生成25 MHz时钟和i_pix_stb输入的作用。
Running to Time
我们的640x480 60 Hz VGA信号需要25 MHz像素时钟,但我们的主板具有100 MHz基本时钟。我们如何有效地将基准时钟除以4?
4是2的幂,所以我们可以使用一个简单的计数器,但在一般情况下这没用。相反,我们将采用Dan Gisselquist的分数时钟分频器方法。这是一种简单而优雅的方式,可以将时钟分成几乎任何数量。这种灵活性使我们能够处理不同的基本和像素时钟。
reg [15:0] cnt;
reg pix_stb;
always @(posedge CLK)
{pix_stb, cnt} <= cnt + 16'h4000; // divide by 4: (2^16)/4 = 0x4000
这是有效的,因为pix_stb只要计数器cnt翻转就会设置值。{x, y}是Verilog连接运算符:{4'b1101, 4'b0011} == 8'b11010011。
如果您的电路板具有不同的时钟,则根据需要调整添加到计数器的值。例如,75 MHz板时钟需要除以3才能达到25 MHz,因此我们将(2 16)/ 3 = 0x5555添加到计数器。
为了利用分频时钟,我们保留灵敏度列表的基准时钟,但为频闪添加测试:
always @ (posedge CLK)
begin
if (pix_stb)
begin
// do stuff once per pixel clock tick
end
end
通过使用来简化这一点很诱人always @ (posedge pix_stb)。不要这样做!虽然这可以用于简单的设计,但它很快就会导致不稳定的设计和调试问题。硬件设计足够困难,不会弄乱时钟分配和跨越时钟域。
Hip to be Square
通过定义VGA模块并创建合适的像素时钟,我们现在可以绘制简单的图形。该PMOD VGA支持每个颜色4比特,使我们每个输出为16级:VGA_R,VGA_G,和VGA_B。例如,VGA_R = 4b'1000将红色设置为半亮度,而VGA_B = 4b'0011将是深蓝色。
我们没有要绘制的位图。相反,我们基于一系列像素在数学上定义每个方格的范围:在存在方形的情况下,我们将方形的输出线设置为1.然后将该线连接到VGA输出以控制颜色信号。
注意:Basys 3板没有复位按钮,所以我们使用BTNC代替。这是激活的High,所以你需要更新wire rst开头附近的行top.v。
创建一个top.v使用以下内容调用的设计源[ 查看源代码 ]:
module top(
input wire CLK, // board clock: 100 MHz on Arty/Basys3/Nexys
input wire RST_BTN, // reset button
output wire VGA_HS_O, // horizontal sync output
output wire VGA_VS_O, // vertical sync output
output wire [3:0] VGA_R, // 4-bit VGA red output
output wire [3:0] VGA_G, // 4-bit VGA green output
output wire [3:0] VGA_B // 4-bit VGA blue output
);
wire rst = ~RST_BTN; // reset is active low on Arty & Nexys Video
// wire rst = RST_BTN; // reset is active high on Basys3 (BTNC)
// generate a 25 MHz pixel strobe
reg [15:0] cnt;
reg pix_stb;
always @(posedge CLK)
{pix_stb, cnt} <= cnt + 16'h4000; // divide by 4: (2^16)/4 = 0x4000
wire [9:0] x; // current pixel x position: 10-bit value: 0-1023
wire [8:0] y; // current pixel y position: 9-bit value: 0-511
vga640x480 display (
.i_clk(CLK),
.i_pix_stb(pix_stb),
.i_rst(rst),
.o_hs(VGA_HS_O),
.o_vs(VGA_VS_O),
.o_x(x),
.o_y(y)
);
// Four overlapping squares
wire sq_a, sq_b, sq_c, sq_d;
assign sq_a = ((x > 120) & (y > 40) & (x < 280) & (y < 200)) ? 1 : 0;
assign sq_b = ((x > 200) & (y > 120) & (x < 360) & (y < 280)) ? 1 : 0;
assign sq_c = ((x > 280) & (y > 200) & (x < 440) & (y < 360)) ? 1 : 0;
assign sq_d = ((x > 360) & (y > 280) & (x < 520) & (y < 440)) ? 1 : 0;
assign VGA_R[3] = sq_b; // square b is red
assign VGA_G[3] = sq_a | sq_d; // squares a and d are green
assign VGA_B[3] = sq_c; // square c is blue
endmodule
例如,如果x是210且y是150,那么我们就在sq_a and sq_b,因此VGA_G[3]并且VGA_R[3]都被设置为1,从而导致黄色像素。
每个VGA输出只能有一个分配,因为您只能有一个输入,而不使用干预逻辑。每种颜色由四个独立的输出组成:您可以为两个分配分别进行分配VGA_R[0],VGA_R[3]但不能分配VGA_R[3]两个分配VGA_R。对于绿色输出,VGA_G[3]我们使用'或'将两个方块的输出组合成一个VGA输出线。
约束
从Time To Explore git repo中获取适当的约束文件并将其添加到项目中:
· Basys3
作为参考,Arty A7-35T约束如下所示:
## FPGA VGA Graphics Part 1: Arty Board Constraints
## Clock
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports {CLK}];
create_clock -add -name sys_clk_pin -period 10.00 \
-waveform {0 5} [get_ports {CLK}];
## Reset Button (active low)
set_property -dict {PACKAGE_PIN C2 IOSTANDARD LVCMOS33} [get_ports {RST_BTN}];
## Pmod VGA Header JB
set_property -dict {PACKAGE_PIN E15 IOSTANDARD LVCMOS33} [get_ports {VGA_R[0]}];
set_property -dict {PACKAGE_PIN E16 IOSTANDARD LVCMOS33} [get_ports {VGA_R[1]}];
set_property -dict {PACKAGE_PIN D15 IOSTANDARD LVCMOS33} [get_ports {VGA_R[2]}];
set_property -dict {PACKAGE_PIN C15 IOSTANDARD LVCMOS33} [get_ports {VGA_R[3]}];
set_property -dict {PACKAGE_PIN J17 IOSTANDARD LVCMOS33} [get_ports {VGA_B[0]}];
set_property -dict {PACKAGE_PIN J18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[1]}];
set_property -dict {PACKAGE_PIN K15 IOSTANDARD LVCMOS33} [get_ports {VGA_B[2]}];
set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports {VGA_B[3]}];
## Pmod VGA Header JC
set_property -dict {PACKAGE_PIN U12 IOSTANDARD LVCMOS33} [get_ports {VGA_G[0]}];
set_property -dict {PACKAGE_PIN V12 IOSTANDARD LVCMOS33} [get_ports {VGA_G[1]}];
set_property -dict {PACKAGE_PIN V10 IOSTANDARD LVCMOS33} [get_ports {VGA_G[2]}];
set_property -dict {PACKAGE_PIN V11 IOSTANDARD LVCMOS33} [get_ports {VGA_G[3]}];
set_property -dict {PACKAGE_PIN U14 IOSTANDARD LVCMOS33} [get_ports {VGA_HS_O}];
set_property -dict {PACKAGE_PIN V14 IOSTANDARD LVCMOS33} [get_ports {VGA_VS_O}];
注意:这些约束对Pmod VGA Rev C有效.Digilent告诉我早期的Pmod修订版从未公开发布,但如果您有较旧的版本,则可能需要交换约束文件中的颜色。
创建和编程
运行synthesis, implementation, bitstream generation。如果您需要有关如何执行此操作的提醒,请参阅FPGA介绍性文章。
接下来,将Pmod VGA连接到Arty或Nexys Video上的中间两个连接器(JB和JC),然后使用VGA线将显示器连接到Pmod VGA。Basys 3用户只需将VGA线缆直接连接到他们的主板上即可。最后,通过USB将您的主板连接到您的计算机并进行编程vga01/vga01.runs/impl_1/top.bit。
你应该在屏幕上看到四个重叠的方块; 从左到右:绿色,红色,蓝色,绿色。如果颜色错误,请检查约束文件。
还不错,但由于像素时钟为25 MHz,而不是25.175 MHz,我们的规格略低于规格:fH应为31.469 kHz,fV为60.0 Hz。
更多颜色
在这个例子中,我们只设置最重要的颜色位的值,对于红色,VGA_R[3]我们的正方形将大约是最大亮度的一半。如果要将它们设置为最大亮度,则设置所有位,例如:assign VGA_R = {4{sq_a}};。您还可以通过组合不同的颜色引脚来制作完整的颜色范围,例如,制作单个橙色方块,删除所有现有的VGA分配并添加以下内容:
assign VGA_R = {4{sq_a}}; // square a is 100% red
assign VGA_G[3] = sq_a; // square a is also 50% green
动画
静态方块都很好,但我们每秒有60帧。为了在不撕裂的情况下进行动画制作,我们将移动限制为帧完成绘制后的时间,但是在下一帧开始之前。vga640x480模块提供一个被调用的输出o_animate,在像素绘制结束后的一个刻度上都是如此。在下一帧开始之前,我们有大约1 ms的动画(33行800像素,每40ns长); 对于我们简单的方块,这很充足。
方形模块
为了表示动画方块,我们将创建一个新模块。我们将把坐标放在广场的中心。这对于正方形并没有太大的区别,但是一旦我们开始处理更复杂的形状和精灵就会有意义。
当i_animate高时,正方形每帧一次在水平和垂直方向上移动一个像素。当它到达屏幕边缘时,它会切换方向。
添加一个名为square.v[ view source ] 的设计源:
module square #(
H_SIZE=80, // half square width (for ease of co-ordinate calculations)
IX=320, // initial horizontal position of square centre
IY=240, // initial vertical position of square centre
IX_DIR=1, // initial horizontal direction: 1 is right, 0 is left
IY_DIR=1, // initial vertical direction: 1 is down, 0 is up
D_WIDTH=640, // width of display
D_HEIGHT=480 // height of display
)
(
input wire i_clk, // base clock
input wire i_ani_stb, // animation clock: pixel clock is 1 pix/frame
input wire i_rst, // reset: returns animation to starting position
input wire i_animate, // animate when input is high
output wire [11:0] o_x1, // square left edge: 12-bit value: 0-4095
output wire [11:0] o_x2, // square right edge
output wire [11:0] o_y1, // square top edge
output wire [11:0] o_y2 // square bottom edge
);
reg [11:0] x = IX; // horizontal position of square centre
reg [11:0] y = IY; // vertical position of square centre
reg x_dir = IX_DIR; // horizontal animation direction
reg y_dir = IY_DIR; // vertical animation direction
assign o_x1 = x - H_SIZE; // left: centre minus half horizontal size
assign o_x2 = x + H_SIZE; // right
assign o_y1 = y - H_SIZE; // top
assign o_y2 = y + H_SIZE; // bottom
always @ (posedge i_clk)
begin
if (i_rst) // on reset return to starting position
begin
x <= IX;
y <= IY;
x_dir <= IX_DIR;
y_dir <= IY_DIR;
end
if (i_animate && i_ani_stb)
begin
x <= (x_dir) ? x + 1 : x - 1; // move left if positive x_dir
y <= (y_dir) ? y + 1 : y - 1; // move down if positive y_dir
if (x <= H_SIZE + 1) // edge of square is at left of screen
x_dir <= 1; // change direction to right
if (x >= (D_WIDTH - H_SIZE - 1)) // edge of square at right
x_dir <= 0; // change direction to left
if (y <= H_SIZE + 1) // edge of square at top of screen
y_dir <= 1; // change direction to down
if (y >= (D_HEIGHT - H_SIZE - 1)) // edge of square at bottom
y_dir <= 0; // change direction to up
end
end
endmodule
此模块使用Verilog参数(例如H_SIZE)来允许自定义方形实例。例如H_SIZE=80,如果用户在创建模块实例时未提供值,则使用默认值。我们使用12位值来表示我们的方形位置,以便模块可用于各种显示分辨率,包括4K。
顶层动画
现在我们只需要更新顶层模块以使用方块模块并创建一些方形实例。在这种情况下,我们正在制作三个彩色方块的动画。用以下内容替换顶部模块,然后重新生成比特流并对电路板进行编程[ 查看源代码 ]:
module top(
input wire CLK, // board clock: 100 MHz on Arty/Basys3/Nexys
input wire RST_BTN, // reset button
output wire VGA_HS_O, // horizontal sync output
output wire VGA_VS_O, // vertical sync output
output wire [3:0] VGA_R, // 4-bit VGA red output
output wire [3:0] VGA_G, // 4-bit VGA green output
output wire [3:0] VGA_B // 4-bit VGA blue output
);
wire rst = ~RST_BTN; // reset is active low on Arty & Nexys Video
// wire rst = RST_BTN; // reset is active high on Basys3 (BTNC)
wire [9:0] x; // current pixel x position: 10-bit value: 0-1023
wire [8:0] y; // current pixel y position: 9-bit value: 0-511
wire animate; // high when we're ready to animate at end of drawing
// generate a 25 MHz pixel strobe
reg [15:0] cnt = 0;
reg pix_stb = 0;
always @(posedge CLK)
{pix_stb, cnt} <= cnt + 16'h4000; // divide by 4: (2^16)/4 = 0x4000
vga640x480 display (
.i_clk(CLK),
.i_pix_stb(pix_stb),
.i_rst(rst),
.o_hs(VGA_HS_O),
.o_vs(VGA_VS_O),
.o_x(x),
.o_y(y),
.o_animate(animate)
);
wire sq_a, sq_b, sq_c;
wire [11:0] sq_a_x1, sq_a_x2, sq_a_y1, sq_a_y2; // 12-bit values: 0-4095
wire [11:0] sq_b_x1, sq_b_x2, sq_b_y1, sq_b_y2;
wire [11:0] sq_c_x1, sq_c_x2, sq_c_y1, sq_c_y2;
square #(.IX(160), .IY(120), .H_SIZE(60)) sq_a_anim (
.i_clk(CLK),
.i_ani_stb(pix_stb),
.i_rst(rst),
.i_animate(animate),
.o_x1(sq_a_x1),
.o_x2(sq_a_x2),
.o_y1(sq_a_y1),
.o_y2(sq_a_y2)
);
square #(.IX(320), .IY(240), .IY_DIR(0)) sq_b_anim (
.i_clk(CLK),
.i_ani_stb(pix_stb),
.i_rst(rst),
.i_animate(animate),
.o_x1(sq_b_x1),
.o_x2(sq_b_x2),
.o_y1(sq_b_y1),
.o_y2(sq_b_y2)
);
square #(.IX(480), .IY(360), .H_SIZE(100)) sq_c_anim (
.i_clk(CLK),
.i_ani_stb(pix_stb),
.i_rst(rst),
.i_animate(animate),
.o_x1(sq_c_x1),
.o_x2(sq_c_x2),
.o_y1(sq_c_y1),
.o_y2(sq_c_y2)
);
assign sq_a = ((x > sq_a_x1) & (y > sq_a_y1) &
(x < sq_a_x2) & (y < sq_a_y2)) ? 1 : 0;
assign sq_b = ((x > sq_b_x1) & (y > sq_b_y1) &
(x < sq_b_x2) & (y < sq_b_y2)) ? 1 : 0;
assign sq_c = ((x > sq_c_x1) & (y > sq_c_y1) &
(x < sq_c_x2) & (y < sq_c_y2)) ? 1 : 0;
assign VGA_R[3] = sq_a; // square a is red
assign VGA_G[3] = sq_b; // square b is green
assign VGA_B[3] = sq_c; // square c is blue
endmodule
如果方块没有移动,那么你的复位按钮可能是高电平(就像在Basys 3上一样); 你需要更新wire rst开头附近的行top.v。
如果您按下电路板上的重置按钮(Basys 3上的BTNC),您应该看到方块返回到它们的起始位置。注意:在Arty上不要将重置按钮与电路板另一角的红色编程按钮混淆。按prog将擦除Arty内存。如果你擦拭你的程序,只需重新编程板,让你的方块回来。
用不同的参数在其他区域试试,如试验H_SIZE,IX_DIR和IY_DIR。
额外奖励:Super VGA!
现在你已经掌握了640x480,为什么不尝试将设计更新到800x600?您需要的所有数据都在视频计时中:VGA,SVGA,720P,1080P。以下设计指针应该让您入门:
· 您需要40 MHz像素时钟,因此时钟选通需要更新
· SVGA的总像素大小(包括空白)为1056x628,因此您需要更多位来表示x和y坐标
· 同步信号为高电平(正),为800x600
如果您遇到困难,可以查看工作示例:vga800x600.v和top_static_800x600.v。
下一步是什么?
我们设法做了很多逻辑。但是,虽然直接从像素位置绘制可以管理几个正方形,但对于任何更复杂的东西来说很快就会变得很麻烦。为了允许更复杂的图形,我们需要存储器来存储和组合值。这是下一部分的主题。
在Time to Explore FPGA Index中查找有关FPGA和Verilog的更多信息。
1:严格来说,该规范要求25.175MHz像素时钟,但大多数VGA显示器都能容忍这一点。
Verilog第2部分中的FPGA VGA图形
2018年2月22日 在教程中
介绍
欢迎使用Digilent Arty,Basys 3或Nexys Video板回到我的FPGA图形教程系列。在第1部分中,我们创建了一个VGA模块,并用它来为屏幕上的方块设置动画。在第二部分中,我们介绍了位图显示。在本教程结束时,您将能够将自己的映像加载到FPGA内存中并将其显示在监视器上。
完成本教程后,请转到第3部分中的精灵动画和双缓冲。
在github.com/WillGreen/timetoexplore上查找此教程和其他FPGA教程的代码和资源。
欢迎收到@WillFlux的反馈。2019年1月更新。
要求
该部分的要求与第1部分相同:
- Digilent Arty,Basys 3或Nexys Video板(其他板卡参见下文)
- Pmod VGA如果使用Arty或Nexys Video(Basys 3内置VGA)
- 支持VGA的显示器和电缆
- Micro USB线缆可为电路板编程和供电
- 安装Xilinx Vivado(包括Digilent板文件)
其他FPGA板
本教程需要至少1,350 Kbits的板载FPGA内存(块或分布式RAM)。如果您可以满足ram要求,则通过对top.v
模块进行更改,可以相对容易地使本教程适应其他板:
- 硬件I / O:更新硬件端口,例如
CLK
和VGA_R
,以匹配您的主板。 - 时钟:如果您的电路板时钟不是100 MHz,则需要更新像素时钟码。
- VGA输出:如果您的VGA输出不是每种颜色4位,请调整VGA分配语句。
位图
位图显示由内存阵列支持:每个像素对应于内存中的位置。要创建显示,我们从其内存位置读出当前像素的值。要设置像素的颜色,我们修改其相关内存位置的内容。位图提供了两个显着的好处:我们可以使用我们喜欢的任何技术自由创建复杂的图形,并且像素颜色的设置与实际的屏幕绘制过程分开。位图显示的一大缺点是我们需要大量内存来存储像素数据。
内存要求:分辨率和颜色深度
第一个考虑因素是VGA输出的能力:它支持红色,绿色和蓝色各4位,每像素总共12位。如果我们使用标准640x480 VGA显示器每像素12位,我们的存储器阵列需要3,600 Kbits:640 x 480 x 12位= 3,686,400位。但是,我们的XC7A35T FPGA只有1,800 Kbits的块ram。虽然Arty板还包含256 MB的DDR3L内存,但Basys3却没有,而使用DDR内存会大大增加我们设计的复杂性。
为了使我们的设计适合块内存,我们将屏幕分辨率降低到640 x 360,并将自己限制为6位颜色:640 x 360 x 6位= 1,350 Kbits。640 x 360的另一个好处是具有16:9的宽屏比例,可与现代显示器和电视相匹配。
64种颜色
如果每个像素由6位表示,则它可以具有2 6或64种可能的颜色。如果我们分配2位给每个红,绿,蓝,那么每种颜色可以有4个二进制值之一:00
,01
,10
,或11
。但是,我们的VGA输出是12位:它预计每像素4位(0到15)。要使用显示器的全亮度,我们需要将2位颜色扩展为4位; 为此,我们复制了导致四个可能值的位: 0000
(0),0101
(5),1010
(10)和1111
(15)。
现代计算机显示器通常是24位,即它们可以显示1670万种颜色。为了感受我们固定的64种颜色的样子,我创建了一个样本:
麻烦地看看我们如何使用6位值直接驱动显示器,我们不会这样做!相反,我们将使用一点间接来将我们的颜色可能性扩展到4,096的全范围。
间接:索引颜色和调色板
它可以代表调色板查找表中的颜色,而不是每个表示颜色的6位值。如果特定像素具有值001010
(十),那么我们参考颜色表中的第十个条目,其中包含要使用的12位颜色。虽然我们仍然限制为总共64种颜色,但它们现在可以是VGA输出能够产生的4,096种颜色中的任何64种颜色。例如,森林的图片可能有很多绿色,而一个城市的图片会使用更多的灰色。这种设计在较旧的计算机中很常见,例如,原始的Amiga芯片组支持32种颜色,可能是4,096种:与我们的设计非常相似!GIF和PNG格式仍然使用这种方法来挤出256色图像中的最佳质量。
如果您有兴趣了解有关不同调色板的更多信息,可以在维基百科上找到一个很好的调色板列表页面。
这就是足够的色彩理论,让我们继续创造一个形象!
为FPGA内存准备映像
为了显示我们的图像,我们需要一种机制来将它和它的调色板加载到我们的设计中。Verilog支持一种名为的方法$readmemh
,可以从十六进制值的文本文件中将值加载到内存中。我创建了一个方便的Python脚本(img2fmem.py
),为图像及其调色板生成合适的文件。它可以使用优化的12位调色板将源图像转换为6位颜色。
如果您不想准备自己的图像,那么您可以使用现成的图形并跳到下一部分(存储器映射)。
使用img2fmem.py
img2fmem.py
从Github上的FPGATools存储库中获取一份副本。
img2fmem.py
需要Pillow包运行。您可以使用Python的pip工具安装它:
pip install pillow
使用图像编辑器将图像裁剪/调整为640 x 360像素,并将其保存为JPEG,PNG,BMP或TIFF格式。注意。关键的是您的图像正好是640 x 360像素,否则将无法正确显示。
获得合适的源图像后,可以运行以下命令(替换game.png
为图像文件名):
python img2fmem.py game.png 6 mem
完成后,您应该在与源图像相同的目录中找到三个新文件:
game_preview.png
- 您可以查看的普通PNG以预览图像转换game.mem
- 图像的文本十六进制版本game_palette.mem
- 图像调色板的文本十六进制版本
在Web浏览器或图像编辑器中加载预览以检查转换是否正常。
如果在文本编辑器中打开调色板文件,您将看到图像的64个12位调色板值。
现在我们有一个准备好的图像,是时候把它加载到内存中了。
内存映射
在Vivado中创建一个新的RTL项目,vga02
并使用适当的板作为目标。如果您需要有关项目创建的建议,请参阅我的入门教程系列的第1部分。
我们需要创建一个内存数组来保存我们的图像数据。为此,我们将使用一个sram
适用于FPGA块ram 的简单模块。
添加一个名为sram.v
[ view source ] 的设计源:
module sram #(parameter ADDR_WIDTH=8, DATA_WIDTH=8, DEPTH=256, MEMFILE="") (
input wire i_clk,
input wire [ADDR_WIDTH-1:0] i_addr,
input wire i_write,
input wire [DATA_WIDTH-1:0] i_data,
output reg [DATA_WIDTH-1:0] o_data
);
reg [DATA_WIDTH-1:0] memory_array [0:DEPTH-1];
initial begin
if (MEMFILE > 0)
begin
$display("Loading memory init file '" + MEMFILE + "' into array.");
$readmemh(MEMFILE, memory_array);
end
end
always @ (posedge i_clk)
begin
if(i_write) begin
memory_array[i_addr] <= i_data;
end
else begin
o_data <= memory_array[i_addr];
end
end
endmodule
如果您想了解更多关于使用滑块的信息,请查看我的Verilog滑块配方。
VGA 640x360
我们将在第1部分中使用我们的VGA驱动程序模块的修改版本。它将有效垂直分辨率更改为360像素,同时保持标准的640x480 VGA时序。这意味着我们的输出仍然可以使用标准VGA显示器:360垂直像素将在上方和下方有黑条(信箱)。
有三个变化:
- 我们为垂直活动开始添加
VA_STA
值为60的参数 - 我们将垂直活动结束的值更改
VA_END
为420 - 我们
VA_STA
从assign o_y =
线上的有效像素值中减去。
vga640x360.v
使用以下内容创建一个名为[ view source ] 的新模块:
module vga640x360(
input wire i_clk, // base clock
input wire i_pix_stb, // pixel clock strobe
input wire i_rst, // reset: restarts frame
output wire o_hs, // horizontal sync
output wire o_vs, // vertical sync
output wire o_blanking, // high during blanking interval
output wire o_active, // high during active pixel drawing
output wire o_screenend, // high for one tick at the end of screen
output wire o_animate, // high for one tick at end of active drawing
output wire [9:0] o_x, // current pixel x position
output wire [8:0] o_y // current pixel y position
);
// VGA timings https://timetoexplore.net/blog/video-timings-vga-720p-1080p
localparam HS_STA = 16; // horizontal sync start
localparam HS_END = 16 + 96; // horizontal sync end
localparam HA_STA = 16 + 96 + 48; // horizontal active pixel start
localparam VS_STA = 480 + 10; // vertical sync start
localparam VS_END = 480 + 10 + 2; // vertical sync end
localparam VA_STA = 60; // vertical active pixel start
localparam VA_END = 420; // vertical active pixel end
localparam LINE = 800; // complete line (pixels)
localparam SCREEN = 525; // complete screen (lines)
reg [9:0] h_count; // line position
reg [9:0] v_count; // screen position
// generate sync signals (active low for 640x480)
assign o_hs = ~((h_count >= HS_STA) & (h_count < HS_END));
assign o_vs = ~((v_count >= VS_STA) & (v_count < VS_END));
// keep x and y bound within the active pixels
assign o_x = (h_count < HA_STA) ? 0 : (h_count - HA_STA);
assign o_y = (v_count >= VA_END) ?
(VA_END - VA_STA - 1) : (v_count - VA_STA);
// blanking: high within the blanking period
assign o_blanking = ((h_count < HA_STA) | (v_count > VA_END - 1));
// active: high during active pixel drawing
assign o_active = ~((h_count < HA_STA) |
(v_count > VA_END - 1) |
(v_count < VA_STA));
// screenend: high for one tick at the end of the screen
assign o_screenend = ((v_count == SCREEN - 1) & (h_count == LINE));
// animate: high for one tick at the end of the final active pixel line
assign o_animate = ((v_count == VA_END - 1) & (h_count == LINE));
always @ (posedge i_clk)
begin
if (i_rst) // reset to start of frame
begin
h_count <= 0;
v_count <= 0;
end
if (i_pix_stb) // once per pixel
begin
if (h_count == LINE) // end of line
begin
h_count <= 0;
v_count <= v_count + 1;
end
else
h_count <= h_count + 1;
if (v_count == SCREEN) // end of screen
v_count <= 0;
end
end
endmodule
整合到一起
创建一个top.v
使用以下设计[ view source ] 调用的设计源。
顶部模块引用的图形和调色板game.mem
和game_palette.mem
分别。如果您使用其他名称创建自己的图形,请确保更新顶部模块中的相应行以进行匹配。
module top(
input wire CLK, // board clock: 100 MHz on Arty/Basys3/Nexys
input wire RST_BTN, // reset button
output wire VGA_HS_O, // horizontal sync output
output wire VGA_VS_O, // vertical sync output
output reg [3:0] VGA_R, // 4-bit VGA red output
output reg [3:0] VGA_G, // 4-bit VGA green output
output reg [3:0] VGA_B // 4-bit VGA blue output
);
wire rst = ~RST_BTN; // reset is active low on Arty & Nexys Video
// wire rst = RST_BTN; // reset is active high on Basys3 (BTNC)
// generate a 25 MHz pixel strobe
reg [15:0] cnt;
reg pix_stb;
always @(posedge CLK)
{pix_stb, cnt} <= cnt + 16'h4000; // divide by 4: (2^16)/4 = 0x4000
wire [9:0] x; // current pixel x position: 10-bit value: 0-1023
wire [8:0] y; // current pixel y position: 9-bit value: 0-511
wire active; // high during active pixel drawing
vga640x360 display (
.i_clk(CLK),
.i_pix_stb(pix_stb),
.i_rst(rst),
.o_hs(VGA_HS_O),
.o_vs(VGA_VS_O),
.o_x(x),
.o_y(y),
.o_active(active)
);
// VRAM frame buffers (read-write)
localparam SCREEN_WIDTH = 640;
localparam SCREEN_HEIGHT = 360;
localparam VRAM_DEPTH = SCREEN_WIDTH * SCREEN_HEIGHT;
localparam VRAM_A_WIDTH = 18; // 2^18 > 640 x 360
localparam VRAM_D_WIDTH = 6; // colour bits per pixel
reg [VRAM_A_WIDTH-1:0] address;
wire [VRAM_D_WIDTH-1:0] dataout;
sram #(
.ADDR_WIDTH(VRAM_A_WIDTH),
.DATA_WIDTH(VRAM_D_WIDTH),
.DEPTH(VRAM_DEPTH),
.MEMFILE("game.mem")) // bitmap to load
vram (
.i_addr(address),
.i_clk(CLK),
.i_write(0), // we're always reading
.i_data(0),
.o_data(dataout)
);
reg [11:0] palette [0:63]; // 64 x 12-bit colour palette entries
reg [11:0] colour;
initial begin
$display("Loading palette.");
$readmemh("game_palette.mem", palette); // bitmap palette to load
end
always @ (posedge CLK)
begin
address <= y * SCREEN_WIDTH + x;
if (active)
colour <= palette[dataout];
else
colour <= 0;
VGA_R <= colour[11:8];
VGA_G <= colour[7:4];
VGA_B <= colour[3:0];
end
endmodule
该sram
模块有四个参数:数据宽度,要存储的值的数量(深度),访问它们所需的地址宽度,以及在构建期间加载的内存初始化文件的名称。对于我们的显示器,我们使用6位作为数据宽度,640 x 360作为深度,18位作为地址,因为2 18是最大值大于640 x 360。
要了解有关存储器初始化的更多信息,请参阅Verilog中的FPGA菜谱条目Initialize Memory。
将图像文件添加到项目
要使Vivado能够找到图像文件和调色板,您需要将它们添加到项目中。您可以像设计源一样使用“Add Sources”,然后选择“design sources”和“Files of type: Memory Initialization Files”,然后找到game.mem
和game_palette.mem
(或者您调用它们)。Vivado会自动将它们识别为内存文件,并使其可用于您的设计。
约束
从Time To Explore git repo中获取适当的约束文件并将其添加到项目中:
作为参考,Basys 3约束看起来像这样:
## FPGA VGA Graphics Part 2: Basys 3 Board Constraints
## Clock
set_property -dict {PACKAGE_PIN W5 IOSTANDARD LVCMOS33} [get_ports {CLK}];
create_clock -add -name sys_clk_pin -period 10.00 \
-waveform {0 5} [get_ports {CLK}];
## Use BTNC as Reset Button (active high)
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports {RST_BTN}];
## VGA Connector
set_property -dict {PACKAGE_PIN G19 IOSTANDARD LVCMOS33} [get_ports {VGA_R[0]}];
set_property -dict {PACKAGE_PIN H19 IOSTANDARD LVCMOS33} [get_ports {VGA_R[1]}];
set_property -dict {PACKAGE_PIN J19 IOSTANDARD LVCMOS33} [get_ports {VGA_R[2]}];
set_property -dict {PACKAGE_PIN N19 IOSTANDARD LVCMOS33} [get_ports {VGA_R[3]}];
set_property -dict {PACKAGE_PIN N18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[0]}];
set_property -dict {PACKAGE_PIN L18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[1]}];
set_property -dict {PACKAGE_PIN K18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[2]}];
set_property -dict {PACKAGE_PIN J18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[3]}];
set_property -dict {PACKAGE_PIN J17 IOSTANDARD LVCMOS33} [get_ports {VGA_G[0]}];
set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports {VGA_G[1]}];
set_property -dict {PACKAGE_PIN G17 IOSTANDARD LVCMOS33} [get_ports {VGA_G[2]}];
set_property -dict {PACKAGE_PIN D17 IOSTANDARD LVCMOS33} [get_ports {VGA_G[3]}];
set_property -dict {PACKAGE_PIN P19 IOSTANDARD LVCMOS33} [get_ports {VGA_HS_O}];
set_property -dict {PACKAGE_PIN R19 IOSTANDARD LVCMOS33} [get_ports {VGA_VS_O}];
建立和计划
运行synthesis, implementation, bitstream generation。如果您需要有关如何执行此操作的提醒,请参阅FPGA介绍性文章。
接下来,将Pmod VGA连接到Arty或Nexys Video上的中间两个连接器(JB和JC),然后使用VGA线将显示器连接到Pmod VGA。Basys 3用户只需将VGA线缆直接连接到他们的主板上即可。最后,通过USB将您的主板连接到您的计算机并进行编程vga02/vga02.runs/impl_1/top.bit
。
您应该看到显示的位图图像。如果图像失真或颜色错误,请确保您的图像文件正好是640 x 360,并且您已经加载了正确的位图和调色板文件top.v
。
额外奖励:超级VGA位图
如果你有幸拥有Nexys视频(或其他具有至少4 Mbit块ram的FPGA),那么请尝试使用8位800x600位图。更高的分辨率和颜色深度可以获得更好的效果,特别是对于照片。如果您在创建800x600 VGA驱动程序和更新顶层模块时需要帮助,请参阅第1部分的结尾。
下一步是什么?
现在我们有了一个位图显示器,我们已准备好尝试精灵和双缓冲。这是第3部分的主题。
Verilog第3部分中的FPGA VGA图形
2018年6月11日 在教程中
介绍
欢迎使用Digilent主板回到我的FPGA图形教程系列的第三部分。在第2部分中,我们将位图显示和加载的图形文件引入内存。在第三部分中,我们使用位图和双缓冲动画精灵:这种经典技术是2D游戏的主要内容。在本教程结束时,您将能够使用FPGA板上的开关控制精灵。
在github.com/WillGreen/timetoexplore上查找此教程和其他FPGA教程的代码和资源。
要求
该部分的要求与前面的部分相同:
- Digilent Arty A7-35T,Arty S7-50T,Basys 3或Nexys Video板(其他板卡参见下文)
- VGA Pmod如果使用Arty或Nexys视频(Basys 3内置VGA)
- 支持VGA的显示器和电缆
- Micro USB线缆可为电路板编程和供电
- 安装Xilinx Vivado(包括Digilent板文件)
其他FPGA板
本教程需要至少964 Kbits的板载FPGA内存(块或分布式RAM)。如果您可以满足ram要求,则通过对top.v
模块进行更改,可以相对容易地使本教程适应其他板:
- 硬件I / O:更新硬件端口,例如
CLK
和VGA_R
,以匹配您的主板。 - 时钟:如果您的电路板时钟不是100 MHz,则需要更新像素时钟码。
- VGA输出:如果您的VGA输出不是每种颜色4位,请调整VGA分配语句。
双缓冲
双缓冲的原理很简单:在从另一个驱动显示器的同时在一个缓冲区上绘制。您有一整帧(1/60秒)来创建图像,同时避免任何屏幕撕裂。有两个缺点:您需要两倍的内存,并且您将延迟增加一帧。
除了我们的两个缓冲区的内存,我们还需要一些内存来存储精灵。因此,我们将分辨率降低了一半,达到320x180。320 x 180 x 8位缓冲器需要450 Kbit的RAM; 我们的FPGA上有1,800 Kbits的块内存,这提供了充足的摆动空间。
通过将分辨率减半,我们可以轻松实现驱动程序:使用右移可以有效地实现除以2 。在Verilog中,>>
操作员实现了右移; 就像在C,Go或Python中一样。
VGA 320x180
在Vivado中创建一个新的RTL项目,vga03
并使用适当的板作为目标。如果您需要有关项目创建的建议,请参阅我的入门教程系列的第1部分。
我们将使用第2部分中熟悉的VGA驱动程序模块的修改版本。它将水平和垂直分辨率减半,同时保持标准的640x480 VGA时序。第2部分中的640x360版本只有一处变化:
- 我们正确地转变
o_x
并将o_y
有效解决方案减半
vga320x180.v
使用以下内容创建一个名为[ view source ] 的新模块:
module vga320x180(
input wire i_clk, // base clock
input wire i_pix_stb, // pixel clock strobe
input wire i_rst, // reset: restarts frame
output wire o_hs, // horizontal sync
output wire o_vs, // vertical sync
output wire o_blanking, // high during blanking interval
output wire o_active, // high during active pixel drawing
output wire o_screenend, // high for one tick at the end of screen
output wire o_animate, // high for one tick at end of active drawing
output wire [9:0] o_x, // current pixel x position
output wire [8:0] o_y // current pixel y position
);
// VGA timings https://timetoexplore.net/blog/video-timings-vga-720p-1080p
localparam HS_STA = 16; // horizontal sync start
localparam HS_END = 16 + 96; // horizontal sync end
localparam HA_STA = 16 + 96 + 48; // horizontal active pixel start
localparam VS_STA = 480 + 10; // vertical sync start
localparam VS_END = 480 + 10 + 2; // vertical sync end
localparam VA_STA = 60; // vertical active pixel start
localparam VA_END = 420; // vertical active pixel end
localparam LINE = 800; // complete line (pixels)
localparam SCREEN = 525; // complete screen (lines)
reg [9:0] h_count; // line position
reg [9:0] v_count; // screen position
// generate sync signals (active low for 640x480)
assign o_hs = ~((h_count >= HS_STA) & (h_count < HS_END));
assign o_vs = ~((v_count >= VS_STA) & (v_count < VS_END));
// keep x and y bound within the active pixels
assign o_x = ((h_count < HA_STA) ? 0 : (h_count - HA_STA)) >> 1;
assign o_y = ((v_count >= VA_END) ?
(VA_END - VA_STA - 1) : (v_count - VA_STA)) >> 1;
// blanking: high within the blanking period
assign o_blanking = ((h_count < HA_STA) | (v_count > VA_END - 1));
// active: high during active pixel drawing
assign o_active = ~((h_count < HA_STA) |
(v_count > VA_END - 1) |
(v_count < VA_STA));
// screenend: high for one tick at the end of the screen
assign o_screenend = ((v_count == SCREEN - 1) & (h_count == LINE));
// animate: high for one tick at the end of the final active pixel line
assign o_animate = ((v_count == VA_END - 1) & (h_count == LINE));
always @ (posedge i_clk)
begin
if (i_rst) // reset to start of frame
begin
h_count <= 0;
v_count <= 0;
end
if (i_pix_stb) // once per pixel
begin
if (h_count == LINE) // end of line
begin
h_count <= 0;
v_count <= v_count + 1;
end
else
h_count <= h_count + 1;
if (v_count == SCREEN) // end of screen
v_count <= 0;
end
end
endmodule
记住我?
我们使用与前一部分完全相同的sram模块。
添加一个名为sram.v
[ view source ] 的设计源:
module sram #(parameter ADDR_WIDTH=8, DATA_WIDTH=8, DEPTH=256, MEMFILE="") (
input wire i_clk,
input wire [ADDR_WIDTH-1:0] i_addr,
input wire i_write,
input wire [DATA_WIDTH-1:0] i_data,
output reg [DATA_WIDTH-1:0] o_data
);
reg [DATA_WIDTH-1:0] memory_array [0:DEPTH-1];
initial begin
if (MEMFILE > 0)
begin
$display("Loading memory init file '" + MEMFILE + "' into array.");
$readmemh(MEMFILE, memory_array);
end
end
always @ (posedge i_clk)
begin
if(i_write) begin
memory_array[i_addr] <= i_data;
end
else begin
o_data <= memory_array[i_addr];
end
end
endmodule
精灵表
我们的设计中有八个精灵。每个精灵都是32 x 32像素。我们将它们存储在一个32 x 256像素的8位PNG中。您可以在下方看到我们的精灵(旋转到水平线以更好地适应此页面)。
我使用了与第2部分相同的FPGATools脚本将PNG精灵表转换为Verilog内存初始化格式。
- 从教程git repo中复制sprites.mem和sprites_palette.mem
- 在Vivado中选择“添加源”,然后选择“设计源”和“文件类型:内存初始化文件”
- 找到
sprites.mem
并sprites_palette.mem
选择“确定”,然后选择“完成”
如果你想创建自己的精灵,你可以。在一个32x256图像上保存8个32x32像素精灵,然后使用FPGATools中的img2fmem.py
脚本生成内存初始化文件。
随着精灵加载到项目中,我们已准备好绘图!
太空空间
我们将采用一种简单的精灵方法:每次绘制帧中的每个像素。我们从背景开始,然后添加我们的船。这种方法很浪费,因为我们最终重绘了没有改变的像素,但是无需跟踪需要重绘的内容。
要创建我们的背景,我们将背景精灵平铺以填充屏幕。当我们在一个缓冲区中绘制精灵时,我们将把另一个缓冲区输出到VGA监视器。
创建一个top.v
使用以下设计[ view source ] 调用的设计源:
module top(
input wire CLK, // board clock: 100 MHz on Arty/Basys3/Nexys
input wire RST_BTN, // reset button
input wire [3:0] sw, // four switches
output wire VGA_HS_O, // horizontal sync output
output wire VGA_VS_O, // vertical sync output
output reg [3:0] VGA_R, // 4-bit VGA red output
output reg [3:0] VGA_G, // 4-bit VGA green output
output reg [3:0] VGA_B // 4-bit VGA blue output
);
wire rst = ~RST_BTN; // reset is active low on Arty & Nexys Video
// wire rst = RST_BTN; // reset is active high on Basys3 (BTNC)
// generate a 25 MHz pixel strobe
reg [15:0] cnt;
reg pix_stb;
always @(posedge CLK)
{pix_stb, cnt} <= cnt + 16'h4000; // divide by 4: (2^16)/4 = 0x4000
wire [9:0] x; // current pixel x position: 10-bit value: 0-1023
wire [8:0] y; // current pixel y position: 9-bit value: 0-511
wire blanking; // high within the blanking period
wire active; // high during active pixel drawing
wire screenend; // high for one tick at the end of screen
wire animate; // high for one tick at end of active drawing
vga320x180 display (
.i_clk(CLK),
.i_pix_stb(pix_stb),
.i_rst(rst),
.o_hs(VGA_HS_O),
.o_vs(VGA_VS_O),
.o_x(x),
.o_y(y),
.o_blanking(blanking),
.o_active(active),
.o_screenend(screenend),
.o_animate(animate)
);
// VRAM frame buffers (read-write)
localparam SCREEN_WIDTH = 320;
localparam SCREEN_HEIGHT = 180;
localparam VRAM_DEPTH = SCREEN_WIDTH * SCREEN_HEIGHT;
localparam VRAM_A_WIDTH = 16; // 2^16 > 320 x 180
localparam VRAM_D_WIDTH = 8; // colour bits per pixel
reg [VRAM_A_WIDTH-1:0] address_a, address_b;
reg [VRAM_D_WIDTH-1:0] datain_a, datain_b;
wire [VRAM_D_WIDTH-1:0] dataout_a, dataout_b;
reg we_a = 0, we_b = 1; // write enable bit
// frame buffer A VRAM
sram #(
.ADDR_WIDTH(VRAM_A_WIDTH),
.DATA_WIDTH(VRAM_D_WIDTH),
.DEPTH(VRAM_DEPTH),
.MEMFILE(""))
vram_a (
.i_addr(address_a),
.i_clk(CLK),
.i_write(we_a),
.i_data(datain_a),
.o_data(dataout_a)
);
// frame buffer B VRAM
sram #(
.ADDR_WIDTH(VRAM_A_WIDTH),
.DATA_WIDTH(VRAM_D_WIDTH),
.DEPTH(VRAM_DEPTH),
.MEMFILE(""))
vram_b (
.i_addr(address_b),
.i_clk(CLK),
.i_write(we_b),
.i_data(datain_b),
.o_data(dataout_b)
);
// sprite buffer (read-only)
localparam SPRITE_SIZE = 32; // dimensions of square sprites in pixels
localparam SPRITE_COUNT = 8; // number of sprites in buffer
localparam SPRITEBUF_D_WIDTH = 8; // colour bits per pixel
localparam SPRITEBUF_DEPTH = SPRITE_SIZE * SPRITE_SIZE * SPRITE_COUNT;
localparam SPRITEBUF_A_WIDTH = 13; // 2^13 == 8,096 == 32 x 256
reg [SPRITEBUF_A_WIDTH-1:0] address_s;
wire [SPRITEBUF_D_WIDTH-1:0] dataout_s;
// sprite buffer memory
sram #(
.ADDR_WIDTH(SPRITEBUF_A_WIDTH),
.DATA_WIDTH(SPRITEBUF_D_WIDTH),
.DEPTH(SPRITEBUF_DEPTH),
.MEMFILE("sprites.mem"))
spritebuf (
.i_addr(address_s),
.i_clk(CLK),
.i_write(0), // read only
.i_data(0),
.o_data(dataout_s)
);
reg [11:0] palette [0:255]; // 256 x 12-bit colour palette entries
reg [11:0] colour;
initial begin
$display("Loading palette.");
$readmemh("sprites_palette.mem", palette);
end
// sprites to load and position of player sprite in frame
localparam SPRITE_BG_INDEX = 7; // background sprite
localparam SPRITE_PL_INDEX = 0; // player sprite
localparam SPRITE_BG_OFFSET = SPRITE_BG_INDEX * SPRITE_SIZE * SPRITE_SIZE;
localparam SPRITE_PL_OFFSET = SPRITE_PL_INDEX * SPRITE_SIZE * SPRITE_SIZE;
localparam SPRITE_PL_X = SCREEN_WIDTH - SPRITE_SIZE >> 1; // centre
localparam SPRITE_PL_Y = SCREEN_HEIGHT - SPRITE_SIZE; // bottom
reg [9:0] draw_x;
reg [8:0] draw_y;
reg [9:0] pl_x = SPRITE_PL_X;
reg [9:0] pl_y = SPRITE_PL_Y;
reg [9:0] pl_pix_x;
reg [8:0] pl_pix_y;
// pipeline registers for for address calculation
reg [VRAM_A_WIDTH-1:0] address_fb1;
reg [VRAM_A_WIDTH-1:0] address_fb2;
always @ (posedge CLK)
begin
// reset drawing
if (rst)
begin
draw_x <= 0;
draw_y <= 0;
pl_x <= SPRITE_PL_X;
pl_y <= SPRITE_PL_Y;
pl_pix_x <= 0;
pl_pix_y <= 0;
end
// draw background
if (address_fb1 < VRAM_DEPTH)
begin
if (draw_x < SCREEN_WIDTH)
draw_x <= draw_x + 1;
else
begin
draw_x <= 0;
draw_y <= draw_y + 1;
end
// calculate address of sprite and frame buffer (with pipeline)
address_s <= SPRITE_BG_OFFSET +
(SPRITE_SIZE * draw_y[4:0]) + draw_x[4:0];
address_fb1 <= (SCREEN_WIDTH * draw_y) + draw_x;
address_fb2 <= address_fb1;
if (we_a)
begin
address_a <= address_fb2;
datain_a <= dataout_s;
end
else
begin
address_b <= address_fb2;
datain_b <= dataout_s;
end
end
if (pix_stb) // once per pixel
begin
if (we_a) // when drawing to A, output from B
begin
address_b <= y * SCREEN_WIDTH + x;
colour <= active ? palette[dataout_b] : 0;
end
else // otherwise output from A
begin
address_a <= y * SCREEN_WIDTH + x;
colour <= active ? palette[dataout_a] : 0;
end
if (screenend) // switch active buffer once per frame
begin
we_a <= ~we_a;
we_b <= ~we_b;
// reset background position at start of frame
draw_x <= 0;
draw_y <= 0;
// reset player position
pl_pix_x <= 0;
pl_pix_y <= 0;
// reset frame address
address_fb1 <= 0;
end
end
VGA_R <= colour[11:8];
VGA_G <= colour[7:4];
VGA_B <= colour[3:0];
end
endmodule
记忆延迟
您可能已经发现了两个不寻常的寄存器:address_fb1
和address_fb2
。
address_fb1 <= (sprite_pos_y + SPRITE_Y) * SCREEN_WIDTH
+ sprite_pos_x + SPRITE_X;
address_fb2 <= address_fb1;
...
address_a <= address_fb2;
我们使用的内存有两个时钟周期延迟:需要两个时钟周期才能从子画面缓冲区中检索像素颜色。因此,对帧缓冲器地址的写入也需要延迟两个时钟周期。如果我们不这样做,我们的精灵将被画到右边两个像素。
如果您没有使用Arty,Basys 3或Nexys视频板,则可能需要通过移除或插入其他寄存器来调整此延迟的长度。
电路板编程
约束
从Time To Explore git repo中获取适当的约束文件并将其添加到项目中:
注意:尚未在S7-50T上测试滑动开关约束。请报告任何成功或失败@WillFlux ·
作为参考,Nexys视频约束如下所示:
## FPGA VGA Graphics Part 3: Nexys Video Board Constraints
## Clock
set_property -dict {PACKAGE_PIN R4 IOSTANDARD LVCMOS33} [get_ports {CLK}];
create_clock -add -name sys_clk_pin -period 10.00 \
-waveform {0 5} [get_ports {CLK}];
## Reset Button (active low)
set_property -dict {PACKAGE_PIN G4 IOSTANDARD LVCMOS15} [get_ports {RST_BTN}];
## Slide Switches
set_property -dict {PACKAGE_PIN E22 IOSTANDARD LVCMOS12} [get_ports {sw[0]}];
set_property -dict {PACKAGE_PIN F21 IOSTANDARD LVCMOS12} [get_ports {sw[1]}];
set_property -dict {PACKAGE_PIN G21 IOSTANDARD LVCMOS12} [get_ports {sw[2]}];
set_property -dict {PACKAGE_PIN G22 IOSTANDARD LVCMOS12} [get_ports {sw[3]}];
## VGA Pmod Header JB
set_property -dict {PACKAGE_PIN V9 IOSTANDARD LVCMOS33} [get_ports {VGA_R[0]}];
set_property -dict {PACKAGE_PIN V8 IOSTANDARD LVCMOS33} [get_ports {VGA_R[1]}];
set_property -dict {PACKAGE_PIN V7 IOSTANDARD LVCMOS33} [get_ports {VGA_R[2]}];
set_property -dict {PACKAGE_PIN W7 IOSTANDARD LVCMOS33} [get_ports {VGA_R[3]}];
set_property -dict {PACKAGE_PIN W9 IOSTANDARD LVCMOS33} [get_ports {VGA_B[0]}];
set_property -dict {PACKAGE_PIN Y9 IOSTANDARD LVCMOS33} [get_ports {VGA_B[1]}];
set_property -dict {PACKAGE_PIN Y8 IOSTANDARD LVCMOS33} [get_ports {VGA_B[2]}];
set_property -dict {PACKAGE_PIN Y7 IOSTANDARD LVCMOS33} [get_ports {VGA_B[3]}];
## VGA Pmod Header JC
set_property -dict {PACKAGE_PIN Y6 IOSTANDARD LVCMOS33} [get_ports {VGA_G[0]}];
set_property -dict {PACKAGE_PIN AA6 IOSTANDARD LVCMOS33} [get_ports {VGA_G[1]}];
set_property -dict {PACKAGE_PIN AA8 IOSTANDARD LVCMOS33} [get_ports {VGA_G[2]}];
set_property -dict {PACKAGE_PIN AB8 IOSTANDARD LVCMOS33} [get_ports {VGA_G[3]}];
set_property -dict {PACKAGE_PIN R6 IOSTANDARD LVCMOS33} [get_ports {VGA_HS_O}];
set_property -dict {PACKAGE_PIN T6 IOSTANDARD LVCMOS33} [get_ports {VGA_VS_O}];
创建和编程
运行synthesis, implementation, bitstream generation。如果您需要有关如何执行此操作的提醒,请参阅FPGA介绍性文章。
接下来,将VGA Pmod连接到Arty上的中间两个连接器(JB和JC),然后使用VGA线将显示器连接到VGA Pmod。Basys 3用户可以将VGA线缆直接连接到他们的主板上。最后,通过USB将您的主板连接到您的计算机并进行编程vga03/vga03.runs/impl_1/top.bit
。
你应该看到一个紫色的星球区域。如果你的屏幕是黑的,检查你正确添加sprites.mem
,并sprites_palette.mem
到项目中。
Ready 玩家一
接下来,我们在屏幕底部添加我们的玩家船精灵。
在其中top.v
,在绘制背景块之后和行if (pix_stb)
[ 查看完整源 ] 之前添加以下内容:
// draw player ship
if (address_fb1 >= VRAM_DEPTH) // background drawing is finished
begin
if (pl_pix_y < SPRITE_SIZE)
begin
if (pl_pix_x < SPRITE_SIZE - 1)
pl_pix_x <= pl_pix_x + 1;
else
begin
pl_pix_x <= 0;
pl_pix_y <= pl_pix_y + 1;
end
address_s <= SPRITE_PL_OFFSET
+ (SPRITE_SIZE * pl_pix_y) + pl_pix_x;
address_fb1 <= SCREEN_WIDTH * (pl_y + pl_pix_y)
+ pl_x + pl_pix_x;
address_fb2 <= address_fb1;
if (we_a)
begin
address_a <= address_fb2;
datain_a <= dataout_s;
end
else
begin
address_b <= address_fb2;
datain_b <= dataout_s;
end
end
end
重新生成比特流并再次编程您的电路板。您应该在屏幕底部看到一艘船。
学习飞行
静态船只不是飞船。让我们用FPGA板上的滑动开关控制船的位置。我们使用标有4个滑动开关SW0
到SW3
。
将以下内容添加到if (screenend)
块的底部,在address_fb1 <= 0;
[ 查看完整源代码 ] 行下方:
// update ship position based on switches
if (sw[0] && pl_x < SCREEN_WIDTH - SPRITE_SIZE)
pl_x <= pl_x + 1;
if (sw[1] && pl_x > 0)
pl_x <= pl_x - 1;
if (sw[2] && pl_y < SCREEN_HEIGHT - SPRITE_SIZE)
pl_y <= pl_y + 1;
if (sw[3] & pl_y > 0)
pl_y <= pl_y - 1;
重新生成比特流并再次编程您的电路板。
尝试尝试使用滑动开关(不是按钮;它们不会做任何事情)。您可以通过组合开关使您的船沿对角方向移动,例如SW1
和SW3
。
您还可以通过更新相关行来更改绘制的精灵top.v
:
localparam SPRITE_BG_INDEX = 7; // background sprite
localparam SPRITE_PL_INDEX = 0; // player sprite
例如,如果SPRITE_PL_INDEX
设置为4
,那么您将获得外星飞船。
额外奖励:Super VGA和Hires Sprites
如果你足够幸运拥有一个Nexys Video(或其他至少具有9 Mbit BRAM的FPGA),那么尝试创建一个800x600缓冲区并使用高分辨率精灵:hires_sprites.mem和hires_sprites_palette.mem。
这些精灵是128x128像素,因此您需要将精灵缓冲区的大小增加到128 * 1024,将地址宽度(SPRITEBUF_A_WIDTH
)增加到17. 如果您需要帮助创建800x600 VGA驱动程序并更新您的,请参阅第1部分的结尾顶级模块。
下一步是什么?
我正在编写下一部分,它将利用其他精灵来添加小行星和外星人。关注@WillFlux以获取更新。
在Time to Explore FPGA Index中查找有关FPGA和Verilog的更多信息。
©2018-2019 Will Green。
图片来源: 示例太空船游戏图形来自KenneyNL,属于公开渠道。