Введение
Простая задержка через HAL_Delay() блокирует выполнение всего остального кода на время ожидания — для одновременного выполнения нескольких задач с разной периодичностью (аналогично паттерну millis() без delay() из основного Arduino-цикла, статья 220) нужны аппаратные таймеры, генерирующие прерывания с точной, не зависящей от остального кода периодичностью. В статье разберём настройку таймеров STM32 и обработку их прерываний.
Концепция
STM32 содержит несколько независимых аппаратных таймеров (TIM1, TIM2 и так далее, количество и возможности различаются между моделями чипов), каждый способный генерировать прерывание по истечении заданного периода — настройка таймера через HAL включает выбор источника тактирования, предделителя (prescaler, уменьшающего эффективную частоту тактирования таймера) и периода счёта, определяющих итоговую частоту срабатывания прерывания. Приоритеты прерываний в ARM Cortex-M (через NVIC — Nested Vectored Interrupt Controller) позволяют явно указать, какие прерывания важнее других — прерывание с более высоким приоритетом может прервать обработку прерывания с более низким приоритетом, что важно для систем, где одни события (аварийная остановка) однозначно важнее других (периодическое обновление показаний датчика).
Пример кода: Прерывания и таймеры
// Настройка таймера TIM2 для генерации прерывания каждые 100 мс
TIM_HandleTypeDef htim2;
void TIM2_Init(void)
{
__HAL_RCC_TIM2_CLK_ENABLE();
htim2.Instance = TIM2;
// Расчёт периода: при тактовой частоте таймера 84 МГц (типично для STM32F4),
// предделитель 8399 даёт эффективную частоту счёта 84000000/(8399+1) = 10000 Гц (10 кГц),
// а период 999 даёт прерывание раз в (999+1)/10000 = 0.1 секунды = 100 мс
htim2.Init.Prescaler = 8399;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); // приоритет 1 — ниже, чем у более критичных прерываний
HAL_NVIC_EnableIRQ(TIM2_IRQn);
HAL_TIM_Base_Start_IT(&htim2); // запуск таймера с генерацией прерывания
}
// Обработчик прерывания таймера — вызывается аппаратно каждые 100 мс
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim2); // стандартная диспетчеризация HAL, вызывающая callback ниже
}
volatile uint32_t sensorReadCounter = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2) {
sensorReadCounter++; // минимальная работа в прерывании — основная логика в основном цикле
}
}
// Использование в основном цикле — аналогично паттерну millis() из Arduino-цикла,
// но основанному на аппаратном счётчике прерываний, а не на программном опросе
int main(void)
{
// ... инициализация ...
TIM2_Init();
uint32_t lastProcessedCount = 0;
while (1) {
if (sensorReadCounter != lastProcessedCount) {
lastProcessedCount = sensorReadCounter;
// выполняется ровно раз в 100 мс, независимо от того, сколько занимает остальной код цикла
readAndTransmitSensorData();
}
handleOtherTasks(); // другая работа выполняется в промежутках, без блокировки задержками
}
}
Пояснения к коду
Расчёт Prescaler/Period, явно прокомментированный в коде, показывает типичную арифметику настройки таймера STM32 — итоговая частота прерывания определяется как (тактовая частота таймера) / ((prescaler + 1) × (period + 1)), и оба эти значения нужно подбирать совместно для получения нужного периода с учётом ограничений по максимальным значениям обоих регистров (обычно 16-битные, то есть до 65535 для большинства базовых таймеров). HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0) демонстрирует настройку приоритета конкретного прерывания через NVIC — для системы с несколькими разными источниками прерываний более низкое числовое значение приоритета (в реализации NVIC Cortex-M) означает более высокий реальный приоритет, и это (нередко противоречащее интуиции) соответствие стоит явно перепроверять в документации используемой модели чипа при настройке нескольких конкурирующих прерываний.
Подводные камни
- Путаница в численном представлении приоритетов NVIC — поскольку меньшее число обычно означает более высокий приоритет (противоречащее интуитивному «больше число — выше приоритет» из многих других контекстов программирования), при настройке нескольких таймеров/прерываний с разной важностью легко перепутать порядок и присвоить наиболее критичному прерыванию более низкий, а не более высокий приоритет — стоит явно перепроверять это соответствие в документации конкретного семейства Cortex-M, а не полагаться на интуитивное предположение.
- Неверный расчёт значений
Prescaler/Periodбез учёта реальной, итоговой тактовой частоты, на которой работает конкретный таймер (которая зависит от настройкиSystemClock_Config(), статья 353, и может различаться между разными таймерами на шинах APB1/APB2 с разными максимальными частотами) — использование значений, рассчитанных для другой тактовой частоты (например, скопированных из примера для другой модели чипа без пересчёта), приведёт к прерываниям, срабатывающим со скоростью, отличной от ожидаемой, без какой-либо явной ошибки компиляции или прошивки, что делает эту ошибку особенно незаметной до тестирования реального тайминга. - Выполнение существенной по времени работы непосредственно в
HAL_TIM_PeriodElapsedCallback(), аналогично подводному камню из статьи 354 про UART — любая задержка внутри обработчика прерывания таймера задерживает обработку других, возможно более приоритетных прерываний системы, и реальную обработку (какreadAndTransmitSensorData()в примере) стоит выполнять в основном цикле, реагируя на минимальный флаг/счётчик, установленный самим обработчиком прерывания. - Использование переменной
sensorReadCounter, изменяемой в обработчике прерывания и читаемой в основном цикле, без объявленияvolatile(что в примере сделано правильно) — без этого ключевого слова компилятор может агрессивно оптимизировать чтение переменной в основном цикле, предполагая (некорректно для случая, когда переменная изменяется асинхронно прерыванием), что её значение не может измениться между двумя последовательными чтениями в рамках одной итерации цикла, что приводит к пропуску изменений переменной, фактически произошедших в прерывании.