Ethernet · Module 24
Ethernet against PCIe
An address that cannot miss, a read that comes back, and the 425 088 bits of state a 400 Gb/s return path must hold — against an Ethernet transmitter that holds nothing.
Two interconnects sit inside every server in this track's Module 23, and engineers move between them daily without noticing that they disagree about what an address is.
Module 24 is a comparison module, and a comparison chapter is where a track goes soft. The failure mode is a feature table: two columns, a row per attribute, a tick in each. It teaches nothing, because a tick is not a quantity and a design decision is not a preference. This chapter's rule — and Module 24's — is that a comparison earns its place only by deriving a number the compared thing forces that Ethernet does not, and by building the adapter between them in RTL so the difference is a structure rather than an adjective.
Two differences do all the work here and everything else follows from them.
| Ethernet | PCIe | |
|---|---|---|
| an address is | a name | a destination |
| a lookup | can miss — Chapter 12.4 floods | cannot miss — an unmatched address is an error |
| a transfer | completes when the last octet leaves | completes when something comes back |
| the transmitter is gated by | nothing | a credit counter the far end owns |
| state held after sending | none | everything, until the completion returns |
Row five is this chapter's centre and Section 4 prices it.
1. Scope — A Name, a Destination, and a Return Path
This chapter owns three derivations and one bridge.
| What is derived | |
|---|---|
| Section 2 | what a name costs against a destination — 196 608 BCE per port against 7 680 |
| Sections 4 to 5 | what a return path costs at 400 Gb/s — 196 tags and 425 088 BCE |
| Sections 6 to 9 | what credits and ordering cost, in the same unit |
| Sections 10 to 13 | the bridge, built, and the property that does not survive it |
What this chapter does not own. It is not a PCIe tutorial: the transaction-layer packet format, the physical layer's training sequences, the configuration space's structure and the enumeration algorithm are all outside it. It uses exactly as much PCIe as the comparison needs, and every PCIe quantity it uses is derived here rather than quoted.
It also does not argue that either interconnect is better. They answer different questions and the chapter's closing position is that the question each answers is visible in the state each holds, which is a structural fact rather than an opinion.
2. Addressing: What a Lookup Costs That a Decode Does Not
An Ethernet destination address is a name. A PCIe address is a position in a space. The difference is not philosophical — it is 25.6 times the silicon, per port, and one failure mode.
Start with what each one is asked to do.
| Ethernet | PCIe | |
|---|---|---|
| the field | 48-bit destination address | 64-bit memory address |
| the question | which port is this name at? | which port's window contains this position? |
| answered by | a table populated by observation — Chapter 12.2 | a range comparison against registers written at enumeration |
| if nothing matches | flood — Chapter 12.4 | Unsupported Request — a reported error |
| the table's contents come from | the traffic | software |
Row three is the whole difference and rows four and five are its consequences.
Price the Ethernet side first, from Chapter 23.3 §2's table.
| Size | BCE | |
|---|---|---|
| MAC table, 128k × 96 b | 12.6 Mbit | 1.26 × 10⁷ |
| across 64 ports | — | 196 608 per port |
Then the PCIe side, derived. A PCIe switch port decodes an address against three windows — non-prefetchable memory, prefetchable memory, and I/O — and each window is a base and a limit. At 64 bits each that is:
3 windows x 2 registers x 64 bits = 384 flip-flops per port
384 x 20 BCE = 7 680 BCE per port| Per port | BCE | × the datapath |
|---|---|---|
| Ethernet MAC table's share | 196 608 | 0.694 |
| PCIe address decode | 7 680 | 0.027 |
| ratio | 25.6× | — |
Naming a station costs twenty-six times what locating one does, and the name can still not be found.
So the trade is stated exactly.
| Ethernet buys | By paying | |
|---|---|---|
| zero configuration | a station may be plugged anywhere | a table that can miss, and a flood when it does |
| PCIe buys | a decode that cannot miss | an enumeration pass before anything works |
And neither is a preference. A fabric whose endpoints are enumerated at boot by one authority can afford the decode. A fabric whose endpoints arrive and leave without telling anyone cannot, and that is the difference between a backplane and a network.
3. RTL 1 — The Comparison Package and the Address Model
// ---------------------------------------------------------------------
// pciecmp_pkg -- the constants this chapter's comparison needs, with
// every derived figure computed here rather than written as a literal.
//
// The unit is Chapter 23.3 Section 2's bitcell equivalent:
// 1 BCE = one bit of usable on-die SRAM = 0.35 GE
// 1 flip-flop = 20 BCE
// Chapter 19.7 Section 19's MAC receive datapath = 283 320 BCE
// ---------------------------------------------------------------------
package pciecmp_pkg;
// ---- the anchor ------------------------------------------------------
localparam int unsigned DATAPATH_BCE = 283_320;
localparam int unsigned BCE_PER_FLOP = 20;
// ---- the Ethernet side ----------------------------------------------
localparam int unsigned MAC_TBL_ENTRIES = 128 * 1024;
localparam int unsigned MAC_TBL_WIDTH = 96;
localparam int unsigned SWITCH_PORTS = 64;
// ---- the PCIe side ---------------------------------------------------
// Three decode windows -- non-prefetchable memory, prefetchable
// memory, and I/O -- each a base and a limit, at 64 bits.
localparam int unsigned DECODE_WINDOWS = 3;
localparam int unsigned DECODE_REGS = 2; // base, limit
localparam int unsigned DECODE_WIDTH = 64;
// ---- the return path -------------------------------------------------
// PCIe Gen5 supports 10-bit tags; the number a link NEEDS is set by
// Little's law and is derived in Section 4 rather than assumed.
localparam int unsigned TAG_BITS = 10;
localparam int unsigned TAG_ENTRY_BITS = 128; // tag, requester, addr,
// length, BE, ptr, valid
localparam int unsigned MAX_PAYLOAD_B = 256;
// ---- credits ---------------------------------------------------------
// Three credit types (posted, non-posted, completion) x two units
// (header 8 b, data 12 b) x three registers (limit, consumed,
// allocated), per virtual channel, per direction.
localparam int unsigned CREDIT_TYPES = 3;
localparam int unsigned CREDIT_HDR_BITS = 8;
localparam int unsigned CREDIT_DAT_BITS = 12;
localparam int unsigned CREDIT_REGS = 3;
localparam int unsigned VIRTUAL_CHANS = 8;
typedef enum logic [1:0] {
XACT_POSTED = 2'd0, // memory write -- no completion
XACT_NONPOSTED = 2'd1, // memory read -- a completion is owed
XACT_COMPLETION = 2'd2 // the completion itself
} xact_e;
typedef enum logic [1:0] {
CPL_SUCCESSFUL = 2'd0,
CPL_UNSUPPORTED_REQ = 2'd1,
CPL_ABORT = 2'd2,
CPL_RETRY = 2'd3
} cpl_status_e;
// ---- derived: the Ethernet lookup, per port -------------------------
function automatic int unsigned mac_table_bce();
return MAC_TBL_ENTRIES * MAC_TBL_WIDTH; // an array: 1 BCE/bit
endfunction
function automatic int unsigned mac_table_bce_per_port();
return mac_table_bce() / SWITCH_PORTS;
endfunction
// ---- derived: the PCIe decode, per port ------------------------------
function automatic int unsigned decode_flops();
return DECODE_WINDOWS * DECODE_REGS * DECODE_WIDTH;
endfunction
function automatic int unsigned decode_bce_per_port();
return decode_flops() * BCE_PER_FLOP; // registers: 20/bit
endfunction
// ---- derived: Little's law at the return path ------------------------
// bytes in flight = rate x round trip. Expressed in the units the
// callers have: Gb/s and nanoseconds give bytes directly, because
// 1 Gb/s x 1 ns = 0.125 bytes and the /8 does the conversion.
function automatic int unsigned bytes_in_flight(int unsigned gbps,
int unsigned rtt_ns);
return (gbps * rtt_ns) / 8;
endfunction
function automatic int unsigned tags_needed(int unsigned gbps,
int unsigned rtt_ns);
int unsigned b;
b = bytes_in_flight(gbps, rtt_ns);
return (b + MAX_PAYLOAD_B - 1) / MAX_PAYLOAD_B; // round up
endfunction
function automatic int unsigned return_path_bce(int unsigned gbps,
int unsigned rtt_ns);
// The tag table is an array, and so is the reorder buffer.
return (tags_needed(gbps, rtt_ns) * TAG_ENTRY_BITS)
+ (bytes_in_flight(gbps, rtt_ns) * 8);
endfunction
// ---- derived: credit state -------------------------------------------
function automatic int unsigned credit_flops_per_port();
return VIRTUAL_CHANS * CREDIT_TYPES
* (CREDIT_HDR_BITS + CREDIT_DAT_BITS)
* CREDIT_REGS * 2; // two directions
endfunction
function automatic int unsigned credit_bce_per_port();
return credit_flops_per_port() * BCE_PER_FLOP;
endfunction
// ---- reporting -------------------------------------------------------
// Datapaths, in thousandths, so an integer output can carry a ratio
// smaller than one without becoming zero.
function automatic int unsigned datapaths_milli(int unsigned bce);
return (bce * 1000) / DATAPATH_BCE;
endfunction
endpackage// ---------------------------------------------------------------------
// address_model -- what a NAME costs against a POSITION, and the
// failure mode each one has.
//
// The area numbers are the easy half. The output that matters is
// lookup_can_miss, which is 1 on one side and 0 on the other and is
// the reason the two sides need different diagnostic chapters.
// ---------------------------------------------------------------------
module address_model
import pciecmp_pkg::*;
(
input logic clk,
input logic rst_n,
input logic is_ethernet, // 1 = name lookup, 0 = decode
input logic [15:0] ports,
input logic lookup_hit,
input logic decode_hit,
output logic [31:0] addressing_bce_per_port,
output logic [31:0] addressing_dp_milli,
output logic lookup_can_miss,
output logic [15:0] copies_on_miss,
output logic miss_is_reported,
output logic miss_occurred,
output logic [31:0] c_misses,
output logic [31:0] c_flooded_copies
);
always_comb begin
addressing_bce_per_port = is_ethernet ? 32'(mac_table_bce_per_port())
: 32'(decode_bce_per_port());
addressing_dp_milli = 32'(datapaths_milli(addressing_bce_per_port));
// THE difference. A name can be absent from a table; a position
// cannot be absent from a space.
lookup_can_miss = is_ethernet;
// Chapter 12.4 Section 6: a miss replicates to every other port in
// the VLAN. A decode miss produces exactly one completion.
copies_on_miss = is_ethernet ? (ports - 16'd1) : 16'd1;
// And the one that decides whether a chapter like 21.6 has to exist.
miss_is_reported = !is_ethernet;
miss_occurred = is_ethernet ? !lookup_hit : !decode_hit;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_misses <= '0; c_flooded_copies <= '0;
end else if (miss_occurred) begin
c_misses <= c_misses + 32'd1;
c_flooded_copies <= c_flooded_copies + 32'(copies_on_miss);
end
end
endmoduleClassification: an area model with one boolean that is the chapter's thesis.
What it teaches: that lookup_can_miss is a one-bit difference with a two-order-of-magnitude consequence. The area comparison — 196 608 BCE against 7 680, a factor of 25.6 — is the part that is easy to quote and the less interesting half. copies_on_miss is the other half: an Ethernet miss on a 64-port switch produces 63 copies, a PCIe miss produces one completion, and the ratio there is 63.
And it teaches why miss_is_reported is the field that decides how a fabric is debugged. Chapter 21.6 exists because an Ethernet loss has no reporter; that chapter's entire apparatus — the selectivity index, the drop-shape classifier, the clustering probe — is an attempt to infer a cause from a count. A fabric whose every failure names itself needs none of it, and a PCIe engineer reading Module 21 would find it strange in a way that is worth noticing.
Deliberately simplified: copies_on_miss assumes the whole port set is one VLAN, where Chapter 13.3 makes the flood domain the VLAN's member set. The decode path assumes three windows, where a real switch port also has bus-number ranges for routing configuration transactions downstream. And addressing_bce_per_port divides the MAC table by the port count, which is the right way to compare a shared resource against a per-port one and hides that the table is shared — so a single port can consume all of it, which the per-port figure cannot express.
Production implication: the per-port division is where a real capacity argument goes wrong. A 128k-entry table on a 64-port switch is 2 048 entries per port only if the traffic is uniform, and Chapter 12.5 §7 showed it never is: one port facing a server farm can hold tens of thousands of addresses while its neighbours hold four. The table then thrashes for everyone, and the symptom is flooding on ports whose own address count is tiny. A PCIe switch cannot have this failure at all, because a port's decode windows are its own and no other port can consume them — which is the same trade as before, stated as a capacity property instead of a latency one.
4. The Return Path, and the State It Forces
Ethernet's transmit path holds nothing after the last octet leaves. PCIe's holds every outstanding read until its data comes back, and at 400 Gb/s that is 425 088 bits.
Start with what each protocol calls completion.
| Ethernet | PCIe, posted write | PCIe, non-posted read | |
|---|---|---|---|
| the transfer is done when | the last octet is on the wire | the packet is handed to the link layer | the completion returns |
| who decides | this port | this port | the far end |
| state held afterwards | none | none | the whole request |
| a failure is | invisible | reported at a third party | a timeout at the requester |
Column four is the one that costs, and Little's law sizes it.
bytes in flight = rate × round trip
At 400 Gb/s — 50 gigabytes per second — against three round trips.
| Round trip | Bytes in flight | Completions at 256 B |
|---|---|---|
| 0.5 µs | 25 000 | 98 |
| 1.0 µs | 50 000 | 196 |
| 2.0 µs | 100 000 | 391 |
Take the middle row, which is the same microsecond Chapter 23.4 §7 used, and price the state it forces.
| Structure | Size | BCE | × the datapath |
|---|---|---|---|
| tag table, 196 × 128 b | 25 088 bits | 25 088 | 0.089 |
| completion reorder buffer | 50 000 B = 400 000 bits | 400 000 | 1.412 |
| total return-path state | — | 425 088 | 1.500 |
A 400 Gb/s load/store link must hold one and a half MAC receive datapaths of state simply to have asked questions it has not yet had answered.
And the comparison the direction of this module demands.
| Structure | BCE | × the datapath | |
|---|---|---|---|
| Chapter 23.4 §7 | 149 descriptors × 16 B | 19 072 | 0.067 |
| this section | 196 completions + reorder | 425 088 | 1.500 |
| an Ethernet MAC, after transmission | nothing | 0 | 0 |
The middle row is 22.3 times the top one, and the top one is the number that chapter called large.
The reorder buffer is 94.1% of the return-path cost and it is worth saying why it cannot be avoided.
PCIe completions for different requests may return out of order. A requester that issued reads A, B and C may receive B's data first, and it must place B's data where B's requester wanted it — which means either holding the data until the gaps fill, or having somewhere to put each completion the moment it arrives.
| Strategy | Buffer needed | What it costs elsewhere |
|---|---|---|
| hold until in order | the full bandwidth-delay product | nothing |
| scatter on arrival | almost none | the consumer must tolerate out-of-order arrival |
| one outstanding at a time | one completion | Chapter 21.8 §5's ceiling — 2.0 Gb/s at 1 µs |
Row three is the honest bound and it is why nobody does it. One 256-byte completion per microsecond is 2.048 Gb/s, which is 0.5% of a 400 Gb/s link.
5. RTL 2 — The Outstanding-Read Model
// ---------------------------------------------------------------------
// outstanding_reads -- the tag table and the reorder buffer a return
// path forces, sized by Little's law and reported in BCE.
//
// The important outputs are ethernet_equivalent_bce, which is zero and
// stays zero, and reorder_share_pct, which says where the cost is.
// ---------------------------------------------------------------------
module outstanding_reads
import pciecmp_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [15:0] rate_gbps,
input logic [15:0] rtt_ns,
input logic [15:0] tag_limit, // what the design implements
input logic read_issued,
input logic completion_rx,
output logic [15:0] tags_required,
output logic [31:0] bytes_inflight,
output logic [31:0] tag_table_bce,
output logic [31:0] reorder_bce,
output logic [31:0] return_path_bce_o,
output logic [15:0] reorder_share_pct,
output logic [31:0] return_path_dp_milli,
output logic [31:0] ethernet_equivalent_bce,
output logic tag_limit_binds,
output logic [15:0] achievable_gbps,
output logic [15:0] outstanding_now,
output logic [31:0] c_tag_exhausted
);
always_comb begin
bytes_inflight = 32'(bytes_in_flight(int'(rate_gbps), int'(rtt_ns)));
tags_required = 16'(tags_needed(int'(rate_gbps), int'(rtt_ns)));
tag_table_bce = 32'(tags_required) * 32'(TAG_ENTRY_BITS);
reorder_bce = bytes_inflight * 32'd8;
return_path_bce_o = tag_table_bce + reorder_bce;
reorder_share_pct = (return_path_bce_o == 0) ? 16'd0
: 16'((reorder_bce * 100) / return_path_bce_o);
return_path_dp_milli = 32'(datapaths_milli(return_path_bce_o));
// The comparison, and it is a constant. An Ethernet transmitter
// holds nothing about a frame it has sent.
ethernet_equivalent_bce = 32'd0;
// Chapter 21.8 Section 5's ceiling, at this boundary.
tag_limit_binds = (tag_limit < tags_required);
achievable_gbps = (rtt_ns == 0) ? rate_gbps
: 16'(((32'(tag_limit) * 32'(MAX_PAYLOAD_B) * 8) * 1000)
/ (32'(rtt_ns) * 1000));
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
outstanding_now <= '0; c_tag_exhausted <= '0;
end else begin
case ({read_issued, completion_rx})
2'b10: outstanding_now <= outstanding_now + 16'd1;
2'b01: if (outstanding_now != 0)
outstanding_now <= outstanding_now - 16'd1;
default: ;
endcase
if (read_issued && (outstanding_now >= tag_limit))
c_tag_exhausted <= c_tag_exhausted + 32'd1;
end
end
endmoduleClassification: Little's law at a boundary where the design owns neither end, producing an area rather than a rate.
What it teaches: that ethernet_equivalent_bce is a hard-wired zero and that is the comparison. Every other output in the module is a number a PCIe designer must compute and an Ethernet designer never encounters. At 400 Gb/s and 1 µs the module reports 425 088 BCE — 1.500 datapaths — against zero.
And it teaches that reorder_share_pct is 94%, so a conversation about tag counts is a conversation about 6% of the cost. Doubling the tag limit from 196 to 392 adds 25 088 BCE; halving the round trip removes 200 000. The lever that matters is latency and it belongs to somebody else.
Deliberately simplified: achievable_gbps assumes every completion carries a full MAX_PAYLOAD_B, where a read that crosses a 4 kB boundary is split and the pieces are smaller. The reorder buffer is sized at the full bandwidth-delay product, which assumes the worst reordering; a design whose consumer tolerates out-of-order data needs a fraction of it. And outstanding_now counts reads without distinguishing their sizes, where a real tracker weights by payload because one 4 kB read and sixteen 256-byte reads consume the same tag count and sixteen times the buffer.
Production implication: the tag limit is the parameter that gets set from a previous generation and it is the one Chapter 21.8 §5's rejected property was written about. A part with 64 tags at 400 Gb/s and 1 µs achieves 64 × 256 × 8 ÷ 1 000 = 131 Gb/s — 32.8% of its link — and every assertion in it passes, because the design is doing exactly what it was built to do as fast as it is allowed to. tag_limit_binds is the one bit that says the shortfall is structural, and a design that reports it turns a three-week performance investigation into a one-line answer.
6. Credits: Gating a Transmitter on a Number It Does Not Own
An Ethernet transmitter with a frame ready and a link up transmits. A PCIe transmitter with a packet ready and a link up transmits only if a counter the far end last updated says it may.
That is a difference in liveness and it is worth writing both properties down to see it.
// Ethernet. This is true, provable locally, and nothing can make it false.
p_eth_progress: assert property (@(posedge clk) disable iff (!rst_n)
(frame_ready && link_up && !paused) |-> ##[1:IFG_CYCLES] tx_start);
// PCIe. The same shape, and the antecedent now contains a term whose
// value the far end decides.
p_pcie_progress: assert property (@(posedge clk) disable iff (!rst_n)
(tlp_ready && link_up && credits_available) |-> ##[1:ARB_CYCLES] tx_start);!paused in the first and credits_available in the second look like the same term and are not. Chapter 14.2's PAUSE is exceptional: it is asserted during congestion, it has a timer, and its default state is not-paused. Credits are continuous: the transmitter is gated on every packet, always, and the default state at link-up is zero credits — nothing may be sent until the partner advertises.
| Ethernet PAUSE | PCIe credits | |
|---|---|---|
| state at link-up | transmit permitted | transmit forbidden |
| granularity | the whole link, or a priority | per type, per virtual channel |
| the gate is checked | when a PAUSE is in force | before every packet |
| if the mechanism fails silent | the link runs | the link stops |
Row four is the one that decides how each is debugged. A broken PAUSE receiver runs at full rate and drops under congestion — a performance bug. A broken credit return stops the link entirely and is found in the first second of bring-up. The lossy protocol's flow-control bug is a field escape; the lossless protocol's is a bring-up failure.
Price the credit state, derived.
per virtual channel, per direction:
3 types (posted, non-posted, completion)
x (8-bit header credit + 12-bit data credit)
x 3 registers (limit, consumed, allocated)
= 3 x 20 x 3 = 180 flip-flops| Flops | BCE | × the datapath | |
|---|---|---|---|
| one VC, one direction | 180 | 3 600 | 0.013 |
| 8 VCs, both directions, per port | 2 880 | 57 600 | 0.203 |
| 64 ports | 184 320 | 3 686 400 | 13.01 |
Thirteen MAC datapaths of pure bookkeeping, and it does not move a single bit of data.
7. RTL 3 — The Credit Accountant
// ---------------------------------------------------------------------
// credit_accountant -- the three-register credit loop, per type, and
// the buffer obligation the advertisement creates.
//
// The counters are exact and small. The output that matters is
// buffer_obligation_bce, which is the promise the counters imply.
// ---------------------------------------------------------------------
module credit_accountant
import pciecmp_pkg::*;
#(
parameter int unsigned VCS = 8
)(
input logic clk,
input logic rst_n,
input logic [15:0] rate_gbps,
input logic [15:0] rtt_ns,
input logic per_vc_guaranteed, // each VC gets its own space
input logic tlp_sent,
input logic [1:0] tlp_type, // xact_e
input logic [11:0] tlp_data_credits,
input logic credit_return,
input logic [1:0] ret_type,
input logic [11:0] ret_data_credits,
output logic [31:0] credit_flops_o,
output logic [31:0] credit_bce_o,
output logic [31:0] buffer_obligation_bce,
output logic [15:0] obligation_ratio, // buffer / counters
output logic may_transmit,
output logic stalled_on_credit,
output logic [31:0] c_credit_stall_cycles
);
// limit, consumed, allocated -- per type, header and data.
logic [7:0] hdr_limit [3];
logic [7:0] hdr_consumed [3];
logic [11:0] dat_limit [3];
logic [11:0] dat_consumed [3];
always_comb begin
credit_flops_o = 32'(credit_flops_per_port());
credit_bce_o = 32'(credit_bce_per_port());
// The obligation: a round trip of buffer, times the VCs that need
// their own guaranteed share.
buffer_obligation_bce =
32'(bytes_in_flight(int'(rate_gbps), int'(rtt_ns))) * 32'd8
* (per_vc_guaranteed ? 32'(VCS) : 32'd1);
obligation_ratio = (credit_bce_o == 0) ? 16'd0
: 16'(buffer_obligation_bce / credit_bce_o);
// The gate. Note that it is checked on EVERY packet, and that at
// reset every limit is zero -- so the default is "do not transmit".
may_transmit = (hdr_consumed[tlp_type] < hdr_limit[tlp_type])
&& ({4'd0, dat_consumed[tlp_type]} + {4'd0, tlp_data_credits}
<= {4'd0, dat_limit[tlp_type]});
stalled_on_credit = tlp_sent && !may_transmit;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < 3; i++) begin
hdr_limit[i] <= '0; // zero at reset: nothing may be sent
hdr_consumed[i] <= '0;
dat_limit[i] <= '0;
dat_consumed[i] <= '0;
end
c_credit_stall_cycles <= '0;
end else begin
if (tlp_sent && may_transmit) begin
hdr_consumed[tlp_type] <= hdr_consumed[tlp_type] + 8'd1;
dat_consumed[tlp_type] <= dat_consumed[tlp_type] + tlp_data_credits;
end
if (credit_return) begin
hdr_limit[ret_type] <= hdr_limit[ret_type] + 8'd1;
dat_limit[ret_type] <= dat_limit[ret_type] + ret_data_credits;
end
if (stalled_on_credit)
c_credit_stall_cycles <= c_credit_stall_cycles + 32'd1;
end
end
endmoduleClassification: six counters and one comparison, wrapped around a promise that costs 6.9 times what the counters do.
What it teaches: that obligation_ratio is the number nobody computes. The module reports 57 600 BCE of counters against 400 000 BCE of buffer at 400 Gb/s and 1 µs — 6.9× — and with eight guaranteed virtual channels, 3 200 000 BCE against 57 600, a ratio of 55.6. A design review that examines the credit logic has examined 1.8% of what the mechanism costs.
And it teaches that the reset state is hdr_limit = 0, which means the link comes up unable to transmit. That is not a quirk; it is the only safe initial condition, because a credit is a claim about a buffer and no buffer is known to exist until the partner says so. Chapter 11.3 established that Ethernet's bring-up ends with a link that may immediately carry traffic; PCIe's ends with a link that may carry nothing until a DLLP has crossed it in each direction.
Deliberately simplified: the credit counters are modelled as saturating adders where the real mechanism uses modular arithmetic with a wrap comparison, so that a returned credit count can be compared against a consumed count across a wrap without either being reset. may_transmit checks one virtual channel where a real arbiter considers all of them and picks among those that may go. And the three transaction types are treated symmetrically, where completions have a special rule: a completer must never be unable to return a completion, so completion credits are conventionally advertised as infinite.
Production implication: c_credit_stall_cycles separates two failures that look identical from outside. A link running at half rate because the partner returns credits slowly and a link running at half rate because the partner's buffer is genuinely small produce the same throughput and the same everything else. The stall counter distinguishes them only in combination with the advertised limit: stalls with a high limit means slow return; stalls with a low limit means a small buffer. A design that reports both turns the question into a one-glance answer, and a design that reports neither sends two teams to instrument a link that is behaving exactly as specified.
8. Ordering: What PCIe Enforces and Ethernet Does Not
Ethernet guarantees ordering within a flow on a path and nothing else. PCIe guarantees a set of relations between transaction types, always, and the guarantee is what makes a load/store programming model work.
State each precisely, because the loose version of either is wrong.
| Ethernet's guarantee | PCIe's guarantee | |
|---|---|---|
| what is ordered | frames taking the same path | transactions in the same traffic class |
| against what | each other | each other, by type |
| who may reorder | any switch choosing a different path — Chapter 15.2 | nobody, for the required relations |
| how a design relies on it | it must not | producer-consumer correctness depends on it |
Row four is the consequence and it is why a bridge between them is hard.
The producer-consumer pattern, which is the reason the ordering rules exist:
producer: write data to memory (posted)
write a flag to memory (posted)
consumer: read the flag (non-posted)
if set, read the data (non-posted)This is correct on PCIe and only on PCIe. The rule that makes it work is that a posted write may not pass an earlier posted write in the same traffic class — so the data write reaches memory before the flag write does, and a consumer that sees the flag is guaranteed to see the data.
The same pattern over Ethernet is a race, and the reason is Chapter 15.2: two frames from the same source to the same destination may hash to different members of a link aggregation group and arrive in either order. A protocol above Ethernet that needs ordering must implement sequence numbers, which is what TCP does and what Chapter 24.2 will find InfiniBand's transport doing too.
Price the enforcement.
The naive structure is a pairwise comparison: each outstanding transaction against every other, to decide whether it may pass. At Section 4's 196 outstanding:
196 x 195 / 2 = 19 110 comparisonsNobody builds that, and the reason is worth understanding because the alternative is a general technique. The ordering rules are per type, not per transaction, so a counter per type is sufficient: a transaction may proceed if no earlier transaction of a blocking type is still pending.
| Structure | Flops | BCE |
|---|---|---|
| pairwise matrix, 196 outstanding | 19 110 comparators | prohibitive |
| three type counters, 8 bits each | 24 | 480 |
| ratio | — | the comparison collapses |
The ordering rules cost 480 BCE because they are stated over types rather than over transactions, and a specification written the other way would have been unimplementable.
9. RTL 4 — The Ordering Scoreboard
// ---------------------------------------------------------------------
// ordering_scoreboard -- PCIe's ordering rules, enforced with a counter
// per transaction type instead of a comparison per transaction pair.
//
// The module also reports what the naive structure would have cost, so
// the specification's shape is visible as a number.
// ---------------------------------------------------------------------
module ordering_scoreboard
import pciecmp_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [15:0] outstanding_max,
input logic xact_issue,
input logic [1:0] xact_kind, // xact_e
input logic xact_retire,
input logic [1:0] retire_kind,
output logic may_proceed,
output logic blocked_by_posted,
output logic [15:0] pending_posted,
output logic [15:0] pending_nonposted,
output logic [15:0] pending_completion,
output logic [31:0] pairwise_comparisons,
output logic [31:0] counter_flops,
output logic [31:0] counter_bce,
output logic ethernet_orders_this,
output logic [31:0] c_order_blocks
);
always_comb begin
// What the rules cost, stated over types.
counter_flops = 32'd3 * 32'd8;
counter_bce = counter_flops * 32'(BCE_PER_FLOP);
// What they would have cost, stated over transactions.
pairwise_comparisons = (32'(outstanding_max)
* (32'(outstanding_max) - 32'd1)) / 32'd2;
// The rule that makes producer-consumer work: a posted write may
// not pass an earlier posted write, and a completion may not pass
// an earlier posted write.
blocked_by_posted = (pending_posted != 16'd0)
&& ((xact_kind == 2'(XACT_COMPLETION))
|| (xact_kind == 2'(XACT_POSTED)));
may_proceed = xact_issue && !blocked_by_posted;
// And the comparison this chapter exists to make.
ethernet_orders_this = 1'b0;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pending_posted <= '0;
pending_nonposted <= '0;
pending_completion <= '0;
c_order_blocks <= '0;
end else begin
if (xact_issue && may_proceed) begin
case (xact_kind)
2'(XACT_POSTED): pending_posted <= pending_posted + 16'd1;
2'(XACT_NONPOSTED): pending_nonposted <= pending_nonposted + 16'd1;
default: pending_completion <= pending_completion + 16'd1;
endcase
end
if (xact_retire) begin
case (retire_kind)
2'(XACT_POSTED):
if (pending_posted != 0) pending_posted <= pending_posted - 16'd1;
2'(XACT_NONPOSTED):
if (pending_nonposted != 0) pending_nonposted <= pending_nonposted - 16'd1;
default:
if (pending_completion != 0) pending_completion <= pending_completion - 16'd1;
endcase
end
if (xact_issue && blocked_by_posted)
c_order_blocks <= c_order_blocks + 32'd1;
end
end
endmoduleClassification: three counters standing in for a quadratic relation, and a constant-zero output that names the other protocol.
What it teaches: that pairwise_comparisons and counter_flops are both reported so the specification's shape is a measured quantity. At 196 outstanding the module reports 19 110 against 24, and the reason the second number is achievable is entirely that the rules are written over three classes instead of over 196 objects.
And it teaches that ethernet_orders_this is zero and that this is not a deficiency. Ethernet does not order across paths because Chapter 15.2 lets a flow take several, and that choice bought the bisection bandwidth Chapter 23.1 is built on. A fabric that ordered strictly could not spread a flow across 32 paths at all, and Chapter 23.2 §12's striping — the remedy that took a collective from 28.4% to 77.1% — is only legal because nothing downstream requires order.
Deliberately simplified: the module enforces two of PCIe's ordering relations and the full table has more, including the rules about I/O and configuration transactions and the relaxed-ordering and ID-based-ordering attribute bits that let software opt out. blocked_by_posted blocks on any pending posted write, where the real rule is scoped to the same traffic class. And pending_* are flat counters where a real implementation must also respect the ordering rules within a completer, which is a second scoreboard at the other end.
Production implication: the relaxed-ordering bit is the field that turns this module's guarantee off, and it is set by software on a per-transaction basis for exactly the traffic where ordering is not needed — bulk data whose flag is written separately. A design that ignores the bit is correct and slow; a design that honours it is correct and fast; a design that honours it incorrectly produces the worst bug class in this chapter, because the failure is a consumer reading a flag and finding stale data, which is silent, intermittent and reproduces on one machine in fifty. c_order_blocks counting zero on a workload that should be ordered is the signal that the bit is being honoured where it should not be, and it costs one counter.
10. The Bridge, and What Crosses It
A network interface card is a bridge between these two protocols, and every NIC in Chapter 23.4 is one. Building the bridge makes the differences into signals.
The bridge's job, stated as a mapping.
| Ethernet side | PCIe side | What the bridge must supply |
|---|---|---|
| a received frame | a posted write to host memory | an address, from a descriptor |
| a descriptor | a non-posted read, then a completion | a tag, and somewhere to put the answer |
| a transmitted frame | a posted read of the payload | the same, per fragment |
| an interrupt | a posted write to a message address | nothing — it is fire and forget |
| a frame drop | nothing | a counter, because there is no other reporter |
Row five is the interesting one and Section 12 is about it.
Four quantities have to be reconciled at the boundary and none of them match.
| Ethernet | PCIe | The bridge's problem | |
|---|---|---|---|
| unit of transfer | a frame, 64 to 9 018 octets | a TLP, up to MAX_PAYLOAD_B | one frame is many TLPs |
| address | a name, resolved by a table | a position, supplied by software | the descriptor is the translation |
| ordering | none across paths | by type | the bridge must impose what it needs |
| completion | local, at transmission | remote, on return | two different "done" events |
Row four is the one that does not reconcile, and the rest of this chapter follows from it.
Work the numbers for one received frame at 400 Gb/s.
| Value | |
|---|---|
| a 1 518-octet frame | 1 518 octets of payload to write |
| at 256 octets per TLP | 6 posted writes |
| plus the descriptor write-back | 1 posted write |
| plus the descriptor fetch, amortised | 1 non-posted read per descriptor |
| TLPs per frame | 8 |
| at 32.51 Mpps | 260.1 million TLPs per second |
And for a minimum frame the arithmetic inverts.
| 64-octet frame | 1 518-octet frame | 9 018-octet frame | |
|---|---|---|---|
| payload TLPs | 1 | 6 | 36 |
| overhead TLPs | 2 | 2 | 2 |
| overhead share | 66.7% | 25.0% | 5.3% |
The bridge's own overhead is two transactions per frame regardless of the frame's size, which is Chapter 8.3's hyperbola again, in a different protocol, with a different constant.
11. RTL 5 — The Ethernet-to-PCIe Bridge
// ---------------------------------------------------------------------
// eth_pcie_bridge -- one received frame becomes a sequence of posted
// writes and one descriptor write-back, and the module reports what
// the conversion cost.
//
// The output that matters is frame_complete_ethernet against
// frame_complete_pcie: the two protocols disagree about when this
// frame is done, and Section 12 is about the disagreement.
// ---------------------------------------------------------------------
module eth_pcie_bridge
import pciecmp_pkg::*;
(
input logic clk,
input logic rst_n,
input logic frame_start,
input logic frame_end,
input logic [15:0] frame_octets,
input logic [63:0] descriptor_addr,
input logic descriptor_valid,
input logic tlp_accepted, // the PCIe side took one
input logic writeback_acked, // the descriptor write landed
output logic tlp_request,
output logic [1:0] tlp_kind,
output logic [63:0] tlp_addr,
output logic [15:0] tlp_octets,
output logic [15:0] tlps_for_frame,
output logic [15:0] payload_tlps,
output logic [15:0] overhead_tlps,
output logic [15:0] overhead_share_pct,
output logic frame_complete_ethernet,
output logic frame_complete_pcie,
output logic completions_disagree,
output logic [31:0] c_frames_bridged,
output logic [31:0] c_tlps_issued
);
logic [15:0] tlps_sent;
logic [63:0] cursor;
logic in_frame;
always_comb begin
payload_tlps = (frame_octets + 16'(MAX_PAYLOAD_B) - 16'd1)
/ 16'(MAX_PAYLOAD_B);
overhead_tlps = 16'd2; // fetch + write-back
tlps_for_frame = payload_tlps + overhead_tlps;
overhead_share_pct = (tlps_for_frame == 0) ? 16'd0
: (overhead_tlps * 16'd100) / tlps_for_frame;
tlp_request = in_frame && descriptor_valid
&& (tlps_sent < payload_tlps);
tlp_kind = 2'(XACT_POSTED);
tlp_addr = cursor;
tlp_octets = ((frame_octets - (tlps_sent * 16'(MAX_PAYLOAD_B)))
> 16'(MAX_PAYLOAD_B))
? 16'(MAX_PAYLOAD_B)
: (frame_octets - (tlps_sent * 16'(MAX_PAYLOAD_B)));
// THE two events. On Ethernet the frame is done when its last
// octet arrived. On PCIe it is done when the far end says so.
frame_complete_ethernet = frame_end;
frame_complete_pcie = writeback_acked;
// They are never the same cycle, and the gap is a host round trip.
completions_disagree = frame_complete_ethernet ^ frame_complete_pcie;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tlps_sent <= '0; cursor <= '0; in_frame <= 1'b0;
c_frames_bridged <= '0; c_tlps_issued <= '0;
end else begin
if (frame_start) begin
in_frame <= 1'b1;
tlps_sent <= '0;
cursor <= descriptor_addr;
end
if (tlp_accepted) begin
tlps_sent <= tlps_sent + 16'd1;
cursor <= cursor + 64'(MAX_PAYLOAD_B);
c_tlps_issued <= c_tlps_issued + 32'd1;
end
if (writeback_acked) begin
in_frame <= 1'b0;
c_frames_bridged <= c_frames_bridged + 32'd1;
end
end
end
endmoduleClassification: a fragmenter with an address cursor, whose real output is a pair of complete signals that never coincide.
What it teaches: that completions_disagree is asserted for the whole interval between the two protocols' notions of done, and that interval is a host round trip — 1 µs, which at 32.51 Mpps is 32 frames. A design that reports "frame received" on the Ethernet event and a design that reports it on the PCIe event disagree by thirty-two frames at all times, and both are correct.
And it teaches where the descriptor sits in the addressing argument. Chapter 23.4 §5 priced descriptors as a bandwidth overhead — 38.1% of payload at minimum frame size. Section 2 of this chapter says what they are: the descriptor is the translation from a name to a position. Ethernet delivered a frame to a name; the host wants it at a position; the descriptor is the only place those two are related, and it was written by software before the frame existed.
Deliberately simplified: the bridge issues payload TLPs strictly in address order where a real design interleaves several frames' writes to keep the link busy. overhead_tlps is a constant two, where batching amortises the descriptor fetch across many frames — Chapter 23.4 §6 showed a batch of 8 takes TLP efficiency from 40.0% to 72.7% without moving the descriptor bandwidth at all. And cursor advances linearly, which assumes the descriptor points at a contiguous buffer; scatter-gather descriptors point at a list, and the bridge must walk it.
Production implication: the bridge is where a frame drop becomes invisible, and it is worth being explicit about the mechanism. If descriptor_valid is low when a frame arrives, the frame cannot be written anywhere and is discarded — this is Chapter 23.4 §7's prefetch underrun. On the Ethernet side there is no signal for it: the frame was received correctly, its FCS was good, and it is gone. On the PCIe side nothing happened at all, because no transaction was ever issued. So the event is visible only in a counter that the bridge chooses to keep — which is why c_frames_bridged must be compared against the MAC's receive counter rather than trusted alone, and a design that reports only one of them has made a class of loss unobservable.
12. Which Ethernet Property Becomes Unstateable
Section 11 built the bridge. This section takes one Ethernet property across it and finds that it cannot be written on the other side — not that it is false, that there is no way to say it.
The property is Ethernet's most basic liveness claim and every MAC in Module 19 carries some version of it.
// Ethernet. The transfer is complete when the last octet has left,
// and this port knows it without asking anyone.
p_tx_complete_is_local: assert property (@(posedge clk) disable iff (!rst_n)
last_octet_sent |-> ##1 frame_complete);Three things are true of it that are worth naming separately.
| On Ethernet | |
|---|---|
| every term is a local signal | last_octet_sent and frame_complete are both in this MAC |
| the guarantee is about this port | nothing about the far end is claimed |
| it is checkable in the field | a hardware monitor can evaluate it forever |
Now carry it across the bridge. Every term has a counterpart.
| Ethernet term | PCIe counterpart | Well-defined? |
|---|---|---|
last_octet_sent | tlp_handed_to_link_layer | yes |
frame_complete | transaction_complete | yes |
So the translated property is well-formed:
// PCIe. Every term translated, the property compiles, and it asserts
// something the original never said.
p_xact_complete_is_local: assert property (@(posedge clk) disable iff (!rst_n)
tlp_handed_to_link_layer |-> ##1 transaction_complete);It is true for a posted write and false for a non-posted read, and that is not the interesting part. The interesting part is that transaction_complete for a read is not a fact about this port at all. It is a fact about a relation between this port and a completer that may be four switch hops away, and the local signal that carries it is a report of something that arrived, not an observation of something that happened here.
The Ethernet property says I am done. The translated property says somebody told me I am done. Both are written as a local implication and only one of them is one.
State the general form, because it is this batch's first rejected class and Section 20 develops it.
| Before the port | After the port | |
|---|---|---|
| what the property asserts | a local fact | a relation between two endpoints |
| where it is evaluated | at the port that owns the fact | at one of the two endpoints |
| does it pass? | yes, correctly | yes, and it means something else |
| who introduced the error | nobody — it was right | the translation |
Four Ethernet properties and what happens to each at the boundary.
| Ethernet property | Fate on PCIe |
|---|---|
p_tx_complete_is_local | unstateable — completion is remote for reads |
p_progress_when_ready | false — the transmitter is credit-gated |
p_frame_dropped_silently | unstateable — there is no silent drop |
p_fcs_covers_the_frame | translates cleanly — LCRC is the counterpart |
Row four is the one that works and it is worth saying why. The FCS property is about the contents of a thing in one place at one time, and both protocols have such a thing. The three that fail are all about an event whose owner differs between the protocols.
13. RTL 6 — The Completion Timeout
// ---------------------------------------------------------------------
// completion_timer -- the only liveness a requester can claim.
//
// Section 20 establishes that a remote completion is a report rather
// than an observation. The structural consequence is that every
// outstanding read needs a timer, and Ethernet needs none because
// nothing is outstanding.
// ---------------------------------------------------------------------
module completion_timer
import pciecmp_pkg::*;
#(
parameter int unsigned TAGS = 256,
parameter int unsigned TIMEOUT_US = 50
)(
input logic clk,
input logic rst_n,
input logic tick_us,
input logic read_issued,
input logic [7:0] issued_tag,
input logic completion_rx,
input logic [7:0] cpl_tag,
input logic [1:0] cpl_status,
output logic timeout_fired,
output logic [7:0] timeout_tag,
output logic [15:0] outstanding_now,
output logic [31:0] timer_flops,
output logic [31:0] timer_bce,
output logic [31:0] ethernet_timer_bce,
output logic [31:0] c_timeouts,
output logic [31:0] c_unexpected_cpl,
output logic [31:0] c_error_cpl
);
logic [15:0] age [TAGS];
logic live [TAGS];
always_comb begin
// One age counter per tag, and the counter must span the timeout.
timer_flops = 32'(TAGS) * 32'd16 + 32'(TAGS);
timer_bce = timer_flops * 32'(BCE_PER_FLOP);
// The comparison. An Ethernet transmitter has nothing to time.
ethernet_timer_bce = 32'd0;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < TAGS; i++) begin
age[i] <= '0; live[i] <= 1'b0;
end
timeout_fired <= 1'b0; timeout_tag <= '0; outstanding_now <= '0;
c_timeouts <= '0; c_unexpected_cpl <= '0; c_error_cpl <= '0;
end else begin
timeout_fired <= 1'b0;
if (read_issued) begin
live[issued_tag] <= 1'b1;
age[issued_tag] <= '0;
outstanding_now <= outstanding_now + 16'd1;
end
if (completion_rx) begin
// Property 4 of Section 20, as a counter: a completion for a
// tag nobody requested is evidence about the completer.
if (!live[cpl_tag]) c_unexpected_cpl <= c_unexpected_cpl + 32'd1;
else begin
live[cpl_tag] <= 1'b0;
outstanding_now <= outstanding_now - 16'd1;
end
if (cpl_status != 2'(CPL_SUCCESSFUL))
c_error_cpl <= c_error_cpl + 32'd1;
end
if (tick_us) begin
for (int i = 0; i < TAGS; i++) begin
if (live[i]) begin
if (age[i] >= 16'(TIMEOUT_US)) begin
live[i] <= 1'b0;
timeout_fired <= 1'b1;
timeout_tag <= 8'(i);
c_timeouts <= c_timeouts + 32'd1;
end else begin
age[i] <= age[i] + 16'd1;
end
end
end
end
end
end
endmoduleClassification: one timer per outstanding transaction, which is a structure with no Ethernet counterpart at all.
What it teaches: that ethernet_timer_bce is zero and timer_bce is 87 040 at 256 tags — 256 × 17 = 4 352 flops at 20 BCE each, 0.307 datapaths. It is a small number and it is a structure that exists only because completion is remote, so it belongs in the same column as Section 4's reorder buffer rather than in a list of implementation details.
And it teaches that c_unexpected_cpl is the counter Section 20's property 4 turns into hardware. A completion for a tag that was never issued is either a completer misbehaving, a tag reused before its predecessor's completion returned, or a completion from a previous reset epoch arriving late — and all three are serious, all three are silent, and none of them produces a symptom that any other counter reports.
Deliberately simplified: the timer is a per-tag age counter ticking at one microsecond, where a real design uses a coarse timer wheel because 256 sixteen-bit counters that all increment on the same tick is a large amount of switching for very little information. TIMEOUT_US is one value where the specification defines four programmable ranges spanning 50 µs to 64 seconds, because a completer behind several switches and a retry-capable endpoint may legitimately take very long. And the loop over TAGS on every tick is written for clarity rather than for synthesis; the synthesisable form is a rotating pointer that ages one group per cycle.
Production implication: the timeout value is where a working system becomes an intermittently failing one, and the mechanism is worth stating because it catches teams out. A completion timeout is a fatal, unrecoverable error for most drivers — the transaction is abandoned and the device is usually reset. So a timeout set too short turns a slow completer into a device removal, and the symptom is a NIC that disappears from the system under load and reappears after a reset, with nothing in any log but the timeout itself. c_timeouts counting at all is the signal, and a design that reports the age distribution rather than just the count tells the operator whether the value is marginal or the completer is genuinely dead — which is the difference between a configuration change and a hardware replacement.
14. What a Bridge Must Never Do
Five prohibitions, and each is a bug this chapter's structures make possible.
| # | Never | Because |
|---|---|---|
| 1 | report a frame received before its write-back is acknowledged | Section 11: the two completions are a host round trip apart, and the earlier one is a claim about the wrong protocol |
| 2 | reuse a tag before its completion has returned or timed out | a late completion then lands on a live tag and the data goes to the wrong buffer, silently |
| 3 | advertise credits for buffer that is shared with another virtual channel | Section 6: a credit is a promise, and a promise backed by somebody else's space is a deadlock |
| 4 | drop a frame because a descriptor was unavailable without counting it | Section 11: neither protocol reports it, so an uncounted one is unobservable |
| 5 | assume a completion's data is correct because its status is Successful | status is the completer's claim about itself — Section 20's property 4 |
Row two is the one that produces the worst bug in this chapter and it is worth expanding.
A tag is reused legally the moment its completion returns. A tag is reused illegally the moment its completion times out, because a timeout does not mean the completion will not arrive — it means it has not arrived yet. A completer that was merely slow may return the data after the requester has reassigned the tag, and the requester will accept it, match it to the new request, and write somebody else's data into the new request's buffer.
| What happens | |
|---|---|
| the timeout fires | tag 17 is freed |
| a new read is issued | tag 17 is reallocated to a different address |
| the old completion arrives | it matches tag 17 and is accepted |
| the result | the new read's buffer holds the old read's data |
| what any assertion sees | a successful completion for a live tag |
The defence is an epoch bit or a generation counter on the tag, so that a completion carries evidence of which allocation it belongs to — and the mechanism is the same one Chapter 19.5 §7 used to distinguish a full FIFO from an empty one, which is that a pointer that wraps needs one more bit than the space it indexes.
15. RTL 7 — Bridge Telemetry
// ---------------------------------------------------------------------
// pcie_cmp_telemetry -- what a bridge must report for the two sides to
// be reconcilable after the fact.
//
// The design rule this module follows: every counter that exists on
// one side alone is useless, because the question at a bridge is
// always "where did the difference come from".
// ---------------------------------------------------------------------
module pcie_cmp_telemetry
import pciecmp_pkg::*;
(
input logic clk,
input logic rst_n,
input logic clear,
// the Ethernet side
input logic eth_frame_rx,
input logic eth_frame_dropped,
input logic [15:0] eth_frame_octets,
// the PCIe side
input logic tlp_issued,
input logic tlp_credit_stalled,
input logic cpl_received,
input logic cpl_timeout,
input logic cpl_unexpected,
// the bridge's own
input logic descriptor_starved,
output logic [47:0] c_eth_frames,
output logic [47:0] c_eth_dropped,
output logic [47:0] c_tlps,
output logic [47:0] c_completions,
output logic [47:0] c_timeouts_o,
output logic [47:0] c_unexpected_o,
output logic [47:0] c_desc_starved,
output logic [31:0] c_credit_stalls,
output logic [15:0] tlps_per_frame_x100,
output logic [15:0] loss_attributable_pct,
output logic sides_reconcile
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || clear) begin
c_eth_frames <= '0; c_eth_dropped <= '0; c_tlps <= '0;
c_completions <= '0; c_timeouts_o <= '0; c_unexpected_o <= '0;
c_desc_starved <= '0; c_credit_stalls <= '0;
end else begin
if (eth_frame_rx) c_eth_frames <= c_eth_frames + 48'd1;
if (eth_frame_dropped) c_eth_dropped <= c_eth_dropped + 48'd1;
if (tlp_issued) c_tlps <= c_tlps + 48'd1;
if (cpl_received) c_completions <= c_completions + 48'd1;
if (cpl_timeout) c_timeouts_o <= c_timeouts_o + 48'd1;
if (cpl_unexpected) c_unexpected_o <= c_unexpected_o + 48'd1;
if (descriptor_starved) c_desc_starved <= c_desc_starved + 48'd1;
if (tlp_credit_stalled) c_credit_stalls <= c_credit_stalls + 32'd1;
end
end
always_comb begin
// The ratio Section 10 derived, measured rather than assumed.
tlps_per_frame_x100 = (c_eth_frames == 0) ? 16'd0
: 16'((c_tlps * 48'd100) / c_eth_frames);
// Of the frames that were lost, how many can be attributed to a
// named cause? Anything below 100% is loss with no reporter.
loss_attributable_pct = (c_eth_dropped == 0) ? 16'd100
: 16'((c_desc_starved * 48'd100) / c_eth_dropped);
// The two sides agree only if every issued read got an answer.
sides_reconcile = (c_completions + c_timeouts_o) >= (c_tlps - c_eth_frames);
end
endmoduleClassification: a counter bank whose design rule is that no counter is meaningful alone.
What it teaches: that loss_attributable_pct is the field that separates the two protocols' debugging cultures. On the PCIe side every failure has a name — a timeout, an unexpected completion, an error status — so the attributable share is 100% by construction. On the Ethernet side it is whatever fraction of the drops the bridge happened to instrument, and Chapter 21.6 §13 established that the reason field does not exist in the frame, the standard or most implementations. A bridge that reports 60% attributable is reporting that 40% of its losses have no explanation anywhere in the system.
And it teaches that tlps_per_frame_x100 is Section 10's hyperbola as a measurement. The module reports 800 on a link carrying 1 518-octet frames and 300 on one carrying minimum frames, and the same part reports the same two numbers whether it is healthy or not — so the counter is a workload descriptor rather than a health indicator, and reading it as the latter is Chapter 23.4 §20's class 109 in a new place.
Deliberately simplified: sides_reconcile uses a subtraction that assumes exactly one descriptor read per frame, which Section 11's batching breaks. All counters are free-running 48-bit where a real part needs clear-on-read for some and sticky for others — Chapter 19.7 §6's argument about which semantics belong to which counter applies unchanged. And the module observes both sides from one clock domain, where a real bridge has the MAC's clock on one side and the PCIe core clock on the other — Chapter 18.1 §19's class 74 is exactly this hazard and the counters must be synchronised before they are compared.
Production implication: the counter that is always missing is c_desc_starved, and its absence is what makes Chapter 23.4 §7's prefetch underrun a three-week investigation. The symptom is receive drops at line rate with an idle host and a non-full ring, which looks like a fabric problem and is not — and the only structure that distinguishes the two is a counter in the bridge that nobody thought to specify, because the bridge is where two teams' ownership ends and neither one's test plan covers the gap.
16. RTL 8 — The Bridge Conformance Monitor
// ---------------------------------------------------------------------
// pcie_cmp_conformance -- the checks that only make sense at the
// boundary, gathered in one place.
//
// Every check here is about a RELATION between the two sides. None of
// them can be written on either side alone, which is the point.
// ---------------------------------------------------------------------
module pcie_cmp_conformance
import pciecmp_pkg::*;
#(
parameter int unsigned TAGS = 256
)(
input logic clk,
input logic rst_n,
input logic eth_frame_end,
input logic pcie_writeback_ack,
input logic read_issued,
input logic [7:0] issued_tag,
input logic tag_live_in,
input logic completion_rx,
input logic [7:0] cpl_tag,
input logic cpl_tag_live,
input logic [1:0] cpl_status,
input logic credits_available,
input logic tlp_transmitted,
input logic frame_dropped,
input logic drop_counted,
input logic [15:0] advertised_credits,
input logic [15:0] buffer_entries,
output logic v_premature_complete,
output logic v_tag_reuse,
output logic v_orphan_completion,
output logic v_credit_overrun,
output logic v_overadvertised,
output logic v_uncounted_drop,
output logic [5:0] violations,
output logic conformant
);
always_comb begin
// 1. Section 14 prohibition 1 -- the Ethernet completion is not
// the PCIe one, so it must not be reported as the transfer.
v_premature_complete = eth_frame_end && !pcie_writeback_ack;
// 2. Prohibition 2 -- a tag must be free before it is issued.
v_tag_reuse = read_issued && tag_live_in;
// 3. Section 20 property 4 -- a completion for a tag nobody holds.
v_orphan_completion = completion_rx && !cpl_tag_live;
// 4. Section 6 -- the gate is checked on every packet.
v_credit_overrun = tlp_transmitted && !credits_available;
// 5. Prohibition 3 -- credits may not exceed the buffer behind them.
v_overadvertised = (advertised_credits > buffer_entries);
// 6. Prohibition 4 -- a drop with no reporter anywhere.
v_uncounted_drop = frame_dropped && !drop_counted;
violations = { v_uncounted_drop, v_overadvertised, v_credit_overrun,
v_orphan_completion, v_tag_reuse, v_premature_complete };
conformant = (violations == 6'b000000);
end
// v_premature_complete is a WARNING rather than an error: the two
// events are legitimately a host round trip apart. It fires only if
// something downstream treats the Ethernet event as the transfer.
endmoduleClassification: six checks, none of which can be written on either side alone.
What it teaches: that a conformance monitor at a boundary is a different object from one inside a block. Chapter 19.1 §16's monitor checks a MAC against its own specification; this one checks two specifications against each other, and every one of its six signals mentions terms from both sides. A monitor that could be partitioned onto one side would not be a bridge monitor.
And it teaches why v_premature_complete is documented as a warning in the code rather than treated as a violation. The two completion events are a host round trip apart and that is correct — the violation is not the gap, it is a consumer that treats the earlier event as the transfer. The monitor cannot see that consumer, so it reports the condition and leaves the judgement to whoever integrated it, which is the honest thing for a check whose correctness depends on something outside its scope.
Deliberately simplified: the tag-liveness inputs are supplied from outside rather than tracked here, so the monitor trusts a structure it is partly checking — a real deployment binds it to completion_timer's live array directly. v_overadvertised compares two numbers that are set once at configuration, so it is an elaboration check masquerading as a runtime one. And there is no check for prohibition 5 — that a Successful status does not imply correct data — because no monitor at this boundary can write it: verifying the data requires knowing what the completer should have returned, which is information the bridge does not have and Section 20's class 112 is exactly about pretending otherwise.
Production implication: v_orphan_completion is the check worth putting in silicon rather than in simulation, and the reason is the tag-reuse bug in Section 14. An orphan completion in the field is evidence that a timeout fired and the completer answered anyway, which is a system-integration fact that no amount of pre-silicon verification will produce, because pre-silicon completers are models that answer or do not. One sticky bit and a captured tag is the whole cost, and it converts the worst class of silent data corruption in this chapter into a named event with a timestamp.
17. The Two Fabrics, Priced Side by Side
Everything this chapter derived, in one table, in one unit.
| Mechanism | Ethernet, BCE | PCIe, BCE | Ratio |
|---|---|---|---|
| addressing, per port | 196 608 | 7 680 | PCIe 25.6× cheaper |
| state after a transfer is issued | 0 | 425 088 | Ethernet holds nothing |
| flow-control counters, per port | ~600 — a PAUSE timer and a watermark | 57 600 | PCIe 96× more |
| flow-control buffer obligation, per port | 7.67 KiB at 100 Gb/s — Chapter 14.2 §9 | 50 000 B at 400 Gb/s | both are bandwidth-delay products |
| ordering enforcement | 0 | 480 | Ethernet enforces nothing |
| per-transaction timers | 0 | 87 040 at 256 tags | Ethernet times nothing |
Four of the six rows are zero on the Ethernet side and that is the chapter's summary.
A load/store fabric's cost is the state it holds about things it has not yet finished. A packet network holds nothing, and its cost is that nobody can tell it whether anything finished.
Restate that as the design question each answers.
| Ethernet answers | PCIe answers | |
|---|---|---|
| the question | how do I deliver to a station whose location I do not know? | how do I read a location and get the value back? |
| what it must have | a learning table and a flood path | tags, ordering, credits and timers |
| what it gives up | any notion of completion | any tolerance for an unknown address |
And the cost of each side's answer, totalled at a 64-port switch.
| BCE | × the datapath | |
|---|---|---|
| Chapter 23.3's Ethernet switch, total | 5.62 × 10⁸ | 1 985 |
| its forwarding tables — the naming cost | 5.04 × 10⁷ | 178 |
| a PCIe switch's decode, 64 ports | 491 520 | 1.73 |
| a PCIe switch's credit counters, 64 ports | 3 686 400 | 13.01 |
Row two against row three is 102 times, and the reason is exactly one bit: an Ethernet address can be absent from the table and a PCIe address cannot be absent from the space.
18. What the Comparison Assumes
Eight assumptions, and the chapter is wrong in a stated direction if any of them fails.
| # | Assumption | If it is false |
|---|---|---|
| 1 | the round trip is 1 µs | every return-path figure scales linearly — 2 µs doubles 425 088 BCE |
| 2 | completions carry 256 octets | a smaller MAX_PAYLOAD_B raises the tag count proportionally and leaves the reorder buffer unchanged |
| 3 | the MAC table is 128k × 96 b | Chapter 23.3 §2's figure; a smaller table narrows the 25.6× and raises the flood rate |
| 4 | a switch port has three decode windows | a port that also routes configuration transactions needs bus-number ranges too, adding perhaps 64 flops |
| 5 | credits are per virtual channel and there are eight | a single-VC link divides Section 6's 57 600 BCE by eight |
| 6 | the reorder buffer is the full bandwidth-delay product | a consumer tolerating out-of-order data needs a fraction of it — and most do not |
| 7 | an Ethernet transmitter holds no state after transmission | true of the MAC; false of anything above it, and TCP's retransmission buffer is the counterexample |
| 8 | BCE applies to both sides | Section 19 examines this and it holds |
Assumption 7 is the one that deserves its own paragraph, because it is the sentence most likely to be quoted out of context.
An Ethernet MAC holds nothing after transmission. An Ethernet system usually holds a great deal, because TCP above it holds every unacknowledged segment until it is acknowledged — which is a return path, implemented in software, over a protocol that has none. The retransmission buffer at 400 Gb/s and 1 µs is the same 50 kB Section 4 derived.
| Where the return-path state lives | |
|---|---|
| PCIe | in the requester's hardware, mandatory |
| Ethernet plus TCP | in the sender's host memory, by the transport's choice |
| Ethernet plus UDP | nowhere, and the application accepts loss |
So the difference is not whether the state exists. It is whether the protocol requires it, and therefore whether a design that omits it is broken or merely making a different trade. Chapter 25.1 is about the confusion this creates.
19. The Cost, Accounted — in BCE
This chapter's blocks, and the design each describes.
| Block | Flops | BCE | × the datapath |
|---|---|---|---|
address_model | 64 — two counters | 1 280 | 0.005 |
outstanding_reads | 48 | 960 | 0.003 |
credit_accountant, 3 types | 60 + 32 stall | 1 840 | 0.006 |
ordering_scoreboard | 48 + 32 | 1 600 | 0.006 |
eth_pcie_bridge | 144 | 2 880 | 0.010 |
completion_timer, 256 tags | 4 352 | 87 040 | 0.307 |
pcie_cmp_telemetry | 352 | 7 040 | 0.025 |
pcie_cmp_conformance | 0 — combinational | 0 | 0 |
| this chapter's additions | 5 132 | 102 640 | 0.362 |
completion_timer is 84.8% of the total and it is the only block that scales with the tag count, which is the chapter's structure showing through: the expensive thing about a return path is having one entry per outstanding question.
And the designs the blocks describe, which are three orders of magnitude larger.
| BCE | × the datapath | |
|---|---|---|
| a 400 Gb/s PCIe requester's return path | 425 088 | 1.500 |
| its credit buffer obligation, 1 VC | 400 000 | 1.412 |
| the same, 8 guaranteed VCs | 3 200 000 | 11.29 |
| Chapter 23.3's Ethernet switch | 5.62 × 10⁸ | 1 985 |
| Chapter 23.4's NIC | 5.07 × 10⁷ | 179 |
Row five is a part that contains both columns of this chapter, and the reason a NIC is harder than either a switch port or a root complex is that it must hold the return-path state of one protocol while terminating a protocol that has none.
20. Properties Worth Asserting, and One Worth Refusing
Fifty-one properties in six groups, and every one of them is about a boundary rather than about a block.
Group A — addressing (8).
// A1. An Ethernet lookup may miss. A PCIe decode may not.
p_ad_eth_may_miss: assert property (@(posedge clk) disable iff (!rst_n)
(is_ethernet && !lookup_hit) |-> miss_occurred);
// A2. And a decode miss is an error rather than a delivery.
p_ad_pcie_miss_is_error: assert property (@(posedge clk) disable iff (!rst_n)
(!is_ethernet && !decode_hit) |-> (miss_occurred && miss_is_reported));
// A3. The replication factor is the port count less one, on Ethernet.
p_ad_flood_width: assert property (@(posedge clk) disable iff (!rst_n)
(is_ethernet && miss_occurred) |-> (copies_on_miss == (ports - 16'd1)));
// A4. And exactly one, on PCIe.
p_ad_one_completion: assert property (@(posedge clk) disable iff (!rst_n)
(!is_ethernet && miss_occurred) |-> (copies_on_miss == 16'd1));
// A5. lookup_can_miss is the addressing model, not a runtime condition.
p_ad_model_is_static: assert property (@(posedge clk) disable iff (!rst_n)
$stable(is_ethernet) |-> $stable(lookup_can_miss));
// A6. The area figure follows the model and nothing else.
p_ad_area_follows_model: assert property (@(posedge clk) disable iff (!rst_n)
is_ethernet |-> (addressing_bce_per_port == 32'(mac_table_bce_per_port())));
// A7. A miss always increments, so the count is never behind.
p_ad_miss_counted: assert property (@(posedge clk) disable iff (!rst_n)
miss_occurred |=> (c_misses == $past(c_misses) + 32'd1));
// A8. Flooded copies accumulate at the replication factor.
p_ad_copies_accumulate: assert property (@(posedge clk) disable iff (!rst_n)
miss_occurred |=> (c_flooded_copies ==
$past(c_flooded_copies) + 32'($past(copies_on_miss))));Group B — the return path (10).
// B1. Little's law, evaluated rather than assumed.
p_rp_littles_law: assert property (@(posedge clk) disable iff (!rst_n)
(rate_gbps != 0) |-> (bytes_inflight == ((32'(rate_gbps) * 32'(rtt_ns)) / 32'd8)));
// B2. The tag count rounds up, never down.
p_rp_tags_round_up: assert property (@(posedge clk) disable iff (!rst_n)
(32'(tags_required) * 32'(MAX_PAYLOAD_B)) >= bytes_inflight);
// B3. The reorder buffer dominates the tag table.
p_rp_reorder_dominates: assert property (@(posedge clk) disable iff (!rst_n)
(bytes_inflight > 32'd1024) |-> (reorder_share_pct >= 16'd80));
// B4. The Ethernet comparison is a constant and stays one.
p_rp_ethernet_zero: assert property (@(posedge clk) disable iff (!rst_n)
(ethernet_equivalent_bce == 32'd0));
// B5. Outstanding never exceeds the limit the design implements.
p_rp_within_limit: assert property (@(posedge clk) disable iff (!rst_n)
(outstanding_now <= tag_limit));
// B6. Issue and retire move the count by exactly one.
p_rp_count_moves_by_one: assert property (@(posedge clk) disable iff (!rst_n)
(read_issued && !completion_rx) |=>
(outstanding_now == $past(outstanding_now) + 16'd1));
// B7. And a completion with no issue may not decrement below zero.
p_rp_no_underflow: assert property (@(posedge clk) disable iff (!rst_n)
(completion_rx && ($past(outstanding_now) == 16'd0)) |=>
(outstanding_now == 16'd0));
// B8. Chapter 21.8 Section 5's ceiling, stated as an implication.
p_rp_ceiling: assert property (@(posedge clk) disable iff (!rst_n)
tag_limit_binds |-> (achievable_gbps < rate_gbps));
// B9. A tag issued beyond the limit is counted as exhaustion.
p_rp_exhaustion_counted: assert property (@(posedge clk) disable iff (!rst_n)
(read_issued && (outstanding_now >= tag_limit)) |=>
(c_tag_exhausted == $past(c_tag_exhausted) + 32'd1));
// B10. The return path's total is the sum of its two parts and nothing more.
p_rp_total_is_sum: assert property (@(posedge clk) disable iff (!rst_n)
(return_path_bce_o == tag_table_bce + reorder_bce));Group C — credits (9).
// C1. At reset nothing may be transmitted.
p_cr_reset_forbids: assert property (@(posedge clk)
$rose(rst_n) |-> !may_transmit);
// C2. A packet is sent only with credit for it.
p_cr_gate_honoured: assert property (@(posedge clk) disable iff (!rst_n)
(tlp_sent && !may_transmit) |-> stalled_on_credit);
// C3. Consumed never passes limit.
p_cr_never_overrun: assert property (@(posedge clk) disable iff (!rst_n)
(hdr_consumed[0] <= hdr_limit[0]) &&
(hdr_consumed[1] <= hdr_limit[1]) &&
(hdr_consumed[2] <= hdr_limit[2]));
// C4. A credit return raises the limit and nothing else.
p_cr_return_raises_limit: assert property (@(posedge clk) disable iff (!rst_n)
(credit_return && (ret_type == 2'd0)) |=>
(hdr_limit[0] == $past(hdr_limit[0]) + 8'd1));
// C5. The obligation is the bandwidth-delay product, times the VCs.
p_cr_obligation: assert property (@(posedge clk) disable iff (!rst_n)
!per_vc_guaranteed |->
(buffer_obligation_bce ==
32'(bytes_in_flight(int'(rate_gbps), int'(rtt_ns))) * 32'd8));
// C6. And it is always larger than the counters that account for it.
p_cr_buffer_exceeds_counters: assert property (@(posedge clk) disable iff (!rst_n)
(rate_gbps >= 16'd100) |-> (buffer_obligation_bce > credit_bce_o));
// C7. A stall is counted on the cycle it occurs.
p_cr_stall_counted: assert property (@(posedge clk) disable iff (!rst_n)
stalled_on_credit |=>
(c_credit_stall_cycles == $past(c_credit_stall_cycles) + 32'd1));
// C8. The flop figure is a function of the parameters, not of traffic.
p_cr_flops_static: assert property (@(posedge clk) disable iff (!rst_n)
(credit_flops_o == 32'(credit_flops_per_port())));
// C9. Eight guaranteed VCs is eight times one.
p_cr_vc_scaling: assert property (@(posedge clk) disable iff (!rst_n)
(per_vc_guaranteed && (VCS == 8)) |->
(buffer_obligation_bce ==
32'd8 * 32'(bytes_in_flight(int'(rate_gbps), int'(rtt_ns))) * 32'd8));Group D — ordering (8).
// D1. A completion may not pass a pending posted write.
p_or_cpl_blocked: assert property (@(posedge clk) disable iff (!rst_n)
(xact_issue && (xact_kind == 2'(XACT_COMPLETION)) && (pending_posted != 0))
|-> !may_proceed);
// D2. A non-posted read is never blocked by a posted write.
p_or_np_not_blocked: assert property (@(posedge clk) disable iff (!rst_n)
(xact_issue && (xact_kind == 2'(XACT_NONPOSTED))) |-> may_proceed);
// D3. Ethernet orders nothing, and the model says so.
p_or_ethernet_none: assert property (@(posedge clk) disable iff (!rst_n)
(ethernet_orders_this == 1'b0));
// D4. The counter form costs three counters, always.
p_or_counter_cost: assert property (@(posedge clk) disable iff (!rst_n)
(counter_flops == 32'd24));
// D5. And the pairwise form is quadratic in the outstanding count.
p_or_pairwise_quadratic: assert property (@(posedge clk) disable iff (!rst_n)
(outstanding_max >= 16'd2) |->
(pairwise_comparisons ==
((32'(outstanding_max) * (32'(outstanding_max) - 32'd1)) / 32'd2)));
// D6. Issue increments exactly one class.
p_or_one_class: assert property (@(posedge clk) disable iff (!rst_n)
(xact_issue && may_proceed && (xact_kind == 2'(XACT_POSTED))) |=>
(pending_posted == $past(pending_posted) + 16'd1));
// D7. No pending count goes negative.
p_or_no_underflow: assert property (@(posedge clk) disable iff (!rst_n)
(pending_posted >= 16'd0) && (pending_nonposted >= 16'd0) &&
(pending_completion >= 16'd0));
// D8. A block is counted.
p_or_block_counted: assert property (@(posedge clk) disable iff (!rst_n)
(xact_issue && blocked_by_posted) |=>
(c_order_blocks == $past(c_order_blocks) + 32'd1));Group E — the bridge (10).
// E1. The payload TLP count rounds up.
p_br_tlps_round_up: assert property (@(posedge clk) disable iff (!rst_n)
(payload_tlps * 16'(MAX_PAYLOAD_B)) >= frame_octets);
// E2. Overhead is two transactions per frame, whatever the size.
p_br_overhead_constant: assert property (@(posedge clk) disable iff (!rst_n)
(overhead_tlps == 16'd2));
// E3. So the overhead share falls as the frame grows.
p_br_share_falls: assert property (@(posedge clk) disable iff (!rst_n)
(frame_octets > 16'd1500) |-> (overhead_share_pct <= 16'd25));
// E4. A TLP is requested only inside a frame with a descriptor.
p_br_needs_descriptor: assert property (@(posedge clk) disable iff (!rst_n)
tlp_request |-> descriptor_valid);
// E5. The cursor advances by the payload size on every acceptance.
p_br_cursor_advances: assert property (@(posedge clk) disable iff (!rst_n)
tlp_accepted |=> (tlp_addr == $past(tlp_addr) + 64'(MAX_PAYLOAD_B)));
// E6. The last TLP of a frame is the remainder, not a full payload.
p_br_last_is_remainder: assert property (@(posedge clk) disable iff (!rst_n)
(tlp_request && (tlps_for_frame - overhead_tlps == payload_tlps) &&
(frame_octets % 16'(MAX_PAYLOAD_B) != 0)) |->
(tlp_octets <= 16'(MAX_PAYLOAD_B)));
// E7. The two completions are never the same event.
p_br_completions_differ: assert property (@(posedge clk) disable iff (!rst_n)
(frame_complete_ethernet && frame_complete_pcie) |-> !completions_disagree);
// E8. A frame is counted only when the PCIe side finished.
p_br_count_on_pcie: assert property (@(posedge clk) disable iff (!rst_n)
writeback_acked |=> (c_frames_bridged == $past(c_frames_bridged) + 32'd1));
// E9. Every accepted TLP is counted.
p_br_tlps_counted: assert property (@(posedge clk) disable iff (!rst_n)
tlp_accepted |=> (c_tlps_issued == $past(c_tlps_issued) + 32'd1));
// E10. Section 14 prohibition 1, as a bound rather than a prohibition:
// the gap between the two completions is at most a timeout.
p_br_gap_bounded: assert property (@(posedge clk) disable iff (!rst_n)
frame_complete_ethernet |-> ##[1:CPL_TIMEOUT_CYCLES]
(frame_complete_pcie || cpl_timeout));Group F — timers, telemetry and conformance (6).
// F1. Every issued read becomes live.
p_tm_issue_makes_live: assert property (@(posedge clk) disable iff (!rst_n)
read_issued |=> live[$past(issued_tag)]);
// F2. The class-112 replacement property 4: a completion for a dead tag is an orphan.
p_tm_orphan: assert property (@(posedge clk) disable iff (!rst_n)
(completion_rx && !live[cpl_tag]) |=>
(c_unexpected_cpl == $past(c_unexpected_cpl) + 32'd1));
// F3. A timeout frees the tag.
p_tm_timeout_frees: assert property (@(posedge clk) disable iff (!rst_n)
timeout_fired |=> !live[$past(timeout_tag)]);
// F4. Ethernet needs no timers, and the model reports zero.
p_tm_ethernet_zero: assert property (@(posedge clk) disable iff (!rst_n)
(ethernet_timer_bce == 32'd0));
// F5. The conformance monitor's violation vector is its own disjunction.
p_cf_vector: assert property (@(posedge clk) disable iff (!rst_n)
conformant |-> (violations == 6'b000000));
// F6. Loss attribution is 100% only when every drop has a named cause.
p_tl_attribution: assert property (@(posedge clk) disable iff (!rst_n)
(loss_attributable_pct == 16'd100) |->
((c_eth_dropped == 48'd0) || (c_desc_starved >= c_eth_dropped)));Coverage — the states a bridge only reaches under load.
c_ad_flood: cover property (@(posedge clk) is_ethernet && miss_occurred);
c_ad_unsupported: cover property (@(posedge clk) !is_ethernet && miss_occurred);
c_rp_tag_bound: cover property (@(posedge clk) tag_limit_binds);
c_rp_at_limit: cover property (@(posedge clk) outstanding_now == tag_limit);
c_cr_stalled: cover property (@(posedge clk) stalled_on_credit);
c_cr_zero_credit: cover property (@(posedge clk) hdr_limit[0] == 8'd0);
c_or_blocked: cover property (@(posedge clk) blocked_by_posted);
c_br_min_frame: cover property (@(posedge clk) frame_start && (frame_octets <= 16'd64));
c_br_jumbo: cover property (@(posedge clk) frame_start && (frame_octets >= 16'd9000));
c_br_disagree: cover property (@(posedge clk) completions_disagree);
c_tm_timeout: cover property (@(posedge clk) timeout_fired);
c_tm_orphan_cpl: cover property (@(posedge clk) completion_rx && !live[cpl_tag]);
c_cf_overadv: cover property (@(posedge clk) v_overadvertised);
c_cf_uncounted: cover property (@(posedge clk) v_uncounted_drop);21. Verification Scenarios
Fifty-eight scenarios in six groups, plus one directed test that random stimulus will not produce.
Group 1 — addressing (9).
| # | Scenario | Expect |
|---|---|---|
| 1 | Ethernet mode, lookup hits | no miss; 196 608 BCE per port reported |
| 2 | Ethernet mode, lookup misses | 63 copies on a 64-port switch; c_flooded_copies += 63 |
| 3 | PCIe mode, decode hits | no miss; 7 680 BCE per port |
| 4 | PCIe mode, decode misses | 1 completion; miss_is_reported high |
| 5 | the ratio between modes | 25.6× — p_ad_area_follows_model |
| 6 | is_ethernet toggled mid-run | p_ad_model_is_static fires |
| 7 | a 128-port switch, Ethernet miss | 127 copies |
| 8 | a 2-port switch, Ethernet miss | 1 copy — the flood degenerates |
| 9 | 1 000 consecutive misses, Ethernet | 63 000 flooded copies |
Group 2 — the return path (11).
| # | Scenario | Expect |
|---|---|---|
| 10 | 400 Gb/s, 1 µs round trip | 50 000 B in flight, 196 tags |
| 11 | 400 Gb/s, 0.5 µs | 25 000 B, 98 tags |
| 12 | 400 Gb/s, 2 µs | 100 000 B, 391 tags |
| 13 | 100 Gb/s, 1 µs | 12 500 B, 49 tags — Chapter 19.6 §4's figure |
| 14 | the return-path total at 400 Gb/s, 1 µs | 425 088 BCE, 1.500 datapaths |
| 15 | reorder_share_pct at the same point | 94 — p_rp_reorder_dominates |
| 16 | ethernet_equivalent_bce at any setting | 0 |
| 17 | a tag limit of 64 at 400 Gb/s, 1 µs | tag_limit_binds; 131 Gb/s achievable — 32.8% |
| 18 | a tag limit of 8 at 400 Gb/s, 1 µs | 16.4 Gb/s — 4.1% of the link |
| 19 | a tag limit of 256 at the same point | does not bind |
| 20 | issue at the limit | c_tag_exhausted increments |
Group 3 — credits (10).
| # | Scenario | Expect |
|---|---|---|
| 21 | reset released, no DLLP yet | may_transmit low — p_cr_reset_forbids |
| 22 | one header credit returned | one packet may go, the next may not |
| 23 | a data credit return of 16 | dat_limit rises by 16 |
| 24 | transmit with may_transmit low | stalled_on_credit; p_cr_gate_honoured |
| 25 | credit counters, 8 VCs, both directions | 2 880 flops, 57 600 BCE per port |
| 26 | 64 ports | 3 686 400 BCE, 13.01 datapaths |
| 27 | the obligation at 400 Gb/s, 1 µs, 1 VC | 400 000 BCE — 6.9× the counters |
| 28 | the same with 8 guaranteed VCs | 3 200 000 BCE — 55.6× the counters |
| 29 | a partner that returns credits every 10 µs | the link runs at a tenth of its rate and nothing fails |
| 30 | a partner that never returns credits | the link stops; c_credit_stall_cycles climbs forever |
Group 4 — ordering (9).
| # | Scenario | Expect |
|---|---|---|
| 31 | a completion issued with a posted write pending | blocked — p_or_cpl_blocked |
| 32 | a non-posted read with a posted write pending | proceeds — p_or_np_not_blocked |
| 33 | the producer-consumer sequence | the data write retires before the flag write |
| 34 | the same sequence with relaxed ordering set | no block; c_order_blocks stays at zero |
| 35 | 196 outstanding, pairwise model | 19 110 comparisons reported |
| 36 | the counter model at any depth | 24 flops, 480 BCE |
| 37 | ethernet_orders_this at any setting | 0 |
| 38 | retire with nothing pending | no underflow — p_or_no_underflow |
| 39 | 1 024 outstanding, pairwise model | 523 776 comparisons — the shape, at scale |
Group 5 — the bridge (10).
| # | Scenario | Expect |
|---|---|---|
| 40 | a 64-octet frame | 1 payload TLP, 2 overhead, 66.7% overhead share |
| 41 | a 1 518-octet frame | 6 payload TLPs, 25.0% overhead share |
| 42 | a 9 018-octet frame | 36 payload TLPs, 5.3% overhead share |
| 43 | 1 518-octet frames at 400 Gb/s | 32.51 Mpps, 260.1 MTLP/s |
| 44 | descriptor_valid low when a frame arrives | no TLP issued; the frame is lost with no protocol event |
| 45 | the same, with descriptor_starved wired | loss_attributable_pct stays at 100 |
| 46 | the same, without it | attribution falls; the drop has no reporter anywhere |
| 47 | frame_end without writeback_ack | completions_disagree for a host round trip — 32 frames |
| 48 | v_premature_complete observed | a warning, not an error — the gap is correct |
| 49 | a frame whose length is an exact multiple of 256 | no remainder TLP; p_br_last_is_remainder vacuous |
Group 6 — timers and conformance (9).
| # | Scenario | Expect |
|---|---|---|
| 50 | 256 tags, 16-bit ages | 4 352 flops, 87 040 BCE, 0.307 datapaths |
| 51 | a read with no completion for 50 µs | timeout_fired; the tag is freed |
| 52 | a completion for a tag never issued | c_unexpected_cpl increments; v_orphan_completion |
| 53 | a completion with CPL_UNSUPPORTED_REQ | c_error_cpl increments; the tag still frees |
| 54 | advertised_credits above buffer_entries | v_overadvertised |
| 55 | a frame dropped with no counter | v_uncounted_drop |
| 56 | a TLP transmitted without credit | v_credit_overrun |
| 57 | ethernet_timer_bce at any setting | 0 |
| 58 | all six violations clear | conformant high |
22. Debugging a Bridge
Six symptoms, and each is a place where the two protocols' vocabularies stop translating.
| Symptom | First question | Where to look |
|---|---|---|
| receive drops at line rate, idle host, non-full ring | is c_desc_starved counting? | Chapter 23.4 §7 — the prefetch store is sized for a previous generation |
| a link that runs at exactly a third of its rate | is tag_limit_binds set? | Section 5 — the ceiling is tags × payload ÷ latency and the latency is not yours |
| a link that runs at exactly a third of its rate, and the tag limit is fine | what is c_credit_stall_cycles against hdr_limit? | Section 7 — stalls with a high limit is slow return; with a low limit it is a small buffer |
| a device that disappears under load and returns after reset | is c_timeouts non-zero? | Section 13 — the timeout is probably marginal rather than the completer dead |
| intermittent wrong data, one machine in fifty | is c_unexpected_cpl non-zero? | Section 21's directed test — tag reuse after a timeout |
| the two sides' frame counts differ by a constant | which side counts first? | Section 11 — they are a host round trip apart and both are right |
Row six is the one that wastes the most time and it is not a bug.
A bridge's Ethernet frame counter and its PCIe write-back counter differ by the number of frames in flight, which at 400 Gb/s, 1 µs and 1 518-octet frames is 32.5 — so a snapshot taken at any instant shows a difference of about 32 and it never converges while traffic flows. The correct procedure is to quiesce, wait a round trip, and then compare, and a team that compares under load concludes that frames are being lost at a rate of thirty-two per sample.
23. Misconceptions
Misconception 1 — "PCIe is faster than Ethernet."
The wrong model: a load/store fabric has lower latency and higher bandwidth, so it is the better interconnect wherever both would fit.
What it costs: a design that reaches for PCIe over distance and discovers that its round-trip-proportional state is unaffordable. At 400 Gb/s, Section 4's return path is 425 088 BCE at 1 µs and 850 176 at 2 µs — and 2 µs is barely thirty metres of fibre plus two switch hops. A fabric that spans a data-centre row does not get to hold a bandwidth-delay product of state per link.
The corrected model: PCIe's advantages are properties of short distances and fixed membership, and both terms appear in its cost. A comparison that measures only rate and latency is measuring the two quantities that do not distinguish them.
Misconception 2 — "the credit counters are the cost of credit-based flow control."
The wrong model: credits are a few registers per port and are therefore cheap.
What it costs: a buffer budget that is wrong by a factor between 7 and 56. Section 7 derives 57 600 BCE of counters against 400 000 BCE of obligation at one VC, and 3 200 000 at eight guaranteed VCs. A design review that inspects the credit logic has inspected 1.8% of the mechanism.
The corrected model: a credit is a promise that buffer exists, and the promise is the cost. The counters merely record it.
Misconception 3 — "an Ethernet address and a PCIe address do the same job."
The wrong model: both identify where a transfer is going, so the difference is encoding.
What it costs: a bridge design that assumes a translation table can be built, and a debugging model that expects the wrong failures. Section 2: a name can be absent and a position cannot, and that one bit produces the flood path, the aging rule, Chapter 12.5's capacity argument and Chapter 21.6's entire diagnostic apparatus on one side, and none of it on the other.
The corrected model: the descriptor is the translation and software wrote it before the frame existed. There is no hardware mapping between the two address spaces and there cannot be one.
Misconception 4 — "Ethernet holds no state, so it is simpler."
The wrong model: four of Section 17's six rows are zero on the Ethernet side, so an Ethernet design has less to get right.
What it costs: the expectation that Module 21 should have been short. Ethernet holds no state about a transfer and therefore has no reporter for its failure, which is why Chapter 21.6 needs a selectivity index, a shape classifier and a clustering probe to infer from counts what PCIe reads off a status field.
The corrected model: the state a protocol holds is also the evidence it can produce. Holding nothing is cheap in silicon and expensive in diagnosis, and the two costs land on different teams in different years.
Misconception 5 — "Chapter 23.4's 149 descriptors in flight is a NIC's Ethernet sizing problem."
The wrong model: a high-rate NIC needs a deep prefetch store because Ethernet frames arrive quickly.
What it costs: looking for the fix on the wrong side. Section 4 establishes that the 149 are non-posted reads awaiting completions — every term is PCIe vocabulary — and that the Ethernet side of the same part holds nothing at all. A team that treats it as a MAC problem tunes the MAC and the underruns continue.
The corrected model: it is PCIe's return path measured inside an Ethernet part, and it scales with the host's latency rather than with the line rate alone.
Misconception 6 — "verification IP ports across a bridge if the signal names map."
The wrong model: a property is a relation between signals; if every signal has a counterpart, the property has one.
What it costs: a passing regression on the side that was not verified. Section 20's class 112: frame_complete and transaction_complete both exist, the ported property compiles and passes, and it certifies that a message arrived rather than that an event occurred.
The corrected model: check whether each term's OWNER is the same on both sides. A property whose terms are local on one side and remote on the other has changed subject, and the vocabulary will not tell you.
24. Interview Questions
Six, with what a strong answer contains.
1. What is the difference between an Ethernet address and a PCIe address?
An Ethernet address is a name and a PCIe address is a position. A strong answer goes to the consequence rather than the definition: a name is resolved by a lookup that can fail, and the failure produces a flood; a position is decoded against a range and cannot fail, and an unmatched one is a reported error. The best answers put a number on it — 196 608 BCE per port of MAC table against 7 680 of decode windows, 25.6× — and then observe that the expensive side is the one that can still not find the destination.
2. Why does a 400 Gb/s PCIe link need hundreds of tags?
Little's law. bytes in flight = rate × round trip; at 50 GB/s and 1 µs that is 50 000 bytes, and at 256 bytes per completion, 196 outstanding reads. A strong answer continues to the part people forget: the tags are 25 088 BCE and the reorder buffer behind them is 400 000 — 94% of the cost — so a conversation about tag counts is a conversation about 6% of the problem.
3. A 400 Gb/s link is delivering 131 Gb/s and every assertion passes. What do you check?
The outstanding limit against the round trip. 131 = 64 × 256 × 8 ÷ 1 000, so 64 tags at a microsecond is exactly 131 Gb/s and the design is working perfectly. A strong answer names Chapter 21.8 §20's class 101 — a rate asserted on a mechanism that bounds a count — and says that the useful output is not a failing assertion but a reported tag_limit_binds bit.
4. Why can a producer-consumer pattern be written directly on PCIe and not on Ethernet?
Because PCIe orders posted writes against each other and Ethernet orders nothing across paths. A strong answer names Chapter 15.2 as the reason — two frames between the same pair may hash to different link-aggregation members — and then makes the positive case: that lack of ordering is what lets Chapter 23.2 §12 stripe a collective across eight paths and take it from 28.4% to 77.1%. The ordering is not missing; it was traded.
5. What does the ordering specification's shape cost?
Twenty-four flip-flops, because it is stated over three transaction types rather than over transactions. The strong answer contrasts it with the specification a naive author writes — transactions complete in issue order — which is a relation between individuals and would need 19 110 comparisons at 196 outstanding. The general point: a relation over a small set of classes is linear where the same relation over individuals is quadratic, and most expensive requirements have been stated over the wrong objects.
6. You port your Ethernet assertion suite onto the PCIe side of a NIC and it all passes. What have you verified?
The bridge's own reporting. A strong answer walks one property: last_octet_sent |-> ##1 frame_complete becomes tlp_handed_to_link_layer |-> ##1 transaction_complete, every term has a counterpart, and transaction_complete is a message from a completer rather than an observation here. The best answers name the repair — split it into a local half, a relation stated as a relation, and a timeout — and note that the property that catches a completer inventing completions is the one the ported version made impossible to write.
25. Questions and Answers
26. What's Next
This chapter set Ethernet against a fabric that answers a different question. Chapter 24.2 sets it against one that answers the same question differently.
InfiniBand carries packets between endpoints, like Ethernet, and it does not drop them. Credit-based flow control at the link layer means a sender transmits only into space the receiver has guaranteed — which removes the event Chapter 21.6 built an entire chapter around, and appears at first reading to be strictly better.
It is not, and the arithmetic says why. The buffer a lossless link must reserve is a round trip of the link's own bandwidth, per virtual lane, per hop — Section 6 of this chapter derived that obligation for one PCIe port and found it 6.9 times the counters that account for it. Chapter 24.2 takes the same derivation to a 64-port fabric at 400 Gb/s and asks what fraction of Chapter 23.3's 5.62 × 10⁸ BCE switch it consumes. The answer at data-centre link lengths is a large fraction, and at two kilometres it exceeds the whole switch.
And it asks what the credit scheme puts back in exchange for the drop it removed, which is Chapter 14.3's head-of-line blocking and congestion spreading made deterministic rather than probabilistic — because with credits the upstream is not merely likely to stall, it is required to. Then it places RoCE precisely: which parent it takes its transport from, which its link layer, and what it inherits from each.
Continue learning
Related tutorials
- Related topic
PCIe vs Ethernet — Where the Cost of Overload Lands
The same overload into two fabrics: one stalled the sender 59,405 times and lost nothing, the other discarded 59,405 frames. That single choice explains why one needs TCP and the other does not.
- Related topic
From Coax to Twisted Pair to Switched Links
Coax, repeater, hub, bridge, switch — four steps, and only the last touched contention. A repeater reproduces a signal and cannot buffer, so it spends collision-domain budget and partitions nothing; a bridge holds the whole frame, and that buffer is what makes every other capability possible.
- Related topic
Throughput Problems
Three ceilings sit below the line rate and the lowest is usually the one nobody computes: eight outstanding requests at a kilobyte and a microsecond cap a 100 Gb/s port at 65.5.
- Related topic
A High-Rate Data-Centre NIC
Descriptors are 38% of the payload at minimum frame size and break a Gen5 host interface at 400 Gb/s — and the part's dominant state is a context cache with a cliff at 39.4%.
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.
