PCIe · Module 5
PCIe Gen3 — Why the Encoding Changed
Gen3 raised the signalling rate to 8 GT/s and replaced 8b/10b with 128b/130b. Deriving why capacity grew by 1.97× when the rate only grew by 1.6×, what the encoding change cost in PHY complexity, and how to measure whether a workload used the headroom.
Chapter 5.2 was a clean controlled experiment: Gen2 doubled the signalling rate and changed nothing else, so capacity doubled exactly. Gen3 is not that.
Why did Gen3 not simply double Gen2's signalling rate, and why did changing the encoding become architecturally important?
Two things moved at once, which makes the arithmetic more interesting and the engineering considerably harder.
1. The Pressure Gen2 Left Behind
Recall where Chapter 5.2 ended. Gen2's derivation:
5 GT/s × 8/10 = 4.0 Gb/s per lane, per direction.
The 8b/10b encoding costs 20% of everything transmitted, and that cost is a fraction, not a fixed quantity. So it scales with the rate: at 2.5 GT/s it consumes 0.5 GT/s worth of signalling; at 5 GT/s it consumes 1.0 GT/s worth.
Extend that mentally. Had PCIe kept 8b/10b and simply doubled again to 10 GT/s, the encoding would have consumed the equivalent of 2 GT/s — and the cost keeps growing in absolute terms with every generation, because the percentage never moves.
2. 128b/130b Efficiency
Gen3's encoding groups 128 bits of data into a 130-bit transmitted block.
The efficiency follows directly:
128 / 130 = 0.984615384615...
As a percentage: ≈ 98.4615%.
Compare with 8b/10b's 80%. Encoding overhead falls from 20% to approximately 1.54% — a reduction of roughly thirteen-fold in the fraction of the wire spent on encoding.
That is the headline, and it is worth pausing on how large it is. At Gen2, one byte in five of transmitted capacity went to encoding. At Gen3, it is closer to one bit in sixty-five.
3. The Derivation
Step 1 — signalling rate. 8 × 10⁹ transfers/second, per lane, per direction.
Step 2 — apply encoding.
8 GT/s × (128/130) = 7.876923076923... Gb/s
Rounded for use: ≈ 7.8769 Gb/s.
Step 3 — convert to bytes.
7.876923... Gb/s ÷ 8 bits/byte = 0.984615384615... GB/s
Step 4 — express conventionally. ≈ 984.615 MB/s, decimal units.
4. Why Capacity Grew More Than the Rate Did
Here is the quantitative payoff, and it is the thing most Gen3 explanations skip.
The signalling rate went from 5 to 8 GT/s — a ratio of 1.6×. It would be natural to expect capacity to grow by 1.6×.
It does not:
(8 GT/s × 128/130) ÷ (5 GT/s × 8/10)
= 7.876923... Gb/s ÷ 4.0 Gb/s
= 1.969230769...
≈ 1.969× — very nearly a doubling.
| Gen2 | Gen3 | Ratio | |
|---|---|---|---|
| Signalling rate | 5 GT/s | 8 GT/s | 1.600× |
| Encoding efficiency | 0.8 | ≈ 0.984615 | 1.2308× |
| Encoding-adjusted bit rate | 4.0 Gb/s | ≈ 7.8769 Gb/s | 1.969× |
| Encoding-adjusted byte rate | 500 MB/s | ≈ 984.6 MB/s | 1.969× |
The capacity ratio is the product of the two:
1.600 × 1.23077 = 1.96923
That decomposition is the chapter's central arithmetic. Gen3 delivered close to a doubling of capacity while raising the signalling rate by only 60%, because the encoding change supplied a further ≈23% multiplicatively on top of it — the two factors multiply, they do not add. The physical layer was asked to do substantially less than a doubling of rate, and the system still got substantially a doubling of capacity.
Gen2→Gen3 is not a rate story. It is a rate story multiplied by an efficiency story.
5. What the Encoding Change Actually Involves
The ratio is the easy part. Being honest about what it costs matters more.
Moving from a scheme that maps small groups to a scheme that operates on much larger blocks changes what the physical layer has to do. Regardless of the specific mechanisms, a receiver still needs to solve problems that do not disappear:
- Block alignment — finding where each transmitted block begins in an arriving bit stream.
- Distinguishing data from control — some transmitted blocks carry data and others carry control information, and the receiver must tell them apart.
- Maintaining suitable signal characteristics — a serial stream with no accompanying clock needs sufficient transition density and spectral properties for timing to be recovered, which 8b/10b provided partly as a by-product of its mapping. With far less encoding overhead available, that property has to be obtained another way.
The point is not the specific answers. It is that a more efficient encoding does not remove those requirements; it changes how they are met, and generally makes the implementation more sophisticated.
6. Computing Rates When the Ratio Is Not Round
The rate package from Chapter 5.1 needs revisiting, because 128/130 exposes a precision problem that 8/10 hid.
// Compile-time only (elaboration constants and pure functions).
// Extends the Gen1/Gen2 package to handle ratios that do not divide evenly.
//
// PRECISION NOTE: with 8b/10b every result was an exact integer in MB/s.
// 128/130 is not. Computing in MB/s would give floor(984.615) = 984, losing
// 0.6 MB/s per lane — small alone, but it compounds across comparisons and
// makes ratios wrong in the third digit. Working in KILOBYTES/s keeps three
// more digits of the fraction, which is sufficient for teaching comparisons.
package pcie_gen3_rate_pkg;
// Rates in MEGA-transfers/s so every generation is an exact integer input.
localparam longint unsigned GEN1_MTPS = 2500;
localparam longint unsigned GEN2_MTPS = 5000;
localparam longint unsigned GEN3_MTPS = 8000;
// Encoding ratios as explicit numerator/denominator pairs.
localparam int unsigned ENC_8B10B_NUM = 8;
localparam int unsigned ENC_8B10B_DEN = 10;
localparam int unsigned ENC_128B130B_NUM = 128;
localparam int unsigned ENC_128B130B_DEN = 130;
// Encoding-adjusted bit rate in KILOBITS/s, per lane, per direction.
// Multiplication precedes division so truncation cannot discard
// significance; the ×1000 scaling happens before the divide for the same
// reason. Result is floored — see the note above.
function automatic longint unsigned encoded_kbits_per_s(
input longint unsigned mtps,
input int unsigned enc_num,
input int unsigned enc_den
);
if (enc_den == 0 || enc_num > enc_den) return 0; // illegal configuration
return (mtps * 1000 * enc_num) / enc_den;
endfunction
// Encoding-adjusted byte rate in KILOBYTES/s, per lane, per direction.
function automatic longint unsigned encoded_kbytes_per_s(
input longint unsigned mtps,
input int unsigned enc_num,
input int unsigned enc_den
);
return encoded_kbits_per_s(mtps, enc_num, enc_den) / 8;
endfunction
// Per lane, per direction, theoretical, encoding-adjusted.
// Gen2: 4_000_000 kb/s -> 500_000 kB/s = 500.000 MB/s (exact)
// Gen3: 7_876_923 kb/s -> 984_615 kB/s ≈ 984.615 MB/s (floored)
localparam longint unsigned GEN2_KBPS =
encoded_kbits_per_s(GEN2_MTPS, ENC_8B10B_NUM, ENC_8B10B_DEN);
localparam longint unsigned GEN3_KBPS =
encoded_kbits_per_s(GEN3_MTPS, ENC_128B130B_NUM, ENC_128B130B_DEN);
localparam longint unsigned GEN2_KBYTES =
encoded_kbytes_per_s(GEN2_MTPS, ENC_8B10B_NUM, ENC_8B10B_DEN);
localparam longint unsigned GEN3_KBYTES =
encoded_kbytes_per_s(GEN3_MTPS, ENC_128B130B_NUM, ENC_128B130B_DEN);
// Capacity ratio in thousandths — 1969 means 1.969x. Computed from the
// kB/s figures so the fractional part survives into the ratio.
localparam longint unsigned GEN3_OVER_GEN2_X1000 =
(GEN3_KBYTES * 1000) / GEN2_KBYTES; // 1969
endpackageClassification: compile-time only.
What it teaches: that unit scaling is a precision decision, not a cosmetic one. The same arithmetic in MB/s gives a ratio of 984/500 = 1.968; in kB/s it gives 1.969. The second is right, and the difference came entirely from where the truncation landed.
Deliberately simplified: no lane count — Link width is Module 6's subject and including it here would invite the width tables this chapter avoids. No protocol overhead, which is workload-dependent rather than constant. Results are floored rather than rounded, which biases slightly low by a bounded and stated amount.
What to notice: the guard if (enc_den == 0 || enc_num > enc_den) return 0 rejects a configuration claiming efficiency above 100%. Silent nonsense from a bad parameter is worse than a zero, because a zero is obviously wrong.
Production implication: a real design that needs these figures at runtime would either precompute them or store them, not divide at runtime — division is expensive in hardware and the operands are known at elaboration.
7. Selecting an Encoding Mode
A controller supporting several generations needs the ratio to be selectable. That is a small piece of logic with one real correctness concern.
// Illustrative internal configuration — NOT a PCIe configuration encoding.
// PCIe defines its own normative means of establishing and reporting the
// operating rate; this is how a design might organise the arithmetic internally.
typedef enum logic {
ENC_8B10B = 1'b0, // Gen1, Gen2
ENC_128B130B = 1'b1 // Gen3, Gen4, Gen5 — NOT Gen6, which is unencoded
} encoding_mode_e;// Illustrative synthesizable RTL — encoding ratio selection.
// NOT a PCIe controller. The ratio must be stable while any measurement or
// calculation depends on it, which is the only subtle requirement here.
module encoding_select (
input logic clk,
input logic rst_n,
input logic mode_valid, // a new mode is being programmed
input encoding_mode_e mode_in,
input logic epoch_active, // a measurement epoch is in progress
output encoding_mode_e mode,
output logic [7:0] enc_num,
output logic [7:0] enc_den,
output logic mode_locked
);
encoding_mode_e mode_q;
logic locked_q;
assign mode = mode_q;
assign mode_locked = locked_q;
// Ratio is derived from mode rather than programmed separately, so an
// inconsistent pair (say 8b/10b mode with a 128/130 ratio) cannot be
// expressed at all.
always_comb begin
unique case (mode_q)
ENC_8B10B: begin enc_num = 8'd8; enc_den = 8'd10; end
ENC_128B130B: begin enc_num = 8'd128; enc_den = 8'd130; end
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mode_q <= ENC_8B10B; // defined power-on default
locked_q <= 1'b0;
end else begin
// The mode may only change while no measurement depends on it. A change
// mid-epoch would make the epoch's derived figures meaningless without
// anything reporting an error.
if (mode_valid && !epoch_active) begin
mode_q <= mode_in;
end
locked_q <= epoch_active;
end
end
endmoduleClassification: synthesizable.
What it teaches: deriving the ratio from the mode rather than programming them independently, so an inconsistent combination is unrepresentable — the same principle Chapter 4.1 applied to role and capability.
Deliberately simplified: two modes only; no representation of how the operating rate is actually established, which is Module 17's subject; no reporting of the mode to software.
What to notice: the mode cannot change while epoch_active. Without that guard, a rate change part-way through a measurement window produces derived figures computed against two different assumptions, and nothing signals a problem — the numbers simply come out wrong in a plausible-looking way.
Production implication: a real controller would tie the mode to the actual operating rate rather than accepting it as an independent input, and would expose it through the normative status mechanism.
8. Assertions
// SVA over the illustrative encoding selection. Implementation invariants for
// THIS design — not PCIe protocol requirements.
// LEGALITY — P1: the ratio never claims efficiency above 100%.
property p_ratio_legal;
@(posedge clk) disable iff (!rst_n)
(enc_den != 0) && (enc_num <= enc_den);
endproperty
a_ratio_legal : assert property (p_ratio_legal);
// LEGALITY — P2: the ratio always corresponds to the selected mode. Catches
// a decode that drifts out of step with the mode register.
property p_ratio_matches_mode;
@(posedge clk) disable iff (!rst_n)
(mode == ENC_8B10B) -> (enc_num == 8 && enc_den == 10) and
(mode == ENC_128B130B) -> (enc_num == 128 && enc_den == 130);
endproperty
a_ratio_matches : assert property (p_ratio_matches_mode);
// STABILITY — P3: the mode does not change while a measurement epoch is
// active. A mid-epoch change silently invalidates every derived figure.
property p_mode_stable_in_epoch;
@(posedge clk) disable iff (!rst_n)
epoch_active |=> $stable(mode);
endproperty
a_mode_stable : assert property (p_mode_stable_in_epoch);P1 catches a parameter or decode error that would make every downstream calculation nonsense. Simulation may not flag it because the arithmetic still produces a number.
P2 catches the mode and ratio drifting apart — the specific failure that motivated deriving one from the other. It is the assertion that proves the design decision was actually implemented.
P3 is the one worth dwelling on. A mode change mid-epoch does not crash anything; it produces a measurement computed against two different encoding assumptions, which looks entirely plausible and is wrong. This is the class of bug assertions exist for, because inspection will not find it.
9. Did the Workload Use the Headroom?
Gen3 provides roughly 1.97× Gen2's capacity. Whether an application sees that depends on whether the Link was the constraint — the governing question from Chapter 5.2, which applies here unchanged.
The utilisation instrumentation from 5.1 and 5.2 answers it directly, and one addition is worth making for Gen3: distinguishing payload bytes from other traffic, where the observed interface exposes the difference.
// Illustrative synthesizable RTL — capacity-utilisation instrumentation.
// NOT a PCIe controller. Separates payload bytes from other transferred bytes
// so "the Link is busy" and "the Link is carrying useful data" are distinct.
module capacity_monitor #(
parameter int unsigned CNT_W = 48
) (
input logic clk,
input logic rst_n,
// Observed interface — watched only.
input logic obs_valid,
input logic obs_ready,
input logic [15:0] obs_bytes,
input logic obs_is_payload, // abstraction of the observation point
input logic epoch_start,
input logic epoch_stop,
output logic [CNT_W-1:0] snap_cycles,
output logic [CNT_W-1:0] snap_transfer,
output logic [CNT_W-1:0] snap_stalled,
output logic [CNT_W-1:0] snap_idle,
output logic [CNT_W-1:0] snap_payload_bytes,
output logic [CNT_W-1:0] snap_other_bytes,
output logic snap_valid
);
logic [CNT_W-1:0] c_cycles, c_transfer, c_stalled, c_idle, c_pay, c_oth;
logic running_q;
wire transfer = obs_valid && obs_ready;
wire stalled = obs_valid && !obs_ready;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
running_q <= 1'b0;
snap_valid <= 1'b0;
c_cycles <= '0; c_transfer <= '0; c_stalled <= '0;
c_idle <= '0; c_pay <= '0; c_oth <= '0;
end else if (epoch_start) begin
running_q <= 1'b1;
snap_valid <= 1'b0;
c_cycles <= '0; c_transfer <= '0; c_stalled <= '0;
c_idle <= '0; c_pay <= '0; c_oth <= '0;
end else if (running_q && epoch_stop) begin
// Freeze the whole set in one cycle so software reads a coherent epoch.
running_q <= 1'b0;
snap_cycles <= c_cycles;
snap_transfer <= c_transfer;
snap_stalled <= c_stalled;
snap_idle <= c_idle;
snap_payload_bytes <= c_pay;
snap_other_bytes <= c_oth;
snap_valid <= 1'b1;
end else if (running_q) begin
c_cycles <= c_cycles + 1'b1;
if (transfer) c_transfer <= c_transfer + 1'b1;
else if (stalled) c_stalled <= c_stalled + 1'b1;
else c_idle <= c_idle + 1'b1;
if (transfer) begin
if (obs_is_payload) c_pay <= c_pay + obs_bytes;
else c_oth <= c_oth + obs_bytes;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that a highly efficient encoding does not make a Link efficient. Encoding overhead fell from 20% to ~1.5%, but packet and control traffic did not change, and a busy Link carrying mostly non-payload bytes is a different problem from an idle one.
Deliberately simplified: one direction; a single epoch rather than rolling statistics; obs_is_payload assumed available at the observation point.
What to notice: payload and other bytes are counted separately rather than as a total plus a fraction. Separate counters let you compute the split after the fact at whatever granularity you want, and they make it impossible to lose one component to rounding.
10. Verification
What the monitor observes: the handshake, bytes per accepted beat, the payload/other split, and epoch boundaries.
What the scoreboard computes independently: expected capacity from first principles — its own copy of rate × encoding ÷ 8 — and expected payload throughput from the stimulus it generated. As Chapter 5.1 argued, the scoreboard must not call the design's rate function, because a shared implementation makes its own arithmetic bugs invisible, and arithmetic bugs produce plausible numbers rather than crashes.
Scenarios:
- Same workload at Gen2 and Gen3 rate models. Measure Gen2 utilisation first. If Gen2 was saturated, expect substantial headroom at Gen3; if it was not, expect little change and treat that as the correct result.
- Low-demand workload. Similar application throughput at both, high idle at both. This is the case most often misreported as a failed upgrade.
- Small transactions. Encoding efficiency improved dramatically; packet overhead did not. Expect efficiency to remain poor and the payload/other split to show it — this is the scenario the split counters exist for.
- Sustained large transfers. Closest to the ceiling, and still below it because packet and link-level traffic remain.
- Rate-model consistency. Verify the design's computed capacity against the scoreboard's independent figure for both generations, including the ratio.
Corner cases: mode changed while an epoch is active (should be prevented); illegal encoding configuration; epoch boundaries during each cycle class; payload-only and other-only epochs.
Coverage: each cycle class dominant; both encoding modes exercised; payload fraction across its range; ratio verified for both generations.
11. Debugging: Expected a Large Gain, Got a Small One
Ordered by how often each is the answer.
1. Was Gen2 actually saturated? If not, the Link was not the constraint and raising it changes nothing. Check idle and stall from the previous configuration before anything else.
2. Is the operating rate actually Gen3? Verify rather than assume — in a model, that the correct parameters are in use; in a system, that the Link is operating at the expected rate.
3. Is the source generating enough demand? High idle means the Link is waiting. A faster Link does not make a device produce data faster.
4. Are transactions too small? Check the payload/other split. Encoding improved; packet overhead did not. A small-transaction workload will show a poor payload fraction regardless of generation.
5. Is the topology the limit? If the device sits behind a Switch, its own Link is one hop (Chapter 4.2). A faster first hop does not help if a shared upstream segment is binding.
6. Is memory or the host side the limit? Everything converges there (Chapter 4.1).
7. Is backpressure high? Check stall. Something downstream is the constraint.
8. Is the measurement counting the right bytes? Payload versus total makes a large difference at small transaction sizes, and comparing a payload figure against a total-byte figure produces an apparent shortfall that is purely an accounting error.
12. Common Misconceptions
13. Understanding Check
14. Summary
Gen3 changed two things at once: the signalling rate rose to 8 GT/s, and the encoding moved from 8b/10b to 128b/130b, raising efficiency from 80% to ≈98.4615%.
The derivation:
8 GT/s × 128/130 ≈ 7.8769 Gb/s → ÷ 8 ≈ 0.9846 GB/s → ≈ 984.6 MB/s
per lane, per direction, theoretical, before higher-layer overhead.
The central arithmetic is the decomposition: capacity grew by ≈1.969×, which is the rate ratio (1.600×) multiplied by the efficiency ratio (≈1.2308×). Gen3 delivered close to a doubling while asking the physical layer for only a 60% rate increase — the encoding change bought the rest more cheaply than another rate doubling would have.
The efficiency gain was not free. A far more efficient encoding still has to solve block alignment, distinguishing data from control, and obtaining suitable signal characteristics — generally with more sophisticated implementation, which is why Module 17 exists.
And efficiency at the wire is not efficiency at the application. Encoding overhead fell from 20% to ~1.5%; packet and control overhead did not change, so small-transaction workloads remain inefficient. Measuring the difference requires separating payload bytes from other transferred bytes.
Hold the model: Gen3 is a rate increase multiplied by an efficiency increase, and the second was the cheaper half.
15. What Comes Next
Gen3 established the efficient encoding that Gen4 and Gen5 retain unchanged — and that Gen6 eventually removes altogether, having taken encoding overhead about as far as it could usefully go. Chapter 5.4 — PCIe Gen4 therefore returns to a single-variable change — the rate doubles to 16 GT/s with the encoding unchanged — and takes up the question this chapter deferred: what actually becomes difficult when the signalling rate doubles. That is where the channel stops being background and becomes part of the architecture.
Revisit PCIe Gen2 for the baseline this chapter compares against, or Physical Layer for where encoding sits in the stack. Browse the full path on the PCIe tutorials index.