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 three — Chapter 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 ordered | What this chapter must order |
|---|---|
| one field write | up to 18 data bursts, plus the field write |
| one descriptor | a chain of five |
| one completion | a 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.
| Term | Magnitude | Against Chapter 16.5's 24.2 ns |
|---|---|---|
| DMA arbitration, 8 masters | 700 ns | 28.9× |
| interrupt coalescing, 100 µs timer | 100 000 ns | 4 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.
| # | Boundary | Clock domain | Variable? |
|---|---|---|---|
| 1 | PHY to MAC receive FIFO | recovered | no |
| 2 | FIFO to host domain | the crossing | slightly |
| 3 | descriptor fetched | host | yes — memory latency |
| 4 | data written to the buffer | host | yes — DMA arbitration |
| 5 | status written | host | yes |
| 6 | ownership transferred | host | yes — after 4 and 5 |
| 7 | interrupt asserted | host | yes — 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:
| Section | Builds |
|---|---|
| 3 | the FIFO reader — where backpressure stops being possible |
| 4 | buffer exhaustion, and what happens at each stage |
| 6, 7 | scatter-gather across buffers and pages |
| 8, 10 | the write engine and the enlarged ordering barrier |
| 11 | the jumbo chain, published in reverse |
| 13, 15 | the two jitter terms, priced |
| 17 | the 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.
| Step | Fails when | Symptom without instrumentation |
|---|---|---|
| FIFO fills | the memory system stalls | CRC errors — Chapter 18.1 §9 |
| no descriptor | the driver is behind | frames simply absent |
| buffer too small | buf_len below the frame | truncation, or a scatter chain |
| a burst errors | a bad address, a decode error | a partial frame written |
| ownership released early | an ordering bug | a frame read before it is written |
| the interrupt is late | coalescing | latency, and jitter |
| the interrupt never comes | a count threshold with no timer | frames 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.
| Stage | Typical duration at 100 Gb/s |
|---|---|
| a minimum frame arrives | 6.72 ns |
| the FIFO crossing | ~20 ns |
| a descriptor fetch (prefetched) | ~0 — it is already held |
| a data burst's completion | 100–800 ns |
| the ownership write's completion | 100–800 ns |
| the interrupt | 0–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 pipelining — Chapter 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.
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".
// -----------------------------------------------------------------------
// 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// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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 do — Chapter 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.
| Stage | What absorbs it | Capacity at 10 Gb/s | Then |
|---|---|---|---|
| 1 | the prefetch window | 8 descriptors | refetch — Section 5 |
| 2 | the receive FIFO | 16 KiB = 13.1 µs | fill |
| 3 | Chapter 14.2's flow control | if configured | the partner stops |
| 4 | nothing | — | drop |
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 rate | 16 KiB FIFO covers | At minimum frames, that is |
|---|---|---|
| 1 Gb/s | 131.1 µs | 195 frames |
| 10 Gb/s | 13.1 µs | 195 frames |
| 25 Gb/s | 5.2 µs | 195 frames |
| 100 Gb/s | 1.31 µs | 195 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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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 size | 64-octet frame | 1518-octet | 9000-octet jumbo |
|---|---|---|---|
| 256 | 1 | 6 | 36 |
| 512 | 1 | 3 | 18 |
| 2048 | 1 | 1 | 5 |
| 4096 | 1 | 1 | 3 |
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 size | P(crosses a 4 KiB boundary) | Mean extra bursts |
|---|---|---|
| 64 | 1.54% | 0.015 |
| 128 | 3.10% | 0.031 |
| 512 | 12.48% | 0.125 |
| 1518 | 37.04% | 0.370 |
| 9000 | 100% | 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 buffer | Reason 2 — page crossing | |
|---|---|---|
| produces | several descriptors | several bursts, one descriptor |
| visible to software | yes — a chain to reassemble | no |
| ordering requirement | all data before the FIRST descriptor's ownership | all bursts before the ownership |
| fixable by the driver | bigger buffers | alignment |
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.
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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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:
| Write | Count | Beats each |
|---|---|---|
| data bursts | 2 | 16, then 8 |
| status and length | 1 | 1 |
| ownership | 1 | 1 |
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.
| Strategy | With 2 writes | With 20 writes |
|---|---|---|
| one atomic write | works if the descriptor fits a beat | impossible — the data is 1518 octets |
| one shared AXI ID | works | works, and serialises the data |
| wait for the response | costs one round trip | costs 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.
| Condition | Provided by |
|---|---|
| every issued burst has responded | Section 8's counters |
| no further bursts will be issued | the walker's frame_end — a "sealed" flag |
| the status write has responded | its 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.
| Design | Descriptor writes per frame | Transactions per frame, batched by 8 |
|---|---|---|
| status and ownership separate | 2 | 1/8 + 1 + 1/8 + 1 = 2.250 |
| combined into one writeback | 1 | 1/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.
10. RTL 5 — The Receive Ordering Barrier
The conjunction from Section 9, per frame, for thirty-two frames at once.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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.
| Descriptor | Filled | Must be released |
|---|---|---|
| 0 — FIRST | first | LAST |
| 1 | second | fourth |
| 2 | third | third |
| 3 | fourth | second |
| 4 — LAST | last | first |
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, thenN−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:
| Frame | Buffers (2 KiB) | Data bursts | Total transactions | Per octet |
|---|---|---|---|---|
| 64 | 1 | 1 | 4 | 0.0625 |
| 1518 | 1 | 2 | 5 | 0.0033 |
| 9000 | 5 | 9 | 24 | 0.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.0033 — an 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:
| Frame | Buffers | Total transactions | Per octet | Against 2 KiB |
|---|---|---|---|---|
| 9000 | 5 × 2 KiB | 24 | 0.0027 | — |
| 9000 | 3 × 4 KiB | 18 | 0.0020 | 26% 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 CPU — Chapter 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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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 masters | Best | Worst | Jitter, peak to peak |
|---|---|---|---|
| 1 — the MAC alone | 100 ns | 100 ns | 0 ns |
| 2 | 100 ns | 200 ns | 100 ns |
| 4 | 100 ns | 400 ns | 300 ns |
| 8 | 100 ns | 800 ns | 700 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.
| Quantity | Value | Ratio to Chapter 16.5's 24.2 ns |
|---|---|---|
| Chapter 16.5's synchronisation accuracy | 24.2 ns | 1× |
| DMA arbitration jitter, 8 masters | 700 ns | 28.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:
| Qualification | Effect |
|---|---|
| round-robin is the friendly case | a priority arbiter starves a low-priority master for longer |
t is not constant | a competing master's 1 KiB burst is not 100 ns |
| DRAM refresh adds its own | a 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 can | Effect on the jitter |
|---|---|
| issue earlier | shifts the mean, not the spread |
| raise its arbitration priority | reduces it — at everyone else's expense |
| use more outstanding transactions | hides it for throughput, not for latency |
| capture the timestamp before this stage | removes 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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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 timer | Isolated frame waits | In a burst | Jitter, peak to peak |
|---|---|---|---|
| 10 µs | 10 µs | ≈0 | 10 µs |
| 50 µs | 50 µs | ≈0 | 50 µs |
| 100 µs | 100 µs | ≈0 | 100 µs |
| 200 µs | 200 µs | ≈0 | 200 µs |
Now the comparison that settles Chapter 16.1's argument:
| Term | Magnitude | Against 24.2 ns |
|---|---|---|
| Chapter 16.5's achieved accuracy | 24.2 ns | 1× |
| DMA arbitration, 8 masters | 700 ns | 28.9× |
| coalescing, 100 µs timer | 100 000 ns | 4 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 analysis — and 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.
| Arrival | Delay | How often |
|---|---|---|
| isolated | 100 µs | usually |
| inside a burst | ≈0 | occasionally |
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:
| Response | Result |
|---|---|
| turn coalescing down | costs the throughput it was added for — Chapter 18.1 §10's 297.6% of a CPU |
| Section 14's PTP bypass | removes it for PTP traffic, keeps it for the rest |
| Chapter 16.3's hardware timestamp | removes 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.
// -----------------------------------------------------------------------
// 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");
endmoduleClassification: 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:
| Component | 64-octet frame | 1518-octet | 9000-octet, 2 KiB buffers |
|---|---|---|---|
| descriptor fetch | 0.125 | 0.125 | 0.625 |
| data bursts | 1 | 2 | 9 |
| descriptor writeback | 0.125 | 0.125 | 0.625 |
| total | 1.250 | 2.250 | 10.250 |
The 64-octet column is the one that binds because it has the highest frame rate:
| Line rate | Frame rate | Transactions/s | Per cycle at 250 MHz |
|---|---|---|---|
| 1 Gb/s | 1.488 Mfps | 1.86 M | 0.007 |
| 10 Gb/s | 14.881 Mfps | 18.60 M | 0.074 |
| 25 Gb/s | 37.202 Mfps | 46.50 M | 0.186 |
| 100 Gb/s | 148.810 Mfps | 186.01 M | 0.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 size | Transactions/frame | Frame rate at 100 Gb/s | Transactions/s | Per cycle |
|---|---|---|---|---|
| 64 | 1.250 | 148.810 M | 186.01 M | 0.744 |
| 512 | 1.250 | 23.496 M | 29.37 M | 0.117 |
| 1518 | 2.250 | 8.127 M | 18.29 M | 0.073 |
| 9000 | 10.250 | 1.386 M | 14.20 M | 0.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:
| Bandwidth | Transactions | |
|---|---|---|
| unbatched | 114.29 Gb/s | 1.786/cycle |
| batched by 8 | 114.29 Gb/s | 0.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.
18. The Cost, Accounted
Eight blocks, and the distribution is different again from the two chapters before it.
| Block | Approximate cost | Dominated by |
|---|---|---|
rx_fifo_reader | ~180 flops | counters |
descriptor_fetcher | ~200 flops | counters |
scatter_gather_walker | ~350 flops + three 16-bit comparators | the three-way minimum |
rx_write_engine | ~700 flops + 32 × 2 burst counters | the per-tag accounting |
rx_ordering_barrier | ~350 flops + a 32-way priority selector | the ready computation |
status_writeback | ~200 flops | the batch state |
rx_interrupt_coalescer | ~250 flops | counters |
rxdma_conformance_monitor | ~120 flops | comparators |
About 2350 flops, which is less than either of the two preceding chapters — Chapter 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:
| Structure | Cost | Why it is hard |
|---|---|---|
| the walker's three-way minimum | three 16-bit compares and a mux | in the burst-issue path |
the barrier's ready_v | 32 × 4 AND terms | evaluated every cycle |
| the barrier's priority selector | a 32-way encoder | five levels |
w_strb generation | 64 comparators | trivially 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:
| Chapter | Logic | Memory | Contribution |
|---|---|---|---|
| Chapter 18.1 | ~3 300 flops | ~187 KiB SRAM | the interfaces and the FIFO |
| Chapter 18.2 | ~4 800 flops | 512 KiB DRAM | the ring and the ordering |
| this chapter | ~2 350 flops | the buffers themselves | the data path |
| total | ~10 450 flops | 187 KiB on-chip + rings + buffers | — |
Ten thousand flops is a small block by any modern measure and the SRAM is not:
| Size | Of a 4 MiB on-chip budget | |
|---|---|---|
| logic | negligible | — |
| receive FIFO, lossless to 2 km | 187 KiB | 4.6% |
| both directions | ~374 KiB | 9.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 depth | 2 KiB buffers | 4 KiB buffers |
|---|---|---|
| 256 | 0.50 MiB | 1.00 MiB |
| 1024 | 2.00 MiB | 4.00 MiB |
| 4096 | 8.00 MiB | 16.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.
// 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.
// 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.
// 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.
// 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.
| # | Scenario | Expected |
|---|---|---|
| 1 | a minimum frame, engine ready | streamed, c_frames_read = 1 |
| 2 | the engine stalls mid-frame | cannot_keep_up, backpressure counted |
| 3 | the FIFO empties mid-frame | c_stall_no_beat rises |
| 4 | early start at 128 octets | descriptor requested before the frame ends |
| 5 | window empty, a frame arriving | urgent fetch, c_urgent_fetches rises |
| 6 | window at the low-water mark | a batch fetched, not urgent |
| 7 | a batch starting 3 entries from the ring's end | shortened to 3, c_short_batches |
| 8 | a fetch returns an error | counted; the window stays short |
| 9 | back-to-back frames at minimum IFG | both streamed, no loss |
Scatter-gather — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 10 | 64-octet frame, 2 KiB buffer, aligned | 1 element, 1 descriptor |
| 11 | 1518-octet frame, 2 KiB buffer, aligned | 2 elements, 1 descriptor |
| 12 | 1518-octet frame starting 3 KiB into a page | an extra element, c_page_splits |
| 13 | 9000-octet frame, 2 KiB buffers | 5 descriptors, 9 elements |
| 14 | 9000-octet frame, 4 KiB buffers | 3 descriptors, 9 elements |
| 15 | 9000-octet frame, 256-octet buffers | 36 descriptors — chain_overflow at MAX_SG 8 |
| 16 | a buffer ending exactly on a page boundary | no split |
| 17 | a descriptor with buf_len 0 | no element; the next descriptor is taken |
| 18 | frame ends mid-buffer | the last element is short |
| 19 | the window runs dry mid-chain | the walker stalls, no corruption |
Write engine and ordering — 12 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 20 | one burst, response before the next frame | frame_data_visible after the response |
| 21 | two bursts, responses out of order | visible only after both |
| 22 | a frame's counters at 0 == 0 before its first burst | NOT visible — the seal is low |
| 23 | sealed, all responded | visible; ownership released |
| 24 | sealed, one response outstanding | not released; c_wait_cycles rises |
| 25 | a burst returns an error | counted; the frame is still released with status |
| 26 | 32 frames open | c_peak_open = 32; a 33rd stalls |
| 27 | a tag reused while open | tag_reused_while_open |
| 28 | the final beat is 46 octets | w_strb has 46 bits set |
| 29 | a beat is exactly the bus width | all strobes set, w_last if it is the end |
| 30 | responses arrive for two frames interleaved | each frame's count advances independently |
| 31 | a frame is released before a response | released_before_data — must never happen |
Writeback and chains — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 32 | 8 contiguous releases | one writeback of 128 octets |
| 33 | a release out of index order | the batch flushes, c_broken_by_order |
| 34 | a release at ring index 0 | the batch flushes, c_broken_by_wrap |
| 35 | 3 releases then silence, timer set | flushed by timeout |
| 36 | 3 releases then silence, timer 0 | held — the failure the timer prevents |
| 37 | batching disabled | one write per release |
| 38 | a 5-descriptor chain | released 4, 3, 2, 1, 0 in that order |
| 39 | the chain's descriptor 0 released first | a protocol violation — must not occur |
| 40 | a chain whose last buffer errors | the whole chain reports the error |
Coalescing — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 41 | count 1 | an interrupt per frame |
| 42 | count 32, 31 frames, timer 100 µs | interrupt at 100 µs, c_irq_by_timer |
| 43 | count 32, 32 frames | immediate, c_irq_by_count |
| 44 | count 32, timer 0, 31 frames | coalesce_stalls; frames held |
| 45 | a PTP frame, bypass enabled | immediate, c_irq_bypass rises |
| 46 | a PTP frame, bypass disabled | waits the full timer |
| 47 | an error frame | immediate regardless of bypass config |
| 48 | worst_delay_us after a 100 µs wait | 100 |
| 49 | a burst of 32 PTP frames | 32 immediate interrupts — the bypass's cost |
Verdicts — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 50 | 1000 bursts, 100 page splits | alignment_poor |
| 51 | 1000 bursts, 30 page splits | not alignment_poor |
| 52 | 1000 chains, 1500 buffer splits | buffers_too_small |
| 53 | c_peak_open = 32 | in_flight_too_small |
| 54 | mean batch 1.3 | batching_ineffective |
| 55 | mean batch 7.8 | not batching_ineffective |
| 56 | PTP traffic, no bypass, timer firing | ptp_bypass_absent |
| 57 | released_before_data pulses | ordering_fault, not rx_path_ok |
| 58 | everything clean | none_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.
| Run | Frame | Response pattern | Seal timing | Expected |
|---|---|---|---|---|
| A | 64 octets, 1 burst | immediate | before the response | released after the response |
| B | 64 octets, 1 burst | delayed 300 ns | before the response | released after 300 ns |
| C | 9000 octets, 9 bursts | all immediate | after burst 9 issues | released once — never early |
| D | 9000 octets, 9 bursts | bursts 1–8 respond, then a 500 ns gap, then 9 | after burst 9 issues | NOT released during the gap |
| E | 9000 octets, 9 bursts | as D | SEAL DISABLED | released during the gap — the bug |
| F | 9000 octets, 9 bursts | as D | seal enabled | correct — 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:
| Check | Runs A–D, F | Run E |
|---|---|---|
released_before_data | never asserts | asserts during the gap |
| buffer contents at release | complete | last buffer stale |
| software's frame check | passes | passes — the FCS was checked in the MAC |
| a payload comparison against the scoreboard | matches | last 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."
| Check | If yes | Meaning |
|---|---|---|
released_before_data ever asserted? | the barrier is broken | an RTL bug — fatal |
| is the corruption always at the frame's END? | a missing burst response | the same |
| only on frames above one buffer's size? | a chain bug | Section 11 |
| the frame's FCS checked at the MAC? | it was — before memory | no 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."
| Check | If yes | Meaning |
|---|---|---|
c_peak_open at IN_FLIGHT? | frames are waiting for a tag | Section 10 — raise it |
mean_batch_x100 below 400? | batching is not working | Section 12 — order or sparsity |
c_page_splits above a sixteenth of bursts? | buffers are not aligned | the driver's allocator |
c_urgent_fetches near c_batches? | the window is chronically empty | raise the low-water mark |
c_stall_no_beat dominant? | the bus is waiting for the wire | not 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."
| Check | If yes | Meaning |
|---|---|---|
c_irq_bypass zero with PTP traffic present? | every event message waits the full timer | Section 15 — 4 132× the clock |
worst_delay_us equal to the timer? | confirmed | the same |
| the servo's offset bimodal? | isolated versus in-burst arrivals | Section 15's structure |
| hardware timestamps enabled? | then only the delivery is late | the 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."
| Check | If yes | Meaning |
|---|---|---|
c_overflows from the walker non-zero? | MAX_SG exceeded | Section 7 — buffer size × MAX_SG |
c_buffer_splits high | chains on every frame | buffers are small |
| truncation status set? | buf_len below the frame | the driver's configuration |
| only above a specific size? | compute MAX_SG × buffer size | the 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 KiB — so 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:
| Symptom | Instinct | This chapter's cause |
|---|---|---|
| a frame whose tail is wrong, FCS valid | a MAC or PHY bug | ownership released before the last burst responded |
| poor PTP performance with hardware timestamps | the clock, the servo, the network | a 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.
| Chapter | Takes from here |
|---|---|
| Chapter 18.4 — The Transmit DMA Path | the same ordering problem, mirrored: the driver publishes, the MAC consumes |
| Chapter 18.5 — Mastering Memory over AXI | Section 6's splits and Section 8's bursts, shaped properly |
| Chapter 18.6 — Interrupts, Coalescing and Completion | Section 15's 4 132×, made adaptive |
| Chapter 18.7 — Offload and Multi-Queue | Section 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 ns — 28.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
Related tutorials
- Related topic
The Ethernet MAC as an SoC IP Block
A MAC integrated into an SoC demands 1.143 times its line rate in memory bandwidth, crosses four clock domains, and at 100 Gb/s asks for 1.79 bus transactions per cycle.
- Related topic
Descriptor Rings and the Ownership Model
A ring hands buffers between a DMA engine and a driver with one bit and no lock — provided the descriptor's other fields are visible before that bit is, which no memory system promises.
- Related topic
Scatter-Gather — One Logical Transfer Across Many Segments
A buffer that looks contiguous to an application is usually not contiguous in memory. Scatter-gather walks a list of segments — and the list itself has to be fetched by DMA, which makes the control plane a data plane.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
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.
