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()