DigitalClock
From Hamsterworks Wiki!
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.
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;