Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

Decoder Design with `case` and `always`

Recommended resources

Welcome back! In our previous lesson, we investigated one of the most common issues in RTL design: the unintended latch. You learned that latches are inferred when an output in a combinational always block isn't assigned a value for every possible input combination, and you saw how to prevent this by either fully specifying all branches or using default assignments.

Today, we'll put that knowledge into practice by modeling another fundamental building block: the decoder. We will focus on using a case statement within an always @(*) block to implement this logic. This is an industry-standard technique that results in clean, readable, and efficient hardware descriptions. By the end of this lesson, you'll be able to model a decoder from its truth table using this powerful Verilog construct.

1. What is a Decoder?

At its core, a decoder is a combinational circuit that converts a binary input code into a specific output signal. For an N-bit input, a decoder typically has output lines. Its primary function is to assert exactly one of these output lines based on the value of the input code.

For example, a 2-to-4 decoder has two inputs and four outputs. If the input is 2'b00, the first output line is activated. If the input is 2'b01, the second is activated, and so on. This "one-hot" output format (where only one bit is 'hot' or active) is used in many applications, such as memory address decoding or selecting which sub-unit of a larger circuit should be active.

2-4 Decoder Circuit and Truth Table for Verilog Implementation
This image shows the logic diagram and truth table for a basic 2-to-4 decoder. Notice in the truth table how each unique input combination (X1, X0) corresponds to exactly one output (D0-D3) being high.

The following video provides a concise overview of a 2-to-4 decoder, including its truth table and the logic expressions derived from it.

Decoder 2: 4 | verilog code for 2 to 4 decoder in data flow and behavioral description

This video from Explore Electronics explains the basic functionality of a 2-to-4 decoder.

Please watch the first part of the video covering the decoder's function and truth table. Don't worry about the logic expressions or the gate-level circuit diagram for now; focus on understanding the relationship between the inputs and the one-hot outputs.


2. The case Statement: A Better Tool for the Job

While you could model a decoder using a series of if-else if statements, this can become cumbersome and less readable as the number of inputs grows. A much better tool for this kind of "one-of-many" selection logic is the case statement.

The case statement compares an expression against a list of possible values and executes the statements associated with the first match it finds. For hardware design, this structure maps very naturally to a multiplexer or, in our case, decoder logic.

To learn the syntax and advantages of the case statement, watch the following video.

#27 "case" statement in verilog | if-else vs CASE || when to use if-else and case in verilog

This video from Component Byte introduces the Verilog case statement and compares it to if-else.

Please watch the following segments: Start with the introduction to what a case statement is. Then, learn why it's preferred over if-else for many conditions. Next, review the syntax, paying close attention to the default keyword. Understand how a case statement in an always @(*) block creates purely combinational logic. Finally, watch the performance comparison with if-else.

As the video highlights, a key reason to prefer case for decoder logic is that it implies a parallel structure to the synthesis tool, whereas a nested if-else chain implies priority. For a decoder, all inputs should be treated with equal priority, making case a more accurate representation of the hardware's intent.

Most importantly, the default clause in a case statement is our primary tool for preventing unintended latches, directly applying what we learned in the last lesson.


3. Modeling a Decoder in Verilog

Now let's combine these concepts to write the Verilog for a decoder. We will use an always @(*) block with a case statement to model a 3-to-8 decoder. This module will have a 3-bit input and an 8-bit output.

The following article provides a perfect, concise example of this.

3x8 Decoder Implementation in Verilog

This article by RAO MUHAMMAD UMER on Medium provides a clear and direct implementation of a 3x8 decoder using a case statement.

Read the introduction and the "Explanation" section that describes the decoder_3_8 module. Study the Verilog code provided.

Let's break down the key parts of the code from the article:

// Code based on the resource [LINK](https://medium.com/@raoumer160903/3x8-decoder-implementation-in-verilog-9a8b659650c0)
module decoder_3_8(
    output reg [7:0] y, 
    input [2:0] i
);

    always @(*) begin
        case (i)
            3'b000: y = 8'b00000001;
            3'b001: y = 8'b00000010;
            3'b010: y = 8'b00000100;
            3'b011: y = 8'b00001000;
            3'b100: y = 8'b00010000;
            3'b101: y = 8'b00100000;
            3'b110: y = 8'b01000000;
            3'b111: y = 8'b10000000;
            default: y = 8'b00000000; // Important!
        endcase
    end
endmodule

Key points to notice:

  • output reg [7:0] y: The output y is declared as reg because it is assigned from within a procedural always block.
  • always @(*): This sensitivity list tells the simulator and synthesis tool to re-evaluate the block whenever any of the inputs (i in this case) change, creating combinational logic.
  • case (i): The case statement selects which branch to execute based on the value of the 3-bit input i.
  • 3'b000: y = 8'b00000001;: Each case item maps a specific 3-bit input value to a unique 8-bit one-hot output value.
  • default: y = 8'b00000000;: This is critical. While our case statement covers all 8 possible values of the 3-bit input i, explicitly adding a default is excellent practice. It prevents latch inference in cases where not all inputs are specified and makes the designer's intent clear. Here, we choose to output all zeros if an unexpected value (like 3'bxxz) were to appear in simulation.

This case-based structure is very powerful and readable, even for more complex decoding tasks. For instance, decoding a 4-bit binary number to drive a seven-segment display uses the exact same pattern, just with different output values.

Verilog Seven-Segment Display Decoder using Case Statement
This example of a seven-segment display decoder shows how the `case` statement provides a clean and readable way to map a 4-bit binary input to the 7-bit patterns needed to form digits on the display.

4. Your Turn: Code a 2-to-4 Decoder

Now it's your turn to apply what you've learned. Based on the 3-to-8 decoder example and the truth table from the beginning of the lesson (Image LINK), write the Verilog module for a 2-to-4 decoder named decoder_2_to_4.

  • Inputs: A 2-bit wire named i.
  • Outputs: A 4-bit reg named y.
  • Use an always @(*) block and a case statement.
  • Remember to include a default case.
Solution
// Verilog module for a 2-to-4 decoder
module decoder_2_to_4 (
    output reg [3:0] y,
    input [1:0] i
);

    always @(*) begin
        case (i)
            2'b00: y = 4'b0001;
            2'b01: y = 4'b0010;
            2'b10: y = 4'b0100;
            2'b11: y = 4'b1000;
            default: y = 4'b0000;
        endcase
    end

endmodule

Conclusion

In this lesson, you've successfully combined your knowledge of always blocks and latch avoidance to build a standard digital component. The case statement is the perfect tool for describing decoders, resulting in Verilog that is not only functionally correct but also clearly communicates the design's intent to both simulators and synthesis tools.

Key Takeaways:

  • A decoder converts an N-bit binary input into a -bit one-hot output.
  • The Verilog case statement is the ideal structure for modeling decoder logic within an always @(*) block.
  • Using case implies parallel logic, which is a better fit for decoders than the priority logic implied by if-else chains.
  • Always include a default case to fully specify the logic and prevent the inference of unintended latches.

In our next lesson, we will move on to another essential combinational circuit: the adder. We'll learn how to implement a parameterized N-bit adder, which will introduce the powerful concept of creating flexible and reusable Verilog modules.