-------------------------------------------------------------------------------
-- Title      : White Rabbit TAI to IRIG_B converter
--            : based on ZYNQ Z030/Z035/Z045
-- Project    : ET Pathfinder
-- URL        : https://gitlab.nikhef.nl/bosp/wr_irig_b
-------------------------------------------------------------------------------
-- File       : wr_irigb_conf.vhd
-- Author(s)  : Pascal Bos <bosp@nikhef.nl>
-- Company    : Nikhef
-- Created    : 2021-04-29
-- Last update: 2021-05-04
-- Standard   : VHDL'93
-------------------------------------------------------------------------------
-- Description: A module that takes the TAI signal from white rabbit and 
-- converts it into IRIG_B data. The PR signal is in sync with the PPS.
-- 
-------------------------------------------------------------------------------
-- Copyright (c) 2021 Nikhef
-------------------------------------------------------------------------------
-- GNU LESSER GENERAL PUBLIC LICENSE
--
-- This source file is free software; you can redistribute it
-- and/or modify it under the terms of the GNU Lesser General
-- Public License as published by the Free Software Foundation;
-- either version 2.1 of the License, or (at your option) any
-- later version.
--
-- This source is distributed in the hope that it will be
-- useful, but WITHOUT ANY WARRANTY; without even the implied
-- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-- PURPOSE.  See the GNU Lesser General Public License for more
-- details.
--
-- You should have received a copy of the GNU Lesser General
-- Public License along with this source; if not, download it
-- from http://www.gnu.org/licenses/lgpl-2.1.html
--
-------------------------------------------------------------------------------

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use ieee.numeric_std.all;

entity wr_irigb_conv is
  generic(
    clk_freq : natural := 125000000
  );
  Port ( 
    clk_i           : in std_logic;
    rst_n_i         : in std_logic;
    pps_i           : in std_logic;
    irig_b_o        : out std_logic;
    irig_b_ctrl_i   : in std_logic_vector(17 downto 0) := (others => '0'); 
    tm_time_valid_i : in std_logic;
    tm_tai_i        : in std_logic_vector(39 downto 0)
  );
end wr_irigb_conv;

architecture Behavioral of wr_irigb_conv is
  signal tai_time_s    : std_logic_vector(39 downto 0);
  signal irig_b_ctrl_s : std_logic_vector(17 downto 0);
  signal pps_reg       : std_logic;
  signal pps_trigger   : std_logic;
  type state_type_irig is (IDLE, PR,SEC,P1,MIN,P2,HOUR,P3,DAY_L,P4,DAY_H,P5,YEAR,P6,CTRL_1,P7,CTRL_2,P8,SBS_L,P9,SBS_H,P0,WAIT_FOR_PPS);
  signal prs,nxt : state_type_irig;
  
  type divisors_type   is array(0 to 4) of std_logic_vector(23 downto 0);
  type divideds_type   is array(0 to 4) of std_logic_vector(39 downto 0);
  type quotients_type  is array(0 to 4) of std_logic_vector(39 downto 0);  
  type remainders_type is array(0 to 4) of std_logic_vector(19 downto 0);
  
  constant divisors : divisors_type := (x"00003c", --60 seconds in minute
                                        x"00003c", --60 minutes in hour
                                        x"000018", --24 hours in day
                                        x"0005b5", --365.25*4 days in 4 years
                                        x"015180"); --seconds in a day
  signal divideds : divideds_type;                                   
  signal quotients : quotients_type; 
  signal remainders : remainders_type; 
  signal divider_pipe_valids : std_logic_vector(0 to 5);
  signal start_dividers : std_logic;
  
  signal seconds : std_logic_vector(5 downto 0);
  signal minutes : std_logic_vector(5 downto 0);
  signal hours   : std_logic_vector(3 downto 0);
  signal days    : std_logic_vector(8 downto 0);
  signal years   : std_logic_vector(39 downto 0); --should outlast the sun
  signal sbs     : std_logic_vector(17 downto 0);
  
  signal enable_bcd_conv : std_logic;
  
  
  --all bcd signals are 4bits * amount * of digits + 1 index marker
  signal seconds_bcd        : std_logic_vector(7 downto 0);
  signal minutes_bcd        : std_logic_vector(8 downto 0);
  signal hours_bcd          : std_logic_vector(8 downto 0);
  signal days_bcd           : std_logic_vector(17 downto 0);
  signal years_bcd          : std_logic_vector(8 downto 0); 

  signal days_bcd_tdata_2   : std_logic_vector(23 downto 0);
  signal years_bcd_tdata    : std_logic_vector(23 DOWNTO 0);
  signal days_bcd_tdata_1   : std_logic_vector(23 downto 0);
  signal hours_bcd_tdata    : std_logic_vector(23 DOWNTO 0);
  signal seconds_bcd_tdata  : std_logic_vector(23 DOWNTO 0);
  signal minutes_bcd_tdata  : std_logic_vector(23 DOWNTO 0);
  signal days_bcd_valid_1   : std_logic;
  
  signal bcd_ready : std_logic;
  
  type irig_signal_type is (ONE, ZERO, MARK);
  signal irig_sig : irig_signal_type;
  signal irig_counter_enable : std_logic;
  signal irig_bit_comp : std_logic; 
  signal irig_word_cnt : integer range 0 to 8;
  
  constant bit_time : integer := clk_freq/100; 
  constant zero_time : natural := bit_time / 5;
  constant one_time  : natural := bit_time / 2;
  constant mark_time : natural := bit_time - zero_time;
  signal   irig_cnt : integer range 0 to bit_time;
  
COMPONENT tai_divider
  PORT (
    aclk : IN STD_LOGIC;
    s_axis_divisor_tvalid : IN STD_LOGIC;
    s_axis_divisor_tready : OUT STD_LOGIC;
    s_axis_divisor_tdata : IN STD_LOGIC_VECTOR(23 DOWNTO 0);
    s_axis_dividend_tvalid : IN STD_LOGIC;
    s_axis_dividend_tready : OUT STD_LOGIC;
    s_axis_dividend_tdata : IN STD_LOGIC_VECTOR(39 DOWNTO 0);
    m_axis_dout_tvalid : OUT STD_LOGIC;
    m_axis_dout_tdata : OUT STD_LOGIC_VECTOR(63 DOWNTO 0)
  );
END COMPONENT;

COMPONENT bcd_divider
  PORT (
    aclk : IN STD_LOGIC;
    s_axis_divisor_tvalid : IN STD_LOGIC;
    s_axis_divisor_tready : OUT STD_LOGIC;
    s_axis_divisor_tdata : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
    s_axis_dividend_tvalid : IN STD_LOGIC;
    s_axis_dividend_tready : OUT STD_LOGIC;
    s_axis_dividend_tdata : IN STD_LOGIC_VECTOR(15 DOWNTO 0);
    m_axis_dout_tvalid : OUT STD_LOGIC;
    m_axis_dout_tdata : OUT STD_LOGIC_VECTOR(23 DOWNTO 0)
  );
END COMPONENT;
begin

--
-- This process latches the tai_time when its valid and starts the divider --
--
tai_register : process (clk_i, rst_n_i ) is
begin
  if rst_n_i = '0' then
    tai_time_s <= (others => '0');
    start_dividers <= '0';
  elsif rising_edge(clk_i) then
    if tm_time_valid_i = '1' then
      tai_time_s <= tm_tai_i;
      start_dividers <= '1';
    else
      start_dividers <= '0';
    end if;
  end if;
end process tai_register;

ctrl_register : process (clk_i, rst_n_i ) is
begin
  if rst_n_i = '0' then
    irig_b_ctrl_s <= (others => '0');
  elsif rising_edge(clk_i) then
    if pps_i = '1' then
      irig_b_ctrl_s <= irig_b_ctrl_i;
    end if;
  end if;
end process ctrl_register;

pps_register : process (clk_i, rst_n_i ) is
begin
  if rst_n_i = '0' then
    pps_reg <= '0';
  elsif rising_edge(clk_i) then
    pps_reg <= pps_i;
  end if;
end process pps_register;
pps_trigger <= '1' when pps_i = '1' and pps_reg = '0' else '0';
--
-- this statement generates the 5 dividers for: Seconds&Minutes, Hours, Days, Years and SBS --
-- it utilizes both the dividers "Answer/Quotient" and "Remainder", as can be seen below    --
--
--            ___________                 ___________                 ___________                 _________________      ___________                
--  --------->|           |---------------|           |---------------|           |---------------|shift 2bits left |----|           |--------[current year since 1970]
--  Tai_Time  | DIVIDE BY |(Answer)       | DIVIDE BY |(Answer)       | DIVIDE BY |(Answer)       | (Multiply by 4) |    | DIVIDE BY |(Answer)       
--  (seconds) |    60     |               |    60     |               |    24     |               |_________________|    |   1425    |               
--            |           |------------   |           |------------   |           |------------                          |           |------------   
--            |           |(Remainder) |  |           |(Remainder) |  |           |(Remainder) |                         |           |(Remainder) |  
--            |___________|            |  |___________|            |  |___________|            |                         |___________|            |  
--                                     V                           V                           V                                                  V  
--                        [seconds in the current minute]  [minutes in the current hour]  [Hour in the current day]                 [Day in the current year - 1]
--
--

gen_tai_dividers : for I in 0 to 4 generate
  signal answer : std_logic_vector(63 downto 0);
begin
  divider_inst : tai_divider
    PORT MAP (
      aclk => clk_i,
      s_axis_divisor_tvalid  => divider_pipe_valids(I),
      s_axis_divisor_tready  => open,
      s_axis_divisor_tdata   => divisors(I),
      s_axis_dividend_tvalid => divider_pipe_valids(I),
      s_axis_dividend_tready => open,
      s_axis_dividend_tdata  => divideds(I),
      m_axis_dout_tvalid     => divider_pipe_valids(I+1),
      m_axis_dout_tdata      => answer
    );
    quotients(I) <= answer(63 downto 24);
    remainders(I) <= answer(19 downto 0);
end generate gen_tai_dividers;

divider_pipe_valids(0) <= start_dividers;
divideds(0) <= tai_time_s;
divideds(1) <= quotients(0); -- quotient of seconds will be divided by 60 to get minutes
divideds(2) <= quotients(1); -- quotient of minutes will be divided by 24 to get hours
divideds(3) <= quotients(2)(37 downto 0) & "00"; -- quotient of hours will be multiplied by 4 and divided by 4 years to get 4 times the days
divideds(4) <= tai_time_s;

--latches the output of all dividers after the final dividers have finished (could be cleaner, for now it works)
--starts the bcd_dividers
date_register : process (clk_i, rst_n_i) is
begin
  if rst_n_i = '0' then
    seconds <= (others => '0');
    minutes <= (others => '0');
    hours   <= (others => '0');
    days    <= (others => '0');
    years   <= (others => '0');  
    sbs     <= (others => '0'); 
    enable_bcd_conv <= '0';
  elsif rising_edge(clk_i) then
    if divider_pipe_valids(5) = '1' then
      seconds <= remainders(0)(5 downto 0);
      minutes <= remainders(1)(5 downto 0);
      hours   <= remainders(2)(3 downto 0);
      days    <= std_logic_vector(unsigned(remainders(3)(10 downto 2)) + 1); --there is no day zero
      years   <= quotients(3);
      sbs(16 downto 0)     <= remainders(4)(16 downto 0);
      sbs(17) <= '0';
      enable_bcd_conv <= '1';
    else
      enable_bcd_conv <= '0';
    end if;
  end if;
end process date_register;

--counts the clock cycles in each irig bit
irig_counter : process (clk_i, rst_n_i) is
begin
  if rst_n_i = '0' then
    irig_cnt <= 0;
  elsif rising_edge(clk_i) then
    if prs = IDLE or prs = WAIT_FOR_PPS then
      irig_cnt <= 1; --when a pps is seen it starts from one to compensate for that clock cycle
    elsif irig_cnt = bit_time-1 then
      irig_cnt <= 0;
    else
      irig_cnt <= irig_cnt + 1;
    end if;
  end if;
end process irig_counter;
irig_bit_comp <= '1' when irig_cnt = bit_time-1 else '0';  

--the driver of the irig_b outbut signal
--this is combinatoric to ensure a same-clock cycle start with the pps.
irig_b_o <= pps_i when (prs = IDLE or prs = WAIT_FOR_PPS) 
            else '0' when (rst_n_i = '0')               
                       or (irig_cnt > mark_time) 
                       or (irig_cnt > one_time and irig_sig = ONE)
                       or (irig_cnt > zero_time and irig_sig = ZERO)        
            else '1';
         
--counts its position within the current irig_b word ( f.e.: 1,2,4,8,10,20,40)    
word_counter : process (clk_i, rst_n_i) is
begin
  if rst_n_i = '0' then
    irig_word_cnt <= 0;
  elsif rising_edge(clk_i) then
    if prs /= nxt then
      irig_word_cnt <= 0;
    elsif irig_bit_comp = '1' then
      irig_word_cnt <= irig_word_cnt + 1;
    end if;
  end if;
end process word_counter;


--State machine, a three process Moore model. AS IT SHOULD BE!
state_register : process (clk_i, rst_n_i) is
begin
  if rst_n_i = '0' then
    prs <= IDLE;
  elsif rising_edge(clk_i) then
    prs <= nxt;
  end if;
end process state_register;

next_state_decoder : process(prs,rst_n_i,pps_trigger,irig_word_cnt,irig_bit_comp,irig_cnt) is 
begin
  if rst_n_i = '0' then
    nxt <= IDLE;
  elsif pps_trigger = '1' then --A PPS signal will ALWAYS result in a new frame being send.
    nxt <= PR;
  else
    case prs is
    when IDLE =>  
      nxt <= IDLE; -- stay here until pps_trigger jumps to PR.
    when PR =>
      if irig_bit_comp = '1' then
        nxt <= SEC;
      else
        nxt <= PR;
      end if;
    when SEC => 
      if irig_word_cnt >= 7 and irig_bit_comp = '1' then --checks if the last bit has been send
        nxt <= P1;
      else
        nxt <= SEC;
      end if;
    when P1 =>
      if irig_bit_comp = '1' then
        nxt <= MIN;
      else
        nxt <= P1;
      end if;
    when MIN =>    
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P2;
      else
        nxt <= MIN;
      end if; 
    when P2 => 
      if irig_bit_comp = '1' then
        nxt <= HOUR;
      else
        nxt <= P2;
      end if;
    when HOUR =>
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P3;
      else
        nxt <= HOUR;
      end if; 
    when P3 => 
      if irig_bit_comp = '1' then
        nxt <= DAY_L;
      else
        nxt <= P3;
      end if;
    when DAY_L =>
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P4;
      else
        nxt <= DAY_L;
      end if;
    when P4 =>
      if irig_bit_comp = '1' then
        nxt <= DAY_H;
      else
        nxt <= P4;
      end if;
    when DAY_H =>
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P5;
      else
        nxt <= DAY_H;
      end if;
    when P5 =>
      if irig_bit_comp = '1' then
        nxt <= YEAR;
      else
        nxt <= P5;
      end if;
    when YEAR =>
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P6;
      else
        nxt <= YEAR;
      end if;
    when P6 =>
      if irig_bit_comp = '1' then
        nxt <= CTRL_1;
      else
        nxt <= P6;
      end if;
    when CTRL_1 => 
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P7;
      else
        nxt <= CTRL_1;
      end if;
    when P7 =>
      if irig_bit_comp = '1' then
        nxt <= CTRL_2;
      else
        nxt <= P7;
      end if;
    when CTRL_2 =>
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P8;
      else
        nxt <= CTRL_2;
      end if;
    when P8 =>
      if irig_bit_comp = '1' then
        nxt <= SBS_L;
      else
        nxt <= P8;
      end if;
    when SBS_L =>
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P9;
      else
        nxt <= SBS_L;
      end if;
    when P9 =>
      if irig_bit_comp = '1' then
        nxt <= SBS_H;
      else
        nxt <= P9;
      end if;
    when SBS_H =>
      if irig_word_cnt >= 8 and irig_bit_comp = '1' then
        nxt <= P0;
      else
        nxt <= SBS_H;
      end if;
    when P0 =>
      if irig_cnt > mark_time then
        nxt <= WAIT_FOR_PPS;
      else
        nxt <= P0;
      end if;
    when WAIT_FOR_PPS =>
        nxt <= WAIT_FOR_PPS; -- stay here until pps_trigger jumps to PR.
    end case;
  end if;
end process next_state_decoder;

output_decoder : process(prs, irig_word_cnt, seconds_bcd, minutes_bcd, hours_bcd, days_bcd, years_bcd, irig_b_ctrl_s, sbs) is 
  variable irig_bit  : std_logic;
  variable irig_mark : std_logic;
begin

  irig_mark := '0';
  irig_bit  := '0';
  
  case prs is 
  when IDLE | PR | P1 | P2 | P3 | P4 | P5 | P6 | P7 | P8 | P9 | P0 | WAIT_FOR_PPS => 
    irig_mark := '1'; -- All these states are MARK signals.
  when SEC => 
    irig_bit :=  seconds_bcd(irig_word_cnt);
  when MIN =>
    irig_bit :=  minutes_bcd(irig_word_cnt);
  when HOUR =>
    irig_bit :=  hours_bcd(irig_word_cnt);
  when DAY_L =>
    irig_bit :=  days_bcd(irig_word_cnt);
  when DAY_H =>
    irig_bit :=  days_bcd(irig_word_cnt+9);
  when YEAR =>
    irig_bit :=  years_bcd(irig_word_cnt);
  when CTRL_1 =>
    irig_bit :=  irig_b_ctrl_s(irig_word_cnt);
  when CTRL_2 =>
    irig_bit :=  irig_b_ctrl_s(irig_word_cnt+9);
  when SBS_L =>
    irig_bit :=  sbs(irig_word_cnt);
  when SBS_H =>
    irig_bit :=  sbs(irig_word_cnt+9); 
  end case;
  
  if irig_mark = '1' then
    irig_sig <= MARK;
  elsif irig_bit = '1' then
    irig_sig <= ONE;
  else
    irig_sig <= ZERO;
  end if;
end process output_decoder;

--The divide-by-ten dividers to convert binary values into bcd values
--              ___________                 
--    --------->|           |---------> 4
--    Value     | DIVIDE BY |  (Answer)       
--    (binary)  |    10     |               
--   [f.e: 42]  |           |---------> 2 
--              |           |  (Remainder)     
--              |___________|              
--                                         
--The diveder for "days" has an additional divide-by-100 divider because there are more then 99 days in a year.
seconds_bcd_divider : bcd_divider
  PORT MAP (
    aclk => clk_i,
    s_axis_divisor_tvalid  => enable_bcd_conv,
    s_axis_divisor_tready  => open,
    s_axis_divisor_tdata   => x"0A", --divides by 10
    s_axis_dividend_tvalid => enable_bcd_conv,
    s_axis_dividend_tready => open,
    s_axis_dividend_tdata(15 downto 6) => (others => '0'),
    s_axis_dividend_tdata(5 downto 0)  => seconds,
    m_axis_dout_tvalid => open,
    m_axis_dout_tdata => seconds_bcd_tdata
  );
  --all the bcd data has some '0' inserted between then to separate decimal and unit, as is requierd by IRIG
  seconds_bcd <= seconds_bcd_tdata(10 downto 8) & '0' & seconds_bcd_tdata(3 downto 0);
   
minutes_bcd_divider : bcd_divider
  PORT MAP (
    aclk => clk_i,
    s_axis_divisor_tvalid  => enable_bcd_conv,
    s_axis_divisor_tready  => open,
    s_axis_divisor_tdata   => x"0A",
    s_axis_dividend_tvalid => enable_bcd_conv,
    s_axis_dividend_tready => open,
    s_axis_dividend_tdata(15 downto 6) => (others => '0'),
    s_axis_dividend_tdata(5 downto 0)  => minutes,
    m_axis_dout_tvalid => open,
    m_axis_dout_tdata => minutes_bcd_tdata
  );
  minutes_bcd   <= '0' & minutes_bcd_tdata(10 downto 8) & '0' & minutes_bcd_tdata(3 downto 0);
  
  
hours_bcd_divider : bcd_divider
  PORT MAP (
    aclk => clk_i,
    s_axis_divisor_tvalid  => enable_bcd_conv,
    s_axis_divisor_tready  => open,
    s_axis_divisor_tdata   => x"0A",
    s_axis_dividend_tvalid => enable_bcd_conv,
    s_axis_dividend_tready => open,
    s_axis_dividend_tdata(15 downto 4) => (others => '0'),
    s_axis_dividend_tdata(3 downto 0)  => hours,
    m_axis_dout_tvalid => open,
    m_axis_dout_tdata => hours_bcd_tdata
  );
  
  hours_bcd <= "00" & hours_bcd_tdata(9 downto 8) & '0' & hours_bcd_tdata(3 downto 0);
  
years_bcd_divider : bcd_divider
  PORT MAP (
    aclk => clk_i,
    s_axis_divisor_tvalid  => enable_bcd_conv,
    s_axis_divisor_tready  => open,
    s_axis_divisor_tdata   => x"0A",
    s_axis_dividend_tvalid => enable_bcd_conv,
    s_axis_dividend_tready => open,
    s_axis_dividend_tdata  => years(15 downto 0),
    m_axis_dout_tvalid => open,
    m_axis_dout_tdata => years_bcd_tdata
  );
  years_bcd  <= years_bcd_tdata(11 downto 8) & '0' & years_bcd_tdata(3 downto 0);

days_bcd_divider_1 : bcd_divider
  PORT MAP (
    aclk => clk_i,
    s_axis_divisor_tvalid  => enable_bcd_conv,
    s_axis_divisor_tready  => open,
    s_axis_divisor_tdata   => x"64", --100
    s_axis_dividend_tvalid => enable_bcd_conv,
    s_axis_dividend_tready => open,
    s_axis_dividend_tdata(15 downto 9) => (others => '0'),
    s_axis_dividend_tdata(8 downto 0)  => days,
    m_axis_dout_tvalid => days_bcd_valid_1,
    m_axis_dout_tdata => days_bcd_tdata_2
  );
  
days_bcd_divider_2 : bcd_divider
  PORT MAP (
    aclk => clk_i,
    s_axis_divisor_tvalid  => days_bcd_valid_1,
    s_axis_divisor_tready  => open,
    s_axis_divisor_tdata   => x"0A", 
    s_axis_dividend_tvalid => days_bcd_valid_1,
    s_axis_dividend_tready => open,
    s_axis_dividend_tdata(15 downto 8) => (others => '0'),
    s_axis_dividend_tdata(7 downto 0)  => days_bcd_tdata_2(7 downto 0),
    m_axis_dout_tvalid => bcd_ready,
    m_axis_dout_tdata => days_bcd_tdata_1
  ); 
    
  days_bcd <= "0000000" & days_bcd_tdata_2(9 downto 8) & days_bcd_tdata_1(11 downto 8) & '0' & days_bcd_tdata_1(3 downto 0);

end Behavioral;
