Jump to content
View in the app

A better way to browse. Learn more.

T.M.I IThub

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Почему 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 для генерации кода инициализации

Установка:

  1. Скачать STM32CubeIDE с сайта st.com (требует регистрации, бесплатно)

  2. Установить, выбрать пакеты для нужных семейств

  3. Подключить программатор 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 — читайте их. Они написаны хорошо и содержат ответы на все вопросы.

User Feedback

Create an account or sign in to leave a review

There are no reviews to display.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.