Transparent Logo

Chisel Multiplexers Tutorial

mux

Hello FPGAmigos ! Last time, we covered the data types and operators in Chisel. Today, we will delve into the topic of multiplexers. While it is possible to construct a multiplexer using combinational logic, this operation is widely used in FPGA and digital design, prompting Chisel to include it as a standard function.

All the code is available on the github repository.

I. What is a multiplexer ?

A multiplexer is an operation that allows you to select between inputs using a select signal. Multiplexers, often referred to as Mux for short, are commonly denoted as Mux n:1, where n represents the number of inputs. The simplest form of a multiplexer is the Mux 2:1, which has two inputs and one output. The select bit determines which input is replicated on the output.

mux
Mux n:1 schema

II. Mux

Prerequisites:

import chisel3._
import chisel3.util._ // for MuxCase, PriorityMux, MuxLookup and Mux1H 

As I said, Chisel made it a standard function you just write:

class MuxBundle extends Bundle{
    val a = Input(Bool())
    val b = Input(Bool())
    val select = Input(Bool())
    val out = Output(Bool())
}

class MuxExample extends Module{
    val io = IO(new MuxBundle)

    io.out := Mux(io.select, io.a, io.b)
}

The “Mux” constructor create a multiplexer 2:1. The first parameter is the select signal that should be of bool type. You can use the .asBool function if your signal is a UInt or a Bit. If io.select is 0 then the mux output io.a, otherwise it outputs b. Please note that io.a, io.b and io.out should be of the same type and size. Here they are all boolean but they can be UInt, SInt and Bits too.

I used this module to turn on a LED. The select signal is connected to a push button, “a” is connected to switch 1, and “b” is connected to switch 2. Here is the truth table:

selectabout
0xxa
1xxb
Mux 2:1 truth table

Please note that “x” represents don’t-care values, meaning the specific values of “a” and “b” do not matter when the select signal is high (1) since the output will always be “b”.

The generated verilog:

module MuxExample(
  input   io_a,
  input   io_b,
  input   io_select,
  output  io_out
);
  wire  select_bt_pushed = ~io_select; // @[main.scala 16:28]
  assign io_out = select_bt_pushed ? io_a : io_b; // @[main.scala 18:18]
endmodule

You may want to create a mux with more inputs and create a nested Mux like this:

// don't do that
val nested_mux = Mux(sel1, input_1, Mux(sel2, input_2, Mux(.....)))

But this is not recommended for readability and useless since nested mux is already provided as a build-in function with MuxCase.

III. MuxCase

When should you use MuxCase ? You should use MuxCase when you have several conditions that are mutually exclusive and prioritize them.

class MuxCaseBundle extends Bundle{
    val switch1 = Input(Bool())
    val switch2 = Input(Bool())
    val out = Output(UInt(2.W))
}
class MuxCaseExample extends Module{
    val io = IO(new MuxCaseBundle)

    val my_cases = Seq(
        io.switch1 -> 1.U,
        io.switch2 -> 2.U
    )
    io.out := MuxCase(0.U, my_cases)
}

MuxCase takes two parameters:

  • The first parameter is the default value to be returned if none of the specified conditions are satisfied.
  • The second parameter is a Scala sequence (Seq) containing tuples with the condition and the corresponding value to be returned (condition -> value). The condition should be of boolean type, so if it is a signal, it should be cast using .asBool.

The created multiplexer will output the value associated with the first condition that evaluates to true. In this example, if switch1 is on, it will activate Led9 on the board, and if switch2 is on, it will activate Led10. If both switches are on, only Led9 will be activated since switch1 is the first condition in the sequence.

The generated Verilog:

module MuxCaseExample(
  input        io_switch1,
  input        io_switch2,
  output [1:0] io_out
);
  wire [1:0] _io_out_T = io_switch2 ? 2'h2 : 2'h0; // @[Mux.scala 101:16]
  assign io_out = io_switch1 ? 2'h1 : _io_out_T; // @[Mux.scala 101:16]
endmodule

IV. PriorityMux

When should you use PriorityMux ? When you want to prioritize the conditions. If several conditions are true at the same time, the first condition to be true in the sequence will have the priority. What is the difference with MuxCase ? No default value, If no condition are true the last condition will be output.

class PriorityMuxExample extends Module{
    val io = IO(new MuxCaseBundle)

    val my_cases = Seq(
        io.switch1 -> 1.U,
        io.switch2 -> 2.U,
        io.switch3 -> 4.U
    )
    io.out := PriorityMux(my_cases)
}

Here you can notice that it is exactly the same example than MuxCase. Look at the generated Verilog now:

module PriorityMuxExample(
  input        io_switch1,
  input        io_switch2,
  output [2:0] io_out
);
  wire [2:0] _io_out_T = io_switch2 ? 3'h2 : 3'h4; // @[Mux.scala 47:70]
  assign io_out = io_switch1 ? 3'h1 : _io_out_T; // @[Mux.scala 47:70]
endmodule

You can see that the switch3 has been deleted by chisel. If switch1 is on the output will be 1. If switch1 and switch2 are on, the output will still be 1 (because the priority is given to switch1). If only switch2 is on, the output is 2. But if no switch is on, then the output is 4.

I guess it is used to reduce resource consumption. Your last condition can be used as a default case with less resource than in MuxCase. Tell me what you think in the comment.

V. MuxLookUp

When should you use MuxLookup ? You should use MuxLookup when your select signal is single signal that can take multiple values.

class MuxLookUpBundle extends Bundle{
    val index = Input(Bits(3.W))
    val out   = Output(UInt(8.W))
}

class MuxLookupExample extends Module{
    val io = IO(new MuxLookUpBundle)

    val my_cases = Seq.tabulate(8)(idx =>(idx.U -> (1.U<<idx)))
    io.out := MuxLookup(io.index, 0.U(8.W), my_cases)
}

MuxLookup takes 3 parameters:

  • The first parameter is the signal upon which the output depends.
  • The second parameter is the default value to be output when none of the specified conditions are met.
  • The third parameter is a Scala sequence of tuples in the form of key -> value. If the select signal is equal to a specific key, the multiplexer will output the corresponding value.

In this example, the inputs (io.index) consist of three switches that represent a 3-bit integer ranging from 0 to 7. The outputs (io.out) control eight LEDs. Each LED’s index corresponds to the input switch integer, meaning that the LED with the same index as the input switch integer will be turned on.

The Seq.tabulate function creates a sequence and iterates from 0 to 7, storing the current index in the variable “idx”. The “=>” operator applies a function to “idx” and returns a tuple (idx.U -> (1.U << idx)). The first element, “idx.U”, represents the key. If the value of io.index matches this key, the multiplexer will output the value 1.U shifted left by “idx”. Here, the eight LEDs represent an 8-bit unsigned integer, so shifting 1.U left by the index turns on the LED with the corresponding index.

The generated Verilog:

module MuxLookupExample(
  input  [2:0] io_index,
  output [7:0] io_out
);
  wire [1:0] _io_out_T_1 = 3'h1 == io_index ? 2'h2 : 2'h1; // @[Mux.scala 81:58]
  wire [2:0] _io_out_T_3 = 3'h2 == io_index ? 3'h4 : {{1'd0}, _io_out_T_1}; // @[Mux.scala 81:58]
  wire [3:0] _io_out_T_5 = 3'h3 == io_index ? 4'h8 : {{1'd0}, _io_out_T_3}; // @[Mux.scala 81:58]
  wire [4:0] _io_out_T_7 = 3'h4 == io_index ? 5'h10 : {{1'd0}, _io_out_T_5}; // @[Mux.scala 81:58]
  wire [5:0] _io_out_T_9 = 3'h5 == io_index ? 6'h20 : {{1'd0}, _io_out_T_7}; // @[Mux.scala 81:58]
  wire [6:0] _io_out_T_11 = 3'h6 == io_index ? 7'h40 : {{1'd0}, _io_out_T_9}; // @[Mux.scala 81:58]
  assign io_out = 3'h7 == io_index ? 8'h80 : {{1'd0}, _io_out_T_11}; // @[Mux.scala 81:58]
endmodule

VI. Mux1H

When should you use Mux1h ? For resource optimization.

When using constructs like MuxCase, a large binary-encoded select signal would need to route to each multiplexer, potentially leading to more routing congestion and a larger area.

Mux1H, on the other hand, uses one-hot(1H) encoding, where each select line directly corresponds to an input line. Therefore, no logic is required to decode the select lines, and each input only needs to consider a single select line. This can potentially lead to a more efficient implementation in terms of both logic resources and signal routing, particularly for large multiplexers.

You should use Mux1h with a “One Hot” selector, which means having only one bit set to 1 in a bit field, while the rest are set to 0. This ensures that only one condition is true at a time. It is your responsibility to ensure that the “One Hot” property is respected, as the output of Mux1H becomes undefined if there are multiple bits set to 1 or if all bits are set to 0.

Example of One Hot encoding on 4 bits: 0001, 0010, 0100, 1000.

class Mux1hBundle extends Bundle{
    val switches = Input(Bits(2.W))
    val out      = Output(Bits(2.W))
}

class Mux1hExample extends Module{
    val io = IO(new Mux1hBundle)

    val my_cases = Seq(
        io.switches(0) -> 1.U,
        io.switches(1) -> 2.U
        )
    io.out := Mux1H(my_cases)
}

Here if I turn a switch ON the corresponding led will turn ON. But if I turn both switch on (and don’t respect the One Hot property), both LED will turn on. In more complex design, you could have more serious border effect.

The generated Verilog:

module Mux1hExample(
  input  [1:0] io_switches,
  output [1:0] io_out
);
  wire [1:0] _io_out_T_1 = io_switches[1] ? 2'h2 : 2'h0; // @[Mux.scala 27:73]
  wire [1:0] _GEN_0 = {{1'd0}, io_switches[0]}; // @[Mux.scala 27:73]
  assign io_out = _GEN_0 | _io_out_T_1; // @[Mux.scala 27:73]
endmodule

VII. The Top

Here is the top of my design where you can see why I separated the bundle classes from the example. The Module, Bundle, IO topics will be cover in a future article, so stay tuned. I use the alchitry CU board with the element IO shield.

class AlchitryCUTop extends Module {
    val io_mux         = IO(new MuxBundle)
    val io_muxcase     = IO(new MuxCaseBundle)
    val io_muxlookup   = IO(new MuxLookUpBundle)
    val io_mux1H       = IO(new Mux1hBundle)
    val io_priorityMux = IO(new MuxCaseBundle)

    // the alchitry CU board has an active low reset
    val reset_n = !reset.asBool

    withReset(reset_n){
        val my_mux         = Module(new MuxExample)
        val my_muxcase     = Module(new MuxCaseExample)
        val my_muxlookup   = Module(new MuxLookupExample)
        val my_mux1h       = Module(new Mux1hExample)
        val my_priorityMux = Module(new PriorityMuxExample)

        io_mux         <> my_mux.io
        io_muxcase     <> my_muxcase.io
        io_muxlookup   <> my_muxlookup.io
        io_mux1H       <> my_mux1h.io
        io_priorityMux <> my_priorityMux.io
    }
}

The pcf file is on the github page. For educational purpose and code readability, I did not add registers to this design. Since multiplexers can be big combinational structures, it can be a good idea to add a register whenever you can on the multiplexer output. Of course input switches should be de-bounced for signal integrity too.

Conclusion

Today, we learned about utilizing the various Mux functions offered by Chisel and understood their appropriate usage. If you found this tutorial helpful, please remember to bookmark, share, comment, and subscribe to the newsletter using the provided ebook form.

We hope you enjoy it!

Leave a Reply

Your email address will not be published. Required fields are marked *

Chisel Multiplexers Tutorial

dark blue dot

Summary

Share it !

Get my Ebook ?

ebook_hero-home

Jumpstart you FPGA journey by

• Understanding the place of FPGA in industry
• Learn about internal composition of an FPGA
• A simple beginner friendly project
• An overview of the FPGA workflow
ebook_banner_11