Multiplexor, Shifter, Encoder, Decoder
Introduction to multiplexers, shifters, encoders, and decoders.
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.
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
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}
meansY[3:1] <= I[2:0]
andY[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