From Hamsterworks Wiki!

Jump to: navigation, search

This FPGA Project was started in April 2018

Somebody from the Internet (named Richard) is building a Nixie tube clock, powered by an FPGA, and wanted a bit of help for the logic.

That got me thinking "what is a simple way to build a working clock?". I decided on the way with the least logic - so it is based around two lookup tables and avoids as much math as possible.


Thought processes behind the design

You would think that building the logic behind a digital clock is a clear-cut thing, and there is only one "true way". But it isn't simple there are quite a few different ways to build this, and which one is best depends on your requirements (e.g. lowest power, least resources, methods of adjustment, display requirements)

Possible Solutions

1. A seconds counter, and then using DIV and MOD to split out the HHMMSS digits for display. This would be the standard way to make a clock on a microcontroller.

           display_digit(0) <= MOD(counter,10);
           display_digit(1) <= MOD(counter/10,6);
           display_digit(2) <= MOD(counter/60,10);
           display_digit(3) <= MOD(counter/600,6);
           display_digit(4) <= MOD(counter/3600,10);
           display_digit(5) <= counter/36000;

Pros: Timekeeping is easy. Time adjustment is easy.

Cons: Requires a log of logic resources. Display digit update may not be synchronous. FPGAs suck at division.

2. A large table (86400 entries, 24-bits), that makes the time counter from binary to the HHMMSS digits.

   display_digits <= big_lookup_table(counter)

Pros: Very simple, very easy to adjust time.

Cons: Requires a lot of memory resources

3. A separate counter for hours (0-23), minutes (0-59) and seconds (0-59), and then decoding for display using a smaller amount of logic

           display_digit(0) <= MOD(secs,10);
           display_digit(1) <= secs/10;
           display_digit(2) <= MOD(mins,10);
           display_digit(3) <= mins/10;
           display_digit(4) <= MOD(hrs,10);
           display_digit(5) <= hrs/10;

Pros: Moderate complexity

Cons: Updating the time gets complex due to carry/borrow signals between the counters

4. A separate counter for each digit.

Pros: Moderately complexity. Display decoding is very easy.

Cons: Updating the time gets more complex due to carry/borrow signals between more counters, and the complexity of hours rolling over between 23:59:59 and 00:00:00 depends on the state of all six counters.

5. A one-hot shift register for each digit. So the seconds would be a 6-bit shift register for the tens and a 10 bit shift register for the units value.

Pros: Makes for very easy logic for rollovers (e.g. "if secs_tens(5) = '1' and secs_units(9) = '1' then")

Cons: Adjusting time is harder. You may also need to decode the one-hot time representation into values for the display.

6. Use a six-digit hex accumulator, and then add values other than just '1' to make it count in hours, mins, seconds. For example, For the rollover between 00:01:59 and 00:02:00 you could use x"000159"+x"0000A7" giving x"000200".

This gives you six different constants you can add to the advance the time accumulator.

Pros: Display decoding is easy. This would be an cheeky way to answer an course project that expects you to use counters.

Cons: Logic to select what to add and when to add it is quite involved. Adjusting time backwards requires quite a bit of logic too.

7. Use three table driven Finite State Machines to update the hour, minute and seconds, and to not have any time counters at all!

Pros: Very simple logic, with each FSM consisting of an 2-way MUX and minimal logic to select which value to use next. The display outputs can be held in the table and do not need to be a binary representation of the time - for example they could be colour values for RGB LEDs, patterns on a 7-segment display

Cons: Uses a moderate amount of memory - a 24 entry lookup table for hours and a 60 entry lookup table for minutes and seconds.

For me option 7 is the most interesting way, because what you are actually trying to keep track of (the time) is not really present in the design - you are just moving a 17-bit state vector through different values based on a few different inputs, and it so happens that it works like a digital clock!

Source files


Set the value of clk_frequency in the entity declaration to your input clock frequency.

library IEEE;

entity top_level is
    generic(clk_frequency : natural := 100000000);
    Port ( clk         : in  STD_LOGIC;
           btn_hr_inc  : in  STD_LOGIC;
           btn_hr_dec  : in  STD_LOGIC;
           btn_min_inc : in  STD_LOGIC;
           btn_min_dec : in  STD_LOGIC;
           digits      : out  STD_LOGIC_VECTOR (23 downto 0));
end top_level;

architecture Behavioral of top_level is
	component clock_counter is
       generic(clk_frequency     : natural);
		 Port ( clk                : in  STD_LOGIC;
				  once_per_sec       : out  STD_LOGIC := '0';
				  four_times_per_sec : out  STD_LOGIC := '0');
	end component;

	signal once_per_sec      : STD_LOGIC := '0';
	signal four_times_per_sec : STD_LOGIC := '0';
	component lut_based is
		 Port ( clk     : in  STD_LOGIC;
				  inc_hr  : in  STD_LOGIC;
				  dec_hr  : in  STD_LOGIC;
				  inc_min : in  STD_LOGIC;
				  dec_min : in  STD_LOGIC;
				  inc     : in  STD_LOGIC;
				  digits  : out STD_LOGIC_VECTOR (23 downto 0));
	end component;

   signal inc_hr  : STD_LOGIC;
   signal dec_hr  : STD_LOGIC;
   signal inc_min : STD_LOGIC;
   signal dec_min : STD_LOGIC;
	signal inc     : STD_LOGIC;

i_clocks: clock_counter generic map (
      clk_frequency => clk_frequency
    ) port map (
		clk                => clk,
		once_per_sec       => once_per_sec,
		four_times_per_sec => four_times_per_sec);

    -- Buttons are acted on only four times per second, and I don't bother debouncing or syncing them (I am so nasty!)
    inc_hr  <= btn_hr_inc  and four_times_per_sec;
    dec_hr  <= btn_hr_dec  and four_times_per_sec;
    inc_min <= btn_min_inc and four_times_per_sec;
    dec_min <= btn_min_dec and four_times_per_sec;
time_keeper: lut_based Port map ( 
           clk     => clk,
           inc_hr  => inc_hr,
           dec_hr  => dec_hr,
           inc_min => inc_min,
           dec_min => dec_min,
			  inc     => once_per_sec,
           digits  => digits);

end Behavioral;


This generates a one cycle pulse every second for time keeping, and a four pulses per second signal used to sample the buttons.

library IEEE;

entity clock_counter is
    generic(clk_frequency     : natural);
    Port ( clk                : in  STD_LOGIC;
           once_per_sec       : out  STD_LOGIC := '0';
           four_times_per_sec : out  STD_LOGIC := '0');
end clock_counter;

architecture Behavioral of clock_counter is
   -- These initial values ensure that the two outputs are not asserted at the same time;
	signal second_counter     : unsigned(27 downto 0) := to_unsigned(clk_frequency-1,28);
	signal four_times_counter : unsigned(27 downto 0) := to_unsigned(clk_frequency/4/2-1,28);

clk_proc: process(clk)
		if rising_edge(clk) then
			once_per_sec       <= '0';
			four_times_per_sec <= '0';
			if second_counter = 0 then
			   once_per_sec    <= '1';
				second_counter  <= to_unsigned(clk_frequency-1,28);
			   second_counter  <= second_counter-1;
		   end if;
			if four_times_counter = 0 then
				four_times_per_sec <= '1';
				four_times_counter <= to_unsigned(clk_frequency/4-1,28);
			   four_times_counter <= four_times_counter-1;
		   end if;
		end if;
	end process;
end Behavioral;


This is about my fifth different design, this one is based around lookup tables, rather than using counters.

library IEEE;

entity lut_based is
    Port ( clk     : in  STD_LOGIC;
           inc_hr  : in  STD_LOGIC;
           dec_hr  : in  STD_LOGIC;
           inc_min : in  STD_LOGIC;
           dec_min : in  STD_LOGIC;
			  inc     : in  STD_LOGIC;
           digits  : out STD_LOGIC_VECTOR (23 downto 0));
end lut_based;

architecture Behavioral of lut_based is
  type a_hr_lookup      is array (0 to 31) of std_logic_vector(23 downto 0);
  type a_min_sec_lookup is array (0 to 63) of std_logic_vector(23 downto 0);
  -- bit ranges in this array.
  -- 23:16 is the output for the display
  -- 15    is the "overflow when incrementing"
  -- 14    is not used
  -- 13:8  is the next index when incrementing
  -- 7     is the "underflow when decrementing" - not used in this design
  -- 6     is not used
  -- 5:0   is the next index when decrementing
  signal hr_lookup      : a_hr_lookup := (


  signal min_sec_lookup : a_min_sec_lookup := (

  signal hr  : unsigned(4 downto 0) := (others => '0');
  signal min : unsigned(5 downto 0) := (others => '0');
  signal sec : unsigned(5 downto 0) := (others => '0');

  signal lookup_hr  : std_logic_vector(23 downto 0) := (others => '0');
  signal lookup_min : std_logic_vector(23 downto 0) := (others => '0');
  signal lookup_sec : std_logic_vector(23 downto 0) := (others => '0');

  -- Perform the lookups
  lookup_hr  <= hr_lookup(to_integer(hr));
  lookup_min <= min_sec_lookup(to_integer(min));
  lookup_sec <= min_sec_lookup(to_integer(sec));

    if rising_edge(clk) then
       -- Register the display outputs
       digits <= lookup_hr(23 downto 16) & lookup_min(23 downto 16) & lookup_sec(23 downto 16);
       -- counting up when 'inc' asserted
       if inc = '1' then
          sec <= unsigned(lookup_sec(13 downto 8));
          if lookup_sec(15) = '1' then
             min <= unsigned(lookup_min(13 downto 8));
             if lookup_min(15) = '1' then
                hr <= unsigned(lookup_hr(12 downto 8));
             end if;
          end if;
       end if;

       -- Manual set - mins - does not change hrs!
       if inc_min = '1' and dec_min = '0' then
          min <= unsigned(lookup_min(13 downto 8));
       elsif dec_min = '1' then
          min <= unsigned(lookup_min(5 downto 0));
       end if;
       -- Manual set - hrs
       if inc_hr = '1' and dec_hr = '0' then
          hr <= unsigned(lookup_hr(12 downto 8));
       elsif dec_hr = '1' then
          hr <= unsigned(lookup_hr(4 downto 0));
       end if;
    end if;  
  end process;
end Behavioral;


The top level has been set up to default clk_frequency to 100MHz, but the test bench explicitly sets it to 100, so a reasonably fast simulation is possible.

You can see how the "per second tick" and the "four times per second tick" are not in phase, avoiding the hard case of when a button is asserted at exactly the same time as the second ticks by.


USE ieee.std_logic_1164.ALL;
ENTITY tb_top_module IS
END tb_top_module;
ARCHITECTURE behavior OF tb_top_module IS 
    COMPONENT top_level
    generic(clk_frequency : natural := 100000000);
         clk : IN  std_logic;
         btn_hr_inc : IN  std_logic;
         btn_hr_dec : IN  std_logic;
         btn_min_inc : IN  std_logic;
         btn_min_dec : IN  std_logic;
         digits : OUT  std_logic_vector(23 downto 0)

   signal clk : std_logic := '0';
   signal btn_hr_inc : std_logic := '0';
   signal btn_hr_dec : std_logic := '0';
   signal btn_min_inc : std_logic := '0';
   signal btn_min_dec : std_logic := '0';

   signal digits : std_logic_vector(23 downto 0);

   -- Clock period definitions
   constant clk_period : time := 10 ns;
	-- Instantiate the Unit Under Test (UUT)
uut: top_level GENERIC MAP (clk_frequency => 100)
	    PORT MAP (
          clk         => clk,
          btn_hr_inc  => btn_hr_inc,
          btn_hr_dec  => btn_hr_dec,
          btn_min_inc => btn_min_inc,
          btn_min_dec => btn_min_dec,
          digits      => digits

   -- Clock process definitions
   clk_process :process
		clk <= '0';
		wait for clk_period/2;
		clk <= '1';
		wait for clk_period/2;
   end process;

   -- Stimulus process
   stim_proc: process
      -- hold reset state for 100 ns.
      wait for 100 ns;	
      wait for clk_period*10;
      -- insert stimulus here 
   end process;

Personal tools