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 fromBaseIO
- this is expected byBaseDriver
andBaseMonitor
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 DUTname
- the common name of the bus, orNone
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 thedata
and thevalid
qualifierresp_sigs
- a list of signals that are driven by the responder to an interface, in this case this is just theready
signalio_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 witho_
- 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 orNone
if unusedcomponent
- name of the interface component as a stringrole_bus
- role of the entire bus fromIORole
(e.g.IORole.INITIATOR
)role_comp
- role of the specific component fromIORole
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 |