Ethernet · Module 21
DMA Failures
Eight descriptor-path failures sorted into three classes by one existing counter, and a consumer stall priced at 148.8 frames per microsecond beyond the buffer's 2.621.
Chapter 20.5 §12 proved that no sequence of frames can overflow a correctly sized receive FIFO: the arrival rate is the line rate and Chapter 19.5 §4's drain rate exceeds it. The consumer has to stop. This is the chapter where that stop is the subject rather than the stimulus.
| Value | |
|---|---|
| descriptor-path failures enumerated | 8 |
| classes Chapter 19.6 §5's outstanding count sorts them into | 3 |
| receive FIFO capacity, in wire time | 2.621 µs |
| Chapter 19.6 §8's design stall budget | 2.002 µs |
| the margin between them | 0.620 µs |
| frames lost per microsecond beyond it, minimum size | 148.8 |
| maximum size | 8.13 |
The last three rows are the pricing the direction of this chapter asked for, and they turn a vague symptom into a schedule.
| The stall | Beyond the FIFO by | Minimum-size frames lost |
|---|---|---|
| a DDR4 refresh — 350 ns | nothing | 0 |
| Chapter 19.6 §8's budget — 2.000 µs | nothing | 0 |
| the FIFO's capacity — 2.621 µs | nothing, exactly | 0 |
| a 10 µs interconnect stall | 7.379 µs | 1 098 |
| a 1 ms scheduling gap | 997.4 µs | 148 419 |
A DRAM refresh is free and a driver that misses a millisecond costs a hundred and fifty thousand frames, and everything between the two is linear in the overshoot.
And one counter sorts the eight failures into three classes before any of that arithmetic is needed.
| Outstanding count | Means | Failures |
|---|---|---|
| falls to zero | nothing is being requested | 4 |
| pinned at its maximum | responses are not being consumed | 1 |
| normal | the transfer works and something else is wrong | 3 |
1. Scope, and a Fault With No Frame Behind It
Scope: the descriptor path's failure modes, what each does to Chapter 19.6 §5's outstanding count, and what a consumer stall costs in frames.
Not in scope: the interface's design. Chapter 19.6 built the mechanism — the request shaper, the outstanding tracker, the (O − 1)-burst reorder buffer, the write commit tracker — and this chapter reads its instruments as evidence. Nor the drop's visibility: Chapter 21.6 established that a DMA drop is a class G drop, silent in every error counter, and named the two free ratios that reach it.
Start from what makes this chapter different from every other one in Module 21.
| Every other chapter's fault | This one's | |
|---|---|---|
| reachable from the wire | yes | NO |
| Chapter 20.5 §10's category | an injection | neither of its two |
| what produces it | a frame, or a design fault | the memory system stopping |
| what a generator can do about it | produce it | nothing at all |
Row one is Chapter 20.5 §12's proof and it is worth restating exactly. The receive FIFO's arrival rate is bounded by the line rate — that is what a line rate is — and Chapter 19.5 §4's system-side drain is 128 Gb/s against 100. So with the consumer running, the buffer empties, whatever the frames look like. Minimum size back to back, jumbo back to back, any mixture: the arrival rate is the same in every case and it is below the drain rate.
The consumer must stop. Nothing on the wire can make it stop, and that is why this fault has no stimulus and needs a chapter of its own.
Which gives the chapter its two halves.
| Half | Question |
|---|---|
| Sections 2 to 9 | why did the consumer stop — eight failures, three classes |
| Sections 10 to 13 | what did the stop cost — 148.8 frames per microsecond |
And one structural note about where the evidence lives. Every instrument in this chapter is on the memory side — Chapter 19.6 §5's outstanding count, §13's commit tracker, the descriptor ring's own state — and none of it is a frame counter. Chapter 21.6's selectivity index sees the consequence; this chapter's instruments see the cause, and they are in a different register block behind a different driver.
2. The Descriptor Ring, as a Failure Space
A descriptor ring is a circular array of entries, each with a buffer pointer, a length field and an ownership bit. Everything that goes wrong with it goes wrong with one of those three.
| Owned by hardware | Owned by software | |
|---|---|---|
| an empty descriptor | ready for the MAC to fill | waiting for the driver to refill |
| a filled descriptor | — | waiting for the driver to reap |
| the transition | the driver sets it | the MAC sets it |
Two parties, one bit, and a ring that both walk in the same direction. The eight failures are what happens when that arrangement breaks, and they group by which of the three fields is at fault.
| # | Failure | Field |
|---|---|---|
| 1 | the ring is exhausted — no descriptor owned by hardware | ownership |
| 2 | the ownership bit is read stale | ownership |
| 3 | the descriptor is written before the frame data lands | ordering |
| 4 | the descriptor is committed per burst, not per frame | ordering |
| 5 | head and tail pointers diverge | position |
| 6 | the buffer is smaller than the frame | length |
| 7 | the completion is never signalled — no interrupt | ownership, indirectly |
| 8 | the reorder buffer is undersized and the read channel stalls | none — it is Chapter 19.6 §19's |
Failure 8 is in the list for a reason that Section 17 is about, and it is the one that produces the chapter's most counter-intuitive diagnosis: it is a fault on the transmit read path whose symptom is loss on the receive path.
Three of the eight deserve a sentence now because they are the ones that look alike.
Failures 1 and 7 are indistinguishable from the MAC's side. A ring with nothing owned by hardware is a ring with nothing owned by hardware, whether the driver is slow, descheduled, or never learned there was work to do. The completion mechanism is in the host and the MAC has no visibility into whether its interrupt was taken.
Failures 3 and 4 both produce a descriptor the driver believes but should not. Chapter 18.3's class 76 named the general shape — custody is not visibility — and Chapter 19.6 §13's commit tracker exists to get it right: a frame is several bursts, and the descriptor may be marked done only when the last of them has returned its write response. Marking it when the last was issued, or when the first responded, are failures 3 and 4.
Failure 6 is Chapter 21.6's G_DMA_SIZE and it is the only member of this list that chapter's free selectivity index can name. Everything else here is invisible to a frame-side instrument, which is why the outstanding count is the axis this chapter uses.
And the two chapters' instruments are complementary rather than overlapping, which is worth laying out once.
| Failure | Chapter 21.6's free ratios say | this chapter's outstanding count says |
|---|---|---|
| 1 — ring exhausted | flat, clustered — class C | zero |
| 2 — stale ownership | nothing — no frames are lost | normal |
| 3 — descriptor early | nothing | normal |
| 4 — commit per burst | nothing | normal |
| 5 — pointers diverged | flat, clustered — class C | zero |
| 6 — buffer too small | SELECTIVE — named | zero |
| 7 — no completion | flat, clustered — class C | zero |
| 8 — reorder buffer | flat, clustered — class C | PINNED — named |
Rows two, three and four are the group Chapter 21.6 cannot see at all, because it reasons about frames that did not arrive and these deliver every frame. Rows six and eight are named by one instrument each and neither names both.
| Named by | |
|---|---|
| failure 6 | the selectivity index — a frame-side ratio |
| failure 8 | the outstanding count — a memory-side register |
| failures 1, 5, 7 | neither, and they are class C's three |
| failures 2, 3, 4 | neither, and no counter anywhere |
Two instruments in two register blocks behind two drivers, and together they name two of the eight. The four in rows three and four of that table are the chapter's real subject, and Sections 7 to 9 are what is left to say about them.
3. RTL 1 — The DMA Diagnostic Package and the Ownership Model
// ---------------------------------------------------------------------
// dmadiag_pkg -- eight failures, one counter, and the three readings it
// takes.
//
// The package's claim: Chapter 19.6 Section 5's outstanding count is a
// three-valued diagnostic. Zero means nothing is being asked for;
// pinned means nothing is being answered; normal means the transfer
// works and the fault is in ownership or ordering.
// ---------------------------------------------------------------------
package dmadiag_pkg;
typedef enum logic [3:0] {
F_RING_EMPTY = 4'd0, // no descriptor owned by hardware
F_OWN_STALE = 4'd1, // the ownership bit was read stale
F_DESC_EARLY = 4'd2, // descriptor written before the data landed
F_COMMIT_BURST = 4'd3, // committed per burst, not per frame
F_PTR_DIVERGE = 4'd4, // head and tail disagree
F_BUF_SMALL = 4'd5, // Chapter 21.6's G_DMA_SIZE
F_NO_COMPLETION= 4'd6, // the interrupt never arrived
F_REORDER_SMALL= 4'd7, // Chapter 19.6 Section 19's 7 KiB
F_NONE = 4'd15
} dfail_e;
typedef logic [7:0] fmask_t;
localparam fmask_t ALL_F = 8'hFF;
// Section 4's three readings of the outstanding count.
typedef enum logic [1:0] {
O_ZERO = 2'd0, // nothing is being requested
O_PINNED = 2'd1, // requests issued, responses not consumed
O_NORMAL = 2'd2, // Little's law is satisfied
O_UNKNOWN= 2'd3
} ocount_e;
localparam fmask_t CLASS_ZERO = 8'b0111_0001; // ring, ptr, buf, completion
localparam fmask_t CLASS_PINNED = 8'b1000_0000; // the reorder buffer
localparam fmask_t CLASS_NORMAL = 8'b0000_1110; // stale own, early, per-burst
// Chapter 19.5 Section 3's receive FIFO, in beats and in wire time.
localparam int FIFO_BEATS = 512;
localparam int BEAT_BITS = 512;
localparam int LINE_GBPS = 100;
// 512 x 512 / 100e9 = 2.621 us, in nanoseconds.
localparam int FIFO_NS = 2621;
// Chapter 19.6 Section 8's design budget: 391 beats.
localparam int BUDGET_NS = 2002;
localparam int MARGIN_NS = FIFO_NS - BUDGET_NS; // 619
// Section 12: frames lost per nanosecond of overshoot, x1000.
localparam int MIN_FRAMES_PER_US_X10 = 1488; // 148.8
localparam int MAX_FRAMES_PER_US_X10 = 81; // 8.1
function automatic int popcount8(fmask_t m);
int n = 0;
for (int i = 0; i < 8; i++) if (m[i]) n++;
return n;
endfunction
endpackageClassification: a package whose one structural claim is that a single counter is three-valued rather than numeric.
What it teaches: that the outstanding count's value is almost useless and its regime is decisive. Chapter 19.6 §4's Little's-law arithmetic says the count should be R × L / burst — 13 bursts at 1 µs of memory latency with 1 KiB bursts — but knowing it is 13 rather than 11 tells a diagnosis nothing. Knowing it is zero or pinned at the limit eliminates four failures or seven.
And it teaches that the three class masks are disjoint and exhaust the eight. CLASS_ZERO has four members, CLASS_PINNED one, CLASS_NORMAL three; their union is ALL_F and their pairwise intersections are empty. That is a genuine partition, unlike Chapter 21.6's two axes which had to be intersected — because the outstanding count is a single quantity with three mutually exclusive regimes.
Deliberately simplified: FIFO_NS and BUDGET_NS are computed constants in a package where they should be derived from FIFO_BEATS, BEAT_BITS and LINE_GBPS — and a design at a different rate gets the wrong numbers silently, which is Chapter 20.6 §6's problem arriving again. MIN_FRAMES_PER_US_X10 hardcodes the minimum frame size. And the eight failures are treated as mutually exclusive, which Section 18's row six is about.
Production implication: the rate-dependence of the three timing constants is the one to parameterise before anything else. At 10 Gb/s a 512-beat FIFO at 64 bits per beat holds 4 096 octets, which is 3.277 µs of wire time rather than 2.621 — a 25% larger budget for the same buffer — and the frames-per-microsecond figure falls by a factor of ten. A diagnosis calibrated at 100 Gb/s over-states the cost of a stall by an order of magnitude at 10 Gb/s, which turns a real finding into a number nobody believes.
// ---------------------------------------------------------------------
// desc_ownership_model -- the ring's state as the MAC can see it, and
// the two transitions that are not observable from here.
//
// The MAC sets ownership to software. The driver sets it to hardware.
// The MAC can see its own transition and can only INFER the other,
// which is the asymmetry the whole chapter turns on.
// ---------------------------------------------------------------------
module desc_ownership_model
import dmadiag_pkg::*;
#(
parameter int RING_SIZE = 256
)(
input logic clk,
input logic rst_n,
input logic desc_fetched,
input logic desc_owned_by_hw, // the bit we read
input logic desc_released, // we set it to software
input logic [15:0] head_ptr, // ours
input logic [15:0] tail_ptr, // the driver's, if visible
output logic [15:0] hw_owned_count,
output logic [15:0] c_fetches,
output logic [15:0] c_releases,
output logic [15:0] c_empty_fetches,
output logic ring_exhausted,
output logic ptr_diverged,
output logic tail_visible
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_fetches <= '0; c_releases <= '0; c_empty_fetches <= '0;
end else begin
if (desc_fetched) begin
c_fetches <= c_fetches + 16'd1;
// A fetch that found the descriptor owned by SOFTWARE is the
// ring's exhaustion, observed one descriptor at a time.
if (!desc_owned_by_hw) c_empty_fetches <= c_empty_fetches + 16'd1;
end
if (desc_released) c_releases <= c_releases + 16'd1;
end
end
// What the MAC believes it owns. It cannot know what the driver has
// refilled until it fetches and looks.
assign hw_owned_count = (c_fetches >= c_releases)
? (c_fetches - c_releases) : 16'd0;
assign ring_exhausted = (c_empty_fetches > 16'd0);
// The driver's tail pointer is visible only if the design mirrors it
// into a register the MAC can read, which many do not.
assign tail_visible = 1'b1;
assign ptr_diverged = tail_visible &&
((head_ptr - tail_ptr) > 16'(RING_SIZE));
endmoduleClassification: a state model that is deliberately incomplete, because the state it models is shared with a party it cannot observe.
What it teaches: that c_empty_fetches is the ring's exhaustion observed one descriptor at a time, and it is the only direct evidence of it the MAC has. The driver's refilling is invisible: the MAC learns a descriptor is available by fetching it and finding the ownership bit set. A fetch that finds it clear is the only positive signal the ring is empty — and a MAC that stops fetching when it is empty produces no such signal at all.
And it teaches that tail_visible is a design choice most designs get wrong. The driver's tail pointer — how far it has reaped — is in host memory and is not read by the MAC. Mirroring it into a readable register costs sixteen flops and turns failure 5 from an inference into a comparison, which Section 8's table shows is one of the three the outstanding count cannot separate.
Deliberately simplified: tail_visible is hardcoded to 1, which is the assumption rather than the common case. hw_owned_count is a difference of two counters that wrap independently. ptr_diverged compares a raw difference against the ring size where a circular comparison is needed. And the block cannot distinguish "the ring is empty" from "we stopped fetching" — Section 4's first ambiguity.
Production implication: the counter this block is missing is the one that separates failures 1 and 7, and it is in the host rather than in the MAC. A driver that records how long since it last refilled turns "the ring is empty" into "the ring is empty and the driver has not run in 3 ms", which is failure 7 rather than failure 1 — and it is a software counter, free, and almost never collected alongside the MAC's.
4. What Each Failure Does to the Outstanding Count
Chapter 19.6 §5's outstanding tracker counts requests issued minus responses returned. Under Chapter 19.6 §4's Little's law it should sit near R × L / burst.
| Memory latency | Outstanding, 1 KiB bursts | Reorder buffer |
|---|---|---|
| 400 ns | 5 | 4 KiB |
| 1 µs | 13 | 12 KiB |
| 2 µs | 25 | 24 KiB |
| 5.5 µs | 68 | 67 KiB |
Now each of Section 2's eight failures, and what it does to that count.
| # | Failure | Outstanding count | Why |
|---|---|---|---|
| 1 | the ring is exhausted | falls to zero | nothing to write, so nothing is issued |
| 2 | the ownership bit is read stale | normal | the MAC issues happily, into the wrong buffer |
| 3 | descriptor written before data | normal | both are issued; only the order is wrong |
| 4 | committed per burst | normal | the bursts are issued correctly |
| 5 | pointers diverge | falls to zero | the ring appears full and fetching stops |
| 6 | the buffer is too small | falls | frames are dropped before any burst is issued |
| 7 | no completion signalled | falls to zero | the driver never reaps, so the ring exhausts |
| 8 | the reorder buffer is undersized | pinned at maximum | RREADY is low; responses stop returning |
Three regimes, and they are mutually exclusive.
| Reading | Failures | Count |
|---|---|---|
| zero | 1, 5, 6, 7 | 4 |
| pinned at maximum | 8 | 1 |
| normal | 2, 3, 4 | 3 |
One counter, eight failures, three classes — and the counter already exists, because Chapter 19.6 §5 built it to enforce the outstanding limit rather than to diagnose anything.
The pinned reading is the most decisive and the least expected.
A count pinned at its maximum means requests are being issued and responses are not being consumed. There is exactly one mechanism in this list that does that, and it is a buffer on the transmit path.
| Pinned at maximum means | |
|---|---|
| the shaper is issuing | yes — it is at the limit |
| the interconnect is responding | possibly, and RREADY is low |
| the reorder buffer is full | Chapter 19.6 §19's buffer_undersized |
| the receive path is losing frames | and there is no signal connecting the two |
Row four is Section 17's subject and Chapter 19.6 §22's first complaint: the buffer that overflowed is on the transmit read path, the frames lost are on the receive path, and the only thing coupling them is a shared RREADY that appears in neither path's interface.
And the zero reading's four members are what the rest of the chapter has to separate.
| Failure | Distinguishing evidence | Where it lives |
|---|---|---|
| 1 — ring exhausted | c_empty_fetches climbing | the MAC |
| 5 — pointers diverged | the tail pointer, if mirrored | a register most designs omit |
| 6 — buffer too small | Chapter 21.6's selectivity index | four existing counters |
| 7 — no completion | time since the driver last ran | the host, and not collected |
Two of the four are named by evidence that already exists and two need something nobody builds — sixteen flops for a mirrored tail pointer, and a software timestamp. Section 19 prices both.
5. RTL 2 — The Outstanding-Count Classifier
// ---------------------------------------------------------------------
// outstanding_regime -- read Chapter 19.6 Section 5's counter as three
// values rather than as a number.
//
// The regime is what discriminates; the value is what Little's law
// predicts and neither confirms nor denies a fault. A count of 13 at
// 1 us of memory latency is exactly right and says nothing about
// ownership, ordering or the ring.
// ---------------------------------------------------------------------
module outstanding_regime
import dmadiag_pkg::*;
#(
parameter int MAX_OUTSTANDING = 32,
parameter int WINDOW_CYCLES = 4096
)(
input logic clk,
input logic rst_n,
input logic [7:0] outstanding, // from Chapter 19.6 Section 5
input logic [15:0] expected_by_little, // R x L / burst
input logic rready,
output ocount_e regime,
output fmask_t candidates,
output logic [7:0] min_seen,
output logic [7:0] max_seen,
output logic [15:0] c_at_zero,
output logic [15:0] c_at_max,
output logic regime_stable
);
logic [15:0] window;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
min_seen <= 8'hFF; max_seen <= 8'd0;
c_at_zero <= '0; c_at_max <= '0; window <= '0;
end else begin
if (outstanding < min_seen) min_seen <= outstanding;
if (outstanding > max_seen) max_seen <= outstanding;
if (outstanding == 8'd0) c_at_zero <= c_at_zero + 16'd1;
if (outstanding == 8'(MAX_OUTSTANDING)) c_at_max <= c_at_max + 16'd1;
window <= window + 16'd1;
end
end
always_comb begin
// The regime is about where the count SPENDS its time, not where
// it is at any instant -- a healthy count touches zero between
// bursts and touches the limit under load.
if (c_at_zero > ((window * 16'd3) / 16'd4)) regime = O_ZERO;
else if (c_at_max > ((window * 16'd3) / 16'd4)) regime = O_PINNED;
else if (window > 16'd1000) regime = O_NORMAL;
else regime = O_UNKNOWN;
case (regime)
O_ZERO: candidates = CLASS_ZERO;
O_PINNED: candidates = CLASS_PINNED;
O_NORMAL: candidates = CLASS_NORMAL;
default: candidates = ALL_F;
endcase
regime_stable = (window > 16'(WINDOW_CYCLES));
end
endmoduleClassification: a three-way regime detector over an existing counter, and the thresholds are about occupancy in time rather than about instantaneous value.
What it teaches: that a healthy outstanding count visits both extremes and lives at neither. It touches zero between bursts and touches the limit under load — so an instantaneous reading of zero proves nothing and three quarters of a window at zero proves a great deal. The regime test is a time-occupancy test, which is the same shape as Chapter 21.6 §9's clustering probe and for the same reason: the interesting property is where a quantity spends its time.
And it teaches that expected_by_little is an input the block does not use, deliberately. Chapter 19.6 §4's Little's-law figure is what the count should be — 13 bursts at 1 µs and 1 KiB — and comparing the measured count against it separates nothing in Section 4's table. A count of 9 against an expected 13 is a slower interconnect, not a fault; a count of 0 is a fault whatever the expectation was.
Deliberately simplified: the three-quarters thresholds are judgements, and a burst-y workload can put a healthy count at zero for three quarters of a short window. window counts cycles and never resets, so the ratios are since reset — the windowing gap Chapter 21.6 §18 and Chapter 21.3 §3 both flagged. And rready is an input that is never read, where it would immediately confirm the pinned regime's cause.
Production implication: rready is the signal that turns the pinned regime from one class into a proof. A count pinned at the limit with RREADY continuously low is Chapter 19.6 §19's undersized reorder buffer with no ambiguity at all; the same count with RREADY high is an interconnect that has stopped responding, which is somebody else's fault entirely. One signal, already routed to the reorder sink, and it splits the pinned class's single member into two whose owners are different teams.
6. Three Classes From One Counter
Section 4's table, read as a diagnosis.
| Regime | Candidates | What it eliminates |
|---|---|---|
| zero | ring exhausted, pointers diverged, buffer too small, no completion | the ordering failures and the reorder buffer |
| pinned | the reorder buffer | everything else — 7 of 8 |
| normal | stale ownership, descriptor early, per-burst commit | everything that stops the flow |
One counter, eight failures, and the information content is worth computing because it is unusually good.
| Regime | Failures it names | Bits |
|---|---|---|
| zero | 4 of 8 | 1.00 |
| pinned | 1 of 8 | 3.00 |
| normal | 3 of 8 | 1.42 |
| expected | — | 1.41 |
An expected 1.41 bits from a counter that already exists, against ceil(log₂ 8) = 3 bits to name a failure outright. Just under half the job, from one register read — and the pinned regime alone is worth three bits, because it has exactly one member.
And the three classes have completely different characters, which decides what to do next.
| Class | Character | Next step |
|---|---|---|
| zero | the flow stopped | why is nothing being asked for |
| pinned | the flow is blocked | RREADY, and then Chapter 19.6 §19 |
| normal | the flow is fine and the data is wrong | Chapter 19.6 §13's commit test |
Row three is the class that produces no loss at all and the worst bugs. Stale ownership, a descriptor written early, a per-burst commit — all three deliver every frame, the counters are healthy, the outstanding count is exactly what Little's law predicts, and the driver reads frames with stale tails. Chapter 19.6 §22's second complaint is that symptom and its test is one counter: c_frames_committed against the frame count.
Two of the three classes are about frames that did not arrive. The third is about frames that arrived wrong, and no drop counter anywhere will ever show it.
Which is the boundary between this chapter and Chapter 21.6.
| Chapter 21.6 | this chapter's normal regime | |
|---|---|---|
| frames delivered | short | complete |
frames_in − frames_delivered | non-zero | zero |
| the fault | something dropped them | something delivered them wrong |
| the instrument | counters | the data itself |
Row four is why the normal regime is the hardest of the three. Its faults are invisible to every counter in Module 21 — the frames are all there — and the only evidence is that the contents are wrong, which is Chapter 20.3's scoreboard rather than a diagnostic counter.
7. RTL 3 — The Ownership-Transition Monitor
// ---------------------------------------------------------------------
// ownership_transition_monitor -- watch the one bit two parties write,
// and catch the transitions that should not happen.
//
// The MAC sets ownership to software. The driver sets it to hardware.
// Anything else -- a descriptor that becomes hardware-owned without the
// driver acting, or one the MAC writes twice -- is failure 2 or 5.
// ---------------------------------------------------------------------
module ownership_transition_monitor
import dmadiag_pkg::*;
#(
parameter int RING_SIZE = 256
)(
input logic clk,
input logic rst_n,
input logic desc_read,
input logic [15:0] desc_index,
input logic own_bit_read,
input logic we_released,
output logic [15:0] c_hw_owned_reads,
output logic [15:0] c_sw_owned_reads,
output logic [15:0] c_reread_after_release,
output logic double_ownership,
output logic stale_read_suspected,
output logic [15:0] c_releases
);
logic released [RING_SIZE];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_hw_owned_reads <= '0; c_sw_owned_reads <= '0;
c_reread_after_release <= '0; c_releases <= '0;
double_ownership <= 1'b0; stale_read_suspected <= 1'b0;
for (int i = 0; i < RING_SIZE; i++) released[i] <= 1'b0;
end else begin
if (we_released) begin
released[desc_index] <= 1'b1;
c_releases <= c_releases + 16'd1;
end
if (desc_read) begin
if (own_bit_read) c_hw_owned_reads <= c_hw_owned_reads + 16'd1;
else c_sw_owned_reads <= c_sw_owned_reads + 16'd1;
// We released this descriptor to software and are now reading
// it as hardware-owned again. Either the driver refilled it --
// normal -- or we are seeing a stale copy of our own write.
if (released[desc_index] && own_bit_read) begin
c_reread_after_release <= c_reread_after_release + 16'd1;
released[desc_index] <= 1'b0;
end
end
end
end
// Chapter 18.2's class 75 in this chapter's vocabulary: the ownership
// bit is a memory location and a memory system may reorder the writes
// that reach it. A re-read that is TOO SOON after our own release is
// the signature.
always_comb begin
stale_read_suspected = (c_reread_after_release > (c_releases >> 2));
double_ownership = 1'b0; // needs the driver's view -- Section 8
end
endmoduleClassification: a transition watcher over a bit that two parties write and only one of which this block can see.
What it teaches: that the MAC can detect a suspicious re-read and cannot detect a genuine double ownership. A descriptor the MAC released and then reads as hardware-owned again is either the driver having refilled it — which is the normal cycle — or a stale copy of the MAC's own write. The two are distinguished by how soon, and "soon" is a property of the memory system rather than of the ring.
And it teaches that double_ownership is wired to zero deliberately. Detecting that both parties believe they own a descriptor requires the driver's view, which is in host memory and is not read by the MAC. It is the same shape as Chapter 21.4's partner status: the decisive observation belongs to the other party, and a design that does not mirror it cannot make the observation at any price.
Deliberately simplified: released is a RING_SIZE-bit array of flops — 256 flops for a 256-entry ring, and a real ring is thousands. The quarter-of-releases threshold for stale_read_suspected is a judgement with no basis in the memory system's actual reordering window. And there is no timestamp, so "too soon" is not measured at all: the block counts re-reads and infers.
Production implication: the missing timestamp is what would make this block decisive rather than suggestive. Chapter 18.2 §19's class 75 is about a multi-word structure observed while the memory system may reorder the writes, and the defence there was ordering rather than detection. Here a single cycle counter between a release and its re-read — sixteen flops — turns "we re-read it soon after releasing" into "we re-read it 40 cycles after releasing, and the interconnect's write-to-read visibility is 200", which is a proof. The instrument is cheap and the number it needs comes from the interconnect's documentation.
8. The Ordering Failures the Count Cannot See
Section 6's normal regime holds three failures and they share a property: every frame is delivered and the outstanding count is exactly what Little's law predicts.
| Failure | Frames delivered | Outstanding | What is wrong |
|---|---|---|---|
| stale ownership read | all of them | normal | into a buffer the driver also owns |
| descriptor written before data | all of them | normal | the driver reads before the data lands |
| committed per burst | all of them | normal | the tail of a multi-burst frame is stale |
No counter in Module 21 sees any of the three, because every counter in Module 21 counts frames and all the frames are there.
What the three have in common is Chapter 18.3's class 76 — custody is not visibility — and Chapter 19.6 §13's commit tracker was built to get it right:
A frame is several bursts, and its descriptor may be marked done only when the last of them has returned its write response. Not when the last was issued, and not when the first responded.
Those three moments are the three failures.
| When the descriptor is marked | Failure | Symptom |
|---|---|---|
| when the last burst returns its response | none — correct | — |
| when the last burst is issued | failure 3 | the driver may read before the write lands |
| when the first burst responds | failure 4 | the tail of the frame is stale |
And Chapter 19.6 §22's second complaint gives the test for the pair, in one counter.
| Test | |
|---|---|
c_frames_committed against the frame count | equal is correct; larger is per-burst committing |
| why it works | a multi-burst frame commits once, not once per burst |
| what it costs | one comparison of two existing counters |
A frame at 1 518 octets with 1 KiB bursts is two bursts, so a per-burst commit produces two commits per frame and c_frames_committed runs at roughly twice the frame count. The ratio is the diagnosis and the ratio's value is the mean bursts per frame, which is computable from the mean frame size and the burst size.
| Mean frame size | Bursts per frame, 1 KiB | c_frames_committed ratio if per-burst |
|---|---|---|
| 64 octets | 1 | 1.00 — invisible |
| 645.6 octets | 1 | 1.00 — invisible |
| 1 518 octets | 2 | 2.00 |
| 9 000 octets | 9 | 9.00 |
Rows one and two are the blind case and they are the common one. A frame smaller than a burst is one burst, so committing per burst and committing per frame are the same thing — and the bug is invisible on any traffic whose frames are below the burst size. Chapter 19.6 §22's fourth row said the same thing from the other direction: slowing the interconnect makes the bug disappear, and this is the traffic-side version of the same conditionality.
Which is Chapter 21.5's structural zero appearing for the third time in the module.
| Chapter | The fault is invisible when |
|---|---|
| Chapter 21.5 §6 | the frames are 64 octets — no late collision is possible |
| Chapter 21.6 §6 | the traffic is one size — the index cannot move |
| this chapter | the frames fit in one burst — one commit either way |
Three different faults, three different mechanisms, and the same shape: a threshold in the frame size below which the evidence does not exist. In all three the threshold is a design constant — the slot time, the size variance, the burst length — and in all three a regression built from minimum-size frames finds nothing.
9. RTL 4 — The Commit-Completeness Checker
// ---------------------------------------------------------------------
// commit_completeness -- Chapter 19.6 Section 13's `closed` term, read
// as a diagnostic rather than enforced as a design rule.
//
// One counter against another: commits against frames. Equal is
// correct. A ratio near the mean bursts per frame is a per-burst
// commit, which is failure 4 and produces stale frame tails.
// ---------------------------------------------------------------------
module commit_completeness
import dmadiag_pkg::*;
#(
parameter int BURST_OCTETS = 1024
)(
input logic clk,
input logic rst_n,
input logic [31:0] c_frames_written,
input logic [31:0] c_frames_committed,
input logic [31:0] c_bursts_issued,
input logic [15:0] mean_frame_octets,
output logic [15:0] commit_ratio_x100,
output logic [15:0] bursts_per_frame_x100,
output logic commits_per_burst,
output logic commits_correct,
output logic test_is_blind,
output logic ordering_suspect
);
always_comb begin
commit_ratio_x100 = (c_frames_written == 32'd0) ? 16'd0
: 16'((c_frames_committed * 32'd100) / c_frames_written);
bursts_per_frame_x100 = (c_frames_written == 32'd0) ? 16'd100
: 16'((c_bursts_issued * 32'd100) / c_frames_written);
// A ratio near 1.00 is correct. A ratio near the bursts-per-frame
// figure is a commit per burst.
commits_correct = (commit_ratio_x100 >= 16'd95) &&
(commit_ratio_x100 <= 16'd105);
commits_per_burst = (bursts_per_frame_x100 > 16'd150) &&
(commit_ratio_x100 > (bursts_per_frame_x100 - 16'd30));
// Section 8: a frame smaller than a burst is one burst, so the two
// policies are indistinguishable. The test cannot fire.
test_is_blind = (mean_frame_octets <= 16'(BURST_OCTETS));
// Failure 3 -- descriptor before data -- does not change either
// count. It changes the ORDER, and only Chapter 19.6 Section 12's
// sequencing sees it.
ordering_suspect = commits_correct && !test_is_blind;
end
endmoduleClassification: a ratio test between two counters that a correct design keeps equal.
What it teaches: that test_is_blind covers most real traffic and that is the finding, not the caveat. A 1 KiB burst against a 645.6-octet mean frame means almost every frame is one burst, so committing per burst and per frame produce identical counts — and the bug that produces stale frame tails is completely invisible on ordinary traffic. It appears when the traffic turns to jumbo frames or when the burst size is reduced, which is a configuration change rather than a fault.
And it teaches that ordering_suspect is a residual rather than a detection. Failure 3 — the descriptor written before the data lands — changes neither counter: both the data bursts and the descriptor write are issued, both complete, and only their order is wrong. The commit ratio is exactly 1.00 and the frames are corrupt, so this block's output on failure 3 is "commits look correct", which is true and useless.
Deliberately simplified: commits_per_burst's tolerance of 30 hundredths is a judgement. bursts_per_frame_x100 is derived from a burst counter that may include descriptor fetches, which would inflate it. And ordering_suspect asserts on every healthy link with large frames, which makes it a poor name for what is really "this block has nothing to say."
Production implication: failure 3 is the one this chapter cannot reach with counters and it is the most damaging of the three. A descriptor marked done before the frame data has landed hands the driver a buffer whose contents are partly stale, and the corruption is silent, intermittent and rate-dependent — Chapter 19.6 §22's second complaint noted that slowing the interconnect makes it disappear. The instrument that catches it is not a counter at all: it is Chapter 19.6 §12's sequencing, enforced rather than observed — the design either orders the data before the descriptor or it does not, and a property is the right tool because the fault is a design decision rather than a runtime event.
10. Pricing the Consumer Stall
Chapter 20.5 §12 proved the consumer has to stop for a frame to be lost. This section is what each duration of stop costs.
The receive FIFO's capacity in wire time:
512 beats × 512 bits / 100 Gb/s = 2.621 µsChapter 19.6 §8's design budget is 391 beats — 2.002 µs — and the margin between them is 0.620 µs.
| Time | |
|---|---|
| the FIFO's capacity | 2.621 µs |
| Chapter 19.6 §8's budgeted stall | 2.002 µs |
| the margin | 0.620 µs |
| what the margin covers | the pause round trip and the synchroniser reserve |
Beyond 2.621 µs, frames are lost at the line rate.
| Frame size | Frames per microsecond at 100 Gb/s |
|---|---|
| 64 octets | 148.8 |
| 1 518 octets | 8.13 |
So the price list:
| The stall | Beyond the FIFO | Minimum-size frames | Maximum-size |
|---|---|---|---|
| a DDR4 refresh, 350 ns | 0 | 0 | 0 |
| Chapter 19.6 §8's 2.000 µs budget | 0 | 0 | 0 |
| 2.621 µs — exactly the FIFO | 0 | 0 | 0 |
| 10 µs | 7.379 µs | 1 098 | 60 |
| 100 µs | 97.4 µs | 14 493 | 792 |
| 1 ms | 997.4 µs | 148 419 | 8 106 |
Four readings of that table.
First, a DRAM refresh is free with a factor of seven to spare. DDR4's tRFC is around 350 ns and the FIFO absorbs 2 621. A design that loses frames to refreshes has a buffer far below Chapter 19.5 §3's sizing, which is Chapter 19.6 §22's first complaint and is caught at elaboration by buffer_undersized.
Second, the margin is 0.620 µs and it is not spare capacity. Chapter 19.5 §3 sized the FIFO as a sum — the memory stall, the pause round trip, the synchroniser reserve — so the 0.620 µs beyond the stall budget is allocated to the other two terms. Spending it on a longer stall means a pause frame arrives too late.
Third, the relationship is linear and steep. Every microsecond beyond the FIFO costs 148.8 minimum-size frames, which at a typical 1 ms scheduling quantum is a hundred and fifty thousand.
Fourth, the maximum-size column is eighteen times smaller and that is not a mitigation. A link carrying large frames loses fewer frames and the same octets — 8.13 frames per microsecond at 1 518 octets is 12.3 KiB, and 148.8 at 64 octets is 9.5 KiB — so the byte loss is within 30% either way. The frame count differs by eighteen and the data loss barely differs at all.
Which gives the one framing of this cost that survives a change of rate or frame size.
A stall beyond the FIFO loses the wire. Every nanosecond past 2.621 µs is a nanosecond of line rate that arrived and had nowhere to go — 12.5 gigabytes per second, whatever it was carved into.
| Lost per microsecond of overshoot | |
|---|---|
| at 100 Gb/s | 12.5 KB |
| at 10 Gb/s | 1.25 KB |
| frames, at 64 octets | 148.8 or 14.88 |
| frames, at 1 518 octets | 8.13 or 0.813 |
The octet figure is the rate and nothing else, which makes it the number to quote when the frame size is unknown — and the frame figure is what a driver's ring-refill logic actually has to keep up with, which is why both appear in Section 11's estimate.
11. RTL 5 — The Stall Budget Tracker
// ---------------------------------------------------------------------
// stall_budget_tracker -- measure the consumer's stalls against the
// budget Chapter 19.5 Section 3 allocated, and price the overshoot.
//
// The block's premise is Chapter 20.5 Section 12's proof: no frame can
// cause this. So the stall's duration is a property of the memory
// system and the loss is a deterministic function of it.
// ---------------------------------------------------------------------
module stall_budget_tracker
import dmadiag_pkg::*;
(
input logic clk,
input logic rst_n,
input logic tick_ns,
input logic consumer_stalled,
input logic [15:0] mean_frame_octets,
output logic [31:0] current_stall_ns,
output logic [31:0] longest_stall_ns,
output logic [31:0] c_stalls,
output logic [31:0] c_stalls_over_budget,
output logic [31:0] c_stalls_over_fifo,
output logic [31:0] frames_lost_estimate,
output logic within_budget,
output logic within_fifo
);
logic stalled_prev;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
current_stall_ns <= '0; longest_stall_ns <= '0;
c_stalls <= '0; c_stalls_over_budget <= '0; c_stalls_over_fifo <= '0;
frames_lost_estimate <= '0; stalled_prev <= 1'b0;
end else begin
stalled_prev <= consumer_stalled;
if (consumer_stalled && tick_ns)
current_stall_ns <= current_stall_ns + 32'd1;
if (!consumer_stalled && stalled_prev) begin
c_stalls <= c_stalls + 32'd1;
if (current_stall_ns > longest_stall_ns)
longest_stall_ns <= current_stall_ns;
if (current_stall_ns > 32'(BUDGET_NS))
c_stalls_over_budget <= c_stalls_over_budget + 32'd1;
if (current_stall_ns > 32'(FIFO_NS)) begin
c_stalls_over_fifo <= c_stalls_over_fifo + 32'd1;
// Section 10: 148.8 minimum-size frames per microsecond of
// overshoot, scaled by the mean frame size.
frames_lost_estimate <= frames_lost_estimate +
((current_stall_ns - 32'(FIFO_NS)) * 32'd1488) /
(32'd10000 * 32'(mean_frame_octets) / 32'd64);
end
current_stall_ns <= '0;
end
end
end
assign within_budget = (longest_stall_ns <= 32'(BUDGET_NS));
assign within_fifo = (longest_stall_ns <= 32'(FIFO_NS));
endmoduleClassification: a duration histogram collapsed to three counters and a loss estimate.
What it teaches: that the three thresholds are three different statements and a design needs all three. within_budget says the stalls fit what Chapter 19.6 §8 planned for; within_fifo says they fit what the buffer can absorb — and between them is Chapter 19.5 §3's 0.620 µs of margin, which is allocated to the pause round trip and the synchroniser reserve rather than to stalls. A design living in that margin is losing nothing and has spent somebody else's budget.
And it teaches that longest_stall_ns matters more than c_stalls. A thousand stalls of 300 ns cost nothing; one stall of 10 µs costs 1 098 frames. The loss is a function of the maximum rather than of the mean or the count, which is the opposite of how a stall counter is usually read.
Deliberately simplified: tick_ns at nanosecond granularity is a 1 GHz reference the design does not have — a real implementation counts in its own clock and scales. The frames_lost_estimate arithmetic divides twice with integer truncation and will read zero for mean frame sizes above about 6 400 octets. And consumer_stalled is an input, so the block does not say what "stalled" means: Chapter 19.6 §11's arbiter, a deasserted RREADY, or a descriptor fetch that has not returned are three different conditions with the same consequence.
Production implication: that last ambiguity is worth resolving in the port list rather than in the documentation. Three stall sources produce identical FIFO behaviour and completely different fixes — an arbitration policy, an interconnect, a descriptor ring — and a single consumer_stalled bit merges them exactly as Chapter 21.2 §11 warned about merging discard reasons. Three bits instead of one, one per source, and the stall's duration histogram becomes attributable. It is the same three-bit argument Chapter 21.6 §13 made about drop reasons, in a different block.
12. 148.8 Frames per Microsecond, and Where the Budget Goes
Chapter 19.5 §3 sized the receive FIFO as a sum of three terms. This section is what each term is for and what spending it costs.
| Term | Beats | Time | What it covers |
|---|---|---|---|
| the memory stall allowance | 391 | 2.002 µs | Chapter 19.6 §8's consumer stall |
| the pause round trip | 123 | 0.630 µs | Chapter 14.2's headroom |
| the synchroniser reserve | 4 | 0.020 µs | clock-domain crossing |
| rounded down to | 512 | 2.621 µs | the FIFO |
The three terms are additive and they are not interchangeable.
A stall of 2.4 µs loses no frames and consumes 0.4 µs of the pause round trip's headroom. Nothing is dropped and a pause frame sent at that moment arrives too late.
Which makes the budget's boundaries three different failures rather than one gradient.
| Longest stall | Frames lost | What else is affected |
|---|---|---|
| below 2.002 µs | 0 | nothing — this is the design point |
| 2.002 to 2.621 µs | 0 | the pause headroom, partly spent |
| above 2.621 µs | 148.8 per µs | everything |
Row two is the invisible failure and it is the one a stall counter reports as healthy. No frames are lost, every counter is clean, and the link's flow control has quietly lost its margin — so the next event that would have been absorbed is not.
And the loss rate itself is worth putting beside the things it is usually compared against.
| Frames lost | |
|---|---|
| a 10 µs stall | 1 098 |
| a 1-in-10⁷ bit error rate for a second, at minimum size | about 15 |
| Chapter 21.5's duplex mismatch at 10% load, per second | about 13.6 million |
| a 1 ms scheduling gap | 148 419 |
Row one against row two is the comparison that reframes a stall. A ten-microsecond interconnect hiccup loses seventy times more frames than a second of a marginal channel — and it leaves no error counter moving at all, because Chapter 21.6 established that a FIFO drop is a class G drop. The loudest fault in Module 21 is silent and the quietest one is loud.
Row four is the number to put in a driver review. A millisecond is an ordinary scheduling quantum, an ordinary interrupt-coalescing interval and an ordinary NAPI budget; at 100 Gb/s it is a hundred and forty-eight thousand frames. The mitigation is not a bigger FIFO — a millisecond of FIFO at 100 Gb/s is 12.5 MB — it is never stalling for a millisecond, which is a software property and is why Chapter 19.5 §3 budgeted 2 µs rather than 1 ms.
| FIFO needed to absorb it | |
|---|---|
| 2.0 µs | 32 KiB — built |
| 10 µs | 122 KiB |
| 1 ms | 12.5 MB — not a buffer, a memory |
The curve is linear and the answer is not a buffer. Chapter 19.5 §3's sizing is an argument that beyond a few microseconds the correct response is flow control rather than storage, and the pause round trip in the FIFO's own budget is that argument made into 123 beats.
13. RTL 6 — The Cross-Path Bridge
// ---------------------------------------------------------------------
// crosspath_bridge -- the block that exists because a transmit-path
// stall becomes a receive-path loss with no signal connecting them.
//
// Chapter 19.6 Section 22's first complaint: the buffer that overflowed
// is on the TRANSMIT read path and the frames lost are on the RECEIVE
// path. The only coupling is a shared RREADY, which appears in neither
// path's interface.
// ---------------------------------------------------------------------
module crosspath_bridge
import dmadiag_pkg::*;
(
input logic clk,
input logic rst_n,
// Transmit read path.
input logic tx_reorder_full,
input logic tx_rready,
input logic buffer_undersized, // Chapter 19.6 Section 19
// Receive write path.
input logic rx_fifo_full,
input logic rx_frame_dropped,
// The shared resource.
input logic shared_interconnect,
output logic [31:0] c_rx_drops_during_tx_stall,
output logic [31:0] c_rx_drops_total,
output logic [15:0] coincidence_ppt,
output logic cross_path_coupled,
output logic coupling_invisible_locally
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_rx_drops_during_tx_stall <= '0; c_rx_drops_total <= '0;
end else if (rx_frame_dropped) begin
c_rx_drops_total <= c_rx_drops_total + 32'd1;
// The correlation nobody looks for: a receive drop while the
// TRANSMIT path's reorder buffer is full.
if (tx_reorder_full || !tx_rready)
c_rx_drops_during_tx_stall <= c_rx_drops_during_tx_stall + 32'd1;
end
end
always_comb begin
coincidence_ppt = (c_rx_drops_total == 32'd0) ? 16'd0
: 16'((c_rx_drops_during_tx_stall * 32'd1000) / c_rx_drops_total);
// Nearly every receive drop happening while the transmit reorder
// buffer is full is not a coincidence.
cross_path_coupled = shared_interconnect &&
(c_rx_drops_total > 32'd64) &&
(coincidence_ppt > 16'd800);
// Section 20's rejected class: neither path's interface contains
// the other's signals, so a property scoped to one path cannot
// express this and a reviewer of one path cannot see it.
coupling_invisible_locally = shared_interconnect;
end
endmoduleClassification: a correlation between two paths that share nothing in their interfaces.
What it teaches: that coincidence_ppt is a number nobody computes because nobody thinks to. A receive drop and a transmit reorder buffer are in different blocks, owned by different engineers, reviewed in different documents — and eight hundred parts per thousand of receive drops happening while the transmit buffer is full is a proof of coupling that costs two counters and a division.
And it teaches that coupling_invisible_locally is a property of the design's structure rather than of its state. It is true whenever the two paths share an interconnect, which is always, and it means: no property written against the receive path's interface can express this fault, because the transmit path's signals are not in that interface. Section 20's rejected class is exactly that.
Deliberately simplified: the correlation is instantaneous rather than windowed, so a receive drop just after the transmit buffer drains is not counted. shared_interconnect is a parameter-like input describing the SoC's topology. And the block cannot distinguish "the transmit stall caused the receive drop" from "a third thing caused both" — which is the honest position, because a busy interconnect does cause both.
Production implication: the third-cause ambiguity is not a weakness of the measurement; it is the correct reading. Chapter 19.6 §22's first complaint traced it: an undersized transmit reorder buffer deasserts RREADY, which stalls the shared read channel, which delays the receive path's descriptor fetches, which fills the receive FIFO. The chain has four links and buffer_undersized at elaboration identifies it before any of them happens — which is why that chapter concluded an elaboration-time check is worth more than any runtime counter. This block is what to do when the elaboration check was not there.
14. What a DMA Diagnosis Must Never Do
Six prohibitions. Two are about the outstanding count, two about the stall, and two about where a symptom's cause lives.
| Never | Because | |
|---|---|---|
| 1 | read the outstanding count's value as a diagnosis | Section 5 — the regime discriminates, the number does not |
| 2 | conclude a fault from a count below Little's law | a slower interconnect gives a lower count and no fault |
| 3 | report a stall's mean or count | Section 11 — the loss is a function of the maximum |
| 4 | treat the 0.620 µs margin as spare | Section 12 — it is the pause round trip's |
| 5 | look for a receive drop's cause on the receive path | Section 13 — the coupling is a shared interconnect |
| 6 | conclude "the DMA is fine" from a healthy frame count | Section 8 — three failures deliver every frame |
Row five is the prohibition that this chapter exists to state. Chapter 19.6 §22's first complaint traced a four-link chain: an undersized transmit reorder buffer deasserts RREADY, the shared read channel stalls, the receive path's descriptor fetches are delayed, the receive FIFO fills. A team investigating receive drops does not look at the transmit reorder buffer and has no reason to — it is not in the receive path's interface, its document or its owner's remit.
Row six is the prohibition with the worst consequences. Stale ownership, a descriptor written early and a per-burst commit all deliver every frame — frames_in equals frames_delivered, the outstanding count matches Little's law, every counter in Module 21 is clean — and the driver reads frames with stale tails. "No drops" and "the DMA is fine" are different statements and nothing in a counter distinguishes them.
And the two that look like statistical hygiene and are not:
| Why it is a prohibition | |
|---|---|
| row three | a thousand 300 ns stalls cost nothing; one 10 µs stall costs 1 098 frames |
| row four | a stall of 2.4 µs drops nothing and spends the flow-control headroom |
Both report a number that is true and is about the wrong moment — which is what all six share: in this chapter the extremes matter and the averages do not.
15. RTL 7 — DMA Diagnostic Telemetry
// ---------------------------------------------------------------------
// dmadiag_telemetry -- three groups: the regime, the cost, and the
// coupling.
//
// The cost group is the one no other chapter in Module 21 has, because
// this is the only fault whose damage is a deterministic function of a
// measurable duration.
// ---------------------------------------------------------------------
module dmadiag_telemetry
import dmadiag_pkg::*;
(
input logic clk,
input logic rst_n,
input ocount_e regime,
input fmask_t candidates,
input logic regime_stable,
input logic [31:0] longest_stall_ns,
input logic [31:0] frames_lost_estimate,
input logic [15:0] commit_ratio_x100,
input logic test_is_blind,
input logic [15:0] coincidence_ppt,
input logic cross_path_coupled,
input logic buffer_undersized,
// Regime.
output logic [2:0] n_candidates,
output logic [1:0] regime_out,
output logic single_failure,
// Cost.
output logic [31:0] worst_stall_ns,
output logic [31:0] overshoot_ns,
output logic [31:0] frames_lost,
output logic headroom_spent,
output logic loss_occurring,
// Coupling.
output logic look_at_transmit_path,
output logic elaboration_check_would_have_caught_it
);
assign n_candidates = 3'(popcount8(candidates));
assign regime_out = regime[1:0];
assign single_failure = (n_candidates == 3'd1) && regime_stable;
assign worst_stall_ns = longest_stall_ns;
assign frames_lost = frames_lost_estimate;
always_comb begin
overshoot_ns = (longest_stall_ns > 32'(FIFO_NS))
? (longest_stall_ns - 32'(FIFO_NS)) : 32'd0;
// Section 12's row two: no frames lost and the pause round trip's
// headroom partly consumed. The invisible failure.
headroom_spent = (longest_stall_ns > 32'(BUDGET_NS)) &&
(longest_stall_ns <= 32'(FIFO_NS));
loss_occurring = (overshoot_ns != 32'd0);
// Section 13: the receive path's drops have a transmit-path cause.
look_at_transmit_path = cross_path_coupled;
// Chapter 19.6 Section 22: buffer_undersized fires at elaboration
// and is worth more than every runtime counter in this block.
elaboration_check_would_have_caught_it = buffer_undersized;
end
endmoduleClassification: a reporting block whose middle group converts a duration into a frame count.
What it teaches: that headroom_spent names a failure with no symptom. A longest stall between 2.002 and 2.621 µs drops nothing — every counter is clean, the link is perfect — and Chapter 19.5 §3's pause round trip has lost part of its allowance. The next event that would have been absorbed will not be, and the only warning is a comparison between two constants and one measurement.
And it teaches that elaboration_check_would_have_caught_it is the most humbling output in Module 21. Chapter 19.6 §19's buffer_undersized is a compile-time comparison of a parameter against (O − 1) × burst — it needs no traffic, no fault and no run — and when it is set, every counter in this block is describing a consequence of something that was decidable before the design was built.
Deliberately simplified: regime_out truncates a three-valued enum to two bits and loses O_UNKNOWN. frames_lost is passed through from an estimate with two integer divisions. And there is no best_possible — the ceiling output the previous three chapters all had — because this chapter's ceiling is not a property of the instrumentation but of whether the ordering failures are present, which cannot be known in advance.
Production implication: the missing ceiling is worth noting rather than adding. Chapter 21.1 §15, Chapter 21.4 §15 and Chapter 21.6 §15 all computed a ceiling from which counters exist — a platform fact, knowable at step zero. Here the three normal-regime failures are invisible to every counter regardless of which exist, so the ceiling depends on whether the fault is one of those three, which is exactly what is being diagnosed. The honest output is a warning: a healthy regime and healthy counters do not exonerate the DMA path.
16. RTL 8 — The DMA Diagnostic Conformance Monitor
// ---------------------------------------------------------------------
// dmadiag_conformance_monitor -- six verdicts, and the first one fires
// at elaboration on a design that will lose frames months later.
// ---------------------------------------------------------------------
module dmadiag_conformance_monitor
import dmadiag_pkg::*;
#(
parameter int REORDER_BYTES = 12288,
parameter int OUTSTANDING = 13,
parameter int BURST_BYTES = 1024
)(
input logic clk,
input logic rst_n,
input ocount_e regime,
input logic regime_stable,
input logic [31:0] longest_stall_ns,
input logic test_is_blind,
input logic [15:0] commit_ratio_x100,
input logic tail_visible,
input logic stale_read_suspected,
input logic [15:0] coincidence_ppt,
output logic buffer_undersized,
output logic headroom_spent,
output logic commit_test_blind,
output logic tail_not_mirrored,
output logic ordering_untested,
output logic regime_not_settled,
output logic diagnosis_sound
);
// Chapter 19.6 Section 19, at elaboration: the reorder buffer must
// hold (O - 1) bursts or the read channel will stall.
localparam int REQUIRED_BYTES = (OUTSTANDING - 1) * BURST_BYTES;
assign buffer_undersized = (REORDER_BYTES < REQUIRED_BYTES);
// Section 12's invisible failure.
assign headroom_spent = (longest_stall_ns > 32'(BUDGET_NS)) &&
(longest_stall_ns <= 32'(FIFO_NS));
assign commit_test_blind = test_is_blind;
// Section 3: sixteen flops would turn failure 5 from an inference
// into a comparison.
assign tail_not_mirrored = !tail_visible;
// Section 8: failure 3 changes neither counter, so a clean commit
// ratio is not evidence about ordering.
assign ordering_untested = (commit_ratio_x100 >= 16'd95) &&
(commit_ratio_x100 <= 16'd105);
assign regime_not_settled = !regime_stable;
assign diagnosis_sound = !regime_not_settled && !commit_test_blind;
always_ff @(posedge clk) begin
if (rst_n && buffer_undersized)
$display("[dmadiag] reorder buffer %0d B below the required %0d B -- this was decidable at elaboration",
REORDER_BYTES, REQUIRED_BYTES);
end
endmoduleClassification: an auditor whose first verdict is a parameter comparison and whose others are run-time.
What it teaches: that buffer_undersized is the only verdict in Module 21 that is decidable before the design exists. (O − 1) × burst against the reorder buffer's size is two parameters and a multiplication — Chapter 19.6 §19 derived it and §22's first complaint showed its consequence — and a design that fails it will lose receive frames during DRAM refreshes, months later, for a reason four links away.
And it teaches that ordering_untested asserts on every healthy design, deliberately. A commit ratio near 1.00 is what correct commits look like and also what failure 3 looks like, because a descriptor written before the data still commits once per frame. The verdict says "this test had nothing to say about ordering", which is true on every run where the ratio is clean — and folding it into diagnosis_sound would make every sound diagnosis unsound.
Deliberately simplified: OUTSTANDING is a parameter where Chapter 19.6 §4 derives it from the bandwidth-latency product, so a design whose interconnect is slower than assumed passes the check and fails in the field. headroom_spent duplicates the telemetry's. And diagnosis_sound omits buffer_undersized, which is right: a design with an undersized buffer can still be correctly diagnosed — the diagnosis will simply be "the buffer is undersized".
Production implication: the OUTSTANDING parameter is the weak link and Chapter 19.6 §4 gives the fix. The required outstanding count is R × L / burst where L is the memory latency the interconnect actually delivers — 13 bursts at 1 µs, 68 at 5.5 µs — and a design parameterised on an assumed latency is a design whose reorder buffer is sized for a system it may not be integrated into. The check should take the latency as a parameter and the integration should supply it, which turns a silent field failure into a build error in somebody else's repository.
17. The Cross-Path Coupling, Measured
Chapter 19.6 §22's first complaint is the strangest diagnosis in the whole track, and it is worth following link by link.
| Link | What happens | Which path |
|---|---|---|
| 1 | the reorder buffer is below (O − 1) bursts | transmit read |
| 2 | it fills, and RREADY deasserts | transmit read |
| 3 | the shared read channel stalls | neither — the interconnect |
| 4 | receive descriptor fetches are delayed | receive write |
| 5 | the receive FIFO fills past 2.621 µs | receive |
| 6 | frames are dropped | receive |
Six links, two paths, and the only thing connecting them is link three — a shared channel that appears in neither path's interface.
Which makes the fault invisible to three different kinds of review.
| Reviewer | Sees | Misses |
|---|---|---|
| the receive path's owner | frames dropped, FIFO full | why the fetches were slow |
| the transmit path's owner | a full reorder buffer | that anything was dropped |
| the integrator | both, in different documents | that they are the same event |
And the correlation that proves it is two counters. Section 13's coincidence_ppt — receive drops occurring while the transmit reorder buffer is full, as a share of all receive drops — is above 800 parts per thousand when the coupling is real and near the duty cycle of the transmit buffer's fullness when it is not.
| Coupled | Coincidental | |
|---|---|---|
| receive drops during a transmit stall | most of them | as often as the stall occurs |
coincidence_ppt | > 800 | ≈ the stall's duty cycle |
| cost to measure | two counters and a division | the same |
The asymmetry in the two paths is what makes the coupling one-directional, and Chapter 19.6 §8 derived it.
| Transmit path | Receive path | |
|---|---|---|
| what a stall does first | the transmit FIFO drains — 0.74 µs | the receive FIFO fills — 2.00 µs |
| what happens at the end | the wire idles | frames are dropped |
| recoverable? | yes, completely | no |
| who can wait | the transmit path can | the receive path cannot |
Time is only useful to a path that can spend it waiting. The transmit path has 0.74 µs and does not need it; the receive path has 2.00 µs and cannot use it, because its arrival rate does not depend on anything this design does.
So a shared stall costs the transmit path throughput and the receive path frames, and the receive path is the one that has no choice — Chapter 19.1 §11's "the receive path has no backpressure" stated as a consequence.
And the elaboration check dominates every runtime instrument here.
| Instrument | When it fires | What it costs |
|---|---|---|
buffer_undersized | at elaboration | two parameters and a multiply |
coincidence_ppt | after the drops | two counters |
| the receive drop counter | after the drops | already there |
| a capture | hours later | hours |
Row one is Chapter 19.6 §22's conclusion and it is the module's cleanest example of the general principle: a fault that is decidable from parameters should never be diagnosed from counters, and the cost difference is a multiplication against a field investigation.
And the coupling generalises past the reorder buffer, which is why Section 20 refuses a property rather than a design.
| Shared resource | Couples | In either path's interface? |
|---|---|---|
| the read channel | transmit reads and descriptor fetches | no |
| the write channel | receive writes and descriptor writebacks | no |
| the outstanding-ID space | every path using the same AXI ID | no — Chapter 19.6 §6 |
| memory bandwidth itself | everything | no |
Four shared resources, none of them in a path's signal list, and every one of them can turn one path's pressure into another path's loss. Chapter 19.6 §6's ID space is the subtlest: two logically independent streams that share an AXI ID must be returned in order, so a slow response on one blocks the other — a coupling created by a four-bit field rather than by a buffer.
18. What the Diagnosis Assumes
Nine assumptions. Three are about the counter, three about the stall and three about the ring — and the first is false at every rate except the one it was written for.
| Assumption | From | If false | |
|---|---|---|---|
| 1 | the FIFO is 2.621 µs of wire time | Chapter 19.5 §3 at 100 Gb/s | at 10 Gb/s it is 3.277 µs and the loss rate is a tenth |
| 2 | the outstanding count is visible | Chapter 19.6 §5 | the whole three-class method is unavailable |
| 3 | a healthy count visits zero and the limit | burst-y traffic | a smooth workload never touches either |
| 4 | the eight failures are mutually exclusive | convenience | two at once give a mixed regime |
| 5 | consumer_stalled names one condition | the port list | Section 11 — three sources, one bit |
| 6 | the driver's tail pointer is mirrored | most designs do not | failure 5 is an inference, not a comparison |
| 7 | the frames are larger than a burst | nothing | Section 8 — the commit test is blind |
| 8 | the memory latency matches the design's parameter | integration | the reorder buffer is sized for another system |
| 9 | the loss is a function of the longest stall | Chapter 19.5 §3 | back-to-back stalls compound before the FIFO refills |
Row nine is the assumption this chapter's arithmetic quietly makes and it is the one that understates the loss. Section 10's table prices each stall independently, as if the FIFO were empty when it began. Two stalls a microsecond apart do not give the buffer time to drain, so the second one starts partly full and overshoots sooner:
| Two 2.0 µs stalls | |
|---|---|
| 1 ms apart | the FIFO drains completely; no loss |
| 1 µs apart | the second starts with 1.0 µs of occupancy |
| effective capacity for the second | 1.621 µs |
| frames lost | 56 at minimum size |
Two stalls that are individually within budget lose frames when they are close together, and nothing in Section 11's tracker sees it — it measures each stall's duration and not the FIFO's occupancy when it began. The instrument that would is Chapter 19.5 §15's occupancy telemetry, which exists and is in a different block.
Row one is the rate-dependence and it cuts both ways. At 10 Gb/s a 512-beat FIFO at 64 bits per beat is 3.277 µs — a 25% larger budget — and 14.88 minimum-size frames per microsecond rather than 148.8. So the same stall is more survivable and ten times cheaper: a 10 µs stall costs 1 098 frames at 100 Gb/s and 100 at 10 Gb/s.
And three things deliberately not assumed:
| Not assumed | Why not |
|---|---|
| that a healthy frame count exonerates the DMA | Section 8's three ordering failures |
| that a receive drop has a receive cause | Section 17's six-link chain |
| that a stall within the FIFO is free | Section 12 — it spends the pause headroom |
Row three is the one that is hardest to hold onto, because the instrument reports zero drops and the design is measurably worse off. Chapter 19.5 §3's FIFO is a sum of three allocations and a stall spends them in order — the memory budget first, then the pause round trip, then the synchroniser reserve — and only the third overflow drops a frame.
And one assumption the chapter shares with Chapter 21.6 and should not: that the eight failures occur one at a time.
| Two failures at once | |
|---|---|
| an exhausted ring and an undersized reorder buffer | zero, then pinned, alternating |
| the regime classifier's reading | whichever it saw in the last window |
| the candidate mask | wrong in both directions |
| what would detect it | a regime transition counter |
Row four is the instrument this chapter does not have and Section 21's run E shows why it matters. A memory system that stops responding pins the count while requests are outstanding and then drops it to zero when the ring exhausts — two regimes in one fault, in sequence — and the classifier reports whichever was last. A transition counter is about sixteen flops and it turns a sequence into evidence rather than into a race between two readings.
19. The Cost, Accounted
A DMA diagnosis is counters over an existing counter, plus one array that is a copy of the ring.
| Block | Flops | Nature |
|---|---|---|
dmadiag_pkg | 0 | eight failures, three regimes, four constants |
desc_ownership_model | ~64 | four counters |
outstanding_regime | ~64 | two extrema and two occupancy counters |
ownership_transition_monitor — 256-entry ring | ~320 | the released array, and it scales with the ring |
commit_completeness | 0 | combinational |
stall_budget_tracker | ~161 | durations and an estimate |
crosspath_bridge | ~64 | two counters |
dmadiag_telemetry | 0 | combinational |
dmadiag_conformance_monitor | 0 | combinational |
| total | ~673 flops |
One block is 48% of it and it scales with the ring. The ownership transition monitor's released array is one flop per descriptor — 256 for a small ring and four thousand for a 4 096-entry one — which makes it the only structure in Module 21 whose cost is set by a software data structure.
And the additions to a design are three, with very different value.
| Addition | Flops | Buys |
|---|---|---|
| mirror the driver's tail pointer | ~16 | failure 5 becomes a comparison |
| three stall-source bits instead of one | ~3 | Section 11's attribution |
| a release-to-reread cycle counter | ~16 | failure 2 becomes a proof |
| all three | ~35 | 0.25% of the datapath |
Thirty-five flops, and none of them is a counter in the usual sense — a mirrored pointer, three source bits and a timestamp. Compare what the chapter's own machinery costs to observe the same things indirectly: 673 flops, and it still cannot separate failures 1 and 7 or detect failure 3 at all.
Thirty-five flops of representation beat 673 flops of inference, and that is the fourth time Module 21 has reached the same conclusion.
| Chapter | The representation it wanted | Flops |
|---|---|---|
| Chapter 21.2 §19 | c_unclassified | ~99 |
| Chapter 21.3 §19 | the residue, logged per failing frame | ~0 |
| Chapter 21.4 §19 | a latched gate-vector snapshot | ~5 |
| Chapter 21.6 §13 | a three-bit drop reason | ~3 |
| this chapter | a mirrored pointer and three source bits | ~35 |
Five chapters, five tiny additions, about 142 flops between them — one per cent of the datapath — and every one of them replaces a substantial inference apparatus with a read.
Module 21's running bill, seven chapters in:
| Chapter | Flops to add | Share |
|---|---|---|
| Chapter 21.1 §19 | ~297 | 2.1% |
| Chapter 21.2 §19 | ~426 | 3.0% |
| Chapter 21.3 §19, logged | ~792 | 5.6% |
| Chapter 21.4 §19 | ~71 | 0.5% |
| Chapter 21.5 §19 | ~455 | 3.2% |
| Chapter 21.6 §19 | 0 | 0.0% |
| this chapter | ~35 | 0.25% |
| total | ~2 076 | 14.7% |
And Chapter 21.6 §17's saving still applies, so the module's realistic bill is about 1 977 flops — 14.0% — for every diagnosis seven chapters have built.
20. Properties Worth Asserting, and One Worth Refusing
Thirty-three properties and eight covers, in four groups: the regime, the ownership bit, the stall budget, and the coupling.
Group one — the outstanding count and its three regimes.
// Chapter 19.6 Section 5's counter, read as three mutually exclusive
// regimes rather than as a number.
p_regimes_exclusive:assert property (@(posedge clk) disable iff (!rst_n)
regime_stable |-> ($countones({regime == O_ZERO,
regime == O_PINNED,
regime == O_NORMAL}) == 1));
p_classes_partition:assert property (@(posedge clk) disable iff (!rst_n)
((CLASS_ZERO | CLASS_PINNED | CLASS_NORMAL) == ALL_F) &&
((CLASS_ZERO & CLASS_PINNED) == 8'd0) &&
((CLASS_ZERO & CLASS_NORMAL) == 8'd0) &&
((CLASS_PINNED & CLASS_NORMAL) == 8'd0));
p_zero_is_four: assert property (@(posedge clk) disable iff (!rst_n)
(regime == O_ZERO) |-> (popcount8(candidates) == 4));
p_pinned_is_one: assert property (@(posedge clk) disable iff (!rst_n)
(regime == O_PINNED) |-> (popcount8(candidates) == 1));
p_count_bounded: assert property (@(posedge clk) disable iff (!rst_n)
outstanding <= 8'(MAX_OUTSTANDING));
p_pinned_needs_max: assert property (@(posedge clk) disable iff (!rst_n)
(regime == O_PINNED) |-> (max_seen == 8'(MAX_OUTSTANDING)));
p_regime_needs_win: assert property (@(posedge clk) disable iff (!rst_n)
(regime != O_UNKNOWN) |-> (window > 16'd1000));
p_little_not_used: assert property (@(posedge clk) disable iff (!rst_n)
$stable(expected_by_little) |-> 1'b1);Group two — the ownership bit, which two parties write.
p_release_sets_sw: assert property (@(posedge clk) disable iff (!rst_n)
desc_released |=> released[$past(desc_index)]);
p_fetch_counts: assert property (@(posedge clk) disable iff (!rst_n)
desc_fetched |=> (c_fetches == $past(c_fetches) + 16'd1));
p_empty_fetch: assert property (@(posedge clk) disable iff (!rst_n)
(desc_fetched && !desc_owned_by_hw) |=>
(c_empty_fetches == $past(c_empty_fetches) + 16'd1));
p_exhausted_sticky: assert property (@(posedge clk) disable iff (!rst_n)
ring_exhausted |=> always ring_exhausted);
p_owned_nonneg: assert property (@(posedge clk) disable iff (!rst_n)
(c_fetches >= c_releases) |->
(hw_owned_count == (c_fetches - c_releases)));
p_double_needs_far: assert property (@(posedge clk) disable iff (!rst_n)
!double_ownership);
p_reread_counted: assert property (@(posedge clk) disable iff (!rst_n)
(desc_read && own_bit_read && released[desc_index]) |=>
(c_reread_after_release ==
$past(c_reread_after_release) + 16'd1));
p_stale_threshold: assert property (@(posedge clk) disable iff (!rst_n)
stale_read_suspected |->
(c_reread_after_release > (c_releases >> 2)));Group three — the stall budget, which is three constants and one measurement.
p_budget_lt_fifo: assert property (@(posedge clk) disable iff (!rst_n)
(BUDGET_NS < FIFO_NS) && (MARGIN_NS == (FIFO_NS - BUDGET_NS)));
p_stall_monotone: assert property (@(posedge clk) disable iff (!rst_n)
(consumer_stalled && tick_ns) |=>
(current_stall_ns > $past(current_stall_ns)));
p_longest_is_max: assert property (@(posedge clk) disable iff (!rst_n)
longest_stall_ns >= current_stall_ns ||
consumer_stalled);
p_within_budget: assert property (@(posedge clk) disable iff (!rst_n)
within_budget |-> within_fifo);
p_no_loss_in_fifo: assert property (@(posedge clk) disable iff (!rst_n)
within_fifo |-> (frames_lost_estimate == 32'd0));
p_headroom_window: assert property (@(posedge clk) disable iff (!rst_n)
headroom_spent |-> (!within_budget && within_fifo));
p_overshoot_zero: assert property (@(posedge clk) disable iff (!rst_n)
within_fifo |-> (overshoot_ns == 32'd0));
p_loss_needs_over: assert property (@(posedge clk) disable iff (!rst_n)
loss_occurring |-> (overshoot_ns > 32'd0));
p_elab_check: assert property (@(posedge clk) disable iff (!rst_n)
buffer_undersized |-> (REORDER_BYTES < REQUIRED_BYTES));Group four — the coupling, which is Section 20's subject.
p_coupling_needs_shared:assert property (@(posedge clk) disable iff (!rst_n)
cross_path_coupled |-> shared_interconnect);
p_coincidence_bounded:assert property (@(posedge clk) disable iff (!rst_n)
coincidence_ppt <= 16'd1000);
p_drops_counted: assert property (@(posedge clk) disable iff (!rst_n)
rx_frame_dropped |=>
(c_rx_drops_total == $past(c_rx_drops_total) + 32'd1));
p_during_is_subset: assert property (@(posedge clk) disable iff (!rst_n)
c_rx_drops_during_tx_stall <= c_rx_drops_total);
p_invisible_flag: assert property (@(posedge clk) disable iff (!rst_n)
shared_interconnect |-> coupling_invisible_locally);
p_look_at_tx: assert property (@(posedge clk) disable iff (!rst_n)
cross_path_coupled |-> look_at_transmit_path);
p_commit_ratio_one: assert property (@(posedge clk) disable iff (!rst_n)
commits_correct |-> (commit_ratio_x100 >= 16'd95));
p_blind_below_burst:assert property (@(posedge clk) disable iff (!rst_n)
(mean_frame_octets <= 16'(BURST_OCTETS)) |-> test_is_blind);
p_sound_excludes: assert property (@(posedge clk) disable iff (!rst_n)
diagnosis_sound |-> (!regime_not_settled && !commit_test_blind));And eight covers, because six of these states need a fault no frame can produce.
c_regime_zero: cover property (@(posedge clk) regime == O_ZERO);
c_regime_pinned: cover property (@(posedge clk) regime == O_PINNED);
c_headroom_spent: cover property (@(posedge clk) headroom_spent);
c_loss: cover property (@(posedge clk) loss_occurring);
c_coupled: cover property (@(posedge clk) cross_path_coupled);
c_stale_read: cover property (@(posedge clk) stale_read_suspected);
c_per_burst: cover property (@(posedge clk) commits_per_burst);
c_empty_fetch: cover property (@(posedge clk) desc_fetched && !desc_owned_by_hw);21. Verification Scenarios
Fifty-eight scenarios, plus a five-run directed test whose stimulus is the memory system rather than the wire — because Chapter 20.5 §12 proved no frame can produce this fault.
The outstanding count's three regimes — 11 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 1 | steady traffic, 1 µs memory latency, 1 KiB bursts | count near 13 — normal |
| 2 | the same at 5.5 µs latency | near 68 — still normal |
| 3 | the ring emptied | count falls to zero |
| 4 | RREADY held low | count pinned at the limit |
| 5 | a stale ownership read | count normal throughout |
| 6 | a per-burst commit | count normal throughout |
| 7 | the count at zero for 10 cycles | regime still normal — it visits zero |
| 8 | at zero for three quarters of a window | regime O_ZERO |
| 9 | the three class masks intersected | pairwise empty |
| 10 | their union | all eight failures |
| 11 | fewer than 1 000 cycles | O_UNKNOWN |
The ring and ownership — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 12 | a fetch finding the bit clear | c_empty_fetches increments |
| 13 | one such fetch, ever | ring_exhausted sticky |
| 14 | the MAC stops fetching when empty | no evidence at all — the blind case |
| 15 | a release then an immediate re-read as hw-owned | c_reread_after_release |
| 16 | a quarter of releases re-read | stale_read_suspected |
| 17 | the driver genuinely refilling | the same signature |
| 18 | double_ownership | always low — it needs the driver's view |
| 19 | head minus tail exceeding the ring size | ptr_diverged, if the tail is mirrored |
| 20 | the tail not mirrored | tail_not_mirrored; failure 5 is an inference |
| 21 | failures 1 and 7, from the MAC's side | identical |
The commit test — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 22 | 1 518-octet frames, 1 KiB bursts, per-frame commit | ratio 1.00 |
| 23 | the same with a per-burst commit | ratio 2.00 |
| 24 | 645.6-octet mean, per-burst commit | ratio 1.00 — blind |
| 25 | 64-octet frames | blind |
| 26 | 9 000-octet frames, per-burst | ratio 9.00 |
| 27 | test_is_blind on scenario 24 | asserts |
| 28 | failure 3 — descriptor before data | ratio 1.00; neither counter moves |
| 29 | ordering_untested on it | asserts, and says nothing useful |
| 30 | slowing the interconnect | failure 3 disappears — Chapter 19.6 §22 |
The stall budget — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 31 | a 350 ns stall | within budget; nothing lost |
| 32 | a 2.000 µs stall | exactly the budget; nothing lost |
| 33 | a 2.400 µs stall | headroom_spent; nothing lost |
| 34 | a 2.621 µs stall | exactly the FIFO; nothing lost |
| 35 | a 2.622 µs stall | loss begins |
| 36 | a 10 µs stall | 1 098 minimum-size frames |
| 37 | the same, maximum-size traffic | 60 frames, and about the same octets |
| 38 | a 1 ms stall | 148 419 frames |
| 39 | a thousand 300 ns stalls | nothing lost; c_stalls = 1 000 |
| 40 | two 2.0 µs stalls, 1 µs apart | 56 frames — Section 18's row nine |
The coupling — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 41 | an undersized transmit reorder buffer | buffer_undersized at elaboration |
| 42 | the same design, at run time | receive frames dropped |
| 43 | coincidence_ppt | above 800 |
| 44 | cross_path_coupled | asserts |
| 45 | a receive-path-only property | passes throughout |
| 46 | the receive path's reviewer | exonerates it, correctly |
| 47 | a shared-channel progress property | fails — the right scope |
| 48 | the transmit path idle throughout the run | the coupling cannot occur |
| 49 | c_both_paths_busy on that run | never fires |
Rates and blindness — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 50 | the FIFO at 100 Gb/s | 2.621 µs |
| 51 | 512 beats of 64 bits at 10 Gb/s | 3.277 µs |
| 52 | a 10 µs stall at 10 Gb/s | 100 frames, not 1 098 |
| 53 | the constants unparameterised | a tenfold over-estimate at 10 Gb/s |
| 54 | a smooth workload | the count never visits zero or the limit |
| 55 | regime on that workload | O_NORMAL by default |
| 56 | one consumer_stalled bit, three sources | attribution impossible |
| 57 | three source bits | the stall histogram becomes attributable |
| 58 | elaboration_check_would_have_caught_it | the humbling output |
And the directed test, because random stimulus will not produce it.
The case: five memory-system behaviours, applied to a design whose wire-side stimulus is identical in every run.
Chapter 20.5 §10 divided injections into what a wire can produce and what needs a fault inside the design. This test needs neither. Every run sends the same conformant frames at the same rate; what varies is what the memory model does, and Chapter 20.5 §12 proved that no arrangement of frames can substitute for it.
| Run | The memory model | Duration |
|---|---|---|
| A | responds within 400 ns, always | — |
| B | a 350 ns refresh every 7.8 µs | within budget |
| C | a 2.4 µs stall once per millisecond | within the FIFO |
| D | a 10 µs stall once per millisecond | beyond it |
| E | stops responding for 1 ms | far beyond it |
The oracle is four-part and none of it looks at a frame's contents.
| Part | A | B | C | D | E |
|---|---|---|---|---|---|
| outstanding regime | normal | normal | normal | normal | pinned, then zero |
| frames lost | 0 | 0 | 0 | 1 098 | 148 419 |
headroom_spent | no | no | YES | yes | yes |
| every error counter | zero | zero | zero | zero | zero |
Row four is the control and it is Chapter 21.6's finding restated: a DMA drop is a class G drop, so a hundred and forty-eight thousand lost frames move no error counter at all.
Run C is the chapter's most useful run and it is the one nobody writes. A 2.4 µs stall drops nothing — every counter is clean, the link is perfect — and Chapter 19.5 §3's pause round trip has lost 0.4 µs of its 0.63 µs allowance. The failure is invisible in every instrument except a comparison of the longest stall against two constants, and a regression that checks for dropped frames passes it.
Run E's regime transition is the other thing worth watching. The count pins while requests are outstanding and unanswered, then falls to zero when the ring exhausts because nothing further can be written — two of the three regimes in one fault, in sequence, which Section 5's classifier reports as whichever it saw last. A regime that has changed is itself evidence, and nothing in this chapter records the transition.
22. Debugging a DMA Path
Six complaints.
Complaint 1 — "frames are dropped during DRAM refreshes."
| Check | If yes | Meaning |
|---|---|---|
what is tRFC? | about 350 ns | the FIFO absorbs 2 621 |
buffer_undersized? | often | Chapter 19.6 §19, at elaboration |
| is the longest stall much larger than a refresh? | yes | the refresh is not the stall |
| is the drop on the receive side? | yes | and the buffer is on the transmit side |
A refresh has a factor of seven of margin, so a design losing frames to one has a buffer far below Chapter 19.5 §3's sizing — and the buffer at fault is usually Chapter 19.6 §19's transmit reorder buffer rather than the receive FIFO, which is Section 17's six-link chain.
Complaint 2 — "the outstanding count is 9 and the design says 13."
| Check | If yes | Meaning |
|---|---|---|
| is the regime normal? | yes | there is no fault here |
| what is the measured memory latency? | 700 ns | Little's law gives 9 |
| is the design losing frames? | no | then 9 is correct |
| is the reorder buffer sized for 13? | yes | and that is fine — it is oversized |
The value is not a diagnosis. Chapter 19.6 §4's figure is R × L / burst and L is what the interconnect delivers, not what the design assumed — a faster interconnect gives a lower count and no fault at all. Only the regime matters, and normal is normal.
Complaint 3 — "the driver sees frames with stale tails."
| Check | If yes | Meaning |
|---|---|---|
| is the frame multi-burst? | yes | the commit test can see it |
c_frames_committed against the frame count | about 2 | committing per burst |
| is the mean frame below the burst size? | no | otherwise the test is blind |
| does slowing the interconnect fix it? | yes | failure 3 instead — the ordering |
Rows two and four separate the two ordering failures. A commit ratio near the bursts-per-frame figure is failure 4; a ratio of exactly 1.00 that disappears when the interconnect slows is failure 3 — and Chapter 19.6 §22 noted that the second is reported as intermittent because it is rate-dependent.
Complaint 4 — "the ring is always empty and the driver says it is refilling."
| Check | If yes | Meaning |
|---|---|---|
c_empty_fetches climbing? | yes | the MAC finds the bit clear |
| is the tail pointer mirrored? | no | failure 5 cannot be ruled out |
| when did the driver last run? | not collected | failure 7 cannot be ruled out |
| how many candidates remain? | two, of the four | and both need host-side evidence |
Failures 1, 5 and 7 all produce an empty ring from the MAC's side, and two of the three are distinguished only by evidence in the host: a mirrored tail pointer — sixteen flops — and a software timestamp of the last refill. Neither is usually collected, and the combination is the second-commonest real DMA complaint after an undersized buffer.
Complaint 5 — "no frames are dropped but throughput is down."
| Check | If yes | Meaning |
|---|---|---|
| is the regime pinned? | yes | responses are not being consumed |
is RREADY low? | yes | Chapter 19.6 §19's reorder buffer |
| is the transmit FIFO draining? | yes — 0.74 µs | the wire idles |
| is anything lost? | no | Chapter 19.6 §8's asymmetry |
This is the transmit-path half of the same stall and it costs throughput rather than frames. Chapter 19.6 §8 put it exactly: the transmit path drains in 0.74 µs and recovers completely; the receive path fills in 2.00 µs and loses frames — and the path with less time is the one that can afford it.
Complaint 6 — "we fixed the reorder buffer and the receive drops stopped."
| Check | If yes | Meaning |
|---|---|---|
| did anything change on the receive path? | no | Section 17's chain |
was coincidence_ppt above 800 before? | if it was measured | the correlation was there |
would buffer_undersized have fired? | at elaboration | before the design was built |
| how long did the investigation take? | weeks, usually | against a parameter check |
Row three is the whole moral of the chapter. (O − 1) × burst against the reorder buffer's size is decidable from two parameters and a multiplication, and the alternative is a receive-path investigation that is correctly scoped, correctly executed and looking in the wrong place.
Complaint 7 — "the outstanding count looks perfect and frames are corrupt."
| Check | If yes | Meaning |
|---|---|---|
| are all frames delivered? | yes | Section 8's normal regime |
c_frames_committed against the frame count | 1.00 | either correct, or failure 3 |
| is the mean frame above the burst size? | no | the commit test is blind anyway |
| does the corruption move with the interconnect's speed? | yes | failure 3 — the ordering |
This is the hardest complaint in the chapter and the counters contribute almost nothing. Chapter 19.6 §12's sequencing is what prevents failure 3 — the design either orders the data before the descriptor or it does not — and the evidence is a scoreboard comparing delivered contents against what was sent, which is Chapter 20.3's instrument rather than a diagnostic counter.
Complaint 8 — "the stall counter says a thousand stalls and nothing is lost."
| Check | If yes | Meaning |
|---|---|---|
what is longest_stall_ns? | 300 | far inside the budget |
| what is the count? | 1 000 | and it is irrelevant |
| is anything lost? | no | correctly |
| what would change that? | one stall of 2 622 ns | the maximum, not the count |
A thousand short stalls and one long one are completely different states and a stall counter reports the first as worse. The loss is a function of the maximum, so the useful record is longest_stall_ns against two constants — and a design reporting a large count and a small maximum is a memory system that interrupts often and briefly, which is exactly what Chapter 19.5 §3's 391-beat allowance was sized for.
And the three symptoms this chapter is systematically blamed for:
| Symptom | Blamed on | Usually is |
|---|---|---|
| receive drops during refreshes | the receive FIFO | the transmit reorder buffer |
| stale frame tails | the interconnect | a commit test missing its closed term |
| an empty descriptor ring | the MAC | the driver, and there is no counter for it |
23. Misconceptions
Misconception 1 — "a receive drop means the receive path is broken."
The wrong model: the symptom is on the receive path, so the fault is too.
What it costs: Chapter 19.6 §22's chain has six links and the first three are on the transmit read path and the interconnect. An undersized transmit reorder buffer deasserts RREADY, the shared channel stalls, the receive path's descriptor fetches are delayed, and the receive FIFO overflows. A receive-path investigation is correctly scoped, correctly executed, and looking at links five and six of a six-link chain.
The corrected model: two paths that share an interconnect are not independent, and the coupling appears in neither path's interface. The correlation is two counters — receive drops occurring while the transmit reorder buffer is full — and the elaboration check that prevents it entirely is two parameters and a multiplication.
Misconception 2 — "the outstanding count should equal Little's law's figure."
The wrong model: Chapter 19.6 §4 says 13 bursts at 1 µs, so a count of 9 is a fault.
What it costs: R × L / burst depends on the delivered memory latency, not on the assumed one. A faster interconnect gives a lower count and no fault at all, and chasing the difference finds nothing. Meanwhile the readings that do discriminate — zero, and pinned at the limit — are ignored because they look like extremes rather than measurements.
The corrected model: the count is a three-valued diagnostic. Zero eliminates four of the eight failures, pinned eliminates seven, and normal eliminates five — 1.41 bits on average from a register that already exists, and the numeric value between the extremes says nothing.
Misconception 3 — "no frames dropped means the DMA path is healthy."
The wrong model: the counters are clean, so the transfer is correct.
What it costs: three of the eight failures deliver every frame. A stale ownership read, a descriptor written before its data lands, a commit taken per burst — all three have frames_in equal to frames_delivered, an outstanding count that matches Little's law, and a driver reading frames with stale tails. No counter in Module 21 sees any of them.
The corrected model: "no drops" and "correct data" are different claims. The first is a counter; the second is Chapter 20.3's scoreboard, and the only diagnostic counter that touches it is c_frames_committed against the frame count — which is blind whenever the frames fit in one burst.
Misconception 4 — "a stall inside the FIFO's capacity is free."
The wrong model: nothing was dropped, so nothing was lost.
What it costs: Chapter 19.5 §3 sized the FIFO as a sum — 391 beats of memory stall, 123 of pause round trip, 4 of synchroniser reserve — and a stall spends them in order. A 2.4 µs stall drops nothing and consumes 0.4 µs of the flow-control headroom, so the next event that would have been absorbed is not.
The corrected model: three thresholds, three different statements. Below 2.002 µs is the design point; between there and 2.621 µs is headroom spent with no symptom; beyond it is 148.8 frames per microsecond. The middle band is the invisible failure and it is one comparison away from visible.
Misconception 5 — "count the stalls."
The wrong model: a stall counter measures how often the memory system misbehaves, so it measures the problem.
What it costs: the loss is a function of the longest stall and not of the count or the mean. A thousand 300 ns stalls cost exactly nothing; one 10 µs stall costs 1 098 frames. A design reporting a thousand stalls and one reporting one may be in completely different states, and the counter that distinguishes them is a maximum.
The corrected model: record the maximum and compare it against two constants. longest_stall_ns against 2 002 and against 2 621 is the whole diagnosis, and the frame cost follows by multiplication.
Misconception 6 — "bigger buffers would fix it."
The wrong model: frames are lost because the buffer filled, so make the buffer bigger.
What it costs: the arithmetic does not scale. Absorbing a 2 µs stall needs 32 KiB; a 10 µs stall needs 122 KiB; a 1 ms scheduling gap needs 12.5 MB — which is not a buffer, it is a memory. And Chapter 19.5 §3's sizing already allocated 123 beats to the pause round trip precisely because beyond a few microseconds the correct response is flow control rather than storage.
The corrected model: the answer beyond a few microseconds is not stalling for that long. A millisecond is an ordinary scheduling quantum and an ordinary coalescing interval; at 100 Gb/s it is a hundred and forty-eight thousand frames, and the fix is in the driver rather than in the RTL.
24. Interview Questions
Question 1 — "What is the only way to lose a frame in the receive FIFO?"
What the answer should establish: the consumer has to stop. The arrival rate is bounded by the line rate — that is what a line rate is — and Chapter 19.5 §4's drain rate is 128 Gb/s against 100, so the buffer empties whatever the frames look like. No sequence of frames overflows it. A strong answer names the consequence: this fault has no wire-side stimulus at all, which is why it needs a chapter rather than an injector.
Question 2 — "You read the outstanding count. What does it tell you?"
What the answer should establish: three things, depending on its regime, and nothing from its value. Zero means nothing is being requested — the ring, the pointers, the buffer size, a missing completion. Pinned at the limit means requests are issued and responses are not consumed, which is exactly one mechanism. Normal means the transfer works and the fault is in ownership or ordering. A strong answer prices it at 1.41 bits from a register that already exists.
Question 3 — "How much does a 10 µs memory stall cost?"
What the answer should establish: 7.379 µs beyond the FIFO's 2.621, at 148.8 minimum-size frames per microsecond — about 1 098 frames. A strong answer notes what is not lost: a DDR4 refresh at 350 ns has a factor of seven of margin, and a stall inside the FIFO drops nothing. The strongest answer adds the invisible band: between 2.002 and 2.621 µs nothing is dropped and the pause round trip's headroom is partly spent.
Question 4 — "Receive frames are being dropped. Where do you look?"
What the answer should establish: the transmit path, among other places. Chapter 19.6 §22's chain: an undersized transmit reorder buffer deasserts RREADY, the shared read channel stalls, receive descriptor fetches are delayed, the receive FIFO overflows. A strong answer gives the correlation — receive drops occurring while the transmit reorder buffer is full, two counters — and notes that a receive-path property passes correctly throughout.
Question 5 — "Which DMA failures deliver every frame?"
What the answer should establish: three of eight. A stale ownership read, a descriptor written before the data lands, and a commit taken per burst. All three have clean counters and an outstanding count matching Little's law, and all three hand the driver frames whose contents are wrong. A strong answer gives the one counter that touches them — c_frames_committed against the frame count — and notes it is blind whenever the mean frame fits in one burst, which is most traffic at a 1 KiB burst size.
Question 6 — "What would you add to make this diagnosable?"
What the answer should establish: about thirty-five flops of representation, not more counters. A mirrored copy of the driver's tail pointer (16), three stall-source bits instead of one (3), and a release-to-re-read cycle counter (16). Against 673 flops of the inference machinery this chapter builds, which still cannot separate an exhausted ring from a missing completion. The strongest answer names the elaboration check first: (O − 1) × burst against the reorder buffer's size, decidable before the design exists.
25. Questions and Answers
26. What's Next
Module 21 has two chapters left, and this one has sharpened what the next one is about.
| Chapter | Takes | What is left to observe |
|---|---|---|
| this chapter | the descriptor path | an outstanding count and a stall duration |
| Chapter 21.8 | a link that is slow and correct | nothing that counts an event at all |
| Chapter 21.9 | the capture | Chapter 21.1 §12's step 6 |
Chapter 21.8 is the module's last inversion and this chapter has drawn its boundary. Section 6's normal regime holds three failures where the flow is healthy and the data is wrong; the next chapter's subject is the case where the flow is healthy, the data is right, and there is simply less of it than the rate implies. No drops, no errors, no stalls beyond budget — and a throughput number below the line rate.
The instruments change completely. Chapter 8.3's efficiency arithmetic replaces the counters: the framing overhead, the interframe gap, the frame-rate ceiling, and the distinction between throughput and goodput that this chapter's retransmission-free world never needed. And Chapter 19.6 §4's Little's law returns as a bound rather than as an expectation — a design with too few outstanding requests cannot reach the line rate and drops nothing at all while failing to.
And the series is now one hundred classes long. Class 100 is about a property scoped to a path in a design whose paths are coupled through something outside both — sound, complete, and structurally unable to name the cause. Together with Chapter 18.7 §19's class 80 — a property of every member of a partition concluded of the whole — it names a family about partitions that are not partitions, and Chapter 21.8 will decide whether a third member exists: a throughput budget is a sum over terms that are assumed independent, and the whole of that chapter is about which of them are not.
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
The MAC's Memory Interface
A full reorder buffer stalls the read data channel, which is legal for transmit and fatal for receive — because they share a channel and only one of them may wait.
- Related topic
A Method for Debugging Ethernet
The receive path has twelve fault sites and the RMON-required counters separate them into seven classes, one holding five — so the method's ceiling is knowable from a datasheet.
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.
