--
-- Copyright (C) 2015-2016 secunet Security Networks AG
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- This program 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 General Public License for more details.
--

with HW.Time;
with HW.GFX.GMA.Config;
with HW.GFX.GMA.Registers;

package body HW.GFX.GMA.PCH.FDI is

   FDI_RX_CTL_FDI_RX_ENABLE               : constant := 1 * 2 ** 31;
   FDI_RX_CTL_FS_ERROR_CORRECTION_ENABLE  : constant := 1 * 2 ** 27;
   FDI_RX_CTL_FE_ERROR_CORRECTION_ENABLE  : constant := 1 * 2 ** 26;
   FDI_RX_CTL_PORT_WIDTH_SEL_SHIFT        : constant :=          19;
   FDI_RX_CTL_FDI_PLL_ENABLE              : constant := 1 * 2 ** 13;
   FDI_RX_CTL_COMPOSITE_SYNC_SELECT       : constant := 1 * 2 ** 11;
   FDI_RX_CTL_FDI_AUTO_TRAIN              : constant := 1 * 2 ** 10;
   FDI_RX_CTL_ENHANCED_FRAMING_ENABLE     : constant := 1 * 2 **  6;
   FDI_RX_CTL_RAWCLK_TO_PCDCLK_SEL_MASK   : constant := 1 * 2 **  4;
   FDI_RX_CTL_RAWCLK_TO_PCDCLK_SEL_RAWCLK : constant := 0 * 2 **  4;
   FDI_RX_CTL_RAWCLK_TO_PCDCLK_SEL_PCDCLK : constant := 1 * 2 **  4;

   function TP_SHIFT return Natural is
     (if Config.Has_New_FDI_Sink then 8 else 28);

   function FDI_RX_CTL_TRAINING_PATTERN_MASK
      return Word32 is (Shift_Left (3, TP_SHIFT));

   function FDI_RX_CTL_TRAINING_PATTERN (TP : Training_Pattern) return Word32 is
     (case TP is
         when TP_1     => Shift_Left (0, TP_SHIFT),
         when TP_2     => Shift_Left (1, TP_SHIFT),
         when TP_Idle  => Shift_Left (2, TP_SHIFT),
         when TP_None  => Shift_Left (3, TP_SHIFT));

   function FDI_RX_CTL_PORT_WIDTH_SEL (Lane_Count : DP_Lane_Count) return Word32
   is
   begin
      return Shift_Left
        (Word32 (Lane_Count_As_Integer (Lane_Count)) - 1,
         FDI_RX_CTL_PORT_WIDTH_SEL_SHIFT);
   end FDI_RX_CTL_PORT_WIDTH_SEL;

   function FDI_RX_CTL_BPC (BPC : BPC_Type) return Word32
   with Pre => True
   is
   begin
      return
        (case BPC is
            when      6 => 2 * 2 ** 16,
            when     10 => 1 * 2 ** 16,
            when     12 => 3 * 2 ** 16,
            when others => 0 * 2 ** 16);
   end FDI_RX_CTL_BPC;

   FDI_RX_MISC_FDI_RX_PWRDN_LANE1_SHIFT   : constant :=               26;
   FDI_RX_MISC_FDI_RX_PWRDN_LANE1_MASK    : constant :=      3 * 2 ** 26;
   FDI_RX_MISC_FDI_RX_PWRDN_LANE0_SHIFT   : constant :=               24;
   FDI_RX_MISC_FDI_RX_PWRDN_LANE0_MASK    : constant :=      3 * 2 ** 24;
   FDI_RX_MISC_TP1_TO_TP2_TIME_48         : constant :=      2 * 2 ** 20;
   FDI_RX_MISC_FDI_DELAY_90               : constant := 16#90# * 2 **  0;

   function FDI_RX_MISC_FDI_RX_PWRDN_LANE1 (Value : Word32) return Word32
   with Pre => True
   is
   begin
      return Shift_Left (Value, FDI_RX_MISC_FDI_RX_PWRDN_LANE1_SHIFT);
   end FDI_RX_MISC_FDI_RX_PWRDN_LANE1;

   function FDI_RX_MISC_FDI_RX_PWRDN_LANE0 (Value : Word32) return Word32
   with Pre => True
   is
   begin
      return Shift_Left (Value, FDI_RX_MISC_FDI_RX_PWRDN_LANE0_SHIFT);
   end FDI_RX_MISC_FDI_RX_PWRDN_LANE0;

   FDI_RX_TUSIZE_SHIFT                    : constant :=          25;

   function FDI_RX_TUSIZE (Value : Word32) return Word32 is
   begin
      return Shift_Left (Value - 1, FDI_RX_TUSIZE_SHIFT);
   end FDI_RX_TUSIZE;

   FDI_RX_INTERLANE_ALIGNMENT             : constant := 1 * 2 ** 10;
   FDI_RX_SYMBOL_LOCK                     : constant := 1 * 2 **  9;
   FDI_RX_BIT_LOCK                        : constant := 1 * 2 **  8;

   ----------------------------------------------------------------------------

   type FDI_Registers is record
      RX_CTL      : Registers.Registers_Index;
      RX_MISC     : Registers.Registers_Index;
      RX_TUSIZE   : Registers.Registers_Index;
      RX_IMR      : Registers.Registers_Index;
      RX_IIR      : Registers.Registers_Index;
   end record;
   type FDI_Registers_Array is array (PCH.FDI_Port_Type) of FDI_Registers;
   FDI_Regs : constant FDI_Registers_Array := FDI_Registers_Array'
     (PCH.FDI_A => FDI_Registers'
        (RX_CTL      => Registers.FDI_RXA_CTL,
         RX_MISC     => Registers.FDI_RX_MISC_A,
         RX_TUSIZE   => Registers.FDI_RXA_TUSIZE1,
         RX_IMR      => Registers.FDI_RXA_IMR,
         RX_IIR      => Registers.FDI_RXA_IIR),
      PCH.FDI_B => FDI_Registers'
        (RX_CTL      => Registers.FDI_RXB_CTL,
         RX_MISC     => Registers.FDI_RX_MISC_B,
         RX_TUSIZE   => Registers.FDI_RXB_TUSIZE1,
         RX_IMR      => Registers.FDI_RXB_IMR,
         RX_IIR      => Registers.FDI_RXB_IIR),
      PCH.FDI_C => FDI_Registers'
        (RX_CTL      => Registers.FDI_RXC_CTL,
         RX_MISC     => Registers.FDI_RX_MISC_C,
         RX_TUSIZE   => Registers.FDI_RXC_TUSIZE1,
         RX_IMR      => Registers.FDI_RXC_IMR,
         RX_IIR      => Registers.FDI_RXC_IIR));

   ----------------------------------------------------------------------------

   procedure Pre_Train (Port : PCH.FDI_Port_Type; Port_Cfg : Port_Config)
   is
      Power_Down_Lane_Bits : constant Word32 :=
        (if Config.Has_FDI_RX_Power_Down then
            FDI_RX_MISC_FDI_RX_PWRDN_LANE1 (2) or
            FDI_RX_MISC_FDI_RX_PWRDN_LANE0 (2)
         else 0);
      RX_CTL_Settings : constant Word32 :=
         FDI_RX_CTL_PORT_WIDTH_SEL (Port_Cfg.FDI.Lane_Count) or
        (if Config.Has_FDI_BPC then
            FDI_RX_CTL_BPC (Port_Cfg.Mode.BPC) else 0) or
        (if Config.Has_FDI_Composite_Sel then
            FDI_RX_CTL_COMPOSITE_SYNC_SELECT else 0) or
        (if Port_Cfg.FDI.Enhanced_Framing then
            FDI_RX_CTL_ENHANCED_FRAMING_ENABLE else 0);
   begin
      -- TODO: HSW: check DISPIO_CR_TX_BMU_CR4, seems Linux doesn't know it

      Registers.Write
        (Register => FDI_Regs (Port).RX_MISC,
         Value    => Power_Down_Lane_Bits or
                     FDI_RX_MISC_TP1_TO_TP2_TIME_48 or
                     FDI_RX_MISC_FDI_DELAY_90);

      Registers.Write
        (Register => FDI_Regs (Port).RX_TUSIZE,
         Value    => FDI_RX_TUSIZE (64));

      Registers.Unset_Mask
        (Register => FDI_Regs (Port).RX_IMR,
         Mask     => FDI_RX_INTERLANE_ALIGNMENT or
                     FDI_RX_SYMBOL_LOCK or
                     FDI_RX_BIT_LOCK);
      Registers.Posting_Read (FDI_Regs (Port).RX_IMR);
      -- clear stale lock bits
      Registers.Write
        (Register => FDI_Regs (Port).RX_IIR,
         Value    => FDI_RX_INTERLANE_ALIGNMENT or
                     FDI_RX_SYMBOL_LOCK or
                     FDI_RX_BIT_LOCK);

      Registers.Write
        (Register => FDI_Regs (Port).RX_CTL,
         Value    => FDI_RX_CTL_FDI_PLL_ENABLE or
                     RX_CTL_Settings);
      Registers.Posting_Read (FDI_Regs (Port).RX_CTL);
      Time.U_Delay (220);

      Registers.Set_Mask
        (Register => FDI_Regs (Port).RX_CTL,
         Mask     => FDI_RX_CTL_RAWCLK_TO_PCDCLK_SEL_PCDCLK);
   end Pre_Train;

   procedure Train
     (Port     : in     PCH.FDI_Port_Type;
      TP       : in     Training_Pattern;
      Success  :    out Boolean)
   is
      Lock_Bit : constant Word32 :=
        (if TP = TP_1 then FDI_RX_BIT_LOCK else FDI_RX_SYMBOL_LOCK);

      procedure Check_Lock (Lock_Bit : Word32)
      is
      begin
         for I in 1 .. 5 loop
            Registers.Is_Set_Mask
              (Register => FDI_Regs (Port).RX_IIR,
               Mask     => Lock_Bit,
               Result   => Success);
            if Success then
               -- clear the lock bit
               Registers.Write
                 (Register => FDI_Regs (Port).RX_IIR,
                  Value    => Lock_Bit);
            end if;
            exit when Success;
            Time.U_Delay (1);
         end loop;
      end Check_Lock;
   begin
      Registers.Unset_And_Set_Mask
        (Register    => FDI_Regs (Port).RX_CTL,
         Mask_Unset  => FDI_RX_CTL_TRAINING_PATTERN_MASK,
         Mask_Set    => FDI_RX_CTL_FDI_RX_ENABLE or
                        FDI_RX_CTL_TRAINING_PATTERN (TP));
      Registers.Posting_Read (FDI_Regs (Port).RX_CTL);

      if TP <= TP_2 then
         Time.U_Delay (1);
         if TP = TP_1 then
            Check_Lock (FDI_RX_BIT_LOCK);
         else
            Check_Lock (FDI_RX_SYMBOL_LOCK);
            if Success then
               Check_Lock (FDI_RX_INTERLANE_ALIGNMENT);
            end if;
         end if;
      else
         Time.U_Delay (31);
         Success := True;
      end if;
   end Train;

   procedure Auto_Train (Port : PCH.FDI_Port_Type)
   is
   begin
      Registers.Set_Mask
        (Register => FDI_Regs (Port).RX_CTL,
         Mask     => FDI_RX_CTL_FDI_RX_ENABLE or
                     FDI_RX_CTL_FDI_AUTO_TRAIN);
      Registers.Posting_Read (FDI_Regs (Port).RX_CTL);

      if Config.Has_FDI_RX_Power_Down then
         Time.U_Delay (30);
         Registers.Unset_And_Set_Mask
           (Register    => FDI_Regs (Port).RX_MISC,
            Mask_Unset  => FDI_RX_MISC_FDI_RX_PWRDN_LANE1_MASK or
                           FDI_RX_MISC_FDI_RX_PWRDN_LANE0_MASK,
            Mask_Set    => FDI_RX_MISC_FDI_RX_PWRDN_LANE1 (0) or
                           FDI_RX_MISC_FDI_RX_PWRDN_LANE0 (0));
         Registers.Posting_Read (FDI_Regs (Port).RX_MISC);
      end if;

      Time.U_Delay (5);
   end Auto_Train;

   procedure Enable_EC (Port : PCH.FDI_Port_Type)
   is
   begin
      Registers.Set_Mask
        (Register => FDI_Regs (Port).RX_CTL,
         Mask     => FDI_RX_CTL_FS_ERROR_CORRECTION_ENABLE or
                     FDI_RX_CTL_FE_ERROR_CORRECTION_ENABLE);
   end Enable_EC;

   ----------------------------------------------------------------------------

   procedure Off (Port : PCH.FDI_Port_Type; OT : Off_Type)
   is
   begin
      Registers.Unset_Mask
        (Register => FDI_Regs (Port).RX_CTL,
         Mask     => FDI_RX_CTL_FDI_RX_ENABLE or
                     FDI_RX_CTL_FDI_AUTO_TRAIN);

      if Config.Has_FDI_RX_Power_Down and then OT >= Lanes_Off then
         Registers.Unset_And_Set_Mask
           (Register    => FDI_Regs (Port).RX_MISC,
            Mask_Unset  => FDI_RX_MISC_FDI_RX_PWRDN_LANE1_MASK or
                           FDI_RX_MISC_FDI_RX_PWRDN_LANE0_MASK,
            Mask_Set    => FDI_RX_MISC_FDI_RX_PWRDN_LANE1 (2) or
                           FDI_RX_MISC_FDI_RX_PWRDN_LANE0 (2));
         Registers.Posting_Read (FDI_Regs (Port).RX_MISC);
      end if;

      if OT >= Clock_Off then
         Registers.Unset_And_Set_Mask
           (Register    => FDI_Regs (Port).RX_CTL,
            Mask_Unset  => FDI_RX_CTL_RAWCLK_TO_PCDCLK_SEL_MASK,
            Mask_Set    => FDI_RX_CTL_RAWCLK_TO_PCDCLK_SEL_RAWCLK);

         Registers.Unset_Mask
           (Register    => FDI_Regs (Port).RX_CTL,
            Mask        => FDI_RX_CTL_FDI_PLL_ENABLE);
      end if;
   end Off;

end HW.GFX.GMA.PCH.FDI;
