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 Идея: задача, своя очередь, автомат
Активный объект — три вещи вместе: задача, очередь, принадлежащая только ей, и автомат, который получает из очереди по одному событию. Правил пять.
- Одно место ожидания. Объект ждёт только на своей очереди. Нет события — задача спит и процессор не занимает.
- Шаг до конца. Получив событие, автомат делает работу и возвращается.
Внутри шага нет ни
vTaskDelay, ни ожидания очереди, семафора или мьютекса. - Общих данных нет. Поля объекта трогает только его задача. Другому объекту нужен результат — ему шлют событие, параметр едет значением.
- Время — тоже событие. «Подождать 20 мс» значит взвести таймер и вернуться. Когда время выйдет, в ту же очередь придёт событие.
- Прерывание только кладёт событие. Это отложенная обработка из 28.4, доведённая до правила.
Пример на весь раздел — прибор из трёх объектов. Кнопка давит дребезг и сообщает о нажатии. Измеритель опрашивает датчик раз в 25 мс и раз в 100 мс отдаёт среднее. Индикатор мигает светодиодом: нажатие меняет полупериод (500 или 250 мс), показание выше порога включает тревогу — частое мигание с полупериодом 50 мс.
Что это даёт. Мьютексы не нужны совсем: защищать нечего, значит, нечему дать инверсию приоритетов или взаимную блокировку. Реакция не ждёт, пока кто-то проснётся: событие попало в очередь — задача готова. Каждый объект можно читать отдельно: всё, что с ним бывает, — это перечень событий и состояний.
Чем за это платят: очередь на каждый объект, задача-демон таймеров (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 Итоги
- Активный объект — задача, своя очередь и автомат. Ждёт он только на очереди, шаг доводит до конца, данными не делится, время получает событием, прерывание только кладёт событие.
- Каркас — файл в 90 строк и 470 байт кода: событие 8 байт, объект 20 байт, событие по времени 12 байт.
- Прибор из трёх объектов обошёлся без мьютексов и семафоров и отвечает на нажатие за 26 тиков при любом режиме мигания; блокирующий вариант — за 60…500 тиков.
- Плата — память: 3752 байта кучи против 2640, 9412 байт кода против 7956, 52–58 слов стека на задачу против 30–34.
- Ожидание в шаге — 11 потерянных событий и отбой на 800 тиков позже; долгий шаг объекта с высоким приоритетом — три потери у кнопки; адрес из стека в событии — 5276 вместо 215.
- Длину очереди выбирают по измеренному
peak: 1 из 4 в обычном прогоне, 5 из 8 в худшем. Место в очереди стоит 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 строка за строкой и режим без тика, в котором
прибор спит между событиями.