Архитектура 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 есть регистры общего назначения и специальные регистры. На практике чаще всего встречаются:
R0–R12 — регистры общего назначения
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(), бесконечного цикла и базовой отладки