29. Программные таймеры
Аппаратных таймеров на кристалле (раздел 13) — от четырёх у F103C8 до четырнадцати у F407, и все они нужны под ШИМ, захват, АЦП. А приборам постоянно требуется «сделать через полсекунды», «делать каждые сто миллисекунд», «если за две секунды ничего не пришло — тревога». Программные таймеры дают столько таких, сколько нужно, ценой одной задачи и 44 байт на штуку.
Как это называется в документации
Оригинал Перевод software timer программный таймер timer daemon / service task задача-демон, обслуживающая таймеры timer command queue очередь команд таймеров callback function функция обратного вызова one-shot однократный auto-reload с автоперезапуском dormant бездействующий, не запущенный timer ID идентификатор таймера, произвольный указатель pend function call попросить демона выполнить функцию
29.1 Как это устроено
Программный таймер — не аппаратура. Это запись в списке: когда сработать, что вызвать, повторять ли. Список обслуживает одна задача ядра, которую называют демоном: она спит до ближайшего срока, просыпается, вызывает нужную функцию и засыпает до следующего.
Отсюда три следствия, которые определяют всё поведение.
Функция обратного вызова выполняется в задаче-демоне. Не в прерывании, не в задаче, которая таймер запустила. Со всеми вытекающими: стек демона, приоритет демона, и если функция заблокируется — заблокируется демон, а с ним встанут все таймеры прибора разом.
Все действия с таймером — команды через очередь. xTimerStart
не запускает таймер сам: он кладёт команду в очередь демона и возвращается.
Демон получит процессор, вычитает команду и запустит таймер.
Точность — тик. Демон просыпается по тику, поэтому лучше миллисекунды при 1 кГц не получится. Для чего-то точнее — аппаратный таймер.
| Аппаратный таймер | Программный таймер | |
|---|---|---|
| число | 4–14 на кристалл | сколько угодно |
| разрешение | такт | тик |
| обработчик выполняется | в прерывании | в задаче-демоне |
| цена | занят аппаратный блок | 44 байта |
| годится для | ШИМ, захват, микросекунды | задержки, периоды, сторожа |
29.2 Создание и запуск
static TimerHandle_t t_blink;
static void blink_cb(TimerHandle_t t)
{
GPIOC->ODR ^= GPIO_ODR_ODR13;
}
t_blink = xTimerCreate("blink", /* имя для отладки */
pdMS_TO_TICKS(500), /* период в тиках */
pdTRUE, /* автоперезапуск */
NULL, /* идентификатор */
blink_cb); /* что вызывать */
xTimerStart(t_blink, 0);
Два вида, различаются третьим аргументом:
| Аргумент | Вид | Поведение |
|---|---|---|
pdTRUE |
с автоперезапуском | срабатывает каждые N тиков, пока не остановят |
pdFALSE |
однократный | срабатывает один раз и переходит в бездействие |
Таймер после создания не запущен. Он бездействует, пока не придёт команда запуска; и однократный после срабатывания возвращается в то же бездействие.
Про второй аргумент xTimerStart
xTimerStart(t_blink, pdMS_TO_TICKS(10));
Здесь легко ошибиться: это не задержка перед запуском. Это время ожидания места в очереди команд — сколько вызывающая задача готова ждать, если очередь демона переполнена. Обычно ставят ноль: очередь на десять команд переполняется редко, а если переполнилась — это признак беды, а не повод ждать.
⚠️ Запуск до старта планировщика допустим и обычен. Команда ляжет в очередь и исполнится, как только демон получит процессор. Но отсчёт периода начнётся именно тогда, а не в момент вызова.
Команды
| Функция | Что делает |
|---|---|
xTimerStart |
запустить; если уже идёт — перезапустить с нуля |
xTimerStop |
остановить |
xTimerReset |
перезапустить отсчёт с нуля |
xTimerChangePeriod |
сменить период; заодно запускает, если стоял |
xTimerDelete |
удалить |
У каждой — двойник FromISR без времени ожидания и с признаком
пробуждения (раздел 28).
⚠️
xTimerChangePeriodзапускает остановленный таймер. Сменили период у таймера, который специально держали выключенным, — и он пошёл. Если это не нужно, сразу за сменой периода —xTimerStop.
29.3 Задача-демон
Настраивается тремя макросами, и каждый имеет значение.
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 256
Приоритет демона — это приоритет всех обратных вызовов. Ставят максимальный: иначе функция таймера, которой пора сработать, будет ждать, пока освободится процессор, и точность таймеров исчезнет. Плата — любой длинный обратный вызов задержит всё, что ниже.
Длина очереди — сколько команд может накопиться, пока демон занят.
Десяти хватает почти всегда; отказ xTimerStart с нулевым ожиданием
означает, что очередь полна.
Стек — общий для всех обратных вызовов. 256 слов, то есть килобайт, хватает на простые действия. Форматированный вывод в обратном вызове съест его целиком (раздел 26).
29.4 Обратный вызов: что можно и чего нельзя
Функция выполняется в задаче, а не в прерывании, — значит, ей доступны
обычные функции ядра, не FromISR. Но с одной оговоркой.
⚠️ Обратный вызов не имеет права блокироваться. Ни
vTaskDelay, ни ожидания на очереди или семафоре с ненулевым временем. Заблокируется демон — встанут все таймеры прибора, и мигание, и сторожа, и задержки. Правило простое: в обратном вызове время ожидания всегда ноль.
Что обычно делают в обратном вызове: переключить вывод, послать сообщение в очередь с нулевым ожиданием, отдать семафор, выставить флаг, поменять состояние автомата. Что не делают: ждать, считать долго, обмениваться по шине.
Если срабатывание таймера требует длинной работы — обратный вызов будит задачу, а работу делает она. Та же отложенная обработка, что и с прерываниями.
29.5 🔧 Идентификатор: один обработчик на много таймеров
В таймер можно положить произвольный указатель — идентификатор. Так одна функция обслуживает группу таймеров, различая их по нему:
typedef struct { uint8_t channel; uint8_t state; } led_ctx_t;
static led_ctx_t leds[3];
static void led_cb(TimerHandle_t t)
{
led_ctx_t *ctx = pvTimerGetTimerID(t); /* чей это таймер */
ctx->state ^= 1u;
set_led(ctx->channel, ctx->state);
}
for (i = 0; i < 3; i++) {
t_leds[i] = xTimerCreate("led", pdMS_TO_TICKS(100 * (i + 1)),
pdTRUE, &leds[i], led_cb);
}
Три таймера с разными периодами, одна функция, 132 байта на всё.
Идентификатор можно и менять на ходу через vTimerSetTimerID —
удобно для хранения состояния между срабатываниями.
29.6 🔧 Сторожевой таймер связи
Самое полезное применение однократного таймера.
Задача: если по линии связи две секунды ничего не приходило — считать связь потерянной и перевести прибор в безопасное состояние.
static void timeout_cb(TimerHandle_t t)
{
enter_safe_state(); /* связь молчала две секунды */
}
t_timeout = xTimerCreate("timeout", pdMS_TO_TICKS(2000), pdFALSE,
NULL, timeout_cb);
xTimerStart(t_timeout, 0);
/* в обработчике приёма */
void USART1_IRQHandler(void)
{
BaseType_t woken = pdFALSE;
byte = USART1->DR;
xTimerResetFromISR(t_timeout, &woken); /* отодвинуть срабатывание */
portYIELD_FROM_ISR(woken);
}
Каждый принятый байт перезапускает таймер. Пока данные идут, он не срабатывает никогда. Замолчали — сработает ровно через две секунды после последнего байта. Ни одной строки логики про время в самом протоколе.
Цена — на каждом байте. Каждый вызов xTimerResetFromISR — команда
в очередь демона, переключение на него и обратно. Байт на 115 200 бод
длится 6250 тактов F103 на 72 МГц; в опыте с FreeRTOS 10.5.1 на
Cortex-M3 в QEMU «байт» приходил примерно через столько же команд. На
каждый ушло около 760 команд — 12 % процессора, и это оценка снизу:
команда стоит не меньше такта. Прерывание, которое только запоминает
время последнего байта, обошлось в 24 команды. Поэтому на быстрой линии
перезапускают реже: по целому кадру — из задачи разбора или по прерыванию
«линия замолчала» (раздел 16.5), — либо в прерывании только запоминают
время, а проверяют его программным таймером раз в 100 мс.
Тот же приём годится для гашения экрана по бездействию, автоматического выхода из меню, отпускания захвата.
29.7 🔧 Сторож прибора на программном таймере
Соблазн очевиден: завести таймер, который раз в 200 мс кормит аппаратный сторож (раздел 12), — раз демон работает, система жива. Не жива. Демон — это отдельная задача с высшим приоритетом, и он крутится, даже когда все остальные задачи зависли. Такой сторож повторяет ошибку «кормления из прерывания» (12.7) и защищает только от смерти самого ядра.
Правильно — кормить, только если с прошлого раза отметились все задачи.
Каждая ставит свой бит в группе событий (раздел 31), а обратный вызов таймера
одним вызовом читает и сбрасывает биты: xEventGroupClearBits возвращает
значение группы до сброса. Кормит он, лишь если собраны все биты.
#define ALIVE_ALL 0x07u /* три задачи — биты 0–2 */
/* каждая задача — раз за оборот своего цикла */
void i_am_alive(unsigned task_no)
{
xEventGroupSetBits(alive, 1u << task_no);
}
/* неправильно: демон жив всегда, даже когда все задачи зависли */
static void feed_blind(TimerHandle_t t)
{
(void)t;
hw_watchdog_feed();
}
/* правильно: кормить, только если с прошлого раза отметились все */
static void feed_checked(TimerHandle_t t)
{
(void)t;
EventBits_t was = xEventGroupClearBits(alive, ALIVE_ALL); /* вернёт значение до сброса */
if ((was & ALIVE_ALL) == ALIVE_ALL)
hw_watchdog_feed();
}
Опыт поставлен на настоящем ядре с портом POSIX. Три задачи отмечаются раз в 100 мс, таймер срабатывает раз в 200 мс, а модель аппаратного сторожа сбрасывает камень, если кормления не было 500 мс. На 1000-й миллисекунде задача 1 зависает — ждёт то, что никогда не придёт.
| Кормление | Что вышло |
|---|---|
| «демон жив — кормлю» | сброса нет за 3 с: прибор с зависшей задачей работает дальше |
| только при всех отметках | сброс на 1510-й миллисекунде: последнее кормление было на 1000-й |
Порядок величин обязателен: задачи отмечаются чаще, чем срабатывает таймер, а таймер — чаще, чем истекает сторож: 100 < 200 < 500 мс. Задача, которая ждёт очередь или событие без ограничения времени, отметиться не сможет. Её ожидание ограничивают тайм-аутом короче периода таймера — или не включают её в маску.
29.8 Демон как исполнитель по запросу
У демона есть ещё одна роль, не связанная с таймерами: выполнить произвольную функцию.
static void process_sample(void *arg1, uint32_t arg2)
{
/* arg2 — отсчёт, переданный из прерывания */
}
void ADC1_2_IRQHandler(void)
{
BaseType_t woken = pdFALSE;
xTimerPendFunctionCallFromISR(process_sample, NULL, ADC1->DR, &woken);
portYIELD_FROM_ISR(woken);
}
Функция собирается, только если в конфигурации стоит
#define INCLUDE_xTimerPendFunctionCall 1 (по умолчанию 0), иначе компоновщик
её не найдёт.
Это отложенная обработка прерывания без собственной задачи: прерывание просит демона выполнить функцию, тот выполняет её, когда получит процессор. Экономия — целая задача с её стеком на каждое такое прерывание.
Ограничение очевидно: все запросы выполняет одна задача, по очереди и с приоритетом демона. Для редких событий — отлично; для потока, где важна независимость обработчиков, — своя задача.
29.9 Грабли
Таймер не срабатывает. Не запущен: создание не запускает.
Либо планировщик не стартовал, либо configUSE_TIMERS выключен.
Все таймеры встали разом. Обратный вызов заблокировался и остановил демона. Искать ожидание с ненулевым временем.
Таймер срабатывает с опозданием. Приоритет демона ниже задачи, которая занимает процессор; либо длинный обратный вызов другого таймера.
Остановленный таймер пошёл сам. xTimerChangePeriod запускает.
xTimerStart вернула отказ. Очередь команд полна — демон
не успевает разбирать. Либо демон заблокирован (см. выше).
Переполнение стека в демоне. printf или большой массив
в обратном вызове. Стек общий на все таймеры.
Второй аргумент xTimerStart не задерживает запуск. Это ожидание
места в очереди, а не пауза перед стартом.
Точнее миллисекунды не получается. И не получится: разрешение — тик. Нужен аппаратный таймер.
Прибор с зависшей задачей не перезагружается. Сторож кормит программный таймер без проверки, а демон жив всегда. Кормить только при отметках всех задач (29.7).
29.10 Итоги
- Программный таймер — запись в списке, который обслуживает одна задача-демон; стоит 44 байта, число не ограничено.
- Обратный вызов выполняется в демоне: не в прерывании и не в задаче, запустившей таймер.
- Все действия — команды через очередь; второй аргумент
xTimerStart— ожидание места в очереди, а не задержка запуска. - Автоперезапуск повторяется, однократный срабатывает раз и бездействует; созданный таймер не запущен.
- Обратный вызов не имеет права блокироваться: заблокируется демон, встанут все таймеры.
- Приоритет демона — максимальный, стек — общий на все обратные вызовы.
xTimerChangePeriodзапускает остановленный таймер.- Идентификатор позволяет одной функции обслуживать группу таймеров.
- Однократный таймер с перезапуском по событию — готовый сторож связи; перезапуск на каждый байт на 115 200 бод съедает 12 % процессора F103, и на быстрой линии перезапускают по кадру.
- Сторож, которого кормит таймер, полезен только с отметками задач: демон крутится, даже когда задачи зависли (29.7).
xTimerPendFunctionCallFromISRдаёт отложенную обработку без своей задачи, но все запросы идут по очереди через демона.
Дальше: раздел 30 «Синхронизация и потокобезопасность» — гонки, критические секции, мьютексы и та самая инверсия приоритетов, из-за которой перезагружался Mars Pathfinder.