-- SPDX-FileCopyrightText: 2022 CERN (home.cern)
--
-- SPDX-License-Identifier: CERN-OHL-W-2.0+

-------------------------------------------------------------------------------
-- Title      : ACAM TDX-GPX timestamp postprocessor
-- Project    : Fine Delay Core (FmcDelay1ns4cha)
-------------------------------------------------------------------------------
-- File       : fd_acam_timestamp_postprocessor.vhd
-- Author     : Tomasz Wlostowski
-- Company    : CERN
-- Created    : 2011-08-29
-- Last update: 2019-09-28
-- Platform   : FPGA-generic
-- Standard   : VHDL'93
-------------------------------------------------------------------------------
-- Description: Merges the coarse timestamp produced with the internal FPGA
-- counter with the fractional part obtained from the ACAM TDC. Merged timestamp
-- is then converted to standard White Rabbit time format, generating a final
-- UTC timestamp used for further processing. 
-------------------------------------------------------------------------------
--
-- Copyright (c) 2011 CERN / BE-CO-HT
-- 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
--
-------------------------------------------------------------------------------
-- Revisions  :
-- Date        Version  Author          Description
-- 2011-08-29  1.0      twlostow        Created
-------------------------------------------------------------------------------

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

use work.fine_delay_pkg.all;
use work.fd_main_wbgen2_pkg.all;

entity fd_acam_timestamp_postprocessor is
  generic(
    -- number of the bits in the fractional part
    g_frac_bits : integer := 12);
  port(
    clk_ref_i : in std_logic;
    rst_n_i   : in std_logic;

    ---------------------------------------------------------------------------
    -- Timestamp input, from the ACAM FS
    ---------------------------------------------------------------------------

    raw_valid_i : in std_logic;
    raw_utc_i   : in std_logic_vector(c_TIMESTAMP_UTC_BITS-1 downto 0);

    -- "start number" (value of coarse counter, counting at every start pulse of the
    -- TDC, i.e. 125 MHz / 32 = 3.90625 MHz)
    raw_coarse_i : in std_logic_vector(c_TIMESTAMP_COARSE_BITS-5-1 downto 0);

    -- raw fractional timestamp generated by ACAM
    raw_frac_i : in std_logic_vector(22 downto 0);

    -- coarse offset (in 125 MHz clock cycles) from the last ACAM's start pulse to the
    -- input pulse (0..15)
    raw_start_offset_i : in std_logic_vector(4 downto 0);

    -- Offset between the actual timescale and the ACAM fixed start signal generated
    -- by the AD9516 PLL. Used to align the timestamps to the externally
    -- provided time base (e.g. by White Rabbit).
    acam_timebase_offset_i : in std_logic_vector(5 downto 0);

    ---------------------------------------------------------------------------
    -- Post-processed timestamp. WARNING! DE-NORMALIZED!
    ---------------------------------------------------------------------------

    tag_valid_o   : out std_logic;
    tag_utc_o     : out std_logic_vector(c_TIMESTAMP_UTC_BITS-1 downto 0);
    tag_coarse_o  : out std_logic_vector(c_TIMESTAMP_COARSE_BITS-1 downto 0);
    tag_frac_o    : out std_logic_vector(g_frac_bits-1 downto 0);
    tag_dbg_raw_o : out std_logic_vector(31 downto 0);

    -- Wishbone regs
    regs_i : in t_fd_main_out_registers
    );

end fd_acam_timestamp_postprocessor;

architecture behavioral of fd_acam_timestamp_postprocessor is

  -- number of the fractional bits to ignore in the rescaled ACAM's fractional
  -- timestamp. Too little = low resolution, too high = crappy timing.
  constant c_SCALER_SHIFT : integer := 12;

  signal pp_pipe : std_logic_vector(4 downto 0);

  signal post_tag_coarse         : unsigned(c_TIMESTAMP_COARSE_BITS-1 downto 0);
  signal post_tag_frac           : unsigned(g_frac_bits-1 downto 0);
  signal post_tag_utc            : unsigned(c_TIMESTAMP_UTC_BITS-1 downto 0);
  signal post_frac_multiplied    : signed(c_SCALER_SHIFT + g_frac_bits + 8 downto 0);
  signal post_frac_multiplied_d0 : signed(c_SCALER_SHIFT + g_frac_bits + 8 downto 0);
  signal post_frac_start_adj     : signed(22 downto 0);

  signal adsfr_d0 : signed(17 downto 0);
  
  signal tag_valid   : std_logic;
  signal tag_utc     : std_logic_vector(c_TIMESTAMP_UTC_BITS-1 downto 0);
  signal tag_coarse  : std_logic_vector(c_TIMESTAMP_COARSE_BITS-1 downto 0);
  signal tag_frac    : std_logic_vector(g_frac_bits-1 downto 0);
  signal tag_dbg_raw : std_logic_vector(31 downto 0);

begin  -- behavioral

  -- Place an intermediate register on the ADSFR register (user as multiplicand
  -- later), so the multiplier can be balanced by the synthesis tool.
  p_buffer_adsfr : process(clk_ref_i)
  begin
    if rising_edge(clk_ref_i) then
      if rst_n_i = '0' then
        adsfr_d0 <= (others => '0');
      else
        adsfr_d0 <= signed(regs_i.adsfr_o);
      end if;
    end if;
  end process;


  p_postprocess_tags : process(clk_ref_i)
  begin
    if rising_edge(clk_ref_i) then
      if rst_n_i = '0' then
        tag_valid   <= '0';
        tag_coarse  <= (others => '0');
        tag_utc     <= (others => '0');
        tag_frac    <= (others => '0');
        tag_dbg_raw <= (others => '0');
      else

        -- Pipeline stage 1:
        -- Subtract the start offset from the fractional value got from the ACAM. 
        --
        -- ACAM logic is stupid and can't handle negative numbers correctly (which can occur
        -- when the start edge is too close to the stop edge). In order to avoid such
        -- situations, ACAM internally adds a constant offset (StartOffset), which we have
        -- to subtract here.
        
        
        pp_pipe(0) <= raw_valid_i;

        post_frac_start_adj         <= signed(raw_frac_i) - signed(regs_i.asor_offset_o);
        post_tag_coarse(4 downto 0) <= (others => '0');
        post_tag_utc                <= unsigned(raw_utc_i);

        -- pipeline stage 2:
        -- Check for the "wraparound" condition and adjust the coarse start counter.
        --
        -- Wraparound occurs when the ACAM's hasn't yet processed the latest start pulse
        -- (resulting with a value of the fractional timestamp close to the upper
        -- bound), but the FPGA counter had already "noticed" the next start. This
        -- happens because of different routing delays and jitter.

        pp_pipe(1) <= pp_pipe(0);

        if (unsigned(raw_start_offset_i) <= unsigned(regs_i.atmcr_c_thr_o)) and (post_frac_start_adj > signed(regs_i.atmcr_f_thr_o)) then
          post_tag_coarse(post_tag_coarse'left downto 5) <= unsigned(raw_coarse_i) - 1;
        else
          post_tag_coarse(post_tag_coarse'left downto 5) <= unsigned(raw_coarse_i);
        end if;

        -- Pipeline stage 3:
        -- Rescale the fractional part to our internal time base
        --
        -- ACAM counts in bins (of 27.something picoseconds), while we count in
        -- fractions of 8 ns period. A simple multiply operation does the trick
        -- here.

        pp_pipe(2)           <= pp_pipe(1);
        post_frac_multiplied <= resize(signed(post_frac_start_adj) * adsfr_d0, post_frac_multiplied'length);

        -- Pipeline stage 4
        -- Pass the multiplication result through another register, allowing
        -- the synthesis tool to spread the multiplier across several stages,
        -- improving the timing of the design.

        pp_pipe(3)              <= pp_pipe(2);
        post_frac_multiplied_d0 <= post_frac_multiplied;

        -- Pipeline stage 4:
        -- Split the rescaled fractional part into the (mod 4096) tag_frac_o and add
        -- the rest to the coarse part, along with the start-to-timescale offset.
        --
        -- A short explanation about the latter:
        -- We don't have control of the relation between the WR timescale (WR PPS)
        -- and the TDC start signal (which is the WR reference clock divided by
        -- 16 at the AD9516 PLL). So, every time there's a counter resync event
        -- (from associated WR PTP Core or an internal one), we simply count
        -- the number of ref clock cycles between the 1-PPS and the nearest TDC
        -- start edge and store it in acam_timebase_offset_i.
        --
        -- This value is added here to align the result to our timescale
        -- without messing around with the PLL.

        pp_pipe(4) <= pp_pipe(3);

        if(regs_i.tsbcr_raw_o = '0') then
          
          tag_utc    <= std_logic_vector(post_tag_utc);
          tag_coarse <= std_logic_vector(
            signed(post_tag_coarse)     -- index of start pulse (mod 16 = 0)
            + signed(acam_timebase_offset_i)  -- start-to-timescale offset
            + signed(post_frac_multiplied_d0(post_frac_multiplied_d0'left downto c_SCALER_SHIFT + g_frac_bits))); 
          -- extra coarse counts from ACAM's frac part after rescaling


          tag_frac  <= std_logic_vector(post_frac_multiplied_d0(c_SCALER_SHIFT + g_frac_bits-1 downto c_SCALER_SHIFT));
          tag_valid <= pp_pipe(4);

        elsif(raw_valid_i = '1') then
          
          tag_utc                   <= raw_utc_i;
          tag_coarse                <= raw_coarse_i & raw_start_offset_i;
          tag_frac                  <= raw_frac_i(11 downto 0);
          tag_dbg_raw(10 downto 0)  <= raw_frac_i(22 downto 12);
          tag_dbg_raw(15 downto 11) <= acam_timebase_offset_i(4 downto 0);
          tag_dbg_raw(23 downto 16) <= raw_coarse_i(7 downto 0);
          tag_dbg_raw(30 downto 24) <= raw_utc_i(6 downto 0);
          tag_dbg_raw(31)           <= acam_timebase_offset_i(5);

          tag_valid <= '1';
        else
          tag_valid <= '0';
        end if;


      end if;
    end if;
  end process;

  -- One more set of registers to help with timing
  p_output_reg : process(clk_ref_i)
  begin
    if rising_edge(clk_ref_i) then
      if rst_n_i = '0' then
        tag_valid_o   <= '0';
        tag_coarse_o  <= (others => '0');
        tag_utc_o     <= (others => '0');
        tag_frac_o    <= (others => '0');
        tag_dbg_raw_o <= (others => '0');
      else
        tag_valid_o   <= tag_valid;
        tag_coarse_o  <= tag_coarse;
        tag_utc_o     <= tag_utc;
        tag_frac_o    <= tag_frac;
        tag_dbg_raw_o <= tag_dbg_raw;
      end if;
    end if;
  end process p_output_reg;

end behavioral;
