Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1636003e2c | |||
| bebafc3f73 | |||
|
|
c6872e6951 | ||
|
|
e9232d6bc9 | ||
|
|
71be7f50d5 | ||
|
|
7077200912 | ||
|
|
2e356c3fc5 | ||
|
|
c25e2e6b23 | ||
|
|
bdebdd4897 | ||
|
|
3f7a0ad919 | ||
|
|
b0c3671ca0 | ||
|
|
73df05c706 | ||
|
|
bc8dbb039c | ||
|
|
afb7d2bd10 | ||
|
|
3b75a9a1a7 | ||
|
|
7d8d214b57 | ||
|
|
c44f928bea | ||
|
|
357dd26aae | ||
|
|
6131079494 | ||
|
|
9079ca34f2 | ||
|
|
08fd179dac | ||
|
|
1fa122e4ee | ||
|
|
4b13e61dc8 | ||
|
|
72f7290488 | ||
|
|
6d1afe0904 | ||
|
|
d61362e79b | ||
|
|
2bfe8a0e50 | ||
|
|
9c88b0440e | ||
|
|
37b23c358b | ||
|
|
dd35d734f9 | ||
|
|
45ee1193cb | ||
|
|
5caafbb9e7 | ||
|
|
2d4450e048 | ||
|
|
c5d28182c4 | ||
|
|
79991205b5 | ||
|
|
a22123649c | ||
|
|
b5ba332ecc | ||
|
|
eb62e43fd1 | ||
|
|
5c4ef258ac | ||
|
|
6c5845fad3 | ||
|
|
32f6e449c0 | ||
|
|
2af7852006 | ||
|
|
3325568406 | ||
|
|
6a35c31b4b | ||
|
|
73fe54705f | ||
|
|
448451e274 | ||
|
|
21c2c05c57 | ||
|
|
ab84a3b100 | ||
|
|
0f3060b9ba | ||
|
|
2ee51890f5 | ||
|
|
fbdf4149b3 | ||
|
|
03156ff759 | ||
|
|
8956de42b5 | ||
|
|
de18e62024 | ||
|
|
14738c1dae | ||
|
|
3d43812c7b | ||
|
|
008d903bb9 | ||
|
|
0bd66da868 | ||
|
|
cdb4474cd2 | ||
|
|
ce55e93289 | ||
|
|
8465fe66aa | ||
|
|
11843359f0 | ||
|
|
a54121b503 | ||
|
|
4a23d8a69c | ||
|
|
40e3bd59ba | ||
|
|
030e088b25 | ||
|
|
295db437f7 | ||
|
|
a34a1cd125 | ||
|
|
5c6510faea | ||
|
|
f52f6dbe33 | ||
|
|
63e6eafc07 | ||
|
|
f4054cfd65 | ||
|
|
448815b16d | ||
|
|
78bc288812 | ||
|
|
2b030f120d | ||
|
|
facd770568 | ||
|
|
faad752b9a | ||
|
|
5855644670 | ||
|
|
a81d43216e | ||
|
|
50a60b649e | ||
|
|
bf19679007 | ||
|
|
6835e921f8 | ||
|
|
1b180a1d64 | ||
|
|
63b60341ed | ||
|
|
f92bbaaa70 | ||
|
|
a73c0b734e | ||
|
|
aa97848450 | ||
|
|
1bd01ae879 | ||
|
|
cfbc80c0cb | ||
|
|
1d5688778a | ||
|
|
71d7c7e9d2 | ||
|
|
30bc6f68a1 | ||
|
|
16eaea6967 | ||
|
|
a1b88dfbf1 | ||
|
|
15ff9f9907 | ||
|
|
cda8910ccf | ||
|
|
7404c5cf5f | ||
|
|
2e0502fc6e | ||
|
|
5eeffc0c68 | ||
|
|
a3df4ed1f9 | ||
|
|
318f48785a | ||
|
|
4b768e267d | ||
|
|
a8a1bbde30 | ||
|
|
e7a3850dd2 | ||
|
|
d97208d3c8 | ||
|
|
0456e4af60 | ||
|
|
c550a7315e | ||
|
|
e858721dcc | ||
|
|
19306c9b55 | ||
|
|
4dd82f1499 | ||
|
|
c3207d68dc | ||
|
|
7a5473ab39 | ||
|
|
af8fda0250 | ||
|
|
58b22a2bd4 | ||
|
|
fb553347d7 |
63
.github/workflows/build.yaml
vendored
Normal file
63
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 'dev/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build distributions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install build
|
||||
|
||||
- name: Build sdist
|
||||
run: python -m build
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: |
|
||||
dist/*.tar.gz
|
||||
dist/*.whl
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
deploy:
|
||||
needs:
|
||||
- build
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
# Only publish when a Gitea Release is created.
|
||||
if: gitea.event_name == 'release'
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
|
||||
- run: python3 -m pip install twine --user --break-system-packages
|
||||
- run: python3 -m pip install -U packaging --user --break-system-packages
|
||||
- run: TWINE_PASSWORD=${{ secrets.PYPI_PAT }} TWINE_USERNAME=bslathi19 python -m twine upload --repository-url ${{ vars.CI_API_URL }} dist/*
|
||||
8
.github/workflows/regression-tests.yml
vendored
8
.github/workflows/regression-tests.yml
vendored
@@ -5,17 +5,17 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
name: Python ${{matrix.python-version}}
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
534
README.md
534
README.md
@@ -1,8 +1,9 @@
|
||||
# Ethernet interface modules for Cocotb
|
||||
|
||||
[](https://github.com/alexforencich/cocotbext-eth/actions/)
|
||||
[](https://github.com/alexforencich/cocotbext-eth/actions/workflows/regression-tests.yml)
|
||||
[](https://codecov.io/gh/alexforencich/cocotbext-eth)
|
||||
[](https://pypi.org/project/cocotbext-eth)
|
||||
[](https://pepy.tech/project/cocotbext-eth)
|
||||
|
||||
GitHub repository: https://github.com/alexforencich/cocotbext-eth
|
||||
|
||||
@@ -10,6 +11,8 @@ GitHub repository: https://github.com/alexforencich/cocotbext-eth
|
||||
|
||||
Ethernet interface models for [cocotb](https://github.com/cocotb/cocotb).
|
||||
|
||||
Includes PHY-attach interface models for MII, GMII, RGMII, and XGMII; PHY chip interface models for MII, GMII, and RGMII; PTP clock simulation models; and a generic Ethernet MAC model that supports rate enforcement and PTP timestamping.
|
||||
|
||||
## Installation
|
||||
|
||||
Installation from pip (release version, stable):
|
||||
@@ -27,11 +30,11 @@ Installation for active development:
|
||||
|
||||
## Documentation and usage examples
|
||||
|
||||
See the `tests` directory and [verilog-ethernet](https://github.com/alexforencich/verilog-ethernet) for complete testbenches using these modules.
|
||||
See the `tests` directory, [taxi](https://github.com/fpganinja/taxi), [verilog-ethernet](https://github.com/alexforencich/verilog-ethernet), and [corundum](https://github.com/corundum/corundum) for complete testbenches using these modules.
|
||||
|
||||
### GMII
|
||||
|
||||
The `GmiiSource` and `GmiiSink` classes can be used to drive, receive, and monitor GMII traffic. The `GmiiSource` drives GMII traffic into a design. The `GmiiSink` receives GMII traffic, including monitoring internal interfaces.
|
||||
The `GmiiSource` and `GmiiSink` classes can be used to drive, receive, and monitor GMII traffic. The `GmiiSource` drives GMII traffic into a design. The `GmiiSink` receives GMII traffic, including monitoring internal interfaces. The `GmiiPhy` class is a wrapper around `GmiiSource` and `GmiiSink` that also provides clocking and rate-switching to emulate a GMII PHY chip.
|
||||
|
||||
To use these modules, import the one you need and connect it to the DUT:
|
||||
|
||||
@@ -40,15 +43,34 @@ To use these modules, import the one you need and connect it to the DUT:
|
||||
gmii_source = GmiiSource(dut.rxd, dut.rx_er, dut.rx_en, dut.clk, dut.rst)
|
||||
gmii_sink = GmiiSink(dut.txd, dut.tx_er, dut.tx_en, dut.clk, dut.rst)
|
||||
|
||||
To send data into a design with an `GmiiSource`, call `send()`. Accepted data types are iterables that can be converted to bytearray or `GmiiFrame` objects. Call `wait()` to wait for the transmit operation to complete. Example:
|
||||
To send data into a design with a `GmiiSource`, call `send()` or `send_nowait()`. Accepted data types are iterables that can be converted to bytearray or `GmiiFrame` objects. Optionally, call `wait()` to wait for the transmit operation to complete. Example:
|
||||
|
||||
gmii_source.send(GmiiFrame.from_payload(b'test data'))
|
||||
await gmii_source.send(GmiiFrame.from_payload(b'test data'))
|
||||
# wait for operation to complete (optional)
|
||||
await gmii_source.wait()
|
||||
|
||||
To receive data with a `GmiiSink`, call `recv()`. 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 `GmiiFrame` object, and then awaiting the event. The frame, with simulation time fields set, will be returned in the event data. Example:
|
||||
|
||||
await gmii_sink.wait()
|
||||
data = gmii_sink.recv()
|
||||
frame = GmiiFrame.from_payload(b'test data', tx_complete=Event())
|
||||
await gmii_source.send(frame)
|
||||
await frame.tx_complete.wait()
|
||||
print(frame.tx_complete.data.sim_time_sfd)
|
||||
|
||||
To receive data with a `GmiiSink`, call `recv()` or `recv_nowait()`. Optionally call `wait()` to wait for new receive data.
|
||||
|
||||
data = await gmii_sink.recv()
|
||||
|
||||
The `GmiiPhy` class provides a model of a GMII PHY chip. It wraps instances of `GmiiSource` (`rx`) and `GmiiSink` (`tx`), provides the necessary clocking components, and provides the `set_speed()` method to change the link speed. `set_speed()` changes the `tx_clk` and `rx_clk` frequencies, switches between `gtx_clk` and `tx_clk`, and selects the appropriate mode (MII or GMII) on the source and sink instances. In general, the `GmiiPhy` class is intended to be used for integration tests where the design expects to be directly connected to an external GMII PHY chip and contains all of the necessary IO and clocking logic. Example:
|
||||
|
||||
from cocotbext.eth import GmiiFrame, GmiiPhy
|
||||
|
||||
gmii_phy = GmiiPhy(dut.txd, dut.tx_er, dut.tx_en, dut.tx_clk, dut.gtx_clk,
|
||||
dut.rxd, dut.rx_er, dut.rx_en, dut.rx_clk, dut.rst, speed=1000e6)
|
||||
|
||||
gmii_phy.set_speed(100e6)
|
||||
|
||||
await gmii_phy.rx.send(GmiiFrame.from_payload(b'test RX data'))
|
||||
tx_data = await gmii_phy.tx.recv()
|
||||
|
||||
#### Signals
|
||||
|
||||
@@ -65,22 +87,44 @@ To receive data with a `GmiiSink`, call `recv()`. Call `wait()` to wait for new
|
||||
* _reset_: reset signal (optional)
|
||||
* _enable_: clock enable (optional)
|
||||
* _mii_select_: MII mode select (optional)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _queue_occupancy_bytes_: number of bytes in queue
|
||||
* _queue_occupancy_frames_: number of frames in queue
|
||||
* _queue_occupancy_limit_bytes_: max number of bytes in queue allowed before backpressure is applied (source only)
|
||||
* _queue_occupancy_limit_frames_: max number of frames in queue allowed before backpressure is applied (source only)
|
||||
* _mii_mode_: control MII mode when _mii_select_ signal is not connected
|
||||
|
||||
#### Methods
|
||||
|
||||
* `send(frame)`: send _frame_ (source)
|
||||
* `recv()`: receive a frame as a `GmiiFrame` (sink)
|
||||
* `send(frame)`: send _frame_ (blocking) (source)
|
||||
* `send_nowait(frame)`: send _frame_ (non-blocking) (source)
|
||||
* `recv()`: receive a frame as a `GmiiFrame` (blocking) (sink)
|
||||
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
||||
* `count()`: returns the number of items in the queue (all)
|
||||
* `empty()`: returns _True_ if the queue is empty (all)
|
||||
* `full()`: returns _True_ if the queue occupancy limits are met (source)
|
||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||
* `clear()`: drop all data in queue (all)
|
||||
* `wait()`: wait for idle (source)
|
||||
* `wait(timeout=0, timeout_unit='ns')`: wait for frame received (sink)
|
||||
|
||||
#### GMII timing diagram
|
||||
|
||||
Example transfer via GMII at 1 Gbps:
|
||||
|
||||
__ __ __ __ _ __ __ __ __
|
||||
tx_clk __/ \__/ \__/ \__/ \__/ ... _/ \__/ \__/ \__/ \__
|
||||
_____ _____ _____ _ _ _____ _____
|
||||
tx_d[7:0] XXXXXXXXX_55__X_55__X_55__X_ ... _X_72__X_fb__XXXXXXXXXXXX
|
||||
|
||||
tx_er ____________________________ ... _________________________
|
||||
___________________ _____________
|
||||
tx_en ________/ ... \___________
|
||||
|
||||
|
||||
#### GmiiFrame object
|
||||
|
||||
The `GmiiFrame` object is a container for a frame to be transferred via GMII. The `data` field contains the packet data in the form of a list of bytes. `error` contains the `er` signal level state associated with each byte as a list of ints.
|
||||
@@ -89,7 +133,11 @@ Attributes:
|
||||
|
||||
* `data`: bytearray
|
||||
* `error`: error field, optional; list, each entry qualifies the corresponding entry in `data`.
|
||||
* `rx_sim_time`: simulation time when packet was received by sink.
|
||||
* `sim_time_start`: simulation time of first transfer cycle of frame.
|
||||
* `sim_time_sfd`: simulation time at which the SFD was transferred.
|
||||
* `sim_time_end`: simulation time of last transfer cycle of frame.
|
||||
* `start_lane`: byte lane in which the start control character was transferred.
|
||||
* `tx_complete`: event or callable triggered when frame is transmitted.
|
||||
|
||||
Methods:
|
||||
|
||||
@@ -103,9 +151,102 @@ Methods:
|
||||
* `normalize()`: pack `error` to the same length as `data`, replicating last element if necessary, initialize to list of `0` if not specified.
|
||||
* `compact()`: remove `error` if all zero
|
||||
|
||||
### MII
|
||||
|
||||
The `MiiSource` and `MiiSink` classes can be used to drive, receive, and monitor MII traffic. The `MiiSource` drives MII traffic into a design. The `MiiSink` receives MII traffic, including monitoring internal interfaces. The `MiiPhy` class is a wrapper around `MiiSource` and `MiiSink` that also provides clocking and rate-switching to emulate an MII PHY chip.
|
||||
|
||||
To use these modules, import the one you need and connect it to the DUT:
|
||||
|
||||
from cocotbext.eth import MiiSource, MiiSink
|
||||
|
||||
mii_source = MiiSource(dut.rxd, dut.rx_er, dut.rx_en, dut.clk, dut.rst)
|
||||
mii_sink = MiiSink(dut.txd, dut.tx_er, dut.tx_en, dut.clk, dut.rst)
|
||||
|
||||
All signals must be passed separately into these classes.
|
||||
|
||||
To send data into a design with an `MiiSource`, call `send()` or `send_nowait()`. Accepted data types are iterables that can be converted to bytearray or `GmiiFrame` objects. Optionally, call `wait()` to wait for the transmit operation to complete. Example:
|
||||
|
||||
await mii_source.send(GmiiFrame.from_payload(b'test data'))
|
||||
# wait for operation to complete (optional)
|
||||
await mii_source.wait()
|
||||
|
||||
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 `GmiiFrame` object, and then awaiting the event. The frame, with simulation time fields set, will be returned in the event data. Example:
|
||||
|
||||
frame = GmiiFrame.from_payload(b'test data', tx_complete=Event())
|
||||
await mii_source.send(frame)
|
||||
await frame.tx_complete.wait()
|
||||
print(frame.tx_complete.data.sim_time_sfd)
|
||||
|
||||
To receive data with an `MiiSink`, call `recv()` or `recv_nowait()`. Optionally call `wait()` to wait for new receive data.
|
||||
|
||||
data = await mii_sink.recv()
|
||||
|
||||
The `MiiPhy` class provides a model of an MII PHY chip. It wraps instances of `MiiSource` (`rx`) and `MiiSink` (`tx`), provides the necessary clocking components, and provides the `set_speed()` method to change the link speed. `set_speed()` changes the `tx_clk` and `rx_clk` frequencies. In general, the `MiiPhy` class is intended to be used for integration tests where the design expects to be directly connected to an external MII PHY chip and contains all of the necessary IO and clocking logic. Example:
|
||||
|
||||
from cocotbext.eth import GmiiFrame, MiiPhy
|
||||
|
||||
mii_phy = MiiPhy(dut.txd, dut.tx_er, dut.tx_en, dut.tx_clk,
|
||||
dut.rxd, dut.rx_er, dut.rx_en, dut.rx_clk, dut.rst, speed=100e6)
|
||||
|
||||
mii_phy.set_speed(10e6)
|
||||
|
||||
await mii_phy.rx.send(GmiiFrame.from_payload(b'test RX data'))
|
||||
tx_data = await mii_phy.tx.recv()
|
||||
|
||||
#### Signals
|
||||
|
||||
* `txd`, `rxd`: data
|
||||
* `tx_er`, `rx_er`: error (when asserted with `tx_en` or `rx_dv`)
|
||||
* `tx_en`, `rx_dv`: data valid
|
||||
|
||||
#### Constructor parameters:
|
||||
|
||||
* _data_: data signal (txd, rxd, etc.)
|
||||
* _er_: error signal (tx_er, rx_er, etc.) (optional)
|
||||
* _dv_: data valid signal (tx_en, rx_dv, etc.)
|
||||
* _clock_: clock signal
|
||||
* _reset_: reset signal (optional)
|
||||
* _enable_: clock enable (optional)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _queue_occupancy_bytes_: number of bytes in queue
|
||||
* _queue_occupancy_frames_: number of frames in queue
|
||||
* _queue_occupancy_limit_bytes_: max number of bytes in queue allowed before backpressure is applied (source only)
|
||||
* _queue_occupancy_limit_frames_: max number of frames in queue allowed before backpressure is applied (source only)
|
||||
|
||||
#### Methods
|
||||
|
||||
* `send(frame)`: send _frame_ (blocking) (source)
|
||||
* `send_nowait(frame)`: send _frame_ (non-blocking) (source)
|
||||
* `recv()`: receive a frame as a `GmiiFrame` (blocking) (sink)
|
||||
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
||||
* `count()`: returns the number of items in the queue (all)
|
||||
* `empty()`: returns _True_ if the queue is empty (all)
|
||||
* `full()`: returns _True_ if the queue occupancy limits are met (source)
|
||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||
* `clear()`: drop all data in queue (all)
|
||||
* `wait()`: wait for idle (source)
|
||||
* `wait(timeout=0, timeout_unit='ns')`: wait for frame received (sink)
|
||||
|
||||
#### MII timing diagram
|
||||
|
||||
Example transfer via MII at 100 Mbps:
|
||||
|
||||
_ _ _ _ _ _ _ _ _ _
|
||||
tx_clk _/ \_/ \_/ \_/ \_/ \_/ ... _/ \_/ \_/ \_/ \_
|
||||
___ ___ ___ ___ _ _ ___ ___
|
||||
tx_d[3:0] XXXXXX_5_X_5_X_5_X_5_X_ ... _X_f_X_b_XXXXXXXX
|
||||
|
||||
tx_er _______________________ ... _________________
|
||||
_________________ _________
|
||||
tx_en _____/ ... \_______
|
||||
|
||||
|
||||
### RGMII
|
||||
|
||||
The `RgmiiSource` and `RgmiiSink` classes can be used to drive, receive, and monitor RGMII traffic. The `RgmiiSource` drives RGMII traffic into a design. The `RgmiiSink` receives RGMII traffic, including monitoring internal interfaces.
|
||||
The `RgmiiSource` and `RgmiiSink` classes can be used to drive, receive, and monitor RGMII traffic. The `RgmiiSource` drives RGMII traffic into a design. The `RgmiiSink` receives RGMII traffic, including monitoring internal interfaces. The `RgmiiPhy` class is a wrapper around `RgmiiSource` and `RgmiiSink` that also provides clocking and rate-switching to emulate an RGMII PHY chip.
|
||||
|
||||
To use these modules, import the one you need and connect it to the DUT:
|
||||
|
||||
@@ -116,15 +257,34 @@ To use these modules, import the one you need and connect it to the DUT:
|
||||
|
||||
All signals must be passed separately into these classes.
|
||||
|
||||
To send data into a design with an `RgmiiSource`, call `send()`. Accepted data types are iterables that can be converted to bytearray or `GmiiFrame` objects. Call `wait()` to wait for the transmit operation to complete. Example:
|
||||
To send data into a design with an `RgmiiSource`, call `send()` or `send_nowait()`. Accepted data types are iterables that can be converted to bytearray or `GmiiFrame` objects. Optionally, call `wait()` to wait for the transmit operation to complete. Example:
|
||||
|
||||
rgmii_source.send(GmiiFrame.from_payload(b'test data'))
|
||||
await rgmii_source.send(GmiiFrame.from_payload(b'test data'))
|
||||
# wait for operation to complete (optional)
|
||||
await rgmii_source.wait()
|
||||
|
||||
To receive data with an `RgmiiSink`, call `recv()`. 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 `GmiiFrame` object, and then awaiting the event. The frame, with simulation time fields set, will be returned in the event data. Example:
|
||||
|
||||
await rgmii_sink.wait()
|
||||
data = rgmii_sink.recv()
|
||||
frame = GmiiFrame.from_payload(b'test data', tx_complete=Event())
|
||||
await rgmii_source.send(frame)
|
||||
await frame.tx_complete.wait()
|
||||
print(frame.tx_complete.data.sim_time_sfd)
|
||||
|
||||
To receive data with an `RgmiiSink`, call `recv()` or `recv_nowait()`. Optionally call `wait()` to wait for new receive data.
|
||||
|
||||
data = await rgmii_sink.recv()
|
||||
|
||||
The `RgmiiPhy` class provides a model of an RGMII PHY chip. It wraps instances of `RgmiiSource` (`rx`) and `RgmiiSink` (`tx`), provides the necessary clocking components, and provides the `set_speed()` method to change the link speed. `set_speed()` changes the `rx_clk` frequency and selects the appropriate mode (SDR or DDR) on the source and sink instances. In general, the `RgmiiPhy` class is intended to be used for integration tests where the design expects to be directly connected to an external RGMII PHY chip and contains all of the necessary IO and clocking logic. Example:
|
||||
|
||||
from cocotbext.eth import GmiiFrame, RgmiiPhy
|
||||
|
||||
rgmii_phy = RgmiiPhy(dut.txd, dut.tx_ctl, dut.tx_clk,
|
||||
dut.rxd, dut.rx_ctl, dut.rx_clk, dut.rst, speed=1000e6)
|
||||
|
||||
rgmii_phy.set_speed(100e6)
|
||||
|
||||
await rgmii_phy.rx.send(GmiiFrame.from_payload(b'test RX data'))
|
||||
tx_data = await rgmii_phy.tx.recv()
|
||||
|
||||
#### Signals
|
||||
|
||||
@@ -139,22 +299,42 @@ To receive data with an `RgmiiSink`, call `recv()`. Call `wait()` to wait for n
|
||||
* _reset_: reset signal (optional)
|
||||
* _enable_: clock enable (optional)
|
||||
* _mii_select_: MII mode select (optional)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _queue_occupancy_bytes_: number of bytes in queue
|
||||
* _queue_occupancy_frames_: number of frames in queue
|
||||
* _queue_occupancy_limit_bytes_: max number of bytes in queue allowed before backpressure is applied (source only)
|
||||
* _queue_occupancy_limit_frames_: max number of frames in queue allowed before backpressure is applied (source only)
|
||||
* _mii_mode_: control MII mode when _mii_select_ signal is not connected
|
||||
|
||||
#### Methods
|
||||
|
||||
* `send(frame)`: send _frame_ (source)
|
||||
* `recv()`: receive a frame as a `GmiiFrame` (sink)
|
||||
* `send(frame)`: send _frame_ (blocking) (source)
|
||||
* `send_nowait(frame)`: send _frame_ (non-blocking) (source)
|
||||
* `recv()`: receive a frame as a `GmiiFrame` (blocking) (sink)
|
||||
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
||||
* `count()`: returns the number of items in the queue (all)
|
||||
* `empty()`: returns _True_ if the queue is empty (all)
|
||||
* `full()`: returns _True_ if the queue occupancy limits are met (source)
|
||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||
* `clear()`: drop all data in queue (all)
|
||||
* `wait()`: wait for idle (source)
|
||||
* `wait(timeout=0, timeout_unit='ns')`: wait for frame received (sink)
|
||||
|
||||
#### RGMII timing diagram
|
||||
|
||||
Example transfer via RGMII at 1 Gbps:
|
||||
|
||||
___ ___ ___ _ ___ ___
|
||||
tx_clk _/ \___/ \___/ \___/ ... _/ \___/ \___
|
||||
___ ___ ___ ___ ___ ___ ___
|
||||
tx_d[3:0] XXXXXXXX_5_X_5_X_5_X_5_X_5_ ... _f_X_b_XXXXXXXXXX
|
||||
___________________ _______
|
||||
tx_ctl _______/ ... \_________
|
||||
|
||||
|
||||
### XGMII
|
||||
|
||||
The `XgmiiSource` and `XgmiiSink` classes can be used to drive, receive, and monitor XGMII traffic. The `XgmiiSource` drives XGMII traffic into a design. The `XgmiiSink` receives XGMII traffic, including monitoring internal interfaces. The modules are capable of operating with XGMII interface widths of 32 or 64 bits.
|
||||
@@ -168,15 +348,22 @@ To use these modules, import the one you need and connect it to the DUT:
|
||||
|
||||
All signals must be passed separately into these classes.
|
||||
|
||||
To send data into a design with an `XgmiiSource`, call `send()`. Accepted data types are iterables that can be converted to bytearray or `XgmiiFrame` objects. Call `wait()` to wait for the transmit operation to complete. Example:
|
||||
To send data into a design with an `XgmiiSource`, call `send()` or `send_nowait()`. Accepted data types are iterables that can be converted to bytearray or `XgmiiFrame` objects. Optionally, call `wait()` to wait for the transmit operation to complete. Example:
|
||||
|
||||
xgmii_source.send(XgmiiFrame.from_payload(b'test data'))
|
||||
await xgmii_source.send(XgmiiFrame.from_payload(b'test data'))
|
||||
# wait for operation to complete (optional)
|
||||
await xgmii_source.wait()
|
||||
|
||||
To receive data with an `XgmiiSink`, call `recv()`. 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 `XgmiiFrame` object, and then awaiting the event. The frame, with simulation time fields set, will be returned in the event data. Example:
|
||||
|
||||
await xgmii_sink.wait()
|
||||
data = xgmii_sink.recv()
|
||||
frame = XgmiiFrame.from_payload(b'test data', tx_complete=Event())
|
||||
await xgmii_source.send(frame)
|
||||
await frame.tx_complete.wait()
|
||||
print(frame.tx_complete.data.sim_time_sfd)
|
||||
|
||||
To receive data with an `XgmiiSink`, call `recv()` or `recv_nowait()`. Optionally call `wait()` to wait for new receive data.
|
||||
|
||||
data = await xgmii_sink.recv()
|
||||
|
||||
#### Signals
|
||||
|
||||
@@ -190,22 +377,55 @@ To receive data with an `XgmiiSink`, call `recv()`. Call `wait()` to wait for n
|
||||
* _clock_: clock signal
|
||||
* _reset_: reset signal (optional)
|
||||
* _enable_: clock enable (optional)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _queue_occupancy_bytes_: number of bytes in queue
|
||||
* _queue_occupancy_frames_: number of frames in queue
|
||||
* _queue_occupancy_limit_bytes_: max number of bytes in queue allowed before backpressure is applied (source only)
|
||||
* _queue_occupancy_limit_frames_: max number of frames in queue allowed before backpressure is applied (source only)
|
||||
|
||||
#### Methods
|
||||
|
||||
* `send(frame)`: send _frame_ (source)
|
||||
* `recv()`: receive a frame as an `XgmiiFrame` (sink)
|
||||
* `send(frame)`: send _frame_ (blocking) (source)
|
||||
* `send_nowait(frame)`: send _frame_ (non-blocking) (source)
|
||||
* `recv()`: receive a frame as an `XgmiiFrame` (blocking) (sink)
|
||||
* `recv_nowait()`: receive a frame as an `XgmiiFrame` (non-blocking) (sink)
|
||||
* `count()`: returns the number of items in the queue (all)
|
||||
* `empty()`: returns _True_ if the queue is empty (all)
|
||||
* `full()`: returns _True_ if the queue occupancy limits are met (source)
|
||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||
* `clear()`: drop all data in queue (all)
|
||||
* `wait()`: wait for idle (source)
|
||||
* `wait(timeout=0, timeout_unit='ns')`: wait for frame received (sink)
|
||||
|
||||
#### XGMII timing diagram
|
||||
|
||||
Example transfer via 64-bit XGMII:
|
||||
|
||||
__ __ __ __ __ _ __ __
|
||||
tx_clk __/ \__/ \__/ \__/ \__/ \__/ ... _/ \__/ \__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txd[63:56] __X_07__X_d5__X_51__X_01__X_09__X_ ... _X_fb__X_07__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txd[55:48] __X_07__X_55__X_5a__X_00__X_08__X_ ... _X_72__X_07__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txd[47:40] __X_07__X_55__X_d5__X_00__X_07__X_ ... _X_0d__X_07__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txd[39:32] __X_07__X_55__X_d4__X_80__X_06__X_ ... _X_37__X_07__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txd[31:24] __X_07__X_55__X_d3__X_55__X_05__X_ ... _X_2d__X_07__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txd[23:16] __X_07__X_55__X_d2__X_54__X_04__X_ ... _X_2c__X_07__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txd[15:8] __X_07__X_55__X_d1__X_53__X_03__X_ ... _X_2b__X_07__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txd[7:0] __X_07__X_fb__X_da__X_52__X_02__X_ ... _X_2a__X_fd__
|
||||
__ _____ _____ _____ _____ _____ _ _ _____ _____
|
||||
txc[7:0] __X_ff__X_01__X_00__X_00__X_00__X_ ... _X_00__X_ff__
|
||||
|
||||
|
||||
#### XgmiiFrame object
|
||||
|
||||
The `XgmiiFrame` object is a container for a frame to be transferred via XGMII. The `data` field contains the packet data in the form of a list of bytes. `ctrl` contains the control signal level state associated with each byte as a list of ints. When `ctrl` is high, the corresponding `data` byte is interpreted as an XGMII control character.
|
||||
@@ -214,8 +434,11 @@ Attributes:
|
||||
|
||||
* `data`: bytearray
|
||||
* `ctrl`: control field, optional; list, each entry qualifies the corresponding entry in `data` as an XGMII control character.
|
||||
* `rx_sim_time`: simulation time when packet was received by sink.
|
||||
* `rx_start_lane`: byte lane that the frame start control character was received in.
|
||||
* `sim_time_start`: simulation time of first transfer cycle of frame.
|
||||
* `sim_time_sfd`: simulation time at which the SFD was transferred.
|
||||
* `sim_time_end`: simulation time of last transfer cycle of frame.
|
||||
* `start_lane`: byte lane in which the start control character was transferred.
|
||||
* `tx_complete`: event or callable triggered when frame is transmitted.
|
||||
|
||||
Methods:
|
||||
|
||||
@@ -226,20 +449,151 @@ Methods:
|
||||
* `get_payload(strip_fcs=True)`: return payload, optionally strip FCS
|
||||
* `get_fcs()`: return FCS
|
||||
* `check_fcs()`: returns _True_ if FCS is correct
|
||||
* `normalize()`: pack `error` to the same length as `data`, replicating last element if necessary, initialize to list of `0` if not specified.
|
||||
* `compact()`: remove `error` if all zero
|
||||
* `normalize()`: pack `ctrl` to the same length as `data`, replicating last element if necessary, initialize to list of `0` if not specified.
|
||||
* `compact()`: remove `ctrl` if all zero
|
||||
|
||||
### Ethernet MAC model
|
||||
|
||||
The `EthMac`, `EthMacTx` and `EthMacRx` modules are models of an Ethernet MAC with an AXI stream interface. The `EthMacRx` module drives Ethernet frames in the form of AXI stream traffic into a design. The `EthMacTx` module accepts Ethernet frames in the form of AXI stream traffic from a design. `EthMac` is a wrapper module containing `EthMacRx` (`rx`) and `EthMacTx` (`tx`). The modules are capable of operating with any interface width. The MAC models enforce the correct data rates and timings in both the receive and transmit direction, and can also collect PTP timestamps from a PTP hardware clock.
|
||||
|
||||
To use these modules, import the one you need and connect it to the DUT:
|
||||
|
||||
from cocotbext.axi import AxiStreamBus
|
||||
from cocotbext.eth import EthMac
|
||||
|
||||
mac = EthMac(
|
||||
tx_clk=dut.tx_clk,
|
||||
tx_rst=dut.tx_rst,
|
||||
tx_bus=AxiStreamBus.from_prefix(dut, "tx_axis"),
|
||||
tx_ptp_time=dut.tx_ptp_time,
|
||||
tx_ptp_ts=dut.tx_ptp_ts,
|
||||
tx_ptp_ts_tag=dut.tx_ptp_ts_tag,
|
||||
tx_ptp_ts_valid=dut.tx_ptp_ts_valid,
|
||||
rx_clk=dut.rx_clk,
|
||||
rx_rst=dut.rx_rst,
|
||||
rx_bus=AxiStreamBus.from_prefix(dut, "rx_axis"),
|
||||
rx_ptp_time=dut.rx_ptp_time,
|
||||
ifg=12, speed=speed
|
||||
)
|
||||
|
||||
To send data into a design, call `send()` or `send_nowait()`. Accepted data types are iterables that can be converted to bytearray or `EthMacFrame` objects. Optionally, call `wait()` to wait for the transmit operation to complete. Example:
|
||||
|
||||
await mac.tx.send(EthMacFrame.from_payload(b'test data'))
|
||||
# wait for operation to complete (optional)
|
||||
await mac.tx.wait()
|
||||
|
||||
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 `EthMacFrame` object, and then awaiting the event. The frame, with simulation time fields set, will be returned in the event data. Example:
|
||||
|
||||
frame = EthMacFrame.from_payload(b'test data', tx_complete=Event())
|
||||
await mac.tx.send(frame)
|
||||
await frame.tx_complete.wait()
|
||||
print(frame.tx_complete.data.sim_time_sfd)
|
||||
|
||||
To receive data, call `recv()` or `recv_nowait()`. Optionally call `wait()` to wait for new receive data.
|
||||
|
||||
data = await mac.tx.recv()
|
||||
|
||||
PTP timestamping requires free-running PTP clocks driving the PTP time inputs, synchronous with the corresponding MAC clocks. The values of these fields are then captured when the frame SFD is transferred and returned either on tuser (for received frames) or on a separate streaming interface (for transmitted frames). Additionally, on the transmit path, a tag value from tuser is returned along with the timestamp.
|
||||
|
||||
#### Signals
|
||||
|
||||
* `tdata`: payload data, must be a multiple of 8 bits
|
||||
* `tvalid`: qualifies all other signals
|
||||
* `tready`: indicates sink is ready for data (tx only)
|
||||
* `tlast`: marks the last cycle of a frame
|
||||
* `tkeep`: qualifies data byte, data bus width must be evenly divisible by `tkeep` signal width
|
||||
* `tuser`: user data, carries frame error mark and captured receive PTP timestamp (RX) or PTP timestamp tag (TX)
|
||||
* `ptp_time`: PTP time input from PHC, captured into `ptp_timestamp` field coincident with transfer of frame SFD and output on `ptp_ts` (TX) or `tuser` (RX)
|
||||
* `ptp_ts`: captured transmit PTP timestamp
|
||||
* `ptp_ts_tag`: captured transmit PTP timestamp tag
|
||||
* `ptp_ts_valid`: qualifies captured transmit PTP timestamp
|
||||
|
||||
#### Constructor parameters (`EthMacRx` and `EthMacTx`):
|
||||
|
||||
* _bus_: `AxiStreamBus` object containing AXI stream interface signals
|
||||
* _clock_: clock signal
|
||||
* _reset_: reset signal (optional)
|
||||
* _ptp_time_: PTP time input from PHC (optional)
|
||||
* _ptp_ts_: PTP timestamp (optional) (tx)
|
||||
* _ptp_ts_tag_: PTP timestamp tag (optional) (tx)
|
||||
* _ptp_ts_valid_: PTP timestamp valid (optional) (tx)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
* _ifg_: IFG size in byte times (optional, default `12`)
|
||||
* _speed_: link speed in bits per second (optional, default `1000e6`)
|
||||
|
||||
#### Constructor parameters (`EthMac`):
|
||||
|
||||
* _tx_bus_: `AxiStreamBus` object containing transmit AXI stream interface signals
|
||||
* _tx_clk_: transmit clock
|
||||
* _tx_rst_: transmit reset (optional)
|
||||
* _tx_ptp_time_: transmit PTP time input from PHC (optional)
|
||||
* _tx_ptp_ts_: transmit PTP timestamp (optional)
|
||||
* _tx_ptp_ts_tag_: transmit PTP timestamp tag (optional)
|
||||
* _tx_ptp_ts_valid_: transmit PTP timestamp valid (optional)
|
||||
* _rx_bus_: `AxiStreamBus` object containing receive AXI stream interface signals
|
||||
* _rx_clk_: receive clock
|
||||
* _rx_rst_: receive reset (optional)
|
||||
* _rx_ptp_time_: receive PTP time input from PHC (optional)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
* _ifg_: IFG size in byte times (optional, default `12`)
|
||||
* _speed_: link speed in bits per second (optional, default `1000e6`)
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _queue_occupancy_bytes_: number of bytes in queue
|
||||
* _queue_occupancy_frames_: number of frames in queue
|
||||
* _queue_occupancy_limit_bytes_: max number of bytes in queue allowed before backpressure is applied (RX only)
|
||||
* _queue_occupancy_limit_frames_: max number of frames in queue allowed before backpressure is applied (RX only)
|
||||
* _ifg_: IFG size in byte times
|
||||
* _speed_: link speed in bits per second
|
||||
|
||||
#### Methods
|
||||
|
||||
* `send(frame)`: send _frame_ (blocking) (rx)
|
||||
* `send_nowait(frame)`: send _frame_ (non-blocking) (rx)
|
||||
* `recv()`: receive a frame as an `EthMacFrame` (blocking) (tx)
|
||||
* `recv_nowait()`: receive a frame as an `EthMacFrame` (non-blocking) (tx)
|
||||
* `count()`: returns the number of items in the queue (all)
|
||||
* `empty()`: returns _True_ if the queue is empty (all)
|
||||
* `full()`: returns _True_ if the queue occupancy limits are met (rx)
|
||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (rx)
|
||||
* `clear()`: drop all data in queue (all)
|
||||
* `wait()`: wait for idle (rx)
|
||||
* `wait(timeout=0, timeout_unit='ns')`: wait for frame received (tx)
|
||||
|
||||
#### EthMacFrame object
|
||||
|
||||
The `EthMacFrame` object is a container for a frame to be transferred via XGMII. The `data` field contains the packet data in the form of a list of bytes.
|
||||
|
||||
Attributes:
|
||||
|
||||
* `data`: bytearray
|
||||
* `sim_time_start`: simulation time of first transfer cycle of frame.
|
||||
* `sim_time_sfd`: simulation time at which the SFD was transferred.
|
||||
* `sim_time_end`: simulation time of last transfer cycle of frame.
|
||||
* `ptp_tag`: PTP timestamp tag for transmitted frames.
|
||||
* `ptp_timestamp`: captured value of `ptp_time` at frame SFD
|
||||
* `tx_complete`: event or callable triggered when frame is transmitted.
|
||||
|
||||
Methods:
|
||||
|
||||
* `from_payload(payload, min_len=60)`: create `EthMacFrame` from payload data, zero-pads frame to minimum length and computes and inserts FCS (class method)
|
||||
* `from_raw_payload(payload)`: create `EthMacFrame` from payload data (class method)
|
||||
* `get_payload(strip_fcs=True)`: return payload, optionally strip FCS
|
||||
* `get_fcs()`: return FCS
|
||||
* `check_fcs()`: returns _True_ if FCS is correct
|
||||
|
||||
### PTP clock
|
||||
|
||||
The `PtpClock` class implements a PTP hardware clock that produces IEEE 1588 format 96 and 64 bit PTP timestamps.
|
||||
The `PtpClock` class implements a PTP hardware clock that produces IEEE 1588 format 96-bit time-of-day and 64-bit relative PTP timestamps.
|
||||
|
||||
To use this module, import it and connect it to the DUT:
|
||||
|
||||
from cocotbext.eth import PtpClock
|
||||
|
||||
ptp_clock = PtpClock(
|
||||
ts_96=dut.ts_96,
|
||||
ts_64=dut.ts_64,
|
||||
ts_tod=dut.ts_tod,
|
||||
ts_rel=dut.ts_rel,
|
||||
ts_step=dut.ts_step,
|
||||
pps=dut.pps,
|
||||
clock=dut.clk,
|
||||
@@ -249,46 +603,108 @@ To use this module, import it and connect it to the DUT:
|
||||
|
||||
Once the clock is instantiated, it will generate a continuous stream of monotonically increasing PTP timestamps on every clock edge.
|
||||
|
||||
Internally, the `PtpClock` module uses 32-bit fractional ns fields for higher frequency resolution. Only the upper 16 bits are returned in the timestamps, but the full fns value can be accessed with the _ts_tod_fns_ and _ts_rel_fns_ attributes.
|
||||
|
||||
All APIs that handle fractional values use the `Decimal` type for maximum precision, as the combination of timestamp range and resolution is usually too much for normal floating point numbers to handle without significant loss of precision.
|
||||
|
||||
#### Signals
|
||||
|
||||
* `ts_96`: 96-bit timestamp (48 bit seconds, 32 bit ns, 16 bit fractional ns)
|
||||
* `ts_64`: 64-bit timestamp (48 bit ns, 16 bit fractional ns)
|
||||
* `ts_tod`: 96-bit time-of-day timestamp (48 bit seconds, 32 bit ns, 16 bit fractional ns)
|
||||
* `ts_rel`: 64-bit relative timestamp (48 bit ns, 16 bit fractional ns)
|
||||
* `ts_step`: step output, pulsed when non-monotonic step occurs
|
||||
* `pps`: pulse-per-second output, pulsed when ts_96 seconds field increments
|
||||
* `pps`: pulse-per-second output, pulsed when ts_tod seconds field increments
|
||||
|
||||
#### Constructor parameters:
|
||||
|
||||
* _ts_96_: 96-bit timestamp signal (optional)
|
||||
* _ts_64_: 64-bit timestamp signal (optional)
|
||||
* _ts_tod_: 96-bit time-of-day timestamp signal (optional)
|
||||
* _ts_rel_: 64-bit relative timestamp signal (optional)
|
||||
* _ts_step_: timestamp step signal (optional)
|
||||
* _pps_: pulse-per-second signal (optional)
|
||||
* _clock_: clock
|
||||
* _reset_: reset (optional)
|
||||
* _period_ns_: clock period (nanoseconds)
|
||||
* _reset_active_level_: reset active level (optional, default `True`)
|
||||
* _period_ns_: clock period (nanoseconds, default `6.4`)
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _ts_96_s_: current 96-bit timestamp seconds field
|
||||
* _ts_96_ns_: current 96-bit timestamp ns field
|
||||
* _ts_96_fns_: current 96-bit timestamp fractional ns field
|
||||
* _ts_64_ns_: current 64-bit timestamp ns field
|
||||
* _ts_64_fns_: current 64-bit timestamp fractional ns field
|
||||
* _ts_tod_s_: current 96-bit ToD timestamp seconds field
|
||||
* _ts_tod_ns_: current 96-bit ToD timestamp ns field
|
||||
* _ts_tod_fns_: current 96-bit ToD timestamp fractional ns field
|
||||
* _ts_rel_ns_: current 64-bit relative timestamp ns field
|
||||
* _ts_rel_fns_: current 64-bit relative timestamp fractional ns field
|
||||
|
||||
#### Methods
|
||||
|
||||
* `set_period(ns, fns)`: set clock period from separate fields
|
||||
* `set_drift(ns, fns, rate)`: set clock drift from separate fields
|
||||
* `set_period_ns(t)`: set clock period in ns (float)
|
||||
* `get_period_ns()`: return current clock period in ns (float)
|
||||
* `set_ts_96(ts_s, ts_ns=None, ts_fns=None)`: set 96-bit timestamp from integer or from separate fields
|
||||
* `set_ts_96_ns(t)`: set 96-bit timestamp from ns (float)
|
||||
* `set_ts_96_s(t)`: set 96-bit timestamp from seconds (float)
|
||||
* `get_ts_96()`: return current 96-bit timestamp as an integer
|
||||
* `get_ts_96_ns()`: return current 96-bit timestamp in ns (float)
|
||||
* `get_ts_96_s()`: return current 96-bit timestamp in seconds (float)
|
||||
* `set_ts_64(ts_ns, ts_fns=None)`: set 64-bit timestamp from integer or from separate fields
|
||||
* `set_ts_64_ns(t)`: set 64-bit timestamp from ns (float)
|
||||
* `set_ts_64_s(t)`: set 64-bit timestamp from seconds (float)
|
||||
* `get_ts_64()`: return current 64-bit timestamp as an integer
|
||||
* `get_ts_64_ns()`: return current 64-bit timestamp in ns (float)
|
||||
* `get_ts_64_s()`: return current 64-bit timestamp in seconds (float)
|
||||
* `set_drift(num, denom)`: set clock drift from separate fields
|
||||
* `set_period_ns(t)`: set clock period and drift in ns (Decimal)
|
||||
* `get_period_ns()`: return current clock period in ns (Decimal)
|
||||
* `set_ts_tod(ts_s, ts_ns, ts_fns)`: set 96-bit ToD timestamp from separate fields
|
||||
* `set_ts_tod_96(ts)`: set 96-bit ToD timestamp from integer
|
||||
* `set_ts_tod_ns(t)`: set 96-bit ToD timestamp from ns (Decimal)
|
||||
* `set_ts_tod_s(t)`: set 96-bit ToD timestamp from seconds (Decimal)
|
||||
* `set_ts_tod_sim_time()`: set 96-bit ToD timestamp from sim time
|
||||
* `get_ts_tod()`: return current 96-bit ToD timestamp as separate fields
|
||||
* `get_ts_tod_96()`: return current 96-bit ToD timestamp as an integer
|
||||
* `get_ts_tod_ns()`: return current 96-bit ToD timestamp in ns (Decimal)
|
||||
* `get_ts_tod_s()`: return current 96-bit ToD timestamp in seconds (Decimal)
|
||||
* `set_ts_rel(ts_ns, ts_fns)`: set 64-bit relative timestamp from separate fields
|
||||
* `set_ts_rel_64(ts)`: set 64-bit relative timestamp from integer
|
||||
* `set_ts_rel_ns(t)`: set 64-bit relative timestamp from ns (Decimal)
|
||||
* `set_ts_rel_s(t)`: set 64-bit relative timestamp from seconds (Decimal)
|
||||
* `set_ts_rel_sim_time()`: set 64-bit relative timestamp from sim time
|
||||
* `get_ts_rel()`: return current 64-bit relative timestamp as separate fields
|
||||
* `get_ts_rel_64()`: return current 64-bit relative timestamp as an integer
|
||||
* `get_ts_rel_ns()`: return current 64-bit relative timestamp in ns (Decimal)
|
||||
* `get_ts_rel_s()`: return current 64-bit relative timestamp in seconds (Decimal)
|
||||
|
||||
### PTP clock (sim time)
|
||||
|
||||
The `PtpClockSimTime` class implements a PTP hardware clock that produces IEEE 1588 format 96-bit time-of-day and 64-bit relative PTP timestamps, derived from the current simulation time. This module can be used in place of `PtpClock` so that captured PTP timestamps can be easily compared to captured simulation time.
|
||||
|
||||
To use this module, import it and connect it to the DUT:
|
||||
|
||||
from cocotbext.eth import PtpClockSimTime
|
||||
|
||||
ptp_clock = PtpClockSimTime(
|
||||
ts_tod=dut.ts_tod,
|
||||
ts_rel=dut.ts_rel,
|
||||
pps=dut.pps,
|
||||
clock=dut.clk
|
||||
)
|
||||
|
||||
Once the clock is instantiated, it will generate a continuous stream of monotonically increasing PTP timestamps on every clock edge.
|
||||
|
||||
All APIs that handle fractional values use the `Decimal` type for maximum precision, as the combination of timestamp range and resolution is usually too much for normal floating point numbers to handle without significant loss of precision.
|
||||
|
||||
#### Signals
|
||||
|
||||
* `ts_tod`: 96-bit time-of-day timestamp (48 bit seconds, 32 bit ns, 16 bit fractional ns)
|
||||
* `ts_rel`: 64-bit relative timestamp (48 bit ns, 16 bit fractional ns)
|
||||
* `pps`: pulse-per-second output, pulsed when ts_tod seconds field increments
|
||||
|
||||
#### Constructor parameters:
|
||||
|
||||
* _ts_tod_: 96-bit time-of-day timestamp signal (optional)
|
||||
* _ts_rel_: 64-bit relative timestamp signal (optional)
|
||||
* _pps_: pulse-per-second signal (optional)
|
||||
* _clock_: clock
|
||||
|
||||
#### Attributes:
|
||||
|
||||
* _ts_tod_s_: current 96-bit ToD timestamp seconds field
|
||||
* _ts_tod_ns_: current 96-bit ToD timestamp ns field
|
||||
* _ts_tod_fns_: current 96-bit ToD timestamp fractional ns field
|
||||
* _ts_rel_ns_: current 64-bit relative timestamp ns field
|
||||
* _ts_rel_fns_: current 64-bit relative timestamp fractional ns field
|
||||
|
||||
#### Methods
|
||||
|
||||
* `get_ts_tod()`: return current 96-bit ToD timestamp as separate fields
|
||||
* `get_ts_tod_96()`: return current 96-bit ToD timestamp as an integer
|
||||
* `get_ts_tod_ns()`: return current 96-bit ToD timestamp in ns (Decimal)
|
||||
* `get_ts_tod_s()`: return current 96-bit ToD timestamp in seconds (Decimal)
|
||||
* `get_ts_rel()`: return current 64-bit relative timestamp as separate fields
|
||||
* `get_ts_rel_96()`: return current 64-bit relative timestamp as an integer
|
||||
* `get_ts_rel_ns()`: return current 64-bit relative timestamp in ns (Decimal)
|
||||
* `get_ts_rel_s()`: return current 64-bit relative timestamp in seconds (Decimal)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -24,8 +24,10 @@ THE SOFTWARE.
|
||||
|
||||
from .version import __version__
|
||||
|
||||
from .gmii import GmiiFrame, GmiiSource, GmiiSink
|
||||
from .rgmii import RgmiiSource, RgmiiSink
|
||||
from .gmii import GmiiFrame, GmiiSource, GmiiSink, GmiiPhy
|
||||
from .mii import MiiSource, MiiSink, MiiPhy
|
||||
from .rgmii import RgmiiSource, RgmiiSink, RgmiiPhy
|
||||
from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink
|
||||
from .eth_mac import EthMacFrame, EthMacTx, EthMacRx, EthMac
|
||||
|
||||
from .ptp import PtpClock
|
||||
from .ptp import PtpClock, PtpClockSimTime
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -42,12 +42,12 @@ class XgmiiCtrl(enum.IntEnum):
|
||||
TERM = 0xfd
|
||||
ERROR = 0xfe
|
||||
SEQ_OS = 0x9c
|
||||
RES0 = 0x1c
|
||||
RES1 = 0x3c
|
||||
RES2 = 0x7c
|
||||
RES3 = 0xbc
|
||||
RES4 = 0xdc
|
||||
RES5 = 0xf7
|
||||
RES_0 = 0x1c
|
||||
RES_1 = 0x3c
|
||||
RES_2 = 0x7c
|
||||
RES_3 = 0xbc
|
||||
RES_4 = 0xdc
|
||||
RES_5 = 0xf7
|
||||
SIG_OS = 0x5c
|
||||
|
||||
|
||||
@@ -93,3 +93,41 @@ class BaseRBlockType(enum.IntEnum):
|
||||
TERM_5 = 0xd2 # C7 C6 D4 D3 D2 D1 D0 BT
|
||||
TERM_6 = 0xe1 # C7 D5 D4 D3 D2 D1 D0 BT
|
||||
TERM_7 = 0xff # D6 D5 D4 D3 D2 D1 D0 BT
|
||||
|
||||
|
||||
xgmii_ctrl_to_baser_mapping = {
|
||||
XgmiiCtrl.IDLE: BaseRCtrl.IDLE,
|
||||
XgmiiCtrl.LPI: BaseRCtrl.LPI,
|
||||
XgmiiCtrl.ERROR: BaseRCtrl.ERROR,
|
||||
XgmiiCtrl.RES_0: BaseRCtrl.RES_0,
|
||||
XgmiiCtrl.RES_1: BaseRCtrl.RES_1,
|
||||
XgmiiCtrl.RES_2: BaseRCtrl.RES_2,
|
||||
XgmiiCtrl.RES_3: BaseRCtrl.RES_3,
|
||||
XgmiiCtrl.RES_4: BaseRCtrl.RES_4,
|
||||
XgmiiCtrl.RES_5: BaseRCtrl.RES_5,
|
||||
}
|
||||
|
||||
|
||||
baser_ctrl_to_xgmii_mapping = {
|
||||
BaseRCtrl.IDLE: XgmiiCtrl.IDLE,
|
||||
BaseRCtrl.LPI: XgmiiCtrl.LPI,
|
||||
BaseRCtrl.ERROR: XgmiiCtrl.ERROR,
|
||||
BaseRCtrl.RES_0: XgmiiCtrl.RES_0,
|
||||
BaseRCtrl.RES_1: XgmiiCtrl.RES_1,
|
||||
BaseRCtrl.RES_2: XgmiiCtrl.RES_2,
|
||||
BaseRCtrl.RES_3: XgmiiCtrl.RES_3,
|
||||
BaseRCtrl.RES_4: XgmiiCtrl.RES_4,
|
||||
BaseRCtrl.RES_5: XgmiiCtrl.RES_5,
|
||||
}
|
||||
|
||||
|
||||
block_type_term_lane_mapping = {
|
||||
BaseRBlockType.TERM_0: 0,
|
||||
BaseRBlockType.TERM_1: 1,
|
||||
BaseRBlockType.TERM_2: 2,
|
||||
BaseRBlockType.TERM_3: 3,
|
||||
BaseRBlockType.TERM_4: 4,
|
||||
BaseRBlockType.TERM_5: 5,
|
||||
BaseRBlockType.TERM_6: 6,
|
||||
BaseRBlockType.TERM_7: 7,
|
||||
}
|
||||
|
||||
564
cocotbext/eth/eth_mac.py
Normal file
564
cocotbext/eth/eth_mac.py
Normal file
@@ -0,0 +1,564 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2021-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
import cocotb
|
||||
from cocotb.queue import Queue, QueueFull
|
||||
from cocotb.triggers import RisingEdge, Timer, First, Event
|
||||
from cocotb.utils import get_sim_time
|
||||
|
||||
from cocotbext.axi.stream import define_stream
|
||||
|
||||
from .version import __version__
|
||||
from .reset import Reset
|
||||
|
||||
AxiStreamBus, AxiStreamTransaction, AxiStreamSource, AxiStreamSink, AxiStreamMonitor = define_stream("AxiStream",
|
||||
signals=["tvalid", "tdata", "tkeep", "tlast", "tuser"],
|
||||
optional_signals=["tready"]
|
||||
)
|
||||
|
||||
|
||||
class EthMacFrame:
|
||||
def __init__(self, data=b'', tx_complete=None):
|
||||
self.data = b''
|
||||
self.sim_time_start = None
|
||||
self.sim_time_sfd = None
|
||||
self.sim_time_end = None
|
||||
self.ptp_timestamp = None
|
||||
self.ptp_tag = None
|
||||
self.tx_complete = None
|
||||
|
||||
if type(data) is EthMacFrame:
|
||||
self.data = bytes(data.data)
|
||||
self.sim_time_start = data.sim_time_start
|
||||
self.sim_time_sfd = data.sim_time_sfd
|
||||
self.sim_time_end = data.sim_time_end
|
||||
self.ptp_timestamp = data.ptp_timestamp
|
||||
self.ptp_tag = data.ptp_tag
|
||||
self.tx_complete = data.tx_complete
|
||||
else:
|
||||
self.data = bytes(data)
|
||||
|
||||
if tx_complete is not None:
|
||||
self.tx_complete = tx_complete
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload, min_len=60, tx_complete=None):
|
||||
payload = bytearray(payload)
|
||||
if len(payload) < min_len:
|
||||
payload.extend(bytearray(min_len-len(payload)))
|
||||
payload.extend(struct.pack('<L', zlib.crc32(payload)))
|
||||
return cls(payload, tx_complete=tx_complete)
|
||||
|
||||
@classmethod
|
||||
def from_raw_payload(cls, payload, tx_complete=None):
|
||||
return cls(payload, tx_complete=tx_complete)
|
||||
|
||||
def get_payload(self, strip_fcs=True):
|
||||
if strip_fcs:
|
||||
return self.data[:-4]
|
||||
else:
|
||||
return self.data
|
||||
|
||||
def get_fcs(self):
|
||||
return self.data[-4:]
|
||||
|
||||
def check_fcs(self):
|
||||
return self.get_fcs() == struct.pack('<L', zlib.crc32(self.get_payload(strip_fcs=True)))
|
||||
|
||||
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):
|
||||
if type(other) is EthMacFrame:
|
||||
return self.data == other.data
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"{type(self).__name__}(data={self.data!r}, "
|
||||
f"sim_time_start={self.sim_time_start!r}, "
|
||||
f"sim_time_sfd={self.sim_time_sfd!r}, "
|
||||
f"sim_time_end={self.sim_time_end!r}, "
|
||||
f"ptp_timestamp={self.ptp_timestamp!r}, "
|
||||
f"ptp_tag={self.ptp_tag!r})"
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __iter__(self):
|
||||
return self.data.__iter__()
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes(self.data)
|
||||
|
||||
|
||||
class EthMacTx(Reset):
|
||||
def __init__(self, bus, clock, reset=None, ptp_time=None, ptp_ts=None, ptp_ts_tag=None, ptp_ts_valid=None,
|
||||
reset_active_level=True, ifg=12, speed=1000e6, *args, **kwargs):
|
||||
|
||||
self.bus = bus
|
||||
self.clock = clock
|
||||
self.reset = reset
|
||||
self.ptp_time = ptp_time
|
||||
self.ptp_ts = ptp_ts
|
||||
self.ptp_ts_tag = ptp_ts_tag
|
||||
self.ptp_ts_valid = ptp_ts_valid
|
||||
self.ifg = ifg
|
||||
self.speed = speed
|
||||
if bus._name:
|
||||
self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}")
|
||||
else:
|
||||
self.log = logging.getLogger(f"cocotb.{bus._entity._name}")
|
||||
|
||||
self.log.info("Ethernet MAC TX model")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2021-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.stream = AxiStreamSink(bus, clock, reset, reset_active_level=reset_active_level)
|
||||
self.stream.queue_occupancy_limit = 4
|
||||
|
||||
self.active = False
|
||||
self.queue = Queue()
|
||||
self.active_event = Event()
|
||||
|
||||
self.ts_queue = Queue()
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
self.time_scale = cocotb.utils.get_sim_steps(1, 'sec')
|
||||
|
||||
self.width = len(self.bus.tdata)
|
||||
self.byte_lanes = 1
|
||||
|
||||
if hasattr(self.bus, "tkeep"):
|
||||
self.byte_lanes = len(self.bus.tkeep)
|
||||
|
||||
self.byte_size = self.width // self.byte_lanes
|
||||
self.byte_mask = 2**self.byte_size-1
|
||||
|
||||
self.log.info("Ethernet MAC TX model configuration")
|
||||
self.log.info(" Byte size: %d bits", self.byte_size)
|
||||
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
|
||||
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, "tuser"):
|
||||
self.log.info(" tuser width: %d bits", len(self.bus.tuser))
|
||||
else:
|
||||
self.log.info(" tuser: not present")
|
||||
if self.ptp_time is not None:
|
||||
self.log.info(" ptp_time width: %d bits", len(self.ptp_time))
|
||||
else:
|
||||
self.log.info(" ptp_time: not present")
|
||||
|
||||
if self.bus.tready is None:
|
||||
raise ValueError("tready is required")
|
||||
|
||||
if self.byte_size != 8:
|
||||
raise ValueError("Byte size must be 8")
|
||||
|
||||
if self.byte_lanes * self.byte_size != self.width:
|
||||
raise ValueError(f"Bus does not evenly divide into byte lanes "
|
||||
f"({self.byte_lanes} * {self.byte_size} != {self.width})")
|
||||
|
||||
if self.ptp_ts is not None:
|
||||
self.ptp_ts.setimmediatevalue(0)
|
||||
if self.ptp_ts_tag is not None:
|
||||
self.ptp_ts_tag.setimmediatevalue(0)
|
||||
if self.ptp_ts_valid is not None:
|
||||
self.ptp_ts_valid.setimmediatevalue(0)
|
||||
|
||||
self._run_cr = None
|
||||
self._run_ts_cr = None
|
||||
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def _recv(self, frame):
|
||||
if self.queue.empty():
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
return frame
|
||||
|
||||
async def recv(self):
|
||||
frame = await self.queue.get()
|
||||
return self._recv(frame)
|
||||
|
||||
def recv_nowait(self):
|
||||
frame = self.queue.get_nowait()
|
||||
return self._recv(frame)
|
||||
|
||||
def count(self):
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return self.queue.empty()
|
||||
|
||||
def idle(self):
|
||||
return not self.active
|
||||
|
||||
def clear(self):
|
||||
while not self.queue.empty():
|
||||
self.queue.get_nowait()
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
async def wait(self, timeout=0, timeout_unit=None):
|
||||
if not self.empty():
|
||||
return
|
||||
if timeout:
|
||||
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||
else:
|
||||
await self.active_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
if self._run_ts_cr is not None:
|
||||
self._run_ts_cr.cancel()
|
||||
self._run_ts_cr = None
|
||||
|
||||
if self.ptp_ts_valid is not None:
|
||||
self.ptp_ts_valid.value = 0
|
||||
|
||||
self.active = False
|
||||
|
||||
while not self.ts_queue.empty():
|
||||
self.ts_queue.get_nowait()
|
||||
else:
|
||||
self.log.info("Reset de-asserted")
|
||||
if self._run_cr is None:
|
||||
self._run_cr = cocotb.start_soon(self._run())
|
||||
if self._run_ts_cr is None and self.ptp_ts is not None:
|
||||
self._run_ts_cr = cocotb.start_soon(self._run_ts())
|
||||
|
||||
async def _run(self):
|
||||
frame = None
|
||||
self.active = False
|
||||
|
||||
while True:
|
||||
# wait for data
|
||||
cycle = await self.stream.recv()
|
||||
|
||||
frame = EthMacFrame()
|
||||
data = bytearray()
|
||||
|
||||
frame.sim_time_start = get_sim_time()
|
||||
|
||||
# wait for preamble time
|
||||
await Timer(self.time_scale*8*8//self.speed, 'step')
|
||||
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
|
||||
if self.ptp_time is not None:
|
||||
frame.ptp_timestamp = int(self.ptp_time.value)
|
||||
frame.ptp_tag = int(cycle.tuser) >> 1
|
||||
self.ts_queue.put_nowait((frame.ptp_timestamp, frame.ptp_tag))
|
||||
|
||||
# process frame data
|
||||
while True:
|
||||
byte_count = 0
|
||||
|
||||
for offset in range(self.byte_lanes):
|
||||
if not hasattr(self.bus, "tkeep") or (int(cycle.tkeep) >> offset) & 1:
|
||||
data.append((int(cycle.tdata) >> (offset * self.byte_size)) & self.byte_mask)
|
||||
byte_count += 1
|
||||
|
||||
# wait for serialization time
|
||||
await Timer(self.time_scale*byte_count*8//self.speed, 'step')
|
||||
|
||||
if int(cycle.tlast):
|
||||
frame.data = bytes(data)
|
||||
frame.sim_time_end = get_sim_time()
|
||||
self.log.info("RX frame: %s", frame)
|
||||
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
await self.queue.put(frame)
|
||||
self.active_event.set()
|
||||
|
||||
frame = None
|
||||
|
||||
break
|
||||
|
||||
# get next cycle
|
||||
# TODO improve underflow handling
|
||||
assert not self.stream.empty(), "underflow"
|
||||
cycle = await self.stream.recv()
|
||||
|
||||
# wait for IFG
|
||||
await Timer(self.time_scale*self.ifg*8//self.speed, 'step')
|
||||
|
||||
async def _run_ts(self):
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
while True:
|
||||
await clock_edge_event
|
||||
self.ptp_ts_valid.value = 0
|
||||
|
||||
if not self.ts_queue.empty():
|
||||
ts, tag = self.ts_queue.get_nowait()
|
||||
self.ptp_ts.value = ts
|
||||
if self.ptp_ts_tag is not None:
|
||||
self.ptp_ts_tag.value = tag
|
||||
self.ptp_ts_valid.value = 1
|
||||
|
||||
|
||||
class EthMacRx(Reset):
|
||||
def __init__(self, bus, clock, reset=None, ptp_time=None,
|
||||
reset_active_level=True, ifg=12, speed=1000e6, *args, **kwargs):
|
||||
|
||||
self.bus = bus
|
||||
self.clock = clock
|
||||
self.reset = reset
|
||||
self.ptp_time = ptp_time
|
||||
self.ifg = ifg
|
||||
self.speed = speed
|
||||
if bus._name:
|
||||
self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}")
|
||||
else:
|
||||
self.log = logging.getLogger(f"cocotb.{bus._entity._name}")
|
||||
|
||||
self.log.info("Ethernet MAC RX model")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2021-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.stream = AxiStreamSource(bus, clock, reset, reset_active_level=reset_active_level)
|
||||
self.stream.queue_occupancy_limit = 4
|
||||
|
||||
self.active = False
|
||||
self.queue = Queue()
|
||||
self.dequeue_event = Event()
|
||||
self.current_frame = None
|
||||
self.idle_event = Event()
|
||||
self.idle_event.set()
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
self.queue_occupancy_limit_bytes = -1
|
||||
self.queue_occupancy_limit_frames = -1
|
||||
|
||||
self.time_scale = cocotb.utils.get_sim_steps(1, 'sec')
|
||||
|
||||
self.width = len(self.bus.tdata)
|
||||
self.byte_lanes = 1
|
||||
|
||||
if hasattr(self.bus, "tkeep"):
|
||||
self.byte_lanes = len(self.bus.tkeep)
|
||||
|
||||
self.byte_size = self.width // self.byte_lanes
|
||||
self.byte_mask = 2**self.byte_size-1
|
||||
|
||||
self.log.info("Ethernet MAC RX model configuration")
|
||||
self.log.info(" Byte size: %d bits", self.byte_size)
|
||||
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
|
||||
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, "tuser"):
|
||||
self.log.info(" tuser width: %d bits", len(self.bus.tuser))
|
||||
else:
|
||||
self.log.info(" tuser: not present")
|
||||
if self.ptp_time is not None:
|
||||
self.log.info(" ptp_time width: %d bits", len(self.ptp_time))
|
||||
else:
|
||||
self.log.info(" ptp_time: not present")
|
||||
|
||||
if self.byte_size != 8:
|
||||
raise ValueError("Byte size must be 8")
|
||||
|
||||
if self.byte_lanes * self.byte_size != self.width:
|
||||
raise ValueError(f"Bus does not evenly divide into byte lanes "
|
||||
f"({self.byte_lanes} * {self.byte_size} != {self.width})")
|
||||
|
||||
self._run_cr = None
|
||||
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
async def send(self, frame):
|
||||
while self.full():
|
||||
self.dequeue_event.clear()
|
||||
await self.dequeue_event.wait()
|
||||
frame = EthMacFrame(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 = EthMacFrame(frame)
|
||||
self.queue.put_nowait(frame)
|
||||
self.idle_event.clear()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
def count(self):
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return self.queue.empty()
|
||||
|
||||
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
|
||||
|
||||
def clear(self):
|
||||
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.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
async def wait(self):
|
||||
await self.idle_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
frame = None
|
||||
frame_offset = 0
|
||||
tuser = 0
|
||||
self.active = False
|
||||
|
||||
while True:
|
||||
# wait for data
|
||||
frame = await self.queue.get()
|
||||
tuser = 0
|
||||
self.dequeue_event.set()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
self.current_frame = frame
|
||||
frame.sim_time_start = get_sim_time()
|
||||
frame.sim_time_sfd = None
|
||||
frame.sim_time_end = None
|
||||
self.log.info("TX frame: %s", frame)
|
||||
frame_offset = 0
|
||||
|
||||
# wait for preamble time
|
||||
await Timer(self.time_scale*8*8//self.speed, 'step')
|
||||
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
|
||||
if self.ptp_time is not None:
|
||||
frame.ptp_timestamp = int(self.ptp_time.value)
|
||||
tuser |= frame.ptp_timestamp << 1
|
||||
|
||||
# process frame data
|
||||
while frame is not None:
|
||||
byte_count = 0
|
||||
|
||||
cycle = AxiStreamTransaction()
|
||||
|
||||
cycle.tdata = 0
|
||||
cycle.tkeep = 0
|
||||
cycle.tlast = 0
|
||||
cycle.tuser = tuser
|
||||
|
||||
for offset in range(self.byte_lanes):
|
||||
cycle.tdata |= (frame.data[frame_offset] & self.byte_mask) << (offset * self.byte_size)
|
||||
cycle.tkeep |= 1 << offset
|
||||
byte_count += 1
|
||||
frame_offset += 1
|
||||
|
||||
if frame_offset >= len(frame.data):
|
||||
cycle.tlast = 1
|
||||
frame.sim_time_end = get_sim_time()
|
||||
frame.handle_tx_complete()
|
||||
frame = None
|
||||
self.current_frame = None
|
||||
break
|
||||
|
||||
await self.stream.send(cycle)
|
||||
|
||||
# wait for serialization time
|
||||
await Timer(self.time_scale*byte_count*8//self.speed, 'step')
|
||||
|
||||
# wait for IFG
|
||||
await Timer(self.time_scale*self.ifg*8//self.speed, 'step')
|
||||
|
||||
|
||||
class EthMac:
|
||||
def __init__(self, tx_bus=None, tx_clk=None, tx_rst=None, tx_ptp_time=None, tx_ptp_ts=None, tx_ptp_ts_tag=None,
|
||||
tx_ptp_ts_valid=None, rx_bus=None, rx_clk=None, rx_rst=None, rx_ptp_time=None,
|
||||
reset_active_level=True, ifg=12, speed=1000e6, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.tx = EthMacTx(tx_bus, tx_clk, tx_rst, tx_ptp_time, tx_ptp_ts, tx_ptp_ts_tag, tx_ptp_ts_valid,
|
||||
reset_active_level=reset_active_level, ifg=ifg, speed=speed)
|
||||
self.rx = EthMacRx(rx_bus, rx_clk, rx_rst, rx_ptp_time,
|
||||
reset_active_level=reset_active_level, ifg=ifg, speed=speed)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -25,43 +25,53 @@ THE SOFTWARE.
|
||||
import logging
|
||||
import struct
|
||||
import zlib
|
||||
from collections import deque
|
||||
|
||||
import cocotb
|
||||
from cocotb.triggers import RisingEdge, ReadOnly, Timer, First, Event
|
||||
from cocotb.utils import get_sim_time
|
||||
from cocotb.queue import Queue, QueueFull
|
||||
from cocotb.triggers import RisingEdge, Timer, First, Event
|
||||
from cocotb.utils import get_sim_time, get_sim_steps
|
||||
|
||||
from .version import __version__
|
||||
from .constants import EthPre, ETH_PREAMBLE
|
||||
from .reset import Reset
|
||||
|
||||
|
||||
class GmiiFrame(object):
|
||||
def __init__(self, data=None, error=None):
|
||||
class GmiiFrame:
|
||||
def __init__(self, data=None, error=None, tx_complete=None):
|
||||
self.data = bytearray()
|
||||
self.error = None
|
||||
self.rx_sim_time = None
|
||||
self.sim_time_start = None
|
||||
self.sim_time_sfd = None
|
||||
self.sim_time_end = None
|
||||
self.tx_complete = None
|
||||
|
||||
if type(data) is GmiiFrame:
|
||||
self.data = bytearray(data.data)
|
||||
self.error = data.error
|
||||
self.rx_sim_time = data.rx_sim_time
|
||||
self.sim_time_start = data.sim_time_start
|
||||
self.sim_time_sfd = data.sim_time_sfd
|
||||
self.sim_time_end = data.sim_time_end
|
||||
self.tx_complete = data.tx_complete
|
||||
else:
|
||||
self.data = bytearray(data)
|
||||
self.error = error
|
||||
|
||||
if tx_complete is not None:
|
||||
self.tx_complete = tx_complete
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload, min_len=60):
|
||||
def from_payload(cls, payload, min_len=60, tx_complete=None):
|
||||
payload = bytearray(payload)
|
||||
if len(payload) < min_len:
|
||||
payload.extend(bytearray(min_len-len(payload)))
|
||||
payload.extend(struct.pack('<L', zlib.crc32(payload)))
|
||||
return cls.from_raw_payload(payload)
|
||||
return cls.from_raw_payload(payload, tx_complete=tx_complete)
|
||||
|
||||
@classmethod
|
||||
def from_raw_payload(cls, payload):
|
||||
def from_raw_payload(cls, payload, tx_complete=None):
|
||||
data = bytearray(ETH_PREAMBLE)
|
||||
data.extend(payload)
|
||||
return cls(data)
|
||||
return cls(data, tx_complete=tx_complete)
|
||||
|
||||
def get_preamble_len(self):
|
||||
return self.data.index(EthPre.SFD)+1
|
||||
@@ -90,18 +100,26 @@ class GmiiFrame(object):
|
||||
self.error = [0]*n
|
||||
|
||||
def compact(self):
|
||||
if not any(self.error):
|
||||
if self.error is not None and not any(self.error):
|
||||
self.error = None
|
||||
|
||||
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):
|
||||
if type(other) is GmiiFrame:
|
||||
return self.data == other.data
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"{type(self).__name__}(data={repr(self.data)}, "
|
||||
f"error={repr(self.error)}, "
|
||||
f"rx_sim_time={repr(self.rx_sim_time)})"
|
||||
f"{type(self).__name__}(data={self.data!r}, "
|
||||
f"error={self.error!r}, "
|
||||
f"sim_time_start={self.sim_time_start!r}, "
|
||||
f"sim_time_sfd={self.sim_time_sfd!r}, "
|
||||
f"sim_time_end={self.sim_time_end!r})"
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
@@ -110,10 +128,13 @@ class GmiiFrame(object):
|
||||
def __iter__(self):
|
||||
return self.data.__iter__()
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes(self.data)
|
||||
|
||||
class GmiiSource(object):
|
||||
|
||||
def __init__(self, data, er, dv, clock, reset=None, enable=None, mii_select=None, *args, **kwargs):
|
||||
class GmiiSource(Reset):
|
||||
|
||||
def __init__(self, data, er, dv, clock, reset=None, enable=None, mii_select=None, reset_active_level=True, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.{data._path}")
|
||||
self.data = data
|
||||
self.er = er
|
||||
@@ -125,24 +146,31 @@ class GmiiSource(object):
|
||||
|
||||
self.log.info("GMII source")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020 Alex Forencich")
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.active = False
|
||||
self.queue = deque()
|
||||
self.queue = Queue()
|
||||
self.dequeue_event = Event()
|
||||
self.current_frame = None
|
||||
self.idle_event = Event()
|
||||
self.idle_event.set()
|
||||
self.active_event = Event()
|
||||
|
||||
self.ifg = 12
|
||||
self.mii_mode = False
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
self.queue_occupancy_limit_bytes = -1
|
||||
self.queue_occupancy_limit_frames = -1
|
||||
|
||||
self.width = 8
|
||||
self.byte_width = 1
|
||||
|
||||
self.reset = reset
|
||||
|
||||
assert len(self.data) == 8
|
||||
self.data.setimmediatevalue(0)
|
||||
if self.er is not None:
|
||||
@@ -151,94 +179,177 @@ class GmiiSource(object):
|
||||
assert len(self.dv) == 1
|
||||
self.dv.setimmediatevalue(0)
|
||||
|
||||
cocotb.fork(self._run())
|
||||
self._run_cr = None
|
||||
|
||||
def send(self, frame):
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
async def send(self, frame):
|
||||
while self.full():
|
||||
self.dequeue_event.clear()
|
||||
await self.dequeue_event.wait()
|
||||
frame = GmiiFrame(frame)
|
||||
await self.queue.put(frame)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
def send_nowait(self, frame):
|
||||
if self.full():
|
||||
raise QueueFull()
|
||||
frame = GmiiFrame(frame)
|
||||
self.queue.put_nowait(frame)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
self.queue.append(frame)
|
||||
|
||||
def count(self):
|
||||
return len(self.queue)
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return not self.queue
|
||||
return self.queue.empty()
|
||||
|
||||
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
|
||||
|
||||
def clear(self):
|
||||
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):
|
||||
while not self.idle():
|
||||
await RisingEdge(self.clock)
|
||||
await self.idle_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
self.data.value = 0
|
||||
if self.er is not None:
|
||||
self.er.value = 0
|
||||
self.dv.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
|
||||
|
||||
if self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
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):
|
||||
frame = None
|
||||
frame_offset = 0
|
||||
frame_data = None
|
||||
frame_error = None
|
||||
ifg_cnt = 0
|
||||
self.active = False
|
||||
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
enable_event = None
|
||||
if self.enable is not None:
|
||||
enable_event = RisingEdge(self.enable)
|
||||
|
||||
while True:
|
||||
await ReadOnly()
|
||||
await clock_edge_event
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
frame = None
|
||||
ifg_cnt = 0
|
||||
self.active = False
|
||||
self.data <= 0
|
||||
if self.er is not None:
|
||||
self.er <= 0
|
||||
self.dv <= 0
|
||||
continue
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
|
||||
if self.enable is None or self.enable.value:
|
||||
if self.enable is None or int(self.enable.value):
|
||||
if ifg_cnt > 0:
|
||||
# in IFG
|
||||
ifg_cnt -= 1
|
||||
|
||||
elif frame is None and self.queue:
|
||||
elif frame is None and not self.queue.empty():
|
||||
# send frame
|
||||
frame = self.queue.popleft()
|
||||
frame = self.queue.get_nowait()
|
||||
self.dequeue_event.set()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
self.current_frame = frame
|
||||
frame.sim_time_start = get_sim_time()
|
||||
frame.sim_time_sfd = None
|
||||
frame.sim_time_end = None
|
||||
self.log.info("TX frame: %s", frame)
|
||||
frame.normalize()
|
||||
|
||||
if self.mii_select is not None and self.mii_select.value:
|
||||
mii_data = []
|
||||
mii_error = []
|
||||
if self.mii_select is not None:
|
||||
self.mii_mode = bool(int(self.mii_select.value))
|
||||
|
||||
if self.mii_mode:
|
||||
# convert to MII
|
||||
frame_data = []
|
||||
frame_error = []
|
||||
for b, e in zip(frame.data, frame.error):
|
||||
mii_data.append(b & 0x0F)
|
||||
mii_data.append(b >> 4)
|
||||
mii_error.append(e)
|
||||
mii_error.append(e)
|
||||
frame.data = mii_data
|
||||
frame.error = mii_error
|
||||
frame_data.append(b & 0x0F)
|
||||
frame_data.append(b >> 4)
|
||||
frame_error.append(e)
|
||||
frame_error.append(e)
|
||||
else:
|
||||
frame_data = frame.data
|
||||
frame_error = frame.error
|
||||
|
||||
self.active = True
|
||||
frame_offset = 0
|
||||
|
||||
if frame is not None:
|
||||
self.data <= frame.data.pop(0)
|
||||
d = frame_data[frame_offset]
|
||||
if frame.sim_time_sfd is None and d in (EthPre.SFD, 0xD):
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
self.data.value = d
|
||||
if self.er is not None:
|
||||
self.er <= frame.error.pop(0)
|
||||
self.dv <= 1
|
||||
self.er.value = frame_error[frame_offset]
|
||||
self.dv.value = 1
|
||||
frame_offset += 1
|
||||
|
||||
if not frame.data:
|
||||
if frame_offset >= len(frame_data):
|
||||
ifg_cnt = max(self.ifg, 1)
|
||||
frame.sim_time_end = get_sim_time()
|
||||
frame.handle_tx_complete()
|
||||
frame = None
|
||||
self.current_frame = None
|
||||
else:
|
||||
self.data <= 0
|
||||
self.data.value = 0
|
||||
if self.er is not None:
|
||||
self.er <= 0
|
||||
self.dv <= 0
|
||||
self.er.value = 0
|
||||
self.dv.value = 0
|
||||
self.active = False
|
||||
|
||||
if ifg_cnt == 0 and self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
await self.active_event.wait()
|
||||
|
||||
class GmiiSink(object):
|
||||
elif self.enable is not None and not self.enable.value:
|
||||
await enable_event
|
||||
|
||||
def __init__(self, data, er, dv, clock, reset=None, enable=None, mii_select=None, *args, **kwargs):
|
||||
|
||||
class GmiiSink(Reset):
|
||||
|
||||
def __init__(self, data, er, dv, clock, reset=None, enable=None, mii_select=None, reset_active_level=True, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.{data._path}")
|
||||
self.data = data
|
||||
self.er = er
|
||||
@@ -250,14 +361,16 @@ class GmiiSink(object):
|
||||
|
||||
self.log.info("GMII sink")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020 Alex Forencich")
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.active = False
|
||||
self.queue = deque()
|
||||
self.sync = Event()
|
||||
self.queue = Queue()
|
||||
self.active_event = Event()
|
||||
|
||||
self.mii_mode = False
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
@@ -265,70 +378,103 @@ class GmiiSink(object):
|
||||
self.width = 8
|
||||
self.byte_width = 1
|
||||
|
||||
self.reset = reset
|
||||
|
||||
assert len(self.data) == 8
|
||||
if self.er is not None:
|
||||
assert len(self.er) == 1
|
||||
if self.dv is not None:
|
||||
assert len(self.dv) == 1
|
||||
|
||||
cocotb.fork(self._run())
|
||||
self._run_cr = None
|
||||
|
||||
def recv(self):
|
||||
if self.queue:
|
||||
frame = self.queue.popleft()
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def _recv(self, frame, compact=True):
|
||||
if self.queue.empty():
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
if compact:
|
||||
frame.compact()
|
||||
return frame
|
||||
return None
|
||||
|
||||
async def recv(self, compact=True):
|
||||
frame = await self.queue.get()
|
||||
return self._recv(frame, compact)
|
||||
|
||||
def recv_nowait(self, compact=True):
|
||||
frame = self.queue.get_nowait()
|
||||
return self._recv(frame, compact)
|
||||
|
||||
def count(self):
|
||||
return len(self.queue)
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return not self.queue
|
||||
return self.queue.empty()
|
||||
|
||||
def idle(self):
|
||||
return not self.active
|
||||
|
||||
def clear(self):
|
||||
while not self.queue.empty():
|
||||
self.queue.get_nowait()
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
async def wait(self, timeout=0, timeout_unit=None):
|
||||
if not self.empty():
|
||||
return
|
||||
self.sync.clear()
|
||||
if timeout:
|
||||
await First(self.sync.wait(), Timer(timeout, timeout_unit))
|
||||
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||
else:
|
||||
await self.sync.wait()
|
||||
await self.active_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
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):
|
||||
frame = None
|
||||
self.active = False
|
||||
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
active_event = RisingEdge(self.dv)
|
||||
|
||||
enable_event = None
|
||||
if self.enable is not None:
|
||||
enable_event = RisingEdge(self.enable)
|
||||
|
||||
while True:
|
||||
await ReadOnly()
|
||||
await clock_edge_event
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
frame = None
|
||||
self.active = False
|
||||
continue
|
||||
|
||||
if self.enable is None or self.enable.value:
|
||||
d_val = self.data.value.integer
|
||||
dv_val = self.dv.value.integer
|
||||
er_val = 0 if self.er is None else self.er.value.integer
|
||||
if self.enable is None or int(self.enable.value):
|
||||
d_val = int(self.data.value)
|
||||
dv_val = int(self.dv.value)
|
||||
er_val = 0 if self.er is None else int(self.er.value)
|
||||
|
||||
if frame is None:
|
||||
if dv_val:
|
||||
# start of frame
|
||||
frame = GmiiFrame(bytearray(), [])
|
||||
frame.rx_sim_time = get_sim_time()
|
||||
frame.sim_time_start = get_sim_time()
|
||||
else:
|
||||
if not dv_val:
|
||||
# end of frame
|
||||
|
||||
if self.mii_select is not None and self.mii_select.value:
|
||||
if self.mii_select is not None:
|
||||
self.mii_mode = bool(int(self.mii_select.value))
|
||||
|
||||
if self.mii_mode:
|
||||
odd = True
|
||||
sync = False
|
||||
b = 0
|
||||
@@ -350,18 +496,80 @@ class GmiiSink(object):
|
||||
frame.error = error
|
||||
|
||||
frame.compact()
|
||||
frame.sim_time_end = get_sim_time()
|
||||
self.log.info("RX frame: %s", frame)
|
||||
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
self.queue.append(frame)
|
||||
self.sync.set()
|
||||
self.queue.put_nowait(frame)
|
||||
self.active_event.set()
|
||||
|
||||
frame = None
|
||||
|
||||
if frame is not None:
|
||||
if frame.sim_time_sfd is None and d_val in (EthPre.SFD, 0xD):
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
|
||||
frame.data.append(d_val)
|
||||
frame.error.append(er_val)
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
if not dv_val:
|
||||
await active_event
|
||||
|
||||
elif self.enable is not None and not self.enable.value:
|
||||
await enable_event
|
||||
|
||||
|
||||
class GmiiPhy:
|
||||
def __init__(self, txd, tx_er, tx_en, tx_clk, gtx_clk, rxd, rx_er, rx_dv, rx_clk,
|
||||
reset=None, reset_active_level=True, speed=1000e6, *args, **kwargs):
|
||||
|
||||
self.gtx_clk = gtx_clk
|
||||
self.tx_clk = tx_clk
|
||||
self.rx_clk = rx_clk
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.tx = GmiiSink(txd, tx_er, tx_en, tx_clk, reset, reset_active_level=reset_active_level)
|
||||
self.rx = GmiiSource(rxd, rx_er, rx_dv, rx_clk, reset_active_level=reset_active_level)
|
||||
|
||||
self.rx_clk.setimmediatevalue(0)
|
||||
|
||||
self._clock_cr = None
|
||||
self.set_speed(speed)
|
||||
|
||||
def set_speed(self, speed):
|
||||
if speed in (10e6, 100e6, 1000e6):
|
||||
self.speed = speed
|
||||
else:
|
||||
raise ValueError("Invalid speed selection")
|
||||
|
||||
if self._clock_cr is not None:
|
||||
self._clock_cr.cancel()
|
||||
|
||||
if self.speed == 1000e6:
|
||||
self._clock_cr = cocotb.start_soon(self._run_clocks(8*1e9/self.speed))
|
||||
self.tx.mii_mode = False
|
||||
self.rx.mii_mode = False
|
||||
self.tx.clock = self.gtx_clk
|
||||
else:
|
||||
self._clock_cr = cocotb.start_soon(self._run_clocks(4*1e9/self.speed))
|
||||
self.tx.mii_mode = True
|
||||
self.rx.mii_mode = True
|
||||
self.tx.clock = self.tx_clk
|
||||
|
||||
self.tx.assert_reset()
|
||||
self.rx.assert_reset()
|
||||
|
||||
async def _run_clocks(self, period):
|
||||
half_period = get_sim_steps(period / 2.0, 'ns')
|
||||
t = Timer(half_period)
|
||||
|
||||
while True:
|
||||
await t
|
||||
self.rx_clk.value = 1
|
||||
self.tx_clk.value = 1
|
||||
await t
|
||||
self.rx_clk.value = 0
|
||||
self.tx_clk.value = 0
|
||||
|
||||
449
cocotbext/eth/mii.py
Normal file
449
cocotbext/eth/mii.py
Normal file
@@ -0,0 +1,449 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import cocotb
|
||||
from cocotb.queue import Queue, QueueFull
|
||||
from cocotb.triggers import RisingEdge, Timer, First, Event
|
||||
from cocotb.utils import get_sim_time, get_sim_steps
|
||||
|
||||
from .version import __version__
|
||||
from .gmii import GmiiFrame
|
||||
from .constants import EthPre
|
||||
from .reset import Reset
|
||||
|
||||
|
||||
class MiiSource(Reset):
|
||||
|
||||
def __init__(self, data, er, dv, clock, reset=None, enable=None, reset_active_level=True, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.{data._path}")
|
||||
self.data = data
|
||||
self.er = er
|
||||
self.dv = dv
|
||||
self.clock = clock
|
||||
self.reset = reset
|
||||
self.enable = enable
|
||||
|
||||
self.log.info("MII source")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.active = False
|
||||
self.queue = Queue()
|
||||
self.dequeue_event = Event()
|
||||
self.current_frame = None
|
||||
self.idle_event = Event()
|
||||
self.idle_event.set()
|
||||
self.active_event = Event()
|
||||
|
||||
self.ifg = 12
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
self.queue_occupancy_limit_bytes = -1
|
||||
self.queue_occupancy_limit_frames = -1
|
||||
|
||||
self.width = 4
|
||||
self.byte_width = 1
|
||||
|
||||
assert len(self.data) == 4
|
||||
self.data.setimmediatevalue(0)
|
||||
if self.er is not None:
|
||||
assert len(self.er) == 1
|
||||
self.er.setimmediatevalue(0)
|
||||
assert len(self.dv) == 1
|
||||
self.dv.setimmediatevalue(0)
|
||||
|
||||
self._run_cr = None
|
||||
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
async def send(self, frame):
|
||||
while self.full():
|
||||
self.dequeue_event.clear()
|
||||
await self.dequeue_event.wait()
|
||||
frame = GmiiFrame(frame)
|
||||
await self.queue.put(frame)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
def send_nowait(self, frame):
|
||||
if self.full():
|
||||
raise QueueFull()
|
||||
frame = GmiiFrame(frame)
|
||||
self.queue.put_nowait(frame)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
def count(self):
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return self.queue.empty()
|
||||
|
||||
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
|
||||
|
||||
def clear(self):
|
||||
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):
|
||||
await self.idle_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
self.data.value = 0
|
||||
if self.er is not None:
|
||||
self.er.value = 0
|
||||
self.dv.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
|
||||
|
||||
if self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
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):
|
||||
frame = None
|
||||
frame_offset = 0
|
||||
frame_data = None
|
||||
frame_error = None
|
||||
ifg_cnt = 0
|
||||
self.active = False
|
||||
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
enable_event = None
|
||||
if self.enable is not None:
|
||||
enable_event = RisingEdge(self.enable)
|
||||
|
||||
while True:
|
||||
await clock_edge_event
|
||||
|
||||
if self.enable is None or int(self.enable.value):
|
||||
if ifg_cnt > 0:
|
||||
# in IFG
|
||||
ifg_cnt -= 1
|
||||
|
||||
elif frame is None and not self.queue.empty():
|
||||
# send frame
|
||||
frame = self.queue.get_nowait()
|
||||
self.dequeue_event.set()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
self.current_frame = frame
|
||||
frame.sim_time_start = get_sim_time()
|
||||
frame.sim_time_sfd = None
|
||||
frame.sim_time_end = None
|
||||
self.log.info("TX frame: %s", frame)
|
||||
frame.normalize()
|
||||
|
||||
# convert to MII
|
||||
frame_data = []
|
||||
frame_error = []
|
||||
for b, e in zip(frame.data, frame.error):
|
||||
frame_data.append(b & 0x0F)
|
||||
frame_data.append(b >> 4)
|
||||
frame_error.append(e)
|
||||
frame_error.append(e)
|
||||
|
||||
self.active = True
|
||||
frame_offset = 0
|
||||
|
||||
if frame is not None:
|
||||
d = frame_data[frame_offset]
|
||||
if frame.sim_time_sfd is None and d == 0xD:
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
self.data.value = d
|
||||
if self.er is not None:
|
||||
self.er.value = frame_error[frame_offset]
|
||||
self.dv.value = 1
|
||||
frame_offset += 1
|
||||
|
||||
if frame_offset >= len(frame_data):
|
||||
ifg_cnt = max(self.ifg, 1)
|
||||
frame.sim_time_end = get_sim_time()
|
||||
frame.handle_tx_complete()
|
||||
frame = None
|
||||
self.current_frame = None
|
||||
else:
|
||||
self.data.value = 0
|
||||
if self.er is not None:
|
||||
self.er.value = 0
|
||||
self.dv.value = 0
|
||||
self.active = False
|
||||
|
||||
if ifg_cnt == 0 and self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
await self.active_event.wait()
|
||||
|
||||
elif self.enable is not None and not self.enable.value:
|
||||
await enable_event
|
||||
|
||||
|
||||
class MiiSink(Reset):
|
||||
|
||||
def __init__(self, data, er, dv, clock, reset=None, enable=None, reset_active_level=True, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.{data._path}")
|
||||
self.data = data
|
||||
self.er = er
|
||||
self.dv = dv
|
||||
self.clock = clock
|
||||
self.reset = reset
|
||||
self.enable = enable
|
||||
|
||||
self.log.info("MII sink")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.active = False
|
||||
self.queue = Queue()
|
||||
self.active_event = Event()
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
self.width = 4
|
||||
self.byte_width = 1
|
||||
|
||||
assert len(self.data) == 4
|
||||
if self.er is not None:
|
||||
assert len(self.er) == 1
|
||||
if self.dv is not None:
|
||||
assert len(self.dv) == 1
|
||||
|
||||
self._run_cr = None
|
||||
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def _recv(self, frame, compact=True):
|
||||
if self.queue.empty():
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
if compact:
|
||||
frame.compact()
|
||||
return frame
|
||||
|
||||
async def recv(self, compact=True):
|
||||
frame = await self.queue.get()
|
||||
return self._recv(frame, compact)
|
||||
|
||||
def recv_nowait(self, compact=True):
|
||||
frame = self.queue.get_nowait()
|
||||
return self._recv(frame, compact)
|
||||
|
||||
def count(self):
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return self.queue.empty()
|
||||
|
||||
def idle(self):
|
||||
return not self.active
|
||||
|
||||
def clear(self):
|
||||
while not self.queue.empty():
|
||||
self.queue.get_nowait()
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
async def wait(self, timeout=0, timeout_unit=None):
|
||||
if not self.empty():
|
||||
return
|
||||
if timeout:
|
||||
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||
else:
|
||||
await self.active_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
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):
|
||||
frame = None
|
||||
self.active = False
|
||||
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
active_event = RisingEdge(self.dv)
|
||||
|
||||
enable_event = None
|
||||
if self.enable is not None:
|
||||
enable_event = RisingEdge(self.enable)
|
||||
|
||||
while True:
|
||||
await clock_edge_event
|
||||
|
||||
if self.enable is None or int(self.enable.value):
|
||||
d_val = int(self.data.value)
|
||||
dv_val = int(self.dv.value)
|
||||
er_val = 0 if self.er is None else int(self.er.value)
|
||||
|
||||
if frame is None:
|
||||
if dv_val:
|
||||
# start of frame
|
||||
frame = GmiiFrame(bytearray(), [])
|
||||
frame.sim_time_start = get_sim_time()
|
||||
else:
|
||||
if not dv_val:
|
||||
# end of frame
|
||||
odd = True
|
||||
sync = False
|
||||
b = 0
|
||||
be = 0
|
||||
data = bytearray()
|
||||
error = []
|
||||
for n, e in zip(frame.data, frame.error):
|
||||
odd = not odd
|
||||
b = (n & 0x0F) << 4 | b >> 4
|
||||
be |= e
|
||||
if not sync and b == EthPre.SFD:
|
||||
odd = True
|
||||
sync = True
|
||||
if odd:
|
||||
data.append(b)
|
||||
error.append(be)
|
||||
be = 0
|
||||
frame.data = data
|
||||
frame.error = error
|
||||
|
||||
frame.compact()
|
||||
frame.sim_time_end = get_sim_time()
|
||||
self.log.info("RX frame: %s", frame)
|
||||
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
self.queue.put_nowait(frame)
|
||||
self.active_event.set()
|
||||
|
||||
frame = None
|
||||
|
||||
if frame is not None:
|
||||
if frame.sim_time_sfd is None and d_val == 0xD:
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
|
||||
frame.data.append(d_val)
|
||||
frame.error.append(er_val)
|
||||
|
||||
if not dv_val:
|
||||
await active_event
|
||||
|
||||
elif self.enable is not None and not self.enable.value:
|
||||
await enable_event
|
||||
|
||||
|
||||
class MiiPhy:
|
||||
def __init__(self, txd, tx_er, tx_en, tx_clk, rxd, rx_er, rx_dv, rx_clk, reset=None,
|
||||
reset_active_level=True, speed=100e6, *args, **kwargs):
|
||||
|
||||
self.tx_clk = tx_clk
|
||||
self.rx_clk = rx_clk
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.tx = MiiSink(txd, tx_er, tx_en, tx_clk, reset, reset_active_level=reset_active_level)
|
||||
self.rx = MiiSource(rxd, rx_er, rx_dv, rx_clk, reset, reset_active_level=reset_active_level)
|
||||
|
||||
self.tx_clk.setimmediatevalue(0)
|
||||
self.rx_clk.setimmediatevalue(0)
|
||||
|
||||
self._clock_cr = None
|
||||
self.set_speed(speed)
|
||||
|
||||
def set_speed(self, speed):
|
||||
if speed in (10e6, 100e6):
|
||||
self.speed = speed
|
||||
else:
|
||||
raise ValueError("Invalid speed selection")
|
||||
|
||||
if self._clock_cr is not None:
|
||||
self._clock_cr.cancel()
|
||||
|
||||
self._clock_cr = cocotb.start_soon(self._run_clocks(4*1e9/self.speed))
|
||||
|
||||
async def _run_clocks(self, period):
|
||||
half_period = get_sim_steps(period / 2.0, 'ns')
|
||||
t = Timer(half_period)
|
||||
|
||||
while True:
|
||||
await t
|
||||
self.tx_clk.value = 1
|
||||
self.rx_clk.value = 1
|
||||
await t
|
||||
self.tx_clk.value = 0
|
||||
self.rx_clk.value = 0
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -23,223 +23,347 @@ THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from decimal import Decimal, Context
|
||||
from fractions import Fraction
|
||||
|
||||
import cocotb
|
||||
from cocotb.triggers import RisingEdge, ReadOnly
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.utils import get_sim_time
|
||||
|
||||
from .version import __version__
|
||||
from .reset import Reset
|
||||
|
||||
|
||||
class PtpClock(object):
|
||||
class PtpClock(Reset):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ts_96=None,
|
||||
ts_64=None,
|
||||
ts_tod=None,
|
||||
ts_rel=None,
|
||||
ts_step=None,
|
||||
pps=None,
|
||||
clock=None,
|
||||
reset=None,
|
||||
reset_active_level=True,
|
||||
period_ns=6.4,
|
||||
*args, **kwargs):
|
||||
|
||||
self.log = logging.getLogger(f"cocotb.eth.{type(self).__name__}")
|
||||
self.ts_96 = ts_96
|
||||
self.ts_64 = ts_64
|
||||
self.ts_tod = ts_tod
|
||||
self.ts_rel = ts_rel
|
||||
self.ts_step = ts_step
|
||||
self.pps = pps
|
||||
self.clock = clock
|
||||
self.reset = reset
|
||||
|
||||
self.period_ns = 0
|
||||
self.period_fns = 0
|
||||
self.drift_ns = 0
|
||||
self.drift_fns = 0
|
||||
self.drift_rate = 0
|
||||
self.set_period_ns(period_ns)
|
||||
|
||||
self.log.info("PTP clock")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020 Alex Forencich")
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.ts_96_s = 0
|
||||
self.ts_96_ns = 0
|
||||
self.ts_96_fns = 0
|
||||
self.ctx = Context(prec=60)
|
||||
|
||||
self.ts_64_ns = 0
|
||||
self.ts_64_fns = 0
|
||||
self.period_ns = 0
|
||||
self.period_fns = 0
|
||||
self.drift_num = 0
|
||||
self.drift_denom = 0
|
||||
self.drift_cnt = 0
|
||||
self.set_period_ns(period_ns)
|
||||
|
||||
self.ts_tod_s = 0
|
||||
self.ts_tod_ns = 0
|
||||
self.ts_tod_fns = 0
|
||||
|
||||
self.ts_rel_ns = 0
|
||||
self.ts_rel_fns = 0
|
||||
|
||||
self.ts_updated = False
|
||||
|
||||
self.drift_cnt = 0
|
||||
|
||||
if self.ts_96 is not None:
|
||||
self.ts_96.setimmediatevalue(0)
|
||||
if self.ts_64 is not None:
|
||||
self.ts_64.setimmediatevalue(0)
|
||||
if self.ts_tod is not None:
|
||||
self.ts_tod.setimmediatevalue(0)
|
||||
if self.ts_rel is not None:
|
||||
self.ts_rel.setimmediatevalue(0)
|
||||
if self.ts_step is not None:
|
||||
self.ts_step.setimmediatevalue(0)
|
||||
if self.pps is not None:
|
||||
self.pps.setimmediatevalue(0)
|
||||
|
||||
cocotb.fork(self._run())
|
||||
self._run_cr = None
|
||||
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def set_period(self, ns, fns):
|
||||
self.period_ns = int(ns)
|
||||
self.period_fns = int(fns) & 0xffff
|
||||
self.period_fns = int(fns) & 0xffffffff
|
||||
|
||||
def set_drift(self, ns, fns, rate):
|
||||
self.drift_ns = int(ns)
|
||||
self.drift_fns = int(fns) & 0xffff
|
||||
self.drift_rate = int(rate)
|
||||
def set_drift(self, num, denom):
|
||||
self.drift_num = int(num)
|
||||
self.drift_denom = int(denom)
|
||||
|
||||
def set_period_ns(self, t):
|
||||
drift, period = math.modf(t*2**16)
|
||||
t = Decimal(t)
|
||||
period, drift = self.ctx.divmod(Decimal(t) * Decimal(2**32), Decimal(1))
|
||||
period = int(period)
|
||||
frac = Fraction(drift).limit_denominator(2**16)
|
||||
drift = frac.numerator
|
||||
rate = frac.denominator
|
||||
self.period_ns = period >> 16
|
||||
self.period_fns = period & 0xffff
|
||||
self.drift_ns = drift >> 16
|
||||
self.drift_fns = drift & 0xffff
|
||||
self.drift_rate = rate
|
||||
frac = Fraction(drift).limit_denominator(2**16-1)
|
||||
self.set_period(period >> 32, period & 0xffffffff)
|
||||
self.set_drift(frac.numerator, frac.denominator)
|
||||
|
||||
self.log.info("Set period: %s ns", t)
|
||||
self.log.info("Period: 0x%x ns 0x%08x fns", self.period_ns, self.period_fns)
|
||||
self.log.info("Drift: 0x%04x / 0x%04x fns", self.drift_num, self.drift_denom)
|
||||
|
||||
def get_period_ns(self):
|
||||
p = ((self.period_ns << 16) | self.period_fns) / 2**16
|
||||
if self.drift_rate:
|
||||
return p + ((self.drift_ns << 16) | self.drift_fns) / self.drift_rate / 2**16
|
||||
return p
|
||||
p = Decimal((self.period_ns << 32) | self.period_fns)
|
||||
if self.drift_denom:
|
||||
p += Decimal(self.drift_num) / Decimal(self.drift_denom)
|
||||
return p / Decimal(2**32)
|
||||
|
||||
def set_ts_96(self, ts_s, ts_ns=None, ts_fns=None):
|
||||
ts_s = int(ts_s)
|
||||
if ts_fns is not None:
|
||||
# got separate fields
|
||||
self.ts_96_s = ts_s
|
||||
self.ts_96_ns = int(ts_ns)
|
||||
self.ts_96_fns = int(ts_fns)
|
||||
else:
|
||||
# got timestamp as integer
|
||||
self.ts_96_s = ts_s >> 48
|
||||
self.ts_96_ns = (ts_s >> 16) & 0x3fffffff
|
||||
self.ts_96_fns = ts_s & 0xffff
|
||||
def set_ts_tod(self, ts_s, ts_ns, ts_fns):
|
||||
self.ts_tod_s = int(ts_s)
|
||||
self.ts_tod_ns = int(ts_ns)
|
||||
self.ts_tod_fns = int(ts_fns)
|
||||
self.ts_updated = True
|
||||
|
||||
def set_ts_96_ns(self, t):
|
||||
self.set_ts_96_s(t*1e-9)
|
||||
def set_ts_tod_96(self, ts):
|
||||
ts = int(ts)
|
||||
self.set_ts_tod(ts >> 48, (ts >> 32) & 0x3fffffff, (ts & 0xffff) << 16)
|
||||
|
||||
def set_ts_96_s(self, t):
|
||||
ts_ns, ts_s = math.modf(t)
|
||||
ts_ns *= 1e9
|
||||
ts_fns, ts_ns = math.modf(ts_ns)
|
||||
ts_fns *= 2**16
|
||||
self.set_ts_96(ts_s, ts_ns, ts_fns)
|
||||
def set_ts_tod_ns(self, t):
|
||||
ts_s, ts_ns = self.ctx.divmod(Decimal(t), Decimal(1000000000))
|
||||
ts_ns, ts_fns = self.ctx.divmod(ts_ns, Decimal(1))
|
||||
ts_ns = ts_ns.to_integral_value()
|
||||
ts_fns = (ts_fns * Decimal(2**32)).to_integral_value()
|
||||
self.set_ts_tod(ts_s, ts_ns, ts_fns)
|
||||
|
||||
def get_ts_96(self):
|
||||
return (self.ts_96_s << 48) | (self.ts_96_ns << 16) | self.ts_96_fns
|
||||
def set_ts_tod_s(self, t):
|
||||
self.set_ts_tod_ns(Decimal(t).scaleb(9, self.ctx))
|
||||
|
||||
def get_ts_96_ns(self):
|
||||
return self.ts_96_s*1e9+self.ts_96_ns+self.ts_96_fns/2**16
|
||||
def set_ts_tod_sim_time(self):
|
||||
self.set_ts_tod_ns(Decimal(get_sim_time('fs')).scaleb(-6))
|
||||
|
||||
def get_ts_96_s(self):
|
||||
return self.get_ts_96_ns()*1e-9
|
||||
def get_ts_tod(self):
|
||||
return (self.ts_tod_s, self.ts_tod_ns, self.ts_tod_fns)
|
||||
|
||||
def set_ts_64(self, ts_ns, ts_fns=None):
|
||||
ts_ns = int(ts_ns)
|
||||
if ts_fns is not None:
|
||||
# got separate fields
|
||||
self.ts_64_ns = ts_ns
|
||||
self.ts_64_fns = int(ts_fns)
|
||||
else:
|
||||
# got timestamp as integer
|
||||
self.ts_64_ns = ts_ns >> 16
|
||||
self.ts_64_fns = ts_ns & 0xffff
|
||||
def get_ts_tod_96(self):
|
||||
ts_s, ts_ns, ts_fns = self.get_ts_tod()
|
||||
return (ts_s << 48) | (ts_ns << 16) | (ts_fns >> 16)
|
||||
|
||||
def get_ts_tod_ns(self):
|
||||
ts_s, ts_ns, ts_fns = self.get_ts_tod()
|
||||
ns = Decimal(ts_fns) / Decimal(2**32)
|
||||
ns = self.ctx.add(ns, Decimal(ts_ns))
|
||||
return self.ctx.add(ns, Decimal(ts_s).scaleb(9))
|
||||
|
||||
def get_ts_tod_s(self):
|
||||
return self.get_ts_tod_ns().scaleb(-9, self.ctx)
|
||||
|
||||
def set_ts_rel(self, ts_ns, ts_fns):
|
||||
self.ts_rel_ns = int(ts_ns)
|
||||
self.ts_rel_fns = int(ts_fns)
|
||||
self.ts_updated = True
|
||||
|
||||
def set_ts_64_ns(self, t):
|
||||
self.set_ts_64(t*2**16)
|
||||
def set_ts_rel_64(self, ts):
|
||||
ts = int(ts)
|
||||
self.set_ts_rel(ts >> 16, (ts & 0xffff) << 16)
|
||||
|
||||
def set_ts_64_s(self, t):
|
||||
self.set_ts_64_ns(t*1e9)
|
||||
def set_ts_rel_ns(self, t):
|
||||
ts_ns, ts_fns = self.ctx.divmod(Decimal(t), Decimal(1))
|
||||
ts_ns = ts_ns.to_integral_value()
|
||||
ts_fns = (ts_fns * Decimal(2**32)).to_integral_value()
|
||||
self.set_ts_rel(ts_ns, ts_fns)
|
||||
|
||||
def get_ts_64(self):
|
||||
return (self.ts_64_ns << 16) | self.ts_64_fns
|
||||
def set_ts_rel_s(self, t):
|
||||
self.set_ts_rel_ns(Decimal(t).scaleb(9, self.ctx))
|
||||
|
||||
def get_ts_64_ns(self):
|
||||
return self.get_ts_64()/2**16
|
||||
def set_ts_rel_sim_time(self):
|
||||
self.set_ts_rel_ns(Decimal(get_sim_time('fs')).scaleb(-6))
|
||||
|
||||
def get_ts_64_s(self):
|
||||
return self.get_ts_64()*1e-9
|
||||
def get_ts_rel(self):
|
||||
return (self.ts_rel_ns, self.ts_rel_fns)
|
||||
|
||||
def get_ts_rel_64(self):
|
||||
ts_ns, ts_fns = self.get_ts_rel()
|
||||
return (ts_ns << 16) | (ts_fns >> 16)
|
||||
|
||||
def get_ts_rel_ns(self):
|
||||
ts_ns, ts_fns = self.get_ts_rel()
|
||||
return self.ctx.add(Decimal(ts_fns) / Decimal(2**32), Decimal(ts_ns))
|
||||
|
||||
def get_ts_rel_s(self):
|
||||
return self.get_ts_rel_ns().scaleb(-9, self.ctx)
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.ts_tod_s = 0
|
||||
self.ts_tod_ns = 0
|
||||
self.ts_tod_fns = 0
|
||||
self.ts_rel_ns = 0
|
||||
self.ts_rel_fns = 0
|
||||
self.drift_cnt = 0
|
||||
if self.ts_tod is not None:
|
||||
self.ts_tod.value = 0
|
||||
if self.ts_rel is not None:
|
||||
self.ts_rel.value = 0
|
||||
if self.ts_step is not None:
|
||||
self.ts_step.value = 0
|
||||
if self.pps is not None:
|
||||
self.pps.value = 0
|
||||
else:
|
||||
self.log.info("Reset de-asserted")
|
||||
if self._run_cr is None:
|
||||
self._run_cr = cocotb.start_soon(self._run())
|
||||
|
||||
async def _run(self):
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
while True:
|
||||
await ReadOnly()
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
self.ts_96_s = 0
|
||||
self.ts_96_ns = 0
|
||||
self.ts_96_fns = 0
|
||||
self.ts_64_ns = 0
|
||||
self.ts_64_fns = 0
|
||||
self.drift_cnt = 0
|
||||
if self.ts_96 is not None:
|
||||
self.ts_96 <= 0
|
||||
if self.ts_64 is not None:
|
||||
self.ts_64 <= 0
|
||||
if self.ts_step is not None:
|
||||
self.ts_step <= 0
|
||||
if self.pps is not None:
|
||||
self.pps <= 0
|
||||
continue
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
await clock_edge_event
|
||||
|
||||
if self.ts_step is not None:
|
||||
self.ts_step <= self.ts_updated
|
||||
self.ts_step.value = self.ts_updated
|
||||
self.ts_updated = False
|
||||
|
||||
if self.pps is not None:
|
||||
self.pps <= 0
|
||||
self.pps.value = 0
|
||||
|
||||
# increment 96 bit timestamp
|
||||
if self.ts_96 is not None or self.pps is not None:
|
||||
t = ((self.ts_96_ns << 16) + self.ts_96_fns) + ((self.period_ns << 16) + self.period_fns)
|
||||
# increment tod bit timestamp
|
||||
self.ts_tod_fns += (self.period_ns << 32) + self.period_fns
|
||||
|
||||
if self.drift_rate and self.drift_cnt == 0:
|
||||
t += (self.drift_ns << 16) + self.drift_fns
|
||||
if self.drift_denom and self.drift_cnt == 0:
|
||||
self.ts_tod_fns += self.drift_num
|
||||
|
||||
if t > (1000000000 << 16):
|
||||
self.ts_96_s += 1
|
||||
t -= (1000000000 << 16)
|
||||
ns_inc = self.ts_tod_fns >> 32
|
||||
self.ts_tod_fns &= 0xffffffff
|
||||
|
||||
self.ts_tod_ns += ns_inc
|
||||
|
||||
if self.ts_tod_ns >= 1000000000:
|
||||
self.ts_tod_s += 1
|
||||
self.ts_tod_ns -= 1000000000
|
||||
if self.pps is not None:
|
||||
self.pps <= 1
|
||||
self.pps.value = 1
|
||||
|
||||
self.ts_96_fns = t & 0xffff
|
||||
self.ts_96_ns = t >> 16
|
||||
if self.ts_tod is not None:
|
||||
self.ts_tod.value = (self.ts_tod_s << 48) | (self.ts_tod_ns << 16) | (self.ts_tod_fns >> 16)
|
||||
|
||||
if self.ts_96 is not None:
|
||||
self.ts_96 <= (self.ts_96_s << 48) | (self.ts_96_ns << 16) | (self.ts_96_fns)
|
||||
# increment rel bit timestamp
|
||||
self.ts_rel_fns += (self.period_ns << 32) + self.period_fns
|
||||
|
||||
# increment 64 bit timestamp
|
||||
if self.ts_64 is not None:
|
||||
t = ((self.ts_64_ns << 16) + self.ts_64_fns) + ((self.period_ns << 16) + self.period_fns)
|
||||
if self.drift_denom and self.drift_cnt == 0:
|
||||
self.ts_rel_fns += self.drift_num
|
||||
|
||||
if self.drift_rate and self.drift_cnt == 0:
|
||||
t += ((self.drift_ns << 16) + self.drift_fns)
|
||||
ns_inc = self.ts_rel_fns >> 32
|
||||
self.ts_rel_fns &= 0xffffffff
|
||||
|
||||
self.ts_64_fns = t & 0xffff
|
||||
self.ts_64_ns = t >> 16
|
||||
self.ts_rel_ns = (self.ts_rel_ns + ns_inc) & 0xffffffffffff
|
||||
|
||||
self.ts_64 <= (self.ts_64_ns << 16) | self.ts_64_fns
|
||||
if self.ts_rel is not None:
|
||||
self.ts_rel.value = (self.ts_rel_ns << 16) | (self.ts_rel_fns >> 16)
|
||||
|
||||
if self.drift_rate:
|
||||
if self.drift_denom:
|
||||
if self.drift_cnt > 0:
|
||||
self.drift_cnt -= 1
|
||||
else:
|
||||
self.drift_cnt = self.drift_rate-1
|
||||
self.drift_cnt = self.drift_denom-1
|
||||
|
||||
|
||||
class PtpClockSimTime:
|
||||
|
||||
def __init__(self, ts_tod=None, ts_rel=None, pps=None, clock=None, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.eth.{type(self).__name__}")
|
||||
self.ts_tod = ts_tod
|
||||
self.ts_rel = ts_rel
|
||||
self.pps = pps
|
||||
self.clock = clock
|
||||
|
||||
self.log.info("PTP clock (sim time)")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.ctx = Context(prec=60)
|
||||
|
||||
self.ts_tod_s = 0
|
||||
self.ts_tod_ns = 0
|
||||
self.ts_tod_fns = 0
|
||||
|
||||
self.ts_rel_ns = 0
|
||||
self.ts_rel_fns = 0
|
||||
|
||||
self.last_ts_tod_s = 0
|
||||
|
||||
if self.ts_tod is not None:
|
||||
self.ts_tod.setimmediatevalue(0)
|
||||
if self.ts_rel is not None:
|
||||
self.ts_rel.setimmediatevalue(0)
|
||||
if self.pps is not None:
|
||||
self.pps.value = 0
|
||||
|
||||
self._run_cr = cocotb.start_soon(self._run())
|
||||
|
||||
def get_ts_tod(self):
|
||||
return (self.ts_tod_s, self.ts_tod_ns, self.ts_tod_fns)
|
||||
|
||||
def get_ts_tod_96(self):
|
||||
ts_s, ts_ns, ts_fns = self.get_ts_tod()
|
||||
return (ts_s << 48) | (ts_ns << 16) | (ts_fns >> 16)
|
||||
|
||||
def get_ts_tod_ns(self):
|
||||
ts_s, ts_ns, ts_fns = self.get_ts_tod()
|
||||
ns = Decimal(ts_fns) / Decimal(2**32)
|
||||
ns = self.ctx.add(ns, Decimal(ts_ns))
|
||||
return self.ctx.add(ns, Decimal(ts_s).scaleb(9))
|
||||
|
||||
def get_ts_tod_s(self):
|
||||
return self.get_ts_tod_ns().scaleb(-9, self.ctx)
|
||||
|
||||
def get_ts_rel(self):
|
||||
return (self.ts_rel_ns, self.ts_rel_fns)
|
||||
|
||||
def get_ts_rel_64(self):
|
||||
ts_ns, ts_fns = self.get_ts_rel()
|
||||
return (ts_ns << 16) | (ts_fns >> 16)
|
||||
|
||||
def get_ts_rel_ns(self):
|
||||
ts_ns, ts_fns = self.get_ts_rel()
|
||||
return self.ctx.add(Decimal(ts_fns) / Decimal(2**32), Decimal(ts_ns))
|
||||
|
||||
def get_ts_rel_s(self):
|
||||
return self.get_ts_rel_ns().scaleb(-9, self.ctx)
|
||||
|
||||
async def _run(self):
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
while True:
|
||||
await clock_edge_event
|
||||
|
||||
ts_ns, ts_fns = self.ctx.divmod(Decimal(get_sim_time('fs')).scaleb(-6), Decimal(1))
|
||||
|
||||
self.ts_rel_ns = int(ts_ns.to_integral_value()) & 0xffffffffffff
|
||||
self.ts_rel_fns = int((ts_fns * Decimal(2**16)).to_integral_value())
|
||||
|
||||
ts_s, ts_ns = self.ctx.divmod(ts_ns, Decimal(1000000000))
|
||||
|
||||
self.ts_tod_s = int(ts_s.scaleb(-9).to_integral_value())
|
||||
self.ts_tod_ns = int(ts_ns.to_integral_value())
|
||||
self.ts_tod_fns = self.ts_rel_fns
|
||||
|
||||
if self.ts_tod is not None:
|
||||
self.ts_tod.value = (self.ts_tod_s << 48) | (self.ts_tod_ns << 16) | self.ts_tod_fns
|
||||
|
||||
if self.ts_rel is not None:
|
||||
self.ts_rel.value = (self.ts_rel_ns << 16) | self.ts_rel_fns
|
||||
|
||||
if self.pps is not None:
|
||||
self.pps.value = int(self.last_ts_tod_s != self.ts_tod_s)
|
||||
|
||||
self.last_ts_tod_s = self.ts_tod_s
|
||||
|
||||
69
cocotbext/eth/reset.py
Normal file
69
cocotbext/eth/reset.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import cocotb
|
||||
from cocotb.triggers import Edge
|
||||
|
||||
|
||||
class Reset:
|
||||
def _init_reset(self, reset_signal=None, active_level=True):
|
||||
self._local_reset = False
|
||||
self._ext_reset = False
|
||||
self._reset_state = True
|
||||
|
||||
if reset_signal is not None:
|
||||
cocotb.start_soon(self._run_reset(reset_signal, bool(active_level)))
|
||||
|
||||
self._update_reset()
|
||||
|
||||
def assert_reset(self, val=None):
|
||||
if val is None:
|
||||
self.assert_reset(True)
|
||||
self.assert_reset(False)
|
||||
else:
|
||||
self._local_reset = bool(val)
|
||||
self._update_reset()
|
||||
|
||||
def _update_reset(self):
|
||||
new_state = self._local_reset or self._ext_reset
|
||||
if self._reset_state != new_state:
|
||||
self._reset_state = new_state
|
||||
self._handle_reset(new_state)
|
||||
|
||||
def _handle_reset(self, state):
|
||||
pass
|
||||
|
||||
async def _run_reset(self, reset_signal, active_level):
|
||||
while True:
|
||||
await reset_signal.value_change
|
||||
try:
|
||||
level = bool(int(reset_signal.value))
|
||||
except ValueError:
|
||||
continue
|
||||
if level:
|
||||
self._ext_reset = active_level
|
||||
self._update_reset()
|
||||
else:
|
||||
self._ext_reset = not active_level
|
||||
self._update_reset()
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -23,20 +23,23 @@ THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
import cocotb
|
||||
from cocotb.triggers import RisingEdge, FallingEdge, ReadOnly, Timer, First, Event
|
||||
from cocotb.utils import get_sim_time
|
||||
from cocotb.queue import Queue, QueueFull
|
||||
from cocotb.triggers import RisingEdge, FallingEdge, Timer, First, Event
|
||||
from cocotb.utils import get_sim_time, get_sim_steps
|
||||
|
||||
from .version import __version__
|
||||
from .gmii import GmiiFrame
|
||||
from .constants import EthPre
|
||||
from .reset import Reset
|
||||
|
||||
|
||||
class RgmiiSource(object):
|
||||
class RgmiiSource(Reset):
|
||||
|
||||
def __init__(self, data, ctrl, clock, reset=None, enable=None, mii_select=None,
|
||||
reset_active_level=True, *args, **kwargs):
|
||||
|
||||
def __init__(self, data, ctrl, clock, reset=None, enable=None, mii_select=None, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.{data._path}")
|
||||
self.data = data
|
||||
self.ctrl = ctrl
|
||||
@@ -47,127 +50,224 @@ class RgmiiSource(object):
|
||||
|
||||
self.log.info("RGMII source")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020 Alex Forencich")
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.active = False
|
||||
self.queue = deque()
|
||||
self.queue = Queue()
|
||||
self.dequeue_event = Event()
|
||||
self.current_frame = None
|
||||
self.idle_event = Event()
|
||||
self.idle_event.set()
|
||||
self.active_event = Event()
|
||||
|
||||
self.ifg = 12
|
||||
self.mii_mode = False
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
self.queue_occupancy_limit_bytes = -1
|
||||
self.queue_occupancy_limit_frames = -1
|
||||
|
||||
self.width = 8
|
||||
self.byte_width = 1
|
||||
|
||||
self.reset = reset
|
||||
|
||||
assert len(self.data) == 4
|
||||
self.data.setimmediatevalue(0)
|
||||
assert len(self.ctrl) == 1
|
||||
self.ctrl.setimmediatevalue(0)
|
||||
|
||||
cocotb.fork(self._run())
|
||||
self._run_cr = None
|
||||
|
||||
def send(self, frame):
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
async def send(self, frame):
|
||||
while self.full():
|
||||
self.dequeue_event.clear()
|
||||
await self.dequeue_event.wait()
|
||||
frame = GmiiFrame(frame)
|
||||
await self.queue.put(frame)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
def send_nowait(self, frame):
|
||||
if self.full():
|
||||
raise QueueFull()
|
||||
frame = GmiiFrame(frame)
|
||||
self.queue.put_nowait(frame)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
self.queue.append(frame)
|
||||
|
||||
def count(self):
|
||||
return len(self.queue)
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return not self.queue
|
||||
return self.queue.empty()
|
||||
|
||||
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
|
||||
|
||||
def clear(self):
|
||||
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):
|
||||
while not self.idle():
|
||||
await RisingEdge(self.clock)
|
||||
await self.idle_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
self.data.value = 0
|
||||
self.ctrl.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
|
||||
|
||||
if self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
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):
|
||||
frame = None
|
||||
frame_offset = 0
|
||||
frame_data = None
|
||||
frame_error = None
|
||||
ifg_cnt = 0
|
||||
in_ifg = False
|
||||
self.active = False
|
||||
d = 0
|
||||
er = 0
|
||||
en = 0
|
||||
|
||||
clock_rising_edge_event = RisingEdge(self.clock)
|
||||
clock_falling_edge_event = FallingEdge(self.clock)
|
||||
|
||||
enable_event = None
|
||||
if self.enable is not None:
|
||||
enable_event = RisingEdge(self.enable)
|
||||
|
||||
while True:
|
||||
await ReadOnly()
|
||||
await clock_falling_edge_event
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
frame = None
|
||||
ifg_cnt = 0
|
||||
self.active = False
|
||||
self.data <= 0
|
||||
self.ctrl <= 0
|
||||
continue
|
||||
# send low nibble after falling edge, leading in to rising edge
|
||||
self.data.value = d & 0x0F
|
||||
self.ctrl.value = en
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
await clock_rising_edge_event
|
||||
|
||||
if self.mii_select is None or not self.mii_select.value:
|
||||
# send high nibble after rising edge, leading in to falling edge
|
||||
self.data <= d >> 4
|
||||
self.ctrl <= en ^ er
|
||||
self.data.value = d >> 4
|
||||
self.ctrl.value = en ^ er
|
||||
|
||||
if self.enable is None or int(self.enable.value):
|
||||
in_ifg = False
|
||||
|
||||
if self.enable is None or self.enable.value:
|
||||
if ifg_cnt > 0:
|
||||
# in IFG
|
||||
ifg_cnt -= 1
|
||||
in_ifg = True
|
||||
|
||||
elif frame is None and self.queue:
|
||||
elif frame is None and not self.queue.empty():
|
||||
# send frame
|
||||
frame = self.queue.popleft()
|
||||
frame = self.queue.get_nowait()
|
||||
self.dequeue_event.set()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
self.current_frame = frame
|
||||
frame.sim_time_start = get_sim_time()
|
||||
frame.sim_time_sfd = None
|
||||
frame.sim_time_end = None
|
||||
self.log.info("TX frame: %s", frame)
|
||||
frame.normalize()
|
||||
|
||||
if self.mii_select is not None and self.mii_select.value:
|
||||
mii_data = []
|
||||
mii_error = []
|
||||
if self.mii_select is not None:
|
||||
self.mii_mode = bool(int(self.mii_select.value))
|
||||
|
||||
if self.mii_mode:
|
||||
# convert to MII
|
||||
frame_data = []
|
||||
frame_error = []
|
||||
for b, e in zip(frame.data, frame.error):
|
||||
mii_data.append(b & 0x0F)
|
||||
mii_data.append(b >> 4)
|
||||
mii_error.append(e)
|
||||
mii_error.append(e)
|
||||
frame.data = mii_data
|
||||
frame.error = mii_error
|
||||
frame_data.append((b & 0x0F)*0x11)
|
||||
frame_data.append((b >> 4)*0x11)
|
||||
frame_error.append(e)
|
||||
frame_error.append(e)
|
||||
else:
|
||||
frame_data = frame.data
|
||||
frame_error = frame.error
|
||||
|
||||
self.active = True
|
||||
frame_offset = 0
|
||||
|
||||
if frame is not None:
|
||||
d = frame.data.pop(0)
|
||||
er = frame.error.pop(0)
|
||||
d = frame_data[frame_offset]
|
||||
er = frame_error[frame_offset]
|
||||
en = 1
|
||||
frame_offset += 1
|
||||
|
||||
if not frame.data:
|
||||
if frame.sim_time_sfd is None and d in (EthPre.SFD, 0xD, 0xDD):
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
|
||||
if frame_offset >= len(frame_data):
|
||||
ifg_cnt = max(self.ifg, 1)
|
||||
in_ifg = True
|
||||
frame.sim_time_end = get_sim_time()
|
||||
frame.handle_tx_complete()
|
||||
frame = None
|
||||
self.current_frame = None
|
||||
else:
|
||||
d = 0
|
||||
er = 0
|
||||
en = 0
|
||||
self.active = False
|
||||
|
||||
await FallingEdge(self.clock)
|
||||
if not in_ifg and self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
await self.active_event.wait()
|
||||
|
||||
# send low nibble after falling edge, leading in to rising edge
|
||||
self.data <= d & 0x0F
|
||||
self.ctrl <= en
|
||||
elif self.enable is not None and not self.enable.value:
|
||||
await enable_event
|
||||
|
||||
|
||||
class RgmiiSink(object):
|
||||
class RgmiiSink(Reset):
|
||||
|
||||
def __init__(self, data, ctrl, clock, reset=None, enable=None, mii_select=None,
|
||||
reset_active_level=True, *args, **kwargs):
|
||||
|
||||
def __init__(self, data, ctrl, clock, reset=None, enable=None, mii_select=None, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.{data._path}")
|
||||
self.data = data
|
||||
self.ctrl = ctrl
|
||||
@@ -178,14 +278,16 @@ class RgmiiSink(object):
|
||||
|
||||
self.log.info("RGMII sink")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020 Alex Forencich")
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.active = False
|
||||
self.queue = deque()
|
||||
self.sync = Event()
|
||||
self.queue = Queue()
|
||||
self.active_event = Event()
|
||||
|
||||
self.mii_mode = False
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
@@ -193,38 +295,66 @@ class RgmiiSink(object):
|
||||
self.width = 8
|
||||
self.byte_width = 1
|
||||
|
||||
self.reset = reset
|
||||
|
||||
assert len(self.data) == 4
|
||||
assert len(self.ctrl) == 1
|
||||
|
||||
cocotb.fork(self._run())
|
||||
self._run_cr = None
|
||||
|
||||
def recv(self):
|
||||
if self.queue:
|
||||
frame = self.queue.popleft()
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def _recv(self, frame, compact=True):
|
||||
if self.queue.empty():
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
if compact:
|
||||
frame.compact()
|
||||
return frame
|
||||
return None
|
||||
|
||||
async def recv(self, compact=True):
|
||||
frame = await self.queue.get()
|
||||
return self._recv(frame, compact)
|
||||
|
||||
def recv_nowait(self, compact=True):
|
||||
frame = self.queue.get_nowait()
|
||||
return self._recv(frame, compact)
|
||||
|
||||
def count(self):
|
||||
return len(self.queue)
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return not self.queue
|
||||
return self.queue.empty()
|
||||
|
||||
def idle(self):
|
||||
return not self.active
|
||||
|
||||
def clear(self):
|
||||
while not self.queue.empty():
|
||||
self.queue.get_nowait()
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
async def wait(self, timeout=0, timeout_unit=None):
|
||||
if not self.empty():
|
||||
return
|
||||
self.sync.clear()
|
||||
if timeout:
|
||||
await First(self.sync.wait(), Timer(timeout, timeout_unit))
|
||||
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||
else:
|
||||
await self.sync.wait()
|
||||
await self.active_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
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):
|
||||
frame = None
|
||||
@@ -233,31 +363,43 @@ class RgmiiSink(object):
|
||||
dv_val = 0
|
||||
er_val = 0
|
||||
|
||||
clock_rising_edge_event = RisingEdge(self.clock)
|
||||
clock_falling_edge_event = FallingEdge(self.clock)
|
||||
|
||||
active_event = RisingEdge(self.ctrl)
|
||||
|
||||
enable_event = None
|
||||
if self.enable is not None:
|
||||
enable_event = RisingEdge(self.enable)
|
||||
|
||||
while True:
|
||||
await ReadOnly()
|
||||
await clock_rising_edge_event
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
frame = None
|
||||
self.active = False
|
||||
continue
|
||||
if self.enable is None or int(self.enable.value):
|
||||
|
||||
# capture high nibble after rising edge, leading in to falling edge
|
||||
d_val |= self.data.value.integer << 4
|
||||
er_val = dv_val ^ self.ctrl.value.integer
|
||||
# capture low nibble on rising edge
|
||||
d_val = int(self.data.value)
|
||||
dv_val = int(self.ctrl.value)
|
||||
|
||||
if self.enable is None or self.enable.value:
|
||||
await clock_falling_edge_event
|
||||
|
||||
# capture high nibble on falling edge
|
||||
d_val |= int(self.data.value) << 4
|
||||
er_val = dv_val ^ int(self.ctrl.value)
|
||||
|
||||
if frame is None:
|
||||
if dv_val:
|
||||
# start of frame
|
||||
frame = GmiiFrame(bytearray(), [])
|
||||
frame.rx_sim_time = get_sim_time()
|
||||
frame.sim_time_start = get_sim_time()
|
||||
else:
|
||||
if not dv_val:
|
||||
# end of frame
|
||||
|
||||
if self.mii_select is not None and self.mii_select.value:
|
||||
if self.mii_select is not None:
|
||||
self.mii_mode = bool(int(self.mii_select.value))
|
||||
|
||||
if self.mii_mode:
|
||||
odd = True
|
||||
sync = False
|
||||
b = 0
|
||||
@@ -279,25 +421,72 @@ class RgmiiSink(object):
|
||||
frame.error = error
|
||||
|
||||
frame.compact()
|
||||
frame.sim_time_end = get_sim_time()
|
||||
self.log.info("RX frame: %s", frame)
|
||||
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
self.queue.append(frame)
|
||||
self.sync.set()
|
||||
self.queue.put_nowait(frame)
|
||||
self.active_event.set()
|
||||
|
||||
frame = None
|
||||
|
||||
if frame is not None:
|
||||
if frame.sim_time_sfd is None and d_val in (EthPre.SFD, 0xD, 0xDD):
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
|
||||
frame.data.append(d_val)
|
||||
frame.error.append(er_val)
|
||||
|
||||
await FallingEdge(self.clock)
|
||||
await ReadOnly()
|
||||
if not dv_val:
|
||||
await active_event
|
||||
|
||||
# capture low nibble after falling edge, leading in to rising edge
|
||||
d_val = self.data.value.integer
|
||||
dv_val = self.ctrl.value.integer
|
||||
elif self.enable is not None and not self.enable.value:
|
||||
await enable_event
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
|
||||
class RgmiiPhy:
|
||||
def __init__(self, txd, tx_ctl, tx_clk, rxd, rx_ctl, rx_clk, reset=None,
|
||||
reset_active_level=True, speed=1000e6, *args, **kwargs):
|
||||
|
||||
self.tx_clk = tx_clk
|
||||
self.rx_clk = rx_clk
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.tx = RgmiiSink(txd, tx_ctl, tx_clk, reset, reset_active_level=reset_active_level)
|
||||
self.rx = RgmiiSource(rxd, rx_ctl, rx_clk, reset, reset_active_level=reset_active_level)
|
||||
|
||||
self.rx_clk.setimmediatevalue(0)
|
||||
|
||||
self._clock_cr = None
|
||||
self.set_speed(speed)
|
||||
|
||||
def set_speed(self, speed):
|
||||
if speed in (10e6, 100e6, 1000e6):
|
||||
self.speed = speed
|
||||
else:
|
||||
raise ValueError("Invalid speed selection")
|
||||
|
||||
if self._clock_cr is not None:
|
||||
self._clock_cr.cancel()
|
||||
|
||||
if self.speed == 1000e6:
|
||||
self._clock_cr = cocotb.start_soon(self._run_clock(8*1e9/self.speed))
|
||||
self.tx.mii_mode = False
|
||||
self.rx.mii_mode = False
|
||||
else:
|
||||
self._clock_cr = cocotb.start_soon(self._run_clock(4*1e9/self.speed))
|
||||
self.tx.mii_mode = True
|
||||
self.rx.mii_mode = True
|
||||
|
||||
async def _run_clock(self, period):
|
||||
half_period = get_sim_steps(period / 2.0, 'ns')
|
||||
t = Timer(half_period)
|
||||
|
||||
while True:
|
||||
await t
|
||||
self.rx_clk.value = 1
|
||||
await t
|
||||
self.rx_clk.value = 0
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.1.24"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -25,45 +25,55 @@ THE SOFTWARE.
|
||||
import logging
|
||||
import struct
|
||||
import zlib
|
||||
from collections import deque
|
||||
|
||||
import cocotb
|
||||
from cocotb.triggers import RisingEdge, ReadOnly, Timer, First, Event
|
||||
from cocotb.queue import Queue, QueueFull
|
||||
from cocotb.triggers import Edge, RisingEdge, Timer, First, Event
|
||||
from cocotb.utils import get_sim_time
|
||||
|
||||
from .version import __version__
|
||||
from .constants import EthPre, ETH_PREAMBLE, XgmiiCtrl
|
||||
from .reset import Reset
|
||||
|
||||
|
||||
class XgmiiFrame(object):
|
||||
def __init__(self, data=None, ctrl=None):
|
||||
class XgmiiFrame:
|
||||
def __init__(self, data=None, ctrl=None, tx_complete=None):
|
||||
self.data = bytearray()
|
||||
self.ctrl = None
|
||||
self.rx_sim_time = None
|
||||
self.rx_start_lane = None
|
||||
self.sim_time_start = None
|
||||
self.sim_time_sfd = None
|
||||
self.sim_time_end = None
|
||||
self.start_lane = None
|
||||
self.tx_complete = None
|
||||
|
||||
if type(data) is XgmiiFrame:
|
||||
self.data = bytearray(data.data)
|
||||
self.ctrl = data.ctrl
|
||||
self.rx_sim_time = data.rx_sim_time
|
||||
self.rx_start_lane = data.rx_start_lane
|
||||
self.sim_time_start = data.sim_time_start
|
||||
self.sim_time_sfd = data.sim_time_sfd
|
||||
self.sim_time_end = data.sim_time_end
|
||||
self.start_lane = data.start_lane
|
||||
self.tx_complete = data.tx_complete
|
||||
else:
|
||||
self.data = bytearray(data)
|
||||
self.ctrl = ctrl
|
||||
|
||||
if tx_complete is not None:
|
||||
self.tx_complete = tx_complete
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload, min_len=60):
|
||||
def from_payload(cls, payload, min_len=60, tx_complete=None):
|
||||
payload = bytearray(payload)
|
||||
if len(payload) < min_len:
|
||||
payload.extend(bytearray(min_len-len(payload)))
|
||||
payload.extend(struct.pack('<L', zlib.crc32(payload)))
|
||||
return cls.from_raw_payload(payload)
|
||||
return cls.from_raw_payload(payload, tx_complete=tx_complete)
|
||||
|
||||
@classmethod
|
||||
def from_raw_payload(cls, payload):
|
||||
def from_raw_payload(cls, payload, tx_complete=None):
|
||||
data = bytearray(ETH_PREAMBLE)
|
||||
data.extend(payload)
|
||||
return cls(data)
|
||||
return cls(data, tx_complete=tx_complete)
|
||||
|
||||
def get_preamble_len(self):
|
||||
return self.data.index(EthPre.SFD)+1
|
||||
@@ -92,19 +102,27 @@ class XgmiiFrame(object):
|
||||
self.ctrl = [0]*n
|
||||
|
||||
def compact(self):
|
||||
if not any(self.ctrl):
|
||||
if self.ctrl is not None and not any(self.ctrl):
|
||||
self.ctrl = None
|
||||
|
||||
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):
|
||||
if type(other) is XgmiiFrame:
|
||||
return self.data == other.data
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"{type(self).__name__}(data={repr(self.data)}, "
|
||||
f"ctrl={repr(self.ctrl)}, "
|
||||
f"rx_sim_time={repr(self.rx_sim_time)}, "
|
||||
f"rx_start_lane={repr(self.rx_start_lane)})"
|
||||
f"{type(self).__name__}(data={self.data!r}, "
|
||||
f"ctrl={self.ctrl!r}, "
|
||||
f"sim_time_start={self.sim_time_start!r}, "
|
||||
f"sim_time_sfd={self.sim_time_sfd!r}, "
|
||||
f"sim_time_end={self.sim_time_end!r}, "
|
||||
f"start_lane={self.start_lane!r})"
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
@@ -113,10 +131,13 @@ class XgmiiFrame(object):
|
||||
def __iter__(self):
|
||||
return self.data.__iter__()
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes(self.data)
|
||||
|
||||
class XgmiiSource(object):
|
||||
|
||||
def __init__(self, data, ctrl, clock, reset=None, enable=None, *args, **kwargs):
|
||||
class XgmiiSource(Reset):
|
||||
|
||||
def __init__(self, data, ctrl, clock, reset=None, enable=None, reset_active_level=True, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.{data._path}")
|
||||
self.data = data
|
||||
self.ctrl = ctrl
|
||||
@@ -126,13 +147,18 @@ class XgmiiSource(object):
|
||||
|
||||
self.log.info("XGMII source")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020 Alex Forencich")
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.active = False
|
||||
self.queue = deque()
|
||||
self.queue = Queue()
|
||||
self.dequeue_event = Event()
|
||||
self.current_frame = None
|
||||
self.idle_event = Event()
|
||||
self.idle_event.set()
|
||||
self.active_event = Event()
|
||||
|
||||
self.enable_dic = True
|
||||
self.ifg = 12
|
||||
@@ -141,69 +167,129 @@ class XgmiiSource(object):
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
self.queue_occupancy_limit_bytes = -1
|
||||
self.queue_occupancy_limit_frames = -1
|
||||
|
||||
self.width = len(self.data)
|
||||
self.byte_width = len(self.ctrl)
|
||||
self.byte_size = 8
|
||||
self.byte_lanes = len(self.ctrl)
|
||||
|
||||
self.reset = reset
|
||||
assert self.width == self.byte_lanes * self.byte_size
|
||||
|
||||
assert self.width == self.byte_width * 8
|
||||
self.log.info("XGMII source model configuration")
|
||||
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.idle_d = 0
|
||||
self.idle_c = 0
|
||||
|
||||
for k in range(self.byte_width):
|
||||
for k in range(self.byte_lanes):
|
||||
self.idle_d |= XgmiiCtrl.IDLE << k*8
|
||||
self.idle_c |= 1 << k
|
||||
|
||||
self.data.setimmediatevalue(0)
|
||||
self.ctrl.setimmediatevalue(0)
|
||||
|
||||
cocotb.fork(self._run())
|
||||
self._run_cr = None
|
||||
|
||||
def send(self, frame):
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
async def send(self, frame):
|
||||
while self.full():
|
||||
self.dequeue_event.clear()
|
||||
await self.dequeue_event.wait()
|
||||
frame = XgmiiFrame(frame)
|
||||
await self.queue.put(frame)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
def send_nowait(self, frame):
|
||||
if self.full():
|
||||
raise QueueFull()
|
||||
frame = XgmiiFrame(frame)
|
||||
self.queue.put_nowait(frame)
|
||||
self.idle_event.clear()
|
||||
self.active_event.set()
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
self.queue.append(frame)
|
||||
|
||||
def count(self):
|
||||
return len(self.queue)
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return not self.queue
|
||||
return self.queue.empty()
|
||||
|
||||
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
|
||||
|
||||
def clear(self):
|
||||
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):
|
||||
while not self.idle():
|
||||
await RisingEdge(self.clock)
|
||||
await self.idle_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
self.data.value = 0
|
||||
self.ctrl.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
|
||||
|
||||
if self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
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):
|
||||
frame = None
|
||||
frame_offset = 0
|
||||
ifg_cnt = 0
|
||||
deficit_idle_cnt = 0
|
||||
self.active = False
|
||||
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
enable_event = None
|
||||
if self.enable is not None:
|
||||
enable_event = RisingEdge(self.enable)
|
||||
|
||||
while True:
|
||||
await ReadOnly()
|
||||
await clock_edge_event
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
frame = None
|
||||
ifg_cnt = 0
|
||||
deficit_idle_cnt = 0
|
||||
self.active = False
|
||||
self.data <= 0
|
||||
self.ctrl <= 0
|
||||
continue
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
|
||||
if self.enable is None or self.enable.value:
|
||||
if ifg_cnt + deficit_idle_cnt > self.byte_width-1 or (not self.enable_dic and ifg_cnt > 4):
|
||||
if self.enable is None or int(self.enable.value):
|
||||
if ifg_cnt + deficit_idle_cnt > self.byte_lanes-1 or (not self.enable_dic and ifg_cnt > 4):
|
||||
# in IFG
|
||||
ifg_cnt = ifg_cnt - self.byte_width
|
||||
ifg_cnt = ifg_cnt - self.byte_lanes
|
||||
if ifg_cnt < 0:
|
||||
if self.enable_dic:
|
||||
deficit_idle_cnt = max(deficit_idle_cnt+ifg_cnt, 0)
|
||||
@@ -211,13 +297,19 @@ class XgmiiSource(object):
|
||||
|
||||
elif frame is None:
|
||||
# idle
|
||||
if self.queue:
|
||||
if not self.queue.empty():
|
||||
# send frame
|
||||
frame = self.queue.popleft()
|
||||
frame = self.queue.get_nowait()
|
||||
self.dequeue_event.set()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
self.current_frame = frame
|
||||
frame.sim_time_start = get_sim_time()
|
||||
frame.sim_time_sfd = None
|
||||
frame.sim_time_end = None
|
||||
self.log.info("TX frame: %s", frame)
|
||||
frame.normalize()
|
||||
frame.start_lane = 0
|
||||
assert frame.data[0] == EthPre.PRE
|
||||
assert frame.ctrl[0] == 0
|
||||
frame.data[0] = XgmiiCtrl.START
|
||||
@@ -231,8 +323,9 @@ class XgmiiSource(object):
|
||||
else:
|
||||
min_ifg = 0
|
||||
|
||||
if self.byte_width > 4 and (ifg_cnt > min_ifg or self.force_offset_start):
|
||||
if self.byte_lanes > 4 and (ifg_cnt > min_ifg or self.force_offset_start):
|
||||
ifg_cnt = ifg_cnt-4
|
||||
frame.start_lane = 4
|
||||
frame.data = bytearray([XgmiiCtrl.IDLE]*4)+frame.data
|
||||
frame.ctrl = [1]*4+frame.ctrl
|
||||
|
||||
@@ -240,6 +333,7 @@ class XgmiiSource(object):
|
||||
deficit_idle_cnt = max(deficit_idle_cnt+ifg_cnt, 0)
|
||||
ifg_cnt = 0
|
||||
self.active = True
|
||||
frame_offset = 0
|
||||
else:
|
||||
# clear counters
|
||||
deficit_idle_cnt = 0
|
||||
@@ -249,29 +343,44 @@ class XgmiiSource(object):
|
||||
d_val = 0
|
||||
c_val = 0
|
||||
|
||||
for k in range(self.byte_width):
|
||||
for k in range(self.byte_lanes):
|
||||
if frame is not None:
|
||||
d_val |= frame.data.pop(0) << k*8
|
||||
c_val |= frame.ctrl.pop(0) << k
|
||||
d = frame.data[frame_offset]
|
||||
if frame.sim_time_sfd is None and d == EthPre.SFD:
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
d_val |= d << k*8
|
||||
c_val |= frame.ctrl[frame_offset] << k
|
||||
frame_offset += 1
|
||||
|
||||
if not frame.data:
|
||||
ifg_cnt = max(self.ifg - (self.byte_width-k), 0)
|
||||
if frame_offset >= len(frame.data):
|
||||
ifg_cnt = max(self.ifg - (self.byte_lanes-k), 0)
|
||||
frame.sim_time_end = get_sim_time()
|
||||
frame.handle_tx_complete()
|
||||
frame = None
|
||||
self.current_frame = None
|
||||
else:
|
||||
d_val |= XgmiiCtrl.IDLE << k*8
|
||||
c_val |= 1 << k
|
||||
|
||||
self.data <= d_val
|
||||
self.ctrl <= c_val
|
||||
self.data.value = d_val
|
||||
self.ctrl.value = c_val
|
||||
else:
|
||||
self.data <= self.idle_d
|
||||
self.ctrl <= self.idle_c
|
||||
self.data.value = self.idle_d
|
||||
self.ctrl.value = self.idle_c
|
||||
self.active = False
|
||||
|
||||
if ifg_cnt == 0 and self.queue.empty():
|
||||
self.idle_event.set()
|
||||
self.active_event.clear()
|
||||
await self.active_event.wait()
|
||||
|
||||
class XgmiiSink(object):
|
||||
elif self.enable is not None and not self.enable.value:
|
||||
await enable_event
|
||||
|
||||
def __init__(self, data, ctrl, clock, reset=None, enable=None, *args, **kwargs):
|
||||
|
||||
class XgmiiSink(Reset):
|
||||
|
||||
def __init__(self, data, ctrl, clock, reset=None, enable=None, reset_active_level=True, *args, **kwargs):
|
||||
self.log = logging.getLogger(f"cocotb.{data._path}")
|
||||
self.data = data
|
||||
self.ctrl = ctrl
|
||||
@@ -281,77 +390,117 @@ class XgmiiSink(object):
|
||||
|
||||
self.log.info("XGMII sink")
|
||||
self.log.info("cocotbext-eth version %s", __version__)
|
||||
self.log.info("Copyright (c) 2020 Alex Forencich")
|
||||
self.log.info("Copyright (c) 2020-2025 Alex Forencich")
|
||||
self.log.info("https://github.com/alexforencich/cocotbext-eth")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.active = False
|
||||
self.queue = deque()
|
||||
self.sync = Event()
|
||||
self.queue = Queue()
|
||||
self.active_event = Event()
|
||||
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
self.width = len(self.data)
|
||||
self.byte_width = len(self.ctrl)
|
||||
self.byte_size = 8
|
||||
self.byte_lanes = len(self.ctrl)
|
||||
|
||||
self.reset = reset
|
||||
assert self.width == self.byte_lanes * self.byte_size
|
||||
|
||||
assert self.width == self.byte_width * 8
|
||||
self.log.info("XGMII sink model configuration")
|
||||
self.log.info(" Byte size: %d bits", self.byte_size)
|
||||
self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes)
|
||||
|
||||
cocotb.fork(self._run())
|
||||
self._run_cr = None
|
||||
|
||||
def recv(self):
|
||||
if self.queue:
|
||||
frame = self.queue.popleft()
|
||||
self._init_reset(reset, reset_active_level)
|
||||
|
||||
def _recv(self, frame, compact=True):
|
||||
if self.queue.empty():
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes -= len(frame)
|
||||
self.queue_occupancy_frames -= 1
|
||||
if compact:
|
||||
frame.compact()
|
||||
return frame
|
||||
return None
|
||||
|
||||
async def recv(self, compact=True):
|
||||
frame = await self.queue.get()
|
||||
return self._recv(frame, compact)
|
||||
|
||||
def recv_nowait(self, compact=True):
|
||||
frame = self.queue.get_nowait()
|
||||
return self._recv(frame, compact)
|
||||
|
||||
def count(self):
|
||||
return len(self.queue)
|
||||
return self.queue.qsize()
|
||||
|
||||
def empty(self):
|
||||
return not self.queue
|
||||
return self.queue.empty()
|
||||
|
||||
def idle(self):
|
||||
return not self.active
|
||||
|
||||
def clear(self):
|
||||
while not self.queue.empty():
|
||||
self.queue.get_nowait()
|
||||
self.active_event.clear()
|
||||
self.queue_occupancy_bytes = 0
|
||||
self.queue_occupancy_frames = 0
|
||||
|
||||
async def wait(self, timeout=0, timeout_unit=None):
|
||||
if not self.empty():
|
||||
return
|
||||
self.sync.clear()
|
||||
if timeout:
|
||||
await First(self.sync.wait(), Timer(timeout, timeout_unit))
|
||||
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||
else:
|
||||
await self.sync.wait()
|
||||
await self.active_event.wait()
|
||||
|
||||
def _handle_reset(self, state):
|
||||
if state:
|
||||
self.log.info("Reset asserted")
|
||||
if self._run_cr is not None:
|
||||
self._run_cr.cancel()
|
||||
self._run_cr = None
|
||||
|
||||
self.active = False
|
||||
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):
|
||||
frame = None
|
||||
self.active = False
|
||||
|
||||
clock_edge_event = RisingEdge(self.clock)
|
||||
|
||||
active_event = First(Edge(self.data), Edge(self.ctrl))
|
||||
|
||||
enable_event = None
|
||||
if self.enable is not None:
|
||||
enable_event = RisingEdge(self.enable)
|
||||
|
||||
idle_d = sum([XgmiiCtrl.IDLE << n*8 for n in range(self.byte_lanes)])
|
||||
idle_c = 2**self.byte_lanes-1
|
||||
|
||||
while True:
|
||||
await ReadOnly()
|
||||
await clock_edge_event
|
||||
|
||||
if self.reset is not None and self.reset.value:
|
||||
await RisingEdge(self.clock)
|
||||
frame = None
|
||||
self.active = False
|
||||
continue
|
||||
|
||||
if self.enable is None or self.enable.value:
|
||||
for offset in range(self.byte_width):
|
||||
d_val = (self.data.value.integer >> (offset*8)) & 0xff
|
||||
c_val = (self.ctrl.value.integer >> offset) & 1
|
||||
if self.enable is None or int(self.enable.value):
|
||||
data_val = int(self.data.value)
|
||||
ctrl_val = int(self.ctrl.value)
|
||||
for offset in range(self.byte_lanes):
|
||||
d_val = (data_val >> (offset*8)) & 0xff
|
||||
c_val = (ctrl_val >> offset) & 1
|
||||
|
||||
if frame is None:
|
||||
if c_val and d_val == XgmiiCtrl.START:
|
||||
# start
|
||||
frame = XgmiiFrame(bytearray([EthPre.PRE]), [0])
|
||||
frame.rx_sim_time = get_sim_time()
|
||||
frame.rx_start_lane = offset
|
||||
frame.sim_time_start = get_sim_time()
|
||||
frame.start_lane = offset
|
||||
else:
|
||||
if c_val:
|
||||
# got a control character; terminate frame reception
|
||||
@@ -361,17 +510,25 @@ class XgmiiSink(object):
|
||||
frame.ctrl.append(c_val)
|
||||
|
||||
frame.compact()
|
||||
frame.sim_time_end = get_sim_time()
|
||||
self.log.info("RX frame: %s", frame)
|
||||
|
||||
self.queue_occupancy_bytes += len(frame)
|
||||
self.queue_occupancy_frames += 1
|
||||
|
||||
self.queue.append(frame)
|
||||
self.sync.set()
|
||||
self.queue.put_nowait(frame)
|
||||
self.active_event.set()
|
||||
|
||||
frame = None
|
||||
else:
|
||||
if frame.sim_time_sfd is None and d_val == EthPre.SFD:
|
||||
frame.sim_time_sfd = get_sim_time()
|
||||
|
||||
frame.data.append(d_val)
|
||||
frame.ctrl.append(c_val)
|
||||
|
||||
await RisingEdge(self.clock)
|
||||
if data_val == idle_d and ctrl_val == idle_c:
|
||||
await active_event
|
||||
|
||||
elif self.enable is not None and not self.enable.value:
|
||||
await enable_event
|
||||
|
||||
40
setup.cfg
40
setup.cfg
@@ -13,21 +13,22 @@ project_urls =
|
||||
Source Code = https://github.com/alexforencich/cocotbext-eth
|
||||
download_url = https://github.com/alexforencich/cocotbext-eth/tarball/master
|
||||
long_description = file: README.md
|
||||
long-description-content-type = text/markdown
|
||||
long_description_content_type = text/markdown
|
||||
platforms = any
|
||||
classifiers =
|
||||
Development Status :: 3 - Alpha
|
||||
Programming Language :: Python :: 3
|
||||
Framework :: cocotb
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python :: 3
|
||||
Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
||||
|
||||
[options]
|
||||
packages = find_namespace:
|
||||
python_requires = >=3.6
|
||||
install_requires =
|
||||
cocotb
|
||||
cocotbext-axi
|
||||
cocotb >= 1.6.0
|
||||
cocotbext-axi >= 0.1.16
|
||||
|
||||
[options.extras_require]
|
||||
test =
|
||||
@@ -46,32 +47,41 @@ addopts =
|
||||
|
||||
# tox configuration
|
||||
[tox:tox]
|
||||
envlist = py36, py37, py38, py39
|
||||
envlist = py38, py39, py310, py311, py312, py313
|
||||
skip_missing_interpreters = true
|
||||
minversion = 3.18.0
|
||||
requires = virtualenv >= 16.1
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.6: py36
|
||||
3.7: py37
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311
|
||||
3.12: py312
|
||||
3.13: py313
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
COVERAGE=1
|
||||
usedevelop = True
|
||||
|
||||
deps =
|
||||
pytest
|
||||
pytest-xdist
|
||||
cocotb-test
|
||||
coverage
|
||||
pytest-cov
|
||||
git+https://github.com/cocotb/cocotb.git@e892a3ea48#egg=cocotb
|
||||
pytest == 8.3.4
|
||||
pytest-xdist == 3.6.1
|
||||
cocotb == 1.9.2
|
||||
cocotb-bus == 0.2.1
|
||||
cocotb-test == 0.2.6
|
||||
cocotbext-axi == 0.1.26
|
||||
coverage == 7.0.5
|
||||
pytest-cov == 4.0.0
|
||||
|
||||
commands =
|
||||
pytest --cov=cocotbext --cov=tests --cov-branch -n auto
|
||||
pytest --cov=cocotbext --cov=tests --cov-branch {posargs:-n auto --verbose}
|
||||
bash -c 'find . -type f -name "\.coverage" | xargs coverage combine --append'
|
||||
coverage report
|
||||
|
||||
whitelist_externals =
|
||||
allowlist_externals =
|
||||
bash
|
||||
|
||||
# combine if paths are different
|
||||
|
||||
57
tests/eth_mac/Makefile
Normal file
57
tests/eth_mac/Makefile
Normal file
@@ -0,0 +1,57 @@
|
||||
# Copyright (c) 2021-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
TOPLEVEL_LANG = verilog
|
||||
|
||||
SIM ?= icarus
|
||||
WAVES ?= 0
|
||||
|
||||
COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_eth_mac
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
# module parameters
|
||||
export PARAM_PTP_TS_WIDTH := 96
|
||||
export PARAM_PTP_TAG_WIDTH := 16
|
||||
export PARAM_AXIS_DATA_WIDTH := 64
|
||||
export PARAM_AXIS_KEEP_WIDTH := $(shell expr $(PARAM_AXIS_DATA_WIDTH) / 8 )
|
||||
export PARAM_AXIS_TX_USER_WIDTH := $(shell expr $(PARAM_PTP_TAG_WIDTH) + 1 )
|
||||
export PARAM_AXIS_RX_USER_WIDTH := $(shell expr $(PARAM_PTP_TS_WIDTH) + 1 )
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
228
tests/eth_mac/test_eth_mac.py
Normal file
228
tests/eth_mac/test_eth_mac.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2021-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
|
||||
import cocotb_test.simulator
|
||||
import pytest
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.eth import EthMacFrame, EthMac, PtpClockSimTime
|
||||
from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink
|
||||
|
||||
|
||||
class TB:
|
||||
def __init__(self, dut, speed=10e9):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
if len(dut.tx_axis_tdata) == 8:
|
||||
clk_period = 8
|
||||
elif len(dut.tx_axis_tdata) == 32:
|
||||
clk_period = 3.102
|
||||
elif len(dut.tx_axis_tdata) == 64:
|
||||
if speed == 25e9:
|
||||
clk_period = 2.56
|
||||
else:
|
||||
clk_period = 6.206
|
||||
elif len(dut.tx_axis_tdata) == 512:
|
||||
clk_period = 3.102
|
||||
|
||||
cocotb.start_soon(Clock(dut.tx_clk, clk_period, units="ns").start())
|
||||
cocotb.start_soon(Clock(dut.rx_clk, clk_period, units="ns").start())
|
||||
|
||||
self.mac = EthMac(
|
||||
tx_clk=dut.tx_clk,
|
||||
tx_rst=dut.tx_rst,
|
||||
tx_bus=AxiStreamBus.from_prefix(dut, "tx_axis"),
|
||||
tx_ptp_time=dut.tx_ptp_time,
|
||||
tx_ptp_ts=dut.tx_ptp_ts,
|
||||
tx_ptp_ts_tag=dut.tx_ptp_ts_tag,
|
||||
tx_ptp_ts_valid=dut.tx_ptp_ts_valid,
|
||||
rx_clk=dut.rx_clk,
|
||||
rx_rst=dut.rx_rst,
|
||||
rx_bus=AxiStreamBus.from_prefix(dut, "rx_axis"),
|
||||
rx_ptp_time=dut.rx_ptp_time,
|
||||
ifg=12, speed=speed
|
||||
)
|
||||
|
||||
self.tx_ptp = PtpClockSimTime(
|
||||
ts_tod=dut.tx_ptp_time,
|
||||
clock=dut.tx_clk
|
||||
)
|
||||
|
||||
self.rx_ptp = PtpClockSimTime(
|
||||
ts_tod=dut.rx_ptp_time,
|
||||
clock=dut.rx_clk
|
||||
)
|
||||
|
||||
self.source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "tx_axis"), dut.tx_clk, dut.tx_rst)
|
||||
self.sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "rx_axis"), dut.rx_clk, dut.rx_rst)
|
||||
|
||||
async def reset(self):
|
||||
self.dut.tx_rst.setimmediatevalue(0)
|
||||
self.dut.rx_rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.tx_clk)
|
||||
await RisingEdge(self.dut.tx_clk)
|
||||
self.dut.tx_rst.value = 1
|
||||
self.dut.rx_rst.value = 1
|
||||
await RisingEdge(self.dut.tx_clk)
|
||||
await RisingEdge(self.dut.tx_clk)
|
||||
self.dut.tx_rst.value = 0
|
||||
self.dut.rx_rst.value = 0
|
||||
await RisingEdge(self.dut.tx_clk)
|
||||
await RisingEdge(self.dut.tx_clk)
|
||||
|
||||
|
||||
async def run_test_tx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=10e9):
|
||||
|
||||
tb = TB(dut, speed)
|
||||
|
||||
tb.mac.tx.ifg = ifg
|
||||
tb.mac.rx.ifg = ifg
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = EthMacFrame.from_payload(test_data)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.mac.tx.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
|
||||
assert tb.mac.tx.empty()
|
||||
|
||||
await RisingEdge(dut.tx_clk)
|
||||
await RisingEdge(dut.tx_clk)
|
||||
|
||||
|
||||
async def run_test_rx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=10e9):
|
||||
|
||||
tb = TB(dut, speed)
|
||||
|
||||
tb.mac.tx.ifg = ifg
|
||||
tb.mac.rx.ifg = ifg
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = EthMacFrame.from_payload(test_data)
|
||||
await tb.mac.rx.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
check_frame = EthMacFrame(rx_frame.tdata)
|
||||
|
||||
assert check_frame.get_payload() == test_data
|
||||
assert check_frame.check_fcs()
|
||||
|
||||
assert tb.sink.empty()
|
||||
|
||||
await RisingEdge(dut.rx_clk)
|
||||
await RisingEdge(dut.rx_clk)
|
||||
|
||||
|
||||
def size_list():
|
||||
return list(range(60, 128)) + [512, 1514, 9214] + [60]*10
|
||||
|
||||
|
||||
def incrementing_payload(length):
|
||||
return bytearray(itertools.islice(itertools.cycle(range(256)), length))
|
||||
|
||||
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
if len(cocotb.top.tx_axis_tdata) == 8:
|
||||
speed = [100e6, 1e9]
|
||||
elif len(cocotb.top.tx_axis_tdata) == 32:
|
||||
speed = [10e9]
|
||||
elif len(cocotb.top.tx_axis_tdata) == 64:
|
||||
speed = [10e9, 25e9]
|
||||
elif len(cocotb.top.tx_axis_tdata) == 512:
|
||||
speed = [100e9]
|
||||
|
||||
for test in [run_test_tx, run_test_rx]:
|
||||
|
||||
factory = TestFactory(test)
|
||||
factory.add_option("payload_lengths", [size_list])
|
||||
factory.add_option("payload_data", [incrementing_payload])
|
||||
factory.add_option("speed", speed)
|
||||
factory.generate_tests()
|
||||
|
||||
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data_width", [8, 32, 64, 512])
|
||||
def test_eth_mac(request, data_width):
|
||||
dut = "test_eth_mac"
|
||||
module = os.path.splitext(os.path.basename(__file__))[0]
|
||||
toplevel = dut
|
||||
|
||||
verilog_sources = [
|
||||
os.path.join(tests_dir, f"{dut}.v"),
|
||||
]
|
||||
|
||||
parameters = {}
|
||||
|
||||
parameters['PTP_TS_WIDTH'] = 96
|
||||
parameters['PTP_TAG_WIDTH'] = 16
|
||||
parameters['AXIS_DATA_WIDTH'] = data_width
|
||||
parameters['AXIS_KEEP_WIDTH'] = parameters['AXIS_DATA_WIDTH'] // 8
|
||||
parameters['AXIS_TX_USER_WIDTH'] = parameters['PTP_TAG_WIDTH']+1
|
||||
parameters['AXIS_RX_USER_WIDTH'] = parameters['PTP_TS_WIDTH']+1
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
verilog_sources=verilog_sources,
|
||||
toplevel=toplevel,
|
||||
module=module,
|
||||
parameters=parameters,
|
||||
sim_build=sim_build,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
65
tests/eth_mac/test_eth_mac.v
Normal file
65
tests/eth_mac/test_eth_mac.v
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2021-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Language: Verilog 2001
|
||||
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
/*
|
||||
* Ethernet MAC model test
|
||||
*/
|
||||
module test_eth_mac #
|
||||
(
|
||||
parameter PTP_TS_WIDTH = 96,
|
||||
parameter PTP_TAG_WIDTH = 16,
|
||||
parameter AXIS_DATA_WIDTH = 64,
|
||||
parameter AXIS_KEEP_WIDTH = (AXIS_DATA_WIDTH/8),
|
||||
parameter AXIS_TX_USER_WIDTH = PTP_TAG_WIDTH+1,
|
||||
parameter AXIS_RX_USER_WIDTH = PTP_TS_WIDTH+1
|
||||
)
|
||||
(
|
||||
inout wire tx_clk,
|
||||
inout wire tx_rst,
|
||||
inout wire [AXIS_DATA_WIDTH-1:0] tx_axis_tdata,
|
||||
inout wire [AXIS_KEEP_WIDTH-1:0] tx_axis_tkeep,
|
||||
inout wire tx_axis_tlast,
|
||||
inout wire [AXIS_TX_USER_WIDTH-1:0] tx_axis_tuser,
|
||||
inout wire tx_axis_tvalid,
|
||||
inout wire tx_axis_tready,
|
||||
inout wire [PTP_TS_WIDTH-1:0] tx_ptp_time,
|
||||
inout wire [PTP_TS_WIDTH-1:0] tx_ptp_ts,
|
||||
inout wire [PTP_TAG_WIDTH-1:0] tx_ptp_ts_tag,
|
||||
inout wire tx_ptp_ts_valid,
|
||||
|
||||
inout wire rx_clk,
|
||||
inout wire rx_rst,
|
||||
inout wire [AXIS_DATA_WIDTH-1:0] rx_axis_tdata,
|
||||
inout wire [AXIS_KEEP_WIDTH-1:0] rx_axis_tkeep,
|
||||
inout wire rx_axis_tlast,
|
||||
inout wire [AXIS_RX_USER_WIDTH-1:0] rx_axis_tuser,
|
||||
inout wire rx_axis_tvalid,
|
||||
inout wire [PTP_TS_WIDTH-1:0] rx_ptp_time
|
||||
);
|
||||
|
||||
endmodule
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2020 Alex Forencich
|
||||
# Copyright (c) 2020-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,39 +27,23 @@ COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_gmii
|
||||
TOPLEVEL = $(DUT)
|
||||
MODULE = $(DUT)
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
SIM_BUILD ?= sim_build_$(MODULE)
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
VERILOG_SOURCES += iverilog_dump.v
|
||||
COMPILE_ARGS += -s iverilog_dump
|
||||
endif
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
iverilog_dump.v:
|
||||
echo 'module iverilog_dump();' > $@
|
||||
echo 'initial begin' >> $@
|
||||
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
|
||||
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
|
||||
echo 'end' >> $@
|
||||
echo 'endmodule' >> $@
|
||||
|
||||
clean::
|
||||
@rm -rf sim_build_*
|
||||
@rm -rf iverilog_dump.v
|
||||
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -37,7 +37,7 @@ from cocotb.regression import TestFactory
|
||||
from cocotbext.eth import GmiiFrame, GmiiSource, GmiiSink
|
||||
|
||||
|
||||
class TB(object):
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
@@ -47,7 +47,7 @@ class TB(object):
|
||||
self._enable_generator = None
|
||||
self._enable_cr = None
|
||||
|
||||
cocotb.fork(Clock(dut.clk, 2, units="ns").start())
|
||||
cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
|
||||
|
||||
self.source = GmiiSource(dut.gmii_d, dut.gmii_er, dut.gmii_en,
|
||||
dut.clk, dut.rst, dut.gmii_clk_en, dut.gmii_mii_sel)
|
||||
@@ -61,30 +61,32 @@ class TB(object):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst <= 1
|
||||
self.dut.rst.value = 1
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst <= 0
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
def set_enable_generator(self, generator=None):
|
||||
if self._enable_cr is not None:
|
||||
self._enable_cr.kill()
|
||||
self._enable_cr.cancel()
|
||||
self._enable_cr = None
|
||||
|
||||
self._enable_generator = generator
|
||||
|
||||
if self._enable_generator is not None:
|
||||
self._enable_cr = cocotb.fork(self._run_enable())
|
||||
self._enable_cr = cocotb.start_soon(self._run_enable())
|
||||
|
||||
def clear_enable_generator(self):
|
||||
self.set_enable_generator(None)
|
||||
|
||||
async def _run_enable(self):
|
||||
clock_edge_event = RisingEdge(self.dut.clk)
|
||||
|
||||
for val in self._enable_generator:
|
||||
self.dut.gmii_clk_en <= val
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.gmii_clk_en.value = val
|
||||
await clock_edge_event
|
||||
|
||||
|
||||
async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_gen=None, mii_sel=False):
|
||||
@@ -92,7 +94,7 @@ async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_
|
||||
tb = TB(dut)
|
||||
|
||||
tb.source.ifg = ifg
|
||||
tb.dut.gmii_mii_sel <= mii_sel
|
||||
tb.dut.gmii_mii_sel.value = mii_sel
|
||||
|
||||
if enable_gen is not None:
|
||||
tb.set_enable_generator(enable_gen())
|
||||
@@ -103,11 +105,10 @@ async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
tb.source.send(test_frame)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
await tb.sink.wait()
|
||||
rx_frame = tb.sink.recv()
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
@@ -131,7 +132,7 @@ def cycle_en():
|
||||
return itertools.cycle([0, 0, 0, 1])
|
||||
|
||||
|
||||
if cocotb.SIM_NAME:
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
factory = TestFactory(run_test)
|
||||
factory.add_option("payload_lengths", [size_list])
|
||||
@@ -145,7 +146,6 @@ if cocotb.SIM_NAME:
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
|
||||
|
||||
|
||||
def test_gmii(request):
|
||||
@@ -161,8 +161,8 @@ def test_gmii(request):
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir,
|
||||
"sim_build_"+request.node.name.replace('[', '-').replace(']', ''))
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
49
tests/gmii_phy/Makefile
Normal file
49
tests/gmii_phy/Makefile
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2020-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
TOPLEVEL_LANG = verilog
|
||||
|
||||
SIM ?= icarus
|
||||
WAVES ?= 0
|
||||
|
||||
COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_gmii_phy
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
183
tests/gmii_phy/test_gmii_phy.py
Normal file
183
tests/gmii_phy/test_gmii_phy.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
|
||||
import cocotb_test.simulator
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.eth import GmiiFrame, GmiiSource, GmiiSink, GmiiPhy
|
||||
|
||||
|
||||
class TB:
|
||||
def __init__(self, dut, speed=1000e6):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
cocotb.start_soon(Clock(dut.phy_gtx_clk, 8, units="ns").start())
|
||||
|
||||
self.gmii_phy = GmiiPhy(dut.phy_txd, dut.phy_tx_er, dut.phy_tx_en, dut.phy_tx_clk, dut.phy_gtx_clk,
|
||||
dut.phy_rxd, dut.phy_rx_er, dut.phy_rx_dv, dut.phy_rx_clk, dut.phy_rst, speed=speed)
|
||||
|
||||
if speed == 1000e6:
|
||||
self.source = GmiiSource(dut.phy_txd, dut.phy_tx_er, dut.phy_tx_en, dut.phy_gtx_clk, dut.phy_rst)
|
||||
self.source.mii_mode = False
|
||||
self.sink = GmiiSink(dut.phy_rxd, dut.phy_rx_er, dut.phy_rx_dv, dut.phy_rx_clk, dut.phy_rst)
|
||||
self.sink.mii_mode = False
|
||||
else:
|
||||
self.source = GmiiSource(dut.phy_txd, dut.phy_tx_er, dut.phy_tx_en, dut.phy_tx_clk, dut.phy_rst)
|
||||
self.source.mii_mode = True
|
||||
self.sink = GmiiSink(dut.phy_rxd, dut.phy_rx_er, dut.phy_rx_dv, dut.phy_rx_clk, dut.phy_rst)
|
||||
self.sink.mii_mode = True
|
||||
|
||||
async def reset(self):
|
||||
self.dut.phy_rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
self.dut.phy_rst.value = 1
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
self.dut.phy_rst.value = 0
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
|
||||
|
||||
async def run_test_tx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=1000e6):
|
||||
|
||||
tb = TB(dut, speed)
|
||||
|
||||
tb.gmii_phy.rx.ifg = ifg
|
||||
tb.source.ifg = ifg
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.gmii_phy.tx.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
assert rx_frame.error is None
|
||||
|
||||
assert tb.gmii_phy.tx.empty()
|
||||
|
||||
await RisingEdge(dut.phy_tx_clk)
|
||||
await RisingEdge(dut.phy_tx_clk)
|
||||
|
||||
|
||||
async def run_test_rx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=1000e6):
|
||||
|
||||
tb = TB(dut, speed)
|
||||
|
||||
tb.gmii_phy.rx.ifg = ifg
|
||||
tb.source.ifg = ifg
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
await tb.gmii_phy.rx.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
assert rx_frame.error is None
|
||||
|
||||
assert tb.sink.empty()
|
||||
|
||||
await RisingEdge(dut.phy_rx_clk)
|
||||
await RisingEdge(dut.phy_rx_clk)
|
||||
|
||||
|
||||
def size_list():
|
||||
return list(range(60, 128)) + [512, 1514] + [60]*10
|
||||
|
||||
|
||||
def incrementing_payload(length):
|
||||
return bytearray(itertools.islice(itertools.cycle(range(256)), length))
|
||||
|
||||
|
||||
def cycle_en():
|
||||
return itertools.cycle([0, 0, 0, 1])
|
||||
|
||||
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
for test in [run_test_tx, run_test_rx]:
|
||||
|
||||
factory = TestFactory(test)
|
||||
factory.add_option("payload_lengths", [size_list])
|
||||
factory.add_option("payload_data", [incrementing_payload])
|
||||
factory.add_option("speed", [1000e6, 100e6, 10e6])
|
||||
factory.generate_tests()
|
||||
|
||||
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def test_gmii_phy(request):
|
||||
dut = "test_gmii_phy"
|
||||
module = os.path.splitext(os.path.basename(__file__))[0]
|
||||
toplevel = dut
|
||||
|
||||
verilog_sources = [
|
||||
os.path.join(tests_dir, f"{dut}.v"),
|
||||
]
|
||||
|
||||
parameters = {}
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
verilog_sources=verilog_sources,
|
||||
toplevel=toplevel,
|
||||
module=module,
|
||||
parameters=parameters,
|
||||
sim_build=sim_build,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
46
tests/gmii_phy/test_gmii_phy.v
Normal file
46
tests/gmii_phy/test_gmii_phy.v
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Language: Verilog 2001
|
||||
|
||||
`timescale 1ns / 1ns
|
||||
|
||||
/*
|
||||
* GMII PHY test
|
||||
*/
|
||||
module test_gmii_phy
|
||||
(
|
||||
inout wire phy_rst,
|
||||
inout wire [7:0] phy_txd,
|
||||
inout wire phy_tx_er,
|
||||
inout wire phy_tx_en,
|
||||
inout wire phy_tx_clk,
|
||||
inout wire phy_gtx_clk,
|
||||
inout wire [7:0] phy_rxd,
|
||||
inout wire phy_rx_er,
|
||||
inout wire phy_rx_dv,
|
||||
inout wire phy_rx_clk
|
||||
);
|
||||
|
||||
endmodule
|
||||
49
tests/mii/Makefile
Normal file
49
tests/mii/Makefile
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2020-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
TOPLEVEL_LANG = verilog
|
||||
|
||||
SIM ?= icarus
|
||||
WAVES ?= 0
|
||||
|
||||
COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_mii
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
172
tests/mii/test_mii.py
Normal file
172
tests/mii/test_mii.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
|
||||
import cocotb_test.simulator
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.eth import GmiiFrame, MiiSource, MiiSink
|
||||
|
||||
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
self._enable_generator = None
|
||||
self._enable_cr = None
|
||||
|
||||
cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
|
||||
|
||||
self.source = MiiSource(dut.mii_d, dut.mii_er, dut.mii_en,
|
||||
dut.clk, dut.rst, dut.mii_clk_en)
|
||||
self.sink = MiiSink(dut.mii_d, dut.mii_er, dut.mii_en,
|
||||
dut.clk, dut.rst, dut.mii_clk_en)
|
||||
|
||||
dut.mii_clk_en.setimmediatevalue(1)
|
||||
|
||||
async def reset(self):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst.value = 1
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
def set_enable_generator(self, generator=None):
|
||||
if self._enable_cr is not None:
|
||||
self._enable_cr.cancel()
|
||||
self._enable_cr = None
|
||||
|
||||
self._enable_generator = generator
|
||||
|
||||
if self._enable_generator is not None:
|
||||
self._enable_cr = cocotb.start_soon(self._run_enable())
|
||||
|
||||
def clear_enable_generator(self):
|
||||
self.set_enable_generator(None)
|
||||
|
||||
async def _run_enable(self):
|
||||
clock_edge_event = RisingEdge(self.dut.clk)
|
||||
|
||||
for val in self._enable_generator:
|
||||
self.dut.mii_clk_en.value = val
|
||||
await clock_edge_event
|
||||
|
||||
|
||||
async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_gen=None):
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
tb.source.ifg = ifg
|
||||
|
||||
if enable_gen is not None:
|
||||
tb.set_enable_generator(enable_gen())
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
assert rx_frame.error is None
|
||||
|
||||
assert tb.sink.empty()
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
|
||||
def size_list():
|
||||
return list(range(60, 128)) + [512, 1514, 9214] + [60]*10
|
||||
|
||||
|
||||
def incrementing_payload(length):
|
||||
return bytearray(itertools.islice(itertools.cycle(range(256)), length))
|
||||
|
||||
|
||||
def cycle_en():
|
||||
return itertools.cycle([0, 0, 0, 1])
|
||||
|
||||
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
factory = TestFactory(run_test)
|
||||
factory.add_option("payload_lengths", [size_list])
|
||||
factory.add_option("payload_data", [incrementing_payload])
|
||||
factory.add_option("ifg", [12, 0])
|
||||
factory.add_option("enable_gen", [None, cycle_en])
|
||||
factory.generate_tests()
|
||||
|
||||
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def test_mii(request):
|
||||
dut = "test_mii"
|
||||
module = os.path.splitext(os.path.basename(__file__))[0]
|
||||
toplevel = dut
|
||||
|
||||
verilog_sources = [
|
||||
os.path.join(tests_dir, f"{dut}.v"),
|
||||
]
|
||||
|
||||
parameters = {}
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
verilog_sources=verilog_sources,
|
||||
toplevel=toplevel,
|
||||
module=module,
|
||||
parameters=parameters,
|
||||
sim_build=sim_build,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
46
tests/mii/test_mii.v
Normal file
46
tests/mii/test_mii.v
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Language: Verilog 2001
|
||||
|
||||
`timescale 1ns / 1ns
|
||||
|
||||
/*
|
||||
* MII test
|
||||
*/
|
||||
module test_mii #
|
||||
(
|
||||
parameter DATA_WIDTH = 4
|
||||
)
|
||||
(
|
||||
input wire clk,
|
||||
input wire rst,
|
||||
|
||||
inout wire [DATA_WIDTH-1:0] mii_d,
|
||||
inout wire mii_er,
|
||||
inout wire mii_en,
|
||||
inout wire mii_clk_en
|
||||
);
|
||||
|
||||
endmodule
|
||||
49
tests/mii_phy/Makefile
Normal file
49
tests/mii_phy/Makefile
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2020 Alex-2025 Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
TOPLEVEL_LANG = verilog
|
||||
|
||||
SIM ?= icarus
|
||||
WAVES ?= 0
|
||||
|
||||
COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_mii_phy
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
174
tests/mii_phy/test_mii_phy.py
Normal file
174
tests/mii_phy/test_mii_phy.py
Normal file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
|
||||
import cocotb_test.simulator
|
||||
|
||||
import cocotb
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.eth import GmiiFrame, MiiSource, MiiSink, MiiPhy
|
||||
|
||||
|
||||
class TB:
|
||||
def __init__(self, dut, speed=100e6):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
self.mii_phy = MiiPhy(dut.phy_txd, dut.phy_tx_er, dut.phy_tx_en, dut.phy_tx_clk,
|
||||
dut.phy_rxd, dut.phy_rx_er, dut.phy_rx_dv, dut.phy_rx_clk, dut.phy_rst, speed=speed)
|
||||
|
||||
self.source = MiiSource(dut.phy_txd, dut.phy_tx_er, dut.phy_tx_en,
|
||||
dut.phy_tx_clk, dut.phy_rst)
|
||||
self.sink = MiiSink(dut.phy_rxd, dut.phy_rx_er, dut.phy_rx_dv,
|
||||
dut.phy_rx_clk, dut.phy_rst)
|
||||
|
||||
async def reset(self):
|
||||
self.dut.phy_rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
self.dut.phy_rst.value = 1
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
self.dut.phy_rst.value = 0
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
|
||||
|
||||
async def run_test_tx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=100e6):
|
||||
|
||||
tb = TB(dut, speed)
|
||||
|
||||
tb.mii_phy.rx.ifg = ifg
|
||||
tb.source.ifg = ifg
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.mii_phy.tx.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
assert rx_frame.error is None
|
||||
|
||||
assert tb.mii_phy.tx.empty()
|
||||
|
||||
await RisingEdge(dut.phy_tx_clk)
|
||||
await RisingEdge(dut.phy_tx_clk)
|
||||
|
||||
|
||||
async def run_test_rx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=100e6):
|
||||
|
||||
tb = TB(dut, speed)
|
||||
|
||||
tb.mii_phy.rx.ifg = ifg
|
||||
tb.source.ifg = ifg
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
await tb.mii_phy.rx.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
assert rx_frame.error is None
|
||||
|
||||
assert tb.sink.empty()
|
||||
|
||||
await RisingEdge(dut.phy_rx_clk)
|
||||
await RisingEdge(dut.phy_rx_clk)
|
||||
|
||||
|
||||
def size_list():
|
||||
return list(range(60, 128)) + [512, 1514] + [60]*10
|
||||
|
||||
|
||||
def incrementing_payload(length):
|
||||
return bytearray(itertools.islice(itertools.cycle(range(256)), length))
|
||||
|
||||
|
||||
def cycle_en():
|
||||
return itertools.cycle([0, 0, 0, 1])
|
||||
|
||||
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
for test in [run_test_tx, run_test_rx]:
|
||||
|
||||
factory = TestFactory(test)
|
||||
factory.add_option("payload_lengths", [size_list])
|
||||
factory.add_option("payload_data", [incrementing_payload])
|
||||
factory.add_option("speed", [100e6, 10e6])
|
||||
factory.generate_tests()
|
||||
|
||||
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def test_mii_phy(request):
|
||||
dut = "test_mii_phy"
|
||||
module = os.path.splitext(os.path.basename(__file__))[0]
|
||||
toplevel = dut
|
||||
|
||||
verilog_sources = [
|
||||
os.path.join(tests_dir, f"{dut}.v"),
|
||||
]
|
||||
|
||||
parameters = {}
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
verilog_sources=verilog_sources,
|
||||
toplevel=toplevel,
|
||||
module=module,
|
||||
parameters=parameters,
|
||||
sim_build=sim_build,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
45
tests/mii_phy/test_mii_phy.v
Normal file
45
tests/mii_phy/test_mii_phy.v
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Language: Verilog 2001
|
||||
|
||||
`timescale 1ns / 1ns
|
||||
|
||||
/*
|
||||
* MII PHY test
|
||||
*/
|
||||
module test_mii_phy
|
||||
(
|
||||
inout wire phy_rst,
|
||||
inout wire [3:0] phy_txd,
|
||||
inout wire phy_tx_er,
|
||||
inout wire phy_tx_en,
|
||||
inout wire phy_tx_clk,
|
||||
inout wire [3:0] phy_rxd,
|
||||
inout wire phy_rx_er,
|
||||
inout wire phy_rx_dv,
|
||||
inout wire phy_rx_clk
|
||||
);
|
||||
|
||||
endmodule
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2020 Alex Forencich
|
||||
# Copyright (c) 2020-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,39 +27,23 @@ COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ps
|
||||
|
||||
DUT = test_ptp_clock
|
||||
TOPLEVEL = $(DUT)
|
||||
MODULE = $(DUT)
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
SIM_BUILD ?= sim_build_$(MODULE)
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
VERILOG_SOURCES += iverilog_dump.v
|
||||
COMPILE_ARGS += -s iverilog_dump
|
||||
endif
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
iverilog_dump.v:
|
||||
echo 'module iverilog_dump();' > $@
|
||||
echo 'initial begin' >> $@
|
||||
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
|
||||
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
|
||||
echo 'end' >> $@
|
||||
echo 'endmodule' >> $@
|
||||
|
||||
clean::
|
||||
@rm -rf sim_build_*
|
||||
@rm -rf iverilog_dump.v
|
||||
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -25,29 +25,30 @@ THE SOFTWARE.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
import cocotb_test.simulator
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.triggers import RisingEdge, ClockCycles
|
||||
from cocotb.utils import get_sim_time
|
||||
|
||||
from cocotbext.eth import PtpClock
|
||||
|
||||
|
||||
class TB(object):
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
cocotb.fork(Clock(dut.clk, 6.4, units="ns").start())
|
||||
cocotb.start_soon(Clock(dut.clk, 6.4, units="ns").start())
|
||||
|
||||
self.ptp_clock = PtpClock(
|
||||
ts_96=dut.ts_96,
|
||||
ts_64=dut.ts_64,
|
||||
ts_tod=dut.ts_tod,
|
||||
ts_rel=dut.ts_rel,
|
||||
ts_step=dut.ts_step,
|
||||
pps=dut.pps,
|
||||
clock=dut.clk,
|
||||
@@ -59,13 +60,21 @@ class TB(object):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst <= 1
|
||||
self.dut.rst.value = 1
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst <= 0
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
def get_ts_tod_ns(self):
|
||||
ts = int(self.dut.ts_tod.value)
|
||||
return Decimal(ts >> 48).scaleb(9) + (Decimal(ts & 0xffffffffffff) / Decimal(2**16))
|
||||
|
||||
def get_ts_rel_ns(self):
|
||||
ts = int(self.dut.ts_rel.value)
|
||||
return Decimal(ts) / Decimal(2**16)
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def run_default_rate(dut):
|
||||
@@ -75,32 +84,32 @@ async def run_default_rate(dut):
|
||||
await tb.reset()
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
start_time = get_sim_time('sec')
|
||||
start_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
start_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
start_ts_tod = tb.get_ts_tod_ns()
|
||||
start_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
for k in range(10000):
|
||||
await RisingEdge(dut.clk)
|
||||
await ClockCycles(dut.clk, 10000)
|
||||
|
||||
stop_time = get_sim_time('sec')
|
||||
stop_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
stop_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
stop_ts_tod = tb.get_ts_tod_ns()
|
||||
stop_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
time_delta = stop_time-start_time
|
||||
ts_96_delta = stop_ts_96-start_ts_96
|
||||
ts_64_delta = stop_ts_64-start_ts_64
|
||||
ts_tod_delta = stop_ts_tod-start_ts_tod
|
||||
ts_rel_delta = stop_ts_rel-start_ts_rel
|
||||
|
||||
ts_96_diff = time_delta - ts_96_delta
|
||||
ts_64_diff = time_delta - ts_64_delta
|
||||
tb.log.info("sim time delta : %s ns", time_delta)
|
||||
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
|
||||
tb.log.info("rel ts delta : %s ns", ts_rel_delta)
|
||||
|
||||
tb.log.info("sim time delta : %g s", time_delta)
|
||||
tb.log.info("96 bit ts delta : %g s", ts_96_delta)
|
||||
tb.log.info("64 bit ts delta : %g s", ts_64_delta)
|
||||
tb.log.info("96 bit ts diff : %g s", ts_96_diff)
|
||||
tb.log.info("64 bit ts diff : %g s", ts_64_diff)
|
||||
ts_tod_diff = time_delta - ts_tod_delta
|
||||
ts_rel_diff = time_delta - ts_rel_delta
|
||||
|
||||
assert abs(ts_96_diff) < 1e-12
|
||||
assert abs(ts_64_diff) < 1e-12
|
||||
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
|
||||
tb.log.info("rel ts diff : %s ns", ts_rel_diff)
|
||||
|
||||
assert abs(ts_tod_diff) < 1e-3
|
||||
assert abs(ts_rel_diff) < 1e-3
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
@@ -113,41 +122,41 @@ async def run_load_timestamps(dut):
|
||||
|
||||
await tb.reset()
|
||||
|
||||
tb.ptp_clock.set_ts_96(12345678)
|
||||
tb.ptp_clock.set_ts_64(12345678)
|
||||
tb.ptp_clock.set_ts_tod_ns(12345678)
|
||||
tb.ptp_clock.set_ts_rel_ns(12345678)
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
assert dut.ts_96.value.integer == 12345678+((tb.ptp_clock.period_ns << 16) + tb.ptp_clock.period_fns)
|
||||
assert dut.ts_64.value.integer == 12345678+((tb.ptp_clock.period_ns << 16) + tb.ptp_clock.period_fns)
|
||||
assert dut.ts_step.value.integer == 1
|
||||
assert int(dut.ts_tod.value) == (12345678 << 16) + (tb.ptp_clock.period_ns << 16) + (tb.ptp_clock.period_fns >> 16)
|
||||
assert int(dut.ts_rel.value) == (12345678 << 16) + (tb.ptp_clock.period_ns << 16) + (tb.ptp_clock.period_fns >> 16)
|
||||
assert int(dut.ts_step.value) == 1
|
||||
|
||||
start_time = get_sim_time('sec')
|
||||
start_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
start_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
start_ts_tod = tb.get_ts_tod_ns()
|
||||
start_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
for k in range(2000):
|
||||
await RisingEdge(dut.clk)
|
||||
await ClockCycles(dut.clk, 2000)
|
||||
|
||||
stop_time = get_sim_time('sec')
|
||||
stop_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
stop_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
stop_ts_tod = tb.get_ts_tod_ns()
|
||||
stop_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
time_delta = stop_time-start_time
|
||||
ts_96_delta = stop_ts_96-start_ts_96
|
||||
ts_64_delta = stop_ts_64-start_ts_64
|
||||
ts_tod_delta = stop_ts_tod-start_ts_tod
|
||||
ts_rel_delta = stop_ts_rel-start_ts_rel
|
||||
|
||||
ts_96_diff = time_delta - ts_96_delta
|
||||
ts_64_diff = time_delta - ts_64_delta
|
||||
tb.log.info("sim time delta : %s ns", time_delta)
|
||||
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
|
||||
tb.log.info("rel ts delta : %s ns", ts_rel_delta)
|
||||
|
||||
tb.log.info("sim time delta : %g s", time_delta)
|
||||
tb.log.info("96 bit ts delta : %g s", ts_96_delta)
|
||||
tb.log.info("64 bit ts delta : %g s", ts_64_delta)
|
||||
tb.log.info("96 bit ts diff : %g s", ts_96_diff)
|
||||
tb.log.info("64 bit ts diff : %g s", ts_64_diff)
|
||||
ts_tod_diff = time_delta - ts_tod_delta
|
||||
ts_rel_diff = time_delta - ts_rel_delta
|
||||
|
||||
assert abs(ts_96_diff) < 1e-12
|
||||
assert abs(ts_64_diff) < 1e-12
|
||||
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
|
||||
tb.log.info("rel ts diff : %s ns", ts_rel_diff)
|
||||
|
||||
assert abs(ts_tod_diff) < 1e-3
|
||||
assert abs(ts_rel_diff) < 1e-3
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
@@ -160,45 +169,48 @@ async def run_seconds_increment(dut):
|
||||
|
||||
await tb.reset()
|
||||
|
||||
tb.ptp_clock.set_ts_96(999990000*2**16)
|
||||
tb.ptp_clock.set_ts_64(999990000*2**16)
|
||||
tb.ptp_clock.set_ts_tod_ns(999990000)
|
||||
tb.ptp_clock.set_ts_rel_ns(999990000)
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
start_time = get_sim_time('sec')
|
||||
start_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
start_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
start_ts_tod = tb.get_ts_tod_ns()
|
||||
start_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
saw_pps = False
|
||||
|
||||
for k in range(3000):
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
if dut.pps.value.integer:
|
||||
if int(dut.pps.value):
|
||||
saw_pps = True
|
||||
assert dut.ts_96.value.integer >> 48 == 1
|
||||
assert dut.ts_96.value.integer & 0xffffffffffff < 10*2**16
|
||||
assert int(dut.ts_tod.value) >> 48 == 1
|
||||
assert int(dut.ts_tod.value) & 0xffffffffffff < 10*2**16
|
||||
|
||||
assert saw_pps
|
||||
|
||||
stop_time = get_sim_time('sec')
|
||||
stop_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
stop_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
stop_ts_tod = tb.get_ts_tod_ns()
|
||||
stop_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
time_delta = stop_time-start_time
|
||||
ts_96_delta = stop_ts_96-start_ts_96
|
||||
ts_64_delta = stop_ts_64-start_ts_64
|
||||
ts_tod_delta = stop_ts_tod-start_ts_tod
|
||||
ts_rel_delta = stop_ts_rel-start_ts_rel
|
||||
|
||||
ts_96_diff = time_delta - ts_96_delta
|
||||
ts_64_diff = time_delta - ts_64_delta
|
||||
tb.log.info("sim time delta : %s ns", time_delta)
|
||||
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
|
||||
tb.log.info("rel ts delta : %s ns", ts_rel_delta)
|
||||
|
||||
tb.log.info("sim time delta : %g s", time_delta)
|
||||
tb.log.info("96 bit ts delta : %g s", ts_96_delta)
|
||||
tb.log.info("64 bit ts delta : %g s", ts_64_delta)
|
||||
tb.log.info("96 bit ts diff : %g s", ts_96_diff)
|
||||
tb.log.info("64 bit ts diff : %g s", ts_64_diff)
|
||||
ts_tod_diff = time_delta - ts_tod_delta
|
||||
ts_rel_diff = time_delta - ts_rel_delta
|
||||
|
||||
assert abs(ts_96_diff) < 1e-12
|
||||
assert abs(ts_64_diff) < 1e-12
|
||||
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
|
||||
tb.log.info("rel ts diff : %s ns", ts_rel_diff)
|
||||
|
||||
assert abs(ts_tod_diff) < 1e-3
|
||||
assert abs(ts_rel_diff) < 1e-3
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
@@ -211,36 +223,35 @@ async def run_frequency_adjustment(dut):
|
||||
|
||||
await tb.reset()
|
||||
|
||||
tb.ptp_clock.period_ns = 0x6
|
||||
tb.ptp_clock.period_fns = 0x6624
|
||||
tb.ptp_clock.set_period(0x6, 0x66240000)
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
start_time = get_sim_time('sec')
|
||||
start_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
start_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
start_ts_tod = tb.get_ts_tod_ns()
|
||||
start_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
for k in range(10000):
|
||||
await RisingEdge(dut.clk)
|
||||
await ClockCycles(dut.clk, 10000)
|
||||
|
||||
stop_time = get_sim_time('sec')
|
||||
stop_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
stop_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
stop_ts_tod = tb.get_ts_tod_ns()
|
||||
stop_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
time_delta = stop_time-start_time
|
||||
ts_96_delta = stop_ts_96-start_ts_96
|
||||
ts_64_delta = stop_ts_64-start_ts_64
|
||||
ts_tod_delta = stop_ts_tod-start_ts_tod
|
||||
ts_rel_delta = stop_ts_rel-start_ts_rel
|
||||
|
||||
ts_96_diff = time_delta - ts_96_delta * 6.4/(6+(0x6624+2/5)/2**16)
|
||||
ts_64_diff = time_delta - ts_64_delta * 6.4/(6+(0x6624+2/5)/2**16)
|
||||
tb.log.info("sim time delta : %s ns", time_delta)
|
||||
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
|
||||
tb.log.info("rel ts delta : %s ns", ts_rel_delta)
|
||||
|
||||
tb.log.info("sim time delta : %g s", time_delta)
|
||||
tb.log.info("96 bit ts delta : %g s", ts_96_delta)
|
||||
tb.log.info("64 bit ts delta : %g s", ts_64_delta)
|
||||
tb.log.info("96 bit ts diff : %g s", ts_96_diff)
|
||||
tb.log.info("64 bit ts diff : %g s", ts_64_diff)
|
||||
ts_tod_diff = time_delta - ts_tod_delta * Decimal(6.4)/tb.ptp_clock.get_period_ns()
|
||||
ts_rel_diff = time_delta - ts_rel_delta * Decimal(6.4)/tb.ptp_clock.get_period_ns()
|
||||
|
||||
assert abs(ts_96_diff) < 1e-12
|
||||
assert abs(ts_64_diff) < 1e-12
|
||||
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
|
||||
tb.log.info("rel ts diff : %s ns", ts_rel_diff)
|
||||
|
||||
assert abs(ts_tod_diff) < 1e-3
|
||||
assert abs(ts_rel_diff) < 1e-3
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
@@ -253,37 +264,35 @@ async def run_drift_adjustment(dut):
|
||||
|
||||
await tb.reset()
|
||||
|
||||
tb.ptp_clock.drift_ns = 0
|
||||
tb.ptp_clock.drift_fns = 20
|
||||
tb.ptp_clock.drift_rate = 5
|
||||
tb.ptp_clock.set_drift(20000, 5)
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
start_time = get_sim_time('sec')
|
||||
start_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
start_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
start_ts_tod = tb.get_ts_tod_ns()
|
||||
start_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
for k in range(10000):
|
||||
await RisingEdge(dut.clk)
|
||||
await ClockCycles(dut.clk, 10000)
|
||||
|
||||
stop_time = get_sim_time('sec')
|
||||
stop_ts_96 = (dut.ts_96.value.integer >> 48) + ((dut.ts_96.value.integer & 0xffffffffffff)/2**16*1e-9)
|
||||
stop_ts_64 = dut.ts_64.value.integer/2**16*1e-9
|
||||
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
stop_ts_tod = tb.get_ts_tod_ns()
|
||||
stop_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
time_delta = stop_time-start_time
|
||||
ts_96_delta = stop_ts_96-start_ts_96
|
||||
ts_64_delta = stop_ts_64-start_ts_64
|
||||
ts_tod_delta = stop_ts_tod-start_ts_tod
|
||||
ts_rel_delta = stop_ts_rel-start_ts_rel
|
||||
|
||||
ts_96_diff = time_delta - ts_96_delta * 6.4/(6+(0x6666+20/5)/2**16)
|
||||
ts_64_diff = time_delta - ts_64_delta * 6.4/(6+(0x6666+20/5)/2**16)
|
||||
tb.log.info("sim time delta : %s ns", time_delta)
|
||||
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
|
||||
tb.log.info("rel ts delta : %s ns", ts_rel_delta)
|
||||
|
||||
tb.log.info("sim time delta : %g s", time_delta)
|
||||
tb.log.info("96 bit ts delta : %g s", ts_96_delta)
|
||||
tb.log.info("64 bit ts delta : %g s", ts_64_delta)
|
||||
tb.log.info("96 bit ts diff : %g s", ts_96_diff)
|
||||
tb.log.info("64 bit ts diff : %g s", ts_64_diff)
|
||||
ts_tod_diff = time_delta - ts_tod_delta * Decimal(6.4)/tb.ptp_clock.get_period_ns()
|
||||
ts_rel_diff = time_delta - ts_rel_delta * Decimal(6.4)/tb.ptp_clock.get_period_ns()
|
||||
|
||||
assert abs(ts_96_diff) < 1e-12
|
||||
assert abs(ts_64_diff) < 1e-12
|
||||
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
|
||||
tb.log.info("rel ts diff : %s ns", ts_rel_diff)
|
||||
|
||||
assert abs(ts_tod_diff) < 1e-3
|
||||
assert abs(ts_rel_diff) < 1e-3
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
@@ -292,7 +301,6 @@ async def run_drift_adjustment(dut):
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
|
||||
|
||||
|
||||
def test_ptp_clock(request):
|
||||
@@ -308,8 +316,8 @@ def test_ptp_clock(request):
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir,
|
||||
"sim_build_"+request.node.name.replace('[', '-').replace(']', ''))
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -34,8 +34,8 @@ module test_ptp_clock
|
||||
input wire clk,
|
||||
input wire rst,
|
||||
|
||||
inout wire [95:0] ts_96,
|
||||
inout wire [63:0] ts_64,
|
||||
inout wire [95:0] ts_tod,
|
||||
inout wire [63:0] ts_rel,
|
||||
inout wire ts_step,
|
||||
inout wire pps
|
||||
);
|
||||
|
||||
49
tests/ptp_clock_sim_time/Makefile
Normal file
49
tests/ptp_clock_sim_time/Makefile
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2020-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
TOPLEVEL_LANG = verilog
|
||||
|
||||
SIM ?= icarus
|
||||
WAVES ?= 0
|
||||
|
||||
COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ps
|
||||
|
||||
DUT = test_ptp_clock_sim_time
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
134
tests/ptp_clock_sim_time/test_ptp_clock_sim_time.py
Normal file
134
tests/ptp_clock_sim_time/test_ptp_clock_sim_time.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2021-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
import cocotb_test.simulator
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge, ClockCycles
|
||||
from cocotb.utils import get_sim_time
|
||||
|
||||
from cocotbext.eth import PtpClockSimTime
|
||||
|
||||
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
cocotb.start_soon(Clock(dut.clk, 6.4, units="ns").start())
|
||||
|
||||
self.ptp_clock = PtpClockSimTime(
|
||||
ts_tod=dut.ts_tod,
|
||||
ts_rel=dut.ts_rel,
|
||||
pps=dut.pps,
|
||||
clock=dut.clk
|
||||
)
|
||||
|
||||
def get_ts_tod_ns(self):
|
||||
ts = int(self.dut.ts_tod.value)
|
||||
return Decimal(ts >> 48).scaleb(9) + (Decimal(ts & 0xffffffffffff) / Decimal(2**16))
|
||||
|
||||
def get_ts_rel_ns(self):
|
||||
ts = int(self.dut.ts_rel.value)
|
||||
return Decimal(ts) / Decimal(2**16)
|
||||
|
||||
|
||||
@cocotb.test()
|
||||
async def run_test(dut):
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
start_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
start_ts_tod = tb.get_ts_tod_ns()
|
||||
start_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
await ClockCycles(dut.clk, 10000)
|
||||
|
||||
stop_time = Decimal(get_sim_time('fs')).scaleb(-6)
|
||||
stop_ts_tod = tb.get_ts_tod_ns()
|
||||
stop_ts_rel = tb.get_ts_rel_ns()
|
||||
|
||||
time_delta = stop_time-start_time
|
||||
ts_tod_delta = stop_ts_tod-start_ts_tod
|
||||
ts_rel_delta = stop_ts_rel-start_ts_rel
|
||||
|
||||
tb.log.info("sim time delta : %s ns", time_delta)
|
||||
tb.log.info("ToD ts delta : %s ns", ts_tod_delta)
|
||||
tb.log.info("rel ts delta : %s ns", ts_rel_delta)
|
||||
|
||||
ts_tod_diff = time_delta - ts_tod_delta
|
||||
ts_rel_diff = time_delta - ts_rel_delta
|
||||
|
||||
tb.log.info("ToD ts diff : %s ns", ts_tod_diff)
|
||||
tb.log.info("rel ts diff : %s ns", ts_rel_diff)
|
||||
|
||||
assert abs(ts_tod_diff) < 1e-3
|
||||
assert abs(ts_rel_diff) < 1e-3
|
||||
|
||||
await RisingEdge(dut.clk)
|
||||
await RisingEdge(dut.clk)
|
||||
|
||||
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def test_ptp_clock(request):
|
||||
dut = "test_ptp_clock_sim_time"
|
||||
module = os.path.splitext(os.path.basename(__file__))[0]
|
||||
toplevel = dut
|
||||
|
||||
verilog_sources = [
|
||||
os.path.join(tests_dir, f"{dut}.v"),
|
||||
]
|
||||
|
||||
parameters = {}
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
verilog_sources=verilog_sources,
|
||||
toplevel=toplevel,
|
||||
module=module,
|
||||
parameters=parameters,
|
||||
sim_build=sim_build,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
41
tests/ptp_clock_sim_time/test_ptp_clock_sim_time.v
Normal file
41
tests/ptp_clock_sim_time/test_ptp_clock_sim_time.v
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2021-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Language: Verilog 2001
|
||||
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
/*
|
||||
* PTP clock test
|
||||
*/
|
||||
module test_ptp_clock_sim_time
|
||||
(
|
||||
input wire clk,
|
||||
|
||||
inout wire [95:0] ts_tod,
|
||||
inout wire [63:0] ts_rel,
|
||||
inout wire pps
|
||||
);
|
||||
|
||||
endmodule
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2020 Alex Forencich
|
||||
# Copyright (c) 2020-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,39 +27,23 @@ COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_rgmii
|
||||
TOPLEVEL = $(DUT)
|
||||
MODULE = $(DUT)
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
SIM_BUILD ?= sim_build_$(MODULE)
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
VERILOG_SOURCES += iverilog_dump.v
|
||||
COMPILE_ARGS += -s iverilog_dump
|
||||
endif
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
iverilog_dump.v:
|
||||
echo 'module iverilog_dump();' > $@
|
||||
echo 'initial begin' >> $@
|
||||
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
|
||||
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
|
||||
echo 'end' >> $@
|
||||
echo 'endmodule' >> $@
|
||||
|
||||
clean::
|
||||
@rm -rf sim_build_*
|
||||
@rm -rf iverilog_dump.v
|
||||
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -37,7 +37,7 @@ from cocotb.regression import TestFactory
|
||||
from cocotbext.eth import GmiiFrame, RgmiiSource, RgmiiSink
|
||||
|
||||
|
||||
class TB(object):
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
@@ -47,7 +47,7 @@ class TB(object):
|
||||
self._enable_generator = None
|
||||
self._enable_cr = None
|
||||
|
||||
cocotb.fork(Clock(dut.clk, 2, units="ns").start())
|
||||
cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
|
||||
|
||||
self.source = RgmiiSource(dut.rgmii_d, dut.rgmii_ctl, dut.clk, dut.rst, dut.rgmii_clk_en, dut.rgmii_mii_sel)
|
||||
self.sink = RgmiiSink(dut.rgmii_d, dut.rgmii_ctl, dut.clk, dut.rst, dut.rgmii_clk_en, dut.rgmii_mii_sel)
|
||||
@@ -59,30 +59,32 @@ class TB(object):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst <= 1
|
||||
self.dut.rst.value = 1
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst <= 0
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
def set_enable_generator(self, generator=None):
|
||||
if self._enable_cr is not None:
|
||||
self._enable_cr.kill()
|
||||
self._enable_cr.cancel()
|
||||
self._enable_cr = None
|
||||
|
||||
self._enable_generator = generator
|
||||
|
||||
if self._enable_generator is not None:
|
||||
self._enable_cr = cocotb.fork(self._run_enable())
|
||||
self._enable_cr = cocotb.start_soon(self._run_enable())
|
||||
|
||||
def clear_enable_generator(self):
|
||||
self.set_enable_generator(None)
|
||||
|
||||
async def _run_enable(self):
|
||||
clock_edge_event = RisingEdge(self.dut.clk)
|
||||
|
||||
for val in self._enable_generator:
|
||||
self.dut.rgmii_clk_en <= val
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rgmii_clk_en.value = val
|
||||
await clock_edge_event
|
||||
|
||||
|
||||
async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_gen=None, mii_sel=False):
|
||||
@@ -90,7 +92,7 @@ async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_
|
||||
tb = TB(dut)
|
||||
|
||||
tb.source.ifg = ifg
|
||||
tb.dut.rgmii_mii_sel <= mii_sel
|
||||
tb.dut.rgmii_mii_sel.value = mii_sel
|
||||
|
||||
if enable_gen is not None:
|
||||
tb.set_enable_generator(enable_gen())
|
||||
@@ -101,11 +103,10 @@ async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
tb.source.send(test_frame)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
await tb.sink.wait()
|
||||
rx_frame = tb.sink.recv()
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
@@ -129,7 +130,7 @@ def cycle_en():
|
||||
return itertools.cycle([0, 0, 0, 1])
|
||||
|
||||
|
||||
if cocotb.SIM_NAME:
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
factory = TestFactory(run_test)
|
||||
factory.add_option("payload_lengths", [size_list])
|
||||
@@ -142,7 +143,6 @@ if cocotb.SIM_NAME:
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
|
||||
|
||||
|
||||
def test_rgmii(request):
|
||||
@@ -158,8 +158,8 @@ def test_rgmii(request):
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir,
|
||||
"sim_build_"+request.node.name.replace('[', '-').replace(']', ''))
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
49
tests/rgmii_phy/Makefile
Normal file
49
tests/rgmii_phy/Makefile
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2020-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
TOPLEVEL_LANG = verilog
|
||||
|
||||
SIM ?= icarus
|
||||
WAVES ?= 0
|
||||
|
||||
COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_rgmii_phy
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
187
tests/rgmii_phy/test_rgmii_phy.py
Normal file
187
tests/rgmii_phy/test_rgmii_phy.py
Normal file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
|
||||
import cocotb_test.simulator
|
||||
|
||||
import cocotb
|
||||
from cocotb.clock import Clock
|
||||
from cocotb.triggers import RisingEdge
|
||||
from cocotb.regression import TestFactory
|
||||
|
||||
from cocotbext.eth import GmiiFrame, RgmiiSource, RgmiiSink, RgmiiPhy
|
||||
|
||||
|
||||
class TB:
|
||||
def __init__(self, dut, speed=1000e6):
|
||||
self.dut = dut
|
||||
|
||||
self.log = logging.getLogger("cocotb.tb")
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
if speed == 1000e6:
|
||||
cocotb.start_soon(Clock(dut.phy_tx_clk, 8, units="ns").start())
|
||||
elif speed == 100e6:
|
||||
cocotb.start_soon(Clock(dut.phy_tx_clk, 40, units="ns").start())
|
||||
elif speed == 10e6:
|
||||
cocotb.start_soon(Clock(dut.phy_tx_clk, 400, units="ns").start())
|
||||
|
||||
self.rgmii_phy = RgmiiPhy(dut.phy_txd, dut.phy_tx_ctl, dut.phy_tx_clk,
|
||||
dut.phy_rxd, dut.phy_rx_ctl, dut.phy_rx_clk, dut.phy_rst, speed=speed)
|
||||
|
||||
self.source = RgmiiSource(dut.phy_txd, dut.phy_tx_ctl, dut.phy_tx_clk, dut.phy_rst)
|
||||
self.sink = RgmiiSink(dut.phy_rxd, dut.phy_rx_ctl, dut.phy_rx_clk, dut.phy_rst)
|
||||
|
||||
if speed == 1000e6:
|
||||
self.source.mii_mode = False
|
||||
self.sink.mii_mode = False
|
||||
else:
|
||||
self.source.mii_mode = True
|
||||
self.sink.mii_mode = True
|
||||
|
||||
async def reset(self):
|
||||
self.dut.phy_rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
self.dut.phy_rst.value = 1
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
self.dut.phy_rst.value = 0
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
await RisingEdge(self.dut.phy_tx_clk)
|
||||
|
||||
|
||||
async def run_test_tx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=1000e6):
|
||||
|
||||
tb = TB(dut, speed)
|
||||
|
||||
tb.rgmii_phy.rx.ifg = ifg
|
||||
tb.source.ifg = ifg
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.rgmii_phy.tx.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
assert rx_frame.error is None
|
||||
|
||||
assert tb.rgmii_phy.tx.empty()
|
||||
|
||||
await RisingEdge(dut.phy_tx_clk)
|
||||
await RisingEdge(dut.phy_tx_clk)
|
||||
|
||||
|
||||
async def run_test_rx(dut, payload_lengths=None, payload_data=None, ifg=12, speed=1000e6):
|
||||
|
||||
tb = TB(dut, speed)
|
||||
|
||||
tb.rgmii_phy.rx.ifg = ifg
|
||||
tb.source.ifg = ifg
|
||||
|
||||
await tb.reset()
|
||||
|
||||
test_frames = [payload_data(x) for x in payload_lengths()]
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = GmiiFrame.from_payload(test_data)
|
||||
await tb.rgmii_phy.rx.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
assert rx_frame.error is None
|
||||
|
||||
assert tb.sink.empty()
|
||||
|
||||
await RisingEdge(dut.phy_rx_clk)
|
||||
await RisingEdge(dut.phy_rx_clk)
|
||||
|
||||
|
||||
def size_list():
|
||||
return list(range(60, 128)) + [512, 1514] + [60]*10
|
||||
|
||||
|
||||
def incrementing_payload(length):
|
||||
return bytearray(itertools.islice(itertools.cycle(range(256)), length))
|
||||
|
||||
|
||||
def cycle_en():
|
||||
return itertools.cycle([0, 0, 0, 1])
|
||||
|
||||
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
for test in [run_test_tx, run_test_rx]:
|
||||
|
||||
factory = TestFactory(test)
|
||||
factory.add_option("payload_lengths", [size_list])
|
||||
factory.add_option("payload_data", [incrementing_payload])
|
||||
factory.add_option("speed", [1000e6, 100e6, 10e6])
|
||||
factory.generate_tests()
|
||||
|
||||
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def test_rgmii_phy(request):
|
||||
dut = "test_rgmii_phy"
|
||||
module = os.path.splitext(os.path.basename(__file__))[0]
|
||||
toplevel = dut
|
||||
|
||||
verilog_sources = [
|
||||
os.path.join(tests_dir, f"{dut}.v"),
|
||||
]
|
||||
|
||||
parameters = {}
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
verilog_sources=verilog_sources,
|
||||
toplevel=toplevel,
|
||||
module=module,
|
||||
parameters=parameters,
|
||||
sim_build=sim_build,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
43
tests/rgmii_phy/test_rgmii_phy.v
Normal file
43
tests/rgmii_phy/test_rgmii_phy.v
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
// Language: Verilog 2001
|
||||
|
||||
`timescale 1ns / 1ns
|
||||
|
||||
/*
|
||||
* RGMII PHY test
|
||||
*/
|
||||
module test_rgmii_phy
|
||||
(
|
||||
inout wire phy_rst,
|
||||
inout wire [3:0] phy_txd,
|
||||
inout wire phy_tx_ctl,
|
||||
inout wire phy_tx_clk,
|
||||
inout wire [3:0] phy_rxd,
|
||||
inout wire phy_rx_ctl,
|
||||
inout wire phy_rx_clk
|
||||
);
|
||||
|
||||
endmodule
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2020 Alex Forencich
|
||||
# Copyright (c) 2020-2025 Alex Forencich
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,49 +27,27 @@ COCOTB_HDL_TIMEUNIT = 1ns
|
||||
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||
|
||||
DUT = test_xgmii
|
||||
TOPLEVEL = $(DUT)
|
||||
MODULE = $(DUT)
|
||||
COCOTB_TEST_MODULES = $(DUT)
|
||||
COCOTB_TOPLEVEL = $(DUT)
|
||||
MODULE = $(COCOTB_TEST_MODULES)
|
||||
TOPLEVEL = $(COCOTB_TOPLEVEL)
|
||||
VERILOG_SOURCES += $(DUT).v
|
||||
|
||||
# module parameters
|
||||
export PARAM_DATA_WIDTH ?= 64
|
||||
export PARAM_CTRL_WIDTH ?= $(shell expr $(PARAM_DATA_WIDTH) / 8 )
|
||||
|
||||
SIM_BUILD ?= sim_build_$(MODULE)-$(PARAM_DATA_WIDTH)
|
||||
export PARAM_DATA_WIDTH := 64
|
||||
export PARAM_CTRL_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 )
|
||||
|
||||
ifeq ($(SIM), icarus)
|
||||
PLUSARGS += -fst
|
||||
|
||||
COMPILE_ARGS += -P $(TOPLEVEL).DATA_WIDTH=$(PARAM_DATA_WIDTH)
|
||||
COMPILE_ARGS += -P $(TOPLEVEL).CTRL_WIDTH=$(PARAM_CTRL_WIDTH)
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
VERILOG_SOURCES += iverilog_dump.v
|
||||
COMPILE_ARGS += -s iverilog_dump
|
||||
endif
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v)))
|
||||
else ifeq ($(SIM), verilator)
|
||||
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
|
||||
|
||||
COMPILE_ARGS += -GDATA_WIDTH=$(PARAM_DATA_WIDTH)
|
||||
COMPILE_ARGS += -GCTRL_WIDTH=$(PARAM_CTRL_WIDTH)
|
||||
COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v)))
|
||||
|
||||
ifeq ($(WAVES), 1)
|
||||
COMPILE_ARGS += --trace-fst
|
||||
VERILATOR_TRACE = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
iverilog_dump.v:
|
||||
echo 'module iverilog_dump();' > $@
|
||||
echo 'initial begin' >> $@
|
||||
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
|
||||
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
|
||||
echo 'end' >> $@
|
||||
echo 'endmodule' >> $@
|
||||
|
||||
clean::
|
||||
@rm -rf sim_build_*
|
||||
@rm -rf iverilog_dump.v
|
||||
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||
|
||||
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -38,7 +38,7 @@ from cocotb.regression import TestFactory
|
||||
from cocotbext.eth import XgmiiFrame, XgmiiSource, XgmiiSink
|
||||
|
||||
|
||||
class TB(object):
|
||||
class TB:
|
||||
def __init__(self, dut):
|
||||
self.dut = dut
|
||||
|
||||
@@ -48,7 +48,7 @@ class TB(object):
|
||||
self._enable_generator = None
|
||||
self._enable_cr = None
|
||||
|
||||
cocotb.fork(Clock(dut.clk, 2, units="ns").start())
|
||||
cocotb.start_soon(Clock(dut.clk, 2, units="ns").start())
|
||||
|
||||
self.source = XgmiiSource(dut.xgmii_d, dut.xgmii_c, dut.clk, dut.rst, dut.xgmii_clk_en)
|
||||
self.sink = XgmiiSink(dut.xgmii_d, dut.xgmii_c, dut.clk, dut.rst, dut.xgmii_clk_en)
|
||||
@@ -59,30 +59,32 @@ class TB(object):
|
||||
self.dut.rst.setimmediatevalue(0)
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst <= 1
|
||||
self.dut.rst.value = 1
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.rst <= 0
|
||||
self.dut.rst.value = 0
|
||||
await RisingEdge(self.dut.clk)
|
||||
await RisingEdge(self.dut.clk)
|
||||
|
||||
def set_enable_generator(self, generator=None):
|
||||
if self._enable_cr is not None:
|
||||
self._enable_cr.kill()
|
||||
self._enable_cr.cancel()
|
||||
self._enable_cr = None
|
||||
|
||||
self._enable_generator = generator
|
||||
|
||||
if self._enable_generator is not None:
|
||||
self._enable_cr = cocotb.fork(self._run_enable())
|
||||
self._enable_cr = cocotb.start_soon(self._run_enable())
|
||||
|
||||
def clear_enable_generator(self):
|
||||
self.set_enable_generator(None)
|
||||
|
||||
async def _run_enable(self):
|
||||
clock_edge_event = RisingEdge(self.dut.clk)
|
||||
|
||||
for val in self._enable_generator:
|
||||
self.dut.xgmii_clk_en <= val
|
||||
await RisingEdge(self.dut.clk)
|
||||
self.dut.xgmii_clk_en.value = val
|
||||
await clock_edge_event
|
||||
|
||||
|
||||
async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_dic=True,
|
||||
@@ -103,11 +105,10 @@ async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = XgmiiFrame.from_payload(test_data)
|
||||
tb.source.send(test_frame)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
await tb.sink.wait()
|
||||
rx_frame = tb.sink.recv()
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
@@ -124,7 +125,7 @@ async def run_test_alignment(dut, payload_data=None, ifg=12, enable_dic=True,
|
||||
|
||||
tb = TB(dut)
|
||||
|
||||
byte_width = tb.source.width // 8
|
||||
byte_lanes = tb.source.byte_lanes
|
||||
|
||||
tb.source.ifg = ifg
|
||||
tb.source.enable_dic = enable_dic
|
||||
@@ -142,17 +143,16 @@ async def run_test_alignment(dut, payload_data=None, ifg=12, enable_dic=True,
|
||||
|
||||
for test_data in test_frames:
|
||||
test_frame = XgmiiFrame.from_payload(test_data)
|
||||
tb.source.send(test_frame)
|
||||
await tb.source.send(test_frame)
|
||||
|
||||
for test_data in test_frames:
|
||||
await tb.sink.wait()
|
||||
rx_frame = tb.sink.recv()
|
||||
rx_frame = await tb.sink.recv()
|
||||
|
||||
assert rx_frame.get_payload() == test_data
|
||||
assert rx_frame.check_fcs()
|
||||
assert rx_frame.ctrl is None
|
||||
|
||||
start_lane.append(rx_frame.rx_start_lane)
|
||||
start_lane.append(rx_frame.start_lane)
|
||||
|
||||
tb.log.info("length: %d", length)
|
||||
tb.log.info("start_lane: %s", start_lane)
|
||||
@@ -166,23 +166,23 @@ async def run_test_alignment(dut, payload_data=None, ifg=12, enable_dic=True,
|
||||
for test_data in test_frames:
|
||||
if ifg == 0:
|
||||
lane = 0
|
||||
if force_offset_start and byte_width > 4:
|
||||
if force_offset_start and byte_lanes > 4:
|
||||
lane = 4
|
||||
|
||||
start_lane_ref.append(lane)
|
||||
lane = (lane + len(test_data)+4+ifg) % byte_width
|
||||
lane = (lane + len(test_data)+4+ifg) % byte_lanes
|
||||
|
||||
if enable_dic:
|
||||
offset = lane % 4
|
||||
if deficit_idle_count+offset >= 4:
|
||||
offset += 4
|
||||
lane = (lane - offset) % byte_width
|
||||
lane = (lane - offset) % byte_lanes
|
||||
deficit_idle_count = (deficit_idle_count + offset) % 4
|
||||
else:
|
||||
offset = lane % 4
|
||||
if offset > 0:
|
||||
offset += 4
|
||||
lane = (lane - offset) % byte_width
|
||||
lane = (lane - offset) % byte_lanes
|
||||
|
||||
tb.log.info("start_lane_ref: %s", start_lane_ref)
|
||||
|
||||
@@ -208,7 +208,7 @@ def cycle_en():
|
||||
return itertools.cycle([0, 0, 0, 1])
|
||||
|
||||
|
||||
if cocotb.SIM_NAME:
|
||||
if getattr(cocotb, 'top', None) is not None:
|
||||
|
||||
factory = TestFactory(run_test)
|
||||
factory.add_option("payload_lengths", [size_list])
|
||||
@@ -231,7 +231,6 @@ if cocotb.SIM_NAME:
|
||||
# cocotb-test
|
||||
|
||||
tests_dir = os.path.dirname(__file__)
|
||||
rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl'))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data_width", [32, 64])
|
||||
@@ -251,8 +250,8 @@ def test_xgmii(request, data_width):
|
||||
|
||||
extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()}
|
||||
|
||||
sim_build = os.path.join(tests_dir,
|
||||
"sim_build_"+request.node.name.replace('[', '-').replace(']', ''))
|
||||
sim_build = os.path.join(tests_dir, "sim_build",
|
||||
request.node.name.replace('[', '-').replace(']', ''))
|
||||
|
||||
cocotb_test.simulator.run(
|
||||
python_search=[tests_dir],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020 Alex Forencich
|
||||
Copyright (c) 2020-2025 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
Reference in New Issue
Block a user