https://projectf.io/posts/verilog-sim-verilator-sdl/ Project F - FPGA Development About Site Map Cookbook Graphics Maths Tools #testing | #tools | #verilator 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 * Verilator & SDL * Installing Dependencies * Working with Verilator * Building & Running * Animation * Taking it Further * Acknowledgements 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 #include #include #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 - for printf; you can use iostream and cout if you prefer 2. #include - SDL header 3. #include - 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: * SDL_CreateWindow * SDL_CreateRenderer * SDL_CreateTexture 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++: * top_bounce.sv * main_bounce.cpp 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: * 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 (on draft git branch at present) * CXXRTL, a Yosys Simulation Backend by Tom Verbeure - an alternative to Verilator + Using CXXRTL for graphic simulation by Konrad Beckmann (Twitter) 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. Similar articles: * Verilog Lint with Verilator * Building iCE40 FPGA Toolchain on Linux * FPGA Tooling on Ubuntu 20.04 (c)2021 Will Green, Project F