Переривання таймера 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);
// Додаткові дії: зчитування сенсорів, вивід у Serial тощо.
}
}
Приклад 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);
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);
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) {
Serial.printf("Подія таймера: #%lu\n", (unsigned long)stamp);
}
}
Обмеження та правила для ISR (обробника переривання)
- Коротко і швидко. ISR має виконуватися якнайшвидше (десятки мкс).
IRAM_ATTR. Обов’язково позначайте обробник, щоб він був у швидкій пам’яті.- Без блокувань. Не можна використовувати
delay(),vTaskDelay(),while-очікування. - Без важкого I/O. Заборонено
Serial.print(), Wi-Fi, файлову систему,malloc/new,String, I2C/SPI у ISR. - Спільні дані. Використовуйте
volatile, для складних операцій — критичні секції. - Передавайте роботу далі. В ISR тільки ставте прапор, інкрементуйте лічильник або надсилайте у чергу — основні дії виконуйте у
loop()чи у задачі FreeRTOS. - Точна частота. Формула:
період(мкс) = alarmTicks / (APB/переддільник). При APB=80 МГц і переддільнику 80 отримаємо 1 МГц тіків.
Корисні функції для роботи з таймером
timerAlarmDisable(timer)/timerAlarmEnable(timer)— зупинити/запустити alarm.timerWrite(timer, value)— записати значення лічильника.timerRestart(timer)— перезапустити лічильник із нуля.timerEnd(timer)— звільнити таймер.
Висновок
Переривання таймерів ESP32 дозволяють викликати код строго за часом, не блокуючи основний цикл. Дотримуйтеся правил ISR — тримайте обробник коротким, виконуйте важку логіку поза перериваннями, захищайте спільні дані — і ви отримаєте точну й надійну роботу навіть у складних проєктах реального часу.







