Встраиваемые системы: карта трека и чем эта разработка отличается
Что такое встраиваемая система
Определение «программа для маленького компьютера» бесполезно — оно описывает габарит, а не работу. Рабочее определение звучит иначе.
Встраиваемая система — это вычислитель внутри изделия, существующий ради конкретной физической функции: он работает на ресурсах, зафиксированных в момент пайки, обязан укладываться во время, часто питается от невосполнимого источника, обновляется редко или никогда, а его отказ имеет физические последствия.
Разберём по частям, потому что каждая порождает класс задач, которых нет в бэкенде.
- Ресурсы зафиксированы в момент пайки. Нельзя «добавить памяти» — 192 КБ ОЗУ на плате будут там до конца жизни изделия. Профиль потребления памяти известен на этапе компоновки, а не на проде.
- Физическая функция. Код существует, чтобы вращать вал, открывать клапан, измерять давление. Правильный ответ, полученный на 20 мс позже, — неправильный ответ.
- Невосполнимая энергия. Датчик на литиевой таблетке должен прожить два года. Это не «оптимизация», это требование, из которого вырастает вся архитектура прошивки.
- Обновления редки. Устройство может стоять в подвале за бетонной стеной. Каждая прошивка — потенциальный кирпич, и откатить её вы не сможете.
- Отказ физичен. Зависший веб-сервис отдаёт 500. Зависший контроллер привода продолжает подавать ток в обмотку.
Отсюда общий тезис трека: встраиваемая разработка — это не «программирование на C поменьше», это инженерия систем с жёсткими ограничениями по памяти, энергии и времени. Если снять любое из трёх ограничений, дисциплина схлопывается обратно в обычную разработку. Именно поэтому программист «больших» машин, впервые взявший микроконтроллер, чаще всего спотыкается не о синтаксис, а о привычки.
Три сдвига, которые ломают серверные привычки
Сдвиг первый: память конечна, известна заранее и никем не защищена
На сервере память — это абстракция, которую поддерживает ядро ОС и MMU: у процесса своё виртуальное пространство, ошибка адресации ловится страничным исключением, аллокатор берёт страницы у ядра, а если не хватило — прилетит OOM-killer. На микроконтроллере ничего этого нет.
| Свойство | Сервер (Linux, x86-64) | Микроконтроллер (Cortex-M4) |
|---|---|---|
| ОЗУ | десятки гигабайт, виртуальное | 20–512 КБ, физическое |
| Защита памяти | MMU, страницы, сегфолт | опциональный MPU, чаще выключен |
| Куча | нормальный инструмент | почти всегда запрещена |
| Переполнение стека | сигнал SIGSEGV | молча портит соседние переменные |
| «Кончилась память» | OOM в логе, рестарт сервиса | зависание либо тихо неверные данные |
| Единица измерения | мегабайты | байты, и вы их считаете руками |
Практическое следствие: весь объём памяти распределяется статически, а отчёт компоновщика (arm-none-eabi-size) становится метрикой сборки наравне с тестами. Динамическая аллокация выбрасывается не из аскетизма, а по двум причинам: фрагментация в системе, которая работает годами без перезапуска, гарантированно приведёт к отказу malloc в самый неудачный момент; а время работы аллокатора не ограничено сверху, то есть ломает детерминизм. Подробный разбор — в статье про C для встраиваемых систем.
Как устроено адресное пространство и почему указатель здесь — буквально физический адрес:
Сдвиг второй: энергия — расходный материал, а не фон
В серверной разработке энергия — статья расходов дата-центра, к коду отношения почти не имеющая. Здесь это ограничение первого порядка. Ключевая интуиция: потребление — это площадь под графиком тока во времени, и в устройствах с батарейным питанием эту площадь набирает не активная работа, а сон, потому что сна в тысячи раз больше.
Считать надо до написания кода, а не после. Вот честный бюджет узла, который просыпается раз в 10 минут, читает датчик и отправляет пакет:
# Бюджет энергии: считаем миллиампер-секунды за один цикл, потом делим ёмкость батареи
# Сложность вычисления O(1), но именно это O(1) определяет архитектуру всей прошивки
PERIOD_S = 600.0 # цикл: раз в 10 минут
PHASES = [ # (название, ток в мА, длительность в секундах)
("измерение датчика", 6.0, 0.040),
("передача по радио", 12.0, 0.120),
]
def cycle_charge_mas(sleep_current_ma: float) -> float:
"""Заряд за один цикл в мА*с: активные фазы плюс сон на всё остальное время."""
active = sum(cur * dur for _, cur, dur in PHASES)
active_time = sum(dur for _, _, dur in PHASES)
sleep = sleep_current_ma * (PERIOD_S - active_time)
return active + sleep
def battery_years(capacity_mah: float, sleep_current_ma: float, usable: float = 0.8) -> float:
charge = cycle_charge_mas(sleep_current_ma)
cycles = capacity_mah * 3600.0 * usable / charge # мА*ч переводим в мА*с
return cycles * PERIOD_S / (365.25 * 24 * 3600)
CR2032 = 220.0 # типичная ёмкость литиевой таблетки, мА*ч
for sleep_ua in (5.0, 50.0):
years = battery_years(CR2032, sleep_ua / 1000.0)
print(f"ток сна {sleep_ua:5.1f} мкА -> {years:4.1f} года работы от CR2032")
# ток сна 5.0 мкА -> 2.6 года работы от CR2032
# ток сна 50.0 мкА -> 0.4 года работы от CR2032
Вывод, который стоит запомнить на весь трек: десятикратная разница в токе сна превратила два с половиной года в пять месяцев, хотя активный код не изменился ни на строку. Забытая подтяжка, светодиод питания на плате, неотключённый регулятор датчика — каждая мелочь стоит месяцев жизни изделия. Детальный разбор режимов и способов их измерять — в статье про питание и ограничения.
Микроконтроллер живёт не в двух состояниях «работает / не работает», а в лестнице режимов с разной ценой выхода:
Обратите внимание на асимметрию: чем глубже сон, тем дороже выход. Из Standby устройство выходит фактически через сброс — ОЗУ потеряно, стартовый код отрабатывает заново, состояние приходится хранить в отдельных регистрах или во Flash. Архитектура прошивки датчика на батарее — это, по сути, конечный автомат, спроектированный вокруг этой лестницы.
Сдвиг третий: время — часть функционального контракта
В вебе латентность — метрика качества: p99 в 300 мс хуже, чем 200 мс, но продукт работает. В управлении шаговым двигателем опоздание импульса на 50 мкс означает пропуск шага и потерянную позицию. Разница фундаментальна: там время — характеристика, здесь — условие корректности.
Отсюда три понятия, которых нет в обычной разработке:
- WCET (worst-case execution time) — не среднее и не медиана, а гарантированная верхняя граница времени выполнения участка. Средняя латентность вас не спасает: важно, что произойдёт в самом плохом стечении обстоятельств.
- Джиттер — разброс момента реакции. Регулятор, который считает с периодом «10 мс плюс-минус 3 мс», физически считает неправильные производные и интегралы, потому что формулы предполагают равномерный шаг.
- Инверсия приоритетов — низкоприоритетная задача держит мьютекс, нужный высокоприоритетной, а среднеприоритетная тем временем вытесняет обеих. Это не теория: в июле 1997 года марсоход Pathfinder регулярно перезагружался на поверхности Марса именно из-за инверсии приоритетов в VxWorks; лечение выкатили патчем, включив наследование приоритетов на мьютексе информационной шины (разбор в RISKS Digest 19.49).
Из этого следуют вещи, которые на сервере кажутся дикостью: сборщик мусора недопустим не потому, что «медленный», а потому что пауза непредсказуема; кэш данных иногда специально отключают ради повторяемости; ветвление кода с непредсказуемой длиной заменяют таблицами. Механика прерываний и планирования — в статьях про прерывания и реальное время и RTOS.
Полезное разграничение, которое стоит усвоить сразу: hard real-time — пропуск дедлайна равен отказу системы (управление инжектором, ABS, привод станка); firm real-time — просроченный результат бесполезен, но не катастрофичен (кадр видео); soft real-time — просрочка ухудшает качество (обновление экрана). Дисциплина проектирования у них разная, а цена ошибки различается на порядки.
Что вообще находится внутри
Главное, что видно на схеме: микроконтроллер — это законченный компьютер на одном кристалле, где периферия отображена в то же адресное пространство, что и память. Записать байт в UART и записать байт в массив — синтаксически одно и то же действие; разница в том, что первое имеет физический побочный эффект, поэтому регистры описываются как volatile, а компилятору запрещено «оптимизировать» такие обращения. Как из этого вырастает вся работа с железом — в статье про периферию.
Терминологическая развилка, которую путают чаще всего:
- MCU (микроконтроллер) — ядро, Flash и ОЗУ на одном кристалле, работает без ОС или под RTOS, стартует за миллисекунды, потребляет микроамперы во сне. STM32, ESP32, AVR, RP2040.
- MPU / SoC (микропроцессор) — только ядро с MMU, память внешняя, нужен полноценный Linux, стартует за секунды, потребляет ватты. Raspberry Pi, i.MX, Rockchip.
Граница проходит по MMU и внешней памяти, а не по частоте. Именно она делит трек на две половины: до неё вы пишете прошивку, после неё — обычное Linux-приложение, просто на ARM. Как работает ядро ОС на второй половине, разобрано в треке Операционные системы, а устройство самого процессора — в статье Как работает процессор.
Откуда взялась дисциплина
Три вывода. Порог входа упал, а потолок вырос: собрать мигалку теперь можно за вечер, но требования к продакшн-прошивке (безопасность, OTA, сертификация) выросли многократно. Восьмибитная эра закончилась в новых проектах, хотя AVR остаётся отличным учебным инструментом. Граница между «прошивкой» и «софтом» размывается: типовой робот сегодня — это микроконтроллер реального времени плюс Linux-машина, обменивающиеся сообщениями.
Железо: что брать и когда
Самый частый вопрос новичка — «с какой платы начать». Правильный ответ зависит от четырёх вопросов: нужен ли детерминизм реального времени; нужна ли беспроводная связь; нужна ли файловая система, камера или тяжёлые вычисления; сколько живёт батарея. Ответы разводят по разным семействам.
| Платформа | Ядро и частота | Память | Чем сильна | Когда брать |
|---|---|---|---|---|
| Arduino Uno (ATmega328P) | AVR 8 бит, 16 МГц | 32 КБ Flash, 2 КБ ОЗУ | простейшая модель, гигантская библиотека примеров | первое знакомство, разовые макеты |
| STM32 (F0/G0 … F4/H7) | Cortex-M0+ … M7, 48–480 МГц | 16 КБ – 2 МБ Flash, 4–1024 КБ ОЗУ | богатейшая периферия, детерминизм, промышленный ассортимент | продакшн, управление, реальное время |
| ESP32 / ESP32-C3 / S3 | Xtensa или RISC-V, 160–240 МГц | 320–520 КБ ОЗУ, внешний Flash | Wi-Fi и BLE на кристалле, цена модуля порядка 2 USD | всё, где нужна связь и не нужен жёсткий realtime |
| RP2040 / RP2350 (Pico) | 2 x Cortex-M0+ или M33, 133–150 МГц | 264–520 КБ ОЗУ, внешний Flash | блоки PIO — программируемая логика для своих протоколов | нестандартные интерфейсы, обучение, дёшево |
| nRF52840 / nRF54 | Cortex-M4F или M33, 64–128 МГц | до 1 МБ Flash, 256 КБ ОЗУ | BLE на уровне эталона, микроамперы во сне | носимые устройства, датчики на батарее |
| Raspberry Pi 4/5, Zero 2W | Cortex-A, 1–2,4 ГГц | 0,5–8 ГБ ОЗУ | полноценный Linux, камера, Python, ROS 2 | зрение, сеть, прототипы роботов |
Практические правила, сэкономленные чужой болью. Не начинайте продакшн с Arduino-библиотек: они прячут регистры, и первая же нестандартная задача заставит переписывать всё. ESP32 — не микроконтроллер реального времени: стек Wi-Fi занимает ядро на непредсказуемое время, и если вам нужен строгий период, выносите его на отдельный чип или на аппаратный таймер. Raspberry Pi — не контроллер привода: Linux без патчей PREEMPT_RT даёт джиттер в единицы и десятки миллисекунд, чего достаточно для навигации и категорически мало для токовой петли мотора. Учитывайте доступность на 5 лет вперёд: дефицит 2021–2023 годов похоронил проекты, завязанные на один дефицитный корпус.
Отдельно про два соседних мира, которые часто путают с embedded. FPGA берут, когда нужны параллельные жёсткие тайминги на наносекундах (обработка сигналов, нестандартные высокоскоростные интерфейсы) — это не программирование, а описание схемы. SoM (system-on-module) — готовый модуль с процессором и памятью, который вы ставите на свою плату: способ получить Linux, не разводя BGA с DDR самому.
Код: C, C++ и Python — кто где
Языковой расклад в отрасли устойчив и объясняется ограничениями, а не вкусами.
C — язык прошивок. Он даёт предсказуемый машинный код, работает без рантайма и поддерживается любым компилятором для любой архитектуры. Вот мигание светодиодом через регистры — уровень, с которого стоит начать, чтобы понимать, что делают библиотеки:
#include "stm32f4xx.h"
int main(void) {
// 1. Тактирование порта включается отдельно: без этой строки запись
// в регистры GPIOD физически уходит в никуда, и это классические
// два часа отладки на первом проекте
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
// 2. Режим вывода: два бита на ножку, поэтому сдвиг на удвоенный номер
GPIOD->MODER &= ~(3U << (12 * 2)); // очищаем оба бита
GPIOD->MODER |= (1U << (12 * 2)); // 01 — цифровой выход
while (1) {
GPIOD->BSRR = (1U << 12); // атомарная установка бита
for (volatile int i = 0; i < 800000; i++) { }
GPIOD->BSRR = (1U << (12 + 16)); // атомарный сброс: старшая половина регистра
for (volatile int i = 0; i < 800000; i++) { }
}
}
Здесь уже видны два принципиальных момента. BSRR вместо чтения-модификации-записи ODR — потому что прерывание, случившееся между чтением и записью, испортит соседний бит. volatile у счётчика — потому что без него компилятор с оптимизацией просто удалит пустой цикл, и светодиод замигает с частотой в мегагерцы.
Никакой динамики и никаких сюрпризов — вот как выглядит типовая структура данных прошивки, кольцевой буфер для приёма из обработчика прерывания:
#include <stdint.h>
#include <stdbool.h>
#define RB_SIZE 64u // степень двойки: маска дешевле деления на такты
typedef struct {
uint8_t buf[RB_SIZE];
volatile uint16_t head; // двигает только обработчик прерывания
volatile uint16_t tail; // двигает только основной цикл
} ring_t;
// Вызывается из ISR приёмника UART. Время O(1) и оно ограничено сверху,
// память O(RB_SIZE) и выделена на этапе компоновки. Один писатель и один
// читатель делают конструкцию безопасной без единого мьютекса.
static inline bool rb_push(ring_t *rb, uint8_t byte) {
uint16_t next = (uint16_t)((rb->head + 1u) & (RB_SIZE - 1u));
if (next == rb->tail) {
return false; // переполнение: теряем байт осознанно и считаем потери
}
rb->buf[rb->head] = byte;
__DMB(); // барьер: данные попадут в память до публикации индекса
rb->head = next;
return true;
}
Сравните с серверным рефлексом «возьму очередь из стандартной библиотеки»: здесь размер буфера — проектное решение, потеря данных — спроектированное поведение, а барьер памяти — обязательная деталь, потому что ядро может переупорядочить записи.
C++ — там, где нужна абстракция без цены. Шаблоны и constexpr позволяют строить типобезопасные обёртки, которые компилируются в те же одну-две инструкции:
// Обёртка над выводом: типобезопасная, но стоит ноль байт и ноль тактов
template <std::uintptr_t GpioBase, unsigned Pin>
struct OutputPin {
static void high() { gpio()->BSRR = 1u << Pin; }
static void low() { gpio()->BSRR = 1u << (Pin + 16); }
private:
static GPIO_TypeDef *gpio() { return reinterpret_cast<GPIO_TypeDef *>(GpioBase); }
};
using StatusLed = OutputPin<GPIOD_BASE, 12>;
// StatusLed::high() превращается в одну инструкцию str — ровно как ручная запись в регистр
Правило простое: берите из C++ шаблоны, enum class, constexpr и RAII, но отключайте исключения и RTTI (-fno-exceptions -fno-rtti) и держитесь подальше от std::function, потоков и всего, что аллоцирует. Растущее третье направление — Rust: борроу-чекер снимает целый класс ошибок с памятью, а экосистема embedded-hal и probe-rs уже пригодна для продакшна.
Python — язык прототипа и робототехники. На микроконтроллере это MicroPython и CircuitPython: отличный способ за десять минут проверить, что датчик вообще отвечает, ценой десятикратного проигрыша по скорости и памяти. На Linux-половине робота Python — основной рабочий язык через ROS 2:
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
class SafetyStop(Node):
"""Минимальный узел ROS 2: едем вперёд, пока впереди свободно."""
STOP_DISTANCE_M = 0.35
def __init__(self) -> None:
super().__init__("safety_stop")
self.cmd = self.create_publisher(Twist, "cmd_vel", 10)
# Для лидара берём политику "лучшие усилия": свежие данные важнее полных
self.create_subscription(LaserScan, "scan", self.on_scan, 10)
def on_scan(self, msg: LaserScan) -> None:
ahead = [r for r in msg.ranges[:20] + msg.ranges[-20:] if msg.range_min < r < msg.range_max]
nearest = min(ahead, default=float("inf"))
cmd = Twist()
cmd.linear.x = 0.0 if nearest < self.STOP_DISTANCE_M else 0.2
self.cmd.publish(cmd)
def main() -> None:
rclpy.init()
node = SafetyStop()
try:
rclpy.spin(node)
finally:
node.destroy_node()
rclpy.shutdown()
Ключевое здесь — разделение ответственности: этот узел решает «ехать или нет» с периодом в десятки миллисекунд, а импульсы на моторы формирует микроконтроллер с периодом в десятки микросекунд. Смешивать эти два уровня в одном процессе — типовая ошибка новичка в робототехнике.
Как код попадает в железо
Пайплайн сборки прошивки короче серверного, но в нём есть два шага, которых у вас раньше не было: скрипт компоновщика и стартовый код.
Reset_Handler, копия .data, обнуление .bss"] --> CC LD["link.ld: где Flash, где SRAM,
вершина стека, размеры секций"] --> LNK CC --> OBJ["Объектные файлы .o"] OBJ --> LNK["Компоновщик"] LNK --> ELF["firmware.elf
код + символы + отладочная информация"] ELF --> SIZE["arm-none-eabi-size:
влезли или нет — проверять в CI"] ELF --> BIN["objcopy -O binary / ihex
чистый образ для прошивальщика"] ELF --> DBG["GDB: символы для отладки"] BIN --> FLASH["Программатор: OpenOCD, probe-rs,
ST-Link, DFU по USB"] FLASH --> TGT["Плата"] DBG --> PROBE["Отладчик по SWD"] PROBE --> TGT TGT --> HIL["Тесты на реальном железе в CI
hardware-in-the-loop"] HIL -->|регрессия| SRC
Два наблюдения. Первое: firmware.elf содержит символы и отладочную информацию, а в чип уезжает только .bin — поэтому артефакт сборки надо сохранять целиком, иначе разобрать аварийный дамп с поля будет нечем. Второе: контроль размера прошивки — такой же обязательный шаг CI, как тесты; проект, где .bss внезапно вырос на 20 КБ, узнаёт об этом не на плате, а на пулл-реквесте. Как строить конвейер и раскатывать обновления по воздуху — в статье про отладку и производство, а общие практики доставки — в треке DevOps.
Отладка без принтов
Это тот раздел, ради которого стоит перестроить мышление сильнее всего. На сервере отладка начинается с логов; здесь лог сам по себе меняет поведение системы.
Почему printf через UART — плохой первый инструмент:
- Он ломает тайминги. Одна строка в 40 байт на скорости 115200 бод занимает около 3,5 мс. Обработчик прерывания, который должен укладываться в 50 мкс, с принтом внутри опаздывает в семьдесят раз. Гонка, которую вы ищете, исчезает ровно тогда, когда вы добавляете лог — классический гейзенбаг.
- Он стоит памяти. Полноценный
printfс поддержкой чисел с плавающей точкой тянет десятки килобайт кода — иногда больше, чем вся ваша логика. - Он не видит того, что происходит вне процессора. Если сигнал не доходит до ножки датчика, в программе ничего не видно: вы будете отлаживать корректный код.
Правильная реакция — подобрать инструмент под симптом:
доходит до этого места?"} Q1 -->|Неизвестно| SWD["Отладчик по SWD:
точки останова, шаг, регистры,
просмотр памяти"] Q1 -->|Доходит| Q2{"Проблема связана
со временем?"} Q2 -->|Да| Q3{"Нужны такты
или профиль?"} Q3 -->|Профиль| GPIO["Переключение ножки в начале
и конце участка + осциллограф:
прямое измерение длительности"] Q3 -->|Такты| DWT["Счётчик тактов DWT
плюс вывод через SWO или RTT"] Q2 -->|Нет| Q4{"Данные по шине
вообще ходят?"} Q4 -->|Проверить| LA["Логический анализатор:
декодеры I2C, SPI, UART, CAN,
десятки каналов, длинные записи"] Q4 -->|Ходят, но мусор| OSC["Осциллограф: форма фронта,
звон, уровни, подтяжки, помехи"] S --> Q5{"Перезагружается
или зависает?"} Q5 -->|Да| HF["Разбор аварии: HardFault,
сохранённый регистр состояния,
причина сброса, сторожевой таймер"] Q5 -->|Потребление не то| PWR["Измерение тока с записью:
ищем режим, из которого не вышли"]
Разберём инструменты по существу.
SWD и JTAG — окно внутрь ядра. JTAG — исторический стандарт: четыре-пять сигналов и цепочка устройств. SWD (Serial Wire Debug) — то же самое на двух проводах, SWDIO и SWCLK, и именно его используют на Cortex-M. Через этот интерфейс отладчик читает и пишет память и регистры без остановки ядра, ставит аппаратные точки останова (на Cortex-M4 их шесть — не потому, что «так решили», а потому что столько компараторов в блоке FPB) и точки наблюдения за адресами через DWT. Практический смысл: вы ставите точку останова на запись в переменную и узнаёте, кто её портит, — задача, которая по логам не решается никогда. Софт: OpenOCD с GDB, pyOCD, probe-rs; железо: ST-Link (порядка 12 USD), CMSIS-DAP или picoprobe почти бесплатно, J-Link — эталон с ценой от 60 USD за учебную версию.
Важная оговорка: отладчик с точками останова не годится для отладки реального времени. Остановленный на брейкпойнте контроллер продолжает подавать ток в мотор, а собеседник по шине CAN уже отвалился по таймауту. Для реального времени нужны неинтрузивные способы.
SWO и RTT — логи, которые почти ничего не стоят. Вывод SWO вместе с блоком ITM даёт односторонний поток трассировки на скорости в единицы мегабит через один пин: сообщение стоит десятки тактов вместо миллисекунд. Ещё быстрее SEGGER RTT — прошивка кладёт байты в кольцевой буфер в ОЗУ, а отладчик вычитывает их фоновым доступом по SWD, не трогая ядро. Это и есть «принты», которыми можно пользоваться в продакшн-прошивке.
Логический анализатор — микроскоп для цифровых шин. Восемь или шестнадцать каналов, запись на секунды и главное — декодеры протоколов: вы видите не «фронты», а разобранные транзакции I2C с адресами и подтверждениями. Типичные находки: устройство отвечает NACK, потому что адрес сдвинут на бит; SPI работает не в том режиме тактирования; между посылками не выдержана пауза из даташита. Бюджетный клон на FX2 стоит порядка 10–15 USD и вместе с открытым sigrok и PulseView закрывает большинство задач; Saleae — удобнее и дороже на порядок. Правило выбора частоты дискретизации: минимум вчетверо, лучше в десять раз выше частоты шины, иначе короткий импульс просто не попадёт в выборку.
Осциллограф — единственный, кто видит физику. Логический анализатор говорит «единица или ноль»; осциллограф показывает, что там на самом деле. Что видно только им: заваленный фронт на I2C из-за слишком слабой подтяжки (сигнал не успевает дойти до порога — шина «иногда не работает»); звон на длинном шлейфе, который приёмник считает лишними тактами; просадка питания в момент включения мотора, роняющая контроллер в сброс; дребезг контактов кнопки, дающий десяток срабатываний вместо одного. Два практических правила: полоса прибора должна быть примерно в пять раз выше основной частоты сигнала, а произведение полосы на время нарастания фронта — это константа около 0,35, откуда и считают, увидите ли вы фронт вообще. И второе, ежедневное: длинный крокодил земли на щупе создаёт звон, которого в схеме нет — для быстрых сигналов используйте пружину заземления.
Измерение тока с записью во времени. Для устройств на батарее это главный отладочный прибор: график тока сразу показывает, что контроллер не ушёл в Stop, что радиомодуль остался включённым или что датчик потребляет 3 мА вместо заявленных 300 мкА. Подходят как специализированные приборы вроде Nordic Power Profiler, так и осциллограф с токовым шунтом.
Наконец, разбор аварий. Прошивка обязана уметь рассказать о своей смерти: сохранять причину сброса из регистра состояния, ловить HardFault и складывать в невыключаемую память адрес, по которому упали, вести счётчики срабатываний сторожевого таймера. Пять минут работы на этапе проектирования экономят недели на поле, где к устройству не подключиться отладчиком.
Сессия отладки живого устройства выглядит так:
не останавливает физику IDE->>Probe: чтение буфера RTT без остановки ядра Probe-->>IDE: журнал последних событий из ОЗУ
Робототехника: где встраиваемое встречается с большим
Робот — самый наглядный пример системы, где обе половины трека работают вместе. Типовая архитектура двухуровневая: нижний уровень на микроконтроллере закрывает токовые петли моторов, чтение энкодеров и аварийную остановку с периодом в сотни микросекунд; верхний уровень на Linux-машине занимается зрением, планированием маршрута и связью, работая с периодом в десятки миллисекунд. Связывает их шина — CAN, UART или Ethernet, — и именно на этой границе живут самые интересные инженерные компромиссы.
Стандартом верхнего уровня стал ROS 2: узлы-процессы, обмен сообщениями через DDS, готовые пакеты для навигации, SLAM и симуляции. На микроконтроллер спускается micro-ROS, позволяющий контроллеру быть полноценным узлом той же сети. Разбор кинематики, регуляторов и обратной связи — в статье про робототехнику, а датчики и приводы — в статье про датчики и приводы.
Замечу, что робот — это распределённая система в миниатюре: несколько узлов, ненадёжный канал, частичные отказы и необходимость договариваться о состоянии. Многие идеи трека Распределённые системы применимы напрямую, только вместо сетевых таймаутов у вас пропавшие кадры CAN.
На портале эта тема продолжена в разделе НИР — там разобрана инженерия роботизированных систем на уровне архитектуры, а не отдельного контроллера: определение и классификация роботизированных систем, теоретическое обоснование микросервисной архитектуры для распределённых роботизированных систем, программная реализация сервисов и итоговые результаты работы. Полезно прочитать после статей про протоколы и робототехнику: там видно, как из контроллеров и сервисов собирается работающая система целиком.
Типичные ошибки на входе
- Отлаживать принтами. Первое же явление, связанное со временем, исчезнет от вашего лога. Ставьте отладчик и логический анализатор с первого дня.
- Использовать
malloc. Работает на столе, отказывает через три недели непрерывной работы из-за фрагментации. - Забыть
volatileна регистре или на переменной, разделяемой с обработчиком прерывания. С-O0работает, с-Osперестаёт — и это выглядит как «баг компилятора». - Писать длинные обработчики прерываний. ISR должен положить данные в буфер и выйти; вся обработка — в основном цикле или в задаче RTOS.
- Считать, что стек не переполнится. Один рекурсивный вызов или буфер на 4 КБ в локальной переменной — и вы молча портите чужие данные. Заполняйте стек шаблоном и проверяйте отметку максимума.
- Игнорировать электрику. Отсутствие развязывающего конденсатора, слабая подтяжка I2C, общий провод, проложенный «как получилось» — это баги, которые не чинятся в коде.
- Тестировать при комнатной температуре. Кварц уходит, батарея теряет ёмкость, пластик деформируется. Морозильник и фен — законные инструменты тестировщика.
- Прошивать без плана отката. Обновление по воздуху без второго слота и проверки подписи однажды превратит парк устройств в кирпичи.
- Не бюджетировать энергию до начала разработки. Обнаружить в конце проекта, что вместо двух лет получается два месяца, — самый дорогой способ узнать про ток сна.
- Переносить серверные абстракции целиком. Слои, фабрики и полиморфизм на устройстве с 8 КБ ОЗУ съедают ресурс, который был нужен для буферов.
Карта трека
Трек идёт от железа к системе: сначала физика и язык, потом периферия и время, потом связь и энергия, в конце — робототехника и производство.
- Железо для программиста — микроконтроллеры и платы, чтение схем и даташитов, питание, подтяжки, что на самом деле происходит на ножке.
- C для встраиваемых систем — модель памяти, указатели,
volatile, битовые операции, фиксированная точка, жизнь без динамики. - Периферия — GPIO, АЦП и ЦАП, ШИМ, таймеры, захват и сравнение, DMA как способ не тратить такты ядра.
- Прерывания и реальное время — векторы и приоритеты, латентность, гонки, атомарность, детерминизм и WCET.
- Протоколы связи — UART, I2C, SPI, CAN и 1-Wire: физика, кадры, арбитраж, типичные отказы и их вид на анализаторе.
- RTOS — задачи и планировщик, приоритеты и вытеснение, семафоры и очереди, FreeRTOS и Zephyr на практике.
- Питание и ограничения — режимы сна, бюджет энергии, выбор источника, экономия Flash и ОЗУ.
- IoT-связь — Wi-Fi, BLE, LoRa и MQTT, топология сетей, обновления по воздуху и безопасность устройств.
- Датчики и приводы — считывание и шум, фильтрация и калибровка, моторы, драйверы и обратная связь.
- Робототехника — кинематика, регуляторы, ROS 2 и micro-ROS, одометрия и замыкание петли управления.
- Отладка и производство — SWD и трассировка, логический анализатор, тестирование на железе, прошивка на конвейере и OTA.
Смежное на портале: Операционные системы для драйверов и ввода-вывода, Компьютерные науки для битов и представления данных, Безопасность для подписи прошивок и хранения ключей, Тестирование для дисциплины проверок, Машинное обучение для моделей, которые сегодня уезжают на сам контроллер. Материалы по сетям и производительности живут в отдельных треках портала.
Мини-итог
Встраиваемая разработка отличается от «большой» не размером кода, а тремя жёсткими ограничениями: память конечна и распределяется статически, энергия невосполнима и тратится в основном во сне, время — часть контракта, а не метрика качества. Из них выводится почти всё остальное: запрет динамической аллокации, короткие обработчики прерываний, volatile на каждом регистре, конечные автоматы вместо длинных сценариев и отладка приборами вместо принтов. Выбор платформы сводится к четырём вопросам про детерминизм, связь, вычисления и батарею: STM32 — когда нужно управлять, ESP32 — когда нужно связаться, RP2040 — когда нужно дёшево и нестандартно, Raspberry Pi — когда нужен Linux и зрение.
Практический старт на ближайшую неделю: возьмите любую плату на Cortex-M и отдельный отладчик (не только USB-кабель), мигните светодиодом через регистры без библиотек, затем измерьте осциллографом реальную длительность вашей задержки — расхождение с расчётом и будет вашим первым настоящим уроком. После этого добавьте датчик по I2C и посмотрите обмен логическим анализатором: увидеть транзакцию своими глазами — быстрее любого чтения документации.
Источники
- Elecia White. Making Embedded Systems, O’Reilly — https://www.oreilly.com/library/view/making-embedded-systems/9781449308889/
- Joseph Yiu. The Definitive Guide to Arm Cortex-M3 and Cortex-M4 Processors — https://www.sciencedirect.com/book/9780124080829/the-definitive-guide-to-arm-cortex-m3-and-cortex-m4-processors
- Arm. Cortex-M4 Devices Generic User Guide — https://developer.arm.com/documentation/dui0553/latest/
- Barr Group. Embedded C Coding Standard — https://barrgroup.com/embedded-systems/books/embedded-c-coding-standard
- Jack Ganssle. The Embedded Muse, заметки о практике отладки — http://www.ganssle.com/
- Miro Samek. Practical UML Statecharts и подход к конечным автоматам в прошивках — https://www.state-machine.com/
- STMicroelectronics. Справочные руководства на STM32 — https://www.st.com/en/microcontrollers-microprocessors/stm32-32-bit-arm-cortex-mcus.html
- Espressif. Документация ESP-IDF — https://docs.espressif.com/projects/esp-idf/en/stable/esp32/
- Raspberry Pi. Документация на микроконтроллеры RP2040 и RP2350 — https://www.raspberrypi.com/documentation/microcontrollers/
- FreeRTOS — https://www.freertos.org/ и Zephyr Project — https://docs.zephyrproject.org/latest/
- OpenOCD — https://openocd.org/ , probe-rs — https://probe.rs/ , sigrok и PulseView — https://sigrok.org/wiki/PulseView
- SEGGER. Real Time Transfer, отладочный вывод без остановки ядра — https://www.segger.com/products/debug-probes/j-link/technology/about-real-time-transfer/
- The Embedded Rust Book — https://docs.rust-embedded.org/book/
- ROS 2 — https://docs.ros.org/ и micro-ROS — https://micro.ros.org/
- MISRA C, отраслевые правила для критичного кода — https://misra.org.uk/
- Что на самом деле случилось на Марсе: разбор инверсии приоритетов в Pathfinder, RISKS Digest 19.49 — https://catless.ncl.ac.uk/Risks/19.49.html
- Отчёт комиссии по аварии Ariane 5 Flight 501 — https://esamultimedia.esa.int/docs/esa-x-1819eng.pdf
Что дальше
Железо для программиста: микроконтроллеры, платы, схемы, питание — разберём, как устроен микроконтроллер на уровне выводов и корпусов, как читать даташит и схему, откуда берётся питание и почему развязывающий конденсатор рядом с чипом важнее, чем кажется, а также как собрать минимальный рабочий стенд и не спалить первую плату.