/** @file
*
*  Copyright (c) 2011-2015, ARM Limited. All rights reserved.
*
*  Copyright 2019-2020 NXP
*
*  SPDX-License-Identifier: BSD-2-Clause-Patent
*
**/

#include <PiPei.h>

#include <Library/ArmMmuLib.h>
#include <Library/ArmPlatformLib.h>
#include <Library/ArmSmcLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/HobLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PcdLib.h>

#include "MemoryInitPeiLib.h"


VOID
BuildMemoryTypeInformationHob (
  VOID
  );

VOID
InitMmu (
  IN ARM_MEMORY_REGION_DESCRIPTOR  *MemoryTable
  )
{

  VOID                          *TranslationTableBase;
  UINTN                         TranslationTableSize;
  RETURN_STATUS                 Status;

  //Note: Because we called PeiServicesInstallPeiMemory() before
  //to call InitMmu() the MMU Page Table resides in DRAM
  //(even at the top of DRAM as it is the first permanent memory allocation)
  Status = ArmConfigureMmu (MemoryTable, &TranslationTableBase, &TranslationTableSize);
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Error: Failed to enable MMU\n"));
  }
}

STATIC
UINTN
GetDramSize (
  IN VOID
  )
{
  ARM_SMC_ARGS  ArmSmcArgs;

  ArmSmcArgs.Arg0 = SMC_DRAM_BANK_INFO;
  ArmSmcArgs.Arg1 = SMC_DRAM_TOTAL_DRAM_ARG1;

  ArmCallSmc (&ArmSmcArgs);

  if (ArmSmcArgs.Arg0 == SMC_OK) {
    return ArmSmcArgs.Arg1;
  }

  // return 0 means no DDR found.
  return 0;
}

STATIC
EFI_STATUS
GetDramRegionsInfo (
  OUT DRAM_REGION_INFO *DramRegions,
  IN  UINT32           NumRegions
  )
{
  ARM_SMC_ARGS  ArmSmcArgs;
  UINT32        Index;
  UINTN         RemainingDramSize;
  UINTN         BaseAddress;
  UINTN         Size;

  RemainingDramSize = GetDramSize ();
  DEBUG ((DEBUG_INFO, "DRAM Total Size 0x%lx \n", RemainingDramSize));

  // Ensure Total Dram Size is valid
  ASSERT (RemainingDramSize != 0);

  for (Index = 0; Index < NumRegions; Index++) {
    ArmSmcArgs.Arg0 = SMC_DRAM_BANK_INFO;
    ArmSmcArgs.Arg1 = Index;

    ArmCallSmc (&ArmSmcArgs);

    if (ArmSmcArgs.Arg0 == SMC_OK) {
      BaseAddress = ArmSmcArgs.Arg1;
      Size = ArmSmcArgs.Arg2;
      ASSERT (BaseAddress && Size);

      DramRegions[Index].BaseAddress = BaseAddress;
      DramRegions[Index].Size = Size;
      RemainingDramSize -= Size;

      DEBUG ((DEBUG_INFO, "DRAM Region[%d]: start 0x%lx, size 0x%lx\n",
              Index, BaseAddress, Size));

      if (RemainingDramSize == 0) {
        return EFI_SUCCESS;
      }
    } else {
      break;
    }
  }

  DEBUG ((DEBUG_ERROR, "RemainingDramSize = %u !! Ensure that all DDR regions "
          "have been accounted for\n", RemainingDramSize));

  return EFI_BUFFER_TOO_SMALL;
}

/**
  Get the installed RAM information.
  Initialize Memory HOBs (Resource Descriptor HOBs)
  Set the PcdSystemMemoryBase and PcdSystemMemorySize.

  @return  EFI_SUCCESS  Successfuly retrieved the system memory information
**/
EFI_STATUS
EFIAPI
MemoryInitPeiLibConstructor (
  VOID
  )
{
  INT32                         Index;
  UINTN                         BaseAddress;
  UINTN                         Size;
  UINTN                         Top;
  DRAM_REGION_INFO              DramRegions[MAX_DRAM_REGIONS];
  EFI_RESOURCE_ATTRIBUTE_TYPE   ResourceAttributes;
  UINTN                         FdBase;
  UINTN                         FdTop;
  BOOLEAN                       FoundSystemMem;

  ResourceAttributes = (
    EFI_RESOURCE_ATTRIBUTE_PRESENT |
    EFI_RESOURCE_ATTRIBUTE_INITIALIZED |
    EFI_RESOURCE_ATTRIBUTE_WRITE_COMBINEABLE |
    EFI_RESOURCE_ATTRIBUTE_WRITE_THROUGH_CACHEABLE |
    EFI_RESOURCE_ATTRIBUTE_WRITE_BACK_CACHEABLE |
    EFI_RESOURCE_ATTRIBUTE_TESTED
  );

  FoundSystemMem = FALSE;
  ZeroMem (DramRegions, sizeof (DramRegions));

  (VOID)GetDramRegionsInfo (DramRegions, ARRAY_SIZE (DramRegions));

  FdBase = (UINTN)PcdGet64 (PcdFdBaseAddress);
  FdTop = FdBase + (UINTN)PcdGet32 (PcdFdSize);

  // Declare memory regions to system
  // The DRAM region info is sorted based on the RAM address is SOC memory map.
  // i.e. DramRegions[0] is at lower address, as compared to DramRegions[1].
  // The goal to start from last region is to find the topmost RAM region that
  // can contain UEFI DXE region i.e. PcdSystemMemoryUefiRegionSize.
  // If UEFI were to allocate any reserved or runtime region, it would be
  // allocated from topmost RAM region.
  // This ensures that maximum amount of lower RAM (32 bit addresses) are left
  // for OS to allocate to devices that can only work with 32bit physical
  // addresses. E.g. legacy devices that need to DMA to 32bit addresses.
  for (Index = MAX_DRAM_REGIONS - 1; Index >= 0; Index--) {
    if (DramRegions[Index].Size == 0) {
      continue;
    }

    BaseAddress = DramRegions[Index].BaseAddress;
    Top = DramRegions[Index].BaseAddress + DramRegions[Index].Size;

    // EDK2 does not have the concept of boot firmware copied into DRAM.
    // To avoid the DXE core to overwrite this area we must create a memory
    // allocation HOB for the region, but this only works if we split off the
    // underlying resource descriptor as well.
    if (FdBase >= BaseAddress && FdTop <= Top) {
      // Update Size
      Size = FdBase - BaseAddress;
      if (Size) {
        BuildResourceDescriptorHob (
          EFI_RESOURCE_SYSTEM_MEMORY,
          ResourceAttributes,
          BaseAddress,
          Size
        );
      }
      // create the System Memory HOB for the firmware
      BuildResourceDescriptorHob (
        EFI_RESOURCE_SYSTEM_MEMORY,
        ResourceAttributes,
        FdBase,
        PcdGet32 (PcdFdSize)
      );
      // Create the System Memory HOB for the remaining region (top of the FD)s
      Size = Top - FdTop;
      if (Size) {
        BuildResourceDescriptorHob (
          EFI_RESOURCE_SYSTEM_MEMORY,
          ResourceAttributes,
          FdTop,
          Size
        );
      };
      // Mark the memory covering the Firmware Device as boot services data
      BuildMemoryAllocationHob (PcdGet64 (PcdFdBaseAddress),
                                PcdGet32 (PcdFdSize),
                                EfiBootServicesData);
    } else {
      BuildResourceDescriptorHob (
          EFI_RESOURCE_SYSTEM_MEMORY,
          ResourceAttributes,
          DramRegions[Index].BaseAddress,
          DramRegions[Index].Size
      );
    }

    if (FoundSystemMem == TRUE) {
      continue;
    }

    Size = DramRegions[Index].Size;

    if (FdBase >= BaseAddress && FdTop <= Top) {
      Size -= (UINTN)PcdGet32 (PcdFdSize);
    }

    if (Size >= FixedPcdGet32 (PcdSystemMemoryUefiRegionSize)) {
      FoundSystemMem = TRUE;
      PcdSet64S (PcdSystemMemoryBase, BaseAddress);
      PcdSet64S (PcdSystemMemorySize, Size);
    }
  }

  ASSERT (FoundSystemMem == TRUE);

  return EFI_SUCCESS;
}

/**
  Initialize MMU

  @param[in] UefiMemoryBase  Base address of region used by UEFI in
                             permanent memory
  @param[in] UefiMemorySize  Size of the region used by UEFI in permanent memory

  @return  EFI_SUCCESS  Successfuly Initialize MMU
**/
EFI_STATUS
EFIAPI
MemoryPeim (
  IN EFI_PHYSICAL_ADDRESS               UefiMemoryBase,
  IN UINT64                             UefiMemorySize
  )
{
  ARM_MEMORY_REGION_DESCRIPTOR *MemoryTable;

  // Get Virtual Memory Map from the Platform Library
  ArmPlatformGetVirtualMemoryMap (&MemoryTable);

  // Initialize Mmu
  InitMmu (MemoryTable);

  if (FeaturePcdGet (PcdPrePiProduceMemoryTypeInformationHob)) {
    // Optional feature that helps prevent EFI memory map fragmentation.
    BuildMemoryTypeInformationHob ();
  }

  return EFI_SUCCESS;
}
