Подробный гайд: Программирование STM32 (теория и практика)
1. Что такое STM32 и с чего начать
STM32 — семейство 32-битных микроконтроллеров на ядре ARM Cortex-M (M0/M0+, M3, M4, M7, M33).
Выпускается STMicroelectronics. Ключевые особенности:
- Разные линейки под разные задачи (от дешёвых до высокопроизводительных)
- Богатая периферия: таймеры, АЦП, DAC, UART, SPI, I2C, CAN, USB, Ethernet
- Гибкая система тактирования
- Несколько уровней программирования: регистры → LL → HAL
2. Линейки: какую выбрать
| Серия | Ядро | Особенности | Типичное применение |
|---|---|---|---|
| STM32C0 | M0+ | Самые дешёвые, простые | Замена простых 8-бит |
| STM32F0/G0 | M0/M0+ | Недорогие, базовая периферия | Управление, датчики |
| STM32F1 | M3 | Классика, много примеров (Blue Pill) | Обучение, простые устройства |
| STM32F4 | M4 + FPU | Популярная, высокая производительность | Моторы, аудио, DSP |
| STM32F7 | M7 | Кэш, высокая частота | Дисплеи, обработка |
| STM32H7 | M7 (до 480 МГц) | Максимальная производительность | Графика, AI |
| STM32L0/L4 | M0+/M4 | Ультранизкое потребление | Батарейные устройства, IoT |
| STM32U5 | M33 TrustZone | Безопасность + низкое потребление | Носимые, медицина |
| STM32G4 | M4 + FPU | Моторы, аналоговая периферия | Приводы, питание |
| STM32WB/WL | M4+M0+ / LoRa | Беспроводные: BLE, 802.15.4, LoRa | IoT, радио |
Для обучения рекомендую:
STM32F103C8T6 (Blue Pill) или STM32F401/F411 (Black Pill / Nucleo). Для новых проектов чаще берут G0, G4, L4, H7.
3. Инструментарий
Среды разработки
| Инструмент | Плюсы | Минусы |
|---|---|---|
| STM32CubeIDE (Eclipse-based, бесплатно) | Официальная, интеграция с CubeMX, отладчик | Медленная, тяжёлая |
| Keil MDK | Быстрая, удобная | Платная, ограничения бесплатной версии |
| IAR EWARM | Качественный код | Платная |
| VS Code + CMake + Makefile | Гибкая, лёгкая | Требует ручной настройки |
Генератор кода — STM32CubeMX
Графический конфигуратор: выбираете чип → настраиваете пины, тактирование, периферию → генерирует проект (HAL или LL).
Программаторы / отладчики
- ST-LINK (v2, v3) — официальный, поддержка SWD и JTAG
- J-Link — быстрый, удобный, но платный
- DAPLink / CMSIS-DAP — дешёвая альтернатива
Отладчики командной строки
- OpenOCD — открытый, работает с ST-LINK
- pyOCD
- ST-Link Utility / STM32CubeProgrammer — прошивка, стирание, чтение памяти
4. Уровни программирования
┌─────────────────────────────────────────────┐
│ Регистры (прямой доступ) │ ← максимум контроля, сложно
├─────────────────────────────────────────────┤
│ LL (Low-Layer) — тонкие инлайн-обёртки │ ← быстрый код, читаемость
├─────────────────────────────────────────────┤
│ HAL — высокоуровневые драйверы │ ← быстро старт, много готового
└─────────────────────────────────────────────┘
Когда что использовать
- Регистры — обучение, понимание железа, критичные по времени участки
- LL — нужна производительность + контроль (ШИМ моторов, высокоскоростной обмен)
- HAL — быстрый старт, прототип, не критичная по времени задача
Важно:
HAL медленнее и «тяжелее», но экономит время. В реальном проекте часто смешивают: HAL для логики, регистры/LL для горячих участков.
5. Архитектура кратко
Ядро и память
- Ядро Cortex-M: регистры R0-R12, SP, PC, xPSR
- Память: Flash (код), RAM (данные), часто с раздельными шинами
- Карта памяти: периферия отображена в адресное пространство (0x40000000+)
Шины и тактирование
┌────────────┐
HSI (внутр. 8М)─►│ │
HSE (внеш. кв.)─►│ RCC │──► SYSCLK ──► AHB ──► GPIO, DMA, Flash
PLL (умножитель)─►│ (такты) │──► APB1 ──► UART2, I2C, Timer2-7
│ │──► APB2 ──► UART1, SPI1, Timer1
└────────────┘
- HSI — внутренний RC-генератор, ~8 МГц, неточный
- HSE — внешний кварц, точный
- PLL — умножение частоты (например, 8 МГц × 9 = 72 МГц)
- RCC — блок управления тактами: включает/выключает такты на периферию
Прерывания: NVIC
- NVIC — контроллер прерываний
- Приоритеты: прем-приоритет и субприоритет
- Векторы в таблице прерываний (начало Flash)
- Обработчики:
EXTI0_IRQHandler,TIM2_IRQHandler,USART1_IRQHandlerи т.д.
6. Тактирование: первое, что нужно понять
По умолчанию после сброса микроконтроллер работает от внутреннего генератора (HSI), обычно 8 МГц.
Чтобы получить полную скорость (например, 72 МГц на F103):
- Включить внешний кварц (HSE)
- Настроить PLL: HSE × множитель = нужная частота
- Настроить предделители шин
- Переключить SYSCLK на PLL
- Подстроить задержки Flash (латентность)
В CubeMX это делается визуально. Вручную — через регистры RCC.
7. Периферия с примерами
7.1. GPIO — мигание светодиодом на регистрах (STM32F103, Blue Pill)
На Blue Pill светодиод на PC13.
#include "stm32f103xb.h" // заголовок с регистрами
void delay_ms(uint32_t ms)
{
while (ms--) {
volatile uint32_t i;
for (i = 0; i < 1000; i++); // грубо для 8 МГц
}
}
int main(void)
{
// 1. Включаем тактирование порта C
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// 2. Настраиваем PC13 как push-pull выход, 2 МГц
// В F103 регистры конфигурации — CRL (пины 0-7) и CRH (пины 8-15)
// Пин 13 → биты [23:20] в CRH
GPIOC->CRH &= ~(0xF << ((13 - 8) * 4)); // очистить
GPIOC->CRH |= (0x2 << ((13 - 8) * 4)); // MODE=10 (2МГц), CNF=00 (push-pull)
while (1)
{
GPIOC->ODR ^= GPIO_ODR_ODR13; // инвертируем пин
delay_ms(500);
}
}
Что происходит по шагам:
- RCC->APB2ENR — регистр включения тактов. Без него порт не работает.
- CRH — конфигурационный регистр для пинов 8-15. На каждый пин 4 бита: 2 на режим, 2 на тип.
- ODR — выходной регистр данных.
^=— инверсия.
Альтернатива: инверсия через BRR/BSRR:
GPIOC->BSRR = GPIO_BSRR_BR13; // сбросить пин
GPIOC->BSRR = GPIO_BSRR_BS13; // установить пин
7.2. То же самое на HAL (сгенерировано в CubeMX)
#include "main.h"
int main(void)
{
HAL_Init();
SystemInit(); // обычно вызывается автоматически
SystemClock_Config(); // сгенерирована в CubeMX
// GPIO настраивается в MX_GPIO_Init()
MX_GPIO_Init();
while (1)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
HAL_Delay(500);
}
}
HAL-подход:
- HAL_Init() — инициализирует SysTick на 1 мс
- SystemClock_Config() — настройка тактов
- MX_GPIO_Init() — настройка пинов из CubeMX
- HAL_Delay() — задержка в мс через SysTick
7.3. Прерывания и EXTI — кнопка
Подключаем кнопку к PA0, по нажатию зажигаем/тушим светодиод.
// В обработчике прерывания (имя фиксировано таблицей векторов)
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & EXTI_PR_PR0) // проверяем флаг
{
GPIOC->ODR ^= GPIO_ODR_ODR13; // переключаем светодиод
EXTI->PR = EXTI_PR_PR0; // сбрасываем флаг (записью 1)
}
}
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN // такт порта A
| RCC_APB2ENR_IOPCEN // такт порта C
| RCC_APB2ENR_AFIOEN; // такт альтернативных функций
// PA0 — вход с подтяжкой вверх
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x8 << 0); // CNF=10 (вход с подтяжкой), MODE=00
GPIOA->ODR |= GPIO_ODR_ODR0; // подтяжка вверх
// PC13 — выход (аналогично примеру выше)
GPIOC->CRH &= ~(0xF << 20);
GPIOC->CRH |= (0x2 << 20);
// EXTI: привязка линии 0 к порту A
AFIO->EXTICR[0] &= ~AFIO_EXTICR0_EXTI0; // PA0
// Разрешаем прерывание по линии 0
EXTI->IMR |= EXTI_IMR_MR0;
EXTI->RTSR |= EXTI_RTSR_TR0; // по переднему фронту
// Разрешаем прерывание в NVIC
NVIC_EnableIRQ(EXTI0_IRQn);
while (1) { /* спим или работаем */ }
}
Важные моменты:
- Флаг в EXTI->PR сбрасывается записью 1
- Подтяжка: у входа тип push-pull нет, подтяжка делается через ODR
- Обработчик прерывания должен быть коротким
На HAL:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
7.4. Таймеры и ШИМ
Таймеры STM32 — один из самых мощных блоков.
Типы:
- Базовые (TIM6, TIM7): простой счётчик
- Общего назначения (TIM2-5): ШИМ, захват, сравнение
- Расширенные (TIM1, TIM8): моторы, комплементарные выходы, dead-time
Пример:
ШИМ на TIM2, канал 1 (PA0), частота ~1 кГц при 8 МГц тактировании.
void PWM_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // GPIOA
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // TIM2
// PA0 — альтернативная функция, push-pull
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0xB << 0); // MODE=11 (50МГц), CNF=10 (AF push-pull)
// Настройка таймера
TIM2->PSC = 8 - 1; // предделитель: 8 МГц / 8 = 1 МГц
TIM2->ARR = 1000 - 1; // период: 1 МГц / 1000 = 1 кГц ШИМ
// Канал 1: ШИМ режим 1
TIM2->CCMR1 &= ~(TIM_CCMR1_CC1S | TIM_CCMR1_OC1M);
TIM2->CCMR1 |= (6 << 4); // OC1M = 110 (PWM mode 1)
TIM2->CCMR1 |= TIM_CCMR1_OC1PE; // preload
TIM2->CCR1 = 500; // скважность 50%
TIM2->CCER |= TIM_CCER_CC1E; // включить канал 1
TIM2->CR1 |= TIM_CR1_CEN; // запустить таймер
}
Скважность:
TIM2->CCR1 = 250; → 25%, 750 → 75%.
7.5. UART — эхо на прерываниях
Пример на HAL: принимаем байт и сразу отправляем обратно.
UART_HandleTypeDef huart2;
void UART2_Init(void)
{
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&huart2);
}
uint8_t rx_byte;
int main(void)
{
HAL_Init();
SystemClock_Config();
UART2_Init();
// Запускаем приём одного байта по прерыванию
HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
while (1) { }
}
// Колбэк: вызывается после приёма байта
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2) {
HAL_UART_Transmit(huart, &rx_byte, 1, HAL_MAX_DELAY);
HAL_UART_Receive_IT(huart, &rx_byte, 1); // перезапуск приёма
}
}
Для эффективности:
Приём по DMA + кольцевой буфер или прерывание IDLE для пакетов переменной длины.
7.6. АЦП с DMA
Пример на HAL: непрерывное измерение с DMA в буфер.
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
uint16_t adc_buf[64];
int main(void)
{
HAL_Init();
SystemClock_Config();
// MX_ADC1_Init() и MX_DMA_Init() сгенерированы из CubeMX
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 64);
while (1) {
// adc_buf заполняется автоматически
}
}
На регистрах: настроить канал, режим непрерывного преобразования, включить прерывание или DMA.
8. Отладка
Подключение
- SWD (2 провода: SWDIO + SWCLK) — основной способ
- JTAG (5 проводов) — для старых чипов и сложных случаев
Методы отладки
- Точки останова в IDE — классика
- Просмотр регистров периферии (Registers view в STM32CubeIDE)
- Просмотр памяти (память, массивы)
- ITM / SWO — вывод
printfчерез отладочный порт без UART - Логический анализатор — смотреть SPI, I2C, UART
- Осциллограф — ШИМ, аналоговые сигналы
Пример вывода через ITM (printf без UART)
#include <stdio.h>
// Перенаправление stdout в ITM
int _write(int fd, char *ptr, int len)
{
for (int i = 0; i < len; i++) {
ITM_SendChar(ptr[i]);
}
return len;
}
printf("Hello from STM32\r\n");
В отладчике нужно включить ITM Stimulus и посмотреть вывод.
9. Режимы энергопотребления
| Режим | Что работает | Ток | Выход |
|---|---|---|---|
| Run | Всё | мА | — |
| Sleep | Ядро спит, периферия работает | мА | Прерывание |
| Stop | Такты остановлены, RAM сохранён | мкА | EXTI |
| Standby | Почти всё выключено | нА | WKUP, RTC |
Пример входа в Stop:
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
После выхода из Stop нужно перенастроить тактирование.
10. Лучшие практики
Организация кода
- Разделяйте слои: драйверы / логика / приложение
- Конечные автоматы для логики вместо бесконечных задержек
- Кольцевые буферы для UART, DMA
- Очередь команд между прерыванием и основным циклом
Прерывания
- Обработчик должен быть максимально коротким
- Данные между прерыванием и основным циклом —
volatile - Не использовать
HAL_Delay()внутри прерывания - Флаги прерываний сбрасывать корректно
Тактирование
- Всегда проверять задержки Flash при смене частоты
- Подключать конденсаторы на питание (100 нФ на каждый VDD)
- Кварц: нагрузка (2×15-22 пФ)
Память и стек
- Следить за размером стека (особенно при рекурсии)
- Не выделять память динамически в прерываниях
- Для больших буферов — статическое выделение
Периферия
- I2C: не забывать подтягивающие резисторы (обычно 4.7 кОм)
- АЦП: входное сопротивление источника, фильтр
- ШИМ: учитывать мёртвое время для моторов
- UART: проверять уровни (3.3 В или 5 В, нужны преобразователи)
11. Типичные ошибки
- Забыть включить такт порта/периферии — ничего не работает
- Неправильный режим GPIO — пин настроен как вход, а мы в него пишем
- Неправильная частота таймера — ШИМ не той частоты
- Зависание в прерывании — не сброшен флаг
- Дребезг кнопки — нет программной или аппаратной фильтрации
- Конфликт пинов — один пин использует две периферии
- Нет подтяжки на входе — «плавающий» вход ловит помехи
- Неправильный уровень сигнала — подаём 5 В на вход 3.3 В чипа
- Перегруженный стек — переполнение при глубоких вызовах
- Кэш на H7/F7 — DMA видит старые данные из кэша (нужно MPU или обслуживание кэша)
12. Полезные ресурсы
- Документация: datasheet и reference manual на конкретный чип (обязательно)
- STM32CubeMX — конфигуратор
- GitHub STMicroelectronics — примеры HAL/LL
- Форумы: STM32 Community, cxem.net, easyelectronics.ru
- Книги: «The Definitive Guide to ARM Cortex-M» (Joseph Yiu), «Making Embedded Systems» (Elecia White)
13. Пример проекта: от нуля до прошивки
- Открыть STM32CubeIDE → New STM32 Project
- Выбрать чип (например, STM32F103C8T6)
- В CubeMX:
- Настроить RCC: HSE, PLL → 72 МГц
- SYS: Debug → Serial Wire
- GPIO: PC13 → GPIO_Output
- UART1: 115200
- Сгенерировать код
- Добавить логику в
main() - Подключить ST-LINK
- Run (или Debug)
Мы делимся этой технической информацией, чтобы помочь вам в решении задач — используйте её с пониманием. Статья носит рекомендательный характер, поэтому, пожалуйста, применяйте описанные методы осмотрительно.