DDR · Module 5
Channels
A channel has its own command bus, data bus and scheduling, so two channels contend for nothing. That independence means the address alone decides which channel a request uses, making the mapping — not the hardware — the thing that determines whether the parallelism is real.
Chapter 5.4 ended on a diagnostic question: does adding one double the number of data wires between controller and memory? For ranks the answer is no, which is why they add capacity and not bandwidth.
For channels the answer is yes, and everything in this chapter follows from it.
A channel is a largely independent interface path — its own command bus, its own data bus, its own scheduling state. Two channels contend for nothing, so they produce genuinely parallel transfers rather than the overlapped internal work that banks and ranks provide. There is no handoff between them, because there is nothing to hand over.
And that independence has a consequence that is easy to state and easy to underestimate. Because channels share nothing, nothing coordinates them. Which channel a request uses is decided entirely by its address — so whether a system gets one channel's bandwidth or all of them is determined by the address mapping, not by the hardware. A workload whose addresses concentrate on one channel gets exactly one channel's bandwidth, with the others idle and nothing malfunctioning anywhere.
1. What a Channel Owns
Work down Chapter 5.1 §1's list of what each level replicates, and the channel is the level at which the answer becomes "everything."
Its own command and address bus. This is the load-bearing one. Banks share a command path; ranks share a command path; channels do not. A command issued on one channel occupies nothing belonging to another, so the two command streams are genuinely concurrent rather than interleaved on one resource.
Its own data bus. Chapter 5.4 §3 established that ranks share one set of data wires, which is why only one may drive at a time. A second channel is a second set of wires.
Its own scheduling state. Queues, per-bank open-row tables, bank-group history, rank ownership — everything 5.2, 5.3 and 5.4 built exists once per channel. A channel is, structurally, a complete memory subsystem.
And its own population. Each channel has its own ranks, its own devices, its own capacity. Channels need not even be configured identically, though asymmetric population has consequences §4 touches on.
A channel replicates the interface; a rank replicates only the storage behind it. That single sentence is the difference, and it is why the two words — which sound like variations on the same idea — describe structures with almost nothing in common.
2. Independence Cuts Both Ways
Channels sharing nothing is what makes them valuable. It is also what makes them fragile, and the second half is the part that gets designed around badly.
Because channels share nothing, nothing balances them. There is no arbiter deciding that channel 1 is idle and channel 0 is overloaded, because an arbiter would be a shared resource and channels do not have one. Each channel serves whatever requests arrive at it, and requests arrive at it because of their addresses.
So the address mapping is the only mechanism that distributes load. If the mapping sends consecutive addresses to alternating channels, a sequential workload spreads perfectly. If it sends large contiguous regions to one channel, a workload confined to a region uses one channel.
3. Mapping, Without Consuming Module 18
A conceptual example is enough to see the mechanism, and the mechanism is all this chapter needs.
Suppose the channel is selected by some field of bits in the address. Where that field sits determines the granularity at which traffic alternates between channels:
channel field on LOW-ORDER bits
-> consecutive addresses alternate channels
-> a sequential stream spreads across all channels
channel field on HIGH-ORDER bits
-> large contiguous regions map to one channel
-> a stream working within a region uses ONE channelAnd the pathological case is a stride that matches the field's period. If the channel field occupies bits [9:8] and a workload strides by exactly 0x400, those bits never change — every access lands on the same channel, forever, no matter how many channels exist. §7's waveform shows exactly this.
4. DDR5 Sub-Channels Are Not System Channels
Chapter 4.6 described DDR5 splitting each channel into two independent 32-bit sub-channels, each with its own command bus and bank hierarchy. That matches §1's definition of a channel almost exactly, and the resemblance is real — but treating the two as the same concept produces wrong expectations.
What they share structurally. A DDR5 sub-channel has its own CA bus, its own bank and bank-group hierarchy, and its own scheduling state. In terms of scheduling independence, it behaves like a channel.
What differs, and it is what Chapter 4.6 §2 called independent in scheduling and shared in physics. The two sub-channels of a DDR5 module live on one module, sharing its supply and its on-module power regulation, its package and its thermal environment, and its physical slot. They are not independently populated — you cannot fit one sub-channel and leave the other empty — and they do not independently scale capacity.
System channels are separate interfaces from the controller, potentially with different populations, different capacities, and independent physical paths.
The practical consequences of confusing them. Counting a dual-sub-channel DIMM as "two channels" when sizing a system double-counts the physical interfaces available. Expecting to populate them asymmetrically does not work. And expecting them to be thermally or electrically independent is wrong in exactly the way that matters when a module throttles — both sub-channels throttle together, because the thing that got hot is shared.
The useful framing: a DDR5 sub-channel is a channel's worth of scheduling independence delivered within one module's worth of physical resource. Module 25 covers the architecture properly.
5. RTL — Turning an Address Into a Channel
Engineering problem
A request address must select exactly one channel, producing both an index — for indexing per-channel state — and a one-hot vector for distributing the request. Where a non-power-of-two channel count leaves encodings that select nothing, the block must report rather than silently repair, because repairing hides a mapping decision that should be explicit.
And because §2 established that per-channel distribution is the only diagnostic that matters, it must measure the distribution it produces.
Classification
SYNTHESIZABLE RTL. A field extract, a one-hot decode and per-channel saturating counters — the front end of every multi-channel memory controller.
It is not an address mapper. It extracts a contiguous field at a configured position, which is the simplest possible mapping and is deliberately not what a good controller does. Real controllers hash across many address bits so no simple stride can defeat the distribution, and that is Module 18's subject. This block exists to show the mechanism — address bits become concurrency — and to make the failure mode visible when the bits are chosen badly.
It models no channel. No queues, no devices, no transfers, no timing. It decides which, not what happens next.
Interface
req_valid with req_addr presents a request. channel_index and channel_onehot select. channel_invalid reports an encoding that selects nothing. cnt_channel is the per-channel tally and imbalance its spread — the §2 diagnostic, computed in hardware.
State
Per-channel saturating counters only. The selection itself is stateless, which is the structural expression of the fact that channels do not coordinate: nothing about a previous request influences where this one goes.
Combinational logic
The field extract, the range check, the one-hot decode, and the max-minus-min spread.
Sequential logic
Counter increments only.
Simulation
vlog channel_selector.sv tb_channel_selector.sv then vsim -c tb_channel_selector -do "run -all"; VCS vcs -sverilog channel_selector.sv tb_channel_selector.sv && ./simv; Xcelium xrun -sv channel_selector.sv tb_channel_selector.sv.
Expected output: a stride of one channel-field period rotates cleanly through every channel with imbalance returning to zero after each full rotation; a stride equal to the field's whole period pins every request to one channel and imbalance climbs without bound.
// ─────────────────────────────────────────────────────────────────────────
// CHANNEL SELECTOR. Classification: SYNTHESIZABLE RTL.
//
// Turns an address into a channel selection -- the mechanism by which
// address bits become concurrency. Because channels share NOTHING, nothing
// balances them: this decode is the only thing distributing load, so a
// badly chosen field means one channel works and the rest idle, with no
// error anywhere.
//
// THIS IS NOT AN ADDRESS MAPPER. It extracts a CONTIGUOUS field at a fixed
// position, which is the simplest possible mapping and deliberately not
// what a good controller does -- real controllers HASH across many address
// bits so no simple stride can defeat the distribution. Module 18 owns
// that. This block shows the mechanism and makes the failure visible.
//
// MODELS NO CHANNEL: no queues, no devices, no transfers, no timing. It
// decides WHICH, not what happens next.
//
// The per-channel counters are not decoration. Chapter 5.5 Section 2: one
// channel saturated and all channels at half utilisation deliver the same
// aggregate and mean opposite things, and only a per-channel distribution
// tells them apart.
// ─────────────────────────────────────────────────────────────────────────
module channel_selector #(
parameter int NUM_CHANNELS = 4,
parameter int ADDR_W = 32,
// Lowest address bit of the channel field. THE parameter of interest:
// low positions alternate channels on fine strides, high positions map
// large contiguous regions to one channel.
parameter int SELECT_LSB = 8,
parameter int ACC_W = 16,
parameter int CH_W = (NUM_CHANNELS <= 1) ? 1 : $clog2(NUM_CHANNELS)
) (
input logic clk,
input logic rst_n,
input logic req_valid,
input logic [ADDR_W-1:0] req_addr,
output logic sel_valid,
output logic [CH_W-1:0] channel_index,
output logic [NUM_CHANNELS-1:0] channel_onehot,
// The extracted field selects no existing channel. Only possible when
// NUM_CHANNELS is not a power of two. REPORTED, NOT REPAIRED: silently
// folding it with a modulo would hide a mapping decision that belongs to
// whoever chose the channel count.
output logic channel_invalid,
// Per-channel tally and its spread -- the Section 2 diagnostic in
// hardware. Saturating: a wrapped counter would report a flattering
// balance, and an instrument that errs toward what its reader hopes for
// is worse than none.
output logic [ACC_W-1:0] cnt_channel [NUM_CHANNELS],
output logic [ACC_W-1:0] imbalance
);
// ── COMPILE-TIME legality.
if (NUM_CHANNELS < 1) begin : g_nc_min
initial $fatal(1, "channel_selector: NUM_CHANNELS must be >= 1");
end
if (SELECT_LSB < 0) begin : g_lsb_min
initial $fatal(1, "channel_selector: SELECT_LSB must be >= 0");
end
// The field must fit inside the address. Getting this wrong would produce
// an out-of-bounds part-select, which some tools accept silently by
// padding with zeros -- and a channel field that is half constant zero
// is exactly the pathology this block exists to expose, so it must not be
// possible to create it by accident.
if ((SELECT_LSB + CH_W) > ADDR_W) begin : g_field_fit
initial $fatal(1, "channel_selector: channel field exceeds ADDR_W");
end
logic [CH_W-1:0] raw_field;
assign raw_field = req_addr[SELECT_LSB +: CH_W];
// ── Range check, Chapter 5.1's pattern. A power-of-two channel count
// makes every encoding legal and this collapses to a constant.
if (NUM_CHANNELS >= (1 << CH_W)) begin : g_ch_full
assign channel_invalid = 1'b0;
end else begin : g_ch_check
assign channel_invalid = req_valid
&& ({1'b0, raw_field} >= (CH_W+1)'(NUM_CHANNELS));
end
assign sel_valid = req_valid && !channel_invalid;
assign channel_index = raw_field;
// ── One-hot decode. Built from the index so the two cannot disagree:
// Section 6's agreement property then holds by construction rather
// than by maintenance.
always_comb begin
channel_onehot = '0;
if (sel_valid) channel_onehot[channel_index] = 1'b1;
end
// ── Spread across channels: max minus min. A single number that answers
// "is the load distributed", which aggregate throughput cannot.
logic [ACC_W-1:0] max_cnt, min_cnt;
always_comb begin
max_cnt = cnt_channel[0];
min_cnt = cnt_channel[0];
for (int c = 1; c < NUM_CHANNELS; c++) begin
if (cnt_channel[c] > max_cnt) max_cnt = cnt_channel[c];
if (cnt_channel[c] < min_cnt) min_cnt = cnt_channel[c];
end
imbalance = max_cnt - min_cnt;
end
logic [ACC_W:0] c_sum;
always_comb begin
c_sum = {1'b0, cnt_channel[channel_index]} + (ACC_W+1)'(1);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int c = 0; c < NUM_CHANNELS; c++) cnt_channel[c] <= '0;
end else if (sel_valid) begin
cnt_channel[channel_index] <= c_sum[ACC_W] ? {ACC_W{1'b1}}
: c_sum[ACC_W-1:0];
end
end
endmoduleCycle trace
NUM_CHANNELS = 4, SELECT_LSB = 8, so the field is req_addr[9:8]:
| Address | req_addr[9:8] | Channel | Note |
|---|---|---|---|
0x000 | 00 | 0 | |
0x100 | 01 | 1 | stride 0x100 rotates |
0x200 | 10 | 2 | |
0x300 | 11 | 3 | full rotation, imbalance back to 0 |
0x400 | 00 | 0 | |
0x800 | 00 | 0 | stride 0x400 never leaves channel 0 |
0xC00 | 00 | 0 |
The last three rows are the failure mode. A stride of 0x400 leaves bits [9:8] constant, so every access lands on channel 0 forever — three-quarters of the hardware idle, with every request served correctly.
Waveform expectation
§7. imbalance returning to zero after each full rotation is the healthy signature; imbalance climbing monotonically is the pathology.
Synthesis implication
A wire (the field extract is free), a small decoder, NUM_CHANNELS saturating counters, and a max/min tree. The max/min tree is the only thing that grows — it is O(NUM_CHANNELS) comparators and is the price of the diagnostic. At realistic channel counts it is negligible, and if it ever were not, the counters could be read out and differenced in software instead.
Corner cases
NUM_CHANNELS == 1 makes CH_W == 1 through the guard, giving one legal encoding of two — so the range check materialises and half of all addresses report channel_invalid. That is a real and useful warning: a single-channel system should not have a channel field at all, and the block flagging it is more helpful than silently selecting channel 0. A non-power-of-two count leaves genuinely unselectable encodings, reported rather than folded. SELECT_LSB + CH_W > ADDR_W does not elaborate, because an out-of-range part-select can be silently zero-padded by some tools and would manufacture exactly the constant-field pathology this block exists to expose.
Verification
What DV must prove: exactly one channel selected for every valid request and none for an invalid one; the one-hot vector agrees with the index; the extracted field matches the configured bit positions across an address sweep; counters matching an independent tally and saturating rather than wrapping; imbalance equalling max minus min; and — the behavioural test that matters — a stride equal to the field's period producing an imbalance that grows without bound, which is the property that catches a mapping regression.
Debugging
If every request selects channel 0, check SELECT_LSB against the workload's stride before suspecting the block: a constant field is a mapping problem, not a decode problem, and the block is working correctly. If channel_invalid fires constantly at a power-of-two channel count, the truncating comparison is back — the bound must be one bit wider than the field. If imbalance reads zero while one channel is visibly busier, check that the max/min loop covers every index rather than stopping at NUM_CHANNELS-1 exclusive of the last. If the one-hot vector has more than one bit set, it is not being cleared before the indexed assignment.
Limitations
Contiguous field extraction only — no hashing, no interleaving, no XOR folding, all of which real controllers use and Module 18 covers. No per-channel queues, so it cannot see that a channel is backed up rather than merely selected often — and those are different problems with different fixes. No modelling of a channel's contents at all. No notion of request size, so a channel receiving fewer but larger requests looks under-loaded by this counter. The last one is a genuine measurement limitation: counting requests is not counting bytes, and where request sizes vary the two distributions can disagree.
6. Four Assertions Worth Writing
// VERIFICATION-ONLY, inside channel_selector.
// P1 -- EXACTLY ONE channel for a valid selection. $onehot, not $onehot0:
// selecting none would silently drop a request, and a dropped memory
// request is not a performance issue.
property p_exactly_one_channel;
@(posedge clk) disable iff (!rst_n)
sel_valid |-> $onehot(channel_onehot);
endproperty
assert property (p_exactly_one_channel);
// P2 -- and NONE when the selection is not valid. The companion that stops
// P1 from being satisfiable by a block that asserts a channel regardless.
property p_none_when_invalid;
@(posedge clk) disable iff (!rst_n)
!sel_valid |-> (channel_onehot == '0);
endproperty
assert property (p_none_when_invalid);
// P3 -- THE AGREEMENT PROPERTY. The one-hot vector and the index must
// designate the same channel. They are two representations of one decision,
// and downstream logic uses BOTH -- the index to address per-channel state,
// the vector to route the request. A disagreement sends the request to one
// channel and updates another's state, which corrupts the per-channel model
// while every individual channel behaves correctly.
property p_onehot_matches_index;
@(posedge clk) disable iff (!rst_n)
sel_valid |-> channel_onehot[channel_index];
endproperty
assert property (p_onehot_matches_index);
// P4 -- the extracted field is the configured bits, stated independently of
// the extraction expression so the property is not a tautology. A check
// that reuses the logic it checks proves only that the logic equals itself.
property p_field_is_configured_bits;
@(posedge clk) disable iff (!rst_n)
sel_valid |-> (channel_index
== req_addr[SELECT_LSB +: CH_W]);
endproperty
assert property (p_field_is_configured_bits);
// P5 -- the counters advance, which P1 to P4 do not require at all. A
// selector whose counters never move satisfies every property above.
//
// The index is captured in a LOCAL VARIABLE rather than recovered with a
// nested $past. `$past(cnt_channel[$past(channel_index)])` is the form that
// comes naturally and it is both hard to read and rejected or mis-handled
// by some tools -- a $past inside the index of another $past. Capturing the
// index at the antecedent makes the intent explicit and the expression flat.
property p_counter_advances;
int unsigned idx;
@(posedge clk) disable iff (!rst_n)
((sel_valid && (cnt_channel[channel_index] != {ACC_W{1'b1}})),
idx = channel_index)
|=> (cnt_channel[idx] == ($past(cnt_channel[idx]) + ACC_W'(1)));
endproperty
assert property (p_counter_advances);P3 is the one worth understanding, because the failure it prevents is invisible in every per-channel check. If the index and the vector disagree, each channel still behaves perfectly — one channel receives a request it handles correctly, another has its state updated consistently. Nothing anywhere is internally inconsistent. What is wrong is the correspondence between two subsystems, and correspondence failures are exactly what per-component verification cannot see.
Generalise it: whenever one decision is expressed in two representations that different consumers use, assert that the representations agree. It is a small property and it guards a class of bug that no amount of testing either consumer will find.
P4 is written to avoid tautology, and the point is subtle enough to state: an assertion that recomputes an expression the same way the design does proves nothing. Here the property references the parameters and the input, not the design's internal raw_field wire — so a bug in how raw_field is formed is visible. If your property and your design share the expression, you have written documentation, not a check.
What none of them prove. Nothing about whether the chosen bits are the right bits — that is a workload-dependent question and Module 18's. A perfectly correct selector that pins every request to one channel satisfies all five properties. The correctness of a mapping is a statistical property of address streams, not a logical property of a decoder, and no assertion can bridge that gap. §7's growing imbalance is the closest an in-hardware measure gets.
7. Distribution, and Its Failure
channel_selector — four channels selected by address bits 9 and 8
10 cyclesimbalance is the signal to watch, and its shape is what carries the information. Through the first eight requests it rises to 1 and returns to 0 at each full rotation — the sawtooth of a perfectly spread stream. A flat-zero-on-average sawtooth is what healthy distribution looks like.
From cycle 8 the stride changes to 0x400 and bits [9:8] stop changing. Both remaining requests select channel 0, and imbalance no longer comes back down. Continue that stream and imbalance grows without bound while channels 1, 2 and 3 do nothing at all.
Every request in both phases was served correctly. There is no error, no dropped request, no malfunction. The second phase simply uses a quarter of the memory system, and nothing in the system objects — which is §2's point and the reason the counters exist.
What the figure does not claim. Nothing about queueing: this measures which channel was selected, not whether it was backed up. Nothing about bytes — these are request counts, and unequal request sizes would make the two distributions disagree. And the addresses are abbreviated to their low twelve bits for legibility.
8. The Channel, Answered Systematically
| Question | Answer for a channel |
|---|---|
| What does it contain? | Its own ranks, devices, capacity — a complete memory subsystem |
| What resource does it share? | Nothing with another channel, by definition |
| What can operate in parallel with it? | Any other channel, fully and at all times |
| How is it selected? | A channel field in the address — and only that |
| What must the controller track? | Entirely separate scheduling state per channel |
| What opportunity does it create? | Genuinely parallel transfers: bandwidth multiplies |
| What conflict does it create? | None between channels — the conflict is imbalance |
| What must DV verify? | Exactly-one selection, and index/one-hot agreement |
The conflict row is unique in this module. Every other level creates a conflict between its instances — banks conflict on rows, groups on column paths, ranks on the bus. Channels create no conflict at all, and their failure mode is instead that one is used and another is not. That is a categorically different kind of problem: not contention, but waste — and it is diagnosed by a distribution rather than by a stall.
9. Common Misconceptions
"A channel is just a wider data bus." Wrong mental model: channels and width are the same knob. Engineering action: treating a two-channel 64-bit system as equivalent to a one-channel 128-bit system; assuming the only benefit is bytes per transfer. Observable failure / bad conclusion: missing that a wider single channel has one command bus and one scheduling domain, so it still serves one request stream — while two channels serve two independently. The wider channel also has a larger access granularity, which Chapter 4.5 §1 showed can waste bandwidth outright. Correct model: a channel replicates the entire interface — command bus, data bus, scheduling state, population. Two channels are two independent memory subsystems; a wider bus is one subsystem moving more per transfer. Prevention: count the command buses. Width gives you more bytes per command; channels give you more commands.
"Dual-rank and dual-channel are similar." Wrong mental model: both add memory in parallel. Engineering action: sizing for bandwidth by rank count; expecting a second rank to behave like a second channel. Observable failure / bad conclusion: a configuration that cannot reach its bandwidth target at any amount of tuning, because ranks share one data bus and the wires are simply not there. Correct model: ranks share the command bus and the data bus and add capacity, with one driver at a time and a handoff cost on every change. Channels share nothing and add capacity and parallel transfers, with no handoff. Prevention: Chapter 5.4 §4's question — does adding one double the data wires? Channels yes, ranks no.
"More channels automatically means more bandwidth." Wrong mental model: channel count is bandwidth. Engineering action: provisioning channels without examining how addresses map to them; quoting aggregate peak bandwidth for a workload with a concentrated access pattern. Observable failure / bad conclusion: a four-channel system delivering one channel's bandwidth, with three channels idle and every request served correctly. This is the most common multi-channel disappointment and it has no error signature at all. Correct model: channels can deliver parallel bandwidth. Whether they do is decided by the address mapping, because nothing else distributes load — channels share nothing, so there is no arbiter to balance them. Prevention: measure per-channel utilisation. One saturated channel and all channels at half load produce the same aggregate and mean opposite things.
"DDR5 sub-channels are the same as system memory channels." Wrong mental model: any independent CA bus is a channel in the system sense. Engineering action: counting a dual-sub-channel DIMM as two channels when sizing a system; expecting to populate sub-channels asymmetrically; assuming thermal and electrical independence. Observable failure / bad conclusion: double-counting the physical interfaces available, and surprise when both sub-channels throttle together — because the module, its power regulation and its thermal environment are shared. Correct model: a DDR5 sub-channel has a channel's scheduling independence — own CA bus, own bank hierarchy — delivered within one module's physical resource. Chapter 4.6's phrase: independent in scheduling, shared in physics. They cannot be populated separately. Prevention: ask whether the two can be populated, powered and cooled independently. If not, they are sub-channels.
10. Debugging — Three Channels Idle While One Saturates
Symptom. A four-channel system delivers roughly one channel's worth of bandwidth. Per-channel utilisation shows one channel near 100% and the others near zero. No errors; every request is served correctly.
This symptom is already half-diagnosed, because per-channel utilisation was measured — which §2 argued is the measurement that distinguishes "at the ceiling" from "wasting the hardware." What remains is why the distribution is skewed, and the candidates differ in where the skew was introduced.
Mechanism 1 — the workload's stride matches the channel field's period. Inspect: the access stride against SELECT_LSB and the field width. Expected evidence: a stride that is a multiple of 2^(SELECT_LSB + CH_W), leaving the field constant. Discriminator: compute the field's period and compare it with the stride. This is first because it is one arithmetic check and because it is the most common cause — §7's exact scenario. A stride of 0x400 against a field at bits [9:8] pins every access forever.
Mechanism 2 — the channel field is on high-order bits and the workload is local. Inspect: where the channel field sits, against the size of the region the workload touches. Expected evidence: a field above the working set's span, so the workload never reaches an address that changes it. Discriminator: is the field constant because of the stride, or because of the range? Distinguished from mechanism 1 by whether a larger sweep of addresses would spread — if walking the whole address space distributes but the workload does not, it is range, not stride, and interleaving at a finer granularity is the fix.
Mechanism 3 — the requests are distributed but the bytes are not. Inspect: per-channel bytes rather than per-channel request counts. Expected evidence: similar request counts across channels with very different byte totals. Discriminator: do the two distributions disagree? §5's limitation, made concrete: counting requests is not counting bytes, and a channel receiving fewer but much larger requests can be saturated while looking under-selected. Check which quantity your counter actually counts before trusting it.
Mechanism 4 — asymmetric population. Inspect: the capacity and rank configuration of each channel. Expected evidence: channels differing in capacity, with the address map necessarily sending more of the space to the larger one. Discriminator: are the channels identically populated? An asymmetric configuration makes uniform interleaving impossible over the whole space, and the resulting skew is a consequence of the configuration rather than a mapping bug — the fix is populating symmetrically, not remapping.
Mechanism 5 — not distribution: one channel is degraded. Inspect: whether the busy channel is actually slow rather than over-selected — its per-request service time against the others'. Expected evidence: similar request counts across channels, with one channel's queue growing because it serves slowly. Discriminator: is the channel over-selected or under-performing? This inverts the whole diagnosis: a channel whose training or calibration is marginal serves slowly and backs up, and its queue looks like a distribution problem while its selection count is normal. Chapter 4.4 §11's trained-state mechanisms apply per channel.
Discrimination, cheapest first. Compute the channel field's period and compare with the stride — pure arithmetic, resolves mechanism 1. Then compare per-channel bytes with per-channel requests, which separates mechanism 3 and costs one extra counter. Then compare per-channel selection counts with per-channel service times, which separates mechanism 5 — the one where the busy channel is a victim rather than a cause. Then check population symmetry.
The reasoning lesson. A skewed distribution does not tell you where the skew was introduced, and there are three distinct places: the workload's addresses, the mapping that converts them, and the channels' own ability to keep up. Mechanisms 1, 2 and 4 are upstream of the channel; mechanism 5 is inside it; mechanism 3 is in the instrument. The instinct is to treat a skewed channel histogram as a mapping problem, and it usually is — but a slow channel produces a nearly identical picture with an opposite fix, and the discriminator is cheap: compare how often each channel was chosen with how long each took. Selection and service are different measurements, and conflating them is how a calibration problem gets debugged as an address-mapping problem for a week.
11. Interview Reasoning
"How is a channel different from a rank?" A channel replicates the entire interface — its own command bus, its own data bus, its own scheduling state and its own population. A rank replicates only the storage behind a shared interface. So two channels contend for nothing and deliver genuinely parallel transfers, while two ranks share one data bus, can only have one driving at a time, and add capacity rather than bandwidth. The quick diagnostic is whether adding one doubles the data wires between controller and memory: for channels yes, for ranks no.
"Why does multi-channel memory create more potential concurrency?" Because each channel has its own command bus, so commands issue concurrently rather than being interleaved on one resource, and its own data bus, so transfers happen simultaneously rather than taking turns. Every level below the channel — banks, bank groups, ranks — overlaps internal work while serialising the actual transfer, because they all share an interface somewhere. The channel is the level at which the interface itself is replicated, which is why it is the only level that multiplies bandwidth rather than just improving utilisation of a fixed bandwidth.
"If channels share nothing, what determines how well they are used?" The address mapping, and nothing else. Because channels share nothing, there is no arbiter that could notice one is idle and another overloaded — an arbiter would be a shared resource. Each channel simply serves whatever arrives, and what arrives is determined by the channel field of each request's address. So a mapping that puts the field on bits the workload varies spreads load, and one that puts it on bits the workload holds constant sends everything to one channel. Every request is still served correctly; three quarters of the hardware just does nothing.
"Two systems both deliver 20 GB/s. One has a single saturated channel, the other has four channels at 25% each. Same or different?" Completely different, and the aggregate number cannot distinguish them. The saturated single channel is at its ceiling — nothing in the memory system can improve it and the fix is more channels or less traffic. The four channels at a quarter load can deliver four times what they are delivering, and the entire shortfall is a distribution problem solvable in the address mapping at no hardware cost. That is why per-channel utilisation is not an optional diagnostic for a multi-channel system: it is the only thing that tells those two situations apart, and it costs one counter per channel.
"Are DDR5 sub-channels the same thing as system memory channels?" Structurally similar in scheduling and different in physics. A DDR5 sub-channel does have its own command bus, its own bank and bank-group hierarchy and its own scheduling state, so in terms of independent command streams it behaves like a channel. But the two sub-channels of a module share that module's power regulation, package, thermal environment and slot, and they cannot be populated independently — you cannot fit one and leave the other empty. Practically that means counting a dual-sub-channel DIMM as two channels double-counts the physical interfaces, and expecting thermal independence is wrong, because when a module throttles both sub-channels throttle together.
12. Engineering Check
A four-channel system. The channel field occupies address bits
[9:8]. Conceptual mapping for reasoning; real mappings are Module 18's and frequently hash rather than slice.
1. A workload reads sequentially with a 64-byte stride. How does it distribute? The stride is 0x40, so bits [9:8] change every four accesses — giving four consecutive accesses per channel, then rotate. All four channels are used equally. Coarse but even.
2. Stride 0x100. Distribution? Bits [9:8] change every access, so consecutive requests rotate through all four channels. Perfectly interleaved — this is §7's first phase.
3. Stride 0x400. Distribution? 0x400 is 2^10, and the field occupies bits 8 and 9, so the stride leaves both unchanged. Every access lands on channel 0, forever. Three channels idle; aggregate bandwidth is one quarter of peak; nothing malfunctions.
4. Would moving the field to bits [11:10] fix question 3? For that stride, yes — 0x400 changes bit 10, so accesses would rotate. But it breaks question 2, whose 0x100 stride would then leave bits [11:10] constant. There is no bit position that is right for every stride, which is precisely why real controllers hash across many address bits instead of slicing a contiguous field: hashing makes no single stride pathological.
5. The system is reconfigured to 3 channels. What happens with the field at [9:8]? The field has four encodings and three channels, so encoding 3 selects nothing and §5's block reports channel_invalid. A quarter of the address space is unmapped. A real system needs a mapping that folds four encodings onto three — a modulo or a hash — which is a genuine design decision. The block reporting rather than silently folding is deliberate: whoever chose a non-power-of-two channel count owes an explicit answer.
6. Per-channel request counts are equal across four channels and one channel is saturated. What is happening? The counter is measuring the wrong thing. Equal request counts with unequal load means the requests differ in size — one channel is receiving fewer bytes per request or more, and request counts cannot see it. Measure per-channel bytes. This is §10's third mechanism, and it is the case where the instrument, not the system, is producing the wrong picture.
13. Summary
A channel is a largely independent interface path — its own command bus, its own data bus, its own scheduling state, its own ranks and capacity. It is the level at which the interface itself is replicated, which is why it is the only level in this module that multiplies bandwidth rather than improving the utilisation of a fixed bandwidth.
Channels contend for nothing, so they deliver genuinely parallel transfers with no handoff — unlike ranks, which share the data bus, and unlike banks and bank groups, which overlap only internal work.
And because they share nothing, nothing balances them. There is no arbiter — an arbiter would be a shared resource. The address mapping is the only mechanism that distributes load, so whether a multi-channel system delivers multi-channel bandwidth is decided entirely by which address bits form the channel field, and by how the workload's addresses vary in those bits.
The failure mode is unique in this module. Every other level creates contention between its instances. Channels create waste instead: one channel saturated while others idle, every request served correctly, no error anywhere. A stride matching the channel field's period pins every access to one channel forever.
So per-channel utilisation is not optional. One saturated channel and four channels at quarter load deliver the same aggregate and mean opposite things — ceiling reached versus three-quarters of the hardware unused — and no aggregate number distinguishes them.
There is no universally right field position. A position that interleaves one stride perfectly leaves another constant, which is why real controllers hash across many address bits rather than slicing a contiguous field. The channel field also competes with the bank-group and rank fields for the same bits, and those want opposite things — bank groups want rapid variation, ranks want the reverse.
And DDR5 sub-channels are not system channels. They have a channel's scheduling independence within one module's physical resource: shared supply, package, thermals and slot, not independently populated. Independent in scheduling, shared in physics.
14. What Comes Next
Every level so far has been logical — a bank, a group, a rank, a channel are all structures a controller reasons about. Chapter 5.6 looks at the physical object those structures are packaged into.
A memory module exists as a system boundary: devices mounted together, ranks constructed on it, command and data infrastructure routed across it, and a connector joining it to the board. And the interesting question is not what a module contains but why some modules need a buffering layer that others do not.
The answer is an electrical scaling problem — every device attached to a shared bus is a load, and load limits both frequency and how many devices can attach. Registered and load-reduced modules insert buffers to solve it, and each buffer costs a pipeline stage that the controller must account for. That digital consequence is exactly what 5.6's RTL models, and its electrical cause is exactly what it does not.
Return to Ranks for the shared-bus ownership channels avoid, The DDR Device Structure for the replication framing, or DDR5 for the sub-channel split §4 distinguishes. Module 18 owns address mapping. The full path is on the DDR tutorials index.
Continue learning
Related tutorials
- Related topic
DDR5
A 16-beat burst on a 64-bit channel would double granularity. DDR5 halves the channel instead, into two independent 32-bit sub-channels — preserving access size exactly while doubling the number of independent request streams.
- Related topic
Bank Address
A bank address does not select a location — it selects an independent resource. Which is why the position of the bank field is the most consequential choice in an address map, and why two defensible layouts give opposite answers.
- Related topic
Physical Mapping
Five chapters of fields assembled into one map, and then the two questions none of them could ask alone: is the decomposition lossless, and can a monitor invert it from what it actually observed?
- Related topic
Row-Bank-Column Mapping
Two lossless address maps over the same device disagree about every resource a workload touches. The field order decides what changes on the next cache line, and that decides row locality and bank distribution before the controller sees anything.
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.
