/** @file

  A PEIM providing synchronous SMI activations via the
  EFI_PEI_MM_CONTROL_PPI.

  We expect the PEI phase to have covered the following:
  - ensure that the underlying QEMU machine type be Q35
    (responsible: OvmfPkg/SmmAccess/SmmAccessPei.inf)
  - ensure that the ACPI PM IO space be configured
    (responsible: OvmfPkg/PlatformPei/PlatformPei.inf)

  Our own entry point is responsible for confirming the SMI feature and for
  configuring it.

  Copyright (C) 2013, 2015, Red Hat, Inc.<BR>
  Copyright (c) 2009 - 2010, Intel Corporation. All rights reserved.<BR>
  Copyright (c) Microsoft Corporation.

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

**/

#include <PiPei.h>
#include <IndustryStandard/Q35MchIch9.h>
#include <Library/BaseLib.h>
#include <Library/DebugLib.h>
#include <Library/IoLib.h>
#include <Library/PcdLib.h>
#include <Library/PciLib.h>
#include <Library/PeiServicesLib.h>
#include <Library/QemuFwCfgLib.h>
#include <Library/QemuFwCfgS3Lib.h>
#include <Ppi/MmControl.h>

#include "SmiFeatures.h"

//
// The absolute IO port address of the SMI Control and Enable Register. It is
// only used to carry information from the entry point function to the
// S3SaveState protocol installation callback, strictly before the runtime
// phase.
//
STATIC UINTN  mSmiEnable;

/**
  Invokes PPI activation from the PI PEI environment.

  @param  PeiServices           An indirect pointer to the PEI Services Table published by the PEI Foundation.
  @param  This                  The PEI_MM_CONTROL_PPI instance.
  @param  ArgumentBuffer        The value passed to the MMI handler. This value corresponds to the
                                SwMmiInputValue in the RegisterContext parameter for the Register()
                                function in the EFI_MM_SW_DISPATCH_PROTOCOL and in the Context parameter
                                in the call to the DispatchFunction
  @param  ArgumentBufferSize    The size of the data passed in ArgumentBuffer or NULL if ArgumentBuffer is NULL.
  @param  Periodic              An optional mechanism to periodically repeat activation.
  @param  ActivationInterval    An optional parameter to repeat at this period one
                                time or, if the Periodic Boolean is set, periodically.

  @retval EFI_SUCCESS           The MMI has been engendered.
  @retval EFI_DEVICE_ERROR      The timing is unsupported.
  @retval EFI_INVALID_PARAMETER The activation period is unsupported.
  @retval EFI_NOT_STARTED       The MM base service has not been initialized.

**/
STATIC
EFI_STATUS
EFIAPI
MmControlPeiTrigger (
  IN EFI_PEI_SERVICES        **PeiServices,
  IN EFI_PEI_MM_CONTROL_PPI  *This,
  IN OUT INT8                *ArgumentBuffer OPTIONAL,
  IN OUT UINTN               *ArgumentBufferSize OPTIONAL,
  IN BOOLEAN                 Periodic OPTIONAL,
  IN UINTN                   ActivationInterval OPTIONAL
  )
{
  UINT8  Command;
  UINT8  Data;

  //
  // No support for queued or periodic activation.
  //
  if (Periodic || (ActivationInterval > 0)) {
    return EFI_DEVICE_ERROR;
  }

  if ((ArgumentBufferSize != NULL) && (ArgumentBuffer == NULL)) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // The 0th byte as command, 1st as data... Right?
  //
  if ((NULL == ArgumentBuffer) || (*ArgumentBufferSize == 0)) {
    Command = 0;
    Data    = 0;
  } else if (*ArgumentBufferSize == 1) {
    Command = *ArgumentBuffer;
    Data    = 0;
  } else if (*ArgumentBufferSize == 2) {
    Command = ArgumentBuffer[0];
    Data    = ArgumentBuffer[1];
  } else {
    return EFI_INVALID_PARAMETER;
  }

  //
  // The so-called "Advanced Power Management Status Port Register" is in fact
  // a generic data passing register, between the caller and the SMI
  // dispatcher. The ICH9 spec calls it "scratchpad register" --  calling it
  // "status" elsewhere seems quite the misnomer. Status registers usually
  // report about hardware status, while this register is fully governed by
  // software.
  //
  // Write to the status register first, as this won't trigger the SMI just
  // yet. Then write to the control register.
  //
  IoWrite8 (ICH9_APM_STS, Data);
  IoWrite8 (ICH9_APM_CNT, Command);
  return EFI_SUCCESS;
}

/**
  Clears any system state that was created in response to the Trigger() call.

  @param  PeiServices           General purpose services available to every PEIM.
  @param  This                  The PEI_MM_CONTROL_PPI instance.
  @param  Periodic              Optional parameter to repeat at this period one
                                time or, if the Periodic Boolean is set, periodically.

  @retval EFI_SUCCESS           The MMI has been engendered.
  @retval EFI_DEVICE_ERROR      The source could not be cleared.
  @retval EFI_INVALID_PARAMETER The service did not support the Periodic input argument.

**/
STATIC
EFI_STATUS
EFIAPI
MmControlPeiClear (
  IN EFI_PEI_SERVICES        **PeiServices,
  IN EFI_PEI_MM_CONTROL_PPI  *This,
  IN BOOLEAN                 Periodic OPTIONAL
  )
{
  if (Periodic) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // The PI spec v1.4 explains that Clear() is only supposed to clear software
  // status; it is not in fact responsible for deasserting the SMI. It gives
  // two reasons for this: (a) many boards clear the SMI automatically when
  // entering SMM, (b) if Clear() actually deasserted the SMI, then it could
  // incorrectly suppress an SMI that was asynchronously asserted between the
  // last return of the SMI handler and the call made to Clear().
  //
  // In fact QEMU automatically deasserts CPU_INTERRUPT_SMI in:
  // - x86_cpu_exec_interrupt() [target-i386/seg_helper.c], and
  // - kvm_arch_pre_run() [target-i386/kvm.c].
  //
  // So, nothing to do here.
  //
  return EFI_SUCCESS;
}

STATIC EFI_PEI_MM_CONTROL_PPI  mMmControl =
{
  .Trigger = MmControlPeiTrigger,
  .Clear   = MmControlPeiClear
};

STATIC EFI_PEI_PPI_DESCRIPTOR  mSmmControlPpiList =
{
  (EFI_PEI_PPI_DESCRIPTOR_PPI | EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST),
  &gEfiPeiMmControlPpiGuid,
  &mMmControl
};

//
// Entry point of this driver.
//
EFI_STATUS
EFIAPI
MmControlPeiEntryPoint (
  IN       EFI_PEI_FILE_HANDLE  FileHandle,
  IN CONST EFI_PEI_SERVICES     **PeiServices
  )
{
  UINT32      PmBase;
  UINT32      SmiEnableVal;
  EFI_STATUS  Status;

  //
  // This module should only be included if SMRAM support is required.
  //
  ASSERT (FeaturePcdGet (PcdSmmSmramRequire));

  //
  // Calculate the absolute IO port address of the SMI Control and Enable
  // Register. (As noted at the top, the PEI phase has left us with a working
  // ACPI PM IO space.)
  //
  PmBase = PciRead32 (POWER_MGMT_REGISTER_Q35 (ICH9_PMBASE)) &
           ICH9_PMBASE_MASK;
  mSmiEnable = PmBase + ICH9_PMBASE_OFS_SMI_EN;

  //
  // If APMC_EN is pre-set in SMI_EN, that's QEMU's way to tell us that SMI
  // support is not available. (For example due to KVM lacking it.) Otherwise,
  // this bit is clear after each reset.
  //
  SmiEnableVal = IoRead32 (mSmiEnable);
  if ((SmiEnableVal & ICH9_SMI_EN_APMC_EN) != 0) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: this Q35 implementation lacks SMI\n",
      __FUNCTION__
      ));
    goto FatalError;
  }

  //
  // Otherwise, configure the board to inject an SMI when ICH9_APM_CNT is
  // written to. (See the Trigger() method above.)
  //
  SmiEnableVal |= ICH9_SMI_EN_APMC_EN | ICH9_SMI_EN_GBL_SMI_EN;
  IoWrite32 (mSmiEnable, SmiEnableVal);

  //
  // Prevent software from undoing the above (until platform reset).
  //
  PciOr16 (
    POWER_MGMT_REGISTER_Q35 (ICH9_GEN_PMCON_1),
    ICH9_GEN_PMCON_1_SMI_LOCK
    );

  //
  // If we can clear GBL_SMI_EN now, that means QEMU's SMI support is not
  // appropriate.
  //
  IoWrite32 (mSmiEnable, SmiEnableVal & ~(UINT32)ICH9_SMI_EN_GBL_SMI_EN);
  if (IoRead32 (mSmiEnable) != SmiEnableVal) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: failed to lock down GBL_SMI_EN\n",
      __FUNCTION__
      ));
    goto FatalError;
  }

  //
  // We have no pointers to convert to virtual addresses. The handle itself
  // doesn't matter, as protocol services are not accessible at runtime.
  //
  Status = (*PeiServices)->InstallPpi (PeiServices, &mSmmControlPpiList);
  if (EFI_ERROR (Status)) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: InstallMultipleProtocolInterfaces: %r\n",
      __FUNCTION__,
      Status
      ));
    goto FatalError;
  }

  return EFI_SUCCESS;

FatalError:
  //
  // We really don't want to continue in this case.
  //
  ASSERT (FALSE);
  CpuDeadLoop ();
  return EFI_UNSUPPORTED;
}
