Ethernet · Module 19
Statistics Counters
Twenty-seven counters, four of them 64 bits because a 32-bit octet counter wraps every 0.344 seconds, and a shadow bank that doubles the block because two counters must be read together.
Chapter 19.1 §9 sorted the MAC's blocks by timing difficulty, put the CRC engine at the top, and then added a warning about the block at the bottom: the statistics counters are easy per instance and hard in aggregate. This chapter is that aggregate, and the warning turns out to be right for a reason that chapter did not have.
| Chapter 19.1 §9's model | What this chapter finds | |
|---|---|---|
| counters | about twenty | twenty-seven |
| updates per second | 2 976 M | the same — the figure reproduces |
| updates per cycle | 15.24 | the same, and it is not the problem |
| the problem | two frames per cycle on one counter | true, and only for RUNTS |
| counter width | not considered | a 32-bit octet counter wraps in 0.344 s |
| reading them | not considered | the snapshot doubles the block |
Row four is the one this chapter has to resolve rather than repeat, because Chapter 19.4 §7 proved that two frames cannot both end in one beat — the minimum wire period is 81 octets and a beat is 64. A counter that increments at end-of-frame therefore takes +1 and never +2, and Chapter 19.1 §9's argument does not apply to it.
It applies to runts, and a statistics block is the one block in the MAC that may not assume they do not exist.
1. Scope, and the Block That Was Called Easy
Chapter 19.1 §9's table put the statistics increments at about six levels of logic and called them "easy, but see below." The "see below" was the aggregate rate. This chapter finds that the rate is the least of it.
| Difficulty | Where it comes from | Section |
|---|---|---|
| counter width | 12.5 G octets/s against 2³² | 4 |
| the addend | runts, not conformant traffic | 7 |
| atomic reads | 64 bits read through a 32-bit interface | 10 |
| cross-counter consistency | Chapter 19.6 §22's diagnosis needs two counters at once | 12 |
| the update rate | 15.24 per cycle across different counters | 17 |
Row five is the one that was flagged and it is the one that turns out to be fine. Fifteen updates per cycle is fifteen different counters incrementing in parallel — no arbitration, no contention, and no shared resource — which is exactly what a bank of independent registers does for free. Rows one to four are where the flops go.
And row four is the reason this chapter is not optional. Chapter 19.5 §14 established that a FIFO overflow truncates a frame mid-flight, Chapter 19.4 §10 established that such a frame is classified RES_BAD, and Chapter 19.6 §22's complaint 1 established that the only way to tell a buffer problem from a cable problem is to read c_bad and c_frames_truncated together. "Together" is a requirement on this block, and it is what doubles it.
What this chapter owns: the counter set and its widths; the addend and where it comes from; the wrap arithmetic; the snapshot mechanism and its scope; the clock-domain read path; and Module 19's final flop accounting against Chapter 19.1 §18's corrected estimate.
What it does not own: what the counters mean — Chapter 7.3 defines undersize, oversize, fragment and jabber — and the register interface's bus protocol, which is Chapter 18.1 §7's.
What it does not build: Module 20's verification environment. Chapter 20.1 generates the stimulus that reaches these counters' corners, Chapter 20.2 turns the track's assertions into a library, Chapter 20.3 builds the scoreboards, and Chapter 20.4 the coverage model.
2. Twenty Counters Is Twenty-Seven
Chapter 19.1 §9 said "about twenty counters." Writing the set down gives twenty-seven, and the seven extra are a histogram nobody counts because it looks like one thing.
The set, with the chapters that require each entry:
| # | Counter | Width | Required by |
|---|---|---|---|
| 1 | rx_octets | 64 | RMON, and Section 4 |
| 2 | tx_octets | 64 | the same |
| 3 | rx_frames | 64 | Section 4 — 2³² is 28.9 s |
| 4 | tx_frames | 64 | the same |
| 5 | rx_broadcast | 32 | Chapter 5.1's address types |
| 6 | rx_multicast | 32 | the same |
| 7 | tx_broadcast | 32 | the same |
| 8 | tx_multicast | 32 | the same |
| 9 | crc_errors | 32 | Chapter 19.4 §10's RES_BAD |
| 10 | alignment_errors | 32 | Chapter 7.3 |
| 11 | undersize | 32 | Chapter 7.3 — and Section 7 |
| 12 | oversize | 32 | Chapter 5.7 |
| 13 | fragments | 32 | Chapter 7.3 |
| 14 | jabbers | 32 | the same |
| 15 | pause_rx | 32 | Chapter 14.2 |
| 16 | pause_tx | 32 | the same |
| 17 | drop_events | 32 | Chapter 19.5 §14's whole-frame drops |
| 18 | truncations | 32 | Chapter 19.5 §14 — and Section 12 |
| 19 | mcrc_fragments | 32 | Chapter 17.3, via Chapter 19.4 §10 |
| 20 | reorder_overflows | 32 | Chapter 19.6 §9 |
| 21–27 | the size histogram | 32 each | RMON's seven buckets |
The histogram is where "about twenty" loses seven. RMON specifies frame counts in seven size buckets — 64, 65–127, 128–255, 256–511, 512–1023, 1024–1518 and 1519-and-above — and they are seven independent counters, not one counter with a field. Every frame updates exactly one of them, so the seven behave as one counter for rate purposes and as seven for area.
Which makes the storage arithmetic immediate.
| Count | Bits each | Flops | |
|---|---|---|---|
| wide counters | 4 | 64 | 256 |
| ordinary counters | 16 | 32 | 512 |
| histogram buckets | 7 | 32 | 224 |
| total | 27 | — | 992 |
Nine hundred and ninety-two flops of counter, before anything that reads them. Chapter 19.1 §18's corrected estimate left 1 006 flops for this chapter, so the counters alone consume 98.6% of it and the snapshot, the clock crossing, the telemetry and the monitor are all outside the budget. Section 19 settles the account.
And rows one to four are the reason four of the twenty-seven are twice the width of the rest, which is Section 4.
3. RTL 1 — The Counter Bank
// ---------------------------------------------------------------------
// statc_pkg -- the MAC's statistics counters and the two facts that
// shape them. Sections 2 through 13.
//
// Fact one: at 100 Gb/s the port moves 12.5 G octets per second, so a
// 32-bit octet counter wraps in 0.344 seconds -- faster than any polling
// interval. Four counters are therefore 64 bits wide. Section 4.
//
// Fact two: two counters must be readable as one value, because
// Chapter 19.6 Section 22's diagnosis is a subtraction across them.
// That needs a snapshot, and the snapshot is a second bank. Section 12.
// ---------------------------------------------------------------------
package statc_pkg;
localparam int DATA_B = 64; // octets per beat
localparam int NUM_CTRS = 27;
localparam int WIDE_CTRS = 4; // rx/tx octets, rx/tx frames
localparam int NARROW_W = 32;
localparam int WIDE_W = 64;
// The seven RMON size buckets. Every frame lands in exactly one, so
// they cost seven counters and behave as one for rate purposes.
localparam int NUM_BUCKETS = 7;
typedef enum logic [4:0] {
C_RX_OCTETS = 5'd0, C_TX_OCTETS = 5'd1,
C_RX_FRAMES = 5'd2, C_TX_FRAMES = 5'd3, // the four wide ones
C_RX_BCAST = 5'd4, C_RX_MCAST = 5'd5,
C_TX_BCAST = 5'd6, C_TX_MCAST = 5'd7,
C_CRC_ERR = 5'd8, C_ALIGN_ERR = 5'd9,
C_UNDERSIZE = 5'd10, C_OVERSIZE = 5'd11,
C_FRAGMENT = 5'd12, C_JABBER = 5'd13,
C_PAUSE_RX = 5'd14, C_PAUSE_TX = 5'd15,
C_DROP = 5'd16, C_TRUNC = 5'd17,
C_MCRC = 5'd18, C_REORDER = 5'd19,
C_BUCKET0 = 5'd20 // .. 5'd26
} ctr_id_e;
// The addend. Section 7: a conformant stream gives at most one frame
// END per beat -- Chapter 19.4 Section 7's 81-octet floor against a
// 64-octet beat -- but a runt stream gives up to three, and counting
// runts is this block's job. Two bits, not one.
typedef logic [1:0] frame_addend_t;
// The octet addend is the beat's valid octet count, 1 to 64.
typedef logic [6:0] octet_addend_t;
typedef struct packed {
frame_addend_t frames_ended; // 0..3
octet_addend_t octets_valid; // 0..64
logic [2:0] bucket; // which size bucket, if any
logic bucket_valid;
logic is_bcast;
logic is_mcast;
logic is_crc_err;
logic is_undersize;
logic is_oversize;
} stat_event_t;
endpackage// ---------------------------------------------------------------------
// counter_bank -- twenty-seven counters, four of them wide, all of them
// updated in the same cycle. Sections 2, 3 and 17.
//
// There is no arbitration here and that is the point. Chapter 19.1
// Section 9 worried about 15.24 counter updates per cycle; they are
// updates to DIFFERENT counters, so they happen in parallel and cost
// nothing beyond an adder each. What costs something is the WIDTH of
// four of them and the SNAPSHOT that Section 11 adds.
// ---------------------------------------------------------------------
module counter_bank
import statc_pkg::*;
(
input logic clk,
input logic rst_n,
input logic event_valid,
input stat_event_t ev,
input logic snapshot_req,
output logic [WIDE_W-1:0] ctr_out [NUM_CTRS],
// Observability. Sections 15 and 16.
output logic [31:0] c_events,
output logic addend_over_one,
output logic addend_over_two
);
logic [WIDE_W-1:0] ctr [NUM_CTRS];
// Width per counter, at elaboration. A narrow counter's upper bits are
// tied off rather than built; the array is uniform for readability and
// a production version instantiates two different widths.
function automatic int width_of(input int i);
width_of = (i < WIDE_CTRS) ? WIDE_W : NARROW_W;
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < NUM_CTRS; i++) ctr[i] <= '0;
c_events <= '0;
addend_over_one <= 1'b0;
addend_over_two <= 1'b0;
end else if (event_valid) begin
c_events <= c_events + 1;
// The octet counters take the beat's valid count, 1 to 64. This is
// an ADD and never an increment, and it was an add before any
// argument about dual-frame beats -- an octet counter has never
// been incrementable at a datapath wider than one octet.
ctr[C_RX_OCTETS] <= ctr[C_RX_OCTETS] + WIDE_W'(ev.octets_valid);
// The frame counters take 0, 1, 2 or 3. Section 7 derives the
// upper bound and it comes from runts rather than from conformant
// traffic.
ctr[C_RX_FRAMES] <= ctr[C_RX_FRAMES] + WIDE_W'(ev.frames_ended);
if (ev.is_bcast)
ctr[C_RX_BCAST] <= ctr[C_RX_BCAST] + WIDE_W'(ev.frames_ended);
if (ev.is_mcast)
ctr[C_RX_MCAST] <= ctr[C_RX_MCAST] + WIDE_W'(ev.frames_ended);
if (ev.is_crc_err)
ctr[C_CRC_ERR] <= ctr[C_CRC_ERR] + WIDE_W'(ev.frames_ended);
if (ev.is_undersize)
ctr[C_UNDERSIZE] <= ctr[C_UNDERSIZE] + WIDE_W'(ev.frames_ended);
if (ev.is_oversize)
ctr[C_OVERSIZE] <= ctr[C_OVERSIZE] + WIDE_W'(ev.frames_ended);
// Exactly one bucket per frame, so the bucket counters take 1 and
// not frames_ended -- two runts in a beat may fall in DIFFERENT
// buckets, which is why the event carries one bucket and a real
// design carries one per ended frame. Section 7's simplification.
if (ev.bucket_valid)
ctr[C_BUCKET0 + ev.bucket] <= ctr[C_BUCKET0 + ev.bucket] + 1;
if (ev.frames_ended > 2'd1) addend_over_one <= 1'b1;
if (ev.frames_ended > 2'd2) addend_over_two <= 1'b1;
end
end
always_comb begin
for (int i = 0; i < NUM_CTRS; i++) begin
ctr_out[i] = ctr[i];
// Narrow counters wrap at 2^NARROW_W. Wrapping is the RMON
// behaviour and Section 14 is why it is not saturation.
if (width_of(i) == NARROW_W) ctr_out[i][WIDE_W-1:NARROW_W] = '0;
end
end
endmoduleClassification: a register file with an adder per entry and no shared resource in it.
What it teaches: that fifteen updates per cycle costs fifteen adders and nothing else. Chapter 19.1 §9's aggregate figure invites a search for contention and there is none: the counters are different registers, the addends are different signals, and every update is independent. The block is wide and shallow, which is the opposite of the CRC engine that chapter put at the top of the same table.
And it teaches that the octet counters were never incrementable. A datapath wider than one octet adds the beat's octet count, so += 1 was wrong at 8 bits and is wrong at 512. The dual-frame argument is about the FRAME counters, and Section 7 shows that on conformant traffic it does not apply to them either.
Deliberately simplified: the array is uniformly 64 bits wide with the narrow counters' upper bits masked on output, which synthesises to 27 × 64 = 1 728 flops rather than the 992 the widths require — a production version instantiates two banks. ev.bucket carries one bucket per beat, so two runts ending in one beat that fall in different size buckets are miscounted; a real design carries one bucket per ended frame, which is Section 7's cost. And addend_over_one and addend_over_two are sticky observations rather than errors, because both are legal on a non-conformant link.
Production implication: addend_over_one is the bit that says this port has seen runts short enough to pack two frame endings into one beat, which is a statement about the partner rather than about this design — and it is the only place in Module 19 where a counter's addend is itself diagnostic. A port that has never asserted it has never received a frame shorter than 48 octets, which is the normal state of a conformant link and is worth knowing before anybody debugs the counting logic.
4. Why a 32-Bit Octet Counter Is Useless at 100 Gb/s
A counter is useful if it can be read before it wraps twice. This section is the arithmetic, and one line of it decides four of the twenty-seven widths.
At 100 Gb/s the port moves 12.5 G octets per second.
| Width | Octet counter wraps in | Usable? |
|---|---|---|
| 32 bits | 0.344 s | NO |
| 48 bits | 22 518 s — 6.26 hours | marginal |
| 64 bits | 1.476 × 10⁹ s — 46.8 years | yes |
Row one is the whole argument. A management system polling once per second misses two wraps between reads on a saturated port, and there is no way to recover the lost count: a counter that has wrapped an unknown number of times carries no information about the interval.
And the frame counters are only slightly better.
| Width | Frame counter wraps in | At 148.81 M frames/s |
|---|---|---|
| 32 bits | 28.86 s | inside a one-minute poll |
| 64 bits | 1.24 × 10¹¹ s | 3 929 years |
Row one is the one that catches designs out, because 28.86 seconds looks survivable. It is not: RMON's conventional polling interval is 30 seconds and SNMP's default is 60, so a 32-bit frame counter on a saturated 100 Gb/s port wraps between one and two times per poll — and a reader cannot tell one wrap from two.
Which is why RFC 2863 defines high-capacity counters and why the rule it gives is worth stating as arithmetic rather than as a convention.
A counter must not wrap in less than about an hour at the interface's maximum rate.
| Rate | 32-bit octet counter | Passes the rule? |
|---|---|---|
| 10 Mb/s | 3 436 s — 57 min | just |
| 100 Mb/s | 344 s | no |
| 1 Gb/s | 34.4 s | no |
| 100 Gb/s | 0.344 s | no |
Row one is where the 32-bit counter came from and it is why the convention survived. At 10 Mb/s a 32-bit octet counter wraps in just under an hour, which is exactly the threshold — so the width was correct when it was chosen and has been wrong at every rate since. That is a useful shape to recognise: a constant in a standard is often a rate-dependent quantity frozen at the rate that was current.
The four wide counters are therefore not a design preference. They are rx_octets, tx_octets, rx_frames and tx_frames — the four whose increments scale with the line rate — and every other counter in Section 2's table counts an event whose rate is bounded by something other than the line.
| Counter class | Rate bound | Width |
|---|---|---|
| octets | the line rate | 64 |
| frames | the line rate divided by 84 | 64 |
| errors | whatever is broken | 32 |
| address classes | a fraction of the frame rate | 32 |
| size buckets | a fraction of the frame rate | 32 |
Rows three to five are 32-bit because a counter that reaches 2³² in 28.9 seconds has a much larger problem than its width. A port taking 148.81 M CRC errors per second is not a port whose counter precision matters, which is the argument for not widening the other twenty-three and is worth making explicitly rather than by omission.
5. RTL 2 — The Wide Counter
// ---------------------------------------------------------------------
// wide_counter -- a 64-bit counter with a small addend, built so the
// carry does not become the block's critical path. Sections 4 and 5.
//
// The addend is at most 7 bits -- 1 to 64 octets, or 0 to 3 frames --
// but the carry out of those 7 bits can propagate through all 64. A
// ripple structure is 64 levels and does not close at 195.3125 MHz;
// splitting the counter into a hot low word and a cold high word makes
// the per-cycle path 32 bits and the rest a once-per-wrap event.
//
// This is the same idea as Chapter 19.4 Section 6's split between
// per-beat work and per-frame work, applied to a carry chain: the low
// word updates every cycle and the high word updates every 0.344
// seconds.
// ---------------------------------------------------------------------
module wide_counter
import statc_pkg::*;
#(
parameter int LOW_W = 32,
parameter int HIGH_W = 32
) (
input logic clk,
input logic rst_n,
input logic add_en,
input logic [6:0] addend,
output logic [LOW_W+HIGH_W-1:0] value,
// Observability. Sections 15 and 16.
output logic [31:0] c_low_wraps,
output logic high_word_stale
);
logic [LOW_W-1:0] low_q;
logic [HIGH_W-1:0] high_q;
logic [LOW_W:0] low_sum;
logic carry;
assign low_sum = {1'b0, low_q} + (LOW_W+1)'(addend);
assign carry = low_sum[LOW_W];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
low_q <= '0; high_q <= '0;
c_low_wraps <= '0; high_word_stale <= 1'b0;
end else if (add_en) begin
low_q <= low_sum[LOW_W-1:0];
// The high word is a plain increment and it runs at most once per
// 2^LOW_W octets -- 0.344 seconds at 100 Gb/s. Its timing path is
// therefore irrelevant and it may be registered as deeply as the
// floorplan wants.
if (carry) begin
high_q <= high_q + 1;
c_low_wraps <= c_low_wraps + 1;
end
// A carry that arrives while the previous one is still being
// applied would lose a wrap. It cannot happen at any width where
// the low word takes more than one cycle to traverse -- 2^32
// octets at 64 per beat is 67 million beats -- but the flag says
// so rather than leaving it to a comment.
if (carry && high_word_stale) high_word_stale <= 1'b1;
end
end
assign value = {high_q, low_q};
endmoduleClassification: a datapath primitive whose design is entirely about where the carry is allowed to be slow.
What it teaches: that a 64-bit counter's carry chain is a per-cycle path only if you build it as one. The addend is seven bits; the low word absorbs it in a 32-bit add, and the high word sees one increment per 2³² octets — 67.1 million beats, or 0.344 seconds at 100 Gb/s. So the hot path is 32 bits wide and the cold path is 32 bits deep, and only one of them has to close timing at 195.3125 MHz.
And it teaches that this is Chapter 19.4 §6's sort applied to arithmetic. That chapter split the CRC engine's work into per-beat and per-frame and found three of four jobs were per-frame. Here the split is per-beat and per-wrap, and the ratio is far larger: one high-word update for every 67 million low-word updates. Recognising that a carry chain has the same structure as a pipeline is what keeps a 64-bit counter out of a timing report.
Deliberately simplified: the split is fixed at 32 and 32 and the parameters do not generalise — a 48-bit counter wants 32 and 16, and the module would need the addend width checked against LOW_W. high_word_stale can never assert as written, because the condition it tests requires two carries in consecutive cycles and the low word cannot wrap in fewer than 67 million beats; it is there as a documented impossibility rather than as a live check. And value is a combinational concatenation of two registers, so a reader sampling it mid-wrap sees a low word that has wrapped and a high word that has not — which is Section 10's problem and is not solved here.
Production implication: c_low_wraps is the counter that makes a 64-bit value auditable. A management system reading the full 64 bits does not need it; a bring-up engineer reading the low word alone through a 32-bit register interface needs to know how many times it has wrapped, and computing that from two polls requires assuming no poll was missed. The explicit wrap count removes the assumption, and at 0.344 seconds per wrap it is itself a 32-bit counter that lasts 47 years.
6. Adder, Not Increment — and Where the Second Operand Comes From
Chapter 19.1 §9 stated the requirement and attributed it to the dual-frame beat. The requirement is right and the attribution is not, and separating them changes what the design has to handle.
The claim, as published:
So every counter needs an adder, not an increment — and the adder's second operand is "how many frames completed this cycle", which is zero, one or two.
Two of the three numbers in that sentence need checking against Chapter 19.4.
Chapter 19.4 §7 derived that a beat can hold the END of at most one frame, and the derivation is arithmetic rather than statistical:
| Octets | |
|---|---|
| a minimum frame | 64 — Chapter 5.1 |
| preamble and SFD | 8 |
| the minimum legal gap | 9 — Chapter 5.9 |
| absolute minimum wire period | 81 |
| a beat | 64 |
Eighty-one exceeds sixty-four, so two conformant frames cannot both end in one beat. A counter that increments at end-of-frame therefore takes +1 and never +2, and Chapter 19.4 §8 built two_ends_in_a_beat as an error flag on exactly that basis.
So on conformant traffic the dual-frame beat does not produce a +2 on any frame counter. What it produces is one end and one start, and a start updates nothing — the frame has not finished, its length is unknown, its check value has not been computed, and its size bucket is undecided.
| Event in a dual-frame beat | Which counters move? |
|---|---|
the end of frame n | all of frame n's — one increment each |
the start of frame n+1 | none |
| the beat's octets | the octet counters, by the beat's valid count |
Which leaves the requirement standing and the reason changed. The adder is needed because:
| Reason | Addend range | Applies to |
|---|---|---|
| octet counting at a 512-bit datapath | 1 to 64 | the two octet counters |
| runts — Section 7 | 0 to 3 | every frame counter |
| the dual-frame beat | 0 to 1 | nothing extra |
Row one was never in doubt and was never about frames. An octet counter at any datapath wider than one octet adds the beat's valid count, which is a seven-bit addend, and that was true at 8 bits and at 64 and is true at 512. Chapter 19.1 §9's sentence is right about the octet counters for a reason it does not give.
Row two is the reason the frame counters need it, and it is the subject of Section 7.
7. The One-End-Per-Beat Theorem, and Why This Block May Not Use It
Chapter 19.4 §7's theorem has a premise, and the statistics block is the one block in the MAC whose job includes counting the frames that violate it.
The premise is Chapter 5.1's 64-octet minimum frame. A frame shorter than that is a runt — Chapter 7.3 §4 — and a receive path sees runts whenever a partner is broken, a cable is marginal, or a switch upstream is corrupting frames. Counting them is undersize and fragments, entries 11 and 13 of Section 2's table.
So redo the arithmetic without the premise.
wire period = L + 8 (preamble and SFD) + 9 (minimum gap)
= L + 17
ends in one 64-octet beat ≤ floor(64 / (L + 17)) + 1Frame length L | Wire period | Ends per beat |
|---|---|---|
| 48 and above | 65 and above | 1 |
| 16 to 47 | 33 to 64 | 2 |
| 5 to 15 | 22 to 32 | 3 |
Row one contains every conformant frame and is Chapter 19.4 §7's theorem, with the threshold now visible: the theorem holds from 48 octets up, not from 64. The sixteen octets between 48 and 64 are runts that still cannot pair.
Row two is the case that makes the adder two bits wide. A 47-octet runt has a wire period of exactly 64, so a beat can contain two of its endings — and at that length the port is receiving runts back to back, which is precisely the condition an operator needs the counter to measure.
Row three is the bound the design must be built to. Below 16 octets a beat can hold three endings, and a two-bit addend covers 0 to 3 exactly. Shorter units than five octets are not frames by any receive framing that requires a start-of-frame delimiter and a length, so three is the design bound rather than an arbitrary headroom.
Which gives the asymmetry this section exists for.
| Block | May assume one end per beat? | Why |
|---|---|---|
| Chapter 19.4's CRC engine | YES | a runt's check value is discarded anyway |
| Chapter 19.2's parser | effectively | a runt has no parseable header |
| Chapter 19.5's FIFOs | irrelevant | they move octets, not frames |
| this block | NO | counting runts is the job |
Row one is worth stating carefully because it is not sloppiness on that chapter's part. Chapter 19.4 §8's two_ends_in_a_beat treats the condition as a framing failure and stops trusting its own results — which is correct for a CRC engine, because a runt's residue is meaningless and the frame is discarded. The statistics block cannot discard it: the discard is the event being counted.
And the cost of getting this wrong is a counter that under-reports exactly when it matters.
| A 1-bit addend | A 2-bit addend | |
|---|---|---|
| conformant traffic | correct | correct |
| 47-octet runt flood | counts half | correct |
| 12-octet fragment flood | counts a third | correct |
| cost | — | one bit on 25 adders |
Row three is the shape of the failure: the counter is most wrong when the link is most broken, and an operator reading a third of the true fragment rate concludes the problem is smaller than it is. The fix is one bit of addend width on twenty-five adders, which is twenty-five gates, and it is invisible in every simulation that only sends legal frames.
8. RTL 3 — The Frame Event Counter
// ---------------------------------------------------------------------
// frame_event_counter -- turn a beat into a set of counter addends.
// Sections 6, 7 and 8.
//
// This is the block that decides what the counter bank adds. Its only
// difficulty is that a beat may contain up to THREE frame endings when
// the link is delivering runts -- Section 7 -- and each ending carries
// its own classification, so the block produces a VECTOR of per-frame
// attributes rather than one set of flags.
//
// A design that produces one set of flags and a count is correct only
// when the frames in a beat are alike, which is the case a simulation
// produces and the field does not.
// ---------------------------------------------------------------------
module frame_event_counter
import statc_pkg::*;
#(
parameter int MAX_ENDS = 3 // Section 7's bound
) (
input logic clk,
input logic rst_n,
input logic beat_valid,
input logic [6:0] octets_valid,
input logic [MAX_ENDS-1:0] end_valid,
input logic [15:0] end_length [MAX_ENDS],
input logic [MAX_ENDS-1:0] end_crc_err,
input logic [MAX_ENDS-1:0] end_bcast,
input logic [MAX_ENDS-1:0] end_mcast,
output logic event_valid,
output stat_event_t ev,
output logic [2:0] bucket_hits [NUM_BUCKETS],
// Observability. Sections 15 and 16.
output logic [31:0] c_beats,
output logic [31:0] c_multi_end_beats,
output logic [2:0] peak_ends,
output logic ends_exceeded
);
// Chapter 7.3's classes, as thresholds. A frame below 64 octets is
// undersize; below 64 WITH a bad check value it is a fragment; above
// the MTU it is oversize, and oversize with a bad check value is a
// jabber. The four classes are two predicates crossed, which is why
// they are four counters and not four independent conditions.
function automatic logic [2:0] bucket_of(input logic [15:0] len);
if (len <= 16'd64) bucket_of = 3'd0;
else if (len <= 16'd127) bucket_of = 3'd1;
else if (len <= 16'd255) bucket_of = 3'd2;
else if (len <= 16'd511) bucket_of = 3'd3;
else if (len <= 16'd1023) bucket_of = 3'd4;
else if (len <= 16'd1518) bucket_of = 3'd5;
else bucket_of = 3'd6;
endfunction
int unsigned ends_this_beat;
always_comb begin
ends_this_beat = 0;
for (int e = 0; e < MAX_ENDS; e++) if (end_valid[e]) ends_this_beat++;
ev.frames_ended = frame_addend_t'(ends_this_beat);
ev.octets_valid = octets_valid;
// Each ending gets its own bucket, so the bank receives a vector of
// bucket increments rather than one bucket and a count. Section 7:
// two runts in a beat may land in DIFFERENT buckets, and a design
// carrying one bucket miscounts exactly when the link is broken.
for (int b = 0; b < NUM_BUCKETS; b++) bucket_hits[b] = '0;
for (int e = 0; e < MAX_ENDS; e++)
if (end_valid[e])
bucket_hits[bucket_of(end_length[e])] =
bucket_hits[bucket_of(end_length[e])] + 3'd1;
// The per-frame attributes are summed across the endings, because
// the bank adds rather than increments. Two broadcast runts in one
// beat add two to the broadcast counter.
ev.is_bcast = |(end_valid & end_bcast);
ev.is_mcast = |(end_valid & end_mcast);
ev.is_crc_err = |(end_valid & end_crc_err);
ev.is_undersize = 1'b0;
ev.is_oversize = 1'b0;
for (int e = 0; e < MAX_ENDS; e++) begin
if (end_valid[e] && end_length[e] < 16'd64) ev.is_undersize = 1'b1;
if (end_valid[e] && end_length[e] > 16'd1518) ev.is_oversize = 1'b1;
end
ev.bucket = bucket_of(end_length[0]);
ev.bucket_valid = end_valid[0];
end
assign event_valid = beat_valid;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_beats <= '0; c_multi_end_beats <= '0;
peak_ends <= '0; ends_exceeded <= 1'b0;
end else if (beat_valid) begin
c_beats <= c_beats + 1;
if (ends_this_beat > 1) c_multi_end_beats <= c_multi_end_beats + 1;
if (3'(ends_this_beat) > peak_ends) peak_ends <= 3'(ends_this_beat);
// Section 7's bound is arithmetic: a unit shorter than five
// octets is not a frame to any framing that needs a delimiter and
// a length. If four endings arrive, the framing is broken and the
// counts from here on are not trustworthy.
if (ends_this_beat > unsigned'(MAX_ENDS)) ends_exceeded <= 1'b1;
end
end
endmoduleClassification: a classifier that produces a vector rather than a set of flags, and the vector is the whole point.
What it teaches: that ev.is_bcast as a single bit is a simplification that fails on exactly the traffic the counters exist for. Two broadcast runts ending in one beat should add two to rx_broadcast; a design carrying one flag and a count of three adds three if the flag is set and zero if it is not. The correct structure is a per-attribute sum across the endings, which is what bucket_hits does and what the flags in this listing do not — Section 3's deliberate simplification, stated here as the bug it is.
And it teaches that Chapter 7.3's four error classes are two predicates crossed. Undersize and oversize are length; fragment and jabber are the same lengths with a bad check value. So they are not four independent tests but a two-by-two, and a design that implements four independent conditions gets the crossing wrong in one corner — usually by counting a short frame with a bad FCS as both undersize and a fragment, which double-counts it.
Deliberately simplified: the per-attribute flags are ORs rather than sums, so multi-end beats under-report every attribute counter except the buckets — the listing shows the correct structure for buckets and the incorrect one for flags side by side, deliberately, because that is the shape of the bug in the wild. bucket_of is a priority chain of seven comparators, about four levels, which is fine once per beat and would not be once per ending if it were not shared. And end_length arrives as an input, so the block trusts Chapter 19.2 to have measured it.
Production implication: peak_ends is the field number that tells an operator what kind of link this is. One means every frame is 48 octets or longer — a conformant link. Two means runts between 16 and 47 octets are arriving back to back. Three means fragments under 16 octets, which is a physical-layer failure rather than a framing one. The counter is three bits and it classifies the fault before any of the twenty-seven counters is read, which is the cheapest diagnostic in Module 19.
9. RTL 4 — The Size Histogram
// ---------------------------------------------------------------------
// size_histogram -- RMON's seven buckets, which are seven counters and
// one decoder. Sections 2 and 9.
//
// The buckets look like one counter with a field and they are not: every
// frame updates exactly one of them, so they share a decoder and nothing
// else. Seven 32-bit counters is 224 flops, which is 22.6% of Section
// 2's total storage for a structure nobody lists separately.
//
// The bucket boundaries are not powers of two. 64, 127, 255, 511, 1023
// and 1518 -- five of them are 2^n - 1 and the sixth is Chapter 5.1's
// maximum basic frame, which is why the decoder is a comparator chain
// and not a priority encoder on the length's high bits.
// ---------------------------------------------------------------------
module size_histogram
import statc_pkg::*;
(
input logic clk,
input logic rst_n,
input logic event_valid,
input logic [2:0] bucket_hits [NUM_BUCKETS],
output logic [31:0] bucket [NUM_BUCKETS],
// Observability. Sections 15 and 16.
output logic [31:0] c_frames_binned,
output logic [2:0] modal_bucket,
output logic bucket_overflow
);
logic [31:0] max_val;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int b = 0; b < NUM_BUCKETS; b++) bucket[b] <= '0;
c_frames_binned <= '0;
bucket_overflow <= 1'b0;
end else if (event_valid) begin
for (int b = 0; b < NUM_BUCKETS; b++) begin
if (bucket_hits[b] != '0) begin
// Section 7: up to three endings in one beat, and they may
// land in the same bucket, so this is an add of 0 to 3 and
// not a conditional increment.
bucket[b] <= bucket[b] + 32'(bucket_hits[b]);
c_frames_binned <= c_frames_binned + 32'(bucket_hits[b]);
if (bucket[b] + 32'(bucket_hits[b]) < bucket[b])
bucket_overflow <= 1'b1;
end
end
end
end
// The modal bucket -- which size dominates this link. Computed on read
// rather than per beat; it is here because it is the one number an
// operator wants and the seven counters do not give directly.
always_comb begin
modal_bucket = 3'd0;
max_val = bucket[0];
for (int b = 1; b < NUM_BUCKETS; b++)
if (bucket[b] > max_val) begin
max_val = bucket[b];
modal_bucket = 3'(b);
end
end
endmoduleClassification: seven independent counters sharing one decoder, and the only block in this chapter whose output is a distribution.
What it teaches: that the histogram is 22.6% of the counter storage and is invisible in a counter list. "About twenty counters" becomes twenty-seven because seven of them are written as one line of a specification. 224 flops for a structure that appears in Section 2's table as a single row — and it is the row most likely to be dropped from a first implementation and added back after an operator asks for a size distribution.
And it teaches why the boundaries resist a cheap decoder. Five of the six thresholds are 2ⁿ − 1 and the sixth is 1 518 — Chapter 5.1's maximum basic frame, which is not a power of anything. So the decoder is a comparator chain rather than a priority encode on the length's high bits, and a design that optimises it into the latter puts frames of 1 519 to 2 047 octets in the wrong bucket — which is exactly the jumbo range where an operator is most likely to be looking.
Deliberately simplified: modal_bucket is a combinational maximum over seven 32-bit values in the same module that writes them, which is a 32-bit-wide seven-way compare tree and must be computed on read in a real design. bucket_overflow tests for wrap by comparing the sum against the old value, which works and costs a second 32-bit comparator per bucket. And the buckets are RMON's receive-side definitions applied to both directions, where a complete implementation has fourteen.
Production implication: modal_bucket reads back the link's traffic profile in three bits and it belongs next to Chapter 19.4 §15's dual_beat_pct_x100 and Chapter 19.6 §15's mean_burst_beats_x10. All three measure the network rather than the design, and together they explain most of what a port's performance counters are doing: a modal bucket of 0 predicts 52.38% dual-frame beats and sub-burst-sized memory transactions, and a modal bucket of 6 predicts 2.73% and full bursts. Three numbers from three chapters that must agree, and a disagreement between them is a stronger signal than any of them alone.
10. Reading a 64-Bit Counter From a Slower Domain
A 64-bit counter read through a 32-bit register interface is two reads, and the value between them moves.
The arithmetic is Section 4's, used differently. The low word wraps every 2³² octets — 0.344 seconds at 100 Gb/s — and a host performing two register reads separated by Δt straddles a wrap with probability Δt / 0.344 s.
| Read separation | P(straddle) per poll | At 1 poll/s | At 10 polls/s |
|---|---|---|---|
| 1 µs | 2.91 × 10⁻⁶ | once per 3.98 days | once per 9.5 hours |
| 10 µs | 2.91 × 10⁻⁵ | once per 9.5 hours | once per 57 minutes |
And the error when it happens is not small. Reading the high word first and the low word second, across a wrap, reports a value 2³² octets too low — 4.295 GB. On a port that has moved 100 GB, that is a 4.3% error appearing once every few days, which is exactly the shape that gets attributed to the measurement tooling rather than to the counter.
Reading the low word first is worse and is the more common convention.
| Order | If a wrap falls between the reads | Error |
|---|---|---|
| high, then low | high is pre-wrap, low is post-wrap | −2³² octets |
| low, then high | low is pre-wrap, high is post-wrap | +2³² octets |
Neither is detectable from the values, because both results are plausible counter readings. The reader cannot tell a 4.3 GB jump from real traffic.
So the counter must be captured, not read. The standard mechanism has three parts and the third is the one designs omit:
| Part | What it does | Omitted? |
|---|---|---|
| a capture register | holds a consistent 64-bit value | rarely |
| a trigger | one write, or a read of a designated word | rarely |
| a scope | which counters are captured together | usually |
Row three is Section 12's subject and it is where the block's flop count doubles. Capturing one counter atomically costs one 64-bit shadow register. Capturing all twenty-seven atomically costs a shadow of every one of them — 992 flops — and Section 12 is the argument that the second is a requirement rather than a luxury.
11. RTL 5 — The Snapshot Bank
// ---------------------------------------------------------------------
// snapshot_bank -- capture all twenty-seven counters in one cycle so a
// reader sees a consistent set. Sections 10, 11 and 12.
//
// One trigger, one cycle, every counter. The alternative -- capturing
// one counter at a time on demand -- costs one shadow register instead
// of twenty-seven and makes every cross-counter ratio meaningless,
// which is Chapter 19.6 Section 22's diagnosis.
//
// The counters KEEP COUNTING during the read. The snapshot is a copy,
// not a freeze, so no traffic is lost while a management system walks
// the bank at whatever rate its bus allows.
// ---------------------------------------------------------------------
module snapshot_bank
import statc_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [WIDE_W-1:0] ctr_in [NUM_CTRS],
input logic capture,
input logic [4:0] rd_index,
output logic [WIDE_W-1:0] rd_data,
output logic snapshot_valid,
// Observability. Sections 15 and 16.
output logic [31:0] c_captures,
output logic [31:0] captured_at,
output logic [31:0] reads_since_capture,
output logic read_without_capture
);
logic [WIDE_W-1:0] shadow [NUM_CTRS];
logic [31:0] cycle_now;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < NUM_CTRS; i++) shadow[i] <= '0;
cycle_now <= '0; captured_at <= '0;
c_captures <= '0; reads_since_capture <= '0;
snapshot_valid <= 1'b0; read_without_capture <= 1'b0;
end else begin
cycle_now <= cycle_now + 1;
if (capture) begin
// ALL of them, in one cycle. This is 992 flops of copy and it
// is the block's second largest item after the counters
// themselves -- Section 19.
for (int i = 0; i < NUM_CTRS; i++) shadow[i] <= ctr_in[i];
captured_at <= cycle_now;
c_captures <= c_captures + 1;
reads_since_capture <= '0;
snapshot_valid <= 1'b1;
end
if (rd_index < 5'(NUM_CTRS)) begin
reads_since_capture <= reads_since_capture + 1;
// A reader that walks the bank without triggering a capture is
// reading whatever the last capture left, which may be hours
// old. Silent staleness is worse than a wrong value because it
// looks like a link that has stopped.
if (!snapshot_valid) read_without_capture <= 1'b1;
end
end
end
assign rd_data = shadow[rd_index];
endmoduleClassification: a copy, and the largest single item in the chapter after the counters it copies.
What it teaches: that a snapshot is a copy and not a freeze, and the distinction is the whole design. Freezing the counters while a management system walks twenty-seven registers loses every frame that arrives during the walk — at 148.81 M frames/s and a millisecond of bus traffic, 148 810 frames. Copying costs 992 flops and loses nothing, which is why every real counter block has a shadow bank and why its cost is never in the initial estimate.
And it teaches that captured_at is what makes the snapshot a measurement rather than a reading. Two snapshots and their cycle stamps give a rate; two readings without stamps give two numbers. Chapter 18.6's arrival-rate estimator made the same point about interrupts — a count is not a rate until it has an interval — and this is the third block in Modules 18 and 19 to carry a timestamp for that reason alone.
Deliberately simplified: the shadow array is uniformly 64 bits, so it is 1 728 flops as written against the 992 the widths require — the same simplification as Section 3 and the same fix. capture has no clock-domain crossing, so a host-domain write reaching this MAC-domain register needs Chapter 19.5 §7's synchroniser and one pulse-stretch, which Section 13 adds. And cycle_now wraps at 2³² cycles — 22.0 seconds at 195.3125 MHz — so two snapshots more than 22 seconds apart produce a negative interval unless the reader handles it.
Production implication: read_without_capture is the bit that catches the most common integration error in this block. A driver written against a counter block with no snapshot reads the registers directly; ported to one with a snapshot, it reads the shadow and never triggers a capture — so it returns the reset values forever and the port appears to be carrying no traffic. The symptom is indistinguishable from a dead link, and the bit that separates them costs one flop.
12. Why the Snapshot Must Be Across All Counters
Capturing one counter atomically is a 64-bit register. Capturing twenty-seven atomically is 992 flops, and this section is the argument that the second is required — which is an argument made entirely out of other chapters.
Chapter 19.6 §22's complaint 1 is the most expensive field failure in Module 19: frames dropped by a buffer overflow, reported as CRC errors. Its diagnosis is a subtraction:
| Reading | Conclusion |
|---|---|
| 400 CRC errors, 380 truncations | a buffer problem |
| 400 CRC errors, 0 truncations | a cable problem |
Both counters are 32 bits and both are in this block. The diagnosis is their difference, and a difference of values sampled at different times is not a difference of anything.
Put a number on it. Suppose the two counters are read a microsecond apart on a port taking errors at 1 000 per second — a badly broken link.
| Value | |
|---|---|
| error rate | 1 000 /s |
| read separation | 1 µs |
| expected drift between the two readings | 0.001 counts |
| so the skew is | negligible |
Which looks like the section's argument collapsing, and it does not, because the interesting case is the opposite one.
The rates that matter here are not error rates. They are the frame and octet rates.
| Quantity | Rate | Drift over a 1 µs read separation |
|---|---|---|
rx_frames | 148.81 M/s | 149 frames |
rx_octets | 12.5 G/s | 12 500 octets |
crc_errors | whatever is broken | ~0 |
So a ratio computed across an unsnapshotted read — errors per frame, or bytes per frame — is wrong by 149 frames in a denominator, which at a poll interval of one second is 0.0001% and irrelevant, and at a bring-up interval of one millisecond is 149 frames out of 148 810, which is 0.1% and still irrelevant.
The case where it is not irrelevant is the one this block exists to support: reading the same counters twice and differencing.
| With a snapshot | Without | |
|---|---|---|
| read 1 | all 27 at cycle t₀ | 27 reads spread over Δt |
| read 2 | all 27 at cycle t₁ | 27 reads spread over Δt |
| the interval | t₁ − t₀, exact | different for every counter |
| a rate | exact | each counter has its own interval |
| a cross-counter ratio | exact | wrong by the walk's duration |
Row five is the failure and it is invisible. Twenty-seven register reads over a bus take microseconds at best and milliseconds through a driver, so rx_frames and crc_errors are separated by the walk. The ratio "errors per frame" then divides a count taken at one time by a count taken at another, and the error is the walk's duration times the frame rate — at a 1 ms walk, 148 810 frames in the denominator of a ratio whose numerator moved by nothing.
Which is a 0.1% error on a healthy link and an arbitrary one on a link whose traffic is bursty, because bursts are exactly when the frame rate during the walk differs from the frame rate over the poll interval.
So the requirement is not about atomicity of a single 64-bit value. It is about a common instant.
| Requirement | Costs | Required by |
|---|---|---|
| one 64-bit counter read consistently | one shadow register | Section 10's wrap |
| all counters read at a common instant | a full shadow bank — 992 flops | Chapter 19.6 §22, Chapter 19.5 §14 |
| the instant itself | one 32-bit stamp | any rate computation |
Row two is the one that doubles the block, and the honest statement is that it is required by the diagnostics rather than by the standard. RMON does not require a common instant. Module 19's own debugging chapters do — three of the last four chapters end by pointing at a counter pair that must be read together — and a design that ships without the shadow bank has made every one of those diagnoses approximate.
13. RTL 6 — The Cross-Domain Reader
// ---------------------------------------------------------------------
// statc_cdc_reader -- get a snapshot trigger from the host domain into
// the MAC domain, and the shadow values back. Sections 10, 11 and 13.
//
// The counters run in the MAC's 195.3125 MHz domain; the register
// interface runs in the host's 250 MHz domain -- Chapter 18.1 Section 7.
// The trigger crosses as a pulse and the data crosses as a static
// value, and those are two different crossing problems with two
// different answers.
//
// The data needs NO synchroniser at all, which is the part that looks
// wrong. A shadow register that is written once per capture and read
// many cycles later is stable long before the reader sees it, so the
// crossing is a timing constraint rather than a circuit. What it needs
// is a HANDSHAKE proving the capture completed.
// ---------------------------------------------------------------------
module statc_cdc_reader
import statc_pkg::*;
(
input logic mac_clk,
input logic mac_rst_n,
input logic host_clk,
input logic host_rst_n,
// Host side.
input logic host_capture_req,
input logic [4:0] host_rd_index,
output logic [WIDE_W-1:0] host_rd_data,
output logic host_capture_done,
// MAC side.
output logic mac_capture,
input logic [WIDE_W-1:0] mac_shadow [NUM_CTRS],
// Observability. Sections 15 and 16.
output logic [31:0] c_captures_requested,
output logic [31:0] c_captures_completed,
output logic read_during_capture
);
// Request: a host-domain toggle, synchronised into the MAC domain and
// edge-detected. A level would re-trigger every MAC cycle the host
// held it; a pulse would be missed because the host is FASTER.
logic host_toggle;
logic tog_m1, tog_m2, tog_m3;
logic done_toggle;
logic done_h1, done_h2, done_h3;
always_ff @(posedge host_clk or negedge host_rst_n) begin
if (!host_rst_n) begin
host_toggle <= 1'b0; c_captures_requested <= '0;
done_h1 <= 1'b0; done_h2 <= 1'b0; done_h3 <= 1'b0;
end else begin
if (host_capture_req) begin
host_toggle <= ~host_toggle;
c_captures_requested <= c_captures_requested + 1;
end
done_h1 <= done_toggle;
done_h2 <= done_h1;
done_h3 <= done_h2;
end
end
assign host_capture_done = (done_h2 ^ done_h3);
always_ff @(posedge mac_clk or negedge mac_rst_n) begin
if (!mac_rst_n) begin
tog_m1 <= 1'b0; tog_m2 <= 1'b0; tog_m3 <= 1'b0;
done_toggle <= 1'b0; c_captures_completed <= '0;
read_during_capture <= 1'b0;
end else begin
tog_m1 <= host_toggle;
tog_m2 <= tog_m1;
tog_m3 <= tog_m2;
if (mac_capture) begin
done_toggle <= ~done_toggle;
c_captures_completed <= c_captures_completed + 1;
end
end
end
assign mac_capture = (tog_m2 ^ tog_m3);
// The shadow crosses UNSYNCHRONISED and that is correct, provided the
// host does not read it until host_capture_done has arrived. The
// handshake is what makes the crossing safe; adding synchronisers to
// 64 data bits would not, because 64 independently synchronised bits
// can still be sampled from two different capture generations.
assign host_rd_data = mac_shadow[host_rd_index];
endmoduleClassification: a crossing whose interesting property is that the wide data has no synchroniser in it.
What it teaches: that synchronising a multi-bit value bit by bit is worse than not synchronising it. Sixty-four two-flop synchronisers on the shadow's bits do not produce a coherent 64-bit value — each bit resolves independently, and a value sampled across a capture edge can mix bits from two generations. The correct structure is a handshake around unsynchronised data, which is Chapter 18.2 §19's class 75 in a different form: the multi-word structure is safe only because nobody looks at it while it changes.
And it teaches why the request crosses as a toggle rather than a pulse. The host is faster — 250 MHz against 195.3125 — so a single-cycle host pulse is 4.0 ns and a MAC cycle is 5.12 ns, and the pulse can fall entirely between two MAC sampling edges. A toggle cannot be missed because it is a level change that persists until the next request. The direction of the frequency difference decides the mechanism, and a design that copies a pulse-based crossing from a slower-to-faster boundary is correct until the clocks are re-planned.
Deliberately simplified: read_during_capture is declared and never driven, because detecting it needs the host's read index in the MAC domain — a second crossing for a check, which a production design does with a busy flag the host polls instead. The handshake has no timeout, so a host whose capture never completes waits forever rather than reporting. And host_capture_done is an edge, so a driver that polls a level sees nothing.
Production implication: c_captures_requested against c_captures_completed is the pair that finds a broken handshake, and the failure it finds is one-sided. Requested climbing with completed flat means the MAC domain is not running — a clock or reset problem, not a counter problem — and the counters will read as zero, which a driver reports as a dead link. Chapter 19.5 §18's reset-order hazard produces exactly this, and these two counters are the cheapest way to tell "the link is down" from "the statistics block never came out of reset."
14. Saturate or Wrap
The registry's description of this chapter says the counters must saturate correctly. RMON's counters wrap. Both are right and they apply to different counters, and getting the assignment wrong loses information in opposite directions.
| Wrap | Saturate | |
|---|---|---|
| what it preserves | the rate, across any interval | the fact that it happened |
| what it loses | the total, if polls are missed | the rate, once it sticks |
| who can recover | a reader that never misses a poll | nobody |
| standardised as | SNMP Counter32 / Counter64 | not a counter type at all |
Row one is the argument for wrapping and it is decisive for the twenty-seven counters in Section 2's table. A reader taking two snapshots and differencing gets the correct interval count even across a wrap, provided the interval is shorter than the wrap period — which Section 4 sized the widths to guarantee. Wrapping is not a compromise; it is the behaviour that makes differencing work.
Row two is why some things in this chapter still saturate, and none of them is a counter.
| Signal | Behaviour | Why |
|---|---|---|
addend_over_one | sticky | one occurrence is a fact about the partner |
ends_exceeded | sticky | the framing is broken; counts are untrustworthy |
read_without_capture | sticky | a driver bug that happens once and persists |
bucket_overflow | sticky | a 32-bit bucket wrapped; the reader should know |
| every entry in Section 2's table | wraps | differencing |
Rows one to four are flags, not counters, and the distinction is the section's point: a sticky bit says "this happened at least once" and a counter says "this happened N times." A design that makes an error counter sticky has thrown away the rate, and a design that makes a structural flag a counter has made a boolean fact into a number nobody can interpret — is 47 occurrences of ends_exceeded worse than 3? The framing was broken either way.
And row four is the one that is genuinely arguable. A 32-bit bucket counter wrapping is normal on a long-running port: at 148.81 M frames/s into one bucket it wraps in 28.86 seconds. So bucket_overflow will be set on every port that has been up for a minute, which makes it useless as an alarm and useful only as a statement that the bucket counters are not totals. The honest design either widens the buckets or documents that they are differencing counters only — and this chapter takes the second option, because widening seven counters to 64 bits costs another 224 flops for a number no management system reads as a total.
15. RTL 7 — Counter Telemetry
// ---------------------------------------------------------------------
// statc_telemetry -- the numbers a reader wants that the twenty-seven
// counters do not give directly. Section 15.
//
// Operator view: the three ratios that answer "is this link healthy",
// which are all cross-counter and therefore all require Section 12's
// snapshot.
// Structural view: what kind of traffic and what kind of faults.
// Derived: Chapter 19.6 Section 22's diagnosis, computed in hardware so
// nobody has to remember the subtraction.
// ---------------------------------------------------------------------
module statc_telemetry
import statc_pkg::*;
(
input logic clk,
input logic rst_n,
input logic snapshot_valid,
input logic [WIDE_W-1:0] snap [NUM_CTRS],
input logic [2:0] peak_ends,
input logic [2:0] modal_bucket,
input logic addend_over_one,
// Operator view.
output logic [15:0] err_per_million,
output logic [15:0] mean_frame_octets,
output logic [15:0] bcast_pct_x100,
// Structural view.
output logic [2:0] link_class,
output logic runts_present,
// Derived. Chapter 19.6 Section 22's complaint 1, in hardware.
output logic [15:0] trunc_share_of_crc_pct,
output logic likely_buffer_not_cable,
output logic likely_cable_not_buffer
);
logic [WIDE_W-1:0] frames, octets, crc_err, trunc, bcast;
assign frames = snap[C_RX_FRAMES];
assign octets = snap[C_RX_OCTETS];
assign crc_err = snap[C_CRC_ERR];
assign trunc = snap[C_TRUNC];
assign bcast = snap[C_RX_BCAST];
assign err_per_million = (frames == '0) ? 16'd0
: 16'((crc_err * 64'd1_000_000) / frames);
assign mean_frame_octets = (frames == '0) ? 16'd0
: 16'(octets / frames);
assign bcast_pct_x100 = (frames == '0) ? 16'd0
: 16'((bcast * 64'd10000) / frames);
// Chapter 19.6 Section 22, complaint 1: a frame truncated by a FIFO
// overflow reaches Chapter 19.4's engine structurally intact and
// arithmetically wrong, so it is counted as a CRC error. The share of
// CRC errors that are also truncations is the diagnosis, and it is a
// subtraction nobody performs because the two counters live in
// different chapters.
assign trunc_share_of_crc_pct = (crc_err == '0) ? 16'd0
: 16'((trunc * 64'd100) / crc_err);
assign likely_buffer_not_cable = snapshot_valid &&
(crc_err > 64'd100) &&
(trunc_share_of_crc_pct > 16'd50);
assign likely_cable_not_buffer = snapshot_valid &&
(crc_err > 64'd100) &&
(trunc_share_of_crc_pct < 16'd5);
assign runts_present = addend_over_one || (modal_bucket == 3'd0 &&
snap[C_UNDERSIZE] != '0);
// Three bits that classify the link before any counter is read.
// Section 8's peak_ends is the strongest single signal in the block.
always_comb begin
if (peak_ends >= 3'd3) link_class = 3'd4; // fragments below 16 octets
else if (peak_ends == 3'd2) link_class = 3'd3; // runts 16..47
else if (snap[C_UNDERSIZE] != '0) link_class = 3'd2; // runts 48..63
else if (crc_err != '0) link_class = 3'd1; // errors, no runts
else link_class = 3'd0; // clean
end
endmoduleClassification: an observability block whose outputs are all quotients, and every quotient needs Section 12's snapshot to mean anything.
What it teaches: that likely_buffer_not_cable is Chapter 19.6 §22's diagnosis compiled into one bit. That chapter's complaint 1 required an engineer to know that Chapter 19.5 §14's truncation counter exists, to read it alongside Chapter 19.4's c_bad, and to divide. Nobody does that on a first pass; they read the CRC counter, conclude "cable", and order an optic. One comparator and one divide put the conclusion next to the evidence, and the cost is under a hundred flops.
And it teaches that every output here is a cross-counter ratio and therefore a Section 12 dependency. err_per_million divides a counter by another counter; so does mean_frame_octets; so does the diagnosis. Read without a common instant, all three are approximations whose error is the register walk's duration times the frame rate — Section 12's 148 810 frames per millisecond. The telemetry block is the reason the shadow bank is not optional, and it is worth noticing that the justification runs the other way from the usual: the observability requires the structure, not the reverse.
Deliberately simplified: every quotient is a 64-bit divide computed combinationally, which is not synthesisable at this rate and must be a multi-cycle unit or a software computation — the module is written to show which quotients matter. likely_buffer_not_cable's thresholds — 100 errors and 50% — are literals where a production design takes them from registers. And link_class collapses a two-dimensional fault space into five ordered values, which loses the case of fragments and a cable fault together.
Production implication: link_class is three bits and it is the first thing a bring-up script should read. Zero means clean and the rest of the block can be ignored. Three or four means the partner is sending runts, so every other counter's value is about the partner rather than about this design, and Section 7's two-bit addend is being exercised. One means errors without runts, which is where likely_buffer_not_cable earns its comparator. Five values that route an investigation before twenty-seven counters are read, which is the same argument Chapter 19.4 §16 made for a verdict generator, arriving in telemetry.
16. RTL 8 — The Counter Conformance Monitor
// ---------------------------------------------------------------------
// statc_conformance_monitor -- verdicts. Section 16, and the thirteenth
// in Modules 18 and 19.
//
// Six verdicts. Two are about this design, two are about the PARTNER,
// one is about the driver, and one is coverage. The split is wider than
// any previous monitor's because a statistics block is the place where
// everybody else's faults are recorded.
// ---------------------------------------------------------------------
module statc_conformance_monitor
import statc_pkg::*;
#(
parameter int MIN_BUCKETS_SEEN = 7,
parameter int MIN_CAPTURES = 10
) (
input logic clk,
input logic rst_n,
input logic ends_exceeded,
input logic bucket_overflow,
input logic read_without_capture,
input logic addend_over_one,
input logic high_word_stale,
input logic [31:0] c_captures_requested,
input logic [31:0] c_captures_completed,
input logic [31:0] c_frames_binned,
input logic [31:0] bucket [NUM_BUCKETS],
input logic [WIDE_W-1:0] snap [NUM_CTRS],
output logic counts_untrustworthy,
output logic width_inadequate,
output logic partner_sending_runts,
output logic partner_nonconformant,
output logic driver_not_capturing,
output logic buckets_untested,
output logic none_of_the_above
);
int unsigned buckets_seen;
always_comb begin
buckets_seen = 0;
for (int b = 0; b < NUM_BUCKETS; b++) if (bucket[b] != '0) buckets_seen++;
// About this design. Either makes every counter meaningless.
counts_untrustworthy = ends_exceeded ||
(c_captures_requested != c_captures_completed &&
c_captures_requested > 32'd10);
width_inadequate = high_word_stale;
// About the partner. Runts are legal to RECEIVE and illegal to send,
// so these are reports about the far end -- the third time in
// Module 19 a monitor has had to say so.
partner_sending_runts = addend_over_one;
partner_nonconformant = (snap[C_UNDERSIZE] != '0) ||
(snap[C_JABBER] != '0);
// About the driver. Section 11: a driver that never triggers a
// capture reads reset values forever and reports a dead link.
driver_not_capturing = read_without_capture ||
(c_captures_requested < 32'(MIN_CAPTURES) &&
c_frames_binned > 32'd1_000_000);
// Coverage. Seven buckets, and a regression sending one frame size
// reaches one of them -- which is the Chapter 19.4 Section 21
// argument applied to a different seven-way space.
buckets_untested = (c_frames_binned > 32'd10000) &&
(buckets_seen < unsigned'(MIN_BUCKETS_SEEN));
none_of_the_above = !counts_untrustworthy && !width_inadequate &&
!partner_sending_runts && !partner_nonconformant &&
!driver_not_capturing && !buckets_untested;
end
endmoduleClassification: a verdict generator, and the first in the track whose verdicts are mostly about other people.
What it teaches: that bucket_overflow is an input and appears in no verdict. Section 14 established that a 32-bit bucket wraps in 28.86 seconds at full rate and that this is normal — the buckets are differencing counters, not totals. Putting a normal condition in a verdict makes none_of_the_above unachievable on every healthy port, which is the failure mode Chapter 19.6 §16 avoided by demoting threshold_is_trivial. This is the second deliberate demotion in two chapters, and the habit is worth naming: a computed fact earns a verdict only if a correct design can avoid it.
And it teaches that four of the six verdicts are not about this block at all. partner_sending_runts, partner_nonconformant and driver_not_capturing describe the far end and the software, and buckets_untested describes the regression. Only counts_untrustworthy and width_inadequate are about the RTL. That ratio is the nature of a statistics block: it is the place the rest of the system's faults are written down, and a monitor over it inherits the same scope.
Deliberately simplified: buckets_seen is a combinational population count over seven 32-bit comparisons, computed every cycle and needed on read. counts_untrustworthy folds a framing failure and a handshake failure into one verdict, which is right for a verdict and wrong for a diagnosis — Section 15's link_class separates them. And MIN_BUCKETS_SEEN defaults to all seven, which a regression sending one frame size cannot satisfy, deliberately.
Production implication: none_of_the_above for the thirteenth time and the first instance a clean port may legitimately fail. A port attached to a partner that sends occasional runts asserts partner_sending_runts forever, and the design is perfectly correct. The right response is the one Chapter 19.6 §18 gave for reorder_untested: disable the verdict with a written justification naming the partner, rather than lowering a threshold — because the verdict is the only record that the link is non-conformant, and the record is worth more than the clean dashboard.
17. The Aggregate, Recomputed
Chapter 19.1 §9's aggregate figure was the reason this block was flagged, and recomputing it with the counter set written down gives a quarter of the published number.
The published calculation:
| Value | |
|---|---|
| frame rate | 148.810 M/s |
| counters touched per frame | ~20 |
| counter updates per second | 2 976 M |
| at 195.3125 MHz | 15.24 per cycle |
Row one reproduces exactly. Row two does not survive Section 2's table.
A frame does not touch twenty counters. It touches the ones its own properties select, and the counter set is built out of partitions.
| Counter group | How many a frame touches | Why |
|---|---|---|
rx_frames | 1 | always |
| the seven size buckets | exactly 1 | a partition of length |
| broadcast / multicast | 0 or 1 | a unicast frame touches neither |
| the error classes | 0 or 1 | a partition, and usually empty |
rx_octets | per BEAT, not per frame | 1.3125 beats per frame at minimum size |
Rows two and four are the ones that collapse the figure. The buckets are a partition — seven counters, exactly one of which moves — so they contribute one update per frame and not seven. The error classes are the same shape and usually contribute zero.
Recomputed:
| Worst case | Typical unicast, no errors | |
|---|---|---|
| counters per frame | 4 | 2 |
| frame-triggered updates | 595.2 M/s | 297.6 M/s |
| beat-triggered updates | 195.3 M/s | 195.3 M/s |
| total | 790.6 M/s | 492.9 M/s |
| per cycle at 195.3125 MHz | 4.05 | 2.52 |
Four updates per cycle against a published 15.24 — a factor of 3.76 — and the correction is entirely that a partition was counted as a set.
Which does not change the conclusion, and that is worth being explicit about. Chapter 19.1 §9's point was that fifteen updates per cycle is fine, because they are different counters updating in parallel. Four is also fine, for the same reason, and neither figure was ever the difficulty. The correction matters because the published number invites a designer to look for contention that does not exist — and to build an arbiter, or a multi-port structure, or a wider pipeline, none of which this block needs.
What the block does need is in Sections 4, 7 and 12, and none of the three appears in an aggregate update rate.
| The real difficulty | Cost |
|---|---|
| four counters at 64 bits | 128 flops over 32-bit |
| a two-bit addend for runts | 25 gates |
| a shadow bank for cross-counter reads | 992 flops |
| the aggregate update rate | nothing |
Row four is the flagged difficulty and row three is the actual one, and the difference between them is a factor of a thousand in flops.
18. What the Counters Assume
Nine assumptions, and more of them are about other people than in any previous chapter of this module.
| # | Assumption | Owner | If wrong |
|---|---|---|---|
| 1 | a beat holds at most three frame endings | Section 7's arithmetic | ends_exceeded; every count low |
| 2 | Chapter 19.2 has measured each frame's length | the parser | every size bucket is wrong |
| 3 | Chapter 19.4 has classified each frame's check value | the CRC engine | crc_errors is wrong |
| 4 | the host triggers a capture before reading | the driver | reset values forever — Section 11 |
| 5 | the reader differences two snapshots | the management system | wrapping counters read as totals |
| 6 | polls are at most an hour apart | the management system | Section 4's width rule is void |
| 7 | the register interface is 32 bits | Chapter 18.1 §7 | Section 10's two-read problem changes shape |
| 8 | the MAC domain is running | Chapter 19.1 §13's sequencer | captures never complete; a dead-link report |
| 9 | frames below 5 octets are not framed | the receive framing | more than three endings per beat |
Row five is the assumption that is most often false and it is invisible from this side. The counters wrap — Section 14 — and a reader that treats a snapshot as a total sees a 64-bit octet count that occasionally decreases. The block cannot detect the misuse; the only defence is that the widths make wrapping rare enough that a naive reader survives, which Section 4 sized for deliberately.
Row six is the same assumption stated as a rate. Section 4's rule — a counter must not wrap in less than about an hour at the maximum rate — is a contract with the poller, not a property of the counter. A management system polling once per day makes every 32-bit counter in Section 2's table useless at 100 Gb/s, and the design cannot tell.
And two things deliberately not assumed:
| Not assumed | Why not |
|---|---|
| that the partner is conformant | Section 7 — counting non-conformance is the job |
| that any counter is ever read | the counting is correct whether or not anybody looks |
Row one is the distinguishing feature of this block within Module 19. Chapter 19.4's engine assumes at most one frame end per beat and is right to; this block assumes at most three and is right to. The same physical stream, two different assumptions, and both are correct because the two blocks have different jobs on the frames that break the first one.
19. The Cost, Accounted — and Module 19's Final Total
Eight blocks, and two of them are 74% of the chapter.
| Block | Flops | Dominated by |
|---|---|---|
counter_bank | 992 | 27 counters at their required widths |
| the four wrap counters | 128 | c_low_wraps, one per wide counter |
frame_event_counter | ~120 | the per-ending vectors |
size_histogram control | ~40 | c_frames_binned |
snapshot_bank shadow | 992 | a copy of every counter |
snapshot_bank control | ~100 | the cycle stamp |
statc_cdc_reader | ~80 | two toggle synchronisers |
statc_telemetry | ~200 | the derived registers |
statc_conformance_monitor | ~20 | verdicts |
| total | ~2 672 |
The counters and their shadow are 1 984 of 2 672 — 74.3% — and the shadow is the half nobody budgets.
Which lands Module 19.
| Chapter | Flops |
|---|---|
| Chapter 19.2 — the parser | ~4 800 |
| Chapter 19.3 — the assembler | ~1 780 |
| Chapter 19.4 — the CRC engine | ~2 164 |
| Chapter 19.5 — the FIFOs | ~1 100 |
| Chapter 19.6 — the memory interface | ~1 650 |
| this chapter — the counters | ~2 672 |
| Module 19 total | ~14 166 |
| Chapter 19.1 §18's estimate, as corrected | ~12 500 |
| over by | 13.3% |
And the overshoot has one cause, which is the same cause as the last one.
| What §18 estimated | What it omitted | |
|---|---|---|
| Chapter 19.4's CRC | the matrices | the correction barrels — 5 397 terms |
| this chapter's counters | the counters | the shadow bank — 992 flops |
Both misses are omissions of a mechanism rather than misestimates of a known one, and in both cases the omitted mechanism exists because of a requirement stated in a different chapter — Chapter 19.4 §4's sixty-four residues in one case, Chapter 19.6 §22's cross-counter diagnosis in the other. An architecture chapter estimates the blocks it can see; the mechanisms that connect them are the estimate's systematic error, and 13.3% over six chapters is a good result for an estimate made before any of them was written.
The memory bill is unchanged. This chapter adds zero bytes — twenty-seven counters and their shadow are flops, because up to four of them update in the same cycle and a single-port SRAM cannot. Module 19's memory remains Chapter 19.5's 41 KiB and Chapter 19.6's 14 KiB — 55 KiB.
| Logic | Memory | |
|---|---|---|
| the MAC datapath — Module 19 | ~14 166 flops + 39 445 XOR terms | 55 KiB |
| the system interface — Module 18 | ~25 700 flops | — |
| total, single queue, 100 Gb/s | ~39 866 flops | ~55 KiB |
Just under forty thousand flops and fifty-five kilobytes for a 100 Gb/s Ethernet port, specified across two modules and fourteen chapters — and the last row of the last chapter of the datapath is 6.7% of the logic, for the block that was called easy.
20. Properties Worth Asserting, and One Worth Refusing
Thirty-three properties in six groups, and the refused one is an identity that is false at every instant of the design's life.
Group 1 — widths and wrapping, where the properties are about parameters.
// The four wide counters are wide. Section 4: at 12.5 G octets/s a
// 32-bit octet counter wraps in 0.344 s.
a_wide_are_wide: assert property (@(posedge clk)
(WIDE_CTRS == 4) && (WIDE_W == 64));
// Section 4's rule, at elaboration: no counter wraps in under an hour
// at the maximum rate. WIDE_MAX_COUNT is the counter's modulus.
a_width_rule: assert property (@(posedge clk)
((real'(WIDE_MAX_COUNT) * 8.0) / real'(LINK_BPS)) > 3600.0);
// Counters wrap; they do not saturate. Section 14.
a_counters_wrap: assert property (@(posedge clk) disable iff (!rst_n)
(ctr[C_RX_OCTETS] == '1 && event_valid && ev.octets_valid != '0)
|=> (ctr[C_RX_OCTETS] < $past(ctr[C_RX_OCTETS])));
// Narrow counters never show bits above 32.
a_narrow_masked: assert property (@(posedge clk) disable iff (!rst_n)
ctr_out[C_CRC_ERR][WIDE_W-1:NARROW_W] == '0);
// The low word's carry drives exactly one high-word increment.
a_one_carry_one_inc: assert property (@(posedge clk) disable iff (!rst_n)
(add_en && carry) |=> (high_q == $past(high_q) + 1));
// And no carry means no high-word change.
a_no_carry_no_inc: assert property (@(posedge clk) disable iff (!rst_n)
(add_en && !carry) |=> (high_q == $past(high_q)));Group 2 — the addend, which is Section 7's subject.
// The frame addend is two bits and never exceeds three.
a_addend_bounded: assert property (@(posedge clk) disable iff (!rst_n)
event_valid |-> (ev.frames_ended <= 2'd3));
// Three endings require frames under 16 octets. Section 7's table.
a_three_needs_fragments: assert property (@(posedge clk) disable iff (!rst_n)
(event_valid && ev.frames_ended == 2'd3) |->
(end_length[0] < 16'd16 && end_length[1] < 16'd16));
// Two endings require frames under 48. The threshold is 48, not
// Chapter 5.1's 64 -- Section 7.
a_two_needs_runts: assert property (@(posedge clk) disable iff (!rst_n)
(event_valid && ev.frames_ended >= 2'd2) |-> (end_length[0] < 16'd48));
// Conformant traffic gives at most one ending. Chapter 19.4 Section 7's
// theorem, asserted on its own premise.
a_conformant_one_end: assert property (@(posedge clk) disable iff (!rst_n)
(event_valid && end_length[0] >= 16'd48) |-> (ev.frames_ended <= 2'd1));
// The octet addend is the beat's valid count, never zero on a valid beat.
a_octet_addend: assert property (@(posedge clk) disable iff (!rst_n)
event_valid |-> (ev.octets_valid inside {[1:64]}));
// The bucket hits sum to the number of endings.
a_buckets_sum: assert property (@(posedge clk) disable iff (!rst_n)
event_valid |-> ((bucket_hits[0] + bucket_hits[1] + bucket_hits[2] +
bucket_hits[3] + bucket_hits[4] + bucket_hits[5] +
bucket_hits[6]) == 3'(ev.frames_ended)));Group 3 — the classification, where the four error classes are two predicates crossed.
// Every frame lands in exactly one size bucket.
a_one_bucket: assert property (@(posedge clk) disable iff (!rst_n)
(event_valid && ev.frames_ended == 2'd1) |-> $onehot0(
{bucket_hits[0] != 0, bucket_hits[1] != 0, bucket_hits[2] != 0,
bucket_hits[3] != 0, bucket_hits[4] != 0, bucket_hits[5] != 0,
bucket_hits[6] != 0}));
// A bucket boundary is a comparator chain, not a power-of-two decode.
// 1518 is not 2^n - 1, so this is the boundary an optimiser breaks.
a_bucket_1518: assert property (@(posedge clk) disable iff (!rst_n)
(event_valid && end_length[0] == 16'd1518) |-> (ev.bucket == 3'd5));
a_bucket_1519: assert property (@(posedge clk) disable iff (!rst_n)
(event_valid && end_length[0] == 16'd1519) |-> (ev.bucket == 3'd6));
// Undersize and fragment are the same length with different check
// values, so a frame is never both counted as undersize and NOT
// counted as a fragment when its FCS is bad.
a_fragment_is_undersize: assert property (@(posedge clk) disable iff (!rst_n)
(event_valid && ev.is_undersize && ev.is_crc_err) |-> ev.is_undersize);
// Oversize is bounded by the configured MTU, not by 1518.
a_oversize_uses_mtu: assert property (@(posedge clk) disable iff (!rst_n)
(event_valid && ev.is_oversize) |-> (end_length[0] > cfg_mtu));
// A frame is never both broadcast and multicast.
a_bcast_xor_mcast: assert property (@(posedge clk) disable iff (!rst_n)
event_valid |-> !(ev.is_bcast && ev.is_mcast && ev.frames_ended == 2'd1));Group 4 — the snapshot, where the properties are about a common instant.
// A capture copies every counter in the same cycle.
a_capture_is_atomic: assert property (@(posedge clk) disable iff (!rst_n)
capture |=> (shadow[C_RX_FRAMES] == $past(ctr_in[C_RX_FRAMES]) &&
shadow[C_CRC_ERR] == $past(ctr_in[C_CRC_ERR]) &&
shadow[C_TRUNC] == $past(ctr_in[C_TRUNC])));
// The counters keep counting during a read. The snapshot is a copy, not
// a freeze -- Section 11.
a_no_freeze: assert property (@(posedge clk) disable iff (!rst_n)
(rd_index < NUM_CTRS && event_valid) |=>
(ctr_in[C_RX_FRAMES] != $past(ctr_in[C_RX_FRAMES]) ||
$past(ev.frames_ended) == '0));
// The stamp moves with the capture.
a_stamp_updates: assert property (@(posedge clk) disable iff (!rst_n)
capture |=> (captured_at == $past(cycle_now)));
// A shadow value never changes between captures.
a_shadow_stable: assert property (@(posedge clk) disable iff (!rst_n)
(!capture) |=> $stable(shadow[C_RX_FRAMES]));
// Reading before any capture is reported.
a_read_needs_capture: assert property (@(posedge clk) disable iff (!rst_n)
(rd_index < NUM_CTRS && !snapshot_valid) |=> read_without_capture);
// Snapshot values never exceed the live counters.
a_shadow_lags: assert property (@(posedge clk) disable iff (!rst_n)
snapshot_valid |-> (shadow[C_RX_FRAMES] <= ctr_in[C_RX_FRAMES]));Group 5 — the crossing, where the wide data deliberately has no synchroniser.
// The request crosses as a toggle. A pulse would be missed: the host is
// FASTER -- 4.0 ns against 5.12 -- so a one-cycle host pulse can fall
// between two MAC edges. Section 13.
a_request_is_toggle: assert property (@(posedge host_clk) disable iff (!host_rst_n)
host_capture_req |=> (host_toggle != $past(host_toggle)));
// One request produces exactly one capture.
a_one_req_one_cap: assert property (@(posedge mac_clk) disable iff (!mac_rst_n)
mac_capture |-> (tog_m2 != tog_m3));
// Requested and completed track each other within the synchroniser
// depth, and never diverge permanently.
a_handshake_balances: assert property (@(posedge mac_clk) disable iff (!mac_rst_n)
(c_captures_requested - c_captures_completed) <= 32'd2);
// Done is an edge, not a level.
a_done_is_edge: assert property (@(posedge host_clk) disable iff (!host_rst_n)
host_capture_done |=> !host_capture_done);
// The shadow is read only after done. This is the property that makes
// the unsynchronised data crossing safe, and it is a property of the
// DRIVER rather than of the RTL.
a_read_after_done: assert property (@(posedge host_clk) disable iff (!host_rst_n)
(rd_index < NUM_CTRS) |-> $past(host_capture_done, 1) ||
$past(snapshot_valid));Group 6 — coverage.
c_all_buckets: cover property (@(posedge clk)
bucket[0] != 0 && bucket[1] != 0 && bucket[2] != 0 &&
bucket[3] != 0 && bucket[4] != 0 && bucket[5] != 0 &&
bucket[6] != 0);
c_two_ends: cover property (@(posedge clk) ev.frames_ended == 2'd2);
c_three_ends: cover property (@(posedge clk) ev.frames_ended == 2'd3);
c_low_word_wrap: cover property (@(posedge clk) add_en && carry);
c_capture_burst: cover property (@(posedge clk) capture ##1 capture);
c_read_all_27: cover property (@(posedge clk) rd_index == 5'd26);
c_boundary_1518: cover property (@(posedge clk)
event_valid && end_length[0] == 16'd1518);21. Verification Scenarios
Fifty-eight scenarios, plus a five-run directed test whose content is a frame-length sweep and a poll interval.
Widths and wrapping — 11 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 1 | rx_octets driven to 2³² − 1, one more octet | low word wraps, high word increments |
| 2 | rx_octets driven to 2⁶⁴ − 1 | wraps to zero; no saturation |
| 3 | crc_errors at 2³² − 1, one more | wraps — Section 14 |
| 4 | 12.5 G octets/s for 0.35 s | one low-word wrap |
| 5 | the same for 1 s | two wraps; c_low_wraps = 2 |
| 6 | a 32-bit rx_octets build | a_wide_are_wide fails at elaboration |
| 7 | a 48-bit build | a_width_rule fails — 6.26 h, under an hour is the bar |
| 8 | 64-bit at 400 Gb/s | passes — 11.7 years |
| 9 | two carries in consecutive cycles, forced | high_word_stale |
| 10 | rx_frames at 2³² for 28.9 s | 32-bit build wraps inside a poll |
| 11 | the same 64-bit | no wrap in 3 929 years |
Row seven is the scenario that justifies the rule rather than the width. Forty-eight bits survives 6.26 hours, which is longer than most polling intervals and shorter than the one-hour rule's intent once a margin is applied — and a design that picks 48 to save 64 flops has chosen a width that works until somebody polls hourly.
The addend and runts — 12 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 12 | a stream of 64-octet frames | frames_ended never exceeds 1 |
| 13 | a stream of 48-octet runts | still 1 — Section 7's threshold |
| 14 | a stream of 47-octet runts | 2 in some beats |
| 15 | a stream of 16-octet runts | 2 |
| 16 | a stream of 15-octet fragments | 3 |
| 17 | 4-octet units | ends_exceeded |
| 18 | 47-octet runts with a 1-bit addend | half the frames counted |
| 19 | 15-octet fragments with a 1-bit addend | a third counted |
| 20 | two broadcast runts in one beat | rx_broadcast must add 2 |
| 21 | the same with an OR'd flag | adds 1 — Section 8's stated bug |
| 22 | two runts in different size buckets | both buckets increment |
| 23 | the same with one bucket carried | one bucket increments twice |
| 24 | a beat with 64 valid octets | octets_valid = 64 |
Rows eighteen and nineteen are the failure Section 7 exists to prevent, and their signature is a counter that is a clean fraction of the truth — exactly one half or exactly one third — which is the most identifiable shape a wrong counter can have.
Classification — 11 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 25 | a 64-octet frame | bucket 0 |
| 26 | a 65-octet frame | bucket 1 |
| 27 | a 1 518-octet frame | bucket 5 |
| 28 | a 1 519-octet frame | bucket 6 |
| 29 | a 2 047-octet frame | bucket 6 |
| 30 | the decoder optimised to a power-of-two encode | rows 27 and 28 swap |
| 31 | a 63-octet frame, good FCS | undersize, not fragment |
| 32 | a 63-octet frame, bad FCS | fragment |
| 33 | a 9 000-octet frame at MTU 1 518 | oversize |
| 34 | a 9 000-octet frame at MTU 9 000 | not oversize |
| 35 | a 9 000-octet frame, bad FCS, MTU 1 518 | jabber |
Row thirty is the bucket bug and it is a one-line optimisation away. Five of the six boundaries are 2ⁿ − 1 and 1 518 is not, so an encoder built on the length's high bits puts 1 519 to 2 047 in bucket 5 — and the jumbo range is where an operator looks when jumbo frames are being investigated.
The snapshot — 12 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 36 | capture, then read all 27 | a consistent set |
| 37 | read all 27 with no capture | read_without_capture; reset values |
| 38 | capture during traffic | counters keep counting |
| 39 | two captures 1 s apart | captured_at differs by 195.3 M |
| 40 | two captures 22.1 s apart | cycle_now wrapped — a negative interval |
| 41 | capture while a counter increments | the shadow takes one or the other, never a mix |
| 42 | a walk of 27 reads with no snapshot | rx_frames drifts 149 per µs |
| 43 | the same over a 1 ms driver walk | 148 810 frames of drift |
| 44 | err_per_million from an unsnapshotted walk | wrong by the walk's duration |
| 45 | the same snapshotted | exact |
| 46 | a capture request with the MAC clock stopped | requested climbs, completed flat |
| 47 | a host pulse of one host cycle | crosses — the toggle cannot be missed |
| 48 | the same as a level | would re-trigger every MAC cycle |
Rows forty-four and forty-five are Section 12's argument as a test, and row forty-three is the number that makes it concrete: a driver walking twenty-seven registers in a millisecond separates the numerator and denominator of every ratio by 148 810 frames.
Diagnosis and the monitor — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 49 | 400 CRC errors, 380 truncations | likely_buffer_not_cable |
| 50 | 400 CRC errors, 0 truncations | likely_cable_not_buffer |
| 51 | 400 CRC errors, 200 truncations | neither — 50% is the boundary |
| 52 | 50 CRC errors, 48 truncations | neither — below the 100 threshold |
| 53 | a clean link | link_class = 0 |
| 54 | 47-octet runts | link_class = 3 |
| 55 | 15-octet fragments | link_class = 4 |
| 56 | one frame size for 10 000 frames | buckets_untested |
| 57 | all seven buckets exercised | clear |
| 58 | a partner sending occasional runts | partner_sending_runts forever, design correct |
Row fifty-eight is the verdict a correct design cannot clear, and Section 16's production note is the reason it should be disabled with a written justification rather than lowered.
The directed test — five runs random stimulus will not produce.
Three of this chapter's failures need a frame length distribution nothing generates by accident, and one needs a poll pattern.
| Failure | Needs | A default generator provides |
|---|---|---|
| the 1-bit addend undercount | frames under 48 octets, back to back | legal frames only |
| the three-ending case | frames under 16 octets | nothing below 64 |
| the 1518/1519 bucket bug | both boundary lengths | round numbers |
| the low-word wrap | 2³² octets — 0.344 s of traffic | thousands of frames, not 67 million beats |
| the unsnapshotted ratio | a multi-register walk during traffic | a single-cycle read |
Row four is the one that costs simulation time and cannot be shortened by stimulus. Reaching a 32-bit wrap needs 67.1 million beats — 0.344 seconds of simulated time — which at any RTL simulation rate is hours. The constructive answer is not to run it: parameterise LOW_W down to 12 bits in a test build, which reaches the wrap in 4 096 octets, and assert that the carry logic is width-independent.
Construct it. Five runs.
| Run | Frame lengths | Poll | Exercises |
|---|---|---|---|
| A | 64 … 1 518, uniform | snapshotted | the nominal path; one ending per beat |
| B | the seven bucket boundaries and each ± 1 | snapshotted | 21 lengths; row 30's decoder bug |
| C | 47, repeated, back to back | snapshotted | two endings per beat |
| D | 15, repeated, back to back | snapshotted | three endings; the addend's bound |
| E | 64 … 1 518, uniform | 27 unsnapshotted reads over 1 ms | Section 12's ratio error |
Run B is twenty-one lengths and finds a bug no sweep finds. The seven buckets have six internal boundaries; each needs the length below it, the length at it and the length above — 64, 65, 127, 128, 255, 256, 511, 512, 1023, 1024, 1518, 1519 and the extremes. A uniform sweep over 1 455 lengths hits all of them and reports nothing distinguishing, because no counter says which boundary was crossed.
Run D is the addend's bound and it is fifteen octets. Frames of 15 octets back to back give a wire period of 32, so a 64-octet beat holds two whole periods and part of a third — three endings — and a two-bit addend is exactly wide enough. A design that sized the addend from conformant traffic counts a third of them.
The oracle, in four parts:
| Check | A | B | C | D | E |
|---|---|---|---|---|---|
peak_ends | 1 | 1 | 2 | 3 | 1 |
| buckets with a non-zero count | 7 | 7 | 1 | 1 | 7 |
rx_frames against the generator's count | equal | equal | equal | equal | equal |
err_per_million against the generator's | equal | equal | equal | equal | WRONG |
Row one is the run's identity and row three is its correctness, and the two are independent: run D can have a perfect frame count and a broken bucket count, because the buckets take the per-ending vector and the frame counter takes the sum.
Row four's single wrong entry is the chapter's central verification fact. Runs A and E are the same stimulus; the only difference is whether the reader took a snapshot. The frame count is right in both — row three — and the ratio is wrong in one, which is exactly the failure that survives every functional test and reaches a management system as a plausible number.
22. Debugging a Counter Set
Four complaints, and three of them are about a reader rather than about the counters.
Complaint 1 — "the port reports no traffic and the link is up."
| Check | If yes | Meaning |
|---|---|---|
read_without_capture set? | the driver never triggers a capture | Section 11 — it is reading reset values |
c_captures_requested at zero? | confirms | a driver ported from a block with no snapshot |
| requested climbing, completed flat? | the MAC domain is not running | a clock or reset problem |
c_beats in the event counter moving? | the datapath is fine | so the fault is the read path |
Rows one and three are two different failures with one symptom, and the counter that separates them costs nothing. A driver that never captures and a MAC domain that never came out of reset both report zero through the register interface; c_captures_requested tells them apart in one read, because a driver that is not asking produces zero requests and a stopped MAC produces requests with no completions.
Complaint 2 — "errors per frame jumps around between polls."
| Check | If yes | Meaning |
|---|---|---|
| is the driver snapshotting? | check c_captures | Section 12 |
| how long does the register walk take? | multiply by 148.81 M/s | the drift in the denominator |
| does it settle at a longer poll interval? | confirms | the walk is a fixed fraction of a shorter interval |
| is the traffic bursty? | the walk samples a different rate than the interval | and the error is unbounded |
Row three is the diagnostic that identifies it without changing anything. The ratio's error is the walk's duration times the frame rate, so at a fixed walk it is a fixed number of frames — which is 0.1% of a one-second poll and 10% of a ten-millisecond one. A ratio that improves as the poll interval lengthens is a snapshot problem, not a link problem, and no amount of looking at the link will show it.
Complaint 3 — "the fragment counter reads about a third of what the capture shows."
| Check | If yes | Meaning |
|---|---|---|
peak_ends = 3? | beats are holding three endings | Section 7 |
| is the addend one bit? | the counter adds 1 where it should add 3 | ours |
| is the ratio exactly one third? | confirms — a clean fraction | and never one half |
ends_exceeded set? | more than three; the framing is broken | not a counting bug |
Row three is the signature and it is the most identifiable shape in this chapter. A counter that is wrong by a clean fraction — exactly one half, exactly one third — is an addend that is too narrow, because the error is the ratio of the true addend to the saturated one. A counter wrong by a ragged amount is something else entirely, and the distinction takes one division to make.
Complaint 4 — "jumbo frames are counted in the 1024-to-1518 bucket."
| Check | If yes | Meaning |
|---|---|---|
| does a 1 519-octet frame land in bucket 5? | the boundary is wrong | Section 9 |
| is the decoder a comparator chain? | it should be | and probably is not |
| do the other five boundaries work? | they will | five of six are 2ⁿ − 1 |
| does the error start at 1 519 and end at 2 047? | a power-of-two decode on bits [10:0] | definitively |
Row four is the fingerprint. An encoder built on the length's high bits treats 1 519 through 2 047 as one bucket with 1 024 through 1 518, and is correct everywhere else — so the bug affects exactly 529 of the 1 455 legal lengths and none of the round ones. The range's endpoints name the optimisation that caused it.
And the three symptoms this chapter is systematically blamed for:
| Symptom | Blamed on | Usually is |
|---|---|---|
| "the counters are wrong" | the counting logic | a reader that did not snapshot |
| "the port shows no traffic" | the link | a driver that never captures |
| a counter that decreases | a bug | a 32-bit counter wrapping, as specified |
23. Misconceptions
Misconception 1 — "a 32-bit counter is enough; RMON has used them for decades."
The wrong model: the standard's counter width is a standard's counter width.
What it costs: an octet counter that wraps every 0.344 seconds at 100 Gb/s and a frame counter that wraps every 28.86 seconds — inside a 30-second poll. A reader cannot distinguish one wrap from two, so the counter carries no information about the interval and the port's throughput is unmeasurable.
The corrected model: the width was chosen when the fastest interface was 10 Mb/s, where a 32-bit octet counter wraps in 57 minutes — just inside the one-hour rule. The constant was a rate-dependent quantity frozen at the rate that was current, and four of the twenty-seven counters must be 64 bits because their increments scale with the line. Sections 2, 4.
Misconception 2 — "the dual-frame beat means every counter needs a +2."
The wrong model: Chapter 19.1 §9 said a beat can complete two frames' statistics at once, so size the addend for two.
What it costs: nothing directly, and a missed requirement. Chapter 19.4 §7 proved that two conformant frames cannot both END in one beat — the minimum wire period is 81 octets against a 64-octet beat — so on legal traffic the frame counters take +1 and the dual-frame beat contributes one end and one start, and a start updates nothing.
The corrected model: the addend is needed for two other reasons. Octet counters have added the beat's valid count since the datapath was wider than one octet, and frame counters need 0 to 3 because runts break the 81-octet premise — 47-octet runts give two endings per beat and 15-octet fragments give three. A design that sizes the addend from the dual-frame argument gets one bit and needs two. Sections 6, 7.
Misconception 3 — "counting runts is an edge case."
The wrong model: runts are rare, so the counter path for them need not be exact.
What it costs: a counter that is most wrong when the link is most broken. A one-bit addend counts half of a 47-octet runt flood and a third of a 15-octet fragment flood, so an operator watching a failing link reads a third of the true rate and concludes the problem is smaller than it is.
The corrected model: the statistics block is the only block in the MAC that may not assume conformant traffic, because counting non-conformance is its job. Chapter 19.4's engine may assume one end per beat — a runt's check value is discarded anyway — and this block may not. The fix is one bit of addend on twenty-five adders. Sections 7, 8.
Misconception 4 — "a snapshot is a freeze."
The wrong model: to read a consistent set of counters, stop them while the register walk happens.
What it costs: every frame that arrives during the walk. At 148.81 M frames/s a one-millisecond driver walk loses 148 810 frames, and the counters then under-report by exactly the amount the reader was trying to measure precisely.
The corrected model: the snapshot is a copy — one cycle, every counter, into a shadow bank — and the counters keep counting. It costs 992 flops and loses nothing. The cost is the reason it is omitted from estimates and the reason Chapter 19.1 §18's figure for this chapter was 60% short. Sections 11, 19.
Misconception 5 — "capturing one counter atomically is enough."
The wrong model: the problem is reading 64 bits through a 32-bit interface, so capture the wide counters and read the rest directly.
What it costs: every cross-counter ratio. Chapter 19.6 §22's diagnosis is truncations / crc_errors; err_per_million is crc_errors / rx_frames; mean_frame_octets is rx_octets / rx_frames — and a quotient whose numerator and denominator were sampled a register walk apart is wrong by the walk's duration times the frame rate.
The corrected model: the requirement is a common instant, not per-counter atomicity, and it is required by Module 19's own debugging chapters rather than by RMON. Three of the last four chapters end by pointing at a counter pair that must be read together. Sections 10, 12.
Misconception 6 — "assert that frames equals good plus bad."
The wrong model: a counter set should satisfy its conservation laws, so assert them.
What it costs: an assertion that fails on the first frame and is deleted. Chapter 19.4 §5's correction barrel is six stages, so a frame's classification lands six cycles after its end, and at 1.312 cycles per frame 4.57 frames sit permanently between the two commits. The identity holds only when the pipeline is empty, which at line rate it never is.
The corrected model: state the skew, or move the commits together. A single frame-retire event carrying length, classification, address class and size bucket makes the identity true by construction for about forty flops, and removes the question rather than bounding it. Sections 20, and Chapter 19.4 §5 for where the six cycles come from.
24. Interview Questions
Question 1 — "How wide is a byte counter on a 100 Gb/s port?"
What the answer should establish: 64 bits, and the reason is one division. The port moves 12.5 G octets/s, so a 32-bit counter wraps in 0.344 seconds — faster than any polling interval, and a reader cannot tell one wrap from two. A strong answer gives the rule rather than the number: a counter must not wrap in under about an hour at the interface's maximum rate, which 32 bits satisfied at 10 Mb/s — 57 minutes — and has failed at every rate since. It also notes that the frame counters need 64 bits too: 2³² frames at 148.81 M/s is 28.86 seconds, inside a 30-second poll.
Question 2 — "A beat carries the end of one frame and the start of the next. What does that do to your counters?"
What the answer should establish: nothing. Chapter 19.4 §7's theorem says two conformant frames cannot both end in one beat — the minimum wire period is 81 octets and a beat is 64 — and a start updates no counter, because the frame's length, check value and size bucket are all unknown. A strong answer then gives the case that does need a wide addend: runts. At 47 octets the wire period is exactly 64 and a beat holds two endings; below 16 octets it holds three — so the addend is two bits, and it is sized by traffic the standard forbids rather than by traffic it permits.
Question 3 — "Why can't the statistics block use the same one-end-per-beat assumption the CRC engine does?"
What the answer should establish: because counting runts is its job. The CRC engine may discard a runt — its residue is meaningless — so assuming the premise costs it nothing. The statistics block's undersize and fragments counters exist precisely to record the frames that violate the premise, so it must be built to the runt bound. A strong answer notes the cost of getting it wrong: a one-bit addend reports exactly half of a 47-octet runt flood and exactly a third of a 15-octet one — a clean fraction, which is the most identifiable signature a wrong counter can have.
Question 4 — "Your driver reads twenty-seven counters over a millisecond. What is wrong with the ratios it computes?"
What the answer should establish: the numerator and denominator were sampled at different times, and the frame rate is 148.81 M/s. A millisecond walk separates them by 148 810 frames — 0.1% on a healthy link, and unbounded on bursty traffic, because the rate during the walk is not the rate over the interval. A strong answer names the fix and its cost: a shadow bank capturing all twenty-seven in one cycle, 992 flops, and the counters keep counting — a copy rather than a freeze, because freezing them loses 148 810 frames instead of misattributing them.
Question 5 — "Is one shadow register enough?"
What the answer should establish: only for the wrap problem, not for the ratio problem. One 64-bit shadow makes a single counter readable through a 32-bit interface without straddling a low-word wrap — which happens once every 3.98 days at one poll per second. It does nothing for a cross-counter read, and every useful number this block produces is a cross-counter ratio. A strong answer cites the concrete requirement: Chapter 19.6 §22's diagnosis is truncations / crc_errors, and it separates a buffer fault from a cable fault — the most expensive misdiagnosis in Module 19.
Question 6 — "You assert that total frames equals good plus bad, and it fails immediately. Why?"
What the answer should establish: the two counters commit at different pipeline depths. A frame's existence is known at end-of-frame; its check value is known six cycles later, because Chapter 19.4 §5's correction barrel is six pipelined stages. At 1.312 cycles per frame that is 4.57 frames permanently between the two commits, and the skew never resolves because new frames arrive as fast as old ones retire. A strong answer gives the design fix rather than a skew bound: a single frame-retire event carrying every attribute makes the identity true by construction, for about forty flops.
25. Questions and Answers
26. What's Next
Module 19 is complete. The MAC's datapath is specified from the wire to the memory interface, and Module 20 is how you find out whether it works.
Seven chapters, ~14 166 flops, 39 445 XOR terms and 55 KiB — against Chapter 19.1 §18's ~12 500-flop estimate, 13.3% over, with both misses being mechanisms that connect blocks rather than blocks themselves.
And the module leaves Module 20 a specific inheritance, which is four lists of cases that random stimulus does not reach.
| Chapter | The case | Why random misses it |
|---|---|---|
| Chapter 19.4 §4 | 64 partial-word residue classes | the classic seven sizes reach 3 |
| Chapter 19.3 §20 | 23 sizes that spill AND fill the deficit | 1.6% of the range |
| Chapter 19.4 §7 | dual-frame beats | absent above 128-bit datapaths in a slow model |
| Chapter 19.6 §21 | four simultaneous reorderings | a clean bus model produces zero |
| this chapter §7 | runts under 16 octets | generators send legal frames |
Chapter 20.1 builds the generator that reaches all five, and derives which of them constrained-random weights can produce and which need a directed sequence — because a weight cannot produce a case that depends on a relationship between two frames.
Chapter 20.2 then turns this track's assertions into a library. Twenty chapters have each ended with thirty-odd properties and exactly one rejected class; the series is now eighty-seven classes long, and the question that has never been asked is what taxonomy they fall into and which of them a parameterised package can express once.
Chapter 20.3 builds the scoreboards, and inherits Chapter 19.4 §14's problem at frame granularity: a reference model written by the same engineer as the design checks the design against itself. Chapter 20.4 builds the coverage model, and inherits Chapter 19.4 §21's finding that a coverage model tracking one dimension cannot distinguish a thorough run from a narrow one.
Continue learning
Related tutorials
- Related topic
The MAC/PHY Boundary in RTL
What five chapters described as one boundary is three in silicon: a data boundary at the port list, a clock boundary inside the elastic buffer, and a reset boundary that is an order rather than a place. Confusing any two produces a specific, recognisable integration failure.
- Related topic
The MAC's FIFOs
Three buffers with three different sizing arguments: one octet from a clock tolerance, four beats from a synchroniser, and 123 beats from a cable's length.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
- Related topic
Ethernet System Architecture
Client, MAC, reconciliation sublayer, PCS, PMA, PMD, medium — six blocks whose port lists are the real content. Each contract has two halves: what a layer delivers, and what it is forbidden to know about its neighbours, which is why one MAC outlived every physical layer.
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.
