Многозадачная Ардуина: таймеры без боли

Многозадачная Ардуина: таймеры без боли

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

Еще меньше используют такую возможность на практике, т.к. в стандартном не слишком богатом API Arduino она не предусмотрена. И, хотя, доступ ко всем богатствам внутренних возможностей микроконтроллера лежит на расстоянии вытянутой руки через подключение одного-двух системных заголовочных файлов, не каждый пожелает добавить в свой аккуратный маленький скетч пару-тройку экранов довольно специфического настроечного кода (попутно потеряв с ним остатки переносимости между разными платами). Совсем единицы (тем более, среди аудитории Ардуино) решатся и смогут в нем разобраться.

Сегодня я избавлю вас от страданий. и расскажу, как получить настоящие многозадачность и реальное время в прошивке вашего ардуино-робота, добавив в неё ровно 3 строчки кода (включая #include в шапке). Обещаю, что у вас всё получится, даже если вы только что в первый раз запустили Blink.

Начнем сразу с кода

Подключаем библиотеку timer-api.h (раз)

Запускаем таймер с нужной частотой с timer_init_ISR_XYHz: здесь XYHz=1Hz — 1 Герц — один вызов прерывания в секунду (два)

(ISR — interrupt service routine, процедура-обработчик прерывания)

Добавляем в главный цикл loop любую блокирующую или неблокирующую ерунду: печатаем сообщение, ждём 5 секунд (здесь всё, как обычно, поэтому не считаем)

Процедура, вызываемая прерыванием по событию таймера с заданным периодом, — реализация для функции с именем timer_handle_interrupts: печатаем сообщение, мигаем лампочкой (три)

То же самое, только добавим замер времени между двумя вызовами для наглядности и отладки:

Шьем плату, открываем Инструменты > Монитор порта, наблюдаем результат:

Как видим, обработчик timer_handle_interrupts печатает сообщение каждые 1000000 (1 миллион) микросекунд, т.е. ровно раз в секунду. И (о чудо!) постоянная блокирующая задержка на 5 секунд delay(5000) в главном цикле никаким образом ему в этом действии не мешает.

Вот вам реальное время и многозадачность в одном скетче в 3 строчки, я обещал.

Варианты частот для timer_init_ISR_XYHz

(вызов timer_init_ISR_1MHz тоже есть, но он не даёт рабочий результат ни на одном из тестовых контроллеров)

Код прерывания, очевидно, должен выполняться достаточно быстро для того, чтобы успеть завершиться до следующего вызова прерывания и, желательно, еще оставить немного процессорного времени для выполнения главного цикла.

Полагаю, излишне пояснять, что чем выше частота таймера, тем меньше период вызова прерываний, тем быстрее должен выполняться код обработчика. Я бы не рекомендовал помещать в него вызовы блокирующих задержек delay, циклы с неизвестным заранее количеством итераций, любые другие вызовы с плохо предсказуемым временем выполнения (в том числе Serial.print).

Суммирование периодов (деление частоты)

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

Произвольная частота

Есть еще вариант установить практически произвольное (в определенных границах) значение частоты таймера при помощи вызова timer_init_ISR(timer, prescaler, adjustment) с параметрами — системным делителем тактовой частоты процессора prescaler и произвольным значением adjustment для размещения в регистре счетчика таймера.

Не вдаваясь в подробности, чтобы не перегружать пост, приведу ссылку на пример с подробными комментариями: arduino-timer-api/examples/timer-api-custom-clock/timer-api-custom-clock.ino

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

Запуск и остановка таймера в динамике

Для остановки таймера следует использовать вызов timer_stop_ISR, для повторного запуска — любой вариант timer_init_ISR_XYHz, как и раньше.

Установка библиотеки

Клонировать репозиторий прямо в каталог с библиотеками

и перезапустить среду Arduino.

Или на странице проекта arduino-timer-api скачать снапшот репозитория Clone or download > Download ZIP или один из релизов в виде архива, затем установить архив arduino-timer-api-master.zip через меню установки библиотек в среде Arduino (Скетч > Подключить библиотеку > Добавить .ZIP библиотеку. ).

Примеры должны появиться в меню File > Examples > arduino-timer-api

Поддерживаемые чипы и платформы

— Atmega/AVR 16 бит 16МГц на Arduino — SAM/ARM 32 бит 84МГц на Arduino Due — PIC32MX/MIPS 32 бит 80МГц на семействе ChipKIT (PIC32MZ/MIPS 200МГц — частично, в работе)

Ну и, напоследок,

Вращение шаговым мотором через интерфейс step-dir: — в фоне по таймеру генерируем постоянный прямоугольный сигнал для шага по фронту HIGH->LOW на ножке STEP — в главном цикле принимаем от пользователя команды для выбора направления вращения (ножка DIR) или остановки мотора (ножка EN) через последовательный порт

📎📎📎📎📎📎📎📎📎📎