Программирование микроконтроллеров STM32 для начинающих

Курс знакомит с основами работы микроконтроллеров STM32 и средой разработки, а также учит писать и отлаживать простые программы. На практике вы освоите настройку тактирования, GPIO, прерываний и ключевых периферийных модулей (таймеры, UART, ADC) для создания базовых встраиваемых проектов.

1. Введение в STM32 и инструменты разработки

Введение в STM32 и инструменты разработки

STM32 — это семейство микроконтроллеров компании STMicroelectronics, широко используемое в обучении и в промышленной разработке: от простых устройств с кнопками и светодиодами до датчиков, приводов, аудио, связи и IoT.

В этой статье разберём:

  • что такое STM32 и из чего он «состоит» на базовом уровне
  • какие платы чаще всего используют новички
  • типичный цикл разработки прошивки
  • основные инструменты: STM32CubeIDE, CubeMX, CubeProgrammer, отладчик ST-LINK
  • из каких слоёв обычно состоит код под STM32 (CMSIS, HAL, LL)
  • Что такое микроконтроллер STM32

    Микроконтроллер — это маленький компьютер на одном кристалле. Внутри обычно есть:

  • процессорное ядро (в STM32 чаще всего ARM Cortex-M)
  • память программы (Flash)
  • оперативная память (SRAM)
  • периферия: GPIO, таймеры, UART, SPI, I2C, ADC и другие
  • система тактирования (кварц/генераторы, делители частоты)
  • STM32 — это не одна модель, а большое семейство. Разные серии отличаются производительностью, объёмом памяти, набором периферии и энергопотреблением.

    Чтобы не запутаться в начале, полезно помнить простое правило:

  • «одинаковый» код на STM32 часто можно перенести между моделями, если совпадает периферия и выводы, но настройки тактирования, пинов и периферийных блоков могут отличаться
  • Популярные семейства и платы для старта

    Семейства STM32 (очень кратко)

  • STM32F0/F1 — простые и недорогие, часто встречаются в учебных проектах
  • STM32F3/F4 — заметно мощнее, много периферии и возможностей
  • STM32L0/L4 — акцент на низкое энергопотребление
  • STM32G0/G4 — современные «универсальные» серии, часто удобны как стартовые
  • Платы разработки

    Для новичка важнее не «самый мощный» микроконтроллер, а удобство прошивки и отладки.

  • STM32 Nucleo boards — хороший старт: обычно на плате уже есть встроенный программатор/отладчик ST-LINK
  • STM32 Discovery kits — часто имеют больше периферии на самой плате (экраны, датчики)
  • «Blue Pill» (STM32F103) и похожие совместимые платы — дешёвые, но качество/совместимость могут различаться; часто требуют отдельного ST-LINK
  • Как выглядит цикл разработки прошивки

    Типичный процесс почти всегда один и тот же:

  • Создать проект и настроить микроконтроллер (частота, пины, периферия).
  • Написать код (логика программы).
  • Собрать проект компилятором (получить прошивку).
  • Записать прошивку во Flash.
  • Отладить: поставить точки останова, посмотреть значения переменных, проверить периферию.
  • !Схема показывает, как связаны настройка, код, сборка, прошивка и отладка

    Инструменты разработки STM32

    STM32CubeIDE

    STM32CubeIDE — основной инструмент для начинающих: IDE (среда разработки), которая объединяет в одном месте создание проекта, настройку микроконтроллера, написание кода, сборку, прошивку и отладку.

    Что обычно есть внутри CubeIDE:

  • редактор кода и управление проектом
  • интеграция с конфигуратором STM32CubeMX (через файл проекта)
  • компилятор и инструменты сборки (на базе GCC)
  • отладка через ST-LINK (обычно через GDB)
  • STM32CubeMX

    STM32CubeMX — конфигуратор микроконтроллера и генератор «каркаса» проекта.

    CubeMX помогает:

  • выбрать модель STM32 или плату
  • настроить тактирование
  • назначить функции выводов (пинов)
  • включить периферию (UART/SPI/I2C/ADC и т.д.) и базовые параметры
  • сгенерировать начальный код проекта
  • Важная идея: CubeMX не пишет за вас всю программу — он создаёт основу и инициализацию, а прикладную логику вы добавляете сами.

    STM32CubeProgrammer

    STM32CubeProgrammer — утилита для прошивки микроконтроллера и работы с памятью/опциями.

    Она полезна, когда:

  • нужно прошить устройство без IDE
  • нужно стереть Flash, выставить опции загрузки (boot), защиту чтения и т.д.
  • нужно проверить, виден ли микроконтроллер программатору
  • ST-LINK и интерфейс SWD

    ST-LINK — это программатор и отладчик. На многих платах Nucleo/Discovery он уже встроен, а для отдельных микроконтроллеров его можно купить как отдельное устройство.

    Чаще всего используется интерфейс SWD (Serial Wire Debug):

  • требуется обычно 2 сигнальные линии (SWDIO и SWCLK) плюс питание/земля
  • позволяет и прошивать, и отлаживать (брейкпоинты, просмотр памяти)
  • Пример устройства: STLINK-V3SET.

    Из чего состоит «стек» кода: CMSIS, HAL, LL

    Чтобы писать код под STM32, важно понимать уровни абстракции:

  • CMSIS — базовый стандарт от ARM для Cortex-M: единые заголовки и доступ к регистрам ядра и базовым компонентам
  • HAL (Hardware Abstraction Layer) — библиотека ST с более высокоуровневыми функциями (удобно для старта)
  • LL (Low-Layer) — более низкоуровневые функции ST (ближе к регистрам, больше контроля)
  • !Иллюстрация показывает, какие библиотеки над чем находятся

    В этом курсе обычно удобнее начинать с HAL (быстрее получить результат), а затем постепенно разбираться с LL и с тем, что происходит на уровне регистров.

    Что создаётся в проекте CubeIDE и почему это важно

    Когда вы создаёте проект STM32CubeIDE, вы увидите типичные элементы:

  • файл *.ioc — настройки CubeMX (пины, периферия, тактирование); это «источник правды» для генерации кода
  • папки Core/Src и Core/Inc — ваш основной код на C
  • файлы старта (часто startup_*.s) — старт программы и таблица прерываний
  • линкерный скрипт *.ld — описывает, как размещать код и данные в памяти
  • Практическое правило для новичка:

  • настройки меняем через *.ioc
  • пользовательский код пишем в предназначенных местах (CubeIDE/CubeMX обычно отмечают области комментариями), чтобы генерация не затёрла ваши изменения
  • Установка и типичные проблемы (краткий чек-лист)

  • Установите STM32CubeIDE с официальной страницы.
  • Подключите плату и убедитесь, что она определяется системой.
  • Если используете внешний ST-LINK, проверьте кабель, питание и линии SWD.
  • На Linux иногда нужны правила доступа (udev), иначе программатор будет виден только с правами администратора.
  • Что будет дальше

    В следующих уроках вы:

  • создадите первый проект в STM32CubeIDE
  • настроите GPIO и сделаете классический проект мигания светодиодом
  • научитесь прошивать плату и пользоваться базовой отладкой (точки останова и просмотр переменных)
  • 2. Архитектура Cortex-M и основы языка C для STM32

    Архитектура Cortex-M и основы языка C для STM32

    В прошлой статье мы разобрали, что такое STM32, как выглядит цикл разработки и какие инструменты и слои кода (CMSIS, HAL, LL) вы будете встречать в проектах STM32CubeIDE.

    Теперь сделаем следующий шаг: разберёмся, как устроено ядро ARM Cortex-M (на котором работают большинство STM32) и какие конструкции языка C критически важны именно для микроконтроллеров.

    Зачем новичку понимать Cortex-M и C одновременно

    Даже если вы начинаете с HAL и «готовых» функций, в какой-то момент вы всё равно сталкиваетесь с тем, что нужно:

  • понимать, почему программа стартует не с main()
  • различать Flash и SRAM и знать, что куда попадает
  • понимать, что такое прерывания и почему они «перебивают» ваш код
  • читать заголовки CMSIS и не бояться слов вроде __IO и volatile
  • безопасно работать с регистрами периферии (GPIO, таймеры, UART)
  • Это не «теория ради теории»: это основа для уверенной отладки и понимания, что реально делает микроконтроллер.

    Архитектура Cortex-M: что нужно знать на старте

    Большинство STM32 построены на ядрах ARM Cortex-M (например, Cortex-M0/M0+, M3, M4, M7). Это семейство архитектур, оптимизированных под встраиваемые системы.

  • ARM Cortex-M (описание семейства)
  • Регистры ядра: минимальный набор

    В Cortex-M есть регистры общего назначения и специальные регистры. На практике чаще всего встречаются:

  • R0R12 — регистры общего назначения
  • SP — указатель стека (Stack Pointer)
  • LR — регистр возврата (Link Register)
  • PC — счётчик команд (Program Counter)
  • xPSR — регистр состояния (флаги и служебная информация)
  • Важная идея: процессор почти всё делает через регистры, а память и периферия доступны по адресам.

    Два режима выполнения: обычный код и обработчики

    В Cortex-M принято выделять:

  • Thread mode — обычное выполнение вашей программы (включая main())
  • Handler mode — выполнение обработчиков исключений и прерываний
  • Когда приходит прерывание, ядро автоматически сохраняет часть контекста (важные регистры) в стек и передаёт управление обработчику.

    Стек: почему он важен

    Стек — область памяти (в SRAM), которую ядро использует для:

  • сохранения контекста при вызовах функций
  • хранения локальных переменных (обычно)
  • автоматического сохранения состояния при прерываниях
  • Практическое правило: ошибки со стеком на микроконтроллерах часто проявляются как «странные» зависания, уход в HardFault или перезапуски.

    Память STM32 и отображение периферии в адресное пространство

    STM32 (как и другие Cortex-M микроконтроллеры) используют memory-mapped I/O: регистры периферии выглядят для программы как обычные адреса памяти.

    Обычно в проекте вы постоянно имеете дело с такими областями:

  • Flash — где лежит код программы и константы
  • SRAM — где лежат переменные, стек и куча (если используется)
  • Периферия — регистры GPIO, таймеров, UART и т.д.
  • !Упрощённая карта памяти: где живёт код, данные и регистры периферии

    Что такое «регистр периферии» в терминах C

    Упрощённо, регистр — это число фиксированной ширины (часто 32 бита), лежащее по фиксированному адресу. В C это выглядит как «указатель на uint32_t по адресу X».

    Ключевая идея: чтение и запись по такому адресу имеет побочный эффект (например, реально включает тактирование, меняет уровень на пине, сбрасывает флаг).

    Прерывания: что происходит, когда «что-то случилось»

    Прерывание — механизм, который позволяет периферии сообщить ядру о событии (пришли данные по UART, сработал таймер, изменился вход GPIO).

    Таблица векторов

    В начале прошивки (по фиксированному адресу в памяти) находится таблица векторов:

  • первый элемент — начальное значение SP
  • дальше — адреса обработчиков (Reset_Handler, NMI_Handler, HardFault_Handler, обработчики периферии)
  • Именно поэтому программа стартует не с main(): сначала вызывается Reset_Handler, который готовит память и окружение, и только потом вызывает main().

    NVIC и приоритеты

    NVIC (Nested Vectored Interrupt Controller) — контроллер прерываний в Cortex-M. Он отвечает за:

  • включение и выключение прерываний
  • приоритеты
  • вложенность (одно прерывание может прервать другое, если приоритет выше)
  • !Как прерывание временно прерывает основной код и возвращает управление назад

    Как реально стартует проект STM32 (то, что генерирует CubeIDE)

    Когда вы создаёте проект в STM32CubeIDE, обычно присутствуют компоненты, обеспечивающие старт:

  • Файл старта startup_*.s с таблицей векторов и Reset_Handler.
  • Инициализация памяти:
  • - копирование секции .data из Flash в SRAM - обнуление секции .bss в SRAM
  • Вызов системной инициализации (часто SystemInit()), где настраиваются базовые вещи (например, тактирование).
  • Переход в main().
  • Это важно понимать при отладке: если устройство «не доходит до main()», проблема может быть в тактировании, памяти, таблице векторов, HardFault или неправильной прошивке.

    Язык C для STM32: минимум, без которого сложно

    Ниже — темы C, которые чаще всего нужны именно в прошивках.

    Фиксированные целочисленные типы

    В микроконтроллерах важна точная разрядность. Поэтому используют типы из stdint.h:

    | Тип | Размер | Где встречается | |---|---:|---| | uint8_t | 8 бит | байтовые буферы, данные датчиков | | uint16_t | 16 бит | значения АЦП, регистры 16 бит | | uint32_t | 32 бита | регистры периферии, счётчики, маски | | int32_t | 32 бита | вычисления со знаком |

  • Целочисленные типы в C (stdint.h)
  • Практическое правило: для регистров и битовых масок чаще всего подходят uint32_t и uint16_t.

    Указатели и адреса

    Указатель — это переменная, которая хранит адрес. На STM32 это базовый инструмент, потому что периферия доступна по адресам.

    Что важно уметь:

  • оператор & берёт адрес переменной
  • оператор * разыменовывает указатель (даёт доступ к значению по адресу)
  • приведение типов указателей используется, чтобы трактовать адрес как «регистр нужного типа»
  • Пример идеи (без привязки к конкретному STM32):

    Здесь важно слово volatile, о нём дальше.

    volatile: почему без него ломается работа с периферией

    volatile говорит компилятору: это значение может измениться «само», не только из текущего потока кода.

    В прошивках это относится к:

  • регистрам периферии
  • переменным, которые меняются в прерываниях
  • флагам, которые выставляются DMA или аппаратурой
  • Без volatile компилятор имеет право оптимизировать чтения и записи так, что:

  • чтение регистра может «закэшироваться» в регистре процессора
  • цикл ожидания флага может превратиться в бесконечный цикл
  • Ключевое слово volatile в C
  • Битовые операции: основа управления регистрами

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

  • & (AND) — оставить только нужные биты
  • | (OR) — установить биты
  • ~ (NOT) — инвертировать
  • << и >> — сдвиги для формирования масок
  • Типовые шаблоны:

    В реальных проектах вы часто увидите маски и сдвиги в заголовках CMSIS и HAL.

    struct и «карты регистров»

    Чтобы было удобно обращаться к множеству регистров одного периферийного блока, в CMSIS обычно используются структуры.

    Идея:

  • по базовому адресу периферии лежит набор регистров
  • структура описывает их порядок
  • дальше код обращается как PERIPH->REG
  • Смысл для новичка: когда вы видите что-то вроде GPIOA->ODR, это не «магия», а доступ по адресу к полю структуры.

  • CMSIS Version 5 (репозиторий)
  • .c и .h: как организован код прошивки

    STM32-проект почти всегда разбит на модули.

  • .h содержит объявления (что доступно снаружи)
  • .c содержит определения (реализацию)
  • Также важно различать:

  • static у функции или глобальной переменной в .c делает её видимой только внутри этого файла
  • extern в .h объявляет глобальную переменную, определённую в другом .c
  • Практическая цель: не смешивать всю логику в одном main.c, а постепенно учиться выделять драйверы и модули.

    Прерывания в C: обработчик как обычная функция, но с правилами

    Обработчик прерывания (ISR) выглядит как функция с фиксированным именем, которое прописано в таблице векторов.

    На STM32 в проектах CubeIDE часто используются два подхода:

  • вы пишете обработчик напрямую (например, EXTI0_IRQHandler)
  • вы используете HAL, где обработчик вызывает HAL-обработчик, а пользовательская логика размещается в callback-функции
  • Мини-пример с флагом, который выставляется в callback (идея):

    Почему button_pressed здесь volatile: переменная меняется не только в main(), но и из обработчика (в другом контексте выполнения).

    Как это связано с HAL, LL и регистрами

    Из прошлой статьи вы уже знаете про уровни библиотек. Теперь добавим к этому «модель в голове»:

  • HAL вызывает функции, которые в итоге пишут в регистры периферии
  • LL делает то же самое, но с меньшей абстракцией
  • CMSIS даёт базовые определения ядра, регистров и структур периферии
  • Практическое правило обучения:

  • начинайте с HAL, чтобы быстрее получить результат
  • параллельно учитесь читать, какие регистры затрагиваются (это упростит отладку и понимание)
  • Что дальше по курсу

    Дальше вы начнёте практику:

  • создадите проект в STM32CubeIDE
  • настроите GPIO в *.ioc
  • сделаете мигание светодиодом
  • закрепите понимание main(), бесконечного цикла и базовой отладки
  • 3. GPIO, тактирование и работа с прерываниями

    GPIO, тактирование и работа с прерываниями

    В прошлых статьях вы узнали, что такое STM32, как выглядит типичный цикл разработки в STM32CubeIDE и почему важно понимать базовые идеи Cortex-M, памяти и прерываний.

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

  • что такое GPIO и как настраиваются пины
  • почему тактирование (clocks) влияет на работу периферии и почему без него «ничего не работает»
  • как устроены прерывания на практике: EXTI, NVIC, обработчики и callback-и HAL
  • GPIO на STM32: что это и почему это основа

    GPIO (General-Purpose Input/Output) — универсальные цифровые пины микроконтроллера. Один и тот же физический вывод (pin) можно настроить как:

  • вход (читать 0/1)
  • выход (выдавать 0/1)
  • альтернативную функцию (например, UART/SPI/I2C/таймер)
  • аналоговый режим (например, для АЦП)
  • !Иллюстрация показывает, какие основные настройки есть у одного пина GPIO и как они связаны с внешними компонентами

    Основные параметры GPIO, которые вы настраиваете

    Когда вы настраиваете пин в CubeMX или вручную через HAL/регистры, вы почти всегда выбираете такие свойства.

  • Mode (режим)
  • Pull (подтяжка)
  • Speed (скорость переключения выхода)
  • Output type (тип выхода)
  • Alternate function (альтернативная функция)
  • #### Режимы

  • Input: пин читает логический уровень с внешней цепи.
  • Output: пин управляется микроконтроллером (0 или 1).
  • Alternate Function: пин «отдан» периферии (например, TX/RX UART).
  • Analog: цифровая часть обычно отключается (важно для АЦП и снижения потребления).
  • #### Pull-up / Pull-down

    Подтяжка нужна, чтобы вход не «висел в воздухе».

  • No pull: ничего не подтягиваем, уровень должен задаваться внешней схемой.
  • Pull-up: внутренняя подтяжка к 1.
  • Pull-down: внутренняя подтяжка к 0.
  • Практическое правило: если у кнопки нет внешнего резистора, часто включают внутреннюю подтяжку (обычно pull-up) и подключают кнопку на землю.

    #### Тип выхода: push-pull и open-drain

  • Push-pull: микроконтроллер активно выдаёт и 0, и 1. Подходит для светодиодов и большинства обычных цифровых сигналов.
  • Open-drain: микроконтроллер умеет активно тянуть только в 0, а 1 получается через подтяжку. Это типично для шин вроде I2C.
  • #### Скорость (Speed)

    Speed влияет на фронты сигнала и помехи. Для светодиода обычно не требуется высокая скорость.

    Практическое правило: если нет причины — не ставьте максимальную скорость на выходе.

    Тактирование: почему без него GPIO и периферия могут не работать

    В STM32 почти любая периферия (включая GPIO-порты) требует:

  • системного тактирования (чтобы ядро выполняло код)
  • тактирования шины/домена, к которому подключён блок периферии
  • часто отдельного включения тактирования конкретного блока
  • На уровне пользователя это чаще всего проявляется так:

  • вы настроили пин как выход
  • пишете в него 1/0
  • а на ножке ничего не меняется
  • Одна из самых частых причин: не включено тактирование GPIO-порта в RCC.

    Что такое RCC и «включение тактирования периферии»

    RCC (Reset and Clock Control) — блок, который управляет:

  • источниками частоты (HSI/HSE/PLL)
  • делителями шин (AHB/APB)
  • сбросом и подачей тактирования на периферийные блоки
  • Если у блока периферии выключен clock enable, регистры могут быть недоступны или запись в них не будет давать эффекта.

    В проектах с HAL часто используется идея:

  • перед работой с портом вызвать макрос включения тактирования порта
  • Пример (типовая практика HAL):

    CubeMX обычно генерирует такие включения автоматически, но понимать смысл важно для отладки.

    SystemClock и периферия: почему частота важна не только для скорости

    В STM32CubeIDE/CubeMX вы обычно настраиваете тактирование в разделе Clock Configuration, а затем получаете сгенерированную функцию SystemClock_Config().

    От частоты зависят:

  • скорость выполнения кода
  • скорость таймеров
  • параметры UART (baud rate)
  • период SysTick (часто используется HAL для задержек и таймаутов)
  • Практическое правило: если вы поменяли тактирование, а потом «поехали» UART-скорости или тайминги — причина часто в неверной конфигурации Clock Tree.

    Ссылки для контекста:

  • STM32CubeIDE
  • STM32CubeMX
  • Практика GPIO с HAL: выход (LED) и вход (кнопка)

    Ниже — типовые фрагменты кода, которые соответствуют тому, что CubeMX генерирует в gpio.c, а вы используете в main.c.

    LED: настройка выхода и переключение

    Идея:

  • пин LED настраивается как output push-pull
  • в цикле делаем toggle
  • Что важно понимать:

  • MX_GPIO_Init() обычно включает тактирование портов и настраивает режимы пинов
  • HAL_Delay() обычно завязан на SysTick, а SysTick завязан на системную частоту
  • Кнопка: настройка входа с подтяжкой

    Типовой вариант для кнопки:

  • пин как input
  • pull-up включён
  • кнопка замыкает вход на GND
  • Чтение состояния:

    Практическая деталь: «нажато» может быть как RESET, так и SET — зависит от подключения и подтяжки.

    Прерывания: как реагировать на события без постоянного опроса

    Прерывание позволяет микроконтроллеру отвлечься от основного цикла и быстро выполнить обработчик события: таймер, приход байта по UART, изменение уровня на входе.

    В контексте GPIO новичок чаще всего встречает прерывания через EXTI:

  • входной пин генерирует событие по фронту (rising/falling)
  • событие вызывает IRQ в NVIC
  • выполняется обработчик
  • Для общей теории прерываний и NVIC полезны:

  • ARM Cortex-M
  • Nested Vectored Interrupt Controller
  • !Схема показывает полный путь GPIO-прерывания в типовом проекте HAL и связь ISR с основным циклом

    NVIC: включение и приоритет

    Чтобы прерывание заработало, обычно нужно:

  • настроить источник события (EXTI на нужный пин и нужный фронт)
  • разрешить IRQ в NVIC
  • задать приоритет (по необходимости)
  • CubeMX делает это настройками GPIO и вкладкой NVIC.

    Практическое правило: если обработчик не вызывается, проверяйте по цепочке:

  • правильный ли пин и линия EXTI
  • выбран ли правильный фронт
  • включён ли IRQ в NVIC
  • не забыта ли инициализация GPIO/EXTI
  • HAL-подход: ISR вызывает HAL, а вы пишете callback

    В проектах HAL часто действует схема:

  • в файле stm32xx_it.c есть обработчик, например EXTI15_10_IRQHandler()
  • внутри он вызывает HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_x)
  • HAL вызывает ваш пользовательский callback HAL_GPIO_EXTI_Callback()
  • Пример типовой пользовательской логики: в прерывании выставить флаг, а обработать событие в while(1).

    Почему volatile важно:

  • переменная меняется в контексте прерывания
  • без volatile компилятор может оптимизировать чтение в while(1) так, что изменения «не будут замечены»
  • Справка по volatile:

  • volatile type qualifier
  • Что нельзя делать в прерывании (практические ограничения)

    Обработчик прерывания должен быть коротким и предсказуемым.

  • не делайте долгие задержки
  • не выполняйте тяжёлые вычисления
  • не вызывайте код, который может надолго заблокироваться
  • Практическая стратегия: в прерывании зафиксировать факт события (флаг, счётчик, запись в буфер), а основную обработку делать в основном цикле.

    Дребезг кнопки: почему одно нажатие даёт много прерываний

    Механическая кнопка часто даёт дребезг — серию быстрых переключений.

    Что можно сделать на старте:

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

    Как связать GPIO, тактирование и прерывания в одном проекте

    Типовой «первый серьёзный» проект после мигания:

  • LED на выходе
  • кнопка на входе с EXTI
  • по нажатию кнопки переключать LED
  • Ключевые связи:

  • без правильного SystemClock_Config() могут «поплыть» задержки и тайминги
  • без включённого тактирования портов GPIO настройки пинов не применятся
  • без NVIC и правильной EXTI-конфигурации callback не будет вызываться
  • Частые проблемы и быстрые проверки

  • Пин не переключается: проверьте, что это именно тот порт/пин, и включено тактирование порта.
  • Кнопка всегда 0 или всегда 1: проверьте подтяжку (pull-up/pull-down) и как кнопка подключена.
  • Прерывание не приходит: проверьте фронт, линию EXTI и включение IRQ в NVIC.
  • Событие «теряется»: убедитесь, что флаг volatile, и что вы не делаете долгую обработку в ISR.
  • Что дальше

    После этой статьи у вас есть базовый набор для реальной работы с платой:

  • управление GPIO
  • понимание, зачем нужно тактирование и где оно включается
  • базовая событийная модель через прерывания
  • Следующий логичный шаг в практике — закрепить это на проекте и затем перейти к таймерам (как к более надёжному источнику периодических событий, чем HAL_Delay()).

    4. Периферия: таймеры, UART, I2C, SPI

    Периферия: таймеры, UART, I2C, SPI

    В предыдущих статьях вы настроили среду, разобрались с базовой моделью Cortex-M и сделали первые проекты с GPIO, тактированием и прерываниями. Теперь пора перейти к периферии, которая встречается почти в любом устройстве:

  • таймеры как базовый источник времени, событий и ШИМ
  • UART как простой способ общения с ПК и модулями
  • I2C как типичная шина датчиков и памяти
  • SPI как быстрый интерфейс к дисплеям, флеш-памяти и АЦП
  • Цель статьи: дать вам понятную модель в голове и практический минимум, чтобы вы могли включить нужную периферию в CubeMX, собрать проект и уверенно отлаживать типовые проблемы.

    !Общая карта того, как типовые интерфейсы подключаются к внешним устройствам

    Общие принципы работы с периферией STM32

    Почти любая периферия на STM32 включается и работает по одной логике.

  • Нужно включить тактирование периферийного блока в RCC.
  • Нужно настроить пины в нужный режим, чаще всего Alternate Function.
  • Нужно настроить параметры блока (частоты, режимы, прерывания, DMA).
  • Нужно запустить периферию и обрабатывать события.
  • В проектах на HAL обычно это выглядит так.

  • HAL_Init()
  • SystemClock_Config()
  • MX_GPIO_Init()
  • MX_TIMx_Init() или MX_USARTx_UART_Init() или MX_I2Cx_Init() или MX_SPIx_Init()
  • Дальше основной цикл while(1) и, при необходимости, callbacks из прерываний
  • Документация, к которой вы будете возвращаться постоянно.

  • STM32CubeIDE
  • STM32CubeMX
  • HAL API Reference (Wiki ST)
  • Таймеры

    Таймер в STM32 это аппаратный счётчик, который увеличивается с заданной частотой и умеет генерировать события.

    Зачем нужны таймеры

  • Периодические события без HAL_Delay().
  • ШИМ (PWM) для управления яркостью LED, сервоприводами, питанием.
  • Измерение длительности импульсов и частоты (Input Capture).
  • Подсчёт внешних импульсов (Encoder, External Clock).
  • Главная идея: таймер работает в железе и даёт более стабильное время, чем задержки в цикле.

    Типовые режимы

    | Режим таймера | Что делает | Типовое применение | |---|---|---| | Base timer + Interrupt | генерирует прерывание раз в период | системный тик, периодические задачи | | PWM (Output Compare PWM) | выдаёт ШИМ на пин | LED, моторы, серво | | Input Capture | фиксирует время фронта | измерение частоты, длительности |

    Как получается период

    Практически в CubeMX вы настраиваете:

  • входную частоту таймера
  • предделитель (prescaler)
  • период (auto-reload)
  • Смысл простой.

  • Предделитель делает счётчик медленнее.
  • Период задаёт, через сколько тиков случится событие обновления.
  • Если вы пока не уверены в числах, используйте подход новичка.

  • В CubeMX откройте конфигурацию таймера.
  • Выставьте Clock Source.
  • Подбирайте Prescaler и Period так, чтобы CubeMX показывал нужную частоту события.
  • Таймер с прерыванием на период

    Что делаете в CubeMX.

  • Включаете TIMx.
  • Включаете TIMx global interrupt в NVIC.
  • Настраиваете Prescaler и Period.
  • Как запускаете в коде.

    Куда писать пользовательскую логику.

    Почему это лучше, чем HAL_Delay().

  • основной цикл остаётся свободным для логики
  • период формируется аппаратно
  • проще масштабировать на несколько периодических задач
  • PWM для управления яркостью светодиода

    PWM (Pulse Width Modulation) это сигнал, у которого фиксированный период, но меняется длительность состояния 1.

  • период PWM определяет частоту мерцания
  • скважность определяет среднюю мощность на нагрузке и, для LED, видимую яркость
  • Что делаете в CubeMX.

  • Настраиваете TIMx Channel y в режиме PWM.
  • Настраиваете соответствующий пин как Alternate Function.
  • Как запускаете.

    Как менять заполнение.

    Типовая ошибка новичка.

  • PWM запущен, но на пине тишина: пин не в Alternate Function или выбран не тот канал.
  • UART

    UART это асинхронный последовательный интерфейс.

  • две основные линии: TX и RX
  • синхронизации общей линией тактирования нет
  • договорённость по скорости и формату кадра обязательна
  • Что такое baud rate и формат кадра

    Чтобы два устройства понимали друг друга, они должны совпасть по настройкам.

  • скорость (baud rate), например 115200
  • количество бит данных, обычно 8
  • чётность (parity), часто None
  • стоп-биты, часто 1
  • Классическая настройка: 115200 8N1.

    Самый частый сценарий

  • STM32 отправляет отладочные сообщения в терминал на ПК
  • ПК отправляет команды (например, включить LED)
  • Для подключения к ПК обычно используют:

  • встроенный виртуальный COM-порт на платах Nucleo/Discovery
  • внешний USB-UART адаптер
  • UART в HAL: blocking, interrupt, DMA

    | Способ | Как работает | Когда использовать | |---|---|---| | Blocking (HAL_UART_Transmit) | функция ждёт окончания передачи | простая отладка, редкая передача | | Interrupt (HAL_UART_Transmit_IT) | передача в фоне, завершение через callback | когда нельзя блокировать цикл | | DMA (HAL_UART_Transmit_DMA) | передачу делает DMA | большие объёмы, стабильные потоки |

    На старте достаточно blocking, но важно помнить: blocking это пауза в выполнении кода.

    Пример передачи строки

    Приём данных и типичная модель

    Практичная модель для новичка.

  • принимать байты в прерывании
  • складывать в буфер
  • обрабатывать в основном цикле
  • В HAL часто используют HAL_UART_Receive_IT() и callback HAL_UART_RxCpltCallback().

    Документация по UART на уровне концепций.

  • Universal asynchronous receiver-transmitter
  • I2C

    I2C это двухпроводная шина для подключения нескольких устройств.

  • SCL: тактовая линия
  • SDA: линия данных
  • Почему на I2C нужны подтяжки

    Линии I2C обычно работают как open-drain.

  • устройство может активно тянуть линию в 0
  • чтобы получить 1, нужна подтяжка к питанию
  • Поэтому на SDA и SCL почти всегда нужны резисторы подтяжки (часто они уже есть на модуле датчика или на плате).

    Адреса на шине

    У каждого ведомого устройства есть адрес.

  • мастер (обычно STM32) инициирует обмен
  • выбирает ведомого по адресу
  • дальше читает или пишет данные
  • В HAL вы часто увидите адрес в виде 7-битного значения, сдвинутого влево на 1. Это частый источник путаницы.

    Практическое правило.

  • внимательно смотрите, что ожидает конкретная функция HAL и что написано в примерах для вашего датчика
  • Скорости I2C

    Обычно встречаются.

  • Standard Mode: 100 kHz
  • Fast Mode: 400 kHz
  • Типовые операции

  • запись в регистр устройства: адрес устройства + адрес регистра + данные
  • чтение регистра: часто сначала записать адрес регистра, затем прочитать данные
  • HAL даёт готовые функции.

    Для многих датчиков удобны функции работы с памятью.

    Типовые проблемы I2C

  • шина зависла: одна из линий удерживается в 0
  • нет ACK: неправильный адрес, нет подтяжек, устройство не запитано
  • ошибки скорости: длинные провода, слабые подтяжки, слишком высокая частота
  • Документация по шине на уровне концепций.

  • I²C
  • SPI

    SPI это синхронный интерфейс, обычно быстрее и проще по протоколу, чем I2C, но требует больше проводов.

    Типовые линии.

  • SCK: тактирование
  • MOSI: мастер отправляет ведомому
  • MISO: ведомый отправляет мастеру
  • NSS или CS: выбор конкретного ведомого
  • Полный дуплекс и роль CS

    SPI часто полный дуплекс: при каждом такте что-то отправляется и что-то принимается.

    CS важен почти всегда.

  • несколько устройств могут сидеть на общих SCK/MOSI/MISO
  • CS выбирает, кто именно активен
  • CS может быть.

  • аппаратным (NSS)
  • программным (обычный GPIO), что часто проще для новичка
  • Режимы SPI и параметры совместимости

    Чаще всего проблемы в том, что не совпали параметры.

  • CPOL: уровень SCK в простое
  • CPHA: на каком фронте считываются данные
  • порядок бит: MSB first или LSB first
  • В документации устройств это обычно записано как SPI mode 0..3.

    Пример обмена в HAL

    Типовые проблемы SPI

  • неверный CPOL/CPHA: данные выглядят как мусор
  • забыли управлять CS: устройство не отвечает
  • перепутаны MOSI/MISO
  • слишком высокая частота SCK для конкретного устройства или разводки
  • Документация по SPI на уровне концепций.

  • Serial Peripheral Interface
  • Практическая стратегия выбора интерфейса

    | Задача | Частый выбор | Почему | |---|---|---| | отладочный вывод в терминал | UART | простой, понятный, широко поддержан | | много датчиков на 2 проводах | I2C | адресация устройств на одной шине | | дисплей, быстрая память, быстрый АЦП | SPI | высокая скорость и простой протокол | | периодические задачи и ШИМ | таймеры | точное время и аппаратная генерация сигналов |

    Как это связывается с предыдущими темами

  • GPIO нужен для пинов UART/I2C/SPI в режиме Alternate Function и для CS у SPI.
  • Тактирование важно для расчёта скоростей UART и шин I2C/SPI, а также для частоты таймеров.
  • Прерывания применяются, чтобы обслуживать таймеры и обмен данными без блокировок.
  • volatile и аккуратная работа с флагами важны, если вы сигнализируете из callback-ов в основной цикл.
  • Мини-чек-лист отладки периферии

    Если периферия не работает, проверяйте по цепочке.

  • Включено ли тактирование блока и правильно ли настроен SystemClock_Config().
  • Правильные ли пины и правильная ли Alternate Function.
  • Совпадают ли настройки протокола с другой стороной.
  • Есть ли физические условия: земля общая, питание есть, подтяжки для I2C есть.
  • Не блокируете ли вы приложение долгими вызовами blocking-функций.
  • Что дальше

    Следующий шаг практики обычно такой.

  • Таймер: сделать периодическую задачу без HAL_Delay().
  • UART: сделать простой протокол команд из терминала.
  • I2C или SPI: подключить реальный датчик или память и прочитать идентификатор.
  • Эти упражнения закрепляют ключевую идею: настройка периферии это связка RCC + GPIO + параметры блока + способ обслуживания (polling, interrupt, DMA).

    5. Отладка, ADC и мини-проекты начинающего

    Отладка, ADC и мини-проекты начинающего

    После тем про GPIO, тактирование, прерывания и базовую периферию (таймеры, UART, I2C, SPI) у вас уже есть всё, чтобы собирать рабочие прошивки. Но на практике новички чаще всего «застревают» на двух вещах:

  • отладка: как быстро понять, почему прошивка ведёт себя не так
  • ADC (аналогово-цифровой преобразователь): как подключать потенциометр/датчики и получать адекватные числа
  • В этой статье вы соберёте понятную схему действий для отладки в STM32CubeIDE, освоите базовый ADC на HAL (polling, interrupt, DMA на уровне идеи) и получите набор мини-проектов для закрепления.

    Отладка STM32 на практике

    Отладка на STM32 чаще всего делается через ST-LINK и интерфейс SWD. Это даёт вам возможность:

  • ставить точки останова (breakpoints)
  • выполнять код пошагово
  • смотреть значения переменных и регистров
  • видеть стек вызовов (call stack)
  • !Общая картина, как IDE, ST-LINK и микроконтроллер связаны при отладке

    Минимальный набор инструментов в STM32CubeIDE

    В режиме Debug вы чаще всего используете:

  • Breakpoints: остановить выполнение на строке
  • Step Into / Step Over / Step Return: пошаговое выполнение
  • Variables / Expressions: просмотр переменных
  • Watch: наблюдение за выражениями
  • Registers: регистры ядра Cortex-M
  • Memory: просмотр памяти по адресу
  • Ссылка на среду разработки:

  • STM32CubeIDE
  • Как правильно ставить точки останова

    Типовые места для первых брейкпоинтов:

  • начало main()
  • сразу после HAL_Init()
  • сразу после SystemClock_Config()
  • сразу после MX_GPIO_Init() и MX_USARTx_UART_Init()
  • Если программа не доходит до main(), это обычно означает проблему до пользовательского кода:

  • некорректное тактирование в SystemClock_Config()
  • повреждённый стартовый код/проект
  • уход в HardFault
  • Отладка «зависаний»

    Симптом: прошивка «висит», светодиод не мигает, UART молчит.

    Практичный алгоритм:

  • Поставьте breakpoint в начале main() и убедитесь, что выполнение туда дошло.
  • Если дошло, сделайте несколько Step Over и посмотрите, на каком вызове «застряло».
  • Если «застряло» внутри ожидания флага (например, передачи UART), проверьте тактирование и настройки периферии.
  • Если «застряло» в HAL_Delay(), проверьте, что SysTick работает и системная частота настроена корректно.
  • Частая причина «вечного ожидания» в прошивках: ожидание флага в цикле без таймаута или с таймаутом, который никогда не уменьшается из-за проблем со временем.

    Отладка прерываний

    Когда в проекте есть прерывания (EXTI, таймер, UART), важно помнить:

  • код в обработчике прерывания выполняется в другом контексте
  • переменные, которые меняются в прерывании и читаются в while(1), должны быть volatile
  • Что делать в отладчике:

  • поставить breakpoint внутри callback, например HAL_GPIO_EXTI_Callback()
  • проверить, приходит ли управление в callback
  • проверить, какой GPIO_Pin пришёл
  • Что такое HardFault и как его быстро диагностировать

    HardFault — это исключение, которое часто возникает при:

  • обращении по неверному адресу
  • выходе за границы массива (порча памяти)
  • переполнении стека
  • вызове функции через испорченный указатель
  • Быстрая практическая стратегия:

  • поставьте breakpoint в HardFault_Handler()
  • когда отладчик остановится там, откройте Call Stack
  • посмотрите, какая функция была последней перед падением
  • Если стек вызовов выглядит странно, это часто признак порчи памяти или переполнения стека.

    Логи через UART как инструмент отладки

    Иногда удобнее не шагать по коду, а печатать ключевые состояния.

    Варианты:

  • отправлять строки через HAL_UART_Transmit()
  • сделать простую функцию логирования log_write("...")
  • Важно: частые блокирующие логи через UART могут сильно замедлить программу и «сломать» тайминги. Для начала это нормально, но учитывайте эффект.

    Общий справочник по HAL:

  • STM32Cube HAL
  • ADC: как STM32 превращает напряжение в число

    ADC (Analog-to-Digital Converter) измеряет входное напряжение и выдаёт цифровой код.

    Примеры источников аналогового сигнала для новичка:

  • потенциометр
  • фоторезистор (через делитель напряжения)
  • аналоговый выход датчика
  • !Интуитивная картинка, почему на выходе ADC получается дискретное число

    Разрешение и диапазон

    ADC имеет разрешение бит, например 12 бит. Это означает количество возможных кодов:

  • минимум 0
  • максимум
  • Связь кода и напряжения обычно описывают формулой:

    Где:

  • — измеряемое напряжение на входе ADC
  • — число, которое вернул ADC
  • — разрядность ADC в битах (например, 12)
  • — опорное напряжение (обычно связано с питанием аналоговой части микроконтроллера)
  • Практический смысл: вы читаете ADC_code, а затем, если нужно, пересчитываете его в вольты.

    Почему важны Vref и питание

    Ошибки новичка, которые дают «странные» значения:

  • вход ADC выходит за допустимый диапазон относительно земли и питания
  • отсутствует общая земля между платой и датчиком
  • нестабильное питание, из-за чего плавает
  • Всегда проверяйте:

  • общий GND
  • правильный диапазон напряжений для входа
  • Время выборки и источник сигнала

    ADC внутри «снимает» значение через схему sample-and-hold и затем преобразует. Если источник сигнала высокоомный (например, делитель с большими сопротивлениями), иногда нужно увеличить sampling time, иначе измерение будет неточным.

    Для новичка правило простое:

  • если видите нестабильные результаты, попробуйте увеличить sampling time в настройках ADC
  • Настройка ADC в CubeMX и базовое чтение на HAL

    Общий порядок включения ADC такой же, как и у другой периферии:

  • настройка тактирования и GPIO (аналоговый режим для пина)
  • настройка самого ADC
  • запуск и чтение
  • Polling: самый простой способ

    Polling означает: вы запускаете преобразование и ждёте окончания.

    Типовой пример для одного канала (идея, конкретные имена hadc1 зависят от проекта):

    Что важно понимать:

  • HAL_ADC_PollForConversion() блокирует выполнение, пока не придёт результат или таймаут
  • для редких измерений (например, раз в 100 мс) это нормально
  • Пример: читаем ADC и отправляем значение по UART

    Ниже пример основной логики: измеряем, переводим в строку и отправляем.

    Практические замечания:

  • snprintf() удобен, но может быть тяжёлым для маленьких микроконтроллеров и часто увеличивает размер прошивки
  • если нужна максимальная лёгкость, можно отправлять сырые байты или писать простую конвертацию числа в строку
  • Interrupt и DMA: что это даёт на уровне идеи

    Когда измерений много, polling начинает мешать.

  • Interrupt (IT): ADC завершил преобразование и вызвал callback, а основной цикл не блокировался
  • DMA: ADC может автоматически складывать серию измерений в буфер в памяти
  • Типовые случаи:

  • один канал, редко: polling
  • несколько каналов, периодически: DMA + буфер
  • нужно реагировать «по событию»: interrupt
  • Мини-проекты начинающего

    Ниже набор небольших проектов, которые используют темы курса: GPIO, прерывания, таймеры, UART и ADC. Идея в том, чтобы каждый проект был коротким, но давал ощущение законченности.

    Проект: регулятор яркости LED от потенциометра

    Состав:

  • ADC читает потенциометр
  • таймер выдаёт PWM на LED
  • Логика:

  • считывайте ADC_code
  • масштабируйте его в диапазон 0..Period таймера
  • записывайте в CCR через __HAL_TIM_SET_COMPARE()
  • Это закрепляет: ADC, PWM, базовую математику масштаба и отладку по UART.

    Проект: «команды по UART» + измерение ADC

    Состав:

  • UART принимает простые команды (например, read, stream on, stream off)
  • ADC выдаёт значение
  • Логика:

  • по read отправлять одно измерение
  • по stream on отправлять измерение раз в 100 мс
  • Это закрепляет: UART, буферы, обработку событий и аккуратную архитектуру while(1).

    Проект: кнопка по EXTI переключает режим измерений

    Состав:

  • кнопка на EXTI
  • несколько режимов: вывод сырого кода ADC, вывод напряжения, управление PWM
  • Логика:

  • в callback EXTI выставлять флаг
  • в основном цикле менять режим
  • Это закрепляет: прерывания, volatile флаги, антидребезг на базовом уровне.

    Проект: простая «шкала» на LED

    Состав:

  • 4–8 светодиодов (или один RGB)
  • ADC читает сигнал
  • Логика:

  • разбейте диапазон ADC_code на зоны
  • включайте соответствующие LED
  • Это закрепляет: работу с диапазонами и обработку аналогового сигнала.

    Чек-лист типовых проблем ADC и отладки

  • ADC всегда 0: пин не в аналоговом режиме или выбран не тот канал
  • ADC «шумит»: увеличьте sampling time, усредняйте несколько измерений, проверьте проводку и землю
  • Значения «не сходятся с вольтметром»: проверьте и схему делителя
  • Прошивка падает: ловите HardFault_Handler(), проверяйте стек и последние вызовы
  • «Всё работает без отладчика, но ломается с отладчиком»: проверьте тайминги, частые логи и влияние breakpoints
  • Что дальше

    Теперь у вас есть практический набор, чтобы уверенно отлаживать проекты и подключать аналоговые датчики через ADC. Следующий шаг роста обычно такой:

  • перейти от polling к interrupt/DMA там, где важна отзывчивость
  • научиться проектировать код модулями (драйвер датчика, модуль логирования, модуль управления)
  • начать использовать документацию на конкретный STM32: datasheet и reference manual
  • Полезная отправная точка по документации ST:

  • STM32 Documentation