Попрацюємо тепер із захопленням вводу засобами 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(), розглянутої раніше та запускає таймер.
Ось і все.
Проект, із повним текстом програми, скачати можна тут.
Автор видалив цей коментар.
ВідповістиВидалитиНе встиг я на Ваш коментар відповісти, перш ніж Ви його видалили. Літо напруженим було.
ВидалитиВдалося Вам вирішити ту проблему?
Доброго дня!
ВідповістиВидалитиЧи не могли б Ви розповісти про різницю між макро
__HAL_TIM_GetCounter(&h\tim2);
та функцією
HAL_TIM_ReadCapturedValue
Чи це повне дублювання? Та навіщо воно так було зроблене?
__HAL_TIM_GetCounter - возвращает значение счетного регистра таймера
ВидалитиHAL_TIM_ReadCapturedValue - возвращает значение регистра захвата (регистр в который по триггеру переносится значение счетного регистра)
Ншел англоязычный туториал, в котором автор не выбирает источник тактирования для таймера. Могли бы Вы пояснить, можно ли так, и что происходит - пример-то работает... Есть какое-то тактирование по умолчанию?
ВідповістиВидалитиhttps://controllerstech.com/how-to-use-input-capture-in-stm32/