/**
  ******************************************************************************
  * @file    sysclk.c
  * @author  Разваляев Алексей
  * @brief   Драйвер тактирования системы на основе PLIB035.
  *          Этот файл содержит:
  *           + Инициализацию системного тактирования (PLL, осцилляторы)
  *           + Настройку частоты ядра и периферии
  *           + Управление системным таймером (SysTick)
  *           + Функции для работы со временем (millis, micros)
  *           + Систему периодических коллбеков
  *           + Настройку тактирования периферии
  *
  ******************************************************************************
  * @attention
  *
  * Этот драйвер должен быть инициализирован ПЕРВЫМ в программе, до любой другой периферии.
  * Неправильная настройка тактирования может привести к неработоспособности всего МК.
  *
  * Использование этого драйвера предполагает наличие корректных настроек:
  *  - Определены константы SYSCLK_CORE_CLOCK_MHZ и SYSCLK_Oscil_Type в periph_config.h
  *  - Определен тип системного тика SYSCLK_TickType в periph_config.h
  *
  ******************************************************************************
  * @verbatim
  ==============================================================================
                        ##### Как использовать этот драйвер #####
  ==============================================================================
  
  1. Настройка в periph_config.h:
      (+) SYSCLK_CORE_CLOCK_MHZ - частота ядра в МГц (например, 100)
      (+) SYSCLK_Oscil_Type - источник тактирования (RCU_Oscil_OSE или RCU_Oscil_OSI)
      (+) SYSCLK_TickType - период системного тика (от SYSCLK_Tick_1us до SYSCLK_Tick_100ms)

  2. Инициализация (в начале main()):
      (+) sysclk_init() - настраивает PLL, SysTick и счетчики времени
      (+) В SysTick_Handler() вызвать sysclk_irq_handler()

  3. Работа со временем:
      (+) millis() - текущее время в миллисекундах (переполнение через 49 дней)
      (+) micros() - текущее время в микросекундах (точность зависит от SYSCLK_TickType)

  4. Периодические задачи:
      (+) SYSCLK_Set_Callback(func, period_ms) - регистрация функции для периодического вызова
      (+) Максимум 16 функций, период должен быть >= периода системного тика

  5. Настройка тактирования периферии:
      (+) rcu_set_clock_adc(source, freq_mhz, enable) - для АЦП
      (+) Частота АЦП не должна превышать 12.5 МГц согласно datasheet

  ==============================================================================
                          ##### Особенности работы #####
  ==============================================================================
  
  - Выбор SYSCLK_TickType влияет на:
      - Точность micros() (при 1ms тике micros() дает значения с шагом 1000)
      - Нагрузку на ЦП (1us = 1М прерываний в секунду, 100ms = 10 прерываний в секунду)
      - Минимальный период коллбеков (не может быть меньше SYSCLK_TickType)

  - При настройке PLL:
      - Драйвер автоматически подбирает делители для заданной частоты
      - Если частота недостижима - вызовется Error_Handler()
      - Всегда проверяйте supported frequencies в datasheet

  - Коллбеки выполняются в контексте прерывания SysTick:
      - Не должны выполняться долго
      - Не должны вызывать блокирующие функции

  @endverbatim
  ******************************************************************************
  */

/* Includes ------------------------------------------------------------------*/
#include "periph_config.h"

/* Private variables ---------------------------------------------------------*/
/** @brief Счетчик миллисекунд */
__IO uint32_t msTick = 0;

/** @brief Счетчик микросекунд */
__IO uint32_t usTick = 0;

/** @brief Инкремент для миллисекунд */
static __IO uint32_t msTickInc = 0;

/** @brief Инкремент для микросекунд */
static __IO uint32_t usTickInc = 0;

/** @brief Обработчик системных коллбеков */
static SYSCLK_CallbackHandleTypeDef hsyscb = {0};
//-- Defines -------------------------------------------------------------------


//-- Private function prototypes -----------------------------------------------
static inline void millis_inc(void);
static inline void micros_inc(void);

//-- Peripheral init functions -------------------------------------------------
/**
  * @brief  Инициализация системного тактирования.
  * @details Настраивает PLL, SysTick и счетчики времени.
  *          Должна быть вызвана первой в функции main().
  */
void sysclk_init(void)
{
  OperationStatus status;
  status = RCU_PLL_AutoConfig(SYSCLK_CORE_CLOCK_MHZ*__MHZ, SYSCLK_Oscil_Type);
  if (status == ERROR)
  {
    Error_Handler();
  }
  SystemCoreClockUpdate();
//  RCU_ClkOutConfig(RCU_SysPeriphClk_PLLClk, 1, ENABLE);
//  RCU_ClkOutCmd(ENABLE);
      
  SysTick_Config(SYSCLK_CORE_CLOCK_MHZ*__MHZ/SYSCLK_TickType);
  switch(SYSCLK_TickType)
  {
    case SYSCLK_Tick_1us:
      usTickInc = 1;
      msTickInc = 1;
      break;
    case SYSCLK_Tick_10us:
      usTickInc = 10;
      msTickInc = 1;
      break;
    case SYSCLK_Tick_100us:
      usTickInc = 100;
      msTickInc = 1;
      break;
    case SYSCLK_Tick_1ms:
      usTickInc = 1000;
      msTickInc = 1;
      break;
    case SYSCLK_Tick_10ms:
      usTickInc = 10000;
      msTickInc = 10;
      break;
    case SYSCLK_Tick_100ms:
      usTickInc = 100000;
      msTickInc = 100;
      break;
    default:
      /* Должен быть определен в periph_config.h */
      Error_Handler();
      break;
  }
}


/**
  * @brief  Общий обработчик прерываний SysClock
  * @details Обрабатывает мс и мкс и вызывает коллбеки
  */
void sysclk_irq_handler(void)
{
  static uint32_t usAccumulator = 0;  // Накопитель мкс
  /* Инкремент микросекунд */  
  micros_inc();
  
  if(msTickInc == 1)
  {
    /* Аккумулятивный метод для миллисекунд (без деления) */
    usAccumulator += usTickInc;
    if (usAccumulator >= 1000) {
        millis_inc();
        usAccumulator -= 1000;  // Вычитание быстрее деления
    }
  }
  else
  {
    millis_inc();
  }
  
  /* Вызов зарегистрированных коллбеков */
  for(int i = 0; i < hsyscb.CallbackInUse; i++)
  {
    if(hsyscb.Callback[i] != NULL)
    {
      /* Проверка истекшего времени */
      uint32_t elapsed = msTick - hsyscb.CallbackPrevMs[i];
      if(elapsed >= hsyscb.CallbackPeriod[i])
      {
        /* Обновление времени последнего вызова и вызов коллбека */
        hsyscb.CallbackPrevMs[i] = msTick;
        hsyscb.Callback[i]();
      }
    }
  }
}

/**
  * @brief  Добавление периодического коллбека.
  * @param  Callback Указатель на функцию-коллбек
  * @param  PeriodInMs Период вызова коллбека в миллисекундах
  * @retval OperationStatus OK при успехе, ERROR при ошибке
  * @note   Максимальное количество коллбеков: SYSCLK_NUMB_OF_CUSTOM_CALLBACKS
  * @note   Минимальный период: текущий период тика SysTick
  */
OperationStatus SYSCLK_Set_Callback(void (*Callback)(void), uint32_t PeriodInMs)
{
  /* Проверка валидности указателя на функцию */
  if(Callback == NULL)
    return ERROR;
  
  /* Проверка минимального периода */
  if(PeriodInMs < msTickInc)
    return ERROR;
  
  /* Проверка доступности свободных слотов */
  if(hsyscb.CallbackInUse >= SYSCLK_NUMB_OF_CUSTOM_CALLBACKS)
    return ERROR;
  
  /* Регистрация коллбека */
  hsyscb.Callback[hsyscb.CallbackInUse] = Callback;
  hsyscb.CallbackPeriod[hsyscb.CallbackInUse] = PeriodInMs;
  hsyscb.CallbackPrevMs[hsyscb.CallbackInUse] = msTick;
  
  hsyscb.CallbackInUse++;
  
  return OK;
}



/**
  * @brief  Получение текущего времени в миллисекундах.
  * @retval uint32_t Текущее время в миллисекундах
  * @note   Переполнение происходит через ~49 дней
  */
uint32_t millis(void)
{
  return msTick;
}

/**
  * @brief  Получение текущего времени в микросекундах.
  * @retval uint32_t Текущее время в микросекундах
  * @note   Переполнение происходит через ~71 минуту
  */
uint32_t micros(void)
{
  return usTick;
}



/**
  * @brief  Настройка тактирования АЦП.
  * @param  ClkSrc Источник тактирования (RCU_PeriphClk_TypeDef)
  * @param  ClkMHz Желаемая частота АЦП в МГц
  * @param  state  Состояние (ENABLE/DISABLE)
  * @retval OperationStatus OK при успехе, ERROR при ошибке
  * @note   Временное отключает тактирование АЦП во время настройки
  */
OperationStatus rcu_set_clock_adc(RCU_PeriphClk_TypeDef ClkSrc, float ClkMHz, FunctionalState state)
{
  uint32_t adc_raw_clock = 0;
  float adc_clock_div = 0;
  
  /* Отключение тактирования АЦП для настройки */
  RCU_ADCClkCmd(DISABLE);
  
  /* Определение частоты источника тактирования */
  switch(ClkSrc)
  {
    case RCU_PeriphClk_OSEClk:
      adc_raw_clock = RCU_GetOSEClkFreq();
      break;
    case RCU_PeriphClk_OSIClk:
      adc_raw_clock = RCU_GetOSIClkFreq();
      break;
    case RCU_PeriphClk_PLLClk:
      adc_raw_clock = RCU_GetPLLClkFreq();
      break;
    case RCU_PeriphClk_PLLDivClk:
      adc_raw_clock = RCU_GetPLLDivClkFreq();
      break;
    default:
      return ERROR;
  }
  
  /* Расчет делителя частоты */
  adc_clock_div = adc_raw_clock / (ClkMHz * __MHZ);
  if(adc_clock_div < 1)
    return ERROR;
  
  /* Настройка источника тактирования и делителя */
  RCU_ADCClkConfig(ClkSrc, (uint32_t)(adc_clock_div - 1), ENABLE);
  
  /* Включение тактирования, если запрошено */
  if(state == ENABLE)
  {
    RCU_ADCClkCmd(ENABLE);
  }
  
  return OK;
}




/**
  * @brief  Инкремент счетчика миллисекунд.
  */
static inline void millis_inc(void)
{
  msTick+=msTickInc;
}
/**
* @brief  Инкремент счетчика микросекунд.
  */
static inline void micros_inc(void)
{
  usTick+=usTickInc;
}
