PCIe · Module 20
DMA over PCIe — When the Endpoint Becomes the Requester
Everything so far treated the Endpoint as something the host talks to. DMA inverts that: the device issues its own Memory Reads and Writes into host memory — and the hardest part is knowing when it is actually finished.
Every chapter of this curriculum so far has had the same shape. The host initiates and the device responds — Configuration Requests read the device's registers, Memory Requests target its BARs, and the device answers.
Even Module 19's interrupts fit that frame. A notification is a small write the device sends because the host asked it to, at an address the host supplied (Chapter 19.4 §3).
DMA breaks the frame. The Endpoint becomes a Requester in its own right, issuing Memory Reads and Memory Writes at addresses it supplies, moving bulk data into and out of host memory without the CPU touching a byte of it.
What changes when the device stops answering and starts asking — and why is the hardest part of a DMA engine not moving the data, but knowing when it is done?
1. The Verified Sources
2. Programmed I/O and DMA
The contrast that motivates everything.
| Programmed I/O | DMA | |
|---|---|---|
| Who moves the bytes | the CPU, load and store at a time | the device, as a Requester |
| Who initiates PCIe requests | the host | the device |
| CPU cost | proportional to data | proportional to transfers, not bytes |
| Device role | Completer | Requester and Completer |
| Latency per access | a full round trip, per access | amortized across a large transfer |
3. Requester and Completer Are Different Roles
§1's two Command register bits make this concrete, and the pairing is more instructive than either bit alone.
Memory Space Enable → the Function DECODES requests aimed at it (Completer)
Bus Master Enable → the Function ISSUES requests aimed elsewhere (Requester)A device can be one, the other, or both. A simple device with only registers is a Completer and needs no Bus Master Enable at all. A DMA-capable device is both: the host still reads its registers through its BARs, and it independently issues traffic into host memory.
And the traffic is genuinely bidirectional in role, not just in direction. While the device is DMAing into host memory as a Requester, the host may be polling its status registers — with the device answering as a Completer. Two independent conversations on one Link, distinguished by who initiated.
Which is why a DMA engine cannot be understood as "the device's side of a host transaction." It is an originator, and everything hard about it — outstanding state, ownership, retirement — follows from that.
4. Direction: The Confusion Worth Preventing
5. Bus Master Enable
§1 is unambiguous: "When this field is clear, the function is not allowed to issue any memory or I/O requests."
So Bus Master Enable is the permission that makes DMA possible at all. Without it, the Function is a Completer only.
But permission is not sufficiency, and §13's RTL treats it as one condition among several. A DMA engine with BME set still needs: a valid transfer to perform, a valid address, buffer ownership resolved with software, transmit credits (Chapter 16.2), the Link in L0 (Chapter 18.6 §7), and — for reads — an available outstanding context (§10).
And §1 gives a consequence that links this module to the last one. Clearing Bus Master Enable also stops MSI and MSI-X, because they are memory writes. So a driver that clears BME to quiesce DMA has also silenced the device's interrupts — which is correct, intentional, and a genuinely useful diagnostic (Chapter 19.4 §16): "INTx works, MSI does not, and DMA stopped" is one cause, not three.
What this chapter does not do is invent a policy for BME clearing mid-transfer (§14).
6. Writes Are Posted
A device→host DMA write is an ordinary posted Memory Write (Chapter 10.3).
Which means: no Completion is returned; the request is complete from the Transaction Layer's perspective once transmitted; it consumes posted credits; and it is retained in the replay buffer until acknowledged at the Data Link Layer (Chapter 15.1).
7. Reads Are Non-Posted, and That Changes Everything
A host→device DMA read is a Memory Read Request (Chapter 12.1) — non-posted, so data returns in Completions with Data.
Which creates outstanding state, and that is the architectural difference between the two engines.
write engine: issue → done with it
read engine: issue → remember → match Completions → account bytes → retire8. PCIe Defines No Descriptor Format
Stated explicitly because the assumption is common and wrong.
PCIe defines transaction types, routing, ordering and flow control. It does not define how a device is told what to transfer. There is no standard descriptor layout, no standard ring format, no standard "DMA capability."
Every DMA engine invents its own, and the driver is written to match. That is why a NIC's descriptors, an NVMe controller's queues and an FPGA data mover's command format look nothing alike — they are device architecture, not protocol.
This chapter therefore treats a transfer as an abstract command — a direction, an address, a length (§13) — and Chapter 20.2 owns descriptor-driven DMA properly, including rings, ownership bits and the software/hardware handoff.
9. The Transfer Pipeline
The architecture this module will fill in.
command / descriptor what to transfer 20.2
↓
validate length, address, permission
↓
reserve transfer context one per active transfer
↓
choose a chunk bounded by device and protocol limits
↓
build a PCIe request MemWr (device→host) or MemRd (host→device)
↓
common TX ownership credits, arbitration, Link state
↓
track completions READS ONLY -- the hard half (§7)
↓
account bytes issued vs completed, separately (§10)
↓
retire the transfer at a declared boundary (§6)
↓
report → interrupt event → Module 19 (§12)One note on chunking. A transfer is split because protocol and implementation limits bound how much a single request may carry. This chapter uses a normalized local maximum and verifies the arithmetic (§15); the exact PCIe rules — MPS, MRRS and address-boundary constraints — belong to Chapter 20.3 and Chapter 20.5.
10. Issued Versus Completed
The distinction §7 established, as engine state.
| Register | Meaning | Advances on |
|---|---|---|
bytes_total | how much the command asks for | command acceptance |
bytes_issued | how much has been handed to the transmit path | request handshake (§13) |
bytes_completed | how much has actually moved | writes: the declared retirement boundary (§6) · reads: Completion data received (§7) |
For a write they may be close. For a read they are fundamentally different quantities, and §15's 99.5% is what happens when a design uses one where it needs the other.
Two invariants hold for both, and §14 asserts them: bytes_issued <= bytes_total and bytes_completed <= bytes_total. §15 measured the first one's violation: advancing bytes_issued on valid rather than the handshake exceeds the total in 79.5% of randomized cases.
11. Addresses Are Device-Visible, Not Necessarily Physical
A bounded but important qualification.
The address a DMA engine puts in a request is the address software gave it. On many systems that is translated or validated between the device and memory — the same architectural boundary Chapter 19.4 §7 described for interrupts, applied to bulk data.
So the durable statement is: "the driver supplies a device-visible DMA address", not "the DMA address is the CPU physical address." The second is sometimes true, platform-dependent, and a poor thing to design around.
And the device does not interpret it — the same rule as an MSI address (Chapter 19.4 §3). It transmits what it was given.
IOMMU internals are out of scope here, and this chapter names the boundary rather than teaching it.
12. DMA Completion Becomes an Interrupt
The cross-module bridge, and the architecture matters as much as the connection.
DMA transfer retires
↓
normalized completion EVENT (this chapter, §13)
↓
interrupt controller (Module 19)
↓
MSI / MSI-X (19.2 / 19.3)
↓
routing (19.4)The DMA engine must not know the MSI address, the MSI-X table layout, or any CPU destination. It knows that a transfer finished and which queue or vector identity it belongs to — and hands that upward.
Why the layering is worth enforcing: a DMA engine wired directly to an MSI-X table formatter cannot be reused with INTx, cannot be tested without the interrupt subsystem, and duplicates masking and pending logic that Chapter 19.3 already owns. §15's mutation 12 is that design.
And the ordering requirement from Chapter 19.2 §8 lands squarely here. The completion interrupt must not overtake the DMA data it announces — which is now concrete rather than abstract: the writes are this engine's writes, and the interrupt is the event this engine raised.
13. The DMA Architecture Map
14. RTL — Command Owner, Context and Chunker
// SYNTHESIZABLE. Normalized DMA types.
// DIRECTION SEMANTICS ARE THE CANONICAL PART (section 4): device-to-host
// is a Memory WRITE, host-to-device is a Memory READ. Everything else here
// -- field widths, the command shape -- is internal normalization.
package dma_pkg;
parameter int ADDR_W = 64;
parameter int LEN_W = 32;
typedef enum logic {
DMA_TO_HOST = 1'b0, // device -> host : Memory WRITE (posted)
DMA_FROM_HOST = 1'b1 // host -> device : Memory READ (non-posted)
} dma_dir_e;
typedef struct packed {
logic valid;
dma_dir_e dir;
logic [ADDR_W-1:0] addr; // DEVICE-VISIBLE address (section 11)
logic [LEN_W-1:0] len;
} dma_cmd_t;
typedef enum logic [1:0] {
DMA_OK = 2'd0,
DMA_ERR_LEN = 2'd1, // zero length -- local contract says invalid
DMA_ERR_ABORT= 2'd2
} dma_status_e;
endpackageimport dma_pkg::*;
// SYNTHESIZABLE. Own one DMA command, and keep issued/completed separate.
// SECTION 10: bytes_issued and bytes_completed are DIFFERENT QUANTITIES,
// and for reads they are fundamentally so (section 7).
module dma_context #(
parameter int MAX_CHUNK = 256 // normalized local limit (section 9)
) (
input logic clk,
input logic rst_n,
// ---- Command interface -------------------------------------------------
input dma_cmd_t cmd,
output logic cmd_ready,
// ---- Permission and availability (section 5) ---------------------------
input logic bus_master_enable,
input logic link_ok,
// ---- Request issue handshake, from sections 15/16 ----------------------
output logic [LEN_W-1:0] issue_bytes,
output logic issue_valid,
input logic issue_ready,
// ---- Completion accounting ---------------------------------------------
// For a write this is driven by the declared retirement boundary; for a
// read it is driven by Completion data actually received (section 7).
input logic complete_valid,
input logic [LEN_W-1:0] complete_bytes,
output logic active,
output dma_dir_e dir,
output logic [ADDR_W-1:0] cur_addr,
output logic [LEN_W-1:0] bytes_total,
output logic [LEN_W-1:0] bytes_issued,
output logic [LEN_W-1:0] bytes_completed,
output logic xfer_done,
output dma_status_e xfer_status
);
logic act_q;
dma_dir_e dir_q;
logic [ADDR_W-1:0] addr_q;
logic [LEN_W-1:0] tot_q, iss_q, cpl_q;
dma_status_e st_q;
assign active = act_q;
assign dir = dir_q;
assign cur_addr = addr_q;
assign bytes_total = tot_q;
assign bytes_issued = iss_q;
assign bytes_completed = cpl_q;
assign xfer_status = st_q;
assign cmd_ready = !act_q;
// ==================================================================
// THE CHUNK. Bounded by what remains and by the local maximum.
//
// Section 15 verified the arithmetic exhaustively over totals 0..199
// and maximums 1..16: chunks always sum to the total, never zero while
// bytes remain, and never exceed the maximum.
// ==================================================================
wire [LEN_W-1:0] remaining = tot_q - iss_q;
assign issue_bytes = (remaining > LEN_W'(MAX_CHUNK)) ? LEN_W'(MAX_CHUNK)
: remaining;
// EVERY CONDITION MUST HOLD. Bus Master Enable is necessary and not
// sufficient (section 5).
assign issue_valid = act_q && (remaining != '0)
&& bus_master_enable && link_ok;
// ==================================================================
// DONE IS BASED ON COMPLETED BYTES, NEVER ON ISSUED BYTES.
//
// Section 15 measured the alternative: `done = all requests issued`
// declares a read transfer complete early in 99.5% of cases, and
// software then reads a buffer that is mostly stale (section 7).
// ==================================================================
assign xfer_done = act_q && (cpl_q >= tot_q) && (tot_q != '0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
act_q <= 1'b0; dir_q <= DMA_TO_HOST; addr_q <= '0;
tot_q <= '0; iss_q <= '0; cpl_q <= '0; st_q <= DMA_OK;
end else begin
if (!act_q && cmd.valid) begin
if (cmd.len == '0) begin
// ZERO LENGTH IS EXPLICITLY INVALID under this local contract,
// and reported rather than silently accepted -- an accepted
// zero-length command with `done` gated on tot_q != 0 would
// never retire (section 17, mutation 4).
st_q <= DMA_ERR_LEN;
end else begin
// CAPTURED ATOMICALLY, in one assignment. Nothing re-reads the
// command interface afterwards -- Chapter 19.2 section 13's
// rule, and software may rewrite its registers immediately.
act_q <= 1'b1;
dir_q <= cmd.dir;
addr_q <= cmd.addr;
tot_q <= cmd.len;
iss_q <= '0;
cpl_q <= '0;
st_q <= DMA_OK;
end
end else if (act_q) begin
// ============================================================
// bytes_issued ADVANCES ON THE HANDSHAKE, NEVER ON valid.
//
// Section 15: advancing on `valid` exceeded the total in 79.5%
// of randomized cases -- a stalled request counted repeatedly,
// so the engine believes it sent data it never sent.
// ============================================================
if (issue_valid && issue_ready) begin
iss_q <= iss_q + issue_bytes;
addr_q <= addr_q + ADDR_W'(issue_bytes);
end
if (complete_valid) cpl_q <= cpl_q + complete_bytes;
if (xfer_done) act_q <= 1'b0;
end
end
end
endmoduleClassification: synthesizable.
Three decisions, all measured (§15). bytes_issued on the handshake — advancing on valid overshot the total in 79.5% of cases. xfer_done on completed bytes — using issued bytes retires early in 99.5% of read cases. And the chunker's arithmetic — 0 violations across 3,200 (total, maximum) combinations.
The command is captured atomically, because software may rewrite its registers the cycle after acceptance.
And issue_valid requires Bus Master Enable and the Link (§5) — permission is necessary, not sufficient.
Failure — six. Advancing on valid. done from issued bytes. Re-reading cmd after acceptance. A zero-length command accepted and never retiring. Chunking without bounding by remaining, overshooting on the last chunk. And omitting link_ok, offering requests the Link cannot carry.
15. RTL — Write Engine and Bounded Read Tracker
import dma_pkg::*;
// SYNTHESIZABLE. Device -> host: build posted Memory Writes (section 6).
// THE WRITE PATH IS THE SIMPLE ONE because a posted write creates no
// outstanding state -- issue and forget. The payload buffering contract is
// explicit: this block asserts valid only when the payload for the chunk
// is available, so a stall never strands a half-built request.
module dma_write_engine (
input logic clk,
input logic rst_n,
input logic ctx_active,
input dma_dir_e ctx_dir,
input logic [ADDR_W-1:0] ctx_addr,
input logic [LEN_W-1:0] issue_bytes,
input logic issue_valid,
output logic issue_ready,
input logic payload_available,
// To the common PCIe transmit path
output logic tx_valid,
output logic [ADDR_W-1:0] tx_addr,
output logic [LEN_W-1:0] tx_len,
input logic tx_ready,
// Section 6: a posted write has no Completion. The retirement boundary
// used here is acceptance by the transmit path -- DECLARED, not implied.
output logic wr_retire_valid,
output logic [LEN_W-1:0] wr_retire_bytes
);
wire is_write = (ctx_dir == DMA_TO_HOST);
assign tx_valid = ctx_active && is_write && issue_valid && payload_available;
assign tx_addr = ctx_addr;
assign tx_len = issue_bytes;
// The context advances only when the transmit path accepts.
assign issue_ready = is_write && tx_ready && payload_available;
// ==================================================================
// THE DECLARED RETIREMENT BOUNDARY for writes (section 6).
//
// This design retires on acceptance by the transmit path. A design
// that retires later -- on Data Link acknowledgement, say -- is also
// valid; what is NOT valid is retiring on `tx_valid`, which reports
// transfers the path never took (section 17, mutation 1).
// ==================================================================
assign wr_retire_valid = tx_valid && tx_ready;
assign wr_retire_bytes = issue_bytes;
endmoduleimport dma_pkg::*;
// SYNTHESIZABLE. Host -> device: one outstanding Memory Read (section 7).
//
// DELIBERATELY BOUNDED TO A SINGLE OUTSTANDING REQUEST, and this is a
// teaching limitation, stated plainly: a production engine maintains a
// pool of Tags and many outstanding reads, with Completions returning out
// of order and possibly split. TAG POOLS AND COMPLETION REORDERING ARE
// NOT OWNED BY THIS CHAPTER -- see Chapters 20.3 and 13.3.
//
// What this model DOES show correctly is the property that matters most:
// a read transfer retires on RECEIVED bytes, never on issued ones.
module dma_read_tracker (
input logic clk,
input logic rst_n,
input logic ctx_active,
input dma_dir_e ctx_dir,
input logic [ADDR_W-1:0] ctx_addr,
input logic [LEN_W-1:0] issue_bytes,
input logic issue_valid,
output logic issue_ready,
output logic tx_valid,
output logic [ADDR_W-1:0] tx_addr,
output logic [LEN_W-1:0] tx_len,
input logic tx_ready,
// Completion data returning (Chapter 12.3). Simplified: bytes only.
input logic cpl_valid,
input logic [LEN_W-1:0] cpl_bytes,
output logic rd_retire_valid,
output logic [LEN_W-1:0] rd_retire_bytes,
output logic err_unexpected_cpl,
output logic err_overrun
);
logic outstanding_q;
logic [LEN_W-1:0] expected_q, received_q;
logic unexp_q, over_q;
wire is_read = (ctx_dir == DMA_FROM_HOST);
assign err_unexpected_cpl = unexp_q;
assign err_overrun = over_q;
// ONE AT A TIME. A second request cannot be issued while one is
// outstanding -- which is the whole bound of this model.
assign tx_valid = ctx_active && is_read && issue_valid && !outstanding_q;
assign tx_addr = ctx_addr;
assign tx_len = issue_bytes;
assign issue_ready = is_read && tx_ready && !outstanding_q;
// RETIREMENT ON RECEIVED BYTES (section 7).
assign rd_retire_valid = cpl_valid && outstanding_q;
assign rd_retire_bytes = cpl_bytes;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
outstanding_q <= 1'b0; expected_q <= '0; received_q <= '0;
unexp_q <= 1'b0; over_q <= 1'b0;
end else begin
if (tx_valid && tx_ready) begin
outstanding_q <= 1'b1;
expected_q <= issue_bytes;
received_q <= '0;
end else if (cpl_valid) begin
if (!outstanding_q) begin
// ==========================================================
// A COMPLETION WITH NO OUTSTANDING REQUEST IS REPORTED, never
// accounted. Consuming it would credit the transfer with data
// it did not request (section 17, mutation 9).
// ==========================================================
unexp_q <= 1'b1;
end else if ((received_q + cpl_bytes) > expected_q) begin
// More data than was asked for. Reported, and NOT accumulated.
over_q <= 1'b1;
end else begin
received_q <= received_q + cpl_bytes;
// Retire this request only when its expected bytes have all
// arrived. The CONTEXT then decides whether the whole transfer
// is done -- which is section 14's xfer_done, on completed
// bytes.
if ((received_q + cpl_bytes) == expected_q) outstanding_q <= 1'b0;
end
end
end
end
endmoduleClassification: both synthesizable — and the read tracker is explicitly bounded (single outstanding), which is stated in the header rather than implied.
The write engine's retirement boundary is declared, not assumed (§6): acceptance by the transmit path. A different boundary is a legitimate design choice; retiring on tx_valid is not.
And the read tracker reports two errors rather than absorbing them. A Completion with no outstanding request, and more data than requested — both indicate something is wrong upstream, and accounting them would credit the transfer with data it never asked for.
Failure — five. Two outstanding reads in a single-outstanding model. Retiring on issuance (§7's 99.5%). Accumulating an unexpected Completion. Allowing received to exceed expected. And a write engine asserting tx_valid without payload available, which strands a half-built request on a stall.
16. RTL — Completion Event Owner
import dma_pkg::*;
// SYNTHESIZABLE. Hand a retired transfer to the interrupt subsystem.
// SECTION 12: this block knows NOTHING about MSI addresses, MSI-X tables
// or CPU destinations. It produces a normalized event with a queue/vector
// IDENTITY, and Module 19 owns everything after that.
module dma_completion_event #(parameter int VEC_W = 11) (
input logic clk,
input logic rst_n,
input logic xfer_done,
input dma_status_e xfer_status,
input logic [LEN_W-1:0] bytes_completed,
input logic [VEC_W-1:0] queue_id,
output logic done_valid,
input logic done_ready,
output dma_status_e done_status,
output logic [LEN_W-1:0] done_bytes,
output logic [VEC_W-1:0] done_queue,
output logic event_overflow
);
logic v_q, ovf_q;
dma_status_e st_q;
logic [LEN_W-1:0] by_q;
logic [VEC_W-1:0] q_q;
assign done_valid = v_q;
assign done_status = st_q;
assign done_bytes = by_q;
assign done_queue = q_q;
assign event_overflow = ovf_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
v_q <= 1'b0; ovf_q <= 1'b0; st_q <= DMA_OK; by_q <= '0; q_q <= '0;
end else begin
// ==============================================================
// THE EVENT IS HELD UNTIL THE CONSUMER TAKES IT.
//
// A one-cycle pulse would be lost whenever the interrupt
// controller is busy -- coalescing a previous batch, waiting on
// credits, or blocked by the Link (Chapter 19.5 section 8). And a
// lost DMA completion event means software is never told the
// transfer finished: the ring stalls with the data already in
// memory (section 17, mutation 11).
// ==============================================================
if (xfer_done && !v_q) begin
v_q <= 1'b1;
st_q <= xfer_status; // captured whole
by_q <= bytes_completed;
q_q <= queue_id;
end else if (v_q && done_ready) begin
v_q <= 1'b0;
end
// A second completion while one is unconsumed is REPORTED. With a
// single context it cannot happen; with a command queue it can, and
// silence would lose a transfer's notification.
if (xfer_done && v_q) ovf_q <= 1'b1;
end
end
endmoduleClassification: synthesizable.
The event is held, not pulsed, and the reason is Chapter 19.5 §8: the interrupt subsystem is routinely busy. A pulsed completion event is lost exactly when the device is busiest, and the symptom is a stalled ring with the data already in host memory.
And it carries a queue identity, not a vector or an address (§12). Chapter 19.5 §13's mapper turns a queue into a vector; this block does not know how.
17. Assertions
// SVA over the DMA blocks. LOCAL contract only. Nothing asserts that
// Completions arrive, that the Link becomes available, that credits are
// returned, or that software ever issues a command.
// ---- ENVIRONMENT ------------------------------------------------------
assume property (@(posedge clk) disable iff (!rst_n)
(cmd.valid && !cmd_ready) |=> (cmd.valid && $stable(cmd)));
assume property (@(posedge clk) disable iff (!rst_n)
cpl_valid |-> (cpl_bytes != '0));
// ---- COMMAND OWNERSHIP ------------------------------------------------
// P1: the command is captured ATOMICALLY and its fields are stable while
// owned. Software may rewrite its registers the next cycle.
property p_cmd_stable;
@(posedge clk) disable iff (!rst_n)
(active && !xfer_done) |=> ($stable(bytes_total) && $stable(dir));
endproperty
a_cmd : assert property (p_cmd_stable);
// P2: a zero-length command is refused explicitly, not accepted.
property p_zero_len;
@(posedge clk) disable iff (!rst_n)
(cmd.valid && cmd_ready && (cmd.len == '0)) |=> (!active && (xfer_status == DMA_ERR_LEN));
endproperty
a_zero : assert property (p_zero_len);
// ---- BYTE ACCOUNTING -- THE CENTRAL PROPERTIES ------------------------
// P3: bytes_issued NEVER EXCEEDS the total. Section 15 measured the
// violation: advancing on `valid` overshoots in 79.5% of cases.
property p_issued_bounded;
@(posedge clk) disable iff (!rst_n)
active |-> (bytes_issued <= bytes_total);
endproperty
a_iss : assert property (p_issued_bounded);
// P3b: and it advances ONLY on a request handshake.
property p_issued_on_fire;
@(posedge clk) disable iff (!rst_n)
(bytes_issued > $past(bytes_issued))
|-> ($past(issue_valid) && $past(issue_ready));
endproperty
a_issfire : assert property (p_issued_on_fire);
// P4: THE TRANSFER RETIRES ON COMPLETED BYTES, NEVER ON ISSUED BYTES.
// The most important property in this chapter -- section 15 measured the
// alternative at 99.5% early completion for reads.
property p_done_on_completed;
@(posedge clk) disable iff (!rst_n)
xfer_done |-> (bytes_completed >= bytes_total);
endproperty
a_done : assert property (p_done_on_completed);
// P4b: stated the other way, so a design cannot satisfy P4 by making
// bytes_completed track bytes_issued.
property p_completed_bounded;
@(posedge clk) disable iff (!rst_n)
active |-> (bytes_completed <= bytes_total);
endproperty
a_cplb : assert property (p_completed_bounded);
// P5: the chunk is bounded and never zero while bytes remain.
property p_chunk_valid;
@(posedge clk) disable iff (!rst_n)
issue_valid |-> ((issue_bytes != '0)
&& (issue_bytes <= LEN_W'(MAX_CHUNK))
&& (issue_bytes <= (bytes_total - bytes_issued)));
endproperty
a_chunk : assert property (p_chunk_valid);
// ---- PERMISSION AND AVAILABILITY --------------------------------------
// P6: NO REQUEST IS ISSUED WITHOUT BUS MASTER ENABLE. Section 1: "when
// this field is clear, the function is not allowed to issue any memory or
// I/O requests."
property p_bme_required;
@(posedge clk) disable iff (!rst_n)
(tx_valid) |-> bus_master_enable;
endproperty
a_bme : assert property (p_bme_required);
// P7: nor while the Link cannot carry normal traffic.
property p_link_required;
@(posedge clk) disable iff (!rst_n) tx_valid |-> link_ok;
endproperty
a_link : assert property (p_link_required);
// P8: LINK-DOWN OR BME CLEAR DOES NOT DISCARD AN OWNED COMMAND. The
// declared contract (section 18): new REQUESTS stop; owned work is
// retained.
property p_command_retained;
@(posedge clk) disable iff (!rst_n)
(active && (!bus_master_enable || !link_ok) && !xfer_done) |=> active;
endproperty
a_retain : assert property (p_command_retained);
// ---- DIRECTION --------------------------------------------------------
// P9: device-to-host issues WRITES; host-to-device issues READS (section 4).
property p_direction_write;
@(posedge clk) disable iff (!rst_n)
(tx_valid && (dir == DMA_TO_HOST)) |-> dut_tx.is_memory_write;
endproperty
a_dirw : assert property (p_direction_write);
property p_direction_read;
@(posedge clk) disable iff (!rst_n)
(tx_valid && (dir == DMA_FROM_HOST)) |-> dut_tx.is_memory_read;
endproperty
a_dirr : assert property (p_direction_read);
// ---- READ TRACKING ----------------------------------------------------
// P10: NEVER TWO OUTSTANDING READS in this bounded model.
property p_single_outstanding;
@(posedge clk) disable iff (!rst_n)
(outstanding_q && !cpl_valid) |-> !(tx_valid && tx_ready);
endproperty
a_single : assert property (p_single_outstanding);
// P11: A COMPLETION WITH NO OUTSTANDING READ IS REPORTED, NOT ACCOUNTED.
property p_unexpected_cpl;
@(posedge clk) disable iff (!rst_n)
(cpl_valid && !outstanding_q) |=> (err_unexpected_cpl && $stable(received_q));
endproperty
a_unexp : assert property (p_unexpected_cpl);
// P12: received bytes never exceed expected bytes.
property p_no_overrun;
@(posedge clk) disable iff (!rst_n) received_q <= expected_q;
endproperty
a_over : assert property (p_no_overrun);
// ---- COMPLETION EVENT -------------------------------------------------
// P13: the completion event is HELD until the consumer takes it.
property p_event_held;
@(posedge clk) disable iff (!rst_n)
(done_valid && !done_ready) |=> (done_valid && $stable(done_bytes)
&& $stable(done_queue));
endproperty
a_evheld : assert property (p_event_held);
// P14: and it reports the bytes that actually completed.
property p_event_truthful;
@(posedge clk) disable iff (!rst_n)
$rose(done_valid) |-> (done_bytes == $past(bytes_completed));
endproperty
a_evtrue : assert property (p_event_truthful);
// P15: THE DMA ENGINE DOES NOT FORMAT INTERRUPTS. Structural: its only
// interrupt-facing output is a normalized event (section 12).
property p_no_msix_knowledge;
@(posedge clk) disable iff (!rst_n)
done_valid |-> (done_queue < VEC_W'(1 << VEC_W));
endproperty
a_layer : assert property (p_no_msix_knowledge);
// P16: reset.
property p_reset;
@(posedge clk)
!rst_n |=> (!active && !tx_valid && !done_valid && (bytes_issued == '0));
endproperty
a_reset : assert property (p_reset);P3b and P4 are the pair this chapter exists to teach. P3b forbids counting offers as issues; P4 forbids counting issues as completions. §18 measured both — 79.5% and 99.5% respectively.
P4b is stated separately on purpose. A design could satisfy P4 by making bytes_completed merely track bytes_issued; P4b bounds it independently, and the DV scoreboard computes it from Completion data rather than from the DUT.
And P8 is the ownership property (§18): losing permission or losing the Link stops new requests, and does not discard owned work.
No liveness. "Completions eventually arrive", "the Link becomes available" and "credits are returned" are all environment properties this chapter does not assume.
18. Same-Cycle Contracts
Declared, not left to if ordering.
| Case | Declared resolution |
|---|---|
| command accepted + BME clears | the command is owned; new requests stop, the command is retained (P8) |
| request transfer + Link down | the transfer completes (it was accepted); the next request waits (P7) |
| final chunk issued + Completion returns | both accounted; xfer_done evaluates on the updated bytes_completed |
xfer_done + a new command offered | the new command waits — cmd_ready is low until active falls (P1) |
| Completion + reset | reset wins (P16) |
completion event transfer + a new xfer_done | with one context, impossible; with a queue, the second is reported as overflow (§16) |
issue_valid + issue_ready + BME clearing same cycle | the handshake completes; BME gates the next assertion (P6 holds because tx_valid was already qualified) |
19. Verification, Fault Injection, and Model Verification
Executed before publication.
The chunker — exhaustive
Every total from 0 to 199 against every maximum from 1 to 16 — 3,200 cases, checking three invariants: chunks sum to the total, no chunk is zero while bytes remain, no chunk exceeds the maximum.
0 violations. Worked cases: total=9, max=8 → [8,1]; total=65, max=16 → [16,16,16,16,1]; total=0 → [].
bytes_issued — advance on handshake versus on valid
60,000 random (total, max_chunk, ready pattern) cases:
| Implementation | bytes_issued > bytes_total |
|---|---|
| §14 as written (handshake) | 0 |
advance on valid | 47,686 — 79.5% |
Witness: total = 19, max_chunk = 1 → bytes_issued = 27. The descriptor "completes" having sent 19 bytes and counted 27 — a stalled request counted repeatedly.
Read retirement — issued versus completed
60,000 random (transfer size, request size, completion progress) cases:
| Retirement rule | Declares complete early |
|---|---|
done = all bytes received | 0 |
done = all requests issued | 59,691 — 99.5% |
99.5% is near-total because it is nearly a tautology (§7): at the instant the last request is issued, its data has definitionally not returned.
Directed tests
- Small single-chunk write; multi-chunk write; final short chunk (
total = 65,max = 16). MAX_CHUNK= 1, 4, 256 andtotal= 1 — the boundary cases §19 enumerated.- Transmit path stalls for 1, 2, 50 cycles — verify
bytes_issueddoes not advance (P3b). Required. - A DMA read with Completions delayed — verify
xfer_donestays low until bytes arrive (P4). Required. - Completion with no outstanding read — verify reported, not accounted (P11). Required.
- Completion larger than expected — verify overrun reported (P12).
- BME cleared before a command — verify no request is issued (P6).
- BME cleared while a transfer is active — verify the command is retained and requests stop (P8). Required, and §18's declared contract.
- Link unavailable mid-transfer — same verdict (P7, P8).
- Zero-length command — verify explicit refusal (P2). Required.
- Completion event with the consumer stalled — verify it is held (P13). Required.
- Back-to-back commands; reset during each phase.
The scoreboard maintains an independent byte-accounting model — computing expected issued and completed bytes from the raw handshakes and Completion data — and never reads bytes_issued, bytes_completed, active or xfer_done.
Mutations
| # | Mutation | Caught by | System symptom |
|---|---|---|---|
| 1 | bytes_issued advances on valid | P3, P3b | descriptor completes with data never sent — 79.5% (measured) |
| 2 | read done uses issued bytes | P4 | software reads a mostly stale buffer — 99.5% (measured) |
| 3 | command fields re-read after acceptance | P1 | transfer uses an address software has since rewritten |
| 4 | zero-length command accepted | P2 | engine never retires; queue stalls forever |
| 5 | last chunk overshoots the total | P5 | writes past the end of the host buffer |
| 6 | TX stall duplicates request accounting | P3b | same as 1, from the stall path |
| 7 | Link down discards the command | P8 | transfer silently lost across a Recovery event |
| 8 | BME clear destroys an active command | P8 | driver quiescing DMA loses in-flight work |
| 9 | Completion accepted with no outstanding read | P11 | transfer credited with data it never requested |
| 10 | received bytes allowed to exceed expected | P12 | overrun into the device's own buffer |
| 11 | done event pulsed, not held | P13 | data in memory, no notification — ring stalls |
| 12 | DMA engine formats the MSI-X address itself | P15 + review | unreusable layering; duplicated mask/pending logic (§12) |
| 13 | device→host uses Memory Read | P9 | no data moves; Completions return nothing useful (§4) |
| 14 | host→device uses Memory Write | P9 | host memory overwritten with device buffer contents (§4) |
| 15 | posted DMA write waits for a Completion | P4b + timeout | engine hangs; posted writes have no Completion (§6) |
| 16 | second read issued while one is outstanding | P10 | model's bound violated; Completions unattributable |
| 17 | interrupt raised before the transfer completes | P14 | software reads an incomplete buffer (19.2 §8) |
| 18 | device-visible address treated as CPU physical | review + §11 | works on one platform, corrupts on another |
20. Debugging
Symptom → issued, completed, or notified? → signal → distinguishing experiment.
The first three registers to read are always bytes_total, bytes_issued and bytes_completed (§10). They separate the three failure classes immediately.
The descriptor says complete but host memory is missing the last data
Compare bytes_issued against bytes_total, and both against what the analyzer saw.
If bytes_issued == bytes_total but the transmit path accepted less, it is mutation 1 — advancing on valid (79.5%). Distinguished by: summing accepted request lengths on an analyzer and comparing.
If the counts agree and the last chunk is short, check the chunker (mutation 5): a final chunk that overshoots writes past the end of the host buffer, which is worse than missing data.
And if this is a read, go straight to the next scenario — it is almost certainly mutation 2.
DMA read requests appear on the analyzer but the engine never completes
Read bytes_completed. If it is stuck below bytes_total, Completions are not being accounted — and there are three candidates.
err_unexpected_cpl set (P11): Completions are arriving that the tracker does not associate with an outstanding request — a Tag or matching problem, which in a production engine with a Tag pool is where §15's bounded model stops being adequate (Chapter 20.3).
err_overrun set (P12): more data returned than requested.
Neither set, and Completions simply are not arriving: the problem is upstream — completion timeout, an error at the completer, or a routing issue. The device is behaving correctly and waiting.
DMA works, then stops after driver reconfiguration
Read Bus Master Enable first (§5).
§1: "when this field is clear, the function is not allowed to issue any memory or I/O requests." A driver that cleared BME has stopped DMA by design.
And the confirming observation is elegant (Chapter 19.4 §16): MSI and MSI-X stop too, because they are memory writes — while INTx keeps working. So "DMA stopped, MSI stopped, INTx still arrives" is one cause, not three coincident failures.
Then check whether owned work survived (P8). A design that discards an active command on BME clear (mutation 8) loses in-flight work that a driver quiescing the device expected to be able to resume or account for.
Data reaches host memory but no interrupt occurs
Do not start at the DMA data path — it demonstrably worked (§12).
Walk the chain:
xfer_done did the transfer retire? -> §14, P4
done_valid was a completion event raised? -> §16
done_ready did the interrupt subsystem take it? -> Module 19
...and was the event HELD? -> P13, mutation 11
irq sent was a notification transmitted? -> 19.2/19.3
routing did it reach a destination? -> 19.4done_valid pulsed rather than held (mutation 11) is the classic failure, and its signature is exact: the data is correct and complete in host memory, and software was never told. The ring stalls with everything already done.
Data corruption immediately after a DMA completion interrupt
Suspect ordering (Chapter 19.2 §8), now concrete: the interrupt is this engine's event and the data is this engine's writes.
If the completion event is raised before the final writes have been accepted by the transmit path, the interrupt can overtake them — and §16's done_bytes would report bytes that are still in flight. P14 asserts the event reports actually-completed bytes, and the interlock belongs to Chapter 19.2 §13's data_pending.
21. Common Misconceptions
- "DMA means the CPU does nothing." It stops touching the payload; setup, ownership, completion and errors remain (§2).
- "PCIe defines a DMA descriptor format." It defines none (§8).
- "Device→host DMA uses Memory Read." It uses Memory Write (§4).
- "Host→device DMA uses Memory Write." It uses Memory Read, and data returns in Completions (§4).
- "Every DMA transaction gets a Completion." Posted writes get none (§6).
- "Issued bytes equal completed bytes." For reads, almost never — 99.5% early completion (§19).
- "Bus Master Enable means the transfer runs." It is necessary and not sufficient (§5).
- "A DMA address is the CPU physical address." It is a device-visible address (§11).
- "A Link replay means the DMA happened twice." A replayed write is the same write (§6).
- "The DMA engine should know the MSI-X destination." It produces a normalized event (§12).
- "An interrupt is the completion state." The interrupt reports it (§12).
- "Link down means the descriptor can be discarded." This chapter's contract retains it, and says so (§18).
- "A posted write should be waited on." There is nothing to wait for (§6, mutation 15).
- "Bus Master Enable only affects DMA." It also stops MSI and MSI-X (§1, §5).
22. Understanding Check
23. What Module 20 Still Owns
This chapter established the architecture. The depth belongs to what follows.
| Chapter | What it uniquely owns |
|---|---|
| 20.2 DMA Concepts | descriptor-driven DMA — rings, ownership bits, the software/hardware handoff (§8) |
| 20.3 Host Memory Access | reading and writing host RAM via TLPs in detail — Tag pools, many outstanding reads, Completion matching and reassembly (§15's bounded model replaced) |
| 20.4 Scatter-Gather | descriptor lists, fragmented buffers, chained transfers |
| 20.5 High-Speed Data Movement | maximising throughput — MPS, MRRS, boundary rules, outstanding-read scaling, the arithmetic §9 deferred |
| 20.6 FPGA Examples | a real engine end to end |
What this chapter deliberately kept: the Requester/Completer distinction (§3), direction semantics (§4), Bus Master Enable's role (§5), the posted/non-posted split (§§6–7), issued versus completed (§7, §10), and the completion-to-interrupt bridge (§12).
What it deliberately bounded: a single outstanding read (§15) rather than a fake Tag pool, a normalized chunk limit (§9) rather than invented MPS rules, one paragraph on address translation (§11), and no descriptor format at all (§8).
24. What's Next
DMA inverts the relationship this curriculum has assumed throughout: the Endpoint stops answering and starts asking.
Direction is the first thing to get right (§4) — device→host is a write, host→device is a read whose data returns in Completions — and getting it backwards overwrites host memory.
Permission is explicit (§5), and Bus Master Enable turns out to gate the interrupts of Module 19 as well, because those are memory writes too.
And the hardest part is knowing when you are done (§7). Issued is not completed; a read engine that conflates them declares success in 99.5% of cases while the buffer is still filling — with every request legal, every Completion correct, and no error anywhere.
Chapter 20.2 — DMA Concepts takes the next layer: how software tells the device what to move. Descriptors, rings, and the ownership protocol that lets two agents share a queue without a lock — which is Chapter 19.3 §7's problem again, at a different scale and with far more at stake.
The idea to carry forward: "I have asked for everything" and "I have received everything" are different facts.