/***************************************************************************//**
 * @file em_idac.c
 * @brief Current Digital to Analog Converter (IDAC) 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_idac.h"
#if defined(IDAC_COUNT) && (IDAC_COUNT > 0)
#include "em_cmu.h"
#include "em_assert.h"
#include "em_bus.h"

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

/***************************************************************************//**
 * @addtogroup IDAC
 * @{
 ******************************************************************************/

/** @cond DO_NOT_INCLUDE_WITH_DOXYGEN */
/* Fix for errata IDAC_E101 - IDAC output current degradation */
#if defined(_SILICON_LABS_32B_SERIES_0)  \
    && (defined(_EFM32_ZERO_FAMILY) || defined(_EFM32_HAPPY_FAMILY))
#define ERRATA_FIX_IDAC_E101_EN
#endif
/** @endcond */

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


/***************************************************************************//**
 * @brief
 *   Initialize IDAC.
 *
 * @details
 *   Initializes IDAC according to the initialization structure parameter, and
 *   sets the default calibration value stored in the DEVINFO structure.
 *
 * @note
 *   This function will disable the IDAC prior to configuration.
 *
 * @param[in] idac
 *   Pointer to IDAC peripheral register block.
 *
 * @param[in] init
 *   Pointer to IDAC initialization structure.
 ******************************************************************************/
void IDAC_Init(IDAC_TypeDef *idac, const IDAC_Init_TypeDef *init)
{
  uint32_t tmp;

  EFM_ASSERT(IDAC_REF_VALID(idac));

  tmp = (uint32_t)(init->prsSel);

  tmp |= init->outMode;

  if (init->enable)
  {
    tmp |= IDAC_CTRL_EN;
  }
  if (init->prsEnable)
  {
#if defined(_IDAC_CTRL_OUTENPRS_MASK)
    tmp |= IDAC_CTRL_OUTENPRS;
#else
    tmp |= IDAC_CTRL_APORTOUTENPRS;
#endif
  }
  if (init->sinkEnable)
  {
    tmp |= IDAC_CTRL_CURSINK;
  }

  idac->CTRL = tmp;
}


/***************************************************************************//**
 * @brief
 *   Enable/disable IDAC.
 *
 * @param[in] idac
 *   Pointer to IDAC peripheral register block.
 *
 * @param[in] enable
 *   true to enable IDAC, false to disable.
 ******************************************************************************/
void IDAC_Enable(IDAC_TypeDef *idac, bool enable)
{
  EFM_ASSERT(IDAC_REF_VALID(idac));
  BUS_RegBitWrite(&idac->CTRL, _IDAC_CTRL_EN_SHIFT, enable);
}


/***************************************************************************//**
 * @brief
 *   Reset IDAC to same state as after a HW reset.
 *
 * @param[in] idac
 *   Pointer to IDAC peripheral register block.
 ******************************************************************************/
void IDAC_Reset(IDAC_TypeDef *idac)
{
  EFM_ASSERT(IDAC_REF_VALID(idac));

#if defined(ERRATA_FIX_IDAC_E101_EN)
  /* Fix for errata IDAC_E101 - IDAC output current degradation:
     Instead of disabling it we will put it in it's lowest power state (50 nA)
     to avoid degradation over time */

  /* Make sure IDAC is enabled with disabled output */
  idac->CTRL = _IDAC_CTRL_RESETVALUE | IDAC_CTRL_EN;

  /* Set lowest current (50 nA) */
  idac->CURPROG = IDAC_CURPROG_RANGESEL_RANGE0 |
                  (0x0 << _IDAC_CURPROG_STEPSEL_SHIFT);

  /* Enable duty-cycling for all energy modes */
  idac->DUTYCONFIG = IDAC_DUTYCONFIG_DUTYCYCLEEN;
#else
  idac->CTRL       = _IDAC_CTRL_RESETVALUE;
  idac->CURPROG    = _IDAC_CURPROG_RESETVALUE;
  idac->DUTYCONFIG = _IDAC_DUTYCONFIG_RESETVALUE;
#endif
#if defined ( _IDAC_CAL_MASK )
  idac->CAL        = _IDAC_CAL_RESETVALUE;
#endif
}


/***************************************************************************//**
 * @brief
 *   Enable/disable Minimal Output Transition mode.
 *
 * @param[in] idac
 *   Pointer to IDAC peripheral register block.
 *
 * @param[in] enable
 *   true to enable Minimal Output Transition mode, false to disable.
 ******************************************************************************/
void IDAC_MinimalOutputTransitionMode(IDAC_TypeDef *idac, bool enable)
{
  EFM_ASSERT(IDAC_REF_VALID(idac));
  BUS_RegBitWrite(&idac->CTRL, _IDAC_CTRL_MINOUTTRANS_SHIFT, enable);
}


/***************************************************************************//**
 * @brief
 *   Set the current range of the IDAC output.
 *
 * @details
 *   This function sets the current range of the IDAC output. The function
 *   also updates the IDAC calibration register (IDAC_CAL) with the default
 *   calibration value from DEVINFO (factory calibration) corresponding to the
 *   specified range.
 *
 * @param[in] idac
 *   Pointer to IDAC peripheral register block.
 *
 * @param[in] range
 *   Current range value.
 ******************************************************************************/
void IDAC_RangeSet(IDAC_TypeDef *idac, const IDAC_Range_TypeDef range)
{
  uint32_t tmp;
#if defined( _IDAC_CURPROG_TUNING_MASK )
  uint32_t diCal0;
  uint32_t diCal1;
#endif

  EFM_ASSERT(IDAC_REF_VALID(idac));
  EFM_ASSERT(((uint32_t)range >> _IDAC_CURPROG_RANGESEL_SHIFT)
             <= (_IDAC_CURPROG_RANGESEL_MASK >> _IDAC_CURPROG_RANGESEL_SHIFT));

#if defined ( _IDAC_CAL_MASK )

  /* Load proper calibration data depending on selected range */
  switch ((IDAC_Range_TypeDef)range)
  {
    case idacCurrentRange0:
      idac->CAL = (DEVINFO->IDAC0CAL0 & _DEVINFO_IDAC0CAL0_RANGE0_MASK)
                  >> _DEVINFO_IDAC0CAL0_RANGE0_SHIFT;
      break;
    case idacCurrentRange1:
      idac->CAL = (DEVINFO->IDAC0CAL0 & _DEVINFO_IDAC0CAL0_RANGE1_MASK)
                  >> _DEVINFO_IDAC0CAL0_RANGE1_SHIFT;
      break;
    case idacCurrentRange2:
      idac->CAL = (DEVINFO->IDAC0CAL0 & _DEVINFO_IDAC0CAL0_RANGE2_MASK)
                  >> _DEVINFO_IDAC0CAL0_RANGE2_SHIFT;
      break;
    case idacCurrentRange3:
      idac->CAL = (DEVINFO->IDAC0CAL0 & _DEVINFO_IDAC0CAL0_RANGE3_MASK)
                  >> _DEVINFO_IDAC0CAL0_RANGE3_SHIFT;
      break;
  }

  tmp  = idac->CURPROG & ~_IDAC_CURPROG_RANGESEL_MASK;
  tmp |= (uint32_t)range;

#elif defined( _IDAC_CURPROG_TUNING_MASK )

  /* Load calibration data depending on selected range and sink/source mode */
  /* TUNING (calibration) field in CURPROG register. */
  EFM_ASSERT(idac == IDAC0);
  diCal0 = DEVINFO->IDAC0CAL0;
  diCal1 = DEVINFO->IDAC0CAL1;

  tmp = idac->CURPROG & ~(_IDAC_CURPROG_TUNING_MASK
                          | _IDAC_CURPROG_RANGESEL_MASK);
  if (idac->CTRL & IDAC_CTRL_CURSINK)
  {
    switch (range)
    {
      case idacCurrentRange0:
        tmp |= ((diCal1 & _DEVINFO_IDAC0CAL1_SINKRANGE0TUNING_MASK)
                >> _DEVINFO_IDAC0CAL1_SINKRANGE0TUNING_SHIFT)
               << _IDAC_CURPROG_TUNING_SHIFT;
        break;

      case idacCurrentRange1:
        tmp |= ((diCal1 & _DEVINFO_IDAC0CAL1_SINKRANGE1TUNING_MASK)
                >> _DEVINFO_IDAC0CAL1_SINKRANGE1TUNING_SHIFT)
               << _IDAC_CURPROG_TUNING_SHIFT;
        break;

      case idacCurrentRange2:
        tmp |= ((diCal1 & _DEVINFO_IDAC0CAL1_SINKRANGE2TUNING_MASK)
                >> _DEVINFO_IDAC0CAL1_SINKRANGE2TUNING_SHIFT)
               << _IDAC_CURPROG_TUNING_SHIFT;
        break;

      case idacCurrentRange3:
        tmp |= ((diCal1 & _DEVINFO_IDAC0CAL1_SINKRANGE3TUNING_MASK)
                >> _DEVINFO_IDAC0CAL1_SINKRANGE3TUNING_SHIFT)
               << _IDAC_CURPROG_TUNING_SHIFT;
        break;
    }
  }
  else
  {
    switch (range)
    {
      case idacCurrentRange0:
        tmp |= ((diCal0 & _DEVINFO_IDAC0CAL0_SOURCERANGE0TUNING_MASK)
                >> _DEVINFO_IDAC0CAL0_SOURCERANGE0TUNING_SHIFT)
               << _IDAC_CURPROG_TUNING_SHIFT;
        break;

      case idacCurrentRange1:
        tmp |= ((diCal0 & _DEVINFO_IDAC0CAL0_SOURCERANGE1TUNING_MASK)
                >> _DEVINFO_IDAC0CAL0_SOURCERANGE1TUNING_SHIFT)
               << _IDAC_CURPROG_TUNING_SHIFT;
        break;

      case idacCurrentRange2:
        tmp |= ((diCal0 & _DEVINFO_IDAC0CAL0_SOURCERANGE2TUNING_MASK)
                >> _DEVINFO_IDAC0CAL0_SOURCERANGE2TUNING_SHIFT)
               << _IDAC_CURPROG_TUNING_SHIFT;
        break;

      case idacCurrentRange3:
        tmp |= ((diCal0 & _DEVINFO_IDAC0CAL0_SOURCERANGE3TUNING_MASK)
                >> _DEVINFO_IDAC0CAL0_SOURCERANGE3TUNING_SHIFT)
               << _IDAC_CURPROG_TUNING_SHIFT;
        break;
    }
  }

  tmp |= (uint32_t)range;

#else
#warning "IDAC calibration register definition unknown."
#endif

  idac->CURPROG = tmp;
}


/***************************************************************************//**
 * @brief
 *   Set the current step of the IDAC output.
 *
 * @param[in] idac
 *   Pointer to IDAC peripheral register block.
 *
 * @param[in] step
 *   Step value for IDAC output. Valid range is 0-31.
 ******************************************************************************/
void IDAC_StepSet(IDAC_TypeDef *idac, const uint32_t step)
{
  uint32_t tmp;

  EFM_ASSERT(IDAC_REF_VALID(idac));
  EFM_ASSERT(step <= (_IDAC_CURPROG_STEPSEL_MASK >> _IDAC_CURPROG_STEPSEL_SHIFT));

  tmp  = idac->CURPROG & ~_IDAC_CURPROG_STEPSEL_MASK;
  tmp |= step << _IDAC_CURPROG_STEPSEL_SHIFT;

  idac->CURPROG = tmp;
}


/***************************************************************************//**
 * @brief
 *   Enable/disable the IDAC OUT pin.
 *
 * @param[in] idac
 *   Pointer to IDAC peripheral register block.
 *
 * @param[in] enable
 *   true to enable the IDAC OUT pin, false to disable.
 ******************************************************************************/
void IDAC_OutEnable(IDAC_TypeDef *idac, bool enable)
{
  EFM_ASSERT(IDAC_REF_VALID(idac));
#if defined(_IDAC_CTRL_OUTEN_MASK)
  BUS_RegBitWrite(&idac->CTRL, _IDAC_CTRL_OUTEN_SHIFT, enable);
#else
  BUS_RegBitWrite(&idac->CTRL, _IDAC_CTRL_APORTOUTEN_SHIFT, enable);
#endif
}


/** @} (end addtogroup IDAC) */
/** @} (end addtogroup emlib) */

#endif /* defined(IDAC_COUNT) && (IDAC_COUNT > 0) */
