Designing a Custom 7-Segment Display Driver in Verilog for FPGAs
Author : Eim Technology | Published On : 20 Aug 2026
Field-Programmable Gate Arrays (FPGAs) are incredibly versatile platforms for digital logic design, offering hardware-level control that microcontrollers simply cannot match. For beginners and seasoned engineers alike, designing a custom driver for a 7-segment display is often considered a quintessential project. It bridges the gap between abstract digital logic and tangible, visual outputs.
This comprehensive guide will walk you through the entire process of designing a custom 7-segment display driver using Verilog. We will cover the foundational theory behind the hardware, the mathematics of multiplexing, the conversion of binary data to visual characters, and the step-by-step implementation of the Verilog code required to bring your display to life.
Understanding the Anatomy of a 7-Segment Display
Before writing a single line of Verilog code, it is essential to understand the physical hardware you are trying to control. A 7-segment display, as the name suggests, consists of seven individual Light Emitting Diodes (LEDs) arranged in a figure-eight pattern. Most modern displays also include an eighth LED serving as a decimal point (DP). By selectively turning these specific LEDs on and off, you can represent all numerical digits from 0 to 9, as well as several alphabetical characters (like A, B, C, D, E, and F used in hexadecimal notation).
The segments are universally labeled with letters from 'a' through 'g':
-
Segment a: The top horizontal bar.
-
Segment b: The top-right vertical bar.
-
Segment c: The bottom-right vertical bar.
-
Segment d: The bottom horizontal bar.
-
Segment e: The bottom-left vertical bar.
-
Segment f: The top-left vertical bar.
-
Segment g: The middle horizontal bar.
Common Anode vs. Common Cathode
When interfacing these displays with an FPGA, you must identify whether your display is a Common Anode or a Common Cathode type. This distinction entirely changes the logic you will write in your Verilog code.
-
Common Cathode: In this configuration, all the negative terminals (cathodes) of the eight LEDs are connected to a single common pin, which is then connected to ground (0V). To illuminate a specific segment, you must apply a logical high voltage (Logic 1) to its corresponding anode pin.
-
Common Anode: Here, all the positive terminals (anodes) of the LEDs are tied together and connected to the positive supply voltage (VCC). To light up a segment, you must pull its corresponding cathode pin to ground by applying a logical low voltage (Logic 0) from your FPGA.
Many FPGA development boards utilize Common Anode displays because driving a pin low to sink current is often more efficient for the integrated circuits. For the remainder of this article, we will assume a Common Anode configuration, meaning a '0' turns a segment ON and a '1' turns it OFF.
Hardware Setup and Current Limitation
FPGAs are delicate digital devices. Their input/output (I/O) pins are designed to handle very small amounts of current, typically in the range of a few milliamperes (mA). If you connect an FPGA pin directly to an LED on a 7-segment display without a current-limiting resistor, you risk drawing too much current and permanently damaging the FPGA's I/O bank.
Always ensure there is a resistor (usually between 220 ohms and 470 ohms, depending on your FPGA's voltage levels) placed in series between the FPGA pin and the display's segment pin. If you are using a commercial FPGA development board, these resistors are almost always built into the printed circuit board (PCB), allowing you to focus entirely on the digital logic design.
Core Concepts of Multiplexing
If you have a single 7-segment display, you simply need 8 pins on your FPGA (7 for the segments, 1 for the decimal point) to control it. However, most practical applications require displaying multiple digits—for example, a 4-digit clock or a sensor reading.
Connecting four displays directly would require 32 individual I/O pins. This is an inefficient use of resources. Instead, engineers use a technique called multiplexing.
In a multiplexed display, all the 'a' segments of the four digits are wired together, all the 'b' segments are wired together, and so on. This reduces the segment control lines to just 8. To control which digit is displaying the data, each digit has its own independent common control pin (anode or cathode).
This is the core challenge of your FPGA 7-segment display controller setup. You cannot display four different numbers simultaneously. Instead, you must rapidly cycle through them one at a time.
-
Turn on Digit 1, output the data for Digit 1 on the segment lines, wait a fraction of a millisecond.
-
Turn off Digit 1, turn on Digit 2, output the data for Digit 2, wait.
-
Turn off Digit 2, turn on Digit 3, output the data for Digit 3, wait.
-
Turn off Digit 3, turn on Digit 4, output the data for Digit 4, wait.
-
Repeat this process endlessly.
Due to the persistence of vision in the human eye, if this cycle happens fast enough (typically at a refresh rate of 60 Hz or higher), the flashing becomes invisible, and it appears as though all four digits are solidly illuminated with different numbers simultaneously.
Designing the Verilog Module: Step-by-Step
To design this driver in Verilog, we need to break the system down into three distinct sub-modules:
-
A Clock Divider: To slow down the FPGA's main system clock to a usable refresh rate for the displays.
-
A Binary/Hex to 7-Segment Decoder: To convert numerical values into the correct on/off patterns for the LEDs.
-
An Anode Multiplexer/Controller: To cycle through the active digits and route the correct data to the decoder.
Step 1: The Clock Divider
FPGAs typically operate at very high clock frequencies, such as 50 MHz or 100 MHz. If you try to multiplex your displays at 50 million times per second, the LEDs will not have enough time to reach their full brightness, resulting in a very dim display. We need to divide this high-speed clock down to a refresh rate of about 1 kHz (which gives each digit in a 4-digit setup a refresh rate of 250 Hz, well above the threshold for human flicker perception).
module clock_divider (
input wire clk_in, // E.g., 50 MHz system clock
input wire reset, // Asynchronous reset
output reg clk_out // 1 kHz multiplexing clock
);
// For a 50MHz clock, to get 1kHz, we need to divide by 50,000.
// Toggling the clock every 25,000 cycles gives a full period of 50,000 cycles.
reg [15:0] counter;
always @(posedge clk_in or posedge reset) begin
if (reset) begin
counter <= 16'd0;
clk_out <= 1'b0;
end else if (counter == 16'd24999) begin
counter <= 16'd0;
clk_out <= ~clk_out;
end else begin
counter <= counter + 1'b1;
end
end
endmodule
Step 2: Hexadecimal to 7-Segment Decoder
Next, we need a combinatorial block of logic that takes a 4-bit binary input (representing numbers 0-15, or 0-F in Hex) and outputs the corresponding 7-bit pattern to light up the correct segments. Recall that for a Common Anode display, a logical '0' turns the segment ON.
module hex_to_7seg (
input wire [3:0] hex_in,
output reg [6:0] seg_out // Segments ordered: g, f, e, d, c, b, a
);
always @(*) begin
case (hex_in)
// 0 means ON, 1 means OFF (Common Anode)
4'h0: seg_out = 7'b1000000; // 0
4'h1: seg_out = 7'b1111001; // 1
4'h2: seg_out = 7'b0100100; // 2
4'h3: seg_out = 7'b0110000; // 3
4'h4: seg_out = 7'b0011001; // 4
4'h5: seg_out = 7'b0010010; // 5
4'h6: seg_out = 7'b0000010; // 6
4'h7: seg_out = 7'b1111000; // 7
4'h8: seg_out = 7'b0000000; // 8
4'h9: seg_out = 7'b0010000; // 9
4'hA: seg_out = 7'b0001000; // A
4'hB: seg_out = 7'b0000011; // B
4'hC: seg_out = 7'b1000110; // C
4'hD: seg_out = 7'b0100001; // D
4'hE: seg_out = 7'b0000110; // E
4'hF: seg_out = 7'b0001110; // F
default: seg_out = 7'b1111111; // All OFF
case
end
endmodule
Step 3: Top-Level Module and Multiplexing Logic
Now we tie it all together. The top-level module will take the main system clock, a reset signal, and a 16-bit input value (four digits of 4 bits each) that we want to display. It will output the anode control signals and the cathode segment signals.
module display_controller_top (
input wire clk_50MHz,
input wire reset,
input wire [15:0] display_data, // 4 digits, 4 bits each
output wire [3:0] anode, // Active-low anode control
output wire [6:0] segments // Active-low segment control
);
wire refresh_clock;
reg [1:0] active_digit_counter;
reg [3:0] current_hex_val;
reg [3:0] anode_reg;
// Instantiate the clock divider
clock_divider clk_div_inst (
.clk_in(clk_50MHz),
.reset(reset),
.clk_out(refresh_clock)
);
// Instantiate the 7-segment decoder
hex_to_7seg decoder_inst (
.hex_in(current_hex_val),
.seg_out(segments)
);
// Multiplexing logic based on the slow refresh clock
always @(posedge refresh_clock or posedge reset) begin
if (reset) begin
active_digit_counter <= 2'b00;
end else begin
active_digit_counter <= active_digit_counter + 1'b1;
end
end
// Route the correct data and enable the correct anode
always @(*) begin
case (active_digit_counter)
2'b00: begin
anode_reg = 4'b1110; // Turn on Digit 0 (rightmost)
current_hex_val = display_data[3:0];
end
2'b01: begin
anode_reg = 4'b1101; // Turn on Digit 1
current_hex_val = display_data[7:4];
end
2'b10: begin
anode_reg = 4'b1011; // Turn on Digit 2
current_hex_val = display_data[11:8];
end
2'b11: begin
anode_reg = 4'b0111; // Turn on Digit 3 (leftmost)
current_hex_val = display_data[15:12];
end
default: begin
anode_reg = 4'b1111; // Turn off all
current_hex_val = 4'b0000;
end
endcase
end
// Assign the registered anode value to the output wire
assign anode = anode_reg;
endmodule
Advanced Considerations: BCD vs. Hexadecimal
In the example provided above, the design treats the 16-bit input as a raw hexadecimal value. This is perfectly fine if you want to display hex codes (like a memory address). However, if you are building a digital clock or a temperature sensor, humans read base-10 decimal numbers, not hexadecimal.
For example, if your sensor outputs a binary value of 0001_0000 (which is 16 in decimal), displaying this raw hex value will result in "10" on the screen (since 10 in hex is 16 in decimal). To display the actual number "16", you must implement a Binary-Coded Decimal (BCD) converter before passing the data to the multiplexer.
A BCD converter takes a binary number and splits it into discrete decimal digits. The most common algorithm for achieving this in digital hardware is the "Double Dabble" or "Shift and Add 3" algorithm. While writing a BCD module is beyond the strict scope of the display multiplexer itself, it is a necessary precursor if your goal is to present human-readable base-10 numerical information. You would insert this BCD module into your top-level design, routing your raw binary data into the BCD converter, and then routing the separated digits from the BCD converter into your display controller.
Timing Constraints and Pin Planning
Writing the Verilog code is only the first half of the process in FPGA design. Once your logic is sound, you must inform the synthesis tools how your digital design physically connects to the outside world. This is done using a constraints file (such as an .XDC file for Xilinx Vivado or a .QSF file for Intel/Altera Quartus).
In the constraints file, you map your top-level Verilog ports to specific physical pins on the FPGA package. You also specify the I/O standard (such as LVCMOS33 for 3.3V logic).
Example Xilinx constraints mapping:
set_property PACKAGE_PIN W5 [get_ports clk_50MHz]
set_property IOSTANDARD LVCMOS33 [get_ports clk_50MHz]
# Segments
set_property PACKAGE_PIN W7 [get_ports {segments[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {segments[0]}]
# ... (repeat for other segments)
# Anodes
set_property PACKAGE_PIN U2 [get_ports {anode[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {anode[0]}]
# ... (repeat for other anodes)
Failure to define these constraints accurately will result in the FPGA compiling successfully but failing to drive any physical hardware upon programming.
Simulation and Troubleshooting
Before pushing your synthesized bitstream to the physical hardware, it is heavily recommended to simulate your design using a testbench. A Verilog testbench is a separate module that generates simulated clock pulses and input signals, allowing you to observe the internal registers and outputs in a waveform viewer.
If your physical display does not behave as expected after programming, consider these common troubleshooting steps:
-
Dim or Flickering Display: Your clock divider might be configured incorrectly. If the refresh rate is too slow (under 50 Hz), you will see distinct flashing. If it is way too fast (e.g., millions of Hertz), the LEDs will appear incredibly dim because the transistors do not have time to fully switch states.
-
Displaying the Figure '8' Everywhere: This is almost always an issue with the common anode/cathode logic being inverted. If a segment requires a '1' to turn on but you are sending it a '0', all segments might inadvertently light up.
-
Scrambled Characters: Ensure your segment array assignment matches the physical wiring. If your code thinks
segments[0]is segment 'a', but the PCB routes it to segment 'g', every character will look like nonsense. Check your development board's manual thoroughly.
Conclusion
Designing a multiplexed driver from scratch is a highly rewarding entry point into digital logic design. It requires a firm grasp of hardware behavior, timing management, and modular coding practices. By dividing the system into manageable components—a clock divider, a character decoder, and a multiplexer—you can create robust and scalable designs. Mastering this foundational task equips you with the fundamental skills needed to tackle far more complex peripheral interfacing and state machine designs in your future engineering endeavors.
