11 June 2021

Verilog Simulation with Verilator and SDL

It can be challenging to test your FPGA or ASIC graphics designs. You can perform low-level behavioural simulations and examine waveforms, but you also need to verify how the video output will appear on the screen.

By combining Verilator and SDL, you can build Verilog simulations that let you see your design on your computer. The thought of creating a graphical simulation can be intimidating, but it’s surprisingly simple: you can get a simulation running in under an hour.

Updated 2021-06-12. Feedback appreciated: get in touch with @WillFlux or open an issue on GitHub.

Design Sources

The C++ and Verilog designs featured in this post are available from the projf-explore git repo under the open-source MIT licence: build on them to your heart’s content. The rest of the blog content is subject to standard copyright restrictions: don’t republish it without permission.

SystemVerilog
We’ll use a few choice features from SystemVerilog to make Verilog a little more pleasant. If you’re familiar with Verilog, you’ll have no trouble. All the SystemVerilog features used are compatible with recent versions of Verilator, Yosys, and Xilinx Vivado.

Contents

The following screenshot shows a simulation of the bounce design from the FPGA Graphics tutorial.

Simulating top bounce

Verilator & SDL

Verilator is a fast simulator that generates C++ simulations of Verilog designs. SDL (LibSDL) is a cross-platform library that provides low-level access to graphics hardware. Verilator generates a C++ model of your graphics hardware that SDL draws to a window on your PC.

Verilator supports multi-threaded designs, but I’ve stuck to single-threaded designs for graphics testing. Provided your graphics hardware isn’t too complex, a single-threaded Verilator sim manages around one frame per second on a modern PC.

The process is straightforward, even if you’ve never written a line of C++ in your life. Cut and paste will get you most of the way there, and I’ll take you through the C++ step-by-step.

Installing Dependencies

To build the simulations, you need:

  1. C++ Toolchain
  2. Verilator
  3. SDL

The simulations should work on any modern platform, but I’ve confined my instructions to Linux and macOS. Windows installation depends on your choice of compiler, but the sims should work fine there too. For advice on SDL development on Windows, see Lazy Foo’ - Setting up SDL on Windows.

Linux

For Debian and Ubuntu-based distros, you can use the following. Other distros will be similar.

Install a C++ toolchain via ‘build-essential’:

apt update
apt install build-essential

Install packages for Verilator and the dev version of SDL:

apt update
apt install verilator libsdl2-dev

That’s it!

If you want to build the latest version of Verilator yourself, see Building Verilator for Linux.

macOS

Install Xcode to get a C++ toolchain.

Install the Homebrew package manager.

With Homebrew installed, you can run:

brew install verilator sdl2

And you’re ready to go.

Working with Verilator

Verilator compiles your Verilog into a C++ model you can control using a simple interface. We’ll use the first design from the FPGA Graphics tutorial series as a demo. If you’re new to graphics on FPGA or ASIC, I strongly recommend reading the first part of the tutorial before continuing.

To create a simulation, you need a suitable Verilog top module and a C++ main function.

The square design from FPGA Graphics:

A Square

Verilator Top

Our Verilog design is very similar to that for FPGA dev boards: iceBreaker top_square and Arty top_square. The main difference is we skip PLL clock generation and output the screen position and data enable as well as the colours.

Our Verilator [top_square.sv] looks like this:

module top_square #(parameter CORDW=10) (  // coordinate width
    input  wire logic clk_pix,         // pixel clock
    input  wire logic rst,             // reset
    output      logic [CORDW-1:0] sx,  // horizontal screen position
    output      logic [CORDW-1:0] sy,  // vertical screen position
    output      logic de,              // data enable (low in blanking interval)
    output      logic [7:0] sdl_r,     // 8-bit red
    output      logic [7:0] sdl_g,     // 8-bit green
    output      logic [7:0] sdl_b      // 8-bit blue
    );

    // display timings
    simple_display_timings_480p display_timings_inst (
        .clk_pix,
        .rst,
        .sx,
        .sy,
        .hsync(),
        .vsync(),
        .de
    );

    // 32 x 32 pixel square
    logic q_draw;
    always_comb q_draw = (sx < 32 && sy < 32) ? 1 : 0;

    // SDL output
    always_ff @(posedge clk_pix) begin
        sdl_r <= !de ? 8'h00 : (q_draw ? 8'hFF : 8'h00);
        sdl_g <= !de ? 8'h00 : (q_draw ? 8'h88 : 8'h88);
        sdl_b <= !de ? 8'h00 : (q_draw ? 8'h00 : 8'hFF);
    end
endmodule

Our top module depends on one other module: [simple_display_timings_480p.sv]; it’s identical to that used with FPGAs. On real hardware, these timings produce a simple 640x480 output with a 60 Hz refresh rate. To understand how and why this works, read the first part of FPGA Graphics.

module simple_display_timings_480p (
    input  wire logic clk_pix,   // pixel clock
    input  wire logic rst,       // reset
    output      logic [9:0] sx,  // horizontal screen position
    output      logic [9:0] sy,  // vertical screen position
    output      logic hsync,     // horizontal sync
    output      logic vsync,     // vertical sync
    output      logic de         // data enable (low in blanking interval)
    );

    // horizontal timings
    parameter HA_END = 639;           // end of active pixels
    parameter HS_STA = HA_END + 16;   // sync starts after front porch
    parameter HS_END = HS_STA + 96;   // sync ends
    parameter LINE   = 799;           // last pixel on line (after back porch)

    // vertical timings
    parameter VA_END = 479;           // end of active pixels
    parameter VS_STA = VA_END + 10;   // sync starts after front porch
    parameter VS_END = VS_STA + 2;    // sync ends
    parameter SCREEN = 524;           // last line on screen (after back porch)

    always_comb begin
        hsync = ~(sx >= HS_STA && sx < HS_END);  // invert: negative polarity
        vsync = ~(sy >= VS_STA && sy < VS_END);  // invert: negative polarity
        de = (sx <= HA_END && sy <= VA_END);
    end

    // calculate horizontal and vertical screen position
    always_ff @(posedge clk_pix) begin
        if (sx == LINE) begin  // last pixel on line?
            sx <= 0;
            sy <= (sy == SCREEN) ? 0 : sy + 1;  // last line on screen?
        end else begin
            sx <= sx + 1;
        end
        if (rst) begin
            sx <= 0;
            sy <= 0;
        end
    end
endmodule

C++ Interface & SDL

To drive our simulation, we need a C++ main function. SDL has many ways to draw on the screen. I’ve chosen a straightforward approach that should work for any graphics design. We write the Verilog video “beam” to an array of pixels. Once per frame, we convert the pixel array to an SDL texture and update our window.

I’ll show the source file below, then discuss how it works. I’m not a professional C++ developer, so don’t be too horrified by my code. :)

#include <stdio.h>
#include <SDL2/SDL.h>
#include <verilated.h>
#include "Vtop_square.h"

// screen dimensions
const int H_RES = 640;
const int V_RES = 480;

typedef struct Pixel {  // for SDL texture
    uint8_t a;  // transparency
    uint8_t b;  // blue
    uint8_t g;  // green
    uint8_t r;  // red
} Pixel;

int main(int argc, char* argv[]) {
    Verilated::commandArgs(argc, argv);

    if(SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("SDL init failed.\n");
        return 1;
    }

    Pixel screenbuffer[H_RES*V_RES];

    SDL_Window*   sdl_window   = NULL;
    SDL_Renderer* sdl_renderer = NULL;
    SDL_Texture*  sdl_texture  = NULL;

    sdl_window = SDL_CreateWindow("Top Square", SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED, H_RES, V_RES, SDL_WINDOW_SHOWN);
    if (!sdl_window) {
        printf("Window creation failed: %s\n", SDL_GetError());
        return 1;
    }

    sdl_renderer = SDL_CreateRenderer(sdl_window, -1, SDL_RENDERER_ACCELERATED);
    if (!sdl_renderer) {
        printf("Renderer creation failed: %s\n", SDL_GetError());
        return 1;
    }

    sdl_texture = SDL_CreateTexture(sdl_renderer, SDL_PIXELFORMAT_RGBA8888,
        SDL_TEXTUREACCESS_TARGET, H_RES, V_RES);
    if (!sdl_texture) {
        printf("Texture creation failed: %s\n", SDL_GetError());
        return 1;
    }

    // initialize Verilog module
    Vtop_square* top = new Vtop_square;

    top->rst = 1;
    top->clk_pix = 0;
    top->eval();
    top->rst = 0;
    top->eval();

    while (1) {
        // check for quit event
        SDL_Event e;
        if (SDL_PollEvent(&e)) {
            if (e.type == SDL_QUIT) {
                break;
            }
        }

        // cycle the clock
        top->clk_pix = 1;
        top->eval();
        top->clk_pix = 0;
        top->eval();

        // update pixel if not in blanking interval
        if (top->de) {
            Pixel* p = &screenbuffer[top->sy*H_RES + top->sx];
            p->a = 0xFF;  // transparency
            p->b = top->sdl_b;
            p->g = top->sdl_g;
            p->r = top->sdl_r;
        }

        // update texture once per frame at start of blanking
        if (top->sy == V_RES && top->sx == 0) {
            SDL_UpdateTexture(sdl_texture, NULL, screenbuffer, H_RES*sizeof(Pixel));
            SDL_RenderClear(sdl_renderer);
            SDL_RenderCopy(sdl_renderer, sdl_texture, NULL, NULL);
            SDL_RenderPresent(sdl_renderer);
        }
    }

    top->final();  // simulation done

    SDL_DestroyTexture(sdl_texture);
    SDL_DestroyRenderer(sdl_renderer);
    SDL_DestroyWindow(sdl_window);
    SDL_Quit();
    return 0;
}

I’ll now go through the code step-by-step, explaining how it works. Remember, you can find all the source files in the projf-explore repo. If you’re eager to get it running right away, you can skip on to Building & Running.

You might also like to read the official Verilator doc: Connecting to Verilated Models.

C++ Includes

There are four includes:

  1. #include <stdio.h> - for printf; you can use iostream and cout if you prefer
  2. #include <SDL2/SDL.h> - SDL header
  3. #include <verilated.h> - common Verilator routines
  4. #include "Vtop_square.h" - generated by Verilator to match our Verilog top module

NB. The name of the final include depends on the name of your top module.

Screen Size

We need to define our screen size to match the display timings we’ve used:

// screen dimensions
const int H_RES = 640;
const int V_RES = 480;

Pixel Type

We write our colour values into a C++ array of pixels. We define a 32-bit Pixel type for convenience, with a representing transparency:

typedef struct Pixel {  // for SDL texture
    uint8_t a;  // transparency
    uint8_t b;  // blue
    uint8_t g;  // green
    uint8_t r;  // red
} Pixel;

SDL Initialization

The next chunk of code creates the pixel array and three SDL objects: window, renderer, and texture.

Pixel screenbuffer[H_RES*V_RES];

SDL_Window*   sdl_window   = NULL;
SDL_Renderer* sdl_renderer = NULL;
SDL_Texture*  sdl_texture  = NULL;

I’ve not explain the SDL create call options in this post; read more about them on the SDL wiki:

Verilog Initialization

We create an instance of our Verilog module, then reset it:

// initialize Verilog module
Vtop_square* top = new Vtop_square;

top->rst = 1;
top->clk_pix = 0;
top->eval();
top->rst = 0;
top->eval();

The model is run (evaluated) when you call top->eval().

Main Loop

Our simulation runs in the main loop, which has four parts. First, we poll for a quit event, such as the user closing the window. Then, we exit the while loop with break when a quit occurs.

while (1) {
    // check for quit event
    SDL_Event e;
    if (SDL_PollEvent(&e)) {
        if (e.type == SDL_QUIT) {
            break;
        }
    }

The pixel clock drives our hardware; we flip it to 1 and back to 0, evaluating our model each time:

    // cycle the clock
    top->clk_pix = 1;
    top->eval();
    top->clk_pix = 0;
    top->eval();

If we’re in the active drawing part of the screen (i.e. not the blanking interval), we get a pointer to the current pixel then update its colour:

    // update pixel if not in blanking interval
    if (top->de) {
        Pixel* p = &screenbuffer[top->sy*H_RES + top->sx];
        p->a = 0xFF;  // transparency
        p->b = top->sdl_b;
        p->g = top->sdl_g;
        p->r = top->sdl_r;
    }

Once per frame, we convert the pixel array into an SDL texture and display it in the window:

    // update texture once per frame at start of blanking
    if (top->sy == V_RES && top->sx == 0) {
        SDL_UpdateTexture(sdl_texture, NULL, screenbuffer, H_RES*sizeof(Pixel));
        SDL_RenderClear(sdl_renderer);
        SDL_RenderCopy(sdl_renderer, sdl_texture, NULL, NULL);
        SDL_RenderPresent(sdl_renderer);
    }
}

The call to SDL_UpdateTexture is expensive, so we limit it to once per frame. Of course, you can update the texture after every pixel, but your simulation will run approximately 1000x slower!

Clean Up

After breaking out of the while loop, we perform some clean up before quitting:

    top->final();  // simulation done

    SDL_DestroyTexture(sdl_texture);
    SDL_DestroyRenderer(sdl_renderer);
    SDL_DestroyWindow(sdl_window);
    SDL_Quit();
    return 0;
}

Building and Running

Building and running Verilator simulations is pleasantly simple. We use sdl2-config to set the correct linker options for us.

To build and run the square simulation from the Project F repo:

cd projf-explore/graphics/fpga-graphics/verilator
verilator -I../ -cc top_square.sv --exe main_square.cpp -LDFLAGS "`sdl2-config --libs`"
make -C ./obj_dir -f Vtop_square.mk
./obj_dir/Vtop_square

When building your own designs, you may need to adjust the -I option that tells Verilator where to find included Verilog modules.

The simulation window looks like this on Linux (Pop!_OS 20.04):

Simulating top square

And the shell output looks like this:

$ verilator -I../ -cc top_square.sv --exe main_square.cpp -LDFLAGS "`sdl2-config --libs`"

$ make -C ./obj_dir -f Vtop_square.mk
make: Entering directory '/home/flux/src/projf-explore/graphics/fpga-graphics/verilator/obj_dir'
ccache g++  -I.  -MMD -I/usr/local/share/verilator/include -I/usr/local/share/verilator/include/vltstd -DVM_COVERAGE=0 -DVM_SC=0 -DVM_TRACE=0 -DVM_TRACE_FST=0 -faligned-new -fcf-protection=none -Wno-bool-operation -Wno-sign-compare -Wno-uninitialized -Wno-unused-but-set-variable -Wno-unused-parameter -Wno-unused-variable -Wno-shadow      -std=gnu++14 -Os -c -o main_square.o ../main_square.cpp
ccache g++  -I.  -MMD -I/usr/local/share/verilator/include -I/usr/local/share/verilator/include/vltstd -DVM_COVERAGE=0 -DVM_SC=0 -DVM_TRACE=0 -DVM_TRACE_FST=0 -faligned-new -fcf-protection=none -Wno-bool-operation -Wno-sign-compare -Wno-uninitialized -Wno-unused-but-set-variable -Wno-unused-parameter -Wno-unused-variable -Wno-shadow      -std=gnu++14 -Os -c -o verilated.o /usr/local/share/verilator/include/verilated.cpp
/usr/bin/perl /usr/local/share/verilator/bin/verilator_includer -DVL_INCLUDE_OPT=include Vtop_square.cpp Vtop_square__Slow.cpp Vtop_square__Syms.cpp > Vtop_square__ALL.cpp
ccache g++  -I.  -MMD -I/usr/local/share/verilator/include -I/usr/local/share/verilator/include/vltstd -DVM_COVERAGE=0 -DVM_SC=0 -DVM_TRACE=0 -DVM_TRACE_FST=0 -faligned-new -fcf-protection=none -Wno-bool-operation -Wno-sign-compare -Wno-uninitialized -Wno-unused-but-set-variable -Wno-unused-parameter -Wno-unused-variable -Wno-shadow      -std=gnu++14 -Os -c -o Vtop_square__ALL.o Vtop_square__ALL.cpp
Archive ar -rcs Vtop_square__ALL.a Vtop_square__ALL.o
g++    main_square.o verilated.o Vtop_square__ALL.a   -lSDL2    -o Vtop_square
make: Leaving directory '/home/flux/src/projf-explore/graphics/fpga-graphics/verilator/obj_dir'

$ ./obj_dir/Vtop_square

Animation

A static square is all very well, but what about animation? You’ll be delighted to know the same C++ works; we tweak a couple of things to match the Verilog module name. To simulate the bouncing demo from FPGA Graphics, grab the Verilog and C++:

Then build and run it:

cd projf-explore/graphics/fpga-graphics/verilator
verilator -I../ -cc top_bounce.sv --exe main_bounce.cpp -LDFLAGS "`sdl2-config --libs`"
make -C ./obj_dir -f Vtop_bounce.mk
./obj_dir/Vtop_bounce

I’ll be continuing to add additional Verilator simulations, including sprites and framebuffers, to the Project F repo over the summer.

What Next?

To learn more about Verilator, read the Verilating User Guide and check out my guide to Verilog Lint with Verilator. To learn more about SDL, consult the SDL Wiki and Lazy Foo’ Productions.

Find inspiration from these projects using Verilator and SDL:

  • VGA Clock by Matthew Venn - show the time on a 640x480 VGA display
  • Got a suggestion for this list? Ping @WillFlux.

Constructive feedback is always welcome. Get in touch with @WillFlux or open an issue on GitHub.

©2021 Will Green, Project F