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’ll have your first simulation running in under an hour.

Updated 2021-10-20. 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 bounce from Intro to FPGA Graphics.

Simulating top bounce

Verilator & SDL

Verilator is a fast simulator that generates C++ models of Verilog designs. SDL (LibSDL) is a cross-platform library that provides low-level access to graphics hardware. Bring them together, and Verilator generates a 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 for simplicity. A simple graphics sim will run at 60 frames per second on a modern PC, while a design with many sprites or complex drawing will run more slowly.

The process for creating a graphics sim 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 Intro to FPGA Graphics before continuing.

To create your simulation, you need two things:

  1. Verilog top module
  2. C++ main function

Verilator Top

Our Verilog top module is similar to that for FPGA dev boards. For simulation, we skip PLL clock generation and output the screen position and data enable as well as the pixel colour.

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 sync signals and coordinates
    simple_480p display_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

Display Module

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

module simple_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 application 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 <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 | SDL_RENDERER_PRESENTVSYNC);
    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();

    uint64_t frame_count = 0;
    uint64_t start_ticks = SDL_GetPerformanceCounter();
    while (1) {
        // 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) {
            // check for quit event
            SDL_Event e;
            if (SDL_PollEvent(&e)) {
                if (e.type == SDL_QUIT) {
                    break;
                }
            }

            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);
            frame_count++;
        }
    }
    uint64_t end_ticks = SDL_GetPerformanceCounter();
    double duration = ((double)(end_ticks-start_ticks))/SDL_GetPerformanceFrequency();
    double fps = (double)frame_count/duration;
    printf("Frames per second: %.1f\n", fps);

    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 <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 define our screen size to match our display module, simple_480p:

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

Pixel Type

We create a 32-bit Pixel type to represent each pixel:

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 few lines create 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’ll not explain the SDL create call options in this post; you can read 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().

Performance Counters

We create a couple of counters to measure the frame rate:

uint64_t frame_count = 0;
uint64_t start_ticks = SDL_GetPerformanceCounter();

Main Loop

Our simulation runs in the main loop, which has four parts.

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 poll for a quit event:

    // update texture once per frame at start of blanking
    if (top->sy == V_RES && top->sx == 0) {
        // check for quit event
        SDL_Event e;
        if (SDL_PollEvent(&e)) {
            if (e.type == SDL_QUIT) {
                break;
            }
        }

Then we update the texture and increment the frame counter:

        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);
        frame_count++;
}

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 calculate the frame rate:

    uint64_t end_ticks = SDL_GetPerformanceCounter();
    double duration = ((double)(end_ticks-start_ticks))/SDL_GetPerformanceFrequency();
    double fps = (double)frame_count/duration;
    printf("Frames per second: %.1f\n", fps);

The 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 compiler and linker options for us.

To build the square simulation from the Project F repo:

cd projf-explore/graphics/fpga-graphics/sim

verilator -I../ -cc top_square.sv --exe main_square.cpp -o square \
    -CFLAGS "$(sdl2-config --cflags)" -LDFLAGS "$(sdl2-config --libs)"

make -C ./obj_dir -f Vtop_square.mk

You can then run the simulation executable from obj_dir:

./obj_dir/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:

Simulating top 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:

verilator -I../ -cc top_bounce.sv --exe main_bounce.cpp -o bounce \
    -CFLAGS "$(sdl2-config --cflags)" -LDFLAGS "$(sdl2-config --libs)"

make -C ./obj_dir -f Vtop_bounce.mk

./obj_dir/bounce

Sponsor Project F
If you like what I do, consider sponsoring me on GitHub.
I use contributions to spend more time creating open-source FPGA designs and tutorials.

Taking it Further

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 simulating graphics:

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

Acknowledgements

Thanks to Dave Dribin for improving the performance of these designs and adding the framerate reporting.

©2021 Will Green, Project F