Structs

AVL provides a generic structs wrapper.

The main reason for including a struct wrapper is Verilator flattens all structs into a single variable. This means that the user must manually manage the offsets of each field in the struct. This is error prone, difficult to debug and maintain.

Commercial simulators usually provide PLI/VPI access to individual fields in structs, but the avl.Struct can still be useful to ensure a test-bench works across all simulators.

The variables declared in the struct must be variations of the avl.Var class, as these classes have understanding of the width and therefore can be used to pack and unpack the struct.

The declaration order of variables matches those of the Verilog struct syntax.

Example

# Copyright 2024 Apheleia
#
# Description:
# Apheleia attributes example


import copy

import avl
import cocotb
from cocotb.triggers import Timer
from z3 import ULT


class packed_struct_t(avl.Struct):
    single_bit : avl.Bool = avl.Bool(False)
    multi_bit : avl.Uint32 = avl.Uint32(0)
    state_enum : avl.Enum = avl.Enum("S0", {"S0" : 0, "S1" : 1, "S2" : 2})

class struct_a_b_t(avl.Struct):
    var_a   : avl.Logic = avl.Logic(0, width = 4)
    var_b   : avl.Logic = avl.Logic(0, width = 4)

class struct_c_d_t(avl.Struct):
    var_c   : avl.Logic = avl.Logic(0, width = 4)
    var_d   : avl.Logic = avl.Logic(0, width = 4)

class struct_a_b_c_d_t(avl.Struct):
    struct_a_b : struct_a_b_t = struct_a_b_t()
    struct_c_d : struct_c_d_t = struct_c_d_t()

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

        self.s0 = packed_struct_t()
        self.s1 = packed_struct_t()

        assert self.s0.width == self.s1.width == 35

    async def run_phase(self):

        self.raise_objection()

        for i in range(10):
            await Timer(10, unit="ns")

            self.dut.value = self.s0.to_bits()

            await Timer(1, "ns")
            self.s1.from_bits(self.dut)

            assert i%2 == self.s0.single_bit == self.s1.single_bit
            assert i   == self.s0.multi_bit == self.s1.multi_bit
            assert self.s0.state_enum == self.s1.state_enum


            self.s0.single_bit += 1
            self.s0.multi_bit += 1

            if bool(self.s0.single_bit):
                self.s0.state_enum.value = "S2"
            else:
                self.s0.state_enum.value = "S0"


        # Test randomization
        self.s0_copy = copy.deepcopy(self.s0)
        self.s0.multi_bit.add_constraint("c_multi_bit", lambda x: ULT(x,100))
        self.s0_copy.multi_bit.add_constraint("c_multi_bit", lambda x: x == 200)
        self.s0.single_bit.value = 0
        self.s0.multi_bit.value = 0
        self.s0.state_enum.value = "S0"

        for _ in range(10):

            await Timer(10, unit="ns")
            self.randomize()

            self.s0.to_hdl(self.dut)

            await Timer(1, "ns")
            self.s1.from_hdl(self.dut)

            assert self.s0.single_bit == self.s1.single_bit
            assert self.s0.multi_bit == self.s1.multi_bit
            assert self.s0.multi_bit < 100
            assert self.s0.state_enum == self.s1.state_enum

            assert self.s0_copy.multi_bit.value == 200

        await Timer(10, unit="ns")

        # Test nested structs
        self.test_nested_structs()
        await Timer(10, unit="ns")

        # Test the .value shortcut
        self.s0.value = 0
        assert self.s0.single_bit.value == 0 and self.s0.multi_bit.value == 0 and self.s0.state_enum.value == 0
        assert self.s0.value == 0

        self.s0.value = 1
        assert self.s0.single_bit.value == 0 and self.s0.multi_bit.value == 0 and self.s0.state_enum.value == 1
        assert self.s0.value == 1

        # Test the slice shortcuts
        self.s0.value = 0
        self.s0[34] = 1
        assert self.s0.single_bit.value == 1 and self.s0.multi_bit.value == 0 and self.s0.state_enum.value == 0
        assert self.s0[34] == 1

        self.s0.value = 0
        self.s0[2:34] = 0xdeadbeef
        assert self.s0.single_bit.value == 0 and self.s0.multi_bit.value == 0xdeadbeef and self.s0.state_enum.value == 0
        assert self.s0[2:34] == 0xdeadbeef

        self.drop_objection()

    def check_struct(self, struct, expected):
        self.info(f"Struct:{struct}")
        for field_name, _field_type in struct._fields_:
            field_val = getattr(struct, field_name)
            if hasattr(field_val, '_fields_'):  # it's a nested struct
                self.check_struct(field_val, expected)
            else:  # it's a leaf value
                self.info(f"  {field_name}: {field_val}")
                assert field_val == expected[field_name], \
                    f"variable {field_name} is not correctly assigned. Expected: {hex(expected[field_name])}, Actual: {hex(field_val)}"

    def test_nested_structs(self):
        _var_a = 0xA
        _var_b = 0xB
        _var_c = 0xC
        _var_d = 0xD
        init_val = (_var_a << 12) | (_var_b << 8) | (_var_c << 4) | _var_d

        # Create a dict to map field names to expected values
        expected = {
            "var_a": _var_a,
            "var_b": _var_b,
            "var_c": _var_c,
            "var_d": _var_d,
        }

        self.info(f"Testing nested struct with init_val = {hex(init_val)}")
        full_struct = struct_a_b_c_d_t()
        #using the from_bits function
        full_struct.from_bits(init_val)
        self.info("Calling check_struct for inital value")
        self.check_struct(full_struct, expected)
        #using the to_bits function
        self.info("Setting a varible using to_bits function")
        to_bits_var = full_struct.to_bits()
        self.info(f"Value set is equal to {hex(to_bits_var)}")
        assert to_bits_var == init_val, f"to_bits is not working. Expected {hex(init_val)}, Actual: {hex(to_bits_var)}"

@cocotb.test
async def test(dut):
    e = example_env("env", None)
    e.dut = dut.data

    await e.start()