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)— полусекундой. Ни одна проверка этого не поймает: ядро верит конфигурации. Симптом узнаваемый — «все задержки в приборе не те, но пропорционально».
Частота тика. 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 тиков:
- останавливает
SysTick; - перепрограммирует его на весь ожидаемый простой;
- выполняет
WFI— процессор засыпает; - по пробуждению считает, сколько прошло, и корректирует счётчик тиков так, будто тики шли.
Прибор спит до ближайшего срока или до внешнего прерывания — что раньше. Задачи разницы не замечают: их задержки истекают вовремя.
#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, а не по своему сроку. Каждое пробуждение — те же расходы,
только реже.
Два пути дальше:
Держать ядро на меньшей частоте всё время. Предел 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 Итоги
- Ядро знает около семидесяти макросов
configи полутора десятковINCLUDE_; обязательные (их шесть) останавливают сборку понятной ошибкой, а у мьютексов и таймеров умолчание 0 — забытая строка молча выключает их. configCPU_CLOCK_HZобязан совпадать с реальной частотой: иначе все задержки масштабируются молча.- Приоритеты для ядра — две сдвинутые строки:
0xF0дляPendSVиSysTick, порогBASEPRI0x50; нулевой порог останавливает запуск наconfigASSERT, а самconfigASSERTвключают всегда. - Тик 1 кГц — разумное умолчание; нижний предел при 72 МГц — четыре герца.
INCLUDE_добавляют и убирают код; забытый даёт ошибку компоновки.- Ловушка простоя не блокируется, ловушка тика — только
FromISR. - Режим без тика останавливает
SysTick, перепрограммирует его на весь простой и засыпает поWFI; задачи разницы не видят. - Предел одного сна на
SysTickпри 72 МГц — 233 мс; дольше — либо держать ядро на меньшей частоте всё время, либо переопределить функцию сна на другой таймер. Менять частоту вPRE_SLEEP_PROCESSINGнельзя: перезагрузкаSysTickк этому моменту уже рассчитана на 72 МГц. - Адаптация делает только
WFI— режим Sleep. Stop — только в своей функции сна на таймере, который идёт в Stop (LPTIM или RTC), с поправкой счётчика черезvTaskStepTick. Standby — выключение с потерей памяти: пробуждение из него — запуск прошивки заново, а не продолжение задач. - Периферия в режиме без тика живёт и потребляет как обычно.
configPRE_SLEEP_PROCESSINGполучает копию времени простоя, и ноль в ней отменяетWFIядра. Макрос, пишущий ноль до вызова своей функции, выключает сон, если та не засыпает сама.
Дальше: раздел 33а «Сторожевой таймер для нескольких задач: кто завис и как это заметить» — задача-сторож со сроком для каждой задачи, IWDG и WWDG, слепок перед сбросом.