Питание и ограничения: режимы сна, бюджет энергии, память и флеш
На «большой» машине ресурсы — это то, что кончается медленно и о чём сообщает мониторинг. Память кончилась — OOM killer, диск кончился — алерт, процессор загружен — добавили под. Все три ресурса эластичны: их можно докупить, и почти всегда дешевле докупить, чем оптимизировать. Во встраиваемой системе ресурсы не эластичны и заданы на этапе, когда паяльник уже остыл. 64 КБ ОЗУ — это навсегда 64 КБ. 2200 мА·ч в батарейном отсеке — это навсегда 2200 мА·ч, и если прошивка тратит на 30 % больше расчётного, устройство не «немного замедлится», а умрёт на восемь месяцев раньше — у клиента, в поле, в количестве десяти тысяч штук. Флеш выдержит 10 000 стираний страницы, и если вы сохраняете настройки раз в 15 минут прямой перезаписью, устройство гарантированно ломается через три с половиной месяца — одинаково у всех, в один и тот же день эксплуатации. Это статья про три бюджета: энергии, оперативной памяти и ресурса флеша. Все три считаются заранее, на бумаге, и все три проверяются измерением, а не логами. Аппаратная база берётся из статьи про железо для программиста, работа с памятью на уровне языка — из C для встраиваемых систем, а механика прерываний и таймеров, без которой не бывает пробуждений, — из прерываний и реального времени.
Смена системы координат: энергия — это ресурс, а не свойство
Первый сдвиг мышления: энергия тратится не «процессором», а работой конкретных блоков в конкретные промежутки времени. Профилировать надо не функции, а фазы. Классический бэкендерский рефлекс «оптимизируем горячий цикл» здесь чаще всего бесполезен: в типичном датчике 77 % заряда уходит на 185 миллисекунд работы радиопередатчика, и любое ускорение вычислений экономит доли процента.
Второй сдвиг: среднее важнее пикового, но пиковое убивает. Срок жизни определяется средним током, а работоспособность — пиковым: CR2032 с внутренним сопротивлением 15 Ом при импульсе 30 мА просядет на 0,45 В, микроконтроллер уйдёт в brownout-reset посреди передачи, и устройство будет бесконечно перезагружаться при «полной» батарейке. Третий сдвиг: то, что вы измеряете, вы меняете. Подключённый отладчик держит живым домен отладки и превращает 2 мкА в 2 мА, а один printf на 40 байт по UART 115200 — это 3,5 мс, в течение которых устройство не спит. Отладка энергопотребления печатью невозможна принципиально, отсюда отдельный раздел про осциллограф и логический анализатор.
Немного физики, без которой цифры не сходятся
Ёмкость батареи указывают в миллиампер-часах — это заряд, а не энергия. Заряд удобен, пока напряжение примерно постоянно; при сравнении химий переходите к энергии: $E = C \cdot V_{\text{ср}}$, мВт·ч. Средний ток за цикл длиной $T$ из фаз с током $I_i$ и длительностью $t_i$, и срок жизни при полезной ёмкости $C_{\text{пол}}$ (это не паспортная ёмкость: вычтите саморазряд, потери на холоде и остаток ниже порога отключения):
$$ I_{\text{ср}} = \frac{1}{T}\sum_{i} I_i \cdot t_i, \qquad T_{\text{жизни}} = \frac{C_{\text{пол}}}{I_{\text{ср}}} $$
Считать удобно в миллиампер-секундах на цикл — величина маленькая, наглядная и линейно складывается: 1 мА·с = 1/3600 мА·ч. Динамическая мощность цифровой логики:
$$ P_{\text{дин}} = \alpha \cdot C_{\text{экв}} \cdot V^2 \cdot f, \qquad P_{\text{полн}} = P_{\text{дин}} + P_{\text{утечки}} $$
Отсюда два следствия, которые определяют всю стратегию. Первое: квадратичная зависимость от напряжения — снижение питания ядра с 3,3 до 1,8 В даёт трёхкратную экономию, поэтому в STM32L4 есть Range 2 с пониженным напряжением ядра и потолком 26 МГц. Второе: динамическая энергия на одну инструкцию от частоты почти не зависит (частота вдвое выше — время вдвое меньше), поэтому «посчитать быстро и уснуть» обычно выгоднее, чем «считать медленно» — при условии, что утечки малы, а вход в сон и выход из него дёшевы.
Откуда берётся энергия: характер источников
| Источник | Ёмкость / напряжение | Пиковый ток | Саморазряд | Где применяют и чем подводит |
|---|---|---|---|---|
| CR2032 (литий-марганец) | 220 мА·ч, 3,0 В | 3–15 мА, внутреннее сопротивление 10–40 Ом и растёт со временем | ~1 %/год | Брелоки, BLE-метки. Импульс радио берут из буферного конденсатора 10–100 мкФ, иначе просадка и сброс |
| 2×AA щелочные | 2000–2900 мА·ч, 3,0 В, сильно падающая кривая | сотни мА | 2–3 %/год | Массовая розница. Ёмкость резко падает на морозе и на большом токе (эффект Пейкерта) |
| Li-SOCl₂ (ER14505) | 2400 мА·ч, 3,6 В, ровная полка | десятки мА, нужен гибридный конденсатор | 1 %/10 лет | Счётчики, LoRaWAN-датчики на 10 лет. Пассивация: после долгого хранения первый импульс проседает |
| Li-Ion / LiPo, LiFePO₄ | 2000–3500 мА·ч, 4,2→3,0 В (у LiFePO₄ 3,2 В) | единицы ампер | 2–5 %/мес | Носимое, роботы. Нужен контроллер заряда, защита и запрет заряда на морозе; LiFePO₄ безопаснее и живёт 2000+ циклов |
| Суперконденсатор | единицы Ф, 2,7 В | огромный | утечка 1–20 мкА | Резерв на запись во флеш при обрыве питания. Утечка сравнима с током сна всего узла |
| Сбор энергии (PV, TEG, RF) | 10–100 мкВт/см² в помещении | зависит от буфера | — | Датчики без обслуживания. Проблема холодного старта: DC/DC вроде BQ25570 стартует от 330 мВ |
Практическое правило: паспортная ёмкость — это верхняя граница в лабораторных условиях. Закладывайте 80 % для щелочных, 85 % для литиевых, и отдельно вычитайте саморазряд. Для очень экономного узла саморазряд становится главной нагрузкой: 2,5 %/год от 2500 мА·ч — это 7,1 мкА эквивалентного постоянного тока, больше, чем ест сам микроконтроллер во сне.
Бюджет энергии: считаем раньше, чем паяем
Возьмём реальную задачу: датчик температуры и влажности, STM32L4 + BME280 + LoRa-модуль SX1276, питание 3,0 В от двух AA (полезно 2200 мА·ч), передача раз в 10 минут.
Считать это глазами по осциллографу неудобно — считайте скриптом ещё до заказа платы; таблица фаз и есть техническое задание на прошивку.
"""Бюджет энергии узла: фазы -> заряд -> средний ток -> срок жизни.
Сложность O(n log n) из-за сортировки вывода, память O(n)."""
PERIOD, CAPACITY_MAH = 600.0, 2200.0
# имя фазы, секунды, ток в амперах (с датчиками и регулятором)
node = [("сон Stop 2", PERIOD - 0.333, 2.1e-6), # МК + RTC + утечки платы
("старт и тактовая", 3e-3, 6.0e-3), # разгон MSI, инициализация
("измерение BME280", 40e-3, 350e-6), # МК спит, датчик считает сам
("расчёт и упаковка", 5e-3, 8.0e-3),
("передача LoRa SF9", 185e-3, 45.0e-3), # +14 дБм, 20 байт полезных
("окно приёма", 100e-3, 12.0e-3)]
charges = [(name, s * a * 1000.0) for name, s, a in node] # заряд фазы в мА·с
total = sum(q for _, q in charges)
for name, q in sorted(charges, key=lambda x: -x[1]):
print(f" {name:<22} {q:8.3f} мА·с {100 * q / total:5.1f} %")
avg_ma = total / PERIOD # средний ток за цикл
hours = CAPACITY_MAH / avg_ma
print(f" ИТОГО {total:.3f} мА·с за {PERIOD:.0f} с")
print(f" средний ток {avg_ma * 1000:.1f} мкА -> {hours:.0f} ч = {hours / 8760:.2f} года")
передача LoRa SF9 8.325 мА·с 76.7 % | сон Stop 2 1.258 мА·с 11.6 %
окно приёма 1.200 мА·с 11.1 % | расчёт 0.040 мА·с 0.4 %
старт и тактовая 0.018 мА·с 0.2 % | измерение 0.014 мА·с 0.1 %
ИТОГО 10.855 мА·с за 600 с; средний ток 18.1 мкА -> 121547 ч = 13.87 года
Что читается из этой раскладки — и почему её надо строить до написания кода:
- Оптимизировать вычисления бессмысленно. Уберите расчёт целиком — выиграете 0,4 %. Уменьшите время в эфире (меньший SF, короче пакет, реже отправка) — выиграете десятки процентов. Расход определяется протоколом, а не алгоритмом.
- Окно приёма стоит почти как сон за все десять минут. Отсюда классы устройств LoRaWAN: класс A слушает только два коротких окна после своей передачи именно потому, что «просто слушать» дорого. Подробности радиочастей — в статье про IoT-связь.
- 13,9 года — это фикция. Щелочные AA столько не живут: саморазряд съест их за 7–8 лет. Расчёт говорит другое — «энергопотребление перестало быть ограничением, дальше упираемся в химию». А вот сдвиг периода до 1 минуты даёт 162 мкА и 1,55 года: десятикратное учащение сократило жизнь в девять раз.
- Плохой линейный регулятор с собственным током 20 мкА вместо 2: средний ток 36 мкА, срок 7 лет вместо 14. Одна деталь за 20 центов уполовинила срок службы устройства.
Тот же скрипт превращается в регрессионный тест: сохраняйте измеренный профиль в CI и падайте, если средний ток вырос больше чем на 10 %. Идея та же, что в измерении производительности, только метрика — заряд.
Режимы сна: что именно выключается
Слово «сон» скрывает лестницу компромиссов. Чем глубже режим, тем меньше ток, но тем дольше пробуждение и тем меньше состояния переживает сон.
80 МГц, 8-10 мА Sleep: Sleep — стоит только ядро
периферия и DMA живут, 1-2 мА Stop: Stop 2 — генераторы стоят
ОЗУ и регистры сохранены, 1,1 мкА Standby: Standby — питание ядра снято
жив резервный домен, 120 нА Shutdown: Shutdown — снят и LSE-домен
30 нА, живёт вывод WKUP Run --> Sleep: WFI при пустом цикле Sleep --> Run: любое прерывание, ~0 тактов Run --> Stop: WFI при SLEEPDEEP + LPMS=Stop2 Stop --> Run: EXTI, RTC, LPUART, LPTIM
задержка 5-10 мкс Run --> Standby: WFI при LPMS=Standby Standby --> Reset: пробуждение = СБРОС
задержка ~260 мкс Run --> Shutdown: LPMS=Shutdown Shutdown --> Reset: только WKUP-вывод или RTC Reset --> Run: старт с main(), контекст утерян
Cortex-M даёт только два примитива. Инструкция WFI (wait for interrupt) останавливает ядро до прерывания, WFE — до события. Глубину задаёт бит SLEEPDEEP в SCB->SCR плюс регистры блока PWR конкретного вендора. То есть «сон» — это всегда WFI, а различие между 1 мА и 30 нА живёт вне ядра и у каждого производителя своё.
static volatile uint8_t work_pending; /* выставляется в ISR, см. статью о прерываниях */
void idle_wait(void) /* не «пустой цикл», а остановка ядра */
{
__disable_irq(); /* закрываем окно гонки: проверка и WFI атомарны */
if (!work_pending) { __DSB(); __WFI(); } /* WFI будится даже при PRIMASK=1 */
__enable_irq(); /* обработчик выполнится здесь */
}
Гонка, которую закрывает __disable_irq(), обязательна к пониманию: приди прерывание между проверкой work_pending и WFI, ядро уснуло бы с уже готовой работой. Приём работает потому, что на Cortex-M WFI пробуждается даже при закрытом PRIMASK.
STM32: лестница из пяти ступеней
Для семейств L0/L4/U5 (цифры — типичные, для L476 при 3,0 В и 25 °C):
| Режим | Ток | Что живо | Пробуждение | Когда брать |
|---|---|---|---|---|
| Run 80 МГц | 8–10 мА | всё | — | Считаем; чем короче, тем лучше |
| Low-power run 2 МГц | ~130 мкА | всё, но медленно | — | Долгие опросы без спешки |
| Sleep | 1–2 мА | периферия, DMA, прерывания | мгновенно | Ждём завершения DMA-передачи |
| Stop 2 | 1,1 мкА (+0,3 с RTC) | ОЗУ, регистры, LPUART, LPTIM, I2C-адрес | 5–10 мкс | Рабочая лошадка: сон между измерениями |
| Standby | 120 нА | резервный домен, RTC, 32 байта регистров | ~260 мкс, через сброс | Сон часами, контекст не нужен |
| Shutdown | 30 нА | только вывод WKUP | сброс | Хранение, транспортировка |
Ключевой практический вывод: Stop 2 почти всегда правильный выбор. Он на порядок дороже Standby по току, но сохраняет ОЗУ и просыпается в 30 раз быстрее — а разница между 1,1 мкА и 0,12 мкА теряется на фоне утечек платы и саморазряда батареи. Переход в Standby оправдан, только когда вы измерили плату и она реально стоит меньше микроампера.
/* Сон до пробуждения по RTC. Порядок важен, каждый пункт стоил кому-то дня отладки. */
void enter_stop2_for(uint32_t seconds)
{
gpio_park_unused_pins(); /* 1. неиспользуемые выводы — в аналоговый режим:
плавающий вход со звоном на пороге жжёт десятки мкА */
bme280_sleep(); /* 2. внешние микросхемы усыпляются ИХ командами: сами они */
sx1276_sleep(); /* не спят, забытое радио тихо ест 1,5 мА круглосуточно */
HAL_DBGMCU_DisableDBGStopMode(); /* 3. в релизе: домен отладки держит генераторы (~1 мА) */
rtc_set_wakeup(seconds); /* LSE 32768 Гц, независим от основного такта */
HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
SystemClock_Config(); /* проснулись следующей строкой: ОЗУ и регистры целы,
но тактовая система сброшена на MSI — восстановить */
}
ESP32: другая философия и другая цена
ESP32 проектировался вокруг Wi-Fi, и его лестница подчинена именно этому. Modem-sleep — радио спит между beacon-интервалами, ядра живы: 20–30 мА, годится для устройства от блока питания. Light sleep — ядра стоят, ОЗУ сохранено, соединение можно удержать: 0,8–1,5 мА, пробуждение за сотни микросекунд. Deep sleep — жив только RTC-домен и его медленная память: 10 мкА у ESP32, ~5 мкА у ESP32-C3, но пробуждение — это сброс, app_main() начинается заново, и пережить сон могут лишь переменные с атрибутом RTC_DATA_ATTR (esp_sleep_enable_timer_wakeup(600ull * 1000000ull); esp_deep_sleep_start(); — возврата из последнего вызова нет). Hibernation — отключён и медленный RTC-осциллятор: единицы микроампер, пробуждение по внешнему выводу.
Ловушка ESP32 не в токе сна, а в стоимости пробуждения. Установка Wi-Fi-соединения с DHCP после deep sleep занимает 1,5–3 с при токе 100–150 мА — это 200–450 мА·с, в 20–40 раз дороже всего цикла нашего LoRa-узла. Смягчается статическим IP, сохранением канала и BSSID в RTC-памяти (подключение сокращается до 200–400 мс) и переходом на BLE или ESP-NOW вместо TCP/IP. Вывод: ESP32 на батарейке — не для частой отправки, и уж точно не для монетки. Отдельно стоит ULP-сопроцессор: крошечное ядро в RTC-домене опрашивает АЦП или GPIO при токе ~150 мкА и будит основные ядра только по условию — правильный способ сделать «датчик, реагирующий на порог». Для сравнения, nRF52840 — эталон BLE от монетки: System OFF 0,4 мкА, System ON idle 1,5 мкА, приём 4,6 мА, передача 4,8 мА при 0 дБм, ядро 64 МГц около 3,3 мА с включённым DC/DC. Разница с Wi-Fi принципиальна: рекламный пакет BLE — единицы миллиампер на сотни микросекунд против сотни миллиампер на секунды.
Периферия работает, ядро спит
Главный приём экономии: пусть работу делает периферия, а ядро при этом стоит. Это прямое продолжение темы GPIO, АЦП, ШИМ и таймеров, только с точки зрения энергии.
ядро НЕ участвует ADC-->>CPU: прерывание «передача завершена» CPU->>CPU: усреднить, откалибровать, упаковать 20 байт CPU->>RF: TX +14 дБм, затем окно приёма 100 мс Note over RF: 185 мс в эфире, 45 мА — 77 % заряда цикла RF-->>CPU: TX done, окно пусто или команда с сервера CPU->>RF: sleep (иначе 1,5 мА круглосуточно) CPU->>RTC: завести следующее пробуждение и вернуться в Stop 2 deactivate CPU
Шаги 5–7 — это и есть приём: 64 отсчёта АЦП без единой инструкции ядра, примерно шестикратная экономия заряда против опроса в цикле. Механизмы того же рода: LPUART на STM32 будит ядро по совпадению адреса или стартовому биту, оставаясь на 32 кГц, — устройство «слушает шину» ценой микроампер; сторожевое окно АЦП (AWD) даёт прерывание, только когда напряжение вышло за границы; тактовое стробирование — включайте блок в RCC перед использованием и выключайте после, забытый тактируемый USB стоит сотен микроампер.
Tickless idle: как RTOS не мешает спать
RTOS по умолчанию будит систему тиком SysTick 1000 раз в секунду. Каждое пробуждение — вход, выход и обработчик, суммарно единицы микросекунд при токе в миллиамперы; средний ток получается порядка сотен микроампер, даже если задачи ничего не делают. Решение — tickless idle: перед сном планировщик смотрит, когда истекает ближайший таймаут, программирует низкопотребляющий таймер (LPTIM на STM32, RTC на nRF) на этот интервал, уходит в глубокий сон, а после пробуждения компенсирует пропущенные тики.
/* FreeRTOS вызывает это вместо своего сна при configUSE_TICKLESS_IDLE = 2.
xExpectedIdleTime — сколько тиков система гарантированно ничем не занята. */
void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime)
{
if (xExpectedIdleTime < 2) { return; } /* не стоит возни */
uint32_t ticks = xExpectedIdleTime - 1;
if (ticks > LPTIM_MAX_TICKS) { ticks = LPTIM_MAX_TICKS; } /* потолок 16-битного счётчика */
if (eTaskConfirmSleepModeStatus() == eAbortSleep) { return; } /* перепроверка под критической секцией */
lptim_start_oneshot(ticks);
enter_stop2(); /* ядро стоит, LPTIM считает на 32 кГц */
vTaskStepTick(lptim_stop_and_read()); /* могли проснуться раньше — доводим системное время */
}
Три вещи ломают tickless. Периодические задачи с мелким периодом: одна задача с vTaskDelay(pdMS_TO_TICKS(10)) не даёт спать дольше 10 мс никогда. Программные таймеры, заведённые «на всякий случай». Дрейф времени: LPTIM на 32 кГц не делится нацело на 1 кГц, накопленная ошибка — секунды в сутки; если нужны точные метки, синхронизируйте по RTC, а не по счётчику тиков. Механику планировщика и таймаутов смотрите в статье про RTOS, а фон по планировщикам вообще — в процессах и планировании.
Плата ест 3 мА вместо 2 мкА: дерево диагностики
Это самая частая ситуация в жизни: код правильный, режим правильный, амперметр показывает в тысячу раз больше расчёта. Разбирайте по порядку — почти всегда виновник в первых трёх ветках.
по SWD/JTAG?"} B -->|да| B1["Отключите зонд и питайте плату отдельно.
Домен отладки держит генераторы:
DBGMCU_CR, биты DBG_STOP/DBG_STANDBY"] B -->|нет| C{"Ток порядка миллиампер
и не зависит от прошивки?"} C -->|да| C1["Виновата плата, не код:
Iq регулятора, делитель измерения батареи,
подтяжки USB, светодиоды, датчик в непрерывном режиме"] C -->|нет| D{"Все выводы
сконфигурированы?"} D -->|нет| D1["Неиспользуемые — в аналоговый режим.
Плавающий вход со звоном на пороге
жжёт десятки мкА на вывод"] D -->|да| E{"Внешние микросхемы
усыплены командой?"} E -->|нет| E1["Радио, датчики, память,
ускорители — у каждого свой режим сна.
Снятие такта их не выключает"] E -->|да| F{"Тактирование блоков
отключено в RCC?"} F -->|нет| F1["Отключите неиспользуемые:
USB, Ethernet, таймеры, опорный источник"] F -->|да| G{"Ток зависит
от напряжения на шине?"} G -->|да| G1["Паразитное питание через выводы:
притянутая линия I2C питает уснувшую
микросхему через защитные диоды"] G -->|нет| H["Меряйте по секциям:
режьте питание сегментов,
снимайте перемычки, ставьте шунты"]
Последний пункт недооценивают: уснувший датчик, у которого линии I2C подтянуты к 3,3 В, получает питание через защитные диоды входов и «полуработает», потребляя сотни микроампер. Лечение — снимать подтяжки перед сном или гасить питание сегмента ключом на полевом транзисторе.
Отладка энергии без единого printf
Печать здесь не просто бесполезна — она искажает измеряемую величину. Это тот же класс проблем, что и в разделе про отладку и производство, но с собственным инструментарием. Задача тяжела динамическим диапазоном: от 2 мкА во сне до 150 мА в передаче — пять десятичных порядков. Обычный мультиметр врёт дважды: медленное усреднение плюс заметное падение напряжения на шунте, из-за которого устройство уходит в brownout на всплеске.
| Инструмент | Диапазон и полоса | Для чего |
|---|---|---|
| Шунт 1 Ом + осциллограф | полоса мегагерцы | Форма всплеска передачи, просадки, дребезг, длительность фаз |
| Nordic Power Profiler Kit II | 200 нА – 1 А, 100 кГц | Дешёвый и правильный первый прибор: логарифмическая шкала, счёт заряда |
| Joulescope, Otii Arc | нА – А, автопереключение, синхронный лог GPIO | Профили в CI, приёмка бюджета, автоматические отчёты |
| Мультиметр в режиме мкА | усреднение по секунде | Только стационарный ток сна, с байпасом на время всплесков |
Осциллограф покажет форму, но заряд надо интегрировать: PPK2 и Joulescope делают это сами, с осциллографа же снимайте курсорами длительности фаз и подставляйте в скрипт бюджета.
GPIO-маркеры и логический анализатор
Самая полезная техника превращает график тока в понятный текст: заведите отладочный вывод (или четыре, тогда это код фазы) и переключайте его на границах фаз. Логический анализатор пишет маркеры, прибор пишет ток, и вы видите, какому куску кода принадлежит каждый миллиампер.
/* Маркер фазы: одна запись в BSRR, без чтения-модификации-записи. Стоимость — такт
и один вывод; в релизной сборке PROFILE_POWER=0 и макрос схлопывается в ((void)0). */
#define PHASE(n) do { GPIOB->BSRR = phase_bits[(n)]; } while (0)
void measure_cycle(void)
{
PHASE(PH_WAKE); clocks_up();
PHASE(PH_SENSE); bme280_read_forced();
PHASE(PH_CALC); pack_payload();
PHASE(PH_TX); lora_send(payload, sizeof payload);
PHASE(PH_RX); lora_listen(100);
PHASE(PH_SLEEP); enter_stop2_for(600);
}
С открытыми инструментами это sigrok/PulseView и любой клон анализатора на 8 каналов за 10 USD — для маркеров скорости хватает с запасом; у Otii и Joulescope есть синхронный ввод цифровых каналов, и тогда таблица «фаза → заряд» строится автоматически. Ещё два способа получить данные наружу, не мешая измерению. SEGGER RTT: сообщение кладётся в кольцевой буфер в ОЗУ, отладчик вычитывает его через SWD в фоне, стоимость около микросекунды — на порядок дешевле UART; но RTT требует подключённого зонда, а значит живого домена отладки, и для финального измерения тока не годится. DWT->CYCCNT — 32-битный счётчик тактов в блоке отладки Cortex-M: uint32_t t0 = DWT->CYCCNT; do_work(); stats.cycles = DWT->CYCCNT - t0; даёт точное время фазы вообще без вывода наружу (переполнение при 80 МГц — через 53 секунды).
И финальное правило, которое стоит принять как аксиому: при подключённом SWD-зонде измерения энергопотребления недействительны. На STM32 биты DBG_SLEEP, DBG_STOP, DBG_STANDBY в DBGMCU_CR заставляют держать тактирование живым, чтобы отладчик не терял связь, — вместо 1,1 мкА вы увидите 1–3 мА. На ESP32 подключённый JTAG даёт свои миллиамперы. Отсюда рабочий процесс: отлаживаете логику с зондом, измеряете энергию — на автономном питании, с прошивкой без DEBUG_BUILD, снимая данные только маркерами и прибором. Джентльменский набор для этой работы — осциллограф, логический анализатор и SWD-зонд, и ни один из них не заменяет два других.
Быстро и спать или медленно и не спать
Спор «race to idle против медленной работы» решается арифметикой. Пусть задача — 2 миллиона тактов. При 80 МГц: 25 мс при 9 мА = 0,225 мА·с. При 16 МГц: 125 мс при 2,2 мА = 0,275 мА·с. Быстрый режим выиграл, потому что ток растёт медленнее частоты — часть потребления постоянна (опорные источники, регулятор, аналоговые блоки) и оплачивается временем.
Но правило переворачивается в двух случаях. Первый: задача ждёт, а не считает. Если 90 % времени уходит на ожидание датчика по I2C, «быстро посчитать» нечего — ускорение частоты просто дороже оплачивает то же ожидание; правильный ответ — понизить частоту или уснуть в Sleep, пока идёт обмен по DMA. Второй: смена режима стоит дорого. Разгон PLL — сотни микросекунд при полном токе; если работы на 50 мкс, на разгон уйдёт больше, чем на работу, поэтому для коротких задач держат MSI/HSI на умеренной частоте и не трогают PLL вообще. Ориентир один: считайте энергию на задачу, а не мощность — метрика мА·с на цикл обработки снимается с прибора теми же маркерами.
Выбор платы: за что вы платите током
- ATmega328P и Arduino. Сам чип в power-down ест 0,1 мкА (со сторожевым таймером — 4–6 мкА) и вполне годится для батарейных поделок. Но плата Arduino Uno ест 45 мА постоянно — линейный регулятор, микросхема USB, два светодиода. Разница между чипом и платой стократная, и это общее правило: отладочная плата никогда не показывает энергетику конечного изделия.
- STM32L0/L4/U5. Лучший выбор, когда нужен срок жизни в годах: микроамперный сон, богатая низкопотребляющая периферия (LPUART, LPTIM, автономный АЦП), пробуждение за микросекунды. L0 — когда задача тривиальна и важна цена; L4 — когда нужны вычисления и DSP; U5 — новее и ещё экономнее.
- nRF52/nRF54 против ESP32 (-C3, -S3). Есть BLE — начинайте с Nordic: энергетика радио лучшая в классе, есть онлайн-калькулятор профиля для оценки до покупки. Нужен Wi-Fi, сертифицированные дешёвые модули и гигантская экосистема — ESP32, но только при наличии розетки или крупного аккумулятора и редких отправок; для монетки он не подходит.
- RP2040/RP2350. Уникальный PIO, два ядра, отличная документация и цена. Энергетика Pico слабая: dormant на плате — сотни микроампер и выше, правит регулятор. Для батарейного датчика придётся делать свою плату.
- Raspberry Pi и вообще Linux-одноплатники. Полноценного сна нет: 0,5–3 Вт даже в простое. Каноническая связка для робота — Pi как «мозг» плюс микроконтроллер как «спинной мозг» и распорядитель питания: он держит реальное время, следит за батареей и включает Pi по событию.
Второй бюджет: ОЗУ и флеш
64 КБ ОЗУ и 256 КБ флеша — типичный середняк. Это не «мало», это фиксировано: переполнение проявляется не отказом аллокации, а испорченными соседними данными и HardFault в случайном месте через два часа работы. Устройство памяти на уровне языка разобрано в C для встраиваемых систем, здесь — про учёт и контроль.
# text — код и константы во флеш; data — инициализированные переменные (И флеш, И ОЗУ);
# bss — нули (только ОЗУ). Правда о том, кто что притащил, живёт в firmware.map.
arm-none-eabi-size -A build/firmware.elf
arm-none-eabi-nm --print-size --size-sort --radix=d build/firmware.elf | tail -30
arm-none-eabi-gcc ... -Wl,--print-memory-usage # постоянный контроль прямо в сборке
# FLASH: 98304 B / 256 KB = 37.50 % RAM: 41216 B / 64 KB = 62.89 %
Наглядное дерево «файл → функция → размер → кто вызывает» строит puncover поверх той же карты. Две типовые ошибки, дающие внезапный расход ОЗУ: забытый const — таблица static uint32_t sine_table[1024] уезжает в .data и занимает 4 КБ флеша под образ и 4 КБ ОЗУ навсегда, тогда как static const кладёт её в .rodata, только во флеш; и крупный буфер на стеке функции, вызванной из ISR, — стек прерываний короткий, массив на 2 КБ просто затрёт то, что под ним, статический буфер хотя бы виден в size. Третий приём — раскладывать данные по блокам ОЗУ вручную: у STM32F4 есть быстрая CCM-память, недоступная DMA, у L4 — SRAM2 с сохранением в Standby; размещение задаётся как uint8_t buf[256] __attribute__((section(".sram2"), aligned(4))); плюс правка скрипта линковки.
Стек: единственный ресурс, который не проверяет компилятор
Переполнение стека не диагностируется само — оно проявляется порчей чужих данных. Классический приём: до входа в main, пока стек пуст, залить его узором (for (uint32_t *p = &_sstack; p < (uint32_t *)__get_MSP() - 16; ++p) *p = 0xA5A5A5A5u;), а позже линейным поиском найти первое изменённое слово — расстояние до него и есть «никогда не использованный» запас, high water mark. Стоимость O(n) по неиспользованной части, память O(1). В FreeRTOS то же делает uxTaskGetStackHighWaterMark(), а configCHECK_FOR_STACK_OVERFLOW = 2 добавляет проверку канарейки при переключении контекста. Статическая оценка — флаг -fstack-usage: компилятор кладёт рядом с каждым .o файл .su с расходом стека каждой функции, и сложение по графу вызовов даёт верхнюю границу; метод не работает при рекурсии и вызовах через указатели — оба приёма во встраиваемом коде и без того нежелательны. Не забудьте про вложенные прерывания: каждый уровень добавляет кадр исключения (32 байта, 104 с плавающей точкой) плюс локальные обработчика.
Кучи нет, зато есть флаги сборки
malloc во встраиваемой прошивке — почти всегда ошибка. Не потому, что медленно, а потому, что фрагментация не ограничена сверху: система, отработавшая три недели, отказывается выделить 200 байт при 8 КБ свободных, и предсказать этот момент нельзя. Правила отрасли (MISRA C:2012, правило 21.3) прямо запрещают динамическое выделение. Замены: статические буферы, пулы блоков фиксированного размера, кольцевые буферы, статические API RTOS (xTaskCreateStatic, xQueueCreateStatic). Если куча всё же нужна — выделяйте всё на старте и никогда не освобождайте: это переименованное статическое выделение, зато совместимое с чужими библиотеками. Общий фон по аллокаторам — в управлении памятью и памяти как ресурсе производительности.
Сами флаги сборки дают десятки процентов размера почти бесплатно:
-Os # оптимизация по размеру; -O2 обычно на 10-20 % крупнее
-ffunction-sections -fdata-sections -Wl,--gc-sections # выкинуть неиспользуемое
-flto # межмодульная оптимизация: ещё 5-15 % размера
--specs=nano.specs # newlib-nano: printf/scanf в разы компактнее
-u _printf_float # добавлять ТОЛЬКО если реально нужен %f (это +6-8 КБ)
Полный printf из newlib — это 10–14 КБ флеша, вызовы malloc и до 1 КБ стека на вызов. В прошивке на 64 КБ это пятая часть образа ради отладочной печати, которой в продакшне не будет. Практика: собственный минимальный форматтер на 300 байт либо бинарное журналирование, где во флеш пишутся идентификатор строки и аргументы, а текст подставляется на хосте.
Третий бюджет: ресурс флеша и запись, переживающая обрыв питания
Три свойства NOR-флеша определяют всю архитектуру хранения. Стирание переводит биты в 1, запись только гасит их в 0 — «изменить значение на месте» нельзя, можно лишь дописать новое рядом или стереть целую страницу. Стирание идёт страницами (1–2 КБ у L4, до 128 КБ у F4) и занимает 20–100 мс при токе 10–20 мА: за одно стирание уходит примерно столько же заряда, сколько за час сна. Ресурс — около 10 000 циклов стирания на страницу, хранение данных 20 лет при 55 °C и заметно меньше при высокой температуре.
Плюс два подводных камня. У STM32L4 и новее запись идёт двойными словами по 64 бита с ECC, и повторная запись в уже записанное двойное слово вызывает ошибку ECC при последующем чтении — «дописать пару бит поверх» физически нельзя. И главное: во время стирания или записи флеш-контроллер останавливает шину, выборка команд из того же банка встаёт, а задержка обработки прерываний вырастает на десятки миллисекунд. Для системы реального времени это катастрофа — критичный код на время записи выносят в ОЗУ или используют двухбанковые чипы.
Журнал вместо перезаписи
Прямая перезапись страницы при каждом сохранении даёт 10 000 сохранений — 3,5 месяца при одном сохранении в 15 минут. Приём, снимающий проблему: дописывать записи фиксированного размера, а страницу стирать только когда она заполнилась, циклически используя несколько страниц.
typedef struct __attribute__((packed, aligned(8))) {
uint32_t seq; /* монотонный номер; 0xFFFFFFFF = слот пуст */
uint8_t payload[10]; /* сами настройки */
uint16_t crc16; /* пишется ПОСЛЕДНИМ двойным словом */
} cfg_record_t; /* ровно 16 байт: кратно 8, требование ECC L4 */
#define SLOTS (4 * FLASH_PAGE_SIZE / sizeof(cfg_record_t)) /* 4 стр. × 128 = 512 */
/* Поиск актуальной записи при старте: линейный проход, O(N) по слотам (N = 512,
около миллисекунды), память O(1). Сравнение (int32_t)(a - b) > 0 вместо a > b
корректно переживает переполнение счётчика — приём из номеров последовательности TCP. */
const cfg_record_t *cfg_find_latest(void)
{
const cfg_record_t *best = NULL;
uint32_t best_seq = 0;
for (size_t i = 0; i < SLOTS; ++i) {
const cfg_record_t *r = &journal[i];
if (r->seq == 0xFFFFFFFFu) { continue; } /* слот пуст */
if (crc16(r, sizeof *r - 2) != r->crc16) { continue; } /* запись оборвана */
if (!best || (int32_t)(r->seq - best_seq) > 0) { best = r; best_seq = r->seq; }
}
return best; /* NULL = заводские значения */
}
Сохранение зеркально: найти первый пустой слот, собрать запись с seq = latest + 1, посчитать CRC и записать двойными словами так, чтобы CRC ушло последним; когда слоты кончились — стереть самую старую страницу (22 мс, 15 мА, шина стоит) и продолжить. Ресурс вырастает в 128 раз: 4 страницы × 128 записей × 10 000 = 5,1 миллиона сохранений. Писать это самому не обязательно: littlefs (устойчивая к обрыву питания ФС с износовыравниванием), NVS в ESP-IDF и X-CUBE-EEPROM у ST реализуют ту же идею журнала.
Что делать при пропадании питания
Правило одно: никогда не бывает состояния «наполовину сохранено». Достигается порядком записи — сначала данные, последним признак валидности (CRC или магическое число). Пропало питание раньше — при старте признак не сойдётся, запись отбросится, останется предыдущая. Это то же журналирование, что в базах и файловых системах, только в 16 байтах. Плюс три меры на уровне схемы. Детектор пониженного напряжения (BOR) настраивается на уровень, при котором флеш ещё гарантированно программируется (обычно 2,4–2,7 В): ниже — сброс, и запись не начинается вообще. Запас энергии — конденсатор 100–470 мкФ или суперконденсатор на шине питания: время считается как $t = C \cdot \Delta V / I$, для 470 мкФ, запаса 0,6 В и тока 15 мА выходит 19 мс, как раз одно стирание страницы. Сигнал заранее — компаратор на входном напряжении даёт прерывание «питание падает» на миллисекунды раньше BOR, и обработчик успевает завершить транзакцию.
Наконец, схема A/B-слотов на картинке — фундамент безопасного обновления по воздуху: новый образ пишется в неактивный слот, проверяется по CRC и подписи, и только потом загрузчик переключает указатель. Подробности процедуры — в статьях про IoT-связь и отладку и производство.
Питание платы: где теряются проценты
Прошивка может быть идеальной, а плата — съедать всё преимущество.
- LDO против импульсного преобразователя. Линейный регулятор гасит разницу напряжений в тепло: КПД равен $V_{\text{вых}}/V_{\text{вх}}$, то есть 3,3 из 4,2 В — это 79 %, а 3,3 из 12 В — 27 %. Импульсный держит 85–95 % под нагрузкой, но сам ест 15–50 мкА и на нагрузке 2 мкА проигрывает LDO с током покоя 1 мкА в разы. Для батарейных узлов берут LDO со сверхнизким Iq или преобразователь с режимом пропуска импульсов (PFM). Ток покоя — параметр номер один при таком выборе, важнее КПД; значения 1–2 мкА реальны и стоят копейки, но их надо специально искать в даташите.
- Ключи питания сегментов и буферные конденсаторы. Датчик, который ест 300 мкА даже в своём «спящем» режиме, дешевле обесточить полевым транзистором — только не забудьте про время установления и про подтяжки, питающие обесточенную микросхему через защитные диоды входов. Между батареей и радиомодулем ставится буферный конденсатор: он отдаёт импульсный ток, которого источник дать не может, и для CR2032 это обязательный элемент схемы.
- Мелочи, которые складываются: делитель для измерения напряжения батареи из резисторов по 10 кОм ест 165 мкА круглосуточно — больше, чем весь узел; берите 1–10 МОм и подключайте делитель ключом на время измерения. Светодиод «питание» — 2–5 мА постоянно. Подтяжки I2C 4,7 кОм на притянутой к земле линии — 700 мкА, пока линия активна.
Робототехника: бюджет, в котором всё решают моторы
В мобильном роботе пропорции переворачиваются: электроника становится шумом на фоне механики. Типичный четырёхколёсный робот на 4S LiPo (14,8 В, 5000 мА·ч = 74 Вт·ч):
| Потребитель | Мощность | Доля |
|---|---|---|
| Два мотора в движении | 25–45 Вт (пик при старте до 90 Вт) | ~75 % |
| Raspberry Pi 4 с камерой | 4–6 Вт | ~12 % |
| Лидар | 2,5 Вт | ~6 % |
| Сервоприводы в удержании; МК, датчики, IMU | 2–8 Вт и 0,3 Вт | ~6 % |
При средних 38 Вт получается около 1,9 часа хода — и это только если робот едет непрерывно. Отсюда следствия, которых нет в мире датчиков. Пусковой ток: мотор при старте берёт ток заторможенного ротора, в 5–8 раз больше номинального, просадка шины сбрасывает Raspberry Pi, и робот перезагружается ровно тогда, когда трогается с места. Лечение — раздельные преобразователи для силовой и логической части, общая земля в одной точке, крупные конденсаторы на силовой шине, плавный разгон ШИМ вместо ступеньки. Рекуперация: тормозящий мотор работает генератором и поднимает напряжение шины, без тормозного резистора это выбивает преобразователь. Телеметрия батареи: датчик тока (INA226, ACS712) плюс измерение напряжения дают и оценку остатка, и диагностику — аномальный ток мотора означает заклинивший привод или наезд на препятствие.
"""Узел ROS 2: кулонометрия по INA226 и решение «пора на базу». O(1) на итерацию."""
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import BatteryState
CAPACITY_AH = 5.0 # 4S LiPo: 16,8 В заряжен, 13,2 В пуст
class BatteryMonitor(Node):
def __init__(self):
super().__init__("battery_monitor")
self.pub = self.create_publisher(BatteryState, "battery_state", 10)
self.charge_ah = CAPACITY_AH # уточняется по напряжению покоя без нагрузки
self.create_timer(1.0, self.tick)
def tick(self):
volts, amps = read_ina226() # драйвер шины I2C
self.charge_ah -= amps / 3600.0 # кулонометрия: шаг ровно 1 с
msg = BatteryState()
msg.voltage, msg.current = float(volts), float(-amps) # разряд — отрицательный ток
msg.charge, msg.capacity = self.charge_ah, CAPACITY_AH
msg.percentage = max(0.0, min(1.0, self.charge_ah / CAPACITY_AH))
msg.power_supply_status = BatteryState.POWER_SUPPLY_STATUS_DISCHARGING
self.pub.publish(msg)
# Порог считается по ТОКУ, а не по проценту: обратный путь пустого робота
# стоит меньше, чем гружёного, и запас надо считать в минутах хода.
minutes_left = (self.charge_ah / max(amps, 0.1)) * 60.0
if minutes_left < self.estimate_return_minutes() * 1.3:
self.request_return_to_dock() # штатное поведение, не авария
Кулонометрия дрейфует (ошибка датчика накапливается), поэтому её периодически корректируют по напряжению покоя — измеренному после нескольких секунд без нагрузки, когда просадка на внутреннем сопротивлении ушла. Управляющая часть — в статье про робототехнику, а формальные модели роботизированных систем и постановки задач их проектирования разобраны в научных работах раздела НИР портала.
Типичные ошибки
- Измерять ток с подключённым отладчиком. Показания завышены в тысячу раз, причина неочевидна, день потерян.
- Считать, что чип и плата — одно и то же. Отладочная плата ест десятки миллиампер регулятором, светодиодами и мостом USB; энергетика меряется только на целевой плате.
- Оптимизировать вычисления вместо радиообмена. Стройте раскладку заряда до того, как что-то оптимизировать.
- Забыть усыпить внешние микросхемы и оставить выводы плавающими. Снятие такта не выключает датчик — у него свой регистр режима; незаданный вход со звоном на пороге стоит десятков микроампер на каждый.
- Задача с
vTaskDelay(10 мс), убивающая tickless. Один опрос «на всякий случай» съедает всю экономию системы. - Прямая перезапись настроек во флеш. Гарантированный отказ на 10 000-й записи, одинаковый у всей партии.
- Запись во флеш без учёта остановки шины. Прерывание, которое должно отработать за 200 мкс, ждёт 22 мс — регулятор разваливается. А делитель для измерения батареи из резисторов по 10 кОм один потребляет больше, чем всё остальное устройство.
mallocв прошивке и полныйprintfради двух строк. Первое отказывает через недели и не воспроизводится на столе, второе стоит 12 КБ флеша и килобайта стека на вызов.- Планировать по паспортной ёмкости батареи. Реальная — на 15–20 % меньше, на холоде вдвое.
Мини-итог
- Энергия считается в миллиампер-секундах на цикл, и раскладка по фазам говорит, что оптимизировать, точнее любой интуиции.
- Сон — это лестница: у STM32 рабочая ступень Stop 2 (микроамперы, сохранённое ОЗУ, пробуждение за микросекунды), у ESP32 главная цена не в токе сна, а в секундах подключения к Wi-Fi.
- Пусть работает периферия, а ядро стоит: таймер плюс DMA плюс Sleep дешевле опроса в цикле в разы. А в RTOS без tickless idle сна нет вообще — 1000 пробуждений в секунду перекрывают любую экономию.
- Энергию нельзя отлаживать печатью. Инструменты — GPIO-маркеры плюс логический анализатор, шунт с осциллографом, специализированный измеритель; и всё это — без подключённого SWD-зонда.
- ОЗУ и флеш — жёсткие бюджеты, контролируемые
size, картой линковки и высоким водяным знаком стека; кучи нет. Флеш стирается страницами, живёт 10 000 циклов и останавливает шину: пишите журналом с CRC последним полем — и обрыв питания перестанет быть проблемой.
Источники
- Elecia White. Making Embedded Systems, 2nd ed., O’Reilly, 2024 — главы про ограниченные ресурсы и энергопотребление; Horowitz P., Hill W. The Art of Electronics, 3rd ed. — источники питания, регуляторы, реальное поведение батарей.
- FreeRTOS: низкопотребляющий tickless-режим — freertos.org/low-power-tickless-rtos.html
- ESP-IDF: режимы сна и их ограничения — docs.espressif.com; хранилище NVS — раздел storage/nvs_flash
- Nordic Semiconductor: Power Profiler Kit II — nordicsemi.com; документация nRF52/nRF54 — docs.nordicsemi.com
- STMicroelectronics: X-CUBE-EEPROM, эмуляция EEPROM во флеш — st.com/en/embedded-software/x-cube-eeprom.html
- littlefs — ФС, устойчивая к обрыву питания: github.com/littlefs-project/littlefs, см.
DESIGN.md - Приборы и софт: joulescope.com, sigrok.org/wiki/PulseView, github.com/HBehrens/puncover
- Memfault Interrupt Blog — статьи про измерение стека и оптимизацию размера прошивки: interrupt.memfault.com
Что дальше
Мы научились экономить заряд и укладываться в память — и выяснили, что львиную долю бюджета съедает радио. Логично разобраться, как именно оно работает и во что обходится каждый протокол: IoT-связь: Wi-Fi, BLE, LoRa, MQTT, безопасность устройств.