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

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

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

#include <Protocol/BoardDesc.h>
#include <Protocol/DriverBinding.h>
#include <Protocol/Mdio.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 "MvMdioDxe.h"

STATIC
EFI_STATUS
MdioCheckParam (
 INTN PhyAddr,
 INTN RegOff
 )
{
  if (PhyAddr > MVEBU_PHY_ADDR_MASK) {
    DEBUG((DEBUG_ERROR, "Invalid PHY address %d\n", PhyAddr));
    return EFI_INVALID_PARAMETER;
  }

  if (RegOff > MVEBU_PHY_REG_MASK) {
    DEBUG((DEBUG_ERROR, "Invalid register offset %d\n", RegOff));
    return EFI_INVALID_PARAMETER;
  }
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
MdioWaitReady (
  UINTN MdioBase
  )
{
  UINT32 Timeout = MVEBU_SMI_TIMEOUT;
  UINT32 MdioReg;

  /* wait till the SMI is not busy */
  do {
    /* read smi register */
    MdioReg = MmioRead32(MdioBase);
    if (Timeout-- == 0) {
      DEBUG((DEBUG_ERROR, "SMI busy Timeout\n"));
      return EFI_TIMEOUT;
    }
  } while (MdioReg & MVEBU_SMI_BUSY);

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
MdioWaitValid (
  UINTN MdioBase
  )
{
  UINT32 Timeout = MVEBU_SMI_TIMEOUT;
  UINT32 MdioReg;

  /* wait till read value is ready */
  do {
    /* read smi register */
    MdioReg = MmioRead32 (MdioBase);
    if (Timeout-- == 0) {
      DEBUG((DEBUG_ERROR, "SMI read ready time-out\n"));
      return EFI_TIMEOUT;
    }
  } while (!(MdioReg & MVEBU_SMI_READ_VALID));

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
MdioOperation (
  IN CONST MARVELL_MDIO_PROTOCOL *This,
  IN UINT32 PhyAddr,
  IN UINT32 MdioIndex,
  IN UINT32 RegOff,
  IN BOOLEAN Write,
  IN OUT UINT32 *Data
  )
{
  UINTN MdioBase = This->BaseAddresses[MdioIndex];
  UINT32 MdioReg;
  EFI_STATUS Status;

  Status = MdioCheckParam (PhyAddr, RegOff);
  if (EFI_ERROR(Status)) {
    DEBUG((DEBUG_ERROR, "MdioDxe: wrong parameters\n"));
    return Status;
  }

  /* wait till the SMI is not busy */
  Status = MdioWaitReady (MdioBase);
  if (EFI_ERROR(Status)) {
    DEBUG((DEBUG_ERROR, "MdioDxe: MdioWaitReady error\n"));
    return Status;
  }

  /* fill the phy addr and reg offset and write opcode and data */
  MdioReg = (PhyAddr << MVEBU_SMI_DEV_ADDR_OFFS)
      | (RegOff << MVEBU_SMI_REG_ADDR_OFFS);
  if (Write) {
    MdioReg &= ~MVEBU_SMI_OPCODE_READ;
    MdioReg |= (*Data << MVEBU_SMI_DATA_OFFS);
  } else {
    MdioReg |= MVEBU_SMI_OPCODE_READ;
  }

  /* write the smi register */
  MdioRegWrite32 (MdioReg, MdioBase);

  /* make sure that the write transaction  is over */
  Status = Write ? MdioWaitReady (MdioBase) : MdioWaitValid (MdioBase);
  if (EFI_ERROR(Status)) {
    DEBUG((DEBUG_ERROR, "MdioDxe: MdioWaitReady error\n"));
    return Status;
  }

  if (!Write) {
    *Data = MmioRead32 (MdioBase) & MVEBU_SMI_DATA_MASK;
  }

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
MvMdioRead (
  IN CONST MARVELL_MDIO_PROTOCOL *This,
  IN UINT32 PhyAddr,
  IN UINT32 MdioIndex,
  IN UINT32 RegOff,
  IN UINT32 *Data
  )
{
  EFI_STATUS Status;

  Status = MdioOperation (
            This,
            PhyAddr,
            MdioIndex,
            RegOff,
            FALSE,
            Data
            );

  return Status;
}

EFI_STATUS
MvMdioWrite (
  IN CONST MARVELL_MDIO_PROTOCOL *This,
  IN UINT32 PhyAddr,
  IN UINT32 MdioIndex,
  IN UINT32 RegOff,
  IN UINT32 Data
  )
{
  return MdioOperation (
            This,
            PhyAddr,
            MdioIndex,
            RegOff,
            TRUE,
            &Data
            );
}

EFI_STATUS
EFIAPI
MvMdioDxeInitialise (
  IN EFI_HANDLE  ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  MARVELL_BOARD_DESC_PROTOCOL *BoardDescProtocol;
  MV_BOARD_MDIO_DESC *MdioBoardDesc;
  UINT8 Index;
  MARVELL_MDIO_PROTOCOL *Mdio;
  EFI_STATUS Status;
  EFI_HANDLE Handle = NULL;

  /* Obtain list of available controllers */
  Status = gBS->LocateProtocol (&gMarvellBoardDescProtocolGuid,
                  NULL,
                  (VOID **)&BoardDescProtocol);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = BoardDescProtocol->BoardDescMdioGet (BoardDescProtocol,
                                &MdioBoardDesc);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Mdio = AllocateZeroPool (sizeof (MARVELL_MDIO_PROTOCOL));
  if (Mdio == NULL) {
    DEBUG ((DEBUG_ERROR, "MdioDxe: Protocol allocation failed\n"));
    return EFI_OUT_OF_RESOURCES;
  }

  Mdio->BaseAddresses = AllocateZeroPool (MdioBoardDesc->MdioDevCount *
                                          sizeof (UINTN));
  if (Mdio->BaseAddresses == NULL) {
    DEBUG ((DEBUG_ERROR, "MdioDxe: Protocol allocation failed\n"));
    return EFI_OUT_OF_RESOURCES;
  }

  /* Obtain base addresses of all possible controllers */
  for (Index = 0; Index < MdioBoardDesc->MdioDevCount; Index++) {
    Mdio->BaseAddresses[Index] = MdioBoardDesc[Index].SoC->MdioBaseAddress;
  }

  Mdio->ControllerCount = MdioBoardDesc->MdioDevCount;
  Mdio->Read = MvMdioRead;
  Mdio->Write = MvMdioWrite;

  Status = gBS->InstallMultipleProtocolInterfaces (
                  &Handle,
                  &gMarvellMdioProtocolGuid, Mdio,
                  NULL
                  );

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

  BoardDescProtocol->BoardDescFree (MdioBoardDesc);

  return EFI_SUCCESS;
}
