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
op6ulwrapper test here - wbGPIO: General purpose input output wishbone slave written in Chisel. The cocotb testbench is available here