/********************************************************************************
Copyright (C) 2016 Marvell International Ltd.

SPDX-License-Identifier: BSD-2-Clause-Patent

*******************************************************************************/

#include <Protocol/DriverBinding.h>
#include <Protocol/Mdio.h>
#include <Protocol/MvPhy.h>

#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/IoLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PcdLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>

#include "MvPhyDxe.h"

#define TIMEOUT   500

STATIC MARVELL_MDIO_PROTOCOL *Mdio;

//
// Table with available Mdio controllers
//
STATIC UINT8 * CONST MdioDeviceTable = PcdGetPtr (PcdMdioControllersEnabled);
//
// Table with PHY to Mdio controller mappings
//
STATIC UINT8 * CONST Phy2MdioController = PcdGetPtr (PcdPhy2MdioController);
//
// Table with PHYs' SMI addresses
//
STATIC UINT8 * CONST PhySmiAddresses = PcdGetPtr (PcdPhySmiAddresses);

STATIC MV_PHY_DEVICE MvPhyDevices[] = {
  { MV_PHY_DEVICE_1512, MvPhyInit1512 },
  { MV_PHY_DEVICE_1112, MvPhyInit1112 },
  { 0, NULL }
};

EFI_STATUS
MvPhyStatus (
  IN CONST MARVELL_PHY_PROTOCOL *This,
  IN PHY_DEVICE  *PhyDev
  );

EFI_STATUS
MvPhyReset (
  IN PHY_DEVICE *PhyDev
  )
{
  UINT32 Reg = 0;
  INTN timeout = TIMEOUT;

  Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MII_BMCR, &Reg);
  Reg |= BMCR_RESET;
  Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MII_BMCR, Reg);

  while ((Reg & BMCR_RESET) && timeout--) {
    Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MII_BMCR, &Reg);
    gBS->Stall(1000);
  }

  if (Reg & BMCR_RESET) {
    DEBUG((DEBUG_ERROR, "PHY reset timed out\n"));
    return EFI_TIMEOUT;
  }

  return EFI_SUCCESS;
}

/* Marvell 88E1111S */
EFI_STATUS
MvPhyM88e1111sConfig (
  IN PHY_DEVICE *PhyDev
  )
{
  UINT32 Reg;

  if ((PhyDev->Connection == PHY_CONNECTION_RGMII) ||
      (PhyDev->Connection == PHY_CONNECTION_RGMII_ID) ||
      (PhyDev->Connection == PHY_CONNECTION_RGMII_RXID) ||
      (PhyDev->Connection == PHY_CONNECTION_RGMII_TXID)) {
    Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_CR, &Reg);

    if ((PhyDev->Connection == PHY_CONNECTION_RGMII) ||
      (PhyDev->Connection == PHY_CONNECTION_RGMII_ID)) {
      Reg |= (MIIM_88E1111_RX_DELAY | MIIM_88E1111_TX_DELAY);
    } else if (PhyDev->Connection == PHY_CONNECTION_RGMII_RXID) {
      Reg &= ~MIIM_88E1111_TX_DELAY;
      Reg |= MIIM_88E1111_RX_DELAY;
    } else if (PhyDev->Connection == PHY_CONNECTION_RGMII_TXID) {
      Reg &= ~MIIM_88E1111_RX_DELAY;
      Reg |= MIIM_88E1111_TX_DELAY;
    }

    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_CR, Reg);

    Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_SR, &Reg);

    Reg &= ~(MIIM_88E1111_HWCFG_MODE_MASK);

    if (Reg & MIIM_88E1111_HWCFG_FIBER_COPPER_RES)
      Reg |= MIIM_88E1111_HWCFG_MODE_FIBER_RGMII;
    else
      Reg |= MIIM_88E1111_HWCFG_MODE_COPPER_RGMII;

    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_SR, Reg);
  }

  if (PhyDev->Connection == PHY_CONNECTION_SGMII) {
    Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_SR, &Reg);

    Reg &= ~(MIIM_88E1111_HWCFG_MODE_MASK);
    Reg |= MIIM_88E1111_HWCFG_MODE_SGMII_NO_CLK;
    Reg |= MIIM_88E1111_HWCFG_FIBER_COPPER_AUTO;

    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_SR, Reg);
  }

  if (PhyDev->Connection == PHY_CONNECTION_RTBI) {
    Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_CR, &Reg);
    Reg |= (MIIM_88E1111_RX_DELAY | MIIM_88E1111_TX_DELAY);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_CR, Reg);

    Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_SR, &Reg);
    Reg &= ~(MIIM_88E1111_HWCFG_MODE_MASK |
      MIIM_88E1111_HWCFG_FIBER_COPPER_RES);
    Reg |= 0x7 | MIIM_88E1111_HWCFG_FIBER_COPPER_AUTO;
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_SR, Reg);

    /* Soft reset */
    MvPhyReset (PhyDev);

    Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_SR, &Reg);
    Reg &= ~(MIIM_88E1111_HWCFG_MODE_MASK |
      MIIM_88E1111_HWCFG_FIBER_COPPER_RES);
    Reg |= MIIM_88E1111_HWCFG_MODE_COPPER_RTBI |
      MIIM_88E1111_HWCFG_FIBER_COPPER_AUTO;
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1111_PHY_EXT_SR, Reg);
  }

  Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MII_BMCR, &Reg);
  Reg |= (BMCR_ANENABLE | BMCR_ANRESTART);
  Reg &= ~BMCR_ISOLATE;
  Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MII_BMCR, Reg);

  /* Soft reset */
  MvPhyReset (PhyDev);

  return EFI_SUCCESS;
}

EFI_STATUS
MvPhyParseStatus (
  IN PHY_DEVICE *PhyDev
  )
{
  UINT32 Data;
  UINT32 Speed;

  Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1xxx_PHY_STATUS, &Data);

  if ((Data & MIIM_88E1xxx_PHYSTAT_LINK) &&
    !(Data & MIIM_88E1xxx_PHYSTAT_SPDDONE)) {
    INTN i = 0;

    DEBUG((DEBUG_ERROR,"MvPhyDxe: Waiting for PHY realtime link"));
    while (!(Data & MIIM_88E1xxx_PHYSTAT_SPDDONE)) {
      if (i > PHY_AUTONEGOTIATE_TIMEOUT) {
        DEBUG((DEBUG_ERROR," TIMEOUT !\n"));
        PhyDev->LinkUp = FALSE;
        break;
      }

      if ((i++ % 1000) == 0)
        DEBUG((DEBUG_ERROR, "."));
      gBS->Stall(1000);
      Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MIIM_88E1xxx_PHY_STATUS, &Data);
    }
    DEBUG((DEBUG_ERROR," done\n"));
    gBS->Stall(500000);
  } else {
    if (Data & MIIM_88E1xxx_PHYSTAT_LINK) {
      DEBUG((DEBUG_ERROR, "MvPhyDxe: link up, "));
      PhyDev->LinkUp = TRUE;
    } else {
      DEBUG((DEBUG_ERROR, "MvPhyDxe: link down, "));
      PhyDev->LinkUp = FALSE;
    }
  }

  if (Data & MIIM_88E1xxx_PHYSTAT_DUPLEX) {
    DEBUG((DEBUG_ERROR, "full duplex, "));
    PhyDev->FullDuplex = TRUE;
  } else {
    DEBUG((DEBUG_ERROR, "half duplex, "));
    PhyDev->FullDuplex = FALSE;
  }

  Speed = Data & MIIM_88E1xxx_PHYSTAT_SPEED;

  switch (Speed) {
  case MIIM_88E1xxx_PHYSTAT_GBIT:
    DEBUG((DEBUG_ERROR, "speed 1000\n"));
    PhyDev->Speed = SPEED_1000;
    break;
  case MIIM_88E1xxx_PHYSTAT_100:
    DEBUG((DEBUG_ERROR, "speed 100\n"));
    PhyDev->Speed = SPEED_100;
    break;
  default:
    DEBUG((DEBUG_ERROR, "speed 10\n"));
    PhyDev->Speed = SPEED_10;
    break;
  }

  return EFI_SUCCESS;
}

/**
  Configure PHY device autonegotiation.

  @param[in out]   *PhyDevice      A pointer to configured PHY device structure.

**/
STATIC
EFI_STATUS
MvPhyConfigureAutonegotiation (
  IN OUT PHY_DEVICE *PhyDevice
  )
{
  UINT32 Data;
  INTN Index;

  /* Read BMSR register in order to check autoneg capabilities and status. */
  Mdio->Read (Mdio, PhyDevice->Addr, PhyDevice->MdioIndex, MII_BMSR, &Data);

  if ((Data & BMSR_ANEGCAPABLE) && !(Data & BMSR_ANEGCOMPLETE)) {

    DEBUG ((DEBUG_INFO,
      "%a: Waiting for PHY auto negotiation...",
      __FUNCTION__));

    /* Wait for autonegotiation to complete and read media status */
    for (Index = 0; !(Data & BMSR_ANEGCOMPLETE); Index++) {
      if (Index > PHY_AUTONEGOTIATE_TIMEOUT) {
        DEBUG ((DEBUG_ERROR, "%a: Timeout\n", __FUNCTION__));
        PhyDevice->LinkUp = FALSE;
        return EFI_TIMEOUT;
      }
      gBS->Stall (1000);  /* 1 ms */
      Mdio->Read (Mdio, PhyDevice->Addr, PhyDevice->MdioIndex, MII_BMSR, &Data);
    }

    PhyDevice->LinkUp = TRUE;
    DEBUG ((DEBUG_INFO, "%a: link up\n", __FUNCTION__));
  } else {
    Mdio->Read (Mdio, PhyDevice->Addr, PhyDevice->MdioIndex, MII_BMSR, &Data);

    if (Data & BMSR_LSTATUS) {
      PhyDevice->LinkUp = TRUE;
      DEBUG ((DEBUG_INFO, "%a: link up\n", __FUNCTION__));
    } else {
      PhyDevice->LinkUp = FALSE;
      DEBUG ((DEBUG_INFO, "%a: link down\n", __FUNCTION__));
    }
  }

  return EFI_SUCCESS;
}

STATIC
VOID
MvPhy1512WriteBits (
  IN PHY_DEVICE *PhyDev,
  IN UINT8 RegNum,
  IN UINT16 Offset,
  IN UINT16 Len,
  IN UINT16 Data)
{
  UINT32 Reg, Mask;

  if ((Len + Offset) >= 16)
    Mask = 0 - (1 << Offset);
  else
    Mask = (1 << (Len + Offset)) - (1 << Offset);

  Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, RegNum, &Reg);

  Reg &= ~Mask;
  Reg |= Data << Offset;

  Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, RegNum, Reg);
}

STATIC
EFI_STATUS
MvPhyInit1512 (
    IN CONST MARVELL_PHY_PROTOCOL *Snp,
    IN OUT PHY_DEVICE *PhyDev
    )
{
  EFI_STATUS Status;

  if (PhyDev->Connection == PHY_CONNECTION_SGMII) {
    /* Select page 0xff and update configuration registers according to
     * Marvell Release Notes - Alaska 88E1510/88E1518/88E1512 Rev A0,
     * Errata Section 3.1 - needed in SGMII mode.
     */
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 22, 0x00ff);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 17, 0x214B);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 16, 0x2144);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 17, 0x0C28);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 16, 0x2146);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 17, 0xB233);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 16, 0x214D);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 17, 0xCC0C);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 16, 0x2159);

    /* Reset page selection and select page 0x12 */
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 22, 0x0000);
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 22, 0x0012);

    /* Write HWCFG_MODE = SGMII to Copper */
    MvPhy1512WriteBits(PhyDev, 20, 0, 3, 1);

    /* Phy reset - necessary after changing mode */
    MvPhy1512WriteBits(PhyDev, 20, 15, 1, 1);

    /* Reset page selection */
    Mdio->Write (Mdio, PhyDev->Addr, PhyDev->MdioIndex, 22, 0x0000);
    gBS->Stall(100);
  }

  MvPhyM88e1111sConfig (PhyDev);

  /* autonegotiation on startup is not always required */
  if (!PcdGetBool (PcdPhyStartupAutoneg))
    return EFI_SUCCESS;

  Status = MvPhyConfigureAutonegotiation (PhyDev);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  MvPhyParseStatus (PhyDev);

  return EFI_SUCCESS;
}

/**
  Initialize Marvell 88E1112 PHY.

  @param[in]      MvPhyProtocol   Marvell PHY protocol instance.
  @param[in out] *PhyDevice       PHY device structure.

**/
STATIC
EFI_STATUS
MvPhyInit1112 (
  IN CONST MARVELL_PHY_PROTOCOL  *MvPhyProtocol,
  IN OUT PHY_DEVICE              *PhyDevice
  )
{
  EFI_STATUS Status;

  MvPhyM88e1111sConfig (PhyDevice);

  if (PcdGetBool (PcdPhyStartupAutoneg)) {
    Status = MvPhyConfigureAutonegotiation (PhyDevice);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    MvPhyParseStatus (PhyDevice);
  }

  return EFI_SUCCESS;
}

EFI_STATUS
MvPhyInit (
  IN CONST MARVELL_PHY_PROTOCOL *Snp,
  IN UINT32 PhyIndex,
  IN PHY_CONNECTION PhyConnection,
  IN OUT PHY_DEVICE **OutPhyDev
  )
{
  EFI_STATUS Status;
  PHY_DEVICE *PhyDev;
  UINT8 *DeviceIds;
  UINT8 MdioIndex;
  UINT8 PhyId;

  Status = gBS->LocateProtocol (
      &gMarvellMdioProtocolGuid,
      NULL,
      (VOID **) &Mdio
      );
  if (EFI_ERROR(Status))
    return Status;

  MdioIndex = Phy2MdioController[PhyIndex];

  /* Verify correctness of PHY <-> MDIO assignment */
  if ((MdioDeviceTable[MdioIndex] == 0) ||
      (MdioIndex >= Mdio->ControllerCount)) {
    DEBUG ((DEBUG_ERROR, "MvPhyDxe: Incorrect Mdio controller assignment for PHY#%d", PhyIndex));
    return EFI_INVALID_PARAMETER;
  }

  DeviceIds = PcdGetPtr (PcdPhyDeviceIds);
  PhyId = DeviceIds[PhyIndex];
  if (PhyId >= MV_PHY_DEVICE_ID_MAX) {
    DEBUG ((DEBUG_ERROR,
      "%a, Incorrect PHY ID (0x%x) for PHY#%d\n",
      __FUNCTION__,
      PhyId,
      PhyIndex));
    return EFI_INVALID_PARAMETER;
  }

  /* perform setup common for all PHYs */
  PhyDev = AllocateZeroPool (sizeof (PHY_DEVICE));
  PhyDev->Addr = PhySmiAddresses[PhyIndex];
  PhyDev->Connection = PhyConnection;
  PhyDev->MdioIndex = MdioIndex;
  DEBUG ((DEBUG_INFO,
    "MvPhyDxe: MdioIndex is %d, PhyAddr is %d, connection %d\n",
    PhyDev->MdioIndex,
    PhyDev->Addr,
    PhyConnection));
  *OutPhyDev = PhyDev;

  return MvPhyDevices[PhyId].DevInit (Snp, PhyDev);
}

EFI_STATUS
MvPhyStatus (
  IN CONST MARVELL_PHY_PROTOCOL *This,
  IN PHY_DEVICE  *PhyDev
  )
{
  UINT32 Data;

  Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MII_BMSR, &Data);
  Mdio->Read (Mdio, PhyDev->Addr, PhyDev->MdioIndex, MII_BMSR, &Data);

  if ((Data & BMSR_LSTATUS) == 0) {
    PhyDev->LinkUp = FALSE;
  } else {
    PhyDev->LinkUp = TRUE;
  }

  return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
MvPhyDxeInitialise (
  IN EFI_HANDLE  ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  MARVELL_PHY_PROTOCOL *Phy;
  EFI_STATUS Status;
  EFI_HANDLE Handle = NULL;

  Phy = AllocateZeroPool (sizeof (MARVELL_PHY_PROTOCOL));
  Phy->Status = MvPhyStatus;
  Phy->Init = MvPhyInit;

  Status = gBS->InstallMultipleProtocolInterfaces (
                  &Handle,
                  &gMarvellPhyProtocolGuid, Phy,
                  NULL
                  );

  if (EFI_ERROR(Status)) {
    DEBUG((DEBUG_ERROR, "Failed to install interfaces\n"));
    return Status;
  }
  DEBUG((DEBUG_ERROR, "Succesfully installed protocol interfaces\n"));

  return EFI_SUCCESS;
}
