Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
198
README.md
198
README.md
@@ -11,6 +11,8 @@ GitHub repository: https://github.com/alexforencich/cocotbext-eth
|
|||||||
|
|
||||||
Ethernet interface models for [cocotb](https://github.com/cocotb/cocotb).
|
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
|
||||||
|
|
||||||
Installation from pip (release version, stable):
|
Installation from pip (release version, stable):
|
||||||
@@ -28,7 +30,7 @@ Installation for active development:
|
|||||||
|
|
||||||
## Documentation and usage examples
|
## 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, [verilog-ethernet](https://github.com/alexforencich/verilog-ethernet), and [corundum](https://github.com/corundum/corundum) for complete testbenches using these modules.
|
||||||
|
|
||||||
### GMII
|
### GMII
|
||||||
|
|
||||||
@@ -91,6 +93,8 @@ The `GmiiPhy` class provides a model of a GMII PHY chip. It wraps instances of
|
|||||||
|
|
||||||
* _queue_occupancy_bytes_: number of bytes in queue
|
* _queue_occupancy_bytes_: number of bytes in queue
|
||||||
* _queue_occupancy_frames_: number of frames 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
|
* _mii_mode_: control MII mode when _mii_select_ signal is not connected
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
@@ -101,6 +105,7 @@ The `GmiiPhy` class provides a model of a GMII PHY chip. It wraps instances of
|
|||||||
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
||||||
* `count()`: returns the number of items in the queue (all)
|
* `count()`: returns the number of items in the queue (all)
|
||||||
* `empty()`: returns _True_ if the queue is empty (all)
|
* `empty()`: returns _True_ if the queue is empty (all)
|
||||||
|
* `full()`: returns _True_ if the queue occupancy limits are met (source)
|
||||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||||
* `clear()`: drop all data in queue (all)
|
* `clear()`: drop all data in queue (all)
|
||||||
* `wait()`: wait for idle (source)
|
* `wait()`: wait for idle (source)
|
||||||
@@ -208,6 +213,8 @@ The `MiiPhy` class provides a model of an MII PHY chip. It wraps instances of `
|
|||||||
|
|
||||||
* _queue_occupancy_bytes_: number of bytes in queue
|
* _queue_occupancy_bytes_: number of bytes in queue
|
||||||
* _queue_occupancy_frames_: number of frames 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
|
#### Methods
|
||||||
|
|
||||||
@@ -217,6 +224,7 @@ The `MiiPhy` class provides a model of an MII PHY chip. It wraps instances of `
|
|||||||
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
||||||
* `count()`: returns the number of items in the queue (all)
|
* `count()`: returns the number of items in the queue (all)
|
||||||
* `empty()`: returns _True_ if the queue is empty (all)
|
* `empty()`: returns _True_ if the queue is empty (all)
|
||||||
|
* `full()`: returns _True_ if the queue occupancy limits are met (source)
|
||||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||||
* `clear()`: drop all data in queue (all)
|
* `clear()`: drop all data in queue (all)
|
||||||
* `wait()`: wait for idle (source)
|
* `wait()`: wait for idle (source)
|
||||||
@@ -297,6 +305,8 @@ The `RgmiiPhy` class provides a model of an RGMII PHY chip. It wraps instances
|
|||||||
|
|
||||||
* _queue_occupancy_bytes_: number of bytes in queue
|
* _queue_occupancy_bytes_: number of bytes in queue
|
||||||
* _queue_occupancy_frames_: number of frames 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
|
* _mii_mode_: control MII mode when _mii_select_ signal is not connected
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
@@ -307,6 +317,7 @@ The `RgmiiPhy` class provides a model of an RGMII PHY chip. It wraps instances
|
|||||||
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
* `recv_nowait()`: receive a frame as a `GmiiFrame` (non-blocking) (sink)
|
||||||
* `count()`: returns the number of items in the queue (all)
|
* `count()`: returns the number of items in the queue (all)
|
||||||
* `empty()`: returns _True_ if the queue is empty (all)
|
* `empty()`: returns _True_ if the queue is empty (all)
|
||||||
|
* `full()`: returns _True_ if the queue occupancy limits are met (source)
|
||||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||||
* `clear()`: drop all data in queue (all)
|
* `clear()`: drop all data in queue (all)
|
||||||
* `wait()`: wait for idle (source)
|
* `wait()`: wait for idle (source)
|
||||||
@@ -372,6 +383,8 @@ To receive data with an `XgmiiSink`, call `recv()` or `recv_nowait()`. Optional
|
|||||||
|
|
||||||
* _queue_occupancy_bytes_: number of bytes in queue
|
* _queue_occupancy_bytes_: number of bytes in queue
|
||||||
* _queue_occupancy_frames_: number of frames 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
|
#### Methods
|
||||||
|
|
||||||
@@ -381,6 +394,7 @@ To receive data with an `XgmiiSink`, call `recv()` or `recv_nowait()`. Optional
|
|||||||
* `recv_nowait()`: receive a frame as an `XgmiiFrame` (non-blocking) (sink)
|
* `recv_nowait()`: receive a frame as an `XgmiiFrame` (non-blocking) (sink)
|
||||||
* `count()`: returns the number of items in the queue (all)
|
* `count()`: returns the number of items in the queue (all)
|
||||||
* `empty()`: returns _True_ if the queue is empty (all)
|
* `empty()`: returns _True_ if the queue is empty (all)
|
||||||
|
* `full()`: returns _True_ if the queue occupancy limits are met (source)
|
||||||
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
* `idle()`: returns _True_ if no transfer is in progress (all) or if the queue is not empty (source)
|
||||||
* `clear()`: drop all data in queue (all)
|
* `clear()`: drop all data in queue (all)
|
||||||
* `wait()`: wait for idle (source)
|
* `wait()`: wait for idle (source)
|
||||||
@@ -435,8 +449,139 @@ Methods:
|
|||||||
* `get_payload(strip_fcs=True)`: return payload, optionally strip FCS
|
* `get_payload(strip_fcs=True)`: return payload, optionally strip FCS
|
||||||
* `get_fcs()`: return FCS
|
* `get_fcs()`: return FCS
|
||||||
* `check_fcs()`: returns _True_ if FCS is correct
|
* `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.
|
* `normalize()`: pack `ctrl` 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
|
* `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
|
### PTP clock
|
||||||
|
|
||||||
@@ -502,3 +647,50 @@ Once the clock is instantiated, it will generate a continuous stream of monotoni
|
|||||||
* `get_ts_64()`: return current 64-bit timestamp as an integer
|
* `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_ns()`: return current 64-bit timestamp in ns (float)
|
||||||
* `get_ts_64_s()`: return current 64-bit timestamp in seconds (float)
|
* `get_ts_64_s()`: return current 64-bit timestamp in seconds (float)
|
||||||
|
|
||||||
|
### PTP clock (sim time)
|
||||||
|
|
||||||
|
The `PtpClockSimTime` class implements a PTP hardware clock that produces IEEE 1588 format 96 and 64 bit 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_96=dut.ts_96,
|
||||||
|
ts_64=dut.ts_64,
|
||||||
|
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.
|
||||||
|
|
||||||
|
#### 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)
|
||||||
|
* `pps`: pulse-per-second output, pulsed when ts_96 seconds field increments
|
||||||
|
|
||||||
|
#### Constructor parameters:
|
||||||
|
|
||||||
|
* _ts_96_: 96-bit timestamp signal (optional)
|
||||||
|
* _ts_64_: 64-bit timestamp signal (optional)
|
||||||
|
* _pps_: pulse-per-second signal (optional)
|
||||||
|
* _clock_: clock
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
* `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)
|
||||||
|
* `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)
|
||||||
|
|||||||
@@ -28,5 +28,6 @@ from .gmii import GmiiFrame, GmiiSource, GmiiSink, GmiiPhy
|
|||||||
from .mii import MiiSource, MiiSink, MiiPhy
|
from .mii import MiiSource, MiiSink, MiiPhy
|
||||||
from .rgmii import RgmiiSource, RgmiiSink, RgmiiPhy
|
from .rgmii import RgmiiSource, RgmiiSink, RgmiiPhy
|
||||||
from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink
|
from .xgmii import XgmiiFrame, XgmiiSource, XgmiiSink
|
||||||
|
from .eth_mac import EthMacFrame, EthMacTx, EthMacRx, EthMac
|
||||||
|
|
||||||
from .ptp import PtpClock
|
from .ptp import PtpClock, PtpClockSimTime
|
||||||
|
|||||||
553
cocotbext/eth/eth_mac.py
Normal file
553
cocotbext/eth/eth_mac.py
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
Copyright (c) 2021 Alex Forencich
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import 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=None, tx_complete=None):
|
||||||
|
self.data = bytearray()
|
||||||
|
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 = bytearray(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 = bytearray(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
|
||||||
|
self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}")
|
||||||
|
|
||||||
|
self.log.info("Ethernet MAC TX model")
|
||||||
|
self.log.info("cocotbext-eth version %s", __version__)
|
||||||
|
self.log.info("Copyright (c) 2020 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:
|
||||||
|
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:
|
||||||
|
self.ptp_ts.setimmediatevalue(0)
|
||||||
|
if self.ptp_ts_tag:
|
||||||
|
self.ptp_ts_tag.setimmediatevalue(0)
|
||||||
|
if self.ptp_ts_valid:
|
||||||
|
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.kill()
|
||||||
|
self._run_cr = None
|
||||||
|
if self._run_ts_cr is not None:
|
||||||
|
self._run_ts_cr.kill()
|
||||||
|
self._run_ts_cr = None
|
||||||
|
|
||||||
|
if self.ptp_ts_valid:
|
||||||
|
self.ptp_ts_valid <= 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.fork(self._run())
|
||||||
|
if self._run_ts_cr is None and self.ptp_ts:
|
||||||
|
self._run_ts_cr = cocotb.fork(self._run_ts())
|
||||||
|
|
||||||
|
async def _run(self):
|
||||||
|
frame = None
|
||||||
|
self.active = False
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# wait for data
|
||||||
|
cycle = await self.stream.recv()
|
||||||
|
|
||||||
|
frame = EthMacFrame(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:
|
||||||
|
frame.ptp_timestamp = self.ptp_time.value.integer
|
||||||
|
frame.ptp_tag = cycle.tuser.integer >> 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 (cycle.tkeep.integer >> offset) & 1:
|
||||||
|
frame.data.append((cycle.tdata.integer >> (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 cycle.tlast.integer:
|
||||||
|
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):
|
||||||
|
while True:
|
||||||
|
await RisingEdge(self.clock)
|
||||||
|
self.ptp_ts_valid <= 0
|
||||||
|
|
||||||
|
if not self.ts_queue.empty():
|
||||||
|
ts, tag = self.ts_queue.get_nowait()
|
||||||
|
self.ptp_ts <= ts
|
||||||
|
if self.ptp_ts_tag is not None:
|
||||||
|
self.ptp_ts_tag <= tag
|
||||||
|
self.ptp_ts_valid <= 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
|
||||||
|
self.log = logging.getLogger(f"cocotb.{bus._entity._name}.{bus._name}")
|
||||||
|
|
||||||
|
self.log.info("Ethernet MAC RX model")
|
||||||
|
self.log.info("cocotbext-eth version %s", __version__)
|
||||||
|
self.log.info("Copyright (c) 2020 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:
|
||||||
|
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.kill()
|
||||||
|
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.fork(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:
|
||||||
|
frame.ptp_timestamp = self.ptp_time.value.integer
|
||||||
|
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)
|
||||||
@@ -25,9 +25,9 @@ THE SOFTWARE.
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
import zlib
|
import zlib
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import cocotb
|
import cocotb
|
||||||
|
from cocotb.queue import Queue, QueueFull
|
||||||
from cocotb.triggers import RisingEdge, Timer, First, Event
|
from cocotb.triggers import RisingEdge, Timer, First, Event
|
||||||
from cocotb.utils import get_sim_time, get_sim_steps
|
from cocotb.utils import get_sim_time, get_sim_steps
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class GmiiFrame:
|
|||||||
self.error = [0]*n
|
self.error = [0]*n
|
||||||
|
|
||||||
def compact(self):
|
def compact(self):
|
||||||
if not any(self.error):
|
if self.error is not None and not any(self.error):
|
||||||
self.error = None
|
self.error = None
|
||||||
|
|
||||||
def handle_tx_complete(self):
|
def handle_tx_complete(self):
|
||||||
@@ -152,7 +152,11 @@ class GmiiSource(Reset):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.queue = deque()
|
self.queue = Queue()
|
||||||
|
self.dequeue_event = Event()
|
||||||
|
self.current_frame = None
|
||||||
|
self.idle_event = Event()
|
||||||
|
self.idle_event.set()
|
||||||
|
|
||||||
self.ifg = 12
|
self.ifg = 12
|
||||||
self.mii_mode = False
|
self.mii_mode = False
|
||||||
@@ -160,6 +164,9 @@ class GmiiSource(Reset):
|
|||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
|
self.queue_occupancy_limit_bytes = -1
|
||||||
|
self.queue_occupancy_limit_frames = -1
|
||||||
|
|
||||||
self.width = 8
|
self.width = 8
|
||||||
self.byte_width = 1
|
self.byte_width = 1
|
||||||
|
|
||||||
@@ -176,31 +183,53 @@ class GmiiSource(Reset):
|
|||||||
self._init_reset(reset, reset_active_level)
|
self._init_reset(reset, reset_active_level)
|
||||||
|
|
||||||
async def send(self, frame):
|
async def send(self, frame):
|
||||||
self.send_nowait(frame)
|
while self.full():
|
||||||
|
self.dequeue_event.clear()
|
||||||
def send_nowait(self, frame):
|
await self.dequeue_event.wait()
|
||||||
frame = GmiiFrame(frame)
|
frame = GmiiFrame(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 = GmiiFrame(frame)
|
||||||
|
self.queue.put_nowait(frame)
|
||||||
|
self.idle_event.clear()
|
||||||
self.queue_occupancy_bytes += len(frame)
|
self.queue_occupancy_bytes += len(frame)
|
||||||
self.queue_occupancy_frames += 1
|
self.queue_occupancy_frames += 1
|
||||||
self.queue.append(frame)
|
|
||||||
|
|
||||||
def count(self):
|
def count(self):
|
||||||
return len(self.queue)
|
return self.queue.qsize()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
return not self.queue
|
return self.queue.empty()
|
||||||
|
|
||||||
|
def 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):
|
def idle(self):
|
||||||
return self.empty() and not self.active
|
return self.empty() and not self.active
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.queue.clear()
|
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_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
async def wait(self):
|
async def wait(self):
|
||||||
while not self.idle():
|
await self.idle_event.wait()
|
||||||
await RisingEdge(self.clock)
|
|
||||||
|
|
||||||
def _handle_reset(self, state):
|
def _handle_reset(self, state):
|
||||||
if state:
|
if state:
|
||||||
@@ -208,10 +237,6 @@ class GmiiSource(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
else:
|
|
||||||
self.log.info("Reset de-asserted")
|
|
||||||
if self._run_cr is None:
|
|
||||||
self._run_cr = cocotb.fork(self._run())
|
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.data <= 0
|
self.data <= 0
|
||||||
@@ -219,8 +244,23 @@ class GmiiSource(Reset):
|
|||||||
self.er <= 0
|
self.er <= 0
|
||||||
self.dv <= 0
|
self.dv <= 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()
|
||||||
|
else:
|
||||||
|
self.log.info("Reset de-asserted")
|
||||||
|
if self._run_cr is None:
|
||||||
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
frame = None
|
frame = None
|
||||||
|
frame_offset = 0
|
||||||
|
frame_data = None
|
||||||
|
frame_error = None
|
||||||
ifg_cnt = 0
|
ifg_cnt = 0
|
||||||
self.active = False
|
self.active = False
|
||||||
|
|
||||||
@@ -232,11 +272,13 @@ class GmiiSource(Reset):
|
|||||||
# in IFG
|
# in IFG
|
||||||
ifg_cnt -= 1
|
ifg_cnt -= 1
|
||||||
|
|
||||||
elif frame is None and self.queue:
|
elif frame is None and not self.queue.empty():
|
||||||
# send frame
|
# send frame
|
||||||
frame = self.queue.popleft()
|
frame = self.queue.get_nowait()
|
||||||
|
self.dequeue_event.set()
|
||||||
self.queue_occupancy_bytes -= len(frame)
|
self.queue_occupancy_bytes -= len(frame)
|
||||||
self.queue_occupancy_frames -= 1
|
self.queue_occupancy_frames -= 1
|
||||||
|
self.current_frame = frame
|
||||||
frame.sim_time_start = get_sim_time()
|
frame.sim_time_start = get_sim_time()
|
||||||
frame.sim_time_sfd = None
|
frame.sim_time_sfd = None
|
||||||
frame.sim_time_end = None
|
frame.sim_time_end = None
|
||||||
@@ -247,38 +289,44 @@ class GmiiSource(Reset):
|
|||||||
self.mii_mode = bool(self.mii_select.value.integer)
|
self.mii_mode = bool(self.mii_select.value.integer)
|
||||||
|
|
||||||
if self.mii_mode:
|
if self.mii_mode:
|
||||||
mii_data = []
|
# convert to MII
|
||||||
mii_error = []
|
frame_data = []
|
||||||
|
frame_error = []
|
||||||
for b, e in zip(frame.data, frame.error):
|
for b, e in zip(frame.data, frame.error):
|
||||||
mii_data.append(b & 0x0F)
|
frame_data.append(b & 0x0F)
|
||||||
mii_data.append(b >> 4)
|
frame_data.append(b >> 4)
|
||||||
mii_error.append(e)
|
frame_error.append(e)
|
||||||
mii_error.append(e)
|
frame_error.append(e)
|
||||||
frame.data = mii_data
|
else:
|
||||||
frame.error = mii_error
|
frame_data = frame.data
|
||||||
|
frame_error = frame.error
|
||||||
|
|
||||||
self.active = True
|
self.active = True
|
||||||
|
frame_offset = 0
|
||||||
|
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
d = frame.data.pop(0)
|
d = frame_data[frame_offset]
|
||||||
if frame.sim_time_sfd is None and d in (EthPre.SFD, 0xD):
|
if frame.sim_time_sfd is None and d in (EthPre.SFD, 0xD):
|
||||||
frame.sim_time_sfd = get_sim_time()
|
frame.sim_time_sfd = get_sim_time()
|
||||||
self.data <= d
|
self.data <= d
|
||||||
if self.er is not None:
|
if self.er is not None:
|
||||||
self.er <= frame.error.pop(0)
|
self.er <= frame_error[frame_offset]
|
||||||
self.dv <= 1
|
self.dv <= 1
|
||||||
|
frame_offset += 1
|
||||||
|
|
||||||
if not frame.data:
|
if frame_offset >= len(frame_data):
|
||||||
ifg_cnt = max(self.ifg, 1)
|
ifg_cnt = max(self.ifg, 1)
|
||||||
frame.sim_time_end = get_sim_time()
|
frame.sim_time_end = get_sim_time()
|
||||||
frame.handle_tx_complete()
|
frame.handle_tx_complete()
|
||||||
frame = None
|
frame = None
|
||||||
|
self.current_frame = None
|
||||||
else:
|
else:
|
||||||
self.data <= 0
|
self.data <= 0
|
||||||
if self.er is not None:
|
if self.er is not None:
|
||||||
self.er <= 0
|
self.er <= 0
|
||||||
self.dv <= 0
|
self.dv <= 0
|
||||||
self.active = False
|
self.active = False
|
||||||
|
self.idle_event.set()
|
||||||
|
|
||||||
|
|
||||||
class GmiiSink(Reset):
|
class GmiiSink(Reset):
|
||||||
@@ -301,8 +349,8 @@ class GmiiSink(Reset):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.queue = deque()
|
self.queue = Queue()
|
||||||
self.sync = Event()
|
self.active_event = Event()
|
||||||
|
|
||||||
self.mii_mode = False
|
self.mii_mode = False
|
||||||
|
|
||||||
@@ -322,42 +370,46 @@ class GmiiSink(Reset):
|
|||||||
|
|
||||||
self._init_reset(reset, reset_active_level)
|
self._init_reset(reset, reset_active_level)
|
||||||
|
|
||||||
async def recv(self, compact=True):
|
def _recv(self, frame, compact=True):
|
||||||
while self.empty():
|
if self.queue.empty():
|
||||||
self.sync.clear()
|
self.active_event.clear()
|
||||||
await self.sync.wait()
|
|
||||||
return self.recv_nowait(compact)
|
|
||||||
|
|
||||||
def recv_nowait(self, compact=True):
|
|
||||||
if self.queue:
|
|
||||||
frame = self.queue.popleft()
|
|
||||||
self.queue_occupancy_bytes -= len(frame)
|
self.queue_occupancy_bytes -= len(frame)
|
||||||
self.queue_occupancy_frames -= 1
|
self.queue_occupancy_frames -= 1
|
||||||
|
if compact:
|
||||||
|
frame.compact()
|
||||||
return frame
|
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):
|
def count(self):
|
||||||
return len(self.queue)
|
return self.queue.qsize()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
return not self.queue
|
return self.queue.empty()
|
||||||
|
|
||||||
def idle(self):
|
def idle(self):
|
||||||
return not self.active
|
return not self.active
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.queue.clear()
|
while not self.queue.empty():
|
||||||
|
self.queue.get_nowait()
|
||||||
|
self.active_event.clear()
|
||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
async def wait(self, timeout=0, timeout_unit=None):
|
async def wait(self, timeout=0, timeout_unit=None):
|
||||||
if not self.empty():
|
if not self.empty():
|
||||||
return
|
return
|
||||||
self.sync.clear()
|
|
||||||
if timeout:
|
if timeout:
|
||||||
await First(self.sync.wait(), Timer(timeout, timeout_unit))
|
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||||
else:
|
else:
|
||||||
await self.sync.wait()
|
await self.active_event.wait()
|
||||||
|
|
||||||
def _handle_reset(self, state):
|
def _handle_reset(self, state):
|
||||||
if state:
|
if state:
|
||||||
@@ -365,13 +417,13 @@ class GmiiSink(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
|
|
||||||
|
self.active = False
|
||||||
else:
|
else:
|
||||||
self.log.info("Reset de-asserted")
|
self.log.info("Reset de-asserted")
|
||||||
if self._run_cr is None:
|
if self._run_cr is None:
|
||||||
self._run_cr = cocotb.fork(self._run())
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
frame = None
|
frame = None
|
||||||
self.active = False
|
self.active = False
|
||||||
@@ -424,8 +476,8 @@ class GmiiSink(Reset):
|
|||||||
self.queue_occupancy_bytes += len(frame)
|
self.queue_occupancy_bytes += len(frame)
|
||||||
self.queue_occupancy_frames += 1
|
self.queue_occupancy_frames += 1
|
||||||
|
|
||||||
self.queue.append(frame)
|
self.queue.put_nowait(frame)
|
||||||
self.sync.set()
|
self.active_event.set()
|
||||||
|
|
||||||
frame = None
|
frame = None
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ THE SOFTWARE.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import cocotb
|
import cocotb
|
||||||
|
from cocotb.queue import Queue, QueueFull
|
||||||
from cocotb.triggers import RisingEdge, Timer, First, Event
|
from cocotb.triggers import RisingEdge, Timer, First, Event
|
||||||
from cocotb.utils import get_sim_time, get_sim_steps
|
from cocotb.utils import get_sim_time, get_sim_steps
|
||||||
|
|
||||||
@@ -54,13 +54,20 @@ class MiiSource(Reset):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.queue = deque()
|
self.queue = Queue()
|
||||||
|
self.dequeue_event = Event()
|
||||||
|
self.current_frame = None
|
||||||
|
self.idle_event = Event()
|
||||||
|
self.idle_event.set()
|
||||||
|
|
||||||
self.ifg = 12
|
self.ifg = 12
|
||||||
|
|
||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
|
self.queue_occupancy_limit_bytes = -1
|
||||||
|
self.queue_occupancy_limit_frames = -1
|
||||||
|
|
||||||
self.width = 4
|
self.width = 4
|
||||||
self.byte_width = 1
|
self.byte_width = 1
|
||||||
|
|
||||||
@@ -77,31 +84,53 @@ class MiiSource(Reset):
|
|||||||
self._init_reset(reset, reset_active_level)
|
self._init_reset(reset, reset_active_level)
|
||||||
|
|
||||||
async def send(self, frame):
|
async def send(self, frame):
|
||||||
self.send_nowait(frame)
|
while self.full():
|
||||||
|
self.dequeue_event.clear()
|
||||||
def send_nowait(self, frame):
|
await self.dequeue_event.wait()
|
||||||
frame = GmiiFrame(frame)
|
frame = GmiiFrame(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 = GmiiFrame(frame)
|
||||||
|
self.queue.put_nowait(frame)
|
||||||
|
self.idle_event.clear()
|
||||||
self.queue_occupancy_bytes += len(frame)
|
self.queue_occupancy_bytes += len(frame)
|
||||||
self.queue_occupancy_frames += 1
|
self.queue_occupancy_frames += 1
|
||||||
self.queue.append(frame)
|
|
||||||
|
|
||||||
def count(self):
|
def count(self):
|
||||||
return len(self.queue)
|
return self.queue.qsize()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
return not self.queue
|
return self.queue.empty()
|
||||||
|
|
||||||
|
def 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):
|
def idle(self):
|
||||||
return self.empty() and not self.active
|
return self.empty() and not self.active
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.queue.clear()
|
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_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
async def wait(self):
|
async def wait(self):
|
||||||
while not self.idle():
|
await self.idle_event.wait()
|
||||||
await RisingEdge(self.clock)
|
|
||||||
|
|
||||||
def _handle_reset(self, state):
|
def _handle_reset(self, state):
|
||||||
if state:
|
if state:
|
||||||
@@ -109,10 +138,6 @@ class MiiSource(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
else:
|
|
||||||
self.log.info("Reset de-asserted")
|
|
||||||
if self._run_cr is None:
|
|
||||||
self._run_cr = cocotb.fork(self._run())
|
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.data <= 0
|
self.data <= 0
|
||||||
@@ -120,8 +145,23 @@ class MiiSource(Reset):
|
|||||||
self.er <= 0
|
self.er <= 0
|
||||||
self.dv <= 0
|
self.dv <= 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()
|
||||||
|
else:
|
||||||
|
self.log.info("Reset de-asserted")
|
||||||
|
if self._run_cr is None:
|
||||||
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
frame = None
|
frame = None
|
||||||
|
frame_offset = 0
|
||||||
|
frame_data = None
|
||||||
|
frame_error = None
|
||||||
ifg_cnt = 0
|
ifg_cnt = 0
|
||||||
self.active = False
|
self.active = False
|
||||||
|
|
||||||
@@ -133,49 +173,54 @@ class MiiSource(Reset):
|
|||||||
# in IFG
|
# in IFG
|
||||||
ifg_cnt -= 1
|
ifg_cnt -= 1
|
||||||
|
|
||||||
elif frame is None and self.queue:
|
elif frame is None and not self.queue.empty():
|
||||||
# send frame
|
# send frame
|
||||||
frame = self.queue.popleft()
|
frame = self.queue.get_nowait()
|
||||||
|
self.dequeue_event.set()
|
||||||
self.queue_occupancy_bytes -= len(frame)
|
self.queue_occupancy_bytes -= len(frame)
|
||||||
self.queue_occupancy_frames -= 1
|
self.queue_occupancy_frames -= 1
|
||||||
|
self.current_frame = frame
|
||||||
frame.sim_time_start = get_sim_time()
|
frame.sim_time_start = get_sim_time()
|
||||||
frame.sim_time_sfd = None
|
frame.sim_time_sfd = None
|
||||||
frame.sim_time_end = None
|
frame.sim_time_end = None
|
||||||
self.log.info("TX frame: %s", frame)
|
self.log.info("TX frame: %s", frame)
|
||||||
frame.normalize()
|
frame.normalize()
|
||||||
|
|
||||||
mii_data = []
|
# convert to MII
|
||||||
mii_error = []
|
frame_data = []
|
||||||
|
frame_error = []
|
||||||
for b, e in zip(frame.data, frame.error):
|
for b, e in zip(frame.data, frame.error):
|
||||||
mii_data.append(b & 0x0F)
|
frame_data.append(b & 0x0F)
|
||||||
mii_data.append(b >> 4)
|
frame_data.append(b >> 4)
|
||||||
mii_error.append(e)
|
frame_error.append(e)
|
||||||
mii_error.append(e)
|
frame_error.append(e)
|
||||||
frame.data = mii_data
|
|
||||||
frame.error = mii_error
|
|
||||||
|
|
||||||
self.active = True
|
self.active = True
|
||||||
|
frame_offset = 0
|
||||||
|
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
d = frame.data.pop(0)
|
d = frame_data[frame_offset]
|
||||||
if frame.sim_time_sfd is None and d == 0xD:
|
if frame.sim_time_sfd is None and d == 0xD:
|
||||||
frame.sim_time_sfd = get_sim_time()
|
frame.sim_time_sfd = get_sim_time()
|
||||||
self.data <= d
|
self.data <= d
|
||||||
if self.er is not None:
|
if self.er is not None:
|
||||||
self.er <= frame.error.pop(0)
|
self.er <= frame_error[frame_offset]
|
||||||
self.dv <= 1
|
self.dv <= 1
|
||||||
|
frame_offset += 1
|
||||||
|
|
||||||
if not frame.data:
|
if frame_offset >= len(frame_data):
|
||||||
ifg_cnt = max(self.ifg, 1)
|
ifg_cnt = max(self.ifg, 1)
|
||||||
frame.sim_time_end = get_sim_time()
|
frame.sim_time_end = get_sim_time()
|
||||||
frame.handle_tx_complete()
|
frame.handle_tx_complete()
|
||||||
frame = None
|
frame = None
|
||||||
|
self.current_frame = None
|
||||||
else:
|
else:
|
||||||
self.data <= 0
|
self.data <= 0
|
||||||
if self.er is not None:
|
if self.er is not None:
|
||||||
self.er <= 0
|
self.er <= 0
|
||||||
self.dv <= 0
|
self.dv <= 0
|
||||||
self.active = False
|
self.active = False
|
||||||
|
self.idle_event.set()
|
||||||
|
|
||||||
|
|
||||||
class MiiSink(Reset):
|
class MiiSink(Reset):
|
||||||
@@ -197,8 +242,8 @@ class MiiSink(Reset):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.queue = deque()
|
self.queue = Queue()
|
||||||
self.sync = Event()
|
self.active_event = Event()
|
||||||
|
|
||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
@@ -216,42 +261,46 @@ class MiiSink(Reset):
|
|||||||
|
|
||||||
self._init_reset(reset, reset_active_level)
|
self._init_reset(reset, reset_active_level)
|
||||||
|
|
||||||
async def recv(self, compact=True):
|
def _recv(self, frame, compact=True):
|
||||||
while self.empty():
|
if self.queue.empty():
|
||||||
self.sync.clear()
|
self.active_event.clear()
|
||||||
await self.sync.wait()
|
|
||||||
return self.recv_nowait(compact)
|
|
||||||
|
|
||||||
def recv_nowait(self, compact=True):
|
|
||||||
if self.queue:
|
|
||||||
frame = self.queue.popleft()
|
|
||||||
self.queue_occupancy_bytes -= len(frame)
|
self.queue_occupancy_bytes -= len(frame)
|
||||||
self.queue_occupancy_frames -= 1
|
self.queue_occupancy_frames -= 1
|
||||||
|
if compact:
|
||||||
|
frame.compact()
|
||||||
return frame
|
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):
|
def count(self):
|
||||||
return len(self.queue)
|
return self.queue.qsize()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
return not self.queue
|
return self.queue.empty()
|
||||||
|
|
||||||
def idle(self):
|
def idle(self):
|
||||||
return not self.active
|
return not self.active
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.queue.clear()
|
while not self.queue.empty():
|
||||||
|
self.queue.get_nowait()
|
||||||
|
self.active_event.clear()
|
||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
async def wait(self, timeout=0, timeout_unit=None):
|
async def wait(self, timeout=0, timeout_unit=None):
|
||||||
if not self.empty():
|
if not self.empty():
|
||||||
return
|
return
|
||||||
self.sync.clear()
|
|
||||||
if timeout:
|
if timeout:
|
||||||
await First(self.sync.wait(), Timer(timeout, timeout_unit))
|
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||||
else:
|
else:
|
||||||
await self.sync.wait()
|
await self.active_event.wait()
|
||||||
|
|
||||||
def _handle_reset(self, state):
|
def _handle_reset(self, state):
|
||||||
if state:
|
if state:
|
||||||
@@ -259,13 +308,13 @@ class MiiSink(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
|
|
||||||
|
self.active = False
|
||||||
else:
|
else:
|
||||||
self.log.info("Reset de-asserted")
|
self.log.info("Reset de-asserted")
|
||||||
if self._run_cr is None:
|
if self._run_cr is None:
|
||||||
self._run_cr = cocotb.fork(self._run())
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
frame = None
|
frame = None
|
||||||
self.active = False
|
self.active = False
|
||||||
@@ -313,8 +362,8 @@ class MiiSink(Reset):
|
|||||||
self.queue_occupancy_bytes += len(frame)
|
self.queue_occupancy_bytes += len(frame)
|
||||||
self.queue_occupancy_frames += 1
|
self.queue_occupancy_frames += 1
|
||||||
|
|
||||||
self.queue.append(frame)
|
self.queue.put_nowait(frame)
|
||||||
self.sync.set()
|
self.active_event.set()
|
||||||
|
|
||||||
frame = None
|
frame = None
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from fractions import Fraction
|
|||||||
|
|
||||||
import cocotb
|
import cocotb
|
||||||
from cocotb.triggers import RisingEdge
|
from cocotb.triggers import RisingEdge
|
||||||
|
from cocotb.utils import get_sim_time
|
||||||
|
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
from .reset import Reset
|
from .reset import Reset
|
||||||
@@ -186,10 +187,6 @@ class PtpClock(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
else:
|
|
||||||
self.log.info("Reset de-asserted")
|
|
||||||
if self._run_cr is None:
|
|
||||||
self._run_cr = cocotb.fork(self._run())
|
|
||||||
|
|
||||||
self.ts_96_s = 0
|
self.ts_96_s = 0
|
||||||
self.ts_96_ns = 0
|
self.ts_96_ns = 0
|
||||||
@@ -205,6 +202,10 @@ class PtpClock(Reset):
|
|||||||
self.ts_step <= 0
|
self.ts_step <= 0
|
||||||
if self.pps is not None:
|
if self.pps is not None:
|
||||||
self.pps <= 0
|
self.pps <= 0
|
||||||
|
else:
|
||||||
|
self.log.info("Reset de-asserted")
|
||||||
|
if self._run_cr is None:
|
||||||
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
while True:
|
while True:
|
||||||
@@ -253,3 +254,79 @@ class PtpClock(Reset):
|
|||||||
self.drift_cnt -= 1
|
self.drift_cnt -= 1
|
||||||
else:
|
else:
|
||||||
self.drift_cnt = self.drift_rate-1
|
self.drift_cnt = self.drift_rate-1
|
||||||
|
|
||||||
|
|
||||||
|
class PtpClockSimTime:
|
||||||
|
|
||||||
|
def __init__(self, ts_96=None, ts_64=None, pps=None, clock=None, *args, **kwargs):
|
||||||
|
self.log = logging.getLogger(f"cocotb.eth.{type(self).__name__}")
|
||||||
|
self.ts_96 = ts_96
|
||||||
|
self.ts_64 = ts_64
|
||||||
|
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 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.ts_64_ns = 0
|
||||||
|
self.ts_64_fns = 0
|
||||||
|
|
||||||
|
self.last_ts_96_s = 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.pps is not None:
|
||||||
|
self.pps <= 0
|
||||||
|
|
||||||
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
|
def get_ts_96(self):
|
||||||
|
return (self.ts_96_s << 48) | (self.ts_96_ns << 16) | self.ts_96_fns
|
||||||
|
|
||||||
|
def get_ts_96_ns(self):
|
||||||
|
return self.ts_96_s*1e9+self.ts_96_ns+self.ts_96_fns/2**16
|
||||||
|
|
||||||
|
def get_ts_96_s(self):
|
||||||
|
return self.get_ts_96_ns()*1e-9
|
||||||
|
|
||||||
|
def get_ts_64(self):
|
||||||
|
return (self.ts_64_ns << 16) | self.ts_64_fns
|
||||||
|
|
||||||
|
def get_ts_64_ns(self):
|
||||||
|
return self.get_ts_64()/2**16
|
||||||
|
|
||||||
|
def get_ts_64_s(self):
|
||||||
|
return self.get_ts_64()*1e-9
|
||||||
|
|
||||||
|
async def _run(self):
|
||||||
|
while True:
|
||||||
|
await RisingEdge(self.clock)
|
||||||
|
|
||||||
|
self.ts_64_fns, self.ts_64_ns = math.modf(get_sim_time('ns'))
|
||||||
|
|
||||||
|
self.ts_64_ns = int(self.ts_64_ns)
|
||||||
|
self.ts_64_fns = int(self.ts_64_fns*0x10000)
|
||||||
|
|
||||||
|
self.ts_96_s, self.ts_96_ns = divmod(self.ts_64_ns, 1000000000)
|
||||||
|
self.ts_96_fns = self.ts_64_fns
|
||||||
|
|
||||||
|
if self.ts_96 is not None:
|
||||||
|
self.ts_96 <= (self.ts_96_s << 48) | (self.ts_96_ns << 16) | self.ts_96_fns
|
||||||
|
|
||||||
|
if self.ts_64 is not None:
|
||||||
|
self.ts_64 <= (self.ts_64_ns << 16) | self.ts_64_fns
|
||||||
|
|
||||||
|
if self.pps is not None:
|
||||||
|
self.pps <= int(self.last_ts_96_s != self.ts_96_s)
|
||||||
|
|
||||||
|
self.last_ts_96_s = self.ts_96_s
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ THE SOFTWARE.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import cocotb
|
import cocotb
|
||||||
|
from cocotb.queue import Queue, QueueFull
|
||||||
from cocotb.triggers import RisingEdge, FallingEdge, Timer, First, Event
|
from cocotb.triggers import RisingEdge, FallingEdge, Timer, First, Event
|
||||||
from cocotb.utils import get_sim_time, get_sim_steps
|
from cocotb.utils import get_sim_time, get_sim_steps
|
||||||
|
|
||||||
@@ -56,7 +56,11 @@ class RgmiiSource(Reset):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.queue = deque()
|
self.queue = Queue()
|
||||||
|
self.dequeue_event = Event()
|
||||||
|
self.current_frame = None
|
||||||
|
self.idle_event = Event()
|
||||||
|
self.idle_event.set()
|
||||||
|
|
||||||
self.ifg = 12
|
self.ifg = 12
|
||||||
self.mii_mode = False
|
self.mii_mode = False
|
||||||
@@ -64,6 +68,9 @@ class RgmiiSource(Reset):
|
|||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
|
self.queue_occupancy_limit_bytes = -1
|
||||||
|
self.queue_occupancy_limit_frames = -1
|
||||||
|
|
||||||
self.width = 8
|
self.width = 8
|
||||||
self.byte_width = 1
|
self.byte_width = 1
|
||||||
|
|
||||||
@@ -77,31 +84,53 @@ class RgmiiSource(Reset):
|
|||||||
self._init_reset(reset, reset_active_level)
|
self._init_reset(reset, reset_active_level)
|
||||||
|
|
||||||
async def send(self, frame):
|
async def send(self, frame):
|
||||||
self.send_nowait(frame)
|
while self.full():
|
||||||
|
self.dequeue_event.clear()
|
||||||
def send_nowait(self, frame):
|
await self.dequeue_event.wait()
|
||||||
frame = GmiiFrame(frame)
|
frame = GmiiFrame(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 = GmiiFrame(frame)
|
||||||
|
self.queue.put_nowait(frame)
|
||||||
|
self.idle_event.clear()
|
||||||
self.queue_occupancy_bytes += len(frame)
|
self.queue_occupancy_bytes += len(frame)
|
||||||
self.queue_occupancy_frames += 1
|
self.queue_occupancy_frames += 1
|
||||||
self.queue.append(frame)
|
|
||||||
|
|
||||||
def count(self):
|
def count(self):
|
||||||
return len(self.queue)
|
return self.queue.qsize()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
return not self.queue
|
return self.queue.empty()
|
||||||
|
|
||||||
|
def 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):
|
def idle(self):
|
||||||
return self.empty() and not self.active
|
return self.empty() and not self.active
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.queue.clear()
|
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_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
async def wait(self):
|
async def wait(self):
|
||||||
while not self.idle():
|
await self.idle_event.wait()
|
||||||
await RisingEdge(self.clock)
|
|
||||||
|
|
||||||
def _handle_reset(self, state):
|
def _handle_reset(self, state):
|
||||||
if state:
|
if state:
|
||||||
@@ -109,17 +138,28 @@ class RgmiiSource(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
else:
|
|
||||||
self.log.info("Reset de-asserted")
|
|
||||||
if self._run_cr is None:
|
|
||||||
self._run_cr = cocotb.fork(self._run())
|
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.data <= 0
|
self.data <= 0
|
||||||
self.ctrl <= 0
|
self.ctrl <= 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()
|
||||||
|
else:
|
||||||
|
self.log.info("Reset de-asserted")
|
||||||
|
if self._run_cr is None:
|
||||||
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
frame = None
|
frame = None
|
||||||
|
frame_offset = 0
|
||||||
|
frame_data = None
|
||||||
|
frame_error = None
|
||||||
ifg_cnt = 0
|
ifg_cnt = 0
|
||||||
self.active = False
|
self.active = False
|
||||||
d = 0
|
d = 0
|
||||||
@@ -138,11 +178,13 @@ class RgmiiSource(Reset):
|
|||||||
# in IFG
|
# in IFG
|
||||||
ifg_cnt -= 1
|
ifg_cnt -= 1
|
||||||
|
|
||||||
elif frame is None and self.queue:
|
elif frame is None and not self.queue.empty():
|
||||||
# send frame
|
# send frame
|
||||||
frame = self.queue.popleft()
|
frame = self.queue.get_nowait()
|
||||||
|
self.dequeue_event.set()
|
||||||
self.queue_occupancy_bytes -= len(frame)
|
self.queue_occupancy_bytes -= len(frame)
|
||||||
self.queue_occupancy_frames -= 1
|
self.queue_occupancy_frames -= 1
|
||||||
|
self.current_frame = frame
|
||||||
frame.sim_time_start = get_sim_time()
|
frame.sim_time_start = get_sim_time()
|
||||||
frame.sim_time_sfd = None
|
frame.sim_time_sfd = None
|
||||||
frame.sim_time_end = None
|
frame.sim_time_end = None
|
||||||
@@ -153,36 +195,42 @@ class RgmiiSource(Reset):
|
|||||||
self.mii_mode = bool(self.mii_select.value.integer)
|
self.mii_mode = bool(self.mii_select.value.integer)
|
||||||
|
|
||||||
if self.mii_mode:
|
if self.mii_mode:
|
||||||
mii_data = []
|
# convert to MII
|
||||||
mii_error = []
|
frame_data = []
|
||||||
|
frame_error = []
|
||||||
for b, e in zip(frame.data, frame.error):
|
for b, e in zip(frame.data, frame.error):
|
||||||
mii_data.append((b & 0x0F)*0x11)
|
frame_data.append((b & 0x0F)*0x11)
|
||||||
mii_data.append((b >> 4)*0x11)
|
frame_data.append((b >> 4)*0x11)
|
||||||
mii_error.append(e)
|
frame_error.append(e)
|
||||||
mii_error.append(e)
|
frame_error.append(e)
|
||||||
frame.data = mii_data
|
else:
|
||||||
frame.error = mii_error
|
frame_data = frame.data
|
||||||
|
frame_error = frame.error
|
||||||
|
|
||||||
self.active = True
|
self.active = True
|
||||||
|
frame_offset = 0
|
||||||
|
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
d = frame.data.pop(0)
|
d = frame_data[frame_offset]
|
||||||
er = frame.error.pop(0)
|
er = frame_error[frame_offset]
|
||||||
en = 1
|
en = 1
|
||||||
|
frame_offset += 1
|
||||||
|
|
||||||
if frame.sim_time_sfd is None and d in (EthPre.SFD, 0xD, 0xDD):
|
if frame.sim_time_sfd is None and d in (EthPre.SFD, 0xD, 0xDD):
|
||||||
frame.sim_time_sfd = get_sim_time()
|
frame.sim_time_sfd = get_sim_time()
|
||||||
|
|
||||||
if not frame.data:
|
if frame_offset >= len(frame_data):
|
||||||
ifg_cnt = max(self.ifg, 1)
|
ifg_cnt = max(self.ifg, 1)
|
||||||
frame.sim_time_end = get_sim_time()
|
frame.sim_time_end = get_sim_time()
|
||||||
frame.handle_tx_complete()
|
frame.handle_tx_complete()
|
||||||
frame = None
|
frame = None
|
||||||
|
self.current_frame = None
|
||||||
else:
|
else:
|
||||||
d = 0
|
d = 0
|
||||||
er = 0
|
er = 0
|
||||||
en = 0
|
en = 0
|
||||||
self.active = False
|
self.active = False
|
||||||
|
self.idle_event.set()
|
||||||
|
|
||||||
await FallingEdge(self.clock)
|
await FallingEdge(self.clock)
|
||||||
|
|
||||||
@@ -212,8 +260,8 @@ class RgmiiSink(Reset):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.queue = deque()
|
self.queue = Queue()
|
||||||
self.sync = Event()
|
self.active_event = Event()
|
||||||
|
|
||||||
self.mii_mode = False
|
self.mii_mode = False
|
||||||
|
|
||||||
@@ -230,42 +278,46 @@ class RgmiiSink(Reset):
|
|||||||
|
|
||||||
self._init_reset(reset, reset_active_level)
|
self._init_reset(reset, reset_active_level)
|
||||||
|
|
||||||
async def recv(self, compact=True):
|
def _recv(self, frame, compact=True):
|
||||||
while self.empty():
|
if self.queue.empty():
|
||||||
self.sync.clear()
|
self.active_event.clear()
|
||||||
await self.sync.wait()
|
|
||||||
return self.recv_nowait(compact)
|
|
||||||
|
|
||||||
def recv_nowait(self, compact=True):
|
|
||||||
if self.queue:
|
|
||||||
frame = self.queue.popleft()
|
|
||||||
self.queue_occupancy_bytes -= len(frame)
|
self.queue_occupancy_bytes -= len(frame)
|
||||||
self.queue_occupancy_frames -= 1
|
self.queue_occupancy_frames -= 1
|
||||||
|
if compact:
|
||||||
|
frame.compact()
|
||||||
return frame
|
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):
|
def count(self):
|
||||||
return len(self.queue)
|
return self.queue.qsize()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
return not self.queue
|
return self.queue.empty()
|
||||||
|
|
||||||
def idle(self):
|
def idle(self):
|
||||||
return not self.active
|
return not self.active
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.queue.clear()
|
while not self.queue.empty():
|
||||||
|
self.queue.get_nowait()
|
||||||
|
self.active_event.clear()
|
||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
async def wait(self, timeout=0, timeout_unit=None):
|
async def wait(self, timeout=0, timeout_unit=None):
|
||||||
if not self.empty():
|
if not self.empty():
|
||||||
return
|
return
|
||||||
self.sync.clear()
|
|
||||||
if timeout:
|
if timeout:
|
||||||
await First(self.sync.wait(), Timer(timeout, timeout_unit))
|
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||||
else:
|
else:
|
||||||
await self.sync.wait()
|
await self.active_event.wait()
|
||||||
|
|
||||||
def _handle_reset(self, state):
|
def _handle_reset(self, state):
|
||||||
if state:
|
if state:
|
||||||
@@ -273,13 +325,13 @@ class RgmiiSink(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
|
|
||||||
|
self.active = False
|
||||||
else:
|
else:
|
||||||
self.log.info("Reset de-asserted")
|
self.log.info("Reset de-asserted")
|
||||||
if self._run_cr is None:
|
if self._run_cr is None:
|
||||||
self._run_cr = cocotb.fork(self._run())
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
frame = None
|
frame = None
|
||||||
self.active = False
|
self.active = False
|
||||||
@@ -342,8 +394,8 @@ class RgmiiSink(Reset):
|
|||||||
self.queue_occupancy_bytes += len(frame)
|
self.queue_occupancy_bytes += len(frame)
|
||||||
self.queue_occupancy_frames += 1
|
self.queue_occupancy_frames += 1
|
||||||
|
|
||||||
self.queue.append(frame)
|
self.queue.put_nowait(frame)
|
||||||
self.sync.set()
|
self.active_event.set()
|
||||||
|
|
||||||
frame = None
|
frame = None
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.1.6"
|
__version__ = "0.1.16"
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ THE SOFTWARE.
|
|||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
import zlib
|
import zlib
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import cocotb
|
import cocotb
|
||||||
|
from cocotb.queue import Queue, QueueFull
|
||||||
from cocotb.triggers import RisingEdge, Timer, First, Event
|
from cocotb.triggers import RisingEdge, Timer, First, Event
|
||||||
from cocotb.utils import get_sim_time
|
from cocotb.utils import get_sim_time
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ class XgmiiFrame:
|
|||||||
self.ctrl = [0]*n
|
self.ctrl = [0]*n
|
||||||
|
|
||||||
def compact(self):
|
def compact(self):
|
||||||
if not any(self.ctrl):
|
if self.ctrl is not None and not any(self.ctrl):
|
||||||
self.ctrl = None
|
self.ctrl = None
|
||||||
|
|
||||||
def handle_tx_complete(self):
|
def handle_tx_complete(self):
|
||||||
@@ -153,7 +153,11 @@ class XgmiiSource(Reset):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.queue = deque()
|
self.queue = Queue()
|
||||||
|
self.dequeue_event = Event()
|
||||||
|
self.current_frame = None
|
||||||
|
self.idle_event = Event()
|
||||||
|
self.idle_event.set()
|
||||||
|
|
||||||
self.enable_dic = True
|
self.enable_dic = True
|
||||||
self.ifg = 12
|
self.ifg = 12
|
||||||
@@ -162,15 +166,23 @@ class XgmiiSource(Reset):
|
|||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
self.width = len(self.data)
|
self.queue_occupancy_limit_bytes = -1
|
||||||
self.byte_width = len(self.ctrl)
|
self.queue_occupancy_limit_frames = -1
|
||||||
|
|
||||||
assert self.width == self.byte_width * 8
|
self.width = len(self.data)
|
||||||
|
self.byte_size = 8
|
||||||
|
self.byte_lanes = len(self.ctrl)
|
||||||
|
|
||||||
|
assert self.width == self.byte_lanes * self.byte_size
|
||||||
|
|
||||||
|
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_d = 0
|
||||||
self.idle_c = 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_d |= XgmiiCtrl.IDLE << k*8
|
||||||
self.idle_c |= 1 << k
|
self.idle_c |= 1 << k
|
||||||
|
|
||||||
@@ -182,31 +194,53 @@ class XgmiiSource(Reset):
|
|||||||
self._init_reset(reset, reset_active_level)
|
self._init_reset(reset, reset_active_level)
|
||||||
|
|
||||||
async def send(self, frame):
|
async def send(self, frame):
|
||||||
self.send_nowait(frame)
|
while self.full():
|
||||||
|
self.dequeue_event.clear()
|
||||||
def send_nowait(self, frame):
|
await self.dequeue_event.wait()
|
||||||
frame = XgmiiFrame(frame)
|
frame = XgmiiFrame(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 = XgmiiFrame(frame)
|
||||||
|
self.queue.put_nowait(frame)
|
||||||
|
self.idle_event.clear()
|
||||||
self.queue_occupancy_bytes += len(frame)
|
self.queue_occupancy_bytes += len(frame)
|
||||||
self.queue_occupancy_frames += 1
|
self.queue_occupancy_frames += 1
|
||||||
self.queue.append(frame)
|
|
||||||
|
|
||||||
def count(self):
|
def count(self):
|
||||||
return len(self.queue)
|
return self.queue.qsize()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
return not self.queue
|
return self.queue.empty()
|
||||||
|
|
||||||
|
def 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):
|
def idle(self):
|
||||||
return self.empty() and not self.active
|
return self.empty() and not self.active
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.queue.clear()
|
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_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
async def wait(self):
|
async def wait(self):
|
||||||
while not self.idle():
|
await self.idle_event.wait()
|
||||||
await RisingEdge(self.clock)
|
|
||||||
|
|
||||||
def _handle_reset(self, state):
|
def _handle_reset(self, state):
|
||||||
if state:
|
if state:
|
||||||
@@ -214,17 +248,26 @@ class XgmiiSource(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
else:
|
|
||||||
self.log.info("Reset de-asserted")
|
|
||||||
if self._run_cr is None:
|
|
||||||
self._run_cr = cocotb.fork(self._run())
|
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.data <= 0
|
self.data <= 0
|
||||||
self.ctrl <= 0
|
self.ctrl <= 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()
|
||||||
|
else:
|
||||||
|
self.log.info("Reset de-asserted")
|
||||||
|
if self._run_cr is None:
|
||||||
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
frame = None
|
frame = None
|
||||||
|
frame_offset = 0
|
||||||
ifg_cnt = 0
|
ifg_cnt = 0
|
||||||
deficit_idle_cnt = 0
|
deficit_idle_cnt = 0
|
||||||
self.active = False
|
self.active = False
|
||||||
@@ -233,9 +276,9 @@ class XgmiiSource(Reset):
|
|||||||
await RisingEdge(self.clock)
|
await RisingEdge(self.clock)
|
||||||
|
|
||||||
if self.enable is None or self.enable.value:
|
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 ifg_cnt + deficit_idle_cnt > self.byte_lanes-1 or (not self.enable_dic and ifg_cnt > 4):
|
||||||
# in IFG
|
# in IFG
|
||||||
ifg_cnt = ifg_cnt - self.byte_width
|
ifg_cnt = ifg_cnt - self.byte_lanes
|
||||||
if ifg_cnt < 0:
|
if ifg_cnt < 0:
|
||||||
if self.enable_dic:
|
if self.enable_dic:
|
||||||
deficit_idle_cnt = max(deficit_idle_cnt+ifg_cnt, 0)
|
deficit_idle_cnt = max(deficit_idle_cnt+ifg_cnt, 0)
|
||||||
@@ -243,11 +286,13 @@ class XgmiiSource(Reset):
|
|||||||
|
|
||||||
elif frame is None:
|
elif frame is None:
|
||||||
# idle
|
# idle
|
||||||
if self.queue:
|
if not self.queue.empty():
|
||||||
# send frame
|
# send frame
|
||||||
frame = self.queue.popleft()
|
frame = self.queue.get_nowait()
|
||||||
|
self.dequeue_event.set()
|
||||||
self.queue_occupancy_bytes -= len(frame)
|
self.queue_occupancy_bytes -= len(frame)
|
||||||
self.queue_occupancy_frames -= 1
|
self.queue_occupancy_frames -= 1
|
||||||
|
self.current_frame = frame
|
||||||
frame.sim_time_start = get_sim_time()
|
frame.sim_time_start = get_sim_time()
|
||||||
frame.sim_time_sfd = None
|
frame.sim_time_sfd = None
|
||||||
frame.sim_time_end = None
|
frame.sim_time_end = None
|
||||||
@@ -267,7 +312,7 @@ class XgmiiSource(Reset):
|
|||||||
else:
|
else:
|
||||||
min_ifg = 0
|
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
|
ifg_cnt = ifg_cnt-4
|
||||||
frame.start_lane = 4
|
frame.start_lane = 4
|
||||||
frame.data = bytearray([XgmiiCtrl.IDLE]*4)+frame.data
|
frame.data = bytearray([XgmiiCtrl.IDLE]*4)+frame.data
|
||||||
@@ -277,6 +322,7 @@ class XgmiiSource(Reset):
|
|||||||
deficit_idle_cnt = max(deficit_idle_cnt+ifg_cnt, 0)
|
deficit_idle_cnt = max(deficit_idle_cnt+ifg_cnt, 0)
|
||||||
ifg_cnt = 0
|
ifg_cnt = 0
|
||||||
self.active = True
|
self.active = True
|
||||||
|
frame_offset = 0
|
||||||
else:
|
else:
|
||||||
# clear counters
|
# clear counters
|
||||||
deficit_idle_cnt = 0
|
deficit_idle_cnt = 0
|
||||||
@@ -286,19 +332,21 @@ class XgmiiSource(Reset):
|
|||||||
d_val = 0
|
d_val = 0
|
||||||
c_val = 0
|
c_val = 0
|
||||||
|
|
||||||
for k in range(self.byte_width):
|
for k in range(self.byte_lanes):
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
d = frame.data.pop(0)
|
d = frame.data[frame_offset]
|
||||||
if frame.sim_time_sfd is None and d == EthPre.SFD:
|
if frame.sim_time_sfd is None and d == EthPre.SFD:
|
||||||
frame.sim_time_sfd = get_sim_time()
|
frame.sim_time_sfd = get_sim_time()
|
||||||
d_val |= d << k*8
|
d_val |= d << k*8
|
||||||
c_val |= frame.ctrl.pop(0) << k
|
c_val |= frame.ctrl[frame_offset] << k
|
||||||
|
frame_offset += 1
|
||||||
|
|
||||||
if not frame.data:
|
if frame_offset >= len(frame.data):
|
||||||
ifg_cnt = max(self.ifg - (self.byte_width-k), 0)
|
ifg_cnt = max(self.ifg - (self.byte_lanes-k), 0)
|
||||||
frame.sim_time_end = get_sim_time()
|
frame.sim_time_end = get_sim_time()
|
||||||
frame.handle_tx_complete()
|
frame.handle_tx_complete()
|
||||||
frame = None
|
frame = None
|
||||||
|
self.current_frame = None
|
||||||
else:
|
else:
|
||||||
d_val |= XgmiiCtrl.IDLE << k*8
|
d_val |= XgmiiCtrl.IDLE << k*8
|
||||||
c_val |= 1 << k
|
c_val |= 1 << k
|
||||||
@@ -309,6 +357,7 @@ class XgmiiSource(Reset):
|
|||||||
self.data <= self.idle_d
|
self.data <= self.idle_d
|
||||||
self.ctrl <= self.idle_c
|
self.ctrl <= self.idle_c
|
||||||
self.active = False
|
self.active = False
|
||||||
|
self.idle_event.set()
|
||||||
|
|
||||||
|
|
||||||
class XgmiiSink(Reset):
|
class XgmiiSink(Reset):
|
||||||
@@ -329,57 +378,66 @@ class XgmiiSink(Reset):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active = False
|
self.active = False
|
||||||
self.queue = deque()
|
self.queue = Queue()
|
||||||
self.sync = Event()
|
self.active_event = Event()
|
||||||
|
|
||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
self.width = len(self.data)
|
self.width = len(self.data)
|
||||||
self.byte_width = len(self.ctrl)
|
self.byte_size = 8
|
||||||
|
self.byte_lanes = len(self.ctrl)
|
||||||
|
|
||||||
assert self.width == self.byte_width * 8
|
assert self.width == self.byte_lanes * self.byte_size
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
|
|
||||||
self._init_reset(reset, reset_active_level)
|
self._init_reset(reset, reset_active_level)
|
||||||
|
|
||||||
async def recv(self, compact=True):
|
def _recv(self, frame, compact=True):
|
||||||
while self.empty():
|
if self.queue.empty():
|
||||||
self.sync.clear()
|
self.active_event.clear()
|
||||||
await self.sync.wait()
|
|
||||||
return self.recv_nowait(compact)
|
|
||||||
|
|
||||||
def recv_nowait(self, compact=True):
|
|
||||||
if self.queue:
|
|
||||||
frame = self.queue.popleft()
|
|
||||||
self.queue_occupancy_bytes -= len(frame)
|
self.queue_occupancy_bytes -= len(frame)
|
||||||
self.queue_occupancy_frames -= 1
|
self.queue_occupancy_frames -= 1
|
||||||
|
if compact:
|
||||||
|
frame.compact()
|
||||||
return frame
|
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):
|
def count(self):
|
||||||
return len(self.queue)
|
return self.queue.qsize()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
return not self.queue
|
return self.queue.empty()
|
||||||
|
|
||||||
def idle(self):
|
def idle(self):
|
||||||
return not self.active
|
return not self.active
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.queue.clear()
|
while not self.queue.empty():
|
||||||
|
self.queue.get_nowait()
|
||||||
|
self.active_event.clear()
|
||||||
self.queue_occupancy_bytes = 0
|
self.queue_occupancy_bytes = 0
|
||||||
self.queue_occupancy_frames = 0
|
self.queue_occupancy_frames = 0
|
||||||
|
|
||||||
async def wait(self, timeout=0, timeout_unit=None):
|
async def wait(self, timeout=0, timeout_unit=None):
|
||||||
if not self.empty():
|
if not self.empty():
|
||||||
return
|
return
|
||||||
self.sync.clear()
|
|
||||||
if timeout:
|
if timeout:
|
||||||
await First(self.sync.wait(), Timer(timeout, timeout_unit))
|
await First(self.active_event.wait(), Timer(timeout, timeout_unit))
|
||||||
else:
|
else:
|
||||||
await self.sync.wait()
|
await self.active_event.wait()
|
||||||
|
|
||||||
def _handle_reset(self, state):
|
def _handle_reset(self, state):
|
||||||
if state:
|
if state:
|
||||||
@@ -387,13 +445,13 @@ class XgmiiSink(Reset):
|
|||||||
if self._run_cr is not None:
|
if self._run_cr is not None:
|
||||||
self._run_cr.kill()
|
self._run_cr.kill()
|
||||||
self._run_cr = None
|
self._run_cr = None
|
||||||
|
|
||||||
|
self.active = False
|
||||||
else:
|
else:
|
||||||
self.log.info("Reset de-asserted")
|
self.log.info("Reset de-asserted")
|
||||||
if self._run_cr is None:
|
if self._run_cr is None:
|
||||||
self._run_cr = cocotb.fork(self._run())
|
self._run_cr = cocotb.fork(self._run())
|
||||||
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
frame = None
|
frame = None
|
||||||
self.active = False
|
self.active = False
|
||||||
@@ -402,7 +460,7 @@ class XgmiiSink(Reset):
|
|||||||
await RisingEdge(self.clock)
|
await RisingEdge(self.clock)
|
||||||
|
|
||||||
if self.enable is None or self.enable.value:
|
if self.enable is None or self.enable.value:
|
||||||
for offset in range(self.byte_width):
|
for offset in range(self.byte_lanes):
|
||||||
d_val = (self.data.value.integer >> (offset*8)) & 0xff
|
d_val = (self.data.value.integer >> (offset*8)) & 0xff
|
||||||
c_val = (self.ctrl.value.integer >> offset) & 1
|
c_val = (self.ctrl.value.integer >> offset) & 1
|
||||||
|
|
||||||
@@ -427,8 +485,8 @@ class XgmiiSink(Reset):
|
|||||||
self.queue_occupancy_bytes += len(frame)
|
self.queue_occupancy_bytes += len(frame)
|
||||||
self.queue_occupancy_frames += 1
|
self.queue_occupancy_frames += 1
|
||||||
|
|
||||||
self.queue.append(frame)
|
self.queue.put_nowait(frame)
|
||||||
self.sync.set()
|
self.active_event.set()
|
||||||
|
|
||||||
frame = None
|
frame = None
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ deps =
|
|||||||
cocotb-test
|
cocotb-test
|
||||||
coverage
|
coverage
|
||||||
pytest-cov
|
pytest-cov
|
||||||
git+https://github.com/cocotb/cocotb.git@e892a3ea48#egg=cocotb
|
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
pytest --cov=cocotbext --cov=tests --cov-branch -n auto
|
pytest --cov=cocotbext --cov=tests --cov-branch -n auto
|
||||||
|
|||||||
61
tests/eth_mac/Makefile
Normal file
61
tests/eth_mac/Makefile
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Copyright (c) 2021 Alex Forencich
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
TOPLEVEL_LANG = verilog
|
||||||
|
|
||||||
|
SIM ?= icarus
|
||||||
|
WAVES ?= 0
|
||||||
|
|
||||||
|
COCOTB_HDL_TIMEUNIT = 1ns
|
||||||
|
COCOTB_HDL_TIMEPRECISION = 1ns
|
||||||
|
|
||||||
|
DUT = test_eth_mac
|
||||||
|
TOPLEVEL = $(DUT)
|
||||||
|
MODULE = $(DUT)
|
||||||
|
VERILOG_SOURCES += $(DUT).v
|
||||||
|
|
||||||
|
ifeq ($(SIM), icarus)
|
||||||
|
PLUSARGS += -fst
|
||||||
|
|
||||||
|
ifeq ($(WAVES), 1)
|
||||||
|
VERILOG_SOURCES += iverilog_dump.v
|
||||||
|
COMPILE_ARGS += -s iverilog_dump
|
||||||
|
endif
|
||||||
|
else ifeq ($(SIM), verilator)
|
||||||
|
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
|
||||||
|
|
||||||
|
ifeq ($(WAVES), 1)
|
||||||
|
COMPILE_ARGS += --trace-fst
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||||
|
|
||||||
|
iverilog_dump.v:
|
||||||
|
echo 'module iverilog_dump();' > $@
|
||||||
|
echo 'initial begin' >> $@
|
||||||
|
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
|
||||||
|
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
|
||||||
|
echo 'end' >> $@
|
||||||
|
echo 'endmodule' >> $@
|
||||||
|
|
||||||
|
clean::
|
||||||
|
@rm -rf iverilog_dump.v
|
||||||
|
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||||
198
tests/eth_mac/test_eth_mac.py
Normal file
198
tests/eth_mac/test_eth_mac.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
|
||||||
|
Copyright (c) 2021 Alex Forencich
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import 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 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)
|
||||||
|
|
||||||
|
cocotb.fork(Clock(dut.tx_clk, 6.4, units="ns").start())
|
||||||
|
cocotb.fork(Clock(dut.rx_clk, 6.4, 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_96=dut.tx_ptp_time,
|
||||||
|
clock=dut.tx_clk
|
||||||
|
)
|
||||||
|
|
||||||
|
self.rx_ptp = PtpClockSimTime(
|
||||||
|
ts_96=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 <= 1
|
||||||
|
self.dut.rx_rst <= 1
|
||||||
|
await RisingEdge(self.dut.tx_clk)
|
||||||
|
await RisingEdge(self.dut.tx_clk)
|
||||||
|
self.dut.tx_rst <= 0
|
||||||
|
self.dut.rx_rst <= 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 cocotb.SIM_NAME:
|
||||||
|
|
||||||
|
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", [10e9, 1e9])
|
||||||
|
factory.generate_tests()
|
||||||
|
|
||||||
|
|
||||||
|
# cocotb-test
|
||||||
|
|
||||||
|
tests_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def test_eth_mac(request):
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
57
tests/eth_mac/test_eth_mac.v
Normal file
57
tests/eth_mac/test_eth_mac.v
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2021 Alex Forencich
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Language: Verilog 2001
|
||||||
|
|
||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ethernet MAC model test
|
||||||
|
*/
|
||||||
|
module test_eth_mac
|
||||||
|
(
|
||||||
|
inout wire tx_clk,
|
||||||
|
inout wire tx_rst,
|
||||||
|
inout wire [63:0] tx_axis_tdata,
|
||||||
|
inout wire [7:0] tx_axis_tkeep,
|
||||||
|
inout wire tx_axis_tlast,
|
||||||
|
inout wire [16:0] tx_axis_tuser,
|
||||||
|
inout wire tx_axis_tvalid,
|
||||||
|
inout wire tx_axis_tready,
|
||||||
|
inout wire [95:0] tx_ptp_time,
|
||||||
|
inout wire [95:0] tx_ptp_ts,
|
||||||
|
inout wire [15:0] tx_ptp_ts_tag,
|
||||||
|
inout wire tx_ptp_ts_valid,
|
||||||
|
|
||||||
|
inout wire rx_clk,
|
||||||
|
inout wire rx_rst,
|
||||||
|
inout wire [63:0] rx_axis_tdata,
|
||||||
|
inout wire [7:0] rx_axis_tkeep,
|
||||||
|
inout wire rx_axis_tlast,
|
||||||
|
inout wire [96:0] rx_axis_tuser,
|
||||||
|
inout wire rx_axis_tvalid,
|
||||||
|
inout wire [95:0] rx_ptp_time
|
||||||
|
);
|
||||||
|
|
||||||
|
endmodule
|
||||||
61
tests/ptp_clock_sim_time/Makefile
Normal file
61
tests/ptp_clock_sim_time/Makefile
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Copyright (c) 2020 Alex Forencich
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
TOPLEVEL_LANG = verilog
|
||||||
|
|
||||||
|
SIM ?= icarus
|
||||||
|
WAVES ?= 0
|
||||||
|
|
||||||
|
COCOTB_HDL_TIMEUNIT = 1ns
|
||||||
|
COCOTB_HDL_TIMEPRECISION = 1ps
|
||||||
|
|
||||||
|
DUT = test_ptp_clock_sim_time
|
||||||
|
TOPLEVEL = $(DUT)
|
||||||
|
MODULE = $(DUT)
|
||||||
|
VERILOG_SOURCES += $(DUT).v
|
||||||
|
|
||||||
|
ifeq ($(SIM), icarus)
|
||||||
|
PLUSARGS += -fst
|
||||||
|
|
||||||
|
ifeq ($(WAVES), 1)
|
||||||
|
VERILOG_SOURCES += iverilog_dump.v
|
||||||
|
COMPILE_ARGS += -s iverilog_dump
|
||||||
|
endif
|
||||||
|
else ifeq ($(SIM), verilator)
|
||||||
|
COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH
|
||||||
|
|
||||||
|
ifeq ($(WAVES), 1)
|
||||||
|
COMPILE_ARGS += --trace-fst
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||||
|
|
||||||
|
iverilog_dump.v:
|
||||||
|
echo 'module iverilog_dump();' > $@
|
||||||
|
echo 'initial begin' >> $@
|
||||||
|
echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@
|
||||||
|
echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@
|
||||||
|
echo 'end' >> $@
|
||||||
|
echo 'endmodule' >> $@
|
||||||
|
|
||||||
|
clean::
|
||||||
|
@rm -rf iverilog_dump.v
|
||||||
|
@rm -rf dump.fst $(TOPLEVEL).fst
|
||||||
125
tests/ptp_clock_sim_time/test_ptp_clock_sim_time.py
Normal file
125
tests/ptp_clock_sim_time/test_ptp_clock_sim_time.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
|
||||||
|
Copyright (c) 2021 Alex Forencich
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import cocotb_test.simulator
|
||||||
|
|
||||||
|
import cocotb
|
||||||
|
from cocotb.clock import Clock
|
||||||
|
from cocotb.triggers import RisingEdge
|
||||||
|
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.fork(Clock(dut.clk, 6.4, units="ns").start())
|
||||||
|
|
||||||
|
self.ptp_clock = PtpClockSimTime(
|
||||||
|
ts_96=dut.ts_96,
|
||||||
|
ts_64=dut.ts_64,
|
||||||
|
pps=dut.pps,
|
||||||
|
clock=dut.clk
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cocotb.test()
|
||||||
|
async def run_test(dut):
|
||||||
|
|
||||||
|
tb = TB(dut)
|
||||||
|
|
||||||
|
await RisingEdge(dut.clk)
|
||||||
|
await RisingEdge(dut.clk)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
for k in range(10000):
|
||||||
|
await RisingEdge(dut.clk)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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_96_diff = time_delta - ts_96_delta
|
||||||
|
ts_64_diff = time_delta - ts_64_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)
|
||||||
|
|
||||||
|
assert abs(ts_96_diff) < 1e-12
|
||||||
|
assert abs(ts_64_diff) < 1e-12
|
||||||
|
|
||||||
|
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 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_96,
|
||||||
|
inout wire [63:0] ts_64,
|
||||||
|
inout wire pps
|
||||||
|
);
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -123,7 +123,7 @@ async def run_test_alignment(dut, payload_data=None, ifg=12, enable_dic=True,
|
|||||||
|
|
||||||
tb = TB(dut)
|
tb = TB(dut)
|
||||||
|
|
||||||
byte_width = tb.source.width // 8
|
byte_lanes = tb.source.byte_lanes
|
||||||
|
|
||||||
tb.source.ifg = ifg
|
tb.source.ifg = ifg
|
||||||
tb.source.enable_dic = enable_dic
|
tb.source.enable_dic = enable_dic
|
||||||
@@ -164,23 +164,23 @@ async def run_test_alignment(dut, payload_data=None, ifg=12, enable_dic=True,
|
|||||||
for test_data in test_frames:
|
for test_data in test_frames:
|
||||||
if ifg == 0:
|
if ifg == 0:
|
||||||
lane = 0
|
lane = 0
|
||||||
if force_offset_start and byte_width > 4:
|
if force_offset_start and byte_lanes > 4:
|
||||||
lane = 4
|
lane = 4
|
||||||
|
|
||||||
start_lane_ref.append(lane)
|
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:
|
if enable_dic:
|
||||||
offset = lane % 4
|
offset = lane % 4
|
||||||
if deficit_idle_count+offset >= 4:
|
if deficit_idle_count+offset >= 4:
|
||||||
offset += 4
|
offset += 4
|
||||||
lane = (lane - offset) % byte_width
|
lane = (lane - offset) % byte_lanes
|
||||||
deficit_idle_count = (deficit_idle_count + offset) % 4
|
deficit_idle_count = (deficit_idle_count + offset) % 4
|
||||||
else:
|
else:
|
||||||
offset = lane % 4
|
offset = lane % 4
|
||||||
if offset > 0:
|
if offset > 0:
|
||||||
offset += 4
|
offset += 4
|
||||||
lane = (lane - offset) % byte_width
|
lane = (lane - offset) % byte_lanes
|
||||||
|
|
||||||
tb.log.info("start_lane_ref: %s", start_lane_ref)
|
tb.log.info("start_lane_ref: %s", start_lane_ref)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user