Embedded HandbookПриложения
Все разделы › IV. FreeRTOS
STM32F103, STM32F407 · уровень средний · 12 мин чтения
Открыть в интерактивном справочнике

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);
}

Каждый принятый байт перезапускает таймер. Пока данные идут, он не срабатывает никогда. Замолчали — сработает ровно через две секунды после последнего байта. Ни одной строки логики про время в самом протоколе.

Сторож связи на однократном таймере: байты перезапускают отсчёт, после тишины он срабатывает через 2 с

Цена — на каждом байте. Каждый вызов 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 Итоги

Дальше: раздел 30 «Синхронизация и потокобезопасность» — гонки, критические секции, мьютексы и та самая инверсия приоритетов, из-за которой перезагружался Mars Pathfinder.

Открыть в интерактивном справочнике