Skip to main content

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):

  • req and gnt handshake for address/control transfer
  • Manager asserts req with address and control signals
  • Subordinate asserts gnt when ready to accept

Response Phase (R-Channel):

  • rvalid and rready handshake for data transfer
  • Subordinate asserts rvalid with response data
  • Manager asserts rready when 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: ObiBus object containing OBI interface signals
  • clock: Clock signal
  • timeout_cycles: Maximum clock cycles to wait before timing out (optional, default 1000). Set to -1 to disable timeout.
  • max_outstanding: Maximum number of outstanding transactions (optional, default 1). Set to 2 or higher to enable pipelined transactions.

Methods

  • wait(): Blocking wait until all outstanding operations complete
  • write(addr, data, strb=-1, error_expected=False, length=-1, device=0, index=-1): Write data (bytes or int) to addr (int or register name when addrmap is 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_delay idle clock cycles are inserted (default 0).
  • 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_delay idle clock cycles are inserted (default 0).
  • 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, or 0x........ 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 sets exception_occurred flag.
  • 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 async read/write, e.g. a MemoryRegion). Override _read/_write for custom behaviour.
  • ObiRam - ObiDevice pre-mixed with a sparse in-memory Memory store.
  • ObiMonitor - a passive monitor that records ObiTransaction objects 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). ObiHost always constructs the map with multi_device=False.

Forward lookup (name → address)

resolve(addr, device=0, index=-1) converts a register name or integer address to a byte address:

  • If addr is an int, it is returned unchanged (plus any index offset).
  • If addr is a str, the base name is looked up in the map for device. Bracket notation adds N * word_bytes for each [N] suffix (e.g. "AES_KEY_SHARE0[3]" → base + 3 × word_bytes).
  • If index != -1, index * word_bytes is 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 what ObiHost.addaddrmap() delegates to.
  • Direct assignment am[device] = {...} also works (dict subclass), but does not update column width unless add() 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:

  • Default max_outstanding=2 (set 1 for strictly sequential issue)
  • Host and device should use matching max_outstanding values 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
  • Blocking read() waits for earlier writes; read_nowait() issues immediately

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cocotbext_obi-1.1.0.tar.gz (34.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cocotbext_obi-1.1.0-py3-none-any.whl (42.2 kB view details)

Uploaded Python 3

File details

Details for the file cocotbext_obi-1.1.0.tar.gz.

File metadata

  • Download URL: cocotbext_obi-1.1.0.tar.gz
  • Upload date:
  • Size: 34.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cocotbext_obi-1.1.0.tar.gz
Algorithm Hash digest
SHA256 1fa808d3b7e63ba5998472e3b9385cdb93d5b61d79f1f1659b72428f67a04d54
MD5 dfb6f37e339d28bfb0b5b89dc8fbb1d5
BLAKE2b-256 5e0f064b8f8bd81cf4a43d4a6aa0d6c4c5a2ae65a069767d28aac356f6a3621f

See more details on using hashes here.

Provenance

The following attestation bundles were made for cocotbext_obi-1.1.0.tar.gz:

Publisher: test_checkin.yml on daxzio/cocotbext-obi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cocotbext_obi-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: cocotbext_obi-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 42.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cocotbext_obi-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 89d76e868d8b5c40c3f1baeedf0302ce1918bb159c9918a90112fbdace933ae7
MD5 7a3d3e3431f97fb26e8a094c93d39417
BLAKE2b-256 2d22eb736d9514fa5499b9088f97d3e0103f0b6936d3bd532336c031f0e9f8e3

See more details on using hashes here.

Provenance

The following attestation bundles were made for cocotbext_obi-1.1.0-py3-none-any.whl:

Publisher: test_checkin.yml on daxzio/cocotbext-obi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page