вівторок, 22 березня 2016 р.

Таймери STM32 -- захоплення вводу/HAL

Попрацюємо тепер із захопленням вводу засобами HAL. Як і в попередньому пості, фіксуватимемо час натискання кнопки. Вся логіка роботи -- та ж, що у згаданому пості -- по натисканню клавіші, під'єднаної до PA0, пін притягується до одинички, таймер у відповідь повинен зберегти момент натискання. В головному циклі програма виводить отримані значення, користуючись Semihosting. 

Традиційно, створюємо проект STM32CubeMX, вибираємо мікроконтролер і т.д. Далі, налаштовуємо таймер та пін кнопки:

Налаштування таймера. Назва піну (підкреслену чорним) задається на іншій вкладці -- Configuration
  • Для піна обираємо TIM2_CH1
  • Таймер тактується як звично -- Internal clock
  • Канал 1 працює в режимі: Input capture direct, безпосереднього захоплення вводу
Переходимо до вкладки Configure, тиснемо TIM2 і задаємо необхідні параметри:

  • Подільник -- 24000-1, на частоті мікроконтролера 24МГц це дасть один відлік в мілісекунду
  • Внизу є налаштування каналу -- реакція на фронт сигналу, враховувати кожен виклик, фільтр -- "на максимум", щоб мінімізувати дрижання (див. попередній пост за подробицями).
Незвичним може видатися ідентифікатор reload_val в полі Counter period (на нього вказують стрілки). Величина, вказана в цьому полі, нам знадобиться в коді -- для абсолютного відліку часу від моменту запуску таймера. Вручну дублювати -- дуже погана ідея! При цьому всі зміни теж доведеться дублювати вручну -- просто ідеальне джерело помилок. ;-) Cube надає можливість вирішити цю проблему -- створювати свої константи безпосередньо в цьому діалозі, на вкладці User Constants:

Створені таким чином константи можна використовувати у тих полях Cube, де слід вказати чисельні значення (ну, якщо чесно, я не перевіряв, чи у всіх таких полях, але в багатьох -- точно можна :-) -- як і зроблено на поза-попередньому зображенні.

Також, на вкладці GPIO Settings можна назвати пін,  USER_BTN_PA0_TIM2CH1 в цьому випадку:

Можна генерувати код (та конвертувати його для не дуже пристосованого для роботи з "Cube" CoIDE...):

Переходимо до коду.

Структури даних (як би це пафосно не звучало для такої примітивної конструкції) ті ж, що і раніше (але, зауважте, розташовуємо їх між коментарями, на які орієнтується Cube при перегенерації коду):

/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
#define  rb_buf_size 16 // Чистий С const не сприйме тут
volatile uint64_t raw_data[rb_buf_size];
volatile uint64_t abs_data[rb_buf_size];

volatile bool buffer_is_full = false;
volatile bool overcapture = false;

volatile ring_buffer_t raw_buffer = {raw_data, rb_buf_size, 0, 0};
volatile ring_buffer_t abs_buffer = {abs_data, rb_buf_size, 0, 0};

volatile uint64_t time_from_poweron = 0;

/* USER CODE END PV */

Нам потрібно реагувати на події захоплення та переповнення (для глобального відліку часу), тому заміщаємо відповідні слабкі обробники (подробиці див. попередні пости серії):

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
 if( htim->Instance == TIM2 )
 {
  ++time_from_poweron;
 }
}

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
 if( htim->Instance == TIM2  )
 {
  if( htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1 )
  {
   uint64_t res = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
   buffer_is_full = !rb_append(&raw_buffer, res);
   rb_append(&abs_buffer, reload_val*time_from_poweron + res);
   /* //Try overcapture
   int x = 0;
    while(x<10000000)
     ++x;
   */
  }

  if( __HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC1OF) )
  {
   __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC1OF);
   overcapture = true;
  }
 }
}

HAL_TIM_IC_CaptureCallback містить кілька "новинок":
  • Інформація про те, який канал опрацьовує конкретний виклик, знаходиться в htim->Channel, тому ми перевіряємо, чи там HAL_TIM_ACTIVE_CHANNEL_1, і якщо так -- реагуємо.
  • Для читання захопленого значення (фактично, вмісту CCR1), використовується функція HAL_TIM_ReadCapturedValue().
  • Перевірка, чи не відбулося overcapture, здійснюється макросом __HAL_TIM_GET_FLAG(), очищення відповідного біта -- __HAL_TIM_CLEAR_FLAG().
Формально, в цьому прикладі, від іншого каналу чи від іншого таймеру переривання прийти не може -- можна б було опустити перевірки, але, по перше -- це погана практика, вона, зазвичай, не вартує економії десятка байт пам'яті програм навіть на мікроконтролерах, по друге -- приклад навчальний, має демонструвати відповідні техніки написання коду, та й навіть в трішки складніших навчальних прикладах така перевірка стає обов'язковою
Переходимо до коду main(). Спочатку йде звична ініціалізація: 

 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM2_Init();

Далі -- запускаємо таймер, переривання від каналу буде дозволено цим викликом (див. суфікс _IT), але переривання переповнення слід дозволити окремо:

/* USER CODE BEGIN 2 */

  --time_from_poweron; // Чомусь зразу прилітає одна подія оновлення...
 // Тому віднімаємо 1, щоб після неї почати з нуля.

  HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
  __HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
  /* USER CODE END 2 */

Логіка роботи програми залишилася тією ж, що і раніше, єдине, замість NVIC_EnableIRQ() використовується HAL_NVIC_EnableIRQ(), котра перевіряє (assert-ом) аргументи та викликає NVIC_EnableIRQ(), тому код тут не наводжу --- див. попередній пост або качайте готовий проект тут.

Ініціалізація

Традиційно, подивимося, що там нагенерував нам Cube.

/* TIM2 init function */
void MX_TIM2_Init(void)
{
  TIM_ClockConfigTypeDef sClockSourceConfig;
  TIM_MasterConfigTypeDef sMasterConfig;
  TIM_IC_InitTypeDef sConfigIC;

  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 23999;
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = reload_val;
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_Base_Init(&htim2);

  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig);

  HAL_TIM_IC_Init(&htim2);

  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);

  sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
  sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
  sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
  sConfigIC.ICFilter = 15;
  HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);

}

Перша частина звична, новим є ініціалізація Input capture викликом HAL_TIM_IC_Init() та відповідного каналу, викликом HAL_TIM_IC_ConfigChannel(). Останній функції передається структура типу TIM_IC_InitTypeDef, в якій цілком само-документовано (особливо якщо глянути на скріншоти вище) вказано: ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING, ICSelection = TIM_ICSELECTION_DIRECTTI, ICPrescaler = TIM_ICPSC_DIV1, ICFilter = 15.

Перш ніж детальніше розібрати ці функції, наведемо також функцію зворотного виклику, якою HAL ініціалізуватиме піни та переривання таймера (а то давно ми ці Msp-функції не нагадували собі):

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{

  GPIO_InitTypeDef GPIO_InitStruct;
  if(htim_base->Instance==TIM2)
  {
  /* USER CODE BEGIN TIM2_MspInit 0 */

  /* USER CODE END TIM2_MspInit 0 */
    /* Peripheral clock enable */
    __TIM2_CLK_ENABLE();
  
    /**TIM2 GPIO Configuration    
    PA0-WKUP     ------> TIM2_CH1 
    */
    GPIO_InitStruct.Pin = USER_BTN_PA0_TIM2CH1_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(USER_BTN_PA0_TIM2CH1_GPIO_Port, &GPIO_InitStruct);

    /* Peripheral interrupt init*/
    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);
  /* USER CODE BEGIN TIM2_MspInit 1 */

  /* USER CODE END TIM2_MspInit 1 */
  }
}

Все у ній ніби зрозуміло, але пам'ятати про необхідність цих операцій, слід.

Отож, HAL_TIM_IC_Init():

HAL_StatusTypeDef HAL_TIM_IC_Init(TIM_HandleTypeDef *htim)
{
  /* Check the TIM handle allocation */
  if(htim == NULL)
  {
    return HAL_ERROR;
  }

  /* Check the parameters */
  assert_param(IS_TIM_INSTANCE(htim->Instance));
  assert_param(IS_TIM_COUNTER_MODE(htim->Init.CounterMode));
  assert_param(IS_TIM_CLOCKDIVISION_DIV(htim->Init.ClockDivision));

  if(htim->State == HAL_TIM_STATE_RESET)
  {
    /* Allocate lock resource and initialize it */
    htim->Lock = HAL_UNLOCKED;
    
    /* Init the low level hardware : GPIO, CLOCK, NVIC and DMA */
    HAL_TIM_IC_MspInit(htim);
  }

  /* Set the TIM state */
  htim->State= HAL_TIM_STATE_BUSY;

  /* Init the base time for the input capture */
  TIM_Base_SetConfig(htim->Instance, &htim->Init);

  /* Initialize the TIM state*/
  htim->State= HAL_TIM_STATE_READY;

  return HAL_OK;
}

Як завжди -- перевіряються параметри, якщо все добре, викликається функція зворотного виклику для ініціалізації, HAL_TIM_IC_MspInit() -- ми (чи Cube) її не заміщали, далі викликає TIM_Base_SetConfig(), яку ми розглядали раніше.

Ініціалізація каналу трішки цікавіша:

HAL_StatusTypeDef HAL_TIM_IC_ConfigChannel(TIM_HandleTypeDef *htim, TIM_IC_InitTypeDef* sConfig, uint32_t Channel)
{
  /* Check the parameters */
  assert_param(IS_TIM_CC1_INSTANCE(htim->Instance));
  assert_param(IS_TIM_IC_POLARITY(sConfig->ICPolarity));
  assert_param(IS_TIM_IC_SELECTION(sConfig->ICSelection));
  assert_param(IS_TIM_IC_PRESCALER(sConfig->ICPrescaler));
  assert_param(IS_TIM_IC_FILTER(sConfig->ICFilter));

  __HAL_LOCK(htim);

  htim->State = HAL_TIM_STATE_BUSY;

  if (Channel == TIM_CHANNEL_1)
  {
    /* TI1 Configuration */
    TIM_TI1_SetConfig(htim->Instance,
               sConfig->ICPolarity,
               sConfig->ICSelection,
               sConfig->ICFilter);

    /* Reset the IC1PSC Bits */
    htim->Instance->CCMR1 &= ~TIM_CCMR1_IC1PSC;

    /* Set the IC1PSC value */
    htim->Instance->CCMR1 |= sConfig->ICPrescaler;
  }
  else if (Channel == TIM_CHANNEL_2)
  {
    /* TI2 Configuration */
   //...................skipped...................
  }
  else if (Channel == TIM_CHANNEL_3)
  {
    /* TI3 Configuration */
//...................skipped...................
  }
  else
  {
    /* TI4 Configuration */
//...................skipped...................
  }

  htim->State = HAL_TIM_STATE_READY;

  __HAL_UNLOCK(htim);

  return HAL_OK;
}

Вона, для обраного каналу, викликає TIM_TIx_SetConfig (де x -- номер каналу), потім встановлює подільник, безпосередньо маніпулюючи регістром CCMR1.

TIM_TI1_SetConfig(), у свою чергу, доналаштовує решту:

void TIM_TI1_SetConfig(TIM_TypeDef *TIMx, uint32_t TIM_ICPolarity, uint32_t TIM_ICSelection,
                       uint32_t TIM_ICFilter)
{
  uint32_t tmpccmr1 = 0;
  uint32_t tmpccer = 0;

  /* Disable the Channel 1: Reset the CC1E Bit */
  TIMx->CCER &= ~TIM_CCER_CC1E;
  tmpccmr1 = TIMx->CCMR1;
  tmpccer = TIMx->CCER;

  /* Select the Input */
  if(IS_TIM_CC2_INSTANCE(TIMx) != RESET)
  {
    tmpccmr1 &= ~TIM_CCMR1_CC1S;
    tmpccmr1 |= TIM_ICSelection;
  }
  else
  {
    tmpccmr1 |= TIM_CCMR1_CC1S_0;
  }

  /* Set the filter */
  tmpccmr1 &= ~TIM_CCMR1_IC1F;
  tmpccmr1 |= ((TIM_ICFilter << 4) & TIM_CCMR1_IC1F);

  /* Select the Polarity and set the CC1E Bit */
  tmpccer &= ~(TIM_CCER_CC1P | TIM_CCER_CC1NP);
  tmpccer |= (TIM_ICPolarity & (TIM_CCER_CC1P | TIM_CCER_CC1NP));

  /* Write to TIMx CCMR1 and CCER registers */
  TIMx->CCMR1 = tmpccmr1;
  TIMx->CCER = tmpccer;
}

Для тих, хто читав "симетричний" CMSIS пост, все має бути зрозумілим.

З ініціалізацією розібралися. Поглянемо ще на використані в коді програми функції HAL

Використані функції та макроси HAL

Читання захопленого значення:

uint32_t HAL_TIM_ReadCapturedValue(TIM_HandleTypeDef *htim, uint32_t Channel)
{
  uint32_t tmpreg = 0;

  __HAL_LOCK(htim);

  switch (Channel)
  {
  case TIM_CHANNEL_1:
    {
      /* Check the parameters */
      assert_param(IS_TIM_CC1_INSTANCE(htim->Instance));

      /* Return the capture 1 value */
      tmpreg =  htim->Instance->CCR1;

      break;
    }
  case TIM_CHANNEL_2:
    {
      /* Check the parameters */
      assert_param(IS_TIM_CC2_INSTANCE(htim->Instance));

      /* Return the capture 2 value */
      tmpreg =   htim->Instance->CCR2;

      break;
    }

  case TIM_CHANNEL_3: // ................... skipped .....................

  case TIM_CHANNEL_4: // ................... skipped .....................

  default:
    break;
  }

  __HAL_UNLOCK(htim);
  return tmpreg;
}  

Все просто, до примітивного -- обирається канал, за його номером, читається відповідний регістр CCRx. Все ж, з використанням шаблонів С++, це ж можна було зробити значно ефективніше...

Маніпуляція із прапорцями подій: 

/**
  * @brief  Checks whether the specified TIM interrupt flag is set or not.
  * @param  __HANDLE__: specifies the TIM Handle.
  * @param  __FLAG__: specifies the TIM interrupt flag to check.
  *        This parameter can be one of the following values:
  *            @arg TIM_FLAG_UPDATE: Update interrupt flag
  *            @arg TIM_FLAG_CC1: Capture/Compare 1 interrupt flag
  *            @arg TIM_FLAG_CC2: Capture/Compare 2 interrupt flag
  *            @arg TIM_FLAG_CC3: Capture/Compare 3 interrupt flag
  *            @arg TIM_FLAG_CC4: Capture/Compare 4 interrupt flag
  *            @arg TIM_FLAG_COM:  Commutation interrupt flag
  *            @arg TIM_FLAG_TRIGGER: Trigger interrupt flag
  *            @arg TIM_FLAG_BREAK: Break interrupt flag   
  *            @arg TIM_FLAG_CC1OF: Capture/Compare 1 overcapture flag
  *            @arg TIM_FLAG_CC2OF: Capture/Compare 2 overcapture flag
  *            @arg TIM_FLAG_CC3OF: Capture/Compare 3 overcapture flag
  *            @arg TIM_FLAG_CC4OF: Capture/Compare 4 overcapture flag
  * @retval The new state of __FLAG__ (TRUE or FALSE).
  */
#define __HAL_TIM_GET_FLAG(__HANDLE__, __FLAG__)          (((__HANDLE__)->Instance->SR &(__FLAG__)) == (__FLAG__))

#define __HAL_TIM_CLEAR_FLAG(__HANDLE__, __FLAG__)        ((__HANDLE__)->Instance->SR = ~(__FLAG__))

Як видно -- шар абстракції зоооовсім тоненький. :-)

Такі же прості макроси маніпулюють дозволом на переривання від таймера:

/**
  * @brief  Enables the specified TIM interrupt.
  * @param  __HANDLE__: specifies the TIM Handle.
  * @param  __INTERRUPT__: specifies the TIM interrupt source to enable.
  *          This parameter can be one of the following values:
  *            @arg TIM_IT_UPDATE: Update interrupt
  *            @arg TIM_IT_CC1:   Capture/Compare 1 interrupt
  *            @arg TIM_IT_CC2:  Capture/Compare 2 interrupt
  *            @arg TIM_IT_CC3:  Capture/Compare 3 interrupt
  *            @arg TIM_IT_CC4:  Capture/Compare 4 interrupt
  *            @arg TIM_IT_COM:   Commutation interrupt
  *            @arg TIM_IT_TRIGGER: Trigger interrupt
  *            @arg TIM_IT_BREAK: Break interrupt
  * @retval None
  */
#define __HAL_TIM_ENABLE_IT(__HANDLE__, __INTERRUPT__)    ((__HANDLE__)->Instance->DIER |= (__INTERRUPT__))

#define __HAL_TIM_DISABLE_IT(__HANDLE__, __INTERRUPT__)   ((__HANDLE__)->Instance->DIER &= ~(__INTERRUPT__))

Нарешті, глянемо на використану функцію запуску таймера:

HAL_StatusTypeDef HAL_TIM_IC_Start_IT (TIM_HandleTypeDef *htim, uint32_t Channel)
{
  /* Check the parameters */
  assert_param(IS_TIM_CCX_INSTANCE(htim->Instance, Channel));

  switch (Channel)
  {
    case TIM_CHANNEL_1:
    {
      /* Enable the TIM Capture/Compare 1 interrupt */
      __HAL_TIM_ENABLE_IT(htim, TIM_IT_CC1);
    }
    break;

    case TIM_CHANNEL_2:
    {
      /* Enable the TIM Capture/Compare 2 interrupt */
      __HAL_TIM_ENABLE_IT(htim, TIM_IT_CC2);
    }
    break;

    case TIM_CHANNEL_3:
    {
      /* Enable the TIM Capture/Compare 3 interrupt */
      __HAL_TIM_ENABLE_IT(htim, TIM_IT_CC3);
    }
    break;

    case TIM_CHANNEL_4:
    {
      /* Enable the TIM Capture/Compare 4 interrupt */
      __HAL_TIM_ENABLE_IT(htim, TIM_IT_CC4);
    }
    break;

    default:
    break;
  }
  /* Enable the Input Capture channel */
  TIM_CCxChannelCmd(htim->Instance, Channel, TIM_CCx_ENABLE);

  /* Enable the Peripheral */
  __HAL_TIM_ENABLE(htim);

  /* Return function status */
  return HAL_OK;
}

Вона дозволяє переривання від переданого каналу, ініціалізує його викликом TIM_CCxChannelCmd(), розглянутої раніше  та запускає таймер.

Ось і все.




Проект, із повним текстом програми, скачати можна тут.

5 коментарів:

  1. Відповіді
    1. Не встиг я на Ваш коментар відповісти, перш ніж Ви його видалили. Літо напруженим було.

      Вдалося Вам вирішити ту проблему?

      Видалити
  2. Доброго дня!
    Чи не могли б Ви розповісти про різницю між макро
    __HAL_TIM_GetCounter(&h\tim2);
    та функцією
    HAL_TIM_ReadCapturedValue
    Чи це повне дублювання? Та навіщо воно так було зроблене?

    ВідповістиВидалити
    Відповіді
    1. __HAL_TIM_GetCounter - возвращает значение счетного регистра таймера
      HAL_TIM_ReadCapturedValue - возвращает значение регистра захвата (регистр в который по триггеру переносится значение счетного регистра)

      Видалити
  3. Ншел англоязычный туториал, в котором автор не выбирает источник тактирования для таймера. Могли бы Вы пояснить, можно ли так, и что происходит - пример-то работает... Есть какое-то тактирование по умолчанию?
    https://controllerstech.com/how-to-use-input-capture-in-stm32/

    ВідповістиВидалити