/***************************************************************************//**
 * @file em_lcd.c
 * @brief Liquid Crystal Display (LCD) 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_lcd.h"
#if defined(LCD_COUNT) && (LCD_COUNT > 0)
#include "em_assert.h"
#include "em_bus.h"

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

/***************************************************************************//**
 * @addtogroup LCD
 * @brief Liquid Crystal Display (LCD) Peripheral API
 * @details
 *  This module contains functions to control the LDC peripheral of Silicon
 *  Labs 32-bit MCUs and SoCs. The LCD driver can drive up to 8x36 segmented
 *  LCD directly. The animation feature makes it possible to have active
 *  animations without CPU intervention.
 * @{
 ******************************************************************************/

/***************************************************************************//**
 * @brief
 *   Initalize Liquid Crystal Display (LCD) controller
 *
 * @details
 *   This function call will only configure the LCD controller. You must enable
 *   it afterwards, potentially configuring Frame Control and interrupts first
 *   according to requirements.
 *
 * @param[in] lcdInit
 *   Pointer to initialization structure which configures LCD controller.
 *
 ******************************************************************************/
void LCD_Init(const LCD_Init_TypeDef *lcdInit)
{
  uint32_t dispCtrl = LCD->DISPCTRL;

  EFM_ASSERT(lcdInit != (void *) 0);

  /* Disable controller before reconfiguration */
  LCD_Enable(false);

  /* Make sure we don't touch other bit fields (i.e. voltage boost) */
  dispCtrl &= ~(0
#if defined(LCD_DISPCTRL_MUXE)
                | _LCD_DISPCTRL_MUXE_MASK
#endif
                | _LCD_DISPCTRL_MUX_MASK
                | _LCD_DISPCTRL_BIAS_MASK
                | _LCD_DISPCTRL_WAVE_MASK
                | _LCD_DISPCTRL_VLCDSEL_MASK
                | _LCD_DISPCTRL_CONCONF_MASK);

  /* Configure controller according to initialization structure */
  dispCtrl |= lcdInit->mux; /* also configures MUXE */
  dispCtrl |= lcdInit->bias;
  dispCtrl |= lcdInit->wave;
  dispCtrl |= lcdInit->vlcd;
  dispCtrl |= lcdInit->contrast;

  /* Update display controller */
  LCD->DISPCTRL = dispCtrl;

  /* Enable controller if wanted */
  if (lcdInit->enable)
  {
    LCD_Enable(true);
  }
}


/***************************************************************************//**
 * @brief
 *   Select source for VLCD
 *
 * @param[in] vlcd
 *   Select source for VLD voltage
 ******************************************************************************/
void LCD_VLCDSelect(LCD_VLCDSel_TypeDef vlcd)
{
  uint32_t dispctrl = LCD->DISPCTRL;

  /* Select VEXT or VDD */
  dispctrl &= ~_LCD_DISPCTRL_VLCDSEL_MASK;
  switch (vlcd)
  {
    case lcdVLCDSelVExtBoost:
      dispctrl |= LCD_DISPCTRL_VLCDSEL_VEXTBOOST;
      break;
    case lcdVLCDSelVDD:
      dispctrl |= LCD_DISPCTRL_VLCDSEL_VDD;
      break;
    default:
      break;
  }

  LCD->DISPCTRL = dispctrl;
}


/***************************************************************************//**
 * @brief
 *   Configure Update Control
 *
 * @param[in] ud
 *   Configures LCD update method
 ******************************************************************************/
void LCD_UpdateCtrl(LCD_UpdateCtrl_TypeDef ud)
{
  LCD->CTRL = (LCD->CTRL & ~_LCD_CTRL_UDCTRL_MASK) | ud;
}


/***************************************************************************//**
 * @brief
 *   Initialize LCD Frame Counter
 *
 * @param[in] fcInit
 *   Pointer to Frame Counter initialization structure
 ******************************************************************************/
void LCD_FrameCountInit(const LCD_FrameCountInit_TypeDef *fcInit)
{
  uint32_t bactrl = LCD->BACTRL;

  EFM_ASSERT(fcInit != (void *) 0);

  /* Verify FC Top Counter to be within limits */
  EFM_ASSERT(fcInit->top < 64);

  /* Reconfigure frame count configuration */
  bactrl &= ~(_LCD_BACTRL_FCTOP_MASK
              | _LCD_BACTRL_FCPRESC_MASK);
  bactrl |= (fcInit->top << _LCD_BACTRL_FCTOP_SHIFT);
  bactrl |= fcInit->prescale;

  /* Set Blink and Animation Control Register */
  LCD->BACTRL = bactrl;

  LCD_FrameCountEnable(fcInit->enable);
}


/***************************************************************************//**
 * @brief
 *   Configures LCD controller Animation feature
 *
 * @param[in] animInit
 *   Pointer to LCD Animation initialization structure
 ******************************************************************************/
void LCD_AnimInit(const LCD_AnimInit_TypeDef *animInit)
{
  uint32_t bactrl = LCD->BACTRL;

  EFM_ASSERT(animInit != (void *) 0);

  /* Set Animation Register Values */
  LCD->AREGA = animInit->AReg;
  LCD->AREGB = animInit->BReg;

  /* Configure Animation Shift and Logic */
  bactrl &= ~(_LCD_BACTRL_AREGASC_MASK
              | _LCD_BACTRL_AREGBSC_MASK
              | _LCD_BACTRL_ALOGSEL_MASK);

  bactrl |= (animInit->AShift << _LCD_BACTRL_AREGASC_SHIFT);
  bactrl |= (animInit->BShift << _LCD_BACTRL_AREGBSC_SHIFT);
  bactrl |= animInit->animLogic;

#if defined(LCD_BACTRL_ALOC)
  bactrl &= ~(_LCD_BACTRL_ALOC_MASK);

  if(animInit->startSeg == 0)
  {
    bactrl |= LCD_BACTRL_ALOC_SEG0TO7;
  }
  else if(animInit->startSeg == 8)
  {
    bactrl |= LCD_BACTRL_ALOC_SEG8TO15;
  }
#endif

  /* Reconfigure */
  LCD->BACTRL = bactrl;

  /* Enable */
  LCD_AnimEnable(animInit->enable);
}


/***************************************************************************//**
 * @brief
 *   Enables update of this range of LCD segment lines
 *
 * @param[in] segmentRange
 *   Range of 4 LCD segments lines to enable or disable, for all enabled COM
 *   lines
 *
 * @param[in] enable
 *   Bool true to enable segment updates, false to disable updates
 ******************************************************************************/
void LCD_SegmentRangeEnable(LCD_SegmentRange_TypeDef segmentRange, bool enable)
{
  if (enable)
  {
    LCD->SEGEN |= segmentRange;
  }
  else
  {
    LCD->SEGEN &= ~((uint32_t)segmentRange);
  }
}


/***************************************************************************//**
 * @brief
 *   Turn on or clear a segment
 *
 * @note
 *    On Gecko Family, max configuration is (COM-lines x Segment-Lines) 4x40
 *    On Tiny Family, max configuration is 8x20 or 4x24
 *    On Giant Family, max configuration is 8x36 or 4x40
 *
 * @param[in] com
 *   COM line to change
 *
 * @param[in] bit
 *   Bit index of which field to change
 *
 * @param[in] enable
 *   When true will set segment, when false will clear segment
 ******************************************************************************/
void LCD_SegmentSet(int com, int bit, bool enable)
{
#if defined(_LCD_SEGD7L_MASK)
  /* Tiny and Giant Family supports up to 8 COM lines */
  EFM_ASSERT(com < 8);
#else
  /* Gecko Family supports up to 4 COM lines */
  EFM_ASSERT(com < 4);
#endif

#if defined(_LCD_SEGD0H_MASK)
  EFM_ASSERT(bit < 40);
#else
  /* Tiny Gecko Family supports only "low" segment registers */
  EFM_ASSERT(bit < 32);
#endif

  /* Use bitband access for atomic bit set/clear of segment */
  switch (com)
  {
    case 0:
      if (bit < 32)
      {
        BUS_RegBitWrite(&(LCD->SEGD0L), bit, enable);
      }
#if defined(_LCD_SEGD0H_MASK)
      else
      {
        bit -= 32;
        BUS_RegBitWrite(&(LCD->SEGD0H), bit, enable);
      }
#endif
      break;
    case 1:
      if (bit < 32)
      {
        BUS_RegBitWrite(&(LCD->SEGD1L), bit, enable);
      }
#if defined(_LCD_SEGD1H_MASK)
      else
      {
        bit -= 32;
        BUS_RegBitWrite(&(LCD->SEGD1H), bit, enable);
      }
#endif
      break;
    case 2:
      if (bit < 32)
      {
        BUS_RegBitWrite(&(LCD->SEGD2L), bit, enable);
      }
#if defined(_LCD_SEGD2H_MASK)
      else
      {
        bit -= 32;
        BUS_RegBitWrite(&(LCD->SEGD2H), bit, enable);
      }
#endif
      break;
    case 3:
      if (bit < 32)
      {
        BUS_RegBitWrite(&(LCD->SEGD3L), bit, enable);
      }
#if defined(_LCD_SEGD3H_MASK)
      else
      {
        bit -= 32;
        BUS_RegBitWrite(&(LCD->SEGD3H), bit, enable);
      }
#endif
      break;
#if defined(_LCD_SEGD4L_MASK)
    case 4:
      if (bit < 32)
      {
        BUS_RegBitWrite(&(LCD->SEGD4L), bit, enable);
      }
#if defined(_LCD_SEGD4H_MASK)
      else
      {
        bit -= 32;
        BUS_RegBitWrite(&(LCD->SEGD4H), bit, enable);
      }
#endif
      break;
#endif
#if defined(_LCD_SEGD5L_MASK)
    case 5:
      if (bit < 32)
      {
        BUS_RegBitWrite(&(LCD->SEGD5L), bit, enable);
      }
#if defined(_LCD_SEGD5H_MASK)
      else
      {
        bit -= 32;
        BUS_RegBitWrite(&(LCD->SEGD5H), bit, enable);
      }
#endif
      break;
#endif
    case 6:
#if defined(_LCD_SEGD6L_MASK)
      if (bit < 32)
      {
        BUS_RegBitWrite(&(LCD->SEGD6L), bit, enable);
      }
#if defined(_LCD_SEGD6H_MASK)
      else
      {
        bit -= 32;
        BUS_RegBitWrite(&(LCD->SEGD6H), bit, enable);
      }
#endif
      break;
#endif
#if defined(_LCD_SEGD7L_MASK)
    case 7:
      if (bit < 32)
      {
        BUS_RegBitWrite(&(LCD->SEGD7L), bit, enable);
      }
#if defined(_LCD_SEGD7H_MASK)
      else
      {
        bit -= 32;
        BUS_RegBitWrite(&(LCD->SEGD7H), bit, enable);
      }
#endif
      break;
#endif

    default:
      EFM_ASSERT(0);
      break;
  }
}


/***************************************************************************//**
 * @brief
 *   Updates the 0-31 lowest segments on a given COM-line in one operation,
 *   according to bit mask
 *
 * @param[in] com
 *   Which COM line to update
 *
 * @param[in] mask
 *   Bit mask for segments 0-31
 *
 * @param[in] bits
 *   Bit pattern for segments 0-31
 ******************************************************************************/
void LCD_SegmentSetLow(int com, uint32_t mask, uint32_t bits)
{
  uint32_t segData;

  /* Maximum number of com lines */
#if defined(_LCD_SEGD7L_MASK)
  EFM_ASSERT(com < 8);
#else
  /* Gecko Family supports up to 4 COM lines */
  EFM_ASSERT(com < 4);
#endif

  switch (com)
  {
    case 0:
      segData     = LCD->SEGD0L;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD0L = segData;
      break;
    case 1:
      segData     = LCD->SEGD1L;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD1L = segData;
      break;
    case 2:
      segData     = LCD->SEGD2L;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD2L = segData;
      break;
    case 3:
      segData     = LCD->SEGD3L;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD3L = segData;
      break;
#if defined(_LCD_SEGD4L_MASK)
    case 4:
      segData     = LCD->SEGD4L;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD4L = segData;
      break;
#endif
#if defined(_LCD_SEGD5L_MASK)
    case 5:
      segData     = LCD->SEGD5L;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD5L = segData;
      break;
#endif
#if defined(_LCD_SEGD6L_MASK)
    case 6:
      segData     = LCD->SEGD6L;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD6L = segData;
      break;
#endif
#if defined(_LCD_SEGD7L_MASK)
    case 7:
      segData     = LCD->SEGD7L;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD7L = segData;
      break;
#endif
    default:
      EFM_ASSERT(0);
      break;
  }
}


#if defined(_LCD_SEGD0H_MASK)
/***************************************************************************//**
 * @brief
 *   Updated the high (32-39) segments on a given COM-line in one operation
 *
 * @param[in] com
 *   Which COM line to update
 *
 * @param[in] mask
 *   Bit mask for segments 32-39
 *
 * @param[in] bits
 *   Bit pattern for segments 32-39
 ******************************************************************************/
void LCD_SegmentSetHigh(int com, uint32_t mask, uint32_t bits)
{
  uint32_t segData;

#if defined(_LCD_SEGD7H_MASK)
  EFM_ASSERT(com < 8);
#else
  EFM_ASSERT(com < 4);
#endif

  /* Maximum number of com lines */
  switch (com)
  {
    case 0:
      segData     = LCD->SEGD0H;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD0H = segData;
      break;
    case 1:
      segData     = LCD->SEGD1H;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD1H = segData;
      break;
    case 2:
      segData     = LCD->SEGD2H;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD2H = segData;
      break;
    case 3:
      segData     = LCD->SEGD3H;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD3H = segData;
      break;
#if defined(_LCD_SEGD4H_MASK)
    case 4:
      segData     = LCD->SEGD4H;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD4H = segData;
      break;
#endif
#if defined(_LCD_SEGD5H_MASK)
    case 5:
      segData     = LCD->SEGD5H;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD5H = segData;
      break;
#endif
#if defined(_LCD_SEGD6H_MASK)
    case 6:
      segData     = LCD->SEGD6H;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD6H = segData;
      break;
#endif
#if defined(_LCD_SEGD7H_MASK)
    case 7:
      segData     = LCD->SEGD7H;
      segData    &= ~(mask);
      segData    |= (mask & bits);
      LCD->SEGD7H = segData;
      break;
#endif
    default:
      break;
  }
}
#endif

/***************************************************************************//**
 * @brief
 *   Configure contrast level on LCD panel
 *
 * @param[in] level
 *   Contrast level in the range 0-31
 ******************************************************************************/
void LCD_ContrastSet(int level)
{
  EFM_ASSERT(level < 32);

  LCD->DISPCTRL = (LCD->DISPCTRL & ~_LCD_DISPCTRL_CONLEV_MASK)
                  | (level << _LCD_DISPCTRL_CONLEV_SHIFT);
}


/***************************************************************************//**
 * @brief
 *   Configure voltage booster
 *
 * The resulting voltage level is described in each part number's data sheet
 *
 * @param[in] vboost
 *   Voltage boost level
 ******************************************************************************/
void LCD_VBoostSet(LCD_VBoostLevel_TypeDef vboost)
{
  /* Reconfigure Voltage Boost */
  LCD->DISPCTRL = (LCD->DISPCTRL & ~_LCD_DISPCTRL_VBLEV_MASK) | vboost;
}


#if defined(LCD_CTRL_DSC)
/***************************************************************************//**
 * @brief
 *   Configure bias level for a specific segment line for Direct Segment Control
 *
 * @note
 *   When DSC is active, each configuration takes up 4 bits in the Segment
 *   Registers (SEGD0L/SEGD1H) which defines bias level.
 *   For optimal use of this feature, the entire SEGD-registers should be set
 *   at once in a optimized routine, so this function is mainly here to
 *   demonstrate how to correctly configure the bias levels, and should be used
 *   with care.
 *
 * @param[in] segmentLine
 *   Segment line number
 *
 * @param[in] biasLevel
 *   Bias configuration level, 0-4. This value must be within the constraint
 *   defined by the LCD_DISPCTRL bias setting, see Reference Manual/Datasheet
 ******************************************************************************/
void LCD_BiasSegmentSet(int segmentLine, int biasLevel)
{
  int               biasRegister;
  int               bitShift;
  volatile uint32_t *segmentRegister;

#if !defined(_LCD_SEGD0H_MASK)
  EFM_ASSERT(segmentLine < 20);

  /* Bias config for 8 segment lines per SEGDnL register */
  biasRegister = segmentLine / 8;
  bitShift     = (segmentLine % 8) * 4;

  switch (biasRegister)
  {
    case 0:
      segmentRegister = &LCD->SEGD0L;
      break;
    case 1:
      segmentRegister = &LCD->SEGD1L;
      break;
    case 2:
      segmentRegister = &LCD->SEGD2L;
      break;
    case 3:
      segmentRegister = &LCD->SEGD3L;
      break;
    default:
      segmentRegister = (uint32_t *)0x00000000;
      EFM_ASSERT(0);
      break;
  }
#else
  EFM_ASSERT(segmentLine < 40);

  /* Bias config for 10 segment lines per SEGDn L+H registers */
  biasRegister = segmentLine / 10;
  bitShift     = (segmentLine % 10) * 4;

  switch (biasRegister)
  {
    case 0:
      if (bitShift < 32)
      {
        segmentRegister = &LCD->SEGD0L;
      }
      else
      {
        segmentRegister = &LCD->SEGD0H;
        bitShift       -= 32;
      }
      break;
    case 1:
      if (bitShift < 32)
      {
        segmentRegister = &LCD->SEGD1L;
      }
      else
      {
        segmentRegister = &LCD->SEGD1H;
        bitShift       -= 32;
      }
      break;
    case 2:
      if (bitShift < 32)
      {
        segmentRegister = &LCD->SEGD2L;
      }
      else
      {
        segmentRegister = &LCD->SEGD1H;
        bitShift       -= 32;
      }
      break;
    case 3:
      if (bitShift < 32)
      {
        segmentRegister = &LCD->SEGD3L;
      }
      else
      {
        segmentRegister = &LCD->SEGD3H;
        bitShift       -= 32;
      }
      break;
    default:
      segmentRegister = (uint32_t *)0x00000000;
      EFM_ASSERT(0);
      break;
  }
#endif

  /* Configure new bias setting */
  *segmentRegister = (*segmentRegister & ~(0xF << bitShift)) | (biasLevel << bitShift);
}
#endif


#if defined(LCD_CTRL_DSC)
/***************************************************************************//**
 * @brief
 *   Configure bias level for a specific segment line
 *
 * @note
 *   When DSC is active, each configuration takes up 4 bits in the Segment
 *   Registers (SEGD4L/SEGD4H) which defines bias level.
 *   For optimal use of this feature, the entire SEGD-registers should be set
 *   at once in a optimized routine, so this function is mainly here to
 *   demonstrate how to correctly configure the bias levels, and should be used
 *   with care.
 *
 * @param[in] comLine
 *   COM line number, 0-7
 *
 * @param[in] biasLevel
 *   Bias configuration level, 0-4. This value must be within the constraint
 *   defined by the LCD_DISPCTRL bias setting, see Reference Manual/Datasheet
 ******************************************************************************/
void LCD_BiasComSet(int comLine, int biasLevel)
{
  int bitShift;
  EFM_ASSERT(comLine < 8);

  bitShift    = comLine * 4;
  LCD->SEGD4L = (LCD->SEGD4L & ~(0xF << bitShift)) | (biasLevel << bitShift);
}
#endif

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

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