Project F

Verilog Simulation with Verilator and SDL

Published · Updated

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.

Share your thoughts with @WillFlux on Mastodon or Twitter. If you like what I do, sponsor me. 🙏

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 features from SystemVerilog to make Verilog a little more pleasant. If you’re familiar with Verilog, you’ll be fine. All the SystemVerilog features used are compatible with recent versions of Verilator, Yosys, Icarus Verilog, and Xilinx Vivado.

Contents

The following screenshot shows a Verilator simulation of raster bars from my post Racing the Beam.

Simulating Raster Bars

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 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 with build-essential:

apt update
apt install build-essential

Install Verilator and the dev version of SDL:

apt install verilator libsdl2-dev

macOS

Install the Homebrew package manager; this will also install Xcode Command Line Tools.

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 Beginning FPGA Graphics as a demo.

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 sim_rst,             // sim reset
    output      logic [CORDW-1:0] sdl_sx,  // horizontal SDL position
    output      logic [CORDW-1:0] sdl_sy,  // vertical SDL position
    output      logic sdl_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
    logic [CORDW-1:0] sx, sy;
    logic de;
    simple_480p display_inst (
        .clk_pix,
        .rst_pix(sim_rst),
        .sx,
        .sy,
        .hsync(),
        .vsync(),
        .de
    );

    // define a square with screen coordinates
    logic square;
    always_comb begin
        square = (sx > 220 && sx < 420) && (sy > 140 && sy < 340);
    end

    // paint colours: white inside square, blue outside
    logic [3:0] paint_r, paint_g, paint_b;
    always_comb begin
        paint_r = (square) ? 4'hF : 4'h1;
        paint_g = (square) ? 4'hF : 4'h3;
        paint_b = (square) ? 4'hF : 4'h7;
    end

    // SDL output (8 bits per colour channel)
    always_ff @(posedge clk_pix) begin
        sdl_sx <= sx;
        sdl_sy <= sy;
        sdl_de <= de;
        sdl_r <= {2{paint_r}};  // double signal width from 4 to 8 bits
        sdl_g <= {2{paint_g}};
        sdl_b <= {2{paint_b}};
    end
endmodule

NB. SDL colour output is delayed one cycle in “SDL output”, so we need to delay sx, sy, and de to match. If we don’t do this, everything will be shifted left one pixel.

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 Beginning FPGA Graphics.

module simple_480p (
    input  wire logic clk_pix,   // pixel clock
    input  wire logic rst_pix,   // reset in pixel clock domain
    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_pix) 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. :)

Verilator C++ [main_square.cpp]:

#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("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;
    }

    // reference SDL keyboard state array: https://wiki.libsdl.org/SDL_GetKeyboardState
    const Uint8 *keyb_state = SDL_GetKeyboardState(NULL);

    printf("Simulation running. Press 'Q' in simulation window to quit.\n\n");

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

    // reset
    top->sim_rst = 1;
    top->clk_pix = 0;
    top->eval();
    top->clk_pix = 1;
    top->eval();
    top->sim_rst = 0;
    top->clk_pix = 0;
    top->eval();

    // initialize frame rate
    uint64_t start_ticks = SDL_GetPerformanceCounter();
    uint64_t frame_count = 0;

    // main loop
    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->sdl_de) {
            Pixel* p = &screenbuffer[top->sdl_sy*H_RES + top->sdl_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 (in blanking)
        if (top->sdl_sy == V_RES && top->sdl_sx == 0) {
            // check for quit event
            SDL_Event e;
            if (SDL_PollEvent(&e)) {
                if (e.type == SDL_QUIT) {
                    break;
                }
            }

            if (keyb_state[SDL_SCANCODE_Q]) break;  // quit if user presses 'Q'

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

    // calculate 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);

    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:

Keyboard State

We reference the keyboard state and tell the user how to quite the simulation.

// reference SDL keyboard state array: https://wiki.libsdl.org/SDL_GetKeyboardState
const Uint8 *keyb_state = SDL_GetKeyboardState(NULL);

printf("Simulation running. Press 'Q' in simulation window to quit.\n\n");

You can also quit by closing the simulation window or by pressing CMD-Q on macOS.

Verilog Initialization

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

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

// reset
top->sim_rst = 1;
top->clk_pix = 0;
top->eval();
top->clk_pix = 1;
top->eval();
top->sim_rst = 0;
top->clk_pix = 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:

// initialize frame rate
uint64_t start_ticks = SDL_GetPerformanceCounter();
uint64_t frame_count = 0;

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->sdl_de) {
        Pixel* p = &screenbuffer[top->sdl_sy*H_RES + top->sdl_sx];
        p->a = 0xFF;  // transparency
        p->b = top->sdl_b;
        p->g = top->sdl_g;
        p->r = top->sdl_r;
    }

We don’t check the screenbuffer array index is valid. Our display module should handle this correctly, but this assumption is dangerous: you’ll probably get a core dump if the index is not in range.

Once per frame, we poll for a quit event and check if the user pressed ‘Q’ to quit:

    // update texture once per frame (in blanking)
    if (top->sdl_sy == V_RES && top->sdl_sx == 0) {
        // check for quit event
        SDL_Event e;
        if (SDL_PollEvent(&e)) {
            if (e.type == SDL_QUIT) {
                break;
            }
        }

        if (keyb_state[SDL_SCANCODE_Q]) break;  // quit if user presses 'Q'

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:

    // calculate 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.

Makefile

We can automate building with a [Makefile]:

VFLAGS = -O3 --x-assign fast --x-initial fast --noassert
SDL_CFLAGS = `sdl2-config --cflags`
SDL_LDFLAGS = `sdl2-config --libs`

square: square.exe
flag_ethiopia: flag_ethiopia.exe
flag_sweden: flag_sweden.exe
colour: colour.exe

%.exe: %.mk
	make -C ./obj_dir -f Vtop_$<

%.mk: top_%.sv
	verilator ${VFLAGS} -I.. \
		-cc $< --exe main_$(basename $@).cpp -o $(basename $@) \
		-CFLAGS "${SDL_CFLAGS}" -LDFLAGS "${SDL_LDFLAGS}"

all: square flag_ethiopia flag_sweden colour

clean:
	rm -rf ./obj_dir

.PHONY: all clean

With the Makefile in place you can simply run:

make square

ProTip: VFLAGS are options passed to Verilator: I have selected settings for best performance.

Simulation Running

The simulation looks like this:

Simulating Square

Press Q in the simulation window to quit.

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 Racing the Beam, grab the source from projf-explore/graphics/racing-the-beam:

Then build and run it with the Makefile: make bounce and run the sim ./obj_dir/bounce.

Taking it Further

I’ve added simulations to all my FPGA Graphics designs; for example, check out the Pong Simulation.

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:

What’s Next?

Take a look at FPGA Tools for more Verilator content. Or check out my FPGA graphics tutorials.

Acknowledgements

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