Metadata-Version: 2.1
Name: cocotbext_apb
Version: 1.0.0
Summary: apb modules for cocotb
Home-page: https://github.com/daxzio/cocotbext-apb
Download-URL: https://github.com/daxzio/cocotbext-apb/tarball/master
Author: Dave Keeshan
Author-email: dave.keeshan@daxzio.com
License: MIT
Project-URL: Bug Tracker, https://github.com/daxzio/cocotbext-apb/issues
Project-URL: Source Code, https://github.com/daxzio/cocotbext-apb
Keywords: apb,cocotb
Platform: any
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: cocotb
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: test

# APB interface modules for Cocotb

[![Build Status](https://github.com/daxzio/cocotbext-apb/actions/workflows/test_checkin.yml/badge.svg?branch=main)](https://github.com/daxzio/cocotbext-apb/actions/)
[![PyPI version](https://badge.fury.io/py/cocotbext-apb.svg)](https://pypi.org/project/cocotbext-apb)
[![Downloads](https://pepy.tech/badge/cocotbext-apb)](https://pepy.tech/project/cocotbext-apb)

GitHub repository: https://github.com/daxzio/cocotbext-apb

## Introduction

APB simulation models for [cocotb](https://github.com/cocotb/cocotb).

The APB protocol is cover in these documents [APB Protocol Specification](https://github.com/daxzio/cocotbext-apb/blob/main/assets/IHI0024D_amba_apb_protocol_spec.pdf) and [APB Architecture Specification](https://github.com/daxzio/cocotbext-apb/blob/main/assets/IHI0024E_amba_apb_architecture_spec.pdf)

## Installation

Installation from pip (release version, stable):

    $ pip install cocotbext-apb

Installation from git (latest development version, potentially unstable):

    $ pip install https://github.com/daxzio/cocotbext-apb/archive/main.zip

Installation for active development:

    $ git clone https://github.com/daxzio/cocotbext-apb
    $ pip install -e cocotbext-apb

## Documentation and usage examples

See the `tests` directory for complete testbenches using these modules.

### APB Write

![APB Write](https://github.com/daxzio/cocotbext-apb/raw/main/assets/apb_write.png)

### APB Read

![APB Read](https://github.com/daxzio/cocotbext-apb/raw/main/assets/apb_read.png)

### APB Bus

The `APBBus` is used to map to a APB interface on the `dut`.  Class methods `from_entity` and `from_prefix` are provided to facilitate signal default name matching.

#### Required:
* _psel_
* _pwrite_
* _paddr_
* _pwdata_
* _pready_
* _prdata_

#### Optional:
* _pstrb_
* _pprot_
* _pslverr_

### APB Host

The `ApbHost` class implements an APB driver and is capable of generating read and write operations against APB devices.

The host automatically handles data wider than the bus width by splitting transactions into multiple sequential APB accesses at consecutive addresses. This allows seamless transfers of wide data values across narrower APB interfaces.

To use these modules, import the one you need and connect it to the DUT:

    from cocotbext.apb import ApbHost, ApbBus

    bus = ApbBus.from_prefix(dut, "s_apb")
    apb_driver = ApbHost(bus, dut.clk)

The first argument to the constructor accepts an `ApbBus` 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 in a couple of different ways.

`ApbMaster` is a subclass of `ApbHost` and remains available for existing testbenches.

#### `ApbHost` constructor parameters
* _bus_: `ApbBus` object containing APB interface signals
* _clock_: clock signal
* _timeout_max_: Maximum clock cycles to wait for `pready` signal before timing out (optional, default `1000`). Set to `-1` to disable timeout.
* _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`)


#### Additional optional arguments for `ApbHost`
* _seednum_: For random testing a seed can be supplied, default `None`, random seed.

#### Address Mapping

The `ApbHost` supports address mapping through its `addrmap` attribute, an
[`AddressMap`](#addressmap) instance. Register names can be used instead of
numeric addresses in `read()`, `write()`, `read_nowait()`, `write_nowait()`, and
`poll()`, which makes testbenches easier to read and maintain.

Configure the map with `addaddrmap()` or by assigning directly to a device index:

```python
from cocotbext.apb import ApbHost, ApbBus

bus = ApbBus.from_prefix(dut, "s_apb")
apb_driver = ApbHost(bus, dut.clk)

# Preferred: addaddrmap() updates log column alignment automatically
apb_driver.addaddrmap({
    'STATUS'    : 0x00,
    'BUSY'      : 0x04,
    'CONFIG'    : 0x08,
    'INTERRUPT' : 0x0c,
})

# Equivalent for device 0:
# apb_driver.addrmap[0] = { ... }

# Use string names instead of numeric addresses
await apb_driver.write('STATUS', 0x12)
await apb_driver.read('CONFIG')

# Indexed access using string format
await apb_driver.read('STATUS[0]', 0x12)
await apb_driver.read('STATUS[1]', 0x34)

# Indexed access using index parameter (useful with variables)
for i in range(4):
    await apb_driver.write('STATUS', data[i], index=i)
    await apb_driver.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](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.

**Multi-device:** pass `device=N` to `addaddrmap()` or assign `addrmap[N] = {...}`
for each slave. Use the `device` parameter on read/write calls to select the target.

#### Methods
* `enable_logging()`: Enable debug logging
* `disable_logging()`: Disable debug logging
* `enable_backpressure(seednum=None)`: Enable random delays on the interface
* `disable_backpressure()`: Disable random delays on the interface
* `wait()`: blocking wait until all outstanding operations complete
* `write(addr, data, strb=-1, prot=ApbProt.NONSECURE, error_expected=False, device=0, length=-1, index=-1)`: write _data_ (bytes or int), to _addr_ (int or string when `addrmap` is configured), wait for result.  If an slverr is experienced a critical warning will be issued by default, but will reduced this to an info warning if `error_expected=True`. If _data_ is wider than the bus width, it will automatically be split into multiple sequential APB write accesses at consecutive addresses. The optional _length_ parameter can override the automatic length calculation, should be a multiple of the number of bytes in the wdata bus. The optional _device_ parameter specifies the slave index to target. The optional _index_ parameter adds an offset to the address equal to `index * wbytes`, useful for accessing indexed registers (alternative to using string format like `"STATUS[0]"`).
* `write_nowait(addr, data, strb=-1, prot=ApbProt.NONSECURE, error_expected=False, device=0, length=-1, index=-1)`: write _data_ (bytes or int), to _addr_ (int or string when `addrmap` is configured), submit to queue. If an slverr is experienced a critical warning will be issued by default, but will reduced this to an info warning if `error_expected=True`. If _data_ is wider than the bus width, it will automatically be split into multiple sequential APB write accesses at consecutive addresses. The optional _length_ parameter can override the automatic length calculation, should be a multiple of the number of bytes in the wdata bus. The optional _device_ parameter specifies the slave index to target. The optional _index_ parameter adds an offset to the address equal to `index * wbytes`, useful for accessing indexed registers (alternative to using string format like `"STATUS[0]"`).
* `read(addr, data=bytes(), prot=ApbProt.NONSECURE, error_expected=False, device=0, index=-1, length=-1)`: read bytes, at _addr_ (int or string when `addrmap` is configured), if _data_ supplied check for match, wait for result. If an slverr is experienced a critical warning will be issued by default, but will reduced this to an info warning if `error_expected=True`. If _data_ is wider than the bus width, it will automatically be split into multiple sequential APB read accesses at consecutive addresses. The optional _length_ parameter can override the automatic length calculation, should be a multiple of the number of bytes in the wdata bus. The optional _device_ parameter specifies the slave index to target. The optional _index_ parameter adds an offset to the address equal to `index * wbytes`, useful for accessing indexed registers (alternative to using string format like `"STATUS[0]"`).
* `read_nowait(addr, data=bytes(), prot=ApbProt.NONSECURE, error_expected=False, device=0, index=-1, length=-1)`: read bytes, at _addr_ (int or string when `addrmap` is configured), if _data_ supplied check for match, submit to queue. If an slverr is experienced a critical warning will be issued by default, but will reduced this to an info warning if `error_expected=True`. If _data_ is wider than the bus width, it will automatically be split into multiple sequential APB read accesses at consecutive addresses. The optional _length_ parameter can override the automatic length calculation, should be a multiple of the number of bytes in the wdata bus. The optional _device_ parameter specifies the slave index to target. The optional _index_ parameter adds an offset to the address equal to `index * wbytes`, useful for accessing indexed registers (alternative to using string format like `"STATUS[0]"`).
* `poll(addr, data=bytes(), device=0)`: poll address, at _addr_ (int or string when `addrmap` is configured), until data at address matches _data_. The optional _device_ parameter specifies the slave index to target.
* `addaddrmap(addrmap, device=0)`: register a name-to-address map for _device_. 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.

### AddressMap

`AddressMap` is a protocol-agnostic helper for name-to-address resolution on
memory-mapped register maps. It is used internally by `ApbHost` (via the
`addrmap` attribute) and is also exported for standalone use or integration with
other bus masters (for example OBI).

Import:

```python
from cocotbext.apb 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
├── 1 → {"CTRL": 0x1000, ...}                   # device 1 (multi-device)
└── 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 when multiple slaves
  are present (default `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.

```python
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........`.

```python
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 `ApbHost.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. `ApbHost` uses this internally when logging transactions.

#### Standalone example

```python
from cocotbext.apb import AddressMap

REGS = {
    "STATUS": 0x00,
    "BUSY": 0x04,
    "CONFIG": 0x08,
}

am = AddressMap(word_bytes=4)
am.add(REGS)

# Forward lookup for a custom driver
addr = am.resolve("CONFIG")

# Reverse lookup for debug output
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](tests/test_format_addr.py). Cocotb integration tests
are in [tests/test_addrmap](tests/test_addrmap).

### APB Monitor

The `ApbMonitor` class tracks APB bus transactions and verifies signal synchronization.

#### Usage

    from cocotbext.apb import ApbMonitor
    monitor = ApbMonitor(bus, dut.clk)

#### Methods

* `enable_check_sync()`: Enable checking that signal changes are aligned with the clock edge, default.
* `disable_check_sync()`: Disable the synchronous signal check.

### Multi-Device Support

The `ApbHost` supports multiple devices on the same bus instance. To use this feature:

1.  **Signal Connection**:
    *   `psel` must be a vector (e.g., `[1:0]` for 2 slaves).
    *   `prdata` must be a concatenated vector of all slave read data outputs (e.g., `[63:0]` for 2 slaves with 32-bit data width).

2.  **Access**:
    *   Use the `device` parameter in `read`, `write`, `read_nowait`, and `write_nowait` methods to specify the target slave index (integer).
    *   The `ApbHost` will assert the corresponding bit in `psel` (`1 << device`) and slice the `prdata` appropriately.

Example:

```python
# Write to slave 0
await tb.intf.write(0x100, 0xDEADBEEF, device=0)

# Read from slave 1
val = await tb.intf.read(0x200, device=1)
```

### APB Device

The `ApbDevice` class implements an APB device and is capable of completing read and write operations from upstream APB hosts.  This module can either be used to perform memory reads and writes on a `MemoryInterface` on behalf of the DUT, or it can be extended to implement customized functionality.

To use these modules, import the one you need and connect it to the DUT:

    from cocotbext.apb import ApbBus, ApbDevice, MemoryRegion

    apb_device = ApbDevice(ApbBus.from_prefix(dut, "m_apb"), dut.clk, dut.rst)
    region = MemoryRegion(2**apb_device.read_if.address_width)
    apb_device.target = region

The first argument to the constructor accepts an `ApbBus` object.  These objects are containers for the interface signals and include class methods to automate connections.

It is also possible to extend these modules; operation can be customized by overriding the internal `_read()` and `_write()` methods.  See `ApbRam` for an example.

`ApbSlave` is a subclass of `ApbDevice` and remains available for existing testbenches.

#### `ApbDevice` constructor parameters

* _bus_: `ApbBus` object containing APB interface signals
* _clock_: clock signal
* _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`)
* _target_: target region (optional, default `None`)

#### `ApbDevice` editable attibutes

It is possible to set area of addressable memory to be treated a priviledged address space or instruction address space.  If an APB host tries to access these regions, but has not set the correct `prot` value, `NONSECURE` for example, the `ApbDevice` will issue a `slverr` duting the `pready` phase of it response.

The `ApbDevice` has two attributes that can be edited by the user to allocate addresses and/or address ranges to the priviledged or instruction space.

* _privileged_addrs_
* _instruction_addrs_

Both attributes are arrays, and each element can be a single address, or a two element list, with a low address to a high address:

    tb.ram.privileged_addrs =  [[0x1000, 0x1fff], 0x3000]
    tb.ram.instruction_addrs = [[0x2000, 0x2fff], 0x4000]

If there is a read or a write with an address in this space, and the prot from the master does not match, it will report the type of error, as a warning, and assert `slverr`. The access will also be unsuccessful, the write will not occur and a read will result in all zeros being returned.

![APB Write Error](https://github.com/daxzio/cocotbext-apb/raw/main/assets/apb_write_error.png)

### APB RAM

The `ApbRam` class implements APB RAMs and is capable of completing read and write operations from upstream APB hosts.  These modules are extensions of `ApbDevice`.  Internally, `SparseMemory` is used to support emulating very large memories.

To use these modules, import and connect it to the DUT:

    from cocotbext.apb import ApbBus, ApbRam

    apb_ram = ApbRam(ApbBus.from_prefix(dut, "m_apb"), dut.clk, dut.rst, size=2**32)

The first argument to the constructor accepts an `ApbBus` object.  These objects are containers for the interface signals and include class methods to automate connections.

Once the module is instantiated, the memory contents can be accessed in a couple of different ways.  First, the `mmap` object can be accessed directly via the `mem` attribute.  Second, `read()`, `write()`, and various word-access wrappers are available.  Hex dump helper methods are also provided for debugging.  For example:

    apb_ram.write(0x0000, b'test')
    data = apb_ram.read(0x0000, 4)
    apb_ram.hexdump(0x0000, 4, prefix="RAM")

Multi-port memories can be constructed by passing the `mem` object of the first instance to the other instances.  For example, here is how to create a four-port RAM:

    apb_ram_p1 = ApbRam(ApbBus.from_prefix(dut, "m00_apb"), dut.clk, dut.rst, size=2**32)
    apb_ram_p2 = ApbRam(ApbBus.from_prefix(dut, "m01_apb"), dut.clk, dut.rst, mem=apb_ram_p1.mem)
    apb_ram_p3 = ApbRam(ApbBus.from_prefix(dut, "m02_apb"), dut.clk, dut.rst, mem=apb_ram_p1.mem)
    apb_ram_p4 = ApbRam(ApbBus.from_prefix(dut, "m03_apb"), dut.clk, dut.rst, mem=apb_ram_p1.mem)

#### `ApbRam` and `ApbLiteRam` constructor parameters

* _bus_: `ApbBus` object containing APB interface signals
* _clock_: clock signal
* _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`)
* _size_: memory size in bytes (optional, default `2**32`)
* _mem_: `mmap` or `SparseMemory` backing object to use (optional, overrides _size_)

#### Attributes:

* _mem_: directly access shared `mmap` or `SparseMemory` backing object

#### Methods

* `read(address, length)`: read _length_ bytes, starting at _address_
* `read_words(address, count, byteorder='little', ws=2)`: read _count_ _ws_-byte words, starting at _address_
* `read_dwords(address, count, byteorder='little')`: read _count_ 4-byte dwords, starting at _address_
* `read_qwords(address, count, byteorder='little')`: read _count_ 8-byte qwords, starting at _address_
* `read_byte(address)`: read single byte at _address_
* `read_word(address, byteorder='little', ws=2)`: read single _ws_-byte word at _address_
* `read_dword(address, byteorder='little')`: read single 4-byte dword at _address_
* `read_qword(address, byteorder='little')`: read single 8-byte qword at _address_
* `write(address, data)`: write _data_ (bytes), starting at _address_
* `write_words(address, data, byteorder='little', ws=2)`: write _data_ (_ws_-byte words), starting at _address_
* `write_dwords(address, data, byteorder='little')`: write _data_ (4-byte dwords), starting at _address_
* `write_qwords(address, data, byteorder='little')`: write _data_ (8-byte qwords), starting at _address_
* `write_byte(address, data)`: write single byte at _address_
* `write_word(address, data, byteorder='little', ws=2)`: write single _ws_-byte word at _address_
* `write_dword(address, data, byteorder='little')`: write single 4-byte dword at _address_
* `write_qword(address, data, byteorder='little')`: write single 8-byte qword at _address_
* `hexdump(address, length, prefix='')`: print hex dump of _length_ bytes starting from _address_, prefix lines with optional _prefix_
* `hexdump_line(address, length, prefix='')`: return hex dump (list of str) of _length_ bytes starting from _address_, prefix lines with optional _prefix_
* `hexdump_str(address, length, prefix='')`: return hex dump (str) of _length_ bytes starting from _address_, prefix lines with optional _prefix_
