One of things I am planning to do in 2025 is not only speak at conferences like I often do but also have a stand. Of course like all stands at conferences one should have some good SWAG to give away, which of course I do. I will have everything from simple development boards (Leonidas) to water bottles, note books and pens.
Another good attraction for conference stands are fun games and demonstrations. I recently saw a fun example where people were trying to stop the a incrementing clock as close as possible to a defined number e.g. 5 seconds or 10 seconds etc. While this seems pretty simple to do it can be harder to do in reality.
This got me thinking it would be simple to create a FPGA based example of this and a leader board could be created on a stand for example.
As there was nothing really interesting on the TV or Streaming on Saturday night I thought it would be a good idea to create a simple example.
One of the advantages of such a FPGA based approach is that we can do this on pretty much any FPGA provided we can interface to a button and a seven segment display of at least four characters.
For prototyping this idea we can use the Digilent Basys 3 board, this board provides a four element seven segment display and a number of push buttons.
DesignThe design of this project is going to be very simple we will have the following requirements.
- The four 7 Segment displays will update with a minimum resolution of 10 ms of second
- The least significant 7 segment display will increment at 10 ms.
- The remaining seven segment displays will increment at 100ms, 1 second and 10 seconds as appropriate.
- Pressing a button will freeze the count.
- Pressing another button will reset and start the count immediately.
The Basys 3 board is provided with a 100 MHz clock, four push buttons and four segment LED.
The clocking architecture will be simple to reduce the size of the counters needed within the design. the 100 MHz clock will be divided down to create a 20MHz clock.
The design will be split into the following elements, and will be a pure HDL design.
- Clock Wizard 100 MHz to 20 MHz
- Control - De bouncing of the two switch inputs.
- Timer - Timer module which generates counters for 1/100s, 1/10s, seconds and tens of seconds.
- Seven Segment - This converts the counter values to one which can be displayed on the seven segment display.
The control module, is one which de bounces the push button switch, to ensure the level presented to the design does not bounce (oscillate) between 0 and 1 when pushed for a period of time effecting the design.
How I am doing this is very simple, I will use a shift register which shifts in the value of the switch from the IO port. Once all elements of the shift register are at the same value, the switch can be said to have stopped bouncing and we can pass it on to the other elements of the design.
To sample the shift register, I use a 1 KHz clock enable which ensures the faster 20Mhz clock does not sample the push button to quickly.
I use two buttons, stop which freezes the count and reset which clears and restarts the count.
The stop button needs to act as a latch once de bounced if pressed its value should be asserted until the reset button is pressed.
The reset button when pressed must be momentary when pressed it should reset the system and when released the system should start operating again.
The VHDL which implements this can be seen below.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity control is Port (
i_clk : in std_logic;
i_stop_btn : in std_logic;
i_reset_btn : in std_logic;
o_stop : out std_logic;
o_reset : out std_logic
);
end control;
architecture Behavioral of control is
constant c_enable_cnt : integer := 20000;
signal s_shift_reg_stop : STD_LOGIC_VECTOR (7 downto 0) := (others => '0');
signal s_shift_reg_reset : STD_LOGIC_VECTOR (7 downto 0) := (others => '0');
signal s_enable : std_logic;
signal s_counter : unsigned(14 downto 0);
signal s_reset : std_logic;
begin
enb_counter : process(i_clk)
begin
if rising_edge(i_clk) then
if (c_enable_cnt-1) = s_counter then
s_enable <= '1';
s_counter <= (others =>'0');
else
s_enable <= '0';
s_counter <= s_counter + 1;
end if;
end if;
end process;
stop_debounce: process(i_clk)
begin
if rising_edge(i_clk) then
if s_reset = '1' then
o_stop <= '0';
elsif s_enable = '1' then
s_shift_reg_stop <= s_shift_reg_stop(6 downto 0) & i_stop_btn;
if (s_shift_reg_stop = "11111111") then
o_stop <= '1';
end if;
end if;
end if;
end process;
start_debounce: process(i_clk)
begin
if rising_edge(i_clk) then
if s_enable = '1' then
s_shift_reg_reset <= s_shift_reg_reset(6 downto 0) & i_reset_btn;
if (s_shift_reg_reset = "11111111") then
s_reset <= '1';
elsif (s_shift_reg_reset = "00000000") then
s_reset <= '0';
end if;
end if;
end if;
end process;
o_reset <= s_reset;
end Behavioral;
This module has the following interfaces
The timer module contains four counters, each of which counts in binary coded decimal (BCD). Each counter counts from 0 to 9 and when it wraps around from 9 to 0 it indicates to the next counter to increment.
All of these counters are clocked at the 20MHz clock rate but enabled to increment by the previous counter.
To get started we need a counter which counts from 0 to 10 ms, repeatedly. Each time this counter wraps around it increments the 10 ms counter display. When the stop button is pressed and latched high this counter will be prevented from counting until reset freezing the count.
The 10 ms counter, increments from 0 to 9, when wrapping around from 9 to 0 it will signal to the 100 ms counter to increment. The 100ms, second and 10 second counters will do the same when they wrap around from 0 to 9.
This will mean the counters will increment from 0 seconds all the way up to 99.99 seconds on the seven segment display.
Each of the counters will be reset when the reset button is pushed.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- Uncomment the following library declaration if using
-- arithmetic functions with Signed or Unsigned values
use IEEE.NUMERIC_STD.ALL;
-- Uncomment the following library declaration if instantiating
-- any Xilinx leaf cells in this code.
--library UNISIM;
--use UNISIM.VComponents.all;
entity timer is Port (
i_clk : in std_logic;
i_stop : in std_logic;
i_reset : in std_logic;
o_tens : out std_logic_vector(3 downto 0);
o_hundreds : out std_logic_vector(3 downto 0);
o_seconds : out std_logic_vector(3 downto 0);
o_tens_sec : out std_logic_vector(3 downto 0)
);
end timer;
architecture Behavioral of timer is
constant c_tenths : integer := 200_000;
signal s_tenths_count : integer range 0 to c_tenths-1 := 0;
signal s_inc_tenths : std_logic := '0';
signal s_inc_hundreds : std_logic := '0';
signal s_inc_seconds : std_logic := '0';
signal s_inc_tens_sec : std_logic := '0';
signal s_tenths : integer range 0 to 9 := 0;
signal s_hundreds : integer range 0 to 9 := 0;
signal s_seconds : integer range 0 to 9 := 0;
signal s_tens_sec : integer range 0 to 9 := 0;
begin
o_tens <= std_logic_vector(to_unsigned(s_tenths_count,4));
o_hundreds <= std_logic_vector(to_unsigned(s_hundreds,4));
o_seconds <= std_logic_vector(to_unsigned(s_seconds,4));
o_tens_sec <= std_logic_vector(to_unsigned(s_tens_sec,4));
cnt : process(i_clk)
begin
if rising_edge(i_clk) then
if i_reset = '1' then
s_inc_tenths <= '0';
s_tenths_count <= 0;
else
if i_stop = '1' then
s_inc_tenths <= '0';
else
if s_tenths_count = (c_tenths-1)then
s_inc_tenths <= '1';
s_tenths_count <= 0;
else
s_inc_tenths <= '0';
s_tenths_count <= s_tenths_count + 1;
end if;
end if;
end if;
end if;
end process;
tens_cnt : process(i_clk)
begin
if rising_edge(i_clk) then
if i_reset = '1' then
s_inc_hundreds <= '0';
s_tenths <= 0;
else
s_inc_hundreds <= '0';
if s_inc_tenths = '1' then
if s_tenths = 9 then
s_tenths <= 0;
s_inc_hundreds <= '1';
else
s_tenths <= s_tenths + 1;
end if;
end if;
end if;
end if;
end process;
hunds_cnt : process(i_clk)
begin
if rising_edge(i_clk) then
if i_reset = '1' then
s_inc_seconds <= '0';
s_hundreds <= 0;
else
s_inc_seconds <= '0';
if s_inc_hundreds = '1' then
if s_hundreds = 9 then
s_hundreds <= 0;
s_inc_seconds <= '1';
else
s_hundreds <= s_hundreds + 1;
end if;
end if;
end if;
end if;
end process;
sec_cnt : process(i_clk)
begin
if rising_edge(i_clk) then
if i_reset = '1' then
s_inc_tens_sec <= '0';
s_seconds <= 0;
else
s_inc_tens_sec <= '0';
if s_inc_seconds = '1' then
if s_seconds = 9 then
s_seconds <= 0;
s_inc_tens_sec <= '1';
else
s_seconds <= s_seconds + 1;
end if;
end if;
end if;
end if;
end process;
ten_sec_cnt : process(i_clk)
begin
if rising_edge(i_clk) then
if i_reset = '1' then
s_tens_sec <= 0;
else
if s_inc_tens_sec = '1' then
if s_tens_sec = 9 then
s_tens_sec <= 0;
else
s_tens_sec <= s_tens_sec + 1;
end if;
end if;
end if;
end if;
end process;
end Behavioral;
The ports of the block can be seen below.
This module, received a 16 bit vector where each four bits of the number map to one of the seven segment displays.
The least significant four bits map to the first right most seven segment element while the upper four bits map to the left most seven segment.
As the seven segment displays have common cathodes we need to set the pattern up for display and enable the appropriate anode. This will light the seven segment element with the number desired for display.
We can update each of the four seven segment elements by enabling the anodes of all four displays in order, periodically.
In this example the module scans through the anodes at a frequency of 200Hz.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
--! This is a description
--! of the entity.
entity seven_segment is port(
clk: in std_logic; --20 MHz clock
number : in std_logic_vector(15 downto 0);
seven_segement : out std_logic_vector(6 downto 0); --drives the cathode
seven_anode : out std_logic_vector(3 downto 0) --drives the anode
);
end entity;
architecture test of seven_segment is
constant refresh_count : integer := 100000;
signal refresh : unsigned(17 downto 0):=(others=>'0');
signal update : std_logic;
signal anode : std_logic_vector(3 downto 0) :="1110";
signal seven_seg : std_logic_vector(3 downto 0);
signal seven_seg_int :std_logic_vector(6 downto 0);
begin
seven_anode(3 downto 0) <= anode;
seven_segement <= not(seven_seg_int);
--seven_anode(4) <= '1';
process(clk)
begin
if rising_edge(clk) then
if update = '1' then
anode <= anode(anode'high-1 downto anode'low) & anode(anode'high);
end if;
end if;
end process;
process(clk)
begin
if rising_edge(clk) then
if update = '1' then
case anode is
when "0111" =>
seven_seg <= number(3 downto 0);
when "1110" =>
seven_seg <= number(7 downto 4);
when "1101" =>
seven_seg <= number(11 downto 8);
when "1011" =>
seven_seg <= number(15 downto 12);
when others => null;
end case;
end if;
end if;
end process;
process(clk)
begin
if rising_edge(clk) then
if (refresh = refresh_count) then
refresh <= (others =>'0');
update <= '1';
else
refresh <= refresh + 1;
update <= '0';
end if;
end if;
end process;
process(seven_seg)
begin
case seven_seg is
when "0000" => seven_seg_int <= "0111111"; -- "0"
when "0001" => seven_seg_int <= "0000110"; -- "1"
when "0010" => seven_seg_int <= "1011011"; -- "2"
when "0011" => seven_seg_int <= "1001111"; -- "3"
when "0100" => seven_seg_int <= "1100110"; -- "4"
when "0101" => seven_seg_int <= "1101101"; -- "5"
when "0110" => seven_seg_int <= "1111100"; -- "6"
when "0111" => seven_seg_int <= "0000111"; -- "7"
when "1000" => seven_seg_int <= "1111111"; -- "8"
when "1001" => seven_seg_int <= "1100111"; -- "9"
when "1010" => seven_seg_int <= "0000010"; -- a
when "1011" => seven_seg_int <= "1100000"; -- b
when "1100" => seven_seg_int <= "0110001"; -- C
when "1101" => seven_seg_int <= "1000010"; -- d
when "1110" => seven_seg_int <= "0110000"; -- E
when others => seven_seg_int <= "0111000"; -- F
end case;
end process;
end architecture;
The module scans through and updates the 7 segment display depending upon the anode being driven at the time to ensure the number is up to date.
With the three modules we need written in VHDL we are able to created a Vivido project.
Vivado ProjectTo implement the project we need to create a new Vivado project which targets the Basys3 board.
The next step is to add in the files just created into the project creation process. Make sure the option "Do not specify sources at this time" is unchecked
Select the three files just created
In the next stage select the Basys3 development boad.
With the board selected create the project.
The next step is the create a block diagram in IP integrator.
Once the block diagram has been created add in the three modules to the block diagram. We also need to add
- Clocking wizard configured for 100 MHz to 20 MHz op.
- Concatenation block to create the input for the seven segment display.
Configure the concatenation block as shown below to enable four, four bit inputs.
Configure the clokcing wizard as below for 20MHz output, no reset and no locked signal.
We can then create the block diagram as shown below.
The final stage is to create the constraints which for the basys 3 are
##7 segment display
set_property PACKAGE_PIN W7 [get_ports {seven_seg[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg[0]}]
set_property PACKAGE_PIN W6 [get_ports {seven_seg[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg[1]}]
set_property PACKAGE_PIN U8 [get_ports {seven_seg[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg[2]}]
set_property PACKAGE_PIN V8 [get_ports {seven_seg[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg[3]}]
set_property PACKAGE_PIN U5 [get_ports {seven_seg[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg[4]}]
set_property PACKAGE_PIN V5 [get_ports {seven_seg[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg[5]}]
set_property PACKAGE_PIN U7 [get_ports {seven_seg[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg[6]}]
#set_property PACKAGE_PIN V7 [get_ports dp]
#set_property IOSTANDARD LVCMOS33 [get_ports dp]
set_property PACKAGE_PIN U2 [get_ports {seven_seg_led_an[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg_led_an[0]}]
set_property PACKAGE_PIN U4 [get_ports {seven_seg_led_an[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg_led_an[1]}]
set_property PACKAGE_PIN V4 [get_ports {seven_seg_led_an[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg_led_an[2]}]
set_property PACKAGE_PIN W4 [get_ports {seven_seg_led_an[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {seven_seg_led_an[3]}]
set_property PACKAGE_PIN W5 [get_ports sys_clock]
set_property IOSTANDARD LVCMOS33 [get_ports sys_clock]
set_property IOSTANDARD LVCMOS33 [get_ports i_reset_btn]
set_property IOSTANDARD LVCMOS33 [get_ports i_stop_btn]
set_property PACKAGE_PIN U18 [get_ports i_stop_btn]
set_property PACKAGE_PIN T17 [get_ports i_reset_btn]
We can then build the bit stream and test it on the Basys3 board.
When it comes to implementation the resource foot print is small.
This project is a nice and simple project which looks at how we can simply create a engaging game using a FPGA, a few hundred lines of RTL and a simple button and seven segment display. It is a great project to get started with!
Comments
Please log in or sign up to comment.