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 wordsCellsLevels of logic
each output a chain of ? : tests26,28720
each output an array index, in[sel[j]]22,5086

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.

An 8:1 mux as a treeEach box is a 2:1 mux for every bit of the word. Arrows carry words; each column uses one select bit.
An 8:1 mux as a treeEach box is a 2:1 mux for every bit of the word. Arrows carry words; each column uses one select bit. mux d0 / d1 sel[0] mux d2 / d3 sel[0] mux d4 / d5 sel[0] mux d6 / d7 sel[0] mux low half sel[1] mux high half sel[1] mux y sel[2]
  1. Level 1

    Four muxes pick within the pairs, all at once, using sel[0].

  2. Level 2

    Two muxes pick a pair from each half, using sel[1].

  3. 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 muxArray index: cellslevels? : chain: cellslevels
2:18181
4:1242274
8:156311810
16:1180622720
32:1482869541

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.

A 4 x 4 crossbarEvery input word fans out to all four muxes. Each mux has its own select. Arrows carry words.
A 4 x 4 crossbarEvery input word fans out to all four muxes. Each mux has its own select. Arrows carry words. input in[0..3] four words mux mux 0 sel[0] mux mux 1 sel[1] mux mux 2 sel[2] mux mux 3 sel[3] output out[0..3] four words out[0]out[1]out[2]out[3]
  1. Fan out

    All four input words reach every mux.

  2. 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)CellsLevels of logicN x W x (M - 1)
2 x 2, 8 bits16116
4 x 4, 8 bits (the lab)96296
4 x 4, 32 bits3842384
8 x 8, 8 bits4483448
8 x 8, 32 bits1,79231,792
16 x 16, 8 bits2,83661,920
4 x 4, 8 bits, each mux a ? : chain1084
16 x 16, 64 bits, each mux a ? : chain26,28720

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.

  1. 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.

  2. 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

Design brief
PortDirectionWidthMeaning
ininput[M-1:0][W-1:0]the M inputs
selinput[N-1:0][$clog2(M)-1:0]one select per output
outoutput[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
M inputs, N outputs, W bits; 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.

The 4:1 mux as a chain of ? : tests.
assign y = (sel == 0) ? d[0] : (sel == 1) ? d[1] : (sel == 2) ? d[2] : d[3];
Asked in interviews
  1. 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.

  2. 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.

  3. 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.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: 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 wrong

Original 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

Predict the outputThe lab's 4 x 4 crossbar with inputs 11, 22, 33, 44 (in[0] first) and selects 3, 3, 0, 1 (sel[0] first). What does the program print?
// mux_n: an N-to-1 multiplexer of W-bit words. y shows word sel of d.
module mux_n #(parameter int N = 4, parameter int W = 8) (
  input  logic [N-1:0][W-1:0]   d,
  input  logic [$clog2(N)-1:0] sel,
  output logic [W-1:0]         y
);
  assign y = d[sel];             // an array index: the synthesizer builds a tree of 2:1 muxes
endmodule

// xbar: M inputs, N outputs, W bits each. Output j shows input sel[j];
// two outputs may show the same input, and an input may go nowhere.
module xbar #(parameter int M = 4, parameter int N = 4, parameter int W = 8) (
  input  logic [M-1:0][W-1:0]         in,
  input  logic [N-1:0][$clog2(M)-1:0] sel,
  output logic [N-1:0][W-1:0]         out
);
  for (genvar j = 0; j < N; j++) begin : g_out
    mux_n #(.N(M), .W(W)) u_mux (.d(in), .sel(sel[j]), .y(out[j]));
  end
endmodule

module tb;
  logic [3:0][7:0] in;
  logic [3:0][1:0] sel;
  logic [3:0][7:0] out;
  xbar #(.M(4), .N(4), .W(8)) dut (.in(in), .sel(sel), .out(out));
  initial begin
    in[0] = 8'h11; in[1] = 8'h22; in[2] = 8'h33; in[3] = 8'h44;
    sel[0] = 2'd3; sel[1] = 2'd3; sel[2] = 2'd0; sel[3] = 2'd1;
    #1 $display("out: %h %h %h %h", out[0], out[1], out[2], out[3]);
    $finish(0);
  end
endmodule
Spot the bugEach option is the body of a 4:1 mux of 8-bit words. The bench tries all four selects. Which body fails?
ConceptA 4 x 4 crossbar of 8-bit words measured 96 cells. Before you synthesize it, what do you expect for 8 x 8 at 8 bits?
ConceptA brief needs 16 sources to reach 16 sinks with 64-bit words, but only one transfer per cycle is ever needed. Which change cuts the cost most?
ConceptIn the measured table, a 16:1 mux of 8-bit words is 6 levels of logic as an array index and 20 as a ? : chain. Why does the chain go so deep?

Remember

  • A 2:1 mux of W bits is W cells; an N:1 mux written as an array index or a case is 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.