基于FPGA的VGA图形教程一二三

Sytek梭鱼智能_Digilent一级代理    技术分享    基于FPGA的VGA图形教程一二三

Verilog1部分中的FPGA VGA图形

2017127 教程中

介绍

本教程系列介绍了使用FPGA进行视频图形编程,从创建VGA驱动程序开始,转向更高级的功能,包括位图,精灵和效果。FPGA在高速I / O和自定义逻辑方面表现出色:您会惊讶地发现,使用几行Verilog可以实现多少。

该系列是围绕Digilent ArtyBasys 3Nexys Video板设计的。如果你正在使用ArtyNexys Video,你还需要Pmod VGABasys 3内置VGA输出)。详细要求如下。

我假设您对Verilog有基本的了解,并且习惯使用XilinxVivado软件。如果您是FPGA开发的新手,请先试用VerilogVivado入门

完成本教程后,转到2部分,我们将介绍位图显示。

github.com/WillGreen/timetoexplore查找此教程和其他FPGA教程的代码和资源。

欢迎收到@WillFlux反馈。20191月更新。

要求

1.       Digilent Arty A7-35TArty S7-50TBasys 3Nexys Video板(其他板卡参见下文)

2.       Pmod VGA如果使用ArtyNexys VideoBasys 3内置VGA

3.       支持VGA的显示器和电缆

4.       Micro USB线缆可为电路板编程和供电

5.       安装Xilinx Vivado(包括Digilent板文件)

其他FPGA

如果您使用的是不同的FPGA板,您仍然可以通过对top.v模块进行一些更改来学习本教程:

1.       硬件I / O:更新硬件端口,例如CLKVGA_R,以匹配您的主板。

2.       时钟:如果您的电路板时钟不是100 MHz,则需要更新像素时钟码。

3.       VGA输出:如果您的VGA输出不是每种颜色4位,请调整VGA分配语句。

 

VGA如何工作

VGA是使用15D-sub连接器的模拟视频标准。它不需要高时钟速度或复杂的编码,因此在学习FPGA图形时是一个很好的起点。VGA有五个主要信号引脚:红色,绿色和蓝色各一个,同步两个。水平同步划分一条线垂直同步划分屏幕,也称为

VGA信号有两个阶段:绘制像素和消隐间隔。同步信号出现在消隐间隔内通过显示前沿显示沿与像素绘画分开。当开发VGA时,监视器基于阴极射线管(CRT):消隐间隔给出电压电平稳定的时间,并使电子枪返回到线或屏幕的开始。

在本文中,我们将创建一个经典的640x480 60 Hz VGA显示器。所需的像素时钟25 MHz 1,这是我们电路板100 MHz时钟的小部分。产生有效VGA信号的关键是获得正确的时序。为简单起见,我们将按像素和线条进行计时。每个像素是25 MHz像素时钟(40 ns)的刻度。一条线是一组完整的水平像素。

img1

水平像素时序

·       显示前沿Front Porch16

·       同步脉冲Sync Pulse96

·       显示后沿Back Porch48

·       有效视频Active Pixels640

·       总像素Total pixels800

垂直线时序20191月更新)

·       有效像素Active Pixels480

·       显示前沿Front Porch10

·       同步脉冲Sync Pulse2

·       显示后沿Back Porch33

·       Total lines总线数525

要了解有关显示时序的更多信息,包括高清,请参阅视频时序:VGASVGA720P1080P

VGA模块

Vivado创建一个新的RTL项目,调用vga01ArtyBasys 3Nexys Video板作为目标。如果您需要有关项目创建的建议,请参阅我的入门教程系列的1部分

在新项目中创建一个名为vga640x480.vview 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_xo_y表示可见640x480显示屏内的水平和垂直位置:这是您用于定位和绘制图形的位置。h_countv_count表示自行或屏幕开始以来发生的像素和行数(包括消隐间隔):这些用于同步信号。对于640x480同步脉冲是低电平有效,因此我们~在分配它们时使用。现在你可以忽略其他输出它们将在以后使用。

在我们开始绘制图形之前,我们需要了解我们的模块如何生成25 MHz时钟和i_pix_stb输入的作用。

Running to Time

我们的640x480 60 Hz VGA信号需要25 MHz像素时钟,但我们的主板具有100 MHz基本时钟。我们如何有效地将基准时钟除以4

42的幂,所以我们可以使用一个简单的计数器,但在一般情况下这没用。相反,我们将采用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,因此我们将(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比特,使我们每个输出16VGA_RVGA_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

例如,如果x210y150,那么我们就在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中获取适当的约束文件并将其添加到项目中:

·       Arty A7-35T

·       Arty S7-50T

·       Basys3

·       Nexys Video

作为参考,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连接到ArtyNexys Video上的中间两个连接器(JBJC),然后使用VGA线将显示器连接到Pmod VGABasys 3用户只需将VGA线缆直接连接到他们的主板上即可。最后,通过USB将您的主板连接到您的计算机并进行编程vga01/vga01.runs/impl_1/top.bit

你应该在屏幕上看到四个重叠的方块从左到右:绿色,红色,蓝色,绿色。如果颜色错误,请检查约束文件。

 img2

还不错,但由于像素时钟为25 MHz,而不是25.175 MHz,我们的规格略低于规格:fH应为31.469 kHzfV60.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的动画(33800像素,每40ns长)对于我们简单的方块,这很充足。

方形模块

为了表示动画方块,我们将创建一个新模块。我们将把坐标放在广场的中心。这对于正方形并没有太大的区别,但是一旦我们开始处理更复杂的形状和精灵就会有意义。

i_animate高时,正方形每帧一次在水平和垂直方向上移动一个像素。当它到达屏幕边缘时,它会切换方向。

添加一个名为square.vview 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_SIZEIX_DIRIY_DIR

额外奖励:Super VGA

现在你已经掌握了640x480,为什么不尝试将设计更新到800x600?您需要的所有数据都在视频计时中:VGASVGA720P1080P。以下设计指针应该让您入门:

·       您需要40 MHz像素时钟,因此时钟选通需要更新

·       SVGA的总像素大小(包括空白)为1056x628,因此您需要更多位来表示xy坐标

·       同步信号为高电平(正),为800x600

如果您遇到困难,可以查看工作示例:vga800x600.vtop_static_800x600.v

下一步是什么?

我们设法做了很多逻辑。但是,虽然直接从像素位置绘制可以管理几个正方形,但对于任何更复杂的东西来说很快就会变得很麻烦。为了允许更复杂的图形,我们需要存储器来存储和组合值。这是下一部分的主题。

Time to Explore FPGA Index查找有关FPGAVerilog的更多信息。

1:严格来说,该规范要求25.175MHz像素时钟,但大多数VGA显示器都能容忍这一点。

 

Verilog2部分中的FPGA VGA图形

2018222 教程中

介绍

欢迎使用Digilent ArtyBasys 3Nexys Video板回到我的FPGA图形教程系列。在1部分中,我们创建了一个VGA模块,并用它来为屏幕上的方块设置动画。在第二部分中,我们介绍了位图显示。在本教程结束时,您将能够将自己的映像加载到FPGA内存中并将其显示在监视器上。

完成本教程后,请转到3部分中的精灵动画和双缓冲。

github.com/WillGreen/timetoexplore查找此教程和其他FPGA教程的代码和资源。

欢迎收到@WillFlux反馈。20191月更新。

要求

该部分的要求与1部分相同:

  1. Digilent ArtyBasys 3Nexys Video板(其他板卡参见下文)
  2. Pmod VGA如果使用ArtyNexys VideoBasys 3内置VGA
  3. 支持VGA的显示器和电缆
  4. Micro USB线缆可为电路板编程和供电
  5. 安装Xilinx Vivado(包括Digilent板文件)

其他FPGA

本教程需要至少1,350 Kbits的板载FPGA内存(块或分布式RAM)。如果您可以满足ram要求,则通过对top.v模块进行更改,可以相对容易地使本教程适应其他板:

  1. 硬件I / O:更新硬件端口,例如CLKVGA_R,以匹配您的主板。
  2. 时钟:如果您的电路板时钟不是100 MHz,则需要更新像素时钟码。
  3. VGA输出:如果您的VGA输出不是每种颜色4位,请调整VGA分配语句。

位图

位图显示由内存阵列支持:每个像素对应于内存中的位置。要创建显示,我们从其内存位置读出当前像素的值。要设置像素的颜色,我们修改其相关内存位置的内容。位图提供了两个显着的好处:我们可以使用我们喜欢的任何技术自由创建复杂的图形,并且像素颜色的设置与实际的屏幕绘制过程分开。位图显示的一大缺点是我们需要大量内存来存储像素数据。

内存要求:分辨率和颜色深度

第一个考虑因素是VGA输出的能力:它支持红色,绿色和蓝色各4位,每像素总共12位。如果我们使用标准640x480 VGA显示器每像素12位,我们的存储器阵列需要3,600 Kbits640 x 480 x 12= 3,686,400位。但是,我们的XC7A35T FPGA只有1,800 Kbits的块ram。虽然Arty板还包含256 MBDDR3L内存,但Basys3却没有,而使用DDR内存会大大增加我们设计的复杂性。

为了使我们的设计适合块内存,我们将屏幕分辨率降低到640 x 360,并将自己限制为6位颜色:640 x 360 x 6= 1,350 Kbits640 x 360的另一个好处是具有169的宽屏比例,可与现代显示器和电视相匹配。

64种颜色

如果每个像素由6位表示,则它可以具有664种可能的颜色。如果我们分配2位给每个红,绿,蓝,那么每种颜色可以有4个二进制值之一:000110,或11。但是,我们的VGA输出是12位:它预计每像素4位(015)。要使用显示器的全亮度,我们需要将2位颜色扩展为4为此,我们复制了导致四个可能值的位: 00000),01015),101010)和111115)。

现代计算机显示器通常是24位,即它们可以显示1670万种颜色。为了感受我们固定的64种颜色的样子,我创建了一个样本:

麻烦地看看我们如何使用6位值直接驱动显示器,我们不会这样做!相反,我们将使用一点间接来将我们的颜色可能性扩展到4,096的全范围。

间接:索引颜色和调色板

它可以代表调色板查找表中的颜色,而不是每个表示颜色的6位值。如果特定像素具有值001010(十),那么我们参考颜色表中的第十个条目,其中包含要使用的12位颜色。虽然我们仍然限制为总共64种颜色,但它们现在可以是VGA输出能够产生的4,096种颜色中的任何64种颜色。例如,森林的图片可能有很多绿色,而一个城市的图片会使用更多的灰色。这种设计在较旧的计算机中很常见,例如,原始的Amiga芯片组支持32种颜色,可能是4,096种:与我们的设计非常相似!GIFPNG格式仍然使用这种方法来挤出256色图像中的最佳质量。

如果您有兴趣了解有关不同调色板的更多信息,可以在维基百科上找到一个很好的调色板列表页面。

这就是足够的色彩理论,让我们继续创造一个形象!

FPGA内存准备映像

为了显示我们的图像,我们需要一种机制来将它和它的调色板加载到我们的设计中。Verilog支持一种名为的方法$readmemh,可以从十六进制值的文本文件中将值加载到内存中。我创建了一个方便的Python脚本(img2fmem.py),为图像及其调色板生成合适的文件。它可以使用优化的12位调色板将源图像转换为6位颜色。

如果您不想准备自己的图像,那么您可以使用现成的图形并跳到下一部分(存储器映射)。

使用img2fmem.py

img2fmem.pyGithub上的FPGATools存储库中获取一份副本。

img2fmem.py需要Pillow包运行。您可以使用Pythonpip工具安装它:

pip install pillow

使用图像编辑器将图像裁剪/调整为640 x 360像素,并将其保存为JPEGPNGBMPTIFF格式。注意。关键的是您的图像正好是640 x 360像素,否则将无法正确显示。

获得合适的源图像后,可以运行以下命令(替换game.png为图像文件名):

python img2fmem.py game.png 6 mem

完成后,您应该在与源图像相同的目录中找到三个新文件:

  • game_preview.png - 您可以查看的普通PNG以预览图像转换
  • game.mem - 图像的文本十六进制版本
  • game_palette.mem - 图像调色板的文本十六进制版本

Web浏览器或图像编辑器中加载预览以检查转换是否正常。

如果在文本编辑器中打开调色板文件,您将看到图像的6412位调色板值。

现在我们有一个准备好的图像,是时候把它加载到内存中了。

内存映射

Vivado中创建一个新的RTL项目,vga02并使用适当的板作为目标。如果您需要有关项目创建的建议,请参阅我的入门教程系列的1部分

我们需要创建一个内存数组来保存我们的图像数据。为此,我们将使用一个sram适用于FPGAram 的简单模块。

添加一个名为sram.vview 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_END420
  • 我们VA_STAassign 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.memgame_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位作为地址,因为18是最大值大于640 x 360

要了解有关存储器初始化的更多信息,请参阅VerilogFPGA菜谱条目Initialize Memory

将图像文件添加到项目

要使Vivado能够找到图像文件和调色板,您需要将它们添加到项目中。您可以像设计源一样使用Add Sources,然后选择design sourcesFiles of type: Memory Initialization Files,然后找到game.memgame_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连接到ArtyNexys Video上的中间两个连接器(JBJC),然后使用VGA线将显示器连接到Pmod VGABasys 3用户只需将VGA线缆直接连接到他们的主板上即可。最后,通过USB将您的主板连接到您的计算机并进行编程vga02/vga02.runs/impl_1/top.bit

您应该看到显示的位图图像。如果图像失真或颜色错误,请确保您的图像文件正好是640 x 360,并且您已经加载了正确的位图和调色板文件top.v

额外奖励:超级VGA位图

如果你有幸拥有Nexys视频(或其他具有至少4 MbitramFPGA),那么请尝试使用8800x600位图。更高的分辨率和颜色深度可以获得更好的效果,特别是对于照片。如果您在创建800x600 VGA驱动程序和更新顶层模块时需要帮助,请参阅第1部分的结尾。

下一步是什么?

现在我们有了一个位图显示器,我们已准备好尝试精灵和双缓冲。这是第3部分的主题。

 

Verilog3部分中的FPGA VGA图形

2018611 教程中

介绍

欢迎使用Digilent主板回到我的FPGA图形教程系列的第三部分。在2部分中,我们将位图显示和加载的图形文件引入内存。在第三部分中,我们使用位图和双缓冲动画精灵:这种经典技术是2D游戏的主要内容。在本教程结束时,您将能够使用FPGA板上的开关控制精灵。

github.com/WillGreen/timetoexplore查找此教程和其他FPGA教程的代码和资源。

要求

该部分的要求与前面的部分相同:

  1. Digilent Arty A7-35TArty S7-50TBasys 3Nexys Video板(其他板卡参见下文)
  2. VGA Pmod如果使用ArtyNexys视频(Basys 3内置VGA
  3. 支持VGA的显示器和电缆
  4. Micro USB线缆可为电路板编程和供电
  5. 安装Xilinx Vivado(包括Digilent板文件)

其他FPGA

本教程需要至少964 Kbits的板载FPGA内存(块或分布式RAM)。如果您可以满足ram要求,则通过对top.v模块进行更改,可以相对容易地使本教程适应其他板:

  1. 硬件I / O:更新硬件端口,例如CLKVGA_R,以匹配您的主板。
  2. 时钟:如果您的电路板时钟不是100 MHz,则需要更新像素时钟码。
  3. VGA输出:如果您的VGA输出不是每种颜色4位,请调整VGA分配语句。

双缓冲

双缓冲的原理很简单:在从另一个驱动显示器的同时在一个缓冲区上绘制。您有一整帧(1/60秒)来创建图像,同时避免任何屏幕撕裂。有两个缺点:您需要两倍的内存,并且您将延迟增加一帧。

除了我们的两个缓冲区的内存,我们还需要一些内存来存储精灵。因此,我们将分辨率降低了一半,达到320x180320 x 180 x 8位缓冲器需要450 KbitRAM; 我们的FPGA上有1,800 Kbits的块内存,这提供了充足的摆动空间。

通过将分辨率减半,我们可以轻松实现驱动程序:使用右移可以有效地实现除以。在Verilog中,>>操作员实现了右移就像在CGoPython中一样。

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.vview 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像素的8PNG中。您可以在下方看到我们的精灵(旋转到水平线以更好地适应此页面)。

我使用了与2部分相同的FPGATools脚本将PNG精灵表转换为Verilog内存初始化格式。

  1. 教程git repo复制sprites.memsprites_palette.mem
  2. Vivado中选择添加源,然后选择设计源文件类型:内存初始化文件
  3. 找到sprites.memsprites_palette.mem选择确定,然后选择完成

如果你想创建自己的精灵,你可以。在一个32x256图像上保存832x32像素精灵,然后使用FPGAToolsimg2fmem.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_fb1address_fb2

address_fb1 <= (sprite_pos_y + SPRITE_Y) * SCREEN_WIDTH 
    + sprite_pos_x + SPRITE_X;
address_fb2 <= address_fb1;
...
address_a <= address_fb2;

我们使用的内存有两个时钟周期延迟:需要两个时钟周期才能从子画面缓冲区中检索像素颜色。因此,对帧缓冲器地址的写入也需要延迟两个时钟周期。如果我们不这样做,我们的精灵将被画到右边两个像素。

如果您没有使用ArtyBasys 3Nexys视频板,则可能需要通过移除或插入其他寄存器来调整此延迟的长度。

 

 

电路板编程

约束

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上的中间两个连接器(JBJC),然后使用VGA线将显示器连接到VGA PmodBasys 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个滑动开关SW0SW3

将以下内容添加到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;

重新生成比特流并再次编程您的电路板。

尝试尝试使用滑动开关(不是按钮;它们不会做任何事情)。您可以通过组合开关使您的船沿对角方向移动,例如SW1SW3

您还可以通过更新相关行来更改绘制的精灵top.v

localparam SPRITE_BG_INDEX = 7;  // background sprite
localparam SPRITE_PL_INDEX = 0;  // player sprite

例如,如果SPRITE_PL_INDEX设置为4,那么您将获得外星飞船。

额外奖励:Super VGAHires Sprites

如果你足够幸运拥有一个Nexys Video(或其他至少具有9 Mbit BRAMFPGA),那么尝试创建一个800x600缓冲区并使用高分辨率精灵:hires_sprites.memhires_sprites_palette.mem

这些精灵是128x128像素,因此您需要将精灵缓冲区的大小增加到128 * 1024,将地址宽度(SPRITEBUF_A_WIDTH)增加到17. 如果您需要帮助创建800x600 VGA驱动程序并更新您的,请参阅第1部分的结尾顶级模块。

下一步是什么?

我正在编写下一部分,它将利用其他精灵来添加小行星和外星人。关注@WillFlux以获取更新。

Time to Explore FPGA Index查找有关FPGAVerilog的更多信息。

©2018-2019 Will Green

图片来源: 示例太空船游戏图形来自KenneyNL,属于公开渠道。

 

 

2022年8月17日 13:37
浏览量:0
收藏