Skip to content

Adding a behavioral model

Output verification for an inverter

Let's add some test vector checking to the inverter example.

Generating values

We'll first change the Main module from using a clock as the input to the inverter, to a digital source that can provide a pre-determined sequence of values. This 'VerilogSrc' is shown below:

inverter_sim_01.py
from random import randint
from circuitbrew.module import Module
from circuitbrew.ports import InputPort, OutputPort
from circuitbrew.compound_ports import SupplyPort
from circuitbrew.fets import Nfet, Pfet
from circuitbrew.elements import Supply, VerilogClock, VerilogSrc

class Inverter(Module):
    a = InputPort()
    b = OutputPort()
    p = SupplyPort()

    def build(self):
        vdd, gnd = self.p.vdd, self.p.gnd
        self.pup = Pfet(g=self.a, d=vdd, s=self.b, b=vdd)
        self.ndn = Nfet(g=self.a, d=self.b, s=gnd, b=gnd)
        self.finalize()


class Main(Module):

    def build(self):
        # Voltage supply
        self.supply = Supply(name='vdd', voltage=self.sim_setup['voltage'])
        p = self.supply.p
        vdd, gnd = p.vdd, p.gnd

        # Change the clock to drive the VerilogSrc instead of the Inverter
        self.clk_gen = VerilogClock('clk', freq=300e3, enable=vdd)
        # Sequence of input test vectors on output node 'd'
        self.src = VerilogSrc('src', [randint(0,1) for i in range(10)], 
                              clk=self.clk_gen.clk, _reset=vdd)
        inv_in = self.src.d
        self.inv = Inverter('myinv', a=inv_in, p=p)

        self.finalize()

The source module can take a list of values that it loops through and repeats during simulation. In this example, we generate a vector of 10 1-bit values to source.

The waveform of this simulation is below. The top waveform is the source clock, and then below is the input and output of the inverter.

Waveform

Checking the values

Checking the expected values can be done using a 'VerilogBucket'. We provide the expected values list to the bucket by inverting the randomly generated list supplied to the VerilogSrc:

inverter_sim_02.py
from random import randint
from circuitbrew.module import Module
from circuitbrew.ports import InputPort, OutputPort
from circuitbrew.compound_ports import SupplyPort
from circuitbrew.fets import Nfet, Pfet
from circuitbrew.elements import Supply, VerilogClock, VerilogSrc, VerilogBucket

class Inverter(Module):
    a = InputPort()
    b = OutputPort()
    p = SupplyPort()

    def build(self):
        vdd, gnd = self.p.vdd, self.p.gnd
        self.pup = Pfet(g=self.a, d=vdd, s=self.b, b=vdd)
        self.ndn = Nfet(g=self.a, d=self.b, s=gnd, b=gnd)
        self.finalize()


class Main(Module):

    def build(self):
        # Voltage supply
        self.supply = Supply(name='vdd', voltage=self.sim_setup['voltage'])
        p = self.supply.p
        vdd, gnd = p.vdd, p.gnd

        # Change the clock to drive the VerilogSrc instead of the Inverter
        self.clk_gen = VerilogClock('clk', freq=300e3, enable=vdd)

        # Sequence of input test vectors on output node 'd'
        vals = [randint(0,1) for i in range(10)]
        self.src = VerilogSrc('src', values=vals,
                              clk=self.clk_gen.clk, _reset=vdd)
        inv_in = self.src.d
        self.inv = Inverter('myinv', a=inv_in, p=p)

        # Expected values (invert the vals list)
        expected = [1-v for v in vals]
        # A sampling clock for checking expected output of the inverter
        self.clk_buc = VerilogClock('clk_buc', freq=300e3, offset='100p', enable=vdd)

        self.bucket = VerilogBucket(name='buc', values=expected,
                                    clk=self.clk_buc.clk, _reset=vdd, d=self.inv.b)
        self.finalize()

If you then look at the Verilog-A output file (top.valog if you're using hspice), you will see the following output:

At time  1.78 ns verified 0th value 0 to test.dat
At time  5.11 ns verified 1th value 1 to test.dat
At time  8.44 ns verified 2th value 1 to test.dat
At time 11.78 ns verified 3th value 1 to test.dat
At time 15.11 ns verified 4th value 1 to test.dat
At time 18.44 ns verified 5th value 0 to test.dat
At time 21.78 ns verified 6th value 1 to test.dat
At time 25.11 ns verified 7th value 1 to test.dat
At time 28.44 ns verified 8th value 0 to test.dat

Using model simulation to generate expected vectors

While providing the expected values for a simple test case like this is trivial, the real power of CircuitBrew lies in the ability to generate expected vectors from complex circuits that instance multiple blocks.

To do this, CircuitBrew has a simple yet powerful discrete-event-simulation framework built in to it. If you provide a method to implement a simulation model for each Module, then it will execute the complete system model for your circuit instanced in Main and generate the output values for each port. Assuming your model is correct, then you can seamlessly use these output values as the expected values provided to the HSPICE simulation.

For this particular example, we only need to provide a simulation model for the inverter, via an async sim method.

inverter_sim_03.py
from random import randint
from circuitbrew.module import Module
from circuitbrew.ports import InputPort, OutputPort
from circuitbrew.compound_ports import SupplyPort
from circuitbrew.fets import Nfet, Pfet
from circuitbrew.elements import Supply, VerilogClock, VerilogSrc, VerilogBucket

class Inverter(Module):
    a = InputPort()
    b = OutputPort()
    p = SupplyPort()

    def build(self):
        vdd, gnd = self.p.vdd, self.p.gnd
        self.pup = Pfet(g=self.a, d=vdd, s=self.b, b=vdd)
        self.ndn = Nfet(g=self.a, d=self.b, s=gnd, b=gnd)
        self.finalize()

    async def sim(self):
        while True:
            val = await self.a.recv()
            await self.b.send(1-val)

class Main(Module):

    def build(self):
        # Voltage supply
        self.supply = Supply(name='vdd', voltage=self.sim_setup['voltage'])
        p = self.supply.p
        vdd, gnd = p.vdd, p.gnd

        # Change the clock to drive the VerilogSrc instead of the Inverter
        self.clk_gen = VerilogClock('clk', freq=300e3, enable=vdd)

        # Sequence of input test vectors on output node 'd'
        vals = [randint(0,1) for i in range(10)]
        self.src = VerilogSrc('src', values=vals,
                              clk=self.clk_gen.clk, _reset=vdd)
        inv_in = self.src.d
        self.inv = Inverter('myinv', a=inv_in, p=p)

        # A sampling clock for checking expected output of the inverter
        self.clk_buc = VerilogClock('clk_buc', freq=300e3, offset='100p', enable=vdd)

        self.bucket = VerilogBucket(name='buc', # values=expected, no longer needed 
                                    clk=self.clk_buc.clk, _reset=vdd, d=self.inv.b)
        self.finalize()

While we'll delve into more detail on how model simulation works in a different section, in general, every port has a recv and send method. In your sim method for each block all you need to do is:

  • call await self.INPUTPORT.recv() to receive all the inputs to your port
  • compute the outputs based on the inputs
  • call await self.OUTPUTPORT.send(output_val) on all the output ports to send the computed values
  • repeat

We have to use the Python async keywords because the simulation methods relies on concurrent event-based simulation libraries for execution of the sim code. Under the hood, CircuitBrew currently uses the Curio async library, although this may change in future iterations.

For every Module that doesn't provide a sim method, CircuitBrew will execute the default behavior which is to forward all values received on an InputPort to its fanout connections transparently.

If you execute this example, it will run the same way in HSPICE as the last example, except you don't generate and provide expected vectors in the Main method.

In the next example, we will take a look at a more complicated example.