Прерывания таймера 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).
Быстрая схема настройки
timerBegin(timerNum, prescaler, countUp)— создать таймер и задать предделитель.timerAttachInterrupt(timer, isr, edge)— привязать обработчик прерывания (IRAM_ATTR).timerAlarmWrite(timer, alarmTicks, autoReload)— задать порог (в тиках) и авто‑повтор.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 — держите обработчик коротким, выполняйте тяжёлую логику вне прерываний, защищайте общий доступ к данным — и вы получите точное и надёжное поведение даже в сложных проектах реального времени.







