Отладка, энергосбережение и структура проекта
В предыдущих статьях вы настроили STM32CubeIDE, разобрались с архитектурой STM32, работой GPIO, прерываниями, таймерами и интерфейсами UART/I2C/SPI. Теперь перейдём к трём темам, которые превращают набор примеров в реальную прошивку: отладка, энергосбережение и структура проекта.
Отладка на STM32: что это и какие инструменты вы используете
Отладка в embedded отличается от отладки программ на ПК тем, что код выполняется на микроконтроллере, а вы управляете этим процессом с компьютера через отладчик.
Обычно в платах Nucleo/Discovery отладчик уже встроен (ST-LINK). Он умеет:
прошивать микроконтроллер
ставить точки останова и выполнять код пошагово
читать/писать память и регистры
показывать состояние периферии в отладчике!Общая схема: как IDE управляет выполнением прошивки на плате через ST-LINK
Полезные официальные источники по инструментам:
STM32CubeIDE (страница продукта ST)
ST-LINK (страница продукта ST)Базовые приёмы отладки в STM32CubeIDE
Breakpoint, Step и Run
Три базовых действия, которые вы будете делать чаще всего:
Breakpoint (точка останова): ставите остановку на строке, чтобы “поймать” момент.
Step Into/Over (пошаговое выполнение): идёте по коду медленно, наблюдая переменные.
Run/Resume: продолжаете выполнение до следующей точки останова.Практический совет: ставьте breakpoint не в бесконечном while (1), а на событии, которое вы ждёте (например, после HAL_UART_RxCpltCallback, или в месте обработки флага от таймера).
Watch, Variables и volatile
Окна Variables и Expressions/Watch показывают значения переменных. В embedded важно помнить:
если переменная меняется в прерывании, она должна быть volatile, иначе отладка и поведение могут “обманывать”
некоторые значения меняются слишком быстро, и вы видите “случайное” значение в момент остановкиСвязь с предыдущими статьями прямая: флаги button_event, tim_event, rx_flag из примеров должны быть volatile именно потому, что их меняет обработчик прерывания.
Просмотр регистров и периферии
Когда “железо не работает”, полезно смотреть не только переменные, но и состояние периферии.
Типичные вещи, которые проверяют:
включено ли тактирование периферии (через RCC)
правильно ли настроен режим GPIO (Input/Output/AF)
пришёл ли флаг события (например, что UART реально получил байт)В проектах STM32 CubeIDE часто помогает просмотр peripheral registers (регистров периферии) в debug-перспективе.
Assert: быстрый способ поймать ошибку конфигурации
В проектах STM32 HAL есть механизм assert, который может остановить программу, если вы передали неверные параметры или нарушили ожидания библиотеки.
Практический смысл:
в режиме отладки assert помогает быстрее понять, что именно пошло не так
в релизной прошивке assert обычно отключают или превращают в логированиеКак отлаживать “зависания”: HardFault и бесконечные циклы
Типичные причины зависаний
Самые частые причины, почему прошивка “повисла”:
обращение по неверному указателю (ошибка адреса)
переполнение стека (слишком большие локальные массивы, глубокая рекурсия)
ожидание события, которое никогда не произойдёт (например, I2C без подтяжек)
блокирующие вызовы с большим таймаутом в неподходящем месте (например, в прерывании)Из прошлой статьи про интерфейсы связи: I2C-ошибки из-за отсутствия подтяжек часто выглядят как таймаут и “зависание” в ожидании.
Что делать на практике
Последовательность действий, которая обычно быстро приводит к причине:
Поставьте breakpoint в начале main() и убедитесь, что старт действительно проходит.
Поставьте breakpoint до и после подозрительной функции (например, HAL_I2C_Mem_Read).
Если зависание внутри HAL-функции, смотрите параметры и “условия работы” периферии: пины, тактирование, скорость, подключение.
Если падает в HardFault, проверьте последний выполненный участок кода и указатели.Логирование: когда отладчик не подходит
Отладчик отлично работает, пока вы можете остановить программу. Но иногда остановка ломает поведение (например, тайминги SPI или обмен по UART). Тогда используют логирование.
Самый простой вариант для новичка:
лог в терминал через UARTЭто удобно тем, что вы уже умеете настраивать UART, и можете печатать события: старт, ошибка, пришёл байт, состояние автомата.
Практические правила логирования:
лог должен быть коротким, чтобы не “забить” канал
не делайте длинный лог из обработчика прерывания, лучше ставьте флаг и печатайте в while(1)Энергосбережение: почему важно и что реально можно сделать
Даже если ваше устройство питается от USB, понимание энергосбережения важно по двум причинам:
батарейные проекты требуют минимального потребления
низкое потребление обычно означает и более правильную архитектуру: меньше пустых задержек, меньше бесполезной работыОсновные источники потребления
Что чаще всего “ест ток” в прошивке:
высокая частота тактирования CPU и шин
постоянно работающие периферийные блоки (UART, SPI, таймеры)
включённые подтяжки и неправильно настроенные GPIO (например, плавающий вход)
внешние устройства, которые вы забыли перевести в sleepLow-power режимы в STM32 на уровне идеи
В STM32 есть режимы, где ядро и часть периферии могут быть остановлены.
В терминах “на пальцах”:
Sleep: CPU спит, периферия может работать, просыпаемся по прерыванию.
Stop: тактирование ядра сильно ограничено или остановлено, потребление заметно ниже, после пробуждения часто нужно восстановить тактирование.
Standby: максимально глубокий сон, состояние в основном теряется, пробуждение похоже на перезапуск.Точные возможности зависят от конкретной серии STM32, но подход к проектированию один: “работаем быстро, спим долго”.
!Сравнение режимов Sleep/Stop/Standby на концептуальном уровне
Что нужно, чтобы прошивка могла спать
Чтобы перейти от “крутится в while(1)” к энергосбережению, обычно делают так:
основная логика построена на событиях (прерывания, флаги, очереди)
в while(1) нет постоянной работы без причины
в периоды простоя вызывается функция сна (часто это инструкция ожидания прерывания)Практическая связка с предыдущими статьями:
кнопка через EXTI даёт событие для пробуждения
таймер может будить раз в N миллисекунд для периодической задачи
UART RX может будить при приёме данных (в зависимости от режима и серии)SysTick и HAL_Delay в low-power
HAL_Delay() обычно опирается на SysTick, который “тикает” раз в миллисекунду. Для энергосбережения это может быть проблемой: CPU постоянно просыпается.
Практический вывод для начинающего:
для “научебных” проектов HAL_Delay() нормален
для low-power лучше уходить от бесконечных задержек в пользу событий (таймер, EXTI, UART RX)Минимальные практики энергосбережения без сложной настройки
Даже без глубоких режимов можно заметно улучшить потребление и качество прошивки:
отключайте ненужные периферии в CubeMX
не оставляйте входы “плавающими” (выберите pull-up/pull-down или Analog)
не гоняйте CPU в пустом цикле, если можно ждать событие
уменьшайте частоту там, где не нужна высокая производительностьДокументация, от которой обычно отталкиваются:
Документация STM32 на сайте ST (страница документации)Структура проекта: как не утонуть в CubeMX и HAL
На старте CubeMX генерирует много кода, и новичку легко смешать “автоматически сгенерированное” и “свою логику” так, что проект быстро становится неудобным.
Цель структуры проекта:
ваши файлы не затираются при Regenerate Code
код легко читать и расширять
периферия и бизнес-логика разделеныКак CubeMX генерирует код и где безопасно писать
В сгенерированных файлах CubeMX оставляет специальные секции для пользовательского кода. Практическое правило:
пишите свой код только в user sections, иначе при перегенерации вы потеряете измененияЕсли вы хотите более чистую архитектуру, лучший путь:
оставить main.c “тонким”
вынести свою логику в отдельные .c/.h файлыРекомендуемая модульная структура для начинающего
Ниже пример простой структуры, которая хорошо масштабируется:
app/ содержит логику приложения (что устройство делает)
bsp/ содержит привязку к плате (Board Support Package): светодиоды, кнопки, питание датчиков
drivers/ содержит драйверы устройств по UART/I2C/SPI (что и как общаться с железом)Пример содержания модулей:
bsp_led.c/.h: bsp_led_init(), bsp_led_set(), bsp_led_toggle()
bsp_button.c/.h: обработка кнопки, антидребезг, выдача событий
drv_eeprom_i2c.c/.h: чтение/запись EEPROM по I2C
app_main.c/.h: состояние приложения, обработка событийСвязь с прошлой статьёй про static/extern:
внутренние переменные модуля делайте static
наружу экспортируйте только функции модуля и минимально нужные типыПаттерн “флаг из прерывания, работа в main” как основа архитектуры
Вы уже применяли этот подход для EXTI и таймера. Он же полезен для UART RX и вообще для событийной прошивки.
Правила:
в обработчиках прерываний делайте минимум (поставить флаг, положить байт в буфер)
в while(1) забирайте флаги и выполняйте “тяжёлую” логикуЭто помогает одновременно:
упростить отладку (логика в одном месте)
улучшить стабильность (прерывания короткие)
подготовить проект к энергосбережению (между событиями можно спать)Пример “тонкого main.c”
Ниже упрощённый пример идеи: main.c только инициализирует HAL, периферию и запускает приложение.
А внутри app_loop() вы делаете:
обработку событий
обмен по интерфейсам
переход в sleep, если делать нечегоИтог
В этой статье вы собрали “инженерный минимум”, который отличает рабочий проект от набора примеров:
понимаете, как устроена отладка через ST-LINK и что проверять при зависаниях
знаете, когда нужен лог через UART, а когда лучше breakpoint
получили концептуальную картину энергосбережения и почему “событийная” прошивка помогает снижать потребление
научились проектировать структуру проекта, чтобы CubeMX не мешал развивать код, а прерывания оставались короткими и безопасными