Skip to content
VLSI Mentor

Ethernet · Module 18

The Receive DMA Path

A received frame crosses seven boundaries between the MAC's FIFO and a host buffer, and the last two contribute 700 ns and 100 microseconds of jitter to a 24.2 ns clock.

Chapter 18.2 §7 gated one ownership write on one field write. This chapter finds out how many writes there really are.

A 1518-octet frame on a 512-bit bus is 24 beats. At a 16-beat burst limit that is two bursts, and if the buffer is not page-aligned it may be threeChapter 18.1 §5's 4 KiB rule. A 9000-octet jumbo frame into 2 KiB buffers is five descriptors and around eighteen bursts.

And every one of those must be visible before the ownership write is.

What Chapter 18.2 orderedWhat this chapter must order
one field writeup to 18 data bursts, plus the field write
one descriptora chain of five
one completiona chain whose first descriptor must not be released until the last one's data lands

Row three is the chapter's hardest structural requirement and it inverts the natural order: the descriptor a driver reads first is the one that must be published last.

Then there is a debt to settle. Chapter 16.1 §8 traced a software timestamp's path and listed two stages as variable without pricing either: "frame is written to the receive ring — DMA arbitration" and "interrupt is asserted — coalescing." Both were listed as this module's business. They are now computable, and the numbers are severe.

TermMagnitudeAgainst Chapter 16.5's 24.2 ns
DMA arbitration, 8 masters700 ns28.9×
interrupt coalescing, 100 µs timer100 000 ns4 132×

The second row is the one that ends the argument. A hardware clock synchronised to 24.2 ns, read through an interrupt coalesced at 100 µs, is a 24.2 ns clock delivered with four thousand times that much jitter — which is Chapter 16.1's entire thesis, finally with a number attached.


1. Scope, and the Seven Boundaries

This chapter follows one received frame from the last octet leaving the PHY to the moment software is told it exists, and counts what happens in between.

#BoundaryClock domainVariable?
1PHY to MAC receive FIFOrecoveredno
2FIFO to host domainthe crossingslightly
3descriptor fetchedhostyes — memory latency
4data written to the bufferhostyes — DMA arbitration
5status writtenhostyes
6ownership transferredhostyes — after 4 and 5
7interrupt assertedhostyes — coalescing

Rows 4 and 7 are Chapter 16.1 §8's two unpriced terms and Sections 13 and 15 price them.

Row 6 is Chapter 18.2's handoff, enlarged: it must now follow rows 4 and 5, and row 4 is many writes rather than one.

What this chapter builds:

SectionBuilds
3the FIFO reader — where backpressure stops being possible
4buffer exhaustion, and what happens at each stage
6, 7scatter-gather across buffers and pages
8, 10the write engine and the enlarged ordering barrier
11the jumbo chain, published in reverse
13, 15the two jitter terms, priced
17the transaction count, completing Chapter 18.1 §12

What it does not build: the transmit direction is Chapter 18.4, AXI burst shaping in detail is Chapter 18.5, adaptive coalescing is Chapter 18.6, and multi-queue is Chapter 18.7. Those forward references are bold and unlinked because those chapters are not yet published.


2. The Path, and Where Each Step Can Fail

Before any RTL, the failure at each boundary, because the receive path's distinguishing property is that its failures are silent by default.

StepFails whenSymptom without instrumentation
FIFO fillsthe memory system stallsCRC errors — Chapter 18.1 §9
no descriptorthe driver is behindframes simply absent
buffer too smallbuf_len below the frametruncation, or a scatter chain
a burst errorsa bad address, a decode errora partial frame written
ownership released earlyan ordering buga frame read before it is written
the interrupt is latecoalescinglatency, and jitter
the interrupt never comesa count threshold with no timerframes held indefinitely

Rows two and five are the two that produce wrong behaviour rather than lost behaviour, and row five is the worse: software reads a buffer whose contents are partly the current frame and partly the previous one.

And the sequence has a property worth naming at the outset: it is a pipeline whose stages have wildly different latencies.

StageTypical duration at 100 Gb/s
a minimum frame arrives6.72 ns
the FIFO crossing~20 ns
a descriptor fetch (prefetched)~0 — it is already held
a data burst's completion100–800 ns
the ownership write's completion100–800 ns
the interrupt0–100 000 ns

A frame arrives every 6.72 ns and takes something on the order of a microsecond to complete. The only way that works is deep pipeliningChapter 18.2 §9's thirty-two in flight is this chapter's requirement, not that one's — and the whole receive path is therefore a set of concurrent partial frames, not a sequence of complete ones.


A received frame crosses seven boundaries between leaving the PHY and being announced to software. First the physical layer hands it to the MAC's receive FIFO in the recovered clock domain, which is not variable because it is the event itself. Second the FIFO crosses into the host clock domain, which is slightly variable. Third a descriptor is fetched, which is variable with memory latency. Fourth the data is written to the host buffer, which is variable with DMA arbitration — and this is the first term Chapter 16.1 section 8 listed and left unpriced, worth 700 nanoseconds peak to peak on an eight-master round-robin interconnect. Fifth the status and length are written. Sixth ownership is transferred, which must follow the data becoming visible rather than merely being issued. Seventh the interrupt is asserted, which is variable with coalescing — the second unpriced term, worth 100 microseconds on a 100 microsecond timer. Against Chapter 16.5's achieved synchronisation accuracy of 24.2 nanoseconds, those two terms are 28.9 times and 4132 times the clock's own error, which is why Chapter 16.3 captures the timestamp at the start-frame delimiter, before any of this.1. PHY to FIFOrecovered clock — theevent2. FIFO to hostthe crossing3. Descriptorfetchedmemory latency4. Data writtenDMA arbitration — 700 ns5. Status writtenone descriptor write6. Ownership movedonly after 4 is VISIBLE7. Interruptcoalescing — 100 us16.5's clock24.2 ns28.9xarbitration vs the clock4132xcoalescing vs the clock12
Figure 1 — seven boundaries between the wire and software, and the two that carry the jitter.

3. RTL 1 — Reading the Receive FIFO

The first block on the host side, and the place where the receive path's defining constraint becomes concrete: there is no way to say "wait".

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// rxdma_pkg -- the receive DMA path.
// -----------------------------------------------------------------------
package rxdma_pkg;

  localparam int ADDR_W    = 64;
  localparam int BUS_BYTES = 64;         // 512-bit
  localparam int MAX_BEATS = 16;         // 18.1's burst limit
  localparam int MAX_SG    = 8;          // buffers per frame
  localparam int IN_FLIGHT = 32;         // 18.2 section 9's argument

  // A frame, as the host side sees it: a length, a status and a
  // stream of beats. The receive domain's framing is already gone --
  // 18.1 section 7 normalised it.
  typedef struct packed {
    logic [15:0] length;
    logic        fcs_ok;                 // 6.3's residue check
    logic        runt;                   // 7.3
    logic        giant;
    logic        truncated;
  } frame_meta_t;

  // One scatter-gather element: where a piece of the frame went.
  typedef struct packed {
    logic [ADDR_W-1:0] addr;
    logic [15:0]       bytes;
    logic [15:0]       desc_index;
    logic              first;
    logic              last;
  } sg_elem_t;

  // What a completed frame owes the ordering barrier.
  typedef struct packed {
    logic [$clog2(MAX_SG+1)-1:0] sg_count;
    logic [$clog2(IN_FLIGHT)-1:0] tag;
    frame_meta_t                  meta;
  } completion_t;

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// rx_fifo_reader -- drains 18.1's asynchronous FIFO into the DMA.
//
// There is no backpressure toward the wire. Everything this block
// cannot place is lost, and the ONLY variable it controls is how
// quickly it asks for somewhere to put it.
// -----------------------------------------------------------------------
module rx_fifo_reader
  import rxdma_pkg::*;
(
  input  logic                  clk,
  input  logic                  rst_n,

  // From 18.1 section 9's async FIFO, host side.
  input  logic                  fifo_empty,
  output logic                  fifo_rd,
  input  logic [BUS_BYTES*8-1:0] fifo_data,
  input  logic [$clog2(BUS_BYTES+1)-1:0] fifo_bytes,
  input  logic                  fifo_sof,
  input  logic                  fifo_eof,
  input  frame_meta_t           fifo_meta,
  input  logic [15:0]           fifo_occupancy_b,

  // To the write engine.
  output logic                  beat_valid,
  input  logic                  beat_ready,
  output logic [BUS_BYTES*8-1:0] beat_data,
  output logic [$clog2(BUS_BYTES+1)-1:0] beat_bytes,
  output logic                  beat_sof,
  output logic                  beat_eof,
  output frame_meta_t           beat_meta,

  // The only lever: start asking for a descriptor EARLY.
  input  logic [15:0]           cfg_early_start_b,
  output logic                  request_descriptor,

  output logic [31:0]           c_frames_read,
  output logic [31:0]           c_backpressure_cycles,
  output logic [31:0]           c_peak_occupancy,
  output logic                  cannot_keep_up
);

  logic in_frame;

  // The FIFO is drained whenever the write engine will take a beat.
  // If it will not, the FIFO fills, and 18.1 section 9's c_overflow
  // is what eventually reports it -- as a CRC error downstream.
  assign fifo_rd    = !fifo_empty && beat_ready;
  assign beat_valid = !fifo_empty && beat_ready;

  assign beat_data  = fifo_data;
  assign beat_bytes = fifo_bytes;
  assign beat_sof   = fifo_sof;
  assign beat_eof   = fifo_eof;
  assign beat_meta  = fifo_meta;

  // Ask for a descriptor as soon as ENOUGH of a frame has arrived,
  // not when all of it has. At 100 Gb/s a maximum frame takes 123 ns
  // and a descriptor fetch takes longer, so waiting for the frame
  // means the descriptor is always late.
  assign request_descriptor = (fifo_occupancy_b >= cfg_early_start_b) ||
                              (fifo_eof && !fifo_empty);

  // The FIFO growing while the engine is not taking beats is the
  // condition that ends in a drop. It is worth its own signal because
  // by the time c_overflow rises, frames are already lost.
  assign cannot_keep_up = !beat_ready && !fifo_empty;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      in_frame <= 1'b0;
      c_frames_read <= '0; c_backpressure_cycles <= '0;
      c_peak_occupancy <= '0;
    end else begin
      if (fifo_rd && fifo_sof) in_frame <= 1'b1;
      if (fifo_rd && fifo_eof) begin
        in_frame      <= 1'b0;
        c_frames_read <= c_frames_read + 1;
      end

      if (cannot_keep_up)
        c_backpressure_cycles <= c_backpressure_cycles + 1;

      if ({16'b0, fifo_occupancy_b} > c_peak_occupancy)
        c_peak_occupancy <= {16'b0, fifo_occupancy_b};
    end
  end

endmodule

Classification: a straight-through drain with an early-start heuristic and an explicit "falling behind" signal.

What it teaches: that cfg_early_start_b exists because the descriptor fetch is slower than the frame. At 100 Gb/s a maximum frame takes 123 ns on the wire and a descriptor fetch takes 100 to 800 ns. A design that waits for the frame to complete before asking where to put it has already lost the race, so the request goes out when enough of the frame has arrived to be confident there is a frame — and Chapter 18.2 §3's prefetch window exists so that the answer is usually already held.

And it teaches that cannot_keep_up is a leading indicator where c_overflow is a trailing one. Chapter 18.1 §9's overflow counter rises after octets have been lost. This signal rises while the FIFO still has room — it says the write engine is not taking beats, which is the condition that will eventually fill it. The gap between the two is the design's remaining margin, and c_backpressure_cycles measures how often it is being spent.

Deliberately simplified: beat_valid is tied to beat_ready, which is a combinational loop in form if not in fact — a real design registers the handshake and holds a beat. The early-start threshold is a single register where a real design scales it with the line rate and with the measured descriptor-fetch latency. And there is no frame-boundary protection: if the write engine stalls mid-frame and the FIFO overflows, this block happily continues streaming the frame's remaining beats, which a production design would mark as errored so the whole frame is discarded rather than half-written.

Production implication: c_peak_occupancy here and in Chapter 18.1 §13 are the same measurement from two sides, and comparing them is how an integrator separates a FIFO that is too shallow from a DMA that is too slow. A peak near the FIFO's depth with c_backpressure_cycles near zero means the memory system stalled and the DMA had nothing to doChapter 18.1 §17's assumption 3. A peak near depth with backpressure cycles high means the DMA itself is the limit, and Sections 13 and 17 are where that is diagnosed.


4. When the Ring Runs Dry

Chapter 18.2 §3's starved was a signal. Here it becomes a sequence of consequences, and the sequence matters because each stage has a different amount of margin.

The MAC has a frame and no descriptor. Four things happen, in order.

StageWhat absorbs itCapacity at 10 Gb/sThen
1the prefetch window8 descriptorsrefetch — Section 5
2the receive FIFO16 KiB = 13.1 µsfill
3Chapter 14.2's flow controlif configuredthe partner stops
4nothingdrop

Stage 3 is the one most designs do not have, because Chapter 18.1 §18's headroom is expensive — 16.2 KiB at 10 Gb/s over 2 km — and because flow control has Chapter 14.2's 88% collateral cost. So most ports go from stage 2 to stage 4 directly.

And the timing of that transition is worth computing, because it is much faster than intuition suggests.

Line rate16 KiB FIFO coversAt minimum frames, that is
1 Gb/s131.1 µs195 frames
10 Gb/s13.1 µs195 frames
25 Gb/s5.2 µs195 frames
100 Gb/s1.31 µs195 frames

The frame count is constant because both the FIFO's drain time and the frame interval scale with the rate — so a FIFO buys a fixed number of frames, not a fixed time, and 195 frames is about 1.3 µs at 100 Gb/s.

Which puts a hard bound on how long the ring may be dry. Chapter 18.2 §17 showed a 4096-entry ring covers 27.53 µs at 100 Gb/s; once it is exhausted the FIFO adds 1.31 µs and then frames are lost. The FIFO is 4.8% of the ring's coverage — not a second line of defence in any meaningful sense, and certainly not one to design around.

The drop's accounting is the section's last point and it is one people get wrong. A frame dropped for lack of a descriptor is not an Ethernet error. It is not a CRC error, not a runt, not a giant, and it did not happen on the wire. It must be counted in its own counter, because an operator who sees it in the CRC bucket looks at the cable — which Chapter 18.1 §21's first complaint already established as the most expensive misattribution in the module.


5. RTL 2 — The Descriptor Fetcher

Chapter 18.2 §3 held the window. This block fills it, and its policy decides whether the transaction arithmetic works.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// descriptor_fetcher -- keeps the prefetch window full.
//
// The policy is a low-water mark: fetch a full batch whenever the
// window drops below a threshold. Fetching one at a time would meet
// the same demand with eight times the transactions -- 18.1's wall.
// -----------------------------------------------------------------------
module descriptor_fetcher
  import rxdma_pkg::*;
#(
  parameter int BATCH   = 8,
  parameter int WINDOW  = 8,
  parameter int DESC_B  = 16
)(
  input  logic               clk,
  input  logic               rst_n,

  input  logic [$clog2(WINDOW+1)-1:0] window_level,
  input  logic [$clog2(WINDOW+1)-1:0] cfg_low_water,
  input  logic               request_descriptor,   // section 3's early start

  input  logic [ADDR_W-1:0]  ring_base,
  input  logic [15:0]        ring_len,
  input  logic [15:0]        next_index,

  // One batched read request.
  output logic               rd_valid,
  input  logic               rd_ready,
  output logic [ADDR_W-1:0]  rd_addr,
  output logic [15:0]        rd_bytes,
  output logic [15:0]        rd_base_index,

  input  logic               rd_done,
  input  logic               rd_error,

  output logic [31:0]        c_batches,
  output logic [31:0]        c_descs_fetched,
  output logic [31:0]        c_short_batches,
  output logic [31:0]        c_fetch_errors,
  output logic [31:0]        c_urgent_fetches,
  output logic               fetch_in_flight
);

  logic busy;
  logic [15:0] batch_n;

  wire [15:0] mask = ring_len - 16'd1;
  wire [15:0] slot = next_index & mask;

  // A batch must not run off the end of the ring. If the ring wraps
  // inside the batch, the batch is SHORTENED rather than split --
  // two transactions would defeat the point, and the next batch
  // starts at zero anyway.
  wire [15:0] to_end = ring_len - slot;
  wire [15:0] want   = (to_end < 16'(BATCH)) ? to_end : 16'(BATCH);

  // Urgent: the window is empty and a frame is arriving. Fetch a
  // batch immediately rather than waiting for the low-water rule.
  wire urgent = request_descriptor && (window_level == '0);

  wire should_fetch = !busy &&
                      ((window_level <= cfg_low_water) || urgent);

  assign rd_valid      = should_fetch;
  assign rd_addr       = ring_base +
                         ({{(ADDR_W-16){1'b0}}, slot} << $clog2(DESC_B));
  assign rd_bytes      = want * 16'(DESC_B);
  assign rd_base_index = next_index;
  assign fetch_in_flight = busy;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      busy <= 1'b0; batch_n <= '0;
      c_batches <= '0; c_descs_fetched <= '0; c_short_batches <= '0;
      c_fetch_errors <= '0; c_urgent_fetches <= '0;
    end else begin
      if (rd_valid && rd_ready) begin
        busy     <= 1'b1;
        batch_n  <= want;
        c_batches <= c_batches + 1;
        if (want < 16'(BATCH)) c_short_batches <= c_short_batches + 1;
        if (urgent)            c_urgent_fetches <= c_urgent_fetches + 1;
      end

      if (rd_done) begin
        busy <= 1'b0;
        if (rd_error) c_fetch_errors <= c_fetch_errors + 1;
        else c_descs_fetched <= c_descs_fetched + {16'b0, batch_n};
      end
    end
  end

endmodule

Classification: a low-water-mark batch fetcher with an urgent override and wrap-shortened batches.

What it teaches: that the low-water mark, not the batch size, is what keeps the window full. A fetcher that refills only when the window is empty has a hole equal to the fetch latency — 100 to 800 ns, during which Chapter 18.2 §3's starved is asserted and Section 4's sequence begins. Refilling at a low-water mark of 4 starts the next batch while four descriptors remain, which at 100 Gb/s is 26.9 ns of cover — not enough, which is why WINDOW and BATCH both want to be larger at high rates than the 8 this listing uses.

And it teaches that c_short_batches is a real cost, not an accounting curiosity. A batch shortened by the ring's wrap is a transaction that fetched fewer than eight descriptors for the same address-channel cost — and a batch is shortened whenever its starting slot is within BATCH − 1 of the ring's end. On a 4096-entry ring with batches of 8 that is 7 of every 4096 starting positions — 0.17%, which is negligible. On a 64-entry ring it is 7 in 64 — 10.9%, which is not, and which is one more reason a high-rate port wants a deep ring.

Deliberately simplified: one batch in flight at a time, which at 100 Gb/s means the window drains for the whole fetch latency and the urgent path fires constantly. A real design keeps two or three batches outstanding. Fetch errors leave the window short with no retry — the design simply refetches later, which is correct but slow and a production block would retry immediately. And the wrap is handled by shortening rather than by issuing a second transaction, which is the right trade here and would not be if the ring were small.

Production implication: c_urgent_fetches divided by c_batches is the fraction of fetches that happened because the window was already empty, which is the direct measure of whether the low-water mark is set correctly. Near zero means the steady-state policy is keeping up. Near one means every fetch is a rescue, the window is chronically empty, and the port is running at whatever rate the descriptor fetch latency allows — which is a throughput limit with no error counter attached and is invisible without this ratio.


6. Scatter-Gather, and Two Different Reasons for It

A frame may not fit in one buffer, and a buffer may not fit in one burst. These are different problems with different causes, and conflating them produces a design that handles one and breaks on the other.

Reason 1 — the frame exceeds the buffer. The driver supplies fixed-size buffers; a frame larger than one needs several, and the descriptors are chained.

Buffer size64-octet frame1518-octet9000-octet jumbo
2561636
5121318
2048115
4096113

Row one is why small buffers are a bad idea at high rates and the reason is Chapter 18.1 §12's again: six descriptors per ordinary frame is six times the descriptor traffic, and a batch of eight now covers one and a third frames instead of eight.

Row three is the common choice — 2 KiB buffers, one per ordinary frame, five for a jumbo — and it wastes 26% of the buffer on a 1518-octet frame, which is memory rather than bandwidth and is the right thing to spend.

Reason 2 — the buffer crosses a page boundary. This has nothing to do with the frame's size. Chapter 18.1 §5's rule: a burst may not cross a 4 KiB boundary, so a frame written to an address near the end of a page is split regardless of how much room the buffer has.

And the probability is computable for a buffer at a uniformly random offset:

Frame sizeP(crosses a 4 KiB boundary)Mean extra bursts
641.54%0.015
1283.10%0.031
51212.48%0.125
151837.04%0.370
9000100%2.197

The 1518 row is the one that matters and 37.04% is a large number. More than a third of maximum-size frames land across a page boundary if buffers are allocated at arbitrary offsets — each costing one extra transaction, which is Chapter 18.1 §21's c_split_4k and one of its complaint-2 checks.

The fix is entirely the driver's and it is free. Allocate buffers page-aligned, or at least 2 KiB-aligned; a 1518-octet frame in a 2 KiB-aligned buffer never crosses a 4 KiB boundary, because the buffer itself does not. The MAC cannot do anything about it and can count it, which is the division of labour Chapter 18.1 §17 established for every assumption in this module.

The two reasons produce different chains and the distinction matters for Section 11:

Reason 1 — frame exceeds bufferReason 2 — page crossing
producesseveral descriptorsseveral bursts, one descriptor
visible to softwareyes — a chain to reassembleno
ordering requirementall data before the FIRST descriptor's ownershipall bursts before the ownership
fixable by the driverbigger buffersalignment

Row two is the practical difference. A page crossing is a hardware detail software never sees; a buffer chain is a data structure software must walk, and getting it wrong is a protocol bug rather than a performance one.


A received frame is split for two reasons that have nothing to do with each other. The first is that the frame exceeds its buffer: with 2 kilobyte buffers a 64-octet frame and a 1518-octet frame each fit in one, while a 9000-octet jumbo frame needs five descriptors, and with 256-octet buffers a 1518-octet frame needs six and a jumbo needs thirty-six. This split produces extra descriptors that software must walk as a chain, and the fix is larger buffers, which costs memory. The second reason is that a burst may not cross a 4 kilobyte page boundary, which has nothing to do with the frame's size — a frame written to an address near the end of a page is split regardless of how much room the buffer has. At uniformly random alignment the probability is 1.54 percent for a 64-octet frame, 12.48 percent at 512 octets, 37.04 percent at 1518 octets, and certain for a 9000-octet frame. This split produces extra bursts that software never sees, and the fix is allocating buffers page-aligned, which is free. Conflating the two produces a design that handles one and issues illegal bursts on the other.One frameto be placed in memoryExceeds the buffer9000 into 2 KiB = 5Crosses a 4 KiB page37.04% at 1518 octetsExtra DESCRIPTORSsoftware walks a chainExtra BURSTSsoftware never sees themFix: larger bufferscosts memoryFix: align the poolfreeBoth, every frameone walk, three-wayminimum12
Figure 2 — two independent reasons to split a frame, with different fixes and very different costs.

7. RTL 3 — The Scatter-Gather Walker

The block that turns one frame into a list of places it went, handling both of Section 6's reasons in one pass.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// scatter_gather_walker -- splits a frame across buffers and pages.
//
// Two splits, one walk. A buffer boundary ends a descriptor; a 4 KiB
// boundary ends only a burst. The walker emits an element per burst
// and marks which of them also end a descriptor.
// -----------------------------------------------------------------------
module scatter_gather_walker
  import rxdma_pkg::*;
(
  input  logic               clk,
  input  logic               rst_n,

  // A new frame begins.
  input  logic               start,
  input  logic [15:0]        frame_bytes,        // may be unknown -- see below

  // The descriptor chain, supplied one at a time by 18.2's window.
  input  logic               desc_valid,
  output logic               desc_take,
  input  logic [ADDR_W-1:0]  desc_addr,
  input  logic [15:0]        desc_len,
  input  logic [15:0]        desc_index,

  // Elements out, one per burst.
  output logic               sg_valid,
  input  logic               sg_ready,
  output sg_elem_t           sg_elem,
  output logic               sg_ends_descriptor,

  input  logic               frame_end,          // the last beat arrived
  output logic [$clog2(MAX_SG+1)-1:0] sg_count,
  output logic               chain_overflow,     // more than MAX_SG needed

  output logic [31:0]        c_chains,
  output logic [31:0]        c_page_splits,
  output logic [31:0]        c_buffer_splits,
  output logic [31:0]        c_overflows
);

  logic [ADDR_W-1:0] cur_addr;
  logic [15:0]       cur_remaining;      // left in THIS descriptor
  logic              active, first_elem;

  // Distance to the next 4 KiB boundary from the current address.
  wire [12:0] to_page = 13'h1000 - {1'b0, cur_addr[11:0]};

  // The burst is limited by three things and the smallest wins.
  wire [15:0] by_burst = 16'(MAX_BEATS * BUS_BYTES);
  wire [15:0] by_page  = {3'b0, to_page};
  wire [15:0] by_desc  = cur_remaining;

  logic [15:0] this_burst;
  always_comb begin
    this_burst = by_desc;
    if (this_burst > by_burst) this_burst = by_burst;
    if (this_burst > by_page)  this_burst = by_page;
  end

  // A burst ends the descriptor only if it consumed the descriptor's
  // remaining bytes. A page split does NOT end a descriptor -- which
  // is section 6's distinction, in one signal.
  assign sg_ends_descriptor = (this_burst == by_desc);

  assign sg_valid = active && (cur_remaining != '0);
  assign sg_elem  = '{ addr:       cur_addr,
                       bytes:      this_burst,
                       desc_index: desc_index,
                       first:      first_elem,
                       last:       1'b0 };      // patched at frame_end

  assign desc_take = active && (cur_remaining == '0) && desc_valid;

  assign chain_overflow = (sg_count == MAX_SG[$bits(sg_count)-1:0]) &&
                          sg_valid && sg_ready;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      active <= 1'b0; first_elem <= 1'b0;
      cur_addr <= '0; cur_remaining <= '0; sg_count <= '0;
      c_chains <= '0; c_page_splits <= '0; c_buffer_splits <= '0;
      c_overflows <= '0;
    end else begin
      if (start) begin
        active <= 1'b1; first_elem <= 1'b1;
        sg_count <= '0; cur_remaining <= '0;
        c_chains <= c_chains + 1;
      end

      if (desc_take) begin
        cur_addr      <= desc_addr;
        cur_remaining <= desc_len;
      end

      if (sg_valid && sg_ready) begin
        cur_addr      <= cur_addr + {{(ADDR_W-16){1'b0}}, this_burst};
        cur_remaining <= cur_remaining - this_burst;
        sg_count      <= sg_count + 1'b1;
        first_elem    <= 1'b0;

        if (sg_ends_descriptor) c_buffer_splits <= c_buffer_splits + 1;
        else if (this_burst == by_page)
          c_page_splits <= c_page_splits + 1;

        if (chain_overflow) c_overflows <= c_overflows + 1;
      end

      if (frame_end && (cur_remaining == '0)) active <= 1'b0;
    end
  end

endmodule

Classification: a three-way minimum with two distinct split reasons counted separately.

What it teaches: that frame_bytes is an input this block cannot rely on, and the design is built so it does not have to. Chapter 12.6's cut-through argument applies here in a different form: the DMA starts writing before the frame has finished arriving, so the length is not known when the first burst is issued. The walker therefore consumes descriptors as it needs them rather than planning the chain in advance — and frame_end is what stops it, not a length comparison.

And it teaches that c_page_splits and c_buffer_splits measure two entirely different faults. Page splits are the driver's allocator — fixable by aligning buffers, at no cost. Buffer splits are the driver's buffer size — fixable by allocating larger ones, at the cost of memory. A single "split" counter tells an integrator to do something and not which thing, and Section 6's table shows the two have different magnitudes: 37% page splits on maximum frames versus 0% buffer splits at 2 KiB.

Deliberately simplified: sg_elem.last is emitted as zero and noted as patched later, which no real design does — the last element is known only at frame_end and a production walker holds one element back so it can be marked. chain_overflow detects a frame needing more than MAX_SG buffers and has no recovery; the correct behaviour is to abort the frame and release the chain, which is several more states. And the walker takes descriptors one at a time from the window with no lookahead, so a chain of five stalls five times if the window is short.

Production implication: c_overflows is the counter that catches a jumbo-frame configuration mismatch, and it is one of the few in this module whose fix is a one-line driver change. A port configured for 2 KiB buffers and MAX_SG of 8 handles frames up to 16 KiB; one configured for 256-octet buffers handles only 2 KiB, so enabling 9000-octet jumbo frames on such a port produces an overflow on every jumbo — counted here, and otherwise appearing as "large frames do not work", which is a description rather than a diagnosis.


8. RTL 4 — The Write Engine

The block that actually moves the frame, and the one with the most to get wrong per cycle.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// rx_write_engine -- issues the data bursts and tracks their
// completions so section 10 knows when the frame is visible.
//
// The counting is the point. A frame is not "written" when its last
// burst is ISSUED; it is written when the last burst has RESPONDED,
// and the gap is 18.2 section 10's entire subject applied to data
// rather than to a descriptor field.
// -----------------------------------------------------------------------
module rx_write_engine
  import rxdma_pkg::*;
(
  input  logic               clk,
  input  logic               rst_n,

  // Elements from the walker.
  input  logic               sg_valid,
  output logic               sg_ready,
  input  sg_elem_t           sg_elem,
  input  logic [$clog2(IN_FLIGHT)-1:0] frame_tag,

  // Beats from the FIFO reader.
  input  logic               beat_valid,
  output logic               beat_ready,
  input  logic [BUS_BYTES*8-1:0] beat_data,
  input  logic [$clog2(BUS_BYTES+1)-1:0] beat_bytes,

  // Bus write out.
  output logic               aw_valid,
  input  logic               aw_ready,
  output logic [ADDR_W-1:0]  aw_addr,
  output logic [7:0]         aw_len,
  output logic [$clog2(IN_FLIGHT)-1:0] aw_tag,
  output logic               w_valid,
  input  logic               w_ready,
  output logic [BUS_BYTES*8-1:0] w_data,
  output logic [BUS_BYTES-1:0] w_strb,
  output logic               w_last,

  // Responses, by tag.
  input  logic               b_valid,
  input  logic [$clog2(IN_FLIGHT)-1:0] b_tag,
  input  logic               b_error,

  // Per-frame burst accounting, read by section 10.
  output logic [7:0]         bursts_issued  [IN_FLIGHT],
  output logic [7:0]         bursts_done    [IN_FLIGHT],
  output logic               frame_data_visible [IN_FLIGHT],

  output logic [31:0]        c_bursts,
  output logic [31:0]        c_beats,
  output logic [31:0]        c_write_errors,
  output logic [31:0]        c_stall_no_beat
);

  logic [15:0] burst_bytes_left;
  logic        in_burst;

  assign aw_valid = sg_valid && !in_burst;
  assign aw_addr  = sg_elem.addr;
  assign aw_len   = 8'(((sg_elem.bytes + BUS_BYTES - 1) / BUS_BYTES) - 1);
  assign aw_tag   = frame_tag;
  assign sg_ready = aw_valid && aw_ready;

  assign w_valid  = in_burst && beat_valid;
  assign w_data   = beat_data;
  assign w_last   = in_burst && (burst_bytes_left <= 16'(BUS_BYTES));
  assign beat_ready = in_burst && w_ready;

  // Strobes for the final partial beat. A frame is rarely a multiple
  // of 64 octets, and writing the pad would corrupt whatever follows
  // the frame in the buffer.
  always_comb begin
    int i;
    for (i = 0; i < BUS_BYTES; i++)
      w_strb[i] = (i < int'(beat_bytes));
  end

  // A frame's data is visible when every burst it issued has
  // responded -- and only then. Issued-equals-done is NOT sufficient
  // on its own: the count must also be final, which section 10's
  // "sealed" flag provides.
  always_comb begin
    int t;
    for (t = 0; t < IN_FLIGHT; t++)
      frame_data_visible[t] = (bursts_issued[t] != 8'd0) &&
                              (bursts_done[t] == bursts_issued[t]);
  end

  always_ff @(posedge clk or negedge rst_n) begin
    int t;
    if (!rst_n) begin
      in_burst <= 1'b0; burst_bytes_left <= '0;
      for (t = 0; t < IN_FLIGHT; t++) begin
        bursts_issued[t] <= '0; bursts_done[t] <= '0;
      end
      c_bursts <= '0; c_beats <= '0; c_write_errors <= '0;
      c_stall_no_beat <= '0;
    end else begin
      if (aw_valid && aw_ready) begin
        in_burst         <= 1'b1;
        burst_bytes_left <= sg_elem.bytes;
        bursts_issued[frame_tag] <= bursts_issued[frame_tag] + 8'd1;
        c_bursts <= c_bursts + 1;
      end

      if (w_valid && w_ready) begin
        burst_bytes_left <= burst_bytes_left - 16'(BUS_BYTES);
        c_beats <= c_beats + 1;
        if (w_last) in_burst <= 1'b0;
      end

      // The engine has a burst open and no data. At 100 Gb/s this is
      // the FIFO having drained, which means the frame is arriving
      // more slowly than the bus can take it -- normal, and worth
      // counting because a high value means the bus is oversized.
      if (in_burst && !beat_valid)
        c_stall_no_beat <= c_stall_no_beat + 1;

      if (b_valid) begin
        bursts_done[b_tag] <= bursts_done[b_tag] + 8'd1;
        if (b_error) c_write_errors <= c_write_errors + 1;
      end
    end
  end

endmodule

Classification: a burst issuer with per-frame outstanding-write accounting.

What it teaches: that frame_data_visible is the quantity Chapter 18.2 §7 needed and did not have. That chapter gated ownership on one field write's response; here the frame is several bursts and the gate is a count comparison. And the comparison is subtle: issued == done is true at the start of a frame, when both are zero, and true transiently mid-frame whenever responses catch up with issues. The flag is therefore not sufficient alone — Section 10 adds the missing condition.

And it teaches why w_strb matters more than it looks. A 1518-octet frame on a 64-octet bus is 23 full beats and one of 46 octets. Writing the remaining 18 octets overwrites whatever follows the frame in the buffer — which on a 2 KiB buffer is slack and on a tightly packed one is the next frame. The strobes cost a comparator per lane and prevent a corruption that would be attributed to the network.

Deliberately simplified: one burst open at a time, which serialises the address and data phases and is exactly what a 100 Gb/s port cannot do — a real engine pipelines addresses ahead of data. The tag is per frame rather than per burst, so all of a frame's bursts share an ID; that is convenient for the accounting and it also forces same-ID ordering on the data, which is more ordering than needed and costs throughput. And bursts_issued is 8 bits, which caps a frame at 255 bursts — ample, but unchecked.

Production implication: c_stall_no_beat is the counter that tells an integrator the bus is wider than it needs to be. A high value means the engine opens a burst and waits for data, which happens when the frame arrives more slowly than the bus can absorb it — normal at 1 Gb/s on a 512-bit bus, and a sign of over-provisioning if it dominates. Read alongside Chapter 18.1 §5's c_stall_outstanding, the two say whether the MAC or the memory system is waiting, which is the first fork in every throughput investigation.


9. The Ordering Problem, Enlarged

Chapter 18.2 §10 ordered two writes. This chapter orders up to twenty, and the enlargement changes the problem qualitatively rather than just quantitatively.

Count what a 1518-octet frame into a page-aligned 2 KiB buffer actually issues on a 512-bit bus:

WriteCountBeats each
data bursts216, then 8
status and length11
ownership11

Four transactions, and the ownership one must be last to become visible. Chapter 18.2's three strategies were: one atomic write, one shared AXI ID, or wait for the response. Check each against four transactions instead of two.

StrategyWith 2 writesWith 20 writes
one atomic writeworks if the descriptor fits a beatimpossible — the data is 1518 octets
one shared AXI IDworksworks, and serialises the data
wait for the responsecosts one round tripcosts one round trip, for all of them

Row one is simply gone. A frame cannot be written atomically with its descriptor; the best available strategy has been eliminated by scale.

Row two survives and its cost has grown enormously. Same-ID ordering means every data burst of every frame is serialised against every other — AXI completes same-ID transactions in order, so a 100 Gb/s port issuing 186 M transactions per second through one ID has one outstanding at a time. That is a throughput collapse, not a cost.

Row three is therefore the only survivor, and its cost has not grown, which is the section's good news. Waiting for the last data burst's response costs one round trip per frame — the same as waiting for one field write's response — because the bursts are issued concurrently and their responses arrive concurrently. The wait is for the last response, not for the sum of them.

So the receive path's ordering rule is:

Issue every data burst and the status write freely, with whatever IDs give best throughput. Count the responses. Issue the ownership write only when every one of them has returned.

And there is a second condition that Chapter 18.2 did not need, which Section 8 flagged. bursts_issued == bursts_done is true before the frame has issued all its bursts — at the very start, when both are zero, and transiently whenever responses catch up. So the count comparison must be qualified by "and no more bursts are coming", which is a separate piece of information: the walker has finished and the frame has ended.

ConditionProvided by
every issued burst has respondedSection 8's counters
no further bursts will be issuedthe walker's frame_end — a "sealed" flag
the status write has respondedits own tag

And there is a simplification available here that Chapter 18.2 could not use, which is worth taking because it decides the transaction arithmetic.

In Chapter 18.2 the status and the ownership bit had to be two writes, because the ownership write's gating condition was the status write's response. Here the gating condition is the data's, and the status and the ownership bit are adjacent fields of the same 16-octet descriptor. So once the data is visible, both may be written together, in one transaction.

DesignDescriptor writes per frameTransactions per frame, batched by 8
status and ownership separate21/8 + 1 + 1/8 + 1 = 2.250
combined into one writeback11/8 + 1 + 1/8 = 1.250

Row one is 1.339 transactions per cycle at 100 Gb/s and does not fit. Row two is 0.744, which is exactly the figure Chapter 18.1 §12 and Chapter 18.2 §18 quoted — so those chapters' arithmetic assumed the combined form without saying so, and this is where the assumption is discharged.

The combined write is only legal because of the gating change. Writing status and ownership together would be wrong in Chapter 18.2's formulation — the ownership bit would become visible at the same instant as the status rather than after it, which is fine, but the driver-supplied fields the write must not clobber constrain the byte enables, and getting those wrong turns the best option into the worst. Section 12's block carries both fields and Section 10's barrier is what releases it.

All three, then ownership. Section 10 is that conjunction in RTL, and the sealed flag is the piece that is easy to omit — a design that gates on the counters alone releases ownership at the start of every frame, which is the most dramatic possible version of the bug and, mercifully, one that fails immediately rather than intermittently.


A bus write passes through three events that a design frequently treats as one. The address handshake means the interconnect has the address. The data handshake with the last beat means it has the data. Only the write response means the write has reached a point of visibility where a subsequent reader will see it. Everything between the second and third events is a posted write in flight — in a queue, being routed, possibly being retried — and the data is somewhere that is not memory. A design that releases a frame's ownership when its last burst is issued therefore hands software a buffer whose tail is still the previous frame's, and no software check catches it because the frame check sequence was verified in the MAC before the DMA ever moved the data. The correct gate has three conditions per frame: every issued burst has responded, the burst count is sealed because the scatter-gather walker has emitted the frame's last element, and the descriptor writeback is ready. The seal is the condition that is easy to omit, and without it the comparison of responses to issues is trivially true before the first burst is issued.Address acceptedcustody of the addressData acceptedcustody of the dataResponse returnedVISIBILITYThe gateall responded ANDsealedOwnership releasedsoftware may readRelease at issuethe tail is still staleFCS still passeschecked in the MAC,upstreamThe seal0 == 0 is true tooNine burstsnine chances per jumboframe12
Figure 3 — custody is not visibility, and a nine-burst frame offers nine chances to confuse them.

10. RTL 5 — The Receive Ordering Barrier

The conjunction from Section 9, per frame, for thirty-two frames at once.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// rx_ordering_barrier -- releases a frame's ownership write only when
// all of its data is visible.
//
// Three conditions per frame: the burst count is SEALED, every burst
// has responded, and the status write has responded. The seal is the
// condition 18.2 did not need and this chapter cannot do without.
// -----------------------------------------------------------------------
module rx_ordering_barrier
  import rxdma_pkg::*;
(
  input  logic               clk,
  input  logic               rst_n,

  // A frame begins.
  input  logic               frame_start,
  input  logic [$clog2(IN_FLIGHT)-1:0] start_tag,
  input  logic [15:0]        start_desc_index,

  // The walker has emitted this frame's last element.
  input  logic               frame_sealed,
  input  logic [$clog2(IN_FLIGHT)-1:0] seal_tag,

  // From section 8.
  input  logic [7:0]         bursts_issued [IN_FLIGHT],
  input  logic [7:0]         bursts_done   [IN_FLIGHT],

  // The status write's response.
  input  logic               status_done,
  input  logic [$clog2(IN_FLIGHT)-1:0] status_tag,

  // The ownership write, released.
  output logic               own_valid,
  input  logic               own_ready,
  output logic [$clog2(IN_FLIGHT)-1:0] own_tag,
  output logic [15:0]        own_desc_index,

  input  logic               own_done,
  input  logic [$clog2(IN_FLIGHT)-1:0] own_done_tag,

  output logic [31:0]        c_released,
  output logic [31:0]        c_wait_cycles,
  output logic [15:0]        c_peak_open,
  output logic               released_before_data,   // must never assert
  output logic               tag_reused_while_open
);

  logic        open   [IN_FLIGHT];
  logic        sealed [IN_FLIGHT];
  logic        st_ok  [IN_FLIGHT];
  logic        issued [IN_FLIGHT];        // ownership write issued
  logic [15:0] didx   [IN_FLIGHT];

  // Per frame: every burst responded AND the count is final AND the
  // status write is back.
  logic ready_v [IN_FLIGHT];
  always_comb begin
    int t;
    for (t = 0; t < IN_FLIGHT; t++)
      ready_v[t] = open[t] && sealed[t] && st_ok[t] && !issued[t] &&
                   (bursts_done[t] == bursts_issued[t]);
  end

  logic [$clog2(IN_FLIGHT)-1:0] sel;
  logic                         sel_hit;
  always_comb begin
    int t;
    sel = '0; sel_hit = 1'b0;
    for (t = IN_FLIGHT-1; t >= 0; t--)
      if (ready_v[t]) begin
        sel = t[$clog2(IN_FLIGHT)-1:0];
        sel_hit = 1'b1;
      end
  end

  assign own_valid      = sel_hit;
  assign own_tag        = sel;
  assign own_desc_index = didx[sel];

  // The invariant. If an ownership write is ever offered for a frame
  // whose bursts are not all back, the block is broken.
  assign released_before_data = own_valid &&
                                ((bursts_done[own_tag] != bursts_issued[own_tag]) ||
                                 !sealed[own_tag]);

  // A tag allocated while its previous frame is still open means the
  // in-flight table is being reused too early -- a frame's data would
  // be counted against the wrong frame.
  assign tag_reused_while_open = frame_start && open[start_tag];

  always_ff @(posedge clk or negedge rst_n) begin
    int t;
    logic [15:0] n_open;
    if (!rst_n) begin
      for (t = 0; t < IN_FLIGHT; t++) begin
        open[t] <= 1'b0; sealed[t] <= 1'b0;
        st_ok[t] <= 1'b0; issued[t] <= 1'b0; didx[t] <= '0;
      end
      c_released <= '0; c_wait_cycles <= '0; c_peak_open <= '0;
    end else begin
      if (frame_start) begin
        open[start_tag]   <= 1'b1;
        sealed[start_tag] <= 1'b0;
        st_ok[start_tag]  <= 1'b0;
        issued[start_tag] <= 1'b0;
        didx[start_tag]   <= start_desc_index;
      end

      if (frame_sealed) sealed[seal_tag] <= 1'b1;
      if (status_done)  st_ok[status_tag] <= 1'b1;

      if (own_valid && own_ready) issued[sel] <= 1'b1;

      if (own_done) begin
        open[own_done_tag] <= 1'b0;
        c_released <= c_released + 1;
      end

      // How long frames sit sealed but not visible. This is the
      // memory system's write latency, measured per frame.
      n_open = '0;
      for (t = 0; t < IN_FLIGHT; t++) begin
        if (open[t]) n_open = n_open + 16'd1;
        if (open[t] && sealed[t] && (bursts_done[t] != bursts_issued[t]))
          c_wait_cycles <= c_wait_cycles + 1;
      end
      if (n_open > c_peak_open) c_peak_open <= n_open;
    end
  end

endmodule

Classification: a per-frame three-condition gate over an in-flight table, with a priority selector.

What it teaches: that the seal is not optional and its absence produces a spectacular rather than a subtle failure. Without sealed, a frame's bursts_done == bursts_issued holds at 0 == 0 the instant the frame starts — so ownership is released before any data is written at all. That is a bug that fails on the first frame in any testbench, which makes it the one bug in this chapter that gets caught early, and it is worth noting because every other ordering bug here fails intermittently and late.

And it teaches that c_wait_cycles divided by c_released is the write-completion latency, measured per frame, in the design's own clock. Chapter 18.2 §7 measured it for one descriptor field; this measures it for a whole frame's data, which is the number that sizes IN_FLIGHT. At a 300 ns latency and 6.72 ns per frame, 45 frames must be open at once — more than this listing's 32, which c_peak_open sitting at 32 would reveal.

Deliberately simplified: the ready_v computation and the priority selector are combinational over 32 entries and evaluated every cycle, which is a large amount of logic in one path; a production design maintains a small ready queue instead. The selector is fixed-priority, so a low-numbered tag that keeps becoming ready can starve a high-numbered one — in practice the tags cycle, so it does not happen, but nothing here prevents it. And c_wait_cycles is accumulated inside a loop over all 32 entries, which adds up to 32 per cycle and is a per-frame-cycle count rather than a per-cycle one; the division by c_released is still meaningful and the units need stating.

Production implication: released_before_data must be wired to a fatal error and to an interrupt. It is the signal that says software may be reading a buffer the hardware has not finished writing — the single worst failure in the receive path, because it produces a frame whose head is correct and whose tail is the previous frame's, which passes every length check, has a valid descriptor, and is simply wrong. No downstream check catches it; the FCS was verified in the MAC before the data ever reached memory.


11. The Jumbo Chain, Published Backwards

A frame in five buffers is five descriptors, and the order in which they are handed to software is the reverse of the order they were filled. This is the chapter's most counter-intuitive requirement.

Software walks a chain forwards. It reads the first descriptor, sees a FIRST flag and no LAST, and follows the chain to the next index until it finds LAST. So the moment the first descriptor is owned by the driver, software may begin walking.

Which means the first descriptor must be released last.

DescriptorFilledMust be released
0 — FIRSTfirstLAST
1secondfourth
2thirdthird
3fourthsecond
4 — LASTlastfirst

The reasoning is short. If descriptor 0 is released while descriptor 4 is still being written, software reads descriptor 0, follows the chain, reaches descriptor 4, and finds it owned by the MAC — at best a stall, at worst a walk into a descriptor mid-update. Releasing 4 first and 0 last means that when software can see the chain's head, the whole chain is complete.

And a second constraint stacks on top: every descriptor's data must be visible before any of them is released. Not just its own — all of them — because software may read buffer 4's contents as soon as it has walked to descriptor 4, which it can do as soon as descriptor 0 is released.

So the full rule for a chain of N:

Wait until every burst of every descriptor in the chain has responded. Then release descriptor N−1, then N−2, down to descriptor 0.

The cost is one round trip for the whole chain, not N of them — Section 9's argument again: the bursts are concurrent and the wait is for the last response. The releases themselves are serialised in reverse, which is N writes that must be ordered among themselves; same-ID ordering is the natural mechanism, because there are only five and they are inherently serial anyway.

Now what a chain actually costs in transactions, with descriptor fetches and writebacks batched by 8:

FrameBuffers (2 KiB)Data burstsTotal transactionsPer octet
641140.0625
15181250.0033
900059240.0027

The last column is the one to read and it is more modest than jumbo frames' reputation suggests. A 9000-octet frame costs 0.0027 transactions per octet against a 1518-octet frame's 0.0033an 18% improvement, not the order of magnitude people expect, because the descriptor overhead returns once per 2 KiB buffer rather than once per frame.

And the buffer size is the larger lever:

FrameBuffersTotal transactionsPer octetAgainst 2 KiB
90005 × 2 KiB240.0027
90003 × 4 KiB180.002026% better

Going from 2 KiB to 4 KiB buffers buys more than going from 1518 to 9000 octets does, which returns the chapter to Chapter 18.1 §12's conclusion: everything in this module is about transaction count, and the lever is almost always how much payload one transaction covers.

What jumbo frames do buy is on the wire and in the CPUChapter 8.3's efficiency argument and Chapter 18.1 §10's interrupt arithmetic, where the frame rate falls by a factor of six and the interrupt load falls with it. The bus is not where they pay off, and a design that adopts them expecting bus relief will measure an 18% change and be disappointed.


12. RTL 6 — The Descriptor Writeback

The block that reports what happened, carrying Section 9's combined status-and-ownership field, and the one where batching has a correctness constraint the others do not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// status_writeback -- writes each frame's length, status and
// ownership as ONE descriptor write, with optional batching across
// contiguous descriptors.
//
// Batching is 18.1 section 12's second lever: it is what takes 1.265
// transactions per cycle down to 0.744. The constraint is that a
// batch may only cover CONTIGUOUS descriptors, because one write
// covers one contiguous address range.
// -----------------------------------------------------------------------
module status_writeback
  import rxdma_pkg::*;
#(
  parameter int MAX_BATCH = 8,
  parameter int DESC_B    = 16
)(
  input  logic               clk,
  input  logic               rst_n,

  input  logic               cfg_batch_enable,
  input  logic [15:0]        cfg_batch_timeout,

  // A frame is RELEASED by section 10's barrier -- its data is
  // already visible. Nothing here may be issued before that.
  input  logic               rel_valid,
  output logic               rel_ready,
  input  logic [15:0]        rel_index,
  input  frame_meta_t        rel_meta,
  input  logic [15:0]        rel_length,
  input  logic [$clog2(IN_FLIGHT)-1:0] rel_tag,

  input  logic [ADDR_W-1:0]  ring_base,
  input  logic [15:0]        ring_len,
  input  logic               tick,

  output logic               wr_valid,
  input  logic               wr_ready,
  output logic [ADDR_W-1:0]  wr_addr,
  output logic [15:0]        wr_bytes,
  output logic [$clog2(IN_FLIGHT)-1:0] wr_tag,

  output logic [31:0]        c_writes,
  output logic [31:0]        c_batched_entries,
  output logic [31:0]        c_flushed_by_timeout,
  output logic [31:0]        c_broken_by_wrap,
  output logic [31:0]        c_broken_by_order,
  output logic [15:0]        mean_batch_x100
);

  logic [15:0] batch_first, batch_next;
  logic [$clog2(MAX_BATCH+1)-1:0] batch_n;
  logic [15:0] age;
  logic        have;

  wire [15:0] mask = ring_len - 16'd1;

  // A release joins the batch only if it is the NEXT descriptor and
  // the batch does not wrap. A wrap breaks the contiguity one write
  // requires; an out-of-order release breaks it for a different
  // reason, and the two are counted separately.
  wire is_next   = have && (rel_index == batch_next);
  wire no_wrap   = ((rel_index & mask) != 16'd0);
  wire can_join  = cfg_batch_enable && is_next && no_wrap &&
                   (batch_n < MAX_BATCH[$bits(batch_n)-1:0]);

  wire timed_out = have && (age >= cfg_batch_timeout) &&
                   (cfg_batch_timeout != 16'd0);

  assign wr_valid  = have && (timed_out ||
                              (rel_valid && !can_join) ||
                              (batch_n == MAX_BATCH[$bits(batch_n)-1:0]));
  assign wr_addr   = ring_base +
                     ({{(ADDR_W-16){1'b0}}, (batch_first & mask)} << $clog2(DESC_B));
  assign wr_bytes  = 16'(batch_n) * 16'(DESC_B);
  assign wr_tag    = rel_tag;
  assign rel_ready = !have || can_join || wr_ready;

  always_comb begin
    if (c_writes == '0) mean_batch_x100 = 16'd0;
    else mean_batch_x100 = 16'((c_batched_entries * 32'd100) / c_writes);
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      have <= 1'b0; batch_first <= '0; batch_next <= '0;
      batch_n <= '0; age <= '0;
      c_writes <= '0; c_batched_entries <= '0;
      c_flushed_by_timeout <= '0; c_broken_by_wrap <= '0;
      c_broken_by_order <= '0;
    end else begin
      if (tick && have) age <= age + 16'd1;

      if (wr_valid && wr_ready) begin
        c_writes          <= c_writes + 1;
        c_batched_entries <= c_batched_entries + {29'b0, batch_n};
        if (timed_out) c_flushed_by_timeout <= c_flushed_by_timeout + 1;
        have    <= 1'b0;
        batch_n <= '0;
        age     <= '0;
      end

      if (rel_valid && rel_ready) begin
        if (can_join) begin
          batch_next <= rel_index + 16'd1;
          batch_n    <= batch_n + 1'b1;
        end else begin
          if (have && !no_wrap)  c_broken_by_wrap  <= c_broken_by_wrap + 1;
          if (have && no_wrap && !is_next)
            c_broken_by_order <= c_broken_by_order + 1;
          batch_first <= rel_index;
          batch_next  <= rel_index + 16'd1;
          batch_n     <= 1;
          have        <= 1'b1;
          age         <= '0;
        end
      end
    end
  end

endmodule

Classification: a contiguity-constrained write coalescer with a timeout and two distinct break causes.

What it teaches: that batching a writeback has a structural constraint the descriptor fetch does not. A fetch reads a contiguous range and a wrap simply shortens it — Section 5. A writeback must cover exactly the descriptors that were released, and they are contiguous only if frames are released in index order. c_broken_by_wrap and c_broken_by_order separate the two causes, and they need different fixes: the first is arithmetic and unavoidable, the second means Section 10's barrier is releasing frames out of order, which happens as soon as frames have different data volumes and therefore different completion times.

And it teaches that the timeout is mandatory, for the same reason Chapter 18.1 §11's was. A batch of 8 holding 3 entries with no further releases holds three completed frames indefinitely — and since the ownership bits ride in this write, those three frames are invisible to software until traffic resumes. The failure is identical in shape to the interrupt-coalescing one, one layer further down, where nobody looks.

Deliberately simplified: mean_batch_x100 contains a combinational divide, which no design does — software computes it from the two counters. wr_tag is the most recent release's tag, so one response frees one frame's tracking when it should free the whole batch's; a real design carries a tag vector. The byte enables are absent entirely, and they are the part that must not be got wrong: a writeback covering a whole descriptor would clobber the driver-supplied buf_ptr and buf_len, which is Section 9's warning made concrete.

Production implication: mean_batch_x100 says whether batching is working, and it is frequently far below its configured maximum for reasons nobody suspects. A design configured for batches of 8 that measures a mean of 1.3 is paying the timeout's latency and getting almost none of the transaction saving — the per-frame figure is 1/8 + 1 + 1/1.3 = 1.894 instead of 1.250, which at 100 Gb/s is 1.128 transactions per cycle rather than 0.744. Above one, and therefore not a working design. Chapter 18.1 §12's whole conclusion depends on a number this counter is the only way to check.


13. Pricing Chapter 16.1's First Unpriced Term: DMA Arbitration

Chapter 16.1 §8's second row read "frame is written to the receive ring — hardware — slightly variable — DMA arbitration", and pointed here. This section supplies the number.

What varies. The MAC's write request enters an interconnect shared with every other master on the chip. How long it takes to be granted depends on what else is asking, and on a round-robin arbiter with M masters each holding the bus for one transaction time t, a request waits between 0 and (M−1)·t.

Competing mastersBestWorstJitter, peak to peak
1 — the MAC alone100 ns100 ns0 ns
2100 ns200 ns100 ns
4100 ns400 ns300 ns
8100 ns800 ns700 ns

Eight masters is an ordinary number on an application SoC — a CPU cluster, a GPU, a display controller, a video codec, a storage controller, a second network port, a security engine, and this MAC.

Now put 700 ns against the budget it lands in.

QuantityValueRatio to Chapter 16.5's 24.2 ns
Chapter 16.5's synchronisation accuracy24.2 ns
DMA arbitration jitter, 8 masters700 ns28.9×

A software timestamp taken after this stage carries 28.9 times the error of the clock it is reading. Which is Chapter 16.1's thesis, and this is the first row of its table to be given a magnitude.

Three qualifications, because the number is a model rather than a measurement:

QualificationEffect
round-robin is the friendly casea priority arbiter starves a low-priority master for longer
t is not constanta competing master's 1 KiB burst is not 100 ns
DRAM refresh adds its owna refresh stalls a bank for tens of nanoseconds, aperiodically

Row two is the largest correction and it usually makes the number worse. A display controller reading a scanline holds the bus for far longer than one transaction time, so the MAC's worst-case wait is set by the longest competing transaction, not by an average one — and a design with one such master can exceed the eight-master figure on its own.

And the term is irreducible from the MAC's side, which is the section's practical conclusion. The MAC can:

The MAC canEffect on the jitter
issue earliershifts the mean, not the spread
raise its arbitration priorityreduces it — at everyone else's expense
use more outstanding transactionshides it for throughput, not for latency
capture the timestamp before this stageremoves it entirely

Row four is the only one that works and it is Chapter 16.3's answer, arrived at from a completely different direction. A timestamp captured at the SFD — before the FIFO, before the DMA, before the interrupt — carries none of this, and everything in Sections 13 and 15 becomes irrelevant to it. The news arrives late; the value it carries was recorded on time.


14. RTL 7 — The Receive Interrupt Coalescer

The last stage before software, and the one that dominates every latency number in this chapter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// rx_interrupt_coalescer -- count and timer, with a separate
// uncoalesced path for traffic that cannot tolerate the delay.
//
// 18.1 section 11 built the generic version. This one adds the thing
// 16.1's callout asked for: a bypass for timestamped traffic, so the
// throughput tuning and the latency requirement stop competing.
// -----------------------------------------------------------------------
module rx_interrupt_coalescer
  import rxdma_pkg::*;
(
  input  logic               clk,
  input  logic               rst_n,

  // One pulse per frame released to software.
  input  logic               frame_released,
  input  logic               frame_is_urgent,     // PTP, or an error
  input  logic               frame_is_ptp,        // 16.2's EtherType

  input  logic [15:0]        cfg_count,
  input  logic [15:0]        cfg_timer_us,
  input  logic               cfg_ptp_bypass,
  input  logic               tick_1us,
  input  logic               irq_ack,

  output logic               irq,
  output logic [31:0]        c_irq_total,
  output logic [31:0]        c_irq_by_count,
  output logic [31:0]        c_irq_by_timer,
  output logic [31:0]        c_irq_bypass,
  output logic [31:0]        c_frames_signalled,
  output logic [31:0]        c_delay_us_accum,     // for the mean
  output logic [15:0]        worst_delay_us,
  output logic               coalesce_stalls        // count with no timer
);

  logic [15:0] pending;
  logic [15:0] age_us;

  // 18.1 section 11's misconfiguration, restated: a count threshold
  // above one with no timer holds frames until traffic resumes.
  assign coalesce_stalls = (cfg_count > 16'd1) && (cfg_timer_us == 16'd0);

  wire by_count  = (pending >= cfg_count) && (cfg_count != 16'd0);
  wire by_timer  = (age_us >= cfg_timer_us) && (cfg_timer_us != 16'd0) &&
                   (pending != 16'd0);

  // The bypass. A PTP event message is small, isolated and low-rate,
  // so it waits the FULL timer every time -- 16.1's callout. Letting
  // it through immediately costs one interrupt per PTP message, which
  // at 16.2's rates is tens per second.
  wire bypass = frame_released &&
                (frame_is_urgent || (cfg_ptp_bypass && frame_is_ptp));

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      pending <= '0; age_us <= '0; irq <= 1'b0;
      c_irq_total <= '0; c_irq_by_count <= '0; c_irq_by_timer <= '0;
      c_irq_bypass <= '0; c_frames_signalled <= '0;
      c_delay_us_accum <= '0; worst_delay_us <= '0;
    end else begin
      if (frame_released && !bypass) pending <= pending + 16'd1;
      if (tick_1us && (pending != 16'd0)) age_us <= age_us + 16'd1;

      if (bypass) begin
        irq          <= 1'b1;
        c_irq_bypass <= c_irq_bypass + 1;
        if (!irq) c_irq_total <= c_irq_total + 1;
        c_frames_signalled <= c_frames_signalled + 1;
      end else if (by_count || by_timer) begin
        irq <= 1'b1;
        if (!irq) c_irq_total <= c_irq_total + 1;
        if (by_count) c_irq_by_count <= c_irq_by_count + 1;
        else          c_irq_by_timer <= c_irq_by_timer + 1;

        c_frames_signalled <= c_frames_signalled + {16'b0, pending};
        c_delay_us_accum   <= c_delay_us_accum + {16'b0, age_us};
        if (age_us > worst_delay_us) worst_delay_us <= age_us;

        pending <= '0;
        age_us  <= '0;
      end

      if (irq_ack) irq <= 1'b0;
    end
  end

endmodule

Classification: a two-term coalescer with a content-based bypass and explicit delay accounting.

What it teaches: that the bypass is what resolves Chapter 16.1 §8's callout, and it resolves it by separation rather than by compromise. That callout observed that tuning coalescing down to help a timestamp costs exactly the throughput coalescing was added to gain, and that the two goals are irreconcilable at that layer. They are reconcilable one layer down: a frame identified as PTP — Chapter 16.2's EtherType, or a UDP port — bypasses the coalescer entirely, and at that traffic's rate of tens of frames per second the extra interrupts are free.

And it teaches that c_delay_us_accum and worst_delay_us are the coalescer's real output. The interrupt count says how much CPU was saved. These two say what it cost, in the units Section 15 needs: mean delay is the accumulator divided by the interrupt count, and the worst case is recorded directly. A coalescer that reports only its interrupt count is reporting the benefit and hiding the price.

Deliberately simplified: frame_is_ptp arrives as an input, and deciding it requires parsing — the EtherType for a layer-2 PTP message, or a UDP destination port of 319 for a layer-3 one, which is Chapter 19.2's work and not free at 100 Gb/s. There is one coalescer for the whole port where Chapter 18.7's multi-queue needs one per queue. And age_us counts only while frames are pending, so a frame arriving immediately after a flush starts its timer at zero — which is correct and means the worst delay is the full timer rather than twice it.

Production implication: c_irq_bypass should be small and non-zero on a port carrying PTP. Zero means the bypass is not configured or the parser is not identifying the traffic, so every PTP message is waiting the full timer — Section 15's 100 µs, against a 24.2 ns clock. Large means something other than PTP is matching, which is its own bug and turns the coalescer off for a substantial fraction of traffic. The counter is the only way to tell the three cases apart, and the distinction decides whether the port's time synchronisation works.


15. Pricing Chapter 16.1's Second Unpriced Term: Coalescing

Chapter 16.1 §8's third row — "interrupt is asserted — coalescing" — pointed at Chapter 18.6 for the mechanism and here for the number. The number is the largest in Module 16 or 18.

The delay a frame experiences is bounded by the timer and is zero in a burst.

Coalescing timerIsolated frame waitsIn a burstJitter, peak to peak
10 µs10 µs≈010 µs
50 µs50 µs≈050 µs
100 µs100 µs≈0100 µs
200 µs200 µs≈0200 µs

Now the comparison that settles Chapter 16.1's argument:

TermMagnitudeAgainst 24.2 ns
Chapter 16.5's achieved accuracy24.2 ns
DMA arbitration, 8 masters700 ns28.9×
coalescing, 100 µs timer100 000 ns4 132×

Four thousand times. Module 16 spent five chapters getting a hardware clock to 24.2 ns — transparent clocks, a PI servo, asymmetry calibration, granularity analysisand reading that clock through a coalesced interrupt multiplies its error by 4 132.

And the structure of the delay is worse than its magnitude, which is Chapter 16.1 §8's callout restated with numbers. A PTP event message is small, isolated and low-rate, so it is the second row of Section 14's arrival table: it waits the full timer, every time — except when it happens to land inside a burst of other traffic, when it waits nothing.

ArrivalDelayHow often
isolated100 µsusually
inside a burst≈0occasionally

A delay that is usually one value and occasionally another is the worst possible structure for a measurement, because it is neither a constant a calibration can remove nor a random variable an average can suppress. Chapter 16.4's servo sees a bimodal offset error and, being a filter, tracks somewhere between the two modes — which is wrong in both cases.

The three responses, and only one of them is a real fix:

ResponseResult
turn coalescing downcosts the throughput it was added for — Chapter 18.1 §10's 297.6% of a CPU
Section 14's PTP bypassremoves it for PTP traffic, keeps it for the rest
Chapter 16.3's hardware timestampremoves it entirely, for everyone

Row three is the answer and Module 16 already gave it. What this chapter adds is the magnitude of what row three avoids: not "software timestamps are worse", but 4 132 times worse, which is the difference between a number worth arguing about and a number that ends the argument.

Row two is worth keeping anyway, because a system that hardware-timestamps its PTP messages still delivers them to software through an interrupt, and a 100 µs delay in delivering a correct timestamp delays the servo's correction by 100 µs. That is a much smaller problem than a wrong timestamp — it affects the loop's response time rather than its accuracy — but it is not nothing, and the bypass costs tens of interrupts per second.


16. RTL 8 — The Receive DMA Conformance Monitor

The last block, and its verdicts map one to one onto the failures Section 2 enumerated.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// rxdma_conformance_monitor -- what the receive path's counters mean.
//
// Every verdict names a different owner. That is the selection rule:
// a verdict that does not change who fixes it is not worth a bit.
// -----------------------------------------------------------------------
module rxdma_conformance_monitor
  import rxdma_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic [31:0] c_frames_read,
  input  logic [31:0] c_backpressure_cycles,
  input  logic [31:0] c_urgent_fetches,
  input  logic [31:0] c_batches,
  input  logic [31:0] c_page_splits,
  input  logic [31:0] c_buffer_splits,
  input  logic [31:0] c_chains,
  input  logic [31:0] c_overflows,
  input  logic [31:0] c_write_errors,
  input  logic [31:0] c_stall_no_beat,
  input  logic [31:0] c_bursts,
  input  logic [15:0] c_peak_open,
  input  logic [15:0] mean_batch_x100,
  input  logic [31:0] c_irq_by_timer,
  input  logic [31:0] c_irq_by_count,
  input  logic [31:0] c_irq_bypass,
  input  logic [15:0] worst_delay_us,

  input  logic        released_before_data,
  input  logic        tag_reused_while_open,
  input  logic        chain_overflow,
  input  logic        coalesce_stalls,

  output logic        rx_path_ok,
  output logic        ordering_fault,
  output logic        alignment_poor,
  output logic        buffers_too_small,
  output logic        in_flight_too_small,
  output logic        batching_ineffective,
  output logic        interrupt_mistuned,
  output logic        ptp_bypass_absent,
  output logic        none_of_the_above
);

  // Fatal, and never a configuration problem.
  assign ordering_fault = released_before_data | tag_reused_while_open;

  // The driver's allocator: more than a sixteenth of bursts split by
  // a page boundary means buffers are not aligned. Section 6's 37%
  // on maximum frames is what unaligned looks like.
  assign alignment_poor = (c_bursts > 32'd1000) &&
                          (c_page_splits > (c_bursts >> 4));

  // The driver's buffer SIZE: chains on ordinary traffic.
  assign buffers_too_small = (c_chains > 32'd1000) &&
                             (c_buffer_splits > c_chains);

  // Section 10's table: if it is saturated, frames are waiting for a
  // tag rather than for memory.
  assign in_flight_too_small = (c_peak_open >= 16'(IN_FLIGHT));

  // 18.1 section 12's arithmetic assumed 8. Below 4 the transaction
  // rate does not fit.
  assign batching_ineffective = (c_batches > 32'd1000) &&
                                (mean_batch_x100 < 16'd400);

  // Almost all interrupts by timer: the count is never reached.
  assign interrupt_mistuned = coalesce_stalls |
                              ((c_irq_by_timer > (c_irq_by_count << 3)) &&
                               (c_irq_by_timer > 32'd100));

  // Section 14: a port carrying PTP with no bypass hits section 15's
  // 4132x term on every event message.
  assign ptp_bypass_absent = (c_irq_bypass == '0) &&
                             (c_irq_by_timer > 32'd1000);

  assign rx_path_ok = !ordering_fault && (c_write_errors == '0) &&
                      (c_overflows == '0);

  assign none_of_the_above = rx_path_ok && !alignment_poor &&
                             !buffers_too_small && !in_flight_too_small &&
                             !batching_ineffective && !interrupt_mistuned;

  // ---- properties -------------------------------------------------

  p_ordering_fault_is_fatal:
    assert property (@(posedge clk) disable iff (!rst_n)
      ordering_fault |-> !rx_path_ok)
    else $error("rx_path_ok asserted with an ordering fault present");

  p_release_never_before_data:
    assert property (@(posedge clk) disable iff (!rst_n)
      !released_before_data)
    else $error("a frame was released before its data was visible");

  p_bursts_at_least_chains:
    assert property (@(posedge clk) disable iff (!rst_n)
      (c_chains != '0) |-> (c_bursts >= c_chains))
    else $error("fewer bursts than frames -- a frame moved no data");

  p_worst_delay_bounded:
    assert property (@(posedge clk) disable iff (!rst_n)
      (worst_delay_us != 16'hFFFF))
    else $error("the coalescing delay counter saturated");

  p_verdicts_exclusive_with_clear:
    assert property (@(posedge clk) disable iff (!rst_n)
      none_of_the_above |-> (!ordering_fault && !alignment_poor &&
                             !buffers_too_small && !batching_ineffective))
    else $error("none_of_the_above asserted alongside a finding");

endmodule

Classification: a verdict generator whose every output names a different owner.

What it teaches: that alignment_poor and buffers_too_small are both "the driver" and are not the same finding. Alignment is the allocator's base address and costs nothing to fix. Buffer size costs memory — Section 6's table: 2 KiB buffers waste 26% on a maximum frame — and the two are fixed in different places by different code. A monitor that reports "the driver's buffers are wrong" sends an engineer to read the wrong function.

And it teaches that ptp_bypass_absent is a verdict about a missing feature rather than a broken one, which is unusual and is why it needs a bit of its own. Every other verdict here says something is going wrong; this one says something correct is happening — coalescing — to traffic that cannot afford it, and the evidence is an absence: c_irq_bypass at zero while the timer is firing. Section 15's 4 132× is what the absence costs, and nothing else in the counter set would reveal it.

Deliberately simplified: the thresholds are literals — a sixteenth, a factor of eight, a mean batch of 4 — where a production monitor takes them from registers. The counters are absolute rather than windowed, so a long-running port eventually trips several; a real monitor works on deltas. And in_flight_too_small triggers on the peak ever reached, which is sticky and cannot distinguish a single transient from chronic saturation.

Production implication: none_of_the_above for the third and last time in this module. Chapter 18.1 §15, Chapter 18.2 §16 and this block each provide one, and together they cover the MAC's interfaces, its ring protocol and its receive DMA. A port where all three assert is a port whose hardware and driver-facing contracts are clean — which removes roughly fifteen hypotheses in one register read, and is the only evidence that will send an investigation past the network driver to the application.


17. What the Receive Path Costs in Transactions

Chapter 18.1 §12 posed the problem, Chapter 18.2 §18 gave the answer for descriptors, and this section closes it with the data path included.

Per frame, with fetches and writebacks batched by 8 and the status combined with ownership:

Component64-octet frame1518-octet9000-octet, 2 KiB buffers
descriptor fetch0.1250.1250.625
data bursts129
descriptor writeback0.1250.1250.625
total1.2502.25010.250

The 64-octet column is the one that binds because it has the highest frame rate:

Line rateFrame rateTransactions/sPer cycle at 250 MHz
1 Gb/s1.488 Mfps1.86 M0.007
10 Gb/s14.881 Mfps18.60 M0.074
25 Gb/s37.202 Mfps46.50 M0.186
100 Gb/s148.810 Mfps186.01 M0.744

0.744 per cycle, which fits, and 26% of the address channel is left. Chapter 18.1 §12 opened at 1.786 and had no answer; three chapters of mechanism have closed it.

But the 1518-octet column is worth a second look, because its total is higher and its rate is lower:

Frame sizeTransactions/frameFrame rate at 100 Gb/sTransactions/sPer cycle
641.250148.810 M186.01 M0.744
5121.25023.496 M29.37 M0.117
15182.2508.127 M18.29 M0.073
900010.2501.386 M14.20 M0.057

The transaction rate falls monotonically with frame size — by a factor of ten from minimum to maximum frames — so the minimum-frame case is the design point and every other case is comfortable. Which is the same conclusion Chapter 18.1 §4 reached about bandwidth and §10 about interrupts: three independent resources, all bound by the same traffic pattern, and it is the one an attacker or an unlucky application chooses.

And the bandwidth side, for completeness, is unchanged from Chapter 18.1 §4 — batching moves transactions, not octets:

BandwidthTransactions
unbatched114.29 Gb/s1.786/cycle
batched by 8114.29 Gb/s0.744/cycle

Identical bandwidth, less than half the transactions, which is Chapter 18.2's callout about payload-to-transaction ratio demonstrated end to end.


Chapter 18.1 section 12 opened Module 18 with a wall: a 100 gigabit MAC receiving minimum-size frames offers three bus transactions per frame at 148.81 million frames per second, which is 446 million transactions per second, or 1.786 per cycle at 250 megahertz, and a single AXI address channel issues at most one per cycle. Three chapters of mechanism close it. Chapter 18.2's descriptor ring makes the descriptors fetchable in batches, because a ring's entries are at computable addresses while a linked list's are not; batching eight fetches into one transaction takes the figure to 2.125 per frame, or 1.265 per cycle — still above one. This chapter's descriptor writeback combines the status and ownership fields into a single write, legal because both are gated on the frame's data becoming visible rather than on each other, and batches those by eight as well: 1.250 transactions per frame, 186 million per second, 0.744 per cycle. That fits, with 26 percent of the address channel left over for every other master on the chip. The bandwidth is 114.29 gigabits per second in all three cases — batching moves transactions, not octets.18.1's wall1.786 per cycle —impossible18.2: a ringcomputable addressesBatch 8 fetches2.125/frame — 1.265/cycle18.3: combinestatus+ownone descriptor writeBatch 8 writebacks1.250/frame — 0.744/cycleIt fits26% of the channel leftBandwidth unchanged114.29 Gb/s throughout12
Figure 4 — three chapters of mechanism, and the wall that opened Module 18 finally under one.

18. The Cost, Accounted

Eight blocks, and the distribution is different again from the two chapters before it.

BlockApproximate costDominated by
rx_fifo_reader~180 flopscounters
descriptor_fetcher~200 flopscounters
scatter_gather_walker~350 flops + three 16-bit comparatorsthe three-way minimum
rx_write_engine~700 flops + 32 × 2 burst countersthe per-tag accounting
rx_ordering_barrier~350 flops + a 32-way priority selectorthe ready computation
status_writeback~200 flopsthe batch state
rx_interrupt_coalescer~250 flopscounters
rxdma_conformance_monitor~120 flopscomparators

About 2350 flops, which is less than either of the two preceding chaptersChapter 18.1's 3300 and Chapter 18.2's 4800 — and the reason is that this chapter's expensive structure, the in-flight table, was built in Chapter 18.2 §9 and is reused.

The combinational cost is where this chapter is expensive:

StructureCostWhy it is hard
the walker's three-way minimumthree 16-bit compares and a muxin the burst-issue path
the barrier's ready_v32 × 4 AND termsevaluated every cycle
the barrier's priority selectora 32-way encoderfive levels
w_strb generation64 comparatorstrivially parallel

Rows two and three together are the chapter's timing risk, and at 400 MHz they need pipelining — which introduces a cycle of latency between a frame becoming ready and its ownership being written, harmless here because the quantity being waited for is already hundreds of nanoseconds old.

Put the module's three chapters together, for a 100 Gb/s port:

ChapterLogicMemoryContribution
Chapter 18.1~3 300 flops~187 KiB SRAMthe interfaces and the FIFO
Chapter 18.2~4 800 flops512 KiB DRAMthe ring and the ordering
this chapter~2 350 flopsthe buffers themselvesthe data path
total~10 450 flops187 KiB on-chip + rings + buffers

Ten thousand flops is a small block by any modern measure and the SRAM is not:

SizeOf a 4 MiB on-chip budget
logicnegligible
receive FIFO, lossless to 2 km187 KiB4.6%
both directions~374 KiB9.1%

Which is Chapter 18.1 §18's conclusion unchanged: the module's cost is memory, not logic, and the memory is dominated by a flow-control headroom set by a cable length.

And the receive buffers themselves, which are the largest number in the module and belong to software:

Ring depth2 KiB buffers4 KiB buffers
2560.50 MiB1.00 MiB
10242.00 MiB4.00 MiB
40968.00 MiB16.00 MiB

Eight megabytes of receive buffers for one 100 Gb/s port, which dwarfs the 512 KiB of rings and the 187 KiB of FIFO put together — and which is the number an operating system's driver actually allocates, and the one nobody counts as the MAC's cost because it is a malloc rather than a floorplan entry.


19. Properties Worth Asserting, and One Worth Refusing

The receive path's properties fall into four groups, and the rejected one is about an event this chapter has been careful to distinguish throughout.

FIFO reader and descriptor fetcher.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A beat is only taken when the engine will accept it.
p_beat_only_when_ready:
  assert property (@(posedge clk) disable iff (!rst_n)
    fifo_rd |-> beat_ready)
  else $error("the FIFO was read with nowhere to put the beat");

// Every frame that starts in the reader ends.
p_reader_frame_completes:
  assert property (@(posedge clk) disable iff (!rst_n)
    (fifo_rd && fifo_sof) |-> ##[1:$] (fifo_rd && fifo_eof))
  else $error("a frame started in the reader and never ended");

// The early-start request never precedes any data.
p_early_start_needs_data:
  assert property (@(posedge clk) disable iff (!rst_n)
    request_descriptor |-> (fifo_occupancy_b != '0))
  else $error("a descriptor was requested with an empty FIFO");

// A batch never runs past the end of the ring.
p_batch_within_ring:
  assert property (@(posedge clk) disable iff (!rst_n)
    (rd_valid && rd_ready) |->
      ((rd_addr + rd_bytes) <= (ring_base + (ring_len * DESC_B))))
  else $error("a descriptor batch ran off the end of the ring");

// Only one batch is outstanding.
p_one_batch_outstanding:
  assert property (@(posedge clk) disable iff (!rst_n)
    fetch_in_flight |-> !rd_valid)
  else $error("a second batch was issued while one was in flight");

// An urgent fetch only happens with an empty window.
p_urgent_means_empty:
  assert property (@(posedge clk) disable iff (!rst_n)
    urgent |-> (window_level == '0))
  else $error("an urgent fetch was issued with descriptors available");

Scatter-gather walker.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// No element crosses a 4 KiB boundary.
p_element_within_page:
  assert property (@(posedge clk) disable iff (!rst_n)
    (sg_valid && sg_ready) |->
      ((sg_elem.addr[11:0] + sg_elem.bytes) <= 13'h1000))
  else $error("a scatter-gather element crossed a page boundary");

// No element exceeds the burst limit.
p_element_within_burst:
  assert property (@(posedge clk) disable iff (!rst_n)
    (sg_valid && sg_ready) |-> (sg_elem.bytes <= (MAX_BEATS * BUS_BYTES)))
  else $error("an element exceeded the maximum burst size");

// No element exceeds its descriptor's buffer.
p_element_within_buffer:
  assert property (@(posedge clk) disable iff (!rst_n)
    (sg_valid && sg_ready) |-> (sg_elem.bytes <= cur_remaining))
  else $error("an element ran past the end of its buffer");

// Exactly one element per frame is marked first.
p_one_first_per_frame:
  assert property (@(posedge clk) disable iff (!rst_n)
    (sg_valid && sg_ready && sg_elem.first) |=>
      !(sg_valid && sg_ready && sg_elem.first) throughout start[->1])
  else $error("two elements in one frame were marked first");

// A descriptor is only taken when the previous one is exhausted.
p_take_when_exhausted:
  assert property (@(posedge clk) disable iff (!rst_n)
    desc_take |-> (cur_remaining == '0))
  else $error("a descriptor was taken with bytes remaining in the last one");

// The chain never exceeds MAX_SG without flagging.
p_chain_bounded:
  assert property (@(posedge clk) disable iff (!rst_n)
    (sg_count > MAX_SG) |-> chain_overflow)
  else $error("a chain exceeded MAX_SG without a flag");

Write engine and ordering.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Data beats only flow inside an open burst.
p_data_inside_burst:
  assert property (@(posedge clk) disable iff (!rst_n)
    (w_valid && w_ready) |-> in_burst)
  else $error("a data beat was issued outside a burst");

// The last beat of a burst is marked.
p_last_beat_marked:
  assert property (@(posedge clk) disable iff (!rst_n)
    (w_valid && w_ready && (burst_bytes_left <= BUS_BYTES)) |-> w_last)
  else $error("the final beat of a burst was not marked last");

// Strobes never exceed the beat's byte count.
p_strobes_match_bytes:
  assert property (@(posedge clk) disable iff (!rst_n)
    (w_valid && w_ready) |-> ($countones(w_strb) == beat_bytes))
  else $error("the write strobes did not match the beat's byte count");

// Responses never exceed issues, per tag.
p_done_le_issued:
  assert property (@(posedge clk) disable iff (!rst_n)
    bursts_done[0] <= bursts_issued[0])
  else $error("more burst responses than bursts issued");

// The invariant. A release implies every burst responded and sealed.
p_release_requires_visibility:
  assert property (@(posedge clk) disable iff (!rst_n)
    (own_valid && own_ready) |->
      (sealed[own_tag] && (bursts_done[own_tag] == bursts_issued[own_tag])))
  else $error("a frame was released before its data was visible");

// The invariant signal never asserts.
p_never_released_early:
  assert property (@(posedge clk) disable iff (!rst_n)
    !released_before_data)
  else $error("released_before_data asserted");

// A tag is not reused while its frame is open.
p_no_tag_reuse:
  assert property (@(posedge clk) disable iff (!rst_n)
    !tag_reused_while_open)
  else $error("a tag was reused while its frame was still open");

// Every opened frame is eventually released.
p_frame_eventually_released:
  assert property (@(posedge clk) disable iff (!rst_n)
    frame_start |-> ##[1:$] own_done)
  else $error("a frame was opened and never released");

Writeback and interrupt.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A batch covers contiguous descriptors only.
p_batch_contiguous:
  assert property (@(posedge clk) disable iff (!rst_n)
    (rel_valid && rel_ready && can_join) |-> (rel_index == batch_next))
  else $error("a non-contiguous descriptor joined a batch");

// A batch never exceeds its maximum.
p_batch_bounded:
  assert property (@(posedge clk) disable iff (!rst_n)
    batch_n <= MAX_BATCH)
  else $error("the writeback batch exceeded MAX_BATCH");

// A held batch is eventually written, if a timeout is configured.
p_batch_eventually_flushed:
  assert property (@(posedge clk) disable iff (!rst_n)
    (have && (cfg_batch_timeout != '0)) |-> ##[1:$] (wr_valid && wr_ready))
  else $error("a batch was held with a timeout configured");

// Nothing is written back before its frame was released.
p_writeback_after_release:
  assert property (@(posedge clk) disable iff (!rst_n)
    (wr_valid && wr_ready) |-> have)
  else $error("a writeback was issued with no released frame held");

// An urgent or PTP frame bypasses coalescing.
p_bypass_is_immediate:
  assert property (@(posedge clk) disable iff (!rst_n)
    bypass |=> irq)
  else $error("a bypassed frame did not raise an immediate interrupt");

// The pending count is cleared when the interrupt fires.
p_pending_cleared:
  assert property (@(posedge clk) disable iff (!rst_n)
    (by_count || by_timer) |=> (pending == '0))
  else $error("the pending count survived an interrupt");

// Frames signalled never exceed frames released.
p_signalled_le_released:
  assert property (@(posedge clk) disable iff (!rst_n)
    c_frames_signalled <= c_released)
  else $error("more frames were signalled than were released");

// The worst delay never exceeds the configured timer.
p_delay_within_timer:
  assert property (@(posedge clk) disable iff (!rst_n)
    (cfg_timer_us != '0) |-> (worst_delay_us <= cfg_timer_us))
  else $error("a frame waited longer than the coalescing timer");

20. Verification Scenarios

Fifty-eight scenarios, plus a six-run directed test that constructs a correlation random stimulus will not.

FIFO reader and descriptor supply — 9 scenarios.

#ScenarioExpected
1a minimum frame, engine readystreamed, c_frames_read = 1
2the engine stalls mid-framecannot_keep_up, backpressure counted
3the FIFO empties mid-framec_stall_no_beat rises
4early start at 128 octetsdescriptor requested before the frame ends
5window empty, a frame arrivingurgent fetch, c_urgent_fetches rises
6window at the low-water marka batch fetched, not urgent
7a batch starting 3 entries from the ring's endshortened to 3, c_short_batches
8a fetch returns an errorcounted; the window stays short
9back-to-back frames at minimum IFGboth streamed, no loss

Scatter-gather — 10 scenarios.

#ScenarioExpected
1064-octet frame, 2 KiB buffer, aligned1 element, 1 descriptor
111518-octet frame, 2 KiB buffer, aligned2 elements, 1 descriptor
121518-octet frame starting 3 KiB into a pagean extra element, c_page_splits
139000-octet frame, 2 KiB buffers5 descriptors, 9 elements
149000-octet frame, 4 KiB buffers3 descriptors, 9 elements
159000-octet frame, 256-octet buffers36 descriptors — chain_overflow at MAX_SG 8
16a buffer ending exactly on a page boundaryno split
17a descriptor with buf_len 0no element; the next descriptor is taken
18frame ends mid-bufferthe last element is short
19the window runs dry mid-chainthe walker stalls, no corruption

Write engine and ordering — 12 scenarios.

#ScenarioExpected
20one burst, response before the next frameframe_data_visible after the response
21two bursts, responses out of ordervisible only after both
22a frame's counters at 0 == 0 before its first burstNOT visible — the seal is low
23sealed, all respondedvisible; ownership released
24sealed, one response outstandingnot released; c_wait_cycles rises
25a burst returns an errorcounted; the frame is still released with status
2632 frames openc_peak_open = 32; a 33rd stalls
27a tag reused while opentag_reused_while_open
28the final beat is 46 octetsw_strb has 46 bits set
29a beat is exactly the bus widthall strobes set, w_last if it is the end
30responses arrive for two frames interleavedeach frame's count advances independently
31a frame is released before a responsereleased_before_data — must never happen

Writeback and chains — 9 scenarios.

#ScenarioExpected
328 contiguous releasesone writeback of 128 octets
33a release out of index orderthe batch flushes, c_broken_by_order
34a release at ring index 0the batch flushes, c_broken_by_wrap
353 releases then silence, timer setflushed by timeout
363 releases then silence, timer 0held — the failure the timer prevents
37batching disabledone write per release
38a 5-descriptor chainreleased 4, 3, 2, 1, 0 in that order
39the chain's descriptor 0 released firsta protocol violation — must not occur
40a chain whose last buffer errorsthe whole chain reports the error

Coalescing — 9 scenarios.

#ScenarioExpected
41count 1an interrupt per frame
42count 32, 31 frames, timer 100 µsinterrupt at 100 µs, c_irq_by_timer
43count 32, 32 framesimmediate, c_irq_by_count
44count 32, timer 0, 31 framescoalesce_stalls; frames held
45a PTP frame, bypass enabledimmediate, c_irq_bypass rises
46a PTP frame, bypass disabledwaits the full timer
47an error frameimmediate regardless of bypass config
48worst_delay_us after a 100 µs wait100
49a burst of 32 PTP frames32 immediate interrupts — the bypass's cost

Verdicts — 9 scenarios.

#ScenarioExpected
501000 bursts, 100 page splitsalignment_poor
511000 bursts, 30 page splitsnot alignment_poor
521000 chains, 1500 buffer splitsbuffers_too_small
53c_peak_open = 32in_flight_too_small
54mean batch 1.3batching_ineffective
55mean batch 7.8not batching_ineffective
56PTP traffic, no bypass, timer firingptp_bypass_absent
57released_before_data pulsesordering_fault, not rx_path_ok
58everything cleannone_of_the_above

The directed test — a correlation random stimulus will not produce.

The failure this test exists for needs three things at once: a frame that scatters across several buffers, a memory system that responds to those buffers' bursts out of order, and a response pattern that makes bursts_done momentarily equal bursts_issued before the frame is sealed.

Random stimulus produces the first easily and the second only if the memory model reorders responses — most do not. The third is the conjunction that will not happen by chance: it requires the responses to catch up with the issues at a specific moment, which on a 5-descriptor chain with 9 bursts is a narrow window.

Construct it. Six runs, one variable: where the response gap falls.

RunFrameResponse patternSeal timingExpected
A64 octets, 1 burstimmediatebefore the responsereleased after the response
B64 octets, 1 burstdelayed 300 nsbefore the responsereleased after 300 ns
C9000 octets, 9 burstsall immediateafter burst 9 issuesreleased once — never early
D9000 octets, 9 burstsbursts 1–8 respond, then a 500 ns gap, then 9after burst 9 issuesNOT released during the gap
E9000 octets, 9 burstsas DSEAL DISABLEDreleased during the gap — the bug
F9000 octets, 9 burstsas Dseal enabledcorrect — the fix demonstrated

Run E is the point and it is the only way to demonstrate the seal is load-bearing. With the seal removed, bursts_done == bursts_issued holds during the 500 ns gap after burst 8 responds and before burst 9 is issued — so the frame is released with one ninth of its data missing. Software reads a jumbo frame whose last 808 octets are the previous frame's.

And run C shows why the bug hides. With all responses immediate the gap never opens, so a fast memory model never produces it — and a testbench with a zero-latency memory passes run E as readily as run F. The memory model must have a variable response latency, not merely a non-zero one.

The oracle, in four parts:

CheckRuns A–D, FRun E
released_before_datanever assertsasserts during the gap
buffer contents at releasecompletelast buffer stale
software's frame checkpassespasses — the FCS was checked in the MAC
a payload comparison against the scoreboardmatcheslast 808 octets wrong

Row three is the reason this test is necessary and cannot be replaced by an end-to-end check. The frame's FCS was verified in the MAC, before the data ever reached memory — so a corrupted receive-DMA path produces a frame that passes every integrity check software has, and only a byte-for-byte comparison against what was transmitted finds it. A scoreboard that checks lengths and status will report run E as passing.

Row four is therefore the mandatory check, and it is the one an integration testbench most often omits because it is expensive.


21. Debugging the Receive Path

Four complaints, and this chapter's counters resolve all four. Two of them are complaints Chapter 18.1 and Chapter 18.2 could not finish resolving.

Complaint 1 — "frames arrive corrupted, but the FCS passes at the switch."

CheckIf yesMeaning
released_before_data ever asserted?the barrier is brokenan RTL bug — fatal
is the corruption always at the frame's END?a missing burst responsethe same
only on frames above one buffer's size?a chain bugSection 11
the frame's FCS checked at the MAC?it was — before memoryno downstream check helps

Row four is the reason this complaint is so hard. Chapter 6.3's residue check ran in the MAC, on the wire data, long before the DMA touched it — so a receive-DMA corruption produces a frame that every software check accepts. Only a comparison against what was transmitted finds it, which is why Section 20's run E needs a payload scoreboard.

Complaint 2 — "throughput is 60% of line rate."

CheckIf yesMeaning
c_peak_open at IN_FLIGHT?frames are waiting for a tagSection 10 — raise it
mean_batch_x100 below 400?batching is not workingSection 12 — order or sparsity
c_page_splits above a sixteenth of bursts?buffers are not alignedthe driver's allocator
c_urgent_fetches near c_batches?the window is chronically emptyraise the low-water mark
c_stall_no_beat dominant?the bus is waiting for the wirenot a problem

Row five is the one that looks like a fault and is not, and it is worth calling out because it is the most common false lead: a 512-bit bus on a 1 Gb/s port spends most of its time waiting for data, and the counter will be enormous. It means the bus is over-provisioned, which is fine.

Complaint 3 — "PTP synchronisation is worse than the hardware timestamps suggest."

CheckIf yesMeaning
c_irq_bypass zero with PTP traffic present?every event message waits the full timerSection 15 — 4 132× the clock
worst_delay_us equal to the timer?confirmedthe same
the servo's offset bimodal?isolated versus in-burst arrivalsSection 15's structure
hardware timestamps enabled?then only the delivery is latethe loop's response, not its accuracy

Row four is the distinction that decides how serious this is. With Chapter 16.3's hardware timestamping, the value is correct and arrives late — a slower servo. Without it, the value itself is the arrival time, and Section 15's 100 µs is the error. The same symptom, a factor of four thousand apart in consequence.

Complaint 4 — "large frames do not work."

CheckIf yesMeaning
c_overflows from the walker non-zero?MAX_SG exceededSection 7 — buffer size × MAX_SG
c_buffer_splits highchains on every framebuffers are small
truncation status set?buf_len below the framethe driver's configuration
only above a specific size?compute MAX_SG × buffer sizethe exact limit

Row four gives the arithmetic that ends the investigation. A port with 2 KiB buffers and MAX_SG of 8 handles frames to 16 KiB; with 256-octet buffers it handles 2 KiBso jumbo frames fail at exactly 2049 octets, which is a much better bug report than "large frames do not work."

And the two systematically misattributed symptoms of this chapter:

SymptomInstinctThis chapter's cause
a frame whose tail is wrong, FCS valida MAC or PHY bugownership released before the last burst responded
poor PTP performance with hardware timestampsthe clock, the servo, the networka 100 µs coalescing delay on delivery

22. Misconceptions

Misconception 1 — "when the last burst is issued, the frame is in memory."

The wrong model: the master drove the address, the interconnect accepted it, there is nothing left to do.

What it costs: a frame released to software with its last burst still in flight — software reads a buffer whose tail is the previous frame's, and every integrity check it has passes, because the FCS was verified in the MAC before the data ever left.

The corrected model: acceptance is custody, not visibility. Only the write response says the data has reached a point where a reader will see it, and a frame of eighteen bursts has eighteen responses to wait for. Sections 8, 10 and 19.

Misconception 2 — "bursts_done == bursts_issued means the frame is complete."

The wrong model: count the issues, count the responses, release when they match.

What it costs: release at the start of every frame, when both counters are zero, and again at any moment mid-frame when responses transiently catch up. The first case fails immediately and loudly; the second fails on jumbo frames, intermittently.

The corrected model: the count comparison needs a second condition — that no further bursts are coming. The walker's seal provides it, and Section 20's run E exists to demonstrate that removing it corrupts the last 808 octets of a 9000-octet frame. Section 9.

Misconception 3 — "scatter-gather is for jumbo frames."

The wrong model: chains happen when a frame exceeds a buffer, so a port with 2 KiB buffers and 1518-octet frames never scatters.

What it costs: a write engine and a walker that handle one element per frame, meeting a 1518-octet frame that crosses a 4 KiB page boundary — 37.04% of them at random alignment — and issuing a burst that the interconnect rejects as illegal.

The corrected model: there are two independent reasons to split. A buffer boundary ends a descriptor; a page boundary ends only a burst, and the second happens on more than a third of maximum-size frames unless the driver aligns its buffers. The fix for one is bigger buffers; for the other it is alignment, and it is free. Section 6.

Misconception 4 — "a deeper ring or a bigger FIFO covers a slow driver."

The wrong model: frames are dropped when the ring runs dry, so more buffering helps.

What it costs: memory spent on a margin that does not exist. A 16 KiB FIFO buys 195 frames at every line rate — 1.31 µs at 100 Gb/s — which is 4.8% of what a 4096-entry ring already buys, and neither covers a scheduler tick.

The corrected model: buffering converts a rate mismatch into a time budget, and the driver's worst-case latency has no bound. The mechanisms that work are polling under load and Chapter 14.2's flow control; buffering is for hardware's bounded delays, not software's unbounded ones. Section 4.

Misconception 5 — "jumbo frames relieve the bus."

The wrong model: six times fewer frames means six times fewer transactions.

What it costs: a design change adopted for bus relief that measures 18%.

The corrected model: with 2 KiB buffers a 9000-octet frame is five descriptors, so the per-descriptor overhead returns once per buffer rather than once per frame: 0.0027 transactions per octet against a 1518-octet frame's 0.0033. Going from 2 KiB to 4 KiB buffers buys 26% — more than the frame size does. Jumbo frames pay off on the wire and in the interrupt rate, which is Chapter 8.3's and Chapter 18.1 §10's territory. Section 11.

Misconception 6 — "hardware timestamping fixes PTP, so the driver's coalescing does not matter."

The wrong model: the timestamp is captured at the SFD, so nothing downstream can affect it.

What it costs: a servo whose corrections arrive 100 µs late, which is a loop-response problem rather than an accuracy one — and on a port without hardware timestamping, an error of 100 µs against a 24.2 ns clock: 4 132×.

The corrected model: hardware timestamping makes the value correct and leaves the delivery coalesced. Section 14's bypass is the fix and it costs tens of interrupts per second, because PTP event messages are low-rate. And without hardware timestamping, the coalescing delay is not a delivery problem — it is the measurement. Section 15.


23. Interview Questions

Q1 — "When is a received frame safe for software to read?"

When every burst that carried it has responded, not when the last one was issued. Acceptance on a bus means the interconnect has custody; only the write response means the data has reached a point of visibility. A 1518-octet frame on a 512-bit bus with a 16-beat limit is two bursts; a 9000-octet frame into 2 KiB buffers is nine. All of their responses must be back before the ownership bit moves — and there is a second condition, that no further bursts are coming, because done == issued is trivially true before the first burst is issued.

Q2 — "A frame arrives with its last 800 octets wrong, and the FCS passed at the switch. Where do you look?"

At the receive DMA's ordering, not at the MAC or the PHY. Chapter 6.3's residue check ran in the MAC on the wire data, before the DMA touched it — so no software check can catch a DMA corruption. The signature is corruption at the frame's end and only on frames large enough to scatter, which points at an ownership write that overtook the final burst. The check is released_before_data, and the diagnosis needs a byte-for-byte payload scoreboard, because lengths and status will all be correct.

Q3 — "Why must a five-descriptor chain be released in reverse order?"

Because software walks the chain forwards from its head. The moment descriptor 0 is owned by the driver, software may follow the chain to descriptor 4 — so if 4 is still owned by the MAC, software walks into a descriptor mid-update. Releasing 4 first and 0 last means the chain is complete whenever its head is visible. And on top of that, every buffer's data must be visible before any descriptor is released, because software may read buffer 4's contents as soon as it has walked to it.

Q4 — "How much memory bandwidth and how many transactions does a 100 Gb/s receive path need?"

114.29 Gb/s of bandwidth and 186 million transactions per second, which is 0.744 per cycle at 250 MHz and fits. Unbatched it would be 446 M — 1.786 per cycle — which does not. The difference is batching descriptor fetches and writebacks by eight and combining the status and ownership fields into one descriptor write. The bandwidth is identical either way: batching moves transactions, not octets, and the transaction rate was always the harder wall.

Q5 — "Your PTP synchronisation is 100 µs off and the hardware timestamps are accurate to 24 ns. Explain."

Interrupt coalescing. A PTP event message is small, isolated and low-rate, so it is never part of a burst and always waits the full coalescing timer — 100 µs against a 24.2 ns clock, a factor of 4 132. If hardware timestamping is enabled, the value is correct and only its delivery is late, which slows the servo rather than biasing it. If it is not, the arrival time is the measurement and the 100 µs is the error. The fix is a coalescing bypass for PTP traffic, which costs tens of interrupts per second.

Q6 — "You are told large frames do not work. What do you compute?"

MAX_SG × buffer size, which is the largest frame the scatter-gather walker can place. With 2 KiB buffers and eight elements that is 16 KiB — jumbo frames are fine. With 256-octet buffers it is 2 KiB, so anything above 2048 octets overflows the chain. The counter is the walker's c_overflows, and the answer to give is not "large frames do not work" but "frames above 2048 octets overflow the scatter chain because the buffer pool is 256 octets" — which names the configuration to change.


24. Understanding Check


25. What's Next

Module 18 has three chapters left and this one has set up all three.

ChapterTakes from here
Chapter 18.4 — The Transmit DMA Paththe same ordering problem, mirrored: the driver publishes, the MAC consumes
Chapter 18.5 — Mastering Memory over AXISection 6's splits and Section 8's bursts, shaped properly
Chapter 18.6 — Interrupts, Coalescing and CompletionSection 15's 4 132×, made adaptive
Chapter 18.7 — Offload and Multi-QueueSection 17's 0.744 per cycle, parallelised

Chapter 18.4's mirror is not symmetric, which is the interesting part. On receive, the MAC decides when a frame is complete; on transmit, the driver does — and the driver's store ordering, which Chapter 18.2 §12 showed hardware cannot police, becomes the critical path rather than a corner case.

And two debts are now settled. Chapter 16.1 §8's table listed two stages as variable and unpriced. They are 700 ns and 100 000 ns, against a clock Module 16 worked five chapters to bring to 24.2 ns28.9× and 4 132×.

Which closes Module 16's argument rather than merely supporting it. Chapter 16.1 argued that a timestamp must be captured in hardware because everything after the capture point adds error. This chapter has now measured everything after the capture point, and the total is four thousand times the clock's own accuracy. The argument was never about software being imprecise; it was about a transport path with two terms in it that no amount of software care can remove — one of them an arbiter the MAC does not control, and the other a throughput optimisation working exactly as designed.

Continue learning

Standards & specifications

Governing standard
IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)

Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the Ethernet curriculum.