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

32а. Активные объекты: архитектура прибора на событиях — задача, своя очередь и автомат

Разделы 26–32 дали детали: задачи, очереди, программные таймеры, мьютексы, группы событий, буферы. Остался вопрос, как собрать из них прибор целиком. Привычный ответ — задача на каждое дело, внутри vTaskDelay, общие переменные под мьютексом. Он работает, пока задач три. Дальше приходят инверсия приоритетов (30.5), взаимная блокировка (30.6) и задачи, которые спят в ту минуту, когда от них ждут ответа.

Здесь другой порядок — активные объекты. Каждая часть прибора — задача со своей очередью событий и конечным автоматом внутри. Ждёт она в одном месте: на своей очереди. Всё остальное приходит событием: нажатие, результат измерения, истёкшее время. Это приём раздела 23а (автоматы и очередь событий без ОС), но автоматов теперь несколько, у каждого свой приоритет, и чей шаг важнее, решает ядро. Задача-привратник из 30.8 — частный случай: один объект и один вид события.

Идея не новая: на ней построены каркасы QP и FreeACT Миро Самека (github.com/QuantumLeaps/FreeACT). Их исходный код здесь не использован и не разбирается: каркас раздела написан заново, его файл ao.c — 90 строк.

Опоры: ядро FreeRTOS 10.5.1 (queue.c, timers.c, heap_4.c, порт ARM_CM3), разделы 27 (очереди), 28 (прерывания), 29 (программные таймеры), 23а (автоматы). Весь код собран arm-none-eabi-gcc 12.2 с ключом -Os и прогнан в QEMU на модели Cortex-M3 с тем же портом ядра, что у STM32F103. Время модели считается по числу выполненных команд, поэтому прогон повторяется тик в тик. Тик — 1 мс. Все числа раздела — из этого прогона.

Как это называется в документации

Оригинал Перевод
active object, actor активный объект
event, signal событие и его сигнал: что случилось
event queue, mailbox очередь событий объекта
post положить событие в очередь
dispatch передать событие автомату
run to completion (RTC) шаг до конца: событие обработано целиком, потом следующее
time event событие по времени
one-shot / periodic однократное / периодическое
publish — subscribe издатель и подписчик
shared-nothing без общих данных
queue overflow переполнение очереди
stack high-water mark высшая отметка стека

32а.1 Идея: задача, своя очередь, автомат

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

  1. Одно место ожидания. Объект ждёт только на своей очереди. Нет события — задача спит и процессор не занимает.
  2. Шаг до конца. Получив событие, автомат делает работу и возвращается. Внутри шага нет ни vTaskDelay, ни ожидания очереди, семафора или мьютекса.
  3. Общих данных нет. Поля объекта трогает только его задача. Другому объекту нужен результат — ему шлют событие, параметр едет значением.
  4. Время — тоже событие. «Подождать 20 мс» значит взвести таймер и вернуться. Когда время выйдет, в ту же очередь придёт событие.
  5. Прерывание только кладёт событие. Это отложенная обработка из 28.4, доведённая до правила.

Пример на весь раздел — прибор из трёх объектов. Кнопка давит дребезг и сообщает о нажатии. Измеритель опрашивает датчик раз в 25 мс и раз в 100 мс отдаёт среднее. Индикатор мигает светодиодом: нажатие меняет полупериод (500 или 250 мс), показание выше порога включает тревогу — частое мигание с полупериодом 50 мс.

Прибор из трёх активных объектов: прерывание от кнопки кладёт событие EV_EDGE в очередь объекта «Кнопка», её автомат давит дребезг и шлёт EV_PRESS в очередь объекта «Индикатор»; объект «Измеритель» шлёт туда же EV_MEAS; демон таймеров кладёт события времени EV_TIMEOUT и EV_TICK в те же очереди; у каждого объекта своя задача, своя очередь на четыре события и автомат

Что это даёт. Мьютексы не нужны совсем: защищать нечего, значит, нечему дать инверсию приоритетов или взаимную блокировку. Реакция не ждёт, пока кто-то проснётся: событие попало в очередь — задача готова. Каждый объект можно читать отдельно: всё, что с ним бывает, — это перечень событий и состояний.

Чем за это платят: очередь на каждый объект, задача-демон таймеров (29.3) и логика, записанная автоматом, а не строками с задержками между ними. Сколько это стоит в байтах и тиках, посчитано в 32а.4.


32а.2 Каркас: событие, объект, диспетчер, время

Весь каркас — два файла, ao.h и ao.c. Сначала типы:

enum { AO_SIG_INIT, AO_SIG_USER };          /* свои сигналы объявляют начиная с AO_SIG_USER */

typedef struct {
    uint16_t sig;                           /* сигнал: что случилось */
    int32_t par;                            /* параметр — значением, не указателем */
} ao_evt_t;

typedef struct ao ao_t;
typedef void (*ao_step_t)(ao_t *me, const ao_evt_t *e);

struct ao {
    QueueHandle_t q;                        /* своя очередь событий */
    TaskHandle_t task;                      /* своя задача */
    ao_step_t step;                         /* автомат: один шаг на одно событие */
    uint16_t lost;                          /* событий не поместилось в очередь */
    uint16_t longest;                       /* самый долгий шаг, тиков */
    uint8_t peak;                           /* наибольшая заполненность очереди */
};

typedef struct {
    TimerHandle_t t;                        /* программный таймер ядра */
    ao_t *target;                           /* кому придёт событие */
    uint16_t sig;                           /* и какое */
} ao_tmr_t;

Событие занимает 8 байт: сигнал и один параметр. Очередь копирует событие целиком (27.1), поэтому после отправки оно от отправителя не зависит. Объект — 20 байт: очередь, задача, указатель на шаг автомата и три счётчика для наблюдения. Событие по времени — 12 байт: таймер ядра, получатель и сигнал.

Запуск и диспетчер. У всех объектов одна функция задачи. Она вызывает шаг с сигналом AO_SIG_INIT, а потом в цикле берёт событие и отдаёт автомату:

static void ao_task(void *arg)
{
    ao_t *me = arg;
    ao_evt_t e = { AO_SIG_INIT, 0 };
    me->step(me, &e);                                   /* начальный шаг: взвести таймеры, занять исходное состояние */
    for (;;) {
        xQueueReceive(me->q, &e, portMAX_DELAY);        /* единственное место, где объект ждёт */
        UBaseType_t n = uxQueueMessagesWaiting(me->q) + 1u;
        if (n > me->peak)
            me->peak = (uint8_t)n;                      /* сколько событий ждало, считая это */
        TickType_t t0 = xTaskGetTickCount();
        me->step(me, &e);                               /* шаг автомата — до конца, без ожиданий внутри */
        TickType_t dt = xTaskGetTickCount() - t0;
        if (dt > me->longest)
            me->longest = (uint16_t)dt;
    }
}

void ao_start(ao_t *me, const char *name, ao_step_t step, UBaseType_t prio, UBaseType_t qlen, uint16_t stack_words)
{
    me->step = step;
    me->lost = 0;
    me->longest = 0;
    me->peak = 0;
    me->q = xQueueCreate(qlen, sizeof(ao_evt_t));
    configASSERT(me->q != NULL);
    BaseType_t ok = xTaskCreate(ao_task, name, stack_words, me, prio, &me->task);
    configASSERT(ok == pdPASS);
    (void)ok;
}

Попутно цикл ведёт два счётчика: peak — сколько событий ждало в очереди в худший момент, longest — сколько тиков длился самый долгий шаг. Оба пригодятся в 32а.6.

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

static void ao_lost(ao_t *me)
{
    UBaseType_t m = taskENTER_CRITICAL_FROM_ISR();      /* годится и в задаче, и в прерывании */
    me->lost++;
    taskEXIT_CRITICAL_FROM_ISR(m);
#ifdef AO_STRICT
    configASSERT(me->lost == 0);                        /* строгий режим: полная очередь — ошибка проекта */
#endif
}

bool ao_post(ao_t *me, uint16_t sig, int32_t par)
{
    ao_evt_t e = { sig, par };
    if (xQueueSend(me->q, &e, 0) == pdPASS)             /* ждать места нельзя: отправитель сам внутри своего шага */
        return true;
    ao_lost(me);
    return false;
}

bool ao_post_from_isr(ao_t *me, uint16_t sig, int32_t par, BaseType_t *woken)
{
    ao_evt_t e = { sig, par };
    if (xQueueSendFromISR(me->q, &e, woken) == pdPASS)
        return true;
    ao_lost(me);
    return false;
}

Счётчик lost увеличивается в критической секции: отправителей у объекта несколько, среди них прерывание. Макросы с FROM_ISR в порте для Cortex-M годятся и в задаче, и в прерывании. Ключ -DAO_STRICT превращает первую потерю в остановку по configASSERT.

Время. Событие по времени — программный таймер ядра (раздел 29), чей обратный вызов делает одно: кладёт событие получателю. Обратный вызов выполняется в задаче-демоне и блокироваться не вправе (29.4) — у ao_post ожидание нулевое.

static void ao_tmr_cb(TimerHandle_t t)
{
    ao_tmr_t *tm = pvTimerGetTimerID(t);
    ao_post(tm->target, tm->sig, 0);                    /* выполняется в задаче-демоне: только положить событие */
}

void ao_tmr_init(ao_tmr_t *tm, ao_t *target, uint16_t sig)
{
    tm->target = target;
    tm->sig = sig;
    tm->t = xTimerCreate("ao", 1, pdFALSE, tm, ao_tmr_cb);
    configASSERT(tm->t != NULL);
}

void ao_tmr_arm(ao_tmr_t *tm, uint32_t ms, bool periodic)
{
    vTimerSetReloadMode(tm->t, periodic ? pdTRUE : pdFALSE);
    BaseType_t ok = xTimerChangePeriod(tm->t, pdMS_TO_TICKS(ms), 0);   /* новый период и пуск; идущий таймер считает заново */
    configASSERT(ok == pdPASS);
    (void)ok;
}

void ao_tmr_disarm(ao_tmr_t *tm)
{
    BaseType_t ok = xTimerStop(tm->t, 0);
    configASSERT(ok == pdPASS);
    (void)ok;
}

xTimerChangePeriod и задаёт период, и пускает таймер; если таймер уже шёл, отсчёт начинается заново — это и нужно антидребезгу. vTimerSetReloadMode выбирает, придёт событие один раз или будет приходить каждый период.

Каркас занимает 470 байт кода при -Os: ao_start 88, ao_task 76, ao_tmr_arm 64, ao_tmr_init 60, ao_tmr_disarm 44, ao_post_from_isr 40, ao_post 38, ao_lost 32, ao_tmr_cb 20 и 8 байт строк.


32а.3 Прибор из трёх объектов

Параметры прибора (dev.h):

#define DEBOUNCE_MS   20u                   /* вход спокоен столько — дребезг кончился */
#define SAMPLE_MS     25u                   /* опрос датчика */
#define SAMPLES       4                     /* среднее по четырём опросам — раз в 100 мс */
#define LIMIT_ON      800                   /* выше — тревога */
#define LIMIT_OFF     700                   /* ниже — отбой */
#define ALARM_HALF_MS 50u                   /* полупериод мигания в тревоге */
#define HALF_MS(mode) ((mode) ? 250u : 500u) /* полупериод в обычном режиме: кнопка переключает */

Сигналы и объекты (objs.h). Структура каркаса стоит в объекте первым полем, поэтому указатель на ao_t, который получает шаг, приводится к типу объекта:

enum { EV_EDGE = AO_SIG_USER, EV_TIMEOUT, EV_PRESS, EV_TICK, EV_MEAS };

typedef struct {
    ao_t ao;                                /* первым полем: указатель на объект годится как указатель на ao_t */
    ao_tmr_t tm;
    ao_t *client;                           /* кому сообщать о нажатии */
    uint8_t st;
    uint8_t stable;                         /* устоявшийся уровень: 1 — нажата */
} button_t;

typedef struct {
    ao_t ao;
    ao_tmr_t tm;
    uint8_t st;
    uint8_t mode;                           /* 0 — 500 мс, 1 — 250 мс */
} blinker_t;

typedef struct {
    ao_t ao;
    ao_tmr_t tm;
    ao_t *client;                           /* подписчик на результат */
    int32_t sum;
    uint8_t n;
} meter_t;

Кнопка (button.c). Автомат из двух состояний. Каждый фронт заново взводит однократное событие на 20 мс; пришло EV_TIMEOUT — вход молчал 20 мс, можно читать уровень.

enum { BTN_STABLE, BTN_SETTLING };

void button_step(ao_t *ao, const ao_evt_t *e)
{
    button_t *me = (button_t *)ao;
    switch (me->st) {
    case BTN_STABLE:                                    /* уровень устоялся, ждём фронт */
        if (e->sig == EV_EDGE) {
            ao_tmr_arm(&me->tm, DEBOUNCE_MS, false);
            me->st = BTN_SETTLING;
        }
        break;
    case BTN_SETTLING:                                  /* вход ещё скачет */
        if (e->sig == EV_EDGE)
            ao_tmr_arm(&me->tm, DEBOUNCE_MS, false);    /* новый фронт — отсчёт заново */
        if (e->sig == EV_TIMEOUT) {                     /* 20 мс тишины: читаем сам вывод */
            uint8_t now = (uint8_t)bsp_button_level();
            if (now != me->stable) {
                me->stable = now;
                if (now)
                    ao_post(me->client, EV_PRESS, 0);
            }
            me->st = BTN_STABLE;
        }
        break;
    }
}
Состояние EV_EDGE EV_TIMEOUT
устоялся взвести 20 мс → скачет —
скачет взвести 20 мс заново прочитать вход; нажата и раньше не была — EV_PRESS подписчику → устоялся

Сравните с 27.7: там задача кнопки просыпалась каждые 10 мс и опрашивала вход. Здесь между нажатиями объект не работает вовсе.

Индикатор (blinker.c). Мигание — периодическое событие EV_TICK. Сменить частоту — взвести то же событие с другим периодом.

enum { BL_NORMAL, BL_ALARM };

static void blinker_period(blinker_t *me, uint32_t ms)
{
    ao_tmr_arm(&me->tm, ms, true);                      /* периодическое событие EV_TICK */
    trc('M', (int32_t)ms);
}

void blinker_step(ao_t *ao, const ao_evt_t *e)
{
    blinker_t *me = (blinker_t *)ao;
    if (e->sig == AO_SIG_INIT) {
        blinker_period(me, HALF_MS(me->mode));
        return;
    }
    if (e->sig == EV_TICK) {                            /* в любом состоянии: время пришло событием */
        bsp_led_toggle();
        return;
    }
    switch (me->st) {
    case BL_NORMAL:
        if (e->sig == EV_PRESS) {
            me->mode ^= 1u;
            trc('B', me->mode);
            blinker_period(me, HALF_MS(me->mode));
        }
        if (e->sig == EV_MEAS && e->par > LIMIT_ON) {
            me->st = BL_ALARM;
            blinker_period(me, ALARM_HALF_MS);
        }
        break;
    case BL_ALARM:
        if (e->sig == EV_PRESS) {                       /* запомнить; применится после отбоя */
            me->mode ^= 1u;
            trc('B', me->mode);
        }
        if (e->sig == EV_MEAS && e->par < LIMIT_OFF) {
            me->st = BL_NORMAL;
            blinker_period(me, HALF_MS(me->mode));
        }
        break;
    }
}

Нажатие во время тревоги только запоминается: новый полупериод применится после отбоя. Функция trc — отметка в журнале стенда: время, буква, значение.

Измеритель (meter.c). Состояний нет, один счётчик. Результат уходит подписчику событием EV_MEAS, значение — в параметре.

void meter_step(ao_t *ao, const ao_evt_t *e)
{
    meter_t *me = (meter_t *)ao;
    if (e->sig == AO_SIG_INIT) {
        ao_tmr_arm(&me->tm, SAMPLE_MS, true);
        return;
    }
    if (e->sig == EV_TICK) {
        me->sum += bsp_adc_read();
        if (++me->n == SAMPLES) {
            ao_post(me->client, EV_MEAS, me->sum / SAMPLES);   /* результат — значением в событии */
            me->sum = 0;
            me->n = 0;
        }
    }
}

Сборка прибора (app_ao.c). Объекты — статические переменные. Связи между ними — адреса: кому слать. Обработчик прерывания состоит из одной отправки.

static button_t button;
static blinker_t blinker;
static meter_t meter;

void EXTI_IRQHandler(void)                              /* прерывание только кладёт событие */
{
    BaseType_t woken = pdFALSE;
    bsp_button_ack();
    ao_post_from_isr(&button.ao, EV_EDGE, 0, &woken);
    portYIELD_FROM_ISR(woken);
}

void app_start(void)
{
    button.client = &blinker.ao;                        /* связи между объектами — только адреса очередей */
    meter.client = &blinker.ao;
    ao_tmr_init(&button.tm, &button.ao, EV_TIMEOUT);
    ao_tmr_init(&blinker.tm, &blinker.ao, EV_TICK);
    ao_tmr_init(&meter.tm, &meter.ao, EV_TICK);
    ao_start(&button.ao, "button", button_step, PRIO_BUTTON, QLEN, STACK_WORDS);
    ao_start(&blinker.ao, "blinker", blinker_step, PRIO_BLINKER, QLEN, STACK_WORDS);
    ao_start(&meter.ao, "meter", meter_step, PRIO_METER, QLEN, STACK_WORDS);
}

Автоматы (как записывать их таблицей и указателями — 23а.2 и 125.7) не знают о железе: они видят bsp_button_level, bsp_adc_read, bsp_led_toggle. Это приём из 196.2, поэтому тот же код работает в стенде без платы.

Прогон. Стенд подаёт четыре нажатия с дребезгом: пять фронтов за 6 мс при нажатии и три при отпускании через 150 мс. Фронты приходят настоящим прерыванием. С 4215-й по 6215-ю мс датчик показывает 900 при пороге 800.

Что случилось Реакция индикатора Задержка, тиков
нажатие на 1000-й мс полупериод 250 мс на 1026-й 26
нажатие на 2130-й мс полупериод 500 мс на 2156-й 26
нажатие на 3370-й мс полупериод 250 мс на 3396-й 26
датчик выше порога на 4215-й мс тревога, 50 мс, на 4300-й 85
нажатие на 5040-й мс (в тревоге) режим запомнен на 5066-й 26
датчик в норме на 6215-й мс отбой, 500 мс, на 6300-й 85

26 тиков — это 6 мс дребезга и 20 мс тишины после последнего фронта. 85 тиков — время до конца окна усреднения: четыре показания по 25 мс собираются к 4300-й мс. Собственной задержки объекты не добавили: в каждой очереди ждало не больше одного события, потерь нет, самый долгий шаг короче тика. Светодиод переключился 54 раза.


32а.4 Тот же прибор по-старому и сравнение числами

Тот же прибор в блокирующем стиле (app_blk.c): три задачи, каждая спит в vTaskDelay. Прерывание будит задачу кнопки двоичным семафором. Режим и показание лежат в общей структуре под мьютексом.

static SemaphoreHandle_t s_edge;                        /* «на кнопке что-то менялось» */
static SemaphoreHandle_t m_data;                        /* мьютекс на общие данные */
static struct {
    uint8_t mode;                                       /* пишет кнопка, читает индикатор */
    int32_t value;                                      /* пишет измеритель, читает индикатор */
} shared;
static TaskHandle_t h_button, h_led, h_meter;

void EXTI_IRQHandler(void)
{
    BaseType_t woken = pdFALSE;
    bsp_button_ack();
    xSemaphoreGiveFromISR(s_edge, &woken);
    portYIELD_FROM_ISR(woken);
}

static void button_task(void *arg)
{
    uint8_t stable = 0;
    (void)arg;
    for (;;) {
        xSemaphoreTake(s_edge, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));         /* переждать дребезг — задача спит */
        uint8_t now = (uint8_t)bsp_button_level();
        if (now != stable) {
            stable = now;
            if (now) {
                xSemaphoreTake(m_data, portMAX_DELAY);
                shared.mode ^= 1u;
                xSemaphoreGive(m_data);
            }
        }
    }
}

static void meter_task(void *arg)
{
    TickType_t wake = xTaskGetTickCount();
    int32_t sum = 0;
    unsigned n = 0;
    (void)arg;
    for (;;) {
        vTaskDelayUntil(&wake, pdMS_TO_TICKS(SAMPLE_MS));
        sum += bsp_adc_read();
        if (++n == SAMPLES) {
            xSemaphoreTake(m_data, portMAX_DELAY);
            shared.value = sum / SAMPLES;
            xSemaphoreGive(m_data);
            sum = 0;
            n = 0;
        }
    }
}

static void led_task(void *arg)
{
    uint8_t mode = 0, alarm = 0;
    uint32_t half = 0;
    (void)arg;
    for (;;) {
        xSemaphoreTake(m_data, portMAX_DELAY);
        uint8_t m = shared.mode;
        int32_t v = shared.value;
        xSemaphoreGive(m_data);
        if (m != mode) {
            mode = m;
            trc('B', mode);
        }
        if (!alarm && v > LIMIT_ON)
            alarm = 1;
        else if (alarm && v < LIMIT_OFF)
            alarm = 0;
        uint32_t h = alarm ? ALARM_HALF_MS : HALF_MS(mode);
        if (h != half) {
            half = h;
            trc('M', (int32_t)half);
        }
        bsp_led_toggle();
        vTaskDelay(pdMS_TO_TICKS(half));                /* о нажатии и тревоге узнаем только после сна */
    }
}

Запись короче и читается сверху вниз. Оба варианта прогнаны на одном сценарии, с одинаковыми приоритетами (кнопка 4, индикатор 3, измеритель 2) и стеками по 128 слов.

Активные объекты Блокирующий
мьютексов / семафоров 0 / 0 1 / 1
очередей / программных таймеров 3 / 3 0 / 0
стек кнопки, занято слов из 128 56 34
стек индикатора 58 30
стек измерителя 52 30
стек демона таймеров 32 демона нет
куча: объекты прибора, байт 2536 2024
куча: задачи самого ядра, байт 1216 616
куча всего, байт 3752 2640
код прошивки стенда, байт 9412 7956
от нажатия до реакции индикатора, тиков 26, 26, 26, 26 500, 120, 380, 60
от порога датчика до тревоги, тиков 85 285
от возврата датчика до отбоя, тиков 85 135

Стек измерен uxTaskGetStackHighWaterMark (26.7) в конце прогона, куча — xPortGetFreeHeapSize до и после создания объектов. Из чего складывается куча, показывает отдельный замер — по одному объекту каждого вида:

Объект ядра Байт кучи
задача со стеком 128 слов 608 (616, когда в ядре включены мьютексы)
очередь на 4 события по 8 байт 120
программный таймер 56
очередь команд демона — создаётся с первым таймером 184
двоичный семафор 88
мьютекс 88

Активные объекты: 3 × 608 + 3 × 120 + 3 × 56 + 184 = 2536 байт и ещё две задачи ядра, простоя и демон, 2 × 608 = 1216. Блокирующий вариант: 3 × 616 + 88 + 88 = 2024 байта и одна задача простоя, 616. Сами объекты прибора — ещё 120 байт статической памяти (40 + 36 + 44) против 16 у блокирующего варианта (общая структура и два описателя). Почему запрос к куче дороже запрошенного — в 25.3.

По памяти активные объекты дороже: на 1112 байт кучи, на 1456 байт кода (каркас и программные таймеры ядра) и на 22–28 слов стека в каждой задаче. Стек глубже потому, что шаг автомата сам вызывает функции ядра — отправку в очередь, команду таймеру. При 128 словах занято меньше половины.

По времени реакции — наоборот. У активных объектов от нажатия до реакции всегда 26 тиков. В блокирующем варианте задача кнопки меняет общую переменную, но задача индикатора прочтёт её, только когда проснётся: в прогоне через 60, 120, 380 и 500 тиков — до целого полупериода мигания. Тревога опоздала на 200 тиков по той же причине. Починить это можно: пусть индикатор ждёт не vTaskDelay, а очередь с тайм-аутом, как в 27.7. Но это и есть шаг к событийной схеме.

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


32а.5 Что бывает, когда правила нарушают

Три опыта на том же стенде. В каждом изменён один файл.

Ожидание внутри шага. В blinker_bad.c серия вспышек тревоги сделана «как проще» — циклом с задержкой:

static void flash4(void)                                /* НЕЛЬЗЯ: 200 мс сна внутри обработчика */
{
    for (int i = 0; i < 4; i++) {
        bsp_led_toggle();
        vTaskDelay(pdMS_TO_TICKS(ALARM_HALF_MS));
    }
}

Она вызывается прямо из шага, на каждое показание выше порога:

        if (e->sig == EV_MEAS && e->par > LIMIT_ON) {
            me->st = BL_ALARM;
            ao_tmr_disarm(&me->tm);
            trc('M', (int32_t)ALARM_HALF_MS);
            flash4();
        }

Показания приходят раз в 100 мс, шаг длится 200 мс. Очередь на четыре события заполняется, дальше события пропадают.

По правилам С ожиданием в шаге
самый долгий шаг индикатора, тиков 0 200
наибольшая заполненность очереди (из 4) 1 4
потеряно событий 0 11
нажатие на 5040-й мс обработано на 5066-й потеряно
отбой после возврата датчика на 6215-й мс на 6300-й на 7100-й

Первым потерянным событием оказалось нажатие — на 5066-й мс. Человек нажал кнопку, кнопка сработала верно, а прибор не отозвался. Отбой опоздал на 800 тиков: индикатор дочитывал четыре старых показания по 200 мс каждое.

Долгий счёт внутри шага. В meter_long.c перед отправкой результата стоит «тяжёлый фильтр» — цикл на 400 000 оборотов, в модели это 65 тиков:

static uint32_t crunch(uint32_t n)                      /* долгий счёт без единого ожидания */
{
    volatile uint32_t acc = 0;
    while (n--)
        acc += n;
    return acc;
}

void meter_step(ao_t *ao, const ao_evt_t *e)
{
    meter_t *me = (meter_t *)ao;
    if (e->sig == AO_SIG_INIT) {
        ao_tmr_arm(&me->tm, SAMPLE_MS, true);
        return;
    }
    if (e->sig == EV_TICK) {
        me->sum += bsp_adc_read();
        if (++me->n == SAMPLES) {
            crunch(LONG_WORK);                          /* «тяжёлый фильтр» прямо в шаге */
            ao_post(me->client, EV_MEAS, me->sum / SAMPLES);
            me->sum = 0;
            me->n = 0;
        }
    }
}

Ожиданий здесь нет, правило 2 формально соблюдено. Что будет с остальными, зависит от приоритета:

Измеритель ниже всех (приоритет 2) Измеритель выше всех (приоритет 5)
самый долгий шаг измерителя, тиков 65 65
от нажатия до реакции, тиков 26, 26, 26, 26 85, 55, 26, 45
очередь кнопки: заполненность / потери 1 / 0 4 / 3
тревога после порога датчика, тиков 150 150

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

Указатель на локальные данные в событии. В ptr_ao.c отправитель кладёт в параметр события адрес массива из своего стека:

static __attribute__((noinline)) void send_report(int32_t value)
{
    volatile int32_t msg[4] = { value, value + 1, value + 2, value + 3 };   /* живёт в стеке отправителя */
    ao_post(&receiver, EV_REPORT, (int32_t)(intptr_t)msg);                  /* ОШИБКА: в событии — адрес стека */
}

static __attribute__((noinline)) int32_t other_work(void)
{
    volatile int32_t scratch[8];
    for (int i = 0; i < 8; i++)
        scratch[i] = -1;                                /* то же место стека занято другими данными */
    return scratch[0];
}

static void sender_step(ao_t *me, const ao_evt_t *e)
{
    (void)me;
    if (e->sig == AO_SIG_INIT)
        ao_tmr_arm(&tm, 100, true);
    if (e->sig == EV_TICK) {
        send_report(215);
        other_work();
    }
}

static void receiver_step(ao_t *me, const ao_evt_t *e)
{
    (void)me;
    if (e->sig == EV_REPORT) {
        const volatile int32_t *msg = (const volatile int32_t *)(intptr_t)e->par;
        trc('R', msg[0]);                               /* ждём 215 */
    }
}
Приоритет получателя Прочитано девять раз из девяти
3 — выше отправителя (2) 215
1 — ниже отправителя 5276

Получатель выше по приоритету вытесняет отправителя прямо внутри ao_post и читает массив, пока тот жив. Получатель ниже ждёт, пока отправитель закончит шаг, — и читает то, что легло в стек на это место позже. Код одинаков, отличается одно число в настройке. Такая ошибка проходит все проверки и просыпается, когда кто-то меняет приоритеты.

Правило: в событии — значение. Если данных много, передают указатель на блок из пула или на статический буфер, которым после отправки владеет получатель (27.5).


32а.6 Переполнение очереди: что делать и как выбрать длину

Очередь объекта конечна, и отправитель места не ждёт. Значит, событие может не поместиться. Это не ошибка каркаса, а сигнал о проекте: объект не успевает или его заслоняют. Средств четыре.

Считать. Каркас ведёт у каждого объекта lost, peak и longest. Их стоит выводить в журнал или отдавать по команде — как высшую отметку стека.

Останавливаться в отладке. Сборка с -DAO_STRICT на первой же потере даёт в прогоне строку assert ao.c:41 t=5066 — файл, строка и тик. В изделии остановка не годится, там остаётся счётчик.

Выбирать длину по измерению. Длину не угадывают, а меряют peak в худшем сценарии:

Прогон Очередь Длина peak Потерь
по правилам любая из трёх 4 1 0
долгий счёт, измеритель выше всех кнопки 4 4 3
то же кнопки 8 5 0
ожидание в шаге индикатора 4 4 11
то же индикатора 8 8 10

peak меньше длины — очереди хватает, разница и есть запас. peak равен длине — замер упёрся в потолок, настоящая потребность неизвестна: длину увеличивают и меряют снова. У кнопки вышло 5 из 8 — столько фронтов в одной пачке дребезга. Место стоит 8 байт: переход с 4 на 8 событий в трёх очередях прибавил к куче 96 байт (2632 вместо 2536).

Не лечить длиной блокирующий шаг. Последняя строка таблицы: очередь вдвое длиннее, а потерь 10 вместо 11. Нажатие на 5040-й мс теперь не потеряно, но обработано на 5900-й — через 860 тиков; отбой пришёл на 7900-й мс, через 1685 тиков после возврата датчика. Если события приходят быстрее, чем объект их разбирает, любая очередь переполнится (27.6), а длинная ещё и превратит потери в опоздания.

Расставлять приоритеты. Выше — объекты с короткими шагами, которым события приходят пачками (кнопка, приём по связи). Ниже всех — объект с долгой работой. В опыте 32а.5 это разница между нулём и тремя потерями при одном и том же коде.


32а.7 Грабли

Ожидание внутри шага. vTaskDelay, ожидание очереди или семафора с ненулевым временем в обработчике события. В опыте — 11 потерянных событий, среди них нажатие кнопки. Ожидание записывают состоянием автомата и событием по времени.

Длинная очередь вместо исправления. Очередь на 8 вместо 4 при блокирующем шаге: потерь 10 вместо 11, реакция на нажатие — 860 тиков.

Долгая работа в объекте с высоким приоритетом. Шаг в 65 тиков у измерителя с приоритетом 5: кнопка потеряла три фронта, реакция на нажатие выросла с 26 до 85 тиков. Долгую работу отдают объекту с низшим приоритетом или режут на куски, между которыми объект шлёт событие самому себе.

Адрес локальной переменной в событии. Работает, пока получатель выше по приоритету (215), и ломается, когда ниже (5276). В событии — значение.

peak равен длине очереди. Это не «очереди хватило впритык», а «сколько нужно, неизвестно». Увеличить и измерить снова.

Отправка с ожиданием места. Заменить в ao_post нулевое ожидание на portMAX_DELAY кажется способом ничего не терять. Но шаг начинает блокироваться, а два объекта, которые шлют события друг другу, при полных очередях встанут оба — та же взаимная блокировка, что в 30.6, только без мьютексов.

Событие времени, которое уже в очереди. Остановка или перезапуск таймера не отзывает событие, которое демон успел положить. Автомат может получить EV_TIMEOUT от прежнего взвода. Если это важно, в параметр события кладут номер взвода и чужие номера пропускают.

Прямой вызов вместо события. Функция одного объекта, вызванная из шага другого, выполняется в чужой задаче и трогает чужие поля — общие данные вернулись, а с ними гонка из 30.1.

Демон таймеров ниже объектов. События времени кладёт задача-демон; если её приоритет ниже, чем у занятого объекта, время опаздывает (29.3). В стенде демон выше всех.

Прерывание выше порога ядра. ao_post_from_isr зовёт функцию ядра с FromISR — приоритет прерывания должен быть не выше configMAX_SYSCALL_INTERRUPT_PRIORITY (28.5).

Стек по привычке. Задачам активных объектов нужно больше стека, чем блокирующим: в прогоне 52–58 слов против 30–34. Мерить, а не переносить числа из старого проекта.


32а.8 Итоги

Полные исходники

ao.h — каркас активных объектов: типы и вызовы.

/* ao.h — маленький каркас активных объектов поверх FreeRTOS (раздел 32а). */
#ifndef AO_H
#define AO_H
#include <stdbool.h>
#include <stdint.h>
#include "FreeRTOS.h"
#include "queue.h"
#include "task.h"
#include "timers.h"

enum { AO_SIG_INIT, AO_SIG_USER };          /* свои сигналы объявляют начиная с AO_SIG_USER */

typedef struct {
    uint16_t sig;                           /* сигнал: что случилось */
    int32_t par;                            /* параметр — значением, не указателем */
} ao_evt_t;

typedef struct ao ao_t;
typedef void (*ao_step_t)(ao_t *me, const ao_evt_t *e);

struct ao {
    QueueHandle_t q;                        /* своя очередь событий */
    TaskHandle_t task;                      /* своя задача */
    ao_step_t step;                         /* автомат: один шаг на одно событие */
    uint16_t lost;                          /* событий не поместилось в очередь */
    uint16_t longest;                       /* самый долгий шаг, тиков */
    uint8_t peak;                           /* наибольшая заполненность очереди */
};

typedef struct {
    TimerHandle_t t;                        /* программный таймер ядра */
    ao_t *target;                           /* кому придёт событие */
    uint16_t sig;                           /* и какое */
} ao_tmr_t;

void ao_start(ao_t *me, const char *name, ao_step_t step, UBaseType_t prio, UBaseType_t qlen, uint16_t stack_words);
bool ao_post(ao_t *me, uint16_t sig, int32_t par);
bool ao_post_from_isr(ao_t *me, uint16_t sig, int32_t par, BaseType_t *woken);
void ao_tmr_init(ao_tmr_t *tm, ao_t *target, uint16_t sig);
void ao_tmr_arm(ao_tmr_t *tm, uint32_t ms, bool periodic);
void ao_tmr_disarm(ao_tmr_t *tm);
#endif

ao.c — каркас: задача, очередь, события по времени.

/* ao.c — каркас активных объектов: задача, своя очередь, события по времени (раздел 32а). */
#include "ao.h"

static void ao_task(void *arg)
{
    ao_t *me = arg;
    ao_evt_t e = { AO_SIG_INIT, 0 };
    me->step(me, &e);                                   /* начальный шаг: взвести таймеры, занять исходное состояние */
    for (;;) {
        xQueueReceive(me->q, &e, portMAX_DELAY);        /* единственное место, где объект ждёт */
        UBaseType_t n = uxQueueMessagesWaiting(me->q) + 1u;
        if (n > me->peak)
            me->peak = (uint8_t)n;                      /* сколько событий ждало, считая это */
        TickType_t t0 = xTaskGetTickCount();
        me->step(me, &e);                               /* шаг автомата — до конца, без ожиданий внутри */
        TickType_t dt = xTaskGetTickCount() - t0;
        if (dt > me->longest)
            me->longest = (uint16_t)dt;
    }
}

void ao_start(ao_t *me, const char *name, ao_step_t step, UBaseType_t prio, UBaseType_t qlen, uint16_t stack_words)
{
    me->step = step;
    me->lost = 0;
    me->longest = 0;
    me->peak = 0;
    me->q = xQueueCreate(qlen, sizeof(ao_evt_t));
    configASSERT(me->q != NULL);
    BaseType_t ok = xTaskCreate(ao_task, name, stack_words, me, prio, &me->task);
    configASSERT(ok == pdPASS);
    (void)ok;
}

static void ao_lost(ao_t *me)
{
    UBaseType_t m = taskENTER_CRITICAL_FROM_ISR();      /* годится и в задаче, и в прерывании */
    me->lost++;
    taskEXIT_CRITICAL_FROM_ISR(m);
#ifdef AO_STRICT
    configASSERT(me->lost == 0);                        /* строгий режим: полная очередь — ошибка проекта */
#endif
}

bool ao_post(ao_t *me, uint16_t sig, int32_t par)
{
    ao_evt_t e = { sig, par };
    if (xQueueSend(me->q, &e, 0) == pdPASS)             /* ждать места нельзя: отправитель сам внутри своего шага */
        return true;
    ao_lost(me);
    return false;
}

bool ao_post_from_isr(ao_t *me, uint16_t sig, int32_t par, BaseType_t *woken)
{
    ao_evt_t e = { sig, par };
    if (xQueueSendFromISR(me->q, &e, woken) == pdPASS)
        return true;
    ao_lost(me);
    return false;
}

static void ao_tmr_cb(TimerHandle_t t)
{
    ao_tmr_t *tm = pvTimerGetTimerID(t);
    ao_post(tm->target, tm->sig, 0);                    /* выполняется в задаче-демоне: только положить событие */
}

void ao_tmr_init(ao_tmr_t *tm, ao_t *target, uint16_t sig)
{
    tm->target = target;
    tm->sig = sig;
    tm->t = xTimerCreate("ao", 1, pdFALSE, tm, ao_tmr_cb);
    configASSERT(tm->t != NULL);
}

void ao_tmr_arm(ao_tmr_t *tm, uint32_t ms, bool periodic)
{
    vTimerSetReloadMode(tm->t, periodic ? pdTRUE : pdFALSE);
    BaseType_t ok = xTimerChangePeriod(tm->t, pdMS_TO_TICKS(ms), 0);   /* новый период и пуск; идущий таймер считает заново */
    configASSERT(ok == pdPASS);
    (void)ok;
}

void ao_tmr_disarm(ao_tmr_t *tm)
{
    BaseType_t ok = xTimerStop(tm->t, 0);
    configASSERT(ok == pdPASS);
    (void)ok;
}

dev.h — параметры прибора.

/* dev.h — параметры прибора, общие для обоих вариантов (раздел 32а). */
#ifndef DEV_H
#define DEV_H
#include "bsp.h"

#define DEBOUNCE_MS   20u                   /* вход спокоен столько — дребезг кончился */
#define SAMPLE_MS     25u                   /* опрос датчика */
#define SAMPLES       4                     /* среднее по четырём опросам — раз в 100 мс */
#define LIMIT_ON      800                   /* выше — тревога */
#define LIMIT_OFF     700                   /* ниже — отбой */
#define ALARM_HALF_MS 50u                   /* полупериод мигания в тревоге */
#define HALF_MS(mode) ((mode) ? 250u : 500u) /* полупериод в обычном режиме: кнопка переключает */

#ifndef PRIO_BUTTON
#define PRIO_BUTTON   4
#endif
#ifndef PRIO_BLINKER
#define PRIO_BLINKER  3
#endif
#ifndef PRIO_METER
#define PRIO_METER    2
#endif
#ifndef QLEN
#define QLEN          4                     /* длина очереди каждого объекта */
#endif
#define STACK_WORDS   128
#endif

objs.h — сигналы и три объекта прибора.

/* objs.h — три активных объекта прибора: кнопка, индикатор, измеритель (раздел 32а). */
#ifndef OBJS_H
#define OBJS_H
#include "ao.h"
#include "dev.h"

enum { EV_EDGE = AO_SIG_USER, EV_TIMEOUT, EV_PRESS, EV_TICK, EV_MEAS };

typedef struct {
    ao_t ao;                                /* первым полем: указатель на объект годится как указатель на ao_t */
    ao_tmr_t tm;
    ao_t *client;                           /* кому сообщать о нажатии */
    uint8_t st;
    uint8_t stable;                         /* устоявшийся уровень: 1 — нажата */
} button_t;

typedef struct {
    ao_t ao;
    ao_tmr_t tm;
    uint8_t st;
    uint8_t mode;                           /* 0 — 500 мс, 1 — 250 мс */
} blinker_t;

typedef struct {
    ao_t ao;
    ao_tmr_t tm;
    ao_t *client;                           /* подписчик на результат */
    int32_t sum;
    uint8_t n;
} meter_t;

void button_step(ao_t *ao, const ao_evt_t *e);
void blinker_step(ao_t *ao, const ao_evt_t *e);
void meter_step(ao_t *ao, const ao_evt_t *e);
#endif

button.c — объект «кнопка».

/* button.c — активный объект «кнопка»: антидребезг автоматом из двух состояний. */
#include "objs.h"

enum { BTN_STABLE, BTN_SETTLING };

void button_step(ao_t *ao, const ao_evt_t *e)
{
    button_t *me = (button_t *)ao;
    switch (me->st) {
    case BTN_STABLE:                                    /* уровень устоялся, ждём фронт */
        if (e->sig == EV_EDGE) {
            ao_tmr_arm(&me->tm, DEBOUNCE_MS, false);
            me->st = BTN_SETTLING;
        }
        break;
    case BTN_SETTLING:                                  /* вход ещё скачет */
        if (e->sig == EV_EDGE)
            ao_tmr_arm(&me->tm, DEBOUNCE_MS, false);    /* новый фронт — отсчёт заново */
        if (e->sig == EV_TIMEOUT) {                     /* 20 мс тишины: читаем сам вывод */
            uint8_t now = (uint8_t)bsp_button_level();
            if (now != me->stable) {
                me->stable = now;
                if (now)
                    ao_post(me->client, EV_PRESS, 0);
            }
            me->st = BTN_STABLE;
        }
        break;
    }
}

blinker.c — объект «индикатор».

/* blinker.c — активный объект «индикатор»: мигает, кнопкой меняет частоту, по измерению уходит в тревогу. */
#include "objs.h"

enum { BL_NORMAL, BL_ALARM };

static void blinker_period(blinker_t *me, uint32_t ms)
{
    ao_tmr_arm(&me->tm, ms, true);                      /* периодическое событие EV_TICK */
    trc('M', (int32_t)ms);
}

void blinker_step(ao_t *ao, const ao_evt_t *e)
{
    blinker_t *me = (blinker_t *)ao;
    if (e->sig == AO_SIG_INIT) {
        blinker_period(me, HALF_MS(me->mode));
        return;
    }
    if (e->sig == EV_TICK) {                            /* в любом состоянии: время пришло событием */
        bsp_led_toggle();
        return;
    }
    switch (me->st) {
    case BL_NORMAL:
        if (e->sig == EV_PRESS) {
            me->mode ^= 1u;
            trc('B', me->mode);
            blinker_period(me, HALF_MS(me->mode));
        }
        if (e->sig == EV_MEAS && e->par > LIMIT_ON) {
            me->st = BL_ALARM;
            blinker_period(me, ALARM_HALF_MS);
        }
        break;
    case BL_ALARM:
        if (e->sig == EV_PRESS) {                       /* запомнить; применится после отбоя */
            me->mode ^= 1u;
            trc('B', me->mode);
        }
        if (e->sig == EV_MEAS && e->par < LIMIT_OFF) {
            me->st = BL_NORMAL;
            blinker_period(me, HALF_MS(me->mode));
        }
        break;
    }
}

meter.c — объект «измеритель».

/* meter.c — активный объект «измеритель»: опрос раз в 25 мс, среднее по четырём — событием подписчику. */
#include "objs.h"

void meter_step(ao_t *ao, const ao_evt_t *e)
{
    meter_t *me = (meter_t *)ao;
    if (e->sig == AO_SIG_INIT) {
        ao_tmr_arm(&me->tm, SAMPLE_MS, true);
        return;
    }
    if (e->sig == EV_TICK) {
        me->sum += bsp_adc_read();
        if (++me->n == SAMPLES) {
            ao_post(me->client, EV_MEAS, me->sum / SAMPLES);   /* результат — значением в событии */
            me->sum = 0;
            me->n = 0;
        }
    }
}

app_ao.c — сборка прибора из объектов.

/* app_ao.c — прибор на активных объектах: кнопка, индикатор, измеритель (раздел 32а). */
#include "objs.h"
#include "rep.h"

static button_t button;
static blinker_t blinker;
static meter_t meter;

void EXTI_IRQHandler(void)                              /* прерывание только кладёт событие */
{
    BaseType_t woken = pdFALSE;
    bsp_button_ack();
    ao_post_from_isr(&button.ao, EV_EDGE, 0, &woken);
    portYIELD_FROM_ISR(woken);
}

void app_start(void)
{
    button.client = &blinker.ao;                        /* связи между объектами — только адреса очередей */
    meter.client = &blinker.ao;
    ao_tmr_init(&button.tm, &button.ao, EV_TIMEOUT);
    ao_tmr_init(&blinker.tm, &blinker.ao, EV_TICK);
    ao_tmr_init(&meter.tm, &meter.ao, EV_TICK);
    ao_start(&button.ao, "button", button_step, PRIO_BUTTON, QLEN, STACK_WORDS);
    ao_start(&blinker.ao, "blinker", blinker_step, PRIO_BLINKER, QLEN, STACK_WORDS);
    ao_start(&meter.ao, "meter", meter_step, PRIO_METER, QLEN, STACK_WORDS);
}

void app_report(void)
{
    ao_t *all[] = { &button.ao, &blinker.ao, &meter.ao };
    for (unsigned i = 0; i < 3; i++) {
        bsp_puts("ao "); bsp_puts(pcTaskGetName(all[i]->task));
        bsp_puts(" prio="); bsp_putu((uint32_t)uxTaskPriorityGet(all[i]->task));
        bsp_puts(" qlen="); bsp_putu(QLEN);
        bsp_puts(" peak="); bsp_putu(all[i]->peak);
        bsp_puts(" lost="); bsp_putu(all[i]->lost);
        bsp_puts(" longest="); bsp_putu(all[i]->longest); bsp_puts("\n");
        rep_stack(all[i]->task, STACK_WORDS);
    }
    rep_stack(xTimerGetTimerDaemonTaskHandle(), configTIMER_TASK_STACK_DEPTH);
    bsp_puts("sizeof evt="); bsp_putu(sizeof(ao_evt_t)); bsp_puts(" ao="); bsp_putu(sizeof(ao_t));
    bsp_puts(" tmr="); bsp_putu(sizeof(ao_tmr_t)); bsp_puts("\n");
    bsp_puts("sync mutex=0 sem=0 queue=3 timer=3\n");
}

app_blk.c — тот же прибор в блокирующем стиле.

/* app_blk.c — тот же прибор в блокирующем стиле: три задачи с vTaskDelay, общие данные под мьютексом,
 * прерывание кнопки будит задачу семафором (раздел 32а, для сравнения). */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "dev.h"
#include "rep.h"

static SemaphoreHandle_t s_edge;                        /* «на кнопке что-то менялось» */
static SemaphoreHandle_t m_data;                        /* мьютекс на общие данные */
static struct {
    uint8_t mode;                                       /* пишет кнопка, читает индикатор */
    int32_t value;                                      /* пишет измеритель, читает индикатор */
} shared;
static TaskHandle_t h_button, h_led, h_meter;

void EXTI_IRQHandler(void)
{
    BaseType_t woken = pdFALSE;
    bsp_button_ack();
    xSemaphoreGiveFromISR(s_edge, &woken);
    portYIELD_FROM_ISR(woken);
}

static void button_task(void *arg)
{
    uint8_t stable = 0;
    (void)arg;
    for (;;) {
        xSemaphoreTake(s_edge, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));         /* переждать дребезг — задача спит */
        uint8_t now = (uint8_t)bsp_button_level();
        if (now != stable) {
            stable = now;
            if (now) {
                xSemaphoreTake(m_data, portMAX_DELAY);
                shared.mode ^= 1u;
                xSemaphoreGive(m_data);
            }
        }
    }
}

static void meter_task(void *arg)
{
    TickType_t wake = xTaskGetTickCount();
    int32_t sum = 0;
    unsigned n = 0;
    (void)arg;
    for (;;) {
        vTaskDelayUntil(&wake, pdMS_TO_TICKS(SAMPLE_MS));
        sum += bsp_adc_read();
        if (++n == SAMPLES) {
            xSemaphoreTake(m_data, portMAX_DELAY);
            shared.value = sum / SAMPLES;
            xSemaphoreGive(m_data);
            sum = 0;
            n = 0;
        }
    }
}

static void led_task(void *arg)
{
    uint8_t mode = 0, alarm = 0;
    uint32_t half = 0;
    (void)arg;
    for (;;) {
        xSemaphoreTake(m_data, portMAX_DELAY);
        uint8_t m = shared.mode;
        int32_t v = shared.value;
        xSemaphoreGive(m_data);
        if (m != mode) {
            mode = m;
            trc('B', mode);
        }
        if (!alarm && v > LIMIT_ON)
            alarm = 1;
        else if (alarm && v < LIMIT_OFF)
            alarm = 0;
        uint32_t h = alarm ? ALARM_HALF_MS : HALF_MS(mode);
        if (h != half) {
            half = h;
            trc('M', (int32_t)half);
        }
        bsp_led_toggle();
        vTaskDelay(pdMS_TO_TICKS(half));                /* о нажатии и тревоге узнаем только после сна */
    }
}

void app_start(void)
{
    s_edge = xSemaphoreCreateBinary();
    m_data = xSemaphoreCreateMutex();
    configASSERT(s_edge != NULL && m_data != NULL);
    xTaskCreate(button_task, "button", STACK_WORDS, NULL, PRIO_BUTTON, &h_button);
    xTaskCreate(led_task, "led", STACK_WORDS, NULL, PRIO_BLINKER, &h_led);
    xTaskCreate(meter_task, "meter", STACK_WORDS, NULL, PRIO_METER, &h_meter);
}

void app_report(void)
{
    rep_stack(h_button, STACK_WORDS);
    rep_stack(h_led, STACK_WORDS);
    rep_stack(h_meter, STACK_WORDS);
    bsp_puts("sync mutex=1 sem=1 queue=0 timer=0\n");
}

blinker_bad.c — нарушение: ожидание внутри шага.

/* blinker_bad.c — нарушение правила: тот же индикатор, но серия вспышек тревоги сделана ожиданием
 * внутри шага (опыт 32а.5). Пока шаг спит, события копятся в очереди. */
#include "objs.h"

enum { BL_NORMAL, BL_ALARM };

static void blinker_period(blinker_t *me, uint32_t ms)
{
    ao_tmr_arm(&me->tm, ms, true);
    trc('M', (int32_t)ms);
}

static void flash4(void)                                /* НЕЛЬЗЯ: 200 мс сна внутри обработчика */
{
    for (int i = 0; i < 4; i++) {
        bsp_led_toggle();
        vTaskDelay(pdMS_TO_TICKS(ALARM_HALF_MS));
    }
}

void blinker_step(ao_t *ao, const ao_evt_t *e)
{
    blinker_t *me = (blinker_t *)ao;
    if (e->sig == AO_SIG_INIT) {
        blinker_period(me, HALF_MS(me->mode));
        return;
    }
    if (e->sig == EV_TICK) {
        bsp_led_toggle();
        return;
    }
    switch (me->st) {
    case BL_NORMAL:
        if (e->sig == EV_PRESS) {
            me->mode ^= 1u;
            trc('B', me->mode);
            blinker_period(me, HALF_MS(me->mode));
        }
        if (e->sig == EV_MEAS && e->par > LIMIT_ON) {
            me->st = BL_ALARM;
            ao_tmr_disarm(&me->tm);
            trc('M', (int32_t)ALARM_HALF_MS);
            flash4();
        }
        break;
    case BL_ALARM:
        if (e->sig == EV_PRESS) {
            me->mode ^= 1u;
            trc('B', me->mode);
        }
        if (e->sig == EV_MEAS && e->par > LIMIT_ON)
            flash4();                                   /* тревога держится — ещё серия */
        if (e->sig == EV_MEAS && e->par < LIMIT_OFF) {
            me->st = BL_NORMAL;
            blinker_period(me, HALF_MS(me->mode));
        }
        break;
    }
}

meter_long.c — нарушение: долгий счёт внутри шага.

/* meter_long.c — нарушение правила: тот же измеритель с долгим счётом внутри шага (опыт 32а.5). */
#include "objs.h"

static uint32_t crunch(uint32_t n)                      /* долгий счёт без единого ожидания */
{
    volatile uint32_t acc = 0;
    while (n--)
        acc += n;
    return acc;
}

void meter_step(ao_t *ao, const ao_evt_t *e)
{
    meter_t *me = (meter_t *)ao;
    if (e->sig == AO_SIG_INIT) {
        ao_tmr_arm(&me->tm, SAMPLE_MS, true);
        return;
    }
    if (e->sig == EV_TICK) {
        me->sum += bsp_adc_read();
        if (++me->n == SAMPLES) {
            crunch(LONG_WORK);                          /* «тяжёлый фильтр» прямо в шаге */
            ao_post(me->client, EV_MEAS, me->sum / SAMPLES);
            me->sum = 0;
            me->n = 0;
        }
    }
}

ptr_ao.c — нарушение: указатель на стек отправителя в событии.

/* ptr_ao.c — нарушение правила: в событии уезжает адрес данных из стека отправителя (опыт 32а.5).
 * Получатель выше отправителя по приоритету — успевает прочитать, пока данные живы; ниже — читает то,
 * что легло на их место. Приоритет получателя задаёт -DRX_PRIO. */
#include "ao.h"
#include "bsp.h"
#include "rep.h"

enum { EV_TICK = AO_SIG_USER, EV_REPORT };

static ao_t sender, receiver;
static ao_tmr_t tm;

void EXTI_IRQHandler(void) { }

static __attribute__((noinline)) void send_report(int32_t value)
{
    volatile int32_t msg[4] = { value, value + 1, value + 2, value + 3 };   /* живёт в стеке отправителя */
    ao_post(&receiver, EV_REPORT, (int32_t)(intptr_t)msg);                  /* ОШИБКА: в событии — адрес стека */
}

static __attribute__((noinline)) int32_t other_work(void)
{
    volatile int32_t scratch[8];
    for (int i = 0; i < 8; i++)
        scratch[i] = -1;                                /* то же место стека занято другими данными */
    return scratch[0];
}

static void sender_step(ao_t *me, const ao_evt_t *e)
{
    (void)me;
    if (e->sig == AO_SIG_INIT)
        ao_tmr_arm(&tm, 100, true);
    if (e->sig == EV_TICK) {
        send_report(215);
        other_work();
    }
}

static void receiver_step(ao_t *me, const ao_evt_t *e)
{
    (void)me;
    if (e->sig == EV_REPORT) {
        const volatile int32_t *msg = (const volatile int32_t *)(intptr_t)e->par;
        trc('R', msg[0]);                               /* ждём 215 */
    }
}

void app_start(void)
{
    ao_tmr_init(&tm, &sender, EV_TICK);
    ao_start(&sender, "sender", sender_step, 2, 4, 128);
    ao_start(&receiver, "recv", receiver_step, RX_PRIO, 4, 128);
}

void app_report(void)
{
    bsp_puts("ptr rx_prio="); bsp_putu(RX_PRIO); bsp_puts(" tx_prio=2\n");
}

Дальше: раздел 33 «Конфигурирование и энергосбережение» — FreeRTOSConfig.h строка за строкой и режим без тика, в котором прибор спит между событиями.

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