 /*

	PWM.c - PWM API file.
   includes:
      PWM_Output_Init, PWM_Enable, PWM_Disable, PWM_SetFreq, PWM_SetDC, PWM_SetDT

   Copyright (C) 2011 Christos Choutouridis (http://houtouridis.blogspot.com/)

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.

   Author:     Christos Choutouridis <houtouridis.ch@gmail.com>
   Date:       11/2011
   Version:    0.1

*/

#include <PWM.h>


/*================   Static Functions    ======================*/

static int _pow (int base, int exp)
{
   int r=1;
   while (exp)
   {
      r *= base;
   }
   return r;
}

/**
   PWM_Timer_Freq_t  GetTimerFreq (PWM_Timer_t *t)
      
  * @brief  Returns the Timer Frequency
  * @param  PWM_Timer_t *t    : The desired Timer
  * @retval PWM_Timer_Freq_t  : The Timer's frequency
*/
static PWM_Timer_Freq_t  GetTimerFreq (PWM_Timer_t *t)
{
   uint8_t           AHB_Div, ABP1_Div, APB2_Div;

   Sys_Sysclk_Update ();
   // Get Prescalers and TimerFreq;
   AHB_Div = (RCC->CFGR & RCC_CFGR_HPRE) >> 4;
   ABP1_Div = (RCC->CFGR & RCC_CFGR_PPRE1) >> 8;
   if ( !(AHB_Div & 0x08))    AHB_Div = 1;
   else                       AHB_Div = 2 * (_pow (2, ABP1_Div & 0x07));
   ///*ABP1_Div = (RCC->CFGR & RCC_CFGR_PPRE1) >> 8;
   if ( !(ABP1_Div & 0x04))   ABP1_Div = 1;
   else                       ABP1_Div = 2 * (_pow (2, ABP1_Div & 0x03));
   APB2_Div = (RCC->CFGR & RCC_CFGR_PPRE2) >> 11;
   if ( !(APB2_Div & 0x04))   APB2_Div = 1;
   else                       APB2_Div = 2 * (_pow (2, APB2_Div & 0x03));
   
   if ((t == TIM2) || (t == TIM3) || (t == TIM4) || (t == TIM6) || (t == TIM7))
      return (PWM_Timer_Freq_t) SystemCoreClock / (AHB_Div * APB2_Div);
   if ((t == TIM1) || (t == TIM15) || (t == TIM16) || (t == TIM17))
      return (PWM_Timer_Freq_t) SystemCoreClock / (AHB_Div * APB2_Div * ABP1_Div);
   return 0;
}

/**
   PWM_PSC_t  PSC_Calc (PWM_Timer_Freq_t tc, PWM_Freq_t f)
      
  * @brief  Calculates the Prescaler needed for PWM frequency
            Depending on Timer frequency
  * @param  PWM_Timer_Freq_t tc  :  The timer frequency
            PWM_Freq_t f         :  The desired PWM frequency
  * @retval PWM_PSC_t            :  The prescaler for
*/
static PWM_PSC_t  PSC_Calc (PWM_Timer_Freq_t tc, PWM_Freq_t f)
{
   PWM_PSC_t         psc = 1;
   uint32_t          r = 0;

   // Prescaler and AutoReload Register calculation
   r = tc / (int)f;
   if ( r < PWM_MINIMUM_STEPS )
      return 0;

   while ( (tc / ((int)f * psc)) > PWM_MAXIMUM_STEPS)
      ++psc;
   if (psc > PWM_MAXIMUM_STEPS)
      return 0;
   return psc;
}





/*================  Exported Functions   ======================*/

/**
   PWM_EXIT_t   PWM_Output_Init (PWM_t *pwm)
      
  * @brief  This is the initialization rutine. Sets Timer for
            PWM output
  * @param  PWM_t *pwm:    The desired PWM
  * @retval PWM_EXIT_t
*/
PWM_EXIT_t   PWM_Output_Init (PWM_t *pwm)
{
   //Clock the dahm Timer module
   if (pwm->tim==TIM1)  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
   if (pwm->tim==TIM2)  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
   if (pwm->tim==TIM3)  RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
   #ifdef RCC_APB1ENR_TIM4EN
      if (pwm->tim==TIM4)  RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
   #endif
   if (pwm->tim==TIM6)  RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
   if (pwm->tim==TIM7)  RCC->APB1ENR |= RCC_APB1ENR_TIM7EN;
   if (pwm->tim==TIM15) RCC->APB2ENR |= RCC_APB2ENR_TIM15EN;
   if (pwm->tim==TIM16) RCC->APB2ENR |= RCC_APB2ENR_TIM16EN;
   if (pwm->tim==TIM17) RCC->APB2ENR |= RCC_APB2ENR_TIM17EN;
   
   #if   PWM_DEBUG_MODE == 1
   if (pwm->tim==TIM1)  DBGMCU->CR |= DBGMCU_CR_DBG_TIM1_STOP;
   if (pwm->tim==TIM2)  DBGMCU->CR |= DBGMCU_CR_DBG_TIM2_STOP;
   if (pwm->tim==TIM3)  DBGMCU->CR |= DBGMCU_CR_DBG_TIM3_STOP;
   #ifdef RCC_APB1ENR_TIM4EN
      if (pwm->tim==TIM4)  DBGMCU->CR |= DBGMCU_CR_DBG_TIM4_STOP;
   #endif
   if (pwm->tim==TIM6)  DBGMCU->CR |= DBGMCU_CR_DBG_TIM6_STOP;
   if (pwm->tim==TIM7)  DBGMCU->CR |= DBGMCU_CR_DBG_TIM7_STOP;
   if (pwm->tim==TIM15) DBGMCU->CR |= DBGMCU_CR_DBG_TIM15_STOP;
   if (pwm->tim==TIM16) DBGMCU->CR |= DBGMCU_CR_DBG_TIM16_STOP;
   if (pwm->tim==TIM17) DBGMCU->CR |= DBGMCU_CR_DBG_TIM17_STOP;
   #endif //#if   PWM_DEBUG_MODE == 1
   
   if (!pwm->dc)     pwm->dc = PWM_STD_DC;
   if (!pwm->freq)   pwm->freq = PWM_STD_FREQ;
   
   if (PWM_SetFreq (pwm, pwm->freq))
      return PWM_EXIT_ERROR;
   if (PWM_SetDC (pwm, pwm->dc))
      return PWM_EXIT_ERROR;

   pwm->tim->CR1 |= TIM_CR1_ARPE;   //Enable auto-reload
   // Up - Down selection in comparison with modes
   if ((pwm->mode == PWM_MODE_1_DOWN || pwm->mode == PWM_MODE_2_DOWN) &&
       (pwm->tim==TIM1 || pwm->tim==TIM2 || pwm->tim==TIM3 || pwm->tim==TIM4)
      )
      pwm->tim->CR1 |= TIM_CR1_DIR;

   // Channel Selection
   switch (pwm->ch)
   {
      case TIM_CH1:     
         pwm->tim->CCER &= ~TIM_CCER_CC1P;
         goto COM1;
      case TIM_CH1N:
         pwm->tim->CCER &= ~TIM_CCER_CC1NP;
         goto COM1;
      case TIM_CH1CMP:
         pwm->tim->CCER &= ~(TIM_CCER_CC1P + TIM_CCER_CC1NP);
         //add dead-time
      COM1:
         if (pwm->mode == PWM_MODE_1_UP || pwm->mode == PWM_MODE_1_DOWN)
            pwm->tim->CCMR1 |= PWM_TIM_CCMR1_1_MODE1;
         else
            pwm->tim->CCMR1 |= PWM_TIM_CCMR1_1_MODE2;
         pwm->tim->CCMR1 |= TIM_CCMR1_OC1PE;
         break;

      case TIM_CH2:
         pwm->tim->CCER &= ~TIM_CCER_CC2P;
         goto COM2;
      case TIM_CH2N:
         pwm->tim->CCER &= ~TIM_CCER_CC2NP;
         goto COM2;
      case TIM_CH2CMP:
         pwm->tim->CCER &= ~(TIM_CCER_CC2P + TIM_CCER_CC2NP);
         //add dead-time
      COM2:
         if (pwm->mode == PWM_MODE_1_UP || pwm->mode == PWM_MODE_1_DOWN)
            pwm->tim->CCMR1 |= PWM_TIM_CCMR1_2_MODE1;
         else
            pwm->tim->CCMR1 |= PWM_TIM_CCMR1_2_MODE2;
         pwm->tim->CCMR1 |= TIM_CCMR1_OC2PE;  
         break;

      case TIM_CH3:     
         pwm->tim->CCER &= ~TIM_CCER_CC3P;
         goto COM3;
      case TIM_CH3N:
         pwm->tim->CCER &= ~TIM_CCER_CC3NP;
         goto COM3;
      case TIM_CH3CMP:
         pwm->tim->CCER &= ~(TIM_CCER_CC3P + TIM_CCER_CC3NP);
         //add dead-time
      COM3:
         if (pwm->mode == PWM_MODE_1_UP || pwm->mode == PWM_MODE_1_DOWN)
            pwm->tim->CCMR1 |= PWM_TIM_CCMR2_3_MODE1;
         else
            pwm->tim->CCMR1 |= PWM_TIM_CCMR2_3_MODE2;
         pwm->tim->CCMR2 |= TIM_CCMR2_OC3PE;  
         break;

      case TIM_CH4:
         if (pwm->mode == PWM_MODE_1_UP || pwm->mode == PWM_MODE_1_DOWN)
            pwm->tim->CCMR1 |= PWM_TIM_CCMR2_4_MODE1;
         else
            pwm->tim->CCMR1 |= PWM_TIM_CCMR2_4_MODE2;
         pwm->tim->CCMR2 |= TIM_CCMR2_OC4PE;       
         pwm->tim->CCER &= ~TIM_CCER_CC4P;
         break;
   }
   pwm->tim->BDTR |= TIM_BDTR_MOE + TIM_BDTR_OSSR;

   return PWM_EXIT_OK;
}



/**
   PWM_EXIT_t  PWM_Enable (PWM_t *pwm)
      
  * @brief  This enables the PWM output and starts it's timer
  * @param  PWM_t *pwm:    The desired PWM
  * @retval PWM_EXIT_t
*/
PWM_EXIT_t  PWM_Enable (PWM_t *pwm)
{
   // Channel Selection
   switch (pwm->ch)
   {
      case TIM_CH1:
         pwm->tim->CCER |= TIM_CCER_CC1E;  break;
      case TIM_CH1N:
         pwm->tim->CCER |= TIM_CCER_CC1NE; break;
      case TIM_CH1CMP:
         pwm->tim->CCER |= TIM_CCER_CC1E + TIM_CCER_CC1NE; break;
      case TIM_CH2:
         pwm->tim->CCER |= TIM_CCER_CC2E; break;
      case TIM_CH2N:
         pwm->tim->CCER |= TIM_CCER_CC2NE; break;
      case TIM_CH2CMP:
         pwm->tim->CCER |= TIM_CCER_CC2E + TIM_CCER_CC2NE; break;
      case TIM_CH3:
         pwm->tim->CCER |= TIM_CCER_CC3E; break;
      case TIM_CH3N:
         pwm->tim->CCER |= TIM_CCER_CC3NE; break;
      case TIM_CH3CMP:
         pwm->tim->CCER |= TIM_CCER_CC3E + TIM_CCER_CC3NE; break;
      case TIM_CH4:
         pwm->tim->CCER |= TIM_CCER_CC4E; break;
   }

   pwm->tim->CR1 |= TIM_CR1_CEN;
   
   return PWM_EXIT_OK;
}


/**
   PWM_EXIT_t  PWM_Disable (PWM_t *pwm)
      
  * @brief  This disables the PWM output and stops it's timer
  * @param  PWM_t *pwm:    The desired PWM
  * @retval PWM_EXIT_t
*/
PWM_EXIT_t  PWM_Disable (PWM_t *pwm)
{
   switch (pwm->ch)
   {
      case TIM_CH1:
         pwm->tim->CCER &= ~TIM_CCER_CC1E; break;
      case TIM_CH1N:
         pwm->tim->CCER &= ~TIM_CCER_CC1NE; break;
      case TIM_CH1CMP:
         pwm->tim->CCER &= ~(TIM_CCER_CC1E + TIM_CCER_CC1NE); break;
      case TIM_CH2:
         pwm->tim->CCER &= ~TIM_CCER_CC2E; break;
      case TIM_CH2N:
         pwm->tim->CCER &= ~TIM_CCER_CC2NE; break;
      case TIM_CH2CMP:
         pwm->tim->CCER &= ~(TIM_CCER_CC2E + TIM_CCER_CC2NE); break;
      case TIM_CH3:
         pwm->tim->CCER &= ~TIM_CCER_CC3E; break;
      case TIM_CH3N:
         pwm->tim->CCER &= ~TIM_CCER_CC3NE; break;
      case TIM_CH3CMP:
         pwm->tim->CCER &= ~(TIM_CCER_CC3E + TIM_CCER_CC3NE); break;
      case TIM_CH4:
         pwm->tim->CCER &= ~TIM_CCER_CC4E; break;
   }
   pwm->tim->CR1 &= ~TIM_CR1_CEN;
   
   return PWM_EXIT_OK;
}


/**
   PWM_EXIT_t  PWM_SetFreq (PWM_t *pwm, PWM_Freq_t f)
      
  * @brief  This function sets the PWM frequency. On success
            re-sets the DC and DT also.
  * @param  PWM_Freq_t t:  The PWM frequency in (float)Hz
            PWM_t *pwm:    The desired PWM
  * @retval PWM_EXIT_t
*/
PWM_EXIT_t  PWM_SetFreq (PWM_t *pwm, PWM_Freq_t f)
{
   PWM_Timer_Freq_t  tim_clk = GetTimerFreq(pwm->tim);
   PWM_PSC_t         psc = PSC_Calc (tim_clk, f);
   PWM_EXIT_t        ret1, ret2 = PWM_EXIT_OK;;

   if ( !psc )          return PWM_EXIT_ERROR;

   pwm->freq = f;
   pwm->tim_freq = tim_clk / psc;
   pwm->tim->PSC = psc - 1;         //prescaler
   pwm->tim->ARR = pwm->tim_freq / pwm->freq;

   ret1 = PWM_SetDC (pwm, pwm->dc);
   if (pwm->dt)         ret2 = PWM_SetDT (pwm, pwm->dt);

   if (ret1||ret2)      return PWM_EXIT_ERROR;
   else                 return PWM_EXIT_OK;
}


/**
   PWM_EXIT_t  PWM_SetDC (PWM_t *pwm, PWM_DC_t dc)
      
  * @brief  This function sets the Duty cycle
  * @param  PWM_DC_t t:    duty cycle in (int) %
            PWM_t *pwm:    The desired PWM
  * @retval PWM_EXIT_t
*/
PWM_EXIT_t  PWM_SetDC (PWM_t *pwm, PWM_DC_t dc)
{
   if ((dc>100) || (dc<0))
      return PWM_EXIT_ERROR;
   pwm->dc = dc;
   pwm->tim->CCR1 = (pwm->tim->ARR * pwm->dc) / 100.0;

   return PWM_EXIT_OK;
}


/**
   PWM_EXIT_t  PWM_SetDT (PWM_t *pwm, PWM_DT_t dt)
      
  * @brief  This function calculates and sets the DTG in
            BDTR register for the requested deadtime.
  * @param  PWM_DT_t t:    deadtime in sec
            PWM_t *pwm:    The desired PWM
  * @retval PWM_EXIT_t
*/
PWM_EXIT_t  PWM_SetDT (PWM_t *pwm, PWM_DT_t dt)
{
   PWM_DTG_t DTG;

   if (pwm->ch != TIM_CH1CMP)   // Dead time only in timer 1 complementary
      return PWM_EXIT_ERROR;

   if (dt <= PWM_DT_A_MAX * 1.0 / (float)pwm->tim_freq)
      DTG =  PWM_DTG_A_MASK | PWM_DTG_A (dt, pwm->tim_freq);
   else if (dt <= PWM_DT_B_MAX * 1.0 / (float)pwm->tim_freq)
      DTG =  PWM_DTG_B_MASK | PWM_DTG_B (dt, pwm->tim_freq);
   else if (dt <= PWM_DT_C_MAX * 1.0 / (float)pwm->tim_freq)
      DTG =  PWM_DTG_C_MASK | PWM_DTG_C (dt, pwm->tim_freq);
   else if (dt <= PWM_DT_D_MAX * 1.0 / (float)pwm->tim_freq)
      DTG =  PWM_DTG_D_MASK | PWM_DTG_D (dt, pwm->tim_freq);
   else
      return PWM_EXIT_ERROR;

   pwm->tim->BDTR |= DTG;
   return PWM_EXIT_OK;
}

