Tutorial 2: Seven segment display
31 Mar 2021 - Jeremy SeeTutorial 2: Seven Segment Display
In this tutorial, we will control a seven segment display using the FPGA. This will introduce concepts such as module
instantiation where code can be written and reused, a similar paradigm to Object Oriented Programming.
A seven segment display is basically a package of seven/eight LEDs that allow you to form numbers 0-f by lighting them up in specific formats. In this tutorial, we use a common anode version, where the anodes are connected, and pulled HIGH
We start by looking at which combinations of LEDs to light up, to show a specific digit on the display. The truth table for the LED inputs for a given output digit is shown below.
Let’s create a module to light up the LEDs in the correct combination. The module takes a 4-bit input (0 to F in hex) and lights up the corresponding digit. We start by including the truth table as a set of parameters that we can call during operation.
module Seven_Segment
(
input wire CLK_IN,
input wire [3:0] NUMBER_IN,
output reg [6:0] OUTPUT
);
parameter zero = 7'b1111110; //Value for zero
parameter one = 7'b0110000; //Value for one
parameter two = 7'b1101101; //Value for two
parameter three = 7'b1111001; //Value for three
parameter four = 7'b0110011; //Value for four
parameter five = 7'b1011011; //Value for five
parameter six = 7'b1011111; //Value for six
parameter seven = 7'b1110000; //Value for seven
parameter eight = 7'b1111111; //Value for eight
parameter nine = 7'b1110011; //Value for nine
parameter A = 7'b1110111; //Value for A
parameter B = 7'b0011111; //Value for B
parameter C = 7'b1001110; //Value for C
parameter D = 7'b0111101; //Value for D
parameter E = 7'b1001111; //Value for E
parameter F = 7'b1000111; //Value for F
endmodule
Then, we’ll want to define the behaviour of the module at each clock pulse with an always
block. We synchronise this module to a clock’s rising edge posedge
, so that we can update the value on the display whenever we get a new input. You can also use the falling edge with negedge
. Most modules in FPGAs will be synchronised to a clock, allowing you to pipeline data from one module to another sequentially. This is a very important concept in FPGA design, as you will see in more advanced tutorials.
module Seven_Segment
(
input wire CLK_IN,
input wire [3:0] NUMBER_IN,
output reg [6:0] OUTPUT
);
parameter zero = 7'b1111110; //Value for zero
parameter one = 7'b0110000; //Value for one
parameter two = 7'b1101101; //Value for two
parameter three = 7'b1111001; //Value for three
parameter four = 7'b0110011; //Value for four
parameter five = 7'b1011011; //Value for five
parameter six = 7'b1011111; //Value for six
parameter seven = 7'b1110000; //Value for seven
parameter eight = 7'b1111111; //Value for eight
parameter nine = 7'b1110011; //Value for nine
parameter A = 7'b1110111; //Value for A
parameter B = 7'b0011111; //Value for B
parameter C = 7'b1001110; //Value for C
parameter D = 7'b0111101; //Value for D
parameter E = 7'b1001111; //Value for E
parameter F = 7'b1000111; //Value for F
always @(posedge CLK_IN) begin
// Do something
end
endmodule
Inside the always
block, we define the behaviour of the outputs. We invert the output with a ~
operator, as we are using a common anode display. We need to drive the selected LED a/b/c/d/e/f/g LOW
to turn it on. Save this file as Seven_Segment.v
.
module Seven_Segment (
input wire CLK_IN,
input wire [3:0]NUMBER_IN,
output reg [6:0] OUTPUT
);
parameter zero = 7'b1111110; //Value for zero
parameter one = 7'b0110000; //Value for one
parameter two = 7'b1101101; //Value for two
parameter three = 7'b1111001; //Value for three
parameter four = 7'b0110011; //Value for four
parameter five = 7'b1011011; //Value for five
parameter six = 7'b1011111; //Value for six
parameter seven = 7'b1110000; //Value for seven
parameter eight = 7'b1111111; //Value for eight
parameter nine = 7'b1110011; //Value for nine
parameter A = 7'b1110111; //Value for A
parameter B = 7'b0011111; //Value for B
parameter C = 7'b1001110; //Value for C
parameter D = 7'b0111101; //Value for D
parameter E = 7'b1001111; //Value for E
parameter F = 7'b1000111; //Value for F
always @(posedge CLK_IN) begin
case(NUMBER_IN)
4'b0000: OUTPUT <= ~zero;
4'b0001: OUTPUT <= ~one;
4'b0010: OUTPUT <= ~two;
4'b0011: OUTPUT <= ~three;
4'b0100: OUTPUT <= ~four;
4'b0101: OUTPUT <= ~five;
4'b0110: OUTPUT <= ~six;
4'b0111: OUTPUT <= ~seven;
4'b1000: OUTPUT <= ~eight;
4'b1001: OUTPUT <= ~nine;
4'b1010: OUTPUT <= ~A;
4'b1011: OUTPUT <= ~B;
4'b1100: OUTPUT <= ~C;
4'b1101: OUTPUT <= ~D;
4'b1110: OUTPUT <= ~E;
4'b1111: OUTPUT <= ~F;
default: OUTPUT <= ~zero;
endcase
end
endmodule
Now, we’ve created a module that takes in a 4-bit input and displays the corresponding digit on the seven segment display. Let’s do something more advanced. Now, we have a 4-digit seven segment display, as shown below. Let’s show a 16-bit number on it!
Now we have some additional pins, D1-D4. These are used to select the corresponding digit in the display, by driving it HIGH
and the segment side LOW
to create a voltage difference across the LED segment, lighting it up.
How do you light up so many digits if they share a common pin? The answer is simple: LED multiplexing! What you need to do is continuously switch on and off the correct digit so fast that it appears as one continuous image to the naked eye. For that, you’ll need a refresh rate of at least 60Hz. We can comfortably achieve that and a lot more with our mighty FPGA.
In our module, we’ll define a 16-bit input, representing the number we want to display. Our outputs will be all the pins of this 4-digit seven segment display. First, we start by including our Seven_Segment.v
module with include
.
`include "Seven_Segment.v"
module Seven_Segment_Display (
input wire clk,
input wire RST_N,
input wire [15:0] Displayed_number,
output reg [3:0] Cathode,
output wire [6:0] Segment_out
);
endmodule
We instantiate our Seven_Segment.v
module as shown below, adding a signal Digitnumber
to send the 4-bit digit to the module.
`include "Seven_Segment.v"
module Seven_Segment_Display (
input wire clk,
input wire RST_N,
input wire [15:0] Displayed_number,
output reg [3:0] Cathode,
output wire [6:0] Segment_out
);
reg [3:0] Digit_number;
// Creating Seven_Segment instance
Seven_Segment i2
(
.CLK_IN(clk),
.NUMBER_IN(Digit_number),
.OUTPUT(Segment_out[6:0])
);
endmodule
Then, we add in our logic to alternate between the 4 digits of the seven segment display, to rapidly display all digits on them. We use a 2-bit counter LEDactivatingcounter
to choose which one to light up, and DigitNumber
to represent the 4-bit digit displayed on the current display.
`include "Seven_Segment.v"
module Seven_Segment_Display (
input wire clk,
input wire RST_N,
input wire [15:0] Displayed_number,
output reg [3:0] Cathode,
output wire [6:0] Segment_out
);
wire [1:0] LED_activating_counter;
reg [3:0] Digit_number;
reg [15:0] refresh_counter;
// Creating Seven_Segment instance
Seven_Segment i2
(
.CLK_IN(clk),
.NUMBER_IN(Digit_number),
.OUTPUT(Segment_out[6:0])
);
// Switch between 4 digits of display
always @(posedge clk or negedge RST_N)
begin
if (RST_N==0)
refresh_counter <= 0;
else
refresh_counter <= refresh_counter + 1;
end
// every 24M / (2^14) hz switch to next digit in 7-seg display
assign LED_activating_counter = refresh_counter[15:14];
// select digit to light up
always @(posedge clk) begin
case(LED_activating_counter)
2'b00: begin
// pull to ground for first digit
Cathode = 4'b1000;
Digit_number <= Displayed_number[15:11];
end
2'b01: begin
// pull to ground for second digit
Cathode = 4'b0100;
Digit_number <= Displayed_number[10:8];
end
2'b10: begin
// pull to ground for third digit
Cathode = 4'b0010;
Digit_number <= Displayed_number[7:4];
end
2'b11: begin
// pull to ground for fourth digit
Cathode = 4'b0001;
Digit_number <= Displayed_number[3:0];
end
default: begin
// pull to ground for default first digit
Cathode <= 4'b1111;
Digit_number <= 4'b1111;
end
endcase
end
endmodule
Let’s take a closer look at the code above. For our always
block sensitivity list, we added the reset signal negedge rst
to incorporate our reset button, which is active low.
We see a new construct here, the case
block. Similar to C, the case statement checks the input value and behaves accordingly. In this case, we check for values 0-3 to light up digits 1-4 respectively. This block is nested within an always
block to synchronise it with the master clock. This module is enough to display a 16-bit number on the 4-digit seven segment display.
Now, let’s add a Fibonacci counter to automatically increment the number displayed. This module increments the output SEQUENCE
at every clock cycle by adding the previous two values together. Note that when this value overflows, it resets back to 0. Save this file as Fibonacci_Series.v
.
module Fibonacci_Series (
input wire CLK_IN,
input wire RST_N,
output wire [15:0]SEQUENCE
);
reg [15:0] SEQUENCE_I1,SEQUENCE_I2;
assign SEQUENCE = RST_N ? (SEQUENCE_I1 + SEQUENCE_I2) : 16'b1;
always @(posedge CLK_IN) begin
if(SEQUENCE < 16'hDAAA) begin
SEQUENCE_I2 = SEQUENCE_I1;
SEQUENCE_I1 = SEQUENCE;
end
else begin
SEQUENCE_I2 = 16'b1;
SEQUENCE_I1 = 16'b0;
end
end
endmodule
The Lichee Tang has an onboard 24MHz clock that we take in on pin K14
. We divide that clock to get a slower clock to trigger the Fibonacci_Series
module, incrementing it slowly.
`include "Seven_Segment_Display.v"
`include "Fibonacci_Series.v"
module Seven_Segment_Display_Top (
input wire clk,
input wire RST_N,
output wire [3:0] Cathode,
output wire [6:0] Segment_out
);
// Signal to send number to Seven_Segment_Display module
wire [15:0] Displayed_number;
// Frequency of master clock
parameter time1 = 25'd24_000_000; // 24 MHz counter
// Slow clock divider
reg [24:0] count = 24'b0;
reg clk_slow = 1'b0;
// Slow clock to increment number displayed
always @(posedge clk) begin
// Code for reset
if(RST_N==0) begin
count <= 25'd0;
clk_slow <= 1'b0;
end
if(count == time1) begin
count <= 25'd0;
clk_slow <= ~clk_slow;
end
else begin
count <= count + 1'b1;
end
end
// Creating Fibonacci_Series instance
Fibonacci_Series i1
(
.CLK_IN(clk_slow),
.RST_N(RST_N),
.SEQUENCE(Displayed_number[15:0])
);
// Creating 4-digit seven segment display instance
Seven_Segment_Display Seven_Segment_Display_inst
(
.clk(clk),
.RST_N(RST_N),
.Displayed_number(Displayed_number),
.Cathode(Cathode),
.Segment_out(Segment_out)
);
endmodule
Save our top module. Now, let’s create a testbench to simulate our top module, ensuring that the output signals are as expected.
`timescale 1ns/1ns
`include "Seven_Segment_Display_Top.v"
module Seven_Segment_Display_Top_tb ();
// Test signals
reg clk = 1'b0;
reg RST_N = 1'b1;
wire [3:0] Cathode;
wire [6:0] Segment_out;
// Instantiate the top module
Seven_Segment_Display_Top uut
(
.clk(clk),
.RST_N(RST_N),
.Cathode(Cathode),
.Segment_out(Segment_out)
);
initial begin
// Define testbench behaviour
$dumpfile("Seven_Segment_Display_Top_tb.vcd");
$dumpvars(0, Seven_Segment_Display_Top_tb);
// Test conditions
for (integer i=0; i<10; i=i+1) begin
// Pulse clock, 20 units per cycle
clk = ~clk; #10;
end
$display("Test completed!");
end
endmodule
Before running the simulation, let’s make some small changes to allow the simulation to take effect in a small number of timesteps. The simulation only does the 10~1000s of steps, whereas your hardware implementation will do 24,000,000 in a single second at 24MHz. I’ve added a parameter to the children modules (accessible from the top module) to define a faster “slow clock”, so we can see changes in fewer timesteps.
SevenSegmentDisplay.v
`include "Seven_Segment.v"
module Seven_Segment_Display (
input wire clk,
input wire RST_N,
input wire [15:0] Displayed_number,
output reg [3:0] Cathode,
output wire [6:0] Segment_out
);
// For modification during simulation later
parameter startRefreshCounter = 14;
parameter endRefreshCounter = 15;
wire [1:0] LED_activating_counter;
reg [3:0] Digit_number;
reg [15:0] refresh_counter = 16'b0;
// Creating Seven_Segment instance
Seven_Segment i2
(
.CLK_IN(clk),
.NUMBER_IN(Digit_number),
.OUTPUT(Segment_out[6:0])
);
// Switch between 4 digits of display
always @(posedge clk or negedge RST_N)
begin
if (RST_N==0)
refresh_counter <= 0;
else
refresh_counter <= refresh_counter + 1;
end
// every 24M / (2^14) hz switch to next digit in 7-seg display
assign LED_activating_counter = refresh_counter[endRefreshCounter:startRefreshCounter];
// select digit to light up
always @(posedge clk) begin
case(LED_activating_counter)
2'b00: begin
// pull to ground for first digit
Cathode = 4'b1000;
Digit_number <= Displayed_number[15:11];
end
2'b01: begin
// pull to ground for second digit
Cathode = 4'b0100;
Digit_number <= Displayed_number[10:8];
end
2'b10: begin
// pull to ground for third digit
Cathode = 4'b0010;
Digit_number <= Displayed_number[7:4];
end
2'b11: begin
// pull to ground for fourth digit
Cathode = 4'b0001;
Digit_number <= Displayed_number[3:0];
end
default: begin
// pull to ground for default first digit
Cathode <= 4'b1111;
Digit_number <= 4'b1111;
end
endcase
end
endmodule
SevenSegmentDisplayTop.v
`include "Seven_Segment_Display.v"
`include "Fibonacci_Series.v"
module Seven_Segment_Display_Top (
input wire clk,
input wire RST_N,
output wire [3:0] Cathode,
output wire [6:0] Segment_out
);
// Signal to send number to Seven_Segment_Display module
wire [15:0] Displayed_number;
// For modification during simulation later
parameter startRefreshCounter = 14;
parameter endRefreshCounter = 15;
// Frequency of master clock
parameter time1 = 25'd24_000_000; // 24 MHz counter
// Slow clock divider
reg [24:0] count = 24'b0;
reg clk_slow = 1'b0;
// Slow clock to increment number displayed
always @(posedge clk) begin
// Code for reset
if(RST_N==0) begin
count <= 25'd0;
clk_slow <= 1'b0;
end
if(count == time1) begin
count <= 25'd0;
clk_slow <= ~clk_slow;
end
else begin
count <= count + 1'b1;
end
end
// Creating Fibonacci_Series instance
Fibonacci_Series i1
(
.CLK_IN(clk_slow),
.RST_N(RST_N),
.SEQUENCE(Displayed_number)
);
// Creating 4-digit seven segment display instance
Seven_Segment_Display #(
.startRefreshCounter(startRefreshCounter),
.endRefreshCounter(endRefreshCounter)
) Seven_Segment_Display_inst
(
.clk(clk),
.RST_N(RST_N),
.Displayed_number(Displayed_number),
.Cathode(Cathode),
.Segment_out(Segment_out)
);
endmodule
testbench
`timescale 1ns/1ns
`include "Seven_Segment_Display_Top.v"
module Seven_Segment_Display_Top_tb ();
// Test signals
reg clk = 1'b0;
reg RST_N = 1'b1;
wire [3:0] Cathode;
wire [6:0] Segment_out;
// Instantiate the top module
Seven_Segment_Display_Top #(
// Change parameters for simulation purposes, to speed up changes
.time1(2),
.startRefreshCounter(0),
.endRefreshCounter(1)
) uut
(
.clk(clk),
.RST_N(RST_N),
.Cathode(Cathode),
.Segment_out(Segment_out)
);
initial begin
// Define testbench behaviour
$dumpfile("Seven_Segment_Display_Top_tb.vcd");
$dumpvars(0, Seven_Segment_Display_Top_tb);
// Test conditions
for (integer i=0; i<100; i=i+1) begin
// Pulse clock, 20 units per cycle
clk = ~clk; #10;
end
$display("Test completed!");
end
endmodule
Run the file with the following commands.
iverilog -o Seven_Segment_Display_Top_tb.vvp Seven_Segment_Display_Top_tb.v
vvp Seven_Segment_Display_Top_tb.vvp
gtkwave Seven_Segment_Display_Top_tb.vcd
We get the following output waveform when viewed in gtkwave
.
Now, we’ve finally finished this seven segment display project that displays Fibonacci numbers up to the 16-bit limit of FFFF
in hexadecimal. Let’s try deploying it to hardware with the following steps:
- Change back the parameters that we modified for simulation (.time1, .startRefreshCounter, .endRefreshCounter) to (24000000,14,15)
- Add in the Constraints file (io.adc)
- Synthesise the bitstream in Tang Dynasty
- Upload it to your board with the appropriate connections
The constraints file io.adc
is available here, change it according to your wiring. If using a common cathode display instead of a common anode display, simply invert your logic in the Verilog source files.
# set_pin_assignment { RST_N } { LOCATION = K16; }
set_pin_assignment { Segment_out[0] } { LOCATION = A4; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Segment_out[1] } { LOCATION = A3; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Segment_out[2] } { LOCATION = C5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Segment_out[3] } { LOCATION = B6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Segment_out[4] } { LOCATION = C9; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Segment_out[5] } { LOCATION = B10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Segment_out[6] } { LOCATION = B14; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Cathode[0] } { LOCATION = P2; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Cathode[1] } { LOCATION = R2; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Cathode[2] } { LOCATION = N5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { Cathode[3] } { LOCATION = P5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 20; }
set_pin_assignment { clk } { LOCATION = K14; }
## Seven Segment, 7 pins, common anode configuration
Congratulations! You’ve successfully created your first visual interface using an FPGA! Click here for the next tutorial on the UART interface.