В уроке научимся работать с таймерами ESP32 без использования прерываний. Рассмотрим способы формирования временных задержек и создания временных циклов.
Предыдущий урок Список уроков Следующий урок
Первый практический урок был посвящен работе с портами ввода/вывода в минимальном варианте. Порты самый простой способ вывода информации с микроконтроллера. Даже самая примитивная программа должна хотя бы управлять светодиодом или считывать состояние кнопки.
Во втором прикладном уроке я решил рассказать о работе с таймерами. Трудно представить себе программу, в которой не происходит отсчет времени в каком-либо виде, не формируются задержки или циклические сигналы с заданным периодом.
В этом уроке прерывания таймеров мы не используем.
Аппаратные таймеры общего назначения.
Сначала о том, что собой представляют таймеры ESP32.
В системе ESP32 существует 4 таймера общего назначения.
Они разделены на две группы (модуля). В каждой группе по 2 таймера. При работе с ними через API-функции мы будем использовать константы-имена:
- TIMER_GROUP_0 и TIMER_GROUP_1 для групп;
- TIMER_0 и TIMER_1 для таймеров.
Давайте в дальнейшем придерживаться этих обозначений.
Структура таймеров проста, типична для подобных устройств и похожа на схему таймеров STM32 в режиме счетчиков.
По этой схеме без дополнительных объяснений можно понять, как работают таймеры ESP32.
Я выделю главные моменты их функционирования.
- В стандартной конфигурации таймеры тактируются частотой 80 мГц.
- На входе каждого счетчика установлен 16ти разрядный предделитель, который снижает частоту тактирования от 2 до 65536 раз. Коэффициент деления устанавливается программно.
- При значении предделителя равном 1 или 2 входная частота делится на 2.
- При значении 0, коэффициент деления равен 65536.
- Остальные значения предделителя соответствуют коэффициенту деления входной частоты.
- Основной счетчик 64х разрядный. Можно не заботиться о его переполнении. Например, при частоте тактирования 1 мГц (период 1 мкс) счетчик переполниться через почти 600000 лет.
- Он может считать в прямом или обратном направлении, быть остановленным.
- Его значение может быть считано и установлено программно.
- Счетчик может быть загружен значением из регистра перезагрузки по аппаратному событию перезагрузки. В терминологии ESP32 это называется событием тревоги. Оно происходит при совпадении значения счетчика со значением, заданным программно в регистре тревоги.
- Событие перезагрузки вырабатывается не только при строгом равенстве значений счетчика и регистра тревоги, а также и при превышении значением счетчика заданного порога для прямого счета и при уменьшении значения счетчика ниже порога для обратного счета. Это позволяет не пропустить событие перезагрузки в случае опоздания установки регистра тревоги по отношению к состоянию счетчика.
- Каждый модуль таймеров содержит сторожевой таймер и связанные с ним регистры, но об этом в других уроках.
- Каждый модуль таймеров может генерировать 3 типа прерываний:
- прерывание по истечению времени ожидания сторожевого таймера;
- прерывание перезагрузки (тревоги) таймера 0 (TIMER_0);
- прерывание перезагрузки (тревоги) таймера 1 (TIMER_1).
С учетом того, что мы собираемся работать с таймерами через API-функции этих сведений об аппаратной организации таймеров и их функциональных возможностях вполне достаточно. Какие регистры используются для работы с таймерами и их форматы знать не обязательно.
Инициализация таймера.
Для инициализации таймера в общем случае должны быть заданы следующие режимы и параметры:
- значение предделителя;
- разрешение/запрет счета;
- направление счета;
- разрешение/запрет события перезагрузки;
- разрешение/запрет аппаратной перезагрузки счетчика;
- значение порога перезагрузки;
- значение перезагрузки;
- параметры прерывания.
Инициализировать таймер можно задавая режимы отдельными функциями, а можно это сделать одним вызовом функции timer_init(). Во втором случае параметры задаются в элементах структуры инициализации. Но в любом варианте аргументы функций имеют одни и те же имена. Поэтому логичнее, сначала перечислить параметры функций работы с таймерами и соответствующие им константы-значения.
Имя параметра | Значения | Функция установки параметра |
Назначение параметра |
group_num | TIMER_GROUP_0 – группа 0 TIMER_GROUP_1 – группа 1 |
Все | Группа таймеров |
timer_num | TIMER_0 – таймер 0 TIMER_1 – таймер 1 |
Все | Таймер |
alarm_en | TIMER_ALARM_DIS – запрет перезагрузки TIMER_ALARM_EN – разрешение перезагрузки |
timer_set_alarm
timer_init
|
Разрешение события перезагрузки |
counter_en | TIMER_PAUSE – остановка счета TIMER_START – пуск счетчика |
timer_init
timer_start timer_pause |
Разрешение счета |
intr_type | TIMER_INTR_LEVEL TIMER_INTR_MAX |
timer_init | Режим прерывания |
counter_dir | TIMER_COUNT_UP – прямой счет TIMER_COUNT_DOWN – обратный счет |
timer_set_counter_mode
timer_init |
Направление счета |
auto_reload
reload |
TIMER_AUTORELOAD_DIS – запрет перезагрузки TIMER_AUTORELOAD_EN – разрешение перезагрузки |
timer_set_auto_reload
timer_init |
Разрешение перезагрузки счетчика |
divider | 2 … 65536 | timer_set_divider
timer_init |
Делитель входной частоты |
timer_val | 0 … 264 – 1 (uint64_t) | timer_get_counter_value | Значение счетчика |
time | (double) | timer_get_counter_time_sec | Значение счетчика в секундах |
load_val | 0 … 264 - 1 (uint64_t) | timer_set_counter_value | Значение счетчика для загрузки |
alarm_value | 0 … 264 - 1 (uint64_t) | timer_set_alarm_value
timer_get_alarm_value |
Значение порога перезагрузки |
Большую часть параметров инициализации можно установить одним вызовом функции
esp_err_t timer_init(timer_group_t group_num, timer_idx_t timer_num, const timer_config_t *config)
Параметры задаются в структуре config типа timer_config_t.
typedef struct {
timer_alarm_t alarm_en; /* Разрешение события перезагрузки */
timer_start_t counter_en; /*Разрешение счета*/
timer_intr_mode_t intr_type; /* Режим прерывания */
timer_count_dir_t counter_dir; /* Направление счета */
timer_autoreload_t auto_reload; /* Разрешение перезагрузки счетчика */
uint32_t divider; /* Делитель входной частоты */
} timer_config_t;
- alarm_en – разрешает или запрещает событие перезагрузки, т.е. то что вызывает собственно перезагрузку и прерывание.
- counter_en - разрешает или запрещает (останавливает) работу счетчика.
- intr_type – задает режим прерывания.
- counter_dir – задает направление счета: прямое или обратное.
- auto_reload - разрешает или запрещает аппаратную перезагрузку счетчика. Т.е. если запретить это действие и разрешить событие перезагрузки, то при достижении счетчиком порогового значения будет вырабатываться прерывание, а перезагрузка счетчика происходить не будет.
- divider – делитель входной тактовой частоты.
Давайте создадим новый проект и инициализируем таймер на простой счет без перезагрузки.
Напишем название программы, подключим API-драйвер для работы с таймерами.
// простой счет таймера
#include "driver/timer.h"
Создаем главную функцию, объявим структуру инициализации, определим ее элементы и вызовем функцию инициализации.
void app_main(void)
{
// инициализация таймера 0, группы 0
timer_config_t config;
config.divider = 80; // тактирование счетчика 1 мкс
config.counter_dir = TIMER_COUNT_UP; // прямой счет
config.counter_en = TIMER_START; // счетчик работает
config.alarm_en = TIMER_ALARM_DIS; // событие перезагрузка запрещено
config.auto_reload = TIMER_AUTORELOAD_DIS ; // аппаратная перезагрузка запрещена
timer_init(TIMER_GROUP_0, TIMER_0 , &config); // инициализация
}
Значение предделителя я задал 80, что обеспечивает тактирование счетчика частотой 1 мГц. Счетчик будет считать время непосредственно в микросекундах.
Операцию инициализации можно выполнить отдельными функциями. Форматы параметров функций перечислены в таблице выше.
esp_err_t timer_set_divider(timer_group_t group_num, timer_idx_t timer_num, uint32_t divider)
Устанавливает значение предделителя (divider = 2 … 65536).
esp_err_t timer_set_counter_mode(timer_group_t group_num, timer_idx_t timer_num, timer_count_dir_t counter_dir)
Задает направления счета (counter_dir = TIMER_COUNT_UP или TIMER_COUNT_DOWN).
esp_err_t timer_set_auto_reload(timer_group_t group_num, timer_idx_t timer_num, timer_autoreload_t reload)
Разрешает/запрещает аппаратную перезагрузку счетчика по событию (reload = TIMER_AUTORELOAD_EN или TIMER_AUTORELOAD_DIS).
esp_err_t timer_set_alarm_value(timer_group_t group_num, timer_idx_t timer_num, uint64_t alarm_value)
Устанавливает значение порога перезагрузки (alarm_value = 0 … 264 – 1).
esp_err_t timer_get_alarm_value(timer_group_t group_num, timer_idx_t timer_num, uint64_t *alarm_value)
Считывает значение порога перезагрузки.
esp_err_t timer_set_alarm(timer_group_t group_num, timer_idx_t timer_num, timer_alarm_t alarm_en)
Разрешает/запрещает событие перезагрузки (alarm_en = TIMER_ALARM_EN или TIMER_ALARM_DIS).
esp_err_t timer_start(timer_group_t group_num, timer_idx_t timer_num)
Запуск счетчика (разрешение счета).
esp_err_t timer_pause(timer_group_t group_num, timer_idx_t timer_num)
Остановка счетчика (запрет счета).
Ничего не мешает использовать эти функции для изменения режимов таймера не только при начальной инициализации, но и в ходе выполнения программы.
Организация задержек и временных циклов.
Для того, чтобы работать с таймерами без использования прерываний необходимо знать еще 3 функции.
esp_err_t timer_get_counter_value(timer_group_t group_num, timer_idx_t timer_num, uint64_t *timer_val)
Чтение значения счетчика.
esp_err_t timer_set_counter_value(timer_group_t group_num, timer_idx_t timer_num, uint64_t load_val)
Загрузка значений счетчика и регистра перезагрузки.
esp_err_t timer_get_counter_time_sec(timer_group_t group_num, timer_idx_t timer_num, double *time)
Вариант первой функции, при котором считанное значение счетчика (time) пересчитывается в секунды.
Задержка с блокированием программы.
Самый простой вариант.
Сбрасываем счетчик, в цикле считываем его и ждем, пока его значение достигнет требуемого.
Программа зависает при отработке задержки. Но такое решение приемлемо на практике при отработке коротких задержек.
Использование функции ets_delay_us.
Есть стандартная функция, которая выполняет задержку с блокировкой программы.
void ets_delay_us(uint32_t us)
Задержка на время us, заданное в микросекундах.
Для использования функции необходимо включить ее заголовочный файл. Программа с реализацией задержки с помощью ets_delay_us будет выглядеть так.
Вот проект, который демонстрирует описанные выше 2 способа формирования задержек с блокированием программы.
Временной цикл без блокирования программы.
Если мы инициализируем таймер и разрешим работу счетчика в начале программы, никогда не будем его сбрасывать, то получим счетчик, который ведет отсчет времени работы программы, начиная с ее запуска (включения или сброса микроконтроллера).
Функция чтения значения счетчика timer_get_counter_value() будет возвращать текущее время работы микроконтроллера в микросекундах. Она будет эквивалентна функции micros() в Ардуино.
Только в отличие от Ардуино счетчики ESP32 имеют разрядность 64 бита, что позволяет реализовывать временные циклы со значительно более длительными периодами, не заботясь о переполнении счетчика.
Логика формирования временного цикла простая.
- В основном цикле программы считываем текущее время.
- Сравниваем его с предыдущим временем, когда отрабатывалось событие временного цикла.
- Если время превышает заданный период цикла, то формируем событие. В нашем случае инверсию состояния светодиода.
Вот как это выглядит в программе.
При этом программа не зависает. В основном цикле while можно выполнять другие операции. В том числе другие временные циклы.
Вот полный проект, демонстрирующий это способ формирования временного цикла.
Для того, чтобы ошибка периода не накапливалась можно модифицировать код. В переменную для предыдущего времени загружать не текущее время, а расчетное.
previousTime += CYCLE_TIME;
Точность отработки периода цикла, сформированного таким образом, зависит от времени выполнения основного цикла while. Опрос счетчика происходит в нем, значит, любые задержки цикла while - это задержки опроса счетчика.
Но для многих практических приложений такой способ создания временных циклов вполне допустим.
Полностью независимые временные циклы можно реализовать только с использованием прерываний таймера.
Этому будет посвящен следующий урок.
Предыдущий урок Список уроков Следующий урок
Добрый вечер. Обнадеживающий цикл статей. Очень полезный. Хочу спросить. Это все?
Здравствуйте!
Не знаю, как ответить.
Я писал уроки ESP32 по остаточному принципу. Сейчас времени почти нет. Сайт приносит очень маленький доход. Информации на эту тему практически нет. Приходится добывать ее из официальной документации и, много времени уходит на проверку. Остается надеяться на положительные изменения в стране. Как говорят англичане, темнее всего перед рассветом.
Добрый день. Поскольку статьи по прерываниям еще нет, поэтому задам вопрос тут. Меня интересует вопрос с какой максимальной частотой ESP32 обрабатывает прерывания. Допустим на выв 13 повесили кнопку. На выв 14 повесили светодиод, который загорается при срабатывании прерывания на выв 13. Время задержки появления сигнала около 1.8 мкс. Если будите писать про прерывания просьба рассмотреть тему, как можно уменьшить данную задержку. Вроде процессор работает на частоте 240 мгц, неужели на обработку прерываний тратится около 400 тактов?
Здравствуйте!
я не измерял время обработки прерывания ESP32.
Во первых не все ESP32 работают на частоте 240 мГц. В основном это 80 мГц.
Во вторых обработка прерывания достаточно длинная операция, особенно на языке высокого уровня. В данном случае Си.
В обработчике прерывания необходимо занести в стек множество регистров, а при выходе из обработки восстановить содержимое регистров из стека.
В микроконтроллере Atmega328 обработка прерывания занимает более 5 мкс. Но в ESP32 значительно больше регистров общего назначения, используемых для вычислений.
У Atmega328 у меня получалась частота около 170 кгц. На RP2040 около 1100 кгц, но там проблемы с таймером. Решил попробовать ESP32.таймер вроде работает на 40 мгц. и по теории должен измерять разность отсчетов с такой же точностью.Осталось только максимально повысить частоту прерываний. Хочу статейку из радио N9 за 2022 год переписать на новом железе.