/** @file
  OVMF support for QEMU system firmware flash device

  Copyright (c) 2009 - 2013, Intel Corporation. All rights reserved.<BR>

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

**/

#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/PcdLib.h>

#include "QemuFlash.h"

#define WRITE_BYTE_CMD           0x10
#define BLOCK_ERASE_CMD          0x20
#define CLEAR_STATUS_CMD         0x50
#define READ_STATUS_CMD          0x70
#define READ_DEVID_CMD           0x90
#define BLOCK_ERASE_CONFIRM_CMD  0xd0
#define READ_ARRAY_CMD           0xff

#define CLEARED_ARRAY_STATUS  0x00


UINT8 *mFlashBase;

STATIC UINTN       mFdBlockSize = 0;
STATIC UINTN       mFdBlockCount = 0;

STATIC
volatile UINT8*
QemuFlashPtr (
  IN        EFI_LBA                             Lba,
  IN        UINTN                               Offset
  )
{
  return mFlashBase + ((UINTN)Lba * mFdBlockSize) + Offset;
}


/**
  Determines if the QEMU flash memory device is present.

  @retval FALSE   The QEMU flash device is not present.
  @retval TRUE    The QEMU flash device is present.

**/
STATIC
BOOLEAN
QemuFlashDetected (
  VOID
  )
{
  BOOLEAN  FlashDetected;
  volatile UINT8  *Ptr;

  UINTN Offset;
  UINT8 OriginalUint8;
  UINT8 ProbeUint8;

  FlashDetected = FALSE;
  Ptr = QemuFlashPtr (0, 0);

  for (Offset = 0; Offset < mFdBlockSize; Offset++) {
    Ptr = QemuFlashPtr (0, Offset);
    ProbeUint8 = *Ptr;
    if (ProbeUint8 != CLEAR_STATUS_CMD &&
        ProbeUint8 != READ_STATUS_CMD &&
        ProbeUint8 != CLEARED_ARRAY_STATUS) {
      break;
    }
  }

  if (Offset >= mFdBlockSize) {
    DEBUG ((EFI_D_INFO, "QEMU Flash: Failed to find probe location\n"));
    return FALSE;
  }

  DEBUG ((EFI_D_INFO, "QEMU Flash: Attempting flash detection at %p\n", Ptr));

  OriginalUint8 = *Ptr;
  *Ptr = CLEAR_STATUS_CMD;
  ProbeUint8 = *Ptr;
  if (OriginalUint8 != CLEAR_STATUS_CMD &&
      ProbeUint8 == CLEAR_STATUS_CMD) {
    DEBUG ((EFI_D_INFO, "QemuFlashDetected => FD behaves as RAM\n"));
    *Ptr = OriginalUint8;
  } else {
    *Ptr = READ_STATUS_CMD;
    ProbeUint8 = *Ptr;
    if (ProbeUint8 == OriginalUint8) {
      DEBUG ((EFI_D_INFO, "QemuFlashDetected => FD behaves as ROM\n"));
    } else if (ProbeUint8 == READ_STATUS_CMD) {
      DEBUG ((EFI_D_INFO, "QemuFlashDetected => FD behaves as RAM\n"));
      *Ptr = OriginalUint8;
    } else if (ProbeUint8 == CLEARED_ARRAY_STATUS) {
      DEBUG ((EFI_D_INFO, "QemuFlashDetected => FD behaves as FLASH\n"));
      FlashDetected = TRUE;
      *Ptr = READ_ARRAY_CMD;
    }
  }

  DEBUG ((EFI_D_INFO, "QemuFlashDetected => %a\n",
                      FlashDetected ? "Yes" : "No"));
  return FlashDetected;
}


/**
  Read from QEMU Flash

  @param[in] Lba      The starting logical block index to read from.
  @param[in] Offset   Offset into the block at which to begin reading.
  @param[in] NumBytes On input, indicates the requested read size. On
                      output, indicates the actual number of bytes read
  @param[in] Buffer   Pointer to the buffer to read into.

**/
EFI_STATUS
QemuFlashRead (
  IN        EFI_LBA                              Lba,
  IN        UINTN                                Offset,
  IN        UINTN                                *NumBytes,
  IN        UINT8                                *Buffer
  )
{
  UINT8  *Ptr;

  //
  // Only write to the first 64k. We don't bother saving the FTW Spare
  // block into the flash memory.
  //
  if (Lba >= mFdBlockCount) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Get flash address
  //
  Ptr = (UINT8*) QemuFlashPtr (Lba, Offset);

  CopyMem (Buffer, Ptr, *NumBytes);

  return EFI_SUCCESS;
}


/**
  Write to QEMU Flash

  @param[in] Lba      The starting logical block index to write to.
  @param[in] Offset   Offset into the block at which to begin writing.
  @param[in] NumBytes On input, indicates the requested write size. On
                      output, indicates the actual number of bytes written
  @param[in] Buffer   Pointer to the data to write.

**/
EFI_STATUS
QemuFlashWrite (
  IN        EFI_LBA                             Lba,
  IN        UINTN                               Offset,
  IN        UINTN                               *NumBytes,
  IN        UINT8                               *Buffer
  )
{
  volatile UINT8  *Ptr;
  UINTN           Loop;

  //
  // Only write to the first 64k. We don't bother saving the FTW Spare
  // block into the flash memory.
  //
  if (Lba >= mFdBlockCount) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Program flash
  //
  Ptr = QemuFlashPtr (Lba, Offset);
  for (Loop = 0; Loop < *NumBytes; Loop++) {
    *Ptr = WRITE_BYTE_CMD;
    *Ptr = Buffer[Loop];
    Ptr++;
  }

  //
  // Restore flash to read mode
  //
  if (*NumBytes > 0) {
    *(Ptr - 1) = READ_ARRAY_CMD;
  }

  return EFI_SUCCESS;
}


/**
  Erase a QEMU Flash block

  @param Lba    The logical block index to erase.

**/
EFI_STATUS
QemuFlashEraseBlock (
  IN   EFI_LBA      Lba
  )
{
  volatile UINT8  *Ptr;

  if (Lba >= mFdBlockCount) {
    return EFI_INVALID_PARAMETER;
  }

  Ptr = QemuFlashPtr (Lba, 0);
  *Ptr = BLOCK_ERASE_CMD;
  *Ptr = BLOCK_ERASE_CONFIRM_CMD;
  return EFI_SUCCESS;
}


/**
  Initializes QEMU flash memory support

  @retval EFI_WRITE_PROTECTED   The QEMU flash device is not present.
  @retval EFI_SUCCESS           The QEMU flash device is supported.

**/
EFI_STATUS
QemuFlashInitialize (
  VOID
  )
{
  mFlashBase = (UINT8*)(UINTN) PcdGet32 (PcdOvmfFdBaseAddress);
  mFdBlockSize = PcdGet32 (PcdOvmfFirmwareBlockSize);
  ASSERT(PcdGet32 (PcdOvmfFirmwareFdSize) % mFdBlockSize == 0);
  mFdBlockCount = PcdGet32 (PcdOvmfFirmwareFdSize) / mFdBlockSize;

  //
  // execute module specific hooks before probing the flash
  //
  QemuFlashBeforeProbe (
    (EFI_PHYSICAL_ADDRESS)(UINTN) mFlashBase,
    mFdBlockSize,
    mFdBlockCount
    );

  if (!QemuFlashDetected ()) {
    ASSERT (!FeaturePcdGet (PcdSmmSmramRequire));
    return EFI_WRITE_PROTECTED;
  }

  return EFI_SUCCESS;
}

