User Tools

Site Tools


nmigen:nmigen_sim_testbench

Writing a simulation testbench

Example: A seven segment display controller

A seven segment display

As its name stipulates, a seven segment display is made of 7 leds which are denoted ABCDEFG (the dot is omitted here).
If we wanted to control such a display in nMigen, we would need either seven 1-bit signals, or a 7-bit signal. We will do the latter, considering that the LSB is “A” and the MSB is “G”.

We want our controller to take a 4-bit value as input, and output its hexadecimal representation on the display.

Hexadecimal digits

Let's map each hexadecimal digit to its 7 segment output:

digit A B C D E F G
0 1 1 1 1 1 1 0
1 0 1 1 0 0 0 0
2 1 1 0 1 1 0 1
3 1 1 1 1 0 0 1
4 0 1 1 0 0 1 1
5 1 0 1 1 0 1 1
6 1 0 1 1 1 1 1
7 1 1 1 0 0 0 0
8 1 1 1 1 1 1 1
9 1 1 1 1 0 1 1
A 1 1 1 0 1 1 1
B 0 0 1 1 1 1 1
C 1 0 0 1 1 1 0
D 0 1 1 1 1 0 1
E 1 0 0 1 1 1 1
F 1 0 0 0 1 1 1

We can now implement the controller:

from nmigen import *
 
 
class SevenSegController(Elaboratable):
    def __init__(self):
        self.val  = Signal(4)
        self.leds = Signal(7)
 
    def elaborate(self, platform):
        m = Module()
 
        table = Array([
            0b0111111, # 0
            0b0000110, # 1
            0b1011011, # 2
            0b1001111, # 3
            0b1100110, # 4
            0b1101101, # 5
            0b1111101, # 6
            0b0000111, # 7
            0b1111111, # 8
            0b1101111, # 9
            0b1110111, # A
            0b1111100, # B
            0b0111001, # C
            0b1011110, # D
            0b1111001, # E
            0b1110001  # F
        ])
 
        m.d.comb += self.leds.eq(table[self.val])
 
        return m

For every possible hexadecimal digit, we store the corresponding seven segment output in our table.

Notice the Array we used here. A regular Python list is not indexable by a Signal. An Array behaves like a list but is indexable by nMigen values and regular integers.

Playing with the simulator

Let's use the built-in simulator to show the display output:

from nmigen.back.pysim import *
 
 
def print_seven(leds):
    line_top = ["   ", " _ "]
    line_mid = ["   ", "  |", " _ ", " _|", "|  ", "| |", "|_ ", "|_|"]
    line_bot = line_mid
 
    a = leds & 1
    fgb = ((leds >> 1) & 1) | ((leds >> 5) & 2) | ((leds >> 3) & 4)
    edc = ((leds >> 2) & 1) | ((leds >> 2) & 2) | ((leds >> 2) & 4)
 
    print(line_top[a])
    print(line_mid[fgb])
    print(line_bot[edc])
 
 
if __name__ == "__main__":
    dut = SevenSegController()
    with Simulator(dut) as sim:
        def process():
            for i in range(16):
                yield dut.val.eq(i)
                yield Delay()
                print_seven((yield dut.leds))
        sim.add_process(process)
        sim.run()

Running this simulation produces the following output:

% python seven_seg.py
 _
| |
|_|

  |
  |
 _
 _|
|_
 _
 _|
 _|

[snip]
 _
|_
|

Our design seems to work. But what happened ?

The built-in simulator allows us to control our design while it is running. To do so, we have to define a process.

Processes are Python generators that run concurrently within the simulation. They allow a testbench to communicate with the design under test via yield statements.

Let's examine what we did:

def process()
    for i in range(16):
        yield dut.val.eq(i) # Write i to dut.val
        yield Delay() # Wait an infinitesimal delay
        print_seven((yield dut.leds)) # Read dut.leds and print it

Note that the yield Delay() is necessary for our testbench to work.
Our design may be entirely combinatorial, we still have to introduce a delay between updating the inputs and reading the outputs. You can think of it as giving the input signals the time to propagate to the outputs.

If our design was synchronous, we would have to add a clock to the simulation and use yield Tick() to tick the clock.

Writing unit tests

While printing the value of simulated leds may be fun, it is often inconvenient to test a design this way. A more systematic approach is to write unit tests in order to validate its behaviour under particular conditions.

nMigen is well integrated with the Python unittest library, which makes writing tests straightforward.

from nmigen import *
from nmigen.back.pysim import *
from nmigen.test.tools import *
 
from seven_seg import SevenSegController
 
 
def test_leds(val, leds):
    def test(self):
        with Simulator(self.dut) as sim:
            def process():
                yield self.dut.val.eq(val)
                yield Delay()
                self.assertEqual((yield self.dut.leds), leds)
            sim.add_process(process)
            sim.run()
    return test
 
 
class SevenSegControllerTest(FHDLTestCase):
    def setUp(self):
        self.dut = SevenSegController()
 
    test_0 = test_leds(0x0, 0b0111111)
    test_1 = test_leds(0x1, 0b0000110)
    test_2 = test_leds(0x2, 0b1011011)
    test_3 = test_leds(0x3, 0b1001111)
    test_4 = test_leds(0x4, 0b1100110)
    test_5 = test_leds(0x5, 0b1101101)
    test_6 = test_leds(0x6, 0b1111101)
    test_7 = test_leds(0x7, 0b0000111)
    test_8 = test_leds(0x8, 0b1111111)
    test_9 = test_leds(0x9, 0b1101111)
    test_a = test_leds(0xa, 0b1110111)
    test_b = test_leds(0xb, 0b1111100)
    test_c = test_leds(0xc, 0b0111001)
    test_d = test_leds(0xd, 0b1011110)
    test_e = test_leds(0xe, 0b1111001)
    test_f = test_leds(0xf, 0b1110001)

The test cases shown here are identical to our implementation because we used a lookup table, but you get the idea :-)

Let's run them:

% python -m unittest test_seven_seg.py
................
----------------------------------------------------------------------
Ran 16 tests in 0.013s

OK

Next

In the next chapter, we will see how to actually run an nMigen design in an FPGA.

nmigen/nmigen_sim_testbench.txt · Last modified: 2019/09/04 15:14 by jfng