Skip to content
VLSI Mentor

Ethernet · Module 18

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.

Chapter 18.1 §12 ended on a wall: a 100 Gb/s MAC receiving minimum-size frames offers 446 million bus transactions per second, which is 1.79 per cycle at 250 MHz, and one address channel issues one.

A descriptor ring is the structure that gets it under one. Batching eight descriptor fetches into one transaction takes it to 316 M — 1.26 per cycle. Batching the status writebacks as well takes it to 186 M — 0.744 per cycle, which fits.

That is the ring's performance justification and it is not the interesting part.

The interesting part is that a ring hands buffers between two agents — a DMA engine and a device driver — with no lock, no mutex, and no atomic instruction. The whole synchronisation is one bit in each descriptor, and the bit works because of a rule about who is allowed to write it.

And that rule is where this chapter's hard problem lives. The ownership bit is a memory location written by two agents in different clock domains and different coherency domainsChapter 18.1 §6's four domains, plus a CPU cache the MAC cannot see. So the question is not what the bit means.

The question is whether the descriptor's other fields are visible when the bit is — and the answer, on every memory system that reorders writes, is not necessarily.

The MAC writes, in program orderWhat the driver may observe first
1. frame length4. ownership
2. status flags1. frame length
3. errors2. status flags
4. ownership → driver3. errors

The right-hand column is a legal reordering on a memory system with no barrier, and it hands the driver a descriptor it owns, containing a length field from the previous frame. The MAC did nothing wrong. The driver did nothing wrong. The frame is processed at the wrong length.

This chapter builds the ring, derives why the ownership bit is sufficient in the abstract, and then spends its second half on why it is not sufficient in practice and what makes it so.


1. Scope, and the Handoff Without a Lock

Two agents share a set of buffers. One fills them, the other empties them, and they run on different processors at different rates with no shared scheduler.

The textbook answer is a lock. The producer takes it, appends, releases; the consumer takes it, removes, releases. That answer is unavailable here for three reasons and each is fatal on its own.

Why not a lockThe specific problem
the MAC cannot blocka frame is arriving; it cannot wait for a mutex
the MAC has no schedulerthere is nothing to yield to
a lock is an atomic read-modify-writethe bus supports it; the cost is a round trip per frame

Row three is the one worth dwelling on. An atomic operation on the bus is a transaction that must complete before the next one starts — and Chapter 18.1 §12's arithmetic says a 100 Gb/s port has 6.7 ns per frame. A memory round trip is longer than that on any system. A lock per frame is not slow; it is arithmetically impossible.

So the ring is built on a weaker primitive: a single-writer rule.

FieldWritten byRead by
buffer pointerthe driverthe MAC
buffer lengththe driverthe MAC
received lengththe MACthe driver
status and errorsthe MACthe driver
ownershipwhoever currently owns itboth

The last row is the only shared one and it has a rule that makes it safe: the owner is the only agent that may write the descriptor at all, and the last thing an owner does is hand ownership away. After that write, the former owner must not touch the descriptor again.

That rule, plus the ordering guarantee this chapter's second half is about, is the entire synchronisation protocol. No locks, no atomics, one bit.

What this chapter establishes:

SectionEstablishes
2the ring's structure and why it wraps
4the ownership bit and the race it prevents
6full versus empty, and the entry a ring always wastes
8two writers, two domains — the hard problem stated
10what a reordering memory system does to the handoff
12why a barrier on one side alone does not fix it
14the cache line that holds four descriptors
17ring depth, derived from a driver's scheduling latency

2. The Ring, and Why It Is a Ring

A ring is a fixed array of descriptors with two indices that only ever increase, modulo the length. The shape is chosen by elimination.

StructureWhy not
a linked listeach descriptor fetch depends on the previous one — no batching, no prefetch
a flat queueit fills and then what
a ring

Row one is the decisive argument and it is a Chapter 18.1 §12 argument. A linked list's next pointer is inside the current descriptor, so the MAC cannot fetch descriptor n+1 until descriptor n has arrived — which serialises every fetch behind a full memory round trip. A ring's descriptors are at computable addresses, so eight of them are one burst.

StructureDescriptor fetches per frameAt 100 Gb/s, 64-octet frames
linked list1, serialised148.81 M, each a full round trip
ring, one at a time1148.81 M
ring, batches of 80.12518.60 M
ring, batches of 160.06259.30 M

And the batching's returns diminish sharply, which decides where a design stops:

BatchTotal transactions per framePer cycle at 250 MHzGain over the previous
13.00001.786
22.50001.4880.298
42.25001.3390.149
82.12501.2650.074
162.06251.2280.037

Every doubling buys half of what the previous one did, and none of them gets below one on its own — because the data write and the status writeback are still one each. Batching the writebacks too is what closes it:

Batch (both)Transactions per framePer cycle
13.0001.786
41.5000.893
81.2500.744
161.1250.670

A batch of 8 on both sides is where a design lands, and the reason is not the transaction count — it is that eight 16-octet descriptors are 128 octets, which is a natural burst on a 256-bit or 512-bit bus and does not cross a 4 KiB boundary unless the ring is misaligned.

Now the wrap. Indices increase without bound and address the array modulo its length, which makes the ring's length a power of two in every real design so the modulo is a mask. A non-power-of-two ring needs a comparison and a conditional subtract in the address path, which at Chapter 18.1 §12's rates is a cycle nobody has.


A descriptor ring is a fixed array of descriptors shared between a DMA engine and a device driver, with one ownership bit per descriptor deciding which of them may touch it. The driver fills a descriptor with a buffer pointer and a buffer length, then writes the ownership bit to hand it to the MAC. The MAC fetches the descriptor, writes a received frame into the buffer, writes the received length and the status, then writes the ownership bit to hand it back. Each agent writes only one value of the bit and only while it already owns the descriptor, so there is no contention and no atomic read-modify-write is needed — which matters because a lock costs a memory round trip and at 100 gigabits per second the frame interval is 6.7 nanoseconds. The protocol fails in two directions. If the driver observes ownership before the received length, it processes a frame at the previous frame's size, corrupting one frame. If the MAC observes ownership before the buffer pointer, it writes up to 1518 octets into the previous buffer, which the allocator may already have reused — a memory-corruption bug whose symptom appears in an unrelated subsystem.The driverfills buffersThe ringN descriptors, one biteachThe MACfills descriptorsbuf_ptr, buf_lenthen own = MACrx_length, statusthen own = DRIVEROwn seen firststale length — 1 frameOwn seen firststale pointer — freedmemoryThe ruleearlier writes visibleFIRST12
Figure 1 — the ring, the baton, and the two directions in which it can be dropped.

3. RTL 1 — The Descriptor and Its Prefetch Store

The descriptor's layout is the contract between hardware and software, and the field order in it is not cosmetic — Section 9 depends on it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ring_pkg -- descriptor rings and the ownership model.
// -----------------------------------------------------------------------
package ring_pkg;

  localparam int ADDR_W     = 64;
  localparam int DESC_BYTES = 16;
  localparam int PREFETCH   = 8;         // 18.1's transaction arithmetic
  localparam int RING_MAX   = 4096;

  // Ownership. One bit, two values, and the whole protocol.
  typedef enum logic {
    OWN_DRIVER = 1'b0,
    OWN_MAC    = 1'b1
  } owner_e;

  // The receive descriptor, 16 octets. The ORDER of the fields in
  // memory matters: ownership is placed so that a writer emitting
  // ascending addresses writes it LAST. Section 9 is about why that
  // is necessary and section 10 about why it is not sufficient.
  typedef struct packed {
    logic [15:0] rx_length;              // written by the MAC
    logic [7:0]  status;                 // written by the MAC
    logic [6:0]  reserved;
    owner_e      own;                    // the handoff -- highest address
  } desc_ctl_t;

  typedef struct packed {
    logic [ADDR_W-1:0] buf_ptr;          // written by the driver
    logic [15:0]       buf_len;          // written by the driver
    logic [15:0]       flags;
    desc_ctl_t         ctl;
  } descriptor_t;

  // Status bits the MAC sets.
  localparam int ST_GOOD      = 0;
  localparam int ST_FCS_ERR   = 1;       // 6.3's residue check failed
  localparam int ST_RUNT      = 2;       // 7.3
  localparam int ST_GIANT     = 3;
  localparam int ST_TRUNCATED = 4;       // the buffer was too small
  localparam int ST_LAST_FRAG = 5;       // scatter-gather -- 18.3

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// descriptor_store -- the prefetched descriptor window.
//
// Holds up to PREFETCH descriptors fetched in one burst. Its only
// subtlety is that a prefetched descriptor is a COPY, and the copy
// is stale the moment the driver writes the original. Section 4's
// ownership rule is what makes the staleness harmless.
// -----------------------------------------------------------------------
module descriptor_store
  import ring_pkg::*;
(
  input  logic              clk,
  input  logic              rst_n,

  // Fill, from a batched fetch.
  input  logic              fill_valid,
  input  logic [$clog2(PREFETCH)-1:0] fill_idx,
  input  descriptor_t       fill_desc,
  input  logic              fill_done,          // the whole batch landed
  input  logic [15:0]       fill_base_index,    // ring index of entry 0

  // Consume, one per frame.
  input  logic              take,
  output logic              take_valid,
  output descriptor_t       take_desc,
  output logic [15:0]       take_index,

  // Invalidate -- the driver rewrote the ring behind us.
  input  logic              flush,

  output logic [$clog2(PREFETCH+1)-1:0] level,
  output logic [31:0]       c_fills,
  output logic [31:0]       c_takes,
  output logic [31:0]       c_flushes,
  output logic [31:0]       c_not_owned,
  output logic              starved
);

  descriptor_t win [PREFETCH];
  logic [PREFETCH-1:0] win_valid;
  logic [15:0]         base_index;
  logic [$clog2(PREFETCH)-1:0] rd_ptr;

  // A prefetched descriptor the MAC does not own is the ring being
  // empty -- the driver has not replenished. It is NOT an error, and
  // counting it separately from a flush is what distinguishes "the
  // driver is slow" from "the driver rewrote the ring".
  wire head_owned = win_valid[rd_ptr] && (win[rd_ptr].ctl.own == OWN_MAC);

  assign take_valid = head_owned;
  assign take_desc  = win[rd_ptr];
  assign take_index = base_index + {{8{1'b0}}, rd_ptr};
  assign starved    = !head_owned;

  always_comb begin
    int i;
    level = '0;
    for (i = 0; i < PREFETCH; i++)
      if (win_valid[i]) level = level + 1'b1;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      for (i = 0; i < PREFETCH; i++) win[i] <= '0;
      win_valid <= '0; rd_ptr <= '0; base_index <= '0;
      c_fills <= '0; c_takes <= '0; c_flushes <= '0; c_not_owned <= '0;
    end else begin
      if (flush) begin
        win_valid <= '0;
        rd_ptr    <= '0;
        c_flushes <= c_flushes + 1;
      end else begin
        if (fill_valid) begin
          win[fill_idx]       <= fill_desc;
          win_valid[fill_idx] <= 1'b1;
        end
        if (fill_done) begin
          base_index <= fill_base_index;
          rd_ptr     <= '0;
          c_fills    <= c_fills + 1;
        end

        if (take && take_valid) begin
          win_valid[rd_ptr] <= 1'b0;
          rd_ptr            <= rd_ptr + 1'b1;
          c_takes           <= c_takes + 1;
        end

        if (win_valid[rd_ptr] && (win[rd_ptr].ctl.own != OWN_MAC))
          c_not_owned <= c_not_owned + 1;
      end
    end
  end

endmodule

Classification: a small window of prefetched copies, with an ownership gate on the head and an explicit flush.

What it teaches: that a prefetched descriptor is a copy and the copy can be stale, and the ownership rule is what makes that safe. The MAC fetched eight descriptors; the driver may have written any of them since. But the rule says the driver may only write a descriptor it owns — so a descriptor whose copy says OWN_MAC cannot have been written by the driver, and the copy is therefore accurate. A descriptor whose copy says OWN_DRIVER may have become owned by the MAC since, which the MAC will discover on its next fetch. The staleness is always in the safe direction, and that is not luck: it follows directly from the single-writer rule.

And it teaches that c_not_owned and c_flushes must be distinct counters. Both reduce throughput and they have opposite causes. c_not_owned rising means the driver is not replenishing fast enough — a software scheduling problem, Section 17's sizing. c_flushes rising means the driver is rewriting the ring under the MAC, which is a driver bug. One combined "prefetch wasted" counter cannot separate a slow driver from a wrong one.

Deliberately simplified: the window is filled by index with no back-pressure — a real store fills from a burst's beats in order and must handle a partial burst if the bus errors mid-transfer. The flush is a single flat signal where a real design flushes from a given index onward, because the driver's rewrite usually affects the tail rather than the whole window. And level is computed combinationally over all eight valid bits, which is a small adder tree evaluated every cycle for a value only the telemetry reads.

Production implication: starved is the signal an integrator needs and it is not an error. It says the MAC has a frame and no descriptor to put it inChapter 18.3's buffer exhaustion, seen one level up. A port whose starved fraction rises under load is a port whose ring is too short for its driver's scheduling latency, and Section 17 turns that into a depth. Without it, the drops are counted and their cause is not, and the investigation goes to the wire.


4. The Ownership Bit, and the Race It Prevents

One bit per descriptor, two values, and a rule. This section is what the rule buys.

The race, stated concretely. Without an ownership bit, the MAC and the driver both track the ring with indices — the MAC knows where it is writing, the driver knows where it is reading — and each must infer the other's position.

Suppose the driver has consumed up to index 40 and the MAC has filled up to index 43. The driver reads its own index, reads the MAC's index from a register, and processes 41, 42 and 43.

InstantMACDriverProblem
t0filling 43
t1publishes index 43
t2starts filling 44reads index 43
t3still filling 44processes 41, 42, 43fine
t4publishes 44
t5starts filling 45reads 44, processes 44and 44's data?

Row six is the race and it is subtle. The MAC published index 44 after writing 44's data — but "published" means a register write that crossed Chapter 18.1 §6's domain boundary, and the frame data went to memory by a different path. There is no guarantee the data arrived before the index did, because they travelled independently.

The index scheme needs a global ordering between two paths that have none.

The ownership bit removes the second path entirely. The ownership bit is in the descriptor, which is in memory, on the same path as the data. So the question stops being "did two paths arrive in order" and becomes "did two writes on one path arrive in order" — which is a question a memory system can answer, and Sections 9 to 12 are about how.

Three further things the bit buys, which are easy to miss:

PropertyHow the bit provides it
no shared mutable indexeach agent's position is private; the ring is the only shared state
self-describing entriesa descriptor says whose it is without consulting anything
crash tolerancea driver restarting reads the ring and knows where it is

Row three is worth a sentence. A driver that has been descheduled for 10 ms, or restarted entirely, reconstructs its position by scanning for the first descriptor it owns — no state to recover, no index to disbelieve. The ring is the state.

And the rule the bit depends on is worth stating in its final form, because everything after Section 8 is about its second clause:

An agent may write a descriptor only while it owns it, and the last write it performs on that descriptor is the one that transfers ownership — with every earlier write visible to the new owner before the transfer is.

The first clause is a discipline and every implementation gets it right. The second clause is a memory-ordering requirement, and it is the one that is silently violated on a reordering memory system by code that looks entirely correct.


5. RTL 2 — The Ring Pointers

Two indices, a mask, and an address computation that has to be right at Chapter 18.1 §12's rates.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ring_pointers -- address generation and occupancy for one ring.
//
// The indices are FREE-RUNNING and wider than the ring. Masking only
// happens in the address computation. This is what makes full and
// empty distinguishable without wasting an entry -- section 6.
// -----------------------------------------------------------------------
module ring_pointers
  import ring_pkg::*;
#(
  parameter int IDX_W = 20               // wider than $clog2(RING_MAX)
)(
  input  logic               clk,
  input  logic               rst_n,

  input  logic [ADDR_W-1:0]  cfg_base,
  input  logic [15:0]        cfg_len,    // power of two
  input  logic               cfg_valid,

  input  logic               advance_fetch,   // the MAC took a descriptor
  input  logic               advance_done,    // the MAC finished one

  // The driver's position, read back from software's own pointer
  // register. Advisory only -- section 8.
  input  logic [IDX_W-1:0]   driver_index,

  output logic [ADDR_W-1:0]  fetch_addr,
  output logic [ADDR_W-1:0]  done_addr,
  output logic [IDX_W-1:0]   fetch_index,
  output logic [IDX_W-1:0]   done_index,

  output logic [15:0]        mac_occupancy,
  output logic               ring_empty_for_mac,
  output logic               cfg_len_not_pow2,
  output logic [31:0]        c_wraps
);

  logic [IDX_W-1:0] idx_fetch, idx_done;

  // A non-power-of-two length needs a compare-and-subtract in the
  // address path, which at 6.7 ns per frame is a cycle nobody has.
  assign cfg_len_not_pow2 = cfg_valid && (cfg_len != '0) &&
                            ((cfg_len & (cfg_len - 16'd1)) != 16'd0);

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

  // Address = base + (index AND mask) * DESC_BYTES. The multiply is a
  // shift because DESC_BYTES is a power of two; if it were not, this
  // would be a multiplier in the critical path.
  function automatic logic [ADDR_W-1:0] addr_of(input logic [IDX_W-1:0] i);
    logic [15:0] slot;
    slot = i[15:0] & mask;
    return cfg_base + ({{(ADDR_W-16){1'b0}}, slot} << $clog2(DESC_BYTES));
  endfunction

  assign fetch_addr  = addr_of(idx_fetch);
  assign done_addr   = addr_of(idx_done);
  assign fetch_index = idx_fetch;
  assign done_index  = idx_done;

  // How many descriptors the MAC has fetched and not yet completed.
  assign mac_occupancy = idx_fetch[15:0] - idx_done[15:0];

  // The MAC is out of descriptors when its fetch pointer has caught
  // up with the driver's. Advisory: the authoritative answer is the
  // ownership bit in the descriptor itself -- section 8.
  assign ring_empty_for_mac = (idx_fetch == driver_index);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      idx_fetch <= '0; idx_done <= '0; c_wraps <= '0;
    end else if (cfg_valid) begin
      idx_fetch <= '0; idx_done <= '0;
    end else begin
      if (advance_fetch) begin
        idx_fetch <= idx_fetch + 1'b1;
        if ((idx_fetch[15:0] & mask) == mask) c_wraps <= c_wraps + 1;
      end
      if (advance_done) idx_done <= idx_done + 1'b1;
    end
  end

endmodule

Classification: free-running index counters with a masked address projection.

What it teaches: that the indices must be wider than the ring and must not be masked. A pointer masked at increment loses the distinction between having wrapped once and not having wrapped at all — so fetch == done means both empty and full, and Section 6 is the general form of that problem. Keeping the indices free-running and masking only in the address computation preserves the difference at the cost of four extra bits per pointer.

And it teaches that cfg_len_not_pow2 is worth a flag rather than a silent restriction. A driver that configures a 1000-entry ring gets addresses computed with a mask of 999, which is not a mask at all — 1000 & 999 has bits set that a modulo would not — and descriptors land at wrong addresses in a pattern that looks random. The check is two gates and it converts a corruption into a refusal.

Deliberately simplified: there is one pair of pointers where a real design has three — fetch, in-progress and done — because Chapter 18.3's DMA has descriptors it has taken, descriptors whose data is in flight, and descriptors fully complete, and the three do not advance together. driver_index is taken as an input from a register the driver writes, which is advisory and Section 8 explains why it must be. The address function is a combinational path from a 20-bit counter through a mask, a shift and a 64-bit add, which at 400 MHz needs pipelining.

Production implication: c_wraps and mac_occupancy together let an operator size the ring from a running system. Occupancy near the ring length under load means the ring is marginal; occupancy that never exceeds a tenth means it is oversized and its memory could be returned. Section 17 derives the depth from first principles and this counter is how the derivation is checked against reality — which matters, because the input to that derivation is a driver's worst-case scheduling latency, and nobody knows that number in advance.


6. Full, Empty, and the Entry a Ring Usually Wastes

Two indices into a circular array have one classical ambiguity, and how a design resolves it is a small decision with a visible cost.

The ambiguity: with both pointers masked to the ring's length, producer == consumer means the ring is empty — and it also means the ring is full, because after N insertions the producer has wrapped exactly onto the consumer.

Three resolutions, and each gives something up.

ResolutionCostUsed by
waste one entry — full is producer + 1 == consumerone descriptor of every ringmany DMA engines
a separate counta shared mutable counter — two writersrarely, and it reintroduces the race
free-running indices, masked only for addressinga few extra bits per pointerSection 5

Row two is the interesting failure. A count incremented by the producer and decremented by the consumer is a location written by both agents — which is exactly what Section 4's ownership rule exists to avoid, and it needs an atomic read-modify-write, which Section 1 established costs a memory round trip per frame. The obvious fix reintroduces the problem the structure was chosen to avoid.

Row one's cost is small and real. A 256-entry ring holds 255 usable descriptors; at Chapter 18.1 §12's rates that is 0.39% of the ring, which nobody notices. The reason to prefer row three is not the entry — it is that free-running indices also give occupancy for free, which row one does not without a subtraction that the mask has already destroyed.

And there is a fourth answer specific to this structure, which is why the ambiguity matters less here than in a general queue.

The ownership bit already says whether a descriptor is available. The MAC does not need to compare pointers at all: it fetches the descriptor at its own index and reads the bit. Owned by the MAC means available; owned by the driver means the ring is empty at that point, which is the only point that matters.

QuestionAnswered by pointersAnswered by the bit
is there a descriptor for mea comparison of two indicesone bit in the descriptor I already fetched
is the other agent's index currentno — it crossed a domainnot applicable
how many are availablea subtractionnot answerable without scanning

Row two is the decisive one and it is Section 8's subject in miniature. The driver's index reaches the MAC through a register write that crossed Chapter 18.1 §6's host-to-MAC boundary; it is a snapshot of where the driver was, not where it is. The ownership bit is in the descriptor the MAC just read from memory — it is as current as the memory system allows.

So the pointers are an optimisation and the bit is the protocol. A design that treats the pointer comparison as authoritative has a race; one that treats it as a hint about whether fetching is worthwhile has none — and Section 5's ring_empty_for_mac is commented as advisory for exactly that reason.


7. RTL 3 — The Ownership Handoff

The handoff is a single write and the block exists to make sure it is the last one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ownership_handoff -- sequences a descriptor's completion writes.
//
// The MAC has a length, a status and an ownership transfer to write.
// This block refuses to issue the ownership write until the others
// have COMPLETED -- not merely been issued. Section 10 is about the
// difference and why it is the whole correctness argument.
// -----------------------------------------------------------------------
module ownership_handoff
  import ring_pkg::*;
(
  input  logic               clk,
  input  logic               rst_n,

  // A frame finished. Everything the driver must see is here.
  input  logic               complete_valid,
  input  logic [ADDR_W-1:0]  desc_addr,
  input  logic [15:0]        rx_length,
  input  logic [7:0]         status,
  output logic               complete_ready,

  // Write requests out, to 18.1's memory master.
  output logic               wr_valid,
  input  logic               wr_ready,
  output logic [ADDR_W-1:0]  wr_addr,
  output logic [63:0]        wr_data,
  output logic [7:0]         wr_be,
  output logic               wr_is_ownership,

  // Completion in. NOT the same event as issue.
  input  logic               wr_done,
  input  logic               wr_error,

  output logic [31:0]        c_handoffs,
  output logic [31:0]        c_wait_cycles,
  output logic [31:0]        c_handoff_errors,
  output logic               ordering_violation
);

  typedef enum logic [2:0] {
    H_IDLE,
    H_WRITE_FIELDS,        // length, status, errors
    H_AWAIT_FIELDS,        // the part everybody omits
    H_WRITE_OWN,
    H_AWAIT_OWN
  } hstate_e;

  hstate_e st;

  logic [ADDR_W-1:0] addr_q;
  logic [15:0]       len_q;
  logic [7:0]        status_q;

  assign complete_ready = (st == H_IDLE);

  // The ownership write targets the descriptor's control word, which
  // the layout in ring_pkg places at the HIGHEST address. A writer
  // emitting ascending addresses therefore emits it last -- which is
  // necessary and, on a reordering memory system, not sufficient.
  assign wr_addr = (st == H_WRITE_OWN) ? (addr_q + ADDR_W'(DESC_BYTES - 4))
                                       : (addr_q + ADDR_W'(DESC_BYTES - 8));

  assign wr_data = (st == H_WRITE_OWN)
                     ? {56'b0, 7'b0, OWN_DRIVER}
                     : {40'b0, status_q, len_q};

  assign wr_be           = (st == H_WRITE_OWN) ? 8'h0F : 8'hFF;
  assign wr_valid        = (st == H_WRITE_FIELDS) || (st == H_WRITE_OWN);
  assign wr_is_ownership = (st == H_WRITE_OWN);

  // An ownership write issued while a field write is still outstanding
  // is the bug this block exists to prevent. If it is ever observed,
  // the block is broken -- hence a flag rather than a counter.
  assign ordering_violation = wr_is_ownership && (st != H_WRITE_OWN);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      st <= H_IDLE;
      addr_q <= '0; len_q <= '0; status_q <= '0;
      c_handoffs <= '0; c_wait_cycles <= '0; c_handoff_errors <= '0;
    end else begin
      case (st)
        H_IDLE:
          if (complete_valid) begin
            addr_q   <= desc_addr;
            len_q    <= rx_length;
            status_q <= status;
            st       <= H_WRITE_FIELDS;
          end

        H_WRITE_FIELDS:
          if (wr_ready) st <= H_AWAIT_FIELDS;

        // Waiting for the response, not for the request to be taken.
        // On AXI this is BVALID, which means the write has reached a
        // point of visibility -- section 10.
        H_AWAIT_FIELDS: begin
          c_wait_cycles <= c_wait_cycles + 1;
          if (wr_done) begin
            if (wr_error) c_handoff_errors <= c_handoff_errors + 1;
            st <= H_WRITE_OWN;
          end
        end

        H_WRITE_OWN:
          if (wr_ready) st <= H_AWAIT_OWN;

        H_AWAIT_OWN:
          if (wr_done) begin
            if (wr_error) c_handoff_errors <= c_handoff_errors + 1;
            c_handoffs <= c_handoffs + 1;
            st         <= H_IDLE;
          end

        default: st <= H_IDLE;
      endcase
    end
  end

endmodule

Classification: a serialising state machine that converts a write-ordering requirement into a wait for completion.

What it teaches: that the ordering requirement cannot be met by issuing the writes in order. H_AWAIT_FIELDS is the block's entire reason for existing: the field writes must have completed before the ownership write is issued. Issuing them in order puts two writes into an interconnect that is free to deliver them in either order — and Section 10 shows the conditions under which it will.

And it teaches what that costs, which is the reason designs omit it. The handoff now takes a full memory write round trip — the field write's response — before the ownership write even starts. c_wait_cycles measures it, and at Chapter 18.1 §12's 6.7 ns per frame at 100 Gb/s, a 200 ns round trip is thirty frame times. The block must therefore be pipelined across descriptors — several handoffs in flight, each at a different stage — and this listing's single state machine is the un-pipelined version.

Deliberately simplified: one handoff at a time, which as just noted does not meet rate. The two writes are 64-bit, where a real design writes the whole 16-octet descriptor control region in one beat and the ownership bit in another. wr_be distinguishes them crudely. And ordering_violation as written can never assert — it is an assertion expressed as a flag, kept because a pipelined version of this block can violate it, and the flag is what catches the pipelining bug.

Production implication: c_wait_cycles divided by c_handoffs is the memory system's write-completion latency, measured by the MAC, in the MAC's own clock. It is one of the few numbers in Chapter 18.1 §17's assumption table that the MAC can measure rather than merely detect the violation of — and it is the number that says how deeply this block must be pipelined. A design that measures 200 ns needs thirty descriptors in flight at 100 Gb/s, which is a different block from the one that needs two.


8. Two Writers, Two Domains: the Problem Stated Properly

Everything to this point has treated the descriptor as a location. It is not. It is a location seen through two different memory systems, and this section is where that stops being an abstraction.

The MAC's view. The MAC writes through Chapter 18.1 §5's memory master, on the host clock, through an interconnect, to a memory controller. It has no cache. Its writes are ordinary bus transactions and it learns they have completed when a response arrives.

The driver's view. The driver writes through a CPU, which has a store buffer and at least two levels of cache. A store to a descriptor may sit in a store buffer for hundreds of cycles; it may be merged with an adjacent store; it may reach L1 and stay there.

So "the descriptor" has up to four simultaneous values:

WhereValueVisible to
the CPU's store bufferwhat the driver just wrotethe CPU only
the CPU's L1a slightly older valuethe CPU, and a coherent MAC
DRAMolder stilla non-coherent MAC
the MAC's prefetch windowa copy from whenever it fetchedthe MAC only

Row four is Section 3's copy and the ownership rule already handles it. Rows one to three are the problem, and which of them is visible to the MAC depends on a system property nobody writes on a MAC's datasheet: whether the MAC's transactions are coherent with the CPU's caches.

SystemThe MAC readsThe driver must
I/O-coherentwhatever the CPU last stored, wherever it isorder its stores
non-coherentDRAM onlyorder its stores AND clean/invalidate

On a coherent system the problem reduces to ordering. On a non-coherent one it is ordering plus explicit cache maintenance, and Section 14 is about why the maintenance has a granularity that causes a bug of its own.

Now the two failure directions, because they are not symmetric and both have to be stated.

Direction 1 — the MAC hands a descriptor to the driver. The MAC writes length, status, then ownership. If the driver observes ownership before length, it processes the frame with the previous frame's length.

Direction 2 — the driver hands a descriptor to the MAC. The driver writes a buffer pointer, then ownership. If the MAC observes ownership before the buffer pointer, it writes a frame to the previous buffer pointer — which is a buffer the driver may already have freed and reused.

DirectionWrong value usedConsequence
MAC to drivera stale lengtha frame processed at the wrong size
driver to MACa stale buffer pointera frame written into freed memory

Direction 2 is worse and it is the less-discussed one. A stale length corrupts one frame. A stale buffer pointer writes 1518 octets of network data into memory the allocator has given to something else — which is a memory-corruption bug whose symptom appears in an unrelated subsystem, minutes later, and whose cause is a network driver.

And the symmetry of the fix is the section's conclusion. Both directions need the same thing: every write before the ownership write must be visible before the ownership write is. Section 7 does it for direction 1 in hardware; Section 12 is about direction 2, which is software's, and about why hardware cannot do it on software's behalf.


9. RTL 4 — Enforcing the Write Order

Section 7 serialised by waiting. This block is the pipelined version, and it is where the real design lives.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// descriptor_write_order -- several handoffs in flight, each with its
// ownership write gated on ITS OWN field writes completing.
//
// The gate is per descriptor, not global. A global gate would
// serialise the whole engine behind one memory round trip, which at
// 6.7 ns per frame is thirty frame times.
// -----------------------------------------------------------------------
module descriptor_write_order
  import ring_pkg::*;
#(
  parameter int IN_FLIGHT = 32           // section 7's round-trip argument
)(
  input  logic               clk,
  input  logic               rst_n,

  // A completion enters here.
  input  logic               enq_valid,
  output logic               enq_ready,
  input  logic [ADDR_W-1:0]  enq_addr,
  input  logic [15:0]        enq_len,
  input  logic [7:0]         enq_status,

  // Field writes out, tagged so their responses can be matched.
  output logic               fw_valid,
  input  logic               fw_ready,
  output logic [$clog2(IN_FLIGHT)-1:0] fw_tag,
  output logic [ADDR_W-1:0]  fw_addr,
  output logic [63:0]        fw_data,

  // Ownership writes out, also tagged.
  output logic               ow_valid,
  input  logic               ow_ready,
  output logic [$clog2(IN_FLIGHT)-1:0] ow_tag,
  output logic [ADDR_W-1:0]  ow_addr,

  // Responses in, by tag.
  input  logic               fw_done,
  input  logic [$clog2(IN_FLIGHT)-1:0] fw_done_tag,
  input  logic               ow_done,
  input  logic [$clog2(IN_FLIGHT)-1:0] ow_done_tag,

  output logic [15:0]        in_flight,
  output logic [31:0]        c_completed,
  output logic [31:0]        c_max_in_flight,
  output logic               table_full,
  output logic               own_before_fields    // must never assert
);

  typedef enum logic [1:0] {
    E_FREE, E_FIELDS_OUT, E_FIELDS_DONE, E_OWN_OUT
  } estate_e;

  estate_e           est  [IN_FLIGHT];
  logic [ADDR_W-1:0] eadr [IN_FLIGHT];
  logic [15:0]       elen [IN_FLIGHT];
  logic [7:0]        esta [IN_FLIGHT];

  logic [$clog2(IN_FLIGHT)-1:0] alloc_i, fw_i, ow_i;
  logic alloc_hit, fw_hit, ow_hit;

  // Find a free slot, a slot needing a field write, and a slot whose
  // fields are done. Three priority encoders; at IN_FLIGHT = 32 each
  // is a five-level tree.
  always_comb begin
    int i;
    alloc_hit = 1'b0; alloc_i = '0;
    fw_hit    = 1'b0; fw_i    = '0;
    ow_hit    = 1'b0; ow_i    = '0;
    for (i = IN_FLIGHT-1; i >= 0; i--) begin
      if (est[i] == E_FREE)        begin alloc_hit = 1'b1; alloc_i = i[$clog2(IN_FLIGHT)-1:0]; end
      if (est[i] == E_FIELDS_OUT)  begin fw_hit    = 1'b1; fw_i    = i[$clog2(IN_FLIGHT)-1:0]; end
      if (est[i] == E_FIELDS_DONE) begin ow_hit    = 1'b1; ow_i    = i[$clog2(IN_FLIGHT)-1:0]; end
    end
  end

  assign table_full = !alloc_hit;
  assign enq_ready  = alloc_hit;

  assign fw_valid = fw_hit;
  assign fw_tag   = fw_i;
  assign fw_addr  = eadr[fw_i] + ADDR_W'(DESC_BYTES - 8);
  assign fw_data  = {40'b0, esta[fw_i], elen[fw_i]};

  // The gate. An ownership write is only ever offered for a slot in
  // E_FIELDS_DONE -- that is, one whose field write has RESPONDED.
  assign ow_valid = ow_hit;
  assign ow_tag   = ow_i;
  assign ow_addr  = eadr[ow_i] + ADDR_W'(DESC_BYTES - 4);

  // The invariant, expressed as a signal so a pipelining bug is
  // visible rather than silent.
  assign own_before_fields = ow_valid && (est[ow_i] != E_FIELDS_DONE);

  always_comb begin
    int i;
    in_flight = '0;
    for (i = 0; i < IN_FLIGHT; i++)
      if (est[i] != E_FREE) in_flight = in_flight + 1'b1;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      for (i = 0; i < IN_FLIGHT; i++) begin
        est[i] <= E_FREE; eadr[i] <= '0; elen[i] <= '0; esta[i] <= '0;
      end
      c_completed <= '0; c_max_in_flight <= '0;
    end else begin
      if (enq_valid && enq_ready) begin
        est[alloc_i]  <= E_FIELDS_OUT;
        eadr[alloc_i] <= enq_addr;
        elen[alloc_i] <= enq_len;
        esta[alloc_i] <= enq_status;
      end

      if (fw_valid && fw_ready) est[fw_i] <= E_FIELDS_OUT;  // stays until done
      if (fw_done)              est[fw_done_tag] <= E_FIELDS_DONE;
      if (ow_valid && ow_ready) est[ow_i] <= E_OWN_OUT;
      if (ow_done) begin
        est[ow_done_tag] <= E_FREE;
        c_completed      <= c_completed + 1;
      end

      if ({16'b0, in_flight} > c_max_in_flight)
        c_max_in_flight <= {16'b0, in_flight};
    end
  end

endmodule

Classification: a tagged in-flight table whose allocation policy encodes a per-descriptor ordering dependency.

What it teaches: that the ordering constraint is per descriptor and must not become a global one. Section 7's state machine waits for a response before starting the next descriptor's writes, which serialises the engine behind a full round trip. This block waits per slot: descriptor 5's ownership write is gated on descriptor 5's field write, and descriptor 6's field write proceeds meanwhile. The distinction is the difference between 30 frames per round trip and one.

And it teaches that the in-flight depth is derived, not chosen. IN_FLIGHT must cover the memory write round trip divided by the frame interval: at a 200 ns round trip and 6.7 ns per frame, thirty. A table of 8 on a 100 Gb/s port stalls at 26% of line rate for no reason visible anywheretable_full is what makes it visible.

Deliberately simplified: three priority encoders over 32 entries evaluated combinationally every cycle is a large amount of logic in one path; a production design keeps three linked free lists instead. The field write is a single 64-bit write where a real descriptor's status region may span more. Write errors are not handled — a failed field write leaves a slot in E_FIELDS_OUT for ever, and the real design needs a timeout and an error path. And the same descriptor could in principle be enqueued twice, which nothing here prevents.

Production implication: own_before_fields should be tied to a fatal error, not a counter. It is an invariant, and the class of bug it catches is the one this chapter exists for: a pipelining optimisation that lets an ownership write overtake its own field write produces no symptom at the MAC and corrupts one frame in however many thousand the race hits. The flag costs a comparator; finding the bug without it means correlating a driver's occasional wrong-length frame with an RTL change three months earlier.


10. What a Reordering Memory System Does to the Handoff

Section 7 waited for a response instead of issuing in order. This section is why, and it is worth being precise, because "the bus reorders writes" is usually asserted rather than explained.

Consider the naive design: issue the field write, then issue the ownership write, both without waiting.

On AXI, two writes issued back to back may be reordered if they carry different IDs. The protocol's ordering guarantee is per ID: transactions with the same ID complete in order; transactions with different IDs have no ordering relationship at all. A master that assigns IDs to increase throughput — which is the whole reason IDs exist — has just given the interconnect permission to reorder its descriptor writes.

And the interconnect will, for entirely ordinary reasons.

CauseWhy the ownership write may land first
different IDsno ordering is promised
different targetsfields in one bank, ownership in another
a write buffera shorter write overtakes a longer one
a retrythe field write is retried; the ownership write is not
an interleaving switchtwo paths through the interconnect with different depths

Row two is the one that surprises people and it is specific to this structure. The field write and the ownership write are to the same 16-octet descriptor — they could not be closer together — and on a system that interleaves DRAM banks at 8 or 16 octets, they are in different banks, reached by different queues with different occupancies.

Now the consequence, traced.

StepMemoryDriver's view
1MAC issues field write (len = 1518)descriptor owned by MAC
2MAC issues ownership write
3ownership landsdescriptor owned by driver
4driver reads length1518? or the previous frame's 64?
5field write landstoo late

Step 4 reads whatever is in memory, which is the previous frame's length — because that descriptor was used before and the field write that would have updated it has not arrived. The driver processes 1518 octets of frame as 64, or worse, 64 octets of frame as 1518 and reads 1454 octets of whatever follows the buffer.

Three fixes exist and only one of them is generally correct.

FixWorks?Cost
write the fields and ownership in one transactionyes, if the descriptor fits one atomic writea 16-octet descriptor on a 128-bit bus — often available
same AXI ID for both writesyes on AXI — same-ID writes complete in orderone ID's worth of throughput
wait for the field write's responseyes, alwaysa round trip, pipelined — Section 9
issue in address order and hopeno

Row one is the best answer when it is available and it often is. A 16-octet descriptor written in one 128-bit beat is indivisible from the memory system's point of view — there is no ordering question because there is one write. The catch is that the descriptor's driver-written fields must not be clobbered, which is what byte enables are for, and which stops working the moment a descriptor exceeds one beat.

Row two is the cheapest and it is subtly fragile. It depends on the whole path preserving same-ID ordering, which AXI requires — but a bridge, a width converter or a clock crossing in the path may be non-compliant in exactly this respect, and the failure is a corrupted frame once in a million rather than a protocol error anybody sees.

Row three is the one Section 9 implements because it depends on nothing. A write that has responded has reached a point of visibility; any read issued after that response sees it. The cost is a round trip, and pipelining across descriptors is what makes the cost affordable rather than removing it.


A handoff needs two writes — the descriptor's fields and its ownership bit — and the ownership write must become visible last. Four strategies are available. Writing both in one indivisible transaction is the best answer when the descriptor fits a single bus beat, since there is no ordering question with only one write, but it stops working the moment the descriptor exceeds one beat. Giving both writes the same AXI ID works because AXI requires same-ID transactions to complete in order, and it costs one ID's worth of throughput, but it depends on every bridge, width converter and clock crossing in the path being compliant in exactly that respect. Waiting for the field write's response before issuing the ownership write is always correct and depends on nothing, at the cost of a full memory round trip — about 200 nanoseconds, or thirty frame times at 100 gigabits per second — which is made affordable by pipelining thirty-two handoffs, each gated on its own field write. Issuing the two writes in ascending address order and hoping is not a strategy at all: different IDs carry no ordering promise, the two halves of a 16-octet descriptor may sit in different DRAM banks with different queue depths, and a retry reorders.Two writesfields, then ownershipOne atomic writebest — if it fits a beatSame AXI IDfree — if the pathcompliesWait for theresponsealways correctIssue in order andhopenot a strategyCost: noneneeds DESC <= BUS_BYTESCost: one IDneeds a compliant pathCost: 200 ns30 frame times at 100Gb/sCost: a corruptframeat the reordering rate32 in flightmakes 200 ns affordable12
Figure 2 — three correct barriers and one that only looks correct, priced against what each depends on.

11. RTL 5 — The Barrier, in Hardware

A barrier is not an instruction here. It is a rule about which request may be issued, and this block is that rule made explicit so it can be reasoned about and turned off for measurement.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ordering_barrier -- the three strategies of section 10, selectable,
// so a design can measure what each costs on its own interconnect.
//
// Mode 0: same-ID. Cheapest, depends on the path being compliant.
// Mode 1: wait-for-response. Always correct, costs a round trip.
// Mode 2: single atomic write. Best, only if the descriptor fits.
// -----------------------------------------------------------------------
module ordering_barrier
  import ring_pkg::*;
#(
  parameter int BUS_BYTES = 16           // one beat; DESC_BYTES fits if equal
)(
  input  logic               clk,
  input  logic               rst_n,

  input  logic [1:0]         cfg_mode,
  input  logic [3:0]         cfg_shared_id,

  // The two writes a handoff needs.
  input  logic               fields_req,
  input  logic               own_req,
  input  logic               fields_responded,

  output logic               issue_fields,
  output logic               issue_own,
  output logic               issue_combined,
  output logic [3:0]         issue_id,

  output logic [31:0]        c_mode_stalls,
  output logic [31:0]        c_combined,
  output logic               mode_unsupported,
  output logic               barrier_bypassed     // for measurement only
);

  // Mode 2 needs the whole descriptor in one bus beat. If it does not
  // fit, silently falling back would hide a correctness difference,
  // so the module refuses and says so.
  assign mode_unsupported = (cfg_mode == 2'd2) && (BUS_BYTES < DESC_BYTES);

  // Mode 3 is the "issue in address order and hope" of section 10's
  // last table. It exists ONLY so a testbench can demonstrate the
  // corruption, and it is flagged whenever it is selected.
  assign barrier_bypassed = (cfg_mode == 2'd3);

  always_comb begin
    issue_fields   = 1'b0;
    issue_own      = 1'b0;
    issue_combined = 1'b0;
    issue_id       = 4'd0;

    unique case (cfg_mode)
      2'd0: begin                         // same ID for both
        issue_fields = fields_req;
        issue_own    = own_req;
        issue_id     = cfg_shared_id;
      end
      2'd1: begin                         // wait for the response
        issue_fields = fields_req;
        issue_own    = own_req && fields_responded;
        issue_id     = 4'd0;
      end
      2'd2: begin                         // one indivisible write
        issue_combined = mode_unsupported ? 1'b0 : (fields_req && own_req);
        issue_id       = cfg_shared_id;
      end
      default: begin                      // no barrier -- for the testbench
        issue_fields = fields_req;
        issue_own    = own_req;
        issue_id     = own_req ? 4'd1 : 4'd0;   // deliberately different
      end
    endcase
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_mode_stalls <= '0; c_combined <= '0;
    end else begin
      if ((cfg_mode == 2'd1) && own_req && !fields_responded)
        c_mode_stalls <= c_mode_stalls + 1;
      if (issue_combined) c_combined <= c_combined + 1;
    end
  end

endmodule

Classification: a selectable ordering policy, with the unsafe policy present and flagged so the failure can be demonstrated.

What it teaches: that the three correct strategies have genuinely different costs and the right one is a property of the system, not of the MAC. Mode 0 is free and depends on every component in the path honouring same-ID ordering. Mode 1 costs a round trip and depends on nothing. Mode 2 is free and better and requires the descriptor to fit one beat. A MAC that hard-codes one of them is correct on some systems and slow or wrong on others, which is why the mode is configuration.

And it teaches why mode 3 is in the design. A testbench cannot demonstrate that the barrier is necessary if the barrier cannot be removed — and a property that has never been seen to fail is a property nobody trusts. Section 20's directed test selects mode 3 deliberately, reorders the two writes in the interconnect model, and observes the driver reading a stale length. barrier_bypassed makes the mode impossible to select by accident, which is the only safe way to ship an unsafe mode.

Deliberately simplified: the combined write's byte enables are not computed here — a combined write must not clobber the driver's buffer pointer, so the enables cover only the MAC-owned fields, and getting them wrong turns mode 2 from the best option into the worst. There is no mode for a system with a write-through path to a point of coherency, which some interconnects offer and which is cheaper than mode 1. And cfg_shared_id being a single ID for all descriptor traffic serialises all of it, which is more ordering than needed: per-ring IDs would do.

Production implication: c_mode_stalls divided by the handoff count is the fraction of handoffs that paid mode 1's round trip, which is the number a design uses to decide whether mode 0 or mode 2 is worth qualifying on a given system. A port showing 100% stalls at mode 1 and full rate at mode 0 has a measurable case for qualifying the interconnect's same-ID ordering — and one showing 2% stalls has no case at all and should stay on mode 1. The comparison takes an afternoon with this block and is not possible without it.


12. The Barrier on the Other Side, and Why Hardware Cannot Supply It

Section 8's direction 2 is the driver's, and it is worth being explicit that nothing in the MAC can fix it.

The driver's sequence, refilling a descriptor:

StepThe driver does
1allocate a buffer
2write buf_ptr into the descriptor
3write buf_len
4write own = OWN_MAC

A CPU is free to make those stores visible in any order. Store buffers merge and drain out of order; weakly ordered architectures make no promise whatsoever between two independent stores. So the MAC may fetch a descriptor whose ownership bit is set and whose buf_ptr is the previous buffer's.

The driver needs a barrier between steps 3 and 4 — a store barrier, dmb ishst or equivalent — and the MAC cannot provide it, observe it, or detect its absence.

Why not, precisely:

Could the MAC...No, because
check buf_ptr is new?the previous buffer's pointer is a perfectly legal new one
require a magic value first?that write can be reordered too
read the descriptor twice and compare?both reads may see the same stale state
wait a while?a store buffer has no bounded drain time

Row three is the one that looks promising and is not. Reading twice and requiring two identical results detects a torn read, not a reordered store — the stale value is stable, not torn. There is nothing to see.

Which makes this the module's clearest example of a correctness property that lives in neither component. The MAC is correct. The driver's individual stores are correct. The protocol requires an ordering that only the driver can impose, and only by executing an instruction whose absence is invisible.

The practical consequences are three and they are all documentation:

ConsequenceWhat it means in practice
the MAC's datasheet must state the requirement"a store barrier is required before the ownership store"
the reference driver must contain itbecause the datasheet will not be read
a debug mode helpsSection 15's telemetry can record what it fetched

Row three is the only thing hardware contributes and it is worth building. A MAC that logs the buf_ptr of every descriptor it fetched lets a driver author compare the log against the pointers the driver believes it wrote — and a mismatch is the missing barrier, identified in minutes rather than by inspection of a race that reproduces once an hour.

And there is a symmetry worth naming to close the section. Section 7's hardware barrier and the driver's software barrier are the same requirement in two implementationsmake the earlier writes visible before the transferring writeand a system needs both. A design that implements one and not the other is correct in one direction and corrupted in the other; and the corrupted direction is the driver's, which is the one that writes frames into freed memory.


13. RTL 6 — The Coherency Adapter

Whether the MAC's traffic snoops the CPU's caches is a system property. This block is where the MAC declares what it needs and reports what it got.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// coherency_adapter -- attribute generation for descriptor and data
// traffic, and the detection of a system that does not honour it.
//
// Descriptors and frame data want DIFFERENT attributes. Descriptors
// are small, shared, and read by a CPU immediately: coherent.
// Frame data is large and may be read much later or not at all:
// non-coherent is often better, and the driver invalidates.
// -----------------------------------------------------------------------
module coherency_adapter
  import ring_pkg::*;
(
  input  logic              clk,
  input  logic              rst_n,

  input  logic              cfg_desc_coherent,
  input  logic              cfg_data_coherent,
  input  logic [15:0]       cfg_cache_line_b,     // the system's, in octets

  input  logic              req_valid,
  input  logic [1:0]        req_kind,             // 0 desc, 1 data, 2 status
  input  logic [ADDR_W-1:0] req_addr,
  input  logic [15:0]       req_bytes,

  output logic [3:0]        ax_cache,             // AxCACHE
  output logic [2:0]        ax_prot,
  output logic              ax_snoop_required,

  output logic              desc_straddles_line,
  output logic              desc_shares_line,
  output logic [15:0]       desc_per_line,
  output logic [31:0]       c_straddles,
  output logic [31:0]       c_noncoherent_desc
);

  // AxCACHE encodings used here:
  //   4'b0010 -- non-cacheable, bufferable
  //   4'b1111 -- write-back, read+write allocate, shareable
  localparam logic [3:0] CACHE_NC    = 4'b0010;
  localparam logic [3:0] CACHE_WB_SH = 4'b1111;

  wire is_desc = (req_kind == 2'd0) || (req_kind == 2'd2);
  wire coherent_wanted = is_desc ? cfg_desc_coherent : cfg_data_coherent;

  assign ax_cache = coherent_wanted ? CACHE_WB_SH : CACHE_NC;
  assign ax_prot  = 3'b010;                       // unprivileged, non-secure, data
  assign ax_snoop_required = coherent_wanted;

  // How many descriptors share one cache line? Section 14's subject.
  assign desc_per_line = (cfg_cache_line_b == '0) ? 16'd1
                       : (cfg_cache_line_b / 16'(DESC_BYTES));

  assign desc_shares_line = is_desc && (desc_per_line > 16'd1);

  // Does this request cross a cache-line boundary? A descriptor that
  // straddles is worse than one that merely shares: TWO lines must be
  // maintained for one descriptor, and a partial maintenance is a
  // partial update.
  wire [15:0] off_in_line = cfg_cache_line_b == '0 ? 16'd0
                          : (req_addr[15:0] % cfg_cache_line_b);
  assign desc_straddles_line = is_desc && (cfg_cache_line_b != '0) &&
                               ((off_in_line + req_bytes) > cfg_cache_line_b);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_straddles <= '0; c_noncoherent_desc <= '0;
    end else if (req_valid) begin
      if (desc_straddles_line)              c_straddles <= c_straddles + 1;
      if (is_desc && !cfg_desc_coherent)
        c_noncoherent_desc <= c_noncoherent_desc + 1;
    end
  end

endmodule

Classification: per-traffic-class attribute generation with cache-line geometry reporting.

What it teaches: that descriptors and frame data want opposite attributes and a design that gives them the same one is wrong for one of them. A descriptor is 16 octets, written by the MAC, and read by the CPU within microseconds — so it wants to be coherent, and a snoop is cheaper than a cache maintenance operation. Frame data is up to 1518 octets and may never be read at all — a frame destined for a socket nobody is reading, or discarded by a filter — so allocating it into the CPU's cache evicts something useful for no benefit.

And it teaches that desc_per_line is a number the MAC should compute and report. At a 64-octet cache line and 16-octet descriptors it is four, and Section 14 is about the bug that number produces. The MAC cannot fix it; it can be the only component in the system that has both numbers in one place.

Deliberately simplified: AxCACHE is presented as two constants where the encoding has sixteen values with meaningful distinctions — read-allocate without write-allocate is often exactly right for frame data, and it is not offered here. The modulo in off_in_line is a real modulo and will synthesise badly for a non-power-of-two line size; cache lines are powers of two, so a mask is correct and the modulo is written for clarity. There is also no handling of a system where coherency is per-region rather than per-transaction.

Production implication: c_noncoherent_desc rising on a system the integrator believes is coherent is a configuration error with a severe and delayed symptom. Non-coherent descriptor traffic on a coherent system works — the MAC reads DRAM, the driver's stores eventually reach DRAM — until the driver's store is still in a cache, at which point the MAC fetches a descriptor that says OWN_DRIVER when the driver believes it handed it over. The port stalls and recovers when the line is naturally evicted, which produces intermittent stalls of unpredictable duration and no error anywhere.


At a 64-octet cache line and a 16-octet descriptor, four descriptors share one line. On a coherent system this is harmless because the coherency protocol works at line granularity. On a non-coherent system it is silent data loss, in four steps. First the driver reads descriptor 4 and the whole line, containing descriptors 4 through 7, enters its cache. Second the MAC completes frames into descriptors 5, 6 and 7, updating them in DRAM, which the driver's cached copy knows nothing about. Third the driver writes a fresh buffer pointer and ownership into its cached copy of descriptor 4. Fourth the driver cleans the line so that descriptor 4 becomes visible to the MAC — and the clean writes back all 64 octets, overwriting descriptors 5, 6 and 7 in DRAM with the stale copies from step one. Three completed frames revert to MAC-owned with old contents and vanish, with no error counter anywhere. The symptom is the ring appearing to go backwards. The complete fixes are making descriptor traffic coherent, or padding each descriptor to a full cache line, which multiplies the ring's memory by four — 512 kilobytes for both directions of a 4096-entry ring.64B line / 16Bdesc4 descriptors share it1. Driver readsdesc 4line 4,5,6,7 cached2. MAC completes5,6,7in DRAM3. Driver writesdesc 4in its cached copy4. Driver cleansthe linewrites back all 64octets5,6,7 reverted3 frames vanish, noerrorThe ring goesbackwardsthe unambiguous tellFix: coherentdescriptorssnoop bandwidthFix: pad to a line4x the ring memory12
Figure 3 — four descriptors in one cache line, and the clean that reverts three of them.

14. The Cache Line That Holds Four Descriptors

This section is one specific bug. It is here because it is the most common serious defect in descriptor-ring integration, it is invisible in simulation, and the fix costs four times the ring's memory.

The geometry. A 64-octet cache line and a 16-octet descriptor means four descriptors share one line.

Descriptor sizePer 64-octet linePer 128-octet line
16 octets48
32 octets24
64 octets12

On a coherent system this is harmless — the coherency protocol works at line granularity and the hardware handles the sharing. On a non-coherent system it is a data-corruption bug, and here is exactly how.

The driver refills descriptor 4. Descriptors 4, 5, 6 and 7 are in one line.

StepThe driverThe MACMemory
1reads descriptor 4 — the line is cachedline: 4,5,6,7
2completes frames into 5, 6, 75,6,7 updated in DRAM
3writes buf_ptr and own into its cached copy of 4
4cleans the line to make 4 visible5,6,7 OVERWRITTEN with the stale cached copy

Step 4 is the bug and nothing about it is wrong. A cache clean writes back a line, because that is the only granularity a cache has. The driver's cached copy of descriptors 5, 6 and 7 is from step 1 — before the MAC wrote them — so cleaning descriptor 4 silently reverts three completed frames to "owned by the MAC" with stale lengths.

The symptoms, and why they are so hard:

SymptomWhy it misleads
frames disappearcounted as received by the MAC, never seen by software
the ring appears to go backwardsdescriptors the driver processed are owned by the MAC again
it depends on timingonly when the MAC writes 5–7 between the driver's read and clean
it vanishes under a debuggerthe timing window closes
simulation never shows itthe testbench has no CPU cache

Row five is the important one for a verification engineer. A MAC's RTL testbench models memory as an array. There is no cache, so there is no line, so there is no clobber — and the bug is structurally unreachable in the environment where it would be cheapest to find.

The fixes, and their costs:

FixWorksCost
make descriptor traffic coherentyes, completelysnoop bandwidth; not always available
pad descriptors to a full cache lineyes4× the ring memory
allocate the ring from non-cacheable memoryyesevery driver descriptor access is a DRAM round trip
use finer-grained maintenanceno — the granularity is the line

Row two is what a portable driver does and the cost is worth stating properly.

Ring depth16-octet descriptorsPadded to 64Both directions, padded
2564.00 KiB16.00 KiB32.00 KiB
102416.00 KiB64.00 KiB128.00 KiB
409664.00 KiB256.00 KiB512.00 KiB

Half a megabyte of DRAM for the rings of one port, to work around a granularity mismatch of 48 octets per descriptor. And Section 17 shows that a 100 Gb/s port genuinely wants 4096 descriptors, so the bottom row is not hypothetical.

Row three is what many embedded drivers do and its cost is the one people underestimate. A non-cacheable ring means every descriptor field the driver touches is a DRAM access — and a driver examining status, length and flags touches three, each a hundred cycles or more. At Chapter 18.1 §10's frame rates that is its own livelock.

The general lesson, stated so it transfers: whenever two agents write different objects that share a cache line, the coarser agent's maintenance granularity silently reverts the finer agent's writes. It is not specific to Ethernet, to descriptors, or to DMA. It is a property of putting two owners' data in one line, and the only complete fixes are coherency or separation.


15. RTL 7 — Ring Telemetry

Six counters, and between them they answer every question this chapter's failures raise.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ring_telemetry -- the ring's observable state.
//
// Each counter separates a pair of conditions that produce the same
// symptom. That is the selection criterion: a counter that does not
// distinguish two otherwise-identical situations is not worth its
// flops.
// -----------------------------------------------------------------------
module ring_telemetry
  import ring_pkg::*;
(
  input  logic              clk,
  input  logic              rst_n,

  input  logic              desc_fetched,
  input  logic              desc_not_owned,        // section 3
  input  logic              frame_dropped_no_desc,
  input  logic              handoff_done,
  input  logic              prefetch_flushed,
  input  logic              table_full,            // section 9
  input  logic [15:0]       mac_occupancy,
  input  logic [15:0]       ring_len,
  input  logic [31:0]       wait_cycles,

  // For the ownership-bit audit: what the MAC read, and when.
  input  logic              audit_enable,
  input  logic [ADDR_W-1:0] fetched_buf_ptr,

  output logic [31:0]       c_fetched,
  output logic [31:0]       c_starved,
  output logic [31:0]       c_dropped_no_desc,
  output logic [31:0]       c_handoffs,
  output logic [31:0]       c_flushes,
  output logic [31:0]       c_table_full,
  output logic [15:0]       c_peak_occupancy,
  output logic [31:0]       c_mean_wait_num,
  output logic [31:0]       c_mean_wait_den,
  output logic [ADDR_W-1:0] audit_last_ptr,
  output logic [15:0]       occupancy_pct
);

  // Occupancy as a percentage of the ring, which is what section 17's
  // sizing argument is checked against.
  always_comb begin
    if (ring_len == '0) occupancy_pct = 16'd0;
    else occupancy_pct = 16'((32'(mac_occupancy) * 32'd100) / 32'(ring_len));
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_fetched <= '0; c_starved <= '0; c_dropped_no_desc <= '0;
      c_handoffs <= '0; c_flushes <= '0; c_table_full <= '0;
      c_peak_occupancy <= '0; c_mean_wait_num <= '0; c_mean_wait_den <= '0;
      audit_last_ptr <= '0;
    end else begin
      if (desc_fetched)          c_fetched         <= c_fetched + 1;
      if (desc_not_owned)        c_starved         <= c_starved + 1;
      if (frame_dropped_no_desc) c_dropped_no_desc <= c_dropped_no_desc + 1;
      if (prefetch_flushed)      c_flushes         <= c_flushes + 1;
      if (table_full)            c_table_full      <= c_table_full + 1;

      if (handoff_done) begin
        c_handoffs      <= c_handoffs + 1;
        c_mean_wait_num <= c_mean_wait_num + wait_cycles;
        c_mean_wait_den <= c_mean_wait_den + 1;
      end

      if (mac_occupancy > c_peak_occupancy)
        c_peak_occupancy <= mac_occupancy;

      // Section 12: the only thing hardware can contribute to
      // diagnosing a missing store barrier in the driver.
      if (audit_enable && desc_fetched)
        audit_last_ptr <= fetched_buf_ptr;
    end
  end

endmodule

Classification: a counter set chosen so that each entry separates two conditions with identical symptoms.

What it teaches: that c_starved and c_dropped_no_desc are different events and conflating them loses the distinction that matters. Starvation means the MAC looked and found no descriptor and no frame was lost, because the receive FIFO absorbed it — Chapter 18.1 §8's depth. A drop means the FIFO ran out too. The first is a warning with margin remaining; the second is data loss, and the gap between the two counts is exactly how much margin the FIFO provided.

And it teaches that audit_last_ptr is worth the sixty-four flops. Section 12 established that the MAC cannot detect a missing store barrier in the driver. It can record what it actually fetched — and a driver author who compares that against the pointer the driver believes it wrote has the bug identified in minutes. The register is useless in the steady state and decisive once.

Deliberately simplified: occupancy_pct contains a divide, evaluated combinationally, which no design does — a real telemetry block reports the raw occupancy and lets software divide. The mean-wait accumulator will overflow on a long-running system, and the right structure is a windowed accumulator reset on read. And the audit register holds only the most recent pointer where a ring buffer of the last sixteen would be far more useful for a bug that reproduces rarely.

Production implication: the ratio c_starved to c_fetched is the number that sizes the ring, and it is the input to Section 17's derivation that cannot be predicted. Section 17 computes a depth from a driver's worst-case scheduling latency; nobody knows that latency in advance, because it depends on the operating system, the interrupt load, and what else is running. This counter measures it after the fact, and a port showing 0.1% starvation has a ring that is correctly sized for its actual software rather than for a guess.


16. RTL 8 — The Ring Conformance Monitor

The last block checks the invariants that can be checked from the MAC's side, and it is explicit that the most important one cannot be.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ring_conformance_monitor -- ownership-protocol invariants.
//
// The protocol has four rules. Three are checkable here. The fourth
// -- that the driver ordered its stores -- is not checkable by any
// hardware, and section 12 explains why.
// -----------------------------------------------------------------------
module ring_conformance_monitor
  import ring_pkg::*;
(
  input  logic              clk,
  input  logic              rst_n,

  input  logic              fetch_valid,
  input  descriptor_t       fetched_desc,
  input  logic [15:0]       fetch_index,

  input  logic              handoff_valid,
  input  logic [15:0]       handoff_index,

  input  logic              cfg_len_not_pow2,
  input  logic              own_before_fields,
  input  logic              barrier_bypassed,
  input  logic              desc_straddles_line,
  input  logic              mode_unsupported,

  input  logic [31:0]       c_starved,
  input  logic [31:0]       c_fetched,
  input  logic [31:0]       c_flushes,
  input  logic [31:0]       c_table_full,

  output logic              ring_protocol_ok,
  output logic              cfg_fault,
  output logic              ordering_fault,
  output logic              driver_slow,
  output logic              driver_rewriting,
  output logic              in_flight_too_small,
  output logic              none_of_the_above
);

  // Configuration faults: wrong from the moment they are written.
  assign cfg_fault = cfg_len_not_pow2 | mode_unsupported |
                     desc_straddles_line;

  // Ordering faults: a design bug, never a configuration one.
  assign ordering_fault = own_before_fields | barrier_bypassed;

  // The driver is slow if the MAC frequently finds no descriptor.
  assign driver_slow = (c_fetched > 32'd1000) &&
                       (c_starved > (c_fetched >> 5));    // above ~3%

  // The driver is WRONG if it rewrites the ring under the prefetch.
  assign driver_rewriting = (c_flushes > 32'd100);

  // Section 9's table is undersized if it fills.
  assign in_flight_too_small = (c_table_full > 32'd1000);

  assign ring_protocol_ok = !cfg_fault && !ordering_fault;

  assign none_of_the_above = ring_protocol_ok && !driver_slow &&
                             !driver_rewriting && !in_flight_too_small;

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

  // A descriptor the MAC consumes must have been owned by the MAC.
  p_only_consume_owned:
    assert property (@(posedge clk) disable iff (!rst_n)
      fetch_valid && (fetched_desc.ctl.own == OWN_DRIVER)
        |-> ##1 !handoff_valid)
    else $error("a descriptor owned by the driver was consumed");

  // Handoffs follow fetches in order -- the ring is FIFO.
  p_handoff_follows_fetch_in_order:
    assert property (@(posedge clk) disable iff (!rst_n)
      handoff_valid |-> (handoff_index != fetch_index) ||
                        !fetch_valid)
    else $error("a descriptor was handed off in the cycle it was fetched");

  // An ordering fault is never compatible with an OK verdict.
  p_ordering_fault_is_fatal:
    assert property (@(posedge clk) disable iff (!rst_n)
      ordering_fault |-> !ring_protocol_ok)
    else $error("ring_protocol_ok asserted with an ordering fault present");

  // A buffer pointer of zero is a descriptor the driver never filled.
  p_no_null_buffer:
    assert property (@(posedge clk) disable iff (!rst_n)
      (fetch_valid && (fetched_desc.ctl.own == OWN_MAC))
        |-> (fetched_desc.buf_ptr != '0))
    else $error("a MAC-owned descriptor had a null buffer pointer");
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // A buffer length of zero is the same class of bug and has a
  // different consequence: the DMA writes nothing and reports a
  // truncation, which looks like a network problem.
  p_no_zero_length_buffer:
    assert property (@(posedge clk) disable iff (!rst_n)
      (fetch_valid && (fetched_desc.ctl.own == OWN_MAC))
        |-> (fetched_desc.buf_len != 16'd0))
    else $error("a MAC-owned descriptor had a zero-length buffer");

endmodule

Classification: a verdict generator over ownership-protocol invariants, with an explicit statement of what it cannot check.

What it teaches: that p_no_null_buffer is the closest hardware gets to detecting Section 12's missing barrier, and it is not close. A descriptor handed to the MAC with a null pointer is a store-ordering violation caught by luck — the ownership store became visible and the pointer store did not, and the pointer happened to be zero because the descriptor was freshly allocated. On a ring that has wrapped, the stale pointer is the previous buffer's, which is non-null and entirely plausible. So the check fires on the first pass through the ring and never again.

And it teaches why driver_slow and driver_rewriting are separate verdicts. Both reduce throughput. The first is a sizing problem with a known fix — Section 17's depth. The second is a driver bug: rewriting descriptors the MAC has already prefetched means the driver is not respecting ownership, and no amount of ring depth helps. One combined "the driver is the problem" verdict sends the wrong fix.

Deliberately simplified: the thresholds are literals — 3% starvation, 100 flushes, 1000 table-full cycles — where a production monitor takes them from registers. p_handoff_follows_fetch_in_order is much weaker than its name suggests: it checks a single cycle rather than genuinely tracking order, which would need a queue model. And the listing is split across two code blocks purely so the module's length stays readable; it is one module.

Production implication: none_of_the_above again. A port whose ring telemetry is clean says the ring is not the problem — and given that this chapter has described four distinct ring failures, three of which present as "frames are missing", an explicit all-clear removes four hypotheses at once and sends the investigation to Chapter 18.3's DMA path or to Chapter 18.1 §8's FIFO.


17. Sizing the Ring

The ring's depth is set by one question: how long can the driver be away before the MAC runs out of descriptors?

The derivation is a multiplication. Depth equals the frame rate times the driver's worst-case latency — and the frame rate is Chapter 18.1 §4's, at minimum frame size, because that is when descriptors are consumed fastest.

Line rate10 µs100 µs1 ms10 ms
1 Gb/s151491 48814 881
10 Gb/s1491 48814 881148 810
25 Gb/s3723 72037 202372 024
100 Gb/s1 48814 881148 8101 488 095

The column headings are the argument and they are worth naming.

LatencyWhat produces it
10 µsan interrupt taken promptly on an idle core
100 µsa scheduler tick, or a higher-priority interrupt
1 msa scheduler quantum; a driver thread descheduled
10 msa slow path, a page fault, a debugger, a hypervisor

Nobody designs for 10 ms and 1 ms happens. A 100 Gb/s port that must survive a 1 ms driver absence needs 148 810 descriptors, which at 16 octets each is 2.27 MiB and at Section 14's padded 64 octets is 9.09 MiB. That is not a ring; it is a memory allocation nobody will approve.

So the sizing is not "how long can the driver be away" — it is "how long can the driver be away before we accept dropping frames", and the honest answer is much shorter.

Read the table the other way. What does a real ring cover?

Line rateFrame size256 entries4096 entries
1 Gb/s64172.03 µs2 752.51 µs
1 Gb/s15183 149.82 µs50 397.18 µs
10 Gb/s6417.20 µs275.25 µs
25 Gb/s646.88 µs110.10 µs
100 Gb/s641.72 µs27.53 µs
100 Gb/s151831.50 µs503.97 µs

The 100 Gb/s minimum-frame row is the one that decides the design. A 256-entry ring covers 1.72 µs — less than one scheduler tick, less than one interrupt latency on a loaded system. A 4096-entry ring covers 27.53 µs, which is a real interrupt latency and not a real scheduling latency.

Which is why a 100 Gb/s port cannot be driven by interrupts alone and why Chapter 18.1 §10's third mechanism — polling under load — is not an optimisation but a requirement. A polling driver's latency is its loop period, which is microseconds, and that is the only regime in which the table's numbers are affordable.

And the row above it is the escape hatch that real deployments use. At 1518-octet frames the same 4096-entry ring covers 503.97 µseighteen times longer — so a port whose traffic is mostly large frames tolerates a scheduler tick. The ring's adequacy is decided by the frame-size distribution, which is chosen by whoever is sending, which is the same conclusion Chapter 18.1 §10 reached about interrupts and for the same reason.

The practical rule, stated as three numbers to write down:

Write downFor a 100 Gb/s port
the ring's coverage at minimum frame size27.53 µs at 4096
the driver's measured worst-case latencywhatever it is — measure it
the ratiobelow 1 means designed drops

Most projects have the first number and not the second, and the ratio is discovered when a burst of small frames meets a scheduler tick.


A ring's depth buys time, and the amount of time is smaller than intuition suggests. On a 100 gigabit port at minimum frame size the frame rate is 148.81 million per second, so a 256-entry ring covers 1.72 microseconds and a 4096-entry ring covers 27.53 microseconds. Against that, the latencies a driver actually experiences are an interrupt taken promptly on an idle core at about 10 microseconds, a scheduler tick or a higher-priority interrupt at about 100 microseconds, a scheduler quantum with the driver thread descheduled at about 1 millisecond, and a slow path, page fault, debugger or hypervisor at about 10 milliseconds. Only the first of those fits inside a 4096-entry ring. Covering 1 millisecond would require 148810 descriptors, which is 2.27 megabytes raw or 9.09 megabytes once padded to cache lines — an allocation nobody approves. The conclusion is that ring depth cannot cover a descheduled driver, so a 100 gigabit port must be polled under load rather than driven by interrupts alone. The escape hatch is frame size: the same 4096-entry ring covers 503.97 microseconds at 1518-octet frames, eighteen times longer, so a port carrying mostly large frames tolerates a scheduler tick.4096 entriesat 100 Gb/s, 64-octetframesCovers 27.53 us256 entries: 1.72 usInterrupt, idle core~10 us — FITSScheduler tick~100 us — does not fitDescheduled thread~1 ms — needs 148810148810 descriptors2.27 MiB, 9.09 MiB paddedSo: poll under loadnot a deeper ringOr: large frames503.97 us — 18x longer12
Figure 4 — what a ring actually buys, against the latencies a driver really experiences.

18. The Cost, Accounted

The ring's cost is in three places and only one of them is in the MAC.

In the MAC — logic:

BlockApproximate cost
descriptor_store — 8 × 16 octets plus valid bits~1050 flops
ring_pointers — two 20-bit counters and an adder~120 flops
ownership_handoff — a small FSM~150 flops
descriptor_write_order — 32 entries × 90 bits~2900 flops + three priority encoders
ordering_barrier~40 flops
coherency_adapter~80 flops
ring_telemetry~350 flops
ring_conformance_monitor~100 flops

About 4800 flops, dominated by the in-flight table — which is Section 9's IN_FLIGHT = 32, and which exists entirely to make Section 7's round-trip wait affordable. The ordering requirement's hardware cost is one table.

In DRAM — the rings:

Ring depth16-octetPadded to 64Both directions, padded
2564.00 KiB16.00 KiB32.00 KiB
102416.00 KiB64.00 KiB128.00 KiB
409664.00 KiB256.00 KiB512.00 KiB

And in bandwidth — the descriptor traffic Chapter 18.1 §4 derived, now reduced:

DesignTransactions per frameAt 100 Gb/sPer cycle at 250 MHz
no batching3.000446.43 M1.786
fetch batched by 82.125316.22 M1.265
both batched by 81.250186.01 M0.744
both batched by 161.125167.41 M0.670

Row three is the design point and the reason is diminishing returns: row four costs twice the buffering for a 10% further reduction, and 0.744 already fits.

Put the three together for a 100 Gb/s port:

CostAgainst
logic~4800 flopsChapter 18.1's 3300 — comparable
DRAM512 KiBa system's DRAM — negligible
memory transactions186 M/s250 M/s available — 74%

The last row is the one that was the problem and is now merely tight. Chapter 18.1 §12 opened with 1.786 transactions per cycle and no answer; the ring closes it at 0.744, with 26% of the address channel left for everything else on the chip — which is not much, and is the reason Chapter 18.7's multi-queue exists.

And the ordering machinery is close to free in every currency except one. The barrier costs no DRAM, negligible bandwidth, and 2900 flops of in-flight table60% of the chapter's total logic, spent entirely on making a correctness requirement affordable at rate. A design that omits it saves 60% of this chapter's gates and corrupts one frame in however many the race catches, which is the trade nobody states explicitly and several designs have made by accident.


19. Properties Worth Asserting, and One Worth Refusing

The ownership protocol's invariants divide into three groups: the descriptor's own well-formedness, the MAC's write ordering, and the ring's index arithmetic. The rejected property is a fourth thing that looks like the first.

Descriptor well-formedness.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A descriptor the MAC consumes must be owned by the MAC.
p_consume_requires_ownership:
  assert property (@(posedge clk) disable iff (!rst_n)
    take |-> (take_desc.ctl.own == OWN_MAC))
  else $error("a descriptor not owned by the MAC was consumed");

// A MAC-owned descriptor has a usable buffer.
p_owned_has_buffer:
  assert property (@(posedge clk) disable iff (!rst_n)
    (fetch_valid && (fetched_desc.ctl.own == OWN_MAC))
      |-> (fetched_desc.buf_ptr != '0) && (fetched_desc.buf_len != '0))
  else $error("a MAC-owned descriptor was unusable");

// The MAC never writes a descriptor it does not own.
p_no_write_without_ownership:
  assert property (@(posedge clk) disable iff (!rst_n)
    (wr_valid && !wr_is_ownership) |-> (st != H_IDLE))
  else $error("a descriptor field was written outside a handoff");

// After handing ownership away, the MAC does not touch it again.
p_no_touch_after_handoff:
  assert property (@(posedge clk) disable iff (!rst_n)
    (wr_is_ownership && wr_ready) |=> !wr_valid until_with complete_valid)
  else $error("a descriptor was written after ownership was transferred");

// The buffer length the driver supplied is never exceeded.
p_length_within_buffer:
  assert property (@(posedge clk) disable iff (!rst_n)
    complete_valid |-> (rx_length <= take_desc.buf_len))
  else $error("the reported length exceeds the buffer the driver supplied");

// A truncation is reported when and only when it happened.
p_truncation_reported:
  assert property (@(posedge clk) disable iff (!rst_n)
    (complete_valid && (rx_length == take_desc.buf_len))
      |-> status[ST_TRUNCATED] || status[ST_GOOD])
  else $error("a frame filling the buffer exactly reported neither state");

Write ordering — the chapter's subject.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The ownership write is never issued before its field write has
// RESPONDED. This is the whole correctness argument in one property.
p_ownership_after_fields_respond:
  assert property (@(posedge clk) disable iff (!rst_n)
    (ow_valid && ow_ready) |-> (est[ow_tag] == E_FIELDS_DONE))
  else $error("an ownership write was issued before its fields completed");

// The invariant signal never asserts.
p_own_before_fields_never:
  assert property (@(posedge clk) disable iff (!rst_n)
    !own_before_fields)
  else $error("the ordering invariant was violated");

// A slot's field write is issued exactly once.
p_field_write_once:
  assert property (@(posedge clk) disable iff (!rst_n)
    (fw_valid && fw_ready) |=> (est[fw_tag] != E_FREE))
  else $error("a slot was freed with its field write in flight");

// Every field-write response matches an allocated slot.
p_response_matches_slot:
  assert property (@(posedge clk) disable iff (!rst_n)
    fw_done |-> (est[fw_done_tag] != E_FREE))
  else $error("a field-write response arrived for a free slot");

// Same for ownership responses.
p_own_response_matches_slot:
  assert property (@(posedge clk) disable iff (!rst_n)
    ow_done |-> (est[ow_done_tag] == E_OWN_OUT))
  else $error("an ownership response arrived for a slot not awaiting one");

// A slot allocated is eventually freed.
p_slot_eventually_freed:
  assert property (@(posedge clk) disable iff (!rst_n)
    (enq_valid && enq_ready) |-> ##[1:$] (est[alloc_i] == E_FREE))
  else $error("a slot was allocated and never freed");

// The unsafe mode is never selected in a production configuration.
p_barrier_never_bypassed:
  assert property (@(posedge clk) disable iff (!rst_n)
    !barrier_bypassed)
  else $error("the ordering barrier was bypassed");

// Mode 2 is only issued when the descriptor fits one beat.
p_combined_only_when_it_fits:
  assert property (@(posedge clk) disable iff (!rst_n)
    issue_combined |-> !mode_unsupported)
  else $error("a combined write was issued where it cannot be atomic");

Ring index arithmetic.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The fetch pointer never passes the done pointer by more than the
// ring's length.
p_occupancy_bounded:
  assert property (@(posedge clk) disable iff (!rst_n)
    (idx_fetch - idx_done) <= {4'b0, cfg_len})
  else $error("the MAC has more descriptors outstanding than the ring holds");

// Both pointers only advance.
p_pointers_monotonic:
  assert property (@(posedge clk) disable iff (!rst_n)
    ##1 ((idx_fetch >= $past(idx_fetch)) && (idx_done >= $past(idx_done))))
  else $error("a ring pointer went backwards");

// The done pointer never overtakes the fetch pointer.
p_done_never_passes_fetch:
  assert property (@(posedge clk) disable iff (!rst_n)
    idx_done <= idx_fetch)
  else $error("a descriptor was completed before it was fetched");

// Addresses stay inside the ring.
p_address_within_ring:
  assert property (@(posedge clk) disable iff (!rst_n)
    (cfg_len != '0) |->
      ((fetch_addr >= cfg_base) &&
       (fetch_addr < (cfg_base + (cfg_len * DESC_BYTES)))))
  else $error("a descriptor address fell outside the ring");

// A non-power-of-two length is flagged rather than used.
p_len_pow2_flagged:
  assert property (@(posedge clk) disable iff (!rst_n)
    (cfg_valid && ((cfg_len & (cfg_len - 1)) != 0)) |-> cfg_len_not_pow2)
  else $error("a non-power-of-two ring length was accepted silently");

// A configuration write resets both pointers together.
p_cfg_resets_both:
  assert property (@(posedge clk) disable iff (!rst_n)
    cfg_valid |=> ((idx_fetch == '0) && (idx_done == '0)))
  else $error("a ring reconfiguration left a pointer stale");

Prefetch window.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A flush empties the window completely.
p_flush_empties:
  assert property (@(posedge clk) disable iff (!rst_n)
    flush |=> (level == '0))
  else $error("a flush left entries valid");

// A taken entry is invalidated.
p_take_invalidates:
  assert property (@(posedge clk) disable iff (!rst_n)
    (take && take_valid) |=> !$past(win_valid[rd_ptr]))
  else $error("a taken descriptor stayed valid");

// The window never reports more entries than it holds.
p_level_bounded:
  assert property (@(posedge clk) disable iff (!rst_n)
    level <= PREFETCH)
  else $error("the prefetch level exceeded the window size");

// Starvation and a valid take are mutually exclusive.
p_starved_excludes_take:
  assert property (@(posedge clk) disable iff (!rst_n)
    starved |-> !take_valid)
  else $error("a descriptor was offered while starved");

Coherency and cache geometry.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Descriptor traffic carries the configured attribute.
p_desc_attribute_matches_config:
  assert property (@(posedge clk) disable iff (!rst_n)
    (req_valid && (req_kind == 2'd0) && cfg_desc_coherent)
      |-> (ax_cache == 4'b1111))
  else $error("a descriptor fetch was issued non-coherent while configured coherent");

// A straddling descriptor is flagged every time it is seen.
p_straddle_always_flagged:
  assert property (@(posedge clk) disable iff (!rst_n)
    (req_valid && desc_straddles_line) |=>
      (c_straddles > $past(c_straddles)))
  else $error("a cache-line straddle was not counted");

// The descriptors-per-line calculation never reports zero.
p_desc_per_line_nonzero:
  assert property (@(posedge clk) disable iff (!rst_n)
    desc_per_line != '0)
  else $error("descriptors per line computed as zero");

Telemetry.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Every counter is monotonic.
p_counters_monotonic:
  assert property (@(posedge clk) disable iff (!rst_n)
    ##1 ((c_fetched >= $past(c_fetched)) &&
         (c_starved >= $past(c_starved)) &&
         (c_handoffs >= $past(c_handoffs))))
  else $error("a telemetry counter decreased");

// Handoffs never exceed fetches.
p_handoffs_le_fetches:
  assert property (@(posedge clk) disable iff (!rst_n)
    c_handoffs <= c_fetched)
  else $error("more descriptors were completed than were fetched");

// The peak occupancy never exceeds the ring.
p_peak_within_ring:
  assert property (@(posedge clk) disable iff (!rst_n)
    (ring_len != '0) |-> (c_peak_occupancy <= ring_len))
  else $error("peak occupancy exceeded the ring length");

// A drop implies a starve occurred first.
p_drop_implies_starve:
  assert property (@(posedge clk) disable iff (!rst_n)
    frame_dropped_no_desc |-> (c_starved != '0))
  else $error("a frame was dropped for lack of a descriptor with no starvation recorded");

// The audit register only updates when auditing is on.
p_audit_gated:
  assert property (@(posedge clk) disable iff (!rst_n)
    !audit_enable |=> $stable(audit_last_ptr))
  else $error("the audit register updated with auditing disabled");

20. Verification Scenarios

Fifty-seven scenarios. The last group is the directed test, and it is the one that needs a memory model most testbenches do not have.

Descriptor and ownership — 9 scenarios.

#ScenarioExpected
1fetch a descriptor owned by the MACconsumed
2fetch one owned by the drivernot consumed, c_starved rises
3complete a framefields then ownership, in that order
4a descriptor with a null buffer pointerp_owned_has_buffer fires
5a descriptor with buf_len zeroflagged, not used
6a frame longer than buf_lentruncated, ST_TRUNCATED set
7a frame exactly buf_lenST_GOOD, not truncated
8an FCS failureST_FCS_ERR, ownership still transferred
9a runtST_RUNT, ownership still transferred

Row eight and nine matter: a bad frame still needs its descriptor returned, or the ring leaks one entry per error and eventually stops.

Ring pointers and wrap — 8 scenarios.

#ScenarioExpected
10256 fetches on a 256-entry ringone wrap, c_wraps = 1
1165 536 fetchesindices wrap; addresses stay in range
12a ring length of 1000cfg_len_not_pow2, refused
13a ring length of 1degenerate but legal
14reconfigure the base while runningChapter 18.1 §3 refuses the write
15reconfigure while disabledboth pointers reset
16occupancy reaches the ring lengthfetch stalls, no overrun
17a base address not descriptor-alignedaddresses are misaligned — flagged

Prefetch window — 7 scenarios.

#ScenarioExpected
18fill 8, take 8level 8 to 0, c_fills = 1
19fill 8 where entry 3 is driver-owned3 taken, then starved
20flush mid-windowlevel = 0, c_flushes rises
21take with an empty windowtake_valid low
22a partial fill (bus error mid-burst)only filled entries valid
23fill while takingboth proceed
24the driver rewrites a prefetched entrythe MAC uses its stale copy — legal, Section 3

Write ordering — 10 scenarios.

#ScenarioExpected
25mode 1, one handofffields issued, response awaited, then ownership
26mode 1, response delayed 200 nsc_wait_cycles rises; ownership still last
27mode 0both writes carry cfg_shared_id
28mode 2 with BUS_BYTES = 16one combined write
29mode 2 with BUS_BYTES = 8mode_unsupported, nothing issued
30mode 3barrier_bypassed asserts
3132 handoffs in flighttable fills, table_full
3233rd handoff while fullenq_ready low; no drop
33a field write returns an errorcounted; ownership still ordered after
34responses arrive out of order by tageach slot advances independently

Row 34 is the pipelining test and the one that catches the bug own_before_fields exists for.

Coherency and cache geometry — 8 scenarios.

#ScenarioExpected
35descriptors coherent, data non-coherenttwo different ax_cache values
36both configured coherentax_snoop_required on all traffic
37a 64-octet line, 16-octet descriptorsdesc_per_line = 4, desc_shares_line
38a 64-octet line, 64-octet descriptorsdesc_per_line = 1, not shared
39a descriptor at line offset 56, 16 octetsdesc_straddles_line, c_straddles rises
40a descriptor at offset 48fits exactly, no straddle
41line size configured zerodesc_per_line = 1, no division by zero
42non-coherent descriptors on a coherent systemc_noncoherent_desc rises

Telemetry and verdicts — 9 scenarios.

#ScenarioExpected
431000 fetches, 40 starvesdriver_slow — above 3%
441000 fetches, 20 starvesnot driver_slow
45101 flushesdriver_rewriting
461001 table-full cyclesin_flight_too_small
47a straddling descriptorcfg_fault, not ring_protocol_ok
48mode 3 selectedordering_fault, not OK
49everything cleannone_of_the_above
50peak occupancy above the ring lengthp_peak_within_ring fires
51audit disabledaudit_last_ptr stable

The directed test — 6 runs random stimulus will not produce.

Random stimulus will not produce this because the reordering it depends on is not in the testbench. A conventional memory model is an array: two writes land in issue order, always. The failure this chapter exists to prevent requires a model that holds outstanding writes and may deliver them in either order — and no amount of random frame stimulus creates that behaviour if the model does not have it.

So the test is two things: a memory model that reorders, and a set of runs across the barrier modes.

RunBarrier modeMemory modelExpected outcome
Amode 1 — wait for responsereordering, delay up to 200 nsobserver never sees a stale length
Bmode 0 — same IDreordering, same-ID ordering honouredobserver never sees a stale length
Cmode 0 — same IDreordering, same-ID ordering VIOLATEDstale lengths observed
Dmode 2 — combined writereorderingnever stale — one write
Emode 3 — no barrierreorderingstale lengths, at a measurable rate
Fmode 3 — no barrierin-order array (a conventional model)never stale — the false pass

Run F is the point of the whole construction. It is the configuration with no barrier at all, and in a conventional testbench it passes — which is exactly what Section 19's rejected property demonstrates and why the property is worthless. A regression consisting only of run F reports full coverage of the ordering requirement and has tested nothing.

Run C is the second finding and it is the one that decides a real design's mode. Mode 0 is correct only if every component in the path honours same-ID ordering; run C models a path that does not, and it distinguishes a design that depends on that guarantee from one that does not. A design shipping mode 0 has an obligation to have run C and know it fails — because that is the failure it is betting against.

The oracle, in four parts:

CheckRuns A, B, DRun C, ERun F
observer stale-length countzeronon-zerozero — meaningless
own_before_fieldslowlowlow
barrier_bypassedlowlow in C, high in Ehigh
the test's verdictpassfail, correctlypass, falsely

Row two is worth reading carefully. own_before_fields stays low in every run — the MAC is behaving correctly throughout. The corruption in runs C and E happens entirely in the memory system, which is Section 8's point made experimentally: the MAC is correct and the data is wrong.

And row four is the deliverable. A verification report that contains run F alone claims the ordering requirement is covered. One that contains A through F shows which barrier modes are safe on which memory models, which is a statement a system integrator can act on.


21. Debugging a Ring

Ring failures produce four complaints and each has a counter that resolves it. The hard part is that three of the four sound like network problems.

Complaint 1 — "frames are missing."

CheckIf yesMeaning
c_dropped_no_desc non-zero?the ring ran dry and the FIFO tooSection 17's sizing
c_starved high, drops zero?the ring ran dry, the FIFO covered itmarginal, not yet failing
descriptors owned by the MAC that software processed?Section 14's cache-line clobberthe driver's maintenance
frames counted at the PHY, absent in software?the same, or a stale lengthan ordering fault

Row three is the diagnostic that finds Section 14's bug and it is not obvious. The signature is the ring going backwards: descriptors the driver has already processed are owned by the MAC again, with old contents. Nothing else in this chapter produces that, and once seen it is unambiguous.

Complaint 2 — "frames have the wrong length."

CheckIf yesMeaning
own_before_fields ever asserted?the MAC's pipelining is brokenan RTL bug
barrier_bypassed set?the barrier is offa configuration fault
mode 0 with a non-compliant path?same-ID ordering is not honouredSection 20's run C
lengths always from the previous frame?an ordering fault, confirmedthe tell

Row four is the confirmation to look for. A random wrong length is corruption; a length that is consistently the previous frame's for that descriptor is a reordering, because the descriptor's memory still holds what was last written to it.

Complaint 3 — "the port stalls intermittently and recovers."

CheckIf yesMeaning
c_noncoherent_desc non-zero on a coherent system?the MAC is reading DRAM, the driver wrote a cacheSection 13
c_flushes rising?the driver rewrites prefetched entriesa driver bug
c_table_full rising?Section 9's table is undersizedan RTL parameter
stalls end after a fixed interval?a cache line evicted naturallyconfirms the first row

Complaint 4 — "throughput is below line rate and nothing is wrong."

CheckIf yesMeaning
c_mode_stalls near c_handoffs?mode 1's round trip on every handoffqualify mode 0 or 2
descriptor transactions comparable to data?no batchingSection 2's arithmetic
c_table_full rising?not enough handoffs in flightSection 9
all clean?none_of_the_abovenot the ring

And the two symptoms that are systematically misattributed:

SymptomInstinctThis chapter's cause
frames counted by the MAC, absent in softwarea driver bug in the receive loopa cache-line clobber reverting descriptors
occasional wrong-length framesa MAC parsing buga write-ordering violation in the memory system

Both send the investigation to a component that is behaving correctly, and both are resolved by a counter that costs a few flops.


22. Misconceptions

Misconception 1 — "the ownership bit is a lock."

The wrong model: the bit is a mutex; whoever sets it has exclusive access.

What it costs: a design that reads the bit, decides, and then writes — a read-modify-write that two agents can interleave, which is the race the structure was chosen to avoid, reintroduced by an agent that thought it was being careful.

The corrected model: the bit is not contended. Each agent only ever writes one value — the MAC writes OWN_DRIVER, the driver writes OWN_MACand each only writes it when it already owns the descriptor. There is no read-modify-write, no contention, and no atomic operation. The bit is a baton, not a lock. Section 4.

Misconception 2 — "writing the fields before the ownership bit is enough."

The wrong model: the MAC issues the length and status writes first and the ownership write last, so the driver sees them in that order.

What it costs: a frame processed with the previous frame's length, at whatever rate the memory system's reordering happens to produce — which may be one in a thousand or one in a million, and is a different rate on every system the design ships to.

The corrected model: issuing in order guarantees nothing. AXI orders transactions only within an ID; different banks have different queues; a retry reorders. The ownership write must not be issued until the field writes have responded — or the two must share an ID, or be one indivisible write. Sections 9 and 10.

Misconception 3 — "the driver's stores happen in program order."

The wrong model: the driver writes the buffer pointer and then the ownership bit, so the MAC sees the pointer first.

What it costs: the MAC writing a received frame into the previous buffer's address — memory the allocator has already handed to something else. A memory-corruption bug whose symptom appears in an unrelated subsystem and whose cause is a network driver.

The corrected model: a CPU's store buffer makes stores visible in any order it likes. The driver needs an explicit store barrier between the field stores and the ownership store, and no hardware can supply it, detect its absence, or work around it. Section 12.

Misconception 4 — "a testbench that passes the ordering assertion has verified the ordering."

The wrong model: the property says ownership implies a current length; it passes; the requirement is covered.

What it costs: full coverage reported on a requirement that was never tested, and the bug ships. This is the worst outcome in the chapter, because a gap is visible and a false claim is not.

The corrected model: a testbench memory that is an array cannot reorder, so the property is a tautology about the MAC's issue order. The requirement is about what an observer sees through a store buffer, two caches and an interconnect — so the testbench needs a memory model that delays and reorders outstanding writes, and Section 20's run F exists to demonstrate the false pass. Section 19.

Misconception 5 — "sharing a cache line between descriptors is a performance question."

The wrong model: four descriptors per line means some false sharing and slightly worse cache behaviour.

What it costs: on a non-coherent system, silent data loss. A driver cleaning its own descriptor writes back the whole line, reverting three descriptors the MAC completed since the line was read — and the frames in them vanish with no error anywhere.

The corrected model: a cache's maintenance granularity is a line, always. Two owners' data in one line means the coarser agent's maintenance overwrites the finer agent's writes. The fixes are coherency, or padding descriptors to a full line at four times the ring's memory. Section 14.

Misconception 6 — "a bigger ring fixes drops."

The wrong model: frames are dropped for lack of descriptors, so a deeper ring drops fewer.

What it costs: a 100 Gb/s port given a 4096-entry ring to survive a 1 ms scheduler delay — which needs 148 810 entries and 9.09 MiB padded. The ring grows, the drops continue, and the memory is spent.

The corrected model: depth buys time, and the time a realistic ring buys is small: 27.53 µs at 4096 entries on a 100 Gb/s port at minimum frame size. That covers an interrupt latency and not a scheduling one — so the fix is a driver that polls under load, not a deeper ring. Section 17.


23. Interview Questions

Q1 — "Why does a descriptor ring not need a lock?"

Because no location is ever written by both agents at once. Each descriptor has a single owner at any instant; the owner is the only agent that may write it, and the last write it performs is the one transferring ownership. Each agent writes only one value of the ownership bit, so there is no read-modify-write and no contention. And a lock is unavailable anyway — a lock is an atomic bus round trip, and at 100 Gb/s the frame interval is 6.7 ns, which is shorter than any memory round trip.

Q2 — "The MAC writes the frame length, then the status, then the ownership bit. Is that correct?"

No, and it is the chapter's central point. Issuing writes in order guarantees nothing: AXI orders only within an ID, a 16-octet descriptor may straddle two DRAM banks with different queue depths, and a retried write is reordered. The driver may observe ownership before the length and process the frame at the previous frame's size. The fixes are: wait for the field write's response before issuing the ownership write; use one AXI ID for both; or write the whole descriptor in one indivisible transaction. The first depends on nothing and costs a round trip, which is why it is pipelined across 32 descriptors.

Q3 — "Your driver is corrupting memory occasionally. The MAC writes frames into buffers that were freed. Where do you look?"

At the driver's store ordering, between the buffer pointer store and the ownership store. Without a store barrier the CPU may make ownership visible first; the MAC then fetches a descriptor it owns whose pointer is the previous buffer's — freed and reallocated. Hardware cannot detect this: the stale pointer is a legal address and both reads see the same stable stale value. The one thing hardware contributes is a register recording the buf_ptr the MAC actually fetched, which a driver author compares against what they believe they wrote.

Q4 — "Frames are counted as received by the MAC and never appear in software, and the ring appears to go backwards. Why?"

A cache-line clobber on a non-coherent system. At a 64-octet line and 16-octet descriptors, four descriptors share a line. The driver reads descriptor 4, the MAC completes 5, 6 and 7, and the driver's clean of the line writes back its stale copy of 5, 6 and 7 — reverting them to MAC-owned with old contents. The frames vanish with no error. Fixes: make descriptor traffic coherent, or pad descriptors to a full cache line, which costs four times the ring's memory.

Q5 — "How deep should a 100 Gb/s port's receive ring be?"

Depth is the frame rate times the driver's worst-case latency, and at minimum frame size that is 148.81 Mfps. A 4096-entry ring covers 27.53 µs — an interrupt latency, not a scheduling one. Covering 1 ms would need 148 810 entries: 2.27 MiB, or 9.09 MiB padded to cache lines. So the honest answer is that depth cannot cover a descheduled driver, and the design must poll under load rather than buy the time in DRAM.

Q6 — "An assertion says that when the descriptor is owned by the driver, its length field is the current frame's. It passes. Are you satisfied?"

No. The assertion reads the testbench's memory model, which is an in-order single-copy array — so it checks the order the MAC issued the writes, not the order a driver observes them. The requirement is about the observation, and the medium between them is a store buffer, two cache levels and a reordering interconnect. With a memory model that may deliver outstanding writes in either order, the same assertion fails immediately with the barrier disabled — which is the check that was wanted. A passing tautology is worse than no property, because it is recorded as coverage.


24. Understanding Check


25. What's Next

This chapter built the handoff. The next one uses it, and discovers that the handoff's timing is decided by something this chapter did not examine: when the frame's data becomes visible.

Section 7 gated the ownership write on the descriptor's field writes. Chapter 18.3 shows that is not sufficient — the frame's data writes must also be visible, and there may be dozens of them, scattered across pages.

This chapter orderedChapter 18.3 must also order
length and status against ownershipevery data write against ownership
one field writeup to 24 beats across several bursts
one descriptora scatter-gather chain of them

Row three is the part that grows. A 9000-octet jumbo frame into 2 KiB buffers is five descriptors, and the ownership of the first must not transfer until the data of the last is visible — or software reads a frame whose head is complete and whose tail is not.

Chapter 18.3 — The Receive DMA Path traces a frame from Chapter 18.1 §9's FIFO into host buffers: buffer exhaustion when this chapter's starved becomes a drop, scatter-gather across page boundaries, the write-completion ordering above, and the interrupt that follows.

And it closes a debt from Module 16. Chapter 16.1 §8 listed DMA arbitration and interrupt coalescing as jitter sources and left both unpriced, because neither was computable yet. Chapter 18.1 §12's transaction arithmetic and §10's interrupt arithmetic have made them computable, and Chapter 18.3 prices them — which matters, because those two terms land directly in Chapter 16.5's error budget and therefore in Chapter 17.2's guard band.

Continue learning

Standards & specifications

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

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

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

Where this fits

Part of the Ethernet curriculum.