134 Commits

Author SHA1 Message Date
Alex Forencich
7606d7d7bd Release v0.1.18 2021-12-28 17:23:06 -08:00
Alex Forencich
dd345e87c3 Call write from init_write via start_soon so command FIFO size can be limited 2021-12-27 23:29:29 -08:00
Alex Forencich
9c0592c16a Make wstrb optional 2021-12-27 19:44:30 -08:00
Alex Forencich
8aab5a7294 Support overriding allocated region and window types 2021-12-27 17:58:52 -08:00
Alex Forencich
4f26621e2b Make size optional when creating windows 2021-12-27 17:31:08 -08:00
Alex Forencich
1b6993d80d Use start_soon instead of fork 2021-12-27 17:10:37 -08:00
Alex Forencich
6d9ed8a2d2 Specify min package versions 2021-12-27 17:03:19 -08:00
Alex Forencich
d772b73eb2 Specify min tox and venv versions 2021-12-27 17:02:59 -08:00
Alex Forencich
bd88eda17b Skip missing interpreters 2021-12-27 17:02:39 -08:00
Alex Forencich
53313699a9 Test on Python 3.10 2021-12-27 17:00:44 -08:00
Alex Forencich
3f7193b77c Use start_soon instead of fork 2021-12-08 21:38:12 -08:00
Alex Forencich
2b0b12c68d Cache clock edge event objects 2021-12-03 18:40:04 -08:00
Alex Forencich
4a91212f37 Bump to dev version 2021-11-17 00:06:34 -08:00
Alex Forencich
1608af26e5 Release v0.1.16 2021-11-16 22:54:37 -08:00
Alex Forencich
31fb855311 Update readme 2021-11-16 22:40:42 -08:00
Alex Forencich
cb4b0e1738 Wrap access on RAM size 2021-11-16 17:13:18 -08:00
Alex Forencich
ea95eeaf0d Don't pass through extra positional args 2021-11-16 17:01:31 -08:00
Alex Forencich
079f4009b3 Rewrite RAM modules to use common slave implementation 2021-11-16 17:00:48 -08:00
Alex Forencich
612a94c97a Add AXI and AXI lite slave modules 2021-11-16 17:00:05 -08:00
Alex Forencich
757e3a6f2d AXI master modules extend Region 2021-11-16 16:58:50 -08:00
Alex Forencich
b9b9a2da72 Add address space abstraction 2021-11-16 16:55:53 -08:00
Alex Forencich
f7660e9038 Add buddy allocator 2021-11-16 16:53:40 -08:00
Alex Forencich
78693a63d5 Fix types 2021-11-10 23:59:11 -08:00
Alex Forencich
34498f6e5d Add write data type check 2021-11-10 23:49:13 -08:00
Alex Forencich
6329187ced Add address range checks 2021-11-10 23:48:47 -08:00
Alex Forencich
da24857dd2 Cast write data to bytes instead of bytearray 2021-11-10 23:45:46 -08:00
Alex Forencich
c08f22c710 Bring out address and ID signal widths 2021-11-10 21:56:08 -08:00
Alex Forencich
d874d91d05 Use typing.NamedTuple instead of collections.namedtuple to add __bytes__ cast 2021-11-10 21:49:58 -08:00
Alex Forencich
3fd016a84c Lazy logging 2021-11-09 00:53:37 -08:00
Alex Forencich
43de2ea9b0 Use getattr with default value when accessing optional signals 2021-11-09 00:46:37 -08:00
Alex Forencich
558ba51c91 Use correct transaction object 2021-11-09 00:13:19 -08:00
Alex Forencich
f6426bd8f3 Bump to dev version 2021-11-07 13:12:32 -08:00
Alex Forencich
74dd47ca99 Release v0.1.14 2021-11-07 12:39:42 -08:00
Reto Meier
cde2056bb0 Remove deprecated <= assignments
Starting from cocotb v1.6 the use of <= syntax has been deprecated. This
commit replaces all use of this syntax with the ``.value =`` syntax.
2021-10-25 18:27:18 +02:00
Alex Forencich
b6870716ed Fix active state tracking for AXI stream sink/monitor 2021-09-15 00:46:01 -07:00
Alex Forencich
44da562db9 Add cocotb framework classifier 2021-08-31 14:43:59 -07:00
Alex Forencich
8dcdbfefb8 Bump to dev version 2021-04-12 22:56:51 -07:00
Alex Forencich
b8919a095b Release v0.1.12 2021-04-12 22:46:32 -07:00
Alex Forencich
bc7edec289 Make resp and prot signals optional 2021-04-12 22:04:22 -07:00
Alex Forencich
e7c3a31eb0 Improve handling for optional signals 2021-04-12 21:24:33 -07:00
Alex Forencich
ce907ffbb9 Print out signal summary 2021-04-12 19:29:41 -07:00
Alex Forencich
95e2d5800d Store parameters 2021-04-12 19:27:38 -07:00
Alex Forencich
82853b31ff Rename byte_width to byte_lanes 2021-04-12 15:08:30 -07:00
Alex Forencich
8bbabd92df Update readme 2021-04-12 15:07:26 -07:00
Alex Forencich
c060f6c963 Transmit data without using pop 2021-04-12 13:53:28 -07:00
Alex Forencich
a767e00ce5 Transmit frames without using pop 2021-04-12 13:49:20 -07:00
Alex Forencich
d1d7313b98 Add tag context manager to AXI master to reuse per-ID processing components 2021-04-08 19:03:46 -07:00
Alex Forencich
01b43b97f2 Update readme 2021-04-08 19:01:27 -07:00
Alex Forencich
b5b6df84fe Improve burst handling in AXI master 2021-03-25 18:03:36 -07:00
Alex Forencich
babe69f4d3 Bump to dev version 2021-03-24 21:50:10 -07:00
Alex Forencich
c4873ad14c Release v0.1.10 2021-03-24 21:03:16 -07:00
Alex Forencich
77a40bdc8f Limit channel queue depth 2021-03-24 17:55:07 -07:00
Alex Forencich
f991096272 Separate processing coroutines for each ID 2021-03-24 17:07:16 -07:00
Alex Forencich
9e28bd7fbb Revert back to cocotb.fork 2021-03-24 16:20:08 -07:00
Alex Forencich
8c74f747a4 Update readme 2021-03-22 23:19:14 -07:00
Alex Forencich
a285f008ca Refactor reset handling code 2021-03-22 22:02:53 -07:00
Alex Forencich
c677ab245c Reset more internal state 2021-03-22 22:02:06 -07:00
Alex Forencich
11f9db8b06 Add test cases for init_read and init_write 2021-03-22 21:22:13 -07:00
Alex Forencich
344ec8d4ce Return event object from init_read and init_write; remove get_write_resp and get_read_data 2021-03-22 21:21:34 -07:00
Alex Forencich
4ff390481e Extract parameter values from cocotb.top 2021-03-22 15:51:07 -07:00
Alex Forencich
4bee96ea9a Enforce max queue depth on streaming sources 2021-03-21 22:24:59 -07:00
Alex Forencich
a66dfea6f7 Factor out common recv code; throw QueueEmpty exception in get_nowait 2021-03-21 21:02:28 -07:00
Alex Forencich
f1a89e6c12 Trigger transmit complete events when flushing queue to prevent deadlocks 2021-03-21 18:46:30 -07:00
Alex Forencich
11205bde46 Handle dropped transmit frames during reset 2021-03-21 18:39:35 -07:00
Alex Forencich
bce364eef5 Ensure idle event is set when queue is empty 2021-03-21 18:39:10 -07:00
Alex Forencich
e934b69776 Use start_soon instead of fork 2021-03-21 12:22:22 -07:00
Alex Forencich
7fb8c4e28b Reset processing on assert edge only to permit operations to be queued while reset is asserted 2021-03-21 12:13:19 -07:00
Alex Forencich
156fada616 Store commands currently being processed so they can be released when the processing coroutines are killed 2021-03-21 12:04:30 -07:00
Alex Forencich
d88ba7caf3 Warn when operations are dropped during reset 2021-03-21 11:41:25 -07:00
Alex Forencich
6c66776518 Use start_soon instead of fork 2021-03-21 11:40:25 -07:00
Alex Forencich
a71678c7e3 Bump to dev version 2021-03-17 18:32:42 -07:00
Alex Forencich
c0ebb90cd4 Release v0.1.8 2021-03-17 18:31:54 -07:00
Alex Forencich
56caf57fa4 Fix method name 2021-03-17 18:19:30 -07:00
Alex Forencich
abb78308ff Defer idle event until completion of transfer 2021-03-17 18:02:11 -07:00
Alex Forencich
f19ca9f651 Use cocotb async queues 2021-03-17 17:34:26 -07:00
Alex Forencich
1c40b8fa58 Use cocotb-bus 2021-03-16 18:47:32 -07:00
Alex Forencich
cfd5dae6ea Bump to dev version v0.1.7 2021-03-06 18:30:19 -08:00
Alex Forencich
f2c36276f3 Release v0.1.6 2021-03-06 18:10:43 -08:00
Alex Forencich
eab0c7fee0 Update readme 2021-03-06 17:58:11 -08:00
Alex Forencich
35ed1472d6 Add reset_active_level parameters 2021-03-06 17:30:05 -08:00
Alex Forencich
a7fe5d9674 Clean up reset implementation 2021-03-06 17:17:28 -08:00
Alex Forencich
08122c1a65 Use -1 instead of None for no limit 2021-03-06 17:04:37 -08:00
Alex Forencich
8e76f2d24c Update readme 2021-03-06 16:36:49 -08:00
Alex Forencich
69717c1698 Add AXI bus objects 2021-03-06 16:26:51 -08:00
Alex Forencich
c18fdd6e22 Consolidate AXI stream implementation to remove duplicate code 2021-01-08 16:08:36 -08:00
Alex Forencich
222af15437 Clear sim_time_end on start transmit 2021-01-08 15:54:26 -08:00
Alex Forencich
ffaf8932fc Rewrite stream resets 2021-01-07 20:30:04 -08:00
Alex Forencich
8f719daf75 Add queue_occupancy_limit to StreamSink 2021-01-07 20:08:44 -08:00
Alex Forencich
154d3b11f4 Remove drive 2021-01-07 20:08:19 -08:00
Alex Forencich
97abb032d8 Use send instead of drive 2021-01-07 20:07:02 -08:00
Alex Forencich
5593bfe296 Rebase StreamSink from StreamMonitor 2021-01-07 18:48:17 -08:00
Alex Forencich
3caa343845 Remove callbacks 2021-01-07 18:10:33 -08:00
Alex Forencich
6654c707b2 Remove redundant code 2021-01-07 18:10:07 -08:00
Alex Forencich
359e015b35 Add row_size parameter to hexdump methods 2021-01-06 18:39:03 -08:00
Alex Forencich
7e0464f6f3 Update readme 2021-01-04 22:43:36 -08:00
Alex Forencich
4ec0c36892 Support override of tx_complete 2021-01-04 22:43:17 -08:00
Alex Forencich
7c18f9b73f Improve AXI stream transfer tracking 2021-01-03 22:47:38 -08:00
Alex Forencich
2e79ef233f Rework resets 2021-01-03 12:39:25 -08:00
Alex Forencich
7748f871f4 Add clear to AXI stream models 2021-01-02 16:15:51 -08:00
Alex Forencich
f6823fc8fd Remove extraneous code 2020-12-31 03:11:51 -08:00
Alex Forencich
c63d65bb87 Rework sim_build output directory, fix default makefile target 2020-12-29 14:25:44 -08:00
Alex Forencich
1618220a30 Rework AXI stream resets 2020-12-24 23:19:58 -08:00
Alex Forencich
ecb2f5ae81 Add idle event 2020-12-24 19:25:26 -08:00
Alex Forencich
fcf6374c3c Remove inherit from object 2020-12-24 14:44:42 -08:00
Alex Forencich
7582067929 Remove await ReadOnly 2020-12-24 00:24:03 -08:00
Alex Forencich
7b7e8bf3e2 Update readme 2020-12-24 00:23:34 -08:00
Alex Forencich
426c5828f1 Update readme 2020-12-22 17:00:48 -08:00
Alex Forencich
44e58275ad Update test 2020-12-22 17:00:36 -08:00
Alex Forencich
f4f7c0b59d Store receive sim time in AxiStreamFrame 2020-12-21 23:09:29 -08:00
Alex Forencich
ad83d58b01 Init as bytearray instead of convert to bytearray 2020-12-21 23:09:02 -08:00
Alex Forencich
4336c32b40 Bump to dev version v0.1.5 2020-12-18 16:22:45 -08:00
Alex Forencich
8668a6bb4f Release v0.1.4 2020-12-18 16:12:13 -08:00
Alex Forencich
bdaeaad66b Update readme 2020-12-18 16:10:26 -08:00
Alex Forencich
cd272b2a59 Convert send/recv to blocking, add nonblocking send_nowait/recv_nowait 2020-12-18 15:33:23 -08:00
Alex Forencich
01212e37cd Minor refactoring, remove extra conversion 2020-12-17 00:27:17 -08:00
Alex Forencich
40a9bb9eac Remove unnecessary __init__.py files 2020-12-15 18:42:23 -08:00
Alex Forencich
1103783b08 Return tdata when AxiStreamFrame is cast to bytes 2020-12-11 23:45:23 -08:00
Alex Forencich
2451921923 Replace SimLog with logger 2020-12-10 02:20:14 -08:00
Alex Forencich
e6e8a06dfe Accept byte_size or byte_lanes arguments in AXI stream constructors if tkeep is not used 2020-12-07 01:58:49 -08:00
Alex Forencich
3dd8114c05 Rename byte_width to byte_lanes 2020-12-07 01:50:48 -08:00
Alex Forencich
a84b52077b Update readme 2020-12-06 00:50:28 -08:00
Alex Forencich
200c8c0b26 Update readme 2020-12-06 00:02:07 -08:00
Alex Forencich
0ff3d64540 Update readme 2020-12-05 01:36:09 -08:00
Alex Forencich
237745792c Bump to dev version v0.1.3 2020-12-04 13:40:22 -08:00
Alex Forencich
5f032b3c9b Release v0.1.2 2020-12-04 13:39:22 -08:00
Alex Forencich
31081c579b Point badge at correct project 2020-12-04 13:17:53 -08:00
Tomasz Hemperek
595e0b1b49 Add status badges 2020-12-04 13:16:10 -08:00
Alex Forencich
22b10f2dd8 Preload ID queue list 2020-12-04 12:39:25 -08:00
Alex Forencich
db3841dd99 Improve alignment bit masks 2020-12-04 00:51:32 -08:00
Alex Forencich
1057058cb3 Shorten stress test worker name 2020-12-03 21:03:05 -08:00
Alex Forencich
e5f2be4468 Use logger formatting instead of F-strings 2020-12-03 21:02:17 -08:00
Alex Forencich
4b5b62419b Add missing parameters 2020-12-03 19:34:03 -08:00
Alex Forencich
03f8fe0fd3 Use pytest importlib mode 2020-12-03 19:33:40 -08:00
Tomasz Hemperek
435b9c9282 Add coverge reporting in CI and upload to codecov 2020-12-03 12:52:51 -08:00
31 changed files with 3244 additions and 1743 deletions

View File

@@ -9,7 +9,7 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: [3.6, 3.7, 3.8, 3.9] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
@@ -30,3 +30,8 @@ jobs:
- name: Test with tox - name: Test with tox
run: tox run: tox
- name: Upload coverage to codecov
run: |
pip install codecov
codecov

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ __pycache__/
.tox .tox
sim_build_* sim_build_*
.coverage

309
README.md
View File

@@ -1,10 +1,15 @@
# AXI interface modules for Cocotb # AXI interface modules for Cocotb
[![Build Status](https://github.com/alexforencich/cocotbext-axi/workflows/Regression%20Tests/badge.svg?branch=master)](https://github.com/alexforencich/cocotbext-axi/actions/)
[![codecov](https://codecov.io/gh/alexforencich/cocotbext-axi/branch/master/graph/badge.svg)](https://codecov.io/gh/alexforencich/cocotbext-axi)
[![PyPI version](https://badge.fury.io/py/cocotbext-axi.svg)](https://pypi.org/project/cocotbext-axi)
[![Downloads](https://pepy.tech/badge/cocotbext-axi)](https://pepy.tech/project/cocotbext-axi)
GitHub repository: https://github.com/alexforencich/cocotbext-axi GitHub repository: https://github.com/alexforencich/cocotbext-axi
## Introduction ## Introduction
AXI, AXI lite, and AXI stream simulation models for cocotb. AXI, AXI lite, and AXI stream simulation models for [cocotb](https://github.com/cocotb/cocotb).
## Installation ## Installation
@@ -27,53 +32,44 @@ See the `tests` directory, [verilog-axi](https://github.com/alexforencich/verilo
### AXI and AXI lite master ### AXI and AXI lite 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` 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. `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.
To use these modules, import the one you need and connect it to the DUT: To use these modules, import the one you need and connect it to the DUT:
from cocotbext.axi import 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: 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:
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:
await axi_master.write(0x0000, b'test') await axi_master.write(0x0000, b'test')
data = await axi_master.read(0x0000, 4) 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.
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:
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` and `AxiLiteMaster` constructor parameters #### `AxiMaster` and `AxiLiteMaster` constructor parameters
* _entity_: object that contains the AXI slave interface signals * _bus_: `AxiBus` or `AxiLiteBus` object containing AXI interface signals
* _name_: signal name prefix (e.g. for `s_axi_awaddr`, the prefix is `s_axi`)
* _clock_: clock signal * _clock_: clock signal
* _reset_: reset signal (optional) * _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`)
#### Additional parameters for `AxiMaster` #### Additional parameters for `AxiMaster`
@@ -81,32 +77,28 @@ Second, blocking operations can be carried out with `read()` and `write()` and t
#### Methods #### Methods
* `init_read(address, length, ...)`: initiate reading _length_ bytes, starting at _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_ * `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 * `idle()`: returns _True_ when there are no outstanding operations in progress
* `wait()`: blocking wait until all outstanding operations complete * `wait()`: blocking wait until all outstanding operations complete
* `wait_read()`: wait until all outstanding read operations complete * `wait_read()`: wait until all outstanding read operations complete
* `wait_write()`: wait until all outstanding write 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(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_words(address, count, byteorder='little', ws=2, ...)`: read _count_ _ws_-byte words, starting at _address_
* `read_dwords(address, count, byteorder, ...)`: read _count_ 4-byte dwords, starting at _address_, default _byteorder_ `"little"` * `read_dwords(address, count, byteorder='little', ...)`: read _count_ 4-byte dwords, starting at _address_
* `read_qwords(address, count, byteorder, ...)`: read _count_ 8-byte qwords, starting at _address_, default _byteorder_ `"little"` * `read_qwords(address, count, byteorder='little', ...)`: read _count_ 8-byte qwords, starting at _address_
* `read_byte(address, ...)`: read single byte 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_word(address, byteorder='little', ws=2, ...)`: read single _ws_-byte word at _address_
* `read_dword(address, byteorder, ...)`: read single 4-byte dword at _address_, default _byteorder_ `"little"` * `read_dword(address, byteorder='little', ...)`: read single 4-byte dword at _address_
* `read_qword(address, byteorder, ...)`: read single 8-byte qword at _address_, default _byteorder_ `"little"` * `read_qword(address, byteorder='little', ...)`: read single 8-byte qword at _address_
* `write(address, data, ...)`: write _data_ (bytes), starting 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_words(address, data, byteorder='little', ws=2, ...)`: write _data_ (_ws_-byte words), starting at _address_
* `write_dwords(address, data, byteorder, ...)`: write _data_ (4-byte dwords), starting at _address_, default _byteorder_ `"little"` * `write_dwords(address, data, byteorder='little', ...)`: write _data_ (4-byte dwords), starting at _address_
* `write_qwords(address, data, byteorder, ...)`: write _data_ (8-byte qwords), starting at _address_, default _byteorder_ `"little"` * `write_qwords(address, data, byteorder='little', ...)`: write _data_ (8-byte qwords), starting at _address_
* `write_byte(address, data, ...)`: write single byte 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_word(address, data, byteorder='little', ws=2, ...)`: write single _ws_-byte word at _address_
* `write_dword(address, data, byteorder, ...)`: write single 4-byte dword at _address_, default _byteorder_ `"little"` * `write_dword(address, data, byteorder='little', ...)`: write single 4-byte dword at _address_
* `write_qword(address, data, byteorder, ...)`: write single 8-byte qword at _address_, default _byteorder_ `"little"` * `write_qword(address, data, byteorder='little', ...)`: write single 8-byte qword at _address_
#### Additional optional arguments for `AxiMaster` #### Additional optional arguments for `AxiMaster`
@@ -120,45 +112,80 @@ Second, blocking operations can be carried out with `read()` and `write()` and t
* _region_: AXI region field, default `0` * _region_: AXI region field, default `0`
* _user_: AXI user signal (awuser/aruser), default `0` * _user_: AXI user signal (awuser/aruser), default `0`
* _wuser_: AXI wuser signal, default `0` (write-related methods only) * _wuser_: AXI wuser signal, default `0` (write-related methods only)
* _event_: `Event` object used to wait on and retrieve result for specific operation, default `None` (`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`
* _prot_: AXI protection flags, default `AxiProt.NONSECURE` * _prot_: AXI protection flags, default `AxiProt.NONSECURE`
* _event_: `Event` object used to wait on and retrieve result for specific operation, default `None` (`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)
#### `AxiBus` and `AxiLiteBus` objects
The `AxiBus`, `AxiLiteBus`, and related objects are containers for the interface signals. These hold instances of bus objects for the individual channels, which are currently extensions of `cocotb_bus.bus.Bus`. Class methods `from_entity` and `from_prefix` are provided to facilitate signal name matching. For AXI interfaces use `AxiBus`, `AxiReadBus`, or `AxiWriteBus`, as appropriate. For AXI lite interfaces, use `AxiLiteBus`, `AxiLiteReadBus`, or `AxiLiteWriteBus`, as appropriate.
### AXI and AXI lite slave
The `AxiSlave` and `AxiLiteSlave` classes implement AXI 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.
To use these modules, import the one you need and connect it to the DUT:
from cocotbext.axi import AxiBus, AxiSlave, MemoryRegion
axi_slave = AxiSlave(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst)
region = MemoryRegion(2**axi_slave.read_if.address_width)
axi_slave.target = region
The first argument to the constructor accepts an `AxiBus` or `AxiLiteBus` object. These objects are containers for the interface signals and include class methods to automate connections.
It is also possible to extend these modules; operation can be customized by overriding the internal `_read()` and `_write()` methods. See `AxiRam` and `AxiLiteRam` for an example.
#### `AxiSlave` and `AxiLiteSlave` constructor parameters
* _bus_: `AxiBus` or `AxiLiteBus` object containing AXI interface signals
* _clock_: clock signal
* _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`)
* _target_: target region (optional, default `None`)
#### Attributes:
* _target_: target region
### AXI and AXI lite RAM ### AXI and AXI lite RAM
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 `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. These modules are extensions of the corresponding `AxiSlave` and `AxiLiteSlave` modules.
The `AxiRam` is a wrapper around `AxiRamWrite` and `AxiRamRead`. Similarly, `AxiLiteRam` is a wrapper around `AxiLiteRamWrite` and `AxiLiteRamRead`. If a read-only or write-only interface is required instead of a full interface, use the corresponding read-only or write-only variant, the usage and API are exactly the same. The `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.
To use these modules, import the one you need and connect it to the DUT: To use these modules, import the one you need and connect it to the DUT:
from cocotbext.axi import AxiRam from cocotbext.axi import AxiBus, AxiRam
axi_ram = AxiRam(dut, "m_axi", dut.clk, dut.rst, size=2**16) axi_ram = AxiRam(AxiBus.from_prefix(dut, "m_axi"), dut.clk, dut.rst, size=2**16)
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. These objects are containers for the interface signals and include class methods to automate connections.
Once the module is instantiated, the memory contents can be accessed in a couple of different ways. First, the `mmap` object can be accessed directly via the `mem` attribute. Second, `read()`, `write()`, and various word-access wrappers are available. Hex dump helper methods are also provided for debugging. For example: Once the module is instantiated, the memory contents can be accessed in a couple of different ways. First, the `mmap` object can be accessed directly via the `mem` attribute. Second, `read()`, `write()`, and various word-access wrappers are available. Hex dump helper methods are also provided for debugging. For example:
axi_ram.write(0x0000, b'test') axi_ram.write(0x0000, b'test')
data = axi_ram.read(0x0000, 4) data = axi_ram.read(0x0000, 4)
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: 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_p1 = AxiRam(AxiBus.from_prefix(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_p2 = AxiRam(AxiBus.from_prefix(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_p3 = AxiRam(AxiBus.from_prefix(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) axi_ram_p4 = AxiRam(AxiBus.from_prefix(dut, "m03_axi"), dut.clk, dut.rst, mem=axi_ram_p1.mem)
#### `AxiRam` and `AxiLiteRam` constructor parameters #### `AxiRam` and `AxiLiteRam` constructor parameters
* _entity_: object that contains the AXI master interface signals * _bus_: `AxiBus` or `AxiLiteBus` object containing AXI interface signals
* _name_: signal name prefix (e.g. for `m_axi_awaddr`, the prefix is `m_axi`)
* _clock_: clock signal * _clock_: clock signal
* _reset_: reset signal (optional) * _reset_: reset signal (optional)
* _reset_active_level_: reset active level (optional, default `True`)
* _size_: memory size in bytes (optional, default 1024) * _size_: memory size in bytes (optional, default 1024)
* _mem_: mmap object to use (optional, overrides _size_) * _mem_: mmap object to use (optional, overrides _size_)
@@ -169,48 +196,55 @@ Multi-port memories can be constructed by passing the `mem` object of the first
#### Methods #### Methods
* `read(address, length)`: read _length_ bytes, starting at _address_ * `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_words(address, count, byteorder='little', ws=2)`: read _count_ _ws_-byte words, starting at _address_
* `read_dwords(address, count, byteorder)`: read _count_ 4-byte dwords, starting at _address_, default _byteorder_ `"little"` * `read_dwords(address, count, byteorder='little')`: read _count_ 4-byte dwords, starting at _address_
* `read_qwords(address, count, byteorder)`: read _count_ 8-byte qwords, starting at _address_, default _byteorder_ `"little"` * `read_qwords(address, count, byteorder='little')`: read _count_ 8-byte qwords, starting at _address_
* `read_byte(address)`: read single byte 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_word(address, byteorder='little', ws=2)`: read single _ws_-byte word at _address_
* `read_dword(address, byteorder)`: read single 4-byte dword at _address_, default _byteorder_ `"little"` * `read_dword(address, byteorder='little')`: read single 4-byte dword at _address_
* `read_qword(address, byteorder)`: read single 8-byte qword at _address_, default _byteorder_ `"little"` * `read_qword(address, byteorder='little')`: read single 8-byte qword at _address_
* `write(address, data)`: write _data_ (bytes), starting 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_words(address, data, byteorder='little', ws=2)`: write _data_ (_ws_-byte words), starting at _address_
* `write_dwords(address, data, byteorder)`: write _data_ (4-byte dwords), starting at _address_, default _byteorder_ `"little"` * `write_dwords(address, data, byteorder='little')`: write _data_ (4-byte dwords), starting at _address_
* `write_qwords(address, data, byteorder)`: write _data_ (8-byte qwords), starting at _address_, default _byteorder_ `"little"` * `write_qwords(address, data, byteorder='little')`: write _data_ (8-byte qwords), starting at _address_
* `write_byte(address, data)`: write single byte 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_word(address, data, byteorder='little', ws=2)`: write single _ws_-byte word at _address_
* `write_dword(address, data, byteorder)`: write single 4-byte dword at _address_, default _byteorder_ `"little"` * `write_dword(address, data, byteorder='little')`: write single 4-byte dword at _address_
* `write_qword(address, data, byteorder)`: write single 8-byte qword at _address_, default _byteorder_ `"little"` * `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(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_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` * `hexdump_str(address, length, prefix='')`: return hex dump (str) of _length_ bytes starting from _address_, prefix lines with optional _prefix_
### AXI stream ### 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: 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_source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "s_axis"), dut.clk, dut.rst)
axis_sink = AxiStreamSink(dut, "m_axis", dut.clk, dut.rst) axis_sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "m_axis"), dut.clk, dut.rst)
axis_monitor = AxiStreamMonitor(dut.inst, "int_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() 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() frame = AxiStreamFrame(b'test data', tx_complete=Event())
data = axis_sink.recv() 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 #### Signals
@@ -225,36 +259,50 @@ To receive data with an `AxiStreamSink` or `AxiStreamMonitor`, call `recv()` or
#### Constructor parameters: #### Constructor parameters:
* _entity_: object that contains the AXI stream interface signals * _bus_: `AxiStreamBus` object containing AXI stream interface signals
* _name_: signal name prefix (e.g. for `m_axis_tdata`, the prefix is `m_axis`)
* _clock_: clock signal * _clock_: clock signal
* _reset_: reset signal (optional) * _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: #### Attributes:
* _pause_: stall the interface (deassert `tready` or `tvalid`) (source/sink only) * _pause_: stall the interface (deassert `tready` or `tvalid`) (source/sink only)
* _queue_occupancy_bytes_: number of bytes in queue (all) * _queue_occupancy_bytes_: number of bytes in queue (all)
* _queue_occupancy_frames_: number of frames 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_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 tready deassert (sink only) * _queue_occupancy_limit_frames_: max number of frames in queue allowed before backpressure is applied (source/sink only)
#### Methods #### Methods
* `send(frame)`: send _frame_ (source) * `send(frame)`: send _frame_ (blocking) (source)
* `write(data)`: send _data_ (alias of send) (source) * `send_nowait(frame)`: send _frame_ (non-blocking) (source)
* `recv(compact)`: receive a frame, optionally compact frame (sink/monitor) * `write(data)`: send _data_ (alias of send) (blocking) (source)
* `read(count)`: read _count_ bytes from buffer (sink/monitor) * `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) * `count()`: returns the number of items in the queue (all)
* `empty()`: returns _True_ if the queue is empty (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) * `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) * `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: Attributes:
@@ -263,12 +311,75 @@ Attributes:
* `tid`: tid field, optional; int or list with one entry per `tdata`, last value used per cycle when sending. * `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. * `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. * `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: Methods:
* `normalize()`: pack `tkeep`, `tid`, `tdest`, and `tuser` to the same length as `tdata`, replicating last element if necessary, initialize `tkeep` to list of `1` and `tid`, `tdest`, and `tuser` to list of `0` if not specified. * `normalize()`: pack `tkeep`, `tid`, `tdest`, and `tuser` to the same length as `tdata`, replicating last element if necessary, initialize `tkeep` to list of `1` and `tid`, `tdest`, and `tuser` to list of `0` if not specified.
* `compact()`: remove `tdata`, `tid`, `tdest`, and `tuser` values based on `tkeep`, remove `tkeep`, compact `tid`, `tdest`, and `tuser` to an int if all values are identical. * `compact()`: remove `tdata`, `tid`, `tdest`, and `tuser` values based on `tkeep`, remove `tkeep`, compact `tid`, `tdest`, and `tuser` to an int if all values are identical.
### Address space abstraction
The address space abstraction provides a framework for cross-connecting multiple memory-mapped interfaces for testing components that interface with complex systems, including components with DMA engines.
`MemoryInterface` is the base class for all components in the address space abstraction. `MemoryInterface` provides the core `read()` and `write()` methods, which implement bounds checking, as well as word-access wrappers. Methods for creating `Window` and `WindowPool` objects are also provided. The function `get_absolute_address()` translates addresses to the system address space. `MemoryInterface` can be extended to implement custom functionality by overriding `_read()` and `_write()`.
`Window` objects represent views onto a parent address space with some length and offset. `read()` and `write()` operations on a `Window` are translated to the equivalent operations on the parent address space. Multiple `Window` instances can overlap and access the same portion of address space.
`WindowPool` provides a method for dynamically allocating windows from a section of address space. It uses a standard memory management algorithm to provide naturally-aligned `Window` objects of the requested size.
`Region` is the base class for all components which implement a portion of address space. `Region` objects can be registered with `AddressSpace` objects to handle `read()` and `write()` operations in a specified region. `Region` can be extended by components that implement a portion of address space.
`MemoryRegion` is an extension of `Region` that uses an `mmap` instance to handle memory operations. `MemoryRegion` also provides hex dump methods as well as indexing and slicing.
`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, MemoryRegion
from cocotbext.axi import AxiBus, AxiLiteMaster, AxiSlave
# system address space
address_space = AddressSpace(2**32)
# RAM
ram = MemoryRegion(2**24)
address_space.register_region(ram, 0x0000_0000)
ram_pool = address_space.create_window_pool(0x0000_0000, 2**20)
# DUT control register interface
axil_master = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "s_axil_ctrl"), dut.clk, dut.rst)
address_space.register_region(axil_master, 0x8000_0000)
ctrl_regs = address_space.create_window(0x8000_0000, axil_master.size)
# DMA from DUT
axi_slave = AxiSlave(AxiBus.from_prefix(dut, "m_axi_dma"), dut.clk, dut.rst, target=address_space)
# exercise DUT DMA functionality
src_block = ram_pool.alloc_window(1024)
dst_block = ram_pool.alloc_window(1024)
test_data = b'test data'
await src_block.write(0, test_data)
await ctrl_regs.write_dword(DMA_SRC_ADDR, src_block.get_absolute_address(0))
await ctrl_regs.write_dword(DMA_DST_ADDR, dst_block.get_absolute_address(0))
await ctrl_regs.write_dword(DMA_LEN, len(test_data))
await ctrl_regs.write_dword(DMA_CONTROL, 1)
while await ctrl_regs.read_dword(DMA_STATUS) == 0:
pass
assert await dst_block.read(0, len(test_data)) == test_data
### AXI signals ### AXI signals
* Write address channel * Write address channel

View File

@@ -26,10 +26,20 @@ from .version import __version__
from .constants import AxiBurstType, AxiBurstSize, AxiLockType, AxiCacheBit, AxiProt, AxiResp 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, 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_master import AxiLiteMasterWrite, AxiLiteMasterRead, AxiLiteMaster
from .axil_slave import AxiLiteSlaveWrite, AxiLiteSlaveRead, AxiLiteSlave
from .axil_ram import AxiLiteRamWrite, AxiLiteRamRead, AxiLiteRam from .axil_ram import AxiLiteRamWrite, AxiLiteRamRead, AxiLiteRam
from .axi_channels import AxiAWBus, AxiWBus, AxiBBus, AxiARBus, AxiRBus
from .axi_channels import AxiWriteBus, AxiReadBus, AxiBus
from .axi_master import AxiMasterWrite, AxiMasterRead, AxiMaster from .axi_master import AxiMasterWrite, AxiMasterRead, AxiMaster
from .axi_slave import AxiSlaveWrite, AxiSlaveRead, AxiSlave
from .axi_ram import AxiRamWrite, AxiRamRead, AxiRam from .axi_ram import AxiRamWrite, AxiRamRead, AxiRam

View File

@@ -0,0 +1,332 @@
"""
Copyright (c) 2021 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 .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 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

View File

@@ -25,38 +25,109 @@ THE SOFTWARE.
from .stream import define_stream from .stream import define_stream
# Write address channel # Write address channel
AxiAWTransaction, AxiAWSource, AxiAWSink, AxiAWMonitor = define_stream("AxiAW", AxiAWBus, AxiAWTransaction, AxiAWSource, AxiAWSink, AxiAWMonitor = define_stream("AxiAW",
signals=["awid", "awaddr", "awlen", "awsize", "awburst", "awprot", "awvalid", "awready"], signals=["awid", "awaddr", "awlen", "awsize", "awburst", "awvalid", "awready"],
optional_signals=["awlock", "awcache", "awqos", "awregion", "awuser"], optional_signals=["awlock", "awcache", "awprot", "awqos", "awregion", "awuser"],
signal_widths={"awlen": 8, "awsize": 3, "awburst": 2, "awlock": 1, signal_widths={"awlen": 8, "awsize": 3, "awburst": 2, "awlock": 1,
"awcache": 4, "awprot": 3, "awqos": 4, "awregion": 4} "awcache": 4, "awprot": 3, "awqos": 4, "awregion": 4}
) )
# Write data channel # Write data channel
AxiWTransaction, AxiWSource, AxiWSink, AxiWMonitor = define_stream("AxiW", AxiWBus, AxiWTransaction, AxiWSource, AxiWSink, AxiWMonitor = define_stream("AxiW",
signals=["wdata", "wstrb", "wlast", "wvalid", "wready"], signals=["wdata", "wlast", "wvalid", "wready"],
optional_signals=["wuser"], optional_signals=["wstrb", "wuser"],
signal_widths={"wlast": 1} signal_widths={"wlast": 1}
) )
# Write response channel # Write response channel
AxiBTransaction, AxiBSource, AxiBSink, AxiBMonitor = define_stream("AxiB", AxiBBus, AxiBTransaction, AxiBSource, AxiBSink, AxiBMonitor = define_stream("AxiB",
signals=["bid", "bresp", "bvalid", "bready"], signals=["bid", "bvalid", "bready"],
optional_signals=["buser"], optional_signals=["bresp", "buser"],
signal_widths={"bresp": 2} signal_widths={"bresp": 2}
) )
# Read address channel # Read address channel
AxiARTransaction, AxiARSource, AxiARSink, AxiARMonitor = define_stream("AxiAR", AxiARBus, AxiARTransaction, AxiARSource, AxiARSink, AxiARMonitor = define_stream("AxiAR",
signals=["arid", "araddr", "arlen", "arsize", "arburst", "arprot", "arvalid", "arready"], signals=["arid", "araddr", "arlen", "arsize", "arburst", "arvalid", "arready"],
optional_signals=["arlock", "arcache", "arqos", "arregion", "aruser"], optional_signals=["arlock", "arcache", "arprot", "arqos", "arregion", "aruser"],
signal_widths={"arlen": 8, "arsize": 3, "arburst": 2, "arlock": 1, signal_widths={"arlen": 8, "arsize": 3, "arburst": 2, "arlock": 1,
"arcache": 4, "arprot": 3, "arqos": 4, "arregion": 4} "arcache": 4, "arprot": 3, "arqos": 4, "arregion": 4}
) )
# Read data channel # Read data channel
AxiRTransaction, AxiRSource, AxiRSink, AxiRMonitor = define_stream("AxiR", AxiRBus, AxiRTransaction, AxiRSource, AxiRSink, AxiRMonitor = define_stream("AxiR",
signals=["rid", "rdata", "rresp", "rlast", "rvalid", "rready"], signals=["rid", "rdata", "rlast", "rvalid", "rready"],
optional_signals=["ruser"], optional_signals=["rresp", "ruser"],
signal_widths={"rresp": 2, "rlast": 1} 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

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2021 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -22,227 +22,32 @@ THE SOFTWARE.
""" """
import cocotb from .axi_slave import AxiSlaveWrite, AxiSlaveRead
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 .memory import Memory from .memory import Memory
class AxiRamWrite(Memory): class AxiRamWrite(AxiSlaveWrite, 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=1024, mem=None, **kwargs):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
self.log.info("AXI RAM model (write)") async def _write(self, address, data):
self.log.info("cocotbext-axi version %s", __version__) self.write(address % self.size, data)
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(size, mem, *args, **kwargs)
self.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)
class AxiRamRead(Memory): class AxiRamRead(AxiSlaveRead, 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=1024, mem=None, **kwargs):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
self.log.info("AXI RAM model (read)") async def _read(self, address, length):
self.log.info("cocotbext-axi version %s", __version__) return self.read(address % self.size, length)
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(size, mem, *args, **kwargs)
self.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
class AxiRam(Memory): 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=1024, mem=None, **kwargs):
self.write_if = None self.write_if = None
self.read_if = None self.read_if = None
super().__init__(size, mem, *args, **kwargs) super().__init__(size, mem, **kwargs)
self.write_if = AxiRamWrite(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(entity, name, clock, reset, mem=self.mem) self.read_if = AxiRamRead(bus.read, clock, reset, reset_active_level, mem=self.mem)

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

@@ -0,0 +1,341 @@
"""
Copyright (c) 2021 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
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
self.log.info("AXI slave model (write)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2021 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(getattr(aw, 'awburst', AxiBurstType.INCR))
prot = AxiProt(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
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
self.log.info("AXI slave model (read)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2021 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(getattr(ar, 'arburst', AxiBurstType.INCR))
prot = AxiProt(getattr(ar, 'arprot', AxiProt.NONSECURE))
self.log.info("Read burst arid: 0x%x araddr: 0x%08x arlen: %d arsize: %d arprot: %s",
arid, addr, length, size, prot)
num_bytes = 2**size
assert 0 < num_bytes <= self.byte_lanes
aligned_addr = (addr // num_bytes) * num_bytes
length += 1
transfer_size = num_bytes*length
if burst == AxiBurstType.WRAP:
lower_wrap_boundary = (addr // transfer_size) * transfer_size
upper_wrap_boundary = lower_wrap_boundary + transfer_size
if burst == AxiBurstType.INCR:
# check 4k boundary crossing
assert 0x1000-(aligned_addr & 0xfff) >= transfer_size
cur_addr = aligned_addr
for n in range(length):
cur_word_addr = (cur_addr // self.byte_lanes) * self.byte_lanes
r = self.r_channel._transaction_obj()
r.rid = arid
r.rlast = n == length-1
r.rresp = AxiResp.OKAY
try:
data = await self._read(cur_word_addr, self.byte_lanes)
except Exception:
self.log.warning("Read operation failed")
data = bytes(self.byte_lanes)
r.rresp = AxiResp.SLVERR
r.rdata = int.from_bytes(data, 'little')
await self.r_channel.send(r)
if self.log.isEnabledFor(logging.DEBUG):
self.log.debug("Read word awid: 0x%x addr: 0x%08x data: %s",
arid, cur_addr, ' '.join((f'{c:02x}' for c in data)))
if burst != AxiBurstType.FIXED:
cur_addr += num_bytes
if burst == AxiBurstType.WRAP:
if cur_addr == upper_wrap_boundary:
cur_addr = lower_wrap_boundary
class AxiSlave:
def __init__(self, bus, clock, reset=None, target=None, reset_active_level=True, **kwargs):
self.write_if = None
self.read_if = None
super().__init__(**kwargs)
self.write_if = AxiSlaveWrite(bus.write, clock, reset, target, reset_active_level)
self.read_if = AxiSlaveRead(bus.read, clock, reset, target, reset_active_level)

View File

@@ -25,30 +25,106 @@ THE SOFTWARE.
from .stream import define_stream from .stream import define_stream
# Write address channel # Write address channel
AxiLiteAWTransaction, AxiLiteAWSource, AxiLiteAWSink, AxiLiteAWMonitor = define_stream("AxiLiteAW", AxiLiteAWBus, AxiLiteAWTransaction, AxiLiteAWSource, AxiLiteAWSink, AxiLiteAWMonitor = define_stream("AxiLiteAW",
signals=["awaddr", "awprot", "awvalid", "awready"], signals=["awaddr", "awvalid", "awready"],
optional_signals=["awprot"],
signal_widths={"awprot": 3} signal_widths={"awprot": 3}
) )
# Write data channel # Write data channel
AxiLiteWTransaction, AxiLiteWSource, AxiLiteWSink, AxiLiteWMonitor = define_stream("AxiLiteW", AxiLiteWBus, AxiLiteWTransaction, AxiLiteWSource, AxiLiteWSink, AxiLiteWMonitor = define_stream("AxiLiteW",
signals=["wdata", "wstrb", "wvalid", "wready"] signals=["wdata", "wvalid", "wready"],
optional_signals=["wstrb"]
) )
# Write response channel # Write response channel
AxiLiteBTransaction, AxiLiteBSource, AxiLiteBSink, AxiLiteBMonitor = define_stream("AxiLiteB", AxiLiteBBus, AxiLiteBTransaction, AxiLiteBSource, AxiLiteBSink, AxiLiteBMonitor = define_stream("AxiLiteB",
signals=["bresp", "bvalid", "bready"], signals=["bvalid", "bready"],
optional_signals=["bresp"],
signal_widths={"bresp": 2} signal_widths={"bresp": 2}
) )
# Read address channel # Read address channel
AxiLiteARTransaction, AxiLiteARSource, AxiLiteARSink, AxiLiteARMonitor = define_stream("AxiLiteAR", AxiLiteARBus, AxiLiteARTransaction, AxiLiteARSource, AxiLiteARSink, AxiLiteARMonitor = define_stream("AxiLiteAR",
signals=["araddr", "arprot", "arvalid", "arready"], signals=["araddr", "arvalid", "arready"],
optional_signals=["arprot"],
signal_widths={"arprot": 3} signal_widths={"arprot": 3}
) )
# Read data channel # Read data channel
AxiLiteRTransaction, AxiLiteRSource, AxiLiteRSink, AxiLiteRMonitor = define_stream("AxiLiteR", AxiLiteRBus, AxiLiteRTransaction, AxiLiteRSource, AxiLiteRSink, AxiLiteRMonitor = define_stream("AxiLiteR",
signals=["rdata", "rresp", "rvalid", "rready"], signals=["rdata", "rvalid", "rready"],
optional_signals=["rresp"],
signal_widths={"rresp": 2} 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)

View File

@@ -22,155 +22,255 @@ THE SOFTWARE.
""" """
from collections import deque, namedtuple import logging
from typing import NamedTuple
import cocotb import cocotb
from cocotb.queue import Queue
from cocotb.triggers import Event from cocotb.triggers import Event
from cocotb.log import SimLog
from .version import __version__ from .version import __version__
from .constants import AxiProt, AxiResp from .constants import AxiProt, AxiResp
from .axil_channels import AxiLiteAWSource, AxiLiteWSource, AxiLiteBSink, AxiLiteARSource, AxiLiteRSink from .axil_channels import AxiLiteAWSource, AxiLiteWSource, AxiLiteBSink, AxiLiteARSource, AxiLiteRSink
from .address_space import Region
# AXI lite master write from .reset import Reset
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"])
class AxiLiteMasterWrite(object): # AXI lite master write helper objects
def __init__(self, entity, name, clock, reset=None): class AxiLiteWriteCmd(NamedTuple):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) 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
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
self.log.info("AXI lite master (write)") self.log.info("AXI lite master (write)")
self.log.info("cocotbext-axi version %s", __version__) self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich") self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi") 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.write_command_queue = Queue()
self.w_channel = AxiLiteWSource(entity, name, clock, reset) self.write_command_queue.queue_occupancy_limit = 2
self.b_channel = AxiLiteBSink(entity, name, clock, reset) self.current_write_command = None
self.write_command_queue = deque() self.int_write_resp_command_queue = Queue()
self.write_command_sync = Event() self.current_write_resp_command = None
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.in_flight_operations = 0 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.width = len(self.w_channel.bus.wdata)
self.byte_size = 8 self.byte_size = 8
self.byte_width = self.width // self.byte_size self.byte_lanes = self.width // self.byte_size
self.strb_mask = 2**self.byte_width-1 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("AXI lite master configuration:")
self.log.info(" Address width: %d bits", len(self.aw_channel.bus.awaddr)) self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" Byte size: %d bits", self.byte_size) self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_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) self.log.info("AXI lite master signals:")
assert self.byte_width * self.byte_size == self.width 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()) if self.wstrb_present:
cocotb.fork(self._process_write_resp()) 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): 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") 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)) if isinstance(data, int):
self.write_command_sync.set() raise ValueError("Expected bytes or bytearray for data")
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): def idle(self):
return not self.in_flight_operations return not self.in_flight_operations
async def wait(self): async def wait(self):
while not self.idle(): while not self.idle():
self.write_resp_sync.clear() await self._idle.wait()
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
async def write(self, address, data, prot=AxiProt.NONSECURE): 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 not self.awprot_present and prot != AxiProt.NONSECURE:
raise ValueError("awprot sideband signal value specified, but signal is not connected")
event = Event() 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() await event.wait()
return event.data return event.data
async def write_words(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE): async def _write_wrapper(self, address, data, prot, event):
words = data event.set(await self.write(address, data, prot))
data = bytearray()
for w in words:
data.extend(w.to_bytes(ws, byteorder))
await self.write(address, data, prot)
async def write_dwords(self, address, data, byteorder='little', prot=AxiProt.NONSECURE): def _handle_reset(self, state):
await self.write_words(address, data, byteorder, 4, prot) 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): self.aw_channel.clear()
await self.write_words(address, data, byteorder, 8, prot) self.w_channel.clear()
self.b_channel.clear()
async def write_byte(self, address, data, prot=AxiProt.NONSECURE): def flush_cmd(cmd):
await self.write(address, [data], prot) 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): while not self.write_command_queue.empty():
await self.write_words(address, [data], byteorder, ws, prot) cmd = self.write_command_queue.get_nowait()
flush_cmd(cmd)
async def write_dword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE): if self.current_write_command:
await self.write_dwords(address, [data], byteorder, prot) 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): while not self.int_write_resp_command_queue.empty():
await self.write_qwords(address, [data], byteorder, prot) 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): async def _process_write(self):
while True: while True:
if not self.write_command_queue: cmd = await self.write_command_queue.get()
self.write_command_sync.clear() self.current_write_command = cmd
await self.write_command_sync.wait()
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_lanes
end_offset = ((cmd.address + len(cmd.data) - 1) % self.byte_lanes) + 1
start_offset = cmd.address % self.byte_width
end_offset = ((cmd.address + len(cmd.data) - 1) % self.byte_width) + 1
strb_start = (self.strb_mask << start_offset) & self.strb_mask 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) resp_cmd = AxiLiteWriteRespCmd(cmd.address, len(cmd.data), cycles, cmd.prot, cmd.event)
self.int_write_resp_command_queue.append(resp_cmd) await self.int_write_resp_command_queue.put(resp_cmd)
self.int_write_resp_command_sync.set()
offset = 0 offset = 0
if self.log.isEnabledFor(logging.INFO):
self.log.info("Write start addr: 0x%08x prot: %s data: %s", self.log.info("Write start addr: 0x%08x prot: %s data: %s",
cmd.address, cmd.prot, ' '.join((f'{c:02x}' for c in cmd.data))) cmd.address, cmd.prot, ' '.join((f'{c:02x}' for c in cmd.data)))
for k in range(cycles): for k in range(cycles):
start = 0 start = 0
stop = self.byte_width stop = self.byte_lanes
strb = self.strb_mask strb = self.strb_mask
if k == 0: if k == 0:
@@ -186,31 +286,32 @@ class AxiLiteMasterWrite(object):
offset += 1 offset += 1
aw = self.aw_channel._transaction_obj() aw = self.aw_channel._transaction_obj()
aw.awaddr = word_addr + k*self.byte_width aw.awaddr = word_addr + k*self.byte_lanes
aw.awprot = cmd.prot aw.awprot = cmd.prot
if not self.wstrb_present and strb != self.strb_mask:
self.log.warning("Partial operation requested with wstrb not connected, write will be zero-padded (0x%x != 0x%x)", strb, self.strb_mask)
w = self.w_channel._transaction_obj() w = self.w_channel._transaction_obj()
w.wdata = val w.wdata = val
w.wstrb = strb w.wstrb = strb
await self.aw_channel.drive(aw) await self.aw_channel.send(aw)
self.w_channel.send(w) await self.w_channel.send(w)
self.current_write_command = None
async def _process_write_resp(self): async def _process_write_resp(self):
while True: while True:
if not self.int_write_resp_command_queue: cmd = await self.int_write_resp_command_queue.get()
self.int_write_resp_command_sync.clear() self.current_write_resp_command = cmd
await self.int_write_resp_command_sync.wait()
cmd = self.int_write_resp_command_queue.popleft()
resp = AxiResp.OKAY resp = AxiResp.OKAY
for k in range(cmd.cycles): for k in range(cmd.cycles):
await self.b_channel.wait() b = await self.b_channel.recv()
b = self.b_channel.recv()
cycle_resp = AxiResp(b.bresp) cycle_resp = AxiResp(getattr(b, 'bresp', AxiResp.OKAY))
if cycle_resp != AxiResp.OKAY: if cycle_resp != AxiResp.OKAY:
resp = cycle_resp resp = cycle_resp
@@ -220,163 +321,209 @@ class AxiLiteMasterWrite(object):
write_resp = AxiLiteWriteResp(cmd.address, cmd.length, resp) write_resp = AxiLiteWriteResp(cmd.address, cmd.length, resp)
if cmd.event is not None:
cmd.event.set(write_resp) cmd.event.set(write_resp)
else:
self.write_resp_queue.append(write_resp) self.current_write_resp_command = None
self.write_resp_sync.set()
self.in_flight_operations -= 1 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): class AxiLiteMasterRead(Region, Reset):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) def __init__(self, bus, clock, reset=None, reset_active_level=True, **kwargs):
self.bus = bus
self.clock = clock
self.reset = reset
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
self.log.info("AXI lite master (read)") self.log.info("AXI lite master (read)")
self.log.info("cocotbext-axi version %s", __version__) self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich") self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi") 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.read_command_queue = Queue()
self.r_channel = AxiLiteRSink(entity, name, clock, reset) self.read_command_queue.queue_occupancy_limit = 2
self.current_read_command = None
self.read_command_queue = deque() self.int_read_resp_command_queue = Queue()
self.read_command_sync = Event() self.current_read_resp_command = None
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.in_flight_operations = 0 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.width = len(self.r_channel.bus.rdata)
self.byte_size = 8 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("AXI lite master configuration:")
self.log.info(" Address width: %d bits", len(self.ar_channel.bus.araddr)) self.log.info(" Address width: %d bits", self.address_width)
self.log.info(" Byte size: %d bits", self.byte_size) self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_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()) assert self.byte_lanes * self.byte_size == self.width
cocotb.fork(self._process_read_resp())
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): 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") 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)) if not self.arprot_present and prot != AxiProt.NONSECURE:
self.read_command_sync.set() 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): def idle(self):
return not self.in_flight_operations return not self.in_flight_operations
async def wait(self): async def wait(self):
while not self.idle(): while not self.idle():
self.read_data_sync.clear() await self._idle.wait()
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
async def read(self, address, length, prot=AxiProt.NONSECURE): 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 not self.arprot_present and prot != AxiProt.NONSECURE:
raise ValueError("arprot sideband signal value specified, but signal is not connected")
event = Event() 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() await event.wait()
return event.data return event.data
async def read_words(self, address, count, byteorder='little', ws=2, prot=AxiProt.NONSECURE): async def _read_wrapper(self, address, length, prot, event):
data = await self.read(address, count*ws, prot) event.set(await self.read(address, length, prot))
words = []
for k in range(count):
words.append(int.from_bytes(data.data[ws*k:ws*(k+1)], byteorder))
return words
async def read_dwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE): def _handle_reset(self, state):
return await self.read_words(address, count, byteorder, 4, prot) 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): self.ar_channel.clear()
return await self.read_words(address, count, byteorder, 8, prot) self.r_channel.clear()
async def read_byte(self, address, prot=AxiProt.NONSECURE): def flush_cmd(cmd):
return (await self.read(address, 1, prot)).data[0] 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): while not self.read_command_queue.empty():
return (await self.read_words(address, 1, byteorder, ws, prot))[0] cmd = self.read_command_queue.get_nowait()
flush_cmd(cmd)
async def read_dword(self, address, byteorder='little', prot=AxiProt.NONSECURE): if self.current_read_command:
return (await self.read_dwords(address, 1, byteorder, prot))[0] cmd = self.current_read_command
self.current_read_command = None
flush_cmd(cmd)
async def read_qword(self, address, byteorder='little', prot=AxiProt.NONSECURE): while not self.int_read_resp_command_queue.empty():
return (await self.read_qwords(address, 1, byteorder, prot))[0] 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): async def _process_read(self):
while True: while True:
if not self.read_command_queue: cmd = await self.read_command_queue.get()
self.read_command_sync.clear() self.current_read_command = cmd
await self.read_command_sync.wait()
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_lanes-1 + (cmd.address % self.byte_lanes)) // self.byte_lanes
cycles = (cmd.length + self.byte_width-1 + (cmd.address % self.byte_width)) // self.byte_width
resp_cmd = AxiLiteReadRespCmd(cmd.address, cmd.length, cycles, cmd.prot, cmd.event) resp_cmd = AxiLiteReadRespCmd(cmd.address, cmd.length, cycles, cmd.prot, cmd.event)
self.int_read_resp_command_queue.append(resp_cmd) await self.int_read_resp_command_queue.put(resp_cmd)
self.int_read_resp_command_sync.set()
self.log.info("Read start addr: 0x%08x prot: %s length: %d", self.log.info("Read start addr: 0x%08x prot: %s length: %d",
cmd.address, cmd.prot, cmd.length) cmd.address, cmd.prot, cmd.length)
for k in range(cycles): for k in range(cycles):
ar = self.ar_channel._transaction_obj() ar = self.ar_channel._transaction_obj()
ar.araddr = word_addr + k*self.byte_width ar.araddr = word_addr + k*self.byte_lanes
ar.arprot = cmd.prot 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): async def _process_read_resp(self):
while True: while True:
if not self.int_read_resp_command_queue: cmd = await self.int_read_resp_command_queue.get()
self.int_read_resp_command_sync.clear() self.current_read_resp_command = cmd
await self.int_read_resp_command_sync.wait()
cmd = self.int_read_resp_command_queue.popleft() start_offset = cmd.address % self.byte_lanes
end_offset = ((cmd.address + cmd.length - 1) % self.byte_lanes) + 1
start_offset = cmd.address % self.byte_width
end_offset = ((cmd.address + cmd.length - 1) % self.byte_width) + 1
data = bytearray() data = bytearray()
resp = AxiResp.OKAY resp = AxiResp.OKAY
for k in range(cmd.cycles): for k in range(cmd.cycles):
await self.r_channel.wait() r = await self.r_channel.recv()
r = self.r_channel.recv()
cycle_data = int(r.rdata) cycle_data = int(r.rdata)
cycle_resp = AxiResp(r.rresp) cycle_resp = AxiResp(getattr(r, 'rresp', AxiResp.OKAY))
if cycle_resp != AxiResp.OKAY: if cycle_resp != AxiResp.OKAY:
resp = cycle_resp resp = cycle_resp
start = 0 start = 0
stop = self.byte_width stop = self.byte_lanes
if k == 0: if k == 0:
start = start_offset start = start_offset
@@ -386,33 +533,37 @@ class AxiLiteMasterRead(object):
for j in range(start, stop): for j in range(start, stop):
data.extend(bytearray([(cycle_data >> j*8) & 0xff])) data.extend(bytearray([(cycle_data >> j*8) & 0xff]))
if self.log.isEnabledFor(logging.INFO):
self.log.info("Read complete addr: 0x%08x prot: %s resp: %s data: %s", self.log.info("Read complete addr: 0x%08x prot: %s resp: %s data: %s",
cmd.address, cmd.prot, resp, ' '.join((f'{c:02x}' for c in data))) cmd.address, cmd.prot, resp, ' '.join((f'{c:02x}' for c in data)))
read_resp = AxiLiteReadResp(cmd.address, data, resp) read_resp = AxiLiteReadResp(cmd.address, bytes(data), resp)
if cmd.event is not None:
cmd.event.set(read_resp) cmd.event.set(read_resp)
else:
self.read_data_queue.append(read_resp) self.current_read_resp_command = None
self.read_data_sync.set()
self.in_flight_operations -= 1 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.write_if = None
self.read_if = None self.read_if = None
self.write_if = AxiLiteMasterWrite(entity, name, clock, reset) self.write_if = AxiLiteMasterWrite(bus.write, clock, reset, reset_active_level, **kwargs)
self.read_if = AxiLiteMasterRead(entity, name, clock, reset) self.read_if = AxiLiteMasterRead(bus.read, clock, reset, reset_active_level, **kwargs)
super().__init__(max(self.write_if.size, self.read_if.size), **kwargs)
def init_read(self, address, length, prot=AxiProt.NONSECURE, event=None): def init_read(self, address, length, prot=AxiProt.NONSECURE, event=None):
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): 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): def idle(self):
return (not self.read_if or self.read_if.idle()) and (not self.write_if or self.write_if.idle()) return (not self.read_if or self.read_if.idle()) and (not self.write_if or self.write_if.idle())
@@ -428,62 +579,8 @@ class AxiLiteMaster(object):
async def wait_write(self): async def wait_write(self):
await self.write_if.wait() 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): async def read(self, address, length, prot=AxiProt.NONSECURE):
return await self.read_if.read(address, length, prot) return await self.read_if.read(address, length, prot)
async def read_words(self, address, count, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return await self.read_if.read_words(address, count, byteorder, ws, prot)
async def read_dwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_if.read_dwords(address, count, byteorder, prot)
async def read_qwords(self, address, count, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_if.read_qwords(address, count, byteorder, prot)
async def read_byte(self, address, prot=AxiProt.NONSECURE):
return await self.read_if.read_byte(address, prot)
async def read_word(self, address, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return await self.read_if.read_word(address, byteorder, ws, prot)
async def read_dword(self, address, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_if.read_dword(address, byteorder, prot)
async def read_qword(self, address, byteorder='little', prot=AxiProt.NONSECURE):
return await self.read_if.read_qword(address, byteorder, prot)
async def write(self, address, data, prot=AxiProt.NONSECURE): async def write(self, address, data, prot=AxiProt.NONSECURE):
return await self.write_if.write(address, data, prot) return await self.write_if.write(address, data, prot)
async def write_words(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return await self.write_if.write_words(address, data, byteorder, ws, prot)
async def write_dwords(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
return await self.write_if.write_dwords(address, data, byteorder, prot)
async def write_qwords(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
return await self.write_if.write_qwords(address, data, byteorder, prot)
async def write_byte(self, address, data, prot=AxiProt.NONSECURE):
return await self.write_if.write_byte(address, data, prot)
async def write_word(self, address, data, byteorder='little', ws=2, prot=AxiProt.NONSECURE):
return await self.write_if.write_word(address, data, byteorder, ws, prot)
async def write_dword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
return await self.write_if.write_dword(address, data, byteorder, prot)
async def write_qword(self, address, data, byteorder='little', prot=AxiProt.NONSECURE):
return await self.write_if.write_qword(address, data, byteorder, prot)

View File

@@ -1,6 +1,6 @@
""" """
Copyright (c) 2020 Alex Forencich Copyright (c) 2021 Alex Forencich
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -22,147 +22,32 @@ THE SOFTWARE.
""" """
import cocotb from .axil_slave import AxiLiteSlaveWrite, AxiLiteSlaveRead
from cocotb.log import SimLog
from .version import __version__
from .constants import AxiProt, AxiResp
from .axil_channels import AxiLiteAWSink, AxiLiteWSink, AxiLiteBSource, AxiLiteARSink, AxiLiteRSource
from .memory import Memory from .memory import Memory
class AxiLiteRamWrite(Memory): class AxiLiteRamWrite(AxiLiteSlaveWrite, 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=1024, mem=None, **kwargs):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
self.log.info("AXI lite RAM model (write)") async def _write(self, address, data):
self.log.info("cocotbext-axi version %s", __version__) self.write(address % self.size, data)
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(size, mem, *args, **kwargs)
self.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)
class AxiLiteRamRead(Memory): class AxiLiteRamRead(AxiLiteSlaveRead, 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=1024, mem=None, **kwargs):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) super().__init__(bus, clock, reset, reset_active_level=reset_active_level, size=size, mem=mem, **kwargs)
self.log.info("AXI lite RAM model (read)") async def _read(self, address, length):
self.log.info("cocotbext-axi version %s", __version__) return self.read(address % self.size, length)
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(size, mem, *args, **kwargs)
self.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)))
class AxiLiteRam(Memory): 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=1024, mem=None, **kwargs):
self.write_if = None self.write_if = None
self.read_if = None self.read_if = None
super().__init__(size, mem, *args, **kwargs) super().__init__(size, mem, **kwargs)
self.write_if = AxiLiteRamWrite(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(entity, name, clock, reset, mem=self.mem) self.read_if = AxiLiteRamRead(bus.read, clock, reset, reset_active_level, mem=self.mem)

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

@@ -0,0 +1,255 @@
"""
Copyright (c) 2021 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
self.log = logging.getLogger(f"cocotb.{bus.aw._entity._name}.{bus.aw._name}")
self.log.info("AXI lite slave model (write)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2021 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(" Memory size: %d bytes", len(self.mem))
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(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
self.log = logging.getLogger(f"cocotb.{bus.ar._entity._name}.{bus.ar._name}")
self.log.info("AXI lite slave model (read)")
self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2021 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(" Memory size: %d bytes", len(self.mem))
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(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(target, bus.write, clock, reset, reset_active_level)
self.read_if = AxiLiteSlaveRead(target, bus.read, clock, reset, reset_active_level)

View File

@@ -22,23 +22,28 @@ THE SOFTWARE.
""" """
from collections import deque import logging
import cocotb import cocotb
from cocotb.triggers import RisingEdge, ReadOnly, Timer, First, Event from cocotb.queue import Queue, QueueFull
from cocotb.bus import Bus from cocotb.triggers import RisingEdge, Timer, First, Event
from cocotb.log import SimLog from cocotb.utils import get_sim_time
from cocotb_bus.bus import Bus
from .version import __version__ from .version import __version__
from .reset import Reset
class AxiStreamFrame(object): class AxiStreamFrame:
def __init__(self, tdata=b'', tkeep=None, tid=None, tdest=None, tuser=None): def __init__(self, tdata=b'', tkeep=None, tid=None, tdest=None, tuser=None, tx_complete=None):
self.tdata = bytearray() self.tdata = bytearray()
self.tkeep = None self.tkeep = None
self.tid = None self.tid = None
self.tdest = None self.tdest = None
self.tuser = None self.tuser = None
self.sim_time_start = None
self.sim_time_end = None
self.tx_complete = None
if type(tdata) is AxiStreamFrame: if type(tdata) is AxiStreamFrame:
if type(tdata.tdata) is bytearray: if type(tdata.tdata) is bytearray:
@@ -62,6 +67,9 @@ class AxiStreamFrame(object):
self.tuser = tdata.tuser self.tuser = tdata.tuser
else: else:
self.tuser = list(tdata.tuser) self.tuser = list(tdata.tuser)
self.sim_time_start = tdata.sim_time_start
self.sim_time_end = tdata.sim_time_end
self.tx_complete = tdata.tx_complete
elif type(tdata) in (bytes, bytearray): elif type(tdata) in (bytes, bytearray):
self.tdata = bytearray(tdata) self.tdata = bytearray(tdata)
self.tkeep = tkeep self.tkeep = tkeep
@@ -75,6 +83,9 @@ class AxiStreamFrame(object):
self.tdest = tdest self.tdest = tdest
self.tuser = tuser self.tuser = tuser
if tx_complete is not None:
self.tx_complete = tx_complete
def normalize(self): def normalize(self):
# normalize all sideband signals to the same size as tdata # normalize all sideband signals to the same size as tdata
n = len(self.tdata) n = len(self.tdata)
@@ -144,6 +155,12 @@ class AxiStreamFrame(object):
elif all(self.tuser[0] == i for i in self.tuser): elif all(self.tuser[0] == i for i in self.tuser):
self.tuser = self.tuser[0] self.tuser = self.tuser[0]
def handle_tx_complete(self):
if isinstance(self.tx_complete, Event):
self.tx_complete.set(self)
elif callable(self.tx_complete):
self.tx_complete(self)
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, AxiStreamFrame): if not isinstance(other, AxiStreamFrame):
return False return False
@@ -195,11 +212,13 @@ class AxiStreamFrame(object):
def __repr__(self): def __repr__(self):
return ( return (
f"{type(self).__name__}(tdata={repr(self.tdata)}, " f"{type(self).__name__}(tdata={self.tdata!r}, "
f"tkeep={repr(self.tkeep)}, " f"tkeep={self.tkeep!r}, "
f"tid={repr(self.tid)}, " f"tid={self.tid!r}, "
f"tdest={repr(self.tdest)}, " f"tdest={self.tdest!r}, "
f"tuser={repr(self.tuser)})" f"tuser={self.tuser!r}, "
f"sim_time_start={self.sim_time_start!r}, "
f"sim_time_end={self.sim_time_end!r})"
) )
def __len__(self): def __len__(self):
@@ -208,20 +227,48 @@ class AxiStreamFrame(object):
def __iter__(self): def __iter__(self):
return self.tdata.__iter__() return self.tdata.__iter__()
def __bytes__(self):
return bytes(self.tdata)
class AxiStreamSource(object):
class AxiStreamBus(Bus):
_signals = ["tdata"] _signals = ["tdata"]
_optional_signals = ["tvalid", "tready", "tlast", "tkeep", "tid", "tdest", "tuser"] _optional_signals = ["tvalid", "tready", "tlast", "tkeep", "tid", "tdest", "tuser"]
def __init__(self, entity, name, clock, reset=None, *args, **kwargs): def __init__(self, entity=None, prefix=None, **kwargs):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) super().__init__(entity, prefix, self._signals, optional_signals=self._optional_signals, **kwargs)
self.entity = entity
@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 AxiStreamBase(Reset):
_signals = ["tdata"]
_optional_signals = ["tvalid", "tready", "tlast", "tkeep", "tid", "tdest", "tuser"]
_type = "base"
_init_x = False
_valid_init = None
_ready_init = None
def __init__(self, bus, clock, reset=None, reset_active_level=True,
byte_size=None, byte_lanes=None, *args, **kwargs):
self.bus = bus
self.clock = clock self.clock = clock
self.reset = reset 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}")
self.log.info("AXI stream source") self.log.info("AXI stream %s", self._type)
self.log.info("cocotbext-axi version %s", __version__) self.log.info("cocotbext-axi version %s", __version__)
self.log.info("Copyright (c) 2020 Alex Forencich") self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi") self.log.info("https://github.com/alexforencich/cocotbext-axi")
@@ -229,84 +276,109 @@ class AxiStreamSource(object):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.active = False self.active = False
self.queue = deque() self.queue = Queue()
self.dequeue_event = Event()
self.pause = False self.current_frame = None
self._pause_generator = None self.idle_event = Event()
self._pause_cr = None self.idle_event.set()
self.active_event = Event()
self.queue_occupancy_bytes = 0 self.queue_occupancy_bytes = 0
self.queue_occupancy_frames = 0 self.queue_occupancy_frames = 0
self.width = len(self.bus.tdata) self.width = len(self.bus.tdata)
self.byte_width = 1 self.byte_lanes = 1
self.reset = reset if self._valid_init is not None and hasattr(self.bus, "tvalid"):
self.bus.tvalid.setimmediatevalue(self._valid_init)
if self._ready_init is not None and hasattr(self.bus, "tready"):
self.bus.tready.setimmediatevalue(self._ready_init)
for sig in self._signals+self._optional_signals:
if hasattr(self.bus, sig):
if self._init_x and sig not in ("tvalid", "tready"):
v = getattr(self.bus, sig).value
v.binstr = 'x'*len(v)
getattr(self.bus, sig).setimmediatevalue(v)
self.bus.tdata.setimmediatevalue(0)
if hasattr(self.bus, "tvalid"):
self.bus.tvalid.setimmediatevalue(0)
if hasattr(self.bus, "tlast"):
self.bus.tlast.setimmediatevalue(0)
if hasattr(self.bus, "tkeep"): if hasattr(self.bus, "tkeep"):
self.byte_width = len(self.bus.tkeep) self.byte_lanes = len(self.bus.tkeep)
self.bus.tkeep.setimmediatevalue(0) if byte_size is not None or byte_lanes is not None:
if hasattr(self.bus, "tid"): raise ValueError("Cannot specify byte_size or byte_lanes if tkeep is connected")
self.bus.tid.setimmediatevalue(0) else:
if hasattr(self.bus, "tdest"): if byte_lanes is not None:
self.bus.tdest.setimmediatevalue(0) self.byte_lanes = byte_lanes
if hasattr(self.bus, "tuser"): if byte_size is not None:
self.bus.tuser.setimmediatevalue(0) raise ValueError("Cannot specify both byte_size and byte_lanes")
elif byte_size is not None:
self.byte_lanes = self.width // byte_size
self.byte_size = self.width // self.byte_width self.byte_size = self.width // self.byte_lanes
self.byte_mask = 2**self.byte_size-1 self.byte_mask = 2**self.byte_size-1
self.log.info("AXI stream source configuration:") self.log.info("AXI stream %s configuration:", self._type)
self.log.info(" Byte size: %d bits", self.byte_size) self.log.info(" Byte size: %d bits", self.byte_size)
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_width) self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
self.log.info(" tvalid: %s", "present" if hasattr(self.bus, "tvalid") else "not present")
self.log.info(" tready: %s", "present" if hasattr(self.bus, "tready") else "not present")
self.log.info(" tlast: %s", "present" if hasattr(self.bus, "tlast") else "not present")
if hasattr(self.bus, "tkeep"):
self.log.info(" tkeep width: %d bits", len(self.bus.tkeep))
else:
self.log.info(" tkeep: not present")
if hasattr(self.bus, "tid"):
self.log.info(" tid width: %d bits", len(self.bus.tid))
else:
self.log.info(" tid: not present")
if hasattr(self.bus, "tdest"):
self.log.info(" tdest width: %d bits", len(self.bus.tdest))
else:
self.log.info(" tdest: not present")
if hasattr(self.bus, "tuser"):
self.log.info(" tuser width: %d bits", len(self.bus.tuser))
else:
self.log.info(" tuser: not present")
cocotb.fork(self._run()) self.log.info("AXI stream %s signals:", self._type)
for sig in sorted(list(set().union(self.bus._signals, self.bus._optional_signals))):
if hasattr(self.bus, sig):
self.log.info(" %s width: %d bits", sig, len(getattr(self.bus, sig)))
else:
self.log.info(" %s: not present", sig)
def send(self, frame): if self.byte_lanes * self.byte_size != self.width:
frame = AxiStreamFrame(frame) raise ValueError(f"Bus does not evenly divide into byte lanes "
self.queue_occupancy_bytes += len(frame) f"({self.byte_lanes} * {self.byte_size} != {self.width})")
self.queue_occupancy_frames += 1
self.queue.append(frame)
def write(self, data): self._run_cr = None
self.send(data)
self._init_reset(reset, reset_active_level)
def count(self): def count(self):
return len(self.queue) return self.queue.qsize()
def empty(self): def empty(self):
return not self.queue return self.queue.empty()
def idle(self): def clear(self):
return self.empty() and not self.active while not self.queue.empty():
frame = self.queue.get_nowait()
frame.sim_time_end = None
frame.handle_tx_complete()
self.dequeue_event.set()
self.idle_event.set()
self.active_event.clear()
self.queue_occupancy_bytes = 0
self.queue_occupancy_frames = 0
async def wait(self): def _handle_reset(self, state):
while not self.idle(): if state:
await RisingEdge(self.clock) 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 AxiStreamPause:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pause = False
self._pause_generator = None
self._pause_cr = None
def set_pause_generator(self, generator=None): def set_pause_generator(self, generator=None):
if self._pause_cr is not None: if self._pause_cr is not None:
@@ -316,51 +388,125 @@ class AxiStreamSource(object):
self._pause_generator = generator self._pause_generator = generator
if self._pause_generator is not None: if self._pause_generator is not None:
self._pause_cr = cocotb.fork(self._run_pause()) self._pause_cr = cocotb.start_soon(self._run_pause())
def clear_pause_generator(self): def clear_pause_generator(self):
self.set_pause_generator(None) self.set_pause_generator(None)
async def _run_pause(self):
clock_edge_event = RisingEdge(self.clock)
for val in self._pause_generator:
self.pause = val
await clock_edge_event
class AxiStreamSource(AxiStreamBase, AxiStreamPause):
_type = "source"
_init_x = True
_valid_init = 0
_ready_init = None
def __init__(self, bus, clock, reset=None, reset_active_level=True,
byte_size=None, byte_lanes=None, *args, **kwargs):
super().__init__(bus, clock, reset, reset_active_level, byte_size, byte_lanes, *args, **kwargs)
self.queue_occupancy_limit_bytes = -1
self.queue_occupancy_limit_frames = -1
async def send(self, frame):
while self.full():
self.dequeue_event.clear()
await self.dequeue_event.wait()
frame = AxiStreamFrame(frame)
await self.queue.put(frame)
self.idle_event.clear()
self.queue_occupancy_bytes += len(frame)
self.queue_occupancy_frames += 1
def send_nowait(self, frame):
if self.full():
raise QueueFull()
frame = AxiStreamFrame(frame)
self.queue.put_nowait(frame)
self.idle_event.clear()
self.queue_occupancy_bytes += len(frame)
self.queue_occupancy_frames += 1
async def write(self, data):
await self.send(data)
def write_nowait(self, data):
self.send_nowait(data)
def full(self):
if self.queue_occupancy_limit_bytes > 0 and self.queue_occupancy_bytes > self.queue_occupancy_limit_bytes:
return True
elif self.queue_occupancy_limit_frames > 0 and self.queue_occupancy_frames > self.queue_occupancy_limit_frames:
return True
else:
return False
def idle(self):
return self.empty() and not self.active
async def wait(self):
await self.idle_event.wait()
def _handle_reset(self, state):
super()._handle_reset(state)
if state:
self.bus.tdata.value = 0
if hasattr(self.bus, "tvalid"):
self.bus.tvalid.value = 0
if hasattr(self.bus, "tlast"):
self.bus.tlast.value = 0
if hasattr(self.bus, "tkeep"):
self.bus.tkeep.value = 0
if hasattr(self.bus, "tid"):
self.bus.tid.value = 0
if hasattr(self.bus, "tdest"):
self.bus.tdest.value = 0
if hasattr(self.bus, "tuser"):
self.bus.tuser.value = 0
if self.current_frame:
self.log.warning("Flushed transmit frame during reset: %s", self.current_frame)
self.current_frame.handle_tx_complete()
self.current_frame = None
async def _run(self): async def _run(self):
frame = None frame = None
frame_offset = 0
self.active = False self.active = False
clock_edge_event = RisingEdge(self.clock)
while True: while True:
await ReadOnly() await clock_edge_event
# read handshake signals # read handshake signals
tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value
tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value
if self.reset is not None and self.reset.value:
await RisingEdge(self.clock)
frame = None
self.active = False
self.bus.tdata <= 0
if hasattr(self.bus, "tvalid"):
self.bus.tvalid <= 0
if hasattr(self.bus, "tlast"):
self.bus.tlast <= 0
if hasattr(self.bus, "tkeep"):
self.bus.tkeep <= 0
if hasattr(self.bus, "tid"):
self.bus.tid <= 0
if hasattr(self.bus, "tdest"):
self.bus.tdest <= 0
if hasattr(self.bus, "tuser"):
self.bus.tuser <= 0
continue
await RisingEdge(self.clock)
if (tready_sample and tvalid_sample) or not tvalid_sample: if (tready_sample and tvalid_sample) or not tvalid_sample:
if frame is None and self.queue: if frame is None and not self.queue.empty():
frame = self.queue.popleft() frame = self.queue.get_nowait()
self.dequeue_event.set()
self.queue_occupancy_bytes -= len(frame) self.queue_occupancy_bytes -= len(frame)
self.queue_occupancy_frames -= 1 self.queue_occupancy_frames -= 1
self.current_frame = frame
frame.sim_time_start = get_sim_time()
frame.sim_time_end = None
self.log.info("TX frame: %s", frame) self.log.info("TX frame: %s", frame)
frame.normalize() frame.normalize()
self.active = True self.active = True
frame_offset = 0
if frame and not self.pause: if frame and not self.pause:
tdata_val = 0 tdata_val = 0
@@ -370,130 +516,87 @@ class AxiStreamSource(object):
tdest_val = 0 tdest_val = 0
tuser_val = 0 tuser_val = 0
for offset in range(self.byte_width): for offset in range(self.byte_lanes):
tdata_val |= (frame.tdata.pop(0) & self.byte_mask) << (offset * self.byte_size) tdata_val |= (frame.tdata[frame_offset] & self.byte_mask) << (offset * self.byte_size)
tkeep_val |= (frame.tkeep.pop(0) & 1) << offset tkeep_val |= (frame.tkeep[frame_offset] & 1) << offset
tid_val = frame.tid.pop(0) tid_val = frame.tid[frame_offset]
tdest_val = frame.tdest.pop(0) tdest_val = frame.tdest[frame_offset]
tuser_val = frame.tuser.pop(0) tuser_val = frame.tuser[frame_offset]
frame_offset += 1
if len(frame.tdata) == 0: if frame_offset >= len(frame.tdata):
tlast_val = 1 tlast_val = 1
frame.sim_time_end = get_sim_time()
frame.handle_tx_complete()
frame = None frame = None
self.current_frame = None
break break
self.bus.tdata <= tdata_val self.bus.tdata.value = tdata_val
if hasattr(self.bus, "tvalid"): if hasattr(self.bus, "tvalid"):
self.bus.tvalid <= 1 self.bus.tvalid.value = 1
if hasattr(self.bus, "tlast"): if hasattr(self.bus, "tlast"):
self.bus.tlast <= tlast_val self.bus.tlast.value = tlast_val
if hasattr(self.bus, "tkeep"): if hasattr(self.bus, "tkeep"):
self.bus.tkeep <= tkeep_val self.bus.tkeep.value = tkeep_val
if hasattr(self.bus, "tid"): if hasattr(self.bus, "tid"):
self.bus.tid <= tid_val self.bus.tid.value = tid_val
if hasattr(self.bus, "tdest"): if hasattr(self.bus, "tdest"):
self.bus.tdest <= tdest_val self.bus.tdest.value = tdest_val
if hasattr(self.bus, "tuser"): if hasattr(self.bus, "tuser"):
self.bus.tuser <= tuser_val self.bus.tuser.value = tuser_val
else: else:
if hasattr(self.bus, "tvalid"): if hasattr(self.bus, "tvalid"):
self.bus.tvalid <= 0 self.bus.tvalid.value = 0
if hasattr(self.bus, "tlast"): if hasattr(self.bus, "tlast"):
self.bus.tlast <= 0 self.bus.tlast.value = 0
self.active = bool(frame) self.active = bool(frame)
if not frame and self.queue.empty():
async def _run_pause(self): self.idle_event.set()
for val in self._pause_generator:
self.pause = val
await RisingEdge(self.clock)
class AxiStreamSink(object): class AxiStreamMonitor(AxiStreamBase):
_signals = ["tdata"] _type = "monitor"
_optional_signals = ["tvalid", "tready", "tlast", "tkeep", "tid", "tdest", "tuser"]
def __init__(self, entity, name, clock, reset=None, *args, **kwargs): _init_x = False
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
self.entity = entity
self.clock = clock
self.reset = reset
self.bus = Bus(self.entity, name, self._signals, optional_signals=self._optional_signals, **kwargs)
self.log.info("AXI stream sink") _valid_init = None
self.log.info("cocotbext-axi version %s", __version__) _ready_init = None
self.log.info("Copyright (c) 2020 Alex Forencich")
self.log.info("https://github.com/alexforencich/cocotbext-axi")
super().__init__(*args, **kwargs) def __init__(self, bus, clock, reset=None, reset_active_level=True,
byte_size=None, byte_lanes=None, *args, **kwargs):
super().__init__(bus, clock, reset, reset_active_level, byte_size, byte_lanes, *args, **kwargs)
self.active = False
self.queue = deque()
self.sync = Event()
self.read_queue = [] self.read_queue = []
self.pause = False def _recv(self, frame, compact=True):
self._pause_generator = None if self.queue.empty():
self._pause_cr = None self.active_event.clear()
self.queue_occupancy_bytes = 0
self.queue_occupancy_frames = 0
self.queue_occupancy_limit_bytes = None
self.queue_occupancy_limit_frames = None
self.width = len(self.bus.tdata)
self.byte_width = 1
self.reset = reset
if hasattr(self.bus, "tready"):
self.bus.tready.setimmediatevalue(0)
if hasattr(self.bus, "tkeep"):
self.byte_width = len(self.bus.tkeep)
self.byte_size = self.width // self.byte_width
self.byte_mask = 2**self.byte_size-1
self.log.info("AXI stream sink configuration:")
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(" tvalid: %s", "present" if hasattr(self.bus, "tvalid") else "not present")
self.log.info(" tready: %s", "present" if hasattr(self.bus, "tready") else "not present")
self.log.info(" tlast: %s", "present" if hasattr(self.bus, "tlast") else "not present")
if hasattr(self.bus, "tkeep"):
self.log.info(" tkeep width: %d bits", len(self.bus.tkeep))
else:
self.log.info(" tkeep: not present")
if hasattr(self.bus, "tid"):
self.log.info(" tid width: %d bits", len(self.bus.tid))
else:
self.log.info(" tid: not present")
if hasattr(self.bus, "tdest"):
self.log.info(" tdest width: %d bits", len(self.bus.tdest))
else:
self.log.info(" tdest: not present")
if hasattr(self.bus, "tuser"):
self.log.info(" tuser width: %d bits", len(self.bus.tuser))
else:
self.log.info(" tuser: not present")
cocotb.fork(self._run())
def recv(self, compact=True):
if self.queue:
frame = self.queue.popleft()
self.queue_occupancy_bytes -= len(frame) self.queue_occupancy_bytes -= len(frame)
self.queue_occupancy_frames -= 1 self.queue_occupancy_frames -= 1
if compact: if compact:
frame.compact() frame.compact()
return frame return frame
return None
def read(self, count=-1): async def recv(self, compact=True):
while True: frame = await self.queue.get()
frame = self.recv(compact=True) return self._recv(frame, compact)
if frame is None:
break def recv_nowait(self, compact=True):
frame = self.queue.get_nowait()
return self._recv(frame, compact)
async def read(self, count=-1):
while not self.read_queue:
frame = await self.recv(compact=True)
self.read_queue.extend(frame.tdata)
return self.read_nowait(count)
def read_nowait(self, count=-1):
while not self.empty():
frame = self.recv_nowait(compact=True)
self.read_queue.extend(frame.tdata) self.read_queue.extend(frame.tdata)
if count < 0: if count < 0:
count = len(self.read_queue) count = len(self.read_queue)
@@ -501,67 +604,40 @@ class AxiStreamSink(object):
del self.read_queue[:count] del self.read_queue[:count]
return data return data
def count(self):
return len(self.queue)
def empty(self):
return not self.queue
def full(self):
if self.queue_occupancy_limit_bytes and self.queue_occupancy_bytes > self.queue_occupancy_limit_bytes:
return True
elif self.queue_occupancy_limit_frames and self.queue_occupancy_frames > self.queue_occupancy_limit_frames:
return True
else:
return False
def idle(self): def idle(self):
return not self.active return not self.active
async def wait(self, timeout=0, timeout_unit='ns'): async def wait(self, timeout=0, timeout_unit='ns'):
if not self.empty(): if not self.empty():
return return
self.sync.clear()
if timeout: if timeout:
await First(self.sync.wait(), Timer(timeout, timeout_unit)) await First(self.active_event.wait(), Timer(timeout, timeout_unit))
else: else:
await self.sync.wait() await self.active_event.wait()
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.fork(self._run_pause())
def clear_pause_generator(self):
self.set_pause_generator(None)
async def _run(self): async def _run(self):
frame = AxiStreamFrame([], [], [], [], []) frame = None
self.active = False self.active = False
clock_edge_event = RisingEdge(self.clock)
while True: while True:
await ReadOnly() await clock_edge_event
# read handshake signals # read handshake signals
tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value
tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value
if self.reset is not None and self.reset.value:
await RisingEdge(self.clock)
frame = AxiStreamFrame([], [], [], [], [])
self.active = False
if hasattr(self.bus, "tready"):
self.bus.tready <= 0
continue
if tready_sample and tvalid_sample: if tready_sample and tvalid_sample:
for offset in range(self.byte_width): if frame is None:
if self.byte_size == 8:
frame = AxiStreamFrame(bytearray(), [], [], [], [])
else:
frame = AxiStreamFrame([], [], [], [], [])
frame.sim_time_start = get_sim_time()
self.active = True
for offset in range(self.byte_lanes):
frame.tdata.append((self.bus.tdata.value.integer >> (offset * self.byte_size)) & self.byte_mask) frame.tdata.append((self.bus.tdata.value.integer >> (offset * self.byte_size)) & self.byte_mask)
if hasattr(self.bus, "tkeep"): if hasattr(self.bus, "tkeep"):
frame.tkeep.append((self.bus.tkeep.value.integer >> offset) & 1) frame.tkeep.append((self.bus.tkeep.value.integer >> offset) & 1)
@@ -573,153 +649,75 @@ class AxiStreamSink(object):
frame.tuser.append(self.bus.tuser.value.integer) frame.tuser.append(self.bus.tuser.value.integer)
if not hasattr(self.bus, "tlast") or self.bus.tlast.value: if not hasattr(self.bus, "tlast") or self.bus.tlast.value:
if self.byte_size == 8: frame.sim_time_end = get_sim_time()
frame.tdata = bytearray(frame.tdata)
self.log.info("RX frame: %s", frame) self.log.info("RX frame: %s", frame)
self.queue_occupancy_bytes += len(frame) self.queue_occupancy_bytes += len(frame)
self.queue_occupancy_frames += 1 self.queue_occupancy_frames += 1
self.queue.append(frame) self.queue.put_nowait(frame)
self.sync.set() self.active_event.set()
frame = AxiStreamFrame([], [], [], [], []) frame = None
else:
self.active = bool(frame)
await RisingEdge(self.clock)
class AxiStreamSink(AxiStreamMonitor, AxiStreamPause):
_type = "sink"
_init_x = False
_valid_init = None
_ready_init = 0
def __init__(self, bus, clock, reset=None, reset_active_level=True,
byte_size=None, byte_lanes=None, *args, **kwargs):
super().__init__(bus, clock, reset, reset_active_level, byte_size, byte_lanes, *args, **kwargs)
self.queue_occupancy_limit_bytes = -1
self.queue_occupancy_limit_frames = -1
def full(self):
if self.queue_occupancy_limit_bytes > 0 and self.queue_occupancy_bytes > self.queue_occupancy_limit_bytes:
return True
elif self.queue_occupancy_limit_frames > 0 and self.queue_occupancy_frames > self.queue_occupancy_limit_frames:
return True
else:
return False
def _handle_reset(self, state):
super()._handle_reset(state)
if state:
if hasattr(self.bus, "tready"): if hasattr(self.bus, "tready"):
self.bus.tready <= (not self.full() and not self.pause) self.bus.tready.value = 0
async def _run_pause(self):
for val in self._pause_generator:
self.pause = val
await RisingEdge(self.clock)
class AxiStreamMonitor(object):
_signals = ["tdata"]
_optional_signals = ["tvalid", "tready", "tlast", "tkeep", "tid", "tdest", "tuser"]
def __init__(self, entity, name, clock, reset=None, *args, **kwargs):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name))
self.entity = entity
self.clock = clock
self.reset = reset
self.bus = Bus(self.entity, name, self._signals, optional_signals=self._optional_signals, **kwargs)
self.log.info("AXI stream monitor")
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__(*args, **kwargs)
self.active = False
self.queue = deque()
self.sync = Event()
self.read_queue = []
self.queue_occupancy_bytes = 0
self.queue_occupancy_frames = 0
self.width = len(self.bus.tdata)
self.byte_width = 1
self.reset = reset
if hasattr(self.bus, "tkeep"):
self.byte_width = len(self.bus.tkeep)
self.byte_size = self.width // self.byte_width
self.byte_mask = 2**self.byte_size-1
self.log.info("AXI stream monitor configuration:")
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(" tvalid: %s", "present" if hasattr(self.bus, "tvalid") else "not present")
self.log.info(" tready: %s", "present" if hasattr(self.bus, "tready") else "not present")
self.log.info(" tlast: %s", "present" if hasattr(self.bus, "tlast") else "not present")
if hasattr(self.bus, "tkeep"):
self.log.info(" tkeep width: %d bits", len(self.bus.tkeep))
else:
self.log.info(" tkeep: not present")
if hasattr(self.bus, "tid"):
self.log.info(" tid width: %d bits", len(self.bus.tid))
else:
self.log.info(" tid: not present")
if hasattr(self.bus, "tdest"):
self.log.info(" tdest width: %d bits", len(self.bus.tdest))
else:
self.log.info(" tdest: not present")
if hasattr(self.bus, "tuser"):
self.log.info(" tuser width: %d bits", len(self.bus.tuser))
else:
self.log.info(" tuser: not present")
cocotb.fork(self._run())
def recv(self, compact=True):
if self.queue:
frame = self.queue.popleft()
self.queue_occupancy_bytes -= len(frame)
self.queue_occupancy_frames -= 1
if compact:
frame.compact()
return frame
return None
def read(self, count=-1):
while True:
frame = self.recv(compact=True)
if frame is None:
break
self.read_queue.extend(frame.tdata)
if count < 0:
count = len(self.read_queue)
data = self.read_queue[:count]
del self.read_queue[:count]
return data
def count(self):
return len(self.queue)
def empty(self):
return not self.queue
def idle(self):
return not self.active
async def wait(self, timeout=0, timeout_unit='ns'):
if not self.empty():
return
self.sync.clear()
if timeout:
await First(self.sync.wait(), Timer(timeout, timeout_unit))
else:
await self.sync.wait()
async def _run(self): async def _run(self):
frame = AxiStreamFrame([], [], [], [], []) frame = None
self.active = False self.active = False
clock_edge_event = RisingEdge(self.clock)
while True: while True:
await ReadOnly() await clock_edge_event
# read handshake signals # read handshake signals
tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value tready_sample = (not hasattr(self.bus, "tready")) or self.bus.tready.value
tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value tvalid_sample = (not hasattr(self.bus, "tvalid")) or self.bus.tvalid.value
if self.reset is not None and self.reset.value:
await RisingEdge(self.clock)
frame = AxiStreamFrame([], [], [], [], [])
self.active = False
continue
if tready_sample and tvalid_sample: if tready_sample and tvalid_sample:
for offset in range(self.byte_width): if frame is None:
if self.byte_size == 8:
frame = AxiStreamFrame(bytearray(), [], [], [], [])
else:
frame = AxiStreamFrame([], [], [], [], [])
frame.sim_time_start = get_sim_time()
self.active = True
for offset in range(self.byte_lanes):
frame.tdata.append((self.bus.tdata.value.integer >> (offset * self.byte_size)) & self.byte_mask) frame.tdata.append((self.bus.tdata.value.integer >> (offset * self.byte_size)) & self.byte_mask)
if hasattr(self.bus, "tkeep"): if hasattr(self.bus, "tkeep"):
frame.tkeep.append((self.bus.tkeep.value.integer >> offset) & 1) frame.tkeep.append((self.bus.tkeep.value.integer >> offset) & 1)
@@ -731,14 +729,18 @@ class AxiStreamMonitor(object):
frame.tuser.append(self.bus.tuser.value.integer) frame.tuser.append(self.bus.tuser.value.integer)
if not hasattr(self.bus, "tlast") or self.bus.tlast.value: if not hasattr(self.bus, "tlast") or self.bus.tlast.value:
if self.byte_size == 8: frame.sim_time_end = get_sim_time()
frame.tdata = bytearray(frame.tdata)
self.log.info("RX frame: %s", frame) self.log.info("RX frame: %s", frame)
self.queue.append(frame) self.queue_occupancy_bytes += len(frame)
self.sync.set() self.queue_occupancy_frames += 1
frame = AxiStreamFrame([], [], [], [], []) self.queue.put_nowait(frame)
self.active_event.set()
await RisingEdge(self.clock) frame = None
else:
self.active = bool(frame)
if hasattr(self.bus, "tready"):
self.bus.tready.value = (not self.full() and not self.pause)

View File

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

View File

@@ -27,14 +27,14 @@ import mmap
from .utils import hexdump, hexdump_lines, hexdump_str from .utils import hexdump, hexdump_lines, hexdump_str
class Memory(object): class Memory:
def __init__(self, size=1024, mem=None, *args, **kwargs): def __init__(self, size=1024, mem=None, **kwargs):
if mem is not None: if mem is not None:
self.mem = mem self.mem = mem
else: else:
self.mem = mmap.mmap(-1, size) self.mem = mmap.mmap(-1, size)
self.size = len(self.mem) self.size = len(self.mem)
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def read(self, address, length): def read(self, address, length):
self.mem.seek(address) self.mem.seek(address)

66
cocotbext/axi/reset.py Normal file
View File

@@ -0,0 +1,66 @@
"""
Copyright (c) 2020 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 RisingEdge, FallingEdge
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:
if bool(reset_signal.value):
await FallingEdge(reset_signal)
self._ext_reset = not active_level
self._update_reset()
else:
await RisingEdge(reset_signal)
self._ext_reset = active_level
self._update_reset()

View File

@@ -22,15 +22,34 @@ THE SOFTWARE.
""" """
import logging
import cocotb import cocotb
from cocotb.triggers import RisingEdge, ReadOnly, Event, First, Timer from cocotb.queue import Queue, QueueFull
from cocotb.bus import Bus from cocotb.triggers import RisingEdge, Event, First, Timer
from cocotb.log import SimLog from cocotb_bus.bus import Bus
from collections import deque 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"] _signals = ["data"]
@@ -48,7 +67,7 @@ class StreamTransaction(object):
return f"{type(self).__name__}({', '.join(f'{s}={int(getattr(self, s))}' for s in self._signals)})" 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"] _signals = ["data", "valid", "ready"]
_optional_signals = [] _optional_signals = []
@@ -63,18 +82,23 @@ class StreamBase(object):
_ready_init = None _ready_init = None
_transaction_obj = StreamTransaction _transaction_obj = StreamTransaction
_bus_obj = StreamBus
def __init__(self, entity, name, clock, reset=None, *args, **kwargs): def __init__(self, bus, clock, reset=None, reset_active_level=True, *args, **kwargs):
self.log = SimLog("cocotb.%s.%s" % (entity._name, name)) self.bus = bus
self.entity = entity
self.clock = clock self.clock = clock
self.reset = reset 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) super().__init__(*args, **kwargs)
self.queue = deque() self.active = False
self.queue_sync = Event()
self.queue = Queue()
self.dequeue_event = Event()
self.idle_event = Event()
self.idle_event.set()
self.active_event = Event()
self.ready = None self.ready = None
self.valid = None self.valid = None
@@ -98,17 +122,44 @@ class StreamBase(object):
v.binstr = 'x'*len(v) v.binstr = 'x'*len(v)
getattr(self.bus, sig).setimmediatevalue(v) getattr(self.bus, sig).setimmediatevalue(v)
self._run_cr = None
self._init_reset(reset, reset_active_level)
def count(self): def count(self):
return len(self.queue) return self.queue.qsize()
def empty(self): def empty(self):
return not self.queue return self.queue.empty()
def clear(self): 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -124,235 +175,176 @@ class StreamPause(object):
self._pause_generator = generator self._pause_generator = generator
if self._pause_generator is not None: if self._pause_generator is not None:
self._pause_cr = cocotb.fork(self._run_pause()) self._pause_cr = cocotb.start_soon(self._run_pause())
def clear_pause_generator(self): def clear_pause_generator(self):
self.set_pause_generator(None) self.set_pause_generator(None)
async def _run_pause(self): async def _run_pause(self):
clock_edge_event = RisingEdge(self.clock)
for val in self._pause_generator: for val in self._pause_generator:
self.pause = val self.pause = val
await RisingEdge(self.clock) await clock_edge_event
class StreamSource(StreamBase, StreamPause): class StreamSource(StreamBase, StreamPause):
_signals = ["data", "valid", "ready"]
_optional_signals = []
_signal_widths = {"valid": 1, "ready": 1}
_init_x = True _init_x = True
_valid_signal = "valid"
_valid_init = 0 _valid_init = 0
_ready_signal = "ready"
_ready_init = None _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): self.queue_occupancy_limit = -1
super().__init__(entity, name, clock, reset, *args, **kwargs)
self.drive_obj = None async def send(self, obj):
self.drive_sync = Event() while self.full():
self.dequeue_event.clear()
await self.dequeue_event.wait()
await self.queue.put(obj)
self.idle_event.clear()
self.active = False def send_nowait(self, obj):
if self.full():
raise QueueFull()
self.queue.put_nowait(obj)
self.idle_event.clear()
cocotb.fork(self._run_source()) def full(self):
cocotb.fork(self._run()) if self.queue_occupancy_limit > 0 and self.count() >= self.queue_occupancy_limit:
return True
async def drive(self, obj): else:
if self.drive_obj is not None: return False
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 idle(self): def idle(self):
return self.empty() and not self.active return self.empty() and not self.active
async def wait(self): async def wait(self):
while not self.idle(): await self.idle_event.wait()
await RisingEdge(self.clock)
def clear(self): def _handle_reset(self, state):
self.queue.clear() super()._handle_reset(state)
self.drive_obj = None
self.drive_sync.set() if state:
if self.valid is not None:
self.valid.value = 0
async def _run(self):
clock_edge_event = RisingEdge(self.clock)
async def _run_source(self):
while True: while True:
await ReadOnly() await clock_edge_event
# read handshake signals # read handshake signals
ready_sample = self.ready is None or self.ready.value ready_sample = self.ready is None or self.ready.value
valid_sample = self.valid is None or self.valid.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 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 (ready_sample and valid_sample) or (not valid_sample):
if self.drive_obj and not self.pause: if not self.queue.empty() and not self.pause:
self.bus.drive(self.drive_obj) self.bus.drive(self.queue.get_nowait())
self.drive_obj = None self.dequeue_event.set()
self.drive_sync.set()
if self.valid is not None: if self.valid is not None:
self.valid <= 1 self.valid.value = 1
self.active = True self.active = True
else: else:
if self.valid is not None: if self.valid is not None:
self.valid <= 0 self.valid.value = 0
self.active = bool(self.drive_obj) self.active = not self.queue.empty()
if self.queue.empty():
async def _run(self): self.idle_event.set()
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()
# 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 self.ready is not None:
self.ready <= 0
continue
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)
class StreamMonitor(StreamBase): class StreamMonitor(StreamBase):
_signals = ["data", "valid", "ready"]
_optional_signals = []
_signal_widths = {"valid": 1, "ready": 1}
_init_x = False _init_x = False
_valid_signal = "valid"
_valid_init = None _valid_init = None
_ready_signal = "ready"
_ready_init = None _ready_init = None
_transaction_obj = StreamTransaction def _recv(self, item):
if self.queue.empty():
self.active_event.clear()
return item
def __init__(self, entity, name, clock, reset=None, *args, **kwargs): async def recv(self):
super().__init__(entity, name, clock, reset, *args, **kwargs) item = await self.queue.get()
return self._recv(item)
cocotb.fork(self._run_monitor()) def recv_nowait(self):
item = self.queue.get_nowait()
def recv(self): return self._recv(item)
if self.queue:
return self.queue.popleft()
return None
def count(self):
return len(self.queue)
def empty(self):
return not self.queue
async def wait(self, timeout=0, timeout_unit=None): async def wait(self, timeout=0, timeout_unit=None):
if not self.empty(): if not self.empty():
return return
self.queue_sync.clear()
if timeout: if timeout:
await First(self.queue_sync.wait(), Timer(timeout, timeout_unit)) await First(self.active_event.wait(), Timer(timeout, timeout_unit))
else: else:
await self.queue_sync.wait() await self.active_event.wait()
def callback(self, obj): async def _run(self):
self.queue.append(obj) clock_edge_event = RisingEdge(self.clock)
self.queue_sync.set()
async def _run_monitor(self):
while True: while True:
await ReadOnly() await clock_edge_event
# read handshake signals # read handshake signals
ready_sample = self.ready is None or self.ready.value ready_sample = self.ready is None or self.ready.value
valid_sample = self.valid is None or self.valid.value valid_sample = self.valid is None or self.valid.value
if self.reset is not None and self.reset.value: if ready_sample and valid_sample:
await RisingEdge(self.clock) obj = self._transaction_obj()
self.clear() self.bus.sample(obj)
continue self.queue.put_nowait(obj)
self.active_event.set()
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
async def _run(self):
clock_edge_event = RisingEdge(self.clock)
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 ready_sample and valid_sample: if ready_sample and valid_sample:
obj = self._transaction_obj() obj = self._transaction_obj()
self.bus.sample(obj) self.bus.sample(obj)
self.callback(obj) self.queue.put_nowait(obj)
self.active_event.set()
await RisingEdge(self.clock) if self.ready is not None:
self.ready.value = (not self.full() and not self.pause)
def define_stream(name, signals, optional_signals=None, valid_signal=None, ready_signal=None, signal_widths=None): def define_stream(name, signals, optional_signals=None, valid_signal=None, ready_signal=None, signal_widths=None):
@@ -393,6 +385,11 @@ def define_stream(name, signals, optional_signals=None, valid_signal=None, ready
if s not in (ready_signal, valid_signal): if s not in (ready_signal, valid_signal):
filtered_signals.append(s) 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 = {s: 0 for s in filtered_signals}
attrib['_signals'] = filtered_signals attrib['_signals'] = filtered_signals
@@ -405,9 +402,10 @@ def define_stream(name, signals, optional_signals=None, valid_signal=None, ready
attrib['_ready_signal'] = ready_signal attrib['_ready_signal'] = ready_signal
attrib['_valid_signal'] = valid_signal attrib['_valid_signal'] = valid_signal
attrib['_transaction_obj'] = transaction attrib['_transaction_obj'] = transaction
attrib['_bus_obj'] = bus
source = type(name+"Source", (StreamSource,), attrib) source = type(name+"Source", (StreamSource,), attrib)
sink = type(name+"Sink", (StreamSink,), attrib) sink = type(name+"Sink", (StreamSink,), attrib)
monitor = type(name+"Monitor", (StreamMonitor,), attrib) monitor = type(name+"Monitor", (StreamMonitor,), attrib)
return transaction, source, sink, monitor return bus, transaction, source, sink, monitor

View File

@@ -23,28 +23,28 @@ THE SOFTWARE.
""" """
def hexdump_line(data, offset): def hexdump_line(data, offset, row_size=16):
h = "" h = ""
c = "" c = ""
for ch in data[0:16]: for ch in data[0:row_size]:
h += f"{ch:02x} " h += f"{ch:02x} "
c += chr(ch) if 32 < ch < 127 else "." 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) stop = min(start+length, len(data)) if length else len(data)
for k in range(start, stop, 16): for k in range(start, stop, row_size):
print(prefix+hexdump_line(data[k:min(k+16, stop)], k+offset)) 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 = [] lines = []
stop = min(start+length, len(data)) if length else len(data) stop = min(start+length, len(data)) if length else len(data)
for k in range(start, stop, 16): for k in range(start, stop, row_size):
lines.append(prefix+hexdump_line(data[k:min(k+16, stop)], k+offset)) lines.append(prefix+hexdump_line(data[k:min(k+row_size, stop)], k+offset, row_size))
return lines return lines
def hexdump_str(data, start=0, length=None, prefix="", offset=0): def hexdump_str(data, start=0, length=None, row_size=16, prefix="", offset=0):
return "\n".join(hexdump_lines(data, start, length, prefix, offset)) return "\n".join(hexdump_lines(data, start, length, row_size, prefix, offset))

View File

@@ -1 +1 @@
__version__ = "0.1.0" __version__ = "0.1.18"

View File

@@ -17,16 +17,18 @@ long-description-content-type = text/markdown
platforms = any platforms = any
classifiers = classifiers =
Development Status :: 3 - Alpha Development Status :: 3 - Alpha
Programming Language :: Python :: 3 Framework :: cocotb
License :: OSI Approved :: MIT License License :: OSI Approved :: MIT License
Operating System :: OS Independent Operating System :: OS Independent
Programming Language :: Python :: 3
Topic :: Scientific/Engineering :: Electronic Design Automation (EDA) Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
[options] [options]
packages = find_namespace: packages = find_namespace:
python_requires = >=3.6 python_requires = >=3.6
install_requires = install_requires =
cocotb cocotb >= 1.6.0
cocotb-bus
[options.extras_require] [options.extras_require]
test = test =
@@ -40,10 +42,15 @@ include = cocotbext.*
[tool:pytest] [tool:pytest]
testpaths = testpaths =
tests tests
addopts =
--import-mode importlib
# tox configuration # tox configuration
[tox:tox] [tox:tox]
envlist = py36, py37, py38, py39 envlist = py36, py37, py38, py39, py310
skip_missing_interpreters = true
minversion = 3.2.0
requires = virtualenv >= 16.1
[gh-actions] [gh-actions]
python = python =
@@ -51,12 +58,33 @@ python =
3.7: py37 3.7: py37
3.8: py38 3.8: py38
3.9: py39 3.9: py39
3.10: py310
[testenv] [testenv]
setenv =
COVERAGE=1
deps = deps =
pytest pytest
pytest-xdist pytest-xdist
cocotb-test cocotb-test
coverage
pytest-cov
commands = commands =
pytest -n auto pytest --cov=cocotbext --cov=tests --cov-branch -n auto
bash -c 'find . -type f -name "\.coverage" | xargs coverage combine --append'
whitelist_externals =
bash
# combine if paths are different
[coverage:paths]
source =
cocotbext/
/*/cocotbext
# do not report dependencies
[coverage:report]
omit =
.tox/*

View File

@@ -42,8 +42,6 @@ export PARAM_BUSER_WIDTH ?= 1
export PARAM_ARUSER_WIDTH ?= 1 export PARAM_ARUSER_WIDTH ?= 1
export PARAM_RUSER_WIDTH ?= 1 export PARAM_RUSER_WIDTH ?= 1
SIM_BUILD ?= sim_build_$(MODULE)-$(PARAM_DATA_WIDTH)
ifeq ($(SIM), icarus) ifeq ($(SIM), icarus)
PLUSARGS += -fst PLUSARGS += -fst
@@ -79,6 +77,8 @@ else ifeq ($(SIM), verilator)
endif endif
endif endif
include $(shell cocotb-config --makefiles)/Makefile.sim
iverilog_dump.v: iverilog_dump.v:
echo 'module iverilog_dump();' > $@ echo 'module iverilog_dump();' > $@
echo 'initial begin' >> $@ echo 'initial begin' >> $@
@@ -88,9 +88,5 @@ iverilog_dump.v:
echo 'endmodule' >> $@ echo 'endmodule' >> $@
clean:: clean::
@rm -rf sim_build_*
@rm -rf iverilog_dump.v @rm -rf iverilog_dump.v
@rm -rf dump.fst $(TOPLEVEL).fst @rm -rf dump.fst $(TOPLEVEL).fst
include $(shell cocotb-config --makefiles)/Makefile.sim

View File

View File

@@ -35,20 +35,20 @@ from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer from cocotb.triggers import RisingEdge, Timer
from cocotb.regression import TestFactory 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): def __init__(self, dut):
self.dut = dut self.dut = dut
self.log = logging.getLogger("cocotb.tb") self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG) self.log.setLevel(logging.DEBUG)
cocotb.fork(Clock(dut.clk, 2, units="ns").start()) cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
self.axi_master = AxiMaster(dut, "axi", dut.clk, dut.rst) self.axi_master = AxiMaster(AxiBus.from_prefix(dut, "axi"), dut.clk, dut.rst)
self.axi_ram = AxiRam(dut, "axi", dut.clk, dut.rst, size=2**16) self.axi_ram = AxiRam(AxiBus.from_prefix(dut, "axi"), dut.clk, dut.rst, size=2**16)
self.axi_ram.write_if.log.setLevel(logging.DEBUG) self.axi_ram.write_if.log.setLevel(logging.DEBUG)
self.axi_ram.read_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) self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 1 self.dut.rst.value = 1
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 0 self.dut.rst.value = 0
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
@@ -85,7 +85,7 @@ async def run_test_write(dut, idle_inserter=None, backpressure_inserter=None, si
tb = TB(dut) 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 max_burst_size = tb.axi_master.write_if.max_burst_size
if size is None: if size is None:
@@ -96,9 +96,9 @@ async def run_test_write(dut, idle_inserter=None, backpressure_inserter=None, si
tb.set_idle_generator(idle_inserter) tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter) tb.set_backpressure_generator(backpressure_inserter)
for length in list(range(1, byte_width*2))+[1024]: for length in list(range(1, byte_lanes*2))+[1024]:
for offset in list(range(byte_width))+list(range(4096-byte_width, 4096)): for offset in list(range(byte_lanes))+list(range(4096-byte_lanes, 4096)):
tb.log.info(f"length {length}, offset {offset}") tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000 addr = offset+0x1000
test_data = bytearray([x % 256 for x in range(length)]) test_data = bytearray([x % 256 for x in range(length)])
@@ -106,7 +106,7 @@ async def run_test_write(dut, idle_inserter=None, backpressure_inserter=None, si
await tb.axi_master.write(addr, test_data, size=size) await tb.axi_master.write(addr, test_data, size=size)
tb.log.debug("%s", tb.axi_ram.hexdump_str((addr & 0xfffffff0)-16, (((addr & 0xf)+length-1) & 0xfffffff0)+48)) tb.log.debug("%s", tb.axi_ram.hexdump_str((addr & ~0xf)-16, (((addr & 0xf)+length-1) & ~0xf)+48))
assert tb.axi_ram.read(addr, length) == test_data assert tb.axi_ram.read(addr, length) == test_data
assert tb.axi_ram.read(addr-1, 1) == b'\xaa' assert tb.axi_ram.read(addr-1, 1) == b'\xaa'
@@ -120,7 +120,7 @@ async def run_test_read(dut, idle_inserter=None, backpressure_inserter=None, siz
tb = TB(dut) 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 max_burst_size = tb.axi_master.write_if.max_burst_size
if size is None: if size is None:
@@ -131,9 +131,9 @@ async def run_test_read(dut, idle_inserter=None, backpressure_inserter=None, siz
tb.set_idle_generator(idle_inserter) tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter) tb.set_backpressure_generator(backpressure_inserter)
for length in list(range(1, byte_width*2))+[1024]: for length in list(range(1, byte_lanes*2))+[1024]:
for offset in list(range(byte_width))+list(range(4096-byte_width, 4096)): for offset in list(range(byte_lanes))+list(range(4096-byte_lanes, 4096)):
tb.log.info(f"length {length}, offset {offset}") tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000 addr = offset+0x1000
test_data = bytearray([x % 256 for x in range(length)]) test_data = bytearray([x % 256 for x in range(length)])
@@ -151,15 +151,20 @@ async def run_test_write_words(dut):
tb = TB(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() await tb.cycle_reset()
for length in list(range(1, 4)): for length in list(range(1, 4)):
for offset in list(range(byte_width)): for offset in list(range(byte_lanes)):
tb.log.info(f"length {length}, offset {offset}") tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000 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)]) test_data = bytearray([x % 256 for x in range(length)])
await tb.axi_master.write(addr, test_data) await tb.axi_master.write(addr, test_data)
assert tb.axi_ram.read(addr, length) == 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) 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() await tb.cycle_reset()
for length in list(range(1, 4)): for length in list(range(1, 4)):
for offset in list(range(byte_width)): for offset in list(range(byte_lanes)):
tb.log.info(f"length {length}, offset {offset}") tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000 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)]) test_data = bytearray([x % 256 for x in range(length)])
tb.axi_ram.write(addr, test_data) tb.axi_ram.write(addr, test_data)
assert (await tb.axi_master.read(addr, length)).data == test_data assert (await tb.axi_master.read(addr, length)).data == test_data
@@ -254,7 +265,7 @@ async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
tb.set_idle_generator(idle_inserter) tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter) tb.set_backpressure_generator(backpressure_inserter)
async def stress_test_worker(master, offset, aperture, count=16): async def worker(master, offset, aperture, count=16):
for k in range(count): for k in range(count):
length = random.randint(1, min(512, aperture)) length = random.randint(1, min(512, aperture))
addr = offset+random.randint(0, aperture-length) addr = offset+random.randint(0, aperture-length)
@@ -272,7 +283,7 @@ async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
workers = [] workers = []
for k in range(16): for k in range(16):
workers.append(cocotb.fork(stress_test_worker(tb.axi_master, k*0x1000, 0x1000, count=16))) workers.append(cocotb.start_soon(worker(tb.axi_master, k*0x1000, 0x1000, count=16)))
while workers: while workers:
await workers.pop(0).join() await workers.pop(0).join()
@@ -287,9 +298,9 @@ def cycle_pause():
if cocotb.SIM_NAME: if cocotb.SIM_NAME:
data_width = int(os.getenv("PARAM_DATA_WIDTH")) data_width = len(cocotb.top.axi_wdata)
byte_width = data_width // 8 byte_lanes = data_width // 8
max_burst_size = (byte_width-1).bit_length() max_burst_size = (byte_lanes-1).bit_length()
for test in [run_test_write, run_test_read]: for test in [run_test_write, run_test_read]:
@@ -311,7 +322,6 @@ if cocotb.SIM_NAME:
# cocotb-test # cocotb-test
tests_dir = os.path.dirname(__file__) 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]) @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()} extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
sim_build = os.path.join(tests_dir, sim_build = os.path.join(tests_dir, "sim_build",
"sim_build_"+request.node.name.replace('[', '-').replace(']', '')) request.node.name.replace('[', '-').replace(']', ''))
cocotb_test.simulator.run( cocotb_test.simulator.run(
python_search=[tests_dir], python_search=[tests_dir],

View File

@@ -36,8 +36,6 @@ export PARAM_DATA_WIDTH ?= 32
export PARAM_ADDR_WIDTH ?= 32 export PARAM_ADDR_WIDTH ?= 32
export PARAM_STRB_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 ) export PARAM_STRB_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 )
SIM_BUILD ?= sim_build_$(MODULE)-$(PARAM_DATA_WIDTH)
ifeq ($(SIM), icarus) ifeq ($(SIM), icarus)
PLUSARGS += -fst PLUSARGS += -fst
@@ -61,6 +59,8 @@ else ifeq ($(SIM), verilator)
endif endif
endif endif
include $(shell cocotb-config --makefiles)/Makefile.sim
iverilog_dump.v: iverilog_dump.v:
echo 'module iverilog_dump();' > $@ echo 'module iverilog_dump();' > $@
echo 'initial begin' >> $@ echo 'initial begin' >> $@
@@ -70,9 +70,5 @@ iverilog_dump.v:
echo 'endmodule' >> $@ echo 'endmodule' >> $@
clean:: clean::
@rm -rf sim_build_*
@rm -rf iverilog_dump.v @rm -rf iverilog_dump.v
@rm -rf dump.fst $(TOPLEVEL).fst @rm -rf dump.fst $(TOPLEVEL).fst
include $(shell cocotb-config --makefiles)/Makefile.sim

View File

View File

@@ -35,20 +35,20 @@ from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer from cocotb.triggers import RisingEdge, Timer
from cocotb.regression import TestFactory 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): def __init__(self, dut):
self.dut = dut self.dut = dut
self.log = logging.getLogger("cocotb.tb") self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG) self.log.setLevel(logging.DEBUG)
cocotb.fork(Clock(dut.clk, 2, units="ns").start()) cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
self.axil_master = AxiLiteMaster(dut, "axil", dut.clk, dut.rst) self.axil_master = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "axil"), dut.clk, dut.rst)
self.axil_ram = AxiLiteRam(dut, "axil", dut.clk, dut.rst, size=2**16) self.axil_ram = AxiLiteRam(AxiLiteBus.from_prefix(dut, "axil"), dut.clk, dut.rst, size=2**16)
def set_idle_generator(self, generator=None): def set_idle_generator(self, generator=None):
if generator: if generator:
@@ -70,10 +70,10 @@ class TB(object):
self.dut.rst.setimmediatevalue(0) self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 1 self.dut.rst.value = 1
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 0 self.dut.rst.value = 0
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
@@ -82,16 +82,16 @@ async def run_test_write(dut, data_in=None, idle_inserter=None, backpressure_ins
tb = TB(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() await tb.cycle_reset()
tb.set_idle_generator(idle_inserter) tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter) tb.set_backpressure_generator(backpressure_inserter)
for length in range(1, byte_width*2): for length in range(1, byte_lanes*2):
for offset in range(byte_width): for offset in range(byte_lanes):
tb.log.info(f"length {length}, offset {offset}") tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000 addr = offset+0x1000
test_data = bytearray([x % 256 for x in range(length)]) test_data = bytearray([x % 256 for x in range(length)])
@@ -99,7 +99,7 @@ async def run_test_write(dut, data_in=None, idle_inserter=None, backpressure_ins
await tb.axil_master.write(addr, test_data) await tb.axil_master.write(addr, test_data)
tb.log.debug("%s", tb.axil_ram.hexdump_str((addr & 0xfffffff0)-16, (((addr & 0xf)+length-1) & 0xfffffff0)+48)) tb.log.debug("%s", tb.axil_ram.hexdump_str((addr & ~0xf)-16, (((addr & 0xf)+length-1) & ~0xf)+48))
assert tb.axil_ram.read(addr, length) == test_data assert tb.axil_ram.read(addr, length) == test_data
assert tb.axil_ram.read(addr-1, 1) == b'\xaa' assert tb.axil_ram.read(addr-1, 1) == b'\xaa'
@@ -113,16 +113,16 @@ async def run_test_read(dut, data_in=None, idle_inserter=None, backpressure_inse
tb = TB(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() await tb.cycle_reset()
tb.set_idle_generator(idle_inserter) tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter) tb.set_backpressure_generator(backpressure_inserter)
for length in range(1, byte_width*2): for length in range(1, byte_lanes*2):
for offset in range(byte_width): for offset in range(byte_lanes):
tb.log.info(f"length {length}, offset {offset}") tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000 addr = offset+0x1000
test_data = bytearray([x % 256 for x in range(length)]) test_data = bytearray([x % 256 for x in range(length)])
@@ -140,15 +140,20 @@ async def run_test_write_words(dut):
tb = TB(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() await tb.cycle_reset()
for length in list(range(1, 4)): for length in list(range(1, 4)):
for offset in list(range(byte_width)): for offset in list(range(byte_lanes)):
tb.log.info(f"length {length}, offset {offset}") tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000 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)]) test_data = bytearray([x % 256 for x in range(length)])
await tb.axil_master.write(addr, test_data) await tb.axil_master.write(addr, test_data)
assert tb.axil_ram.read(addr, length) == 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) 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() await tb.cycle_reset()
for length in list(range(1, 4)): for length in list(range(1, 4)):
for offset in list(range(byte_width)): for offset in list(range(byte_lanes)):
tb.log.info(f"length {length}, offset {offset}") tb.log.info("length %d, offset %d", length, offset)
addr = offset+0x1000 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)]) test_data = bytearray([x % 256 for x in range(length)])
tb.axil_ram.write(addr, test_data) tb.axil_ram.write(addr, test_data)
assert (await tb.axil_master.read(addr, length)).data == test_data assert (await tb.axil_master.read(addr, length)).data == test_data
@@ -243,7 +254,7 @@ async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
tb.set_idle_generator(idle_inserter) tb.set_idle_generator(idle_inserter)
tb.set_backpressure_generator(backpressure_inserter) tb.set_backpressure_generator(backpressure_inserter)
async def stress_test_worker(master, offset, aperture, count=16): async def worker(master, offset, aperture, count=16):
for k in range(count): for k in range(count):
length = random.randint(1, min(32, aperture)) length = random.randint(1, min(32, aperture))
addr = offset+random.randint(0, aperture-length) addr = offset+random.randint(0, aperture-length)
@@ -261,7 +272,7 @@ async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None):
workers = [] workers = []
for k in range(16): for k in range(16):
workers.append(cocotb.fork(stress_test_worker(tb.axil_master, k*0x1000, 0x1000, count=16))) workers.append(cocotb.start_soon(worker(tb.axil_master, k*0x1000, 0x1000, count=16)))
while workers: while workers:
await workers.pop(0).join() await workers.pop(0).join()
@@ -295,7 +306,6 @@ if cocotb.SIM_NAME:
# cocotb-test # cocotb-test
tests_dir = os.path.dirname(__file__) 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]) @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()} extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
sim_build = os.path.join(tests_dir, sim_build = os.path.join(tests_dir, "sim_build",
"sim_build_"+request.node.name.replace('[', '-').replace(']', '')) request.node.name.replace('[', '-').replace(']', ''))
cocotb_test.simulator.run( cocotb_test.simulator.run(
python_search=[tests_dir], python_search=[tests_dir],

View File

@@ -38,8 +38,6 @@ export PARAM_ID_WIDTH ?= 8
export PARAM_DEST_WIDTH ?= 8 export PARAM_DEST_WIDTH ?= 8
export PARAM_USER_WIDTH ?= 1 export PARAM_USER_WIDTH ?= 1
SIM_BUILD ?= sim_build_$(MODULE)-$(PARAM_DATA_WIDTH)
ifeq ($(SIM), icarus) ifeq ($(SIM), icarus)
PLUSARGS += -fst PLUSARGS += -fst
@@ -67,6 +65,8 @@ else ifeq ($(SIM), verilator)
endif endif
endif endif
include $(shell cocotb-config --makefiles)/Makefile.sim
iverilog_dump.v: iverilog_dump.v:
echo 'module iverilog_dump();' > $@ echo 'module iverilog_dump();' > $@
echo 'initial begin' >> $@ echo 'initial begin' >> $@
@@ -76,9 +76,5 @@ iverilog_dump.v:
echo 'endmodule' >> $@ echo 'endmodule' >> $@
clean:: clean::
@rm -rf sim_build_*
@rm -rf iverilog_dump.v @rm -rf iverilog_dump.v
@rm -rf dump.fst $(TOPLEVEL).fst @rm -rf dump.fst $(TOPLEVEL).fst
include $(shell cocotb-config --makefiles)/Makefile.sim

View File

View File

@@ -35,21 +35,21 @@ from cocotb.clock import Clock
from cocotb.triggers import RisingEdge from cocotb.triggers import RisingEdge
from cocotb.regression import TestFactory 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): def __init__(self, dut):
self.dut = dut self.dut = dut
self.log = logging.getLogger("cocotb.tb") self.log = logging.getLogger("cocotb.tb")
self.log.setLevel(logging.DEBUG) self.log.setLevel(logging.DEBUG)
cocotb.fork(Clock(dut.clk, 2, units="ns").start()) cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
self.source = AxiStreamSource(dut, "axis", dut.clk, dut.rst) self.source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "axis"), dut.clk, dut.rst)
self.sink = AxiStreamSink(dut, "axis", dut.clk, dut.rst) self.sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "axis"), dut.clk, dut.rst)
self.monitor = AxiStreamMonitor(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): def set_idle_generator(self, generator=None):
if generator: if generator:
@@ -63,10 +63,10 @@ class TB(object):
self.dut.rst.setimmediatevalue(0) self.dut.rst.setimmediatevalue(0)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 1 self.dut.rst.value = 1
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
self.dut.rst <= 0 self.dut.rst.value = 0
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk)
@@ -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 = AxiStreamFrame(test_data)
test_frame.tid = cur_id test_frame.tid = cur_id
test_frame.tdest = cur_id test_frame.tdest = cur_id
tb.source.send(test_frame) await tb.source.send(test_frame)
test_frames.append(test_frame) test_frames.append(test_frame)
cur_id = (cur_id + 1) % id_count cur_id = (cur_id + 1) % id_count
for test_frame in test_frames: for test_frame in test_frames:
await tb.sink.wait() rx_frame = await tb.sink.recv()
rx_frame = tb.sink.recv()
assert rx_frame.tdata == test_frame.tdata assert rx_frame.tdata == test_frame.tdata
assert rx_frame.tid == test_frame.tid assert rx_frame.tid == test_frame.tid
assert rx_frame.tdest == test_frame.tdest assert rx_frame.tdest == test_frame.tdest
assert not rx_frame.tuser assert not rx_frame.tuser
await tb.monitor.wait() mon_rx_frame = await tb.monitor.recv()
rx_frame = tb.monitor.recv()
assert rx_frame.tdata == test_frame.tdata assert mon_rx_frame.tdata == test_frame.tdata
assert rx_frame.tid == test_frame.tid assert mon_rx_frame.tid == test_frame.tid
assert rx_frame.tdest == test_frame.tdest assert mon_rx_frame.tdest == test_frame.tdest
assert not rx_frame.tuser 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.sink.empty()
assert tb.monitor.empty() assert tb.monitor.empty()
@@ -125,7 +126,7 @@ def cycle_pause():
def size_list(): def size_list():
data_width = int(os.getenv("PARAM_DATA_WIDTH")) data_width = len(cocotb.top.axis_tdata)
byte_width = data_width // 8 byte_width = data_width // 8
return list(range(1, byte_width*4+1)) + [512] + [1]*64 return list(range(1, byte_width*4+1)) + [512] + [1]*64
@@ -147,7 +148,6 @@ if cocotb.SIM_NAME:
# cocotb-test # cocotb-test
tests_dir = os.path.dirname(__file__) 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]) @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()} extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
sim_build = os.path.join(tests_dir, sim_build = os.path.join(tests_dir, "sim_build",
"sim_build_"+request.node.name.replace('[', '-').replace(']', '')) request.node.name.replace('[', '-').replace(']', ''))
cocotb_test.simulator.run( cocotb_test.simulator.run(
python_search=[tests_dir], python_search=[tests_dir],

View File

@@ -0,0 +1,44 @@
"""
Copyright (c) 2021 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]