Skip to content
VLSI Mentor

DDR · Module 26

Why HBM Exists

Connections scale with perimeter while capability scales with area, and the ratio falls as 4/L. The width that answers it divides into sixteen streams that preserve the 64-byte granule exactly.

Chapter 4.8 already answers the question in this chapter's title, and it answers it well. It establishes that raising the per-pin rate is closed by energy, that adding pins is closed by geometry, that the bottleneck “stops being electrical and becomes geometric,” and that the response is to stack dies on an interposer and buy bandwidth with width instead of rate.

So this chapter does not re-argue any of that. What it does instead is take two things 4.8 states and does not finish.

First, 4.8 asserts the pin-count wall without quantifying it: “Die capability scales with area; external connections scale with perimeter. Those grow at different rates, and past a certain point width simply stops being available.” That is the right argument and it has an exact form. §1 computes it.

Second, 4.8's own scope callout defers the structure of the width, naming “the channel and pseudo-channel structure” as Module 26's. And the word pseudo-channel appears nowhere else in that chapter — it is deferred and never opened. §3 onward opens it, and §4 finds that the split preserves access granularity exactly, which is the result this chapter exists for.

1. The Pin-Count Wall Has an Exact Form

Chapter 4.8 says capability scales with area and connections with perimeter. Put a length on it and the wall stops being a slogan.

The model, stated: take a square die of side L. Its capability — how much logic or array it holds — scales with its area, . Its external connections, brought out around the edge at some fixed manufacturable pitch, scale with its perimeter, 4L.

DERIVED, recomputed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   connections per unit of capability  =  4L / L²  =  4 / L

So the ratio falls as one over the side length. Not slowly, not eventually — monotonically, from the first millimetre. DERIVED, with ILLUSTRATIVE side lengths:

Die side LArea Perimeter 4LConnections per unit capability
5 mm25 mm²20 mm0.800
10 mm100 mm²40 mm0.400
15 mm225 mm²60 mm0.267
20 mm400 mm²80 mm0.200

Doubling the side halves the connections available per unit of capability. Every row recomputed; the 5 mm and 10 mm rows differ by exactly a factor of two, as 4/L requires.

Three things this arithmetic makes visible that the assertion does not.

The wall is not a threshold, it is a gradient. There is no size at which perimeter routing suddenly fails. It degrades continuously, which is why the problem crept up over generations rather than arriving.

Getting a bigger die makes it worse. A larger die has more capability to feed and proportionally fewer edges to feed it through. So the usual answer to wanting more of something — build it bigger — is exactly the wrong move here, and that inversion is what makes the wall interesting rather than merely inconvenient.

And nothing about signalling changes the exponent. A cleverer interface can raise the connections-per-millimetre at a given pitch, which multiplies the numerator by a constant. It cannot turn 4/L into anything that does not fall. Chapter 4.8 puts this as “no amount of signalling cleverness resolves ‘there is nowhere to put more connections’”; the exponent is why.

2. What a Thousand Connections Are For

CURRICULUM-DERIVED, from 4.8 §2's published set: a 1024-bit wide interface, divided into 8 independent channels, at 2.4 Gb/s per pin, delivering up to about 307 GB/s.

4.8 verifies its own arithmetic and this chapter reproduces the check rather than trusting it. DERIVED, recomputed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1024 bits  x  2.4 Gb/s per pin  =  2457.6 Gb/s
                                    /  8 bits per byte
                                    =  307.2 GB/s        matches

So the headline figure is internally consistent — which is worth confirming because every later number in this chapter divides it.

Now the division 4.8 states. DERIVED:

LevelCountWidth each
Stack interface11024 bits
Channel8128 bits

1024 / 8 = 128, exactly. A channel is 128 bits wide, and that follows from two verified figures by division rather than needing a source of its own.

4.8 also explains why the channel count matters, and that argument is not repeated here: a single 1024-bit channel would have a granularity no requester could use, so the width is divided to keep each channel's granularity usable while the aggregate delivers the bandwidth. This chapter takes that as established and asks the question 4.8 defers — whether 128 bits is where the division stops.

3. Eight Channels, and Then Sixteen

It is not where it stops.

Under that model, the hierarchy has three levels. DERIVED:

LevelCount per stackWidth eachCheck
Stack interface11024 bits
Channel8128 bits8 × 128 = 1024
Pseudo-channel1664 bits16 × 64 = 1024

Both decompositions reconstruct the interface exactly, which is the minimum a hierarchy must do and is worth checking rather than assuming.

And the bandwidth divides the same way. DERIVED from §2's 307.2 GB/s:

LevelBandwidth each
Stack307.2 GB/s
Channel38.40 GB/s
Pseudo-channel19.20 GB/s

So a pseudo-channel is a 64-bit interface carrying about 19 GB/s, and there are sixteen of them on one stack.

4. The Split Preserves Granularity Exactly

Here is the result the chapter is for, and it is the same shape as a finding 25.2 §9 made about a different standard: a division that looks like it must cost something turns out to cost nothing at all.

The question. A channel is 128 bits and a pseudo-channel is 64. Halving the width ought to halve the data an access moves — and if it did, the split would be trading granularity for stream count, which is a real trade with a real cost.

The model, stated. Data is transferred on both clock edges, and the burst length differs between the two arrangements: a full-channel access uses a burst of 2, and a pseudo-channel access uses a burst of 4. That burst-length pairing is part of §3's REPORTED model.

DERIVED, recomputed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   full channel     :  128 bits x BL2 x 2 edges  =  512 bits  =  64 B
   pseudo-channel   :   64 bits x BL4 x 2 edges  =  512 bits  =  64 B

Identical. Five hundred and twelve bits either way.

So the split does not halve the access size. It halves the width and doubles the burst, and the product is preserved — which means:

Full channelPseudo-channel
Access granularity64 B64 B
Independent streams per stack816
Bandwidth per stream38.40 GB/s19.20 GB/s

The stream count doubles at no cost in granularity. That is a strictly better position on the one axis and an unchanged position on the other — which is unusual enough to be worth stating plainly, and it is why the mode exists.

5. Why the Two Halves Share a Command Bus

The split is not free of everything — it is free of granularity cost. What it costs is command bandwidth, and the reason is in §3's model: the two pseudo-channels share the channel's command bus.

Why share it at all? Because a command bus costs connections, and §1 established that connections are the scarce resource. Duplicating the command bus per pseudo-channel would spend the very thing the whole architecture exists to conserve — which is a different answer from the one 4.6 §2 reaches for DDR5 sub-channels, where each gets its own command path.

That contrast is instructive and it is not a contradiction. The two standards face different scarcities:

DDR5 sub-channelsHBM pseudo-channels
Command pathduplicated per sub-channelshared within a channel
Independencefull, in schedulingsemi — shared command, independent banks
Scarce resource being conservedconnection count

Chapter 4.6 §2 calls the duplicated command bus “the load-bearing one” of DDR5's duplications, because a shared one would make the two halves “one stream serving two data paths.” HBM accepts exactly that compromise at the pseudo-channel level and takes the win at the channel level instead — eight genuinely independent command buses, each shared by two halves.

So the hierarchy is independent at one level and semi-independent at the next, and knowing which is which is the whole practical content of the structure.

6. What “Semi-Independent” Actually Means

The phrase is doing real work and it decomposes into four specific statements. Under §3's REPORTED model:

ResourceShared or per-pseudo-channel
Command and address busshared within the channel
Clockshared
Command decode and executionindependent
Banksindependent

The consequence follows mechanically. Two pseudo-channels can have different rows open, be in different states, and serve unrelated addresses — because their banks and their decode are their own. But they cannot both receive a command in the same slot, because there is one bus and one slot.

So the contention is on command issue, not on command effect. That distinction is what §10's arbiter models and it is the source of the failure mode §15 describes: a controller that treats sixteen pseudo-channels as sixteen fully independent ports will over-subscribe eight command buses and discover it as unexplained latency rather than as an error.

And the arbitration is not the device's problem. A shared bus means the controller must choose which pseudo-channel gets each slot. Nothing in the memory resolves a conflict that the controller creates — it simply never sees the command it was not given.

7. The Three Quantities at This Level

Every decision in this chapter moves at least two of these, and the pattern is worth naming before the RTL.

DecisionBandwidthEnergy per bitIntegration cost
1024 bits instead of fewerbetterbetter — lower rate per pinworse26.2, 26.3
Dividing into 8 channelsbetter reachable, same peakunchangedworse — 8 command buses
Dividing into 16 pseudo-channelsbetter reachable, same peakunchangednear zero — the bus is shared

The third row is why the pseudo-channel split is the best bargain in the chapter. It buys reachable bandwidth for almost no integration cost, because it reuses a command bus that already exists — and it pays for that with command-slot contention, which is a controller problem rather than a packaging one.

And the first row is the one the rest of this module pays for. Widening the interface to 1024 bits improves bandwidth and energy per bit simultaneously — 4.8 establishes why the lower per-pin rate is the energy win — while worsening integration cost so severely that two further chapters exist to describe it. A trade that improves two quantities and worsens one is not a compromise; it is a decision about which currency you can afford to spend, and HBM spends the one that is paid once at manufacture rather than continuously in operation.

Nothing in this chapter improves energy per bit beyond what the width already bought. Chapter 4.8 owns that argument — a lower per-pin rate moves each bit more cheaply — and the divisions in §2 and §3 do not touch it. Leaving that column mostly unchanged is more honest than implying the hierarchy helps.

8. The Hierarchy as a Structure

How HBM's thousand-bit interface decomposes, in four rows. The top row holds the three things that set the interface: connecting through the die face so connections scale with area rather than perimeter, the per-pin rate of two point four gigabits per second, and the resulting thousand-and-twenty-four-bit interface. The second row holds the hierarchy: eight channels of one hundred twenty-eight bits each, sixteen pseudo-channels of sixty-four bits each, the command bus which is shared between the two pseudo-channels of a channel, and the banks which are independent per pseudo-channel. The third row holds the three consequences: the sixty-four byte access granule which is identical at both levels, the sixteen independent request streams, and the command-slot contention that sharing a bus creates. The bottom row is usable bandwidth, which every consequence feeds and which differs from the peak figure. Edges run from the face-connection and rate nodes into the interface, from the interface into the channels, from the channels into the pseudo-channels and the per-channel bandwidth, from the pseudo-channels into the shared command bus and the independent banks, and from all consequences into usable bandwidth.Connect via the facearea, not perimeter — §12.4 Gb/s per pin4.8's verified figure1024-bit interface307.2 GB/s — §238.40 GB/s / channel307.2 / 8 — §38 channels x 128bindependent — 4.8 §216 pseudo-ch x 64bREPORTED model — §3Shared command busone per channel — §5Independent banksper pseudo-channel — §664 B granuleidentical both levels —§416 streamsdouble, at no cost — §4Slot contentionwhat sharing costs — §6Usable bandwidthnot the peak — 4.8 §212

Two things the structure makes visible that the tables do not.

The two divisions produce different kinds of child. 8 channels yields independence; 16 pseudo-channels yields a shared command bus and independent banks — two arrows out, one to a shared resource and one to a private one. That fork is what “semi-independent” means, drawn rather than asserted.

And Slot contention is the only consequence with a negative sign. Granularity and stream count both feed usable bandwidth favourably; contention is the cost, and it enters from the shared bus alone. So the entire price of the pseudo-channel split arrives through one edge — which is why §10's arbiter is the chapter's load-bearing block.

9. The Organisation Invariant

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// hbm_channel_organisation -- holds the stack interface hierarchy and
// proves at elaboration that both decompositions reconstruct it.
//
// CLASSIFICATION: educational, synthesisable.
//
// PROVENANCE OF THE DEFAULTS:
//   TOTAL_BITS=1024, CHANNELS=8  -- CURRICULUM-DERIVED from 4.8 §2's
//     published set, which this curriculum verified.
//   PC_PER_CHANNEL=2             -- REPORTED (§3). NOT verified from a
//     primary source in this module. The block takes it as a
//     parameter precisely so the unverified part is visible and
//     replaceable rather than baked in.
//
// WHAT IT DOES NOT MODEL:
//   - the stack, the interposer, or TSVs (26.2 and 26.3)
//   - ANY physical dimension. §1's area-versus-perimeter arithmetic is
//     prose and stays prose: there is no millimetre in this file.
//   - any timing parameter, the array, or any access
//   - bandwidth. 4.8 §5's bandwidth_account owns that and is cited,
//     not reimplemented.
// ---------------------------------------------------------------------
module hbm_channel_organisation #(
  parameter int TOTAL_BITS     = 1024,   // CURRICULUM-DERIVED (4.8 §2)
  parameter int CHANNELS       = 8,      // CURRICULUM-DERIVED (4.8 §2)
  parameter int PC_PER_CHANNEL = 2,      // REPORTED (§3) -- see header

  // ── Derived. A caller that could override these could break the
  //    agreement the module exists to enforce.
  parameter int CH_BITS   = TOTAL_BITS / CHANNELS,
  parameter int PC_BITS   = CH_BITS / PC_PER_CHANNEL,
  parameter int PC_TOTAL  = CHANNELS * PC_PER_CHANNEL,
  // Indices into their own level.
  parameter int CH_IDX_W  = (CHANNELS <= 1) ? 1 : $clog2(CHANNELS),
  parameter int PC_IDX_W  = (PC_PER_CHANNEL <= 1) ? 1 : $clog2(PC_PER_CHANNEL),
  parameter int PCG_IDX_W = (PC_TOTAL <= 1) ? 1 : $clog2(PC_TOTAL),
  // A COUNT up to PC_TOTAL needs clog2(PC_TOTAL+1), not clog2(PC_TOTAL).
  parameter int PCCNT_W   = $clog2(PC_TOTAL + 1)
) (
  input  logic                  clk,
  input  logic                  rst_n,

  // ── Query: a global pseudo-channel index in, its coordinates out.
  input  logic                  q_valid,
  input  logic [PCG_IDX_W-1:0]  q_pc_global,

  output logic [CH_IDX_W-1:0]   q_channel,
  output logic [PC_IDX_W-1:0]   q_pc_in_channel,
  output logic                  q_in_range,

  // ── The organisation, published so consumers need not recompute it.
  output logic [15:0]           total_bits,
  output logic [7:0]            n_channels,
  output logic [PCCNT_W-1:0]    n_pseudo_channels,
  output logic [15:0]           channel_bits,
  output logic [15:0]           pc_bits,

  output logic                  err_pc_out_of_range,
  output logic                  any_error
);

  initial begin
    if (TOTAL_BITS < 2 || (TOTAL_BITS & (TOTAL_BITS - 1)) != 0)
      // The >= 2 bound matters: a power-of-two test ALONE accepts 0
      // and 1, either of which makes a clog2 zero and every field
      // width wrong. Module 24 shipped that hole once.
      $fatal(1, "hbm_channel_organisation: TOTAL_BITS must be a power of two >= 2");
    if (CHANNELS < 2 || (CHANNELS & (CHANNELS - 1)) != 0)
      $fatal(1, "hbm_channel_organisation: CHANNELS must be a power of two >= 2");
    if (PC_PER_CHANNEL < 1 || (PC_PER_CHANNEL & (PC_PER_CHANNEL - 1)) != 0)
      $fatal(1, "hbm_channel_organisation: PC_PER_CHANNEL must be a power of two >= 1");
    // §2's check: the channels must reconstruct the interface exactly.
    if ((CH_BITS * CHANNELS) != TOTAL_BITS)
      $fatal(1, "hbm_channel_organisation: %0d channels x %0d bits != %0d",
             CHANNELS, CH_BITS, TOTAL_BITS);
    // §3's check: so must the pseudo-channels.
    if ((PC_BITS * PC_TOTAL) != TOTAL_BITS)
      $fatal(1, "hbm_channel_organisation: %0d pseudo-channels x %0d bits != %0d",
             PC_TOTAL, PC_BITS, TOTAL_BITS);
    if ((PC_BITS * PC_PER_CHANNEL) != CH_BITS)
      $fatal(1, "hbm_channel_organisation: pseudo-channels do not tile a channel");
    if (PCG_IDX_W != $clog2(PC_TOTAL))
      $fatal(1, "hbm_channel_organisation: global index must address PC_TOTAL");
    if ((CH_IDX_W + PC_IDX_W) != PCG_IDX_W)
      // The global index must decompose losslessly into a channel
      // index and a within-channel index. P1 asserts it at runtime;
      // this is the width precondition that makes it possible.
      $fatal(1, "hbm_channel_organisation: index widths do not compose (%0d + %0d != %0d)",
             CH_IDX_W, PC_IDX_W, PCG_IDX_W);
  end

  logic e_range;

  // Declared above the continuous assigns that read it.
  logic in_rng;
  assign in_rng = q_valid &&
                  ({{(32-PCG_IDX_W){1'b0}}, q_pc_global} < 32'(PC_TOTAL));

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) e_range <= 1'b0;
    else        e_range <= q_valid && !in_rng;
  end

  // The channel is the high field and the within-channel index the
  // low field, so consecutive global indices stay within a channel.
  assign q_channel       = q_pc_global[PCG_IDX_W-1 -: CH_IDX_W];
  assign q_pc_in_channel = q_pc_global[PC_IDX_W-1 : 0];
  assign q_in_range      = in_rng;

  assign total_bits        = 16'(TOTAL_BITS);
  assign n_channels        = 8'(CHANNELS);
  assign n_pseudo_channels = PCCNT_W'(PC_TOTAL);
  assign channel_bits      = 16'(CH_BITS);
  assign pc_bits           = 16'(PC_BITS);

  assign err_pc_out_of_range = e_range;
  assign any_error = e_range;
endmodule

Every elaboration check is a check §2 or §3 did by hand, and that is the block's purpose: three published or reported figures describe one hierarchy, and the module refuses to build unless they agree.

The index-width composition check is the subtle one. A global pseudo-channel index must split losslessly into a channel index plus a within-channel index, which requires CH_IDX_W + PC_IDX_W == PCG_IDX_W. That holds for the default parameterisation and fails for plenty of plausible ones — and without the check, the decomposition would silently drop or alias a bit.

10. The Pseudo-Channel Command Arbiter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// pseudo_channel_cmd_arbiter -- one command bus, two pseudo-channels
// that decode independently. Decides which one is issued to, and
// records what the other lost.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHY THIS IS THE CHAPTER'S LOAD-BEARING BLOCK: §8's diagram shows
// every benefit of the pseudo-channel split arriving free and its
// entire cost arriving through one edge -- the shared command bus.
// This is that edge.
//
// THE ARRANGEMENT IS REPORTED, NOT VERIFIED (§3): two pseudo-channels
// sharing a channel's command bus and clock, decoding independently,
// with a bank-address bit selecting between them.
//
// WHAT IT DOES NOT MODEL:
//   - the device, any command encoding, or any timing parameter
//   - the banks, or what a command DOES once issued. §6: contention
//     is on command ISSUE, not on command EFFECT.
//   - the controller's scheduling POLICY. This block resolves a slot;
//     choosing which requester deserves it is a policy question
//     Module 23 owns for a different standard and 26.4 revisits.
// ---------------------------------------------------------------------
module pseudo_channel_cmd_arbiter #(
  parameter int PC_PER_CHANNEL = 2,      // REPORTED (§3)
  parameter int PC_IDX_W = (PC_PER_CHANNEL <= 1) ? 1 : $clog2(PC_PER_CHANNEL),
  parameter int CNT_W    = 24
) (
  input  logic                      clk,
  input  logic                      rst_n,

  // ── One request per pseudo-channel, asserted independently.
  input  logic [PC_PER_CHANNEL-1:0] pc_req,
  // Whether each pseudo-channel is able to accept a command now. A
  // pseudo-channel busy with its own banks cannot use a slot.
  input  logic [PC_PER_CHANNEL-1:0] pc_ready,

  // ── The single shared bus. One grant per slot, at most.
  output logic                      slot_granted,
  output logic [PC_IDX_W-1:0]       grant_pc,
  output logic [PC_PER_CHANNEL-1:0] grant_onehot,

  // ── What sharing costs, measured. §6: the controller never sees
  //    the command it was not given, so the loss must be counted
  //    here or it is invisible everywhere.
  output logic [CNT_W-1:0]          cnt_slots,
  output logic [CNT_W-1:0]          cnt_granted,
  output logic [CNT_W-1:0]          cnt_contended,
  output logic [CNT_W-1:0]          cnt_idle_slots,
  output logic [CNT_W-1:0]          cnt_deferred [PC_PER_CHANNEL],

  // Longest run of consecutive slots a pseudo-channel was denied --
  // the fairness quantity a round-robin is supposed to bound.
  output logic [15:0]               worst_starve_run,
  output logic                      fairness_bound_exceeded,

  output logic                      err_grant_to_unready,
  output logic                      err_multiple_grants,
  output logic                      any_error
);

  initial begin
    if (PC_PER_CHANNEL < 2)
      // With one pseudo-channel there is nothing to arbitrate, and a
      // block that "arbitrates" a single requester would imply a
      // contention cost that does not exist.
      $fatal(1, "pseudo_channel_cmd_arbiter: need at least 2 pseudo-channels");
    if (PC_IDX_W != $clog2(PC_PER_CHANNEL))
      $fatal(1, "pseudo_channel_cmd_arbiter: PC_IDX_W must address PC_PER_CHANNEL");
  end

  logic [PC_IDX_W-1:0]  rr_ptr;
  logic [CNT_W-1:0]     n_slot, n_grant, n_cont, n_idle;
  logic [CNT_W-1:0]     n_def [PC_PER_CHANNEL];
  logic [15:0]          run [PC_PER_CHANNEL];
  logic [15:0]          worst;
  logic                 e_unready, e_multi;

  // ── Only a pseudo-channel that is both requesting AND ready can be
  //    granted. Declared above the continuous assigns that read it.
  logic [PC_PER_CHANNEL-1:0] eligible;
  assign eligible = pc_req & pc_ready;

  // Round-robin from the pointer. Combinational so the grant is
  // available in the same slot the request is presented.
  logic [PC_IDX_W-1:0]       sel;
  logic                      have_sel;
  always_comb begin
    sel      = '0;
    have_sel = 1'b0;
    for (int k = 0; k < PC_PER_CHANNEL; k++) begin
      // Walk from rr_ptr forwards, wrapping, and take the first
      // eligible requester. Fixed priority would starve one half
      // indefinitely, which P7 forbids.
      int unsigned idx;
      idx = (int unsigned'(rr_ptr) + k) % PC_PER_CHANNEL;
      if (!have_sel && eligible[idx]) begin
        sel      = PC_IDX_W'(idx);
        have_sel = 1'b1;
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rr_ptr <= '0;
      n_slot <= '0; n_grant <= '0; n_cont <= '0; n_idle <= '0;
      worst <= '0; e_unready <= 1'b0; e_multi <= 1'b0;
      for (int i = 0; i < PC_PER_CHANNEL; i++) begin
        n_def[i] <= '0; run[i] <= '0;
      end
    end else begin
      e_unready <= 1'b0; e_multi <= 1'b0;

      if (n_slot != {CNT_W{1'b1}}) n_slot <= n_slot + CNT_W'(1);

      // More than one requester eligible in one slot is the
      // contention §6 names -- counted whether or not it is resolved.
      if ($countones(eligible) > 1) begin
        if (n_cont != {CNT_W{1'b1}}) n_cont <= n_cont + CNT_W'(1);
      end

      if (have_sel) begin
        if (n_grant != {CNT_W{1'b1}}) n_grant <= n_grant + CNT_W'(1);
        // Advance past the granted index so the next slot favours the
        // other half.
        rr_ptr <= PC_IDX_W'((int unsigned'(sel) + 1) % PC_PER_CHANNEL);
      end else begin
        if (n_idle != {CNT_W{1'b1}}) n_idle <= n_idle + CNT_W'(1);
      end

      // ── Per-pseudo-channel deferral and starvation runs.
      for (int i = 0; i < PC_PER_CHANNEL; i++) begin
        if (pc_req[i] && !(have_sel && (sel == PC_IDX_W'(i)))) begin
          if (n_def[i] != {CNT_W{1'b1}}) n_def[i] <= n_def[i] + CNT_W'(1);
          if (run[i] != 16'hFFFF) begin
            run[i] <= run[i] + 16'd1;
            // Compare the INCREMENTED value combinationally: run[i]'s
            // write lands on this edge, so reading run[i] here would
            // test the pre-increment value and under-report the peak
            // by one. This is the stale-register hazard Module 21 §10
            // and 25.2 §15 both name.
            if ((run[i] + 16'd1) > worst) worst <= run[i] + 16'd1;
          end
        end else if (have_sel && (sel == PC_IDX_W'(i))) begin
          run[i] <= '0;
        end
      end

      // A grant to a pseudo-channel that is not ready would be a
      // command the device cannot accept.
      if (have_sel && !pc_ready[sel]) e_unready <= 1'b1;
    end
  end

  assign slot_granted = have_sel;
  assign grant_pc     = sel;
  always_comb begin
    grant_onehot = '0;
    if (have_sel) grant_onehot[sel] = 1'b1;
  end

  assign cnt_slots     = n_slot;
  assign cnt_granted   = n_grant;
  assign cnt_contended = n_cont;
  assign cnt_idle_slots = n_idle;
  assign cnt_deferred  = n_def;
  assign worst_starve_run = worst;
  // A round-robin over N requesters should never defer one for more
  // than N-1 consecutive slots in which it was eligible.
  assign fairness_bound_exceeded = (worst > 16'(PC_PER_CHANNEL - 1));
  assign err_grant_to_unready = e_unready;
  assign err_multiple_grants  = e_multi;
  assign any_error = e_unready | e_multi;
endmodule

11. The Hazard Sharing Creates

One line of §10 carries §6's argument and it is worth isolating because the mistake is invisible in the common case.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if ((run[i] + 16'd1) > worst) worst <= run[i] + 16'd1;

The incremented value is compared, not the register. run[i]'s write lands on this same edge, so testing run[i] > worst would compare the pre-increment value and under-report the worst starvation run by exactly one, every time.

Why that is hard to notice: the peak is still tracked, still monotone, still plausible — just always one short. A fairness bound of N-1 would then appear satisfied at exactly the run length that violates it, which is the one case the bound exists to catch.

This is the fourth appearance of the same defect shape in this curriculum — Module 21 §10's search engines, 24.3 §12's CA decoder, 25.2 §15's command framer, and now a starvation counter. In all four, every assertion about the mechanism passes and only the value is wrong. The shape is worth recognising on sight: a value read in the cycle a sequential write to it lands.

12. The Stream-Demand Monitor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// pc_stream_monitor -- how many pseudo-channels are actually carrying
// traffic, and how the idle ones are distributed.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHY IT EXISTS: 4.8 §2 states that "307 GB/s requires traffic that
// keeps all eight channels busy" and that "one channel's worth of
// traffic gets one channel's worth of bandwidth". §4 doubles the
// relevant count to sixteen. That is a claim about a STREAM, so it
// needs measuring rather than deriving -- and 4.8's bandwidth_account
// computes the PEAK, which is exactly the number this does not.
//
// WHAT IT DOES NOT MODEL:
//   - bandwidth in bytes per second (4.8 §5 owns it, cited not rebuilt)
//   - any timing parameter, the array, or the interposer
//   - WHY a stream is idle. It attributes idleness to a
//     pseudo-channel; whether the cause is the requester, the
//     mapping or the arbiter is outside this block.
//
// NO DIVISION: occupancy is published as numerator and denominator so
// the caller states its own denominator.
// ---------------------------------------------------------------------
module pc_stream_monitor #(
  parameter int PC_TOTAL = 16,           // §3: 8 channels x 2
  parameter int CHANNELS = 8,            // CURRICULUM-DERIVED (4.8 §2)
  parameter int PC_IDX_W = (PC_TOTAL <= 1) ? 1 : $clog2(PC_TOTAL),
  // A COUNT up to PC_TOTAL needs clog2(PC_TOTAL+1).
  parameter int PCCNT_W  = $clog2(PC_TOTAL + 1),
  parameter int CHCNT_W  = $clog2(CHANNELS + 1),
  parameter int CNT_W    = 28
) (
  input  logic                  clk,
  input  logic                  rst_n,

  // One bit per pseudo-channel: carrying traffic this cycle.
  input  logic [PC_TOTAL-1:0]   pc_active,

  // ── Instantaneous.
  output logic [PCCNT_W-1:0]    n_active,
  output logic [PCCNT_W-1:0]    n_idle,
  // Channels with at least one active pseudo-channel. A channel whose
  // two halves are both idle is a wholly unused command bus.
  output logic [CHCNT_W-1:0]    n_channels_touched,

  // ── Occupancy, as a PAIR. Never a percentage.
  output logic [CNT_W-1:0]      pc_active_cycles,
  output logic [CNT_W-1:0]      pc_slot_cycles,

  // ── Peaks and the reachability question.
  output logic [PCCNT_W-1:0]    peak_active,
  output logic                  all_streams_seen,
  output logic [CNT_W-1:0]      cy_single_stream,
  output logic [CNT_W-1:0]      cy_fully_idle,

  // The failure 4.8 §2 describes: plenty of width, one stream using
  // it. Reported as a state rather than inferred from a ratio.
  output logic                  width_wasted_now,

  output logic                  err_active_out_of_range,
  output logic                  any_saturated
);

  initial begin
    if (PC_TOTAL < 2)
      $fatal(1, "pc_stream_monitor: need at least 2 pseudo-channels");
    if (CHANNELS < 1 || (PC_TOTAL % CHANNELS) != 0)
      // Pseudo-channels must tile the channels evenly, or "channels
      // touched" cannot be computed from the active mask at all.
      $fatal(1, "pc_stream_monitor: %0d pseudo-channels do not tile %0d channels",
             PC_TOTAL, CHANNELS);
    if (PCCNT_W <= PC_IDX_W)
      // COUNT vs INDEX: a count up to PC_TOTAL needs one more bit
      // than an index into it when PC_TOTAL is a power of two. Too
      // narrow and n_active can never represent "all of them", so
      // all_streams_seen becomes unreachable -- the silent form.
      $fatal(1, "pc_stream_monitor: PCCNT_W too narrow to count PC_TOTAL");
  end

  localparam int PC_PER_CH = PC_TOTAL / CHANNELS;

  logic [CNT_W-1:0]  c_act, c_slot, c_single, c_allidle;
  logic [PCCNT_W-1:0] pk;
  logic              seen_all, sat;
  logic              e_rng;

  // Declared above the continuous assigns that read them.
  logic [PCCNT_W-1:0] n_act_c;
  assign n_act_c = PCCNT_W'($countones(pc_active));

  logic [CHCNT_W-1:0] n_touch_c;
  always_comb begin
    n_touch_c = '0;
    for (int ch = 0; ch < CHANNELS; ch++) begin
      logic any_in_ch;
      any_in_ch = 1'b0;
      for (int k = 0; k < PC_PER_CH; k++)
        if (pc_active[ch*PC_PER_CH + k]) any_in_ch = 1'b1;
      if (any_in_ch) n_touch_c = n_touch_c + CHCNT_W'(1);
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_act <= '0; c_slot <= '0; c_single <= '0; c_allidle <= '0;
      pk <= '0; seen_all <= 1'b0; sat <= 1'b0; e_rng <= 1'b0;
    end else begin
      e_rng <= 1'b0;

      if (c_slot == {CNT_W{1'b1}}) sat <= 1'b1;
      else begin
        c_slot <= c_slot + CNT_W'(1);
        // Accumulate pseudo-channel-cycles, not cycles: the quantity
        // that matters is how much of the width was used.
        c_act  <= c_act + CNT_W'(n_act_c);
      end

      if (n_act_c > pk) pk <= n_act_c;
      if (n_act_c == PCCNT_W'(PC_TOTAL)) seen_all <= 1'b1;
      if (n_act_c == PCCNT_W'(1)) begin
        if (c_single != {CNT_W{1'b1}}) c_single <= c_single + CNT_W'(1);
      end
      if (n_act_c == '0) begin
        if (c_allidle != {CNT_W{1'b1}}) c_allidle <= c_allidle + CNT_W'(1);
      end
    end
  end

  assign n_active            = n_act_c;
  assign n_idle              = PCCNT_W'(PC_TOTAL) - n_act_c;
  assign n_channels_touched  = n_touch_c;
  assign pc_active_cycles    = c_act;
  // The denominator a caller needs: slots x pseudo-channels.
  assign pc_slot_cycles      = c_slot * CNT_W'(PC_TOTAL);
  assign peak_active         = pk;
  assign all_streams_seen    = seen_all;
  assign cy_single_stream    = c_single;
  assign cy_fully_idle       = c_allidle;
  // 4.8 §2's failure, named: traffic present, but on one stream.
  assign width_wasted_now    = (n_act_c == PCCNT_W'(1));
  assign err_active_out_of_range = e_rng;
  assign any_saturated = sat;
endmodule

pc_active_cycles accumulates pseudo-channel-cycles rather than cycles, and the distinction is the whole measurement. A cycle in which one stream is busy and a cycle in which sixteen are busy are both “a busy cycle”; only weighting by the number of active streams distinguishes a system using its width from one wasting it.

And n_channels_touched is separate from n_active. Eight active pseudo-channels could be two per channel across four channels, or one per channel across eight — the same count, very different use of eight command buses. §6 established that the command bus is the shared resource, so the channel-level count is the one that speaks to contention.

13. What the Assertions Prove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bind unit note: P1-P4 reference hbm_channel_organisation's
// internals, P5-P9 pseudo_channel_cmd_arbiter's, P10-P14
// pc_stream_monitor's. Each is written as though bound into its
// own module.

// P1 -- the global index decomposes losslessly. §9's width check is
// the precondition; this is the property it enables.
property p_index_decomposition_is_lossless;
  @(posedge clk) disable iff (!rst_n)
    q_in_range |-> ({q_channel, q_pc_in_channel} == q_pc_global);
endproperty
assert property (p_index_decomposition_is_lossless);

// P2 -- §2 and §3's identities, as runtime invariants: both levels
// reconstruct the interface exactly.
property p_both_levels_reconstruct_the_interface;
  @(posedge clk) disable iff (!rst_n)
    ((16'(n_channels) * channel_bits == total_bits) &&
     (16'(n_pseudo_channels) * pc_bits == total_bits));
endproperty
assert property (p_both_levels_reconstruct_the_interface);

// P3 -- the pseudo-channel count is the channel count times the
// per-channel count. Guards the COUNT-vs-INDEX trap: a too-narrow
// PCCNT_W would wrap and silently report fewer streams.
property p_pc_count_is_the_product;
  @(posedge clk) disable iff (!rst_n)
    (n_pseudo_channels == PCCNT_W'(CHANNELS * PC_PER_CHANNEL));
endproperty
assert property (p_pc_count_is_the_product);

// P4 -- FORBIDDEN. An out-of-range index is never reported in range,
// and is always flagged.
property p_out_of_range_never_accepted;
  @(posedge clk) disable iff (!rst_n)
    (q_valid && ({{(32-PCG_IDX_W){1'b0}}, q_pc_global} >= 32'(PC_TOTAL)))
      |-> (!q_in_range ##1 err_pc_out_of_range);
endproperty
assert property (p_out_of_range_never_accepted);

// P5 -- FORBIDDEN. At most one pseudo-channel is granted per slot.
// §6: there is one bus and one slot, and two grants would model a
// bus that does not exist.
property p_at_most_one_grant_per_slot;
  @(posedge clk) disable iff (!rst_n)
    $onehot0(grant_onehot);
endproperty
assert property (p_at_most_one_grant_per_slot);

// P6 -- FORBIDDEN. A grant never goes to a pseudo-channel that is
// not both requesting and ready.
property p_grant_only_to_eligible;
  @(posedge clk) disable iff (!rst_n)
    slot_granted |-> ((pc_req[grant_pc]) && (pc_ready[grant_pc]));
endproperty
assert property (p_grant_only_to_eligible);

// P7 -- FORBIDDEN. No eligible pseudo-channel is deferred for more
// than PC_PER_CHANNEL-1 consecutive slots. §10: the fairness bound a
// round-robin exists to provide, and the property the stale-register
// bug would let pass at exactly the violating run length.
property p_round_robin_bounds_starvation;
  @(posedge clk) disable iff (!rst_n)
    !fairness_bound_exceeded;
endproperty
assert property (p_round_robin_bounds_starvation);

// P8 -- a slot with any eligible requester always produces a grant.
// Rules out an arbiter that idles a usable slot, which would waste
// the scarce resource §5 says the whole architecture conserves.
property p_eligible_slot_is_never_wasted;
  @(posedge clk) disable iff (!rst_n)
    (((pc_req & pc_ready) != '0) |-> slot_granted);
endproperty
assert property (p_eligible_slot_is_never_wasted);

// P9 -- every slot is accounted exactly once as granted or idle.
property p_slots_are_partitioned;
  @(posedge clk) disable iff (!rst_n)
    ((cnt_granted + cnt_idle_slots) == cnt_slots);
endproperty
assert property (p_slots_are_partitioned);

// P10 -- active and idle stream counts partition the pseudo-channels.
property p_stream_counts_partition;
  @(posedge clk) disable iff (!rst_n)
    ((n_active + n_idle) == PCCNT_W'(PC_TOTAL));
endproperty
assert property (p_stream_counts_partition);

// P11 -- channels touched never exceeds the channel count, and is
// zero exactly when nothing is active.
property p_channels_touched_is_consistent;
  @(posedge clk) disable iff (!rst_n)
    ((n_channels_touched <= CHCNT_W'(CHANNELS)) &&
     ((n_active == '0) == (n_channels_touched == '0)));
endproperty
assert property (p_channels_touched_is_consistent);

// P12 -- channels touched never exceeds active streams. Each active
// pseudo-channel touches exactly one channel, so the channel count
// cannot be the larger of the two.
property p_touched_never_exceeds_active;
  @(posedge clk) disable iff (!rst_n)
    (CHCNT_W'(n_channels_touched) <= CHCNT_W'(n_active));
endproperty
assert property (p_touched_never_exceeds_active);

// P13 -- the peak is monotone, and all_streams_seen is sticky once
// every stream has been simultaneously active.
property p_peak_and_seen_are_monotone;
  @(posedge clk) disable iff (!rst_n)
    ((peak_active >= $past(peak_active, 1)) &&
     ($past(all_streams_seen, 1) |-> all_streams_seen));
endproperty
assert property (p_peak_and_seen_are_monotone);

// P14 -- 4.8 §2's failure state, as an invariant: width_wasted_now
// holds exactly when precisely one stream is carrying traffic.
property p_width_wasted_means_one_stream;
  @(posedge clk) disable iff (!rst_n)
    (width_wasted_now == (n_active == PCCNT_W'(1)));
endproperty
assert property (p_width_wasted_means_one_stream);

// ── Cover.
// Every channel and every within-channel index reached.
cover property (@(posedge clk) disable iff (!rst_n)
                  q_in_range && (q_channel == '0));
cover property (@(posedge clk) disable iff (!rst_n)
                  q_in_range && (q_channel == CH_IDX_W'(CHANNELS - 1)));
cover property (@(posedge clk) disable iff (!rst_n)
                  q_in_range && (q_pc_in_channel == PC_IDX_W'(PC_PER_CHANNEL - 1)));
// Each forbidden condition ATTEMPTED -- vacuous otherwise.
cover property (@(posedge clk) disable iff (!rst_n) err_pc_out_of_range);
cover property (@(posedge clk) disable iff (!rst_n) err_grant_to_unready);
// CONTENTION actually occurring: both halves eligible in one slot.
// §6 says this is the entire cost of the split, so a suite that never
// reaches it has not exercised the chapter's subject.
cover property (@(posedge clk) disable iff (!rst_n) cnt_contended != '0);
cover property (@(posedge clk) disable iff (!rst_n)
                  $countones(pc_req & pc_ready) == PC_PER_CHANNEL);
// A requester deferred, and then served -- the round-robin working.
cover property (@(posedge clk) disable iff (!rst_n)
                  (cnt_deferred[0] != '0) && (cnt_deferred[1] != '0));
// An idle slot with a request pending but not ready.
cover property (@(posedge clk) disable iff (!rst_n)
                  (pc_req != '0) && ((pc_req & pc_ready) == '0) && !slot_granted);
// THE REACHABILITY COVERS. All sixteen streams simultaneously active,
// and the one-stream waste 4.8 §2 warns about.
cover property (@(posedge clk) disable iff (!rst_n) all_streams_seen);
cover property (@(posedge clk) disable iff (!rst_n) width_wasted_now);
cover property (@(posedge clk) disable iff (!rst_n) cy_fully_idle != '0);
// Eight active streams spread across eight channels versus concentrated
// in four -- the same count, different command-bus pressure (§15).
cover property (@(posedge clk) disable iff (!rst_n)
                  (n_active == PCCNT_W'(8)) && (n_channels_touched == CHCNT_W'(8)));
cover property (@(posedge clk) disable iff (!rst_n)
                  (n_active == PCCNT_W'(8)) && (n_channels_touched == CHCNT_W'(4)));

14. Corner Cases

CaseBehaviourWhy
TOTAL_BITS of 0 or 1$fatal at elaborationA power-of-two test alone accepts both
Channels that do not divide the interface$fatal at elaboration§2's identity
Pseudo-channels that do not tile a channel$fatal at elaboration§3's identity
Index widths that do not compose$fatal at elaborationThe decomposition would alias a bit — P1
PC_PER_CHANNEL of 1$fatal in the arbiterNothing to arbitrate; implies a cost that does not exist
Global index ≥ PC_TOTALReported; never in rangeP4
Both halves eligible in one slotOne grant; the other deferred and countedP5 — one bus, one slot
Neither half ready, both requestingNo grant; slot counted idleThe slot is genuinely unusable
One half never requestingRound-robin degenerates to the otherCorrect, and cnt_deferred shows it
A half eligible on consecutive slotsDeferred at most N−1 in a rowP7's fairness bound
Starvation run compared pre-incrementPeak under-reported by one§11's hazard; the bound passes at the violating length
Eligible slot left ungrantedP8 firesWastes the resource §5 says is scarce
All 16 streams activeall_streams_seen stickyThe reachability cover
Exactly one stream activewidth_wasted_now4.8 §2's failure, named
8 streams in 4 channels vs 8 channelsSame n_active, different n_channels_touched§15 — different command-bus pressure
Nothing active at allcy_fully_idle rises; not an errorAn idle memory is idle, not broken
PCCNT_W too narrow for PC_TOTAL$fatal at elaborationall_streams_seen would be unreachable — the silent form

Rows fourteen and fifteen are the pair worth studying together. Eight active streams is not a single condition — spread one per channel it uses eight command buses lightly; concentrated two per channel in four channels it saturates four and leaves four idle. The stream count alone cannot tell those apart, which is why §12 publishes both.

15. DV — Two Representations of One Hierarchy

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Independent hierarchy reference. Builds the pseudo-channel list
// explicitly as a table of (channel, index-within-channel) pairs
// rather than by field arithmetic, so agreement with the DUT is
// evidence rather than a restatement of the same shift.
// SIMULATION-ONLY.
class hbm_hierarchy_ref;
  int ch_of[$];        // ch_of[global] = its channel
  int idx_of[$];       // idx_of[global] = its index within that channel
  int total_bits, channels, pc_per_ch;

  function void build(int bits, int n_ch, int n_pc);
    total_bits = bits; channels = n_ch; pc_per_ch = n_pc;
    ch_of.delete(); idx_of.delete();
    for (int c = 0; c < n_ch; c++)
      for (int p = 0; p < n_pc; p++) begin
        ch_of.push_back(c);
        idx_of.push_back(p);
      end
  endfunction

  function int n_pseudo(); return ch_of.size(); endfunction
  function int channel_bits(); return total_bits / channels; endfunction
  function int pc_bits(); return channel_bits() / pc_per_ch; endfunction

  // §2 and §3's identities, computed independently of the DUT.
  function bit levels_reconstruct();
    return (channel_bits() * channels == total_bits) &&
           (pc_bits() * ch_of.size() == total_bits);
  endfunction

  // §4's granularity identity, as a pair rather than a single number.
  function int access_bits(int width, int burst_len);
    return width * burst_len * 2;          // both clock edges
  endfunction

  function bit granularity_is_preserved();
    return access_bits(channel_bits(), 2) == access_bits(pc_bits(), 4);
  endfunction
endclass
CheckWhat it establishes
Every global index's (channel, index) matches the reference tableP1 against a non-arithmetic representation
Sweep all 16 indices; confirm both channel extremes and both halvesThe three organisation covers
Reference levels_reconstruct() against the DUT's published widths§2 and §3's identities, independently computed
Reference granularity_is_preserved()§4's result, recomputed outside the DUT
Parameterise TOTAL_BITS as 0 and 1The guard a power-of-two test alone would pass
Parameterise channels that do not divide the interfaceElaboration fails rather than truncating
Narrow the index widths by one bitElaboration fails rather than aliasing
Drive both halves eligible every slot for many slotsAlternating grants; neither deferred twice in a row
Drive one half onlycnt_deferred for the other stays at zero
Drive requests with pc_ready lowIdle slots counted; P8 not violated (nothing eligible)
Replace the incremented compare with run[i] > worstworst_starve_run one short; P7 passes at the violating length
Drive all 16 streams active simultaneouslyall_streams_seen; the reachability cover
Drive exactly one stream for a long runwidth_wasted_now; cy_single_stream rises
Drive 8 streams one-per-channel, then 8 streams in 4 channelsSame n_active, n_channels_touched 8 then 4

The eleventh and thirteenth checks together produce the report worth publishing, because both are cases where every property passes and the system is wrong:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  TWO WAYS TO PASS EVERYTHING AND BE WRONG

  (A) the starvation counter that is always one short
      Replace the combinational compare with  run[i] > worst.

      stimulus : both halves eligible every slot, fixed-priority
                 arbiter substituted so half 1 is never granted.
      true worst starvation run          : 2 consecutive slots
      worst_starve_run reports           : 1
      fairness_bound_exceeded (> N-1 = 1): FALSE

      P7 round_robin_bounds_starvation   : PASS
      every other property               : PASS

      diagnosis : the bound is N-1 = 1, the real run is 2, and the
        counter reports 1. The property is evaluated against a value
        that is wrong by exactly the margin that would have failed
        it. A starvation bound off by one is not a small error --
        it is the difference between detecting unfairness and
        certifying it.

  (B) sixteen streams "active", four command buses idle
      stimulus : 8 pseudo-channels active, but both halves of only
                 four channels.

      n_active                 8
      n_channels_touched       4     <-- half the command buses unused
      peak_active              8
      all_streams_seen         0
      every property           PASS

      diagnosis : nothing is illegal. Eight streams are running, and
        4.8 §2's advice to "keep the channels busy" appears
        satisfied by a count of eight. But four of eight command
        buses are idle while the other four carry two halves each --
        maximum contention on half the resource and none on the
        rest.

      what makes it dangerous : the stream count is the number a
        tuning exercise naturally watches, and it is identical to
        the healthy case. Only n_channels_touched distinguishes
        them, and a design that does not publish it cannot see the
        difference.

16. Debugging

SymptomLikely causeHow to confirm
Design will not elaborateA level does not reconstruct the interfaceThe $fatal names the computed product
Index decomposition aliasesIndex widths do not composeP1; the elaboration guard catches it first
Fewer streams reported than existPCCNT_W too narrow — COUNT vs INDEXP3; all_streams_seen unreachable
Bandwidth far below the headline figureToo few streams supplied — 4.8 §2n_active against PC_TOTAL, not the byte count
Stream count healthy, bandwidth still lowStreams concentrated in few channels — §15n_channels_touched against n_active
Unexplained latency on one pseudo-channelCommand-slot contention with its siblingcnt_deferred for that half; cnt_contended
One half consistently slower than the otherFixed priority instead of round-robincnt_deferred asymmetric; worst_starve_run
Fairness bound never trips despite visible unfairnessStarvation run compared pre-increment — §11worst_starve_run exactly one below the real peak
Command slots idle while requests pendRequesters not ready — not an arbiter faultpc_req against pc_ready; P8 holds
A pseudo-channel gets commands it cannot useGrant to an unready halferr_grant_to_unready; P6
Peak bandwidth assumed reachable with one requesterIt never is, at any widthwidth_wasted_now; §4's callout

Row five is the entry this chapter exists to add. A healthy stream count with poor bandwidth points at how the streams are distributed across command buses, not at how many there are — and no byte-counting measurement reveals it.

Row eight is the one that certifies a fault as a pass. A starvation bound computed from a pre-increment read is wrong by exactly the amount that would have failed it.

17. Misconceptions

“The pin-count wall is a threshold.” §1. It is a gradient — connections per unit capability fall as 4/L, monotonically, from the first millimetre.

“A bigger die gets more connections.” §1. It gets more connections and proportionally fewer per unit of capability. Doubling the side halves the ratio.

“Better signalling could have avoided HBM.” §1. Better signalling multiplies the numerator by a constant. It cannot change 4/L into something that does not fall.

“HBM's interface is one 1024-bit channel.” §2. Eight channels of 128 bits — and 4.8 §2 explains that a single 1024-bit channel would have an unusable granularity.

“The division stops at eight channels.” §3. Each channel divides again into two 64-bit pseudo-channels, giving sixteen streams per stack.

“Halving the width halves the access size.” §4. It does not: 128 bits at burst 2 and 64 bits at burst 4 are both 512 bits. The granule is 64 B either way.

“Pseudo-channels are fully independent.” §6. Semi-independent: independent decode and independent banks, but a shared command bus and clock.

“Sixteen streams means sixteen command ports.” §6. Eight command buses, each shared by two halves. A controller that assumes sixteen will over-subscribe eight.

“HBM shares the command bus because DDR5 does.” §5. The opposite — 4.6 §2 duplicates it per DDR5 sub-channel and calls that the load-bearing duplication. HBM shares it because connections are the resource the whole architecture exists to conserve.

“Keeping the channel count busy is enough.” §15. Eight active streams concentrated in four channels leaves four command buses idle and saturates four. The count is identical to the healthy case.

“This chapter verified HBM's pseudo-channel structure.” The provenance callout and §3. It did not. No primary source was obtainable, the structure is REPORTED, and everything derived from it inherits that status.

18. Interview Reasoning

Why can't a conventional package supply enough connections? Because capability scales with die area and edge connections with perimeter, so connections per unit capability go as 4/L — it falls monotonically, and doubling the die side halves it.

Why doesn't a better signalling scheme fix that? It changes the constant, not the exponent. 4/L with a bigger numerator still falls.

What does connecting through the die's face change? The available count scales with area rather than perimeter, so the ratio becomes constant instead of falling. That is a change of exponent, which is why it is the class of answer that works.

How does HBM's 1024-bit interface decompose? Into 8 independent channels of 128 bits, and each channel into 2 pseudo-channels of 64 bits — 16 streams per stack, with both decompositions reconstructing 1024 exactly.

Does the pseudo-channel split cost access granularity? No, and it is exact: 128 bits at burst 2 is 512 bits, and 64 bits at burst 4 is also 512 bits. Sixty-four bytes either way, with twice the streams.

What does the pseudo-channel split actually cost? Command-slot contention. The two halves share the channel's command bus, so only one can be issued to per slot.

Why share the command bus when DDR5 duplicates it? Because connections are the scarce resource HBM exists to conserve, and a second command bus per pseudo-channel would spend exactly that. HBM takes full independence at the channel level and accepts sharing one level down.

What does “semi-independent” mean precisely? Shared command bus and clock; independent decode and independent banks. So the two halves can hold different rows open but cannot both receive a command in the same slot.

Your HBM system reports eight of sixteen streams active and still misses its bandwidth target. Where do you look? At how those eight are distributed across the eight channels. Two halves in each of four channels saturates four command buses and leaves four idle, with a stream count identical to the healthy spread.

How much of what you just told me is verified? The 1024 bits, the 8 channels and the per-pin rate are from a published standard this curriculum verified; 128 and 64 bits follow by division. The two-way pseudo-channel split is reported and not verified from a primary source, and everything I derived from it carries that caveat.

19. Exercises

  1. Derive the connections-per-capability ratio for a rectangular die of sides a and b. Show that the square is the worst case for a given area, and say what that implies about deliberately non-square dies.

  2. §1 says better signalling multiplies the numerator by a constant. Compute the constant needed for a 20 mm die to match a 5 mm die's ratio, and comment on whether it is plausible.

  3. §4's identity holds for burst 2 at 128 bits and burst 4 at 64 bits. Find every (width, burst) pair that yields the same 512-bit access, and state which are physically sensible and why.

  4. Using §3's bandwidth table, compute the aggregate delivered when 10 of 16 pseudo-channels are active, under the stated model that each delivers its share independently. Then say why the answer is an upper bound.

  5. Remove the index-width composition check from §9's block and parameterise 8 channels with 4 pseudo-channels each. Which property fires, and what exactly does the decomposition do to index 17?

  6. Replace §10's round-robin with fixed priority. Derive the worst starvation run as a function of the sibling's request rate, and say at what rate P7 first fires.

  7. Implement §11's hazard by comparing run[i] instead of the incremented value. Prove that the reported peak is understated by exactly one, never more, and explain why that makes the fairness bound certify its own violation.

  8. §12 publishes n_active and n_channels_touched separately. Construct the two eight-stream distributions with the greatest possible difference in command-bus pressure, and propose a single published figure that would distinguish them — then say what it hides.

20. Where This Goes

The width now has a structure. Connections per unit of capability fall as 4/L, which selects a face-connected solution before any engineering is done; the 1024 bits that result divide into 8 independent channels of 128 bits and then into 16 semi-independent pseudo-channels of 64; and the second division doubles the stream count while preserving the 64-byte access granule exactly.

Three results carry forward. The pin-count wall is a gradient with an exponent, and its exponent is why no signalling improvement could have substituted for a packaging change. The pseudo-channel split is the best bargain in the chapter — twice the streams, identical granularity, no additional connections — and it pays in command-slot contention, which is a controller cost rather than a packaging one. And stream count alone does not describe how the width is being used, because eight streams spread across eight channels and eight concentrated in four are the same count and very different systems.

One thing is left genuinely open, and it is the same thing this chapter's provenance callout opened with. The pseudo-channel layer is REPORTED, not verified. JEDEC refused every retrieval, and the two vendor briefs that were obtained yielded no usable text. So §4's granularity identity is exact arithmetic over an unverified structure — if the burst-length pairing differs from the stated model, the identity changes and the conclusion with it. That is a real gap rather than a rhetorical one, and a reader with access to JESD235 should check §3's table first.

What this chapter has taken entirely for granted is the thing that makes the face-connected solution possible at all. §1 concluded that connections must scale with area rather than perimeter, and then simply assumed that a surface could be covered with them at whatever density was needed. It cannot. There is a pitch below which connections cannot be placed, assembled or tested reliably, and that pitch — not the area — is what actually bounds the count.

Chapter 26.2 takes that up: what a silicon interposer is, why its wiring density is the enabling property rather than its shortness, how a bump pitch converts an area into a connection count, and what the stack of dies above it looks like as a layered structure. The arithmetic in §1 says which surface to use; the next chapter says how much of it is actually available.

Continue learning

Standards & specifications

Governing standard
JEDEC JESD79 (DDR SDRAM)(opens JEDEC Solid State Technology Association in a new tab)

Defines the DDR SDRAM device itself — signals, command encoding, mode registers, timing parameters and the initialisation sequence — one document per generation. Memory-controller microarchitecture, address-mapping policy, PHY training algorithms and board-level design are not specified by it.

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 DDR curriculum.