Встраиваемые системы и робототехника Встраиваемые системы: карта трека и чем эта разработка отличается
0%

Встраиваемые системы: карта трека и чем эта разработка отличается

Встраиваемые системы: карта трека и чем эта разработка отличается

Что такое встраиваемая система

Определение «программа для маленького компьютера» бесполезно — оно описывает габарит, а не работу. Рабочее определение звучит иначе.

Встраиваемая система — это вычислитель внутри изделия, существующий ради конкретной физической функции: он работает на ресурсах, зафиксированных в момент пайки, обязан укладываться во время, часто питается от невосполнимого источника, обновляется редко или никогда, а его отказ имеет физические последствия.

Разберём по частям, потому что каждая порождает класс задач, которых нет в бэкенде.

  • Ресурсы зафиксированы в момент пайки. Нельзя «добавить памяти» — 192 КБ ОЗУ на плате будут там до конца жизни изделия. Профиль потребления памяти известен на этапе компоновки, а не на проде.
  • Физическая функция. Код существует, чтобы вращать вал, открывать клапан, измерять давление. Правильный ответ, полученный на 20 мс позже, — неправильный ответ.
  • Невосполнимая энергия. Датчик на литиевой таблетке должен прожить два года. Это не «оптимизация», это требование, из которого вырастает вся архитектура прошивки.
  • Обновления редки. Устройство может стоять в подвале за бетонной стеной. Каждая прошивка — потенциальный кирпич, и откатить её вы не сможете.
  • Отказ физичен. Зависший веб-сервис отдаёт 500. Зависший контроллер привода продолжает подавать ток в обмотку.

Отсюда общий тезис трека: встраиваемая разработка — это не «программирование на C поменьше», это инженерия систем с жёсткими ограничениями по памяти, энергии и времени. Если снять любое из трёх ограничений, дисциплина схлопывается обратно в обычную разработку. Именно поэтому программист «больших» машин, впервые взявший микроконтроллер, чаще всего спотыкается не о синтаксис, а о привычки.

Три сдвига, которые ломают серверные привычки

Сдвиг первый: память конечна, известна заранее и никем не защищена

На сервере память — это абстракция, которую поддерживает ядро ОС и MMU: у процесса своё виртуальное пространство, ошибка адресации ловится страничным исключением, аллокатор берёт страницы у ядра, а если не хватило — прилетит OOM-killer. На микроконтроллере ничего этого нет.

Свойство Сервер (Linux, x86-64) Микроконтроллер (Cortex-M4)
ОЗУ десятки гигабайт, виртуальное 20–512 КБ, физическое
Защита памяти MMU, страницы, сегфолт опциональный MPU, чаще выключен
Куча нормальный инструмент почти всегда запрещена
Переполнение стека сигнал SIGSEGV молча портит соседние переменные
«Кончилась память» OOM в логе, рестарт сервиса зависание либо тихо неверные данные
Единица измерения мегабайты байты, и вы их считаете руками

Практическое следствие: весь объём памяти распределяется статически, а отчёт компоновщика (arm-none-eabi-size) становится метрикой сборки наравне с тестами. Динамическая аллокация выбрасывается не из аскетизма, а по двум причинам: фрагментация в системе, которая работает годами без перезапуска, гарантированно приведёт к отказу malloc в самый неудачный момент; а время работы аллокатора не ограничено сверху, то есть ломает детерминизм. Подробный разбор — в статье про C для встраиваемых систем.

Как устроено адресное пространство и почему указатель здесь — буквально физический адрес:

Карта адресов Cortex-M: Flash, SRAM, регистры периферии и раскладка секций

Сдвиг второй: энергия — расходный материал, а не фон

В серверной разработке энергия — статья расходов дата-центра, к коду отношения почти не имеющая. Здесь это ограничение первого порядка. Ключевая интуиция: потребление — это площадь под графиком тока во времени, и в устройствах с батарейным питанием эту площадь набирает не активная работа, а сон, потому что сна в тысячи раз больше.

Считать надо до написания кода, а не после. Вот честный бюджет узла, который просыпается раз в 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()

Ключевое здесь — разделение ответственности: этот узел решает «ехать или нет» с периодом в десятки миллисекунд, а импульсы на моторы формирует микроконтроллер с периодом в десятки микросекунд. Смешивать эти два уровня в одном процессе — типовая ошибка новичка в робототехнике.

Как код попадает в железо

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

Два наблюдения. Первое: firmware.elf содержит символы и отладочную информацию, а в чип уезжает только .bin — поэтому артефакт сборки надо сохранять целиком, иначе разобрать аварийный дамп с поля будет нечем. Второе: контроль размера прошивки — такой же обязательный шаг CI, как тесты; проект, где .bss внезапно вырос на 20 КБ, узнаёт об этом не на плате, а на пулл-реквесте. Как строить конвейер и раскатывать обновления по воздуху — в статье про отладку и производство, а общие практики доставки — в треке DevOps.

Отладка без принтов

Это тот раздел, ради которого стоит перестроить мышление сильнее всего. На сервере отладка начинается с логов; здесь лог сам по себе меняет поведение системы.

Почему printf через UART — плохой первый инструмент:

  • Он ломает тайминги. Одна строка в 40 байт на скорости 115200 бод занимает около 3,5 мс. Обработчик прерывания, который должен укладываться в 50 мкс, с принтом внутри опаздывает в семьдесят раз. Гонка, которую вы ищете, исчезает ровно тогда, когда вы добавляете лог — классический гейзенбаг.
  • Он стоит памяти. Полноценный printf с поддержкой чисел с плавающей точкой тянет десятки килобайт кода — иногда больше, чем вся ваша логика.
  • Он не видит того, что происходит вне процессора. Если сигнал не доходит до ножки датчика, в программе ничего не видно: вы будете отлаживать корректный код.

Правильная реакция — подобрать инструмент под симптом:

Разберём инструменты по существу.

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 и складывать в невыключаемую память адрес, по которому упали, вести счётчики срабатываний сторожевого таймера. Пять минут работы на этапе проектирования экономят недели на поле, где к устройству не подключиться отладчиком.

Сессия отладки живого устройства выглядит так:

Робототехника: где встраиваемое встречается с большим

Робот — самый наглядный пример системы, где обе половины трека работают вместе. Типовая архитектура двухуровневая: нижний уровень на микроконтроллере закрывает токовые петли моторов, чтение энкодеров и аварийную остановку с периодом в сотни микросекунд; верхний уровень на Linux-машине занимается зрением, планированием маршрута и связью, работая с периодом в десятки миллисекунд. Связывает их шина — CAN, UART или Ethernet, — и именно на этой границе живут самые интересные инженерные компромиссы.

Стандартом верхнего уровня стал ROS 2: узлы-процессы, обмен сообщениями через DDS, готовые пакеты для навигации, SLAM и симуляции. На микроконтроллер спускается micro-ROS, позволяющий контроллеру быть полноценным узлом той же сети. Разбор кинематики, регуляторов и обратной связи — в статье про робототехнику, а датчики и приводы — в статье про датчики и приводы.

Замечу, что робот — это распределённая система в миниатюре: несколько узлов, ненадёжный канал, частичные отказы и необходимость договариваться о состоянии. Многие идеи трека Распределённые системы применимы напрямую, только вместо сетевых таймаутов у вас пропавшие кадры CAN.

На портале эта тема продолжена в разделе НИР — там разобрана инженерия роботизированных систем на уровне архитектуры, а не отдельного контроллера: определение и классификация роботизированных систем, теоретическое обоснование микросервисной архитектуры для распределённых роботизированных систем, программная реализация сервисов и итоговые результаты работы. Полезно прочитать после статей про протоколы и робототехнику: там видно, как из контроллеров и сервисов собирается работающая система целиком.

Типичные ошибки на входе

  1. Отлаживать принтами. Первое же явление, связанное со временем, исчезнет от вашего лога. Ставьте отладчик и логический анализатор с первого дня.
  2. Использовать malloc. Работает на столе, отказывает через три недели непрерывной работы из-за фрагментации.
  3. Забыть volatile на регистре или на переменной, разделяемой с обработчиком прерывания. С -O0 работает, с -Os перестаёт — и это выглядит как «баг компилятора».
  4. Писать длинные обработчики прерываний. ISR должен положить данные в буфер и выйти; вся обработка — в основном цикле или в задаче RTOS.
  5. Считать, что стек не переполнится. Один рекурсивный вызов или буфер на 4 КБ в локальной переменной — и вы молча портите чужие данные. Заполняйте стек шаблоном и проверяйте отметку максимума.
  6. Игнорировать электрику. Отсутствие развязывающего конденсатора, слабая подтяжка I2C, общий провод, проложенный «как получилось» — это баги, которые не чинятся в коде.
  7. Тестировать при комнатной температуре. Кварц уходит, батарея теряет ёмкость, пластик деформируется. Морозильник и фен — законные инструменты тестировщика.
  8. Прошивать без плана отката. Обновление по воздуху без второго слота и проверки подписи однажды превратит парк устройств в кирпичи.
  9. Не бюджетировать энергию до начала разработки. Обнаружить в конце проекта, что вместо двух лет получается два месяца, — самый дорогой способ узнать про ток сна.
  10. Переносить серверные абстракции целиком. Слои, фабрики и полиморфизм на устройстве с 8 КБ ОЗУ съедают ресурс, который был нужен для буферов.

Карта трека

Трек идёт от железа к системе: сначала физика и язык, потом периферия и время, потом связь и энергия, в конце — робототехника и производство.

  1. Железо для программиста — микроконтроллеры и платы, чтение схем и даташитов, питание, подтяжки, что на самом деле происходит на ножке.
  2. C для встраиваемых систем — модель памяти, указатели, volatile, битовые операции, фиксированная точка, жизнь без динамики.
  3. Периферия — GPIO, АЦП и ЦАП, ШИМ, таймеры, захват и сравнение, DMA как способ не тратить такты ядра.
  4. Прерывания и реальное время — векторы и приоритеты, латентность, гонки, атомарность, детерминизм и WCET.
  5. Протоколы связи — UART, I2C, SPI, CAN и 1-Wire: физика, кадры, арбитраж, типичные отказы и их вид на анализаторе.
  6. RTOS — задачи и планировщик, приоритеты и вытеснение, семафоры и очереди, FreeRTOS и Zephyr на практике.
  7. Питание и ограничения — режимы сна, бюджет энергии, выбор источника, экономия Flash и ОЗУ.
  8. IoT-связь — Wi-Fi, BLE, LoRa и MQTT, топология сетей, обновления по воздуху и безопасность устройств.
  9. Датчики и приводы — считывание и шум, фильтрация и калибровка, моторы, драйверы и обратная связь.
  10. Робототехника — кинематика, регуляторы, ROS 2 и micro-ROS, одометрия и замыкание петли управления.
  11. Отладка и производство — SWD и трассировка, логический анализатор, тестирование на железе, прошивка на конвейере и OTA.

Смежное на портале: Операционные системы для драйверов и ввода-вывода, Компьютерные науки для битов и представления данных, Безопасность для подписи прошивок и хранения ключей, Тестирование для дисциплины проверок, Машинное обучение для моделей, которые сегодня уезжают на сам контроллер. Материалы по сетям и производительности живут в отдельных треках портала.

Мини-итог

Встраиваемая разработка отличается от «большой» не размером кода, а тремя жёсткими ограничениями: память конечна и распределяется статически, энергия невосполнима и тратится в основном во сне, время — часть контракта, а не метрика качества. Из них выводится почти всё остальное: запрет динамической аллокации, короткие обработчики прерываний, volatile на каждом регистре, конечные автоматы вместо длинных сценариев и отладка приборами вместо принтов. Выбор платформы сводится к четырём вопросам про детерминизм, связь, вычисления и батарею: STM32 — когда нужно управлять, ESP32 — когда нужно связаться, RP2040 — когда нужно дёшево и нестандартно, Raspberry Pi — когда нужен Linux и зрение.

Практический старт на ближайшую неделю: возьмите любую плату на Cortex-M и отдельный отладчик (не только USB-кабель), мигните светодиодом через регистры без библиотек, затем измерьте осциллографом реальную длительность вашей задержки — расхождение с расчётом и будет вашим первым настоящим уроком. После этого добавьте датчик по I2C и посмотрите обмен логическим анализатором: увидеть транзакцию своими глазами — быстрее любого чтения документации.

Источники

Что дальше

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

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов