basys 3 showing a single digit

Seven-Segment Display – An Initial Exploration

For this project I am going back to exploring new peripherals. In this case the seven-segment display on the Basys 3 board. To keep things simple I am only going to work on displaying digits, specifically hexadecimal digits, on just one of the four digits of the seven-segment display. In a future project I’ll tackle displaying all four digits. Perhaps I will use this to display the output from the LED counter project.

Creating the project

As usual I’ll start by creating a new project in Vivado. Then I copy in the Basys 3 master constraint file. Then in the constraints file I find and uncomment the following signal groups: clk, sw[3:0], seg[6:0], and an[0] which i rename to an. I am going to use the first 4 slide switches as a way for us to input a 4-bit number which will be displayed as one of the seven segment digits. I won’t wire up a button for reset this time.

The complete constraints file is below.

## This file is a general .xdc for the Basys3 rev B board
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project

## Clock signal
set_property PACKAGE_PIN W5 [get_ports clk]
	set_property IOSTANDARD LVCMOS33 [get_ports clk]
	create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk]

## Switches
set_property PACKAGE_PIN V17 [get_ports {sw[0]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {sw[0]}]
set_property PACKAGE_PIN V16 [get_ports {sw[1]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {sw[1]}]
set_property PACKAGE_PIN W16 [get_ports {sw[2]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {sw[2]}]
set_property PACKAGE_PIN W17 [get_ports {sw[3]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {sw[3]}]

##7 segment display
set_property PACKAGE_PIN W7 [get_ports {seg[0]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[0]}]
set_property PACKAGE_PIN W6 [get_ports {seg[1]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[1]}]
set_property PACKAGE_PIN U8 [get_ports {seg[2]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[2]}]
set_property PACKAGE_PIN V8 [get_ports {seg[3]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[3]}]
set_property PACKAGE_PIN U5 [get_ports {seg[4]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[4]}]
set_property PACKAGE_PIN V5 [get_ports {seg[5]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[5]}]
set_property PACKAGE_PIN U7 [get_ports {seg[6]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[6]}]

set_property PACKAGE_PIN U2 [get_ports {an}]
	set_property IOSTANDARD LVCMOS33 [get_ports {an}]

## Configuration options, can be used for all designs
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]

About the seven-segment display

For the most part you treat the seven-segment display like a set of LEDs. And so we just need to come up with a way to map from a 4-bit number to the 7-bit LED segment outputs seg. An important thing to understand is that the signals for the individual segments on all four digits connect together. The Artix-7 output pins can’t drive all for digits at the same time. Trying to do so could damage the hardware! So we will have to come up with a plan for how to deal with this later.

An important thing to understand is that the signals for the individual segments on all four digits connect together. The Artix-7 output pins can’t drive all four digits simultaneously. Trying to do so could damage the hardware!

Note that the an signal represents the anode, or ground, for all seven LEDs on the first seven-segment digit. The an signal is active low, so it is set to 0 to enable it. For this project I will simply set an[0] to 0 at the start for the single digit that I will use. Set the rest of the an signals to 1. In future projects we will use the an signals to enable/disable individual digits.

The seg signals are also active low. So we set a seg signal to 1 to turn it off and 0 to turn it on. The seg bits map to the individual segments starting from the center top and moving clockwise around the digit. The middle center segment is the last. So the digit would be represented as 7’b1000000 which calls for all segments on except for the middle center one which is seg[6].

Designing the seven-segment display driver code

For now let’s assume that we create a module that can take a 4-bit input connected to the slide switches, and generate a 7-bit output connected to the seven-segment display. Let’s assume that I will call that module sseg_display. Then we can go ahead and create the top-level module sseg_top and flesh it out. The code for the top-level module looks like the following.

module sseg_top(
    input clk,
    input [3:0] sw,
    output [6:0] seg,
    output [3:0] an
    );
    
    // an is active low, turn on an[0], others off
    assign an = 4'b1110;
    
    // connect our input to the display
    sseg_display display(.hex(sw), .seg(seg));
    
endmodule

That seems reasonable enough and very similar to what we’ve seen before. So what would sseg_display look like? Well it takes a 4-bit input (hex) value representing a hexadecimal digit. And it has a 7-bit output (seg) representing which segments to enable. The output digit should keep its value until changed so it is a reg type.

And any time an input changes we want the display to update. That suggests I should use an always block sensitive to all of the inputs. In Verilog the symbol @* tells an always block to react to any of the inputs.

Introducing the Case Statement

In order to map from the 4-bit input to the 7-bit output I could use a chain of if-else-if statements. But there is a better way! Let’s learn about the case statement. In Verilog the case statement works similarly to the switch-case statement in C/C++ and many other programming languages. The case statement starts with the case keyword and ends with the endcase keyword. The general form looks like:

case (expression)
    item:
        begin
            statement;
            statement;
        end

    item:
        begin
            statement;
            statement;
        end

    default:
        begin
            statement;
            statement;
        end

endcase

The case keyword takes an expression. The expression matches, or selects, one of the items. The default keyword catches any cases that did not match a specific item. In Verilog it is frequently important that case statements handle every possible expression value. This is a full case statement. As a best practice use the default keyword to avoid missed input cases. Using the case statement the display circuit looks like this:

// seven-segment digit display driver
module sseg_display (
    input [3:0] hex,
    output reg [6:0] seg
    );
    
    // activate on any input change
    always @*
    begin
        case(hex)
            4'h0: seg[6:0] = 7'b1000000;    // digit 0
            4'h1: seg[6:0] = 7'b1111001;    // digit 1
            4'h2: seg[6:0] = 7'b0100100;    // digit 2
            4'h3: seg[6:0] = 7'b0110000;    // digit 3
            4'h4: seg[6:0] = 7'b0011001;    // digit 4
            4'h5: seg[6:0] = 7'b0010010;    // digit 5
            4'h6: seg[6:0] = 7'b0000010;    // digit 6
            4'h7: seg[6:0] = 7'b1111000;    // digit 7
            4'h8: seg[6:0] = 7'b0000000;    // digit 8
            4'h9: seg[6:0] = 7'b0010000;    // digit 9
            4'ha: seg[6:0] = 7'b0001000;    // digit A
            4'hb: seg[6:0] = 7'b0000011;    // digit B
            4'hc: seg[6:0] = 7'b1000110;    // digit C
            4'hd: seg[6:0] = 7'b0100001;    // digit D
            4'he: seg[6:0] = 7'b0000110;    // digit E
            default: seg[6:0] = 7'b0001110; // digit F
        endcase
    end
endmodule

In this code I match the hex input 4’h0 (0 or x00) to an assignment to the seg output of 7’b1000000 which displays a zero character. I match the remaining input patterns to an output pattern for the appropriate character. Note the use of sized numbers to explicitly map a 4-bit hexadecimal value to a 7-bit binary value. Also notice also that I use default to handle, what I believe is, the final input match. If I was incorrect, the default clause will catch the missed cases.

Does it work?

To see if this design works I generate a bitstream in Vivado and program the Basys 3 with the result. The image below shows the result. It is not easy to make out on the image but the first slide switch on the right is up.

So our input value from the first four switches is 4’b0001 or the value 1. And that is precisely the displayed digit. If you try this yourself you can do as I did and test each of the bit patterns to prove that the design works.

basys 3 seven-segment display digit
Displaying a digit

That’s it for today! The source code for this project is available on github. If you have feedback, a question or a suggestion, please leave a comment!

Discover more from FPGA Coding

Subscribe now to keep reading and get access to the full archive.

Continue reading