/***************************************************************************//**
 * @file em_vdac.c
 * @brief Digital to Analog Converter (VDAC) Peripheral API
 * @version 5.1.2
 *******************************************************************************
 * @section License
 * <b>Copyright 2016 Silicon Laboratories, Inc. http://www.silabs.com</b>
 *******************************************************************************
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 *
 * DISCLAIMER OF WARRANTY/LIMITATION OF REMEDIES: Silicon Labs has no
 * obligation to support this Software. Silicon Labs is providing the
 * Software "AS IS", with no express or implied warranties of any kind,
 * including, but not limited to, any implied warranties of merchantability
 * or fitness for any particular purpose or warranties against infringement
 * of any proprietary rights of a third party.
 *
 * Silicon Labs will not be liable for any consequential, incidental, or
 * special damages, or any other relief, or for any claim by any third party,
 * arising from your use of this Software.
 *
 ******************************************************************************/

#include "em_vdac.h"
#if defined(VDAC_COUNT) && (VDAC_COUNT > 0)
#include "em_cmu.h"

/***************************************************************************//**
 * @addtogroup emlib
 * @{
 ******************************************************************************/

/***************************************************************************//**
 * @addtogroup VDAC
 * @{
 ******************************************************************************/

/*******************************************************************************
 *******************************   DEFINES   ***********************************
 ******************************************************************************/

/** @cond DO_NOT_INCLUDE_WITH_DOXYGEN */

/** Validation of VDAC channel for assert statements. */
#define VDAC_CH_VALID(ch)    ((ch) <= 1)

/** Max VDAC clock */
#define VDAC_MAX_CLOCK            1000000

/** Max clock frequency of internal clock oscillator, 10 MHz + 20%. */
#define VDAC_INTERNAL_CLOCK_FREQ  12000000

/** @endcond */

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

/***************************************************************************//**
 * @brief
 *   Enable/disable VDAC channel.
 *
 * @param[in] vdac
 *   Pointer to VDAC peripheral register block.
 *
 * @param[in] ch
 *   Channel to enable/disable.
 *
 * @param[in] enable
 *   true to enable VDAC channel, false to disable.
 ******************************************************************************/
void VDAC_Enable(VDAC_TypeDef *vdac, unsigned int ch, bool enable)
{
  EFM_ASSERT(VDAC_REF_VALID(vdac));
  EFM_ASSERT(VDAC_CH_VALID(ch));

  if (ch == 0)
  {
    if (enable)
    {
      vdac->CMD = VDAC_CMD_CH0EN;
    }
    else
    {
      vdac->CMD = VDAC_CMD_CH0DIS;
      while (vdac->STATUS & VDAC_STATUS_CH0ENS);
    }
  }
  else
  {
    if (enable)
    {
      vdac->CMD = VDAC_CMD_CH1EN;
    }
    else
    {
      vdac->CMD = VDAC_CMD_CH1DIS;
      while (vdac->STATUS & VDAC_STATUS_CH1ENS);
    }
  }
}

/***************************************************************************//**
 * @brief
 *   Initialize VDAC.
 *
 * @details
 *   Initializes common parts for both channels. This function will also load
 *   calibration values from the Device Information (DI) page into the VDAC
 *   calibration register.
 *   To complete a VDAC setup, channel control configuration must also be done,
 *   please refer to VDAC_InitChannel().
 *
 * @note
 *   This function will disable both channels prior to configuration.
 *
 * @param[in] vdac
 *   Pointer to VDAC peripheral register block.
 *
 * @param[in] init
 *   Pointer to VDAC initialization structure.
 ******************************************************************************/
void VDAC_Init(VDAC_TypeDef *vdac, const VDAC_Init_TypeDef *init)
{
  uint32_t cal, tmp = 0;
  uint32_t const volatile *calData;

  EFM_ASSERT(VDAC_REF_VALID(vdac));

  /* Make sure both channels are disabled. */
  vdac->CMD = VDAC_CMD_CH0DIS | VDAC_CMD_CH1DIS;
  while (vdac->STATUS & (VDAC_STATUS_CH0ENS | VDAC_STATUS_CH1ENS));

  /* Get OFFSETTRIM calibration value. */
  cal = ((DEVINFO->VDAC0CH1CAL & _DEVINFO_VDAC0CH1CAL_OFFSETTRIM_MASK)
         >> _DEVINFO_VDAC0CH1CAL_OFFSETTRIM_SHIFT)
        << _VDAC_CAL_OFFSETTRIM_SHIFT;

  if (init->mainCalibration)
  {
    calData = &DEVINFO->VDAC0MAINCAL;
  }
  else
  {
    calData = &DEVINFO->VDAC0ALTCAL;
  }

  /* Get correct GAINERRTRIM calibration value. */
  switch (init->reference)
  {
    case vdacRef1V25Ln:
      tmp = (*calData & _DEVINFO_VDAC0MAINCAL_GAINERRTRIM1V25LN_MASK)
            >> _DEVINFO_VDAC0MAINCAL_GAINERRTRIM1V25LN_SHIFT;
      break;

    case vdacRef2V5Ln:
      tmp = (*calData & _DEVINFO_VDAC0MAINCAL_GAINERRTRIM2V5LN_MASK)
            >> _DEVINFO_VDAC0MAINCAL_GAINERRTRIM2V5LN_SHIFT;
      break;

    case vdacRef1V25:
      tmp = (*calData & _DEVINFO_VDAC0MAINCAL_GAINERRTRIM1V25_MASK)
            >> _DEVINFO_VDAC0MAINCAL_GAINERRTRIM1V25_SHIFT;
      break;

    case vdacRef2V5:
      tmp = (*calData & _DEVINFO_VDAC0MAINCAL_GAINERRTRIM2V5_MASK)
            >> _DEVINFO_VDAC0MAINCAL_GAINERRTRIM2V5_SHIFT;
      break;

    case vdacRefAvdd:
    case vdacRefExtPin:
      tmp = (*calData & _DEVINFO_VDAC0MAINCAL_GAINERRTRIMVDDANAEXTPIN_MASK)
            >> _DEVINFO_VDAC0MAINCAL_GAINERRTRIMVDDANAEXTPIN_SHIFT;
      break;
  }

  /* Set GAINERRTRIM calibration value. */
  cal |= tmp << _VDAC_CAL_GAINERRTRIM_SHIFT;

  /* Get GAINERRTRIMCH1 calibration value. */
  switch (init->reference)
  {
    case vdacRef1V25Ln:
    case vdacRef1V25:
    case vdacRefAvdd:
    case vdacRefExtPin:
      tmp = (DEVINFO->VDAC0CH1CAL && _DEVINFO_VDAC0CH1CAL_GAINERRTRIMCH1A_MASK)
            >> _DEVINFO_VDAC0CH1CAL_GAINERRTRIMCH1A_SHIFT;
      break;

    case vdacRef2V5Ln:
    case vdacRef2V5:
      tmp = (DEVINFO->VDAC0CH1CAL && _DEVINFO_VDAC0CH1CAL_GAINERRTRIMCH1B_MASK)
            >> _DEVINFO_VDAC0CH1CAL_GAINERRTRIMCH1B_SHIFT;
      break;
  }

  /* Set GAINERRTRIM calibration value. */
  cal |= tmp << _VDAC_CAL_GAINERRTRIMCH1_SHIFT;

  tmp = ((uint32_t)init->asyncClockMode << _VDAC_CTRL_DACCLKMODE_SHIFT)
        | ((uint32_t)init->warmupKeepOn << _VDAC_CTRL_WARMUPMODE_SHIFT)
        | ((uint32_t)init->refresh      << _VDAC_CTRL_REFRESHPERIOD_SHIFT)
        | (((uint32_t)init->prescaler   << _VDAC_CTRL_PRESC_SHIFT)
           & _VDAC_CTRL_PRESC_MASK)
        | ((uint32_t)init->reference    << _VDAC_CTRL_REFSEL_SHIFT)
        | ((uint32_t)init->ch0ResetPre  << _VDAC_CTRL_CH0PRESCRST_SHIFT)
        | ((uint32_t)init->outEnablePRS << _VDAC_CTRL_OUTENPRS_SHIFT)
        | ((uint32_t)init->sineEnable   << _VDAC_CTRL_SINEMODE_SHIFT)
        | ((uint32_t)init->diff         << _VDAC_CTRL_DIFF_SHIFT);

  /* Write to VDAC registers. */
  vdac->CAL = cal;
  vdac->CTRL = tmp;
}

/***************************************************************************//**
 * @brief
 *   Initialize a VDAC channel.
 *
 * @param[in] vdac
 *   Pointer to VDAC peripheral register block.
 *
 * @param[in] init
 *   Pointer to VDAC channel initialization structure.
 *
 * @param[in] ch
 *   Channel number to initialize.
 ******************************************************************************/
void VDAC_InitChannel(VDAC_TypeDef *vdac,
                      const VDAC_InitChannel_TypeDef *init,
                      unsigned int ch)
{
  uint32_t vdacChCtrl, vdacStatus;

  EFM_ASSERT(VDAC_REF_VALID(vdac));
  EFM_ASSERT(VDAC_CH_VALID(ch));

  /* Make sure both channels are disabled. */
  vdacStatus = vdac->STATUS;
  vdac->CMD = VDAC_CMD_CH0DIS | VDAC_CMD_CH1DIS;
  while (vdac->STATUS & (VDAC_STATUS_CH0ENS | VDAC_STATUS_CH1ENS));

  vdacChCtrl = ((uint32_t)init->prsSel          << _VDAC_CH0CTRL_PRSSEL_SHIFT)
               | ((uint32_t)init->prsAsync      << _VDAC_CH0CTRL_PRSASYNC_SHIFT)
               | ((uint32_t)init->trigMode      << _VDAC_CH0CTRL_TRIGMODE_SHIFT)
               | ((uint32_t)init->sampleOffMode << _VDAC_CH0CTRL_CONVMODE_SHIFT);

  if (ch == 0)
  {
    vdac->CH0CTRL = vdacChCtrl;
  }
  else
  {
    vdac->CH1CTRL = vdacChCtrl;
  }

  /* Check if the channel must be enabled. */
  if (init->enable)
  {
    if (ch == 0)
    {
      vdac->CMD = VDAC_CMD_CH0EN;
    }
    else
    {
      vdac->CMD = VDAC_CMD_CH1EN;
    }
  }

  /* Check if the other channel had to be turned off above
   * and needs to be turned on again. */
  if (ch == 0)
  {
    if (vdacStatus & VDAC_STATUS_CH1ENS)
    {
      vdac->CMD = VDAC_CMD_CH1EN;
    }
  }
  else
  {
    if (vdacStatus & VDAC_STATUS_CH0ENS)
    {
      vdac->CMD = VDAC_CMD_CH0EN;
    }
  }
}

/***************************************************************************//**
 * @brief
 *   Set the output signal of a VDAC channel to a given value.
 *
 * @details
 *   This function sets the output signal of a VDAC channel by writing @p value
 *   to the corresponding CHnDATA register.
 *
 * @param[in] vdac
 *   Pointer to VDAC peripheral register block.
 *
 * @param[in] channel
 *   Channel number to set output of.
 *
 * @param[in] value
 *   Value to write to the channel output register CHnDATA.
 ******************************************************************************/
void VDAC_ChannelOutputSet(VDAC_TypeDef *vdac,
                           unsigned int channel,
                           uint32_t value)
{
  switch(channel)
  {
    case 0:
      VDAC_Channel0OutputSet(vdac, value);
      break;
    case 1:
      VDAC_Channel1OutputSet(vdac, value);
      break;
    default:
      EFM_ASSERT(0);
      break;
  }
}

/***************************************************************************//**
 * @brief
 *   Calculate prescaler value used to determine VDAC clock.
 *
 * @details
 *   The VDAC clock is given by input clock divided by prescaler+1.
 *
 *     VDAC_CLK = IN_CLK / (prescale + 1)
 *
 *   Maximum VDAC clock is 1 MHz. Input clock is HFPERCLK when VDAC synchronous
 *   mode is selected, or an internal oscillator of 10 MHz +/- 20% when
 *   asynchronous mode is selected.
 *
 * @note
 *   If the requested VDAC frequency is low and the max prescaler value can not
 *   adjust the actual VDAC frequency lower than requested, the max prescaler
 *   value is returned, resulting in a higher VDAC frequency than requested.
 *
 * @param[in] vdacFreq VDAC frequency target. The frequency will automatically
 *   be adjusted to be below max allowed VDAC clock.
 *
 * @param[in] syncMode Set to true if you intend to use VDAC in synchronous
 *   mode.
 *
 * @param[in] hfperFreq Frequency in Hz of HFPERCLK oscillator. Set to 0 to
 *   use currently defined HFPERCLK clock setting. This parameter is only used
 *   when syncMode is set to true.
 *
 * @return
 *   Prescaler value to use for VDAC in order to achieve a clock value less than
 *   or equal to @p vdacFreq.
 ******************************************************************************/
uint32_t VDAC_PrescaleCalc(uint32_t vdacFreq, bool syncMode, uint32_t hfperFreq)
{
  uint32_t ret, refFreq;

  /* Make sure selected VDAC clock is below max value */
  if (vdacFreq > VDAC_MAX_CLOCK)
  {
    vdacFreq = VDAC_MAX_CLOCK;
  }

  if (!syncMode)
  {
    refFreq = VDAC_INTERNAL_CLOCK_FREQ;
  }
  else
  {
    if (hfperFreq)
    {
      refFreq = hfperFreq;
    }
    else
    {
      refFreq = CMU_ClockFreqGet(cmuClock_HFPER);
    }
  }

  /* Iterate in order to determine best prescale value. Start with lowest */
  /* prescaler value in order to get the first equal or less VDAC         */
  /* frequency value. */
  for (ret = 0; ret <= _VDAC_CTRL_PRESC_MASK >> _VDAC_CTRL_PRESC_SHIFT; ret++)
  {
    if ((refFreq / (ret + 1)) <= vdacFreq)
    {
      break;
    }
  }

  /* If ret is higher than the max prescaler value, make sure to return
     the max value. */
  if (ret > (_VDAC_CTRL_PRESC_MASK >> _VDAC_CTRL_PRESC_SHIFT))
  {
    ret = _VDAC_CTRL_PRESC_MASK >> _VDAC_CTRL_PRESC_SHIFT;
  }

  return ret;
}

/***************************************************************************//**
 * @brief
 *   Reset VDAC to same state as after a HW reset.
 *
 * @param[in] vdac
 *   Pointer to VDAC peripheral register block.
 ******************************************************************************/
void VDAC_Reset(VDAC_TypeDef *vdac)
{
  /* Disable channels, before resetting other registers. */
  vdac->CMD     = VDAC_CMD_CH0DIS | VDAC_CMD_CH1DIS;
  while (vdac->STATUS & (VDAC_STATUS_CH0ENS | VDAC_STATUS_CH1ENS));
  vdac->CH0CTRL = _VDAC_CH0CTRL_RESETVALUE;
  vdac->CH1CTRL = _VDAC_CH1CTRL_RESETVALUE;
  vdac->CH0DATA = _VDAC_CH0DATA_RESETVALUE;
  vdac->CH1DATA = _VDAC_CH1DATA_RESETVALUE;
  vdac->CTRL    = _VDAC_CTRL_RESETVALUE;
  vdac->IEN     = _VDAC_IEN_RESETVALUE;
  vdac->IFC     = _VDAC_IFC_MASK;
  vdac->CAL     = _VDAC_CAL_RESETVALUE;
}

/** @} (end addtogroup VDAC) */
/** @} (end addtogroup emlib) */
#endif /* defined(VDAC_COUNT) && (VDAC_COUNT > 0) */
