Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to do interactive user input and output simulation in VHDL or Verilog?

Tags:

verilog

vhdl

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:

  • signals are set by keyboard keys
  • outputs can be displayed on a window

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 not
  • write(output: would need some way to flush data to ensure that the renderer that will emulate, say, a display gets it
  • we need some way to throttle simulation speed

Of 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


2 Answers

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:

enter image description here

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.

like image 24
Jamey Hicks Avatar answered Dec 11 '25 07:12

Jamey Hicks



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!