Embedded HandbookApps
All sections › IV. FreeRTOS
STM32F103, STM32F407 · level intermediate · 41 min read
Open in the interactive handbook

32а. Active objects: an event-driven device architecture — a task, its own queue and a state machine

Sections 26–32 gave you the parts: tasks, queues, software timers, mutexes, event groups, buffers. The question left is how to assemble a whole device from them. The usual answer is a task for each job, vTaskDelay inside, shared variables under a mutex. It works while there are three tasks. After that come priority inversion (30.5), deadlock (30.6) and tasks that are asleep at the very moment a response is expected from them.

Here the order is different — active objects. Each part of the device is a task with its own event queue and a state machine inside. It waits in one place: on its own queue. Everything else arrives as an event: a button press, a measurement result, an elapsed time. This is the technique of section 23а (state machines and an event queue without an OS), but now there are several state machines, each has its own priority, and the kernel decides whose step matters more. The gatekeeper task from 30.8 is a special case: one object and one kind of event.

The idea is not new: Miro Samek's QP and FreeACT frameworks are built on it (github.com/QuantumLeaps/FreeACT). Their source code is neither used nor examined here: the framework of this section is written from scratch, its file ao.c is 90 lines.

Foundations: the FreeRTOS 10.5.1 kernel (queue.c, timers.c, heap_4.c, the ARM_CM3 port), sections 27 (queues), 28 (interrupts), 29 (software timers), 23а (state machines). All the code is built with arm-none-eabi-gcc 12.2 with the -Os option and run in QEMU on a Cortex-M3 model with the same kernel port as the STM32F103. Time in the model is counted by the number of executed instructions, so a run repeats tick for tick. The tick is 1 ms. All the numbers in this section come from that run.

Terms used in the documentation

Term Meaning
active object, actor a task with its own queue and state machine
event, signal an event and its signal: what happened
event queue, mailbox the object's event queue
post put an event into a queue
dispatch hand an event to the state machine
run to completion (RTC) an event is processed completely, then the next one
time event an event generated by time
one-shot / periodic fires once / fires every period
publish — subscribe a publisher and a subscriber
shared-nothing no shared data
queue overflow the queue has no room for an event
stack high-water mark the deepest stack usage reached

32а.1 The idea: a task, its own queue, a state machine

An active object is three things together: a task, a queue that belongs only to it, and a state machine that receives events from the queue one at a time. There are five rules.

  1. One place of waiting. The object waits only on its own queue. No event — the task sleeps and does not use the processor.
  2. Run to completion. Having received an event, the state machine does the work and returns. Inside a step there is no vTaskDelay, and no waiting on a queue, semaphore or mutex.
  3. No shared data. Only the object's own task touches its fields. If another object needs a result, it is sent an event, and the parameter travels by value.
  4. Time is an event too. "Wait 20 ms" means arm a timer and return. When the time is up, an event arrives in the same queue.
  5. An interrupt only posts an event. This is the deferred processing from 28.4, turned into a rule.

The example for the whole section is a device made of three objects. The Button suppresses bounce and reports a press. The Meter polls a sensor every 25 ms and every 100 ms delivers the average. The Indicator blinks an LED: a press changes the half-period (500 or 250 ms), a reading above the threshold turns on the alarm — fast blinking with a half-period of 50 ms.

A device of three active objects: the button interrupt posts the EV_EDGE event into the queue of the "Button" object, its state machine suppresses bounce and sends EV_PRESS into the queue of the "Indicator" object; the "Meter" object sends EV_MEAS there too; the timer daemon posts the time events EV_TIMEOUT and EV_TICK into the same queues; each object has its own task, its own queue for four events and a state machine

What this gives you. Mutexes are not needed at all: there is nothing to protect, so there is nothing to cause priority inversion or deadlock. A reaction does not wait until someone wakes up: once an event is in the queue, the task is ready. Each object can be read on its own: everything that can happen to it is a list of events and states.

What you pay for it: a queue for each object, the timer daemon task (29.3) and logic written as a state machine rather than as lines with delays between them. What this costs in bytes and ticks is counted in 32а.4.


32а.2 The framework: event, object, dispatcher, time

The whole framework is two files, ao.h and ao.c. First the types:

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;

An event takes 8 bytes: a signal and one parameter. A queue copies the event in full (27.1), so after it is sent it no longer depends on the sender. An object is 20 bytes: a queue, a task, a pointer to the state machine step and three counters for monitoring. A time event is 12 bytes: a kernel timer, a recipient and a signal.

Start and dispatcher. All objects share one task function. It calls the step with the signal AO_SIG_INIT, and then in a loop takes an event and hands it to the state machine:

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

Along the way the loop keeps two counters: peak — how many events were waiting in the queue at the worst moment, longest — how many ticks the longest step lasted. Both will be useful in 32а.6.

Posting. An event can be posted from a task and from an interrupt. The wait for space is always zero: the sender is itself inside its own step and has no right to wait. If it does not fit, we count a loss:

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

The lost counter is incremented in a critical section: an object has several senders, an interrupt among them. In the Cortex-M port the FROM_ISR macros work both in a task and in an interrupt. The -DAO_STRICT option turns the first loss into a halt on configASSERT.

Time. A time event is a kernel software timer (section 29) whose callback does one thing: it posts an event to the recipient. The callback runs in the daemon task and must not block (29.4) — ao_post has a zero wait.

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 both sets the period and starts the timer; if the timer was already running, the count starts over — which is exactly what debouncing needs. vTimerSetReloadMode selects whether the event comes once or every period.

The framework takes 470 bytes of code with -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 and 8 bytes of strings.


32а.3 A device of three objects

Device parameters (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) /* полупериод в обычном режиме: кнопка переключает */

Signals and objects (objs.h). The framework structure is the first field of the object, so the pointer to ao_t that the step receives is cast to the object type:

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 (button.c). A state machine with two states. Each edge re-arms a one-shot event for 20 ms; when EV_TIMEOUT arrives, the input has been silent for 20 ms and the level can be read.

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;
    }
}
State EV_EDGE EV_TIMEOUT
settled arm 20 ms → bouncing —
bouncing re-arm 20 ms read the input; pressed and was not before — EV_PRESS to the subscriber → settled

Compare with 27.7: there the button task woke up every 10 ms and polled the input. Here the object does no work at all between presses.

Indicator (blinker.c). Blinking is the periodic event EV_TICK. To change the frequency, arm the same event with a different period.

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

A press during an alarm is only remembered: the new half-period takes effect after the all-clear. The trc function is a mark in the test bench log: time, letter, value.

Meter (meter.c). No states, one counter. The result goes to the subscriber as the EV_MEAS event, with the value in the parameter.

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

Assembling the device (app_ao.c). The objects are static variables. The links between them are addresses: whom to send to. The interrupt handler consists of a single post.

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

The state machines (how to write them as a table and with pointers — 23а.2 and 125.7) know nothing about the hardware: they see bsp_button_level, bsp_adc_read, bsp_led_toggle. This is the technique from 196.2, which is why the same code runs on the test bench without a board.

The run. The test bench applies four presses with bounce: five edges within 6 ms on press and three on release 150 ms later. The edges arrive as a real interrupt. From ms 4215 to ms 6215 the sensor reads 900 with a threshold of 800.

What happened Indicator reaction Delay, ticks
press at ms 1000 half-period 250 ms at ms 1026 26
press at ms 2130 half-period 500 ms at ms 2156 26
press at ms 3370 half-period 250 ms at ms 3396 26
sensor above threshold at ms 4215 alarm, 50 ms, at ms 4300 85
press at ms 5040 (during alarm) mode remembered at ms 5066 26
sensor back to normal at ms 6215 all-clear, 500 ms, at ms 6300 85

26 ticks are 6 ms of bounce and 20 ms of silence after the last edge. 85 ticks are the time to the end of the averaging window: four readings of 25 ms are collected by ms 4300. The objects added no delay of their own: in each queue no more than one event was waiting, there were no losses, the longest step was shorter than a tick. The LED toggled 54 times.


32а.4 The same device the old way, and a comparison in numbers

The same device in blocking style (app_blk.c): three tasks, each sleeps in vTaskDelay. The interrupt wakes the button task with a binary semaphore. The mode and the reading live in a shared structure under a mutex.

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));                /* о нажатии и тревоге узнаем только после сна */
    }
}

The code is shorter and reads from top to bottom. Both variants were run on the same scenario, with the same priorities (button 4, indicator 3, meter 2) and stacks of 128 words each.

Active objects Blocking
mutexes / semaphores 0 / 0 1 / 1
queues / software timers 3 / 3 0 / 0
button stack, words used of 128 56 34
indicator stack 58 30
meter stack 52 30
timer daemon stack 32 no daemon
heap: device objects, bytes 2536 2024
heap: the kernel's own tasks, bytes 1216 616
heap total, bytes 3752 2640
test bench firmware code, bytes 9412 7956
from press to indicator reaction, ticks 26, 26, 26, 26 500, 120, 380, 60
from sensor threshold to alarm, ticks 85 285
from sensor return to all-clear, ticks 85 135

The stack is measured with uxTaskGetStackHighWaterMark (26.7) at the end of the run, the heap with xPortGetFreeHeapSize before and after the objects are created. What the heap is made of is shown by a separate measurement — one object of each kind:

Kernel object Heap bytes
task with a 128-word stack 608 (616 when mutexes are enabled in the kernel)
queue for 4 events of 8 bytes 120
software timer 56
daemon command queue — created with the first timer 184
binary semaphore 88
mutex 88

Active objects: 3 × 608 + 3 × 120 + 3 × 56 + 184 = 2536 bytes, plus two more kernel tasks, idle and the daemon, 2 × 608 = 1216. The blocking variant: 3 × 616 + 88 + 88 = 2024 bytes and one idle task, 616. The device objects themselves take another 120 bytes of static memory (40 + 36 + 44) against 16 for the blocking variant (the shared structure and two handles). Why a heap request costs more than requested — see 25.3.

In memory, active objects are more expensive: by 1112 bytes of heap, by 1456 bytes of code (the framework and the kernel's software timers) and by 22–28 words of stack in each task. The stack is deeper because the state machine step itself calls kernel functions — posting to a queue, a command to a timer. With 128 words, less than half is used.

In reaction time — the opposite. With active objects the time from press to reaction is always 26 ticks. In the blocking variant the button task changes a shared variable, but the indicator task will read it only when it wakes up: in the run after 60, 120, 380 and 500 ticks — up to a whole blink half-period. The alarm was 200 ticks late for the same reason. This can be fixed: let the indicator wait not in vTaskDelay but on a queue with a timeout, as in 27.7. But that is already a step towards the event-driven scheme.

And the last line of the comparison, which is not in the table: in the blocking variant you have to remember the mutex in every place where the shared data is touched. In the run there are no errors because there are three tasks and one mutex.


32а.5 What happens when the rules are broken

Three experiments on the same test bench. In each, one file is changed.

Waiting inside a step. In blinker_bad.c the alarm flash series is done "the simple way" — a loop with a delay:

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

It is called right from the step, on every reading above the threshold:

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

Readings arrive every 100 ms, the step lasts 200 ms. The queue for four events fills up, and after that events are lost.

By the rules With waiting in the step
longest indicator step, ticks 0 200
highest queue fill (of 4) 1 4
events lost 0 11
press at ms 5040 processed at ms 5066 lost
all-clear after the sensor returns at ms 6215 at ms 6300 at ms 7100

The first lost event turned out to be a press — at ms 5066. A person pressed the button, the button worked correctly, and the device did not respond. The all-clear was 800 ticks late: the indicator was still reading four old readings of 200 ms each.

A long computation inside a step. In meter_long.c there is a "heavy filter" before the result is sent — a loop of 400000 iterations, which is 65 ticks in the model:

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

There is no waiting here, rule 2 is formally kept. What happens to the others depends on priority:

Meter lowest (priority 2) Meter highest (priority 5)
longest meter step, ticks 65 65
from press to reaction, ticks 26, 26, 26, 26 85, 55, 26, 45
button queue: fill / losses 1 / 0 4 / 3
alarm after the sensor threshold, ticks 150 150

A long step delays everyone below it in priority. A meter below everyone only hinders itself: the alarm is 65 ticks late, and up to two events pile up in its queue. A meter above everyone stalls the button: bounce edges pile up in its queue, five edges do not fit into four places, and three edges are lost over the run.

A pointer to local data in an event. In ptr_ao.c the sender puts into the event parameter the address of an array on its own stack:

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 */
    }
}
Recipient priority Read nine times out of nine
3 — above the sender (2) 215
1 — below the sender 5276

A recipient with higher priority preempts the sender right inside ao_post and reads the array while it is still alive. A lower recipient waits until the sender finishes its step — and reads whatever was placed at that spot on the stack later. The code is the same; one number in the configuration differs. Such a bug passes all checks and wakes up when someone changes the priorities.

The rule: an event carries a value. If there is a lot of data, pass a pointer to a block from a pool or to a static buffer that the recipient owns after sending (27.5).


32а.6 Queue overflow: what to do and how to choose the length

An object's queue is finite, and the sender does not wait for space. So an event may not fit. This is not a framework error but a signal about the design: the object cannot keep up or is being starved. There are four remedies.

Count. The framework keeps lost, peak and longest for each object. They are worth printing to the log or reporting on command — like the stack high-water mark.

Halt in debugging. A build with -DAO_STRICT on the very first loss gives the line assert ao.c:41 t=5066 in the run — file, line and tick. In a product a halt is not acceptable; the counter stays there.

Choose the length by measurement. The length is not guessed but measured with peak in the worst-case scenario:

Run Queue Length peak Losses
by the rules any of the three 4 1 0
long computation, meter highest button 4 4 3
same button 8 5 0
waiting in the step indicator 4 4 11
same indicator 8 8 10

peak below the length — the queue is big enough, and the difference is the margin. peak equal to the length — the measurement hit the ceiling, the real need is unknown: increase the length and measure again. For the button it came out as 5 of 8 — that is how many edges are in one burst of bounce. A slot costs 8 bytes: going from 4 to 8 events in three queues added 96 bytes to the heap (2632 instead of 2536).

Do not cure a blocking step with length. The last row of the table: the queue is twice as long, and the losses are 10 instead of 11. The press at ms 5040 is now not lost, but it is processed at ms 5900 — 860 ticks later; the all-clear came at ms 7900, 1685 ticks after the sensor returned. If events arrive faster than the object processes them, any queue will overflow (27.6), and a long one will also turn losses into delays.

Assign priorities. Higher — objects with short steps that receive events in bursts (the button, communication reception). Lowest of all — the object with long work. In the experiment of 32а.5 this is the difference between zero and three losses with the very same code.


32а.7 Pitfalls

Waiting inside a step. vTaskDelay, or waiting on a queue or semaphore with a non-zero time in an event handler. In the experiment — 11 lost events, a button press among them. Write the waiting as a state of the state machine and a time event.

A longer queue instead of a fix. A queue of 8 instead of 4 with a blocking step: losses 10 instead of 11, reaction to a press — 860 ticks.

Long work in a high-priority object. A step of 65 ticks in the meter with priority 5: the button lost three edges, the reaction to a press grew from 26 to 85 ticks. Give long work to the lowest-priority object or cut it into pieces, between which the object sends an event to itself.

The address of a local variable in an event. It works while the recipient is higher in priority (215), and breaks when it is lower (5276). An event carries a value.

peak equals the queue length. This does not mean "the queue was just enough", but "how much is needed is unknown". Increase and measure again.

Posting with a wait for space. Replacing the zero wait in ao_post with portMAX_DELAY seems a way to lose nothing. But the step starts to block, and two objects that send events to each other will both stall when their queues are full — the same deadlock as in 30.6, only without mutexes.

A time event that is already in the queue. Stopping or restarting a timer does not recall an event that the daemon has already posted. The state machine may receive EV_TIMEOUT from the previous arming. If this matters, put the arming number into the event parameter and skip foreign numbers.

A direct call instead of an event. A function of one object called from the step of another runs in someone else's task and touches someone else's fields — the shared data is back, and with it the race from 30.1.

The timer daemon below the objects. Time events are posted by the daemon task; if its priority is lower than that of a busy object, time is late (29.3). On the test bench the daemon is the highest of all.

An interrupt above the kernel threshold. ao_post_from_isr calls a kernel function with FromISR — the interrupt priority must be no higher than configMAX_SYSCALL_INTERRUPT_PRIORITY (28.5).

Stack by habit. Active object tasks need more stack than blocking ones: in the run 52–58 words against 30–34. Measure, do not carry numbers over from an old project.


32а.8 Summary

Full sources

ao.h — the active object framework: types and calls.

/* 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 — the framework: task, queue, time events.

/* 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 — device parameters.

/* 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 — signals and the three device objects.

/* 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 — the "button" object.

/* 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 — the "indicator" object.

/* 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 — the "meter" object.

/* 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 — assembling the device from objects.

/* 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 — the same device in blocking style.

/* 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 — a violation: waiting inside a step.

/* 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 — a violation: a long computation inside a step.

/* 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 — a violation: a pointer to the sender's stack in an event.

/* 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");
}

Next: section 33 "Configuration and power saving" — FreeRTOSConfig.h line by line and the tickless mode, in which the device sleeps between events.

Open in the interactive handbook