I describe how to build an interactive testbench for a simple VHDL adder using Python, Cocotb, pygame and the GHDL simulator. While software engineering calls for scriptable, repeatable testing, as afforded with tools like VUnit or Cocotb, the visceral breadboarding/development board activity experience is lost when writing a hardware description design. At least until you have some hardware…in our pandemic afflicted semiconductor supply chain world, getting an FPGA development board is easier said than done. But let’s not digress down that rabbit hole…
I thought this subject would already have many examples and thus this writeup would be redundant, but I found that not to be the case. One such application does exist, written by Théotime BOLLENGIER, called GHDL VPI virtual board. It is cleverly and well written in C++ as a VPI plugin, which will launch as part of a VHDL simulation run by GHDL. A very nice feature is that you do not have to constrain your design or write a separate test bench. The GUI itself drives the stimuli (clock and all).
The first problem I encountered is that GHDL VPI virtual board would not run on my Mac (crash). After some analysis, I discovered the GUI runs in a separate thread, which is not the main thread. The GDK/GTK libs and Mac OS don’t like that (I’m sure this works fine on Linux or maybe Windows). I also find C++ to be tedious and somewhat boring. There is a lot of code one must write to get anything done. So after finding Cocotb, I knew that Python would be simpler and more fun.
A GUI Framework
I am not a Python guru. Ok, I don’t program in Python much at all. So picking a GUI framework is not easy. There are many choices. But since I am thinking of the UI as more like a game, pygame seemed like just the thing. Its simple, cross-platform, very mature, and has many examples. I found this pygame tutorial really helpful in getting started using the module.
The VHDL Design
entity adder is
DATA_WIDTH : positive := 4);
A : in unsigned(DATA_WIDTH-1 downto 0);
B : in unsigned(DATA_WIDTH-1 downto 0);
X : out unsigned(DATA_WIDTH downto 0));
end entity adder;
architecture rtl of adder is
add_proc : process (A, B) is
X <= resize(A, X'length) + B;
end process add_proc;
end architecture rtl;
Theory of Operation
The crux to making this work easily is to use one main driver thread for both the simulation and the GUI event loop. Then use async IO to queue up changes from the GUI (for ports A and B) to forward to the simulation. Likewise, do the same for changes to ports in the simulation (port X). I coded a producer/consumer set of classes to encapsulate this behavior in datamonitor.py.
GUI changes are queued up by examining the pygame event list every time the pygame event loop is ticked. I have sprites added to a sprite group, and the event list is forwarded to each sprite (see line 111 in gui.py).
Simulator changes are queued up by awaiting return of the Edge function in a co-routine. This is set in in a lambda function passed to the ProducerMonitor for dut.X starting at line 66 of gui.py.
Since time critical stimulus is not important for this demonstration, this technique works great.
Source Code and Running the App
You can find the application at my github repo ghdl interactive sim along with instructions in the README on how to run it.