DMA Engine – openenoc_endpoint_dma_engine
Executes local memory operations for peer transfers, raw Ethernet DMA, and scalar remote memory accesses.
Source: openenoc_endpoint_dma_engine.sv.
Parameters
Name |
Default value |
Allowed values |
Description |
|---|---|---|---|
|
|
Integer >= 1; match generated CSR array |
Number of peer-table entries. |
|
|
Integer >= 1; enough bits for the peer count |
Width of local peer indices; all connected interfaces must agree. |
|
|
36..8192 bytes |
Synthesized frame ceiling excluding FCS; reserves 32 bytes for the largest peer-frame overhead. |
|
|
1..256 beats |
Maximum AXI burst length issued by the memory DMA controllers. |
|
|
Integer >= 1 |
Number of local fragment bookkeeping contexts; not wire credits. |
|
|
0 or 1 |
One issued peer fragment at a time when set; zero permits concurrent scheduler contexts. |
|
|
0 or 1 |
Enables unaligned AXI memory accesses in the underlying TAXI DMA. |
Signals
Interface ports contain channels in both directions. The table identifies the selected modport or stream role; member directions follow that interface.
Name |
Type |
Direction / role |
Description |
|---|---|---|---|
|
|
input |
Clock for this component or interface domain. |
|
|
input |
Active-high reset of transaction and control state. |
|
|
bidirectional (core) |
Peer/raw DMA commands and status, IRQ enables, and initiating RMEM accesses. |
|
|
bidirectional (mst) |
Peer-table query client and coherent response snapshot. |
|
openenoc_dma_transfer_if.requester |
bidirectional (requester) |
Commands and status for locally initiated remote transactions. |
|
openenoc_dma_transfer_if.executor |
bidirectional (executor) |
Commands and status for servicing received remote transactions. |
|
|
source |
Memory-read payloads or complete raw frames toward transport. |
|
|
sink |
Memory-write payloads or complete raw frames from transport. |
|
|
bidirectional (wr_mst) |
Outgoing AXI4 memory write address/data/response channels. |
|
|
bidirectional (rd_mst) |
Outgoing AXI4 memory read address/response channels. |
|
|
bidirectional (mst) |
Local scalar-memory master for received RMEM operations. |
|
openenoc_irq_event_if.producer |
bidirectional (producer) |
Peer-DMA, raw-TX, and raw-RX event producers, in that order. |
|
openenoc_irq_event_if.producer |
bidirectional (producer) |
Independent initiating RMEM error-event producer. |
|
openenoc_irq_event_if.producer |
bidirectional (producer) |
Received-request servicing error-event producer. |
Architecture and operation
Responsibilities and boundary
The DMA engine owns memory access for peer bulk DMA, raw non-oETP DMA, received oETP requests, and scalar RMEM. A single TAXI AXI read DMA and a single AXI write DMA are shared by the implemented bulk/raw operation classes. Peer configuration is acquired through shared lookup and retained for the accepted block; subsequent CSR writes do not alter that block’s snapshot.
endpoint_if carries control/status and the external initiating RMEM
request. peer_lookup_if obtains peer entries. initiator_if requests
protocol transactions, while responder_if executes received transactions.
m_axis_oetp/s_axis_oetp carry the companion streams;
m_axi_rd/m_axi_wr access bulk memory. m_local_cpuif is the local RMEM
execution master. The three irq_event_if ports serve peer DMA and raw TX/RX;
rmem_irq_event_if keeps initiating RMEM notification separate, and
responder_irq_event_if reports received-request servicing failures.
AXI, AXI-Stream, and local CPUIF data paths are fixed at 32 bits. AXI byte
strobes and AXI-Stream KEEP masks have four bits. Elaboration checks reject
connected interfaces with other data or byte-enable widths. Receive byte
counting uses the four KEEP bits; valid contiguous masks are 0x1, 0x3,
0x7, and 0xF, with 0xF required before the final beat.
AXI addresses, local CPUIF addresses, and the address fields on the transfer and peer-lookup interfaces are fixed at 32 bits. Elaboration checks reject connected interfaces with other address widths.
Registered control and memory boundaries
The peer scheduler, raw TX, raw RX, responder, memory read, memory write, RMEM, and notification controllers each use a separate clocked process with direct nonblocking register updates. The peer scheduler alone owns the shared peer and fragment contexts; memory controllers report accepted and completed operations through registered event pulses. Arbitration and stream assembly are internal combinational calculations. CSR status fields alias the owning status registers. Hardware clears, lookup requests, transfer commands/completions, CPUIF signals, and IRQ producer signals are registered. A held CSR clear command is applied once; a failure arriving before that command deasserts remains sticky.
A two-entry AXI-Stream register slice (skid buffer) registers each direction of the memory-payload AXIS link. All five AXI channels also use registered buffers, including RREADY and BREADY. Data accepted before downstream backpressure takes effect remains in those buffers. This adds pipeline latency without introducing full-fragment buffering or changing the terminal-status join.
Local read completion counts bytes accepted into the TX register slice.
The raw TX CSR completion and a peer source-read terminal status can therefore
precede the final handshake on m_axis_oetp if that slice still holds data.
Receive completion instead waits for the memory-write result as well as the
required stream/framing outcome. Neither local completion confirms Ethernet
delivery to the remote application.
Streams and fragmentation
The DMA and RMEM Transfer Interface – openenoc_dma_transfer_if contract carries complete Ethernet frames without FCS for raw DMA and exactly the descriptor’s memory bytes for peer bulk DMA. Protocol headers, EndOfData, and protocol padding belong to oETP. Scalar RMEM data uses command/completion fields and never AXIS.
The synthesized raw-frame ceiling reserves 32 bytes for Ethernet, the largest
DMA command metadata, and EndOfData. The common physical peer fragment ceiling
is 4 * floor((MAX_RAW_FRAME_SIZE - 32) / 4), or 8160 bytes at the default.
At locally initiated block acceptance, snapshot
conf.dma_max_fragment_size.bytes after rounding down to a multiple of
four. Its effective value must be between 4 and the physical ceiling.
Intermediate fragments use that limit; the final fragment carries exactly
the remaining bytes and can be shorter than four bytes. Incoming requests are
bounded by the physical ceiling, independently of the receiver’s local CSR
fragment-size setting.
FRAGMENT_SLOTS bounds local bookkeeping contexts; it is not a wire credit
mechanism. The oETP scheduler implements the agreed sequential unicast
request-response policy. Raw frames are not divided into peer protocol
fragments. For a maximum-size unaligned raw TX read that exceeds the underlying
TAXI read counter’s safe budget, the engine reads 8188 bytes followed by the
remainder in a second local descriptor. It suppresses the intermediate TLAST,
producing one Ethernet frame.
Status and notification
Peer dma.error/error_code are shared by bulk DMA and RMEM and remain sticky
across starts and successful completions. dma.clear_error clears the record;
a concurrent new failure wins. Raw TX/RX have the corresponding clear_errors
controls. Error clearing does not acknowledge an IRQ or alter descriptor
ownership, abort, idle, or done state.
Initiator completions preserve final local/remote CSR encoding supplied by oETP. A failed received request records the source peer’s error before its completion becomes visible. Its enabled local IRQ is retained independently of reply readiness and IRQ FIFO capacity. Successful received requests do not generate a responder event. Reporting an incoming failure does not overwrite idle/done for an independent locally initiated block.
Reset clears contexts, pending streams, descriptor state, and event state. No failure rolls back memory writes already performed. Software owns buffer synchronization and any deliberate retransmission.
Scalar RMEM and terminal status
The initiating RMEM controller captures a CPUIF pulse and resolves its virtual address with LOOKUP_BY_RMEM. It validates the full four-byte aligned word, translates it once to the remote base, and sends a scalar command. A pending RMEM operation has priority over unsent bulk fragments; an already advertised or active fragment finishes first. The controller uses IDLE, LOOKUP_REQ, LOOKUP_RSP, READY, SEND, WAIT, and DONE states in its own clocked process.
Successful reads return the completion’s scalar data. Failed reads return
cpuif_rd_data = 0xFFFFFFFF with cpuif_rd_ack; failed writes assert
cpuif_wr_ack. The native CPUIF members are rd_data, rd_ack, and
wr_ack respectively. The generated external CPUIF boundary remains ACK-only.
The shared peer error is recorded and an enabled RMEM_ERROR notification is
retained separately from bus completion.
The enable is captured when the failure is detected, so delayed IRQ admission
cannot reinterpret it. One pending notification bit per peer retains failed
accesses independently of the next CPUIF request. Repeated errors of that peer
before admission coalesce into that notification while each failure updates
the sticky CSR code. Pending peers are selected round-robin; a new failure wins
a simultaneous clear of its pending bit. Once an event has been admitted,
a later failure creates another pending notification. Reset clears these bits.
A lookup miss acknowledges the access as failed but
cannot identify a peer entry for per-peer CSR/IRQ reporting.
Received RMEM requests execute through m_local_cpuif using the request’s
final byte address, 32-bit BitEnable, and scalar data. A validated zero-BitEnable
write succeeds without issuing a local CPUIF request. The local request is a
single-cycle pulse followed by ACK wait, allowing a same-cycle or delayed ACK.
A local CPUIF read/write error uses cause 4/6 respectively.
Every received request is checked against its peer’s local base, size, and permitted mode before memory access. Range arithmetic is 33-bit and rejects wraparound. Accepted addresses and lengths are retained in the responder context; subsequent configuration changes do not revalidate active servicing.
For incoming bulk writes, req_rx_status requires a join of final memory
completion and final framing/EndOfData status. Early data TLAST or a late bad
marker cannot report success. Rejected memory requests drain an accepted
payload without writing it. Local AXI causes 4..7 take precedence over a
consequent framing error. For initiating writes, tx_status holds the final
source-read result until the protocol engine accepts it.
SERIAL_PEER_REQUESTS = 1 permits one issued peer fragment until its local
and protocol completions retire. Unsent fragments of a failed block are
invalidated. Setting the parameter to zero permits multiple outstanding
commands for independent scheduler verification; endpoint integration retains
the sequential default and the protocol engine still has one initiating slot.
FRAGMENT_SLOTS permits preparation and fair selection among peer contexts.
The isolated scheduler fixture selects SERIAL_PEER_REQUESTS = 0 to check
reordered commands/completions independently of the protocol engine’s single
initiating slot. Endpoint integration uses the sequential default. A completion
with no forwarded payload reports zero transferred bytes; a partial failure
reports the actual forwarded count. Internal routing and role metadata follow
DMA and RMEM Transfer Interface – openenoc_dma_transfer_if and are never added to the wire protocol.
RMEM response timers and per-fragment DMA response timers belong to oETP. Zero disables either timer. DMA timeout stops remaining fragments and records TIMEOUT = 8; no whole-block timer or automatic retry is required. The complete timer interval, response-matching rules, and wire-to-CSR error mapping are specified in oETP Engine – openenoc_endpoint_oetp_engine; the corresponding software controls are summarized in Full endpoint register block – openenoc_endpoint_full_csr.
Functional scenarios
These examples connect the command/status channels of DMA and RMEM Transfer Interface – openenoc_dma_transfer_if to the DMA engine’s public memory and payload ports. Interface-only handshakes and firmware IRQ service are covered by DMA and RMEM Transfer Interface – openenoc_dma_transfer_if, IRQ event producer – openenoc_irq_event_if, and Full endpoint – openenoc_endpoint_full.
All data paths are 32 bits wide. A0, A1, and subsequent
A labels denote addresses; I0 denotes a peer index, and
S0/S1 denote transfer sequences. DA0, DA1, …
are data words leaving the local endpoint; DB0, DB1, …
arrive from the peer. Scalar request data uses D0/D1.
Labels are local to each example.
Raw Ethernet examples use 66-byte frames, counted from the destination MAC
through the final frame byte, excluding preamble, SFD, and FCS. Their first
16 beats have tkeep = 0xF; beat 16 has tkeep = 0x3 and
tlast = 1. Peer bulk streams carry the exact memory payload,
with oETP framing added or removed by the protocol engine.
A transfer occurs at the ending rising edge of a shown interval when VALID
and READY are both high. Gaps omit intermediate beats, arbitration, or
memory/transport waiting; timing across a gap is not a fixed latency.
x marks irrelevant values. CSR field paths are relative to
endpoint_if.csr_to_core for inputs and
endpoint_if.core_to_csr for Core outputs, with generated
.value/.next wrappers omitted.
AXI response codes are BRESP = 0 for OKAY and BRESP = 2
for SLVERR.
Raw TX completion with a stalled final output beat
Software has submitted an aligned source buffer A0 and
frame_length.bytes = 66; IRQ admission and descriptor acceptance
have already completed. The read DMA supplies one complete raw frame on
m_axis_oetp. The gap omits DA2 through DA14.
After DA15 transfers, the sink lowers READY and holds the final
DA16 beat. DATA, KEEP, LAST, and VALID remain stable throughout
that stall. The final memory-read result has already arrived and all 66
bytes have entered the TX register slice, so command_status.done
and command_status.idle rise while the final output beat is still
waiting. transferred_length.bytes = 66 reports this local handoff.
The later READY edge transfers DA16 to oETP. Raw TX completion does
not wait for Ethernet serialization or a remote acknowledgement. If IRQ
admission reserved an event, source 1 is committed from the completed local
operation. A pending event commit can still delay acceptance of the next
CSR request even after the raw TX idle flag rises.
Raw TX completion while the final AXIS beat is stalled
Raw RX overflow drains the frame and waits for AXI completion
An aligned receive buffer A0 has capacity 65 bytes. Its descriptor
is armed, exclusively claimed, and assigned to this 66-byte raw frame
before the shown interval. Payload metadata selects
ROUTE_NON_OETP_DMA. The first gap omits DB2 through
DB15 and the corresponding earlier AXI traffic.
The final input beat DB16 carries two valid bytes
(tkeep = 0x3). Only its first byte fits in the descriptor.
The memory DMA writes that byte with wstrb = 0x1, at
A1 = A0 + 64, and consumes the excess frame byte without writing
past the buffer. The final AXI beat holds its data, strobe, and WLAST
while WREADY is low. With the default 16-beat burst limit, this is the
one-beat second burst; the first burst writes the preceding 64 bytes.
Even after input TLAST, the descriptor remains busy until the final AXI
write response and memory-DMA terminal status have arrived. The illustrated
response is OKAY, so the final result is the stream-capacity error:
error_code = LOCAL_OVERFLOW,
received_length.bytes = 65, idle = 1, and done = 0.
The stored length counts bytes written within capacity, not all incoming
frame bytes. Draining through TLAST preserves the next frame boundary.
A 66-byte raw frame exceeds a 65-byte receive buffer
Peer read retirement joins protocol and memory completion
Use the sequential default SERIAL_PEER_REQUESTS = 1. Peer
I0 mirrors a 66-byte remote block into local memory, with an accepted
64-byte fragment limit. Let A0 be the local base and A1
the remote base. The block retains these addresses and its fragment-size
snapshot even if software changes the corresponding CSRs while it runs.
Sequence |
Remote address |
Local write address |
Length |
Block-final flag |
|---|---|---|---|---|
|
|
|
64 |
0 |
|
|
|
2 |
1 |
The first fragment has already retired. The diagram begins with the final
TRANSFER_KIND_DMA/DMA_OP_READ command, using req_addr = A3,
req_len = 2, req_peer_idx = I0, and req_last = 1.
Its returned payload is DB16, written at local address A2.
The companion stream carries tid = S1,
tdest = {ROUTE_PEER, I0}, the initiator role bit clear, and the
block-final user bit set. TLAST ends this fragment’s payload; the user
bit distinguishes the final fragment of the block. These fields select
the matching local memory context and are not Ethernet-link metadata.
A successful protocol completion for I0/S1 arrives before the
local AXI write response. The DMA engine retains that completion, keeps
the peer busy, and waits for memory completion. Only after the final
OKAY response, terminal memory status, and fragment retirement do
dma.idle and dma.done rise. An enabled peer event is emitted
once for the completed block, rather than once per fragment.
Peer read completion waits for the final local AXI write
Received write joins framing status and preserves the AXI cause
A validated received command has already been accepted on responder_if:
kind DMA, operation WRITE, peer I0, sequence S0, length 12,
and req_rx_status = 1. Its destination lies inside the peer’s local
mirror-to-local window. Payload metadata selects {ROUTE_PEER, I0},
sequence S0, and the responder role.
The input ends early after two full words, DB0/DB1, or eight bytes.
The gap includes their AXI write traffic. The memory slave returns
BRESP = SLVERR, which maps to local write cause 6. The protocol
engine later supplies matching receive terminal status with length 8
and framing cause 2. No completion is advertised while this required
status is still missing, even though memory servicing has ended.
Once both outcomes are known, the local AXI error takes precedence:
the responder completion has cpl_error = 1,
cpl_error_code = LOCAL_AXI_WR_SLVERR, and cpl_transferred_len = 8.
The peer CSR error is recorded before that completion becomes visible.
Without an AXI error, the framing failure would instead use wire cause
LOCAL_AXIS in the completion and REMOTE_AXIS in the receiving peer’s CSR.
No error path rolls back memory writes already performed.
Assume the error’s IRQ source is enabled but
responder_irq_event_if.admit_ready = 0 throughout this interval.
The completion still transfers to the ready protocol engine. Its retained
notification keeps responder_if.req_ready = 0 after the reply,
until IRQ admission and any reserved commit finish. Reporting this incoming
failure does not finish or replace an independently initiated peer block.
Received-write completion joins framing status with the original AXI error
Scalar writes with zero and nonzero BitEnable
Both received requests are validated TRANSFER_KIND_RMEM/DMA_OP_WRITE
commands for peer I0, with length four, aligned addresses
A0/A1 inside an RMEM-mode window, and req_rx_status = 0.
They require no AXIS payload or AXI memory descriptor.
The first request, sequence S0, uses M0 = 0x00000000.
It succeeds with four transferred bytes without issuing a local CPUIF
request. Window validation still applies before this zero-enable shortcut.
The second request, sequence S1, uses
M1 = 0x0000000F, enabling bits 3:0 of scalar data D1.
The DMA engine issues one local CPUIF write pulse and retains the address,
data, and per-bit enable while waiting for the delayed write ACK.
A successful ACK permits the four-byte completion, which remains stable
until the requester accepts it.
In the numbered example, requests transfer in cycles 1 and 5. The second request produces the local CPUIF pulse in cycle 7, receives ACK in cycle 9, and presents its completion from cycle 11 through acceptance in cycle 13. The first completion transfers in cycle 3 without any local request pulse.
Zero-enable RMEM write and a scalar write with delayed local acknowledgement
Scalar read with delayed local acknowledgement
A received TRANSFER_KIND_RMEM/DMA_OP_READ command selects peer I0
and sequence S0, with req_len = 4 and req_rx_status = 0.
Its aligned req_addr = A0 is the final local byte address inside the
peer’s RMEM-mode window. Scalar read data returns in cpl_rdata.
The command transfers in cycle 1. The DMA engine issues one local CPUIF
read pulse in cycle 3 with req_is_wr = 0, then waits without repeating
the request. The memory returns rd_ack = 1 and rd_data = D0 in
cycle 6. The engine captures the data at that acknowledgement edge.
The successful completion becomes valid in cycle 8, with
cpl_rdata = D0, cpl_transferred_len = 4, and the original
I0/S0 context. The requester delays acceptance until cycle 10;
VALID, data, and completion metadata remain stable throughout the stall,
even after local rd_ack has returned low. The responder is ready for
another command in cycle 12.
If rd_err accompanies the local ACK, the engine instead reports
LOCAL_AXI_RD_SLVERR in a failed completion and records
the peer diagnostic before advertising that completion.
RMEM read data retained after a delayed local acknowledgement
Verification
The test suite is in dv/core/openenoc_endpoint_dma_engine/.
Testbench
This cocotb suite verifies DMA memory access, fragmentation, peer/raw flows, errors, and backpressure in isolation. It uses AXI memory and AXI4-Stream models, a behavioral peer-lookup responder, and driven IRQ and transaction handshakes. The fixture uses four peers, eight fragment slots, and 32-bit data. Pytest runs 18 cases at raw-frame ceilings of 96 and 8192 bytes; compact transfer cases use a 64-byte local fragment size. The scheduler fixture enables multiple outstanding commands to exercise reordered completions.
A subcycle test checks control, CSR, IRQ, AXIS, and all AXI master outputs during an active memory read, and verifies that a held clear cannot erase a later failure.
Coverage
The suite covers:
raw TX/RX with unaligned addresses, full Ethernet frames, descriptor claims, and length/status reporting;
peer mirror-to-remote writes and mirror-to-local reads with exact payload and guard-byte checks;
fragmentation, slot reuse, concurrent peers, reordered completions, and final-fragment metadata;
fragment-size rounding, configuration snapshots, valid boundaries, and invalid-setting rejection;
8192-byte unaligned raw TX across two local reads, including source errors in either part;
scalar RMEM validation, command kinds, BitEnable, error recording, and separate IRQ notification;
received READ/WRITE servicing, local windows, role disambiguation, and simultaneous initiator/responder traffic;
local/remote error encoding, full-width wire causes, and original AXI-cause precedence;
partial writes, late framing errors, terminal-status joins, and bilateral failure reporting;
sticky peer/raw errors, independent clear commands, simultaneous new failure, and clearing during active transfers;
IRQ gating, retained event metadata, admission/commit stalls, and one commit per admitted event;
command, CSR, IRQ, AXIS, and AXI output stability under backpressure and between clock edges.