Compare commits
176 Commits
v0.1.2
...
first_depl
| Author | SHA1 | Date | |
|---|---|---|---|
| 0496147ddf | |||
|
|
3e1e7fc1ec | ||
|
|
698c29b05f | ||
|
|
33f510688a | ||
|
|
da00960112 | ||
|
|
88b6624a93 | ||
|
|
dcb9a6bd02 | ||
|
|
7136dddd0a | ||
|
|
6c15d7d57d | ||
|
|
4595bd8a08 | ||
|
|
204ad7a517 | ||
|
|
f3dbc07100 | ||
|
|
a0a5b7ee55 | ||
|
|
a28ec41f79 | ||
|
|
f2bf8c0ed8 | ||
|
|
28f4585c08 | ||
|
|
775301c6fe | ||
|
|
39b4ca4a93 | ||
|
|
f70731a8d8 | ||
|
|
28bc97f226 | ||
|
|
e816d6a088 | ||
|
|
af377b2c11 | ||
|
|
cfb52c6130 | ||
|
|
7e32e584ff | ||
|
|
50cf2af49f | ||
|
|
ddfa1e3c92 | ||
|
|
5e8b246159 | ||
|
|
62c2eef4ec | ||
|
|
ad6012aea5 | ||
|
|
432bd81011 | ||
|
|
bde123e05f | ||
|
|
8604017159 | ||
|
|
e21b9ffcc8 | ||
|
|
47cd74eb6c | ||
|
|
4bf5945aa3 | ||
|
|
f3a7652362 | ||
|
|
a84ce5447d | ||
|
|
1c03ec4697 | ||
|
|
824eba793d | ||
|
|
a0aad34698 | ||
|
|
ede6270ed7 | ||
|
|
cd1a8b47a5 | ||
|
|
be6d490adb | ||
|
|
39686b849a | ||
|
|
706051cb89 | ||
|
|
3e4f8d7e92 | ||
|
|
afae9e69ff | ||
|
|
035c1ba803 | ||
|
|
873bb1a034 | ||
|
|
2d70e5cbe5 | ||
|
|
35d9742ae8 | ||
|
|
0f20e2e9bf | ||
|
|
7606d7d7bd | ||
|
|
dd345e87c3 | ||
|
|
9c0592c16a | ||
|
|
8aab5a7294 | ||
|
|
4f26621e2b | ||
|
|
1b6993d80d | ||
|
|
6d9ed8a2d2 | ||
|
|
d772b73eb2 | ||
|
|
bd88eda17b | ||
|
|
53313699a9 | ||
|
|
3f7193b77c | ||
|
|
2b0b12c68d | ||
|
|
4a91212f37 | ||
|
|
1608af26e5 | ||
|
|
31fb855311 | ||
|
|
cb4b0e1738 | ||
|
|
ea95eeaf0d | ||
|
|
079f4009b3 | ||
|
|
612a94c97a | ||
|
|
757e3a6f2d | ||
|
|
b9b9a2da72 | ||
|
|
f7660e9038 | ||
|
|
78693a63d5 | ||
|
|
34498f6e5d | ||
|
|
6329187ced | ||
|
|
da24857dd2 | ||
|
|
c08f22c710 | ||
|
|
d874d91d05 | ||
|
|
3fd016a84c | ||
|
|
43de2ea9b0 | ||
|
|
558ba51c91 | ||
|
|
f6426bd8f3 | ||
|
|
74dd47ca99 | ||
|
|
cde2056bb0 | ||
|
|
b6870716ed | ||
|
|
44da562db9 | ||
|
|
8dcdbfefb8 | ||
|
|
b8919a095b | ||
|
|
bc7edec289 | ||
|
|
e7c3a31eb0 | ||
|
|
ce907ffbb9 | ||
|
|
95e2d5800d | ||
|
|
82853b31ff | ||
|
|
8bbabd92df | ||
|
|
c060f6c963 | ||
|
|
a767e00ce5 | ||
|
|
d1d7313b98 | ||
|
|
01b43b97f2 | ||
|
|
b5b6df84fe | ||
|
|
babe69f4d3 | ||
|
|
c4873ad14c | ||
|
|
77a40bdc8f | ||
|
|
f991096272 | ||
|
|
9e28bd7fbb | ||
|
|
8c74f747a4 | ||
|
|
a285f008ca | ||
|
|
c677ab245c | ||
|
|
11f9db8b06 | ||
|
|
344ec8d4ce | ||
|
|
4ff390481e | ||
|
|
4bee96ea9a | ||
|
|
a66dfea6f7 | ||
|
|
f1a89e6c12 | ||
|
|
11205bde46 | ||
|
|
bce364eef5 | ||
|
|
e934b69776 | ||
|
|
7fb8c4e28b | ||
|
|
156fada616 | ||
|
|
d88ba7caf3 | ||
|
|
6c66776518 | ||
|
|
a71678c7e3 | ||
|
|
c0ebb90cd4 | ||
|
|
56caf57fa4 | ||
|
|
abb78308ff | ||
|
|
f19ca9f651 | ||
|
|
1c40b8fa58 | ||
|
|
cfd5dae6ea | ||
|
|
f2c36276f3 | ||
|
|
eab0c7fee0 | ||
|
|
35ed1472d6 | ||
|
|
a7fe5d9674 | ||
|
|
08122c1a65 | ||
|
|
8e76f2d24c | ||
|
|
69717c1698 | ||
|
|
c18fdd6e22 | ||
|
|
222af15437 | ||
|
|
ffaf8932fc | ||
|
|
8f719daf75 | ||
|
|
154d3b11f4 | ||
|
|
97abb032d8 | ||
|
|
5593bfe296 | ||
|
|
3caa343845 | ||
|
|
6654c707b2 | ||
|
|
359e015b35 | ||
|
|
7e0464f6f3 | ||
|
|
4ec0c36892 | ||
|
|
7c18f9b73f | ||
|
|
2e79ef233f | ||
|
|
7748f871f4 | ||
|
|
f6823fc8fd | ||
|
|
c63d65bb87 | ||
|
|
1618220a30 | ||
|
|
ecb2f5ae81 | ||
|
|
fcf6374c3c | ||
|
|
7582067929 | ||
|
|
7b7e8bf3e2 | ||
|
|
426c5828f1 | ||
|
|
44e58275ad | ||
|
|
f4f7c0b59d | ||
|
|
ad83d58b01 | ||
|
|
4336c32b40 | ||
|
|
8668a6bb4f | ||
|
|
bdaeaad66b | ||
|
|
cd272b2a59 | ||
|
|
01212e37cd | ||
|
|
40a9bb9eac | ||
|
|
1103783b08 | ||
|
|
2451921923 | ||
|
|
e6e8a06dfe | ||
|
|
3dd8114c05 | ||
|
|
a84b52077b | ||
|
|
200c8c0b26 | ||
|
|
0ff3d64540 | ||
|
|
237745792c |
63
.github/workflows/build.yaml
vendored
Normal file
63
.github/workflows/build.yaml
vendored
Normal 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/*
|
||||
8
.github/workflows/regression-tests.yml
vendored
8
.github/workflows/regression-tests.yml
vendored
@@ -5,17 +5,17 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
name: Python ${{matrix.python-version}}
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
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:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
358
README.md
358
README.md
@@ -1,14 +1,15 @@
|
||||
# AXI interface modules for Cocotb
|
||||
|
||||
[](https://github.com/alexforencich/cocotbext-axi/actions/)
|
||||
[](https://github.com/alexforencich/cocotbext-axi/actions/workflows/regression-tests.yml)
|
||||
[](https://codecov.io/gh/alexforencich/cocotbext-axi)
|
||||
[](https://pypi.org/project/cocotbext-axi)
|
||||
[](https://pepy.tech/project/cocotbext-axi)
|
||||
|
||||
GitHub repository: https://github.com/alexforencich/cocotbext-axi
|
||||
|
||||
## Introduction
|
||||
|
||||
AXI, AXI lite, and AXI stream simulation models for cocotb.
|
||||
AXI, AXI lite, AXI stream, and APB simulation models for [cocotb](https://github.com/cocotb/cocotb).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -27,57 +28,48 @@ Installation for active development:
|
||||
|
||||
## 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:
|
||||
|
||||
from cocotbext.axi import AxiMaster
|
||||
from cocotbext.axi import AxiBus, AxiMaster
|
||||
|
||||
axi_master = AxiMaster(dut, "s_axi", dut.clk, dut.rst)
|
||||
axi_master = AxiMaster(AxiBus.from_prefix(dut, "s_axi"), dut.clk, dut.rst)
|
||||
|
||||
The modules use `cocotb.bus.Bus` internally to automatically connect to the corresponding signals in the bus, presuming they are named according to the AXI spec and have a common prefix.
|
||||
The first argument to the constructor accepts an `AxiBus` or `AxiLiteBus` object, as appropriate. These objects are containers for the interface signals and include class methods to automate connections.
|
||||
|
||||
Once the module is instantiated, read and write operations can be initiated in a few different ways.
|
||||
Once the module is instantiated, read and write operations can be initiated in a couple of different ways.
|
||||
|
||||
First, non-blocking operations can be started with `init_read()` and `init_write()`. These methods will queue up a read or write operation to be carried out over the interface. The result of the operation can be retrieved with `get_read_data()` and `get_write_resp()`. To monitor the status of the module, `idle()`, `wait()`, `wait_read()`, and `wait_write()` can be used. For example:
|
||||
|
||||
axi_master.init_write(0x0000, b'test')
|
||||
await axi_master.wait()
|
||||
resp = axi_master.get_write_resp()
|
||||
axi_master.init_read(0x0000, 4)
|
||||
await axi_master.wait()
|
||||
data = axi_master.get_read_data()
|
||||
|
||||
Alternatively, an event object can be provided as an argument to `init_read()` and `init_write()`, and the result can be retrieved from `Event.data`. For example:
|
||||
|
||||
event = Event()
|
||||
axi_master.init_write(0x0000, b'test', event=event)
|
||||
await event.wait()
|
||||
resp = event.data
|
||||
event = Event()
|
||||
axi_master.init_read(0x0000, 4, event=event)
|
||||
await event.wait()
|
||||
resp = event.data
|
||||
|
||||
Second, blocking operations can be carried out with `read()` and `write()` and their associated word-access wrappers. Multiple concurrent operations started from different coroutines are handled correctly. For example:
|
||||
First, operations can be carried out with async blocking `read()`, `write()`, and their associated word-access wrappers. Multiple concurrent operations started from different coroutines are handled correctly, with results returned in the order that the operations complete. For example:
|
||||
|
||||
await axi_master.write(0x0000, b'test')
|
||||
data = await axi_master.read(0x0000, 4)
|
||||
|
||||
`read()`, `write()`, `get_read_data()`, and `get_write_resp()` return `namedtuple` objects containing _address_, _data_ or _length_, and _resp_.
|
||||
Additional parameters can be specified to control sideband signals and burst settings. The transfer will be split into one or more bursts according to the AXI specification. All bursts generated from the same call to `read()` or `write()` will use the same ID, which will be automatically generated if not specified. `read()` and `write()` return `namedtuple` objects containing _address_, _data_ or _length_, and _resp_. This is the preferred style, and this is the only style supported by the word-access wrappers.
|
||||
|
||||
#### `AxiMaster` and `AxiLiteMaster` constructor parameters
|
||||
Alternatively, operations can be initiated with non-blocking `init_read()` and `init_write()`. These functions return `Event` objects which are triggered when the operation completes, and the result can be retrieved from `Event.data`. For example:
|
||||
|
||||
* _entity_: object that contains the AXI slave interface signals
|
||||
* _name_: signal name prefix (e.g. for `s_axi_awaddr`, the prefix is `s_axi`)
|
||||
write_op = axi_master.init_write(0x0000, b'test')
|
||||
await write_op.wait()
|
||||
resp = write_op.data
|
||||
read_op = axi_master.init_read(0x0000, 4)
|
||||
await read_op.wait()
|
||||
resp = read_op.data
|
||||
|
||||
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`, `AxiLiteMaster`, and `ApbMaster` 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`)
|
||||
|
||||
#### Additional parameters for `AxiMaster`
|
||||
|
||||
@@ -85,32 +77,28 @@ Second, blocking operations can be carried out with `read()` and `write()` and t
|
||||
|
||||
#### Methods
|
||||
|
||||
* `init_read(address, length, ...)`: initiate reading _length_ bytes, starting at _address_
|
||||
* `init_write(address, data, ...)`: initiate writing _data_ (bytes), starting from _address_
|
||||
* `init_read(address, length, ...)`: initiate reading _length_ bytes, starting at _address_. Returns an `Event` object.
|
||||
* `init_write(address, data, ...)`: initiate writing _data_ (bytes), starting from _address_. Returns an `Event` object.
|
||||
* `idle()`: returns _True_ when there are no outstanding operations in progress
|
||||
* `wait()`: blocking wait until all outstanding operations complete
|
||||
* `wait_read()`: wait until all outstanding read operations complete
|
||||
* `wait_write()`: wait until all outstanding write operations complete
|
||||
* `read_data_ready()`: determine if any read read data is available
|
||||
* `get_read_data()`: fetch first available read data
|
||||
* `write_resp_ready()`: determine if any write response is available
|
||||
* `get_write_resp()`: fetch first available write response
|
||||
* `read(address, length, ...)`: read _length_ bytes, starting at _address_
|
||||
* `read_words(address, count, byteorder, ws, ...)`: read _count_ _ws_-byte words, starting at _address_, default word size of `2`, default _byteorder_ `"little"`
|
||||
* `read_dwords(address, count, byteorder, ...)`: read _count_ 4-byte dwords, starting at _address_, default _byteorder_ `"little"`
|
||||
* `read_qwords(address, count, byteorder, ...)`: read _count_ 8-byte qwords, starting at _address_, default _byteorder_ `"little"`
|
||||
* `read_words(address, count, byteorder='little', ws=2, ...)`: read _count_ _ws_-byte words, starting at _address_
|
||||
* `read_dwords(address, count, byteorder='little', ...)`: read _count_ 4-byte dwords, starting at _address_
|
||||
* `read_qwords(address, count, byteorder='little', ...)`: read _count_ 8-byte qwords, starting at _address_
|
||||
* `read_byte(address, ...)`: read single byte at _address_
|
||||
* `read_word(address, byteorder, ws, ...)`: read single _ws_-byte word at _address_, default word size of `2`, default _byteorder_ `"little"`
|
||||
* `read_dword(address, byteorder, ...)`: read single 4-byte dword at _address_, default _byteorder_ `"little"`
|
||||
* `read_qword(address, byteorder, ...)`: read single 8-byte qword at _address_, default _byteorder_ `"little"`
|
||||
* `read_word(address, byteorder='little', ws=2, ...)`: read single _ws_-byte word at _address_
|
||||
* `read_dword(address, byteorder='little', ...)`: read single 4-byte dword at _address_
|
||||
* `read_qword(address, byteorder='little', ...)`: read single 8-byte qword at _address_
|
||||
* `write(address, data, ...)`: write _data_ (bytes), starting at _address_
|
||||
* `write_words(address, data, byteorder, ws, ...)`: write _data_ (_ws_-byte words), starting at _address_, default word size of `2`, default _byteorder_ `"little"`
|
||||
* `write_dwords(address, data, byteorder, ...)`: write _data_ (4-byte dwords), starting at _address_, default _byteorder_ `"little"`
|
||||
* `write_qwords(address, data, byteorder, ...)`: write _data_ (8-byte qwords), starting at _address_, default _byteorder_ `"little"`
|
||||
* `write_words(address, data, byteorder='little', ws=2, ...)`: write _data_ (_ws_-byte words), starting at _address_
|
||||
* `write_dwords(address, data, byteorder='little', ...)`: write _data_ (4-byte dwords), starting at _address_
|
||||
* `write_qwords(address, data, byteorder='little', ...)`: write _data_ (8-byte qwords), starting at _address_
|
||||
* `write_byte(address, data, ...)`: write single byte at _address_
|
||||
* `write_word(address, data, byteorder, ws, ...)`: write single _ws_-byte word at _address_, default word size of `2`, default _byteorder_ `"little"`
|
||||
* `write_dword(address, data, byteorder, ...)`: write single 4-byte dword at _address_, default _byteorder_ `"little"`
|
||||
* `write_qword(address, data, byteorder, ...)`: write single 8-byte qword at _address_, default _byteorder_ `"little"`
|
||||
* `write_word(address, data, byteorder='little', ws=2, ...)`: write single _ws_-byte word at _address_
|
||||
* `write_dword(address, data, byteorder='little', ...)`: write single 4-byte dword at _address_
|
||||
* `write_qword(address, data, byteorder='little', ...)`: write single 8-byte qword at _address_
|
||||
|
||||
#### Additional optional arguments for `AxiMaster`
|
||||
|
||||
@@ -124,97 +112,139 @@ Second, blocking operations can be carried out with `read()` and `write()` and t
|
||||
* _region_: AXI region field, default `0`
|
||||
* _user_: AXI user signal (awuser/aruser), default `0`
|
||||
* _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` (`init_read()` and `init_write()` only). If provided, the event will be triggered when the operation completes and the result returned via `Event.data` instead of `get_read_data()` or `get_write_resp()`.
|
||||
* _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`
|
||||
* _event_: `Event` object used to wait on and retrieve result for specific operation, default `None` (`init_read()` and `init_write()` only). If provided, the event will be triggered when the operation completes and the result returned via `Event.data` instead of `get_read_data()` or `get_write_resp()`.
|
||||
* _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)
|
||||
|
||||
### AXI and AXI lite RAM
|
||||
#### `AxiBus`, `AxiLiteBus`, and `ApbBus` objects
|
||||
|
||||
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 `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`.
|
||||
|
||||
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.
|
||||
### AXI, AXI lite, and APB slave
|
||||
|
||||
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 `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 AxiRam
|
||||
from cocotbext.axi import AxiBus, AxiSlave, MemoryRegion
|
||||
|
||||
axi_ram = AxiRam(dut, "m_axi", dut.clk, dut.rst, size=2**16)
|
||||
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 modules use `cocotb.bus.Bus` internally to automatically connect to the corresponding signals in the bus, presuming they are named according to the AXI spec and have a common prefix.
|
||||
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:
|
||||
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.
|
||||
|
||||
axi_ram.write(0x0000, b'test')
|
||||
data = axi_ram.read(0x0000, 4)
|
||||
#### `AxiSlave`, `AxiLiteSlave`, and `ApbSlave` constructor parameters
|
||||
|
||||
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(dut, "m00_axi", dut.clk, dut.rst, size=2**16)
|
||||
axi_ram_p2 = AxiRam(dut, "m01_axi", dut.clk, dut.rst, mem=axi_ram_p1.mem)
|
||||
axi_ram_p3 = AxiRam(dut, "m02_axi", dut.clk, dut.rst, mem=axi_ram_p1.mem)
|
||||
axi_ram_p4 = AxiRam(dut, "m03_axi", dut.clk, dut.rst, mem=axi_ram_p1.mem)
|
||||
|
||||
#### `AxiRam` and `AxiLiteRam` constructor parameters
|
||||
|
||||
* _entity_: object that contains the AXI master interface signals
|
||||
* _name_: signal name prefix (e.g. for `m_axi_awaddr`, the prefix is `m_axi`)
|
||||
* _bus_: `AxiBus`, `AxiLiteBus`, or `ApbBus` object containing interface signals
|
||||
* _clock_: clock signal
|
||||
* _reset_: reset signal (optional)
|
||||
* _size_: memory size in bytes (optional, default 1024)
|
||||
* _mem_: mmap object to use (optional, overrides _size_)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
* _target_: target region (optional, default `None`)
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _mem_: directly access shared `mmap` object
|
||||
* _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:
|
||||
|
||||
from cocotbext.axi import AxiBus, AxiRam
|
||||
|
||||
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`, `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`/`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')
|
||||
data = axi_ram.read(0x0000, 4)
|
||||
axi_ram.hexdump(0x0000, 4, prefix="RAM")
|
||||
|
||||
Multi-port memories can be constructed by passing the `mem` object of the first instance to the other instances. For example, here is how to create a four-port RAM:
|
||||
|
||||
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_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)
|
||||
|
||||
#### `AxiRam`, `AxiLiteRam`, and `ApbRam` 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`)
|
||||
* _size_: memory size in bytes (optional, default `2**64`)
|
||||
* _mem_: `mmap` or `SparseMemory` backing object to use (optional, overrides _size_)
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _mem_: directly access shared `mmap` or `SparseMemory` backing object
|
||||
|
||||
#### Methods
|
||||
|
||||
* `read(address, length)`: read _length_ bytes, starting at _address_
|
||||
* `read_words(address, count, byteorder, ws)`: read _count_ _ws_-byte words, starting at _address_, default word size of `2`, default _byteorder_ `"little"`
|
||||
* `read_dwords(address, count, byteorder)`: read _count_ 4-byte dwords, starting at _address_, default _byteorder_ `"little"`
|
||||
* `read_qwords(address, count, byteorder)`: read _count_ 8-byte qwords, starting at _address_, default _byteorder_ `"little"`
|
||||
* `read_words(address, count, byteorder='little', ws=2)`: read _count_ _ws_-byte words, starting at _address_
|
||||
* `read_dwords(address, count, byteorder='little')`: read _count_ 4-byte dwords, starting at _address_
|
||||
* `read_qwords(address, count, byteorder='little')`: read _count_ 8-byte qwords, starting at _address_
|
||||
* `read_byte(address)`: read single byte at _address_
|
||||
* `read_word(address, byteorder, ws)`: read single _ws_-byte word at _address_, default word size of `2`, default _byteorder_ `"little"`
|
||||
* `read_dword(address, byteorder)`: read single 4-byte dword at _address_, default _byteorder_ `"little"`
|
||||
* `read_qword(address, byteorder)`: read single 8-byte qword at _address_, default _byteorder_ `"little"`
|
||||
* `read_word(address, byteorder='little', ws=2)`: read single _ws_-byte word at _address_
|
||||
* `read_dword(address, byteorder='little')`: read single 4-byte dword at _address_
|
||||
* `read_qword(address, byteorder='little')`: read single 8-byte qword at _address_
|
||||
* `write(address, data)`: write _data_ (bytes), starting at _address_
|
||||
* `write_words(address, data, byteorder, ws)`: write _data_ (_ws_-byte words), starting at _address_, default word size of `2`, default _byteorder_ `"little"`
|
||||
* `write_dwords(address, data, byteorder)`: write _data_ (4-byte dwords), starting at _address_, default _byteorder_ `"little"`
|
||||
* `write_qwords(address, data, byteorder)`: write _data_ (8-byte qwords), starting at _address_, default _byteorder_ `"little"`
|
||||
* `write_words(address, data, byteorder='little', ws=2)`: write _data_ (_ws_-byte words), starting at _address_
|
||||
* `write_dwords(address, data, byteorder='little')`: write _data_ (4-byte dwords), starting at _address_
|
||||
* `write_qwords(address, data, byteorder='little')`: write _data_ (8-byte qwords), starting at _address_
|
||||
* `write_byte(address, data)`: write single byte at _address_
|
||||
* `write_word(address, data, byteorder, ws)`: write single _ws_-byte word at _address_, default word size of `2`, default _byteorder_ `"little"`
|
||||
* `write_dword(address, data, byteorder)`: write single 4-byte dword at _address_, default _byteorder_ `"little"`
|
||||
* `write_qword(address, data, byteorder)`: write single 8-byte qword at _address_, default _byteorder_ `"little"`
|
||||
* `hexdump(address, length, prefix)`: print hex dump of _length_ bytes starting from `address`, prefix lines with optional `prefix`
|
||||
* `hexdump_line(address, length, prefix)`: return hex dump (list of str) of _length_ bytes starting from `address`, prefix lines with optional `prefix`
|
||||
* `hexdump_str(address, length, prefix)`: return hex dump (str) of _length_ bytes starting from `address`, prefix lines with optional `prefix`
|
||||
* `write_word(address, data, byteorder='little', ws=2)`: write single _ws_-byte word at _address_
|
||||
* `write_dword(address, data, byteorder='little')`: write single 4-byte dword at _address_
|
||||
* `write_qword(address, data, byteorder='little')`: write single 8-byte qword at _address_
|
||||
* `hexdump(address, length, prefix='')`: print hex dump of _length_ bytes starting from _address_, prefix lines with optional _prefix_
|
||||
* `hexdump_line(address, length, prefix='')`: return hex dump (list of str) of _length_ bytes starting from _address_, prefix lines with optional _prefix_
|
||||
* `hexdump_str(address, length, prefix='')`: return hex dump (str) of _length_ bytes starting from _address_, prefix lines with optional _prefix_
|
||||
|
||||
### AXI stream
|
||||
|
||||
The `AxiStreamSource`, `AxiStreamSink`, and `AxiStreamMonitor` classes can be used to drive, receive, and monitor traffic on AXI stream interfaces. The `AxiStreamSource` drives all signals except for `tready` and can be used to drive AXI stream traffic into a design. The `AxiStreamSink` drives the `tready` line only and as such can receive AXI stream traffic and exert backpressure. The `AxiStreamMonitor` drives no signals and as such can be connected to internal AXI stream interfaces to monitor traffic.
|
||||
The `AxiStreamSource`, `AxiStreamSink`, and `AxiStreamMonitor` classes can be used to drive, receive, and monitor traffic on AXI stream interfaces. The `AxiStreamSource` drives all signals except for `tready` and can be used to drive AXI stream traffic into a design. The `AxiStreamSink` drives the `tready` line only and as such can receive AXI stream traffic and exert backpressure. The `AxiStreamMonitor` drives no signals and as such can be connected to AXI stream interfaces anywhere within a design to passively monitor traffic.
|
||||
|
||||
To use these modules, import the one you need and connect it to the DUT:
|
||||
|
||||
from cocotbext.axi import AxiStreamSource, AxiStreamSink
|
||||
from cocotbext.axi import (AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamMonitor)
|
||||
|
||||
axis_source = AxiStreamSource(dut, "s_axis", dut.clk, dut.rst)
|
||||
axis_sink = AxiStreamSink(dut, "m_axis", dut.clk, dut.rst)
|
||||
axis_monitor = AxiStreamMonitor(dut.inst, "int_axis", dut.clk, dut.rst)
|
||||
axis_source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "s_axis"), dut.clk, dut.rst)
|
||||
axis_sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "m_axis"), dut.clk, dut.rst)
|
||||
axis_mon= AxiStreamMonitor(AxiStreamBus.from_prefix(dut.inst, "int_axis"), dut.clk, dut.rst)
|
||||
|
||||
The modules use `cocotb.bus.Bus` internally to automatically connect to the corresponding signals in the bus, presuming they are named according to the AXI spec and have a common prefix.
|
||||
The first argument to the constructor accepts an `AxiStreamBus` object. This object is a container for the interface signals and includes class methods to automate connections.
|
||||
|
||||
To send data into a design with an `AxiStreamSource`, call `send()` or `write()`. Accepted data types are iterables or `AxiStreamFrame` objects. Call `wait()` to wait for the transmit operation to complete. Example:
|
||||
To send data into a design with an `AxiStreamSource`, call `send()`/`send_nowait()` or `write()`/`write_nowait()`. Accepted data types are iterables or `AxiStreamFrame` objects. Optionally, call `wait()` to wait for the transmit operation to complete. Example:
|
||||
|
||||
axis_source.send(b'test data')
|
||||
await axis_source.send(b'test data')
|
||||
# wait for operation to complete (optional)
|
||||
await axis_source.wait()
|
||||
|
||||
To receive data with an `AxiStreamSink` or `AxiStreamMonitor`, call `recv()` or `read()`. `recv()` is intended for use with a frame-oriented interface, and by default compacts `AxiStreamFrame`s before returning them. `read()` is intended for non-frame-oriented streams. Calling `read()` internally calls `recv()` for all frames currently in the queue, then compacts and coalesces `tdata` from all frames into a separate read queue, from which read data is returned. All sideband data is discarded. Call `wait()` to wait for new receive data.
|
||||
It is also possible to wait for the transmission of a specific frame to complete by passing an event in the tx_complete field of the `AxiStreamFrame` object, and then awaiting the event. The frame, with simulation time fields set, will be returned in the event data. Example:
|
||||
|
||||
await axis_sink.wait()
|
||||
data = axis_sink.recv()
|
||||
frame = AxiStreamFrame(b'test data', tx_complete=Event())
|
||||
await axis_source.send(frame)
|
||||
await frame.tx_complete.wait()
|
||||
print(frame.tx_complete.data.sim_time_start)
|
||||
|
||||
To receive data with an `AxiStreamSink` or `AxiStreamMonitor`, call `recv()`/`recv_nowait()` or `read()`/`read_nowait()`. Optionally call `wait()` to wait for new receive data. `recv()` is intended for use with a frame-oriented interface, and by default compacts `AxiStreamFrame`s before returning them. `read()` is intended for non-frame-oriented streams. Calling `read()` internally calls `recv()` for all frames currently in the queue, then compacts and coalesces `tdata` from all frames into a separate read queue, from which read data is returned. All sideband data is discarded.
|
||||
|
||||
data = await axis_sink.recv()
|
||||
|
||||
#### Signals
|
||||
|
||||
@@ -229,36 +259,50 @@ To receive data with an `AxiStreamSink` or `AxiStreamMonitor`, call `recv()` or
|
||||
|
||||
#### Constructor parameters:
|
||||
|
||||
* _entity_: object that contains the AXI stream interface signals
|
||||
* _name_: signal name prefix (e.g. for `m_axis_tdata`, the prefix is `m_axis`)
|
||||
* _bus_: `AxiStreamBus` object containing AXI stream interface signals
|
||||
* _clock_: clock signal
|
||||
* _reset_: reset signal (optional)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
* _byte_size_: byte size (optional)
|
||||
* _byte_lanes_: byte lane count (optional)
|
||||
|
||||
Note: _byte_size_, _byte_lanes_, `len(tdata)`, and `len(tkeep)` are all related, in that _byte_lanes_ is set from `tkeep` if it is connected, and `byte_size*byte_lanes == len(tdata)`. So, if `tkeep` is connected, both _byte_size_ and _byte_lanes_ will be computed internally and cannot be overridden. If `tkeep` is not connected, then either _byte_size_ or _byte_lanes_ can be specified, and the other will be computed such that `byte_size*byte_lanes == len(tdata)`.
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _pause_: stall the interface (deassert `tready` or `tvalid`) (source/sink only)
|
||||
* _queue_occupancy_bytes_: number of bytes in queue (all)
|
||||
* _queue_occupancy_frames_: number of frames in queue (all)
|
||||
* _queue_occupancy_limit_bytes_: max number of bytes in queue allowed before tready deassert (sink only)
|
||||
* _queue_occupancy_limit_frames_: max number of frames in queue allowed before tready deassert (sink only)
|
||||
* _queue_occupancy_limit_bytes_: max number of bytes in queue allowed before backpressure is applied (source/sink only)
|
||||
* _queue_occupancy_limit_frames_: max number of frames in queue allowed before backpressure is applied (source/sink only)
|
||||
|
||||
#### Methods
|
||||
|
||||
* `send(frame)`: send _frame_ (source)
|
||||
* `write(data)`: send _data_ (alias of send) (source)
|
||||
* `recv(compact)`: receive a frame, optionally compact frame (sink/monitor)
|
||||
* `read(count)`: read _count_ bytes from buffer (sink/monitor)
|
||||
* `send(frame)`: send _frame_ (blocking) (source)
|
||||
* `send_nowait(frame)`: send _frame_ (non-blocking) (source)
|
||||
* `write(data)`: send _data_ (alias of send) (blocking) (source)
|
||||
* `write_nowait(data)`: send _data_ (alias of send_nowait) (non-blocking) (source)
|
||||
* `recv(compact=True)`: receive a frame as a `GmiiFrame` (blocking) (sink)
|
||||
* `recv_nowait(compact=True)`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
||||
* `read(count)`: read _count_ bytes from buffer (blocking) (sink/monitor)
|
||||
* `read_nowait(count)`: read _count_ bytes from buffer (non-blocking) (sink/monitor)
|
||||
* `count()`: returns the number of items in the queue (all)
|
||||
* `empty()`: returns _True_ if the queue is empty (all)
|
||||
* `full()`: returns _True_ if the queue occupancy limits are met (sink)
|
||||
* `full()`: returns _True_ if the queue occupancy limits are met (source/sink)
|
||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||
* `wait(timeout=0, timeout_unit='ns')`: wait for idle (source) or frame received (sink/monitor)
|
||||
* `clear()`: drop all data in queue (all)
|
||||
* `wait()`: wait for idle (source)
|
||||
* `wait(timeout=0, timeout_unit='ns')`: wait for frame received (sink)
|
||||
* `set_pause_generator(generator)`: set generator for pause signal, generator will be advanced on every clock cycle (source/sink)
|
||||
* `clear_pause_generator()`: remove generator for pause signal
|
||||
* `clear_pause_generator()`: remove generator for pause signal (source/sink)
|
||||
|
||||
#### AxiStreamFrame object
|
||||
#### `AxiStreamBus` object
|
||||
|
||||
The `AxiStreamFrame` object is a container for a frame to be transferred via AXI stream. The `tdata` field contains the packet data in the form of a list of bytes, a `bytearray` if the byte size is 8 bits or a `list` of `int`s otherwise. `tkeep`, `tid`, `tdest`, and `tuser` can either be `None`, an `int`, or a `list` of `int`s.
|
||||
The `AxiStreamBus` object is a container for the interface signals. Currently, it is an extension of `cocotb.bus.Bus`. Class methods `from_entity` and `from_prefix` are provided to facilitate signal name matching.
|
||||
|
||||
#### `AxiStreamFrame` object
|
||||
|
||||
The `AxiStreamFrame` object is a container for a frame to be transferred via AXI stream. The `tdata` field contains the packet data in the form of a list of bytes, which is either a `bytearray` if the byte size is 8 bits or a `list` of `int`s otherwise. `tkeep`, `tid`, `tdest`, and `tuser` can either be `None`, an `int`, or a `list` of `int`s.
|
||||
|
||||
Attributes:
|
||||
|
||||
@@ -267,12 +311,77 @@ Attributes:
|
||||
* `tid`: tid field, optional; int or list with one entry per `tdata`, last value used per cycle when sending.
|
||||
* `tdest`: tdest field, optional; int or list with one entry per `tdata`, last value used per cycle when sending.
|
||||
* `tuser`: tuser field, optional; int or list with one entry per `tdata`, last value used per cycle when sending.
|
||||
* `sim_time_start`: simulation time of first transfer cycle of frame.
|
||||
* `sim_time_end`: simulation time of last transfer cycle of frame.
|
||||
* `tx_complete`: event or callable triggered when frame is transmitted.
|
||||
|
||||
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.
|
||||
* `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
|
||||
|
||||
* Write address channel
|
||||
@@ -362,3 +471,16 @@ Methods:
|
||||
* `tid`: ID signal, can be used for routing
|
||||
* `tdest`: destination signal, can be used for routing
|
||||
* `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
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -26,10 +26,22 @@ from .version import __version__
|
||||
|
||||
from .constants import AxiBurstType, AxiBurstSize, AxiLockType, AxiCacheBit, AxiProt, AxiResp
|
||||
|
||||
from .axis import AxiStreamFrame, AxiStreamSource, AxiStreamSink, AxiStreamMonitor
|
||||
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 .axil_channels import AxiLiteAWBus, AxiLiteWBus, AxiLiteBBus, AxiLiteARBus, AxiLiteRBus
|
||||
from .axil_channels import AxiLiteWriteBus, AxiLiteReadBus, AxiLiteBus
|
||||
from .axil_master import AxiLiteMasterWrite, AxiLiteMasterRead, AxiLiteMaster
|
||||
from .axil_slave import AxiLiteSlaveWrite, AxiLiteSlaveRead, AxiLiteSlave
|
||||
from .axil_ram import AxiLiteRamWrite, AxiLiteRamRead, AxiLiteRam
|
||||
|
||||
from .axi_channels import AxiAWBus, AxiWBus, AxiBBus, AxiARBus, AxiRBus
|
||||
from .axi_channels import AxiWriteBus, AxiReadBus, AxiBus
|
||||
from .axi_master import AxiMasterWrite, AxiMasterRead, AxiMaster
|
||||
from .axi_slave import AxiSlaveWrite, AxiSlaveRead, AxiSlave
|
||||
from .axi_ram import AxiRamWrite, AxiRamRead, AxiRam
|
||||
|
||||
from .apb import ApbBus, ApbMaster, ApbSlave, ApbRam
|
||||
|
||||
362
cocotbext/axi/address_space.py
Normal file
362
cocotbext/axi/address_space.py
Normal 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
|
||||
622
cocotbext/axi/apb.py
Normal file
622
cocotbext/axi/apb.py
Normal file
@@ -0,0 +1,622 @@
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -25,38 +25,109 @@ THE SOFTWARE.
|
||||
from .stream import define_stream
|
||||
|
||||
# Write address channel
|
||||
AxiAWTransaction, AxiAWSource, AxiAWSink, AxiAWMonitor = define_stream("AxiAW",
|
||||
signals=["awid", "awaddr", "awlen", "awsize", "awburst", "awprot", "awvalid", "awready"],
|
||||
optional_signals=["awlock", "awcache", "awqos", "awregion", "awuser"],
|
||||
AxiAWBus, AxiAWTransaction, AxiAWSource, AxiAWSink, AxiAWMonitor = define_stream("AxiAW",
|
||||
signals=["awid", "awaddr", "awlen", "awsize", "awburst", "awvalid", "awready"],
|
||||
optional_signals=["awlock", "awcache", "awprot", "awqos", "awregion", "awuser"],
|
||||
signal_widths={"awlen": 8, "awsize": 3, "awburst": 2, "awlock": 1,
|
||||
"awcache": 4, "awprot": 3, "awqos": 4, "awregion": 4}
|
||||
)
|
||||
|
||||
# Write data channel
|
||||
AxiWTransaction, AxiWSource, AxiWSink, AxiWMonitor = define_stream("AxiW",
|
||||
signals=["wdata", "wstrb", "wlast", "wvalid", "wready"],
|
||||
optional_signals=["wuser"],
|
||||
AxiWBus, AxiWTransaction, AxiWSource, AxiWSink, AxiWMonitor = define_stream("AxiW",
|
||||
signals=["wdata", "wlast", "wvalid", "wready"],
|
||||
optional_signals=["wstrb", "wuser"],
|
||||
signal_widths={"wlast": 1}
|
||||
)
|
||||
|
||||
# Write response channel
|
||||
AxiBTransaction, AxiBSource, AxiBSink, AxiBMonitor = define_stream("AxiB",
|
||||
signals=["bid", "bresp", "bvalid", "bready"],
|
||||
optional_signals=["buser"],
|
||||
AxiBBus, AxiBTransaction, AxiBSource, AxiBSink, AxiBMonitor = define_stream("AxiB",
|
||||
signals=["bid", "bvalid", "bready"],
|
||||
optional_signals=["bresp", "buser"],
|
||||
signal_widths={"bresp": 2}
|
||||
)
|
||||
|
||||
# Read address channel
|
||||
AxiARTransaction, AxiARSource, AxiARSink, AxiARMonitor = define_stream("AxiAR",
|
||||
signals=["arid", "araddr", "arlen", "arsize", "arburst", "arprot", "arvalid", "arready"],
|
||||
optional_signals=["arlock", "arcache", "arqos", "arregion", "aruser"],
|
||||
AxiARBus, AxiARTransaction, AxiARSource, AxiARSink, AxiARMonitor = define_stream("AxiAR",
|
||||
signals=["arid", "araddr", "arlen", "arsize", "arburst", "arvalid", "arready"],
|
||||
optional_signals=["arlock", "arcache", "arprot", "arqos", "arregion", "aruser"],
|
||||
signal_widths={"arlen": 8, "arsize": 3, "arburst": 2, "arlock": 1,
|
||||
"arcache": 4, "arprot": 3, "arqos": 4, "arregion": 4}
|
||||
)
|
||||
|
||||
# Read data channel
|
||||
AxiRTransaction, AxiRSource, AxiRSink, AxiRMonitor = define_stream("AxiR",
|
||||
signals=["rid", "rdata", "rresp", "rlast", "rvalid", "rready"],
|
||||
optional_signals=["ruser"],
|
||||
AxiRBus, AxiRTransaction, AxiRSource, AxiRSink, AxiRMonitor = define_stream("AxiR",
|
||||
signals=["rid", "rdata", "rlast", "rvalid", "rready"],
|
||||
optional_signals=["rresp", "ruser"],
|
||||
signal_widths={"rresp": 2, "rlast": 1}
|
||||
)
|
||||
|
||||
|
||||
class AxiWriteBus:
|
||||
def __init__(self, aw=None, w=None, b=None):
|
||||
self.aw = aw
|
||||
self.w = w
|
||||
self.b = b
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, entity, **kwargs):
|
||||
aw = AxiAWBus.from_entity(entity, **kwargs)
|
||||
w = AxiWBus.from_entity(entity, **kwargs)
|
||||
b = AxiBBus.from_entity(entity, **kwargs)
|
||||
return cls(aw, w, b)
|
||||
|
||||
@classmethod
|
||||
def from_prefix(cls, entity, prefix, **kwargs):
|
||||
aw = AxiAWBus.from_prefix(entity, prefix, **kwargs)
|
||||
w = AxiWBus.from_prefix(entity, prefix, **kwargs)
|
||||
b = AxiBBus.from_prefix(entity, prefix, **kwargs)
|
||||
return cls(aw, w, b)
|
||||
|
||||
@classmethod
|
||||
def from_channels(cls, aw, w, b):
|
||||
return cls(aw, w, b)
|
||||
|
||||
|
||||
class AxiReadBus:
|
||||
def __init__(self, ar=None, r=None):
|
||||
self.ar = ar
|
||||
self.r = r
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, entity, **kwargs):
|
||||
ar = AxiARBus.from_entity(entity, **kwargs)
|
||||
r = AxiRBus.from_entity(entity, **kwargs)
|
||||
return cls(ar, r)
|
||||
|
||||
@classmethod
|
||||
def from_prefix(cls, entity, prefix, **kwargs):
|
||||
ar = AxiARBus.from_prefix(entity, prefix, **kwargs)
|
||||
r = AxiRBus.from_prefix(entity, prefix, **kwargs)
|
||||
return cls(ar, r)
|
||||
|
||||
@classmethod
|
||||
def from_channels(cls, ar, r):
|
||||
return cls(ar, r)
|
||||
|
||||
|
||||
class AxiBus:
|
||||
def __init__(self, write=None, read=None, **kwargs):
|
||||
self.write = write
|
||||
self.read = read
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, entity, **kwargs):
|
||||
write = AxiWriteBus.from_entity(entity, **kwargs)
|
||||
read = AxiReadBus.from_entity(entity, **kwargs)
|
||||
return cls(write, read)
|
||||
|
||||
@classmethod
|
||||
def from_prefix(cls, entity, prefix, **kwargs):
|
||||
write = AxiWriteBus.from_prefix(entity, prefix, **kwargs)
|
||||
read = AxiReadBus.from_prefix(entity, prefix, **kwargs)
|
||||
return cls(write, read)
|
||||
|
||||
@classmethod
|
||||
def from_channels(cls, aw, w, b, ar, r):
|
||||
write = AxiWriteBus.from_channels(aw, w, b)
|
||||
read = AxiReadBus.from_channels(ar, r)
|
||||
return cls(write, read)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,227 +22,32 @@ THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import cocotb
|
||||
from cocotb.log import SimLog
|
||||
|
||||
from .version import __version__
|
||||
from .constants import AxiBurstType, AxiProt, AxiResp
|
||||
from .axi_channels import AxiAWSink, AxiWSink, AxiBSource, AxiARSink, AxiRSource
|
||||
from .axi_slave import AxiSlaveWrite, AxiSlaveRead
|
||||
from .memory import Memory
|
||||
|
||||
|
||||
class AxiRamWrite(Memory):
|
||||
def __init__(self, entity, name, clock, reset=None, size=1024, mem=None, *args, **kwargs):
|
||||
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
|
||||
class AxiRamWrite(AxiSlaveWrite, 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)
|
||||
|
||||
self.log.info("AXI RAM model (write)")
|
||||
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.reset = reset
|
||||
|
||||
self.aw_channel = AxiAWSink(entity, name, clock, reset)
|
||||
self.w_channel = AxiWSink(entity, name, clock, reset)
|
||||
self.b_channel = AxiBSource(entity, name, clock, reset)
|
||||
|
||||
self.in_flight_operations = 0
|
||||
|
||||
self.width = len(self.w_channel.bus.wdata)
|
||||
self.byte_size = 8
|
||||
self.byte_width = self.width // self.byte_size
|
||||
self.strb_mask = 2**self.byte_width-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_width)
|
||||
|
||||
assert self.byte_width == len(self.w_channel.bus.wstrb)
|
||||
assert self.byte_width * self.byte_size == self.width
|
||||
|
||||
assert len(self.b_channel.bus.bid) == len(self.aw_channel.bus.awid)
|
||||
|
||||
cocotb.fork(self._process_write())
|
||||
|
||||
async def _process_write(self):
|
||||
while True:
|
||||
await self.aw_channel.wait()
|
||||
aw = 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_width
|
||||
|
||||
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_width) * self.byte_width
|
||||
|
||||
await self.w_channel.wait()
|
||||
w = 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_width, '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_width):
|
||||
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
|
||||
|
||||
self.b_channel.send(b)
|
||||
async def _write(self, address, data):
|
||||
self.write(address % self.size, data)
|
||||
|
||||
|
||||
class AxiRamRead(Memory):
|
||||
def __init__(self, entity, name, clock, reset=None, size=1024, mem=None, *args, **kwargs):
|
||||
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
|
||||
class AxiRamRead(AxiSlaveRead, 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)
|
||||
|
||||
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.reset = reset
|
||||
|
||||
self.ar_channel = AxiARSink(entity, name, clock, reset)
|
||||
self.r_channel = AxiRSource(entity, name, clock, reset)
|
||||
|
||||
self.in_flight_operations = 0
|
||||
|
||||
self.width = len(self.r_channel.bus.rdata)
|
||||
self.byte_size = 8
|
||||
self.byte_width = 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_width)
|
||||
|
||||
assert self.byte_width * self.byte_size == self.width
|
||||
|
||||
assert len(self.r_channel.bus.rid) == len(self.ar_channel.bus.arid)
|
||||
|
||||
cocotb.fork(self._process_read())
|
||||
|
||||
async def _process_read(self):
|
||||
while True:
|
||||
await self.ar_channel.wait()
|
||||
ar = 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_width
|
||||
|
||||
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_width) * self.byte_width
|
||||
|
||||
self.mem.seek(cur_word_addr % self.size)
|
||||
|
||||
data = self.mem.read(self.byte_width)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
async def _read(self, address, length):
|
||||
return self.read(address % self.size, length)
|
||||
|
||||
|
||||
class AxiRam(Memory):
|
||||
def __init__(self, entity, name, clock, reset=None, 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.read_if = None
|
||||
|
||||
super().__init__(size, mem, *args, **kwargs)
|
||||
super().__init__(size, mem, **kwargs)
|
||||
|
||||
self.write_if = AxiRamWrite(entity, name, clock, reset, mem=self.mem)
|
||||
self.read_if = AxiRamRead(entity, name, clock, reset, 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)
|
||||
|
||||
347
cocotbext/axi/axi_slave.py
Normal file
347
cocotbext/axi/axi_slave.py
Normal 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)
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -25,30 +25,106 @@ THE SOFTWARE.
|
||||
from .stream import define_stream
|
||||
|
||||
# Write address channel
|
||||
AxiLiteAWTransaction, AxiLiteAWSource, AxiLiteAWSink, AxiLiteAWMonitor = define_stream("AxiLiteAW",
|
||||
signals=["awaddr", "awprot", "awvalid", "awready"],
|
||||
AxiLiteAWBus, AxiLiteAWTransaction, AxiLiteAWSource, AxiLiteAWSink, AxiLiteAWMonitor = define_stream("AxiLiteAW",
|
||||
signals=["awaddr", "awvalid", "awready"],
|
||||
optional_signals=["awprot"],
|
||||
signal_widths={"awprot": 3}
|
||||
)
|
||||
|
||||
# Write data channel
|
||||
AxiLiteWTransaction, AxiLiteWSource, AxiLiteWSink, AxiLiteWMonitor = define_stream("AxiLiteW",
|
||||
signals=["wdata", "wstrb", "wvalid", "wready"]
|
||||
AxiLiteWBus, AxiLiteWTransaction, AxiLiteWSource, AxiLiteWSink, AxiLiteWMonitor = define_stream("AxiLiteW",
|
||||
signals=["wdata", "wvalid", "wready"],
|
||||
optional_signals=["wstrb"]
|
||||
)
|
||||
|
||||
# Write response channel
|
||||
AxiLiteBTransaction, AxiLiteBSource, AxiLiteBSink, AxiLiteBMonitor = define_stream("AxiLiteB",
|
||||
signals=["bresp", "bvalid", "bready"],
|
||||
AxiLiteBBus, AxiLiteBTransaction, AxiLiteBSource, AxiLiteBSink, AxiLiteBMonitor = define_stream("AxiLiteB",
|
||||
signals=["bvalid", "bready"],
|
||||
optional_signals=["bresp"],
|
||||
signal_widths={"bresp": 2}
|
||||
)
|
||||
|
||||
# Read address channel
|
||||
AxiLiteARTransaction, AxiLiteARSource, AxiLiteARSink, AxiLiteARMonitor = define_stream("AxiLiteAR",
|
||||
signals=["araddr", "arprot", "arvalid", "arready"],
|
||||
AxiLiteARBus, AxiLiteARTransaction, AxiLiteARSource, AxiLiteARSink, AxiLiteARMonitor = define_stream("AxiLiteAR",
|
||||
signals=["araddr", "arvalid", "arready"],
|
||||
optional_signals=["arprot"],
|
||||
signal_widths={"arprot": 3}
|
||||
)
|
||||
|
||||
# Read data channel
|
||||
AxiLiteRTransaction, AxiLiteRSource, AxiLiteRSink, AxiLiteRMonitor = define_stream("AxiLiteR",
|
||||
signals=["rdata", "rresp", "rvalid", "rready"],
|
||||
AxiLiteRBus, AxiLiteRTransaction, AxiLiteRSource, AxiLiteRSink, AxiLiteRMonitor = define_stream("AxiLiteR",
|
||||
signals=["rdata", "rvalid", "rready"],
|
||||
optional_signals=["rresp"],
|
||||
signal_widths={"rresp": 2}
|
||||
)
|
||||
|
||||
|
||||
class AxiLiteWriteBus:
|
||||
def __init__(self, aw=None, w=None, b=None):
|
||||
self.aw = aw
|
||||
self.w = w
|
||||
self.b = b
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, entity, **kwargs):
|
||||
aw = AxiLiteAWBus.from_entity(entity, **kwargs)
|
||||
w = AxiLiteWBus.from_entity(entity, **kwargs)
|
||||
b = AxiLiteBBus.from_entity(entity, **kwargs)
|
||||
return cls(aw, w, b)
|
||||
|
||||
@classmethod
|
||||
def from_prefix(cls, entity, prefix, **kwargs):
|
||||
aw = AxiLiteAWBus.from_prefix(entity, prefix, **kwargs)
|
||||
w = AxiLiteWBus.from_prefix(entity, prefix, **kwargs)
|
||||
b = AxiLiteBBus.from_prefix(entity, prefix, **kwargs)
|
||||
return cls(aw, w, b)
|
||||
|
||||
@classmethod
|
||||
def from_channels(cls, aw, w, b):
|
||||
return cls(aw, w, b)
|
||||
|
||||
|
||||
class AxiLiteReadBus:
|
||||
def __init__(self, ar=None, r=None):
|
||||
self.ar = ar
|
||||
self.r = r
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, entity, **kwargs):
|
||||
ar = AxiLiteARBus.from_entity(entity, **kwargs)
|
||||
r = AxiLiteRBus.from_entity(entity, **kwargs)
|
||||
return cls(ar, r)
|
||||
|
||||
@classmethod
|
||||
def from_prefix(cls, entity, prefix, **kwargs):
|
||||
ar = AxiLiteARBus.from_prefix(entity, prefix, **kwargs)
|
||||
r = AxiLiteRBus.from_prefix(entity, prefix, **kwargs)
|
||||
return cls(ar, r)
|
||||
|
||||
@classmethod
|
||||
def from_channels(cls, ar, r):
|
||||
return cls(ar, r)
|
||||
|
||||
|
||||
class AxiLiteBus:
|
||||
def __init__(self, write=None, read=None, **kwargs):
|
||||
self.write = write
|
||||
self.read = read
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, entity, **kwargs):
|
||||
write = AxiLiteWriteBus.from_entity(entity, **kwargs)
|
||||
read = AxiLiteReadBus.from_entity(entity, **kwargs)
|
||||
return cls(write, read)
|
||||
|
||||
@classmethod
|
||||
def from_prefix(cls, entity, prefix, **kwargs):
|
||||
write = AxiLiteWriteBus.from_prefix(entity, prefix, **kwargs)
|
||||
read = AxiLiteReadBus.from_prefix(entity, prefix, **kwargs)
|
||||
return cls(write, read)
|
||||
|
||||
@classmethod
|
||||
def from_channels(cls, aw, w, b, ar, r):
|
||||
write = AxiLiteWriteBus.from_channels(aw, w, b)
|
||||
read = AxiLiteReadBus.from_channels(ar, r)
|
||||
return cls(write, read)
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,155 +22,264 @@ THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
from collections import deque, namedtuple
|
||||
import logging
|
||||
from typing import NamedTuple
|
||||
|
||||
import cocotb
|
||||
from cocotb.queue import Queue
|
||||
from cocotb.triggers import Event
|
||||
from cocotb.log import SimLog
|
||||
|
||||
from .version import __version__
|
||||
from .constants import AxiProt, AxiResp
|
||||
from .axil_channels import AxiLiteAWSource, AxiLiteWSource, AxiLiteBSink, AxiLiteARSource, AxiLiteRSink
|
||||
|
||||
# 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
|
||||
AxiLiteReadCmd = namedtuple("AxiLiteReadCmd", ["address", "length", "prot", "event"])
|
||||
AxiLiteReadRespCmd = namedtuple("AxiLiteReadRespCmd", ["address", "length", "cycles", "prot", "event"])
|
||||
AxiLiteReadResp = namedtuple("AxiLiteReadResp", ["address", "data", "resp"])
|
||||
from .address_space import Region
|
||||
from .reset import Reset
|
||||
|
||||
|
||||
class AxiLiteMasterWrite(object):
|
||||
def __init__(self, entity, name, clock, reset=None):
|
||||
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
|
||||
# AXI lite master write helper objects
|
||||
class AxiLiteWriteCmd(NamedTuple):
|
||||
address: int
|
||||
data: bytes
|
||||
prot: AxiProt
|
||||
event: Event
|
||||
|
||||
|
||||
class AxiLiteWriteRespCmd(NamedTuple):
|
||||
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.clock = clock
|
||||
self.reset = reset
|
||||
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 master (write)")
|
||||
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.reset = reset
|
||||
self.aw_channel = AxiLiteAWSource(bus.aw, clock, reset, reset_active_level)
|
||||
self.aw_channel.queue_occupancy_limit = 2
|
||||
self.w_channel = AxiLiteWSource(bus.w, clock, reset, reset_active_level)
|
||||
self.w_channel.queue_occupancy_limit = 2
|
||||
self.b_channel = AxiLiteBSink(bus.b, clock, reset, reset_active_level)
|
||||
self.b_channel.queue_occupancy_limit = 2
|
||||
|
||||
self.aw_channel = AxiLiteAWSource(entity, name, clock, reset)
|
||||
self.w_channel = AxiLiteWSource(entity, name, clock, reset)
|
||||
self.b_channel = AxiLiteBSink(entity, name, clock, reset)
|
||||
self.write_command_queue = Queue()
|
||||
self.write_command_queue.queue_occupancy_limit = 2
|
||||
self.current_write_command = None
|
||||
|
||||
self.write_command_queue = deque()
|
||||
self.write_command_sync = Event()
|
||||
self.write_resp_queue = deque()
|
||||
self.write_resp_sync = Event()
|
||||
self.write_resp_set = set()
|
||||
|
||||
self.int_write_resp_command_queue = deque()
|
||||
self.int_write_resp_command_sync = Event()
|
||||
self.int_write_resp_command_queue = Queue()
|
||||
self.current_write_resp_command = None
|
||||
|
||||
self.in_flight_operations = 0
|
||||
self._idle = Event()
|
||||
self._idle.set()
|
||||
|
||||
self.address_width = len(self.aw_channel.bus.awaddr)
|
||||
self.width = len(self.w_channel.bus.wdata)
|
||||
self.byte_size = 8
|
||||
self.byte_width = self.width // self.byte_size
|
||||
self.strb_mask = 2**self.byte_width-1
|
||||
self.byte_lanes = self.width // self.byte_size
|
||||
self.strb_mask = 2**self.byte_lanes-1
|
||||
|
||||
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(" 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(" Data width: %d bits (%d bytes)", self.width, self.byte_width)
|
||||
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
|
||||
|
||||
assert self.byte_width == len(self.w_channel.bus.wstrb)
|
||||
assert self.byte_width * self.byte_size == self.width
|
||||
self.log.info("AXI lite master 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)
|
||||
|
||||
cocotb.fork(self._process_write())
|
||||
cocotb.fork(self._process_write_resp())
|
||||
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._process_write_resp_cr = None
|
||||
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def init_write(self, address, data, prot=AxiProt.NONSECURE, event=None):
|
||||
if event is not None and not isinstance(event, Event):
|
||||
if event is None:
|
||||
event = Event()
|
||||
|
||||
if not isinstance(event, Event):
|
||||
raise ValueError("Expected event object")
|
||||
|
||||
self.in_flight_operations += 1
|
||||
if address < 0 or address >= 2**self.address_width:
|
||||
raise ValueError("Address out of range")
|
||||
|
||||
self.write_command_queue.append(AxiLiteWriteCmd(address, bytearray(data), prot, event))
|
||||
self.write_command_sync.set()
|
||||
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")
|
||||
|
||||
data = bytes(data)
|
||||
|
||||
cocotb.start_soon(self._write_wrapper(address, bytes(data), prot, event))
|
||||
|
||||
return event
|
||||
|
||||
def idle(self):
|
||||
return not self.in_flight_operations
|
||||
|
||||
async def wait(self):
|
||||
while not self.idle():
|
||||
self.write_resp_sync.clear()
|
||||
await self.write_resp_sync.wait()
|
||||
|
||||
def write_resp_ready(self):
|
||||
return bool(self.write_resp_queue)
|
||||
|
||||
def get_write_resp(self):
|
||||
if self.write_resp_queue:
|
||||
return self.write_resp_queue.popleft()
|
||||
return None
|
||||
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.awprot_present and prot != AxiProt.NONSECURE:
|
||||
raise ValueError("awprot sideband signal value specified, but signal is not connected")
|
||||
|
||||
event = Event()
|
||||
self.init_write(address, data, prot, 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()
|
||||
return event.data
|
||||
|
||||
async def write_words(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
|
||||
words = data
|
||||
data = bytearray()
|
||||
for w in words:
|
||||
data.extend(w.to_bytes(ws, byteorder))
|
||||
await self.write(address, data, prot)
|
||||
async def _write_wrapper(self, address, data, prot, event):
|
||||
event.set(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)
|
||||
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
|
||||
if self._process_write_resp_cr is not None:
|
||||
self._process_write_resp_cr.kill()
|
||||
self._process_write_resp_cr = None
|
||||
|
||||
async def write_qwords(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
|
||||
await self.write_words(address, data, byteorder, 8, prot)
|
||||
self.aw_channel.clear()
|
||||
self.w_channel.clear()
|
||||
self.b_channel.clear()
|
||||
|
||||
async def write_byte(self, address, data, prot=AxiProt.NONSECURE):
|
||||
await self.write(address, [data], prot)
|
||||
def flush_cmd(cmd):
|
||||
self.log.warning("Flushed write operation during reset: %s", cmd)
|
||||
if cmd.event:
|
||||
cmd.event.set(None)
|
||||
|
||||
async def write_word(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
|
||||
await self.write_words(address, [data], byteorder, ws, prot)
|
||||
while not self.write_command_queue.empty():
|
||||
cmd = self.write_command_queue.get_nowait()
|
||||
flush_cmd(cmd)
|
||||
|
||||
async def write_dword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
|
||||
await self.write_dwords(address, [data], byteorder, prot)
|
||||
if self.current_write_command:
|
||||
cmd = self.current_write_command
|
||||
self.current_write_command = None
|
||||
flush_cmd(cmd)
|
||||
|
||||
async def write_qword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
|
||||
await self.write_qwords(address, [data], byteorder, prot)
|
||||
while not self.int_write_resp_command_queue.empty():
|
||||
cmd = self.int_write_resp_command_queue.get_nowait()
|
||||
flush_cmd(cmd)
|
||||
|
||||
if self.current_write_resp_command:
|
||||
cmd = self.current_write_resp_command
|
||||
self.current_write_resp_command = None
|
||||
flush_cmd(cmd)
|
||||
|
||||
self.in_flight_operations = 0
|
||||
self._idle.set()
|
||||
else:
|
||||
self.log.info("Reset de-asserted")
|
||||
if self._process_write_cr is None:
|
||||
self._process_write_cr = cocotb.start_soon(self._process_write())
|
||||
if self._process_write_resp_cr is None:
|
||||
self._process_write_resp_cr = cocotb.start_soon(self._process_write_resp())
|
||||
|
||||
async def _process_write(self):
|
||||
while True:
|
||||
if not self.write_command_queue:
|
||||
self.write_command_sync.clear()
|
||||
await self.write_command_sync.wait()
|
||||
cmd = await self.write_command_queue.get()
|
||||
self.current_write_command = cmd
|
||||
|
||||
cmd = self.write_command_queue.popleft()
|
||||
word_addr = (cmd.address // self.byte_lanes) * self.byte_lanes
|
||||
|
||||
word_addr = (cmd.address // self.byte_width) * self.byte_width
|
||||
|
||||
start_offset = cmd.address % self.byte_width
|
||||
end_offset = ((cmd.address + len(cmd.data) - 1) % self.byte_width) + 1
|
||||
start_offset = cmd.address % self.byte_lanes
|
||||
end_offset = ((cmd.address + len(cmd.data) - 1) % self.byte_lanes) + 1
|
||||
|
||||
strb_start = (self.strb_mask << start_offset) & self.strb_mask
|
||||
strb_end = self.strb_mask >> (self.byte_width - end_offset)
|
||||
strb_end = self.strb_mask >> (self.byte_lanes - end_offset)
|
||||
|
||||
cycles = (len(cmd.data) + (cmd.address % self.byte_width) + self.byte_width-1) // self.byte_width
|
||||
cycles = (len(cmd.data) + (cmd.address % self.byte_lanes) + self.byte_lanes-1) // self.byte_lanes
|
||||
|
||||
resp_cmd = AxiLiteWriteRespCmd(cmd.address, len(cmd.data), cycles, cmd.prot, cmd.event)
|
||||
self.int_write_resp_command_queue.append(resp_cmd)
|
||||
self.int_write_resp_command_sync.set()
|
||||
await self.int_write_resp_command_queue.put(resp_cmd)
|
||||
|
||||
offset = 0
|
||||
|
||||
if self.log.isEnabledFor(logging.INFO):
|
||||
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)))
|
||||
|
||||
for k in range(cycles):
|
||||
start = 0
|
||||
stop = self.byte_width
|
||||
stop = self.byte_lanes
|
||||
strb = self.strb_mask
|
||||
|
||||
if k == 0:
|
||||
@@ -186,31 +295,35 @@ class AxiLiteMasterWrite(object):
|
||||
offset += 1
|
||||
|
||||
aw = self.aw_channel._transaction_obj()
|
||||
aw.awaddr = word_addr + k*self.byte_width
|
||||
if k == 0:
|
||||
aw.awaddr = cmd.address
|
||||
else:
|
||||
aw.awaddr = word_addr + k*self.byte_lanes
|
||||
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.wdata = val
|
||||
w.wstrb = strb
|
||||
|
||||
await self.aw_channel.drive(aw)
|
||||
self.w_channel.send(w)
|
||||
await self.aw_channel.send(aw)
|
||||
await self.w_channel.send(w)
|
||||
|
||||
self.current_write_command = None
|
||||
|
||||
async def _process_write_resp(self):
|
||||
while True:
|
||||
if not self.int_write_resp_command_queue:
|
||||
self.int_write_resp_command_sync.clear()
|
||||
await self.int_write_resp_command_sync.wait()
|
||||
|
||||
cmd = self.int_write_resp_command_queue.popleft()
|
||||
cmd = await self.int_write_resp_command_queue.get()
|
||||
self.current_write_resp_command = cmd
|
||||
|
||||
resp = AxiResp.OKAY
|
||||
|
||||
for k in range(cmd.cycles):
|
||||
await self.b_channel.wait()
|
||||
b = 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:
|
||||
resp = cycle_resp
|
||||
@@ -220,163 +333,227 @@ class AxiLiteMasterWrite(object):
|
||||
|
||||
write_resp = AxiLiteWriteResp(cmd.address, cmd.length, resp)
|
||||
|
||||
if cmd.event is not None:
|
||||
cmd.event.set(write_resp)
|
||||
else:
|
||||
self.write_resp_queue.append(write_resp)
|
||||
self.write_resp_sync.set()
|
||||
|
||||
self.current_write_resp_command = None
|
||||
|
||||
self.in_flight_operations -= 1
|
||||
|
||||
if self.in_flight_operations == 0:
|
||||
self._idle.set()
|
||||
|
||||
class AxiLiteMasterRead(object):
|
||||
def __init__(self, entity, name, clock, reset=None):
|
||||
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
|
||||
|
||||
class AxiLiteMasterRead(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.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("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.reset = reset
|
||||
self.ar_channel = AxiLiteARSource(bus.ar, clock, reset, reset_active_level)
|
||||
self.ar_channel.queue_occupancy_limit = 2
|
||||
self.r_channel = AxiLiteRSink(bus.r, clock, reset, reset_active_level)
|
||||
self.r_channel.queue_occupancy_limit = 2
|
||||
|
||||
self.ar_channel = AxiLiteARSource(entity, name, clock, reset)
|
||||
self.r_channel = AxiLiteRSink(entity, name, clock, reset)
|
||||
self.read_command_queue = Queue()
|
||||
self.read_command_queue.queue_occupancy_limit = 2
|
||||
self.current_read_command = None
|
||||
|
||||
self.read_command_queue = deque()
|
||||
self.read_command_sync = Event()
|
||||
self.read_data_queue = deque()
|
||||
self.read_data_sync = Event()
|
||||
self.read_data_set = set()
|
||||
|
||||
self.int_read_resp_command_queue = deque()
|
||||
self.int_read_resp_command_sync = Event()
|
||||
self.int_read_resp_command_queue = Queue()
|
||||
self.current_read_resp_command = None
|
||||
|
||||
self.in_flight_operations = 0
|
||||
self._idle = Event()
|
||||
self._idle.set()
|
||||
|
||||
self.address_width = len(self.ar_channel.bus.araddr)
|
||||
self.width = len(self.r_channel.bus.rdata)
|
||||
self.byte_size = 8
|
||||
self.byte_width = self.width // self.byte_size
|
||||
self.byte_lanes = self.width // self.byte_size
|
||||
|
||||
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(" 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(" Data width: %d bits (%d bytes)", self.width, self.byte_width)
|
||||
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
|
||||
|
||||
assert self.byte_width * self.byte_size == self.width
|
||||
self.log.info("AXI lite master 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)
|
||||
|
||||
cocotb.fork(self._process_read())
|
||||
cocotb.fork(self._process_read_resp())
|
||||
assert self.byte_lanes * self.byte_size == self.width
|
||||
|
||||
self._process_read_cr = None
|
||||
self._process_read_resp_cr = None
|
||||
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def init_read(self, address, length, prot=AxiProt.NONSECURE, event=None):
|
||||
if event is not None and not isinstance(event, Event):
|
||||
if event is None:
|
||||
event = Event()
|
||||
|
||||
if not isinstance(event, Event):
|
||||
raise ValueError("Expected event object")
|
||||
|
||||
self.in_flight_operations += 1
|
||||
if address < 0 or address >= 2**self.address_width:
|
||||
raise ValueError("Address out of range")
|
||||
|
||||
self.read_command_queue.append(AxiLiteReadCmd(address, length, prot, event))
|
||||
self.read_command_sync.set()
|
||||
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")
|
||||
|
||||
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():
|
||||
self.read_data_sync.clear()
|
||||
await self.read_data_sync.wait()
|
||||
|
||||
def read_data_ready(self):
|
||||
return bool(self.read_data_queue)
|
||||
|
||||
def get_read_data(self):
|
||||
if self.read_data_queue:
|
||||
return self.read_data_queue.popleft()
|
||||
return None
|
||||
await self._idle.wait()
|
||||
|
||||
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.arprot_present and prot != AxiProt.NONSECURE:
|
||||
raise ValueError("arprot sideband signal value specified, but signal is not connected")
|
||||
|
||||
event = Event()
|
||||
self.init_read(address, length, prot, event)
|
||||
|
||||
self.in_flight_operations += 1
|
||||
self._idle.clear()
|
||||
|
||||
await self.read_command_queue.put(AxiLiteReadCmd(address, length, prot, event))
|
||||
|
||||
await event.wait()
|
||||
return event.data
|
||||
|
||||
async def read_words(self, address, count, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
|
||||
data = await self.read(address, count*ws, 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_wrapper(self, address, length, prot, event):
|
||||
event.set(await self.read(address, length, prot))
|
||||
|
||||
async def read_dwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE):
|
||||
return await self.read_words(address, count, byteorder, 4, prot)
|
||||
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
|
||||
if self._process_read_resp_cr is not None:
|
||||
self._process_read_resp_cr.kill()
|
||||
self._process_read_resp_cr = None
|
||||
|
||||
async def read_qwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE):
|
||||
return await self.read_words(address, count, byteorder, 8, prot)
|
||||
self.ar_channel.clear()
|
||||
self.r_channel.clear()
|
||||
|
||||
async def read_byte(self, address, prot=AxiProt.NONSECURE):
|
||||
return (await self.read(address, 1, prot)).data[0]
|
||||
def flush_cmd(cmd):
|
||||
self.log.warning("Flushed read operation during reset: %s", cmd)
|
||||
if cmd.event:
|
||||
cmd.event.set(None)
|
||||
|
||||
async def read_word(self, address, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
|
||||
return (await self.read_words(address, 1, byteorder, ws, prot))[0]
|
||||
while not self.read_command_queue.empty():
|
||||
cmd = self.read_command_queue.get_nowait()
|
||||
flush_cmd(cmd)
|
||||
|
||||
async def read_dword(self, address, byteorder='little', prot=AxiProt.NONSECURE):
|
||||
return (await self.read_dwords(address, 1, byteorder, prot))[0]
|
||||
if self.current_read_command:
|
||||
cmd = self.current_read_command
|
||||
self.current_read_command = None
|
||||
flush_cmd(cmd)
|
||||
|
||||
async def read_qword(self, address, byteorder='little', prot=AxiProt.NONSECURE):
|
||||
return (await self.read_qwords(address, 1, byteorder, prot))[0]
|
||||
while not self.int_read_resp_command_queue.empty():
|
||||
cmd = self.int_read_resp_command_queue.get_nowait()
|
||||
flush_cmd(cmd)
|
||||
|
||||
if self.current_read_resp_command:
|
||||
cmd = self.current_read_resp_command
|
||||
self.current_read_resp_command = None
|
||||
flush_cmd(cmd)
|
||||
|
||||
self.in_flight_operations = 0
|
||||
self._idle.set()
|
||||
else:
|
||||
self.log.info("Reset de-asserted")
|
||||
if self._process_read_cr is None:
|
||||
self._process_read_cr = cocotb.start_soon(self._process_read())
|
||||
if self._process_read_resp_cr is None:
|
||||
self._process_read_resp_cr = cocotb.start_soon(self._process_read_resp())
|
||||
|
||||
async def _process_read(self):
|
||||
while True:
|
||||
if not self.read_command_queue:
|
||||
self.read_command_sync.clear()
|
||||
await self.read_command_sync.wait()
|
||||
cmd = await self.read_command_queue.get()
|
||||
self.current_read_command = cmd
|
||||
|
||||
cmd = self.read_command_queue.popleft()
|
||||
word_addr = (cmd.address // self.byte_lanes) * self.byte_lanes
|
||||
|
||||
word_addr = (cmd.address // self.byte_width) * self.byte_width
|
||||
|
||||
cycles = (cmd.length + self.byte_width-1 + (cmd.address % self.byte_width)) // self.byte_width
|
||||
cycles = (cmd.length + self.byte_lanes-1 + (cmd.address % self.byte_lanes)) // self.byte_lanes
|
||||
|
||||
resp_cmd = AxiLiteReadRespCmd(cmd.address, cmd.length, cycles, cmd.prot, cmd.event)
|
||||
self.int_read_resp_command_queue.append(resp_cmd)
|
||||
self.int_read_resp_command_sync.set()
|
||||
await self.int_read_resp_command_queue.put(resp_cmd)
|
||||
|
||||
self.log.info("Read start addr: 0x%08x prot: %s length: %d",
|
||||
cmd.address, cmd.prot, cmd.length)
|
||||
|
||||
for k in range(cycles):
|
||||
ar = self.ar_channel._transaction_obj()
|
||||
ar.araddr = word_addr + k*self.byte_width
|
||||
if k == 0:
|
||||
ar.araddr = cmd.address
|
||||
else:
|
||||
ar.araddr = word_addr + k*self.byte_lanes
|
||||
ar.arprot = cmd.prot
|
||||
|
||||
await self.ar_channel.drive(ar)
|
||||
await self.ar_channel.send(ar)
|
||||
|
||||
self.current_read_command = None
|
||||
|
||||
async def _process_read_resp(self):
|
||||
while True:
|
||||
if not self.int_read_resp_command_queue:
|
||||
self.int_read_resp_command_sync.clear()
|
||||
await self.int_read_resp_command_sync.wait()
|
||||
cmd = await self.int_read_resp_command_queue.get()
|
||||
self.current_read_resp_command = cmd
|
||||
|
||||
cmd = self.int_read_resp_command_queue.popleft()
|
||||
|
||||
start_offset = cmd.address % self.byte_width
|
||||
end_offset = ((cmd.address + cmd.length - 1) % self.byte_width) + 1
|
||||
start_offset = cmd.address % self.byte_lanes
|
||||
end_offset = ((cmd.address + cmd.length - 1) % self.byte_lanes) + 1
|
||||
|
||||
data = bytearray()
|
||||
|
||||
resp = AxiResp.OKAY
|
||||
|
||||
for k in range(cmd.cycles):
|
||||
await self.r_channel.wait()
|
||||
r = self.r_channel.recv()
|
||||
r = await self.r_channel.recv()
|
||||
|
||||
cycle_data = int(r.rdata)
|
||||
cycle_resp = AxiResp(r.rresp)
|
||||
cycle_resp = AxiResp(int(getattr(r, 'rresp', AxiResp.OKAY)))
|
||||
|
||||
if cycle_resp != AxiResp.OKAY:
|
||||
resp = cycle_resp
|
||||
|
||||
start = 0
|
||||
stop = self.byte_width
|
||||
stop = self.byte_lanes
|
||||
|
||||
if k == 0:
|
||||
start = start_offset
|
||||
@@ -384,35 +561,39 @@ class AxiLiteMasterRead(object):
|
||||
stop = end_offset
|
||||
|
||||
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",
|
||||
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)
|
||||
|
||||
if cmd.event is not None:
|
||||
cmd.event.set(read_resp)
|
||||
else:
|
||||
self.read_data_queue.append(read_resp)
|
||||
self.read_data_sync.set()
|
||||
|
||||
self.current_read_resp_command = None
|
||||
|
||||
self.in_flight_operations -= 1
|
||||
|
||||
if self.in_flight_operations == 0:
|
||||
self._idle.set()
|
||||
|
||||
class AxiLiteMaster(object):
|
||||
def __init__(self, entity, name, clock, reset=None):
|
||||
|
||||
class AxiLiteMaster(Region):
|
||||
def __init__(self, bus, clock, reset=None, reset_active_level=True, **kwargs):
|
||||
self.write_if = None
|
||||
self.read_if = None
|
||||
|
||||
self.write_if = AxiLiteMasterWrite(entity, name, clock, reset)
|
||||
self.read_if = AxiLiteMasterRead(entity, name, clock, reset)
|
||||
self.write_if = AxiLiteMasterWrite(bus.write, clock, reset, reset_active_level, **kwargs)
|
||||
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):
|
||||
self.read_if.init_read(address, length, prot, event)
|
||||
return self.read_if.init_read(address, length, prot, event)
|
||||
|
||||
def init_write(self, address, data, prot=AxiProt.NONSECURE, event=None):
|
||||
self.write_if.init_write(address, data, prot, event)
|
||||
return self.write_if.init_write(address, data, prot, event)
|
||||
|
||||
def idle(self):
|
||||
return (not self.read_if or self.read_if.idle()) and (not self.write_if or self.write_if.idle())
|
||||
@@ -428,62 +609,8 @@ class AxiLiteMaster(object):
|
||||
async def wait_write(self):
|
||||
await self.write_if.wait()
|
||||
|
||||
def read_data_ready(self):
|
||||
return self.read_if.read_data_ready()
|
||||
|
||||
def get_read_data(self):
|
||||
return self.read_if.get_read_data()
|
||||
|
||||
def write_resp_ready(self):
|
||||
return self.write_if.write_resp_ready()
|
||||
|
||||
def get_write_resp(self):
|
||||
return self.write_if.get_write_resp()
|
||||
|
||||
async def read(self, address, length, prot=AxiProt.NONSECURE):
|
||||
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):
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,147 +22,32 @@ THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import cocotb
|
||||
from cocotb.log import SimLog
|
||||
|
||||
from .version import __version__
|
||||
from .constants import AxiProt, AxiResp
|
||||
from .axil_channels import AxiLiteAWSink, AxiLiteWSink, AxiLiteBSource, AxiLiteARSink, AxiLiteRSource
|
||||
from .axil_slave import AxiLiteSlaveWrite, AxiLiteSlaveRead
|
||||
from .memory import Memory
|
||||
|
||||
|
||||
class AxiLiteRamWrite(Memory):
|
||||
def __init__(self, entity, name, clock, reset=None, size=1024, mem=None, *args, **kwargs):
|
||||
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
|
||||
class AxiLiteRamWrite(AxiLiteSlaveWrite, 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)
|
||||
|
||||
self.log.info("AXI lite RAM model (write)")
|
||||
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.reset = reset
|
||||
|
||||
self.aw_channel = AxiLiteAWSink(entity, name, clock, reset)
|
||||
self.w_channel = AxiLiteWSink(entity, name, clock, reset)
|
||||
self.b_channel = AxiLiteBSource(entity, name, clock, reset)
|
||||
|
||||
self.in_flight_operations = 0
|
||||
|
||||
self.width = len(self.w_channel.bus.wdata)
|
||||
self.byte_size = 8
|
||||
self.byte_width = self.width // self.byte_size
|
||||
self.strb_mask = 2**self.byte_width-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_width)
|
||||
|
||||
assert self.byte_width == len(self.w_channel.bus.wstrb)
|
||||
assert self.byte_width * self.byte_size == self.width
|
||||
|
||||
cocotb.fork(self._process_write())
|
||||
|
||||
async def _process_write(self):
|
||||
while True:
|
||||
await self.aw_channel.wait()
|
||||
aw = self.aw_channel.recv()
|
||||
|
||||
addr = (int(aw.awaddr) // self.byte_width) * self.byte_width
|
||||
prot = AxiProt(aw.awprot)
|
||||
|
||||
await self.w_channel.wait()
|
||||
w = 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_width, '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_width):
|
||||
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
|
||||
|
||||
self.b_channel.send(b)
|
||||
async def _write(self, address, data):
|
||||
self.write(address % self.size, data)
|
||||
|
||||
|
||||
class AxiLiteRamRead(Memory):
|
||||
def __init__(self, entity, name, clock, reset=None, size=1024, mem=None, *args, **kwargs):
|
||||
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
|
||||
class AxiLiteRamRead(AxiLiteSlaveRead, 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)
|
||||
|
||||
self.log.info("AXI lite 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.reset = reset
|
||||
|
||||
self.ar_channel = AxiLiteARSink(entity, name, clock, reset)
|
||||
self.r_channel = AxiLiteRSource(entity, name, clock, reset)
|
||||
|
||||
self.in_flight_operations = 0
|
||||
|
||||
self.width = len(self.r_channel.bus.rdata)
|
||||
self.byte_size = 8
|
||||
self.byte_width = 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_width)
|
||||
|
||||
assert self.byte_width * self.byte_size == self.width
|
||||
|
||||
cocotb.fork(self._process_read())
|
||||
|
||||
async def _process_read(self):
|
||||
while True:
|
||||
await self.ar_channel.wait()
|
||||
ar = self.ar_channel.recv()
|
||||
|
||||
addr = (int(ar.araddr) // self.byte_width) * self.byte_width
|
||||
prot = AxiProt(ar.arprot)
|
||||
|
||||
# todo latency
|
||||
|
||||
self.mem.seek(addr % self.size)
|
||||
|
||||
data = self.mem.read(self.byte_width)
|
||||
|
||||
r = self.r_channel._transaction_obj()
|
||||
r.rdata = int.from_bytes(data, 'little')
|
||||
r.rresp = AxiResp.OKAY
|
||||
|
||||
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)))
|
||||
async def _read(self, address, length):
|
||||
return self.read(address % self.size, length)
|
||||
|
||||
|
||||
class AxiLiteRam(Memory):
|
||||
def __init__(self, entity, name, clock, reset=None, 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.read_if = None
|
||||
|
||||
super().__init__(size, mem, *args, **kwargs)
|
||||
super().__init__(size, mem, **kwargs)
|
||||
|
||||
self.write_if = AxiLiteRamWrite(entity, name, clock, reset, mem=self.mem)
|
||||
self.read_if = AxiLiteRamRead(entity, name, clock, reset, 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)
|
||||
|
||||
259
cocotbext/axi/axil_slave.py
Normal file
259
cocotbext/axi/axil_slave.py
Normal 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)
|
||||
File diff suppressed because it is too large
Load Diff
92
cocotbext/axi/buddy_allocator.py
Normal file
92
cocotbext/axi/buddy_allocator.py
Normal 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")
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
|
||||
class Memory(object):
|
||||
def __init__(self, size=1024, mem=None, *args, **kwargs):
|
||||
class Memory:
|
||||
def __init__(self, size=2**64, mem=None, **kwargs):
|
||||
if mem is not None:
|
||||
self.mem = mem
|
||||
else:
|
||||
self.mem = mmap.mmap(-1, size)
|
||||
self.mem = SparseMemory(size)
|
||||
self.size = len(self.mem)
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def read(self, address, length):
|
||||
self.mem.seek(address)
|
||||
return self.mem.read(length)
|
||||
return self.mem[address:address+length]
|
||||
|
||||
def write(self, address, data):
|
||||
self.mem.seek(address)
|
||||
self.mem.write(bytes(data))
|
||||
self.mem[address:address+len(data)] = data
|
||||
|
||||
def write_words(self, address, data, byteorder='little', ws=2):
|
||||
words = data
|
||||
|
||||
69
cocotbext/axi/reset.py
Normal file
69
cocotbext/axi/reset.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020-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 cocotb
|
||||
from cocotb.triggers import Edge
|
||||
|
||||
|
||||
class Reset:
|
||||
def _init_reset(self, reset_signal=None, active_level=True):
|
||||
self._local_reset = False
|
||||
self._ext_reset = False
|
||||
self._reset_state = True
|
||||
|
||||
if reset_signal is not None:
|
||||
cocotb.start_soon(self._run_reset(reset_signal, bool(active_level)))
|
||||
|
||||
self._update_reset()
|
||||
|
||||
def assert_reset(self, val=None):
|
||||
if val is None:
|
||||
self.assert_reset(True)
|
||||
self.assert_reset(False)
|
||||
else:
|
||||
self._local_reset = bool(val)
|
||||
self._update_reset()
|
||||
|
||||
def _update_reset(self):
|
||||
new_state = self._local_reset or self._ext_reset
|
||||
if self._reset_state != new_state:
|
||||
self._reset_state = new_state
|
||||
self._handle_reset(new_state)
|
||||
|
||||
def _handle_reset(self, state):
|
||||
pass
|
||||
|
||||
async def _run_reset(self, reset_signal, active_level):
|
||||
while True:
|
||||
await Edge(reset_signal)
|
||||
try:
|
||||
level = bool(int(reset_signal.value))
|
||||
except ValueError:
|
||||
continue
|
||||
if level:
|
||||
self._ext_reset = active_level
|
||||
self._update_reset()
|
||||
else:
|
||||
self._ext_reset = not active_level
|
||||
self._update_reset()
|
||||
111
cocotbext/axi/sparse_memory.py
Normal file
111
cocotbext/axi/sparse_memory.py
Normal 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")
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,15 +22,39 @@ THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import cocotb
|
||||
from cocotb.triggers import RisingEdge, ReadOnly, Event, First, Timer
|
||||
from cocotb.bus import Bus
|
||||
from cocotb.log import SimLog
|
||||
from cocotb.queue import Queue, QueueFull
|
||||
from cocotb.triggers import RisingEdge, Event, First, Timer
|
||||
from cocotb_bus.bus import Bus
|
||||
|
||||
from collections import deque
|
||||
try:
|
||||
from cocotb.types import LogicArray
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from .reset import Reset
|
||||
|
||||
|
||||
class StreamTransaction(object):
|
||||
class StreamBus(Bus):
|
||||
|
||||
_signals = ["data"]
|
||||
_optional_signals = []
|
||||
|
||||
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 StreamTransaction:
|
||||
|
||||
_signals = ["data"]
|
||||
|
||||
@@ -48,7 +72,7 @@ class StreamTransaction(object):
|
||||
return f"{type(self).__name__}({', '.join(f'{s}={int(getattr(self, s))}' for s in self._signals)})"
|
||||
|
||||
|
||||
class StreamBase(object):
|
||||
class StreamBase(Reset):
|
||||
|
||||
_signals = ["data", "valid", "ready"]
|
||||
_optional_signals = []
|
||||
@@ -63,18 +87,24 @@ class StreamBase(object):
|
||||
_ready_init = None
|
||||
|
||||
_transaction_obj = StreamTransaction
|
||||
_bus_obj = StreamBus
|
||||
|
||||
def __init__(self, entity, name, clock, reset=None, *args, **kwargs):
|
||||
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
|
||||
self.entity = entity
|
||||
def __init__(self, bus, clock, reset=None, reset_active_level=True, *args, **kwargs):
|
||||
self.bus = bus
|
||||
self.clock = clock
|
||||
self.reset = reset
|
||||
self.bus = Bus(self.entity, name, self._signals, optional_signals=self._optional_signals, **kwargs)
|
||||
self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.queue = deque()
|
||||
self.queue_sync = Event()
|
||||
self.active = False
|
||||
|
||||
self.queue = Queue()
|
||||
self.dequeue_event = Event()
|
||||
self.idle_event = Event()
|
||||
self.idle_event.set()
|
||||
self.active_event = Event()
|
||||
self.wake_event = Event()
|
||||
|
||||
self.ready = None
|
||||
self.valid = None
|
||||
@@ -94,28 +124,72 @@ class StreamBase(object):
|
||||
if sig in self._signal_widths:
|
||||
assert len(getattr(self.bus, sig)) == self._signal_widths[sig]
|
||||
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)
|
||||
getattr(self.bus, sig).setimmediatevalue(v)
|
||||
s.setimmediatevalue(v)
|
||||
|
||||
self._run_cr = None
|
||||
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def count(self):
|
||||
return len(self.queue)
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return not self.queue
|
||||
return self.queue.empty()
|
||||
|
||||
def clear(self):
|
||||
self.queue.clear()
|
||||
while not self.queue.empty():
|
||||
self.queue.get_nowait()
|
||||
self.dequeue_event.set()
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.kill()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
|
||||
if self.queue.empty():
|
||||
self.idle_event.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):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class StreamPause(object):
|
||||
class StreamPause:
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.pause = False
|
||||
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()
|
||||
@@ -124,235 +198,241 @@ class StreamPause(object):
|
||||
self._pause_generator = generator
|
||||
|
||||
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):
|
||||
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 RisingEdge(self.clock)
|
||||
await clock_edge_event
|
||||
|
||||
|
||||
class StreamSource(StreamBase, StreamPause):
|
||||
|
||||
_signals = ["data", "valid", "ready"]
|
||||
_optional_signals = []
|
||||
|
||||
_signal_widths = {"valid": 1, "ready": 1}
|
||||
|
||||
_init_x = True
|
||||
|
||||
_valid_signal = "valid"
|
||||
_valid_init = 0
|
||||
_ready_signal = "ready"
|
||||
_ready_init = None
|
||||
|
||||
_transaction_obj = StreamTransaction
|
||||
def __init__(self, bus, clock, reset=None, reset_active_level=True, *args, **kwargs):
|
||||
super().__init__(bus, clock, reset, reset_active_level, *args, **kwargs)
|
||||
|
||||
def __init__(self, entity, name, clock, reset=None, *args, **kwargs):
|
||||
super().__init__(entity, name, clock, reset, *args, **kwargs)
|
||||
self.queue_occupancy_limit = -1
|
||||
|
||||
self.drive_obj = None
|
||||
self.drive_sync = Event()
|
||||
async def send(self, obj):
|
||||
while self.full():
|
||||
self.dequeue_event.clear()
|
||||
await self.dequeue_event.wait()
|
||||
await self.queue.put(obj)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
|
||||
self.active = False
|
||||
def send_nowait(self, obj):
|
||||
if self.full():
|
||||
raise QueueFull()
|
||||
self.queue.put_nowait(obj)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
|
||||
cocotb.fork(self._run_source())
|
||||
cocotb.fork(self._run())
|
||||
|
||||
async def drive(self, obj):
|
||||
if self.drive_obj is not None:
|
||||
self.drive_sync.clear()
|
||||
await self.drive_sync.wait()
|
||||
|
||||
self.drive_obj = obj
|
||||
|
||||
def send(self, obj):
|
||||
self.queue.append(obj)
|
||||
self.queue_sync.set()
|
||||
def full(self):
|
||||
if self.queue_occupancy_limit > 0 and self.count() >= self.queue_occupancy_limit:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def idle(self):
|
||||
return self.empty() and not self.active
|
||||
|
||||
async def wait(self):
|
||||
while not self.idle():
|
||||
await RisingEdge(self.clock)
|
||||
await self.idle_event.wait()
|
||||
|
||||
def clear(self):
|
||||
self.queue.clear()
|
||||
self.drive_obj = None
|
||||
self.drive_sync.set()
|
||||
def _handle_reset(self, state):
|
||||
super()._handle_reset(state)
|
||||
|
||||
async def _run_source(self):
|
||||
while True:
|
||||
await ReadOnly()
|
||||
|
||||
# read handshake signals
|
||||
ready_sample = self.ready is None or self.ready.value
|
||||
valid_sample = self.valid is None or self.valid.value
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
self.clear()
|
||||
if state:
|
||||
if self.valid is not None:
|
||||
self.valid <= 0
|
||||
self.active = False
|
||||
continue
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
|
||||
if (ready_sample and valid_sample) or (not valid_sample):
|
||||
if self.drive_obj and not self.pause:
|
||||
self.bus.drive(self.drive_obj)
|
||||
self.drive_obj = None
|
||||
self.drive_sync.set()
|
||||
if self.valid is not None:
|
||||
self.valid <= 1
|
||||
self.active = True
|
||||
else:
|
||||
if self.valid is not None:
|
||||
self.valid <= 0
|
||||
self.active = bool(self.drive_obj)
|
||||
self.valid.value = 0
|
||||
|
||||
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 not self.queue:
|
||||
self.queue_sync.clear()
|
||||
await self.queue_sync.wait()
|
||||
|
||||
await self.drive(self.queue.popleft())
|
||||
|
||||
|
||||
class StreamSink(StreamBase, StreamPause):
|
||||
|
||||
_signals = ["data", "valid", "ready"]
|
||||
_optional_signals = []
|
||||
|
||||
_signal_widths = {"valid": 1, "ready": 1}
|
||||
|
||||
_init_x = False
|
||||
|
||||
_valid_signal = "valid"
|
||||
_valid_init = None
|
||||
_ready_signal = "ready"
|
||||
_ready_init = 0
|
||||
|
||||
_transaction_obj = StreamTransaction
|
||||
|
||||
def __init__(self, entity, name, clock, reset=None, *args, **kwargs):
|
||||
super().__init__(entity, name, clock, reset, *args, **kwargs)
|
||||
|
||||
cocotb.fork(self._run_sink())
|
||||
|
||||
def recv(self):
|
||||
if self.queue:
|
||||
return self.queue.popleft()
|
||||
return None
|
||||
|
||||
async def wait(self, timeout=0, timeout_unit=None):
|
||||
if not self.empty():
|
||||
return
|
||||
self.queue_sync.clear()
|
||||
if timeout:
|
||||
await First(self.queue_sync.wait(), Timer(timeout, timeout_unit))
|
||||
else:
|
||||
await self.queue_sync.wait()
|
||||
|
||||
def callback(self, obj):
|
||||
self.queue.append(obj)
|
||||
self.queue_sync.set()
|
||||
|
||||
async def _run_sink(self):
|
||||
while True:
|
||||
await ReadOnly()
|
||||
await clock_edge_event
|
||||
|
||||
# read handshake signals
|
||||
ready_sample = self.ready is None or self.ready.value
|
||||
valid_sample = self.valid is None or self.valid.value
|
||||
ready_sample = not has_ready or self.ready.value
|
||||
valid_sample = not has_valid or self.valid.value
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
self.clear()
|
||||
if self.ready is not None:
|
||||
self.ready <= 0
|
||||
continue
|
||||
if (ready_sample and valid_sample) or (not valid_sample):
|
||||
if not self.queue.empty() and not self.pause:
|
||||
self.bus.drive(self.queue.get_nowait())
|
||||
self.dequeue_event.set()
|
||||
if has_valid:
|
||||
self.valid.value = 1
|
||||
self.active = True
|
||||
else:
|
||||
if has_valid:
|
||||
self.valid.value = 0
|
||||
self.active = not self.queue.empty()
|
||||
if self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
|
||||
if ready_sample and valid_sample:
|
||||
obj = self._transaction_obj()
|
||||
self.bus.sample(obj)
|
||||
self.callback(obj)
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
if self.ready is not None:
|
||||
self.ready <= (not self.pause)
|
||||
await self.active_event.wait()
|
||||
|
||||
|
||||
class StreamMonitor(StreamBase):
|
||||
|
||||
_signals = ["data", "valid", "ready"]
|
||||
_optional_signals = []
|
||||
|
||||
_signal_widths = {"valid": 1, "ready": 1}
|
||||
|
||||
_init_x = False
|
||||
|
||||
_valid_signal = "valid"
|
||||
_valid_init = None
|
||||
_ready_signal = "ready"
|
||||
_ready_init = None
|
||||
|
||||
_transaction_obj = StreamTransaction
|
||||
def __init__(self, bus, clock, reset=None, reset_active_level=True, *args, **kwargs):
|
||||
super().__init__(bus, clock, reset, reset_active_level, *args, **kwargs)
|
||||
|
||||
def __init__(self, entity, name, clock, reset=None, *args, **kwargs):
|
||||
super().__init__(entity, name, clock, reset, *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())
|
||||
|
||||
cocotb.fork(self._run_monitor())
|
||||
def _dequeue(self, item):
|
||||
pass
|
||||
|
||||
def recv(self):
|
||||
if self.queue:
|
||||
return self.queue.popleft()
|
||||
return None
|
||||
def _recv(self, item):
|
||||
if self.queue.empty():
|
||||
self.active_event.clear()
|
||||
self._dequeue(item)
|
||||
return item
|
||||
|
||||
def count(self):
|
||||
return len(self.queue)
|
||||
async def recv(self):
|
||||
item = await self.queue.get()
|
||||
return self._recv(item)
|
||||
|
||||
def empty(self):
|
||||
return not self.queue
|
||||
def recv_nowait(self):
|
||||
item = self.queue.get_nowait()
|
||||
return self._recv(item)
|
||||
|
||||
async def wait(self, timeout=0, timeout_unit=None):
|
||||
if not self.empty():
|
||||
return
|
||||
self.queue_sync.clear()
|
||||
if timeout:
|
||||
await First(self.queue_sync.wait(), Timer(timeout, timeout_unit))
|
||||
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||
else:
|
||||
await self.queue_sync.wait()
|
||||
await self.active_event.wait()
|
||||
|
||||
def callback(self, obj):
|
||||
self.queue.append(obj)
|
||||
self.queue_sync.set()
|
||||
async def _run_valid_monitor(self):
|
||||
event = RisingEdge(self.valid)
|
||||
|
||||
async def _run_monitor(self):
|
||||
while True:
|
||||
await ReadOnly()
|
||||
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
|
||||
ready_sample = self.ready is None or self.ready.value
|
||||
valid_sample = self.valid is None or self.valid.value
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
self.clear()
|
||||
continue
|
||||
ready_sample = not has_ready or self.ready.value
|
||||
valid_sample = not has_valid or self.valid.value
|
||||
|
||||
if ready_sample and valid_sample:
|
||||
obj = self._transaction_obj()
|
||||
self.bus.sample(obj)
|
||||
self.callback(obj)
|
||||
self.queue.put_nowait(obj)
|
||||
self.active_event.set()
|
||||
else:
|
||||
self.wake_event.clear()
|
||||
await wake_event
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
|
||||
class StreamSink(StreamMonitor, StreamPause):
|
||||
|
||||
_init_x = False
|
||||
|
||||
_valid_init = None
|
||||
_ready_init = 0
|
||||
|
||||
def __init__(self, bus, clock, reset=None, reset_active_level=True, *args, **kwargs):
|
||||
super().__init__(bus, clock, reset, reset_active_level, *args, **kwargs)
|
||||
|
||||
self.queue_occupancy_limit = -1
|
||||
|
||||
def full(self):
|
||||
if self.queue_occupancy_limit > 0 and self.count() >= self.queue_occupancy_limit:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _handle_reset(self, state):
|
||||
super()._handle_reset(state)
|
||||
|
||||
if state:
|
||||
if self.ready is not None:
|
||||
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):
|
||||
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:
|
||||
pause_sample = bool(self.pause)
|
||||
|
||||
await clock_edge_event
|
||||
|
||||
# read handshake signals
|
||||
ready_sample = not has_ready or self.ready.value
|
||||
valid_sample = not has_valid or self.valid.value
|
||||
|
||||
if ready_sample and valid_sample:
|
||||
obj = self._transaction_obj()
|
||||
self.bus.sample(obj)
|
||||
self.queue.put_nowait(obj)
|
||||
self.active_event.set()
|
||||
|
||||
if has_ready:
|
||||
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):
|
||||
@@ -393,6 +473,11 @@ def define_stream(name, signals, optional_signals=None, valid_signal=None, ready
|
||||
if s not in (ready_signal, valid_signal):
|
||||
filtered_signals.append(s)
|
||||
|
||||
attrib = {}
|
||||
attrib['_signals'] = signals
|
||||
attrib['_optional_signals'] = optional_signals
|
||||
bus = type(name+"Bus", (StreamBus,), attrib)
|
||||
|
||||
attrib = {s: 0 for s in filtered_signals}
|
||||
attrib['_signals'] = filtered_signals
|
||||
|
||||
@@ -405,9 +490,10 @@ def define_stream(name, signals, optional_signals=None, valid_signal=None, ready
|
||||
attrib['_ready_signal'] = ready_signal
|
||||
attrib['_valid_signal'] = valid_signal
|
||||
attrib['_transaction_obj'] = transaction
|
||||
attrib['_bus_obj'] = bus
|
||||
|
||||
source = type(name+"Source", (StreamSource,), attrib)
|
||||
sink = type(name+"Sink", (StreamSink,), attrib)
|
||||
monitor = type(name+"Monitor", (StreamMonitor,), attrib)
|
||||
|
||||
return transaction, source, sink, monitor
|
||||
return bus, transaction, source, sink, monitor
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -23,28 +23,28 @@ THE SOFTWARE.
|
||||
"""
|
||||
|
||||
|
||||
def hexdump_line(data, offset):
|
||||
def hexdump_line(data, offset, row_size=16):
|
||||
h = ""
|
||||
c = ""
|
||||
for ch in data[0:16]:
|
||||
for ch in data[0:row_size]:
|
||||
h += f"{ch:02x} "
|
||||
c += chr(ch) if 32 < ch < 127 else "."
|
||||
return f"{offset:08x}: {h:48} {c}"
|
||||
return f"{offset:08x}: {h:{row_size*3}} {c}"
|
||||
|
||||
|
||||
def hexdump(data, start=0, length=None, prefix="", offset=0):
|
||||
def hexdump(data, start=0, length=None, row_size=16, prefix="", offset=0):
|
||||
stop = min(start+length, len(data)) if length else len(data)
|
||||
for k in range(start, stop, 16):
|
||||
print(prefix+hexdump_line(data[k:min(k+16, stop)], k+offset))
|
||||
for k in range(start, stop, row_size):
|
||||
print(prefix+hexdump_line(data[k:min(k+row_size, stop)], k+offset, row_size))
|
||||
|
||||
|
||||
def hexdump_lines(data, start=0, length=None, prefix="", offset=0):
|
||||
def hexdump_lines(data, start=0, length=None, row_size=16, prefix="", offset=0):
|
||||
lines = []
|
||||
stop = min(start+length, len(data)) if length else len(data)
|
||||
for k in range(start, stop, 16):
|
||||
lines.append(prefix+hexdump_line(data[k:min(k+16, stop)], k+offset))
|
||||
for k in range(start, stop, row_size):
|
||||
lines.append(prefix+hexdump_line(data[k:min(k+row_size, stop)], k+offset, row_size))
|
||||
return lines
|
||||
|
||||
|
||||
def hexdump_str(data, start=0, length=None, prefix="", offset=0):
|
||||
return "\n".join(hexdump_lines(data, start, length, prefix, offset))
|
||||
def hexdump_str(data, start=0, length=None, row_size=16, prefix="", offset=0):
|
||||
return "\n".join(hexdump_lines(data, start, length, row_size, prefix, offset))
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.2"
|
||||
__version__ = "0.1.27"
|
||||
|
||||
37
setup.cfg
37
setup.cfg
@@ -13,20 +13,22 @@ project_urls =
|
||||
Source Code = https://github.com/alexforencich/cocotbext-axi
|
||||
download_url = https://github.com/alexforencich/cocotbext-axi/tarball/master
|
||||
long_description = file: README.md
|
||||
long-description-content-type = text/markdown
|
||||
long_description_content_type = text/markdown
|
||||
platforms = any
|
||||
classifiers =
|
||||
Development Status :: 3 - Alpha
|
||||
Programming Language :: Python :: 3
|
||||
Framework :: cocotb
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python :: 3
|
||||
Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
||||
|
||||
[options]
|
||||
packages = find_namespace:
|
||||
python_requires = >=3.6
|
||||
install_requires =
|
||||
cocotb
|
||||
cocotb >= 1.6.0
|
||||
cocotb-bus
|
||||
|
||||
[options.extras_require]
|
||||
test =
|
||||
@@ -45,31 +47,40 @@ addopts =
|
||||
|
||||
# tox configuration
|
||||
[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]
|
||||
python =
|
||||
3.6: py36
|
||||
3.7: py37
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311
|
||||
3.12: py312
|
||||
3.13: py313
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
COVERAGE=1
|
||||
usedevelop = True
|
||||
|
||||
deps =
|
||||
pytest
|
||||
pytest-xdist
|
||||
cocotb-test
|
||||
coverage
|
||||
pytest-cov
|
||||
pytest == 8.3.4
|
||||
pytest-xdist == 3.6.1
|
||||
cocotb == 1.9.2
|
||||
cocotb-bus == 0.2.1
|
||||
cocotb-test == 0.2.6
|
||||
coverage == 7.0.5
|
||||
pytest-cov == 4.0.0
|
||||
|
||||
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'
|
||||
coverage report
|
||||
|
||||
whitelist_externals =
|
||||
allowlist_externals =
|
||||
bash
|
||||
|
||||
# combine if paths are different
|
||||
|
||||
@@ -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
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
54
tests/apb/Makefile
Normal file
54
tests/apb/Makefile
Normal 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
332
tests/apb/test_apb.py
Normal 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
54
tests/apb/test_apb.v
Normal 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
|
||||
@@ -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
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,70 +27,34 @@ COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_axi
|
||||
TOPLEVEL = $(DUT)
|
||||
MODULE = $(DUT)
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
# module parameters
|
||||
export PARAM_DATA_WIDTH ?= 32
|
||||
export PARAM_ADDR_WIDTH ?= 32
|
||||
export PARAM_STRB_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 )
|
||||
export PARAM_ID_WIDTH ?= 8
|
||||
export PARAM_AWUSER_WIDTH ?= 1
|
||||
export PARAM_WUSER_WIDTH ?= 1
|
||||
export PARAM_BUSER_WIDTH ?= 1
|
||||
export PARAM_ARUSER_WIDTH ?= 1
|
||||
export PARAM_RUSER_WIDTH ?= 1
|
||||
|
||||
SIM_BUILD ?= sim_build_$(MODULE)-$(PARAM_DATA_WIDTH)
|
||||
export PARAM_DATA_WIDTH := 32
|
||||
export PARAM_ADDR_WIDTH := 32
|
||||
export PARAM_STRB_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 )
|
||||
export PARAM_ID_WIDTH := 8
|
||||
export PARAM_AWUSER_WIDTH := 1
|
||||
export PARAM_WUSER_WIDTH := 1
|
||||
export PARAM_BUSER_WIDTH := 1
|
||||
export PARAM_ARUSER_WIDTH := 1
|
||||
export PARAM_RUSER_WIDTH := 1
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH)
|
||||
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
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH -Wno-CASEINCOMPLETE
|
||||
|
||||
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)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
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 sim_build_*
|
||||
@rm -rf iverilog_dump.v
|
||||
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -35,20 +35,20 @@ from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge, Timer
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.axi import AxiMaster, AxiRam
|
||||
from cocotbext.axi import AxiBus, AxiMaster, AxiRam
|
||||
|
||||
|
||||
class TB(object):
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
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(dut, "axi", dut.clk, dut.rst)
|
||||
self.axi_ram = AxiRam(dut, "axi", dut.clk, dut.rst, size=2**16)
|
||||
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.write_if.log.setLevel(logging.DEBUG)
|
||||
self.axi_ram.read_if.log.setLevel(logging.DEBUG)
|
||||
@@ -73,10 +73,10 @@ class TB(object):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
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)
|
||||
self.dut.rst <= 0
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
@@ -85,7 +85,7 @@ async def run_test_write(dut, idle_inserter=None, backpressure_inserter=None, si
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.axi_master.write_if.byte_width
|
||||
byte_lanes = tb.axi_master.write_if.byte_lanes
|
||||
max_burst_size = tb.axi_master.write_if.max_burst_size
|
||||
|
||||
if size is None:
|
||||
@@ -96,8 +96,8 @@ async def run_test_write(dut, idle_inserter=None, backpressure_inserter=None, si
|
||||
tb.set_idle_generator(idle_inserter)
|
||||
tb.set_backpressure_generator(backpressure_inserter)
|
||||
|
||||
for length in list(range(1, byte_width*2))+[1024]:
|
||||
for offset in list(range(byte_width))+list(range(4096-byte_width, 4096)):
|
||||
for length in list(range(1, byte_lanes*2))+[1024]:
|
||||
for offset in list(range(byte_lanes))+list(range(4096-byte_lanes, 4096)):
|
||||
tb.log.info("length %d, offset %d", length, offset)
|
||||
addr = offset+0x1000
|
||||
test_data = bytearray([x % 256 for x in range(length)])
|
||||
@@ -120,7 +120,7 @@ async def run_test_read(dut, idle_inserter=None, backpressure_inserter=None, siz
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.axi_master.write_if.byte_width
|
||||
byte_lanes = tb.axi_master.write_if.byte_lanes
|
||||
max_burst_size = tb.axi_master.write_if.max_burst_size
|
||||
|
||||
if size is None:
|
||||
@@ -131,8 +131,8 @@ async def run_test_read(dut, idle_inserter=None, backpressure_inserter=None, siz
|
||||
tb.set_idle_generator(idle_inserter)
|
||||
tb.set_backpressure_generator(backpressure_inserter)
|
||||
|
||||
for length in list(range(1, byte_width*2))+[1024]:
|
||||
for offset in list(range(byte_width))+list(range(4096-byte_width, 4096)):
|
||||
for length in list(range(1, byte_lanes*2))+[1024]:
|
||||
for offset in list(range(byte_lanes))+list(range(4096-byte_lanes, 4096)):
|
||||
tb.log.info("length %d, offset %d", length, offset)
|
||||
addr = offset+0x1000
|
||||
test_data = bytearray([x % 256 for x in range(length)])
|
||||
@@ -151,15 +151,20 @@ async def run_test_write_words(dut):
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.axi_master.write_if.byte_width
|
||||
byte_lanes = tb.axi_master.write_if.byte_lanes
|
||||
|
||||
await tb.cycle_reset()
|
||||
|
||||
for length in list(range(1, 4)):
|
||||
for offset in list(range(byte_width)):
|
||||
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.axi_master.init_write(addr, test_data)
|
||||
await event.wait()
|
||||
assert tb.axi_ram.read(addr, length) == test_data
|
||||
|
||||
test_data = bytearray([x % 256 for x in range(length)])
|
||||
await tb.axi_master.write(addr, test_data)
|
||||
assert tb.axi_ram.read(addr, length) == test_data
|
||||
@@ -200,15 +205,21 @@ async def run_test_read_words(dut):
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.axi_master.write_if.byte_width
|
||||
byte_lanes = tb.axi_master.write_if.byte_lanes
|
||||
|
||||
await tb.cycle_reset()
|
||||
|
||||
for length in list(range(1, 4)):
|
||||
for offset in list(range(byte_width)):
|
||||
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.axi_ram.write(addr, test_data)
|
||||
event = tb.axi_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.axi_ram.write(addr, test_data)
|
||||
assert (await tb.axi_master.read(addr, length)).data == test_data
|
||||
@@ -272,7 +283,7 @@ async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
|
||||
workers = []
|
||||
|
||||
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:
|
||||
await workers.pop(0).join()
|
||||
@@ -285,11 +296,11 @@ def cycle_pause():
|
||||
return itertools.cycle([1, 1, 1, 0])
|
||||
|
||||
|
||||
if cocotb.SIM_NAME:
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
data_width = int(os.getenv("PARAM_DATA_WIDTH"))
|
||||
byte_width = data_width // 8
|
||||
max_burst_size = (byte_width-1).bit_length()
|
||||
data_width = len(cocotb.top.axi_wdata)
|
||||
byte_lanes = data_width // 8
|
||||
max_burst_size = (byte_lanes-1).bit_length()
|
||||
|
||||
for test in [run_test_write, run_test_read]:
|
||||
|
||||
@@ -311,7 +322,6 @@ if cocotb.SIM_NAME:
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data_width", [8, 16, 32])
|
||||
@@ -338,8 +348,8 @@ def test_axi(request, data_width):
|
||||
|
||||
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(']', ''))
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -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
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,52 +27,28 @@ COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_axil
|
||||
TOPLEVEL = $(DUT)
|
||||
MODULE = $(DUT)
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
# module parameters
|
||||
export PARAM_DATA_WIDTH ?= 32
|
||||
export PARAM_ADDR_WIDTH ?= 32
|
||||
export PARAM_STRB_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 )
|
||||
|
||||
SIM_BUILD ?= sim_build_$(MODULE)-$(PARAM_DATA_WIDTH)
|
||||
export PARAM_DATA_WIDTH := 32
|
||||
export PARAM_ADDR_WIDTH := 32
|
||||
export PARAM_STRB_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 )
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH)
|
||||
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
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
|
||||
|
||||
COMPILE_ARGS += -GDATA_WIDTH=$(PARAM_DATA_WIDTH)
|
||||
COMPILE_ARGS += -GADDR_WIDTH=$(PARAM_ADDR_WIDTH)
|
||||
COMPILE_ARGS += -GSTRB_WIDTH=$(PARAM_STRB_WIDTH)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
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 sim_build_*
|
||||
@rm -rf iverilog_dump.v
|
||||
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -35,20 +35,20 @@ from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge, Timer
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.axi import AxiLiteMaster, AxiLiteRam
|
||||
from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiLiteRam
|
||||
|
||||
|
||||
class TB(object):
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
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(dut, "axil", dut.clk, dut.rst)
|
||||
self.axil_ram = AxiLiteRam(dut, "axil", dut.clk, dut.rst, size=2**16)
|
||||
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)
|
||||
|
||||
def set_idle_generator(self, generator=None):
|
||||
if generator:
|
||||
@@ -70,10 +70,10 @@ class TB(object):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
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)
|
||||
self.dut.rst <= 0
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
@@ -82,15 +82,15 @@ async def run_test_write(dut, data_in=None, idle_inserter=None, backpressure_ins
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.axil_master.write_if.byte_width
|
||||
byte_lanes = tb.axil_master.write_if.byte_lanes
|
||||
|
||||
await tb.cycle_reset()
|
||||
|
||||
tb.set_idle_generator(idle_inserter)
|
||||
tb.set_backpressure_generator(backpressure_inserter)
|
||||
|
||||
for length in range(1, byte_width*2):
|
||||
for offset in range(byte_width):
|
||||
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)])
|
||||
@@ -113,15 +113,15 @@ async def run_test_read(dut, data_in=None, idle_inserter=None, backpressure_inse
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.axil_master.write_if.byte_width
|
||||
byte_lanes = tb.axil_master.write_if.byte_lanes
|
||||
|
||||
await tb.cycle_reset()
|
||||
|
||||
tb.set_idle_generator(idle_inserter)
|
||||
tb.set_backpressure_generator(backpressure_inserter)
|
||||
|
||||
for length in range(1, byte_width*2):
|
||||
for offset in range(byte_width):
|
||||
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)])
|
||||
@@ -140,15 +140,20 @@ async def run_test_write_words(dut):
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.axil_master.write_if.byte_width
|
||||
byte_lanes = tb.axil_master.write_if.byte_lanes
|
||||
|
||||
await tb.cycle_reset()
|
||||
|
||||
for length in list(range(1, 4)):
|
||||
for offset in list(range(byte_width)):
|
||||
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.axil_master.init_write(addr, test_data)
|
||||
await event.wait()
|
||||
assert tb.axil_ram.read(addr, length) == test_data
|
||||
|
||||
test_data = bytearray([x % 256 for x in range(length)])
|
||||
await tb.axil_master.write(addr, test_data)
|
||||
assert tb.axil_ram.read(addr, length) == test_data
|
||||
@@ -189,15 +194,21 @@ async def run_test_read_words(dut):
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.axil_master.write_if.byte_width
|
||||
byte_lanes = tb.axil_master.write_if.byte_lanes
|
||||
|
||||
await tb.cycle_reset()
|
||||
|
||||
for length in list(range(1, 4)):
|
||||
for offset in list(range(byte_width)):
|
||||
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.axil_ram.write(addr, test_data)
|
||||
event = tb.axil_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.axil_ram.write(addr, test_data)
|
||||
assert (await tb.axil_master.read(addr, length)).data == test_data
|
||||
@@ -261,7 +272,7 @@ async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
|
||||
workers = []
|
||||
|
||||
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:
|
||||
await workers.pop(0).join()
|
||||
@@ -274,7 +285,7 @@ def cycle_pause():
|
||||
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]:
|
||||
|
||||
@@ -295,7 +306,6 @@ if cocotb.SIM_NAME:
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data_width", [8, 16, 32])
|
||||
@@ -316,8 +326,8 @@ def test_axil(request, data_width):
|
||||
|
||||
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(']', ''))
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -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
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,58 +27,30 @@ COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_axis
|
||||
TOPLEVEL = $(DUT)
|
||||
MODULE = $(DUT)
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
# module parameters
|
||||
export PARAM_DATA_WIDTH ?= 8
|
||||
export PARAM_KEEP_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 )
|
||||
export PARAM_ID_WIDTH ?= 8
|
||||
export PARAM_DEST_WIDTH ?= 8
|
||||
export PARAM_USER_WIDTH ?= 1
|
||||
|
||||
SIM_BUILD ?= sim_build_$(MODULE)-$(PARAM_DATA_WIDTH)
|
||||
export PARAM_DATA_WIDTH := 8
|
||||
export PARAM_KEEP_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 )
|
||||
export PARAM_ID_WIDTH := 8
|
||||
export PARAM_DEST_WIDTH := 8
|
||||
export PARAM_USER_WIDTH := 1
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH)
|
||||
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
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
|
||||
|
||||
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)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
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 sim_build_*
|
||||
@rm -rf iverilog_dump.v
|
||||
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -35,21 +35,21 @@ from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.axi import AxiStreamFrame, AxiStreamSource, AxiStreamSink, AxiStreamMonitor
|
||||
from cocotbext.axi import AxiStreamFrame, AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamMonitor
|
||||
|
||||
|
||||
class TB(object):
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
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(dut, "axis", dut.clk, dut.rst)
|
||||
self.sink = AxiStreamSink(dut, "axis", dut.clk, dut.rst)
|
||||
self.monitor = AxiStreamMonitor(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.monitor = AxiStreamMonitor(AxiStreamBus.from_prefix(dut, "axis"), dut.clk, dut.rst)
|
||||
|
||||
def set_idle_generator(self, generator=None):
|
||||
if generator:
|
||||
@@ -63,10 +63,10 @@ class TB(object):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
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)
|
||||
self.dut.rst <= 0
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
@@ -90,28 +90,29 @@ async def run_test(dut, payload_lengths=None, payload_data=None, idle_inserter=N
|
||||
test_frame = AxiStreamFrame(test_data)
|
||||
test_frame.tid = cur_id
|
||||
test_frame.tdest = cur_id
|
||||
tb.source.send(test_frame)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
test_frames.append(test_frame)
|
||||
|
||||
cur_id = (cur_id + 1) % id_count
|
||||
|
||||
for test_frame in test_frames:
|
||||
await tb.sink.wait()
|
||||
rx_frame = tb.sink.recv()
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.tdata == test_frame.tdata
|
||||
assert rx_frame.tid == test_frame.tid
|
||||
assert rx_frame.tdest == test_frame.tdest
|
||||
assert not rx_frame.tuser
|
||||
|
||||
await tb.monitor.wait()
|
||||
rx_frame = tb.monitor.recv()
|
||||
mon_rx_frame = await tb.monitor.recv()
|
||||
|
||||
assert rx_frame.tdata == test_frame.tdata
|
||||
assert rx_frame.tid == test_frame.tid
|
||||
assert rx_frame.tdest == test_frame.tdest
|
||||
assert not rx_frame.tuser
|
||||
assert mon_rx_frame.tdata == test_frame.tdata
|
||||
assert mon_rx_frame.tid == test_frame.tid
|
||||
assert mon_rx_frame.tdest == test_frame.tdest
|
||||
assert not mon_rx_frame.tuser
|
||||
|
||||
assert rx_frame.sim_time_start == mon_rx_frame.sim_time_start
|
||||
assert rx_frame.sim_time_end == mon_rx_frame.sim_time_end
|
||||
|
||||
assert tb.sink.empty()
|
||||
assert tb.monitor.empty()
|
||||
@@ -125,7 +126,7 @@ def cycle_pause():
|
||||
|
||||
|
||||
def size_list():
|
||||
data_width = int(os.getenv("PARAM_DATA_WIDTH"))
|
||||
data_width = len(cocotb.top.axis_tdata)
|
||||
byte_width = data_width // 8
|
||||
return list(range(1, byte_width*4+1)) + [512] + [1]*64
|
||||
|
||||
@@ -134,7 +135,7 @@ def incrementing_payload(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.add_option("payload_lengths", [size_list])
|
||||
@@ -147,7 +148,6 @@ if cocotb.SIM_NAME:
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data_width", [8, 16, 32])
|
||||
@@ -170,8 +170,8 @@ def test_axis(request, data_width):
|
||||
|
||||
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(']', ''))
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
|
||||
@@ -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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
44
tests/test_buddy_allocator.py
Normal file
44
tests/test_buddy_allocator.py
Normal 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]
|
||||
Reference in New Issue
Block a user