Muxes and crossbars at scale
Price a multiplexer and a crossbar before you write one: measure how cells and levels of logic grow with inputs, outputs and width, and code the mux as a tree.
- Lesson
- R5.5
- Time
- 25 min
- Runs on
- Your browser
- Check
- 5 questions
A brief asked for "a switch" between 16 sources and 16 sinks of 64-bit words. The RTL wrote each output as a chain of ? : tests. Yosys 0.70 built it from 26,287 cells, with 20 levels of logic on its longest path. Nobody had priced it before writing it.
| 16 x 16 crossbar, 64-bit words | Cells | Levels of logic |
|---|---|---|
each output a chain of ? : tests | 26,287 | 20 |
each output an array index, in[sel[j]] | 22,508 | 6 |
Measured with Yosys 0.70 on this flow. The tree is shallower, and still more than twenty thousand cells: the size comes from the product of ports and width.
From one 2:1 mux to a tree
A multiplexer (mux) passes one of its data inputs to its output, chosen by a select. A 2:1 mux of 8-bit words is eight 1-bit muxes, one per bit: 8 cells and 1 level of logic on this flow.
A wider mux is a tree of 2:1 muxes. Select bit 0 picks within pairs, bit 1 picks between the pairs, and so on. An 8:1 mux has 7 muxes per bit in 3 levels: 7 x 8 = 56 cells. You get that tree by writing an array index, d[sel], or a case on the select.
- Level 1
Four muxes pick within the pairs, all at once, using
sel[0]. - Level 2
Two muxes pick a pair from each half, using
sel[1]. - Level 3
One mux picks the half, using
sel[2]. Seven muxes, three levels: log2(8).
Read the diagram as text
- d0 / d1: sel[0]
- d2 / d3: sel[0]
- d4 / d5: sel[0]
- d6 / d7: sel[0]
- low half: sel[1]
- high half: sel[1]
- y: sel[2]
- d0 / d1 → low half
- d2 / d3 → low half
- d4 / d5 → high half
- d6 / d7 → high half
- low half → y
- high half → y
Measured: an 8:1 mux of 8-bit words is 56 cells, all 2:1 muxes, and 3 levels of logic.
A chain of ? : tests says something else: test select 0, else test select 1, else test select 2. That is a priority chain (R1.5). Every test sits behind the one before, so the levels grow with N instead of log2(N).
| 8-bit mux | Array index: cells | levels | ? : chain: cells | levels |
|---|---|---|---|---|
| 2:1 | 8 | 1 | 8 | 1 |
| 4:1 | 24 | 2 | 27 | 4 |
| 8:1 | 56 | 3 | 118 | 10 |
| 16:1 | 180 | 6 | 227 | 20 |
| 32:1 | 482 | 8 | 695 | 41 |
Measured with Yosys 0.70 on this flow. Up to 8:1 the tree is exactly 8 x (N - 1) muxes; from 16:1 the synthesizer mixes in other gates.
A crossbar is one mux per output
A crossbar connects M inputs to N outputs. Each output has its own M:1 mux and its own select, so any output can show any input. Two outputs may show the same input in the same cycle.
- Fan out
All four input words reach every mux.
- Pick
Each mux picks one word by its own select: 4 x 8 x 3 = 96 2:1 muxes in all.
Read the diagram as text
- in[0..3]: four words
- mux 0: sel[0]
- mux 1: sel[1]
- mux 2: sel[2]
- mux 3: sel[3]
- out[0..3]: four words
- in[0..3] → mux 0
- in[0..3] → mux 1
- in[0..3] → mux 2
- in[0..3] → mux 3
- mux 0 → out[0..3]: out[0]
- mux 1 → out[0..3]: out[1]
- mux 2 → out[0..3]: out[2]
- mux 3 → out[0..3]: out[3]
Cost: N outputs x W bits x (M - 1) 2:1 muxes. Doubling both port counts multiplies it by more than four.
| Crossbar (M x N, width) | Cells | Levels of logic | N x W x (M - 1) |
|---|---|---|---|
| 2 x 2, 8 bits | 16 | 1 | 16 |
| 4 x 4, 8 bits (the lab) | 96 | 2 | 96 |
| 4 x 4, 32 bits | 384 | 2 | 384 |
| 8 x 8, 8 bits | 448 | 3 | 448 |
| 8 x 8, 32 bits | 1,792 | 3 | 1,792 |
| 16 x 16, 8 bits | 2,836 | 6 | 1,920 |
4 x 4, 8 bits, each mux a ? : chain | 108 | 4 | |
16 x 16, 64 bits, each mux a ? : chain | 26,287 | 20 |
Measured with Yosys 0.70 on this flow. Width multiplies the cost and leaves the levels alone; inputs add levels.
Read the table two ways. Width scales the cells and not the levels: 4 x 4 at 32 bits is four times 4 x 4 at 8 bits, at the same 2 levels. Ports scale both: 8 x 8 costs 4.7 times 4 x 4 and adds a level.
When the crossbar is too big
A crossbar lets every output move a word in every cycle. If the brief does not need that, it is paying for parallel paths nobody uses. Two cheaper shapes follow from this module.
A shared bus with an arbiter
One M:1 mux and an arbiter (R5.4) give one transfer per cycle among all sources. Here that is a single mux row instead of N of them.
Time multiplexing
Fewer ports, more cycles: sources take turns on a smaller crossbar. This works when the brief's rate leaves cycles to spare.
Whichever shape you choose, write each mux as an array index or a case, and let the synthesizer build the tree. Keep ? : chains for real priority, where the order is part of the function.
The lab
| Port | Direction | Width | Meaning |
|---|---|---|---|
| in | input | [M-1:0][W-1:0] | the M inputs |
| sel | input | [N-1:0][$clog2(M)-1:0] | one select per output |
| out | output | [N-1:0][W-1:0] | out[j] equals in[sel[j]] in the same cycle; any number of outputs may share an input |
- Latency
- combinational: no clock, no flop, no latch
- Parameters
Minputs,Noutputs,Wbits; the bench uses 4, 4 and 8- Corner cases
- all outputs on one input; an input no output selects; M a power of two, so every select value names an input
Run the lab: six cycles, 0 outputs wrong. Synthesize: 96 cells and 2 levels of logic. Now change the defaults of xbar and synthesize again. W = 32 gives 384 cells. M = 16, N = 1 makes the crossbar one 16:1 mux: 180 cells and 6 levels.
Last, set the defaults back and replace the assign in mux_n with the chain below, which is right for M = 4 only. Run: still 0 outputs wrong. Synthesize: 108 cells and 4 levels of logic.
? : tests.assign y = (sel == 0) ? d[0] : (sel == 1) ? d[1] : (sel == 2) ? d[2] : d[3];Asked in interviews
What does a 16:1 mux cost?
About 15 two-input muxes per bit in a tree of four levels. Measured on this flow at 8 bits: 180 cells and 6 levels, because the synthesizer mixed in other gates. Written as a chain of
? :tests it was 227 cells and 20 levels.How does a crossbar scale?
One M:1 mux per output, per bit: about N x W x (M - 1) two-input muxes. Width multiplies the cells; inputs add levels. 4 x 4 at 8 bits was 96 cells, 8 x 8 was 448.
Mux tree or priority chain?
A tree tests the select bits in parallel stages, so its depth is log2(N). A chain tests one condition after another, so its depth grows with N. Use the chain only when the order of the tests is part of the function.
Try it
Each line lists the four selects, then the four outputs, out[0] first. With sel = 1 1 1 1 every output shows input 1. At e4 the input words change and the paths stay: each output follows its select to the new word.
With the ? : chain in mux_n, the run prints the same seven lines. The function is the same; Synthesize reports 108 cells and 4 levels of logic instead of 96 and 2.
Synthesize the design to measure its cells and inspect the mapped gates.
e0 sel=0 1 2 3 out=10 21 32 43 e1 sel=3 2 1 0 out=43 32 21 10 e2 sel=1 1 1 1 out=21 21 21 21 e3 sel=2 0 3 1 out=32 10 43 21 e4 sel=2 0 3 1 out=cc aa dd bb e5 sel=0 0 3 3 out=aa aa dd dd 6 cycles, 0 outputs wrongOriginal design · 96 cells · 0 flip-flops · 0 latches · 2 logic levels. Synthesis uses Yosys; counts depend on the toolchain.
Read included file: tb.sv
`timescale 1ns/1ps
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
int edge_n = -1;
always @(posedge clk) edge_n = edge_n + 1;
logic [3:0][7:0] in;
logic [3:0][1:0] sel;
logic [3:0][7:0] out;
int wrong = 0, cycles = 0;
// Set the four selects: output j shows input s<j>; then advance one cycle.
task automatic route(input int s0, input int s1, input int s2, input int s3);
sel[0] = 2'(s0); sel[1] = 2'(s1); sel[2] = 2'(s2); sel[3] = 2'(s3);
tick();
endtask
xbar #(.M(4), .N(4), .W(8)) dut (.in(in), .sel(sel), .out(out));
// Advance to the next rising edge, wait for the design to settle, then print one line.
task automatic tick();
@(posedge clk); #1;
cycles++;
for (int j = 0; j < 4; j++) if (out[j] !== in[sel[j]]) wrong++; // the model: out[j] = in[sel[j]]
$display("e%0d sel=%0d %0d %0d %0d out=%h %h %h %h", edge_n, sel[0], sel[1], sel[2], sel[3], out[0], out[1], out[2], out[3]);
endtask
initial begin
in = {8'h43, 8'h32, 8'h21, 8'h10}; // in[0] = 10, in[1] = 21, in[2] = 32, in[3] = 43
route(0, 1, 2, 3); // straight through
route(3, 2, 1, 0); // reversed
route(1, 1, 1, 1); // one input to every output
route(2, 0, 3, 1);
in = {8'hdd, 8'hcc, 8'hbb, 8'haa}; // new words on the same paths
route(2, 0, 3, 1);
route(0, 0, 3, 3);
$display("%0d cycles, %0d outputs wrong", cycles, wrong);
$finish(0); // quiet: no "$finish called" line
end
endmodule
Check yourself
Remember
- A 2:1 mux of W bits is W cells; an N:1 mux written as an array index or a
caseis a tree with log2(N) stages. - A
? :chain is a priority chain: correct, but its levels of logic grow with N (20 levels at 16:1 on this flow, against 6). - A crossbar is one mux per output: about outputs x width x (inputs - 1) two-input muxes. Width multiplies cells; inputs add levels.
- Price it before you write it. If the brief needs one transfer per cycle, a shared mux and an arbiter replace the crossbar.