90 Commits

Author SHA1 Message Date
aa3605a55c Remove extra wait which violated apb spec
Some checks failed
build / Build distributions (push) Successful in 31s
build / deploy (push) Has been skipped
build / Build distributions (release) Successful in 23s
build / deploy (release) Successful in 32s
Regression Tests / Python 3.10 (push) Has been cancelled
Regression Tests / Python 3.11 (push) Has been cancelled
Regression Tests / Python 3.12 (push) Has been cancelled
Regression Tests / Python 3.13 (push) Has been cancelled
Regression Tests / Python 3.8 (push) Has been cancelled
Regression Tests / Python 3.9 (push) Has been cancelled
2026-02-08 00:50:04 -08:00
0496147ddf Add deploy
Some checks failed
build / Build distributions (push) Successful in 38s
build / deploy (push) Has been skipped
build / Build distributions (release) Successful in 30s
build / deploy (release) Successful in 29s
Regression Tests / Python 3.10 (push) Has been cancelled
Regression Tests / Python 3.11 (push) Has been cancelled
Regression Tests / Python 3.12 (push) Has been cancelled
Regression Tests / Python 3.13 (push) Has been cancelled
Regression Tests / Python 3.8 (push) Has been cancelled
Regression Tests / Python 3.9 (push) Has been cancelled
2026-02-08 00:38:42 -08:00
Alex Forencich
3e1e7fc1ec Remove extraneous print
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-30 11:12:50 -07:00
Alex Forencich
698c29b05f Bump to dev version
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-07 15:41:39 -07:00
Alex Forencich
33f510688a Release v0.1.26
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-07 15:29:50 -07:00
Alex Forencich
da00960112 Update copyright dates
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-07 15:28:21 -07:00
Alex Forencich
88b6624a93 Fix X-init for cocotb 2.0
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-07 13:13:03 -07:00
Alex Forencich
dcb9a6bd02 Rework reset logic to better handle X/Z
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-07 12:56:14 -07:00
Alex Forencich
7136dddd0a Testbench cleanup for cocotb 2.0
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-06 22:52:18 -07:00
Alex Forencich
6c15d7d57d Cast to int instead of using .integer
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-06 22:25:25 -07:00
Alex Forencich
4595bd8a08 Update envlist
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-06 18:28:02 -07:00
Alex Forencich
204ad7a517 Update setup.cfg
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-06 18:23:06 -07:00
Alex Forencich
f3dbc07100 Update CI
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-06 17:36:54 -07:00
Alex Forencich
a0a5b7ee55 Add APB modules
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-06 17:13:54 -07:00
Alex Forencich
a28ec41f79 Use append instead of extend in AXI lite master
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2025-09-06 17:04:48 -07:00
Alex Forencich
f2bf8c0ed8 Fix deprecated option name
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-08-13 13:24:33 -07:00
Alex Forencich
28f4585c08 Clean up sink pause handling
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-08-13 13:20:35 -07:00
Alex Forencich
775301c6fe Cache signal presence in generic stream models
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-08-13 13:14:32 -07:00
Alex Forencich
39b4ca4a93 Fix logging when using from_entity
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-05-26 16:45:46 -07:00
Alex Forencich
f70731a8d8 Add Python 3.11 to regression tests
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-05-26 16:34:41 -07:00
Alex Forencich
28bc97f226 Remove recursively-expanded macros for module parameters in makefiles
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-05-26 16:33:34 -07:00
Alex Forencich
e816d6a088 Bump to dev version
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-24 11:05:34 -07:00
Alex Forencich
af377b2c11 Release v0.1.24
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-24 11:05:02 -07:00
Alex Forencich
cfb52c6130 Fix transfer length checks
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-24 10:06:19 -07:00
Alex Forencich
7e32e584ff Bump to dev version
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-24 01:57:09 -07:00
Alex Forencich
50cf2af49f Release v0.1.22
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-24 01:31:54 -07:00
Alex Forencich
ddfa1e3c92 Update readme
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-24 01:28:25 -07:00
Alex Forencich
5e8b246159 Use slices to access memory contents to support both mmap and SparseMemory
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-24 01:23:33 -07:00
Alex Forencich
62c2eef4ec Add SparseMemoryRegion object
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-23 23:44:29 -07:00
Alex Forencich
ad6012aea5 Update memory models to use SparseMemory
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-23 23:43:53 -07:00
Alex Forencich
432bd81011 Add sparse memory model
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-03-23 19:27:57 -07:00
Alex Forencich
bde123e05f Add transfer length checks
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-02-27 16:38:34 -08:00
Alex Forencich
8604017159 For FIXED burst type, issue all bursts with the same starting address
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-02-27 16:36:04 -08:00
Alex Forencich
e21b9ffcc8 Update ubuntu version in CI
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-02-12 17:31:31 -08:00
Alex Forencich
47cd74eb6c Rework parameter handling in makefiles
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-29 19:50:53 -08:00
Alex Forencich
4bf5945aa3 Bump to dev version
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-25 18:11:44 -08:00
Alex Forencich
f3a7652362 Release v0.1.20
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-25 17:55:54 -08:00
Alex Forencich
a84ce5447d Put sinks to sleep when idle
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-25 17:46:46 -08:00
Alex Forencich
1c03ec4697 Pass through full address for unaligned operations
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-25 16:27:14 -08:00
Alex Forencich
824eba793d Update package versions
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-24 12:47:15 -08:00
Alex Forencich
a0aad34698 Fix path issue so latest coverage works
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-20 20:57:30 -08:00
Alex Forencich
ede6270ed7 Put source to sleep when there is no data to send
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-20 15:49:16 -08:00
Alex Forencich
cd1a8b47a5 Fix init sequence
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-20 15:48:46 -08:00
Alex Forencich
be6d490adb Cache signal presence
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-20 15:48:23 -08:00
Alex Forencich
39686b849a Update github actions versions
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-20 15:30:32 -08:00
Alex Forencich
706051cb89 Fix tox config and lock package versions
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-20 15:29:11 -08:00
Alex Forencich
3e4f8d7e92 Python 3.6 is EOL; remove from CI tests
Signed-off-by: Alex Forencich <alex@alexforencich.com>
2023-01-18 14:55:25 -08:00
Leon Woestenberg
afae9e69ff Fix AxiStreamFrame default for self.byte_lanes from 1 to all.
If I connect a AXIS source to an AXIS sink, the #byte_lanes is incorrectly 1 rather than all lanes enabled.
self.source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "sink"), dut.clk, dut.reset)

Root cause is AxiStreamFrame assumes byte width 1 without TKEEP, but it should default to self.width // 8
because the AXIS specification mentions "when TKEEP is absent, TKEEP defaults to all bits HIGH" and "The width of the data payload is an integer number of bytes."

Fix: https://github.com/alexforencich/cocotbext-axi/blob/master/cocotbext/axi/axis.py#L290

self.byte_lanes = 1
self.byte_lanes = self.width // 8

Relevant AXIS Specification:
https://developer.arm.com/documentation/ihi0051/a/Default-Signaling-Requirements/Default-value-signaling/Optional-TKEEP-and-TSTRB?lang=en

Signed-off-by: Leon Woestenberg <leon@sidebranch.com>
2023-01-18 12:55:19 -08:00
Alex Forencich
035c1ba803 Support interleaved read data in AXI master 2022-02-01 00:25:01 -08:00
Alex Forencich
873bb1a034 Explicit cast to integer before converting to enum or flag type 2022-01-07 12:52:41 -08:00
Alex Forencich
2d70e5cbe5 Fix AxiLiteSlave wrapper 2022-01-04 15:29:04 -08:00
Alex Forencich
35d9742ae8 Remove extraneous code 2022-01-04 15:28:48 -08:00
Alex Forencich
0f20e2e9bf Bump to dev version 2021-12-28 20:08:44 -08:00
Alex Forencich
7606d7d7bd Release v0.1.18 2021-12-28 17:23:06 -08:00
Alex Forencich
dd345e87c3 Call write from init_write via start_soon so command FIFO size can be limited 2021-12-27 23:29:29 -08:00
Alex Forencich
9c0592c16a Make wstrb optional 2021-12-27 19:44:30 -08:00
Alex Forencich
8aab5a7294 Support overriding allocated region and window types 2021-12-27 17:58:52 -08:00
Alex Forencich
4f26621e2b Make size optional when creating windows 2021-12-27 17:31:08 -08:00
Alex Forencich
1b6993d80d Use start_soon instead of fork 2021-12-27 17:10:37 -08:00
Alex Forencich
6d9ed8a2d2 Specify min package versions 2021-12-27 17:03:19 -08:00
Alex Forencich
d772b73eb2 Specify min tox and venv versions 2021-12-27 17:02:59 -08:00
Alex Forencich
bd88eda17b Skip missing interpreters 2021-12-27 17:02:39 -08:00
Alex Forencich
53313699a9 Test on Python 3.10 2021-12-27 17:00:44 -08:00
Alex Forencich
3f7193b77c Use start_soon instead of fork 2021-12-08 21:38:12 -08:00
Alex Forencich
2b0b12c68d Cache clock edge event objects 2021-12-03 18:40:04 -08:00
Alex Forencich
4a91212f37 Bump to dev version 2021-11-17 00:06:34 -08:00
Alex Forencich
1608af26e5 Release v0.1.16 2021-11-16 22:54:37 -08:00
Alex Forencich
31fb855311 Update readme 2021-11-16 22:40:42 -08:00
Alex Forencich
cb4b0e1738 Wrap access on RAM size 2021-11-16 17:13:18 -08:00
Alex Forencich
ea95eeaf0d Don't pass through extra positional args 2021-11-16 17:01:31 -08:00
Alex Forencich
079f4009b3 Rewrite RAM modules to use common slave implementation 2021-11-16 17:00:48 -08:00
Alex Forencich
612a94c97a Add AXI and AXI lite slave modules 2021-11-16 17:00:05 -08:00
Alex Forencich
757e3a6f2d AXI master modules extend Region 2021-11-16 16:58:50 -08:00
Alex Forencich
b9b9a2da72 Add address space abstraction 2021-11-16 16:55:53 -08:00
Alex Forencich
f7660e9038 Add buddy allocator 2021-11-16 16:53:40 -08:00
Alex Forencich
78693a63d5 Fix types 2021-11-10 23:59:11 -08:00
Alex Forencich
34498f6e5d Add write data type check 2021-11-10 23:49:13 -08:00
Alex Forencich
6329187ced Add address range checks 2021-11-10 23:48:47 -08:00
Alex Forencich
da24857dd2 Cast write data to bytes instead of bytearray 2021-11-10 23:45:46 -08:00
Alex Forencich
c08f22c710 Bring out address and ID signal widths 2021-11-10 21:56:08 -08:00
Alex Forencich
d874d91d05 Use typing.NamedTuple instead of collections.namedtuple to add __bytes__ cast 2021-11-10 21:49:58 -08:00
Alex Forencich
3fd016a84c Lazy logging 2021-11-09 00:53:37 -08:00
Alex Forencich
43de2ea9b0 Use getattr with default value when accessing optional signals 2021-11-09 00:46:37 -08:00
Alex Forencich
558ba51c91 Use correct transaction object 2021-11-09 00:13:19 -08:00
Alex Forencich
f6426bd8f3 Bump to dev version 2021-11-07 13:12:32 -08:00
Alex Forencich
74dd47ca99 Release v0.1.14 2021-11-07 12:39:42 -08:00
Reto Meier
cde2056bb0 Remove deprecated <= assignments
Starting from cocotb v1.6 the use of <= syntax has been deprecated. This
commit replaces all use of this syntax with the ``.value =`` syntax.
2021-10-25 18:27:18 +02:00
Alex Forencich
b6870716ed Fix active state tracking for AXI stream sink/monitor 2021-09-15 00:46:01 -07:00
Alex Forencich
44da562db9 Add cocotb framework classifier 2021-08-31 14:43:59 -07:00
Alex Forencich
8dcdbfefb8 Bump to dev version 2021-04-12 22:56:51 -07:00
39 changed files with 3403 additions and 1117 deletions

63
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: build
on:
push:
branches:
- master
- 'dev/**'
pull_request:
branches: [ master ]
release:
types:
- published
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build:
name: Build distributions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
name: Install Python
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install build
- name: Build sdist
run: python -m build
- uses: actions/upload-artifact@v3
with:
name: dist
path: |
dist/*.tar.gz
dist/*.whl
#-------------------------------------------------------------------------------
deploy:
needs:
- build
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
# Only publish when a Gitea Release is created.
if: gitea.event_name == 'release'
steps:
- uses: actions/download-artifact@v3
with:
name: dist
path: dist
- run: python3 -m pip install twine --user --break-system-packages
- run: python3 -m pip install -U packaging --user --break-system-packages
- run: TWINE_PASSWORD=${{ secrets.PYPI_PAT }} TWINE_USERNAME=bslathi19 python -m twine upload --repository-url ${{ vars.CI_API_URL }} dist/*

View File

@@ -5,17 +5,17 @@ on: [push, pull_request]
jobs: jobs:
build: build:
name: Python ${{matrix.python-version}} name: Python ${{matrix.python-version}}
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
python-version: [3.6, 3.7, 3.8, 3.9] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}

View File

@@ -1,4 +1,4 @@
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

151
README.md
View File

@@ -1,6 +1,6 @@
# AXI interface modules for Cocotb # AXI interface modules for Cocotb
[![Build Status](https://github.com/alexforencich/cocotbext-axi/workflows/Regression%20Tests/badge.svg?branch=master)](https://github.com/alexforencich/cocotbext-axi/actions/) [![Regression Tests](https://github.com/alexforencich/cocotbext-axi/actions/workflows/regression-tests.yml/badge.svg)](https://github.com/alexforencich/cocotbext-axi/actions/workflows/regression-tests.yml)
[![codecov](https://codecov.io/gh/alexforencich/cocotbext-axi/branch/master/graph/badge.svg)](https://codecov.io/gh/alexforencich/cocotbext-axi) [![codecov](https://codecov.io/gh/alexforencich/cocotbext-axi/branch/master/graph/badge.svg)](https://codecov.io/gh/alexforencich/cocotbext-axi)
[![PyPI version](https://badge.fury.io/py/cocotbext-axi.svg)](https://pypi.org/project/cocotbext-axi) [![PyPI version](https://badge.fury.io/py/cocotbext-axi.svg)](https://pypi.org/project/cocotbext-axi)
[![Downloads](https://pepy.tech/badge/cocotbext-axi)](https://pepy.tech/project/cocotbext-axi) [![Downloads](https://pepy.tech/badge/cocotbext-axi)](https://pepy.tech/project/cocotbext-axi)
@@ -9,7 +9,7 @@ GitHub repository: https://github.com/alexforencich/cocotbext-axi
## Introduction ## Introduction
AXI, AXI lite, and AXI stream simulation models for [cocotb](https://github.com/cocotb/cocotb). AXI, AXI lite, AXI stream, and APB simulation models for [cocotb](https://github.com/cocotb/cocotb).
## Installation ## Installation
@@ -28,13 +28,13 @@ Installation for active development:
## Documentation and usage examples ## Documentation and usage examples
See the `tests` directory, [verilog-axi](https://github.com/alexforencich/verilog-axi), and [verilog-axis](https://github.com/alexforencich/verilog-axis) for complete testbenches using these modules. See the `tests` directory, [taxi](https://github.com/fpganinja/taxi), [verilog-axi](https://github.com/alexforencich/verilog-axi), and [verilog-axis](https://github.com/alexforencich/verilog-axis) for complete testbenches using these modules.
### AXI and AXI lite master ### AXI, AXI lite, and APB master
The `AxiMaster` and `AxiLiteMaster` classes implement AXI masters and are capable of generating read and write operations against AXI slaves. Requested operations will be split and aligned according to the AXI specification. The `AxiMaster` module is capable of generating narrow bursts, handling multiple in-flight operations, and handling reordering and interleaving in responses across different transaction IDs. The `AxiMaster`, `AxiLiteMaster`, and `ApbMaster` classes implement AXI, AXI-lite, and APB masters and are capable of generating read and write operations against the corresponding slaves. Requested operations will be split and aligned according to the AXI specification. The `AxiMaster` module is capable of generating narrow bursts, handling multiple in-flight operations, and handling reordering and interleaving in responses across different transaction IDs. `AxiMaster` and `AxiLiteMaster` and related objects all extend `Region`, so they can be attached to `AddressSpace` objects to handle memory operations in the specified region.
The `AxiMaster` is a wrapper around `AxiMasterWrite` and `AxiMasterRead`. Similarly, `AxiLiteMaster` is a wrapper around `AxiLiteMasterWrite` and `AxiLiteMasterRead`. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same. The `AxiMaster` is a wrapper around `AxiMasterWrite` and `AxiMasterRead`. Similarly, `AxiLiteMaster` is a wrapper around `AxiLiteMasterWrite` and `AxiLiteMasterRead`. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same. APB is not channelized, so only `ApbSlave` is available.
To use these modules, import the one you need and connect it to the DUT: To use these modules, import the one you need and connect it to the DUT:
@@ -64,9 +64,9 @@ Alternatively, operations can be initiated with non-blocking `init_read()` and `
With this method, it is possible to start multiple concurrent operations from the same coroutine. It is also possible to use the events with `Combine`, `First`, and `with_timeout`. With this method, it is possible to start multiple concurrent operations from the same coroutine. It is also possible to use the events with `Combine`, `First`, and `with_timeout`.
#### `AxiMaster` and `AxiLiteMaster` constructor parameters #### `AxiMaster`, `AxiLiteMaster`, and `ApbMaster` constructor parameters
* _bus_: `AxiBus` or `AxiLiteBus` object containing AXI interface signals * _bus_: `AxiBus`, `AxiLiteBus`, or `ApbBus` object containing interface signals
* _clock_: clock signal * _clock_: clock signal
* _reset_: reset signal (optional) * _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`) * _reset_active_level_: reset active level (optional, default `True`)
@@ -114,30 +114,60 @@ With this method, it is possible to start multiple concurrent operations from th
* _wuser_: AXI wuser signal, default `0` (write-related methods only) * _wuser_: AXI wuser signal, default `0` (write-related methods only)
* _event_: `Event` object used to wait on and retrieve result for specific operation, default `None`. The event will be triggered when the operation completes and the result returned via `Event.data`. (`init_read()` and `init_write()` only) * _event_: `Event` object used to wait on and retrieve result for specific operation, default `None`. The event will be triggered when the operation completes and the result returned via `Event.data`. (`init_read()` and `init_write()` only)
#### Additional optional arguments for `AxiLiteMaster` #### Additional optional arguments for `AxiLiteMaster` and `ApbMaster`
* _prot_: AXI protection flags, default `AxiProt.NONSECURE` * _prot_: AXI protection flags, default `AxiProt.NONSECURE`
* _event_: `Event` object used to wait on and retrieve result for specific operation, default `None`. The event will be triggered when the operation completes and the result returned via `Event.data`. (`init_read()` and `init_write()` only) * _event_: `Event` object used to wait on and retrieve result for specific operation, default `None`. The event will be triggered when the operation completes and the result returned via `Event.data`. (`init_read()` and `init_write()` only)
#### `AxiBus` and `AxiLiteBus` objects #### `AxiBus`, `AxiLiteBus`, and `ApbBus` objects
The `AxiBus`, `AxiLiteBus`, and related objects are containers for the interface signals. These hold instances of bus objects for the individual channels, which are currently extensions of `cocotb_bus.bus.Bus`. Class methods `from_entity` and `from_prefix` are provided to facilitate signal name matching. For AXI interfaces use `AxiBus`, `AxiReadBus`, or `AxiWriteBus`, as appropriate. For AXI lite interfaces, use `AxiLiteBus`, `AxiLiteReadBus`, or `AxiLiteWriteBus`, as appropriate. The `AxiBus`, `AxiLiteBus`, `ApbBus`, and related objects are containers for the interface signals. These hold instances of bus objects for the individual channels, which are currently extensions of `cocotb_bus.bus.Bus`. Class methods `from_entity` and `from_prefix` are provided to facilitate signal name matching. For AXI interfaces use `AxiBus`, `AxiReadBus`, or `AxiWriteBus`, as appropriate. For AXI lite interfaces, use `AxiLiteBus`, `AxiLiteReadBus`, or `AxiLiteWriteBus`, as appropriate. For APB interfaces, use `ApbBus`.
### AXI and AXI lite RAM ### AXI, AXI lite, and APB slave
The `AxiRam` and `AxiLiteRam` classes implement AXI RAMs and are capable of completing read and write operations from upstream AXI masters. The `AxiRam` module is capable of handling narrow bursts. The `AxiSlave`, `AxiLiteSlave`, and `ApbSlave` classes implement AXI, AXI-lite, and APB slaves and are capable of completing read and write operations from upstream AXI masters. The `AxiSlave` module is capable of handling narrow bursts. These modules can either be used to perform memory reads and writes on a `MemoryInterface` on behalf of the DUT, or they can be extended to implement customized functionality.
The `AxiRam` is a wrapper around `AxiRamWrite` and `AxiRamRead`. Similarly, `AxiLiteRam` is a wrapper around `AxiLiteRamWrite` and `AxiLiteRamRead`. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same. The `AxiSlave` is a wrapper around `AxiSlaveWrite` and `AxiSlaveRead`. Similarly, `AxiLiteSlave` is a wrapper around `AxiLiteSlaveWrite` and `AxiLiteSlaveRead`. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same. APB is not channelized, so only `ApbSlave` is available.
To use these modules, import the one you need and connect it to the DUT:
from cocotbext.axi import AxiBus, AxiSlave, MemoryRegion
axi_slave = AxiSlave(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst)
region = MemoryRegion(2**axi_slave.read_if.address_width)
axi_slave.target = region
The first argument to the constructor accepts an `AxiBus`, `AxiLiteBus`, or `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 `AxiRam` and `AxiLiteRam` for an example.
#### `AxiSlave`, `AxiLiteSlave`, and `ApbSlave` constructor parameters
* _bus_: `AxiBus`, `AxiLiteBus`, or `ApbBus` object containing interface signals
* _clock_: clock signal
* _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`)
* _target_: target region (optional, default `None`)
#### Attributes:
* _target_: target region
### AXI, AXI lite, and APB RAM
The `AxiRam`, `AxiLiteRam`, and `ApbRam` classes implement AXI, AXI-lite, and APB RAMs and are capable of completing read and write operations from upstream AXI masters. The `AxiRam` module is capable of handling narrow bursts. These modules are extensions of the corresponding `AxiSlave`, `AxiLiteSlave`, and `ApbSlave` modules. Internally, `SparseMemory` is used to support emulating very large memories.
The `AxiRam` is a wrapper around `AxiRamWrite` and `AxiRamRead`. Similarly, `AxiLiteRam` is a wrapper around `AxiLiteRamWrite` and `AxiLiteRamRead`. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same. APB is not channelized, so only `ApbRam` is available.
To use these modules, import the one you need and connect it to the DUT: To use these modules, import the one you need and connect it to the DUT:
from cocotbext.axi import AxiBus, AxiRam from cocotbext.axi import AxiBus, AxiRam
axi_ram = AxiRam(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst, size=2**16) axi_ram = AxiRam(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst, size=2**32)
The first argument to the constructor accepts an `AxiBus` or `AxiLiteBus` object. These objects are containers for the interface signals and include class methods to automate connections. The first argument to the constructor accepts an `AxiBus`, `AxiLiteBus`, or `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: Once the module is instantiated, the memory contents can be accessed in a couple of different ways. First, the `mmap`/`SparseMemory` 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:
axi_ram.write(0x0000, b'test') axi_ram.write(0x0000, b'test')
data = axi_ram.read(0x0000, 4) data = axi_ram.read(0x0000, 4)
@@ -145,23 +175,23 @@ Once the module is instantiated, the memory contents can be accessed in a couple
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: 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:
axi_ram_p1 = AxiRam(AxiBus.from_prefix(dut, "m00_axi"), dut.clk, dut.rst, size=2**16) axi_ram_p1 = AxiRam(AxiBus.from_prefix(dut, "m00_axi"), dut.clk, dut.rst, size=2**32)
axi_ram_p2 = AxiRam(AxiBus.from_prefix(dut, "m01_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem) axi_ram_p2 = AxiRam(AxiBus.from_prefix(dut, "m01_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem)
axi_ram_p3 = AxiRam(AxiBus.from_prefix(dut, "m02_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem) axi_ram_p3 = AxiRam(AxiBus.from_prefix(dut, "m02_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem)
axi_ram_p4 = AxiRam(AxiBus.from_prefix(dut, "m03_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem) axi_ram_p4 = AxiRam(AxiBus.from_prefix(dut, "m03_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem)
#### `AxiRam` and `AxiLiteRam` constructor parameters #### `AxiRam`, `AxiLiteRam`, and `ApbRam` constructor parameters
* _bus_: `AxiBus` or `AxiLiteBus` object containing AXI interface signals * _bus_: `AxiBus`, `AxiLiteBus`, or `ApbBus` object containing interface signals
* _clock_: clock signal * _clock_: clock signal
* _reset_: reset signal (optional) * _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`) * _reset_active_level_: reset active level (optional, default `True`)
* _size_: memory size in bytes (optional, default 1024) * _size_: memory size in bytes (optional, default `2**64`)
* _mem_: mmap object to use (optional, overrides _size_) * _mem_: `mmap` or `SparseMemory` backing object to use (optional, overrides _size_)
#### Attributes: #### Attributes:
* _mem_: directly access shared `mmap` object * _mem_: directly access shared `mmap` or `SparseMemory` backing object
#### Methods #### Methods
@@ -290,6 +320,68 @@ Methods:
* `normalize()`: pack `tkeep`, `tid`, `tdest`, and `tuser` to the same length as `tdata`, replicating last element if necessary, initialize `tkeep` to list of `1` and `tid`, `tdest`, and `tuser` to list of `0` if not specified. * `normalize()`: pack `tkeep`, `tid`, `tdest`, and `tuser` to the same length as `tdata`, replicating last element if necessary, initialize `tkeep` to list of `1` and `tid`, `tdest`, and `tuser` to list of `0` if not specified.
* `compact()`: remove `tdata`, `tid`, `tdest`, and `tuser` values based on `tkeep`, remove `tkeep`, compact `tid`, `tdest`, and `tuser` to an int if all values are identical. * `compact()`: remove `tdata`, `tid`, `tdest`, and `tuser` values based on `tkeep`, remove `tkeep`, compact `tid`, `tdest`, and `tuser` to an int if all values are identical.
### Address space abstraction
The address space abstraction provides a framework for cross-connecting multiple memory-mapped interfaces for testing components that interface with complex systems, including components with DMA engines.
`MemoryInterface` is the base class for all components in the address space abstraction. `MemoryInterface` provides the core `read()` and `write()` methods, which implement bounds checking, as well as word-access wrappers. Methods for creating `Window` and `WindowPool` objects are also provided. The function `get_absolute_address()` translates addresses to the system address space. `MemoryInterface` can be extended to implement custom functionality by overriding `_read()` and `_write()`.
`Window` objects represent views onto a parent address space with some length and offset. `read()` and `write()` operations on a `Window` are translated to the equivalent operations on the parent address space. Multiple `Window` instances can overlap and access the same portion of address space.
`WindowPool` provides a method for dynamically allocating windows from a section of address space. It uses a standard memory management algorithm to provide naturally-aligned `Window` objects of the requested size.
`Region` is the base class for all components which implement a portion of address space. `Region` objects can be registered with `AddressSpace` objects to handle `read()` and `write()` operations in a specified region. `Region` can be extended by components that implement a portion of address space.
`MemoryRegion` is an extension of `Region` that uses an `mmap` instance to handle memory operations. `MemoryRegion` also provides hex dump methods as well as indexing and slicing.
`SparseMemoryRegion` is similar to `MemoryRegion` but is backed by `SparseMemory` instead of `mmap` and as such can emulate extremely large regions of address space.
`PeripheralRegion` is an extension of `Region` that can wrap another object that implements `read()` and `write()`, as an alternative to extending `Region`.
`AddressSpace` is the core object for handling address spaces. `Region` objects can be registered with `AddressSpace` with specified base address, size, and offset. The `AddressSpace` object will then direct `read()` and `write()` operations to the appropriate `Region`s, splitting requests appropriately when necessary and translating addresses. Regions registered with `offset` other than `None` are translated such that accesses to base address + N map to N + offset. Regions registered with an `offset` of `None` are not translated. `Region` objects registered with the same `AddressSpace` cannot overlap, however the same `Region` can be registered multiple times. `AddressSpace` also provides a method for creating `Pool` objects.
`Pool` is an extension of `AddressSpace` that supports dynamic allocation of `MemoryRegion`s. It uses a standard memory management algorithm to provide naturally-aligned `MemoryRegion` objects of the requested size.
#### Example
This is a simple example that shows how the address space abstraction components can be used to connect a DUT to a simulated host system, including simulated RAM, an AXI interface from the DUT for DMA, and an AXI lite interface to the DUT for control.
from cocotbext.axi import AddressSpace, SparseMemoryRegion
from cocotbext.axi import AxiBus, AxiLiteMaster, AxiSlave
# system address space
address_space = AddressSpace(2**32)
# RAM
ram = SparseMemoryRegion(2**24)
address_space.register_region(ram, 0x0000_0000)
ram_pool = address_space.create_window_pool(0x0000_0000, 2**20)
# DUT control register interface
axil_master = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "s_axil_ctrl"), dut.clk, dut.rst)
address_space.register_region(axil_master, 0x8000_0000)
ctrl_regs = address_space.create_window(0x8000_0000, axil_master.size)
# DMA from DUT
axi_slave = AxiSlave(AxiBus.from_prefix(dut, "m_axi_dma"), dut.clk, dut.rst, target=address_space)
# exercise DUT DMA functionality
src_block = ram_pool.alloc_window(1024)
dst_block = ram_pool.alloc_window(1024)
test_data = b'test data'
await src_block.write(0, test_data)
await ctrl_regs.write_dword(DMA_SRC_ADDR, src_block.get_absolute_address(0))
await ctrl_regs.write_dword(DMA_DST_ADDR, dst_block.get_absolute_address(0))
await ctrl_regs.write_dword(DMA_LEN, len(test_data))
await ctrl_regs.write_dword(DMA_CONTROL, 1)
while await ctrl_regs.read_dword(DMA_STATUS) == 0:
pass
assert await dst_block.read(0, len(test_data)) == test_data
### AXI signals ### AXI signals
* Write address channel * Write address channel
@@ -379,3 +471,16 @@ Methods:
* `tid`: ID signal, can be used for routing * `tid`: ID signal, can be used for routing
* `tdest`: destination signal, can be used for routing * `tdest`: destination signal, can be used for routing
* `tuser`: additional sideband data * `tuser`: additional sideband data
### APB signals
* `paddr`: address
* `pprot`: protection bits
* `psel`: select signal, for selecting a target device
* `penable`: enable signal, for performing an operation
* `pwrite`: read/write control signal
* `pwdata`: write data
* `pstrb`: write strobe
* `pready`: ready signal to stall bus
* `prdata`: read data
* `pslverr`: read/write response, indicating SLVERR

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -26,14 +26,22 @@ from .version import __version__
from .constants import AxiBurstType, AxiBurstSize, AxiLockType, AxiCacheBit, AxiProt, AxiResp from .constants import AxiBurstType, AxiBurstSize, AxiLockType, AxiCacheBit, AxiProt, AxiResp
from .address_space import MemoryInterface, Window, WindowPool
from .address_space import Region, MemoryRegion, SparseMemoryRegion, PeripheralRegion
from .address_space import AddressSpace, Pool
from .axis import AxiStreamFrame, AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamMonitor from .axis import AxiStreamFrame, AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamMonitor
from .axil_channels import AxiLiteAWBus, AxiLiteWBus, AxiLiteBBus, AxiLiteARBus, AxiLiteRBus from .axil_channels import AxiLiteAWBus, AxiLiteWBus, AxiLiteBBus, AxiLiteARBus, AxiLiteRBus
from .axil_channels import AxiLiteWriteBus, AxiLiteReadBus, AxiLiteBus from .axil_channels import AxiLiteWriteBus, AxiLiteReadBus, AxiLiteBus
from .axil_master import AxiLiteMasterWrite, AxiLiteMasterRead, AxiLiteMaster from .axil_master import AxiLiteMasterWrite, AxiLiteMasterRead, AxiLiteMaster
from .axil_slave import AxiLiteSlaveWrite, AxiLiteSlaveRead, AxiLiteSlave
from .axil_ram import AxiLiteRamWrite, AxiLiteRamRead, AxiLiteRam from .axil_ram import AxiLiteRamWrite, AxiLiteRamRead, AxiLiteRam
from .axi_channels import AxiAWBus, AxiWBus, AxiBBus, AxiARBus, AxiRBus from .axi_channels import AxiAWBus, AxiWBus, AxiBBus, AxiARBus, AxiRBus
from .axi_channels import AxiWriteBus, AxiReadBus, AxiBus from .axi_channels import AxiWriteBus, AxiReadBus, AxiBus
from .axi_master import AxiMasterWrite, AxiMasterRead, AxiMaster from .axi_master import AxiMasterWrite, AxiMasterRead, AxiMaster
from .axi_slave import AxiSlaveWrite, AxiSlaveRead, AxiSlave
from .axi_ram import AxiRamWrite, AxiRamRead, AxiRam from .axi_ram import AxiRamWrite, AxiRamRead, AxiRam
from .apb import ApbBus, ApbMaster, ApbSlave, ApbRam

View File

@@ -0,0 +1,362 @@
"""
Copyright (c) 2021-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import mmap
from .buddy_allocator import BuddyAllocator
from .sparse_memory import SparseMemory
from .utils import hexdump, hexdump_lines, hexdump_str
class MemoryInterface:
def __init__(self, size, base=0, parent=None, **kwargs):
self._parent = parent
self._size = size
self._base = base
self.window_type = Window
self.window_pool_type = WindowPool
super().__init__(**kwargs)
@property
def parent(self):
return self._parent
@property
def size(self):
return self._size
@property
def base(self):
return self._base
def check_range(self, address, length=0):
if address < 0 or address >= self.size:
raise ValueError("address out of range")
if length < 0:
raise ValueError("invalid length")
if address+length > self.size:
raise ValueError("operation out of range")
def get_absolute_address(self, address):
if self.base is None:
return None
self.check_range(address)
return address+self.base
async def _read(self, address, length, **kwargs):
raise NotImplementedError()
async def read(self, address, length, **kwargs):
self.check_range(address, length)
return await self._read(address, length, **kwargs)
async def read_words(self, address, count, byteorder='little', ws=2, **kwargs):
data = bytes(await self.read(address, count*ws, **kwargs))
words = []
for k in range(count):
words.append(int.from_bytes(data[ws*k:ws*(k+1)], byteorder))
return words
async def read_dwords(self, address, count, byteorder='little', **kwargs):
return await self.read_words(address, count, byteorder, 4, **kwargs)
async def read_qwords(self, address, count, byteorder='little', **kwargs):
return await self.read_words(address, count, byteorder, 8, **kwargs)
async def read_byte(self, address, **kwargs):
return (await self.read(address, 1, **kwargs)).data[0]
async def read_word(self, address, byteorder='little', ws=2, **kwargs):
return (await self.read_words(address, 1, byteorder, ws, **kwargs))[0]
async def read_dword(self, address, byteorder='little', **kwargs):
return (await self.read_dwords(address, 1, byteorder, **kwargs))[0]
async def read_qword(self, address, byteorder='little', **kwargs):
return (await self.read_qwords(address, 1, byteorder, **kwargs))[0]
async def _write(self, address, data, **kwargs):
raise NotImplementedError()
async def write(self, address, data, **kwargs):
self.check_range(address, len(data))
await self._write(address, data, **kwargs)
async def write_words(self, address, data, byteorder='little', ws=2, **kwargs):
words = data
data = bytearray()
for w in words:
data.extend(w.to_bytes(ws, byteorder))
await self.write(address, data, **kwargs)
async def write_dwords(self, address, data, byteorder='little', **kwargs):
await self.write_words(address, data, byteorder, 4, **kwargs)
async def write_qwords(self, address, data, byteorder='little', **kwargs):
await self.write_words(address, data, byteorder, 8, **kwargs)
async def write_byte(self, address, data, **kwargs):
await self.write(address, [data], **kwargs)
async def write_word(self, address, data, byteorder='little', ws=2, **kwargs):
await self.write_words(address, [data], byteorder, ws, **kwargs)
async def write_dword(self, address, data, byteorder='little', **kwargs):
await self.write_dwords(address, [data], byteorder, **kwargs)
async def write_qword(self, address, data, byteorder='little', **kwargs):
await self.write_qwords(address, [data], byteorder, **kwargs)
def create_window(self, offset, size=None, window_type=None):
if not size or size < 0:
size = self.size - offset
window_type = window_type or self.window_type or Window
self.check_range(offset, size)
return window_type(self, offset, size, base=self.get_absolute_address(offset))
def create_window_pool(self, offset=None, size=None, window_pool_type=None, window_type=None):
if offset is None:
offset = 0
if size is None:
size = self.size - offset
window_pool_type = window_pool_type or self.window_pool_type or WindowPool
window_type = window_type or self.window_type
self.check_range(offset, size)
return window_pool_type(self, offset, size, base=self.get_absolute_address(offset), window_type=window_type)
def __len__(self):
return self._size
class Window(MemoryInterface):
def __init__(self, parent, offset, size, base=0, **kwargs):
super().__init__(size, base=base, parent=parent, **kwargs)
self._offset = offset
@property
def offset(self):
return self._offset
def get_parent_address(self, address):
if address < 0 or address >= self.size:
raise ValueError("address out of range")
return address+self.offset
async def _read(self, address, length, **kwargs):
return await self.parent.read(self.get_parent_address(address), length, **kwargs)
async def _write(self, address, data, **kwargs):
await self.parent.write(self.get_parent_address(address), data, **kwargs)
class WindowPool(Window):
def __init__(self, parent, offset, size, base=None, window_type=None, **kwargs):
super().__init__(parent, offset, size, base=base, **kwargs)
self.window_type = window_type or Window
self.allocator = BuddyAllocator(size)
def alloc_window(self, size, window_type=None):
return self.create_window(self.allocator.alloc(size), size, window_type)
class Region(MemoryInterface):
def __init__(self, size, **kwargs):
super().__init__(size, **kwargs)
class MemoryRegion(Region):
def __init__(self, size=4096, mem=None, **kwargs):
super().__init__(size, **kwargs)
if mem is None:
mem = mmap.mmap(-1, size)
self.mem = mem
async def _read(self, address, length, **kwargs):
return self.mem[address:address+length]
async def _write(self, address, data, **kwargs):
self.mem[address:address+len(data)] = data
def hexdump(self, address, length, prefix=""):
hexdump(self.mem[address:address+length], prefix=prefix, offset=address)
def hexdump_lines(self, address, length, prefix=""):
return hexdump_lines(self.mem[address:address+length], prefix=prefix, offset=address)
def hexdump_str(self, address, length, prefix=""):
return hexdump_str(self.mem[address:address+length], prefix=prefix, offset=address)
def __getitem__(self, key):
return self.mem[key]
def __setitem__(self, key, value):
self.mem[key] = value
def __bytes__(self):
return bytes(self.mem)
class SparseMemoryRegion(Region):
def __init__(self, size=2**64, mem=None, **kwargs):
super().__init__(size, **kwargs)
if mem is None:
mem = SparseMemory(size)
self.mem = mem
async def _read(self, address, length, **kwargs):
return self.mem.read(address, length)
async def _write(self, address, data, **kwargs):
self.mem.write(address, data)
def hexdump(self, address, length, prefix=""):
self.mem.hexdump(address, length, prefix=prefix)
def hexdump_lines(self, address, length, prefix=""):
return self.mem.hexdump_lines(address, length, prefix=prefix)
def hexdump_str(self, address, length, prefix=""):
return self.mem.hexdump_str(address, length, prefix=prefix)
def __getitem__(self, key):
return self.mem[key]
def __setitem__(self, key, value):
self.mem[key] = value
class PeripheralRegion(Region):
def __init__(self, obj, size, **kwargs):
super().__init__(size, **kwargs)
self.obj = obj
async def _read(self, address, length, **kwargs):
try:
return await self.obj.read(address, length, **kwargs)
except TypeError:
return self.obj.read(address, length, **kwargs)
async def _write(self, address, data, **kwargs):
try:
await self.obj.write(address, data, **kwargs)
except TypeError:
self.obj.write(address, data, **kwargs)
class AddressSpace(Region):
def __init__(self, size=2**64, base=0, parent=None, **kwargs):
super().__init__(size=size, base=base, parent=parent, **kwargs)
self.pool_type = Pool
self.regions = []
def find_regions(self, address, length=1):
regions = []
if address < 0 or address >= self.size:
raise ValueError("address out of range")
if length < 0:
raise ValueError("invalid length")
length = max(length, 1)
for (base, size, translate, region) in self.regions:
if address < base+size and base < address+length:
regions.append((base, size, translate, region))
regions.sort()
return regions
def register_region(self, region, base, size=None, offset=0):
if size is None:
size = region.size
if self.find_regions(base, size):
raise ValueError("overlaps existing region")
region._parent = self
if offset == 0:
region._base = self.get_absolute_address(base)
else:
region._base = None
self.regions.append((base, size, offset, region))
async def read(self, address, length, **kwargs):
regions = self.find_regions(address, length)
data = bytearray()
if not regions:
raise Exception("Invalid address")
for base, size, offset, region in regions:
if base > address:
raise Exception("Invalid address")
seg_addr = address - base
seg_len = min(size-seg_addr, length)
if offset is None:
seg_addr = address
offset = 0
data.extend(bytes(await region.read(seg_addr+offset, seg_len, **kwargs)))
address += seg_len
length -= seg_len
if length > 0:
raise Exception("Invalid address")
return bytes(data)
async def write(self, address, data, **kwargs):
start = 0
length = len(data)
regions = self.find_regions(address, length)
if not regions:
raise Exception("Invalid address")
for base, size, offset, region in regions:
if base > address:
raise Exception("Invalid address")
seg_addr = address - base
seg_len = min(size-seg_addr, length)
if offset is None:
seg_addr = address
offset = 0
await region.write(seg_addr+offset, data[start:start+seg_len], **kwargs)
address += seg_len
start += seg_len
length -= seg_len
if length > 0:
raise Exception("Invalid address")
def create_pool(self, base=None, size=None, pool_type=None, region_type=None):
if base is None:
base = 0
if size is None:
size = self.size - base
pool_type = pool_type or self.pool_type or Pool
self.check_range(base, size)
pool = pool_type(self, base, size, region_type=region_type)
self.register_region(pool, base, size)
return pool
class Pool(AddressSpace):
def __init__(self, parent, base, size, region_type=None, **kwargs):
super().__init__(parent=parent, base=base, size=size, **kwargs)
self.region_type = region_type or MemoryRegion
self.allocator = BuddyAllocator(size)
def alloc_region(self, size, region_type=None):
region_type = region_type or self.region_type or MemoryRegion
base = self.allocator.alloc(size)
region = region_type(size)
self.register_region(region, base)
return region

620
cocotbext/axi/apb.py Normal file
View File

@@ -0,0 +1,620 @@
"""
Copyright (c) 2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import logging
from typing import NamedTuple
import cocotb
from cocotb.queue import Queue
from cocotb.triggers import RisingEdge, Event
from cocotb_bus.bus import Bus
from .version import __version__
from .constants import AxiResp, AxiProt
from .address_space import Region
from .reset import Reset
from .memory import Memory
# APB master write helper objects
class ApbWriteCmd(NamedTuple):
address: int
data: bytes
prot: AxiProt
event: Event
class ApbWriteResp(NamedTuple):
address: int
length: int
resp: AxiResp
# APB master read helper objects
class ApbReadCmd(NamedTuple):
address: int
length: int
prot: AxiProt
event: Event
class ApbReadResp(NamedTuple):
address: int
data: bytes
resp: AxiResp
def __bytes__(self):
return self.data
class ApbBus(Bus):
_signals = ["paddr", "psel", "penable", "pwrite", "pwdata", "pstrb", "pready", "prdata"]
_optional_signals = ["pprot", "pslverr"]
def __init__(self, entity=None, prefix=None, **kwargs):
super().__init__(entity, prefix, self._signals, optional_signals=self._optional_signals, **kwargs)
@classmethod
def from_entity(cls, entity, **kwargs):
return cls(entity, **kwargs)
@classmethod
def from_prefix(cls, entity, prefix, **kwargs):
return cls(entity, prefix, **kwargs)
class ApbPause:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._pause = False
self._pause_generator = None
self._pause_cr = None
def _pause_update(self, val):
pass
@property
def pause(self):
return self._pause
@pause.setter
def pause(self, val):
if self._pause != val:
self._pause_update(val)
self._pause = val
def set_pause_generator(self, generator=None):
if self._pause_cr is not None:
self._pause_cr.kill()
self._pause_cr = None
self._pause_generator = generator
if self._pause_generator is not None:
self._pause_cr = cocotb.start_soon(self._run_pause())
def clear_pause_generator(self):
self.set_pause_generator(None)
async def _run_pause(self):
clock_edge_event = RisingEdge(self.clock)
for val in self._pause_generator:
self.pause = val
await clock_edge_event
class ApbMaster(ApbPause, Region, Reset):
def __init__(self, bus, clock, reset=None, reset_active_level=True, **kwargs):
self.bus = bus
self.clock = clock
self.reset = reset
if bus._name:
self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus._entity._name}")
self.log.info("APB master")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
self.command_queue = Queue()
self.command_queue.queue_occupancy_limit = 2
self.current_command = None
self.in_flight_operations = 0
self._idle = Event()
self._idle.set()
self.address_width = len(self.bus.paddr)
self.width = len(self.bus.pwdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.strb_mask = 2**self.byte_lanes-1
self.pprot_present = hasattr(self.bus, "pprot")
self.pstrb_present = hasattr(self.bus, "pstrb")
self.pslverr_present = hasattr(self.bus, "pslverr")
super().__init__(2**self.address_width, **kwargs)
self.log.info("APB master configuration:")
self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("APB master signals:")
for sig in sorted(list(set().union(self.bus._signals, self.bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
if self.pstrb_present:
assert self.byte_lanes == len(self.bus.pstrb)
assert self.byte_lanes * self.byte_size == self.width
self.bus.paddr.setimmediatevalue(0)
if self.pprot_present:
self.bus.pprot.setimmediatevalue(0)
self.bus.psel.setimmediatevalue(False)
self.bus.penable.setimmediatevalue(False)
self.bus.pwrite.setimmediatevalue(False)
self.bus.pwdata.setimmediatevalue(0)
if self.pstrb_present:
self.bus.pstrb.setimmediatevalue(0)
self._run_cr = None
self._init_reset(reset, reset_active_level)
def init_write(self, address, data, prot=AxiProt.NONSECURE, event=None):
if event is None:
event = Event()
if not isinstance(event, Event):
raise ValueError("Expected event object")
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if isinstance(data, int):
raise ValueError("Expected bytes or bytearray for data")
if address+len(data) > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if not self.pprot_present and prot != AxiProt.NONSECURE:
raise ValueError("pprot sideband signal value specified, but signal is not connected")
data = bytes(data)
cocotb.start_soon(self._write_wrapper(address, bytes(data), prot, event))
return event
def init_read(self, address, length, prot=AxiProt.NONSECURE, event=None):
if event is None:
event = Event()
if not isinstance(event, Event):
raise ValueError("Expected event object")
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if length < 0:
raise ValueError("Read length must be positive")
if address+length > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if not self.pprot_present and prot != AxiProt.NONSECURE:
raise ValueError("arprot sideband signal value specified, but signal is not connected")
cocotb.start_soon(self._read_wrapper(address, length, prot, event))
return event
def idle(self):
return not self.in_flight_operations
async def wait(self):
while not self.idle():
await self._idle.wait()
async def write(self, address, data, prot=AxiProt.NONSECURE):
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if isinstance(data, int):
raise ValueError("Expected bytes or bytearray for data")
if address+len(data) > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if not self.pprot_present and prot != AxiProt.NONSECURE:
raise ValueError("pprot sideband signal value specified, but signal is not connected")
event = Event()
data = bytes(data)
self.in_flight_operations += 1
self._idle.clear()
await self.command_queue.put(ApbWriteCmd(address, data, prot, event))
await event.wait()
return event.data
async def _write_wrapper(self, address, data, prot, event):
event.set(await self.write(address, data, prot))
async def read(self, address, length, prot=AxiProt.NONSECURE):
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if length < 0:
raise ValueError("Read length must be positive")
if address+length > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if not self.pprot_present and prot != AxiProt.NONSECURE:
raise ValueError("arprot sideband signal value specified, but signal is not connected")
event = Event()
self.in_flight_operations += 1
self._idle.clear()
await self.command_queue.put(ApbReadCmd(address, length, prot, event))
await event.wait()
return event.data
async def _read_wrapper(self, address, length, prot, event):
event.set(await self.read(address, length, prot))
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
self.bus.psel.value = False
self.bus.penable.value = False
if self._run_cr is not None:
self._run_cr.kill()
self._run_cr = None
def flush_cmd(cmd):
self.log.warning("Flushed write operation during reset: %s", cmd)
if cmd.event:
cmd.event.set(None)
while not self.command_queue.empty():
cmd = self.command_queue.get_nowait()
flush_cmd(cmd)
if self.current_command:
cmd = self.current_command
self.current_command = None
flush_cmd(cmd)
self.in_flight_operations = 0
self._idle.set()
else:
self.log.info("Reset de-asserted")
if self._run_cr is None:
self._run_cr = cocotb.start_soon(self._run())
async def _run(self):
clock_edge_event = RisingEdge(self.clock)
while True:
cmd = await self.command_queue.get()
self.current_command = cmd
length = 0
pwrite = False
if isinstance(cmd, ApbWriteCmd):
length = len(cmd.data)
pwrite = True
else:
length = cmd.length
pwrite = False
word_addr = (cmd.address // self.byte_lanes) * self.byte_lanes
start_offset = cmd.address % self.byte_lanes
end_offset = ((cmd.address + length - 1) % self.byte_lanes) + 1
strb_start = (self.strb_mask << start_offset) & self.strb_mask
strb_end = self.strb_mask >> (self.byte_lanes - end_offset)
cycles = (length + (cmd.address % self.byte_lanes) + self.byte_lanes-1) // self.byte_lanes
offset = 0
read_data = bytearray()
resp = AxiResp.OKAY
if self.log.isEnabledFor(logging.INFO):
if pwrite:
self.log.info("Write start addr: 0x%08x prot: %s data: %s",
cmd.address, cmd.prot, ' '.join((f'{c:02x}' for c in cmd.data)))
else:
self.log.info("Read start addr: 0x%08x prot: %s length: %d",
cmd.address, cmd.prot, cmd.length)
await clock_edge_event
self.bus.psel.value = True
for k in range(cycles):
start = 0
stop = self.byte_lanes
strb = self.strb_mask
if k == 0:
start = start_offset
strb &= strb_start
if k == cycles-1:
stop = end_offset
strb &= strb_end
val = 0
if pwrite:
for j in range(start, stop):
val |= cmd.data[offset] << j*8
offset += 1
if not self.pstrb_present and strb != self.strb_mask:
self.log.warning("Partial operation requested with pstrb not connected, write will be zero-padded (0x%x != 0x%x)", strb, self.strb_mask)
else:
strb = 0
while self.pause:
await clock_edge_event
if k == 0:
self.bus.paddr.value = cmd.address
else:
self.bus.paddr.value = word_addr + k*self.byte_lanes
self.bus.pprot.value = cmd.prot
self.bus.penable.value = True
self.bus.pwrite.value = pwrite
self.bus.pwdata.value = val
self.bus.pstrb.value = strb
await clock_edge_event
while not int(self.bus.pready.value):
await clock_edge_event
self.bus.penable.value = False
cycle_data = int(self.bus.prdata.value)
if self.pslverr_present and int(self.bus.pslverr.value):
resp = AxiResp.SLVERR
start = 0
stop = self.byte_lanes
if k == 0:
start = start_offset
if k == cycles-1:
stop = end_offset
for j in range(start, stop):
read_data.append((cycle_data >> j*8) & 0xff)
self.bus.psel.value = False
if pwrite:
self.log.info("Write complete addr: 0x%08x prot: %s resp: %s length: %d",
cmd.address, cmd.prot, resp, length)
write_resp = ApbWriteResp(cmd.address, length, resp)
cmd.event.set(write_resp)
else:
if self.log.isEnabledFor(logging.INFO):
self.log.info("Read complete addr: 0x%08x prot: %s resp: %s data: %s",
cmd.address, cmd.prot, resp, ' '.join((f'{c:02x}' for c in read_data)))
read_resp = ApbReadResp(cmd.address, bytes(read_data), resp)
cmd.event.set(read_resp)
self.current_write_command = None
self.in_flight_operations -= 1
if self.in_flight_operations == 0:
self._idle.set()
class ApbSlave(ApbPause, Reset):
def __init__(self, bus, clock, reset=None, target=None, reset_active_level=True, **kwargs):
self.bus = bus
self.clock = clock
self.reset = reset
self.target = target
if bus._name:
self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus._entity._name}")
self.log.info("APB slave model")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(**kwargs)
self.address_width = len(self.bus.paddr)
self.width = len(self.bus.pwdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.strb_mask = 2**self.byte_lanes-1
self.pprot_present = hasattr(self.bus, "pprot")
self.pstrb_present = hasattr(self.bus, "pstrb")
self.pslverr_present = hasattr(self.bus, "pslverr")
self.log.info("APB slave model configuration:")
self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("APB slave model signals:")
for sig in sorted(list(set().union(self.bus._signals, self.bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
if self.pstrb_present:
assert self.byte_lanes == len(self.bus.pstrb)
assert self.byte_lanes * self.byte_size == self.width
self.bus.pready.setimmediatevalue(False)
self.bus.prdata.setimmediatevalue(0)
if self.pslverr_present:
self.bus.pslverr.setimmediatevalue(0)
self._run_cr = None
self._init_reset(reset, reset_active_level)
async def _write(self, address, data):
await self.target.write(address, data)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
self.bus.pready.value = False
if self._run_cr is not None:
self._run_cr.kill()
self._run_cr = None
else:
self.log.info("Reset de-asserted")
if self._run_cr is None:
self._run_cr = cocotb.start_soon(self._run())
async def _run(self):
clock_edge_event = RisingEdge(self.clock)
self.bus.pready.value = False
while True:
await clock_edge_event
if self.pause:
continue
if not int(self.bus.psel.value) or not int(self.bus.penable.value):
continue
addr = (int(self.bus.paddr.value) // self.byte_lanes) * self.byte_lanes
if self.pprot_present:
prot = AxiProt(int(self.bus.pprot.value))
else:
prot = AxiProt.NONSECURE
pslverr = False
if (int(self.bus.pwrite.value)):
data = int(self.bus.pwdata.value)
if self.pstrb_present:
strb = int(self.bus.pstrb.value)
else:
strb = self.strb_mask
# generate operation list
offset = 0
start_offset = None
write_ops = []
data = data.to_bytes(self.byte_lanes, 'little')
if self.log.isEnabledFor(logging.INFO):
self.log.info("Write data paddr: 0x%08x pprot: %s pstrb: 0x%02x data: %s",
addr, prot, strb, ' '.join((f'{c:02x}' for c in data)))
for i in range(self.byte_lanes):
if strb & (1 << i):
if start_offset is None:
start_offset = offset
else:
if start_offset is not None and offset != start_offset:
write_ops.append((addr+start_offset, data[start_offset:offset]))
start_offset = None
offset += 1
if start_offset is not None and offset != start_offset:
write_ops.append((addr+start_offset, data[start_offset:offset]))
# perform writes
try:
for addr, data in write_ops:
await self._write(addr, data)
except Exception:
self.log.warning("Write operation failed")
pslverr = True
else:
try:
data = await self._read(addr, self.byte_lanes)
except Exception:
self.log.warning("Read operation failed")
data = bytes(self.byte_lanes)
pslverr = True
if self.log.isEnabledFor(logging.INFO):
self.log.info("Read data paddr: 0x%08x pprot: %s data: %s",
addr, prot, ' '.join((f'{c:02x}' for c in data)))
self.bus.prdata.value = int.from_bytes(data, 'little')
await clock_edge_event
if self.pslverr_present:
self.bus.pslverr.value = pslverr
self.bus.pready.value = True
await clock_edge_event
self.bus.pready.value = False
if self.pslverr_present:
self.bus.pslverr.value = False
class ApbRam(ApbSlave, Memory):
def __init__(self, bus, clock, reset=None, reset_active_level=True, size=2**64, mem=None, **kwargs):
super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
async def _write(self, address, data):
self.write(address % self.size, data)
async def _read(self, address, length):
return self.read(address % self.size, length)

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -34,8 +34,8 @@ AxiAWBus, AxiAWTransaction, AxiAWSource, AxiAWSink, AxiAWMonitor = define_stream
# Write data channel # Write data channel
AxiWBus, AxiWTransaction, AxiWSource, AxiWSink, AxiWMonitor = define_stream("AxiW", AxiWBus, AxiWTransaction, AxiWSource, AxiWSink, AxiWMonitor = define_stream("AxiW",
signals=["wdata", "wstrb", "wlast", "wvalid", "wready"], signals=["wdata", "wlast", "wvalid", "wready"],
optional_signals=["wuser"], optional_signals=["wstrb", "wuser"],
signal_widths={"wlast": 1} signal_widths={"wlast": 1}
) )

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -23,7 +23,8 @@ THE SOFTWARE.
""" """
import logging import logging
from collections import namedtuple, Counter from collections import Counter
from typing import List, NamedTuple, Union
import cocotb import cocotb
from cocotb.queue import Queue from cocotb.queue import Queue
@@ -32,21 +33,78 @@ from cocotb.triggers import Event
from .version import __version__ from .version import __version__
from .constants import AxiBurstType, AxiLockType, AxiProt, AxiResp from .constants import AxiBurstType, AxiLockType, AxiProt, AxiResp
from .axi_channels import AxiAWSource, AxiWSource, AxiBSink, AxiARSource, AxiRSink from .axi_channels import AxiAWSource, AxiWSource, AxiBSink, AxiARSource, AxiRSink
from .address_space import Region
from .reset import Reset from .reset import Reset
# AXI master write helper objects # AXI master write helper objects
AxiWriteCmd = namedtuple("AxiWriteCmd", ["address", "data", "awid", "burst", "size", class AxiWriteCmd(NamedTuple):
"lock", "cache", "prot", "qos", "region", "user", "wuser", "event"]) address: int
AxiWriteRespCmd = namedtuple("AxiWriteRespCmd", ["address", "length", "size", "cycles", data: bytes
"prot", "burst_list", "event"]) awid: int
AxiWriteResp = namedtuple("AxiWriteResp", ["address", "length", "resp", "user"]) burst: AxiBurstType
size: int
lock: AxiLockType
cache: int
prot: AxiProt
qos: int
region: int
user: int
wuser: Union[list, int, None]
event: Event
class AxiWriteRespCmd(NamedTuple):
address: int
length: int
size: int
cycles: int
prot: AxiProt
burst_list: List[int]
event: Event
class AxiWriteResp(NamedTuple):
address: int
length: int
resp: AxiResp
user: Union[list, None]
# AXI master read helper objects # AXI master read helper objects
AxiReadCmd = namedtuple("AxiReadCmd", ["address", "length", "arid", "burst", "size", class AxiReadCmd(NamedTuple):
"lock", "cache", "prot", "qos", "region", "user", "event"]) address: int
AxiReadRespCmd = namedtuple("AxiReadRespCmd", ["address", "length", "size", "cycles", length: int
"prot", "burst_list", "event"]) arid: int
AxiReadResp = namedtuple("AxiReadResp", ["address", "data", "resp", "user"]) burst: AxiBurstType
size: int
lock: AxiLockType
cache: int
prot: AxiProt
qos: int
region: int
user: int
event: Event
class AxiReadRespCmd(NamedTuple):
address: int
length: int
size: int
cycles: int
prot: AxiProt
burst_list: List[int]
event: Event
class AxiReadResp(NamedTuple):
address: int
data: bytes
resp: AxiResp
user: Union[list, None]
def __bytes__(self):
return self.data
class TagContext: class TagContext:
@@ -66,7 +124,7 @@ class TagContext:
def _start(self): def _start(self):
if self._cr is None: if self._cr is None:
self._cr = cocotb.fork(self._process_queue()) self._cr = cocotb.start_soon(self._process_queue())
def _flush(self): def _flush(self):
flushed_cmds = [] flushed_cmds = []
@@ -135,16 +193,19 @@ class TagContextManager:
return flushed_cmds return flushed_cmds
class AxiMasterWrite(Reset): class AxiMasterWrite(Region, Reset):
def __init__(self, bus, clock, reset=None, reset_active_level=True, max_burst_len=256): def __init__(self, bus, clock, reset=None, reset_active_level=True, max_burst_len=256, **kwargs):
self.bus = bus self.bus = bus
self.clock = clock self.clock = clock
self.reset = reset self.reset = reset
if bus.aw._name:
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}") self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}")
self.log.info("AXI master (write)") self.log.info("AXI master (write)")
self.log.info("cocotbext-axi version %s", __version__) self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich") self.log.info("Copyright (c) 2020-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi") self.log.info("https://github.com/alexforencich/cocotbext-axi")
self.aw_channel = AxiAWSource(bus.aw, clock, reset, reset_active_level) self.aw_channel = AxiAWSource(bus.aw, clock, reset, reset_active_level)
@@ -155,6 +216,7 @@ class AxiMasterWrite(Reset):
self.b_channel.queue_occupancy_limit = 2 self.b_channel.queue_occupancy_limit = 2
self.write_command_queue = Queue() self.write_command_queue = Queue()
self.write_command_queue.queue_occupancy_limit = 2
self.current_write_command = None self.current_write_command = None
self.id_count = 2**len(self.aw_channel.bus.awid) self.id_count = 2**len(self.aw_channel.bus.awid)
@@ -167,6 +229,8 @@ class AxiMasterWrite(Reset):
self._idle = Event() self._idle = Event()
self._idle.set() self._idle.set()
self.address_width = len(self.aw_channel.bus.awaddr)
self.id_width = len(self.aw_channel.bus.awid)
self.width = len(self.w_channel.bus.wdata) self.width = len(self.w_channel.bus.wdata)
self.byte_size = 8 self.byte_size = 8
self.byte_lanes = self.width // self.byte_size self.byte_lanes = self.width // self.byte_size
@@ -181,12 +245,15 @@ class AxiMasterWrite(Reset):
self.awqos_present = hasattr(self.bus.aw, "awqos") self.awqos_present = hasattr(self.bus.aw, "awqos")
self.awregion_present = hasattr(self.bus.aw, "awregion") self.awregion_present = hasattr(self.bus.aw, "awregion")
self.awuser_present = hasattr(self.bus.aw, "awuser") self.awuser_present = hasattr(self.bus.aw, "awuser")
self.wstrb_present = hasattr(self.bus.w, "wstrb")
self.wuser_present = hasattr(self.bus.w, "wuser") self.wuser_present = hasattr(self.bus.w, "wuser")
self.buser_present = hasattr(self.bus.b, "buser") self.buser_present = hasattr(self.bus.b, "buser")
super().__init__(2**self.address_width, **kwargs)
self.log.info("AXI master configuration:") self.log.info("AXI master configuration:")
self.log.info(" Address width: %d bits", len(self.aw_channel.bus.awaddr)) self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" ID width: %d bits", len(self.aw_channel.bus.awid)) self.log.info(" ID width: %d bits", self.id_width)
self.log.info(" Byte size: %d bits", self.byte_size) self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes) self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info(" Max burst size: %d (%d bytes)", self.max_burst_size, 2**self.max_burst_size) self.log.info(" Max burst size: %d (%d bytes)", self.max_burst_size, 2**self.max_burst_size)
@@ -201,6 +268,7 @@ class AxiMasterWrite(Reset):
else: else:
self.log.info(" %s: not present", sig) self.log.info(" %s: not present", sig)
if self.wstrb_present:
assert self.byte_lanes == len(self.w_channel.bus.wstrb) assert self.byte_lanes == len(self.w_channel.bus.wstrb)
assert self.byte_lanes * self.byte_size == self.width assert self.byte_lanes * self.byte_size == self.width
@@ -220,6 +288,15 @@ class AxiMasterWrite(Reset):
if not isinstance(event, Event): if not isinstance(event, Event):
raise ValueError("Expected event object") raise ValueError("Expected event object")
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if isinstance(data, int):
raise ValueError("Expected bytes or bytearray for data")
if burst != AxiBurstType.FIXED and address+len(data) > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if awid is None or awid < 0: if awid is None or awid < 0:
awid = None awid = None
elif awid > self.id_count: elif awid > self.id_count:
@@ -263,12 +340,10 @@ class AxiMasterWrite(Reset):
else: else:
wuser = list(wuser) wuser = list(wuser)
self.in_flight_operations += 1 data = bytes(data)
self._idle.clear()
cmd = AxiWriteCmd(address, bytearray(data), awid, burst, size, lock, cocotb.start_soon(self._write_wrapper(address, data, awid, burst, size,
cache, prot, qos, region, user, wuser, event) lock, cache, prot, qos, region, user, wuser, event))
self.write_command_queue.put_nowait(cmd)
return event return event
@@ -281,46 +356,76 @@ class AxiMasterWrite(Reset):
async def write(self, address, data, awid=None, burst=AxiBurstType.INCR, size=None, async def write(self, address, data, awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0): lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
event = self.init_write(address, data, awid, burst, size, lock, cache, prot, qos, region, user, wuser)
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if isinstance(data, int):
raise ValueError("Expected bytes or bytearray for data")
if burst != AxiBurstType.FIXED and address+len(data) > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if awid is None or awid < 0:
awid = None
elif awid > self.id_count:
raise ValueError("Requested ID exceeds maximum ID allowed for ID signal width")
burst = AxiBurstType(burst)
if size is None or size < 0:
size = self.max_burst_size
elif size > self.max_burst_size:
raise ValueError("Requested burst size exceeds maximum burst size allowed for bus width")
lock = AxiLockType(lock)
prot = AxiProt(prot)
if not self.awlock_present and lock != AxiLockType.NORMAL:
raise ValueError("awlock sideband signal value specified, but signal is not connected")
if not self.awcache_present and cache != 0b0011:
raise ValueError("awcache sideband signal value specified, but signal is not connected")
if not self.awprot_present and prot != AxiProt.NONSECURE:
raise ValueError("awprot sideband signal value specified, but signal is not connected")
if not self.awqos_present and qos != 0:
raise ValueError("awqos sideband signal value specified, but signal is not connected")
if not self.awregion_present and region != 0:
raise ValueError("awregion sideband signal value specified, but signal is not connected")
if not self.awuser_present and user != 0:
raise ValueError("awuser sideband signal value specified, but signal is not connected")
if not self.wuser_present and wuser != 0:
raise ValueError("wuser sideband signal value specified, but signal is not connected")
if wuser is None:
wuser = 0
elif isinstance(wuser, int):
pass
else:
wuser = list(wuser)
event = Event()
data = bytes(data)
self.in_flight_operations += 1
self._idle.clear()
cmd = AxiWriteCmd(address, data, awid, burst, size, lock,
cache, prot, qos, region, user, wuser, event)
await self.write_command_queue.put(cmd)
await event.wait() await event.wait()
return event.data return event.data
async def write_words(self, address, data, byteorder='little', ws=2, awid=None, burst=AxiBurstType.INCR, size=None, async def _write_wrapper(self, address, data, awid, burst, size,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0): lock, cache, prot, qos, region, user, wuser, event):
words = data event.set(await self.write(address, data, awid, burst, size,
data = bytearray() lock, cache, prot, qos, region, user, wuser))
for w in words:
data.extend(w.to_bytes(ws, byteorder))
await self.write(address, data, awid, burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_dwords(self, address, data, byteorder='little', awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
await self.write_words(address, data, byteorder, 4, awid, burst, size,
lock, cache, prot, qos, region, user, wuser)
async def write_qwords(self, address, data, byteorder='little', awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
await self.write_words(address, data, byteorder, 8, awid, burst, size,
lock, cache, prot, qos, region, user, wuser)
async def write_byte(self, address, data, awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
await self.write(address, [data], awid, burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_word(self, address, data, byteorder='little', ws=2, awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
await self.write_words(address, [data], byteorder, ws, awid, burst, size,
lock, cache, prot, qos, region, user, wuser)
async def write_dword(self, address, data, byteorder='little', awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
await self.write_dwords(address, [data], byteorder, awid, burst, size,
lock, cache, prot, qos, region, user, wuser)
async def write_qword(self, address, data, byteorder='little', awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
await self.write_qwords(address, [data], byteorder, awid, burst, size,
lock, cache, prot, qos, region, user, wuser)
def _handle_reset(self, state): def _handle_reset(self, state):
if state: if state:
@@ -361,9 +466,9 @@ class AxiMasterWrite(Reset):
else: else:
self.log.info("Reset de-asserted") self.log.info("Reset de-asserted")
if self._process_write_cr is None: if self._process_write_cr is None:
self._process_write_cr = cocotb.fork(self._process_write()) self._process_write_cr = cocotb.start_soon(self._process_write())
if self._process_write_resp_cr is None: if self._process_write_resp_cr is None:
self._process_write_resp_cr = cocotb.fork(self._process_write_resp()) self._process_write_resp_cr = cocotb.start_soon(self._process_write_resp())
async def _process_write(self): async def _process_write(self):
while True: while True:
@@ -380,7 +485,7 @@ class AxiMasterWrite(Reset):
cycles = (len(cmd.data) + (cmd.address % num_bytes) + num_bytes-1) // num_bytes cycles = (len(cmd.data) + (cmd.address % num_bytes) + num_bytes-1) // num_bytes
cur_addr = aligned_addr cur_addr = cmd.address
offset = 0 offset = 0
cycle_offset = aligned_addr-word_addr cycle_offset = aligned_addr-word_addr
n = 0 n = 0
@@ -397,6 +502,7 @@ class AxiMasterWrite(Reset):
wuser = cmd.wuser wuser = cmd.wuser
if self.log.isEnabledFor(logging.INFO):
self.log.info("Write start addr: 0x%08x awid: 0x%x prot: %s data: %s", self.log.info("Write start addr: 0x%08x awid: 0x%x prot: %s data: %s",
cmd.address, awid, cmd.prot, ' '.join((f'{c:02x}' for c in cmd.data))) cmd.address, awid, cmd.prot, ' '.join((f'{c:02x}' for c in cmd.data)))
@@ -448,6 +554,9 @@ class AxiMasterWrite(Reset):
n += 1 n += 1
if not self.wstrb_present and strb != self.strb_mask:
self.log.warning("Partial operation requested with wstrb not connected, write will be zero-padded (0x%x != 0x%x)", strb, self.strb_mask)
w = self.w_channel._transaction_obj() w = self.w_channel._transaction_obj()
w.wdata = val w.wdata = val
w.wstrb = strb w.wstrb = strb
@@ -463,6 +572,11 @@ class AxiMasterWrite(Reset):
await self.w_channel.send(w) await self.w_channel.send(w)
if cmd.burst == AxiBurstType.FIXED:
cur_addr = cmd.address
elif k == 0:
cur_addr = aligned_addr + num_bytes
else:
cur_addr += num_bytes cur_addr += num_bytes
cycle_offset = (cycle_offset + num_bytes) % self.byte_lanes cycle_offset = (cycle_offset + num_bytes) % self.byte_lanes
@@ -475,10 +589,9 @@ class AxiMasterWrite(Reset):
while True: while True:
b = await self.b_channel.recv() b = await self.b_channel.recv()
bid = int(b.bid) bid = int(getattr(b, 'bid', 0))
if self.active_id[bid] <= 0: assert self.active_id[bid] > 0, "unexpected burst ID"
raise Exception(f"Unexpected burst ID {bid}")
self.tag_context_manager.put_resp(bid, b) self.tag_context_manager.put_resp(bid, b)
@@ -491,8 +604,8 @@ class AxiMasterWrite(Reset):
for burst_length in cmd.burst_list: for burst_length in cmd.burst_list:
b = await context.get_resp() b = await context.get_resp()
burst_resp = AxiResp(b.bresp) burst_resp = AxiResp(int(getattr(b, 'bresp', AxiResp.OKAY)))
burst_user = int(b.buser) burst_user = int(getattr(b, 'buser', 0))
if burst_resp != AxiResp.OKAY: if burst_resp != AxiResp.OKAY:
resp = burst_resp resp = burst_resp
@@ -500,8 +613,7 @@ class AxiMasterWrite(Reset):
if burst_user is not None: if burst_user is not None:
user.append(burst_user) user.append(burst_user)
if self.active_id[bid] <= 0: assert self.active_id[bid] > 0, "unexpected burst ID"
raise Exception(f"Unexpected burst ID {bid}")
self.active_id[bid] -= 1 self.active_id[bid] -= 1
@@ -523,16 +635,19 @@ class AxiMasterWrite(Reset):
self._idle.set() self._idle.set()
class AxiMasterRead(Reset): class AxiMasterRead(Region, Reset):
def __init__(self, bus, clock, reset=None, reset_active_level=True, max_burst_len=256): def __init__(self, bus, clock, reset=None, reset_active_level=True, max_burst_len=256, **kwargs):
self.bus = bus self.bus = bus
self.clock = clock self.clock = clock
self.reset = reset self.reset = reset
if bus.ar._name:
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}") self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}")
self.log.info("AXI master (read)") self.log.info("AXI master (read)")
self.log.info("cocotbext-axi version %s", __version__) self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich") self.log.info("Copyright (c) 2020-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi") self.log.info("https://github.com/alexforencich/cocotbext-axi")
self.ar_channel = AxiARSource(bus.ar, clock, reset, reset_active_level) self.ar_channel = AxiARSource(bus.ar, clock, reset, reset_active_level)
@@ -541,6 +656,7 @@ class AxiMasterRead(Reset):
self.r_channel.queue_occupancy_limit = 2 self.r_channel.queue_occupancy_limit = 2
self.read_command_queue = Queue() self.read_command_queue = Queue()
self.read_command_queue.queue_occupancy_limit = 2
self.current_read_command = None self.current_read_command = None
self.id_count = 2**len(self.ar_channel.bus.arid) self.id_count = 2**len(self.ar_channel.bus.arid)
@@ -553,6 +669,8 @@ class AxiMasterRead(Reset):
self._idle = Event() self._idle = Event()
self._idle.set() self._idle.set()
self.address_width = len(self.ar_channel.bus.araddr)
self.id_width = len(self.ar_channel.bus.arid)
self.width = len(self.r_channel.bus.rdata) self.width = len(self.r_channel.bus.rdata)
self.byte_size = 8 self.byte_size = 8
self.byte_lanes = self.width // self.byte_size self.byte_lanes = self.width // self.byte_size
@@ -568,9 +686,11 @@ class AxiMasterRead(Reset):
self.aruser_present = hasattr(self.bus.ar, "aruser") self.aruser_present = hasattr(self.bus.ar, "aruser")
self.ruser_present = hasattr(self.bus.r, "ruser") self.ruser_present = hasattr(self.bus.r, "ruser")
super().__init__(2**self.address_width, **kwargs)
self.log.info("AXI master configuration:") self.log.info("AXI master configuration:")
self.log.info(" Address width: %d bits", len(self.ar_channel.bus.araddr)) self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" ID width: %d bits", len(self.ar_channel.bus.arid)) self.log.info(" ID width: %d bits", self.id_width)
self.log.info(" Byte size: %d bits", self.byte_size) self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes) self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info(" Max burst size: %d (%d bytes)", self.max_burst_size, 2**self.max_burst_size) self.log.info(" Max burst size: %d (%d bytes)", self.max_burst_size, 2**self.max_burst_size)
@@ -603,9 +723,15 @@ class AxiMasterRead(Reset):
if not isinstance(event, Event): if not isinstance(event, Event):
raise ValueError("Expected event object") raise ValueError("Expected event object")
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if length < 0: if length < 0:
raise ValueError("Read length must be positive") raise ValueError("Read length must be positive")
if burst != AxiBurstType.FIXED and address+length > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if arid is None or arid < 0: if arid is None or arid < 0:
arid = None arid = None
elif arid > self.id_count: elif arid > self.id_count:
@@ -639,11 +765,8 @@ class AxiMasterRead(Reset):
if not self.aruser_present and user != 0: if not self.aruser_present and user != 0:
raise ValueError("aruser sideband signal value specified, but signal is not connected") raise ValueError("aruser sideband signal value specified, but signal is not connected")
self.in_flight_operations += 1 cocotb.start_soon(self._read_wrapper(address, length, arid, burst, size,
self._idle.clear() lock, cache, prot, qos, region, user, event))
cmd = AxiReadCmd(address, length, arid, burst, size, lock, cache, prot, qos, region, user, event)
self.read_command_queue.put_nowait(cmd)
return event return event
@@ -656,46 +779,64 @@ class AxiMasterRead(Reset):
async def read(self, address, length, arid=None, burst=AxiBurstType.INCR, size=None, async def read(self, address, length, arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0): lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
event = self.init_read(address, length, arid, burst, size, lock, cache, prot, qos, region, user)
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if length < 0:
raise ValueError("Read length must be positive")
if burst != AxiBurstType.FIXED and address+length > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if arid is None or arid < 0:
arid = None
elif arid > self.id_count:
raise ValueError("Requested ID exceeds maximum ID allowed for ID signal width")
burst = AxiBurstType(burst)
if size is None or size < 0:
size = self.max_burst_size
elif size > self.max_burst_size:
raise ValueError("Requested burst size exceeds maximum burst size allowed for bus width")
lock = AxiLockType(lock)
prot = AxiProt(prot)
if not self.arlock_present and lock != AxiLockType.NORMAL:
raise ValueError("arlock sideband signal value specified, but signal is not connected")
if not self.arcache_present and cache != 0b0011:
raise ValueError("arcache sideband signal value specified, but signal is not connected")
if not self.arprot_present and prot != AxiProt.NONSECURE:
raise ValueError("arprot sideband signal value specified, but signal is not connected")
if not self.arqos_present and qos != 0:
raise ValueError("arqos sideband signal value specified, but signal is not connected")
if not self.arregion_present and region != 0:
raise ValueError("arregion sideband signal value specified, but signal is not connected")
if not self.aruser_present and user != 0:
raise ValueError("aruser sideband signal value specified, but signal is not connected")
event = Event()
self.in_flight_operations += 1
self._idle.clear()
cmd = AxiReadCmd(address, length, arid, burst, size, lock, cache, prot, qos, region, user, event)
await self.read_command_queue.put(cmd)
await event.wait() await event.wait()
return event.data return event.data
async def read_words(self, address, count, byteorder='little', ws=2, arid=None, burst=AxiBurstType.INCR, size=None, async def _read_wrapper(self, address, length, arid, burst, size,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0): lock, cache, prot, qos, region, user, event):
data = await self.read(address, count*ws, arid, burst, size, lock, cache, prot, qos, region, user) event.set(await self.read(address, length, arid, burst, size,
words = [] lock, cache, prot, qos, region, user))
for k in range(count):
words.append(int.from_bytes(data.data[ws*k:ws*(k+1)], byteorder))
return words
async def read_dwords(self, address, count, byteorder='little', arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_words(address, count, byteorder, 4, arid, burst, size,
lock, cache, prot, qos, region, user)
async def read_qwords(self, address, count, byteorder='little', arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_words(address, count, byteorder, 8, arid, burst, size,
lock, cache, prot, qos, region, user)
async def read_byte(self, address, arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return (await self.read(address, 1, arid, burst, size, lock, cache, prot, qos, region, user)).data[0]
async def read_word(self, address, byteorder='little', ws=2, arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return (await self.read_words(address, 1, byteorder, ws, arid, burst, size,
lock, cache, prot, qos, region, user))[0]
async def read_dword(self, address, byteorder='little', arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return (await self.read_dwords(address, 1, byteorder, arid, burst, size,
lock, cache, prot, qos, region, user))[0]
async def read_qword(self, address, byteorder='little', arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return (await self.read_qwords(address, 1, byteorder, arid, burst, size,
lock, cache, prot, qos, region, user))[0]
def _handle_reset(self, state): def _handle_reset(self, state):
if state: if state:
@@ -735,9 +876,9 @@ class AxiMasterRead(Reset):
else: else:
self.log.info("Reset de-asserted") self.log.info("Reset de-asserted")
if self._process_read_cr is None: if self._process_read_cr is None:
self._process_read_cr = cocotb.fork(self._process_read()) self._process_read_cr = cocotb.start_soon(self._process_read())
if self._process_read_resp_cr is None: if self._process_read_resp_cr is None:
self._process_read_resp_cr = cocotb.fork(self._process_read_resp()) self._process_read_resp_cr = cocotb.start_soon(self._process_read_resp())
async def _process_read(self): async def _process_read(self):
while True: while True:
@@ -752,7 +893,7 @@ class AxiMasterRead(Reset):
burst_list = [] burst_list = []
cur_addr = aligned_addr cur_addr = cmd.address
n = 0 n = 0
burst_length = 0 burst_length = 0
@@ -778,7 +919,7 @@ class AxiMasterRead(Reset):
burst_list.append(burst_length) burst_list.append(burst_length)
ar = self.r_channel._transaction_obj() ar = self.ar_channel._transaction_obj()
ar.arid = arid ar.arid = arid
ar.araddr = cur_addr ar.araddr = cur_addr
ar.arlen = burst_length-1 ar.arlen = burst_length-1
@@ -797,6 +938,11 @@ class AxiMasterRead(Reset):
self.log.info("Read burst start arid: 0x%x araddr: 0x%08x arlen: %d arsize: %d arprot: %s", self.log.info("Read burst start arid: 0x%x araddr: 0x%08x arlen: %d arsize: %d arprot: %s",
arid, cur_addr, burst_length-1, cmd.size, cmd.prot) arid, cur_addr, burst_length-1, cmd.size, cmd.prot)
if cmd.burst == AxiBurstType.FIXED:
cur_addr = cmd.address
elif k == 0:
cur_addr = aligned_addr + num_bytes
else:
cur_addr += num_bytes cur_addr += num_bytes
resp_cmd = AxiReadRespCmd(cmd.address, cmd.length, cmd.size, cycles, cmd.prot, burst_list, cmd.event) resp_cmd = AxiReadRespCmd(cmd.address, cmd.length, cmd.size, cycles, cmd.prot, burst_list, cmd.event)
@@ -805,27 +951,14 @@ class AxiMasterRead(Reset):
self.current_read_command = None self.current_read_command = None
async def _process_read_resp(self): async def _process_read_resp(self):
burst = []
cur_rid = None
while True: while True:
r = await self.r_channel.recv() r = await self.r_channel.recv()
rid = int(r.rid) rid = int(getattr(r, 'rid', 0))
if cur_rid is not None and cur_rid != rid: assert self.active_id[rid] > 0, "unexpected burst ID"
raise Exception(f"ID not constant within burst (expected {cur_rid}, got {rid})")
if self.active_id[rid] <= 0: self.tag_context_manager.put_resp(rid, r)
raise Exception(f"Unexpected burst ID {rid}")
burst.append(r)
cur_rid = rid
if int(r.rlast):
self.tag_context_manager.put_resp(rid, burst)
burst = []
cur_rid = None
async def _process_read_resp_id(self, context, cmd): async def _process_read_resp_id(self, context, cmd):
rid = context.current_tag rid = context.current_tag
@@ -846,15 +979,19 @@ class AxiMasterRead(Reset):
first = True first = True
for burst_length in cmd.burst_list: for burst_length in cmd.burst_list:
burst = await context.get_resp() for k in range(burst_length):
r = await context.get_resp()
if len(burst) != burst_length: assert self.active_id[rid] > 0, "unexpected burst ID"
raise Exception(f"Burst length incorrect (ID {rid}, expected {burst_length}, got {len(burst)}")
if k == burst_length-1:
assert int(r.rlast), "missing rlast at end of burst"
else:
assert not int(r.rlast), "unexpected rlast within burst"
for r in burst:
cycle_data = int(r.rdata) cycle_data = int(r.rdata)
cycle_resp = AxiResp(r.rresp) cycle_resp = AxiResp(int(getattr(r, "rresp", AxiResp.OKAY)))
cycle_user = int(r.ruser) cycle_user = int(getattr(r, "ruser", 0))
if cycle_resp != AxiResp.OKAY: if cycle_resp != AxiResp.OKAY:
resp = cycle_resp resp = cycle_resp
@@ -884,10 +1021,11 @@ class AxiMasterRead(Reset):
if not self.ruser_present: if not self.ruser_present:
user = None user = None
if self.log.isEnabledFor(logging.INFO):
self.log.info("Read complete addr: 0x%08x prot: %s resp: %s data: %s", self.log.info("Read complete addr: 0x%08x prot: %s resp: %s data: %s",
cmd.address, cmd.prot, resp, ' '.join((f'{c:02x}' for c in data))) cmd.address, cmd.prot, resp, ' '.join((f'{c:02x}' for c in data)))
read_resp = AxiReadResp(cmd.address, data, resp, user) read_resp = AxiReadResp(cmd.address, bytes(data), resp, user)
cmd.event.set(read_resp) cmd.event.set(read_resp)
@@ -897,13 +1035,15 @@ class AxiMasterRead(Reset):
self._idle.set() self._idle.set()
class AxiMaster: class AxiMaster(Region):
def __init__(self, bus, clock, reset=None, reset_active_level=True, max_burst_len=256): def __init__(self, bus, clock, reset=None, reset_active_level=True, max_burst_len=256, **kwargs):
self.write_if = None self.write_if = None
self.read_if = None self.read_if = None
self.write_if = AxiMasterWrite(bus.write, clock, reset, reset_active_level, max_burst_len) self.write_if = AxiMasterWrite(bus.write, clock, reset, reset_active_level, max_burst_len, **kwargs)
self.read_if = AxiMasterRead(bus.read, clock, reset, reset_active_level, max_burst_len) self.read_if = AxiMasterRead(bus.read, clock, reset, reset_active_level, max_burst_len, **kwargs)
super().__init__(max(self.write_if.size, self.read_if.size), **kwargs)
def init_read(self, address, length, arid=None, burst=AxiBurstType.INCR, size=None, def init_read(self, address, length, arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, event=None): lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, event=None):
@@ -932,77 +1072,7 @@ class AxiMaster:
return await self.read_if.read(address, length, arid, return await self.read_if.read(address, length, arid,
burst, size, lock, cache, prot, qos, region, user) burst, size, lock, cache, prot, qos, region, user)
async def read_words(self, address, count, byteorder='little', ws=2, arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_if.read_words(address, count, byteorder, ws, arid,
burst, size, lock, cache, prot, qos, region, user)
async def read_dwords(self, address, count, byteorder='little', arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_if.read_dwords(address, count, byteorder, arid,
burst, size, lock, cache, prot, qos, region, user)
async def read_qwords(self, address, count, byteorder='little', arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_if.read_qwords(address, count, byteorder, arid,
burst, size, lock, cache, prot, qos, region, user)
async def read_byte(self, address, arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_if.read_byte(address, arid,
burst, size, lock, cache, prot, qos, region, user)
async def read_word(self, address, byteorder='little', ws=2, arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_if.read_word(address, byteorder, ws, arid,
burst, size, lock, cache, prot, qos, region, user)
async def read_dword(self, address, byteorder='little', arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_if.read_dword(address, byteorder, arid,
burst, size, lock, cache, prot, qos, region, user)
async def read_qword(self, address, byteorder='little', arid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0):
return await self.read_if.read_qword(address, byteorder, arid,
burst, size, lock, cache, prot, qos, region, user)
async def write(self, address, data, awid=None, burst=AxiBurstType.INCR, size=None, async def write(self, address, data, awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0): lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
return await self.write_if.write(address, data, awid, return await self.write_if.write(address, data, awid,
burst, size, lock, cache, prot, qos, region, user, wuser) burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_words(self, address, data, byteorder='little', ws=2, awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
return await self.write_if.write_words(address, data, byteorder, ws, awid,
burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_dwords(self, address, data, byteorder='little', awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
return await self.write_if.write_dwords(address, data, byteorder, awid,
burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_qwords(self, address, data, byteorder='little', awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
return await self.write_if.write_qwords(address, data, byteorder, awid,
burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_byte(self, address, data, awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
return await self.write_if.write_byte(address, data, awid,
burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_word(self, address, data, byteorder='little', ws=2, awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
return await self.write_if.write_word(address, data, byteorder, ws, awid,
burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_dword(self, address, data, byteorder='little', awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
return await self.write_if.write_dword(address, data, byteorder, awid,
burst, size, lock, cache, prot, qos, region, user, wuser)
async def write_qword(self, address, data, byteorder='little', awid=None, burst=AxiBurstType.INCR, size=None,
lock=AxiLockType.NORMAL, cache=0b0011, prot=AxiProt.NONSECURE, qos=0, region=0, user=0, wuser=0):
return await self.write_if.write_qword(address, data, byteorder, awid,
burst, size, lock, cache, prot, qos, region, user, wuser)

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2021-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -22,278 +22,32 @@ THE SOFTWARE.
""" """
import logging from .axi_slave import AxiSlaveWrite, AxiSlaveRead
import cocotb
from .version import __version__
from .constants import AxiBurstType, AxiProt, AxiResp
from .axi_channels import AxiAWSink, AxiWSink, AxiBSource, AxiARSink, AxiRSource
from .memory import Memory from .memory import Memory
from .reset import Reset
class AxiRamWrite(Memory, Reset): class AxiRamWrite(AxiSlaveWrite, Memory):
def __init__(self, bus, clock, reset=None, reset_active_level=True, size=1024, mem=None, *args, **kwargs): def __init__(self, bus, clock, reset=None, reset_active_level=True, size=2**64, mem=None, **kwargs):
self.bus = bus super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
self.clock = clock
self.reset = reset
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
self.log.info("AXI RAM model (write)") async def _write(self, address, data):
self.log.info("cocotbext-axi version %s", __version__) self.write(address % self.size, data)
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(size, mem, *args, **kwargs)
self.aw_channel = AxiAWSink(bus.aw, clock, reset, reset_active_level) class AxiRamRead(AxiSlaveRead, Memory):
self.aw_channel.queue_occupancy_limit = 2 def __init__(self, bus, clock, reset=None, reset_active_level=True, size=2**64, mem=None, **kwargs):
self.w_channel = AxiWSink(bus.w, clock, reset, reset_active_level) super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
self.w_channel.queue_occupancy_limit = 2
self.b_channel = AxiBSource(bus.b, clock, reset, reset_active_level)
self.b_channel.queue_occupancy_limit = 2
self.width = len(self.w_channel.bus.wdata) async def _read(self, address, length):
self.byte_size = 8 return self.read(address % self.size, length)
self.byte_lanes = self.width // self.byte_size
self.strb_mask = 2**self.byte_lanes-1
self.log.info("AXI RAM model configuration:")
self.log.info(" Memory size: %d bytes", len(self.mem))
self.log.info(" Address width: %d bits", len(self.aw_channel.bus.awaddr))
self.log.info(" ID width: %d bits", len(self.aw_channel.bus.awid))
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("AXI RAM model signals:")
for bus in (self.bus.aw, self.bus.w, self.bus.b):
for sig in sorted(list(set().union(bus._signals, bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
assert self.byte_lanes == len(self.w_channel.bus.wstrb)
assert self.byte_lanes * self.byte_size == self.width
assert len(self.b_channel.bus.bid) == len(self.aw_channel.bus.awid)
self._process_write_cr = None
self._init_reset(reset, reset_active_level)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._process_write_cr is not None:
self._process_write_cr.kill()
self._process_write_cr = None
self.aw_channel.clear()
self.w_channel.clear()
self.b_channel.clear()
else:
self.log.info("Reset de-asserted")
if self._process_write_cr is None:
self._process_write_cr = cocotb.fork(self._process_write())
async def _process_write(self):
while True:
aw = await self.aw_channel.recv()
awid = int(aw.awid)
addr = int(aw.awaddr)
length = int(aw.awlen)
size = int(aw.awsize)
burst = int(aw.awburst)
prot = AxiProt(int(aw.awprot))
self.log.info("Write burst awid: 0x%x awaddr: 0x%08x awlen: %d awsize: %d awprot: %s",
awid, addr, length, size, prot)
num_bytes = 2**size
assert 0 < num_bytes <= self.byte_lanes
aligned_addr = (addr // num_bytes) * num_bytes
length += 1
transfer_size = num_bytes*length
if burst == AxiBurstType.WRAP:
lower_wrap_boundary = (addr // transfer_size) * transfer_size
upper_wrap_boundary = lower_wrap_boundary + transfer_size
if burst == AxiBurstType.INCR:
# check 4k boundary crossing
assert 0x1000-(aligned_addr & 0xfff) >= transfer_size
cur_addr = aligned_addr
for n in range(length):
cur_word_addr = (cur_addr // self.byte_lanes) * self.byte_lanes
w = await self.w_channel.recv()
data = int(w.wdata)
strb = int(w.wstrb)
last = int(w.wlast)
# todo latency
self.mem.seek(cur_word_addr % self.size)
data = data.to_bytes(self.byte_lanes, 'little')
self.log.debug("Write word awid: 0x%x addr: 0x%08x wstrb: 0x%02x data: %s",
awid, cur_addr, strb, ' '.join((f'{c:02x}' for c in data)))
for i in range(self.byte_lanes):
if strb & (1 << i):
self.mem.write(data[i:i+1])
else:
self.mem.seek(1, 1)
assert last == (n == length-1)
if burst != AxiBurstType.FIXED:
cur_addr += num_bytes
if burst == AxiBurstType.WRAP:
if cur_addr == upper_wrap_boundary:
cur_addr = lower_wrap_boundary
b = self.b_channel._transaction_obj()
b.bid = awid
b.bresp = AxiResp.OKAY
await self.b_channel.send(b)
class AxiRamRead(Memory, Reset):
def __init__(self, bus, clock, reset=None, reset_active_level=True, size=1024, mem=None, *args, **kwargs):
self.bus = bus
self.clock = clock
self.reset = reset
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
self.log.info("AXI RAM model (read)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(size, mem, *args, **kwargs)
self.ar_channel = AxiARSink(bus.ar, clock, reset, reset_active_level)
self.ar_channel.queue_occupancy_limit = 2
self.r_channel = AxiRSource(bus.r, clock, reset, reset_active_level)
self.r_channel.queue_occupancy_limit = 2
self.width = len(self.r_channel.bus.rdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.log.info("AXI RAM model configuration:")
self.log.info(" Memory size: %d bytes", len(self.mem))
self.log.info(" Address width: %d bits", len(self.ar_channel.bus.araddr))
self.log.info(" ID width: %d bits", len(self.ar_channel.bus.arid))
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("AXI RAM model signals:")
for bus in (self.bus.ar, self.bus.r):
for sig in sorted(list(set().union(bus._signals, bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
assert self.byte_lanes * self.byte_size == self.width
assert len(self.r_channel.bus.rid) == len(self.ar_channel.bus.arid)
self._process_read_cr = None
self._init_reset(reset, reset_active_level)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._process_read_cr is not None:
self._process_read_cr.kill()
self._process_read_cr = None
self.ar_channel.clear()
self.r_channel.clear()
else:
self.log.info("Reset de-asserted")
if self._process_read_cr is None:
self._process_read_cr = cocotb.fork(self._process_read())
async def _process_read(self):
while True:
ar = await self.ar_channel.recv()
arid = int(ar.arid)
addr = int(ar.araddr)
length = int(ar.arlen)
size = int(ar.arsize)
burst = int(ar.arburst)
prot = AxiProt(ar.arprot)
self.log.info("Read burst arid: 0x%x araddr: 0x%08x arlen: %d arsize: %d arprot: %s",
arid, addr, length, size, prot)
num_bytes = 2**size
assert 0 < num_bytes <= self.byte_lanes
aligned_addr = (addr // num_bytes) * num_bytes
length += 1
transfer_size = num_bytes*length
if burst == AxiBurstType.WRAP:
lower_wrap_boundary = (addr // transfer_size) * transfer_size
upper_wrap_boundary = lower_wrap_boundary + transfer_size
if burst == AxiBurstType.INCR:
# check 4k boundary crossing
assert 0x1000-(aligned_addr & 0xfff) >= transfer_size
cur_addr = aligned_addr
for n in range(length):
cur_word_addr = (cur_addr // self.byte_lanes) * self.byte_lanes
self.mem.seek(cur_word_addr % self.size)
data = self.mem.read(self.byte_lanes)
r = self.r_channel._transaction_obj()
r.rid = arid
r.rdata = int.from_bytes(data, 'little')
r.rlast = n == length-1
r.rresp = AxiResp.OKAY
await self.r_channel.send(r)
self.log.debug("Read word awid: 0x%x addr: 0x%08x data: %s",
arid, cur_addr, ' '.join((f'{c:02x}' for c in data)))
if burst != AxiBurstType.FIXED:
cur_addr += num_bytes
if burst == AxiBurstType.WRAP:
if cur_addr == upper_wrap_boundary:
cur_addr = lower_wrap_boundary
class AxiRam(Memory): class AxiRam(Memory):
def __init__(self, bus, clock, reset=None, reset_active_level=True, size=1024, mem=None, *args, **kwargs): def __init__(self, bus, clock, reset=None, reset_active_level=True, size=2**64, mem=None, **kwargs):
self.write_if = None self.write_if = None
self.read_if = None self.read_if = None
super().__init__(size, mem, *args, **kwargs) super().__init__(size, mem, **kwargs)
self.write_if = AxiRamWrite(bus.write, clock, reset, reset_active_level, mem=self.mem) self.write_if = AxiRamWrite(bus.write, clock, reset, reset_active_level, mem=self.mem)
self.read_if = AxiRamRead(bus.read, clock, reset, reset_active_level, mem=self.mem) self.read_if = AxiRamRead(bus.read, clock, reset, reset_active_level, mem=self.mem)

347
cocotbext/axi/axi_slave.py Normal file
View File

@@ -0,0 +1,347 @@
"""
Copyright (c) 2021-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import logging
import cocotb
from .version import __version__
from .constants import AxiBurstType, AxiProt, AxiResp
from .axi_channels import AxiAWSink, AxiWSink, AxiBSource, AxiARSink, AxiRSource
from .reset import Reset
class AxiSlaveWrite(Reset):
def __init__(self, bus, clock, reset=None, target=None, reset_active_level=True, **kwargs):
self.bus = bus
self.clock = clock
self.reset = reset
self.target = target
if bus.aw._name:
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}")
self.log.info("AXI slave model (write)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2021-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(**kwargs)
self.aw_channel = AxiAWSink(bus.aw, clock, reset, reset_active_level)
self.aw_channel.queue_occupancy_limit = 2
self.w_channel = AxiWSink(bus.w, clock, reset, reset_active_level)
self.w_channel.queue_occupancy_limit = 2
self.b_channel = AxiBSource(bus.b, clock, reset, reset_active_level)
self.b_channel.queue_occupancy_limit = 2
self.address_width = len(self.aw_channel.bus.awaddr)
self.id_width = len(self.aw_channel.bus.awid)
self.width = len(self.w_channel.bus.wdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.strb_mask = 2**self.byte_lanes-1
self.max_burst_size = (self.byte_lanes-1).bit_length()
self.wstrb_present = hasattr(self.bus.w, "wstrb")
self.log.info("AXI slave model configuration:")
self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" ID width: %d bits", self.id_width)
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("AXI slave model signals:")
for bus in (self.bus.aw, self.bus.w, self.bus.b):
for sig in sorted(list(set().union(bus._signals, bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
if self.wstrb_present:
assert self.byte_lanes == len(self.w_channel.bus.wstrb)
assert self.byte_lanes * self.byte_size == self.width
assert len(self.b_channel.bus.bid) == len(self.aw_channel.bus.awid)
self._process_write_cr = None
self._init_reset(reset, reset_active_level)
async def _write(self, address, data):
await self.target.write(address, data)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._process_write_cr is not None:
self._process_write_cr.kill()
self._process_write_cr = None
self.aw_channel.clear()
self.w_channel.clear()
self.b_channel.clear()
else:
self.log.info("Reset de-asserted")
if self._process_write_cr is None:
self._process_write_cr = cocotb.start_soon(self._process_write())
async def _process_write(self):
while True:
aw = await self.aw_channel.recv()
awid = int(getattr(aw, 'awid', 0))
addr = int(aw.awaddr)
length = int(getattr(aw, 'awlen', 0))
size = int(getattr(aw, 'awsize', self.max_burst_size))
burst = AxiBurstType(int(getattr(aw, 'awburst', AxiBurstType.INCR)))
prot = AxiProt(int(getattr(aw, 'awprot', AxiProt.NONSECURE)))
self.log.info("Write burst awid: 0x%x awaddr: 0x%08x awlen: %d awsize: %d awprot: %s",
awid, addr, length, size, prot)
num_bytes = 2**size
assert 0 < num_bytes <= self.byte_lanes
aligned_addr = (addr // num_bytes) * num_bytes
length += 1
transfer_size = num_bytes*length
if burst == AxiBurstType.WRAP:
lower_wrap_boundary = (addr // transfer_size) * transfer_size
upper_wrap_boundary = lower_wrap_boundary + transfer_size
if burst == AxiBurstType.INCR:
# check 4k boundary crossing
assert 0x1000-(aligned_addr & 0xfff) >= transfer_size
cur_addr = aligned_addr
b = self.b_channel._transaction_obj()
b.bid = awid
b.bresp = AxiResp.OKAY
for n in range(length):
cur_word_addr = (cur_addr // self.byte_lanes) * self.byte_lanes
w = await self.w_channel.recv()
data = int(w.wdata)
if self.wstrb_present:
strb = int(getattr(w, 'wstrb', self.strb_mask))
else:
strb = self.strb_mask
last = int(w.wlast)
# generate operation list
offset = 0
start_offset = None
write_ops = []
data = data.to_bytes(self.byte_lanes, 'little')
if self.log.isEnabledFor(logging.DEBUG):
self.log.debug("Write word awid: 0x%x addr: 0x%08x wstrb: 0x%02x data: %s",
awid, cur_addr, strb, ' '.join((f'{c:02x}' for c in data)))
for i in range(self.byte_lanes):
if strb & (1 << i):
if start_offset is None:
start_offset = offset
else:
if start_offset is not None and offset != start_offset:
write_ops.append((cur_word_addr+start_offset, data[start_offset:offset]))
start_offset = None
offset += 1
if start_offset is not None and offset != start_offset:
write_ops.append((cur_word_addr+start_offset, data[start_offset:offset]))
# perform writes
try:
for addr, data in write_ops:
await self._write(addr, data)
except Exception:
self.log.warning("Write operation failed")
b.bresp = AxiResp.SLVERR
assert last == (n == length-1)
if burst != AxiBurstType.FIXED:
cur_addr += num_bytes
if burst == AxiBurstType.WRAP:
if cur_addr == upper_wrap_boundary:
cur_addr = lower_wrap_boundary
await self.b_channel.send(b)
class AxiSlaveRead(Reset):
def __init__(self, bus, clock, reset=None, target=None, reset_active_level=True, **kwargs):
self.bus = bus
self.clock = clock
self.reset = reset
self.target = target
if bus.ar._name:
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}")
self.log.info("AXI slave model (read)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2021-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(**kwargs)
self.ar_channel = AxiARSink(bus.ar, clock, reset, reset_active_level)
self.ar_channel.queue_occupancy_limit = 2
self.r_channel = AxiRSource(bus.r, clock, reset, reset_active_level)
self.r_channel.queue_occupancy_limit = 2
self.address_width = len(self.ar_channel.bus.araddr)
self.id_width = len(self.ar_channel.bus.arid)
self.width = len(self.r_channel.bus.rdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.max_burst_size = (self.byte_lanes-1).bit_length()
self.log.info("AXI slave model configuration:")
self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" ID width: %d bits", self.id_width)
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("AXI slave model signals:")
for bus in (self.bus.ar, self.bus.r):
for sig in sorted(list(set().union(bus._signals, bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
assert self.byte_lanes * self.byte_size == self.width
assert len(self.r_channel.bus.rid) == len(self.ar_channel.bus.arid)
self._process_read_cr = None
self._init_reset(reset, reset_active_level)
async def _read(self, address, length):
return await self.target.read(address, length)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._process_read_cr is not None:
self._process_read_cr.kill()
self._process_read_cr = None
self.ar_channel.clear()
self.r_channel.clear()
else:
self.log.info("Reset de-asserted")
if self._process_read_cr is None:
self._process_read_cr = cocotb.start_soon(self._process_read())
async def _process_read(self):
while True:
ar = await self.ar_channel.recv()
arid = int(getattr(ar, 'arid', 0))
addr = int(ar.araddr)
length = int(getattr(ar, 'arlen', 0))
size = int(getattr(ar, 'arsize', self.max_burst_size))
burst = AxiBurstType(int(getattr(ar, 'arburst', AxiBurstType.INCR)))
prot = AxiProt(int(getattr(ar, 'arprot', AxiProt.NONSECURE)))
self.log.info("Read burst arid: 0x%x araddr: 0x%08x arlen: %d arsize: %d arprot: %s",
arid, addr, length, size, prot)
num_bytes = 2**size
assert 0 < num_bytes <= self.byte_lanes
aligned_addr = (addr // num_bytes) * num_bytes
length += 1
transfer_size = num_bytes*length
if burst == AxiBurstType.WRAP:
lower_wrap_boundary = (addr // transfer_size) * transfer_size
upper_wrap_boundary = lower_wrap_boundary + transfer_size
if burst == AxiBurstType.INCR:
# check 4k boundary crossing
assert 0x1000-(aligned_addr & 0xfff) >= transfer_size
cur_addr = aligned_addr
for n in range(length):
cur_word_addr = (cur_addr // self.byte_lanes) * self.byte_lanes
r = self.r_channel._transaction_obj()
r.rid = arid
r.rlast = n == length-1
r.rresp = AxiResp.OKAY
try:
data = await self._read(cur_word_addr, self.byte_lanes)
except Exception:
self.log.warning("Read operation failed")
data = bytes(self.byte_lanes)
r.rresp = AxiResp.SLVERR
r.rdata = int.from_bytes(data, 'little')
await self.r_channel.send(r)
if self.log.isEnabledFor(logging.DEBUG):
self.log.debug("Read word awid: 0x%x addr: 0x%08x data: %s",
arid, cur_addr, ' '.join((f'{c:02x}' for c in data)))
if burst != AxiBurstType.FIXED:
cur_addr += num_bytes
if burst == AxiBurstType.WRAP:
if cur_addr == upper_wrap_boundary:
cur_addr = lower_wrap_boundary
class AxiSlave:
def __init__(self, bus, clock, reset=None, target=None, reset_active_level=True, **kwargs):
self.write_if = None
self.read_if = None
super().__init__(**kwargs)
self.write_if = AxiSlaveWrite(bus.write, clock, reset, target, reset_active_level)
self.read_if = AxiSlaveRead(bus.read, clock, reset, target, reset_active_level)

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -33,7 +33,8 @@ AxiLiteAWBus, AxiLiteAWTransaction, AxiLiteAWSource, AxiLiteAWSink, AxiLiteAWMon
# Write data channel # Write data channel
AxiLiteWBus, AxiLiteWTransaction, AxiLiteWSource, AxiLiteWSink, AxiLiteWMonitor = define_stream("AxiLiteW", AxiLiteWBus, AxiLiteWTransaction, AxiLiteWSource, AxiLiteWSink, AxiLiteWMonitor = define_stream("AxiLiteW",
signals=["wdata", "wstrb", "wvalid", "wready"] signals=["wdata", "wvalid", "wready"],
optional_signals=["wstrb"]
) )
# Write response channel # Write response channel

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -23,7 +23,7 @@ THE SOFTWARE.
""" """
import logging import logging
from collections import namedtuple from typing import NamedTuple
import cocotb import cocotb
from cocotb.queue import Queue from cocotb.queue import Queue
@@ -32,29 +32,70 @@ from cocotb.triggers import Event
from .version import __version__ from .version import __version__
from .constants import AxiProt, AxiResp from .constants import AxiProt, AxiResp
from .axil_channels import AxiLiteAWSource, AxiLiteWSource, AxiLiteBSink, AxiLiteARSource, AxiLiteRSink from .axil_channels import AxiLiteAWSource, AxiLiteWSource, AxiLiteBSink, AxiLiteARSource, AxiLiteRSink
from .address_space import Region
from .reset import Reset from .reset import Reset
# AXI lite master write
AxiLiteWriteCmd = namedtuple("AxiLiteWriteCmd", ["address", "data", "prot", "event"])
AxiLiteWriteRespCmd = namedtuple("AxiLiteWriteRespCmd", ["address", "length", "cycles", "prot", "event"])
AxiLiteWriteResp = namedtuple("AxiLiteWriteResp", ["address", "length", "resp"])
# AXI lite master read # AXI lite master write helper objects
AxiLiteReadCmd = namedtuple("AxiLiteReadCmd", ["address", "length", "prot", "event"]) class AxiLiteWriteCmd(NamedTuple):
AxiLiteReadRespCmd = namedtuple("AxiLiteReadRespCmd", ["address", "length", "cycles", "prot", "event"]) address: int
AxiLiteReadResp = namedtuple("AxiLiteReadResp", ["address", "data", "resp"]) data: bytes
prot: AxiProt
event: Event
class AxiLiteMasterWrite(Reset): class AxiLiteWriteRespCmd(NamedTuple):
def __init__(self, bus, clock, reset=None, reset_active_level=True): address: int
length: int
cycles: int
prot: AxiProt
event: Event
class AxiLiteWriteResp(NamedTuple):
address: int
length: int
resp: AxiResp
# AXI lite master read helper objects
class AxiLiteReadCmd(NamedTuple):
address: int
length: int
prot: AxiProt
event: Event
class AxiLiteReadRespCmd(NamedTuple):
address: int
length: int
cycles: int
prot: AxiProt
event: Event
class AxiLiteReadResp(NamedTuple):
address: int
data: bytes
resp: AxiResp
def __bytes__(self):
return self.data
class AxiLiteMasterWrite(Region, Reset):
def __init__(self, bus, clock, reset=None, reset_active_level=True, **kwargs):
self.bus = bus self.bus = bus
self.clock = clock self.clock = clock
self.reset = reset self.reset = reset
if bus.aw._name:
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}") self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}")
self.log.info("AXI lite master (write)") self.log.info("AXI lite master (write)")
self.log.info("cocotbext-axi version %s", __version__) self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich") self.log.info("Copyright (c) 2020-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi") self.log.info("https://github.com/alexforencich/cocotbext-axi")
self.aw_channel = AxiLiteAWSource(bus.aw, clock, reset, reset_active_level) self.aw_channel = AxiLiteAWSource(bus.aw, clock, reset, reset_active_level)
@@ -65,6 +106,7 @@ class AxiLiteMasterWrite(Reset):
self.b_channel.queue_occupancy_limit = 2 self.b_channel.queue_occupancy_limit = 2
self.write_command_queue = Queue() self.write_command_queue = Queue()
self.write_command_queue.queue_occupancy_limit = 2
self.current_write_command = None self.current_write_command = None
self.int_write_resp_command_queue = Queue() self.int_write_resp_command_queue = Queue()
@@ -74,15 +116,19 @@ class AxiLiteMasterWrite(Reset):
self._idle = Event() self._idle = Event()
self._idle.set() self._idle.set()
self.address_width = len(self.aw_channel.bus.awaddr)
self.width = len(self.w_channel.bus.wdata) self.width = len(self.w_channel.bus.wdata)
self.byte_size = 8 self.byte_size = 8
self.byte_lanes = self.width // self.byte_size self.byte_lanes = self.width // self.byte_size
self.strb_mask = 2**self.byte_lanes-1 self.strb_mask = 2**self.byte_lanes-1
self.awprot_present = hasattr(self.bus.aw, "awprot") self.awprot_present = hasattr(self.bus.aw, "awprot")
self.wstrb_present = hasattr(self.bus.w, "wstrb")
super().__init__(2**self.address_width, **kwargs)
self.log.info("AXI lite master configuration:") self.log.info("AXI lite master configuration:")
self.log.info(" Address width: %d bits", len(self.aw_channel.bus.awaddr)) self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" Byte size: %d bits", self.byte_size) self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes) self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
@@ -94,6 +140,7 @@ class AxiLiteMasterWrite(Reset):
else: else:
self.log.info(" %s: not present", sig) self.log.info(" %s: not present", sig)
if self.wstrb_present:
assert self.byte_lanes == len(self.w_channel.bus.wstrb) assert self.byte_lanes == len(self.w_channel.bus.wstrb)
assert self.byte_lanes * self.byte_size == self.width assert self.byte_lanes * self.byte_size == self.width
@@ -109,13 +156,21 @@ class AxiLiteMasterWrite(Reset):
if not isinstance(event, Event): if not isinstance(event, Event):
raise ValueError("Expected event object") raise ValueError("Expected event object")
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if isinstance(data, int):
raise ValueError("Expected bytes or bytearray for data")
if address+len(data) > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if not self.awprot_present and prot != AxiProt.NONSECURE: if not self.awprot_present and prot != AxiProt.NONSECURE:
raise ValueError("awprot sideband signal value specified, but signal is not connected") raise ValueError("awprot sideband signal value specified, but signal is not connected")
self.in_flight_operations += 1 data = bytes(data)
self._idle.clear()
self.write_command_queue.put_nowait(AxiLiteWriteCmd(address, bytearray(data), prot, event)) cocotb.start_soon(self._write_wrapper(address, bytes(data), prot, event))
return event return event
@@ -127,34 +182,30 @@ class AxiLiteMasterWrite(Reset):
await self._idle.wait() await self._idle.wait()
async def write(self, address, data, prot=AxiProt.NONSECURE): async def write(self, address, data, prot=AxiProt.NONSECURE):
event = self.init_write(address, data, prot) if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if isinstance(data, int):
raise ValueError("Expected bytes or bytearray for data")
if address+len(data) > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if not self.awprot_present and prot != AxiProt.NONSECURE:
raise ValueError("awprot sideband signal value specified, but signal is not connected")
event = Event()
data = bytes(data)
self.in_flight_operations += 1
self._idle.clear()
await self.write_command_queue.put(AxiLiteWriteCmd(address, data, prot, event))
await event.wait() await event.wait()
return event.data return event.data
async def write_words(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE): async def _write_wrapper(self, address, data, prot, event):
words = data event.set(await self.write(address, data, prot))
data = bytearray()
for w in words:
data.extend(w.to_bytes(ws, byteorder))
await self.write(address, data, prot)
async def write_dwords(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
await self.write_words(address, data, byteorder, 4, prot)
async def write_qwords(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
await self.write_words(address, data, byteorder, 8, prot)
async def write_byte(self, address, data, prot=AxiProt.NONSECURE):
await self.write(address, [data], prot)
async def write_word(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
await self.write_words(address, [data], byteorder, ws, prot)
async def write_dword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
await self.write_dwords(address, [data], byteorder, prot)
async def write_qword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
await self.write_qwords(address, [data], byteorder, prot)
def _handle_reset(self, state): def _handle_reset(self, state):
if state: if state:
@@ -198,9 +249,9 @@ class AxiLiteMasterWrite(Reset):
else: else:
self.log.info("Reset de-asserted") self.log.info("Reset de-asserted")
if self._process_write_cr is None: if self._process_write_cr is None:
self._process_write_cr = cocotb.fork(self._process_write()) self._process_write_cr = cocotb.start_soon(self._process_write())
if self._process_write_resp_cr is None: if self._process_write_resp_cr is None:
self._process_write_resp_cr = cocotb.fork(self._process_write_resp()) self._process_write_resp_cr = cocotb.start_soon(self._process_write_resp())
async def _process_write(self): async def _process_write(self):
while True: while True:
@@ -222,6 +273,7 @@ class AxiLiteMasterWrite(Reset):
offset = 0 offset = 0
if self.log.isEnabledFor(logging.INFO):
self.log.info("Write start addr: 0x%08x prot: %s data: %s", self.log.info("Write start addr: 0x%08x prot: %s data: %s",
cmd.address, cmd.prot, ' '.join((f'{c:02x}' for c in cmd.data))) cmd.address, cmd.prot, ' '.join((f'{c:02x}' for c in cmd.data)))
@@ -243,9 +295,15 @@ class AxiLiteMasterWrite(Reset):
offset += 1 offset += 1
aw = self.aw_channel._transaction_obj() aw = self.aw_channel._transaction_obj()
if k == 0:
aw.awaddr = cmd.address
else:
aw.awaddr = word_addr + k*self.byte_lanes aw.awaddr = word_addr + k*self.byte_lanes
aw.awprot = cmd.prot aw.awprot = cmd.prot
if not self.wstrb_present and strb != self.strb_mask:
self.log.warning("Partial operation requested with wstrb not connected, write will be zero-padded (0x%x != 0x%x)", strb, self.strb_mask)
w = self.w_channel._transaction_obj() w = self.w_channel._transaction_obj()
w.wdata = val w.wdata = val
w.wstrb = strb w.wstrb = strb
@@ -265,7 +323,7 @@ class AxiLiteMasterWrite(Reset):
for k in range(cmd.cycles): for k in range(cmd.cycles):
b = await self.b_channel.recv() b = await self.b_channel.recv()
cycle_resp = AxiResp(b.bresp) cycle_resp = AxiResp(int(getattr(b, 'bresp', AxiResp.OKAY)))
if cycle_resp != AxiResp.OKAY: if cycle_resp != AxiResp.OKAY:
resp = cycle_resp resp = cycle_resp
@@ -285,16 +343,19 @@ class AxiLiteMasterWrite(Reset):
self._idle.set() self._idle.set()
class AxiLiteMasterRead(Reset): class AxiLiteMasterRead(Region, Reset):
def __init__(self, bus, clock, reset=None, reset_active_level=True): def __init__(self, bus, clock, reset=None, reset_active_level=True, **kwargs):
self.bus = bus self.bus = bus
self.clock = clock self.clock = clock
self.reset = reset self.reset = reset
if bus.ar._name:
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}") self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}")
self.log.info("AXI lite master (read)") self.log.info("AXI lite master (read)")
self.log.info("cocotbext-axi version %s", __version__) self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich") self.log.info("Copyright (c) 2020-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi") self.log.info("https://github.com/alexforencich/cocotbext-axi")
self.ar_channel = AxiLiteARSource(bus.ar, clock, reset, reset_active_level) self.ar_channel = AxiLiteARSource(bus.ar, clock, reset, reset_active_level)
@@ -303,6 +364,7 @@ class AxiLiteMasterRead(Reset):
self.r_channel.queue_occupancy_limit = 2 self.r_channel.queue_occupancy_limit = 2
self.read_command_queue = Queue() self.read_command_queue = Queue()
self.read_command_queue.queue_occupancy_limit = 2
self.current_read_command = None self.current_read_command = None
self.int_read_resp_command_queue = Queue() self.int_read_resp_command_queue = Queue()
@@ -312,14 +374,17 @@ class AxiLiteMasterRead(Reset):
self._idle = Event() self._idle = Event()
self._idle.set() self._idle.set()
self.address_width = len(self.ar_channel.bus.araddr)
self.width = len(self.r_channel.bus.rdata) self.width = len(self.r_channel.bus.rdata)
self.byte_size = 8 self.byte_size = 8
self.byte_lanes = self.width // self.byte_size self.byte_lanes = self.width // self.byte_size
self.arprot_present = hasattr(self.bus.ar, "arprot") self.arprot_present = hasattr(self.bus.ar, "arprot")
super().__init__(2**self.address_width, **kwargs)
self.log.info("AXI lite master configuration:") self.log.info("AXI lite master configuration:")
self.log.info(" Address width: %d bits", len(self.ar_channel.bus.araddr)) self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" Byte size: %d bits", self.byte_size) self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes) self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
@@ -345,13 +410,19 @@ class AxiLiteMasterRead(Reset):
if not isinstance(event, Event): if not isinstance(event, Event):
raise ValueError("Expected event object") raise ValueError("Expected event object")
if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if length < 0:
raise ValueError("Read length must be positive")
if address+length > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if not self.arprot_present and prot != AxiProt.NONSECURE: if not self.arprot_present and prot != AxiProt.NONSECURE:
raise ValueError("arprot sideband signal value specified, but signal is not connected") raise ValueError("arprot sideband signal value specified, but signal is not connected")
self.in_flight_operations += 1 cocotb.start_soon(self._read_wrapper(address, length, prot, event))
self._idle.clear()
self.read_command_queue.put_nowait(AxiLiteReadCmd(address, length, prot, event))
return event return event
@@ -363,34 +434,30 @@ class AxiLiteMasterRead(Reset):
await self._idle.wait() await self._idle.wait()
async def read(self, address, length, prot=AxiProt.NONSECURE): async def read(self, address, length, prot=AxiProt.NONSECURE):
event = self.init_read(address, length, prot) if address < 0 or address >= 2**self.address_width:
raise ValueError("Address out of range")
if length < 0:
raise ValueError("Read length must be positive")
if address+length > 2**self.address_width:
raise ValueError("Requested transfer overruns end of address space")
if not self.arprot_present and prot != AxiProt.NONSECURE:
raise ValueError("arprot sideband signal value specified, but signal is not connected")
event = Event()
self.in_flight_operations += 1
self._idle.clear()
await self.read_command_queue.put(AxiLiteReadCmd(address, length, prot, event))
await event.wait() await event.wait()
return event.data return event.data
async def read_words(self, address, count, byteorder='little', ws=2, prot=AxiProt.NONSECURE): async def _read_wrapper(self, address, length, prot, event):
data = await self.read(address, count*ws, prot) event.set(await self.read(address, length, prot))
words = []
for k in range(count):
words.append(int.from_bytes(data.data[ws*k:ws*(k+1)], byteorder))
return words
async def read_dwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_words(address, count, byteorder, 4, prot)
async def read_qwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_words(address, count, byteorder, 8, prot)
async def read_byte(self, address, prot=AxiProt.NONSECURE):
return (await self.read(address, 1, prot)).data[0]
async def read_word(self, address, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return (await self.read_words(address, 1, byteorder, ws, prot))[0]
async def read_dword(self, address, byteorder='little', prot=AxiProt.NONSECURE):
return (await self.read_dwords(address, 1, byteorder, prot))[0]
async def read_qword(self, address, byteorder='little', prot=AxiProt.NONSECURE):
return (await self.read_qwords(address, 1, byteorder, prot))[0]
def _handle_reset(self, state): def _handle_reset(self, state):
if state: if state:
@@ -433,9 +500,9 @@ class AxiLiteMasterRead(Reset):
else: else:
self.log.info("Reset de-asserted") self.log.info("Reset de-asserted")
if self._process_read_cr is None: if self._process_read_cr is None:
self._process_read_cr = cocotb.fork(self._process_read()) self._process_read_cr = cocotb.start_soon(self._process_read())
if self._process_read_resp_cr is None: if self._process_read_resp_cr is None:
self._process_read_resp_cr = cocotb.fork(self._process_read_resp()) self._process_read_resp_cr = cocotb.start_soon(self._process_read_resp())
async def _process_read(self): async def _process_read(self):
while True: while True:
@@ -454,6 +521,9 @@ class AxiLiteMasterRead(Reset):
for k in range(cycles): for k in range(cycles):
ar = self.ar_channel._transaction_obj() ar = self.ar_channel._transaction_obj()
if k == 0:
ar.araddr = cmd.address
else:
ar.araddr = word_addr + k*self.byte_lanes ar.araddr = word_addr + k*self.byte_lanes
ar.arprot = cmd.prot ar.arprot = cmd.prot
@@ -477,7 +547,7 @@ class AxiLiteMasterRead(Reset):
r = await self.r_channel.recv() r = await self.r_channel.recv()
cycle_data = int(r.rdata) cycle_data = int(r.rdata)
cycle_resp = AxiResp(r.rresp) cycle_resp = AxiResp(int(getattr(r, 'rresp', AxiResp.OKAY)))
if cycle_resp != AxiResp.OKAY: if cycle_resp != AxiResp.OKAY:
resp = cycle_resp resp = cycle_resp
@@ -491,12 +561,13 @@ class AxiLiteMasterRead(Reset):
stop = end_offset stop = end_offset
for j in range(start, stop): for j in range(start, stop):
data.extend(bytearray([(cycle_data >> j*8) & 0xff])) data.append((cycle_data >> j*8) & 0xff)
if self.log.isEnabledFor(logging.INFO):
self.log.info("Read complete addr: 0x%08x prot: %s resp: %s data: %s", self.log.info("Read complete addr: 0x%08x prot: %s resp: %s data: %s",
cmd.address, cmd.prot, resp, ' '.join((f'{c:02x}' for c in data))) cmd.address, cmd.prot, resp, ' '.join((f'{c:02x}' for c in data)))
read_resp = AxiLiteReadResp(cmd.address, data, resp) read_resp = AxiLiteReadResp(cmd.address, bytes(data), resp)
cmd.event.set(read_resp) cmd.event.set(read_resp)
@@ -508,13 +579,15 @@ class AxiLiteMasterRead(Reset):
self._idle.set() self._idle.set()
class AxiLiteMaster: class AxiLiteMaster(Region):
def __init__(self, bus, clock, reset=None, reset_active_level=True): def __init__(self, bus, clock, reset=None, reset_active_level=True, **kwargs):
self.write_if = None self.write_if = None
self.read_if = None self.read_if = None
self.write_if = AxiLiteMasterWrite(bus.write, clock, reset, reset_active_level) self.write_if = AxiLiteMasterWrite(bus.write, clock, reset, reset_active_level, **kwargs)
self.read_if = AxiLiteMasterRead(bus.read, clock, reset, reset_active_level) self.read_if = AxiLiteMasterRead(bus.read, clock, reset, reset_active_level, **kwargs)
super().__init__(max(self.write_if.size, self.read_if.size), **kwargs)
def init_read(self, address, length, prot=AxiProt.NONSECURE, event=None): def init_read(self, address, length, prot=AxiProt.NONSECURE, event=None):
return self.read_if.init_read(address, length, prot, event) return self.read_if.init_read(address, length, prot, event)
@@ -539,47 +612,5 @@ class AxiLiteMaster:
async def read(self, address, length, prot=AxiProt.NONSECURE): async def read(self, address, length, prot=AxiProt.NONSECURE):
return await self.read_if.read(address, length, prot) return await self.read_if.read(address, length, prot)
async def read_words(self, address, count, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return await self.read_if.read_words(address, count, byteorder, ws, prot)
async def read_dwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_if.read_dwords(address, count, byteorder, prot)
async def read_qwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_if.read_qwords(address, count, byteorder, prot)
async def read_byte(self, address, prot=AxiProt.NONSECURE):
return await self.read_if.read_byte(address, prot)
async def read_word(self, address, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return await self.read_if.read_word(address, byteorder, ws, prot)
async def read_dword(self, address, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_if.read_dword(address, byteorder, prot)
async def read_qword(self, address, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_if.read_qword(address, byteorder, prot)
async def write(self, address, data, prot=AxiProt.NONSECURE): async def write(self, address, data, prot=AxiProt.NONSECURE):
return await self.write_if.write(address, data, prot) return await self.write_if.write(address, data, prot)
async def write_words(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return await self.write_if.write_words(address, data, byteorder, ws, prot)
async def write_dwords(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
return await self.write_if.write_dwords(address, data, byteorder, prot)
async def write_qwords(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
return await self.write_if.write_qwords(address, data, byteorder, prot)
async def write_byte(self, address, data, prot=AxiProt.NONSECURE):
return await self.write_if.write_byte(address, data, prot)
async def write_word(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return await self.write_if.write_word(address, data, byteorder, ws, prot)
async def write_dword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
return await self.write_if.write_dword(address, data, byteorder, prot)
async def write_qword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
return await self.write_if.write_qword(address, data, byteorder, prot)

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2021-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -22,198 +22,32 @@ THE SOFTWARE.
""" """
import logging from .axil_slave import AxiLiteSlaveWrite, AxiLiteSlaveRead
import cocotb
from .version import __version__
from .constants import AxiProt, AxiResp
from .axil_channels import AxiLiteAWSink, AxiLiteWSink, AxiLiteBSource, AxiLiteARSink, AxiLiteRSource
from .memory import Memory from .memory import Memory
from .reset import Reset
class AxiLiteRamWrite(Memory, Reset): class AxiLiteRamWrite(AxiLiteSlaveWrite, Memory):
def __init__(self, bus, clock, reset=None, reset_active_level=True, size=1024, mem=None, *args, **kwargs): def __init__(self, bus, clock, reset=None, reset_active_level=True, size=2**64, mem=None, **kwargs):
self.bus = bus super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
self.clock = clock
self.reset = reset
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
self.log.info("AXI lite RAM model (write)") async def _write(self, address, data):
self.log.info("cocotbext-axi version %s", __version__) self.write(address % self.size, data)
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(size, mem, *args, **kwargs)
self.aw_channel = AxiLiteAWSink(bus.aw, clock, reset, reset_active_level)
self.aw_channel.queue_occupancy_limit = 2
self.w_channel = AxiLiteWSink(bus.w, clock, reset, reset_active_level)
self.w_channel.queue_occupancy_limit = 2
self.b_channel = AxiLiteBSource(bus.b, clock, reset, reset_active_level)
self.b_channel.queue_occupancy_limit = 2
self.width = len(self.w_channel.bus.wdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.strb_mask = 2**self.byte_lanes-1
self.log.info("AXI lite RAM model configuration:")
self.log.info(" Memory size: %d bytes", len(self.mem))
self.log.info(" Address width: %d bits", len(self.aw_channel.bus.awaddr))
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("AXI lite RAM model signals:")
for bus in (self.bus.aw, self.bus.w, self.bus.b):
for sig in sorted(list(set().union(bus._signals, bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
assert self.byte_lanes == len(self.w_channel.bus.wstrb)
assert self.byte_lanes * self.byte_size == self.width
self._process_write_cr = None
self._init_reset(reset, reset_active_level)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._process_write_cr is not None:
self._process_write_cr.kill()
self._process_write_cr = None
self.aw_channel.clear()
self.w_channel.clear()
self.b_channel.clear()
else:
self.log.info("Reset de-asserted")
if self._process_write_cr is None:
self._process_write_cr = cocotb.fork(self._process_write())
async def _process_write(self):
while True:
aw = await self.aw_channel.recv()
addr = (int(aw.awaddr) // self.byte_lanes) * self.byte_lanes
prot = AxiProt(aw.awprot)
w = await self.w_channel.recv()
data = int(w.wdata)
strb = int(w.wstrb)
# todo latency
self.mem.seek(addr % self.size)
data = data.to_bytes(self.byte_lanes, 'little')
self.log.info("Write data awaddr: 0x%08x awprot: %s wstrb: 0x%02x data: %s",
addr, prot, strb, ' '.join((f'{c:02x}' for c in data)))
for i in range(self.byte_lanes):
if strb & (1 << i):
self.mem.write(data[i:i+1])
else:
self.mem.seek(1, 1)
b = self.b_channel._transaction_obj()
b.bresp = AxiResp.OKAY
await self.b_channel.send(b)
class AxiLiteRamRead(Memory, Reset): class AxiLiteRamRead(AxiLiteSlaveRead, Memory):
def __init__(self, bus, clock, reset=None, reset_active_level=True, size=1024, mem=None, *args, **kwargs): def __init__(self, bus, clock, reset=None, reset_active_level=True, size=2**64, mem=None, **kwargs):
self.bus = bus super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
self.clock = clock
self.reset = reset
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
self.log.info("AXI lite RAM model (read)") async def _read(self, address, length):
self.log.info("cocotbext-axi version %s", __version__) return self.read(address % self.size, length)
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(size, mem, *args, **kwargs)
self.ar_channel = AxiLiteARSink(bus.ar, clock, reset, reset_active_level)
self.ar_channel.queue_occupancy_limit = 2
self.r_channel = AxiLiteRSource(bus.r, clock, reset, reset_active_level)
self.r_channel.queue_occupancy_limit = 2
self.width = len(self.r_channel.bus.rdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.log.info("AXI lite RAM model configuration:")
self.log.info(" Memory size: %d bytes", len(self.mem))
self.log.info(" Address width: %d bits", len(self.ar_channel.bus.araddr))
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("AXI lite RAM model signals:")
for bus in (self.bus.ar, self.bus.r):
for sig in sorted(list(set().union(bus._signals, bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
assert self.byte_lanes * self.byte_size == self.width
self._process_read_cr = None
self._init_reset(reset, reset_active_level)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._process_read_cr is not None:
self._process_read_cr.kill()
self._process_read_cr = None
self.ar_channel.clear()
self.r_channel.clear()
else:
self.log.info("Reset de-asserted")
if self._process_read_cr is None:
self._process_read_cr = cocotb.fork(self._process_read())
async def _process_read(self):
while True:
ar = await self.ar_channel.recv()
addr = (int(ar.araddr) // self.byte_lanes) * self.byte_lanes
prot = AxiProt(ar.arprot)
# todo latency
self.mem.seek(addr % self.size)
data = self.mem.read(self.byte_lanes)
r = self.r_channel._transaction_obj()
r.rdata = int.from_bytes(data, 'little')
r.rresp = AxiResp.OKAY
await self.r_channel.send(r)
self.log.info("Read data araddr: 0x%08x arprot: %s data: %s",
addr, prot, ' '.join((f'{c:02x}' for c in data)))
class AxiLiteRam(Memory): class AxiLiteRam(Memory):
def __init__(self, bus, clock, reset=None, reset_active_level=True, size=1024, mem=None, *args, **kwargs): def __init__(self, bus, clock, reset=None, reset_active_level=True, size=2**64, mem=None, **kwargs):
self.write_if = None self.write_if = None
self.read_if = None self.read_if = None
super().__init__(size, mem, *args, **kwargs) super().__init__(size, mem, **kwargs)
self.write_if = AxiLiteRamWrite(bus.write, clock, reset, reset_active_level, mem=self.mem) self.write_if = AxiLiteRamWrite(bus.write, clock, reset, reset_active_level, mem=self.mem)
self.read_if = AxiLiteRamRead(bus.read, clock, reset, reset_active_level, mem=self.mem) self.read_if = AxiLiteRamRead(bus.read, clock, reset, reset_active_level, mem=self.mem)

259
cocotbext/axi/axil_slave.py Normal file
View File

@@ -0,0 +1,259 @@
"""
Copyright (c) 2021-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import logging
import cocotb
from .version import __version__
from .constants import AxiProt, AxiResp
from .axil_channels import AxiLiteAWSink, AxiLiteWSink, AxiLiteBSource, AxiLiteARSink, AxiLiteRSource
from .reset import Reset
class AxiLiteSlaveWrite(Reset):
def __init__(self, bus, clock, reset=None, target=None, reset_active_level=True, **kwargs):
self.bus = bus
self.clock = clock
self.reset = reset
self.target = target
if bus.aw._name:
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}")
self.log.info("AXI lite slave model (write)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2021-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(**kwargs)
self.aw_channel = AxiLiteAWSink(bus.aw, clock, reset, reset_active_level)
self.aw_channel.queue_occupancy_limit = 2
self.w_channel = AxiLiteWSink(bus.w, clock, reset, reset_active_level)
self.w_channel.queue_occupancy_limit = 2
self.b_channel = AxiLiteBSource(bus.b, clock, reset, reset_active_level)
self.b_channel.queue_occupancy_limit = 2
self.address_width = len(self.aw_channel.bus.awaddr)
self.width = len(self.w_channel.bus.wdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.strb_mask = 2**self.byte_lanes-1
self.wstrb_present = hasattr(self.bus.w, "wstrb")
self.log.info("AXI lite slave model configuration:")
self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("AXI lite slave model signals:")
for bus in (self.bus.aw, self.bus.w, self.bus.b):
for sig in sorted(list(set().union(bus._signals, bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
if self.wstrb_present:
assert self.byte_lanes == len(self.w_channel.bus.wstrb)
assert self.byte_lanes * self.byte_size == self.width
self._process_write_cr = None
self._init_reset(reset, reset_active_level)
async def _write(self, address, data):
await self.target.write(address, data)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._process_write_cr is not None:
self._process_write_cr.kill()
self._process_write_cr = None
self.aw_channel.clear()
self.w_channel.clear()
self.b_channel.clear()
else:
self.log.info("Reset de-asserted")
if self._process_write_cr is None:
self._process_write_cr = cocotb.start_soon(self._process_write())
async def _process_write(self):
while True:
aw = await self.aw_channel.recv()
addr = (int(aw.awaddr) // self.byte_lanes) * self.byte_lanes
prot = AxiProt(int(getattr(aw, 'awprot', AxiProt.NONSECURE)))
w = await self.w_channel.recv()
data = int(w.wdata)
if self.wstrb_present:
strb = int(getattr(w, 'wstrb', self.strb_mask))
else:
strb = self.strb_mask
# generate operation list
offset = 0
start_offset = None
write_ops = []
data = data.to_bytes(self.byte_lanes, 'little')
b = self.b_channel._transaction_obj()
b.bresp = AxiResp.OKAY
if self.log.isEnabledFor(logging.INFO):
self.log.info("Write data awaddr: 0x%08x awprot: %s wstrb: 0x%02x data: %s",
addr, prot, strb, ' '.join((f'{c:02x}' for c in data)))
for i in range(self.byte_lanes):
if strb & (1 << i):
if start_offset is None:
start_offset = offset
else:
if start_offset is not None and offset != start_offset:
write_ops.append((addr+start_offset, data[start_offset:offset]))
start_offset = None
offset += 1
if start_offset is not None and offset != start_offset:
write_ops.append((addr+start_offset, data[start_offset:offset]))
# perform writes
try:
for addr, data in write_ops:
await self._write(addr, data)
except Exception:
self.log.warning("Write operation failed")
b.bresp = AxiResp.SLVERR
await self.b_channel.send(b)
class AxiLiteSlaveRead(Reset):
def __init__(self, bus, clock, reset=None, target=None, reset_active_level=True, **kwargs):
self.bus = bus
self.clock = clock
self.reset = reset
self.target = target
if bus.ar._name:
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}")
self.log.info("AXI lite slave model (read)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2021-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(**kwargs)
self.ar_channel = AxiLiteARSink(bus.ar, clock, reset, reset_active_level)
self.ar_channel.queue_occupancy_limit = 2
self.r_channel = AxiLiteRSource(bus.r, clock, reset, reset_active_level)
self.r_channel.queue_occupancy_limit = 2
self.address_width = len(self.ar_channel.bus.araddr)
self.width = len(self.r_channel.bus.rdata)
self.byte_size = 8
self.byte_lanes = self.width // self.byte_size
self.log.info("AXI lite slave model configuration:")
self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info("AXI lite slave model signals:")
for bus in (self.bus.ar, self.bus.r):
for sig in sorted(list(set().union(bus._signals, bus._optional_signals))):
if hasattr(bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(bus, sig)))
else:
self.log.info(" %s: not present", sig)
assert self.byte_lanes * self.byte_size == self.width
self._process_read_cr = None
self._init_reset(reset, reset_active_level)
async def _read(self, address, length):
return await self.target.read(address, length)
def _handle_reset(self, state):
if state:
self.log.info("Reset asserted")
if self._process_read_cr is not None:
self._process_read_cr.kill()
self._process_read_cr = None
self.ar_channel.clear()
self.r_channel.clear()
else:
self.log.info("Reset de-asserted")
if self._process_read_cr is None:
self._process_read_cr = cocotb.start_soon(self._process_read())
async def _process_read(self):
while True:
ar = await self.ar_channel.recv()
addr = (int(ar.araddr) // self.byte_lanes) * self.byte_lanes
prot = AxiProt(int(getattr(ar, 'arprot', AxiProt.NONSECURE)))
r = self.r_channel._transaction_obj()
r.rresp = AxiResp.OKAY
try:
data = await self._read(addr, self.byte_lanes)
except Exception:
self.log.warning("Read operation failed")
data = bytes(self.byte_lanes)
r.rresp = AxiResp.SLVERR
r.rdata = int.from_bytes(data, 'little')
await self.r_channel.send(r)
if self.log.isEnabledFor(logging.INFO):
self.log.info("Read data araddr: 0x%08x arprot: %s data: %s",
addr, prot, ' '.join((f'{c:02x}' for c in data)))
class AxiLiteSlave:
def __init__(self, bus, clock, reset=None, target=None, reset_active_level=True, **kwargs):
self.write_if = None
self.read_if = None
super().__init__(**kwargs)
self.write_if = AxiLiteSlaveWrite(bus.write, clock, reset, target, reset_active_level)
self.read_if = AxiLiteSlaveRead(bus.read, clock, reset, target, reset_active_level)

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -30,6 +30,11 @@ from cocotb.triggers import RisingEdge, Timer, First, Event
from cocotb.utils import get_sim_time from cocotb.utils import get_sim_time
from cocotb_bus.bus import Bus from cocotb_bus.bus import Bus
try:
from cocotb.types import LogicArray
except ImportError:
pass
from .version import __version__ from .version import __version__
from .reset import Reset from .reset import Reset
@@ -266,11 +271,14 @@ class AxiStreamBase(Reset):
self.bus = bus self.bus = bus
self.clock = clock self.clock = clock
self.reset = reset self.reset = reset
if bus._name:
self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}") self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}")
else:
self.log = logging.getLogger(f"cocotb.{bus._entity._name}")
self.log.info("AXI stream %s", self._type) self.log.info("AXI stream %s", self._type)
self.log.info("cocotbext-axi version %s", __version__) self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich") self.log.info("Copyright (c) 2020-2025 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi") self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -282,12 +290,13 @@ class AxiStreamBase(Reset):
self.idle_event = Event() self.idle_event = Event()
self.idle_event.set() self.idle_event.set()
self.active_event = Event() self.active_event = Event()
self.wake_event = Event()
self.queue_occupancy_bytes = 0 self.queue_occupancy_bytes = 0
self.queue_occupancy_frames = 0 self.queue_occupancy_frames = 0
self.width = len(self.bus.tdata) self.width = len(self.bus.tdata)
self.byte_lanes = 1 self.byte_lanes = self.width // 8
if self._valid_init is not None and hasattr(self.bus, "tvalid"): if self._valid_init is not None and hasattr(self.bus, "tvalid"):
self.bus.tvalid.setimmediatevalue(self._valid_init) self.bus.tvalid.setimmediatevalue(self._valid_init)
@@ -297,9 +306,13 @@ class AxiStreamBase(Reset):
for sig in self._signals+self._optional_signals: for sig in self._signals+self._optional_signals:
if hasattr(self.bus, sig): if hasattr(self.bus, sig):
if self._init_x and sig not in ("tvalid", "tready"): if self._init_x and sig not in ("tvalid", "tready"):
v = getattr(self.bus, sig).value s = getattr(self.bus, sig)
try:
v = LogicArray("x"*len(s.value))
except NameError:
v = s.value
v.binstr = 'x'*len(v) v.binstr = 'x'*len(v)
getattr(self.bus, sig).setimmediatevalue(v) s.setimmediatevalue(v)
if hasattr(self.bus, "tkeep"): if hasattr(self.bus, "tkeep"):
self.byte_lanes = len(self.bus.tkeep) self.byte_lanes = len(self.bus.tkeep)
@@ -366,7 +379,7 @@ class AxiStreamBase(Reset):
else: else:
self.log.info("Reset de-asserted") self.log.info("Reset de-asserted")
if self._run_cr is None: if self._run_cr is None:
self._run_cr = cocotb.fork(self._run()) self._run_cr = cocotb.start_soon(self._run())
async def _run(self): async def _run(self):
raise NotImplementedError() raise NotImplementedError()
@@ -376,10 +389,23 @@ class AxiStreamPause:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.pause = False self._pause = False
self._pause_generator = None self._pause_generator = None
self._pause_cr = None self._pause_cr = None
def _pause_update(self, val):
pass
@property
def pause(self):
return self._pause
@pause.setter
def pause(self, val):
if self._pause != val:
self._pause_update(val)
self._pause = val
def set_pause_generator(self, generator=None): def set_pause_generator(self, generator=None):
if self._pause_cr is not None: if self._pause_cr is not None:
self._pause_cr.kill() self._pause_cr.kill()
@@ -388,15 +414,17 @@ class AxiStreamPause:
self._pause_generator = generator self._pause_generator = generator
if self._pause_generator is not None: if self._pause_generator is not None:
self._pause_cr = cocotb.fork(self._run_pause()) self._pause_cr = cocotb.start_soon(self._run_pause())
def clear_pause_generator(self): def clear_pause_generator(self):
self.set_pause_generator(None) self.set_pause_generator(None)
async def _run_pause(self): async def _run_pause(self):
clock_edge_event = RisingEdge(self.clock)
for val in self._pause_generator: for val in self._pause_generator:
self.pause = val self.pause = val
await RisingEdge(self.clock) await clock_edge_event
class AxiStreamSource(AxiStreamBase, AxiStreamPause): class AxiStreamSource(AxiStreamBase, AxiStreamPause):
@@ -423,6 +451,7 @@ class AxiStreamSource(AxiStreamBase, AxiStreamPause):
frame = AxiStreamFrame(frame) frame = AxiStreamFrame(frame)
await self.queue.put(frame) await self.queue.put(frame)
self.idle_event.clear() self.idle_event.clear()
self.active_event.set()
self.queue_occupancy_bytes += len(frame) self.queue_occupancy_bytes += len(frame)
self.queue_occupancy_frames += 1 self.queue_occupancy_frames += 1
@@ -432,6 +461,7 @@ class AxiStreamSource(AxiStreamBase, AxiStreamPause):
frame = AxiStreamFrame(frame) frame = AxiStreamFrame(frame)
self.queue.put_nowait(frame) self.queue.put_nowait(frame)
self.idle_event.clear() self.idle_event.clear()
self.active_event.set()
self.queue_occupancy_bytes += len(frame) self.queue_occupancy_bytes += len(frame)
self.queue_occupancy_frames += 1 self.queue_occupancy_frames += 1
@@ -459,19 +489,19 @@ class AxiStreamSource(AxiStreamBase, AxiStreamPause):
super()._handle_reset(state) super()._handle_reset(state)
if state: if state:
self.bus.tdata <= 0 self.bus.tdata.value = 0
if hasattr(self.bus, "tvalid"): if hasattr(self.bus, "tvalid"):
self.bus.tvalid <= 0 self.bus.tvalid.value = 0
if hasattr(self.bus, "tlast"): if hasattr(self.bus, "tlast"):
self.bus.tlast <= 0 self.bus.tlast.value = 0
if hasattr(self.bus, "tkeep"): if hasattr(self.bus, "tkeep"):
self.bus.tkeep <= 0 self.bus.tkeep.value = 0
if hasattr(self.bus, "tid"): if hasattr(self.bus, "tid"):
self.bus.tid <= 0 self.bus.tid.value = 0
if hasattr(self.bus, "tdest"): if hasattr(self.bus, "tdest"):
self.bus.tdest <= 0 self.bus.tdest.value = 0
if hasattr(self.bus, "tuser"): if hasattr(self.bus, "tuser"):
self.bus.tuser <= 0 self.bus.tuser.value = 0
if self.current_frame: if self.current_frame:
self.log.warning("Flushed transmit frame during reset: %s", self.current_frame) self.log.warning("Flushed transmit frame during reset: %s", self.current_frame)
@@ -483,15 +513,25 @@ class AxiStreamSource(AxiStreamBase, AxiStreamPause):
frame_offset = 0 frame_offset = 0
self.active = False self.active = False
has_tready = hasattr(self.bus, "tready")
has_tvalid = hasattr(self.bus, "tvalid")
has_tlast = hasattr(self.bus, "tlast")
has_tkeep = hasattr(self.bus, "tkeep")
has_tid = hasattr(self.bus, "tid")
has_tdest = hasattr(self.bus, "tdest")
has_tuser = hasattr(self.bus, "tuser")
clock_edge_event = RisingEdge(self.clock)
while True: while True:
await RisingEdge(self.clock) await clock_edge_event
# read handshake signals # read handshake signals
tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value tready_sample = (not has_tready) or self.bus.tready.value
tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value tvalid_sample = (not has_tvalid) or self.bus.tvalid.value
if (tready_sample and tvalid_sample) or not tvalid_sample: if (tready_sample and tvalid_sample) or not tvalid_sample:
if frame is None and not self.queue.empty(): if not frame and not self.queue.empty():
frame = self.queue.get_nowait() frame = self.queue.get_nowait()
self.dequeue_event.set() self.dequeue_event.set()
self.queue_occupancy_bytes -= len(frame) self.queue_occupancy_bytes -= len(frame)
@@ -528,27 +568,30 @@ class AxiStreamSource(AxiStreamBase, AxiStreamPause):
self.current_frame = None self.current_frame = None
break break
self.bus.tdata <= tdata_val self.bus.tdata.value = tdata_val
if hasattr(self.bus, "tvalid"): if has_tvalid:
self.bus.tvalid <= 1 self.bus.tvalid.value = 1
if hasattr(self.bus, "tlast"): if has_tlast:
self.bus.tlast <= tlast_val self.bus.tlast.value = tlast_val
if hasattr(self.bus, "tkeep"): if has_tkeep:
self.bus.tkeep <= tkeep_val self.bus.tkeep.value = tkeep_val
if hasattr(self.bus, "tid"): if has_tid:
self.bus.tid <= tid_val self.bus.tid.value = tid_val
if hasattr(self.bus, "tdest"): if has_tdest:
self.bus.tdest <= tdest_val self.bus.tdest.value = tdest_val
if hasattr(self.bus, "tuser"): if has_tuser:
self.bus.tuser <= tuser_val self.bus.tuser.value = tuser_val
else: else:
if hasattr(self.bus, "tvalid"): if has_tvalid:
self.bus.tvalid <= 0 self.bus.tvalid.value = 0
if hasattr(self.bus, "tlast"): if has_tlast:
self.bus.tlast <= 0 self.bus.tlast.value = 0
self.active = bool(frame) self.active = bool(frame)
if not frame and self.queue.empty(): if not frame and self.queue.empty():
self.idle_event.set() self.idle_event.set()
self.active_event.clear()
await self.active_event.wait()
class AxiStreamMonitor(AxiStreamBase): class AxiStreamMonitor(AxiStreamBase):
@@ -567,11 +610,20 @@ class AxiStreamMonitor(AxiStreamBase):
self.read_queue = [] self.read_queue = []
if hasattr(self.bus, "tvalid"):
cocotb.start_soon(self._run_tvalid_monitor())
if hasattr(self.bus, "tready"):
cocotb.start_soon(self._run_tready_monitor())
def _dequeue(self, frame):
pass
def _recv(self, frame, compact=True): def _recv(self, frame, compact=True):
if self.queue.empty(): if self.queue.empty():
self.active_event.clear() self.active_event.clear()
self.queue_occupancy_bytes -= len(frame) self.queue_occupancy_bytes -= len(frame)
self.queue_occupancy_frames -= 1 self.queue_occupancy_frames -= 1
self._dequeue(frame)
if compact: if compact:
frame.compact() frame.compact()
return frame return frame
@@ -611,38 +663,64 @@ class AxiStreamMonitor(AxiStreamBase):
else: else:
await self.active_event.wait() await self.active_event.wait()
async def _run_tvalid_monitor(self):
event = RisingEdge(self.bus.tvalid)
while True:
await event
self.wake_event.set()
async def _run_tready_monitor(self):
event = RisingEdge(self.bus.tready)
while True:
await event
self.wake_event.set()
async def _run(self): async def _run(self):
frame = None frame = None
self.active = False self.active = False
has_tready = hasattr(self.bus, "tready")
has_tvalid = hasattr(self.bus, "tvalid")
has_tlast = hasattr(self.bus, "tlast")
has_tkeep = hasattr(self.bus, "tkeep")
has_tid = hasattr(self.bus, "tid")
has_tdest = hasattr(self.bus, "tdest")
has_tuser = hasattr(self.bus, "tuser")
clock_edge_event = RisingEdge(self.clock)
wake_event = self.wake_event.wait()
while True: while True:
await RisingEdge(self.clock) await clock_edge_event
# read handshake signals # read handshake signals
tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value tready_sample = (not has_tready) or self.bus.tready.value
tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value tvalid_sample = (not has_tvalid) or self.bus.tvalid.value
if tready_sample and tvalid_sample: if tready_sample and tvalid_sample:
if frame is None: if not frame:
if self.byte_size == 8: if self.byte_size == 8:
frame = AxiStreamFrame(bytearray(), [], [], [], []) frame = AxiStreamFrame(bytearray(), [], [], [], [])
else: else:
frame = AxiStreamFrame([], [], [], [], []) frame = AxiStreamFrame([], [], [], [], [])
frame.sim_time_start = get_sim_time() frame.sim_time_start = get_sim_time()
self.active = True
for offset in range(self.byte_lanes): for offset in range(self.byte_lanes):
frame.tdata.append((int(self.bus.tdata.value) >> (offset * self.byte_size)) & self.byte_mask)
if has_tkeep:
frame.tkeep.append((int(self.bus.tkeep.value) >> offset) & 1)
if has_tid:
frame.tid.append(int(self.bus.tid.value))
if has_tdest:
frame.tdest.append(int(self.bus.tdest.value))
if has_tuser:
frame.tuser.append(int(self.bus.tuser.value))
frame.tdata.append((self.bus.tdata.value.integer >> (offset * self.byte_size)) & self.byte_mask) if not has_tlast or self.bus.tlast.value:
if hasattr(self.bus, "tkeep"):
frame.tkeep.append((self.bus.tkeep.value.integer >> offset) & 1)
if hasattr(self.bus, "tid"):
frame.tid.append(self.bus.tid.value.integer)
if hasattr(self.bus, "tdest"):
frame.tdest.append(self.bus.tdest.value.integer)
if hasattr(self.bus, "tuser"):
frame.tuser.append(self.bus.tuser.value.integer)
if not hasattr(self.bus, "tlast") or self.bus.tlast.value:
frame.sim_time_end = get_sim_time() frame.sim_time_end = get_sim_time()
self.log.info("RX frame: %s", frame) self.log.info("RX frame: %s", frame)
@@ -653,6 +731,11 @@ class AxiStreamMonitor(AxiStreamBase):
self.active_event.set() self.active_event.set()
frame = None frame = None
else:
self.active = bool(frame)
self.wake_event.clear()
await wake_event
class AxiStreamSink(AxiStreamMonitor, AxiStreamPause): class AxiStreamSink(AxiStreamMonitor, AxiStreamPause):
@@ -667,11 +750,11 @@ class AxiStreamSink(AxiStreamMonitor, AxiStreamPause):
def __init__(self, bus, clock, reset=None, reset_active_level=True, def __init__(self, bus, clock, reset=None, reset_active_level=True,
byte_size=None, byte_lanes=None, *args, **kwargs): byte_size=None, byte_lanes=None, *args, **kwargs):
super().__init__(bus, clock, reset, reset_active_level, byte_size, byte_lanes, *args, **kwargs)
self.queue_occupancy_limit_bytes = -1 self.queue_occupancy_limit_bytes = -1
self.queue_occupancy_limit_frames = -1 self.queue_occupancy_limit_frames = -1
super().__init__(bus, clock, reset, reset_active_level, byte_size, byte_lanes, *args, **kwargs)
def full(self): def full(self):
if self.queue_occupancy_limit_bytes > 0 and self.queue_occupancy_bytes > self.queue_occupancy_limit_bytes: if self.queue_occupancy_limit_bytes > 0 and self.queue_occupancy_bytes > self.queue_occupancy_limit_bytes:
return True return True
@@ -685,40 +768,60 @@ class AxiStreamSink(AxiStreamMonitor, AxiStreamPause):
if state: if state:
if hasattr(self.bus, "tready"): if hasattr(self.bus, "tready"):
self.bus.tready <= 0 self.bus.tready.value = 0
def _pause_update(self, val):
self.wake_event.set()
def _dequeue(self, frame):
self.wake_event.set()
async def _run(self): async def _run(self):
frame = None frame = None
self.active = False self.active = False
has_tready = hasattr(self.bus, "tready")
has_tvalid = hasattr(self.bus, "tvalid")
has_tlast = hasattr(self.bus, "tlast")
has_tkeep = hasattr(self.bus, "tkeep")
has_tid = hasattr(self.bus, "tid")
has_tdest = hasattr(self.bus, "tdest")
has_tuser = hasattr(self.bus, "tuser")
clock_edge_event = RisingEdge(self.clock)
wake_event = self.wake_event.wait()
while True: while True:
await RisingEdge(self.clock) pause_sample = bool(self.pause)
await clock_edge_event
# read handshake signals # read handshake signals
tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value tready_sample = (not has_tready) or self.bus.tready.value
tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value tvalid_sample = (not has_tvalid) or self.bus.tvalid.value
if tready_sample and tvalid_sample: if tready_sample and tvalid_sample:
if frame is None: if not frame:
if self.byte_size == 8: if self.byte_size == 8:
frame = AxiStreamFrame(bytearray(), [], [], [], []) frame = AxiStreamFrame(bytearray(), [], [], [], [])
else: else:
frame = AxiStreamFrame([], [], [], [], []) frame = AxiStreamFrame([], [], [], [], [])
frame.sim_time_start = get_sim_time() frame.sim_time_start = get_sim_time()
self.active = True
for offset in range(self.byte_lanes): for offset in range(self.byte_lanes):
frame.tdata.append((int(self.bus.tdata.value) >> (offset * self.byte_size)) & self.byte_mask)
if has_tkeep:
frame.tkeep.append((int(self.bus.tkeep.value) >> offset) & 1)
if has_tid:
frame.tid.append(int(self.bus.tid.value))
if has_tdest:
frame.tdest.append(int(self.bus.tdest.value))
if has_tuser:
frame.tuser.append(int(self.bus.tuser.value))
frame.tdata.append((self.bus.tdata.value.integer >> (offset * self.byte_size)) & self.byte_mask) if not has_tlast or self.bus.tlast.value:
if hasattr(self.bus, "tkeep"):
frame.tkeep.append((self.bus.tkeep.value.integer >> offset) & 1)
if hasattr(self.bus, "tid"):
frame.tid.append(self.bus.tid.value.integer)
if hasattr(self.bus, "tdest"):
frame.tdest.append(self.bus.tdest.value.integer)
if hasattr(self.bus, "tuser"):
frame.tuser.append(self.bus.tuser.value.integer)
if not hasattr(self.bus, "tlast") or self.bus.tlast.value:
frame.sim_time_end = get_sim_time() frame.sim_time_end = get_sim_time()
self.log.info("RX frame: %s", frame) self.log.info("RX frame: %s", frame)
@@ -729,6 +832,18 @@ class AxiStreamSink(AxiStreamMonitor, AxiStreamPause):
self.active_event.set() self.active_event.set()
frame = None frame = None
else:
self.active = bool(frame)
if hasattr(self.bus, "tready"): if has_tready:
self.bus.tready <= (not self.full() and not self.pause) paused = self.full() or pause_sample
self.bus.tready.value = not paused
if (not tvalid_sample or paused) and (pause_sample == bool(self.pause)):
self.wake_event.clear()
await wake_event
else:
if not tvalid_sample:
self.wake_event.clear()
await wake_event

View File

@@ -0,0 +1,92 @@
"""
Copyright (c) 2021-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
class BuddyAllocator:
def __init__(self, size, min_alloc=1):
self.size = size
self.min_alloc = min_alloc
self.free_lists = [[] for x in range((self.size-1).bit_length())]
self.free_lists.append([0])
self.allocations = {}
def alloc(self, size):
if size < 1 or size > self.size:
raise ValueError("size out of range")
size = max(size, self.min_alloc)
bucket = (size-1).bit_length()
orig_bucket = bucket
while bucket < len(self.free_lists):
if not self.free_lists[bucket]:
# find free block
bucket += 1
continue
while bucket > orig_bucket:
# split block
block = self.free_lists[bucket].pop(0)
bucket -= 1
self.free_lists[bucket].append(block)
self.free_lists[bucket].append(block+2**bucket)
if self.free_lists[bucket]:
# allocate
block = self.free_lists[bucket].pop(0)
self.allocations[block] = bucket
return block
break
raise Exception("out of memory")
def free(self, addr):
if addr not in self.allocations:
raise ValueError("unknown allocation")
bucket = self.allocations.pop(addr)
while bucket < len(self.free_lists):
size = 2**bucket
# find buddy
if (addr // size) % 2:
buddy = addr - size
else:
buddy = addr + size
if buddy in self.free_lists[bucket]:
# buddy is free, merge
self.free_lists[bucket].remove(buddy)
addr = min(addr, buddy)
bucket += 1
else:
# buddy is not free, so add to free list
self.free_lists[bucket].append(addr)
return
raise Exception("failed to free memory")

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -22,27 +22,24 @@ THE SOFTWARE.
""" """
import mmap from .sparse_memory import SparseMemory
from .utils import hexdump, hexdump_lines, hexdump_str from .utils import hexdump, hexdump_lines, hexdump_str
class Memory: class Memory:
def __init__(self, size=1024, mem=None, *args, **kwargs): def __init__(self, size=2**64, mem=None, **kwargs):
if mem is not None: if mem is not None:
self.mem = mem self.mem = mem
else: else:
self.mem = mmap.mmap(-1, size) self.mem = SparseMemory(size)
self.size = len(self.mem) self.size = len(self.mem)
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def read(self, address, length): def read(self, address, length):
self.mem.seek(address) return self.mem[address:address+length]
return self.mem.read(length)
def write(self, address, data): def write(self, address, data):
self.mem.seek(address) self.mem[address:address+len(data)] = data
self.mem.write(bytes(data))
def write_words(self, address, data, byteorder='little', ws=2): def write_words(self, address, data, byteorder='little', ws=2):
words = data words = data

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -23,7 +23,7 @@ THE SOFTWARE.
""" """
import cocotb import cocotb
from cocotb.triggers import RisingEdge, FallingEdge from cocotb.triggers import Edge
class Reset: class Reset:
@@ -33,7 +33,7 @@ class Reset:
self._reset_state = True self._reset_state = True
if reset_signal is not None: if reset_signal is not None:
cocotb.fork(self._run_reset(reset_signal, bool(active_level))) cocotb.start_soon(self._run_reset(reset_signal, bool(active_level)))
self._update_reset() self._update_reset()
@@ -56,11 +56,14 @@ class Reset:
async def _run_reset(self, reset_signal, active_level): async def _run_reset(self, reset_signal, active_level):
while True: while True:
if bool(reset_signal.value): await Edge(reset_signal)
await FallingEdge(reset_signal) try:
self._ext_reset = not active_level level = bool(int(reset_signal.value))
self._update_reset() except ValueError:
else: continue
await RisingEdge(reset_signal) if level:
self._ext_reset = active_level self._ext_reset = active_level
self._update_reset() self._update_reset()
else:
self._ext_reset = not active_level
self._update_reset()

View File

@@ -0,0 +1,111 @@
"""
Copyright (c) 2023-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
from .utils import hexdump, hexdump_lines, hexdump_str
class SparseMemory:
def __init__(self, size):
self.size = size
self.segs = {}
def read(self, address, length, **kwargs):
if address < 0 or address >= self.size:
raise ValueError("address out of range")
if length < 0:
raise ValueError("invalid length")
if address+length > self.size:
raise ValueError("operation out of range")
data = bytearray()
while length > 0:
block_offset = address & 0xfff
block_addr = address - block_offset
block_len = min(4096 - block_offset, length)
try:
block = self.segs[block_addr]
except KeyError:
block = b'\x00'*4096
data.extend(block[block_offset:block_offset+block_len])
address += block_len
length -= block_len
return bytes(data)
def write(self, address, data, **kwargs):
if address < 0 or address >= self.size:
raise ValueError("address out of range")
if address+len(data) > self.size:
raise ValueError("operation out of range")
offset = 0
length = len(data)
while length > 0:
block_offset = address & 0xfff
block_addr = address - block_offset
block_len = min(4096 - block_offset, length)
try:
block = self.segs[block_addr]
except KeyError:
block = bytearray(4096)
self.segs[block_addr] = block
block[block_offset:block_offset+block_len] = data[offset:offset+block_len]
address += block_len
offset += block_len
length -= block_len
def clear(self):
self.segs.clear()
def hexdump(self, address, length, prefix=""):
hexdump(self.read(address, length), prefix=prefix, offset=address)
def hexdump_lines(self, address, length, prefix=""):
return hexdump_lines(self.read(address, length), prefix=prefix, offset=address)
def hexdump_str(self, address, length, prefix=""):
return hexdump_str(self.read(address, length), prefix=prefix, offset=address)
def __len__(self):
return self.size
def __getitem__(self, key):
if isinstance(key, int):
return self.read(key, 1)[0]
elif isinstance(key, slice):
start, stop, step = key.indices(self.size)
if step == 1:
return self.read(start, stop-start)
else:
raise IndexError("specified step size is not supported")
def __setitem__(self, key, value):
if isinstance(key, int):
self.write(key, [value])
elif isinstance(key, slice):
start, stop, step = key.indices(self.size)
if step == 1:
value = bytes(value)
if stop-start != len(value):
raise IndexError("slice assignment is wrong size")
return self.write(start, value)
else:
raise IndexError("specified step size is not supported")

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -29,6 +29,11 @@ from cocotb.queue import Queue, QueueFull
from cocotb.triggers import RisingEdge, Event, First, Timer from cocotb.triggers import RisingEdge, Event, First, Timer
from cocotb_bus.bus import Bus from cocotb_bus.bus import Bus
try:
from cocotb.types import LogicArray
except ImportError:
pass
from .reset import Reset from .reset import Reset
@@ -99,6 +104,7 @@ class StreamBase(Reset):
self.idle_event = Event() self.idle_event = Event()
self.idle_event.set() self.idle_event.set()
self.active_event = Event() self.active_event = Event()
self.wake_event = Event()
self.ready = None self.ready = None
self.valid = None self.valid = None
@@ -118,9 +124,13 @@ class StreamBase(Reset):
if sig in self._signal_widths: if sig in self._signal_widths:
assert len(getattr(self.bus, sig)) == self._signal_widths[sig] assert len(getattr(self.bus, sig)) == self._signal_widths[sig]
if self._init_x and sig not in (self._valid_signal, self._ready_signal): if self._init_x and sig not in (self._valid_signal, self._ready_signal):
v = getattr(self.bus, sig).value s = getattr(self.bus, sig)
try:
v = LogicArray("x"*len(s.value))
except NameError:
v = s.value
v.binstr = 'x'*len(v) v.binstr = 'x'*len(v)
getattr(self.bus, sig).setimmediatevalue(v) s.setimmediatevalue(v)
self._run_cr = None self._run_cr = None
@@ -153,7 +163,7 @@ class StreamBase(Reset):
else: else:
self.log.info("Reset de-asserted") self.log.info("Reset de-asserted")
if self._run_cr is None: if self._run_cr is None:
self._run_cr = cocotb.fork(self._run()) self._run_cr = cocotb.start_soon(self._run())
async def _run(self): async def _run(self):
raise NotImplementedError() raise NotImplementedError()
@@ -163,10 +173,23 @@ class StreamPause:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.pause = False self._pause = False
self._pause_generator = None self._pause_generator = None
self._pause_cr = None self._pause_cr = None
def _pause_update(self, val):
pass
@property
def pause(self):
return self._pause
@pause.setter
def pause(self, val):
if self._pause != val:
self._pause_update(val)
self._pause = val
def set_pause_generator(self, generator=None): def set_pause_generator(self, generator=None):
if self._pause_cr is not None: if self._pause_cr is not None:
self._pause_cr.kill() self._pause_cr.kill()
@@ -175,15 +198,17 @@ class StreamPause:
self._pause_generator = generator self._pause_generator = generator
if self._pause_generator is not None: if self._pause_generator is not None:
self._pause_cr = cocotb.fork(self._run_pause()) self._pause_cr = cocotb.start_soon(self._run_pause())
def clear_pause_generator(self): def clear_pause_generator(self):
self.set_pause_generator(None) self.set_pause_generator(None)
async def _run_pause(self): async def _run_pause(self):
clock_edge_event = RisingEdge(self.clock)
for val in self._pause_generator: for val in self._pause_generator:
self.pause = val self.pause = val
await RisingEdge(self.clock) await clock_edge_event
class StreamSource(StreamBase, StreamPause): class StreamSource(StreamBase, StreamPause):
@@ -204,12 +229,14 @@ class StreamSource(StreamBase, StreamPause):
await self.dequeue_event.wait() await self.dequeue_event.wait()
await self.queue.put(obj) await self.queue.put(obj)
self.idle_event.clear() self.idle_event.clear()
self.active_event.set()
def send_nowait(self, obj): def send_nowait(self, obj):
if self.full(): if self.full():
raise QueueFull() raise QueueFull()
self.queue.put_nowait(obj) self.queue.put_nowait(obj)
self.idle_event.clear() self.idle_event.clear()
self.active_event.set()
def full(self): def full(self):
if self.queue_occupancy_limit > 0 and self.count() >= self.queue_occupancy_limit: if self.queue_occupancy_limit > 0 and self.count() >= self.queue_occupancy_limit:
@@ -228,29 +255,37 @@ class StreamSource(StreamBase, StreamPause):
if state: if state:
if self.valid is not None: if self.valid is not None:
self.valid <= 0 self.valid.value = 0
async def _run(self): async def _run(self):
has_valid = self.valid is not None
has_ready = self.ready is not None
clock_edge_event = RisingEdge(self.clock)
while True: while True:
await RisingEdge(self.clock) await clock_edge_event
# read handshake signals # read handshake signals
ready_sample = self.ready is None or self.ready.value ready_sample = not has_ready or self.ready.value
valid_sample = self.valid is None or self.valid.value valid_sample = not has_valid or self.valid.value
if (ready_sample and valid_sample) or (not valid_sample): if (ready_sample and valid_sample) or (not valid_sample):
if not self.queue.empty() and not self.pause: if not self.queue.empty() and not self.pause:
self.bus.drive(self.queue.get_nowait()) self.bus.drive(self.queue.get_nowait())
self.dequeue_event.set() self.dequeue_event.set()
if self.valid is not None: if has_valid:
self.valid <= 1 self.valid.value = 1
self.active = True self.active = True
else: else:
if self.valid is not None: if has_valid:
self.valid <= 0 self.valid.value = 0
self.active = not self.queue.empty() self.active = not self.queue.empty()
if self.queue.empty(): if self.queue.empty():
self.idle_event.set() self.idle_event.set()
self.active_event.clear()
await self.active_event.wait()
class StreamMonitor(StreamBase): class StreamMonitor(StreamBase):
@@ -260,9 +295,21 @@ class StreamMonitor(StreamBase):
_valid_init = None _valid_init = None
_ready_init = None _ready_init = None
def __init__(self, bus, clock, reset=None, reset_active_level=True, *args, **kwargs):
super().__init__(bus, clock, reset, reset_active_level, *args, **kwargs)
if self.valid is not None:
cocotb.start_soon(self._run_valid_monitor())
if self.ready is not None:
cocotb.start_soon(self._run_ready_monitor())
def _dequeue(self, item):
pass
def _recv(self, item): def _recv(self, item):
if self.queue.empty(): if self.queue.empty():
self.active_event.clear() self.active_event.clear()
self._dequeue(item)
return item return item
async def recv(self): async def recv(self):
@@ -281,19 +328,43 @@ class StreamMonitor(StreamBase):
else: else:
await self.active_event.wait() await self.active_event.wait()
async def _run(self): async def _run_valid_monitor(self):
event = RisingEdge(self.valid)
while True: while True:
await RisingEdge(self.clock) await event
self.wake_event.set()
async def _run_ready_monitor(self):
event = RisingEdge(self.ready)
while True:
await event
self.wake_event.set()
async def _run(self):
has_valid = self.valid is not None
has_ready = self.ready is not None
clock_edge_event = RisingEdge(self.clock)
wake_event = self.wake_event.wait()
while True:
await clock_edge_event
# read handshake signals # read handshake signals
ready_sample = self.ready is None or self.ready.value ready_sample = not has_ready or self.ready.value
valid_sample = self.valid is None or self.valid.value valid_sample = not has_valid or self.valid.value
if ready_sample and valid_sample: if ready_sample and valid_sample:
obj = self._transaction_obj() obj = self._transaction_obj()
self.bus.sample(obj) self.bus.sample(obj)
self.queue.put_nowait(obj) self.queue.put_nowait(obj)
self.active_event.set() self.active_event.set()
else:
self.wake_event.clear()
await wake_event
class StreamSink(StreamMonitor, StreamPause): class StreamSink(StreamMonitor, StreamPause):
@@ -319,15 +390,30 @@ class StreamSink(StreamMonitor, StreamPause):
if state: if state:
if self.ready is not None: if self.ready is not None:
self.ready <= 0 self.ready.value = 0
def _pause_update(self, val):
self.wake_event.set()
def _dequeue(self, item):
self.wake_event.set()
async def _run(self): async def _run(self):
has_valid = self.valid is not None
has_ready = self.ready is not None
clock_edge_event = RisingEdge(self.clock)
wake_event = self.wake_event.wait()
while True: while True:
await RisingEdge(self.clock) pause_sample = bool(self.pause)
await clock_edge_event
# read handshake signals # read handshake signals
ready_sample = self.ready is None or self.ready.value ready_sample = not has_ready or self.ready.value
valid_sample = self.valid is None or self.valid.value valid_sample = not has_valid or self.valid.value
if ready_sample and valid_sample: if ready_sample and valid_sample:
obj = self._transaction_obj() obj = self._transaction_obj()
@@ -335,8 +421,18 @@ class StreamSink(StreamMonitor, StreamPause):
self.queue.put_nowait(obj) self.queue.put_nowait(obj)
self.active_event.set() self.active_event.set()
if self.ready is not None: if has_ready:
self.ready <= (not self.full() and not self.pause) paused = self.full() or pause_sample
self.ready.value = not paused
if (not valid_sample or paused) and (pause_sample == bool(self.pause)):
self.wake_event.clear()
await wake_event
else:
if not valid_sample:
self.wake_event.clear()
await wake_event
def define_stream(name, signals, optional_signals=None, valid_signal=None, ready_signal=None, signal_widths=None): def define_stream(name, signals, optional_signals=None, valid_signal=None, ready_signal=None, signal_widths=None):

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1 +1 @@
__version__ = "0.1.12" __version__ = "0.1.28"

View File

@@ -13,20 +13,21 @@ project_urls =
Source Code = https://github.com/alexforencich/cocotbext-axi Source Code = https://github.com/alexforencich/cocotbext-axi
download_url = https://github.com/alexforencich/cocotbext-axi/tarball/master download_url = https://github.com/alexforencich/cocotbext-axi/tarball/master
long_description = file: README.md long_description = file: README.md
long-description-content-type = text/markdown long_description_content_type = text/markdown
platforms = any platforms = any
classifiers = classifiers =
Development Status :: 3 - Alpha Development Status :: 3 - Alpha
Programming Language :: Python :: 3 Framework :: cocotb
License :: OSI Approved :: MIT License License :: OSI Approved :: MIT License
Operating System :: OS Independent Operating System :: OS Independent
Programming Language :: Python :: 3
Topic :: Scientific/Engineering :: Electronic Design Automation (EDA) Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
[options] [options]
packages = find_namespace: packages = find_namespace:
python_requires = >=3.6 python_requires = >=3.6
install_requires = install_requires =
cocotb cocotb >= 1.6.0
cocotb-bus cocotb-bus
[options.extras_require] [options.extras_require]
@@ -46,31 +47,40 @@ addopts =
# tox configuration # tox configuration
[tox:tox] [tox:tox]
envlist = py36, py37, py38, py39 envlist = py38, py39, py310, py311, py312, py313
skip_missing_interpreters = true
minversion = 3.18.0
requires = virtualenv >= 16.1
[gh-actions] [gh-actions]
python = python =
3.6: py36
3.7: py37
3.8: py38 3.8: py38
3.9: py39 3.9: py39
3.10: py310
3.11: py311
3.12: py312
3.13: py313
[testenv] [testenv]
setenv = setenv =
COVERAGE=1 COVERAGE=1
usedevelop = True
deps = deps =
pytest pytest == 8.3.4
pytest-xdist pytest-xdist == 3.6.1
cocotb-test cocotb == 1.9.2
coverage cocotb-bus == 0.2.1
pytest-cov cocotb-test == 0.2.6
coverage == 7.0.5
pytest-cov == 4.0.0
commands = commands =
pytest --cov=cocotbext --cov=tests --cov-branch -n auto pytest --cov=cocotbext --cov=tests --cov-branch {posargs:-n auto --verbose}
bash -c 'find . -type f -name "\.coverage" | xargs coverage combine --append' bash -c 'find . -type f -name "\.coverage" | xargs coverage combine --append'
coverage report
whitelist_externals = allowlist_externals =
bash bash
# combine if paths are different # combine if paths are different

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2020 Alex Forencich # Copyright (c) 2020-2025 Alex Forencich
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal

54
tests/apb/Makefile Normal file
View File

@@ -0,0 +1,54 @@
# Copyright (c) 2025 Alex Forencich
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
TOPLEVEL_LANG = verilog
SIM ?= icarus
WAVES ?= 0
COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ns
DUT = test_apb
COCOTB_TEST_MODULES = $(DUT)
COCOTB_TOPLEVEL = $(DUT)
MODULE = $(COCOTB_TEST_MODULES)
TOPLEVEL = $(COCOTB_TOPLEVEL)
VERILOG_SOURCES += $(DUT).v
# module parameters
export PARAM_DATA_W := 32
export PARAM_ADDR_W := 32
export PARAM_STRB_W := $(shell expr $(PARAM_DATA_W) / 8 )
ifeq ($(SIM), icarus)
PLUSARGS += -fst
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
else ifeq ($(SIM), verilator)
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
ifeq ($(WAVES), 1)
COMPILE_ARGS += --trace-fst
VERILATOR_TRACE = 1
endif
endif
include $(shell cocotb-config --makefiles)/Makefile.sim

332
tests/apb/test_apb.py Normal file
View File

@@ -0,0 +1,332 @@
"""
Copyright (c) 2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import itertools
import logging
import os
import random
import cocotb_test.simulator
import pytest
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
from cocotb.regression import TestFactory
from cocotbext.axi import ApbBus, ApbMaster, ApbRam
class TB:
def __init__(self, dut):
self.dut = dut
self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG)
cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
self.apb_master = ApbMaster(ApbBus.from_prefix(dut, "apb"), dut.clk, dut.rst)
self.apb_ram = ApbRam(ApbBus.from_prefix(dut, "apb"), dut.clk, dut.rst, size=2**16)
def set_idle_generator(self, generator=None):
if generator:
self.apb_master.set_pause_generator(generator())
def set_backpressure_generator(self, generator=None):
if generator:
self.apb_ram.set_pause_generator(generator())
async def cycle_reset(self):
self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
self.dut.rst.value = 1
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
self.dut.rst.value = 0
await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk)
async def run_test_write(dut, data_in=None, idle_inserter=None, backpressure_inserter=None):
tb = TB(dut)
byte_lanes = tb.apb_master.byte_lanes
await tb.cycle_reset()
tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter)
for length in range(1, byte_lanes*2):
for offset in range(byte_lanes):
tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000
test_data = bytearray([x % 256 for x in range(length)])
tb.apb_ram.write(addr-128, b'\xaa'*(length+256))
await tb.apb_master.write(addr, test_data)
tb.log.debug("%s", tb.apb_ram.hexdump_str((addr & ~0xf)-16, (((addr & 0xf)+length-1) & ~0xf)+48))
assert tb.apb_ram.read(addr, length) == test_data
assert tb.apb_ram.read(addr-1, 1) == b'\xaa'
assert tb.apb_ram.read(addr+length, 1) == b'\xaa'
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
async def run_test_read(dut, data_in=None, idle_inserter=None, backpressure_inserter=None):
tb = TB(dut)
byte_lanes = tb.apb_master.byte_lanes
await tb.cycle_reset()
tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter)
for length in range(1, byte_lanes*2):
for offset in range(byte_lanes):
tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000
test_data = bytearray([x % 256 for x in range(length)])
tb.apb_ram.write(addr, test_data)
data = await tb.apb_master.read(addr, length)
assert data.data == test_data
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
async def run_test_write_words(dut):
tb = TB(dut)
byte_lanes = tb.apb_master.byte_lanes
await tb.cycle_reset()
for length in list(range(1, 4)):
for offset in list(range(byte_lanes)):
tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000
test_data = bytearray([x % 256 for x in range(length)])
event = tb.apb_master.init_write(addr, test_data)
await event.wait()
assert tb.apb_ram.read(addr, length) == test_data
test_data = bytearray([x % 256 for x in range(length)])
await tb.apb_master.write(addr, test_data)
assert tb.apb_ram.read(addr, length) == test_data
test_data = [x * 0x1001 for x in range(length)]
await tb.apb_master.write_words(addr, test_data)
assert tb.apb_ram.read_words(addr, length) == test_data
test_data = [x * 0x10200201 for x in range(length)]
await tb.apb_master.write_dwords(addr, test_data)
assert tb.apb_ram.read_dwords(addr, length) == test_data
test_data = [x * 0x1020304004030201 for x in range(length)]
await tb.apb_master.write_qwords(addr, test_data)
assert tb.apb_ram.read_qwords(addr, length) == test_data
test_data = 0x01*length
await tb.apb_master.write_byte(addr, test_data)
assert tb.apb_ram.read_byte(addr) == test_data
test_data = 0x1001*length
await tb.apb_master.write_word(addr, test_data)
assert tb.apb_ram.read_word(addr) == test_data
test_data = 0x10200201*length
await tb.apb_master.write_dword(addr, test_data)
assert tb.apb_ram.read_dword(addr) == test_data
test_data = 0x1020304004030201*length
await tb.apb_master.write_qword(addr, test_data)
assert tb.apb_ram.read_qword(addr) == test_data
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
async def run_test_read_words(dut):
tb = TB(dut)
byte_lanes = tb.apb_master.byte_lanes
await tb.cycle_reset()
for length in list(range(1, 4)):
for offset in list(range(byte_lanes)):
tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000
test_data = bytearray([x % 256 for x in range(length)])
tb.apb_ram.write(addr, test_data)
event = tb.apb_master.init_read(addr, length)
await event.wait()
assert event.data.data == test_data
test_data = bytearray([x % 256 for x in range(length)])
tb.apb_ram.write(addr, test_data)
assert (await tb.apb_master.read(addr, length)).data == test_data
test_data = [x * 0x1001 for x in range(length)]
tb.apb_ram.write_words(addr, test_data)
assert await tb.apb_master.read_words(addr, length) == test_data
test_data = [x * 0x10200201 for x in range(length)]
tb.apb_ram.write_dwords(addr, test_data)
assert await tb.apb_master.read_dwords(addr, length) == test_data
test_data = [x * 0x1020304004030201 for x in range(length)]
tb.apb_ram.write_qwords(addr, test_data)
assert await tb.apb_master.read_qwords(addr, length) == test_data
test_data = 0x01*length
tb.apb_ram.write_byte(addr, test_data)
assert await tb.apb_master.read_byte(addr) == test_data
test_data = 0x1001*length
tb.apb_ram.write_word(addr, test_data)
assert await tb.apb_master.read_word(addr) == test_data
test_data = 0x10200201*length
tb.apb_ram.write_dword(addr, test_data)
assert await tb.apb_master.read_dword(addr) == test_data
test_data = 0x1020304004030201*length
tb.apb_ram.write_qword(addr, test_data)
assert await tb.apb_master.read_qword(addr) == test_data
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
tb = TB(dut)
await tb.cycle_reset()
tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter)
async def worker(master, offset, aperture, count=16):
for k in range(count):
length = random.randint(1, min(32, aperture))
addr = offset+random.randint(0, aperture-length)
test_data = bytearray([x % 256 for x in range(length)])
await Timer(random.randint(1, 100), 'ns')
await master.write(addr, test_data)
await Timer(random.randint(1, 100), 'ns')
data = await master.read(addr, length)
assert data.data == test_data
workers = []
for k in range(16):
workers.append(cocotb.start_soon(worker(tb.apb_master, k*0x1000, 0x1000, count=16)))
while workers:
await workers.pop(0).join()
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
def cycle_pause():
return itertools.cycle([1, 1, 1, 0])
if getattr(cocotb, 'top', None) is not None:
for test in [run_test_write, run_test_read]:
factory = TestFactory(test)
factory.add_option("idle_inserter", [None, cycle_pause])
factory.add_option("backpressure_inserter", [None, cycle_pause])
factory.generate_tests()
for test in [run_test_write_words, run_test_read_words]:
factory = TestFactory(test)
factory.generate_tests()
factory = TestFactory(run_stress_test)
factory.generate_tests()
# cocotb-test
tests_dir = os.path.dirname(__file__)
@pytest.mark.parametrize("data_w", [8, 16, 32])
def test_apb(request, data_w):
dut = "test_apb"
module = os.path.splitext(os.path.basename(__file__))[0]
toplevel = dut
verilog_sources = [
os.path.join(os.path.dirname(__file__), f"{dut}.v"),
]
parameters = {}
parameters['DATA_W'] = data_w
parameters['ADDR_W'] = 32
parameters['STRB_W'] = parameters['DATA_W'] // 8
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
sim_build = os.path.join(tests_dir, "sim_build",
request.node.name.replace('[', '-').replace(']', ''))
cocotb_test.simulator.run(
python_search=[tests_dir],
verilog_sources=verilog_sources,
toplevel=toplevel,
module=module,
parameters=parameters,
sim_build=sim_build,
extra_env=extra_env,
)

54
tests/apb/test_apb.v Normal file
View File

@@ -0,0 +1,54 @@
/*
Copyright (c) 2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Language: Verilog 2001
`timescale 1ns / 1ns
/*
* APB test module
*/
module test_apb #
(
parameter DATA_W = 32,
parameter ADDR_W = 16,
parameter STRB_W = (DATA_W/8)
)
(
input wire clk,
input wire rst,
inout wire [ADDR_W-1:0] apb_paddr,
inout wire [2:0] apb_pprot,
inout wire apb_psel,
inout wire apb_penable,
inout wire apb_pwrite,
inout wire [DATA_W-1:0] apb_pwdata,
inout wire [STRB_W-1:0] apb_pstrb,
inout wire apb_pready,
inout wire [DATA_W-1:0] apb_prdata,
inout wire apb_pslverr
);
endmodule

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2020 Alex Forencich # Copyright (c) 2020-2025 Alex Forencich
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -27,66 +27,34 @@ COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ns COCOTB_HDL_TIMEPRECISION = 1ns
DUT = test_axi DUT = test_axi
TOPLEVEL = $(DUT) COCOTB_TEST_MODULES = $(DUT)
MODULE = $(DUT) COCOTB_TOPLEVEL = $(DUT)
MODULE = $(COCOTB_TEST_MODULES)
TOPLEVEL = $(COCOTB_TOPLEVEL)
VERILOG_SOURCES += $(DUT).v VERILOG_SOURCES += $(DUT).v
# module parameters # module parameters
export PARAM_DATA_WIDTH ?= 32 export PARAM_DATA_WIDTH := 32
export PARAM_ADDR_WIDTH ?= 32 export PARAM_ADDR_WIDTH := 32
export PARAM_STRB_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 ) export PARAM_STRB_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 )
export PARAM_ID_WIDTH ?= 8 export PARAM_ID_WIDTH := 8
export PARAM_AWUSER_WIDTH ?= 1 export PARAM_AWUSER_WIDTH := 1
export PARAM_WUSER_WIDTH ?= 1 export PARAM_WUSER_WIDTH := 1
export PARAM_BUSER_WIDTH ?= 1 export PARAM_BUSER_WIDTH := 1
export PARAM_ARUSER_WIDTH ?= 1 export PARAM_ARUSER_WIDTH := 1
export PARAM_RUSER_WIDTH ?= 1 export PARAM_RUSER_WIDTH := 1
ifeq ($(SIM), icarus) ifeq ($(SIM), icarus)
PLUSARGS += -fst PLUSARGS += -fst
COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH) COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
COMPILE_ARGS += -P $(TOPLEVEL).ADDR_WIDTH=$(PARAM_ADDR_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).STRB_WIDTH=$(PARAM_STRB_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).ID_WIDTH=$(PARAM_ID_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).AWUSER_WIDTH=$(PARAM_AWUSER_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).WUSER_WIDTH=$(PARAM_WUSER_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).BUSER_WIDTH=$(PARAM_BUSER_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).ARUSER_WIDTH=$(PARAM_ARUSER_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).RUSER_WIDTH=$(PARAM_RUSER_WIDTH)
ifeq ($(WAVES), 1)
VERILOG_SOURCES += iverilog_dump.v
COMPILE_ARGS += -s iverilog_dump
endif
else ifeq ($(SIM), verilator) else ifeq ($(SIM), verilator)
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH -Wno-CASEINCOMPLETE COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
COMPILE_ARGS += -GDATA_WIDTH=$(PARAM_DATA_WIDTH)
COMPILE_ARGS += -GADDR_WIDTH=$(PARAM_ADDR_WIDTH)
COMPILE_ARGS += -GSTRB_WIDTH=$(PARAM_STRB_WIDTH)
COMPILE_ARGS += -GID_WIDTH=$(PARAM_ID_WIDTH)
COMPILE_ARGS += -GAWUSER_WIDTH=$(PARAM_AWUSER_WIDTH)
COMPILE_ARGS += -GWUSER_WIDTH=$(PARAM_WUSER_WIDTH)
COMPILE_ARGS += -GBUSER_WIDTH=$(PARAM_BUSER_WIDTH)
COMPILE_ARGS += -GARUSER_WIDTH=$(PARAM_ARUSER_WIDTH)
COMPILE_ARGS += -GRUSER_WIDTH=$(PARAM_RUSER_WIDTH)
ifeq ($(WAVES), 1) ifeq ($(WAVES), 1)
COMPILE_ARGS += --trace-fst COMPILE_ARGS += --trace-fst
VERILATOR_TRACE = 1
endif endif
endif endif
include $(shell cocotb-config --makefiles)/Makefile.sim include $(shell cocotb-config --makefiles)/Makefile.sim
iverilog_dump.v:
echo 'module iverilog_dump();' > $@
echo 'initial begin' >> $@
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
echo 'end' >> $@
echo 'endmodule' >> $@
clean::
@rm -rf iverilog_dump.v
@rm -rf dump.fst $(TOPLEVEL).fst

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -45,7 +45,7 @@ class TB:
self.log = logging.getLogger("cocotb.tb") self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG) self.log.setLevel(logging.DEBUG)
cocotb.fork(Clock(dut.clk, 2, units="ns").start()) cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
self.axi_master = AxiMaster(AxiBus.from_prefix(dut, "axi"), dut.clk, dut.rst) self.axi_master = AxiMaster(AxiBus.from_prefix(dut, "axi"), dut.clk, dut.rst)
self.axi_ram = AxiRam(AxiBus.from_prefix(dut, "axi"), dut.clk, dut.rst, size=2**16) self.axi_ram = AxiRam(AxiBus.from_prefix(dut, "axi"), dut.clk, dut.rst, size=2**16)
@@ -73,10 +73,10 @@ class TB:
self.dut.rst.setimmediatevalue(0) self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 1 self.dut.rst.value = 1
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 0 self.dut.rst.value = 0
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
@@ -283,7 +283,7 @@ async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
workers = [] workers = []
for k in range(16): for k in range(16):
workers.append(cocotb.fork(worker(tb.axi_master, k*0x1000, 0x1000, count=16))) workers.append(cocotb.start_soon(worker(tb.axi_master, k*0x1000, 0x1000, count=16)))
while workers: while workers:
await workers.pop(0).join() await workers.pop(0).join()
@@ -296,7 +296,7 @@ def cycle_pause():
return itertools.cycle([1, 1, 1, 0]) return itertools.cycle([1, 1, 1, 0])
if cocotb.SIM_NAME: if getattr(cocotb, 'top', None) is not None:
data_width = len(cocotb.top.axi_wdata) data_width = len(cocotb.top.axi_wdata)
byte_lanes = data_width // 8 byte_lanes = data_width // 8

View File

@@ -1,6 +1,6 @@
/* /*
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2020 Alex Forencich # Copyright (c) 2020-2025 Alex Forencich
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -27,48 +27,28 @@ COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ns COCOTB_HDL_TIMEPRECISION = 1ns
DUT = test_axil DUT = test_axil
TOPLEVEL = $(DUT) COCOTB_TEST_MODULES = $(DUT)
MODULE = $(DUT) COCOTB_TOPLEVEL = $(DUT)
MODULE = $(COCOTB_TEST_MODULES)
TOPLEVEL = $(COCOTB_TOPLEVEL)
VERILOG_SOURCES += $(DUT).v VERILOG_SOURCES += $(DUT).v
# module parameters # module parameters
export PARAM_DATA_WIDTH ?= 32 export PARAM_DATA_WIDTH := 32
export PARAM_ADDR_WIDTH ?= 32 export PARAM_ADDR_WIDTH := 32
export PARAM_STRB_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 ) export PARAM_STRB_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 )
ifeq ($(SIM), icarus) ifeq ($(SIM), icarus)
PLUSARGS += -fst PLUSARGS += -fst
COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH) COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
COMPILE_ARGS += -P $(TOPLEVEL).ADDR_WIDTH=$(PARAM_ADDR_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).STRB_WIDTH=$(PARAM_STRB_WIDTH)
ifeq ($(WAVES), 1)
VERILOG_SOURCES += iverilog_dump.v
COMPILE_ARGS += -s iverilog_dump
endif
else ifeq ($(SIM), verilator) else ifeq ($(SIM), verilator)
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
COMPILE_ARGS += -GDATA_WIDTH=$(PARAM_DATA_WIDTH)
COMPILE_ARGS += -GADDR_WIDTH=$(PARAM_ADDR_WIDTH)
COMPILE_ARGS += -GSTRB_WIDTH=$(PARAM_STRB_WIDTH)
ifeq ($(WAVES), 1) ifeq ($(WAVES), 1)
COMPILE_ARGS += --trace-fst COMPILE_ARGS += --trace-fst
VERILATOR_TRACE = 1
endif endif
endif endif
include $(shell cocotb-config --makefiles)/Makefile.sim include $(shell cocotb-config --makefiles)/Makefile.sim
iverilog_dump.v:
echo 'module iverilog_dump();' > $@
echo 'initial begin' >> $@
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
echo 'end' >> $@
echo 'endmodule' >> $@
clean::
@rm -rf iverilog_dump.v
@rm -rf dump.fst $(TOPLEVEL).fst

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -45,7 +45,7 @@ class TB:
self.log = logging.getLogger("cocotb.tb") self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG) self.log.setLevel(logging.DEBUG)
cocotb.fork(Clock(dut.clk, 2, units="ns").start()) cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
self.axil_master = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "axil"), dut.clk, dut.rst) self.axil_master = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "axil"), dut.clk, dut.rst)
self.axil_ram = AxiLiteRam(AxiLiteBus.from_prefix(dut, "axil"), dut.clk, dut.rst, size=2**16) self.axil_ram = AxiLiteRam(AxiLiteBus.from_prefix(dut, "axil"), dut.clk, dut.rst, size=2**16)
@@ -70,10 +70,10 @@ class TB:
self.dut.rst.setimmediatevalue(0) self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 1 self.dut.rst.value = 1
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 0 self.dut.rst.value = 0
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
@@ -272,7 +272,7 @@ async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
workers = [] workers = []
for k in range(16): for k in range(16):
workers.append(cocotb.fork(worker(tb.axil_master, k*0x1000, 0x1000, count=16))) workers.append(cocotb.start_soon(worker(tb.axil_master, k*0x1000, 0x1000, count=16)))
while workers: while workers:
await workers.pop(0).join() await workers.pop(0).join()
@@ -285,7 +285,7 @@ def cycle_pause():
return itertools.cycle([1, 1, 1, 0]) return itertools.cycle([1, 1, 1, 0])
if cocotb.SIM_NAME: if getattr(cocotb, 'top', None) is not None:
for test in [run_test_write, run_test_read]: for test in [run_test_write, run_test_read]:

View File

@@ -1,6 +1,6 @@
/* /*
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2020 Alex Forencich # Copyright (c) 2020-2025 Alex Forencich
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
@@ -27,54 +27,30 @@ COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ns COCOTB_HDL_TIMEPRECISION = 1ns
DUT = test_axis DUT = test_axis
TOPLEVEL = $(DUT) COCOTB_TEST_MODULES = $(DUT)
MODULE = $(DUT) COCOTB_TOPLEVEL = $(DUT)
MODULE = $(COCOTB_TEST_MODULES)
TOPLEVEL = $(COCOTB_TOPLEVEL)
VERILOG_SOURCES += $(DUT).v VERILOG_SOURCES += $(DUT).v
# module parameters # module parameters
export PARAM_DATA_WIDTH ?= 8 export PARAM_DATA_WIDTH := 8
export PARAM_KEEP_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 ) export PARAM_KEEP_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 )
export PARAM_ID_WIDTH ?= 8 export PARAM_ID_WIDTH := 8
export PARAM_DEST_WIDTH ?= 8 export PARAM_DEST_WIDTH := 8
export PARAM_USER_WIDTH ?= 1 export PARAM_USER_WIDTH := 1
ifeq ($(SIM), icarus) ifeq ($(SIM), icarus)
PLUSARGS += -fst PLUSARGS += -fst
COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH) COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
COMPILE_ARGS += -P $(TOPLEVEL).KEEP_WIDTH=$(PARAM_KEEP_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).ID_WIDTH=$(PARAM_ID_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).DEST_WIDTH=$(PARAM_DEST_WIDTH)
COMPILE_ARGS += -P $(TOPLEVEL).USER_WIDTH=$(PARAM_USER_WIDTH)
ifeq ($(WAVES), 1)
VERILOG_SOURCES += iverilog_dump.v
COMPILE_ARGS += -s iverilog_dump
endif
else ifeq ($(SIM), verilator) else ifeq ($(SIM), verilator)
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
COMPILE_ARGS += -GDATA_WIDTH=$(PARAM_DATA_WIDTH)
COMPILE_ARGS += -GKEEP_WIDTH=$(PARAM_KEEP_WIDTH)
COMPILE_ARGS += -GID_WIDTH=$(PARAM_ID_WIDTH)
COMPILE_ARGS += -GDEST_WIDTH=$(PARAM_DEST_WIDTH)
COMPILE_ARGS += -GUSER_WIDTH=$(PARAM_USER_WIDTH)
ifeq ($(WAVES), 1) ifeq ($(WAVES), 1)
COMPILE_ARGS += --trace-fst COMPILE_ARGS += --trace-fst
VERILATOR_TRACE = 1
endif endif
endif endif
include $(shell cocotb-config --makefiles)/Makefile.sim include $(shell cocotb-config --makefiles)/Makefile.sim
iverilog_dump.v:
echo 'module iverilog_dump();' > $@
echo 'initial begin' >> $@
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
echo 'end' >> $@
echo 'endmodule' >> $@
clean::
@rm -rf iverilog_dump.v
@rm -rf dump.fst $(TOPLEVEL).fst

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -45,7 +45,7 @@ class TB:
self.log = logging.getLogger("cocotb.tb") self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG) self.log.setLevel(logging.DEBUG)
cocotb.fork(Clock(dut.clk, 2, units="ns").start()) cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
self.source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "axis"), dut.clk, dut.rst) self.source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "axis"), dut.clk, dut.rst)
self.sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "axis"), dut.clk, dut.rst) self.sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "axis"), dut.clk, dut.rst)
@@ -63,10 +63,10 @@ class TB:
self.dut.rst.setimmediatevalue(0) self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 1 self.dut.rst.value = 1
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 0 self.dut.rst.value = 0
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
@@ -135,7 +135,7 @@ def incrementing_payload(length):
return bytearray(itertools.islice(itertools.cycle(range(256)), length)) return bytearray(itertools.islice(itertools.cycle(range(256)), length))
if cocotb.SIM_NAME: if getattr(cocotb, 'top', None) is not None:
factory = TestFactory(run_test) factory = TestFactory(run_test)
factory.add_option("payload_lengths", [size_list]) factory.add_option("payload_lengths", [size_list])

View File

@@ -1,6 +1,6 @@
/* /*
Copyright (c) 2020 Alex Forencich Copyright (c) 2020-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -0,0 +1,44 @@
"""
Copyright (c) 2021-2025 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
from cocotbext.axi.buddy_allocator import BuddyAllocator
def test_allocator():
ba = BuddyAllocator(1024)
lst = []
for k in range(1, 32):
print(f"Alloc {k} bytes")
addr = ba.alloc(k)
print(f"Got address {addr}")
assert addr & (2**((k-1).bit_length())-1) == 0
lst.append(addr)
for addr in lst:
print(f"Free {addr}")
ba.free(addr)
assert ba.free_lists[-1] == [0]