Multiplexor, Shifter, Encoder, Decoder

Introduction to multiplexers, shifters, encoders, and decoders.

21369

Step 1: Create a New Project

Create a project Vivado project as you have done before.

Step 2: Design a 4:1 Multiplexor

This project starts with designing a 4:1 1-bit multiplexer. Four on-board slide switches will be used to provide the data inputs, two push buttons will be used as select signals, and LED 0 will be used to show the output of the multiplexer. The most common way to define a 4:1 mux in Verilog is to use a case statement inside an always block. Note: we renamed led[0] to Y in our constraints (.xdc) file, so the output port name is Y.

Create a new source file named mux_4_1.v and enter the code as follows.

module mux_4_1 (
    input [3:0] data,
    input [1:0] sel,
    output Y
);

// we can only assign values to registers 
// inside an always block
reg tmp;

always @(data, sel) begin
    case (sel)
    2'b00:   tmp <= data[0];
    2'b01:   tmp <= data[1];
    2'b10:   tmp <= data[2];
    2'b11:   tmp <= data[3];
    default: tmp <= 1'b0;
    endcase
end
assign Y = tmp;
endmodule

We defined our mux to have 4 data inputs, 2 select inputs, and one output signal. log2(4)log_2 (4) data inputs = 2 select inputs.

Since we are using an always block, we created a temporary 1-bit register called tmp.

The always block has the sensitivity list @(data, sel). This means that tmp will be updated whenever data or sel change.

We wrap the contents of the always block with begin…end. This is analogous to wrapping functions with { } in C or Java.

The case statement checks the current value of sel, and then sets tmp to the corresponding data bit. The default keyword tells tmp what it should be if sel doesn’t equal 00, 01, 10, or 11. Every case statement should include a default case. (Each bit of sel can equal 0, 1, x, or z. In this example, the default case covers all cases where one of the bits of sel is an x or z.)

We assign Y to tmp outside the always block. This completes our mux.

Next, change the constraints (.xdc) file as shown. The constraints file renamed led[0] to Y, sw[3:0] to data[3:0], and btn[1:0] to sel[1:0]. Generate a bitstream and upload it to your Blackboard. Verify the mux works as expected.

# Individual LEDS
set_property -dict { PACKAGE_PIN N20   IOSTANDARD LVCMOS33 } [get_ports { Y }]; #IO_L14P_T2_SRCC_34 Schematic=LD0
# set_property -dict { PACKAGE_PIN P20   IOSTANDARD LVCMOS33 } [get_ports { led[1] }]; #IO_L14N_T2_SRCC_34 Schematic=LD1
# set_property -dict { PACKAGE_PIN R19   IOSTANDARD LVCMOS33 } [get_ports { led[2] }]; #IO_0_34 Schematic=LD2
# set_property -dict { PACKAGE_PIN T20   IOSTANDARD LVCMOS33 } [get_ports { led[3] }]; #IO_L15P_T2_DQS_34 Schematic=LD3
# set_property -dict { PACKAGE_PIN T19   IOSTANDARD LVCMOS33 } [get_ports { led[4] }]; #IO_L3P_T0_DWS_PUDC_B_34 Schematic=LD4
# set_property -dict { PACKAGE_PIN U13   IOSTANDARD LVCMOS33 } [get_ports { led[5] }]; #IO_25_34 Schematic=LD5
# set_property -dict { PACKAGE_PIN V20   IOSTANDARD LVCMOS33 } [get_ports { led[6] }]; #IO_L16N_T2_34 Schematic=LD6
# set_property -dict { PACKAGE_PIN W20   IOSTANDARD LVCMOS33 } [get_ports { led[7] }]; #IO_L17N_T2_34  Schematic=LD7
# set_property -dict { PACKAGE_PIN W19   IOSTANDARD LVCMOS33 } [get_ports { led[8] }]; #IO_L16P_T2_34 Schematic=LD8
# set_property -dict { PACKAGE_PIN Y19   IOSTANDARD LVCMOS33 } [get_ports { led[9] }]; #IO_L22N_T3_34 Schematic=LD9


# Switches
set_property -dict { PACKAGE_PIN R17   IOSTANDARD LVCMOS33 } [get_ports { data[0] }]; #IO_L19N_T3_VREF_34 Schematic=SW0
set_property -dict { PACKAGE_PIN U20   IOSTANDARD LVCMOS33 } [get_ports { data[1] }]; #IO_L15N_T2_DQS_34 Schematic=SW1
set_property -dict { PACKAGE_PIN R16   IOSTANDARD LVCMOS33 } [get_ports { data[2] }]; #IO_L19P_T3_34 Schematic=SW2
set_property -dict { PACKAGE_PIN N16   IOSTANDARD LVCMOS33 } [get_ports { data[3] }]; #IO_L21N_T3_DQS_AD14N_35 Schematic=SW3
# set_property -dict { PACKAGE_PIN R14   IOSTANDARD LVCMOS33 } [get_ports { sw[4] }]; #IO_L6N_T0_VREF_34 Schematic=SW4
# set_property -dict { PACKAGE_PIN P14   IOSTANDARD LVCMOS33 } [get_ports { sw[5] }]; #IO_L6P_T0_34 Schematic=SW5
# set_property -dict { PACKAGE_PIN L15   IOSTANDARD LVCMOS33 } [get_ports { sw[6] }]; #IO_L22N_T3_AD7N_35 Schematic=SW6
# set_property -dict { PACKAGE_PIN M15   IOSTANDARD LVCMOS33 } [get_ports { sw[7] }]; #IO_L23N_T3_35 Schematic=SW7
# set_property -dict { PACKAGE_PIN T10   IOSTANDARD LVCMOS33 } [get_ports { sw[8] }]; #IO_L10P_T1_34 Sch=VGA_R4_CON
# set_property -dict { PACKAGE_PIN T12   IOSTANDARD LVCMOS33 } [get_ports { sw[9] }]; #IO_L10N_T1_34 Sch=VGA_R5_CON
# set_property -dict { PACKAGE_PIN T11   IOSTANDARD LVCMOS33 } [get_ports { sw[10] }]; #IO_L18P_T2_34 Sch=VGA_R6_CON
# set_property -dict { PACKAGE_PIN T14   IOSTANDARD LVCMOS33 } [get_ports { sw[11] }]; #IO_L18N_T2_AD13N_35 Sch=VGA_R7_CON

#Push Buttons
set_property -dict { PACKAGE_PIN W14   IOSTANDARD LVCMOS33 } [get_ports { sel[0] }]; #IO_L8P_T1_34 Schematic=BTN0
set_property -dict { PACKAGE_PIN W13   IOSTANDARD LVCMOS33 } [get_ports { sel[1] }]; #IO_L4N_T0_34 Schematic=BTN1
# set_property -dict { PACKAGE_PIN P15   IOSTANDARD LVCMOS33 } [get_ports { btn[2] }]; #IO_L24P_T3_34 Schematic=BTN2
# set_property -dict { PACKAGE_PIN M14   IOSTANDARD LVCMOS33 } [get_ports { btn[3] }]; #IO_L23P_T3_35 Schematic=BTN3

To simulate the mux, add a new simulation source file. Paste the following code, save the file, and then press “Run Simulation.” You may need to right-click on the file in the Sources window and then select “Set as Top” if you have other simulation source files in the project. Become familiar with zooming and panning in the simulation window. Understand how the simulation works and see if you can verify the mux is working correctly from the output waveforms alone.

// simulation file
`timescale 1ns / 1ps

module mux_tb;

// inputs
reg [3:0] data;
reg [1:0] sel;

// outputs
wire Y;

// connect test signals to our mux
mux_4_1 CUT (
    .data(data),
    .sel(sel),
    .Y(Y)
);

integer k;
initial begin
    sel = 2'b00;
    for(k=0; k < 16; k=k+1) begin
        data = k;
        #10; // wait 10ns
    end
    
    sel = 2'b01;
    for(k=0; k < 16; k=k+1) begin
        data = k;
        #10;
    end
    
    sel = 2'b10;
    for(k=0; k < 16; k=k+1) begin
        data = k;
        #10;
    end
    
    sel = 2'b11;
    for(k=0; k < 16; k=k+1) begin
        data = k;
        #10;
    end
    
    sel = 2'b1z;
    for(k=0; k < 16; k=k+1) begin
        data = k;
        #10;
    end
    
    sel = 2'b1x;
    for(k=0; k < 16; k=k+1) begin
        data = k;
        #10;
    end
    $finish;
end

endmodule

Alternative Ways to Create a 4:1 Mux in Verilog

Since the mux is such a common digital circuit element, it shouldn’t too surprising that there are other ways to create a mux. Two common implementations are shown below.

1. Using the ?: selection operator

The first way to code a mux behaviorally is to use the ?: selection operator. This method is most analogous to the if statement. You can think of this statement as follows: assign data[0] to Y if the statement in the parenthesis is true, else assign whatever is after the colon to Y (and so on). This method is most commonly used with 2-input muxes in practice.

assign Y = (sel == 2'd0) ? data[0] : (
                (sel == 2'd1) ? data[1] : (
                    (sel == 2'd2) ? data[2] : data[3]
                )
            );

2. Using an always Block With an if Statement

The second way to code a mux is by using an always block together with an if-else statement. Note that because tmp is assigned in an always block, it must be declared as register type reg (assign statements cannot be used inside an always block). It is important that there is a closing else statement in Verilog, unlike C or Java.

reg tmp;

always @ (sel, data)
begin
    if (sel == 2'd0)
        tmp <= data[0];
    else if (sel == 2'd1)
        tmp <= data[1];
    else if (sel == 2'd2)
        tmp <= data[2];
    else
        tmp <= data[3];
end
assign Y = tmp;

Step 3: Design a 4:1 2-bit Bus Multiplexor

Now let’s design an 4:1 2-bit bus multiplexer (that is, a multiplexor with four 2-bit bus inputs and a 2-bit bus output). Eight on-board slide switches will be used to provide the data inputs (organized as four 2-bit inputs: I0, I1, I2, I3), two push buttons will be used as select signals, while LED 0 and LED 1 will be used to show the 2-bit output of the bus multiplexer.

We will still need 2 select signals, as this lets us choose between 22=42^2 = 4 different input signals.

module mux_4_2 (
    input [1:0] I0, I1, I2, I3,
    input [1:0] sel,
    output [1:0] Y
);

reg [1:0] tmp;

always @(I0, I1, I2, I3, sel) begin
    case (sel)
    2'b00:   tmp <= I0;
    2'b01:   tmp <= I1;
    2'b10:   tmp <= I2;
    2'b11:   tmp <= I3;
    default: tmp <= 2'b00;
    endcase
end

assign Y = tmp;

endmodule

Create a new simulation file and run the following code. Make sure you right-click this simulation file and select “Set as Top”. If the simulation stops running after 1000ns, click Settings -> Simulation -> Simulation and type in a new simulation runtime.

// simulation
`timescale 1ns / 1ps

module mux_4_2_tb;

// inputs
reg [1:0] I0, I1, I2, I3;
reg [1:0] sel;

// outputs
wire [1:0] Y;

// connect test signals to our mux
mux_4_2 CUT (
    .I0(I0),
    .I1(I1),
    .I2(I2),
    .I3(I3),
    .sel(sel),
    .Y(Y)
);

integer k;
initial begin
    sel = 2'b00;
    for(k=0; k < 256; k=k+1) begin
        {I3, I2, I1, I0} = k;
        #10;
    end
    
    sel = 2'b01;
    for(k=0; k < 256; k=k+1) begin
        {I3, I2, I1, I0} = k;
        #10;
    end
    
    sel = 2'b10;
    for(k=0; k < 256; k=k+1) begin
        {I3, I2, I1, I0} = k;
        #10;
    end
    
    sel = 2'b11;
    for(k=0; k < 256; k=k+1) begin
        {I3, I2, I1, I0} = k;
        #10;
    end
    
    sel = 2'b1z;
    for(k=0; k < 256; k=k+1) begin
        {I3, I2, I1, I0} = k;
        #10;
    end
    
    sel = 2'b1x;
    for(k=0; k < 256; k=k+1) begin
        {I3, I2, I1, I0} = k;
        #10;
    end
    $finish;
end

endmodule

Simulate and implement the 4:1 2-bit mux on your Blackboard. To create a bitstream, you will need to modify the constraints file from step 2.

Step 4: Design a Binary Decoder

In this section you are going to design a 3:8 binary decoder. The example presented here uses a 3-bit bus I[2:0] for input signals, and an 8-bit bus Y[7:0] for output signals. Three individual input wires and eight indiviudal output wires could have been used instead, but then the Verilog `code would be less compact.

Declare a 3:8 Binary Decoder

Create a Verilog module called decoder_3_8 with inputs I and outputs Y as follows.

Perhaps the most readable way to describe the behavior of a decoder is to use a case statement in an always block as shown.

module decoder_3_8 (
    input [2:0] I,
    output reg [7:0] Y
);

always @ (I)
begin
    case (I)
        3'd0:    Y <= 8'd1;
        3'd1:    Y <= 8'd2;
        3'd2:    Y <= 8'd4;
        3'd3:    Y <= 8'd8;
        3'd4:    Y <= 8'd16;
        3'd5:    Y <= 8'd32;
        3'd6:    Y <= 8'd64;
        3'd7:    Y <= 8'd128;
        default: Y <= 8'd0;
    endcase
end
endmodule

Step 5: Design a Priority Encoder

In this section, you are going to design a 4-input priority encoder. A 4-bit bus I[3:0] will be used as data inputs, and Ein, will act as the “Enable” signal. A 2-bit output bus Y[1:0] will show the encoded value of the inputs, and two one bit outputs GS and Eout will show the group signal and enable output, respectively.

Declare a 4-Input Priority Encoder

Create another Verilog module called encoder with inputs I, Ein, and outputs Eout, GS, and Y.

The most efficient way to describe the behavior of a priority encoder is to use if-else statement in an always block. The priority encoder has three outputs, and so three always blocks are needed to define the output signals. Notice the use of “nested” if statements.

module priority_encoder(
	input [3:0] I,
	input Ein,
	output reg [1:0] Y,
	output reg GS, 
	output reg Eout
);

always @ (I, Ein)
begin
    if(Ein == 1) begin
        if (I[3] == 1)
            Y <= 2'd3;
        else if (I[2] == 1)
            Y <= 2'd2;
        else if (I[1] == 1)
            Y <= 2'd1;
        else
            Y <= 2'd0;
    end
    else
        Y = 2'd0;
end

always @ (I, Ein)
begin
    if (Ein == 1 && I == 0)
        Eout <= 1'b1;
    else
        Eout <= 1'b0;
end

always @ (I, Ein)
begin
    if (Ein == 1 && I != 0)
        GS <= 1'b1;
    else
        GS <= 1'b0;
end
endmodule

Step 6: Design a Shifter

In this section, you are going to design a 4-input Shifter. A 4-bit bus I[3:0] will be used for data inputs, and four other 1-bit inputs are used for the control signals F (fill), R (rotate/shift), D (direction), and En (enable signal). Bus Y[3:0] will show the output of the shifter.

Declare a Shifter

Create a Verilog module called shifter with inputs I, En, D, R, F and outputs Y as follows:

Similar to previous steps, you will use if-else statement again to implement the shifter.

module shifter (
    input [3:0] I,
    input D,
    input R,
    input F,
    input En,
    output reg [3:0] Y
);

always @ (I, En)
begin
    if (En == 0)
        Y <= I;
    else begin
        if (R == 0)
            Y <= (D == 0) ? {I[2:0], F} : {F, I[3:1]};
        else
            Y <= (D == 0) ? {I[2:0], I[3]} : {I[0], I[3:1]};
    end
end
endmodule

Concatenation

In the shifter’s behavioral code, {A,B} is used to concatenate two groups of signals into a bus. For example, Y <= {I[2:0], F} means Y[3:1] <= I[2:0] and Y[0] <= F.

Aside - Declaring Outputs as Registers

It is also possible to declare outputs as registers. For example, a 4:1 1-bit mux can be written as follows:

module mux_4_1 (
    input [3:0] data,
    input [1:0] sel,
    output reg Y
);

always @(data, sel) begin
    case (sel)
    2'b00:   Y <= data[0];
    2'b01:   Y <= data[1];
    2'b10:   Y <= data[2];
    2'b11:   Y <= data[3];
    default: Y <= 1'b0;
    endcase
end
endmodule

While this does clean up the code and remove reg tmp from the module, it does have one drawback:

Sometimes a digital design is not working properly, and you have no idea where to start looking for the issue. One approach is to begin checking that each individual module simulates correctly. This approach may identify the broken module, but often will not show why the module is not working correctly. More complex modules will have many internal registers, and checking their values as inputs change can be extremely helpful in solving the problem.

To do this, you can copy and paste the body of the broken module into a simulation file, run some tests, and check how the inputs change the internal register values. If an output is declared as a register in the module, it will have to be renamed to a wire in the simulation, as simulation outputs are wires.

Because of this, some digital engineers choose to always declare their module inputs and outputs as wires (as we have done in all previous examples). It makes simulating a broken module a little easier, at the expense of a little more code. Ultimately, this is a preference choice, and there is not right or wrong side to this issue.

Below is an example of a 4:1 mux in a simulation file. Compare and contrast this file to the simulations above.

// simulation
`timescale 1ns / 1ps
module mux_4_1_dev;
// ---- begin inputs and outputs of mux module ----- //
reg [3:0] data;
reg sel;
wire Y;
// ---- end inputs and outputs of mux module ----- //
// ----- begin body of mux module ----- //
// this section is copy/pasted from a module file
reg tmp;

always @(data, sel) begin
    case (sel)
    2'b00:   tmp <= data[0];
    2'b01:   tmp <= data[1];
    2'b10:   tmp <= data[2];
    2'b11:   tmp <= data[3];
    default: tmp <= 1'b0;
    endcase
end
assign Y = tmp;
// ----- end body of mux module ----- //

initial begin
	// TODO: write test code
	// Internal registers will appear in simulated waveforms
end
endmodule