Почему STM32, а не продолжать на Arduino
Arduino — отличный старт. Но в какой-то момент вы упираетесь в потолок: скорость 16 МГц не хватает, Flash/RAM заканчивается, нужны возможности которых у AVR нет — несколько UART, USB Device, Ethernet MAC, криптоускоритель, DSP-инструкции.
STM32 — это семейство 32-битных микроконтроллеров от STMicroelectronics на базе ядер ARM Cortex-M. Характеристики даже бюджетного STM32F103C8T6 ("Blue Pill"):
Параметр | Arduino Uno (ATmega328P) | STM32F103C8T6 |
|---|---|---|
Ядро | AVR 8-бит | ARM Cortex-M3 32-бит |
Тактовая частота | 16 МГц | 72 МГц |
Flash | 32 КБ | 64 КБ |
RAM | 2 КБ | 20 КБ |
GPIO | 23 | 37 |
ADC | 6 × 10-бит | 10 × 12-бит |
Таймеры | 3 | 7 |
SPI / I2C / UART | 1 / 1 / 1 | 2 / 2 / 3 |
USB | Нет | Full-Speed USB 2.0 |
Цена | ~$3–5 (оригинал) | ~$0.8–2 |
И это самый простой STM32. Линейки F4, F7, H7 — ещё на порядок мощнее.
Семейства STM32: как не запутаться
STM32 делится на несколько линеек по ядру и позиционированию:
STM32F0/F1/F3 — Базовые (Cortex-M0/M3/M4)
F103: самый популярный, "Blue Pill", 72 МГц — идеален для старта
F303: F3 с матфлоатом и операционными усилителями внутри
F030/F042: ультрадешёвые, от $0.3, для массового производства
STM32F4 — Производительные (Cortex-M4F с FPU)
F401/F411: 84–100 МГц, USB, хороший баланс
F407/F429: 168 МГц, Ethernet MAC, FMC для внешней SDRAM, камеры
Популярны для DSP-задач, аудио, обработки изображений
STM32F7/H7 — Высокопроизводительные (Cortex-M7)
H743: 480 МГц, двойная точность float, L1-кэш, умереть не встать
Используются в промышленных системах реального времени
STM32L — Низкое энергопотребление (Low Power)
L051/L071: ток в sleep < 1 мкА, для батарейных устройств
STM32G/U — Новые серии (2019–2022)
G431/G474: отличные для силовой электроники (Timer1 с мёртвым временем)
U5: Cortex-M33 с TrustZone, IoT-безопасность
Рекомендации для старта: STM32F103C8T6 (Blue Pill) или STM32G031 для новых проектов.
Настройка среды разработки
STM32CubeIDE + STM32CubeMX
STM32CubeIDE — официальная бесплатная среда от ST. Включает:
Eclipse-based IDE
Компилятор GCC ARM
OpenOCD для программирования/отладки
Встроенный STM32CubeMX для генерации кода инициализации
Установка:
Скачать STM32CubeIDE с сайта st.com (требует регистрации, бесплатно)
Установить, выбрать пакеты для нужных семейств
Подключить программатор ST-Link V2 ($3–5 на AliExpress)
Первое подключение (Blue Pill → ST-Link V2):
ST-Link V2 Blue Pill
SWDIO → PA13
SWCLK → PA14
GND → GND
3.3V → 3V3
Важно: На оригинальных Blue Pill загрузчик прошит неправильно. Для работы с ST-Link через SWD это не проблема — программируем напрямую в flash.
HAL vs LL: что выбрать
STMicroelectronics предоставляет два уровня библиотек:
HAL (Hardware Abstraction Layer):
Высокоуровневый, максимально переносимый код
Автоматически генерируется CubeMX
Проще в использовании, больше overhead
Рекомендуется для большинства проектов
LL (Low Layer):
Тонкие обёртки над регистрами, почти без overhead
Максимальная производительность и предсказуемость
Нужно хорошее знание периферии
Для критичного по времени кода
Смешанный подход (лучший для опытных):
HAL для инициализации (CubeMX генерирует)
LL для критичных по времени операций в прерываниях
GPIO: мигаем светодиодом правильно
CubeMX генерирует такой код инициализации GPIO:
// Автосгенерированный код CubeMX
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE(); // Включаем тактирование порта C
// Настройка PC13 как выход (встроенный LED на Blue Pill)
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // Push-Pull выход
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 2 МГц, достаточно для LED
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// Входной сигнал на PA0 (кнопка)
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // Внутренний pull-up
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
Управление GPIO в программе:
// Включить / выключить / переключить
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // LED on (active low)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // LED off
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // Toggle
// Читать состояние входа
GPIO_PinState btn = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
if (btn == GPIO_PIN_RESET) { // Кнопка нажата (pull-up, нажатие — к GND)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
}
GPIO режимы:
GPIO_MODE_OUTPUT_PP— Push-Pull выход (стандартный)GPIO_MODE_OUTPUT_OD— Open-Drain (для I2C, совместимость 5В)GPIO_MODE_INPUT— ВходGPIO_MODE_IT_RISING/FALLING/RISING_FALLING— Вход с прерываниемGPIO_MODE_AF_PP— Альтернативная функция (UART, SPI, Timer...)GPIO_MODE_ANALOG— Аналоговый режим (для ADC/DAC)
UART: последовательная связь
// Инициализация UART1 на PA9(TX)/PA10(RX), 115200 бод
static void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
}
// Отправка данных (блокирующий режим)
char msg[] = "Hello STM32!\r\n";
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100); // timeout 100ms
// Приём (с таймаутом)
uint8_t rx_buf[64];
uint16_t bytes_received;
HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, rx_buf, 64, 1000);
// printf через UART (настройка retarget)
// В файле syscalls.c добавить:
int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100);
return ch;
}
// После этого можно использовать printf!
printf("Температура: %.2f°C\r\n", temperature);
Приём через прерывания (правильный подход):
// Буфер приёма и флаги
#define RX_BUFFER_SIZE 256
static uint8_t rx_buffer[RX_BUFFER_SIZE];
static uint8_t rx_byte; // Один байт для приёма по прерыванию
static uint16_t rx_index = 0;
static volatile uint8_t line_ready = 0;
// Запускаем прием одного байта в прерывании
// (вызвать после инициализации и после каждого приёма)
void UART_StartReceive(void)
{
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
// Callback — вызывается автоматически при приёме байта
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
if (rx_byte == '\n' || rx_index >= RX_BUFFER_SIZE - 1) {
rx_buffer[rx_index] = '\0';
rx_index = 0;
line_ready = 1; // Сигнализируем что строка готова
} else if (rx_byte != '\r') {
rx_buffer[rx_index++] = rx_byte;
}
// Запускаем следующий приём
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
// В главном цикле:
if (line_ready) {
line_ready = 0;
printf("Получено: %s\r\n", rx_buffer);
process_command((char*)rx_buffer);
}
Таймеры: ШИМ и точное время
Таймеры STM32 — мощнейшая периферия. Используются для ШИМ, измерения частоты, генерации прерываний, управления сервоприводами.
ШИМ (PWM) для управления яркостью/скоростью:
// TIM3, Channel 1, PA6, частота 1 кГц
// Настройка через CubeMX, затем в коде:
// Запуск ШИМ
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
// Изменение скважности (0–999 для ARR=999)
// 500 = 50% скважности
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);
// Плавное изменение яркости LED
for (int i = 0; i <= 999; i++) {
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, i);
HAL_Delay(1);
}
for (int i = 999; i >= 0; i--) {
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, i);
HAL_Delay(1);
}
Управление сервоприводом (50 Гц, 1–2 мс):
// TIM2, 50 Гц (период 20 мс = 20000 тиков при предделителе 72-1)
// ARR = 19999, PSC = 71 → 1 тик = 1 мкс
#define SERVO_MIN_US 1000 // 1 мс = левый предел
#define SERVO_MAX_US 2000 // 2 мс = правый предел
#define SERVO_MID_US 1500 // 1.5 мс = центр
void servo_set_angle(int angle_degrees) // 0–180 градусов
{
// Линейное масштабирование угла → ширина импульса
uint32_t pulse = SERVO_MIN_US +
(uint32_t)(angle_degrees * (SERVO_MAX_US - SERVO_MIN_US) / 180);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse);
}
// Использование:
servo_set_angle(0); // Левый предел
HAL_Delay(1000);
servo_set_angle(90); // Центр
HAL_Delay(1000);
servo_set_angle(180); // Правый предел
Прерывание по таймеру (точный период):
// Прерывание каждые 1 мс от TIM6 (базовый таймер)
// Запуск:
HAL_TIM_Base_Start_IT(&htim6);
// Callback:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6) {
system_tick_ms++; // Собственный миллисекундный счётчик
// Задачи каждые 10 мс
if (system_tick_ms % 10 == 0) {
adc_trigger_conversion();
}
// Задачи каждые 1000 мс
if (system_tick_ms % 1000 == 0) {
led_heartbeat_toggle();
}
}
}
АЦП: чтение аналоговых сигналов
// Одиночное преобразование (блокирующий режим)
uint32_t adc_read_single(void)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 10); // Ждём не более 10 мс
uint32_t value = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);
return value; // 0–4095 для 12-бит АЦП
}
// Перевод в напряжение (опорное 3.3В):
float adc_to_voltage(uint32_t raw)
{
return raw * 3.3f / 4095.0f;
}
// Перевод в температуру для NTC-термистора (10кОм, B=3950):
float ntc_to_celsius(uint32_t raw_adc)
{
float voltage = adc_to_voltage(raw_adc);
float resistance = 10000.0f * voltage / (3.3f - voltage); // Делитель с 10кОм
// Уравнение Стейнхарта-Харта (упрощённое)
float steinhart;
steinhart = resistance / 10000.0f; // R/Rnom
steinhart = logf(steinhart); // ln(R/Rnom)
steinhart /= 3950.0f; // / B
steinhart += 1.0f / (25.0f + 273.15f); // + 1/T0
steinhart = 1.0f / steinhart; // Инверсия
return steinhart - 273.15f; // Кельвин → Цельсий
}
АЦП с DMA (несколько каналов, без участия CPU):
// Настройка: ADC + DMA, Continuous mode, 4 канала (PA0-PA3)
#define ADC_CHANNELS 4
static uint16_t adc_dma_buffer[ADC_CHANNELS];
// Запуск:
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_CHANNELS);
// DMA автоматически обновляет буфер!
// В основном цикле просто читаем:
float temp = ntc_to_celsius(adc_dma_buffer[0]);
float pressure = adc_dma_buffer[1] * 3.3f / 4095.0f;
// Callback при завершении преобразований всех каналов:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// Все 4 канала обновлены
adc_data_ready = 1;
}
I2C: подключение датчиков
// Пример: датчик давления/температуры BMP280
#define BMP280_ADDR 0x76 << 1 // 7-бит адрес, сдвиг влево для HAL
// Читать регистр
uint8_t BMP280_ReadReg(uint8_t reg)
{
uint8_t value;
HAL_I2C_Mem_Read(&hi2c1,
BMP280_ADDR, // Адрес устройства
reg, // Адрес регистра
I2C_MEMADD_SIZE_8BIT,
&value, // Буфер
1, // Количество байт
100); // Таймаут
return value;
}
// Записать регистр
void BMP280_WriteReg(uint8_t reg, uint8_t value)
{
HAL_I2C_Mem_Write(&hi2c1, BMP280_ADDR, reg,
I2C_MEMADD_SIZE_8BIT, &value, 1, 100);
}
// Читать несколько байт подряд
void BMP280_ReadBurst(uint8_t reg, uint8_t *buf, uint8_t len)
{
HAL_I2C_Mem_Read(&hi2c1, BMP280_ADDR, reg,
I2C_MEMADD_SIZE_8BIT, buf, len, 100);
}
// Инициализация BMP280
void BMP280_Init(void)
{
// Проверка ID (должен быть 0x60)
uint8_t id = BMP280_ReadReg(0xD0);
if (id != 0x60) {
printf("BMP280 не найден! ID=0x%02X\r\n", id);
return;
}
// Нормальный режим, oversampling ×4 для давления и температуры
BMP280_WriteReg(0xF4, 0x97); // ctrl_meas: осsp×4, osst×4, normal mode
BMP280_WriteReg(0xF5, 0xA0); // config: t_sb=1000мс, filter=16
}
// Чтение данных (упрощённо, без компенсации)
typedef struct {
float temperature;
float pressure;
} BMP280_Data;
BMP280_Data BMP280_ReadData(void)
{
uint8_t raw[6];
BMP280_ReadBurst(0xF7, raw, 6); // press_msb, press_lsb, press_xlsb, temp×3
int32_t raw_press = ((int32_t)raw[0] << 12) | ((int32_t)raw[1] << 4) | (raw[2] >> 4);
int32_t raw_temp = ((int32_t)raw[3] << 12) | ((int32_t)raw[4] << 4) | (raw[5] >> 4);
// Реальный код должен использовать калибровочные коэффициенты из регистров!
// Это упрощение для иллюстрации
BMP280_Data data;
data.temperature = raw_temp / 5120.0f; // Очень грубо!
data.pressure = raw_press / 25600.0f; // Очень грубо!
return data;
}
SPI: быстрая связь с периферией
// SPI — быстрее I2C, до 45 МГц на STM32F4
// Пример: дисплей ST7735 (128×160 пикселей)
// CS-пин вручную (NSS в software-режиме)
#define LCD_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define LCD_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
#define LCD_DC_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET) // Command
#define LCD_DC_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET) // Data
void LCD_SendCommand(uint8_t cmd)
{
LCD_DC_LOW();
LCD_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
LCD_CS_HIGH();
}
void LCD_SendData(uint8_t *data, uint16_t len)
{
LCD_DC_HIGH();
LCD_CS_LOW();
HAL_SPI_Transmit(&hspi1, data, len, 1000);
LCD_CS_HIGH();
}
// Быстрая передача через DMA (не блокирует CPU)
void LCD_SendDataDMA(uint8_t *data, uint16_t len)
{
LCD_DC_HIGH();
LCD_CS_LOW();
HAL_SPI_Transmit_DMA(&hspi1, data, len);
// CS поднимется в callback после завершения DMA
}
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1) {
LCD_CS_HIGH();
dma_complete = 1;
}
}
Структура хорошего проекта на STM32
project/
├── Core/
│ ├── Inc/
│ │ ├── main.h
│ │ └── stm32f1xx_hal_conf.h
│ └── Src/
│ ├── main.c — Только инициализация + main loop
│ ├── stm32f1xx_it.c — Обработчики прерываний
│ └── syscalls.c — printf retarget
├── Drivers/
│ ├── CMSIS/ — ARM заголовки, системный файл
│ └── STM32F1xx_HAL_Driver/ — HAL библиотека (не трогать)
├── App/ — ВАШ КОД здесь!
│ ├── sensors/
│ │ ├── bmp280.c / .h
│ │ └── ntc.c / .h
│ ├── control/
│ │ ├── pid.c / .h
│ │ └── state_machine.c / .h
│ ├── comm/
│ │ ├── modbus_slave.c / .h
│ │ └── protocol.c / .h
│ └── app.c — Главная логика приложения
└── CMakeLists.txt / .ioc — Конфигурация проекта
Принцип: main.c содержит только вызов App_Init() и App_Run(). Вся логика — в директории App/.
Типичные ошибки и как их избежать
1. Забыли включить тактирование периферии
// Без этого HAL_GPIO_Init ничего не сделает!
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_SPI1_CLK_ENABLE();
// CubeMX делает это автоматически — используйте его для инициализации
2. Неправильные Alternate Functions STM32F1 имеет фиксированный маппинг пинов. STM32F4+ — гибкий (GPIO_AF1_..., GPIO_AF7_...). Смотрите datasheet, раздел "Alternate function mapping".
3. Блокирующие задержки в прерываниях
// НЕЛЬЗЯ! HAL_Delay использует SysTick-прерывание низшего приоритета
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
HAL_Delay(100); // Зависнет если прерывание выше приоритета SysTick!
}
4. Запись в HAL-переменную напрямую Не изменяйте поля структур huart1, htim2 вручную в рантайме — используйте API функции.
5. Stack Overflow STM32F103 имеет только 20 КБ RAM. Большие массивы на стеке — прямой путь к Hard Fault. Объявляйте большие буферы как глобальные или статические.
Отладка: когда всё идёт не так
Hard Fault Handler — ваш лучший друг
void HardFault_Handler(void)
{
// Получаем регистры состояния
__asm volatile("TST lr, #4\n"
"ITE EQ\n"
"MRSEQ r0, MSP\n"
"MRSNE r0, PSP\n"
"B HardFault_HandlerC");
}
void HardFault_HandlerC(uint32_t *stack_frame)
{
printf("=== HARD FAULT ===\r\n");
printf("PC = 0x%08lX\r\n", stack_frame[6]); // Адрес проблемной инструкции
printf("LR = 0x%08lX\r\n", stack_frame[5]);
printf("CFSR= 0x%08lX\r\n", SCB->CFSR); // Причина fault
while(1);
}
SWO (Serial Wire Output) — printf без UART
Если UART занят, используйте SWO для отладочного вывода. В STM32CubeIDE включается за 5 кликов, вывод идёт в консоль IDE без дополнительного кабеля.
Заключение
STM32 — это настоящий профессиональный инструмент. Первые шаги сложнее чем с Arduino: нужно настроить тактирование, разобраться с HAL, понять концепцию прерываний и DMA. Но это окупается многократно: производительность, периферия, потребление, цена в серийном производстве.
Рекомендуемый путь: STM32F103 Blue Pill + ST-Link V2 + STM32CubeIDE. Первый проект — мигание LED через прерывание таймера. Второй — чтение кнопки с антидребезгом. Третий — датчик по I2C с выводом в UART. К четвёртому проекту вы уже будете чувствовать себя уверенно.
Документация ST: datasheet, Reference Manual, Programming Manual — читайте их. Они написаны хорошо и содержат ответы на все вопросы.
Create an account or sign in to leave a review
There are no reviews to display.