This post complements an earlier post about test discovery. This should close the gap between simulation start and actual cocotb test call.
cocotb hello-world in 2 minutes Link to heading
I will write down the basic commands to run the adder
example that ships with cocotb. I am assuming latest iverilog
is installed already.
virtualenv .venv
source .venv/bin/activate
cd cocotb
pip install .
cd examples/adder/tests
make
You will probably see something like this
0.00ns INFO Seeding Python random module with 1642336775
0.00ns WARNING Pytest not found, assertion rewriting will not occur
0.00ns INFO Found test test_adder.adder_basic_test
0.00ns INFO Found test test_adder.adder_randomised_test
0.00ns INFO running adder_basic_test (1/2)
VCD info: dumpfile dump.vcd opened for output.
2.00ns INFO adder_basic_test passed
2.00ns INFO running adder_randomised_test (2/2)
22.00ns INFO adder_randomised_test passed
22.00ns INFO ******************************************************************************************
** TEST STATUS SIM TIME (ns) REAL TIME (s) RATIO (ns/s) **
******************************************************************************************
** test_adder.adder_basic_test PASS 2.00 0.00 869.36 **
** test_adder.adder_randomised_test PASS 20.00 0.00 5147.27 **
******************************************************************************************
** TESTS=2 PASS=2 FAIL=0 SKIP=0 22.00 0.04 615.90 **
******************************************************************************************
You can try make -n
and see iverlog build and run commands. These are the final iverilog commands to run the simulation(without Makefiles)
export PYTHONPATH=$PWD/../model:$PYTHONPATH
iverilog -o sim_build/sim.vvp -D COCOTB_SIM=1 -s adder -f sim_build/cmds.f -g2012 /examples/adder/tests/../hdl/adder.sv
MODULE=test_adder TESTCASE= TOPLEVEL=adder TOPLEVEL_LANG=verilog \
vvp -M ../../../.venv/lib/python3.8/site-packages/cocotb/libs -m libcocotbvpi_icarus sim_build/sim.vvp
The infrastructure Link to heading
The core of cocotb is compiled into the following shared objects
ls .venv/lib/python3.8/site-packages/cocotb/libs/
libcocotb.so libcocotbvhpi_ius.so libcocotbvpi_ghdl.so libcocotbvpi_modelsim.so libembed.so libpygpilog.so
libcocotbutils.so libcocotbvhpi_modelsim.so libcocotbvpi_icarus.vpl libcocotbvpi_vcs.so libgpilog.so
libcocotbvhpi_aldec.so libcocotbvpi_aldec.so libcocotbvpi_ius.so libcocotbvpi_verilator.so libgpi.so
iverilog
load libcocotbvpi_icarus.vpl
which loads the rest of cocotb libraries
$ldd .venv/lib/python3.8/site-packages/cocotb/libs/libcocotbvpi_icarus.vpl
libgpi.so => /.venv/lib/python3.8/site-packages/cocotb/libs/libgpi.so (0x00007f63699b4000)
libgpilog.so => /.venv/lib/python3.8/site-packages/cocotb/libs/libgpilog.so (0x00007f63699af000)
libcocotbutils.so => /.venv/lib/python3.8/site-packages/cocotb/libs/libcocotbutils.so (0x00007f63695a4000)
libembed.so => /.venv/lib/python3.8/site-packages/cocotb/libs/libembed.so (0x00007f636959f000)
Compilation Link to heading
setuptools
is used here to configure the build of external C/C++ files. In setup.py
, the external modules are configured with get_ext
ext_modules=get_ext(),
get_ext
first configures the common libraries used by all simulators
def _get_common_lib_ext(include_dir, share_lib_dir):
"""
Defines common libraries.
All libraries go into the same directory to enable loading without modifying the library path (e.g. LD_LIBRARY_PATH).
In Makefile `LIB_DIR` (s) is used to point to this directory.
"""
#
# libcocotbutils
#
libcocotbutils_sources = [
os.path.join(share_lib_dir, "utils", "cocotb_utils.cpp")
]
if os.name == "nt":
libcocotbutils_sources += ["libcocotbutils.rc"]
libcocotbutils_libraries = ["gpilog"]
if os.name != "nt":
libcocotbutils_libraries.append("dl") # dlopen, dlerror, dlsym
libcocotbutils = Extension(
os.path.join("cocotb", "libs", "libcocotbutils"),
define_macros=[("COCOTBUTILS_EXPORTS", "")] + _extra_defines,
include_dirs=[include_dir],
libraries=libcocotbutils_libraries,
sources=libcocotbutils_sources,
)
Then configures simulator-specific libraries. For icarus, libcocotbvpi
is configured here
#
# Icarus Verilog
#
icarus_extra_lib = []
logger.info("Compiling libraries for Icarus Verilog")
if os.name == "nt":
icarus_extra_lib = ["icarus"]
icarus_vpi_ext = _get_vpi_lib_ext(
include_dir=include_dir,
share_lib_dir=share_lib_dir,
sim_define="ICARUS",
extra_lib=icarus_extra_lib,
)
ext.append(icarus_vpi_ext)
And core VPI files are configured here(common for all simualtors)
def _get_vpi_lib_ext(
include_dir, share_lib_dir, sim_define, extra_lib=[], extra_lib_dir=[]
):
lib_name = "libcocotbvpi_" + sim_define.lower()
libcocotbvpi_sources = [
os.path.join(share_lib_dir, "vpi", "VpiImpl.cpp"),
os.path.join(share_lib_dir, "vpi", "VpiCbHdl.cpp"),
]
Bootstrap and Python interpreter init Link to heading
In VpiImpl.cpp
, vlog_startup_routines
called by simulator when shared objects are loaded
COCOTBVPI_EXPORT void (*vlog_startup_routines[])() = {
register_embed, gpi_load_extra_libs, register_initial_callback,
register_final_callback, nullptr};
These functions register implementations callbacks (VPI, VHPI, FLI). But the important one is gpi_load_exra_libs
as it embeds python
/* Finally embed Python */
embed_init_python();
embed_init_python
calls _embed_init_python
(defined in gpi_embed.cpp
) which start the interpreter
extern "C" COCOTB_EXPORT void _embed_init_python(void) {
assert(!gtstate); // this function should not be called twice
to_python();
set_program_name_in_venv();
Py_Initialize(); /* Initialize the interpreter */
PySys_SetArgvEx(1, argv, 0);
/* Swap out and return current thread state and release the GIL */
gtstate = PyEval_SaveThread();
to_simulator();
Jump to python (sim_init) Link to heading
At this point, python interpreter is initialized. But we need to jump to python to start the test discovery and execution.
Starting with register_initial_callback
on VPI bootstrap list above
static void register_initial_callback() {
sim_init_cb = new VpiStartupCbHdl(vpi_table);
sim_init_cb->arm_callback();
}
And arm_callback
registers cb_rtn
to call handle_vpi_callback
VpiCbHdl::VpiCbHdl(GpiImplInterface *impl) : GpiCbHdl(impl) {
vpi_time.type = vpiSimTime;
cb_data.cb_rtn = handle_vpi_callback;
}
int VpiCbHdl::arm_callback() {
if (m_state == GPI_PRIMED) {
fprintf(stderr, "Attempt to prime an already primed trigger for %s!\n",
m_impl->reason_to_string(cb_data.reason));
}
// Only a problem if we have not been asked to deregister and register
// in the same simulation callback
if (m_obj_hdl != NULL && m_state != GPI_DELETE) {
fprintf(stderr, "We seem to already be registered, deregistering %s!\n",
m_impl->reason_to_string(cb_data.reason));
cleanup_callback();
}
vpiHandle new_hdl = vpi_register_cb(&cb_data);
And handle_vpi_callback
calls run_callback
if (old_state == GPI_PRIMED) {
cb_hdl->set_call_state(GPI_CALL);
cb_hdl->run_callback();
And run_callback
calls gpi_embed_init
int VpiStartupCbHdl::run_callback() {
...
...
gpi_embed_init(info.argc, info.argv);
return 0;
}
In GpiCommon.cpp
, gpi_embed_init
calls embed_sim_init
void gpi_embed_init(int argc, char const *const *argv) {
if (embed_sim_init(argc, argv)) gpi_embed_end();
}
In embed.cpp
,embed_sim_init
calls _embed_sim_init
extern "C" int embed_sim_init(int argc, char const *const *argv) {
if (init_failed) {
// LCOV_EXCL_START
return -1;
// LCOV_EXCL_STOP
} else {
return _embed_sim_init(argc, argv);
}
}
In gpi_embed.cpp
, _embed_sim_init
gets the entry module and function for python
extern "C" COCOTB_EXPORT int _embed_sim_init(int argc,
char const *const *argv) {
auto entry_utility_module = PyImport_ImportModule("pygpi.entry");
auto entry_info_tuple =
PyObject_CallMethod(entry_utility_module, "load_entry", NULL);
if (!entry_info_tuple) {
Here is the lookup code for pygpi/entry.py
, which looks for cocotb:_initialise_testbench
def load_entry() -> Tuple[ModuleType, Callable]:
"""Gather entry point information by parsing :envvar:`PYGPI_ENTRY_POINT`."""
entry_point_str = os.environ.get("PYGPI_ENTRY_POINT", "cocotb:_initialise_testbench")
try:
if ":" not in entry_point_str:
raise ValueError("Invalid PYGPI_ENTRY_POINT, missing entry function (no colon).")
entry_module_str, entry_func_str = entry_point_str.split(":", 1)
entry_module = importlib.import_module(entry_module_str)
entry_func = reduce(getattr, entry_func_str.split('.'), entry_module)
Note that _embed_sim_init
formats argv
for _initialise_testbench_
and then calls it.
auto cocotb_retval =
PyObject_CallFunctionObjArgs(entry_point, argv_list, NULL);
_initialise_testbench_
is defined cocotb/__init__.py
which calls RegressionManager.from_discovery
def _initialise_testbench_(argv_):
...
...
...
# start Regression Manager
global regression_manager
regression_manager = RegressionManager.from_discovery(top)
regression_manager.execute()
Fin.