This repository contains AHB drivers (AHB and AHB Lite) and a monitor for cocotb.
Installation from pip (release version, stable):
$ pip install cocotbext-ahb
Installation for active development:
$ git clone https://github.com/aignacio/cocotbext-ahb
The repository contains a small script that starts a container fetched from Docker Hub, equipped with all the required development tools.
$ cd cocotbext-ahb/
$ ./ship.sh
# To run all tests
$ nox
# To run a specific test
$ nox -s run -- -k "test_ahb_lite.py"
# To run lint
$ nox -s lint
Once the container is up and running, to run the tests through nox and pytest, run the following:
$ nox -l # See the available build options
$ nox -s run-3.xx # Replace xx by a python version you want to test
The command above will take some time to complete (depends on your machine cfg) because it will run all the tests and its variants, generating waveforms in each run_dir/*
folder. At the end of the test run, you can open the waveforms for each test in this format using gtkwave:
gtkwave run_dir/sim_build_icarus_(TEST_NAME)/ahb_template.fst docs_utils/template.gtkw
Running specific tests:
$ nox -s run-3.10 -- -k "test_ahb_monitor_slave" # Runs on python 3.10, the test_ahb_monitor_slave
For the list of test, please check the tests/
directory.
This AHB extension is composed by master, slaves and a single monitor. Thus, the following available classes are:
All the different master/slaves and also the monitor requires an AHBBus object to be passed to their constructors. This AHBBus object will map each of the AHB I/F pins the dut, some IOs are mandatory but others are optional. In order to create an AHBBus object, here are the two ways.
With a prefix:
# In case your DUT has some prefix for the AHB I/F
# For instance if all AHB signals are following this convention:
# - test_haddr
# - test_hsel
# - ...
AHBBus.from_prefix(dut, "test")
Without a prefix:
# In case your DUT has no prefix for the AHB I/F
# For instance if all AHB signals are following this convention:
# - haddr
# - hsel
# - ...
AHBBus.from_entity(dut)
For reference, down below is the class header of AHB Bus. More arguments can also be passed as this extends from cocotb-bus base class.
class AHBBus(Bus):
_signals = ["haddr", "hsize", "htrans", "hwdata",
"hrdata", "hwrite", "hready", "hresp"]
_optional_signals = ["hburst", "hmastlock", "hprot", "hnonsec",
"hexcl", "hmaster", "hexokay", "hsel", "hready_in"]
Notes:
a. This signal is driven high during the txn start but it does not follow the hreadyout loopback from the slave when the slave is not available, if the loopback is expected, suggestion is to connect the hready_in
directly to the hreadyout
.
To customize signals name to your design signals
and optional_signals
dictionnary can be provide.
AHBBus.from_prefix(
dut,
prefix = "slave_driver",
signals = {
"haddr" : "haddr",
"hsize" : "hsize",
"htrans" : "htrans",
"hwdata" : "hwdata",
"hrdata" : "hrdata",
"hwrite" : "hwrite",
"hready" : "hreadyout",
"hresp" : "hresp"
}
,
optional_signals = {
"hsel" : "hsel",
"hready_in" : "hready"
}
)
Both AHB Master [WIP] and AHB Lite Master classes have the same constructor arguments. Within the arguments, it is required to pass the AHB Bus object, the clock and reset DUT pins. As optional args, a timeout value in clock cycles (per AHB txn), the default value of the master driven IOs and the name of the object.
class AHBLiteMaster:
def __init__(
self,
bus: AHBBus,
clock: str,
reset: str,
timeout: int = 100,
def_val: Union[int, str] = "Z",
name: str = "ahb_lite",
**kwargs,
):
In case of AHB Slave error response, the master will cancel the current transaction changing HTRANS from NSEQ to IDLE in the second clock cycle of the error response and then it will retry immediately after (following clock cycle). This is not mandatory but gives time for the master to decide whether it needs to be aborted or not the following transaction.
Its methods are composed by read(), write() and custom().
For writes, the arguments are listed here:
async def write(
self,
address: Union[int, Sequence[int]],
value: Union[int, Sequence[int]],
size: Optional[Union[int, Sequence[int]]] = None,
pip: Optional[bool] = False,
verbose: Optional[bool] = False,
sync: Optional[bool] = False,
format_amba: Optional[bool] = False,
) -> Sequence[dict]:
Arguments
Return
Note: address, value and size have to match their length if provided.
For reads, the arguments are listed here:
async def read(
self,
address: Union[int, Sequence[int]],
size: Optional[Union[int, Sequence[int]]] = None,
pip: Optional[bool] = False,
verbose: Optional[bool] = False,
sync: Optional[bool] = False,
) -> Sequence[dict]:
Arguments
Return
Note: address and size have to match their length if provided.
A third method provides flexibility in case the user wants to perform read or writes together like back-to-back operations such as WAR or RAW. For this, the arguments are listed here:
async def custom(
self,
address: Union[int, Sequence[int]],
value: Union[int, Sequence[int]],
mode: Union[int, Sequence[int]],
size: Optional[Union[int, Sequence[int]]] = None,
pip: Optional[bool],
verbose: Optional[bool] = False,
) -> Sequence[dict]:
Arguments
Return
Note: address, value, mode and size have to match their length if provided.
Both AHB Slave [WIP] and AHB Lite Slave classes have the same constructor arguments. Within the arguments, it is required to pass the AHB Bus object, the clock and reset DUT pins. As optional arg, the default value of the slave driven IOs, a generator function to force back-pressure and the name of the object.
class AHBLiteSlave:
def __init__(
self,
bus: AHBBus,
clock: str,
reset: str,
bp: Generator[int, None, None] = None,
name: str = "ahb_lite",
reset_act_low: bool = True,
**kwargs,
):
The AHB slaves will not provide any specific data (always zero) or unexpected response, they serve as a basic slave just to check its connectivity while testing AHB Master and as a base class for the AHB Lite Slave RAM. The back-pressure feature is a way to force the slave to demonstrated unavailability while the master issue AHB transactions. The generator needs to return bool type values where bool True indicates slave available and bool False indicate slave unavailable.
In case of an AHB error response, the Slave inserts a wait state (HREADY == LOW && HRESP == OKAY) however this not required and might change in the future sticking only to the mandatory obligation of 2-cycle error response:
Note: Following ARM's spec, the slave needs to assert HREADY (or HREADYOUT) high during reset, it is assumed that the reset
argument is active-low.
The AHB Lite Slave RAM is a basic memory slave that can receive reads and write like a normal memory-mapped device. The only difference between the normal slave vs this one is the fact that as part of its constructor, a new argument mem_size is listed. This argument defines a memory size in bytes for the AHB slave. The only limitation for now, is the fact that all memory addresses have to be aligned to the data bus width, i.e for 32-bit slaves, address[1:0] == 2'b00.
class AHBLiteSlaveRAM(AHBLiteSlave):
def __init__(
self,
bus: AHBBus,
clock: str,
reset: str,
bp: Generator[int, None, None] = None,
name: str = "ahb_lite_ram",
mem_size: int = 1024,
**kwargs,
):
Thank you @alexforencich for your work on the memory classes that were leveraged in this project.
A basic AHB monitor was also developed, the idea is to ensure that basic protocol assumptions are respected throughout assertions, its constructor arguments are very similar to the previous discussed classes. For now, the monitor checks for basic protocol violations such as :
class AHBMonitor(Monitor):
def __init__(
self,
bus: AHBBus,
clock: str,
reset: str,
prefix: str = None,
**kwargs: Any
) -> None:
As the monitor is extended from Monitor cocotb-bus class, it is possible to pass its object as a callable reference to a Scoreboard object and use it to compare transactions. For reference, please check the tests/test_ahb_lite_monitor_scoreboard.py
and see how its implemented.
A basic example of this extension usage is demonstrated below and also within the tests folder that are available.
import cocotb
import os
import random
import math
from const import cfg
from cocotb_test.simulator import run
from cocotb.triggers import ClockCycles
from cocotb.clock import Clock
from cocotbext.ahb import AHBBus, AHBLiteMaster, AHBLiteSlaveRAM, AHBResp, AHBMonitor
def rnd_val(bit: int = 0, zero: bool = True):
if zero is True:
return random.randint(0, (2**bit) - 1)
else:
return random.randint(1, (2**bit) - 1)
def pick_random_value(input_list):
if input_list:
return random.choice(input_list)
else:
return None # Return None if the list is empty
async def run_test(dut):
data_width = 32
mem_size_kib = 16
N = 1000
# Map the bus monitor to the I/F slave_h* I/F
ahb_lite_mon = AHBMonitor(
AHBBus.from_prefix(dut, "slave"), dut.hclk, dut.hresetn
)
# Create an AHB Lite Slave RAM connected to the master I/F
ahb_lite_sram = AHBLiteSlaveRAM(
AHBBus.from_prefix(dut, "master"),
dut.hclk,
dut.hresetn,
bp=bp_fn,
mem_size=mem_size_kib * 1024,
)
# Create an AHB Lite Master connected to the slave I/F
ahb_lite_master = AHBLiteMaster(
AHBBus.from_prefix(dut, "slave"), dut.hclk, dut.hresetn, def_val="Z"
)
# Create a lit of non-repeated addresses spaces by 8 from 0->2*current RAM size
address = random.sample(range(0, 2 * mem_size_kib * 1024, 8), N)
value = [rnd_val(data_width) for _ in range(N)]
size = [pick_random_value([1, 2, 4]) for _ in range(N)]
# Perform the writes and reads
resp = await ahb_lite_master.write(address, value, size, pip=pip_mode)
resp = await ahb_lite_master.read(address, size, pip=pip_mode)
Some designs might use asynchronous reset thus typically, the reset toggles before the clock is on in the test run. Considering this case, if an AHB txn is issued at the very first clock edge, it is observed an issue where the address phase of the first AHB transaction does not get driven correctly (similar to the image below).
In order to workaround this issue, it is suggested to:
cocotbext-ahb is licensed under the permissive MIT license.Please refer to the LICENSE file for details.