For example, I would like to run a simulation for an interactive game like: https://github.com/fabioperez/space-invaders-vhdl without an FPGA, such that:
http://www.nand2tetris.org/ does this, but is uses a simplified custom educational language for it.
VHDL's textio's read(input and write(output get somewhat close, but not quite:
read(input waits for a newline, we'd want something that can detect is a keyboard key is pressed or notwrite(output: would need some way to flush data to ensure that the renderer that will emulate, say, a display gets itOf course, I don't need to do everything in VHDL: I just need a minimal way to communicate with VHDL synchronously with other programs, and then I can do the e.g. display with SDL in C.
Also asked at: https://github.com/tgingold/ghdl/issues/92
Verilator
Verilator is a perfect solution for this application.
It exposes the Verilog simulation loop to C++ (and transpiles the Verilog to C++), allowing you to set inputs, and get outputs from C++.
See the CONNECTING TO C++ example from the docs: http://www.veripool.org/projects/verilator/wiki/Manual-verilator
So you can just plug that into SDL / ncurses / etc. without any IPC.
For a simulator independent solution, it might be worth looking into the foreign language APIs of VHDL (VHPI) / Verilog (DPI) as mentioned in this comment, but there are few examples of how to use those, and you'll have to worry about IPC.
Minimal runnable example:

A related project that implements nand2tetris in Verilator + SDL can be found at: https://hackaday.io/project/160865-nand2tetris-in-verilog-part3-verilator-and-sdl2
Install dependencies on Ubuntu 22.04:
sudo apt install libsdl2-dev verilator
Makefile
.POSIX:
.PHONY: all clean run
RUN ?= move
OUT_EXT ?= .out
VERILATOR_DIR = ./obj_dir/
all: $(VERILATOR_DIR)Vmove display$(OUT_EXT)
$(VERILATOR_DIR)Vmove: move.v move.cpp fps.hpp
verilator -Wall --cc move.v --exe move.cpp
make -C obj_dir -f Vmove.mk Vmove CXXFLAGS='--std=c++11 -Wall' LIBS='-lSDL2'
display$(OUT_EXT): display.cpp
g++ -o '$@' '$<' -lm -lSDL2
clean:
rm -rf obj_dir *'$(OUT_EXT)'
run: all
'$(VERILATOR_DIR)V$(RUN)'
move.v
module move(
input wire clock,
input wire reset,
input wire up,
input wire down,
input wire left,
input wire right,
output reg [1:0] x,
output reg [1:0] y
);
always @ (posedge clock) begin
if (reset == 1'b1) begin
x <= 0;
y <= 0;
end
else begin
if (up == 1'b1) begin
y <= y - 1;
end
if (down == 1'b1) begin
y <= y + 1;
end
if (left == 1'b1) begin
x <= x - 1;
end
if (right == 1'b1) begin
x <= x + 1;
end
end
end
endmodule
move.cpp
const char *help = "asdw: move | q: quit";
#include <cmath>
#include <cstdlib>
#include <time.h>
#include <SDL2/SDL.h>
#include "Vmove.h"
#include "verilated.h"
#include "fps.hpp"
#define WINDOW_WIDTH 512
#define RECTS_PER_WINDOW (4)
#define RECT_WIDTH (WINDOW_WIDTH / RECTS_PER_WINDOW)
#define FASTEST_TICK_PERIOD_S (1.0 / 4.0)
int main(int argc, char **argv) {
SDL_Event event;
SDL_Renderer *renderer;
SDL_Window *window;
double current_time_s, last_tick_time_s;
unsigned int current_time, last_time;
const Uint8 *keystate;
Verilated::commandArgs(argc, argv);
Vmove *top = new Vmove;
SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO);
SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_WIDTH, 0, &window, &renderer);
SDL_SetWindowTitle(window, help);
fps_init();
top->clock = 0;
top->eval();
top->reset = 1;
top->clock = 1;
top->eval();
while (1) {
current_time = SDL_GetTicks();
current_time_s = current_time / 1000.0;
/* Deal with keyboard input. */
while (SDL_PollEvent(&event) == 1) {
if (event.type == SDL_QUIT) {
goto quit;
} else if (event.type == SDL_KEYDOWN) {
switch(event.key.keysym.sym) {
case SDLK_q:
goto quit;
default:
break;
}
}
}
keystate = SDL_GetKeyboardState(NULL);
if (keystate[SDL_SCANCODE_ESCAPE]) {
top->reset = 1;
}
if (keystate[SDL_SCANCODE_A]) {
top->left = 1;
}
if (keystate[SDL_SCANCODE_D]) {
top->right = 1;
}
if (keystate[SDL_SCANCODE_W]) {
top->up = 1;
}
if (keystate[SDL_SCANCODE_S]) {
top->down = 1;
}
if (current_time != last_time) {
if (current_time_s - last_tick_time_s > FASTEST_TICK_PERIOD_S) {
/* Draw world. */
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
{
SDL_Rect rect;
rect.w = RECT_WIDTH;
rect.h = RECT_WIDTH;
rect.x = top->x * RECT_WIDTH;
rect.y = top->y * RECT_WIDTH;
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderFillRect(renderer, &rect);
}
SDL_RenderPresent(renderer);
top->clock = 0;
top->eval();
top->clock = 1;
top->eval();
top->up = 0;
top->down = 0;
top->left = 0;
top->right = 0;
top->reset = 0;
/* Update time tracking. */
last_tick_time_s = current_time_s;
fps_update_and_print();
}
}
last_time = current_time;
}
quit:
top->final();
delete top;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return EXIT_SUCCESS;
}
display.cpp
/*
Test a simple virtual SDL display, without user input.
*/
#include <cstdlib>
#include <cmath>
#include <iostream>
#include <SDL2/SDL.h>
#define WINDOW_WIDTH 600
#define WINDOW_HEIGHT (WINDOW_WIDTH)
#define N_PIXELS_WIDTH 10
#define N_PIXELS_HEIGHT (N_PIXELS_WIDTH)
#define N_PIXELS (N_PIXELS_WIDTH * N_PIXELS_HEIGHT)
#define PIXEL_WIDTH (WINDOW_WIDTH / N_PIXELS_WIDTH)
#define PIXEL_HEIGHT (WINDOW_HEIGHT / N_PIXELS_HEIGHT)
#define MAX_COLOR 255
#define PI2 (2*(acos(-1.0)))
#define FREQ (0.05)
int main(int argc, char **argv, char **env) {
SDL_Event event;
SDL_Rect rect;
SDL_Renderer *renderer;
SDL_Window *window;
const unsigned int max_color_half = MAX_COLOR / 2;
int quit;
double current_time_s;
size_t cur, i , j;
unsigned int
bs[N_PIXELS],
current_time,
gs[N_PIXELS],
last_time,
rs[N_PIXELS],
val
;
quit = 0;
SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO);
SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_WIDTH, 0, &window, &renderer);
rect.w = PIXEL_WIDTH;
rect.h = PIXEL_HEIGHT;
last_time = SDL_GetTicks();
while (!quit) {
while (SDL_PollEvent(&event) == 1) {
if (event.type == SDL_QUIT) {
quit = 1;
}
}
current_time = SDL_GetTicks();
if (current_time != last_time) {
for (i = 0; i < N_PIXELS_WIDTH; ++i) {
for (j = 0; j < N_PIXELS_WIDTH; ++j) {
cur = j * N_PIXELS_WIDTH + i;
val = (1 + i) * (1 + j) * PI2 * FREQ * current_time / 1000.0;
rs[cur] = max_color_half * (1.0 + std::sin(1 * val));
gs[cur] = max_color_half * (1.0 + std::sin(2 * val));
bs[cur] = max_color_half * (1.0 + std::sin(3 * val));
}
}
}
for (i = 0; i < N_PIXELS_WIDTH; ++i) {
for (j = 0; j < N_PIXELS_WIDTH; ++j) {
cur = j *N_PIXELS_WIDTH + i;
SDL_SetRenderDrawColor(renderer, rs[cur], gs[cur], bs[cur], 255);
rect.x = i * PIXEL_WIDTH;
rect.y = j * PIXEL_HEIGHT;
SDL_RenderFillRect(renderer, &rect);
}
}
SDL_RenderPresent(renderer);
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return EXIT_SUCCESS;
}
GitHub upstream.
Connectal connects software running on actual CPUs to RTL (BSV, which can link to VHDL and Verilog) on FPGAs or simulators. BSV is free for academic and research use and for open source projects. In any case, Connectal is open source and the software to simulator connection uses SystemVerilog DPI, which you could use in your project without using BSV.
Connectal has one example that displays the output from the FGPA/simulator on a display. It uses Qt to display on the computer monitor when simulating. From an FPGA it displays directly on an HDMI display.
CPUs simulated in Verilog or VHDL tend to be too slow for interactive use, but I have connected a CPU simulated with qemu to devices or accelerators in verilator or on FPGA. Performance of qemu is quite good. I think it would work for your purposes.
I added a plugin FgpaOps API so that the simulator or FPGA could handle CPU load/store instructions:
struct FpgaOps {
uint64_t (*read)(hwaddr addr);
void (*write)(hwaddr addr, uint64_t value);
void (*close)(void);
void *(*alloc_mem)(size_t size);
};
In my case, I used connectal to implement the FpgaOps plugin. This code is under hw/riscv but is not specific to riscv, so it could be used with any processor architecture supported by qemu.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With