This is the home of sample interaction scripts and tests specific to given projects.
The idea is to have a standardized place for sample code and to allow people to try out projects with ease.
As of version 2.0 of the SDK, the demoboard API has been standardized to match that used in Verilog (uo_out, ui_in, etc) and made such that existing cocotb version 2.0 tests should be usable, in many cases as-is.
A system is in place to support cocotb style tests, such that the code used during development simulation should be usable (almost) as-is with hardware-in-the-loop, directly on the demoboard.
For example
@cocotb.test()
async def test_counter(dut):
dut._log.info("Start")
clock = Clock(dut.clk, 10, units="us")
cocotb.start_soon(clock.start())
dut.uio_oe_pico.value = 0 # all inputs on our side
dut.ui_in.value = 0b1
dut.rst_n.value = 0
await ClockCycles(dut.clk, 10)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 1)
dut._log.info("Testing counter")
for i in range(256):
assert dut.uo_out.value == dut.uio_out.value, f"uo_out != uio_out"
assert int(dut.uo_out.value) == i, f"uio value not incremented correctly {dut.uio_out.value} != {i}"
await ClockCycles(dut.clk, 1)
dut._log.info("test_counter passed")
will be detected as a test case and run as part of the testbench. You can see the full sample in tt_um_factory_test.py.
To run an existing test, import the module and call its run()
function:
>>> import examples.tt_um_factory_test as test
>>> test.run()
If you have a project on a chip and would like to run your own tests, you can re-use much of your cocotb testbench (you did have a testbench, right?).
To start
-
create a package using the name of your project as per the submitted info.yaml. This is a subdirectory of that name with an
__init__.py
file. -
create modules that include your
@cocotb.test()
functions -
import those modules and expose a
run()
method in your top level__init__.py
.
The cocotb.test()
functions should work as-is, with the following caveats
-
We don't have access to internals, so restrict the tests to treating the dut as a blackbox, meaning you can only play with I/O (writing to
dut.ui_in
anddut.uio_in
, reading fromdut.uo_out
anddut.uio_out
) -
The bidirectional pin direction is actually set by the design inside the chip. We are outside the ASIC, so you must mirror that (i.e. if the project sets a pin as an input, we want to set the corresponding pin as an output to be able to write to it within tests)
To make the fact that you are setting bidir pin direction from the RP2040 side, the API uses the name uio_oe_pico
.
If, for example, the project verilog sets
assign uio_oe = 0xf0 /* 0b11110000 high nibble out, low nibble in*/
Then, in your test, you would set
dut.uio_oe_pico = 0x0f # 0b00001111 high nibble in, low nibble out
The SDK cocotb implementation supports
-
@cocotb.test() detection, with all optional parameters (as of now name, expect_fail, timeout_* and skip are respected)
-
setting up one or more clocks using Clock() and cocotb.start_soon()
-
get_sim_time()
-
await on Timer, ClockCycles, RisingEdge, and FallingEdge
Within tests, you may read
-
dut.uo_out.value
-
dut.uio_out.value
and write to
-
dut.ui_in.value
-
dut.uio_in.value
and do the usual things, like
assert dut.uo_out.value == 4, f"output should be 4!"
# or
dut.ui_in.value = 0xff
await ClockCycles(dut.clk, 2)
In addition, though this is as of yet unsupported in cocotb v2 (I've submitted patches and there's an ongoing discussion), value bit and slice access is fully supported, for example
dut.ui_in.value[0] = 1
dut.ui_in.value[3:2] = 0b11
assert dut.uo_out.value[7] == 1, "high bit should be TRUE"
This is the area with the greatest delta from standard cocotb functionality.
We need a function to:
-
load and enable the design
-
create a suitable DUT instance
-
get the test runner and run all the detected cocotb.test()s.
from ttboard.demoboard import DemoBoard
from ttboard.cocotb.dut import DUT
def main():
tt = DemoBoard.get()
# make certain this chip has the project
if not tt.shuttle.has('tt_um_factory_test'):
print("This shuttle doesn't have mah project??!!")
return
# enable the project
tt.shuttle.tt_um_factory_test.enable()
dut = DUT()
dut._log.info("enabled project, running, running tests")
runner = cocotb.get_runner()
runner.test(dut)
If your tests are only using the ui_in/uo_out/uio_in/uio_out ports, then the runner above using the default DUT class will just work.
There are cases where tests are safe, in that they do not access any internals of the design, but you've added convenience functionality or renaming to the verilog tb, and your cocotb tests reflect that.
For example, my old neptune testbench looks like this in verilog
// testbench is controlled by test.py
module tb (
input [2:0] clk_config,
input input_pulse,
input display_single_enable,
input display_single_select,
output [6:0] segments,
output prox_select
);
// this part dumps the trace to a vcd file that can be viewed with GTKWave
initial begin
$dumpfile ("tb.vcd");
$dumpvars (0, tb);
#1;
end
// wire up the inputs and outputs
reg clk;
reg rst_n;
reg ena;
// reg [7:0] ui_in;
reg [7:0] uio_in;
wire [7:0] uo_out;
wire [7:0] uio_out;
wire [7:0] uio_oe;
assign prox_select = uo_out[7];
assign segments = uo_out[6:0];
wire [7:0] ui_in = {display_single_select,
display_single_enable,
input_pulse,
clk_config[2], clk_config[1], clk_config[0],
1'b0,1'b0};
/* ... */
and my cocotb tests use the nicely named input_pulse
(a bit), clk_config
(3 bits), etc.
The first option would be to re-write all the cocotb.test() stuff to use only ui_in and such. Yuk.
Rather than do all that work, and have ugly tt.ui_in.value[5]
stuff everywhere as a bonus, you can extend the DUT class to add in wrappers to these values.
To do this, you just derive a new class from ttboard.cocotb.dut.DUT
, create the attributes using add_bit_attribute
or add_slice_attribute
(for things like tt.ui_in[3:1]
).
In my neptune case, this looks like:
import ttboard.cocotb.dut
class DUT(ttboard.cocotb.dut.DUT):
def __init__(self):
super().__init__('Neptune')
self.tt = DemoBoard.get()
# inputs
self.add_bit_attribute('display_single_select', self.tt.ui_in, 7)
self.add_bit_attribute('display_single_enable', self.tt.ui_in, 6)
self.add_bit_attribute('input_pulse', self.tt.ui_in, 5)
self.add_slice_attribute('clk_config', self.tt.ui_in, 4, 2) # tt.ui_in[4:2]
# outputs
self.add_bit_attribute('prox_select', self.tt.uo_out, 7)
self.add_slice_attribute('segments', self.tt.uo_out, 6, 0) # tt.uo_out[6:0]
After instantiation, the DUT object will now have all the requisite attributes, e.g. dut.segments
, dut.clk_config
etc.
Using that class to construct my dut, things like
pulseClock = Clock(dut.input_pulse, 1000*(1.0/tunerInputFreqHz), units='ms')
cocotb.start_soon(pulseClock.start())
# or
val = int(dut.segments.value) << 1
will justwork(tm) in the tests.