Прерывания таймера 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 разметка не поддерживается! Используйте обычный текст.
    Плохо           Хорошо
Модуль блока питания 12В 250мА

Модуль блока питания 12В 250мА

Удобный модуль питания из 220В AC в 12В постоянного тока. Разработан для установки на печатную плату..

155.85грн.

Монтажный шилд для NodeMcu ESP8266

Монтажный шилд для NodeMcu ESP8266

Монтажный модуль расширения для WiFi контроллера NodeMcu ESP8266Позволяет выполнять монтаж прое..

90.58грн.

Припой с канифолью 0,5мм 30г

Припой с канифолью 0,5мм 30г

Нить припоя для тонкой пайки мелких радиодеталей, содержащая флюсСвинца 40 %Олова 60 %Флюса 1,2 %Нап..

211.57грн.

Вентилятор для Orange PI толщиной 10мм

Вентилятор для Orange PI толщиной 10мм

Вентилятор для охлаждения процессора мини-компьютера Orange PI или Raspberry PIРаботает безшумноПита..

60.63грн.

Arduino - что это такое? Популярно для начинающих

Arduino - что это такое? Популярно для начинающих

Самые популярные платы контроллеров популярно для начинающихArduino это недорогая, доступная в смысл..

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

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

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

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

0.00грн.

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

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

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

0.00грн.