Прерывания таймера ESP32: hw_timer_t, настройка обработчика и ограничения ISR

ESP32 имеет аппаратные таймеры общего назначения (General Purpose Timers), которые могут вызывать ваш код по точному расписанию — без блокировок и независимо от loop(). В этой статье разберём, как работает hw_timer_t, как настроить прерывание и обработчик, а также какие ограничения действуют внутри ISR.

Как работает hw_timer_t

  • В ESP32 есть две группы таймеров (Timer Group0 и Group1), в каждой — по 2 таймера (итого 4).
  • Каждый таймер — это счётчик, который тактируется от APB (обычно 80 МГц) через предделитель (prescaler). Пример: предделитель 80 даёт частоту тиков 1 МГц, т.е. 1 тик = 1 мкс.
  • Таймер может работать в режиме однократного срабатывания (one-shot) или периодическом (auto-reload) и генерировать прерывание по достижении порогового значения (alarm).

Быстрая схема настройки

  1. timerBegin(timerNum, prescaler, countUp) — создать таймер и задать предделитель.
  2. timerAttachInterrupt(timer, isr, edge) — привязать обработчик прерывания (IRAM_ATTR).
  3. timerAlarmWrite(timer, alarmTicks, autoReload) — задать порог (в тиках) и авто‑повтор.
  4. timerAlarmEnable(timer) — запустить таймер.

Пример 1. Периодическое прерывание 1 кГц с безопасным флагом

ISR делает минимум работы: выставляет флаг. Основная логика — в loop().


#include <Arduino.h>

#define LED_PIN 2

hw_timer_t* timer = nullptr;
volatile bool tick = false;        // флаг от ISR

void IRAM_ATTR onTimer() {         // ISR должен быть в IRAM
  tick = true;                     // только выставляем флаг
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);

  // 80 МГц / 80 = 1 МГц → 1 тик = 1 мкс
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  // 1000 тиков при 1 МГц = 1000 мкс (1 кГц)
  timerAlarmWrite(timer, 1000, true);
  timerAlarmEnable(timer);
}

void loop() {
  if (tick) {                      // обрабатываем событие таймера вне ISR
    tick = false;
    static bool led = false;
    led = !led;
    digitalWrite(LED_PIN, led);
    // Доп. действия: чтение датчиков, печать и т.д.
    // В ISR этого делать нельзя, а здесь — можно.
  }
}
  

Пример 2. Высокоточный «тайм‑слот»: наносим временные метки

ISR считает «тики», а в loop() мы атомарно читаем счётчик и вычисляем частоту/период. Для безопасного обмена используем критическую секцию.


#include <Arduino.h>

hw_timer_t* timer = nullptr;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

volatile uint32_t isrTicks = 0;    // растёт в ISR каждый период

void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  isrTicks++;                      // очень короткая операция
  portEXIT_CRITICAL_ISR(&timerMux);
}

void setup() {
  Serial.begin(115200);
  // 1 МГц тик → ставим период 10 000 мкс (100 Гц)
  timer = timerBegin(1, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 10000, true);
  timerAlarmEnable(timer);
}

void loop() {
  static uint32_t lastMs = 0;
  if (millis() - lastMs >= 1000) {          // раз в секунду
    lastMs = millis();
    uint32_t ticksCopy;

    portENTER_CRITICAL(&timerMux);          // атомарно читаем и обнуляем
    ticksCopy = isrTicks;
    isrTicks = 0;
    portEXIT_CRITICAL(&timerMux);

    // 100 Гц * число секунд = пришедшие тики
    Serial.printf("Сработало ISR: %lu раз/сек\n", (unsigned long)ticksCopy);
  }
}
  

Пример 3. Передача событий из ISR в задачу (очередь FreeRTOS)

Правильный способ «отложить» тяжёлую работу из ISR — отправить сообщение в очередь.


#include <Arduino.h>

hw_timer_t* timer = nullptr;
QueueHandle_t q;

void IRAM_ATTR onTimer() {
  static uint32_t seq = 0;
  BaseType_t hpTaskWoken = pdFALSE;
  uint32_t stamp = ++seq;                    // счётчик событий
  xQueueSendFromISR(q, &stamp, &hpTaskWoken);
  if (hpTaskWoken) portYIELD_FROM_ISR();    // переключить контекст, если нужно
}

void setup() {
  Serial.begin(115200);
  q = xQueueCreate(8, sizeof(uint32_t));

  timer = timerBegin(2, 80, true);          // 1 МГц тик
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 500000, true);     // каждые 500 мс
  timerAlarmEnable(timer);
}

void loop() {
  uint32_t stamp;
  if (xQueueReceive(q, &stamp, 50 / portTICK_PERIOD_MS) == pdPASS) {
    // Тяжёлая работа — здесь, вне ISR: печать, I2C/SPI, расчёты...
    Serial.printf("Событие таймера: #%lu\n", (unsigned long)stamp);
  }

  // Остальная логика программы
}
  

Ограничения и правила для ISR (обработчика прерывания)

  • Коротко и быстро. ISR должен выполняться как можно быстрее (десятки микросекунд). Долгие ISR ломают детерминизм и приводят к пропуску событий.
  • IRAM_ATTR. Обозначайте обработчик как IRAM_ATTR, чтобы код был доступен при отключённом кешировании флеша.
  • Никаких блокировок. Не используйте delay(), vTaskDelay(), while-ожидания.
  • Без тяжёлого I/O. Нельзя Serial.print(), Wi‑Fi, файловую систему, динамические аллокации (new, malloc), объекты String, I2C/SPI транзакции.
  • Общий доступ к данным. Общие переменные помечайте volatile. Для составных операций используйте критические секции: portENTER_CRITICAL_ISR()/portEXIT_CRITICAL_ISR() в ISR и соответствующие версии без _ISR — вне ISR.
  • Перекладывайте работу. В ISR только отмечайте событие: ставьте флаг, инкрементируйте счётчик либо отправляйте в очередь — подробные действия выполняйте в loop() или задаче FreeRTOS.
  • Точная частота. Формула периода: период(мкс) = alarmTicks / (APB/предделитель). При APB=80 МГц и предделителе 80 частота тиков — 1 МГц.

Полезные функции управления таймером

  • timerAlarmDisable(timer) / timerAlarmEnable(timer) — остановить/запустить alarm.
  • timerWrite(timer, value) — записать текущее значение счётчика.
  • timerRestart(timer) — перезапустить счётчик с нуля.
  • timerEnd(timer) — освободить таймер.

Вывод

Прерывания таймеров ESP32 позволяют вызывать код строго по времени, не блокируя основной поток. Соблюдайте правила ISR — держите обработчик коротким, выполняйте тяжёлую логику вне прерываний, защищайте общий доступ к данным — и вы получите точное и надёжное поведение даже в сложных проектах реального времени.

<< Проекты << Все товары >> Статьи, уроки >>

Написать отзыв

Примечание: HTML разметка не поддерживается! Используйте обычный текст.
    Плохо           Хорошо
Модуль реле 2-канальный 5В 10А

Модуль реле 2-канальный 5В 10А

2-канальный модуль реле для подключения напрямую к дискретному выходу контроллера. Светодиодная инди..

76.14грн.

Двойная шестерня для зубчатого ремня на 20 зубьев под ось 8 мм

Двойная шестерня для зубчатого ремня на 20 зубьев под ось 8 мм

Сдвоенная шестерня для передачи крутящего момента с одного вала на другой.Применяется для разделения..

78.53грн.

Цифровой датчик температуры и влажности DHT11 с подтягивающим резистором

Цифровой датчик температуры и влажности DHT11 с подтягивающим резистором

Измерение относительной влажности в пределах 20 ... 90 %Точность измерения влажности ± 5,0 %RHИ..

74.11грн.

Видео уроки Arduino

Видео уроки Arduino

Курс уроков Arduino будет полезен любому исследователю окружающего мира ..

Какие виды датчиков можно подключить к Arduino для измерения параметров окружающей среды

Какие виды датчиков можно подключить к Arduino для измерения параметров окружающей среды

Какие виды датчиков можно подключить к Arduino для измерения параметров окружающей среды Ar..

Рекомендуемые товары

Создание простого таймера на ESP32: пример периодического вызова функции

Создание простого таймера на ESP32: пример периодического вызова функции

Создание простого таймера на ESP32: пример периодического вызова функции При разработке проектов ..

0.00грн.

ESP32 таймеры: Многозадачность с аппаратными таймерами

ESP32 таймеры: Многозадачность с аппаратными таймерами

ESP32 таймеры: Многозадачность с аппаратными таймерами ESP32 — это мощный микроконтроллер с двухъ..

0.00грн.