/***************************************************************************//**
 * @file
 * @brief Energy Modes management driver.
 * @author Energy Micro AS
 * @version 3.20.2
 * @details
 * This is a energy modes management module consisting of sleep.c and sleep.h
 * source files. The main purpose of the module is to ease energy
 * optimization with a simple API. The module allows the system to always sleep
 * in the lowest possible energy mode. Users could set up callbacks that are
 * being called before and after each and every sleep. A counting semaphore is
 * available for each low energy mode (EM1/EM2/EM3) to protect certain system
 * states from being corrupted. This semaphore has limit set to maximum 255 locks.
 *
 * The module provides the following public API to the users:
 * SLEEP_Init()
 * SLEEP_Sleep()
 * SLEEP_SleepBlockBegin()
 * SLEEP_SleepBlockEnd()
 * SLEEP_ForceSleepInEM4()
 *
 *******************************************************************************
 * @section License
 * <b>(C) Copyright 2014 Silicon Labs, http://www.silabs.com</b>
 *******************************************************************************
 *
 * This file is licensed under the Silicon Labs Software License Agreement. See 
 * "http://developer.silabs.com/legal/version/v11/Silicon_Labs_Software_License_Agreement.txt"  
 * for details. Before using this software for any purpose, you must agree to the 
 * terms of that agreement.
 *
 ******************************************************************************/







/* Chip specific header file(s). */
#include "em_device.h"
#include "em_assert.h"
#include "em_int.h"
#include "em_rmu.h"
#include "em_emu.h"

/* Module header file(s). */
#include "sleep.h"

/* stdlib is needed for NULL definition */
#include <stdlib.h>

/***************************************************************************//**
 * @addtogroup EM_Drivers
 * @{
 ******************************************************************************/

/***************************************************************************//**
 * @addtogroup SLEEP
 * @brief Energy Modes management driver.
 * @details
 * This is a energy modes management module consisting of sleep.c and sleep.h
 * source files. The main purpose of the module is to ease energy
 * optimization with a simple API. The module allows the system to always sleep
 * in the lowest possible energy mode. Users could set up callbacks that are
 * being called before and after each and every sleep. A counting semaphore is
 * available for each low energy mode (EM1/EM2/EM3) to protect certain system
 * states from being corrupted. This semaphore has limit set to maximum 255 locks.
 * @{
 ******************************************************************************/

/*******************************************************************************
 *******************************   MACROS   ************************************
 ******************************************************************************/

/** @cond DO_NOT_INCLUDE_WITH_DOXYGEN */

/* Number of low energy modes (EM1, EM2, EM3). Note: EM4 sleep/wakeup is handled
 * differently therefore it is not part of the list! */
#define SLEEP_NUMOF_LOW_ENERGY_MODES    3U



/*******************************************************************************
 ******************************   TYPEDEFS   ***********************************
 ******************************************************************************/


/*******************************************************************************
 ******************************   CONSTANTS   **********************************
 ******************************************************************************/


/*******************************************************************************
 *******************************   STATICS   ***********************************
 ******************************************************************************/

/* Callback functions to call before and after sleep. */
static SLEEP_CbFuncPtr_t sleepCallback  = NULL;
static SLEEP_CbFuncPtr_t wakeUpCallback = NULL;

/* Sleep block counter array representing the nested sleep blocks for the low
 * energy modes (EM1/EM2/EM3). Array index 0 corresponds to EM1, 1 to EM2 and 2
 * to EM3 accordingly.
 *
 * Note:
 * - EM4 sleep/wakeup is handled differently therefore it is not part of the
 *   list!
 * - Max. number of sleep block nesting is 255. */
static uint8_t sleepBlockCnt[SLEEP_NUMOF_LOW_ENERGY_MODES];

/*******************************************************************************
 ******************************   PROTOTYPES   *********************************
 ******************************************************************************/

static void SLEEP_EnterEMx(SLEEP_EnergyMode_t eMode);
static SLEEP_EnergyMode_t SLEEP_LowestEnergyModeGet(void);

/** @endcond */

/*******************************************************************************
 ***************************   GLOBAL FUNCTIONS   ******************************
 ******************************************************************************/

/***************************************************************************//**
 * @brief
 *   Initialize the Sleep module.
 *
 * @details
 *   Use this function to initialize the Sleep module, should be called
 *   only once! Pointers to sleep and wake-up callback functions shall be
 *   provided when calling this function.
 *   If SLEEP_EM4_WAKEUP_CALLBACK_ENABLED is set to true, this function checks
 *   for the cause of the reset that implicitly called it and calls the wakeup
 *   callback if the reset was a wakeup from EM4 (does not work on Gecko MCU).
 *
 * @param[in] pSleepCb
 *   Pointer to the callback function that is being called before the device is
 *   going to sleep.
 *
 * @param[in] pWakeUpCb
 *   Pointer to the callback function that is being called after wake up.
 ******************************************************************************/
void SLEEP_Init(SLEEP_CbFuncPtr_t pSleepCb, SLEEP_CbFuncPtr_t pWakeUpCb)
{
  /* Initialize callback functions. */
  sleepCallback  = pSleepCb;
  wakeUpCallback = pWakeUpCb;

  /* Reset sleep block counters. Note: not using for() saves code! */
  sleepBlockCnt[0U] = 0U;
  sleepBlockCnt[1U] = 0U;
  sleepBlockCnt[2U] = 0U;

#if (SLEEP_EM4_WAKEUP_CALLBACK_ENABLED == true) && defined(RMU_RSTCAUSE_EM4WURST)
  /* Check if the Init() happened after an EM4 reset. */
  if (RMU_ResetCauseGet() & RMU_RSTCAUSE_EM4WURST)
  {
    /* Clear the cause of the reset. */
    RMU_ResetCauseClear();
    /* Call wakeup callback with EM4 parameter. */
    if (NULL != wakeUpCallback)
    {
      wakeUpCallback(sleepEM4);
    }
  }
#endif
}


/***************************************************************************//**
 * @brief
 *   Sets the system to sleep into the lowest possible energy mode.
 *
 * @details
 *   This function takes care of the system states protected by the sleep block
 *   provided by SLEEP_SleepBlockBegin() / SLEEP_SleepBlockEnd(). It allows
 *   the system to go into the lowest possible energy mode that the device can
 *   be set into at the time of the call of this function.
 *   This function will not go lower than EM3 because leaving EM4 requires
 *   resetting MCU. To enter into EM4 call SLEEP_ForceSleepInEM4().
 *
 * @return
 *   Energy Mode that was entered. Possible values:
 *   @li sleepEM0
 *   @li sleepEM1
 *   @li sleepEM2
 *   @li sleepEM3
 ******************************************************************************/
SLEEP_EnergyMode_t SLEEP_Sleep(void)
{
  SLEEP_EnergyMode_t allowedEM;

  INT_Disable();

  allowedEM = SLEEP_LowestEnergyModeGet();

  if ((allowedEM >= sleepEM1) && (allowedEM <= sleepEM3))
  {
    SLEEP_EnterEMx(allowedEM);
  }
  else
  {
    allowedEM = sleepEM0;
  }

  INT_Enable();

  return(allowedEM);
}


/***************************************************************************//**
 * @brief
 *   Force the device to go to EM4 without doing any checks.
 *
 * @details
 *   This function unblocks the low energy sleep block then goes to EM4.
 *
 * @note
 *   Regular RAM is not retained in EM4 and the wake up causes a reset.
 *   If the configuration option SLEEP_EM4_WAKEUP_CALLBACK_ENABLED is set to
 *   true, the SLEEP_Init() function checks for the reset cause and calls the
 *   EM4 wakeup callback.
 ******************************************************************************/
void SLEEP_ForceSleepInEM4(void)
{
#if (SLEEP_HW_LOW_ENERGY_BLOCK_ENABLED == true)
  /* Unblock the EM2/EM3/EM4 block in the EMU. */
  EMU_EM2UnBlock();
#endif

  /* Request entering to EM4. */
  SLEEP_EnterEMx(sleepEM4);
}

/***************************************************************************//**
 * @brief
 *   Begin sleep block in the requested energy mode.
 *
 * @details
 *   Blocking a critical system state from a certain energy mode makes sure that
 *   the system is not set to that energy mode while the block is not being
 *   released.
 *   Every SLEEP_SleepBlockBegin() increases the corresponding counter and
 *   every SLEEP_SleepBlockEnd() decreases it.
 *
 *   Example:\code
 *      SLEEP_SleepBlockBegin(sleepEM2);  // do not allow EM2 or higher
 *      // do some stuff that requires EM1 at least, like ADC sampling
 *      SLEEP_SleepBlockEnd(sleepEM2);    // remove restriction for EM2\endcode
 *
 * @note
 *   Be aware that there is limit of maximum blocks nesting to 255.
 *
 * @param[in] eMode
 *   Energy mode to begin to block. Possible values:
 *   @li sleepEM1 - Begin to block the system from being set to EM1 (and EM2..4).
 *   @li sleepEM2 - Begin to block the system from being set to EM2 (and EM3/EM4).
 *   @li sleepEM3 - Begin to block the system from being set to EM3 (and EM4).
 ******************************************************************************/
void SLEEP_SleepBlockBegin(SLEEP_EnergyMode_t eMode)
{
  EFM_ASSERT((eMode >= sleepEM1) && (eMode < sleepEM4));
  EFM_ASSERT((sleepBlockCnt[(uint8_t) eMode - 1U]) < 255U);

  /* Increase the sleep block counter of the selected energy mode. */
  sleepBlockCnt[(uint8_t) eMode - 1U]++;

#if (SLEEP_HW_LOW_ENERGY_BLOCK_ENABLED == true)
  /* Block EM2/EM3 sleep if the EM2 block begins. */
  if (eMode == sleepEM2)
  {
    EMU_EM2Block();
  }
#endif
}

/***************************************************************************//**
 * @brief
 *   End sleep block in the requested energy mode.
 *
 * @details
 *   Release restriction for entering certain energy mode. Every call of this
 *   function reduce blocking counter by 1. Once the counter for specific energy
 *   mode is 0 and all counters for lower energy modes are 0 as well, using
 *   particular energy mode is allowed.
 *   Every SLEEP_SleepBlockBegin() increases the corresponding counter and
 *   every SLEEP_SleepBlockEnd() decreases it.
 *
 *   Example:\code
 *      // at start all energy modes are allowed
 *      SLEEP_SleepBlockBegin(sleepEM2); // EM2, EM3, EM4 are blocked
 *      SLEEP_SleepBlockBegin(sleepEM1); // EM1, EM2, EM3, EM4 are blocked
 *      SLEEP_SleepBlockBegin(sleepEM1); // EM1, EM2, EM3, EM4 are blocked
 *      SLEEP_SleepBlockEnd(sleepEM2);   // still EM1, EM2, EM3, EM4 are blocked
 *      SLEEP_SleepBlockEnd(sleepEM1);   // still EM1, EM2, EM3, EM4 are blocked
 *      SLEEP_SleepBlockEnd(sleepEM1);   // all energy modes are allowed now\endcode
 *
 * @param[in] eMode
 *   Energy mode to end to block. Possible values:
 *   @li sleepEM1 - End to block the system from being set to EM1 (and EM2..4).
 *   @li sleepEM2 - End to block the system from being set to EM2 (and EM3/EM4).
 *   @li sleepEM3 - End to block the system from being set to EM3 (and EM4).
 ******************************************************************************/
void SLEEP_SleepBlockEnd(SLEEP_EnergyMode_t eMode)
{
  EFM_ASSERT((eMode >= sleepEM1) && (eMode < sleepEM4));

  /* Decrease the sleep block counter of the selected energy mode. */
  if (sleepBlockCnt[(uint8_t) eMode - 1U] > 0U)
  {
    sleepBlockCnt[(uint8_t) eMode - 1U]--;
  }

#if (SLEEP_HW_LOW_ENERGY_BLOCK_ENABLED == true)
  /* Check if the EM2/EM3 block should be unblocked in the EMU. */
  if (0U == sleepBlockCnt[(uint8_t) sleepEM2 - 1U])
  {
    EMU_EM2UnBlock();
  }
#endif
}

/** @cond DO_NOT_INCLUDE_WITH_DOXYGEN */

/***************************************************************************//**
 * @brief
 *   Gets the lowest energy mode that the system is allowed to be set to.
 *
 * @details
 *   This function uses the low energy mode block counters to determine the
 *   lowest possible that the system is allowed to be set to.
 *
 * @return
 *   Lowest energy mode that the system can be set to. Possible values:
 *   @li sleepEM0
 *   @li sleepEM1
 *   @li sleepEM2
 *   @li sleepEM3
 ******************************************************************************/
static SLEEP_EnergyMode_t SLEEP_LowestEnergyModeGet(void)
{
  SLEEP_EnergyMode_t tmpLowestEM = sleepEM0;

  /* Check which is the lowest energy mode that the system can be set to. */
  if (0U == sleepBlockCnt[(uint8_t) sleepEM1 - 1U])
  {
    tmpLowestEM = sleepEM1;
    if (0U == sleepBlockCnt[(uint8_t) sleepEM2 - 1U])
    {
      tmpLowestEM = sleepEM2;
      if (0U == sleepBlockCnt[(uint8_t) sleepEM3 - 1U])
      {
        tmpLowestEM = sleepEM3;
      }
    }
  }

  /* Compare with the default lowest energy mode setting. */
  if (SLEEP_LOWEST_ENERGY_MODE_DEFAULT < tmpLowestEM)
  {
    tmpLowestEM = SLEEP_LOWEST_ENERGY_MODE_DEFAULT;
  }

  return tmpLowestEM;
}


/***************************************************************************//**
 * @brief
 *   Call the callbacks and enter the requested energy mode.
 *
 * @details
 *   This function is not part of the API, therefore it shall not be called by
 *   the user directly as it doesn not have any checks if the system is ready
 *   for sleep!
 *
 * @note
 *   The EM4 wakeup callback is not being called from this function because
 *   waking up from EM4 causes a reset.
 *   If SLEEP_EM4_WAKEUP_CALLBACK_ENABLED is set to true, SLEEP_Init() function
 *   checks for the cause of the reset and calls the wakeup callback if the
 *   reset was a wakeup from EM4.
 ******************************************************************************/
static void SLEEP_EnterEMx(SLEEP_EnergyMode_t eMode)
{
  EFM_ASSERT((eMode > sleepEM0) && (eMode <= sleepEM4));

  /* Call sleepCallback() before going to sleep. */
  if (NULL != sleepCallback)
  {
    /* Call the callback before going to sleep. */
    sleepCallback(eMode);
  }

  /* Enter the requested energy mode. */
  switch (eMode)
  {
  case sleepEM1:
  {
    EMU_EnterEM1();
  } break;

  case sleepEM2:
  {
    EMU_EnterEM2(true);
  } break;

  case sleepEM3:
  {
    EMU_EnterEM3(true);
  } break;

  case sleepEM4:
  {
    EMU_EnterEM4();
  } break;

  default:
  {
    /* Don't do anything, stay in EM0. */
  } break;
  }

  /* Call the callback after waking up from sleep. */
  if (NULL != wakeUpCallback)
  {
    wakeUpCallback(eMode);
  }
}
/** @endcond */

/** @} (end addtogroup SLEEP */
/** @} (end addtogroup EM_Drivers) */
