Skip to content

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/ODMA
Who moves the bytesthe CPU, load and store at a timethe device, as a Requester
Who initiates PCIe requeststhe hostthe device
CPU costproportional to dataproportional to transfers, not bytes
Device roleCompleterRequester and Completer
Latency per accessa full round trip, per accessamortized 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
write engine:   issue  →  done with it
read engine:    issue  →  remember  →  match Completions  →  account bytes  →  retire

8. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

RegisterMeaningAdvances on
bytes_totalhow much the command asks forcommand acceptance
bytes_issuedhow much has been handed to the transmit pathrequest handshake (§13)
bytes_completedhow much has actually movedwrites: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

DMA architecture. Software provides a command or descriptor to DMA control, which validates and chunks it. The write engine issues Memory Write TLPs for device to host transfers. The read engine issues Memory Read Requests for host to device transfers and a completion tracker accounts the returning Completion data. Both engines share the common PCIe transmit and receive path to host memory. When a transfer retires, a completion event is passed to the interrupt controller, which generates MSI or MSI-X.software / driverDMA controlwrite engineread enginecompletion trackercommon PCIe TX / RXhost memorycompletion eventinterrupt controllerMemWrMemRd12
Figure 1 — the architecture this module builds. Software supplies commands or descriptors; the DMA control block validates and chunks them; a write engine produces Memory Writes for device-to-host transfers while a read engine produces Memory Read Requests and tracks the Completions that return the data. Both share the common PCIe transmit path, where credits, arbitration and Link state apply. Transfer retirement produces a normalized completion event that is handed to the interrupt subsystem — the DMA engine never formats an MSI-X entry itself.

14. RTL — Command Owner, Context and Chunker

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import 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
 
endmodule

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;
 
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import 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
 
endmodule

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.

CaseDeclared resolution
command accepted + BME clearsthe command is owned; new requests stop, the command is retained (P8)
request transfer + Link downthe transfer completes (it was accepted); the next request waits (P7)
final chunk issued + Completion returnsboth accounted; xfer_done evaluates on the updated bytes_completed
xfer_done + a new command offeredthe new command waits — cmd_ready is low until active falls (P1)
Completion + resetreset wins (P16)
completion event transfer + a new xfer_donewith one context, impossible; with a queue, the second is reported as overflow (§16)
issue_valid + issue_ready + BME clearing same cyclethe 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:

Implementationbytes_issued > bytes_total
§14 as written (handshake)0
advance on valid47,686 — 79.5%

Witness: total = 19, max_chunk = 1bytes_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 ruleDeclares complete early
done = all bytes received0
done = all requests issued59,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 and total = 1 — the boundary cases §19 enumerated.
  • Transmit path stalls for 1, 2, 50 cycles — verify bytes_issued does not advance (P3b). Required.
  • A DMA read with Completions delayed — verify xfer_done stays 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

#MutationCaught bySystem symptom
1bytes_issued advances on validP3, P3bdescriptor completes with data never sent — 79.5% (measured)
2read done uses issued bytesP4software reads a mostly stale buffer — 99.5% (measured)
3command fields re-read after acceptanceP1transfer uses an address software has since rewritten
4zero-length command acceptedP2engine never retires; queue stalls forever
5last chunk overshoots the totalP5writes past the end of the host buffer
6TX stall duplicates request accountingP3bsame as 1, from the stall path
7Link down discards the commandP8transfer silently lost across a Recovery event
8BME clear destroys an active commandP8driver quiescing DMA loses in-flight work
9Completion accepted with no outstanding readP11transfer credited with data it never requested
10received bytes allowed to exceed expectedP12overrun into the device's own buffer
11done event pulsed, not heldP13data in memory, no notification — ring stalls
12DMA engine formats the MSI-X address itselfP15 + reviewunreusable layering; duplicated mask/pending logic (§12)
13device→host uses Memory ReadP9no data moves; Completions return nothing useful (§4)
14host→device uses Memory WriteP9host memory overwritten with device buffer contents (§4)
15posted DMA write waits for a CompletionP4b + timeoutengine hangs; posted writes have no Completion (§6)
16second read issued while one is outstandingP10model's bound violated; Completions unattributable
17interrupt raised before the transfer completesP14software reads an incomplete buffer (19.2 §8)
18device-visible address treated as CPU physicalreview + §11works 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.4

done_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.

ChapterWhat it uniquely owns
20.2 DMA Conceptsdescriptor-driven DMA — rings, ownership bits, the software/hardware handoff (§8)
20.3 Host Memory Accessreading 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-Gatherdescriptor lists, fragmented buffers, chained transfers
20.5 High-Speed Data Movementmaximising throughput — MPS, MRRS, boundary rules, outstanding-read scaling, the arithmetic §9 deferred
20.6 FPGA Examplesa 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.