DigitalClock

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.

Contents

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

top_level.vhd

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

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

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;

begin
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;

clock_counter.vhd

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;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

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);
begin

clk_proc: process(clk)
	begin
		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);
		   else
			   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);
			else 
			   four_times_counter <= four_times_counter-1;
		   end if;
		end if;
	end process;
end Behavioral;

lut_based.vhd

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

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

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 := (
    x"000197",x"010200",x"020301",x"030402",
    x"040503",x"050604",x"060705",x"070806",
    x"080907",x"090a08",x"100b09",x"110c0a",
    x"120d0b",x"130e0c",x"140f0d",x"15100e",
    x"16110f",x"171210",x"181311",x"191412",
    x"201513",x"211614",x"221715",x"238016",
    x"248017",x"258018",x"268019",x"27801a",
    x"288017",x"298018",x"308019",x"31801a");

 

  signal min_sec_lookup : a_min_sec_lookup := (
    x"0001bb",x"010200",x"020301",x"030402",
    x"040503",x"050604",x"060705",x"070806",
    x"080907",x"090a08",x"100b09",x"110c0a",
    x"120d0b",x"130e0c",x"140f0d",x"15100e",
    x"16110f",x"171210",x"181311",x"191412",
    x"201513",x"211614",x"221715",x"231816",
    x"241917",x"251a18",x"261b19",x"271c1a",
    x"281d1b",x"291e1c",x"301f1d",x"31201e",
    x"32211f",x"332220",x"342321",x"352422",
    x"362523",x"372624",x"382725",x"392826",
    x"402927",x"412a28",x"422b29",x"432c2a",
    x"442d2b",x"452e2c",x"462f2d",x"47302e",
    x"48312f",x"493230",x"503331",x"513432",
    x"523533",x"533634",x"543735",x"553836",
    x"563937",x"573a38",x"583b39",x"59803a",
    x"60803b",x"61803c",x"62803d",x"63803e");

  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');
begin

  -- 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));

process(clk)
  begin
    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;

tb_top_level.vhd

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.

DigitalClockSim.png

LIBRARY ieee;
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);
    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 COMPONENT;
    

   --Inputs
   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';

 	--Outputs
   signal digits : std_logic_vector(23 downto 0);

   -- Clock period definitions
   constant clk_period : time := 10 ns;
 
BEGIN
 
	-- 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
   begin
		clk <= '0';
		wait for clk_period/2;
		clk <= '1';
		wait for clk_period/2;
   end process;
 

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

Personal tools