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.
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
- Verilator & SDL
- Installing Dependencies
- Working with Verilator
- Building & Running
- Animation
- Taking it Further
- Acknowledgements
The following screenshot shows a Verilator simulation of raster bars from my post Racing the Beam.
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:
- C++ Toolchain
- Verilator
- SDL
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.
Windows
Windows users can run Verilator with SDL under Windows Subsystem for Linux. WSL2 supports GUI Linux apps in Windows 10 Build 19044+ and Windows 11.
Once you have WSL2 running, you can use the Linux instructions (above).
I have successfully tested Verilator/SDL simulations with Debian 12 running on Windows 10.
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:
- Verilog top module
- 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:
#include <stdio.h>
- forprintf
; you can useiostream
andcout
if you prefer#include <SDL.h>
- SDL header#include <verilated.h>
- common Verilator routines#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 quit 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:
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:
- VGA Clock by Matthew Venn - show the time on a 640x480 VGA display
- FPGA 1943: The Battle of Midway by Frederic Requin - re-implements the CAPCOM classic
- Silice Simulation Framework by Sylvain Lefebvre
- CXXRTL, a Yosys Simulation Backend by Tom Verbeure - an alternative to Verilator
- Using CXXRTL for graphic simulation by Konrad Beckmann (Twitter)
What’s Next?
If you enjoyed this post, please sponsor me. Sponsors help me create more FPGA and RISC-V projects for everyone, and they get early access to blog posts and source code. 🙏
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.