Перейти к содержимому

Использование таймеров на ESP32

Добрый день, уважаемый читатель! Сегодня поговорим о таймерах, которые предоставляет нам платформа ESP32.

Наверное я не сильно погрешу против истины, если скажу что в практически всех прошивках используются таймеры для отсчета необходимых временных интервалов. В том или ином виде. Это могут быть временные интервалы между считыванием данных с сенсоров, включение нагрузки на заданное время и так далее. Давайте посмотрим, какие средства предоставляет нам ESP32:

  • Функция ets_delay_us()
  • Функции FreeRTOS – vTaskDelay() и vTaskDelayUntil()
  • Аппаратные таймеры 64-bit General Purpose Timer – 4 шт.
  • Программные таймеры (много)

Возможно, этот список не полный, я пока знаком далеко не со всеми внутренностями ESP-IDF. Если вы знакомы с ещё какими-либо способами – пожалуйста, прошу в комментарии. Начнем с самого простого способа.

Функция ets_delay_us()

Есть простая системная функция, которая выполняет задержку на время us в микросекундах с полной блокировкой программы. Аналог delay() для Arduino, но с большим разрешением.

void ets_delay_us(uint32_t us)

Строго говоря, это не совсем таймер. Точнее – совсем не таймер. CPU тупо выполняет цикл while в течение заданного времени. Можно применять в некоторых случаях (например для формирования небольших временных интервалов при обмене с внешними сенсорами и устройствами).

Функции vTaskDelay() и vTaskDelayUntil()

Еще два “не таймера“. Эти функции предоставляет нам даже не ESP32, а FreeRTOS и предназначены они для приостановки выполнения текущей задачи на заданное количество тиков. В отличие от ets_delay_us() не нагружают процессор, а наоборот – освобождают его от части работы, что гораздо полезнее. А значит могут с успехом применяться при создании довольно точных временных интервалов, например для периодического выполнения той или иной полезной работы.

Задержка задачи на заданное количество тиков:

void vTaskDelay(const TickType_t xTicksToDelay)

Тик – это один “шаг” операционной системы, который в конечном итоге привязан к “кварцованной” таковой частоте процессора, а значит отсчитываемый интервал должен быть достаточно точным. Конечно, это не полноценный таймер, но для отсчета 30 секунд между изменениями с сенсоров вполне подойдет. Пересчитать необходимую длительность в миллисекундах в тики достаточно легко. Для этого у нас есть два макроса:

  • время в тиках = время в мс / portTICK_PERIOD_MS
  • время в тиках = pdMS_TO_TICKS ( время в мс )

portTICK_PERIOD_MS и pdMS_TO_TICKS – это макросы, привязанные к частоте операционной системы, вы можете использовать любой способ (я предпочитаю второй). Но vTaskDelay() не обеспечивает идеального контроля частоты периодической задачи, поскольку время, затраченное на выполнение этого самого периодического кода, а также другие задачи и прерывания будут влиять на частоту, с которой вызывается vTaskDelay(), и, следовательно, на время, при котором выполняется следующая итерация задачи. Чтобы уменьшить влияние этих эффектов, существует немного более продвинутая функция:

BaseType_t xTaskDelayUntil (TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement)

Как видите, здесь добавился ещё один параметр – указатель на переменную pxPreviousWakeTime, где будет хранится время последнего вызова этой функции. И при следующем вызове xTaskDelayUntil() заданное количество тиков будет скорректировано с учетом этих данных. Таким образом легко организовать равномерный интервал выполнения какой-либо задачи. Пример использования vTaskDelay() мы уже рассматривали в одной из предыдущих статей, его можно найти на GitHub

Аппаратные таймеры

Пожалуй, это самый точный тип таймеров на ESP32, хотя и не самый простой в использовании. Вы можете использовать их, когда требуется очень высокая точность измеряемых интервалов.

Для работы с аппаратными таймерами потребуется использование прерываний – если вы избегаете их, то вам следует обратиться к программным таймерам. Хотя, в принципе, можно использовать аппаратные таймеры без прерываний, “вручную” проверяя регистр тревоги, но я не вижу в этом никакого смысла.

Чип ESP32 содержит четыре аппаратных таймера общего назначения – две группы по два таймера. Все они являются 64-битными универсальными таймерами, основанными на 16-битных масштабаторах (их ещё называют предделители) и 64-битных счетчиках увеличения/уменьшения, которые могут перезапускаться автоматически.

Для работы с аппаратными таймерами подключите библиотеку:#include "driver/timer.h"

Общая схема работы с аппаратными таймерами такова:

  • Инициализируйте таймер с помощью функции timer_init(). Здесь вы задаете коэффициент деления входящей частоты (то есть минимальный интервал таймера), направление счета и параметры автоматического аппаратного перезапуска.
  • Задайте начальное и конечное значение счетчика таймера с помощью timer_set_counter_value() и timer_set_alarm_value().
  • Создайте функцию – обработчик прерывания и подключите его к таймеру – timer_isr_callback_add() и timer_enable_intr().
  • Запустите счетчик таймера с помощью функции timer_start()

Рассмотрим все эти этапы поподробнее.

Инициализация таймера и настройка параметров

Для инициализации таймера необходимо вызывать функцию:

esp_err_t timer_init(timer_group_t group_num, timer_idx_t timer_num, const timer_config_t *config)

где:

  • timer_group_t group_num – номер группы таймеров, может принимать всего два значения:TIMER_GROUP_0 илиTIMER_GROUP_1 (нумерация начинается с головы поезда нуля)
  • timer_idx_t timer_num – номер таймера в группе, может принимать всего два значения:TIMER_0 илиTIMER_1
  • const timer_config_t *config – указатель на параметры таймера, которые выглядят так:

Рассмотрим эту структуру подробнее:

  • uint32_t divider – Делитель тактовой частоты, или масштабатор. Диапазон делителя может быть от 2 до 65536. Да, да, делитель не может быть равным 0 или 1, то есть максимальная частота таймера 40MHz, что соответствует минимальному интервалу 0,025 микросекунды.
  • timer_count_dir_t counter_dir – направление счета: TIMER_COUNT_DOWN – вниз или TIMER_COUNT_UP – вверх
  • timer_autoreload_t auto_reload – разрешить автоматический аппаратный рестарт таймера после генерации прерывания; может принимать значения TIMER_AUTORELOAD_DIS или TIMER_AUTORELOAD_EN. При TIMER_AUTORELOAD_EN получится периодический таймер, при TIMER_AUTORELOAD_DIS – однократный.
  • timer_alarm_t alarm_en – разрешить или нет сигнал “будильника” для таймера; может принимать значения TIMER_ALARM_DIS или TIMER_ALARM_EN
  • timer_start_t counter_en – разрешить счет (работу таймера) или нет; может принимать два значения: TIMER_PAUSE или TIMER_START. При TIMER_PAUSE необходимо будет запустить таймер “вручную” после завершения всех настроек, иначе таймер будет запущен немедленно
  • timer_intr_mode_t intr_type – всегда TIMER_INTR_LEVEL (0), других вариантов пока просто нет, можно не заполнять.

Если вы заметили, в данной структуре нет начального и конечного значения счетчиков таймера. Их мы должны установить отдельно, с помощью специальных функций. Для задания начального значения счетчика необходимо вызывать функцию:

esp_err_t timer_set_counter_value(timer_group_t group_num, timer_idx_t timer_num, uint64_t load_val)

Здесь также необходимо задать идентификаторы группы и таймера, а также 64-битное начальное значение счетчика

Для задания конечного значения счетчика (по которому будет сгенерировано прерывание) необходимо вызывать другую похожую функцию:

esp_err_t timer_set_alarm_value(timer_group_t group_num, timer_idx_t timer_num, uint64_t alarm_value) 

Впрочем, для изменения параметров таймера есть ещё целая кучка функций:

  • timer_set_divider() – для изменения значения делителя (во избежание неопределенности рекомендуется остановить таймер перед изменением этого значения)
  • timer_set_counter_mode() – для изменения направления счета
  • timer_set_auto_reload() – для изменения режима аппаратного перезапуска
  • timer_set_alarm() – для включения или отключения “будильника”

Создание обработчика прерываний

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

Обработчики прерывания – не совсем простые функции. Прерывание приостанавливает выполнение всех потоков вашей программы, поэтому к обработчикам прерываний предъявляются определенные требования:

  • Обработчик прерывания должен выполняться как можно меньше по времени, иначе сработает WDT для прерываний и устройство будет аварийно перезагружено.
  • Обработчик прерывания должен постоянно находится в быстрой памяти IRAM, поэтому его следует пометить соответствующим атрибутом IRAM_ATTR.
  • В обработчиках прерываний не допускается использование библиотечных функций ESP_LOGx, но можно использовать специальную облегченную версию ESP_DRAM_LOGx.
  • Следует всегда помнить, что обработчики прерываний выполняются вне контекста прикладных задач. Дабы соблюдать потокобезопасность из обработчика прерываний лучше всего отправить какие-либо данные в очередь другой задачи, включить флаг в EventGroup и т.д. Да, в принципе можно просто изменить значение какой-либо глобальной статической переменной bool или int, которая будет управлять потоком в другой задаче, но это не приветствуется.

Вначале нужно создать callback-функцию, которая будет вызвана при генерации прерывания, называется она обработчик прерывания (ISR handler или ISR callback). Создается она по следующем шаблону:

Ничего сверхсложного, но стоит обратить внимание на два существенных момента:

  • Функция эта должна быть обязательно помечена как IRAM_ATTR, чтобы заставить компилятор постоянно держать её в быстрой памяти.
  • Обратите внимание на возвращаемое значение: какое значение должна вернуть эта функция (кстати, в других ISR-обработчиках возвращаемые значения обычно не используются). Если вы вызывали функции FreeRTOS (например xQueueSendFromISR()) из обработчика прерываний, то вам необходимо вернуть значение true или false на основе возвращаемого значения аргумента pxHigherPriorityTaskWoken. Если возвращаемое значение pxHigherPriorityTaskWoken любых вызовов FreeRTOS равно pdTRUE, то вы должны вернуть true; в противном случае вернуть false.
Простейшая функция обработки прерываний

Простейшая функция обработки прерываний

Затем необходимо подключить её к нашему таймеру с помощью

esp_err_t timer_isr_callback_add(timer_group_t group_num, timer_idx_t timer_num, timer_isr_t isr_handler, void *arg, int intr_alloc_flags) 

где:

  • timer_group_t group_num – номер группы таймеров
  • timer_idx_t timer_num – номер таймера в группе
  • timer_isr_t isr_handler – указатель на функцию обработчик
  • void *arg – указатель на какие-либо данные, которые можно передать в обработчик для идентификации таймера
  • int intr_alloc_flags – просто поставьте 0

После этого необходимо разрешить прерывания:

esp_err_t timer_enable_intr(timer_group_t group_num, timer_idx_t timer_num) 

А если у нас насколько таймеров? Можно использовать два пути:

  • Написать один обработчик для всех таймеров, но передавать в него какие-либо данные через void *arg, которые будут однозначно идентифицировать таймер, с которого поступило прерывание.
  • А можно по простому – написать несколько разных обработчиков и подключить их отдельно.

Управление hardware таймером

Все готово к запуску нашего космического корабля таймера. Делается это совсем просто:

esp_err_t timer_start(timer_group_t group_num, timer_idx_t timer_num) 

Кстати, если Вам по какой-либо причине необходимо временно приостановить таймер, вы можете сделать это с помощью функции timer_pause().

Примеры приложений с hardware таймерами

Официальный пример работы с аппаратными таймерами вы можете посмотреть по ссылке: esp-idf/timer_group_example_main.c В данном примере используется сразу два таймера в двух группах (с разными режимами работы соответственно) – этот пример в полной мере демонстрирует возможности работы с ними.

Но, на мой взгляд, новичку сравнительно непросто будет разобраться в указанном выше примере, поэтому я создал ещё более простой пример всего с одним таймером без всяких “хитростей”: dzen/timer_hardware. В примере всего 1 таймер, настроенный на повтор через каждые 3 секунды. А через каждый 5 секунд работает основной цикл, используя vTaskDelay(). Все предельно просто. Если прошить данный пример в микроконтроллер, по получим следующую картинку:

Как видите, всё работает "как часы". Так это и есть часы!

Как видите, всё работает “как часы”. Так это и есть часы!

В моем примере для просты нет никакого контроля ошибок и возвращаемых значений – сделайте это самостоятельно. Нужна в этом помощь – пишите в комментариях, запилим ещё одну статью.

Конечно, этим библиотека для работы с аппаратными таймерами не огранивается, там есть еще несколько интересных функций, но мы их пока опустим.

Программные таймеры

Аппаратные таймеры – это хорошо, но их только четыре! А в реальном приложении количество требуемых таймеров может легко исчисляться десятками. Для решения этой проблемы в Espressif придумали способ решения – программные таймеры, привязанные к одному из аппаратных таймеров.

Программные таймеры обладают несколько худшими характеристиками, чем аппаратные таймеры общего назначения, но, тем не менее, прекрасно подходят для решения прикладных задач в подавляющем большинстве случаев. Программные таймеры имеют несколько ограничений:

  • Максимальное разрешение (то есть минимальный интервал таймера) равно периоду тика FreeRTOS.
  • Обратные вызовы таймера отправляются из задачи с низким приоритетом

Аппаратные таймеры свободны от обоих ограничений, но зачастую они менее удобны в использовании. Например, компонентам приложения может потребоваться запуск событий таймера в определенное время в будущем, но аппаратный таймер содержит только одно значение «сравнения», используемое для генерации прерывания. Это означает, что поверх аппаратного таймера необходимо создать какое-то средство для управления списком ожидающих событий, которое может отправлять обратные вызовы для этих событий по мере возникновения соответствующих аппаратных прерываний. Внутри все программные таймеры используют всего один 64-битный аппаратный LAC-таймер.

Для работы с программными таймерами используется немного другая библиотека: #include "esp_timer.h". Общая схема работы с программными таймерами похожа на аппаратные:

  • Создайте функцию – обработчик событий таймера
  • Инициализируйте таймер с помощью функции esp_timer_create()
  • Запустите счетчик таймера с помощью функции esp_timer_start_once() (однократно) или esp_timer_start_periodic() (постоянно)

Как видите, процесс запуска программного таймера немного короче. Да и вообще использование программных таймеров, на мой взгляд, проще, чем аппаратных. Настраивать меньше, а для обработки событий используется обычная функция обратного вызова, а не обработчик прерываний (поэтому можно немного порезвиться). Рассмотрим все эти этапы поподробнее.

Создание функции обратного вызова

Как я уже написал, в программных таймерах используется обычная функция обратного вызова, поэтому к ней не предъявляется никаких особых требований. Можно сделать это, например, так:

Создание (инициализация) таймера

Далее нужно создать таймер с помощью функции

esp_err_t esp_timer_create(const esp_timer_create_args_t *create_args, esp_timer_handle_t *out_handle) 

где:

  • const esp_timer_create_args_t *create_args – указатель на структуру, содержащую параметры создаваемого таймера
  • esp_timer_handle_t *out_handle – указатель на хендл таймера, который вы сможете потом использовать для управление этим таймером

Рассмотрим esp_timer_create_args_t поподробнее:

  • esp_timer_cb_t callback – функция обратного вызова
  • void* arg – аргументы для передачи в функцию обратного вызова
  • esp_timer_dispatch_t dispatch_method – метод обработки таймаута: из задачи или через прерывание
  • const char* name – имя таймера, используется только для отладки
  • bool skip_unhandled_events – пропустить обработку событий, если обработка события таймера по какой-либо причине была пропущена. Обратный вызов таймера будет вызван в любом случае, он не будет потерян. В худшем случае, когда таймер не обрабатывался более одного периода (для периодических таймеров), обратные вызовы будут вызываться один за другим, не дожидаясь установленного периода. Это может быть неприемлемо для некоторых приложений, и для устранения такого поведения была введена опция skip_unhandled_events. Если установлено значение skip_unhandled_events = true, то периодический таймер, который истекал несколько раз, не имея возможности вызвать обратный вызов, по-прежнему будет приводить только к одному событию обратного вызова после того, как только станет возможной обработка.

Я заполнил эту структуру так:

Обязательно проверьте хендл таймера на NULL после вызова esp_timer_create(), чтобы избежать исключения и внеплановой перезагрузки MCU.

Запуск таймера

Таймер создали, осталось его запустить. Таймер может быть запущен в однократном режиме или в периодическом режиме.

  • Чтобы запустить таймер в однократном режиме, вызовите esp_timer_start_once(esp_timer_handle_t timer, uint64_t timeout_us), передав временной интервал, через который должен быть вызван обратный вызов. При вызове обратного вызова таймер считается остановленным.
  • Чтобы запустить таймер в периодическом режиме, вызовите esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period), передав период, с которым должен быть вызван обратный вызов. Таймер продолжает работать до тех пор , пока не будет вызван esp_timer_stop().

Обратите внимание, что таймер не должен работать, когда вызывается esp_timer_start_once() или esp_timer_start_periodic(). Чтобы перезапустить работающий таймер, сначала остановите его, а только затем вызовите одну из функций запуска.

В итоге весь процесс запуска таймера будет выглядеть как-то так:

Как видите, всё довольно просто.

Результаты работы примера

Результаты работы примера

Пример для работы с программными таймерами вы найдете на GitHub по ссылке: dzen/timer_software

Метки:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *