OBI interface modules for Cocotb
GitHub repository: https://github.com/daxzio/cocotbext-obi
Introduction
OBI (Open Bus Interface) simulation models for cocotb.
The OBI protocol is defined by the OpenHW Group for use in RISC-V and other open-source processor designs.
Features
- ObiHost: Host/manager driver for OBI protocol
- ObiBus: Bus signal container with auto-discovery
- Wide data support: Automatically splits data wider than bus into multiple transactions
- Transaction IDs: Supports pipelined transactions with ID tracking
- Multiple outstanding transactions: Configurable pipeline depth with in-order completion
- Error handling: Full error response validation
- Timeout support: Configurable transaction timeouts
Installation
Installation from pip (when available):
$ pip install cocotbext-obi
Installation from git (latest development version):
$ pip install https://github.com/daxzio/cocotbext-obi/archive/main.zip
Installation for active development:
$ git clone https://github.com/daxzio/cocotbext-obi
$ pip install -e cocotbext-obi
Optional ObiInterface extra (needs cocotb 2.x):
$ pip install cocotbext-obi[interface]
Requires Python 3.10+. CI runs Python 3.10–3.13 against cocotb v1.9.2 and v2.0.1, and Python 3.10–3.14 against cocotb master.
OBI Protocol Overview
OBI uses a two-phase handshake protocol:
Request Phase (A-Channel):
reqandgnthandshake for address/control transfer- Manager asserts
reqwith address and control signals - Subordinate asserts
gntwhen ready to accept
Response Phase (R-Channel):
rvalidandrreadyhandshake for data transfer- Subordinate asserts
rvalidwith response data - Manager asserts
rreadywhen ready to accept
Usage Example
OBI Bus
The ObiBus is used to map to an OBI interface on the dut. Class methods from_entity and from_prefix are provided to facilitate signal name matching.
Required Signals:
- req - Request valid
- gnt - Grant (ready to accept)
- addr - Address
- we - Write enable
- be - Byte enable
- wdata - Write data
- aid - Address/transaction ID
- rvalid - Response valid
- rready - Response ready
- rdata - Read data
- err - Error flag
- rid - Response ID
OBI Host
The ObiHost class implements an OBI host/manager and is capable of generating read and write operations against OBI devices.
The host automatically handles data wider than the bus width by splitting transactions into multiple sequential OBI accesses at consecutive addresses. This allows seamless transfers of wide data values across narrower OBI interfaces.
To use these modules, import and connect to the DUT:
from cocotbext.obi import ObiHost, ObiBus
bus = ObiBus.from_prefix(dut, "s_obi")
obi_driver = ObiHost(bus, dut.clk)
The first argument to the constructor accepts an ObiBus object. These objects are containers for the interface signals and include class methods to automate connections.
Once the module is instantiated, read and write operations can be initiated:
ObiMaster is a deprecated subclass of ObiHost and remains available for existing testbenches.
# Write operations
await obi_driver.write(0x1000, 0x12345678) # Single 32-bit write
await obi_driver.write(0x2000, 0x123456789ABCDEF0) # Auto-splits to two writes
# Read operations
data = await obi_driver.read(0x1000) # Returns bytes
value = int.from_bytes(data, 'little')
# With data verification
await obi_driver.read(0x1000, 0x12345678) # Raises exception if mismatch
# With error expectation
await obi_driver.write(0xBAD_ADDR, 0xFF, error_expected=True)
ObiHost Constructor Parameters
- bus:
ObiBusobject containing OBI interface signals - clock: Clock signal
- timeout_cycles: Maximum clock cycles to wait before timing out (optional, default
1000). Set to-1to disable timeout. - max_outstanding: Maximum number of outstanding transactions (optional, default
1). Set to2or higher to enable pipelined transactions.
Methods
wait(): Blocking wait until all outstanding operations completewrite(addr, data, strb=-1, error_expected=False, length=-1, device=0, index=-1): Write data (bytes or int) to addr (int or register name whenaddrmapis configured), wait for result. If data is wider than the bus width, it will automatically be split into multiple sequential OBI write accesses at consecutive addresses. After completion,intra_delayidle clock cycles are inserted (default0).write_nowait(addr, data, strb=-1, error_expected=False, length=-1, device=0, index=-1): Write data to addr, queue without waiting.read(addr, data=bytes(), error_expected=False, length=-1, device=0, index=-1): Read bytes at addr (int or register name). If data supplied, verify it matches. If data is wider than the bus width, it will automatically be split into multiple sequential OBI read accesses at consecutive addresses. After completion,intra_delayidle clock cycles are inserted (default0).read_nowait(addr, data=bytes(), error_expected=False, length=-1, device=0, index=-1): Read bytes at addr, queue without waiting.poll(addr, data=bytes(), device=0, index=-1): Repeatedly read addr until the returned data equals data.addaddrmap(addrmap, device=0): Register a name-to-address map. Preferred over direct assignment because it updates log column alignment.format_addr(addr, device=0): Reverse lookup — return the register name for addr, or0x........if unmapped.
Error Handling
The ObiHost includes exception control for error testing:
exception_enabled: When True (default), raises exceptions on unexpected errors. When False, logs warnings and setsexception_occurredflag.exception_occurred: Boolean flag set when an error occurs unexpectedly.
# Normal operation - exceptions enabled
await obi.write(read_only_addr, data, error_expected=True) # OK
# For testing error detection without exceptions
obi.exception_enabled = False
await obi.write(read_only_addr, data, error_expected=False)
assert obi.exception_occurred == True # Error was detected
OBI Device Models
Three device/target models are provided for building self-contained testbenches (no RTL DUT required - the host and device BFMs can drive a shared set of pins):
ObiDevice- a responder backed by a memory target (any object exposing asyncread/write, e.g. aMemoryRegion). Override_read/_writefor custom behaviour.ObiRam-ObiDevicepre-mixed with a sparse in-memoryMemorystore.ObiMonitor- a passive monitor that recordsObiTransactionobjects and can optionally check that bus signals only change on clock edges (enable_check_sync()/disable_check_sync()).
ObiSlave is a deprecated subclass of ObiDevice and remains available for existing testbenches.
from cocotbext.obi import ObiBus, ObiHost, ObiDevice, ObiRam, MemoryRegion
bus = ObiBus.from_prefix(dut, "s_obi")
host = ObiHost(bus, dut.clk)
# Memory-region-backed device
device = ObiDevice(bus, dut.clk)
device.target = MemoryRegion(2**device.address_width)
# ...or a RAM device in one line
ram = ObiRam(bus, dut.clk)
ObiDevice/ObiRam accept size_bytes= to size an auto-created backing store
and max_outstanding= to match the host's pipeline depth.
Address Maps
The ObiHost supports address mapping through its addrmap attribute, an
AddressMap instance. Register names can be used instead of
numeric addresses in read(), write(), read_nowait(), write_nowait(), and
poll().
Configure the map with addaddrmap() or by assigning directly to a device index:
from cocotbext.obi import ObiHost, ObiBus
bus = ObiBus.from_prefix(dut, "s_obi")
host = ObiHost(bus, dut.clk)
# Preferred: addaddrmap() updates log column alignment automatically
host.addaddrmap({
"STATUS": 0x00,
"BUSY": 0x04,
"CONFIG": 0x08,
"INTERRUPT": 0x0c,
})
# Equivalent for device 0:
# host.addrmap[0] = { ... }
await host.write("STATUS", 0x12)
await host.read("CONFIG")
await host.poll("STATUS", 0x1)
# Indexed access using string format
await host.read("STATUS[0]", 0x12)
await host.read("STATUS[1]", 0x34)
# Indexed access using the index parameter (useful with variables)
for i in range(4):
await host.write("STATUS", data[i], index=i)
await host.read("STATUS", expected[i], index=i)
When a map is configured, transaction logs show register names instead of raw
addresses (for example Read STATUS : 0x00000012 rather than
Read 0x00000000: 0x00000012). See tests/test_addrmap
for a complete cocotb example.
Indexed register access: for register arrays, use either bracket notation
("STATUS[0]", "STATUS[1]", …) or the index parameter
(read("STATUS", data, index=0)). Both add index * wbytes to the base address,
where wbytes is the bus data width in bytes.
AddressMap
AddressMap is a protocol-agnostic helper for name-to-address resolution on
memory-mapped register maps. It is used internally by ObiHost (via the
addrmap attribute) and is also exported for standalone use.
Import:
from cocotbext.obi import AddressMap
Data model
AddressMap is a dict subclass keyed by device index. Each value is a
plain dict mapping register name (str) to byte address (int):
AddressMap
├── 0 → {"STATUS": 0x00, "CONFIG": 0x08, ...} # device 0
└── word_bytes, multi_device, _label_width # configuration
Constructor parameters:
- word_bytes: bus data width in bytes (default
4). Used for indexed register offsets and reverse lookup alignment. - multi_device: reserve extra column width in log output (default
False).ObiHostalways constructs the map withmulti_device=False.
Forward lookup (name → address)
resolve(addr, device=0, index=-1) converts a register name or integer address
to a byte address:
- If
addris anint, it is returned unchanged (plus anyindexoffset). - If
addris astr, the base name is looked up in the map for device. Bracket notation addsN * word_bytesfor each[N]suffix (e.g."AES_KEY_SHARE0[3]"→ base + 3 × word_bytes). - If
index != -1,index * word_bytesis added after name resolution.
am = AddressMap(word_bytes=4)
am.add({"STATUS": 0x00, "CONFIG": 0x08})
am.resolve(0x08) # 0x08 (integer passthrough)
am.resolve("STATUS") # 0x00
am.resolve("STATUS[2]") # 0x08
am.resolve("STATUS", index=1) # 0x04
Reverse lookup (address → name)
format(addr, device=0) returns the register name for a byte address. When the
address falls within a mapped register array (aligned to word_bytes), bracket
notation is used for non-zero indices. Unmapped addresses are formatted as
0x.........
am.format(0x00) # "STATUS"
am.format(0x08) # "STATUS[2]" (if STATUS base is 0x00, word_bytes=4)
am.format(0x99) # "0x00000099" (unmapped)
Registering maps
add(addrmap, device=0): store a name→address dict for device and recompute log column width. This is whatObiHost.addaddrmap()delegates to.- Direct assignment
am[device] = {...}also works (dict subclass), but does not update column width unlessadd()or_update_label_width()is called.
Log formatting
format_col(label, prefix="") pads a register label so read/write data columns
align in log output. ObiHost uses this internally when logging transactions.
Standalone example
from cocotbext.obi import AddressMap
REGS = {
"STATUS": 0x00,
"BUSY": 0x04,
"CONFIG": 0x08,
}
am = AddressMap(word_bytes=4)
am.add(REGS)
addr = am.resolve("CONFIG")
label = am.format(addr) # "CONFIG"
col = am.format_col(label) # padded for aligned columns
Unit tests for reverse lookup live in tests/test_format_addr.py. Cocotb integration tests are in tests/test_addrmap. Polling is covered in tests/test_poll.
Error Types
The response-channel err bit maps to OBIError (with InvalidAccess) and the
ObiResp enum, exported for use in custom devices.
Complete Example
import cocotb
from cocotb.triggers import RisingEdge
from cocotbext.obi import ObiBus, ObiHost
@cocotb.test()
async def test_obi(dut):
# Create OBI host
obi_bus = ObiBus.from_prefix(dut, "s_obi")
obi_host = ObiHost(obi_bus, dut.clk)
# Reset
dut.rst.value = 1
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
# Write some data
await obi_host.write(0x00, 0x12345678)
await obi_host.write(0x04, 0xABCDEF00)
# Read back and verify
await obi_host.read(0x00, 0x12345678)
await obi_host.read(0x04, 0xABCDEF00)
# Test 64-bit access on 32-bit bus (auto-splits)
await obi_host.write(0x100, 0x123456789ABCDEF0)
await obi_host.read(0x100, 0x123456789ABCDEF0)
Pipelined Transactions
OBI supports multiple outstanding transactions with strict in-order completion. You can enable pipelining by setting max_outstanding parameter to 2 or higher:
from cocotbext.obi import ObiBus, ObiHost, ObiDevice
# Create host with pipeline depth of 4
host = ObiHost(bus, clock, max_outstanding=4)
# Create device with same pipeline depth
device = ObiDevice(bus, clock, max_outstanding=4)
# Queue multiple writes - they will be pipelined
await host.write(0x1000, 0x11111111)
await host.write(0x1004, 0x22222222)
await host.write(0x1008, 0x33333333)
await host.write(0x100C, 0x44444444)
# Or queue them without waiting
host.write_nowait(0x2000, 0xAAAA0000)
host.write_nowait(0x2004, 0xBBBB1111)
# ... continue queuing
await host.wait() # Wait for all to complete
Key points:
max_outstanding=1(default): Strictly sequential behavior, fully backward compatiblemax_outstanding > 1: Enables pipelining for better throughput- Host and device should use matching
max_outstandingvalues for best performance - Responses are guaranteed to return in the exact order requests were accepted (OBI requirement)
- Backpressure is automatic: when the pipeline is full, new requests wait until space is available
Optional ObiInterface (cocotbext-interface)
cocotbext-interface is not required to use this package. pip install cocotbext-obi still only needs cocotb. Hosts, devices, monitors, and ObiBus work as they always have.
If you want a SystemVerilog-style Interface connection instead of ObiBus, install the extra (needs cocotb 2.x):
$ pip install cocotbext-obi[interface]
ObiInterface is a drop-in for ObiBus: same signal names, from_prefix / from_entity, and the same ObiHost / ObiMonitor / ObiDevice classes.
from cocotbext.obi import ObiInterface, ObiHost, HAVE_COCOTBEXT_INTERFACE
bus = ObiInterface.from_prefix(dut, "s_obi")
obi_driver = ObiHost(bus, dut.clk)
HAVE_COCOTBEXT_INTERFACE is True only when both cocotb 2.x handle types and cocotbext-interface imported successfully. Otherwise import cocotbext.obi still succeeds, but constructing ObiInterface raises ImportError with the install command. Tests that need the extra skip when the flag is false (tests/test_interface, tests/test_interface_noid).
Testing
Package Tests
The cocotbext-obi package includes its own test suite:
cd tests/test_slverr
# Generate RTL
make etana
# Run with Verilator
make sim SIM=verilator
# Run with Icarus
make sim SIM=icarus
Test Results: ✅ 3/3 PASS (Verilator and Icarus)
Integration Tests
See the PeakRDL-etana tests/ directory for comprehensive testbenches using cocotbext-obi across 30+ test scenarios.
License
MIT License. See LICENSE file for details.
References
- OpenHW Group OBI Specification
- Cocotb Documentation
- PeakRDL-etana - Uses this package for OBI testing
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cocotbext_obi-1.0.0.tar.gz.
File metadata
- Download URL: cocotbext_obi-1.0.0.tar.gz
- Upload date:
- Size: 33.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88a88308b39c94f1d4426ac997efbbb1b7282f8b39caa71cf0e782e70f156128
|
|
| MD5 |
744d4000af23bff95d8bae67f1946ff6
|
|
| BLAKE2b-256 |
5625f183ce9d21dad878189962dd039319c2782e4be7cc0605ed4fb2dc95b26b
|
Provenance
The following attestation bundles were made for cocotbext_obi-1.0.0.tar.gz:
Publisher:
test_checkin.yml on daxzio/cocotbext-obi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cocotbext_obi-1.0.0.tar.gz -
Subject digest:
88a88308b39c94f1d4426ac997efbbb1b7282f8b39caa71cf0e782e70f156128 - Sigstore transparency entry: 2502036081
- Sigstore integration time:
-
Permalink:
daxzio/cocotbext-obi@02835f7c89bbb3dbc6b22a8a769022d445f9e900 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/daxzio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
test_checkin.yml@02835f7c89bbb3dbc6b22a8a769022d445f9e900 -
Trigger Event:
push
-
Statement type:
File details
Details for the file cocotbext_obi-1.0.0-py3-none-any.whl.
File metadata
- Download URL: cocotbext_obi-1.0.0-py3-none-any.whl
- Upload date:
- Size: 41.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d51bcc108ebad37cf0061be1f79f3dcad43bcb2bea69ec6d6f5a2aa1e214810b
|
|
| MD5 |
2a2115c03a9e2b378ed3d79d3fe4d0fc
|
|
| BLAKE2b-256 |
6e2cdc766371f6ac4abcbad718c7ce903920b526a42cd2849af2a23d73476862
|
Provenance
The following attestation bundles were made for cocotbext_obi-1.0.0-py3-none-any.whl:
Publisher:
test_checkin.yml on daxzio/cocotbext-obi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cocotbext_obi-1.0.0-py3-none-any.whl -
Subject digest:
d51bcc108ebad37cf0061be1f79f3dcad43bcb2bea69ec6d6f5a2aa1e214810b - Sigstore transparency entry: 2502036093
- Sigstore integration time:
-
Permalink:
daxzio/cocotbext-obi@02835f7c89bbb3dbc6b22a8a769022d445f9e900 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/daxzio
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
test_checkin.yml@02835f7c89bbb3dbc6b22a8a769022d445f9e900 -
Trigger Event:
push
-
Statement type: