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

33. Конфигурирование и энергосбережение

FreeRTOSConfig.h упоминался в каждом разделе части IV, и пора разобрать его целиком. Ядро знает около семи десятков макросов с приставкой config и полтора десятка с INCLUDE_; сборку останавливает отсутствие лишь шести из них, остальные имеют умолчания — и не всегда те, что нужны. Вторая половина раздела — про режим, в котором прибор не просыпается тысячу раз в секунду ради тика, а спит до ближайшего дела.

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

Оригинал Перевод
configuration file файл конфигурации
tick rate частота тика
tickless idle режим без тика при простое
suppress ticks and sleep подавить тики и заснуть
expected idle time ожидаемое время простоя
pre / post sleep processing действия до / после сна
wait for interrupt (WFI) ждать прерывания
idle hook / tick hook ловушка простоя / ловушка тика
run time stats статистика загрузки
trace facility средства трассировки

33.1 Базовые параметры

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

#define configCPU_CLOCK_HZ      72000000u
#define configTICK_RATE_HZ      1000u
#define configMAX_PRIORITIES    5
#define configMINIMAL_STACK_SIZE 128

⚠️ configCPU_CLOCK_HZ — честная частота ядра, как в RCC. Адаптация делит её на частоту тика и получает перезагрузку SysTick: 72 000 тактов при 72 МГц и 1 кГц. Укажете 36 МГц при реальных 72 — тик станет полмиллисекунды, а vTaskDelay(1000) — полусекундой. Ни одна проверка этого не поймает: ядро верит конфигурации. Симптом узнаваемый — «все задержки в приборе не те, но пропорционально».

Честная частота ядра в конфигурации: 72 МГц и тик 1 кГц дают перезагрузку SysTick 72 000 тактов и vTaskDelay(1000) ровно в секунду; 36 МГц в конфигурации при реальных 72 — перезагрузка 36 000, тик полмиллисекунды и та же задержка в полсекунды; нижний предел частоты тика от 24-разрядного SysTick — 4,29 Гц при 72 МГц

Частота тика. 1000 Гц — разумное умолчание: разрешение задержек миллисекунда, накладные расходы — около 0,2 % процессора на 72 МГц (23.4). Понижают до 100 Гц ради экономии в приборах без точных задержек; повышать выше 1000 смысла почти нет — растут расходы, а разрешение лучше миллисекунды делают аппаратными таймерами. Нижний предел задан разрядностью SysTick: при 72 МГц медленнее четырёх герц тик не бывает.

Число приоритетов. Каждый уровень — отдельный список готовых задач в памяти ядра. Пяти хватает большинству приборов; больше — только когда есть чем их заполнить.

Минимальный стек — в словах, и это не «рекомендуемый», а нижняя граница для задачи простоя. Свои задачи получают столько, сколько измерено (раздел 26).

Приоритеты прерываний и проверка. Без этой группы адаптация Cortex‑M не работает:

#define configPRIO_BITS                               4      /* у F1 и F4 */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY       15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY  5
#define configKERNEL_INTERRUPT_PRIORITY      (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
#define configASSERT(x) if ((x) == 0) { taskDISABLE_INTERRUPTS(); for (;;); }

Ядро читает только две сдвинутые строки: 0xF0 — приоритет своих PendSV и SysTick, 0x50 — порог BASEPRI (раздел 28). «Библиотечные» значения — для NVIC_SetPriority, которая сдвигает сама. Порог нулём быть не может: запуск планировщика остановится на configASSERT. Забытый configKERNEL_INTERRUPT_PRIORITY адаптация заменит на 255 — на STM32 это тот же низший уровень 0xF0. configASSERT — самая полезная строка конфигурации: без неё ядро не проверяет ни приоритеты, ни вызовы из обработчиков (раздел 24.4).


33.2 Что включено

Вторая группа — возможности ядра. Каждая включённая стоит кода, а некоторые — ещё и памяти.

Макрос Что включает Умолчание
configUSE_PREEMPTION вытеснение обязателен
configUSE_TIME_SLICING чередование равных по тику 1
configUSE_MUTEXES мьютексы 0
configUSE_RECURSIVE_MUTEXES рекурсивные мьютексы 0
configUSE_COUNTING_SEMAPHORES считающие семафоры 0
configUSE_TIMERS программные таймеры и демон 0
configUSE_TASK_NOTIFICATIONS уведомления задач 1
configUSE_QUEUE_SETS наборы очередей 0
configUSE_TICKLESS_IDLE режим без тика 0
configSUPPORT_STATIC_ALLOCATION статическое создание 0
configSUPPORT_DYNAMIC_ALLOCATION динамическое создание 1

«Обязателен» означает: не задан — сборка остановится с понятной ошибкой из FreeRTOS.h («Missing definition»). Таких строк шесть: кроме configUSE_PREEMPTION, это две ловушки ниже (configUSE_IDLE_HOOK и configUSE_TICK_HOOK), configUSE_16_BIT_TICKS (33.4) и два параметра из 33.1 — configMAX_PRIORITIES и configMINIMAL_STACK_SIZE.

Опаснее всего умолчание 0 у мьютексов и таймеров: забыли строку — функции молча выброшены. Вызов xSemaphoreCreateMutex не соберётся («undeclared function»), а xTimerCreate соберётся и упадёт только при компоновке.

Ловушки

#define configUSE_IDLE_HOOK   0     /* vApplicationIdleHook на каждом проходе простоя */
#define configUSE_TICK_HOOK   0     /* vApplicationTickHook на каждом тике */
#define configUSE_MALLOC_FAILED_HOOK    1
#define configCHECK_FOR_STACK_OVERFLOW  2

Две последние включают всегда — они ловят немые ошибки (разделы 25, 26). Две первые — по надобности. Ловушка простоя вызывается из задачи простоя и не имеет права блокироваться; ловушка тика — из обработчика SysTick и допускает только FromISR.

Функции по одной

#define INCLUDE_vTaskDelay                   1
#define INCLUDE_uxTaskGetStackHighWaterMark  1
#define INCLUDE_xTaskGetSchedulerState       1

Макросы INCLUDE_ не настраивают поведение — они добавляют или убирают код. Забытый даёт ошибку компоновки на функции, которая есть в исходниках. Некоторые тянут друг друга: установке флагов группы событий из прерывания нужен INCLUDE_xTimerPendFunctionCall — xEventGroupSetBitsFromISR работает через демона таймеров, — и без него ошибка компоновки будет на функции, которую вы не вызывали.


33.3 Память и статистика

#define configTOTAL_HEAP_SIZE           (10 * 1024)
#define configUSE_TRACE_FACILITY        1
#define configUSE_STATS_FORMATTING_FUNCTIONS  0
#define configGENERATE_RUN_TIME_STATS   0

Куча — раздел 25 целиком; здесь напомним, что 10 КБ из этой конфигурации — половина памяти F103C8, а своего умолчания у ядра нет.

Трассировка добавляет в структуры ядра поля для отладчика: номера задач, состояние очередей. Стоит несколько байт на объект, даёт видимость в отладчике (раздел 35) — на этапе разработки включают.

Форматирование статистики — функции, которые печатают список задач в текст. Тянут за собой код, похожий на printf, и стек под него. Включают ради отладки, в изделии выключают.

Статистика загрузки требует источник времени точнее тика — обычно аппаратный таймер на 10–100 кГц — и двух макросов для его настройки и чтения. Показывает, какая задача сколько процентов процессора съела; единственный честный ответ на вопрос «почему прибор тормозит».


33.4 Расширенное

#define configUSE_CO_ROUTINES               0
#define configUSE_APPLICATION_TASK_TAG      0
#define configUSE_16_BIT_TICKS              0
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1

Сопрограммы — устаревший механизм для очень малых процессоров; на Cortex‑M не нужны, croutine.c можно не компилировать.

Метки задач — произвольное значение, привязанное к задаче; редко нужно.

16-разрядные тики — нет, никогда на Cortex‑M (раздел 24).

Имя из ядра 11. Там этот макрос заменён на configTICK_TYPE_WIDTH_IN_BITS: в новой конфигурации пишут #define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS, а старое имя остаётся только в конфигурациях, доставшихся от ядра 10 (раздел 24).

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


33.5 Режим без тика

Обычный режим: SysTick будит процессор тысячу раз в секунду, даже когда все задачи спят и делать нечего. Каждое пробуждение — выход из сна, обработчик, проверка списков, возврат в сон. Для прибора на батарейке это главный потребитель.

Режим без тика делает иначе. Когда задача простоя видит, что ближайшее дело — через N тиков:

  1. останавливает SysTick;
  2. перепрограммирует его на весь ожидаемый простой;
  3. выполняет WFI — процессор засыпает;
  4. по пробуждению считает, сколько прошло, и корректирует счётчик тиков так, будто тики шли.

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

#define configUSE_TICKLESS_IDLE                 1
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP   2

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

Предел: 233 миллисекунды

SysTick — 24-разрядный, в нём помещается 16 777 215 тактов. При 72 МГц и тике 1 кГц один тик — 72 000 тактов; значит, за один сон можно подавить не больше 233 тиков.

Ядро Один тик, тактов Предел сна
72 МГц 72 000 233 мс
8 МГц 8 000 2,1 с

Задача, спящая секунду, разбудит процессор пять раз — по пределу SysTick, а не по своему сроку. Каждое пробуждение — те же расходы, только реже.

Секунда простоя: тысяча пробуждений с тиком, пять без тика на 72 МГц и одно на 8 МГц во сне

Два пути дальше:

Держать ядро на меньшей частоте всё время. Предел 2,1 с из таблицы — у прибора, ядро которого и работает на 8 МГц (configCPU_CLOCK_HZ равно 8 000 000). Понижать частоту только на время сна нельзя: функция сна рассчитывает перезагрузку SysTick по тактам 72 МГц и запускает его до вызова макросов, а SysTick тактируется от ядра. Переключились в макросе на 8 МГц — каждый подавленный тик длится не 1 мс, а 9: сон на 233 тика занимает 2,1 с, а счётчик тиков после пробуждения прибавит всё те же 233. Время ядра во сне идёт в 9 раз медленнее, и vTaskDelay(1000) в простаивающем приборе растянется почти до 9 с. Если частоту на время сна менять всё-таки нужно — только в своей функции сна, которая пересчитывает перезагрузку под новую частоту.

Сами макросы функция сна вызывает до и после WFI:

#define configPRE_SLEEP_PROCESSING(x)   periph_off()   /* выключить лишнее */
#define configPOST_SLEEP_PROCESSING(x)  periph_on()    /* вернуть как было */

Аргумент x — ожидаемое время простоя; записав в него ноль, можно отменить сон ядра и сделать свой.

Как ядро обходится с этим аргументом. Функция сна в адаптации Cortex‑M копирует ожидаемое время простоя, отдаёт копию макросу и выполняет WFI, только если копия осталась больше нуля. Вот та же логика отдельно, с двумя вариантами макроса:

/* передать время простоя как есть — ядро заснёт само */
#define PRE_SLEEP_PASS(x)  PreSleepProcessing(x)

/* обнулить аргумент до вызова — ядро не выполнит WFI, функция получит ноль */
#define PRE_SLEEP_ZERO(x)  do { (x) = 0; PreSleepProcessing(x); } while (0)

/* логика port.c: копия, которую макрос вправе обнулить */
#define PORT_SLEEP(PRE, expected)                       \
    do {                                                \
        idle_t xModifiableIdleTime = (expected);        \
        PRE(xModifiableIdleTime);                       \
        if (xModifiableIdleTime > 0)                    \
            WFI();                                      \
    } while (0)

Ожидаемый простой — 100 тиков:

Макрос PreSleepProcessing получает WFI ядра
PRE_SLEEP_PASS 100 тиков выполняется
PRE_SLEEP_ZERO 0 не выполняется — компилятор выбросил её из кода

⚠️ Аргумент, обнулённый заранее, выключает сон. Вариант PRE_SLEEP_ZERO встречается в готовых конфигурациях. С ним режим без тика включён, а ток прежний: ядро не засыпает, а своя функция получает ноль вместо времени простоя и не знает, на сколько можно уснуть. Передавайте время как есть — или засыпайте в PreSleepProcessing сами и тогда пишите ноль осознанно.

Заменить таймер. Функция сна в адаптации объявлена слабой — её можно переопределить целиком и использовать таймер, который идёт в режимах глубокого сна: RTC или низкочастотный таймер (раздел 10). Тогда предел сна — часы, а не миллисекунды. Это единственный способ попасть в режим Stop: сама адаптация делает только WFI, то есть режим Sleep — ядро стоит, тактирование живо. В Stop SysTick стоит, и без своей функции сна счётчик тиков остался бы без поправки (vTaskStepTick). Standby с задачами не сочетается вовсе: память теряется, и пробуждение из него — запуск прошивки заново (раздел 10.4).

⚠️ Периферия в режиме без тика работает как обычно. Заснувший процессор не мешает АЦП, DMA и UART; прерывание от любого из них разбудит его. Но и ток они потребляют как обычно — режим без тика экономит только на ядре. Настоящая экономия начинается с отключения периферии в PRE_SLEEP_PROCESSING.

Что нужно проверить, включив режим

Программные таймеры — их сроки ядро учитывает при расчёте простоя, но тик демона таймеров подавляется наравне с остальными.

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

Отладчик — многие останавливают трассировку в WFI; на время отладки режим часто выключают.


33.6 Грабли

Все задержки не те, но пропорционально. configCPU_CLOCK_HZ не совпадает с реальной частотой.

Ошибка #error из FreeRTOS.h. Обязательный макрос не задан; сообщение называет его.

Ошибка компоновки на функции, которая есть. Не включён её INCLUDE_.

xSemaphoreCreateMutex «не объявлена», xTimerCreate не находится при компоновке. Не задан configUSE_MUTEXES или configUSE_TIMERS: у них умолчание 0.

Программные таймеры не собираются. Нет одного из трёх configTIMER_*.

xEventGroupSetBitsFromISR не компонуется. Нет INCLUDE_xTimerPendFunctionCall: флаги из прерывания ставит демон таймеров.

Прибор не спит в режиме без тика. Какая-то задача просыпается чаще двух тиков; либо ожидание простоя меньше порога.

Режим без тика включён, а ток не упал. Макрос configPRE_SLEEP_PROCESSING обнуляет аргумент до вызова своей функции: ядро не выполняет WFI, а своего сна в функции нет (33.5).

Спит, но недолго. Предел SysTick — 233 мс при 72 МГц. Нужен другой таймер или ядро на меньшей частоте всё время.

Сон в 9 раз длиннее заказанного, часы ядра отстают. Частоту ядра понизили в configPRE_SLEEP_PROCESSING, а перезагрузка SysTick к этому моменту уже рассчитана на 72 МГц (33.5).

Проснулся — время уплыло. Собственная функция сна не скорректировала счётчик тиков через vTaskStepTick.

printf не работает после включения статистики. Форматирующие функции требуют стек, которого нет у вызывающей задачи.


33.7 Итоги

Дальше: раздел 33а «Сторожевой таймер для нескольких задач: кто завис и как это заметить» — задача-сторож со сроком для каждой задачи, IWDG и WWDG, слепок перед сбросом.

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