Easy Verilog Test Benches
I had a recent project where I was building lots of little Verilog "building block" modules. I found that writing good test benches for these small modules was almost as time consuming as creating the modules themselves. I set out to do something about it.
If there is one thing I dread, it is writing user interface code. Since this was just a tool, I decided I wanted to side step as much user interface development as possible. I was looking at the output of Verilog waveform dump, and it occurred to me that it could almost be a spreadsheet with the signals in rows and each column representing a clock cycle.
Of course, you'd get cross-eyed looking at all the binary digits and you also wouldn't want to fill all of them in. After an afternoon of turning it over in my head, I realized I could fix a lot of these problems with spreadsheet formulae. I also remembered seeing a TrueType font made to represent waveforms a few years ago.
It took a little experimenting, but I would finally end up with a pretty generic spreadsheet that could represent both my test inputs and expected outputs for a device (see the figure below).
The first row is an informational header. Some of the columns are just there to tell you what to put in the spreadsheet, but a few have actual meaning to the software I'll show you shortly. In particular, cell
E1 names the top level Verilog module you want to test.
J1 set the timescale used by the test bench. Cell
O1 determines the sampling offset, which I'll explain in a bit.
All the other rows contain your signals, preferably in the order they appear in the module definition. If the first column is blank, the software will ignore that row. This lets you put in comments or — in this case — the scope trace formula that uses the special font to draw the data below it. Of course, you could easily modify the formula to draw the data above it, or group all the numeric data in one block and put the scope traces together under that. Since the software ignores the rows with a blank first column, you can do anything you want. You can even omit the scope traces, if you prefer.
If the first column isn't blank, it is the name of a signal. The second column, then, tells the system (I call it benchscope, by the way) what kind of signal it is. If that second column is the word "clk", then the system ignores the rest of the row and generates a default one-cycle clock.
Another legal keyword for the second column is "input", which means the signal comes from your Verilog module you are testing and, therefore, is an input to the test bench.
Anything else in the second column marks the signal as an output and the test bench will drive the value to the contents of the second column when it starts. That's often a 0 or a 1, although it could be any value appropriate for the signal (like
8'b0, for example).
All the columns starting with D represent a time "tick" in the test bench (which the simulator will interpret based on the timescale you set in the first row). For outputs, you only need to set a value in the time slot you want a change. For inputs, any non-blank value will generate a test. The test bench won't test that value at other times. You can also put an
X in a cell to indicate you don't want a test (same a blank cell). This is mostly useful to force the scope trace to show an indeterminate trace.
My user interface turned out to be nothing more than a TrueType font and some fancy spreadsheet formulae. The second tab of the spreadsheet has some help text and a few special cells that help the first tab. The third tab contains example rows you can copy and paste. You can also delete the third tab, although I suggest you keep it in the master template.
I've used the spreadsheet in both Excel and Libre Office. There's no reason any spreadsheet couldn't handle it, although I've noticed jumping between the two sometimes loses a little cosmetic formatting. What you really want is the output of the first tab in a CSV (comma separated value) format. That's the input benchscope is looking for.
The figure above shows the spreadsheet for a simple counter. The top-level module has a counter that goes from zero to five and also outputs parity (long story, but I needed that feature as a building block). The code for the counter is straightforward:
module counter5(input clk, input reset, output reg [3:0] count); always @(posedge clk) begin if (reset) count<=4'b0; else if (count==4'h5) count=0; else count<=count+1; end endmodule // counter5
The top-level module is even simpler:
module pcounter5(input clk, input reset, output [3:0] count, output parbit); assign parbit=^count; counter5 ctr(clk,reset,count); endmodule // pcounter5
If there is a shorter way to compute the parity, I don't know what it is.
I'll spare you the CSV output file, although you can download it with the online listings along with all the other files. You'll need to run the CSV file through the benchscope backend to create the test bench. As usual, for something quick and dirty I turned to awk as being reasonably portable and fast to develop a file transformation like this. Here's the command line I used to generate the test bench:
awk –f benchscope.awk pcounter5.csv >pcounter5-tb.v
The resulting test bench is reasonably easy to modify if you need to tweak it. Here's an excerpt:
`timescale 1ns/1ps `default_nettype none module test; task tbassert(input a, input reg [512:0] s); begin // TODO: If you want some other logging behavior, put it here if (a==0) $display("%-s",s); end endtask // Outputs to DUT (DUT inputs) reg clk=0; reg reset=0; // Inputs from DUT (DUT outputs) wire [3:0] count; wire parity; // Device under test // TODO: You may need to reorder the ports if they were not in order in the input pcounter5 dut(clk,reset,count,parity); // clock generation always #1 clk=~clk; initial begin $dumpfile("pcounter5.vcd"); // TODO: Change dumpvars if desired $dumpvars; #1.1 reset=1; #2.1 tbassert(count==0, "Mismatch of signal count at time slot 3.1\n"); #0 tbassert(parity==0, "Mismatch of signal parity at time slot 3.1\n"); #0.9 reset=0; . . . #2.1 tbassert(count==1, "Mismatch of signal count at time slot 17.1\n"); #0 tbassert(parity==1, "Mismatch of signal parity at time slot 17.1\n"); #1 $finish; end endmodule
The test bench sets up outputs and delays using the
tbassert task to log any unexpected inputs. You can easily change that task if you prefer to record results in another format (although the test bench also creates a dump file for display).
There are two things noteworthy about the delays used. In general, you may want to test outputs just after a clock edge. That's what the sampling offset is for in the first row of the spreadsheet. In this case, I picked a 0.1 clock offset and the test bench automatically adjusts the times so that everything occurs as expected.
The other thing you'll see is some zero delays (#0). These ensure that outputs get set before the test bench samples inputs. Other than that, the test bench is similar to the ones I hand rolled last time. In addition to the files, I also put the test bench and both Verilog modules (appended into one "file") on EDA Playground so you can run them live in a browser. Here's the resulting waveform:
If you like, try changing one of the
tbassert lines to force an error. You should see the error noted in the console window near the bottom of the screen (or, if you are using your own tools, wherever your
$display output shows up).
I wouldn't dare try to code up a massive test bench like the ones I use for my CPU designs using this method. It would be like trying to build a large design using schematic entry. But it is worthwhile for simple designs. It is also could be a useful tool in a classroom where you want the student to focus (at first) on the design and also to think about the expected inputs and outputs.