A 7-segment display is a set of bar-shaped LEDs that allows representing digits through combinations of on and off states.
In this article, we will apply what we learned about the case statement to build a Hexadecimal Decoder.
The goal is simple: we have a 4-bit number inside the FPGA (from 0 to 15) and we want it to be visually displayed on the display.
The hardware: seven LEDs
A 7-segment display is straightforward; it is literally 7 packaged LEDs with a specific shape (plus an eighth for the decimal point).
Each segment has a standard letter assigned, from a to g, starting at the top and going clockwise, ending with the middle bar (g).
To draw a number, we simply need to turn on the correct combination of LEDs.
- For “1”: Turn on b and c.
- For “8”: Turn on all (a, b, c, d, e, f, g).
Common Anode and Common Cathode
Here comes the first common stumbling block. Depending on how the LEDs are internally connected, there are two types of displays:
- Common Cathode: All negatives are tied to Ground (GND). To light a segment, we put a 1 (Positive Voltage) on its pin. (Positive Logic).
- Common Anode: All positives are tied to VCC (3.3V). To light a segment, we put a 0 (Ground) on its pin to allow current to flow. (Negative Logic).
The vast majority of FPGA development boards (like the IceStick, Go Board, or Alhambra) use Common Anode displays or inverting drivers. This means 0 turns on and 1 turns off.
Decoder Design
We will design a module that takes a 4-bit number (data) and returns a 7-bit bus (segments) where each bit corresponds to a letter: gfe_dcba.
In classic university courses, they would have you draw a giant truth table and solve 7 Karnaugh Maps to find the Boolean equation for each LED.
In Verilog, thanks to abstraction, we simply describe the truth table using a case. The synthesizer will take care of doing the Karnaugh Maps for us.
Verilog Code for Common Cathode
First, we’ll do it thinking “1 turns on”, which is more natural for the brain.
module decoder_7seg (
input wire [3:0] number, // Value from 0 to 15 (Hexadecimal)
output reg [6:0] seg // Output gfe_dcba
);
// Define which segments to light for each number
// Format: gfe_dcba
// 1 means ON
always @(*) begin
case(number)
// gfedcba
4'h0: seg = 7'b0111111; // 0: Everything except g
4'h1: seg = 7'b0000110; // 1: Only b and c
4'h2: seg = 7'b1011011; // 2
4'h3: seg = 7'b1001111; // 3
4'h4: seg = 7'b1100110; // 4
4'h5: seg = 7'b1101101; // 5
4'h6: seg = 7'b1111101; // 6
4'h7: seg = 7'b0000111; // 7
4'h8: seg = 7'b1111111; // 8: All
4'h9: seg = 7'b1101111; // 9
// Hexadecimal (A-F)
4'hA: seg = 7'b1110111; // A
4'hB: seg = 7'b1111100; // b (lowercase)
4'hC: seg = 7'b0111001; // C
4'hD: seg = 7'b1011110; // d (lowercase)
4'hE: seg = 7'b1111001; // E
4'hF: seg = 7'b1110001; // F
default: seg = 7'b0000000; // Off for safety
endcase
end
endmoduleAdaptation to Common Anode
If you load the previous code onto a typical board, you will see the numbers “in negative” (the LEDs that should be off will turn on and vice versa).
To fix it, you don’t need to rewrite the entire case with zeros. We simply invert the output.
We can create a wrapper module or modify the output:
module top_display (
input wire [3:0] switches,
output wire [6:0] display_leds // Connected to physical pins
);
wire [6:0] positive_logic;
// 1. Instantiate our pure decoder
decoder_7seg my_deco (
.number(switches),
.seg(positive_logic)
);
// 2. Invert the bits before going out to the physical world
// If positive_logic is 1 (On), we send 0 to the pin (Ground) to turn on.
assign display_leds = ~positive_logic;
endmoduleConfiguration via Parameters
If we want to make portable code that works on any board, we can add a configuration parameter.
module display_driver #(
parameter COMMON_ANODE = 1 // 1 to invert, 0 for normal
) (
input wire [3:0] num,
output wire [6:0] pin_output
);
reg [6:0] internal_segments;
always @(*) begin
case(num)
// ... (case table) ...
endcase
end
// Conditional XOR operation:
// If COMMON_ANODE is 1, invert (segments ^ 111... = ~segments)
// If COMMON_ANODE is 0, leave as is (segments ^ 000... = segments)
assign pin_output = internal_segments ^ {7{COMMON_ANODE}};
endmodule