Skip to content

Interfaces

Drivers and Monitors are most useful when interacting with bus protocols where the interface may not only transport data but also sideband signals (e.g. transaction IDs) and qualifiers (e.g. valid/ready). To support this, Forastero offers BaseIO which can be extended to define a grouping of signals that are common to your DUTs interfaces.

For example, imagine that our DUT has two interfaces - one incoming stream and one outgoing stream. These stream interfaces carry both data along with valid and ready qualifiers to ensure correct data transport...

module buffer #(
      input  logic        i_clk
    , input  logic        i_rst
    // Upstream
    , input  logic [31:0] i_us_data
    , input  logic        i_us_valid
    , output logic        o_us_ready
    // Downstream
    , output logic [31:0] o_us_data
    , output logic        o_us_valid
    , input  logic        i_us_ready
);

We can declare a matching Forastero I/O definition to wrap up the data, valid, and ready signals into one convenient object:

from forastero import BaseIO, IORole

class StreamIO(BaseIO):
    def __init__(
        self,
        dut: HierarchyObject,
        name: str | None,
        role: IORole,
        io_style: Callable[[str | None, str, IORole, IORole], str] | None = None,
    ) -> None:
        super().__init__(
            dut=dut,
            name=name,
            role=role,
            init_sigs=["data", "valid"],
            resp_sigs=["ready"],
            io_style=io_style,
        )

What's going on here then?

  • StreamIO extends from BaseIO - this is expected by BaseDriver and BaseMonitor and they will raise an error if this is not the case
  • The constructor is provided with
  • dut - a pointer to the top level of your DUT
  • name - the common name of the bus, or None if one is not used;
  • role - the "orientation" of the bus that is being referenced, for example if a stream interface is passing out of a block this makes the block an 'initiator' (role=IORole.INITIATOR) while if a stream interface is passing into a block then this makes the block a 'responder' (role=IORole.RESPONDER)
  • init_sigs - a list of signals that are driven by the initator of an interface, in this case the data and the valid qualifier
  • resp_sigs - a list of signals that are driven by the responder to an interface, in this case this is just the ready signal
  • io_style - how components of the interface are named, see the section below for more details.

In our testbench we can then wrap the upstream and downstream interfaces of the DUT very easily:

from forastero import BaseBench, IORole

from .stream.io import StreamIO

class Testbench(BaseBench):
    def __init__(self, dut):
        # ...
        us_io = StreamIO(dut=dut, name="us", role=IORole.RESPONDER)
        ds_io = StreamIO(dut=dut, name="ds", role=IORole.INITIATOR)

Warning

The role argument to classes derived from BaseIO refers to the role of the DUT's interface, not to the role the testbench plays!

IO Naming Style

When using BaseIO, its default behaviour is to:

  • Expect that inputs are prefixed with i_ and outputs with o_
  • The prefix is followed by the bus name (e.g. i_us_)
  • A final segment is included for the component name (e.g. i_us_data)

Under the hood this behaviour is defined by the io_prefix_style, but this can be overridden either globally or on a case-by-case basis.

An interface naming style is declared by a function that takes four arguments and returns a string:

  • bus - name of the bus as a string or None if unused
  • component - name of the interface component as a string
  • role_bus - role of the entire bus from IORole (e.g. IORole.INITIATOR)
  • role_comp - role of the specific component from IORole

The example below shows the implementation of io_prefix_style:

def io_prefix_style(bus: str | None, component: str, role_bus: IORole, role_comp: IORole) -> str:
    mapping = {
        (IORole.INITIATOR, IORole.INITIATOR): "o",
        (IORole.INITIATOR, IORole.RESPONDER): "i",
        (IORole.RESPONDER, IORole.INITIATOR): "i",
        (IORole.RESPONDER, IORole.RESPONDER): "o",
    }
    full_name = f"{mapping[role_bus, role_comp]}"
    if bus is not None:
        full_name += f"_{bus}"
    return f"{full_name}_{component}"

If you define a custom naming style, this can be used when instancing a class that inherits from BaseIO:

def my_io_style(bus: str | None, component: str, role_bus: IORole, role_comp: IORole) -> str:
    # ...

class Testbench(BaseBench):
    def __init__(self, dut):
        us_io = StreamIO(
            dut=dut, name="us", role=IORole.RESPONDER, io_style=my_io_style
        )

Or, the style can be globally overridden:

from forastero import BaseIO

def my_io_style(bus: str | None, component: str, role_bus: IORole, role_comp: IORole) -> str:
    # ...

BaseIO.DEFAULT_IO_STYLE = my_io_style

class Testbench(BaseBench):
    def __init__(self, dut):
        us_io = StreamIO(dut=dut, name="us", role=IORole.RESPONDER)

Note

Three I/O styles are provided with Forastero, the first (and default) is io_prefix_style where signals are named i/o_<BUS>_<COMPONENT> but there is also io_suffix_style, where the i/o is moved to the end of the signal name <BUS>_<COMPONENT>_i/o, and io_plain_style where there is no i/o prefix or suffix and signals are simply named <BUS>_<COMPONENT>.


forastero.io.IORole

Bases: IntEnum

Role that a particular bus is performing (determines signal suffix)


forastero.io.BaseIO

Wraps a collection of different signals into a single interface that can be used by drivers and monitors to interact with the design.

Parameters:

Name Type Description Default
dut HierarchyObject

Pointer to the DUT boundary

required
name str | None

Name of the signal - acts as a prefix

required
role IORole

Role of this signal on the DUT boundary

required
init_sigs list[str]

Signals driven by the initiator

required
resp_sigs list[str]

Signals driven by the responder

required
io_style Callable[[str | None, str, IORole, IORole], str] | None

Optionally override the default I/O naming style

None

get(comp, default=None)

Get the current value of a particular signal.

Parameters:

Name Type Description Default
comp str

Name of the component

required
default Any

Default value if the signal is not resolved

None

Returns:

Type Description
Any

The resolved value, otherwise the default

has(comp)

Test whether a particular signal has been resolved inside the interface.

Parameters:

Name Type Description Default
comp str

Name of the component

required

Returns:

Type Description
bool

True if exists, False otherwise

initialise(role)

Initialise signals according to the active role

set(comp, value)

Set the value of a particular signal if it exists.

Parameters:

Name Type Description Default
comp str

Name of the component

required
value Any

Value to set

required

set_default(comp, value)

Set the default value to be returned for a signal if it is not available.

Parameters:

Name Type Description Default
comp str

Component name

required
value Any

Value to return

required

width(comp)

Return the width of a particular signal.

Parameters:

Name Type Description Default
comp str

Name of the component

required

Returns:

Type Description
int

The bit width if resolved, else 0