/** @file
  PCH Smbus Executive Code (common PEI/DXE/SMM code)

Copyright (c) 2017, Intel Corporation. All rights reserved.<BR>
SPDX-License-Identifier: BSD-2-Clause-Patent

**/
#include <Uefi/UefiBaseType.h>
#include <Library/IoLib.h>
#include <Library/DebugLib.h>
#include <Library/TimerLib.h>
#include <IndustryStandard/SmBus.h>
#include <IndustryStandard/Pci30.h>
#include <PchAccess.h>
#include <Library/MmPciLib.h>
#include <Library/PchSmbusCommonLib.h>

/**
  Get SMBUS IO Base address


  @retval UINT32                  The SMBUS IO Base Address
**/
UINT32
SmbusGetIoBase (
  VOID
  )
{
  UINT32  SmbusIoBase;

  SmbusIoBase = MmioRead32 (
                  MmPciBase (
                    DEFAULT_PCI_BUS_NUMBER_PCH,
                    PCI_DEVICE_NUMBER_PCH_SMBUS,
                    PCI_FUNCTION_NUMBER_PCH_SMBUS)
                    + R_PCH_SMBUS_BASE) & B_PCH_SMBUS_BASE_BAR;

  ASSERT (SmbusIoBase != B_PCH_SMBUS_BASE_BAR && SmbusIoBase != 0);

  return SmbusIoBase;
}

/**
  This function provides a standard way to read PCH Smbus IO registers.

  @param[in] Offset               Register offset from Smbus base IO address.

  @retval UINT8                   Returns data read from IO.
**/
UINT8
EFIAPI
SmbusIoRead (
  IN      UINT8           Offset
  )
{
  return IoRead8 (SmbusGetIoBase () + Offset);
}

/**
  This function provides a standard way to write PCH Smbus IO registers.

  @param[in] Offset               Register offset from Smbus base IO address.
  @param[in] Data                 Data to write to register.

**/
VOID
EFIAPI
SmbusIoWrite (
  IN      UINT8           Offset,
  IN      UINT8           Data
  )
{
  ///
  /// Write New Value
  ///
  IoWrite8 (SmbusGetIoBase () + Offset, Data);
  return;
}

/**
  This function provides a standard way to check if an SMBus transaction has
  completed.

  @param[in] StsReg               Not used for input. On return, contains the
                                  value of the SMBus status register.

  @retval TRUE                    Transaction is complete
  @retval FALSE                   Otherwise.
**/
BOOLEAN
EFIAPI
IoDone (
  IN      UINT8           *StsReg
  )
{
  ///
  /// Wait for IO to complete
  ///
  UINTN StallIndex;
  UINTN StallTries;

  StallTries = STALL_TIME / STALL_PERIOD;

  for (StallIndex = 0; StallIndex < StallTries; StallIndex++) {
    *StsReg = SmbusIoRead (R_PCH_SMBUS_HSTS);
    if (*StsReg & (B_PCH_SMBUS_INTR | B_PCH_SMBUS_BYTE_DONE_STS | B_PCH_SMBUS_DERR | B_PCH_SMBUS_BERR)) {
      return TRUE;
    } else {
      MicroSecondDelay (STALL_PERIOD);
    }
  }

  return FALSE;
}

/**
  Check if it's ok to use the bus.


  @retval EFI_SUCCESS             SmBus is acquired and it's safe to send commands.
  @retval EFI_TIMEOUT             SmBus is busy, it's not safe to send commands.
**/
EFI_STATUS
AcquireBus (
  VOID
  )
{
  UINT8 StsReg;

  StsReg  = 0;
  StsReg  = SmbusIoRead (R_PCH_SMBUS_HSTS);
  if (StsReg & B_PCH_SMBUS_IUS) {
    return EFI_TIMEOUT;
  } else if (StsReg & B_PCH_SMBUS_HBSY) {
    ///
    /// Clear Status Register and exit
    ///
    SmbusIoWrite (R_PCH_SMBUS_HSTS, B_PCH_SMBUS_HSTS_ALL);
    return EFI_TIMEOUT;
  } else {
    ///
    /// Clear out any odd status information (Will Not Clear In Use)
    ///
    SmbusIoWrite (R_PCH_SMBUS_HSTS, StsReg);
    return EFI_SUCCESS;
  }
}

/**
  This function provides a standard way to execute Smbus protocols
  as defined in the SMBus Specification. The data can either be of
  the Length byte, word, or a block of data. The resulting transaction will be
  either the SMBus Slave Device accepts this transaction or this function
  returns with an error

  @param[in] SlaveAddress         Smbus Slave device the command is directed at
  @param[in] Command              Slave Device dependent
  @param[in] Operation            Which SMBus protocol will be used
  @param[in] PecCheck             Defines if Packet Error Code Checking is to be used
  @param[in, out] Length          How many bytes to read. Must be 0 <= Length <= 32 depending on Operation
                                  It will contain the actual number of bytes read/written.
  @param[in, out] Buffer          Contain the data read/written.

  @retval EFI_SUCCESS             The operation completed successfully.
  @exception EFI_UNSUPPORTED      The operation is unsupported.

  @retval EFI_INVALID_PARAMETER   Length or Buffer is NULL for any operation besides
                                  quick read or quick write.
  @retval EFI_TIMEOUT             The transaction did not complete within an internally
                                  specified timeout period, or the controller is not
                                  available for use.
  @retval EFI_DEVICE_ERROR        There was an Smbus error (NACK) during the operation.
                                  This could indicate the slave device is not present
                                  or is in a hung condition.
**/
EFI_STATUS
SmbusExec (
  IN      EFI_SMBUS_DEVICE_ADDRESS  SlaveAddress,
  IN      EFI_SMBUS_DEVICE_COMMAND  Command,
  IN      EFI_SMBUS_OPERATION       Operation,
  IN      BOOLEAN                   PecCheck,
  IN OUT  UINTN                     *Length,
  IN OUT  VOID                      *Buffer
  )
{
  EFI_STATUS  Status;
  UINT8       AuxcReg;
  UINT8       AuxStsReg;
  UINT8       SmbusOperation;
  UINT8       StsReg;
  UINT8       SlvAddrReg;
  UINT8       HostCmdReg;
  UINT8       BlockCount;
  BOOLEAN     BufferTooSmall;
  UINTN       Index;
  UINTN       BusIndex;
  UINT8       *CallBuffer;
  UINT8       SmbusHctl;
  UINT32      Timeout;

  CallBuffer  = Buffer;
  BlockCount  = 0;

  ///
  /// For any operations besides quick read & write, the pointers to
  /// Length and Buffer must not be NULL.
  ///
  if ((Operation != EfiSmbusQuickRead) && (Operation != EfiSmbusQuickWrite)) {
    if ((Length == NULL) || (Buffer == NULL)) {
      return EFI_INVALID_PARAMETER;
    }
  }
  ///
  /// See if its ok to use the bus based upon INUSE_STS bit.
  ///
  Status = AcquireBus ();
  if (EFI_ERROR (Status)) {
    return Status;
  }
  ///
  /// This is the main operation loop.  If the operation results in a Smbus
  /// collision with another master on the bus, it attempts the requested
  /// transaction again at least BUS_TRIES attempts.
  ///
  for (BusIndex = 0; BusIndex < BUS_TRIES; BusIndex++) {
    ///
    /// Operation Specifics (pre-execution)
    ///
    Status          = EFI_SUCCESS;
    SmbusOperation  = V_PCH_SMBUS_SMB_CMD_QUICK;
    SlvAddrReg      = (UINT8) ((SlaveAddress.SmbusDeviceAddress << 1) | 1);
    HostCmdReg      = (UINT8) Command;
    AuxcReg         = 0;

    switch (Operation) {

      case EfiSmbusQuickWrite:
        SlvAddrReg--;

        ///
        /// The "break;" command is not present here to allow code execution
        /// do drop into the next case, which contains common code to this case.
        ///
      case EfiSmbusQuickRead:
        if (PecCheck == TRUE) {
          Status = EFI_UNSUPPORTED;
        }
        break;

      case EfiSmbusSendByte:
        HostCmdReg = CallBuffer[0];
        SlvAddrReg--;

        ///
        /// The "break;" command is not present here to allow code execution
        /// do drop into the next case, which contains common code to this case.
        ///
      case EfiSmbusReceiveByte:
        SmbusOperation = V_PCH_SMBUS_SMB_CMD_BYTE;
        if (*Length < 1) {
          Status = EFI_BUFFER_TOO_SMALL;
        }

        *Length = 1;
        break;

      case EfiSmbusWriteByte:
        SmbusIoWrite (R_PCH_SMBUS_HD0, CallBuffer[0]);
        SlvAddrReg--;
        *Length = 1;

        ///
        /// The "break;" command is not present here to allow code execution
        /// do drop into the next case, which contains common code to this case.
        ///
      case EfiSmbusReadByte:
        if (*Length < 1) {
          Status = EFI_BUFFER_TOO_SMALL;
        } else if (*Length == 1) {
          SmbusOperation = V_PCH_SMBUS_SMB_CMD_BYTE_DATA;
        } else if (*Length <= 256) {
          if (PecCheck == TRUE) {
            ///
            /// The I2C Read command with either PEC_EN or AAC bit set
            /// produces undefined results.
            ///
            Status = EFI_UNSUPPORTED;
          }

          SmbusOperation = V_PCH_SMBUS_SMB_CMD_IIC_READ;
        } else {
          Status = EFI_INVALID_PARAMETER;
        }

        break;

      case EfiSmbusReadWord:
        SmbusOperation = V_PCH_SMBUS_SMB_CMD_WORD_DATA;
        if (*Length < 2) {
          Status = EFI_BUFFER_TOO_SMALL;
        }

        *Length = 2;
        break;

      case EfiSmbusWriteWord:
        SmbusOperation = V_PCH_SMBUS_SMB_CMD_WORD_DATA;
        SlvAddrReg--;
        SmbusIoWrite (R_PCH_SMBUS_HD1, CallBuffer[1]);
        SmbusIoWrite (R_PCH_SMBUS_HD0, CallBuffer[0]);
        if (*Length < 2) {
          Status = EFI_BUFFER_TOO_SMALL;
        }

        *Length = 2;
        break;

      case EfiSmbusWriteBlock:
        SmbusIoWrite (R_PCH_SMBUS_HD0, *(UINT8 *) Length);
        SlvAddrReg--;
        BlockCount = (UINT8) (*Length);

        ///
        /// The "break;" command is not present here to allow code execution
        /// do drop into the next case, which contains common code to this case.
        ///
      case EfiSmbusReadBlock:
        SmbusOperation = V_PCH_SMBUS_SMB_CMD_BLOCK;
        if ((*Length < 1) || (*Length > 32)) {
          Status = EFI_INVALID_PARAMETER;
          break;
        }

        AuxcReg |= B_PCH_SMBUS_E32B;
        break;

      case EfiSmbusProcessCall:
        SmbusOperation = V_PCH_SMBUS_SMB_CMD_PROCESS_CALL;
        SmbusIoWrite (R_PCH_SMBUS_HD1, CallBuffer[1]);
        SmbusIoWrite (R_PCH_SMBUS_HD0, CallBuffer[0]);
        if (*Length < 2) {
          Status = EFI_BUFFER_TOO_SMALL;
        }

        *Length = 2;
        break;

      case EfiSmbusBWBRProcessCall:
        ///
        /// The write byte count cannot be zero or more than
        /// 32 bytes.
        ///
        if ((*Length < 1) || (*Length > 32)) {
          Status = EFI_INVALID_PARAMETER;
          break;
        }

        SmbusIoWrite (R_PCH_SMBUS_HD0, *(UINT8 *) Length);
        BlockCount      = (UINT8) (*Length);
        SmbusOperation  = V_PCH_SMBUS_SMB_CMD_BLOCK_PROCESS;

        AuxcReg |= B_PCH_SMBUS_E32B;
        break;

      default:
        Status = EFI_INVALID_PARAMETER;
        break;
    }

    if (EFI_ERROR (Status)) {
      break;
    }

    if (PecCheck == TRUE) {
      AuxcReg |= B_PCH_SMBUS_AAC;
    }
    ///
    /// Set Auxiliary Control register
    ///
    SmbusIoWrite (R_PCH_SMBUS_AUXC, AuxcReg);

    ///
    /// Reset the pointer of the internal buffer
    ///
    SmbusIoRead (R_PCH_SMBUS_HCTL);

    ///
    /// Now that the 32 byte buffer is turned on, we can write th block data
    /// into it
    ///
    if ((Operation == EfiSmbusWriteBlock) || (Operation == EfiSmbusBWBRProcessCall)) {
      for (Index = 0; Index < BlockCount; Index++) {
        ///
        /// Write next byte
        ///
        SmbusIoWrite (R_PCH_SMBUS_HBD, CallBuffer[Index]);
      }
    }
    ///
    /// Set SMBus slave address for the device to send/receive from
    ///
    SmbusIoWrite (R_PCH_SMBUS_TSA, SlvAddrReg);

    ///
    /// For I2C read, send DATA1 register for the offset (address)
    /// within the serial memory chips
    ///
    if ((Operation == EfiSmbusReadByte) && (*Length > 1)) {
      SmbusIoWrite (R_PCH_SMBUS_HD1, HostCmdReg);
    } else {
      ///
      /// Set Command register
      ///
      SmbusIoWrite (R_PCH_SMBUS_HCMD, HostCmdReg);
    }
    ///
    /// Set Control Register (Initiate Operation, Interrupt disabled)
    ///
    SmbusIoWrite (R_PCH_SMBUS_HCTL, (UINT8) (SmbusOperation + B_PCH_SMBUS_START));

    ///
    /// Wait for IO to complete
    ///
    if (!IoDone (&StsReg)) {
      Status = EFI_TIMEOUT;
      break;
    } else if (StsReg & B_PCH_SMBUS_DERR) {
      AuxStsReg = SmbusIoRead (R_PCH_SMBUS_AUXS);
      if (AuxStsReg & B_PCH_SMBUS_CRCE) {
        Status = EFI_CRC_ERROR;
      } else {
        Status = EFI_DEVICE_ERROR;
      }
      break;
    } else if (StsReg & B_PCH_SMBUS_BERR) {
      ///
      /// Clear the Bus Error for another try
      ///
      Status = EFI_DEVICE_ERROR;
      SmbusIoWrite (R_PCH_SMBUS_HSTS, B_PCH_SMBUS_BERR);
      ///
      /// Clear Status Registers
      ///
      SmbusIoWrite (R_PCH_SMBUS_HSTS, B_PCH_SMBUS_HSTS_ALL);
      SmbusIoWrite (R_PCH_SMBUS_AUXS, B_PCH_SMBUS_CRCE);
      ///
      /// If bus collision happens, stall some time, then try again
      /// Here we choose 10 milliseconds to avoid MTCP transfer.
      ///
      MicroSecondDelay (STALL_PERIOD);
      continue;
    }
    ///
    /// successfull completion
    /// Operation Specifics (post-execution)
    ///
    switch (Operation) {

      case EfiSmbusReadWord:
        ///
        /// The "break;" command is not present here to allow code execution
        /// do drop into the next case, which contains common code to this case.
        ///
      case EfiSmbusProcessCall:
        CallBuffer[1] = SmbusIoRead (R_PCH_SMBUS_HD1);
        CallBuffer[0] = SmbusIoRead (R_PCH_SMBUS_HD0);
        break;

      case EfiSmbusReadByte:
        if (*Length > 1) {
          for (Index = 0; Index < *Length; Index++) {
            ///
            /// Read the byte
            ///
            CallBuffer[Index] = SmbusIoRead (R_PCH_SMBUS_HBD);
            ///
            /// After receiving byte n-1 (1-base) of the message, the
            /// software will then set the LAST BYTE bit. The software
            /// will then clear the BYTE_DONE_STS bit.
            ///
            if (Index == ((*Length - 1) - 1)) {
              SmbusHctl = SmbusIoRead (R_PCH_SMBUS_HCTL) | (UINT8) B_PCH_SMBUS_LAST_BYTE;
              SmbusIoWrite (R_PCH_SMBUS_HCTL, SmbusHctl);
            } else if (Index == (*Length - 1)) {
              ///
              /// Clear the LAST BYTE bit after receiving byte n (1-base) of the message
              ///
              SmbusHctl = SmbusIoRead (R_PCH_SMBUS_HCTL) & (UINT8) ~B_PCH_SMBUS_LAST_BYTE;
              SmbusIoWrite (R_PCH_SMBUS_HCTL, SmbusHctl);
            }
            ///
            /// Clear the BYTE_DONE_STS bit
            ///
            SmbusIoWrite (R_PCH_SMBUS_HSTS, B_PCH_SMBUS_BYTE_DONE_STS);
            ///
            /// Check BYTE_DONE_STS bit to know if it has completed transmission
            /// of a byte. No need to check it for the last byte.
            ///
            if (Index < (*Length - 1)) {
              ///
              /// If somehow board operates at 10Khz, it will take 0.9 ms (9/10Khz) for another byte.
              /// Add 10 us delay for a loop of 100 that the total timeout is 1 ms to take care of
              /// the slowest case.
              ///
              for (Timeout = 0; Timeout < 100; Timeout++) {
                if ((SmbusIoRead (R_PCH_SMBUS_HSTS) & (UINT8) B_PCH_SMBUS_BYTE_DONE_STS) != 0) {
                  break;
                }
                ///
                /// Delay 10 us
                ///
                MicroSecondDelay (STALL_PERIOD);
              }

              if (Timeout >= 100) {
                Status = EFI_TIMEOUT;
                break;
              }
            }
          }
          break;
        }

      case EfiSmbusReceiveByte:
        CallBuffer[0] = SmbusIoRead (R_PCH_SMBUS_HD0);
        break;

      case EfiSmbusWriteBlock:
        SmbusIoWrite (R_PCH_SMBUS_HSTS, B_PCH_SMBUS_BYTE_DONE_STS);
        break;

      case EfiSmbusReadBlock:
        BufferTooSmall = FALSE;
        ///
        /// Find out how many bytes will be in the block
        ///
        BlockCount = SmbusIoRead (R_PCH_SMBUS_HD0);
        if (*Length < BlockCount) {
          BufferTooSmall = TRUE;
        } else {
          for (Index = 0; Index < BlockCount; Index++) {
            ///
            /// Read the byte
            ///
            CallBuffer[Index] = SmbusIoRead (R_PCH_SMBUS_HBD);
          }
        }

        *Length = BlockCount;
        if (BufferTooSmall) {
          Status = EFI_BUFFER_TOO_SMALL;
        }
        break;

      case EfiSmbusBWBRProcessCall:
        ///
        /// Find out how many bytes will be in the block
        ///
        BlockCount = SmbusIoRead (R_PCH_SMBUS_HD0);
        ///
        /// The read byte count cannot be zero.
        ///
        if (BlockCount < 1) {
          Status = EFI_BUFFER_TOO_SMALL;
          break;
        }
        ///
        /// The combined data payload (the write byte count + the read byte count)
        /// must not exceed 32 bytes
        ///
        if (((UINT8) (*Length) + BlockCount) > 32) {
          Status = EFI_DEVICE_ERROR;
          break;
        }

        for (Index = 0; Index < BlockCount; Index++) {
          ///
          /// Read the byte
          ///
          CallBuffer[Index] = SmbusIoRead (R_PCH_SMBUS_HBD);
        }

        *Length = BlockCount;
        break;

      default:
        break;
    };

    if ((StsReg & B_PCH_SMBUS_BERR) && (Status != EFI_BUFFER_TOO_SMALL)) {
      ///
      /// Clear the Bus Error for another try
      ///
      Status = EFI_DEVICE_ERROR;
      SmbusIoWrite (R_PCH_SMBUS_HSTS, B_PCH_SMBUS_BERR);
      ///
      /// If bus collision happens, stall some time, then try again
      /// Here we choose 10 milliseconds to avoid MTCP transfer.
      ///
      MicroSecondDelay (STALL_PERIOD);
      continue;
    } else {
      break;
    }
  }
  ///
  /// Clear Status Registers and exit
  ///
  SmbusIoWrite (R_PCH_SMBUS_HSTS, B_PCH_SMBUS_HSTS_ALL);
  SmbusIoWrite (R_PCH_SMBUS_AUXS, B_PCH_SMBUS_CRCE);
  SmbusIoWrite (R_PCH_SMBUS_AUXC, 0);
  return Status;
}

/**
  This function initializes the Smbus Registers.

**/
VOID
InitializeSmbusRegisters (
  VOID
  )
{
  UINTN SmbusRegBase;

  SmbusRegBase = MmPciBase (
                   DEFAULT_PCI_BUS_NUMBER_PCH,
                   PCI_DEVICE_NUMBER_PCH_SMBUS,
                   PCI_FUNCTION_NUMBER_PCH_SMBUS
                   );
  ///
  /// Enable the Smbus I/O Enable
  ///
  MmioOr8 (SmbusRegBase + PCI_COMMAND_OFFSET, (UINT8) EFI_PCI_COMMAND_IO_SPACE);

  ///
  /// Enable the Smbus host controller
  ///
  MmioAndThenOr8 (
    SmbusRegBase + R_PCH_SMBUS_HOSTC,
    (UINT8) (~(B_PCH_SMBUS_HOSTC_SMI_EN | B_PCH_SMBUS_HOSTC_I2C_EN)),
    B_PCH_SMBUS_HOSTC_HST_EN
    );

  SmbusIoWrite (R_PCH_SMBUS_HSTS, B_PCH_SMBUS_HSTS_ALL);
}