This cocotb extension contains driver and monitor modules for the Wishbone bus.
Clone the repository:
$ git clone https://github.com/wallento/cocotbext-wishbone.git
Then install it with pip:
$ python3 -m pip install -e cocotbext-wishbone
To install it with pip published globally simply use pip install as usual:
$ python3 -m pip install cocotbext-wishbone
As an example we will instantiate a Wishbone master cocotb driver to read and write on a DUT wishbone slave. First import this
from cocotbext.wishbone.driver import WishboneMaster from cocotbext.wishbone.driver import WBOp
The DUT ports naming in Verilog is following:
input clock, input [1:0] io_wbs_adr, input [15:0] io_wbs_datwr, output [15:0] io_wbs_datrd, input io_wbs_we, input io_wbs_stb, output io_wbs_ack, input io_wbs_cyc,
To initialize our master we have to do this:
self.wbs = WishboneMaster(dut, "io_wbs", dut.clock, width=16, # size of data bus timeout=10) # in clock cycle number
But in actuals port name are rarely the same has seen above. In this case actuals ports names are for example:
input clock input [1:0] io_wbs_adr_i, input [15:0] io_wbs_dat_i, output [15:0] io_wbs_dat_o, input io_wbs_we_i, input io_wbs_stb_i, output io_wbs_ack_o, input io_wbs_cyc_i,
Then we have to rename it with signals_dict
arguments:
self.wbs = WishboneMaster(dut, "io_wbs", dut.clock, width=16, # size of data bus timeout=10, # in clock cycle number signals_dict={"cyc": "cyc_i", "stb": "stb_i", "we": "we_i", "adr": "adr_i", "datwr":"dat_i", "datrd":"dat_o", "ack": "ack_o" })
In the testbench, to make read/write access we have to use the method
send_cycle()
with a list of special class operator named WBOp()
.
WBOp()
is accepting the following arguments, all with default value:
adr: address of the operation dat: data to write, None indicates a read cycle idle: number of clock cycles between asserting cyc and stb sel: the selection mask for the operation WBOp(adr=0, dat=None, idle=0, sel=None)
If no dat
is given, a wishbone read will be done. If dat
is filled, it will be a
write.
For example, to read respectively at address 2
, 3
, 0
then 1
, we will do:
wbRes = async rdbg.wbs.send_cycle([WBOp(2), WBOp(3), WBOp(0), WBOp(1)])
The send_cycle()
method returns a list of Wishbone Result Wrapper Class WBRes()
with
some data declared like it in :file:`driver.py`:
def __init__(self, ack=0, sel=None, adr=0, datrd=None, datwr=None, waitIdle=0, waitStall=0, waitAck=0):
If we want to print the value being read, we just have to read datrd
value like so:
rvalues = [wb.datrd for wb in wbRes] dut.log.info(f"Returned values : {rvalues}")
Which will print a log message like following:
1560.00ns INFO Returned values : [0000000000000000, 0000000000000000, 0000000100000001, 0000000000000000]
We can add some write operations in our send_cycle()
, by adding a second value
in parameters:
wbRes = async rdbg.wbs.send_cycle([WBOp(3, 0xcafe), WBOp(0), WBOp(3)])
The above line will write 0xcafe
at address 3
, then read at address 0
, then read at
address 3
.
The Monitor instantiation works similarly to the Driver instantiation. First import the right module
from cocotbext.wishbone.monitor import WishboneSlave
Then instantiate the object with right signals names
wbm = WishboneSlave(dut, "io_wbm", dut.clock, width=16, # size of data bus signals_dict={"cyc": "cyc_o", "stb": "stb_o", "we": "we_o", "adr": "adr_o", "datwr":"dat_o", "datrd":"dat_i", "ack": "ack_i" })
WishboneSlave
is a monitor, then it's mainly a passive class. It will supervise
the Wishbone signal and records transaction in a list named _recvQ
.
Each time the monitor detect a transaction on the bus, the transaction is append
to _recvQ
.
A transaction is a list of WBRes
objects which contain some signal values read on
the bus
@public class WBRes(): ... def __init__(...): self.ack = ack self.sel = sel self.adr = adr self.datrd = datrd self.datwr = datwr self.waitStall = waitStall self.waitAck = waitAck self.waitIdle = waitIdle
At the end of the simulation, if we want to display the adr
, datrd
and datwr
values
on the bus we will do following for example
for transaction in wbm._recvQ: wbm.log.info(f"{[f'@{hex(v.adr)}r{hex(v.datrd)}w{hex(0 if v.datwr is None else v.datwr)}' for v in transaction]}")
We can also register a callback function that will be called each time a transaction occured:
def simple_callback(transaction): print(transaction) wbm.add_callback(simple_callback)
But be aware that if a callback is registered, _recvQ
will not be populated.
Here are some projects that use this module, to use as examples:
- ChisArmadeus: Useful chisel components for Armadeus boards.
It uses cocotb for testing.
An example is given for
op6ul
wrapper test here - wbGPIO: General purpose input output wishbone slave written in Chisel. The cocotb testbench is available here