AVL - Apheleia Verification Library is a cool library to implement SV (coverage and randomization) and UVM API (similar to pyuvm). This is a deep dive into an AVL example and implementation. Probably this would be the first post about AVL as I read more of the core modules.

Sequence and SequenceItem Link to heading

Starting with SequenceItem and Sequence, which look a lot like SV and UVM combo. The sequence body looks exactly like the UVM body.

class adder_item(avl.SequenceItem):
    def __init__(self, name, parent_sequence):
        super().__init__(name, parent_sequence)

        self.a = avl.Uint8("a", 0, fmt=str)
        self.b = avl.Uint8("b", 0, fmt=str)
        self.c = avl.Logic("c", 0, fmt=str, auto_random=False, width=9)

class adder_sequence(avl.Sequence):
    def __init__(self, name, parent):
        super().__init__(name, parent)
        self.n_items = avl.Factory.get_variable(f"{self.get_full_name()}.n_items", 200)

    async def body(self):
        self._parent_sequencer_.raise_objection()
        for _ in range(self.n_items):
            item = adder_item("item", self)
            await self.start_item(item)
            item.randomize()
            await self.finish_item(item)
        self._parent_sequencer_.drop_objection()

SequenceItem extends from Transaction which extends from Object (I guess that is similar to uvm_void).


class SequenceItem(Transaction):
    def __init__(self, name: str, parent: Component) -> None:
        """
        Initializes the SequenceItem with a name and an optional parent component.

        :param name: Name of the sequence item.
        :type name: str
        :param parent: Parent component (optional).
        :type parent: Component
        """
        super().__init__(name, parent)
        self.add_event("done")
        self.add_event("response")

        self._parent_sequence_ = None
        self._parent_sequencer_ = None

        if isinstance(parent, SequenceItem):
            self._parent_sequence_ = parent
            self._parent_sequencer_ = parent.get_sequencer()
        elif isinstance(parent, Sequencer):
            self._parent_sequencer_ = parent

Sequence extends SequenceItem and implements the start_item and finish_item which are called to pass SequenceItem to the driver.

class Sequence(SequenceItem):
    def __init__(self, name, parent_sequence: Sequence) -> None:
        """
        Initializes the Sequence with a name and parent sequence.

        :param name: Name of the sequence.
        :type name: str
        :param parent_sequence: Parent sequence, if any.
        :type parent_sequence: Sequence
        """
        super().__init__(name, parent_sequence)
        self.priority = 100
        self._idx_ = 0

    async def start_item(
        self, item: SequenceItem, priority: int = None, sequencer: Sequencer = None
    ) -> None:
        """
        Starts an item in the sequence.

        :param item: The item to be started.
        :type item: SequenceItem
        :param priority: The priority of the item (optional).
        :type priority: int
        :param sequencer: The sequencer to be used (optional).
        :type sequencer: Sequencer
        """
        item.set_id(self._idx_)
        self._idx_ += 1

        if sequencer is not None:
            _sqr = sequencer
        else:
            _sqr = self.get_sequencer()

        item.set_sequencer(_sqr)

        await _sqr.wait_for_grant(self, priority)

        self.pre_do(item)

    async def finish_item(self, item: SequenceItem) -> None:
        """
        Finishes an item in the sequence.

        :param item: The item to be finished.
        :type item: SequenceItem
        """
        self.mid_do(item)

        _sqr = item.get_sequencer()
        if _sqr is None:
            raise Exception("Sequence item has no sequencer")

        _sqr.send_request(self, item)

        await item.wait_on_event("done")

        self.post_do(item)

Driver and Monitor Link to heading

Now, we have to look at the pin-level stuff, which uses cocotb to drive and read signals. The run_phase gets transactions from seq_item_port and drives them on hdl.

class adder_driver(avl.Driver):
    def __init__(self, name, parent):
        super().__init__(name, parent)

    async def connect_phase(self):
        self.hdl = avl.Factory.get_variable(f"{self.get_full_name()}.hdl", None)

    async def reset(self):
        self.hdl.valid_in.value = 0
        self.hdl.a.value = 0
        self.hdl.b.value = 0

    async def clear(self):
        await RisingEdge(self.hdl.clk)
        await self.reset()

    async def run_phase(self):
        await self.reset()

        await RisingEdge(self.hdl.clk)
        while True:
            item = await self.seq_item_port.blocking_get()

            while True:
                await RisingEdge(self.hdl.clk)
                if self.hdl.rst_n.value == 0:
                    await self.reset()
                else:
                    break

            self.hdl.valid_in.value = 1
            self.hdl.a.value = item.a.value
            self.hdl.b.value = item.b.value
            item.set_event("done")
            cocotb.start_soon(self.clear())

Driver extends Component and defines seq_item_port.

class Driver(Component):
    def __init__(self, name: str, parent: Component) -> None:
        """
        Initialize Driver.

        :param name: Name of the driver.
        :type name: str
        :param parent: Parent component.
        :type parent: Component
        """
        super().__init__(name, parent)
        self.seq_item_port = List()

The interesting part in AVL is the coverage semantics. In this case, there are a couple of coverpoints. Then the monitor writes the sampled transaction to item_export.

class adder_monitor(avl.Monitor):
    def __init__(self, name, parent):
        super().__init__(name, parent)

        self.cg = avl.Covergroup('cg',self)
        self.cp_a = self.cg.add_coverpoint('cp_a', lambda: self.hdl.a.value)
        self.cp_a.add_bin('a_zero_bin',0)
        self.cp_a_cross = self.cg.add_coverpoint('cp_a_cross', lambda: self.hdl.a.value)
        self.cp_a_cross.add_bin('a_overflow',range(128,255))

        self.cp_b = self.cg.add_coverpoint('cp_b', lambda: self.hdl.b.value)
        self.cp_b.add_bin('b_zero_bin',0)
        self.cp_b_cross = self.cg.add_coverpoint('cp_b_cross', lambda: self.hdl.b.value)
        self.cp_b_cross.add_bin('b_overflow',range(128,255))

        self.cg.add_covercross("overflow_cover",self.cp_a_cross, self.cp_b_cross)

    async def connect_phase(self):
        self.hdl = avl.Factory.get_variable(f"{self.get_full_name()}.hdl", None)

    async def collect_result(self, item):
        await RisingEdge(self.hdl.clk)
        if self.hdl.valid_out.value != 1:
            self.error(f"Expected valid_out to be 1, got {self.hdl.valid_out.value}")

        item.c.value = int(self.hdl.c.value)
        self.item_export.write(item)

    async def run_phase(self):
        while True:
            await RisingEdge(self.hdl.clk)

            if self.hdl.rst_n.value == 0:
                continue

            if self.hdl.valid_in.value == 1:
                item = adder_item("item", None)
                item.a.value = int(self.hdl.a.value)
                item.b.value = int(self.hdl.b.value)
                cocotb.start_soon(self.collect_result(item))

            # collect coverage
            self.cg.sample()

    async def report_phase(self):
        print(self.cg.report(full=True))

Similar to the driver, Monitor just extends Component and defines item_export.

class Monitor(Component):
    def __init__(self, name: str, parent: Component) -> None:
        """
        Initialize the Monitor instance.

        :param name: Name of the monitor.
        :type name: str
        :param parent: Parent component.
        :type parent: Component
        """
        super().__init__(name, parent)
        self.item_export = Port("item_export", self)

Env Link to heading

The rest of the env is the typical UVM stuff where agent, model, scoreboard, and reference are created inside the env. Notice that connections are mainly in the connect_phase of adder_agent, the container of all other components.

class adder_model(avl.Model):
    def __init__(self, name, parent):
        super().__init__(name, parent)

    async def run_phase(self):
        while True:

            monitor_item = await self.item_port.blocking_get()
            model_item = copy.deepcopy(monitor_item)
            model_item.c.value = monitor_item.a.value + monitor_item.b.value
            self.item_export.write(model_item)

class adder_scoreboard(avl.Scoreboard):
    def __init__(self, name, parent):
        super().__init__(name, parent)
        self.set_min_compare_count(100)

class adder_agent(avl.Agent):
    def __init__(self, name, parent):
        super().__init__(name, parent)

    async def build_phase(self):
        self.sqr = adder_sequencer("sqr", self)
        self.seq = adder_sequence("seq", self.sqr)
        self.drv = adder_driver("drv", self)
        self.mon = adder_monitor("mon", self)
        self.model = adder_model("model", self)
        self.sb = adder_scoreboard("sb", self)

        # Set the sequencer for the sequence
        self.seq.set_sequencer(self.sqr)

    async def connect_phase(self):
        # Connect the sequencer to the driver
        self.sqr.seq_item_export.connect(self.drv.seq_item_port)

        # Connect the monitor to the model and scoreboard
        self.mon.item_export.connect(self.model.item_port)
        self.mon.item_export.connect(self.sb.after_port)
        self.model.item_export.connect(self.sb.before_port)

    async def run_phase(self):
        self.raise_objection()

        # Start the sequence
        await self.seq.start()
        self.drop_objection()

class adder_env(avl.Env):
    def __init__(self, name, parent):
        super().__init__(name, parent)

    async def build_phase(self):
        self.agent = avl.Agent("agent", self)

    async def connect_phase(self):
        self.clk = avl.Factory.get_variable(f"{self.get_full_name()}.clk", None)
        self.rst = avl.Factory.get_variable(f"{self.get_full_name()}.rst", None)
        self.clk_freq_mhz = avl.Factory.get_variable(f"{self.get_full_name()}.clk_freq_mhz", 100)
        self.reset_ns = avl.Factory.get_variable(f"{self.get_full_name()}.reset_ns", 100)
        self.timeout_ns = avl.Factory.get_variable(f"{self.get_full_name()}.timeout_ns", 100000)

    async def run_phase(self):
        await cocotb.start(self.clock(self.clk, self.clk_freq_mhz))

        await cocotb.start(self.async_reset(self.rst, self.reset_ns, active_high=False))

        await cocotb.start(self.timeout(self.timeout_ns))

Test Link to heading

The interesting thing here is there is no test class. The cocotb test works as the top-level test for the hierarchy. It seems AVL needs to know which phases are enabled. In this case, the build BUILD and CONNECT.

Besides the test thing, another difference in AVL is it uses Factory and config_db. I guess factory is not really needed as Python doesn’t have the static typing limitations of SystemVerilog.

Finally, the test calls e.start() to start running the phases on the env.

@cocotb.test
async def test(dut):
    avl.PhaseManager.add_phase("BUILD", after=None, top_down=True)
    avl.PhaseManager.add_phase("CONNECT", after=avl.PhaseManager.get_phase("BUILD"), top_down=True)

    # Create the environment
    avl.Factory.set_variable('*.hdl', dut)
    avl.Factory.set_variable('*.clk', dut.clk)
    avl.Factory.set_variable('*.rst', dut.rst_n)
    avl.Factory.set_variable('env.timeout_ns', 100000)
    avl.Factory.set_variable('env.clk_freq_mhz', 100)
    avl.Factory.set_variable('*.n_items', 200)

    e = adder_env('adder_env', None)
    await e.start()