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

Курс знакомит с основами работы микроконтроллеров STM32 и практикой разработки встроенного ПО. Вы научитесь настраивать среду разработки, работать с периферией, таймерами и интерфейсами связи, а также отлаживать и структурировать проекты.

1. Введение в STM32 и подготовка рабочего окружения

Введение в STM32 и подготовка рабочего окружения

Что такое STM32 и почему их выбирают

STM32 — это семейство микроконтроллеров (маленьких компьютеров на одном кристалле) от компании STMicroelectronics. Микроконтроллеры STM32 часто используют в устройствах, которые должны работать автономно и управлять «железом»: датчиками, моторами, дисплеями, интерфейсами связи.

Почему STM32 популярны у начинающих и в индустрии:

  • Большой выбор моделей: от простых и дешёвых до очень производительных.
  • Хорошая экосистема: бесплатные инструменты разработки и много примеров.
  • Много периферии: таймеры, UART, SPI, I2C, USB, ADC и другое.
  • Доступные платы: можно начать без пайки.
  • Из чего состоит «минимальная система разработки»

    Чтобы начать программировать STM32, обычно нужны:

  • Плата с микроконтроллером STM32
  • Отладчик/программатор (часто уже встроен в плату)
  • IDE (среда разработки), компилятор и утилиты прошивки
  • Драйверы для подключения платы к компьютеру
  • Важно понимать два термина:

  • Прошивка — программа, которую вы загружаете во флеш-память микроконтроллера.
  • Отладка — запуск программы с контролем выполнения (остановки, просмотр переменных, пошаговое выполнение).
  • !Общая схема: IDE собирает прошивку, ST-LINK прошивает и позволяет отлаживать на плате

    Какую плату выбрать для старта

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

    Рекомендуемые варианты:

  • STM32 Nucleo (самый удобный старт)
  • - Обычно имеют встроенный ST-LINK (программатор/отладчик) и питаются от USB. - Наиболее «безболезненная» установка драйверов и отладка.
  • STM32 Discovery
  • - Часто добавлены дисплеи, датчики, расширенная периферия.
  • Blue Pill (STM32F103C8T6)
  • - Дешёвая, но часто требует отдельного программатора и больше ручной настройки. - Для самой первой статьи курса лучше не выбирать, если цель — быстрее получить результат.

    Если вы выбираете Nucleo, ориентируйтесь на популярные модели (например, серии F0/F4/G0/L4). В дальнейшем принципы будут одинаковыми.

    Что будет в этом курсе и как мы будем работать

    В курсе мы будем:

  • Создавать проекты в STM32CubeIDE
  • Настраивать микроконтроллер через STM32CubeMX (он встроен в CubeIDE)
  • Писать код на языке C
  • Использовать библиотеку HAL (Hardware Abstraction Layer)
  • Определения, чтобы не было «неизвестных слов»:

  • IDE — программа, где вы пишете код, собираете проект и запускаете отладку.
  • HAL — библиотека от ST, которая упрощает работу с периферией (таймеры, UART и т.д.).
  • CubeMX — графический конфигуратор: вы выбираете, какие выводы и периферия нужны, а он генерирует стартовый код проекта.
  • Установка STM32CubeIDE

    STM32CubeIDE — официальная бесплатная среда разработки от STMicroelectronics.

    Ссылка на загрузку:

  • STM32CubeIDE (страница продукта ST)
  • Что установить:

  • Последнюю стабильную версию STM32CubeIDE под вашу ОС (Windows / Linux / macOS).
  • После установки рекомендуется один раз запустить IDE и выбрать папку рабочего пространства (workspace). Workspace — это каталог, где IDE хранит ваши проекты.

    Драйверы и подключение платы

    Windows

    Обычно CubeIDE ставит необходимые драйверы, но иногда нужно поставить драйвер ST-LINK отдельно.

    Официальная страница утилит ST-LINK (включая драйверы для Windows):

  • ST-LINK, ST-LINK/V2, ST-LINK/V3 (страница продукта ST)
  • Проверка, что плата определилась:

  • Подключите плату по USB.
  • В Диспетчере устройств не должно быть неизвестных устройств с ошибками.
  • Linux

    На Linux чаще всего проблема не в драйвере, а в правах доступа к USB-устройству отладчика.

    Что обычно делают:

  • Подключают плату и проверяют, что устройство видно в системе.
  • Настраивают udev rules для ST-LINK (правила доступа к устройствам).
  • У ST есть документация и пакеты, но на разных дистрибутивах шаги отличаются, поэтому ориентируйтесь на документацию к вашей ОС и сообщения CubeIDE. Если CubeIDE видит ST-LINK и даёт начать отладку — значит базовая настройка выполнена.

    Установка пакетов микроконтроллеров (STM32Cube MCU Packages)

    Для каждой серии STM32 (например, F1/F4/G0/L4) ST предоставляет пакет поддержки: HAL, примеры, описания.

    CubeIDE обычно предлагает скачать нужный пакет автоматически при создании проекта. Если не предложила, пакеты можно поставить через менеджер встроенный в CubeIDE.

    Зачем это нужно:

  • Без пакета IDE не сможет корректно сгенерировать проект для выбранного чипа/платы.
  • В пакете есть примеры и шаблоны инициализации периферии.
  • Первое знакомство со структурой проекта в CubeIDE

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

    Самые важные элементы на старте:

  • Core/Src/main.c
  • - Главный файл программы. Здесь есть функция main().
  • Core/Inc/main.h
  • - Заголовочный файл (объявления, подключения).
  • Код инициализации (генерируется CubeMX)
  • - Настройка тактирования - Настройка GPIO (выводов) - Подготовка периферии (если вы её включили)

    Важно: часть кода генерируется автоматически. Чтобы ваши изменения не затирались при повторной генерации, CubeIDE помечает безопасные зоны комментариями.

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

    Большинство прошивок для STM32 выглядит так:

  • Инициализация (настройка тактирования, выводов, периферии)
  • Бесконечный цикл while (1)
  • Внутри цикла выполняется логика программы
  • Это отличается от обычных приложений на ПК тем, что микроконтроллер обычно работает постоянно и не «закрывает программу».

    Полезные официальные источники

    Чтобы в дальнейшем проверять детали по первоисточнику:

  • STM32CubeIDE (страница продукта ST)
  • STM32CubeMX (страница продукта ST)
  • STM32Cube (страница платформы ST)
  • ST-LINK, ST-LINK/V2, ST-LINK/V3 (страница продукта ST)
  • Итог

    В этой статье вы:

  • Поняли, что такое STM32 и что входит в рабочее окружение
  • Выбрали подходящий тип платы для старта (желательно Nucleo)
  • Узнали, что такое STM32CubeIDE, CubeMX и HAL
  • Подготовили основу для следующего шага: создания первого проекта и прошивки
  • 2. Архитектура MCU и основы C для embedded

    Архитектура MCU и основы C для embedded

    Зачем понимать архитектуру микроконтроллера

    В прошлой статье вы настроили среду разработки и познакомились с STM32CubeIDE/CubeMX/HAL. Следующий шаг — понять, что именно находится внутри микроконтроллера и почему код на C для embedded пишется немного иначе, чем «обычный» C на ПК.

    Это знание помогает:

  • быстрее разбираться в настройках CubeMX (тактирование, GPIO, прерывания);
  • понимать, что происходит во время прошивки и отладки;
  • читать даташиты и reference manual без ощущения «магии»;
  • писать более надёжный код (особенно при работе с регистрами и прерываниями).
  • Из чего состоит STM32 как система

    Типичный STM32 — это «маленький компьютер», где есть процессорное ядро, память и периферия.

    !Общая архитектура: ядро, память, шины и периферия

    Ядро (CPU)

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

    Официальная документация ARM: ARM Developer Documentation

    Память

    В STM32 обычно есть несколько типов памяти:

  • Flash (флеш-память)
  • - Энергонезависимая. - Хранит прошивку (код программы) и константы.
  • SRAM (оперативная память)
  • - Энергозависимая. - Хранит переменные, стек, буферы.
  • System memory / ROM (служебная память)
  • - Там может находиться встроенный загрузчик (bootloader) от производителя.

    Важно: объём и адреса зависят от конкретной модели STM32.

    Периферия

    Периферия — это аппаратные блоки вокруг ядра: GPIO, таймеры, UART, SPI, I2C, ADC и т.д. Вы управляете периферией через регистры.

    Шины (AHB/APB)

    Шины — это «дороги», по которым ядро обращается к памяти и периферии. В STM32 часто встречаются шины семейства AHB и APB. Вам не нужно знать все детали шин на старте, но важно понимать:

  • периферия обычно «висит» на определённой шине;
  • частота этой шины влияет на скорость работы периферии;
  • в CubeMX вы часто настраиваете тактирование именно для шин и блоков.
  • Тактирование и RCC

    Микроконтроллер работает от тактового сигнала. Блок, который распределяет и настраивает тактирование, в STM32 обычно называется RCC.

    Практический смысл:

  • частота CPU влияет на скорость выполнения кода;
  • частоты шин влияют на периферию;
  • некоторой периферии нужны отдельные источники тактирования.
  • Память и memory-mapped I/O

    Ключевой принцип embedded: регистры периферии отображены в адресное пространство памяти. Это называется memory-mapped I/O.

    То есть чтение/запись по «обычному адресу» на самом деле читает/меняет состояние железа.

    !Идея: периферия доступна как память по адресам

    Что такое регистр

    Регистр периферии — это обычно 32-битное число, где отдельные биты включают функции или показывают состояние.

    Пример логики (без привязки к конкретному STM32):

  • бит 0 может включать периферию;
  • бит 1 может включать прерывание;
  • бит 7 может показывать флаг «данные готовы».
  • Отсюда следует важная тема C для embedded: битовые операции.

    Как запускается прошивка на STM32 (упрощённо)

    После сброса (reset) происходит типичный сценарий:

  • Ядро берёт начальные значения из таблицы векторов (vector table).
  • Устанавливается начальное значение стека (stack pointer).
  • Вызывается код инициализации (startup):
  • - копирование инициализированных данных из Flash в SRAM; - обнуление неинициализированных данных; - настройка базовых вещей.
  • Вызывается main().
  • Дальше программа обычно работает в бесконечном цикле и/или реагирует на прерывания.
  • В проектах CubeIDE значительная часть этого уже подготовлена шаблоном и HAL.

    Прерывания и NVIC

    Прерывание — это механизм, при котором железо «срочно зовёт» процессор выполнить специальную функцию-обработчик.

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

  • пришёл байт по UART;
  • сработал таймер;
  • изменился уровень на входном пине (кнопка);
  • завершилось преобразование ADC.
  • В ARM Cortex-M за управление прерываниями отвечает блок NVIC.

    Практические последствия для вашего C-кода:

  • обработчик прерывания должен быть коротким и быстрым;
  • общие данные между main() и прерыванием нужно защищать (минимум — понимать роль volatile).
  • Основы C, которые особенно важны в embedded

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

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

  • uint8_t, int8_t
  • uint16_t, int16_t
  • uint32_t, int32_t
  • Документация по типам: cppreference: Integer types

    Пример:

    Почему это важно:

  • регистры периферии чаще всего 32-битные;
  • протоколы и форматы данных жёстко задают размер полей;
  • переносимость между разными компиляторами и MCU становится выше.
  • Указатели и работа с адресами

    Указатель хранит адрес. В embedded вы часто «смотрите» на регистр как на переменную по фиксированному адресу.

    Схема (учебный пример):

    Здесь важно:

  • 0x40000000u — константа-адрес (суффикс u делает её беззнаковой).
  • приведение (volatile uint32_t *) говорит компилятору, что по этому адресу лежит 32-битный регистр.
  • volatile критически важно для регистров и данных, меняющихся «снаружи» (периферией/прерыванием).
  • Ключевое слово volatile

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

    Используется для:

  • регистров периферии (memory-mapped I/O);
  • переменных, которые меняются в обработчиках прерываний.
  • Пример проблемы без volatile (концептуально): цикл может превратиться в «вечный», потому что компилятор решит, что переменная не меняется.

    const в embedded

    const означает «нельзя менять через этот идентификатор». В embedded это полезно для:

  • таблиц (например, lookup table);
  • неизменяемых конфигураций;
  • повышения надёжности.
  • Важно: const и размещение во Flash зависят от компоновки (linker script) и настроек проекта, но в типичных STM32-проектах константные данные действительно стремятся храниться во Flash.

    Битовые операции

    Регистры часто настраиваются битами. Основные операции:

  • | (OR) — установить биты
  • & (AND) — проверить/сбросить биты
  • ~ (NOT) — инвертировать
  • ^ (XOR) — переключить
  • << и >> — сдвиги
  • Пример: установить бит n:

    Пример: сбросить бит n:

    Пример: проверить бит:

    struct и типы периферии

    В CMSIS/HAL регистры часто описывают через struct, чтобы обращаться к ним как к полям.

    Упрощённая идея:

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

    static, extern и области видимости

    В embedded проектах много файлов, поэтому важно понимать:

  • static для функции/глобальной переменной в .c файле делает символ видимым только в этом файле.
  • extern в заголовке сообщает, что переменная определена в другом месте.
  • Мини-пример:

    internal_state здесь не «торчит наружу», и это уменьшает риск конфликтов.

    Заголовочные файлы и include guards

    Заголовок (.h) обычно содержит объявления, а реализация — в .c.

    Пример шаблона заголовка:

    Как это связано с HAL и CubeMX

    HAL даёт удобные функции уровня:

  • HAL_GPIO_WritePin(...)
  • HAL_UART_Transmit(...)
  • HAL_Delay(...)
  • Но под капотом всё равно происходит запись/чтение регистров, настройка тактирования и обработка прерываний.

    Практический подход для новичка:

  • на старте используйте HAL, чтобы быстрее получать результат;
  • параллельно учитесь понимать базовые вещи: память, регистры, volatile, битовые операции;
  • когда что-то «не работает», вы сможете отлаживать осмысленно: проверять тактирование, включение периферии, флаги и прерывания.
  • Итог

    Теперь у вас есть база, чтобы уверенно двигаться дальше:

  • вы понимаете, что STM32 состоит из ядра, памяти и периферии, связанных шинами;
  • знаете, что периферия управляется через memory-mapped регистры;
  • представляете общий путь запуска прошивки до main();
  • усвоили C-концепции, без которых embedded быстро начинает «ломаться»: stdint.h, указатели, volatile, битовые операции, static/extern.
  • 3. GPIO, прерывания и работа с таймерами

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

    В прошлых статьях вы настроили STM32CubeIDE и разобрали базовую архитектуру STM32: память, периферию, memory-mapped регистры, прерывания, а также ключевые элементы C для embedded, включая volatile. Теперь соберём это в практический набор навыков, без которых почти не бывает реальных проектов: GPIO, внешние прерывания и таймеры.

    GPIO в STM32

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

  • цифровой выход (включить светодиод)
  • цифровой вход (считать кнопку)
  • альтернативная функция (например, UART, SPI, PWM от таймера)
  • аналоговый вход (для ADC)
  • В STM32 пины объединены в порты: GPIOA, GPIOB, GPIOC и так далее. Обычно в коде вы указываете и порт, и номер пина, например GPIOA и GPIO_PIN_5.

    Основные режимы GPIO

    Ниже — ориентир по режимам, которые вы чаще всего будете выбирать в STM32CubeMX.

    | Режим | Что это | Типичный пример | |---|---|---| | Input | Чтение логического уровня с пина | Кнопка, датчик с цифровым выходом | | Output | Управление уровнем на пине | Светодиод, реле через транзистор | | Alternate Function | Пином управляет периферия | UART TX/RX, SPI, PWM от таймера | | Analog | Отключает цифровую часть входа | ADC, снижение потребления |

    Подтяжки Pull-up и Pull-down

    Входной пин может оказаться в состоянии плавающего уровня (не 0 и не 1), если сигнал не задан явно. Поэтому часто используют подтяжку:

  • Pull-up — подтягивает вход к логической 1
  • Pull-down — подтягивает вход к логическому 0
  • Это особенно важно для кнопок.

    !Кнопка с подтяжкой Pull-up: без нажатия читается 1, при нажатии читается 0

    Push-pull и Open-drain (для выходов)

  • Push-pull — пин активно выдаёт и 0, и 1, самый частый вариант для светодиода.
  • Open-drain — пин может тянуть линию к 0, а 1 получается через подтяжку; часто нужен для шин вроде I2C или для совместного использования линии.
  • Настройка GPIO в STM32CubeMX и использование HAL

    Обычно процесс такой:

  • В CubeMX выбираете нужный пин.
  • Устанавливаете режим (Input/Output/EXTI/AF).
  • Настраиваете Pull-up/Pull-down, скорость, начальное состояние.
  • Генерируете код.
  • В main() используете функции HAL.
  • Минимальные функции HAL для GPIO

  • HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_PIN_SET или GPIO_PIN_RESET)
  • HAL_GPIO_ReadPin(GPIOx, GPIO_Pin)
  • HAL_GPIO_TogglePin(GPIOx, GPIO_Pin)
  • Пример: мигание светодиодом (опрос в цикле)

    Пример ниже типичен для плат Nucleo, где светодиод часто висит на PA5, но у вашей платы пин может отличаться.

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

    Внешние прерывания от GPIO (EXTI)

    Опрос кнопки в while(1) работает, но не всегда удобен:

  • вы тратите время CPU на постоянные проверки
  • легко пропустить короткое событие
  • сложнее масштабировать на много событий
  • Прерывание решает это: событие на пине вызывает обработчик, и вы реагируете ровно в момент события.

    Из чего состоит механизм прерывания для пина

  • EXTI — блок, который умеет превращать изменение уровня на пине в событие (например, фронт 0→1 или спад 1→0)
  • NVIC — контроллер прерываний в ядре Cortex-M, который решает, какое прерывание и когда выполнять
  • !Как сигнал на пине превращается в вызов обработчика

    Настройка кнопки как EXTI в CubeMX

    Типичный путь:

  • Выберите пин кнопки.
  • Установите режим GPIO_EXTI (например, GPIO_EXTI13 для PC13 на некоторых Nucleo).
  • Выберите, на каком событии срабатывать: Rising, Falling или Rising/Falling.
  • В разделе NVIC включите соответствующее прерывание EXTI.
  • Как писать код с прерываниями правильно

    Главное правило: обработчик прерывания должен быть коротким.

    Частый шаблон:

  • В прерывании только фиксируете факт события (например, ставите флаг).
  • Основную работу делаете в while(1).
  • Для флага почти всегда нужен volatile, потому что он меняется вне текущего потока выполнения.

    Дребезг кнопки

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

    Популярные способы бороться:

  • Аппаратно: RC-цепочка, триггер Шмитта.
  • Программно по времени: игнорировать события, пришедшие раньше заданного интервала.
  • Простой программный вариант с HAL_GetTick():

    HAL_GetTick() возвращает количество миллисекунд с момента старта (обычно на SysTick). Число 50 здесь — типичная задержка подавления дребезга, но её подбирают под конкретную кнопку.

    Таймеры STM32

    Таймер — аппаратный блок, который считает импульсы тактирования и может:

  • измерять время без загрузки CPU
  • вызывать прерывания с точным периодом
  • генерировать PWM
  • измерять частоту и длительность импульсов
  • Важно различать:

  • SysTick — системный таймер ядра, обычно используется HAL для HAL_Delay() и HAL_GetTick()
  • TIMx (общего назначения и другие) — периферийные таймеры STM32, которые вы настраиваете под задачу
  • Базовая идея периодического таймера

    Чаще всего таймер работает так:

  • Внутренний счётчик увеличивается с некоторой частотой.
  • Когда счётчик достигает заданного значения, возникает событие.
  • По событию можно сгенерировать прерывание.
  • !Как таймер делит частоту и генерирует периодическое событие

    В CubeMX параметры часто выглядят как:

  • Prescaler (PSC) — делитель частоты таймера
  • Counter Period (ARR) — значение, до которого считает таймер
  • Интуитивно:

  • больше PSC или ARR — больше период
  • меньше PSC или ARR — чаще срабатывания
  • Точные расчёты зависят от частоты тактирования конкретного таймера, но на старте достаточно понимать причинно-следственную связь и проверять период на практике.

    Прерывания от таймера (периодическая задача)

    Очень распространённая архитектура прошивки:

  • таймер раз в миллисекунд генерирует прерывание
  • в обработчике выставляется флаг
  • в while(1) выполняется нужная логика по этому флагу
  • Настройка в CubeMX

    Обычно шаги такие:

  • Включить нужный TIMx в режиме Time Base.
  • Выбрать источник тактирования Internal Clock.
  • Настроить Prescaler и Counter Period.
  • Включить прерывание таймера в NVIC.
  • Запуск таймера и обработчик

    CubeMX создаст TIM_HandleTypeDef htimX; и функцию MX_TIMX_Init().

    В main() нужно запустить таймер с прерыванием:

    Обратите внимание:

  • проверка htim->Instance == TIM2 нужна, если у вас несколько таймеров
  • tim_event помечен как volatile, потому что меняется в прерывании
  • PWM на таймерах (очень кратко)

    PWM (широтно-импульсная модуляция) — это способ управлять средним значением мощности, быстро переключая выход между 0 и 1.

    На STM32 PWM обычно делается так:

  • Настраиваете канал таймера в режиме PWM Generation.
  • Настраиваете GPIO-пин в режиме Alternate Function для соответствующего канала.
  • Запускаете PWM через HAL_TIM_PWM_Start(...).
  • Меняете скважность через регистр сравнения, обычно удобной макрокомандой __HAL_TIM_SET_COMPARE(&htimX, TIM_CHANNEL_Y, value).
  • PWM полезен для:

  • управления яркостью светодиодов
  • управления скоростью моторов
  • генерации управляющих сигналов (например, для сервоприводов)
  • Типичные проблемы и как их быстрее находить

  • Пин не реагирует: проверьте, что в CubeMX выбраны правильный порт и пин, и что MX_GPIO_Init() вызывается.
  • Кнопка срабатывает наоборот: перепутаны подтяжка и логика (Pull-up обычно даёт нажатие как 0).
  • Прерывание не приходит: проверьте включение NVIC для нужной EXTI линии и выбор фронта.
  • В прерывании всё ломается: не делайте в обработчиках долгих операций и задержек HAL_Delay().
  • Таймер “не тикает”: убедитесь, что он запущен HAL_TIM_Base_Start_IT, и что обработчик проверяет правильный TIMx.
  • Для справки по инструментам ST:

  • STM32CubeIDE (страница продукта ST)
  • STM32CubeMX (страница продукта ST)
  • STM32Cube MCU Packages (пакеты для серий STM32)
  • Итог

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

  • настроили и используете GPIO как вход и выход
  • понимаете, зачем нужны Pull-up/Pull-down и чем отличается push-pull от open-drain
  • освоили внешний ввод через прерывания EXTI и принцип короткого обработчика
  • научились использовать таймер как источник периодических прерываний
  • получили представление о том, как таймеры используются для PWM
  • 4. Интерфейсы связи: UART, I2C, SPI

    Интерфейсы связи: UART, I2C, SPI

    В предыдущих статьях вы научились работать с GPIO, прерываниями EXTI и таймерами, а также разобрали, почему в embedded важны volatile, фиксированные типы и короткие обработчики прерываний. Следующий практический шаг почти в любом проекте на STM32 — научиться обмениваться данными с внешним миром: компьютером, датчиками, дисплеями, памятью.

    В этой статье разберём три самых популярных интерфейса:

  • UART — простая последовательная связь точка-точка
  • I2C — двухпроводная шина для множества устройств
  • SPI — быстрая синхронная связь, часто для дисплеев и памяти
  • Что общего у UART, I2C, SPI на STM32

    На STM32 почти любая связь строится одинаково по шагам:

  • В STM32CubeMX выбираете периферию и назначаете пины.
  • CubeMX настраивает GPIO в нужные режимы и генерирует MX_..._Init().
  • В коде вызываете HAL-функции для передачи и приёма.
  • При необходимости включаете прерывания и реагируете на события через колбэки.
  • Три важных понятия перед практикой:

  • Скорость (baud rate / bit rate) — как быстро идут биты по линии.
  • Протокол — правила, как именно устроены кадры и сигналы.
  • Уровни сигналов — для STM32 чаще всего 3.3 В логика; нельзя напрямую подключать 5 В устройства без согласования уровней.
  • Сравнение интерфейсов

    | Интерфейс | Провода | Синхронизация | Топология | Типичное применение | Плюсы | Минусы | |---|---:|---|---|---|---|---| | UART | 2 (TX, RX) + GND | Асинхронная | Точка-точка | Отладка, связь с ПК, модули | Очень простой | Обычно только 2 устройства, нет адресации | | I2C | 2 (SCL, SDA) + GND | Синхронная | Шина (много устройств) | Датчики, RTC, EEPROM | Всего 2 линии на много устройств | Требует подтяжек, чувствителен к длине линии | | SPI | 3–4+ (SCK, MOSI, MISO, CS) + GND | Синхронная | Обычно мастер-несколько ведомых | Дисплеи, флеш-память, быстрые АЦП | Очень быстрый, простой протокол | Нужен отдельный CS на устройство (часто) |

    UART

    UART (Universal Asynchronous Receiver/Transmitter) — асинхронный последовательный интерфейс. Асинхронный означает: отдельной линии такта нет, стороны заранее договариваются о скорости и формате кадра.

    Линии UART

  • TX (transmit) — передача
  • RX (receive) — приём
  • GND — общая земля (обязательна)
  • Обычно соединяют крест-накрест:

  • TX микроконтроллера → RX адаптера
  • RX микроконтроллера → TX адаптера
  • !Как физически подключать UART

    Формат кадра UART

    Самый распространённый режим называют 8N1:

  • 8 бит данных
  • N — без бита чётности (parity)
  • 1 стоп-бит
  • Также у кадра есть:

  • старт-бит — обозначает начало передачи (обычно логический 0)
  • стоп-бит — окончание (обычно логическая 1)
  • Это важно понимать, когда вы выбираете настройки в терминале на ПК.

    Настройка UART в CubeMX

    Типовые действия:

  • Включить USARTx или UARTx в режиме Asynchronous.
  • Назначить пины TX/RX.
  • Выставить baud rate (например, 115200).
  • При необходимости включить NVIC для приёма по прерыванию.
  • Передача через HAL (опрос)

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

    Что здесь важно:

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

    Опросом принимать данные неудобно: вы либо блокируете программу, либо постоянно проверяете флаги. Типичный подход в embedded — приём по прерыванию, где обработчик короткий.

    Идея:

  • включаем приём 1 байта в прерывание
  • в колбэке сохраняем байт и снова запускаем приём
  • Почему rx_flag и rx_byte сделаны volatile: они меняются в прерывании, то есть вне основного потока исполнения.

    I2C

    I2C (Inter-Integrated Circuit) — двухпроводная синхронная шина. На одной шине могут жить многие устройства, каждое имеет адрес.

    Линии I2C и подтяжки

  • SCL — тактовая линия
  • SDA — линия данных
  • Критически важная деталь: I2C использует выходы типа open-drain, поэтому для работы нужны подтягивающие резисторы к питанию на SCL и SDA.

    Если подтяжек нет:

  • линия может не подниматься в логическую 1
  • обмен будет нестабильным или не будет работать совсем
  • !Почему I2C требует подтяжек и как подключается несколько устройств

    Адресация и ACK/NACK

    Обмен на I2C обычно выглядит так:

  • Мастер посылает адрес устройства.
  • Ведомое устройство подтверждает приём бита адреса сигналом ACK.
  • Дальше передаются байты данных, каждый подтверждается ACK.
  • Если устройство не отвечает или данные не приняты, может быть NACK.
  • Практический смысл для отладки:

  • если вы постоянно получаете ошибку, часто причина в неправильном адресе или отсутствии подтяжек
  • Настройка I2C в CubeMX

    Типовые действия:

  • Включить I2Cx.
  • Назначить пины SCL/SDA.
  • Выбрать скорость (например, 100 kHz или 400 kHz).
  • Проверить, что GPIO настроены как alternate function open-drain (CubeMX сделает это сам при корректной конфигурации).
  • Обмен через HAL

    Для многих датчиков распространён стиль работы: запись адреса регистра, затем чтение данных. В HAL для этого есть удобные функции семейства Memory.

    Пример чтения 1 байта из регистра reg устройства с 7-битным адресом dev_addr:

    Что здесь важно:

  • многие HAL-функции ждут адрес в формате 8-битного поля (7-битный адрес, сдвинутый на 1). Поэтому часто встречается dev_addr_7bit << 1.
  • I2C_MEMADD_SIZE_8BIT означает, что адрес регистра внутри устройства имеет размер 8 бит. У некоторых микросхем адрес регистра 16 бит.
  • SPI

    SPI (Serial Peripheral Interface) — синхронный интерфейс, обычно очень быстрый и простой по сигналам.

    Линии SPI

    Наиболее типичный вариант (4 линии):

  • SCK (serial clock) — такт
  • MOSI (master out slave in) — данные от мастера к ведомому
  • MISO (master in slave out) — данные от ведомого к мастеру
  • CS/NSS (chip select) — выбор ведомого (часто активный 0)
  • В отличие от I2C, у SPI обычно нет адресации как части протокола: устройство выбирают отдельной линией CS.

    !Как подключаются несколько SPI устройств через отдельный CS

    Режимы SPI и CPOL/CPHA

    У SPI есть важная настройка: на каком фронте такта считывать данные и какой уровень такта считается неактивным. Эти параметры задаются двумя битами:

  • CPOL — полярность такта (уровень SCK в покое)
  • CPHA — фаза (на каком фронте читать/менять данные)
  • В документации устройств это обычно записано как SPI mode 0/1/2/3.

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

  • если SPI “молчит” или данные “битые”, первое что проверяют — совпадает ли mode SPI с требованиями устройства
  • Настройка SPI в CubeMX

    Типовые действия:

  • Включить SPIx в режиме Full-Duplex Master.
  • Назначить пины SCK/MOSI/MISO.
  • NSS часто делают Software, а CS реализуют обычным GPIO, потому что так проще управлять несколькими ведомыми.
  • Обмен через HAL

    SPI по природе полностью двунаправленный: при каждом такте что-то передаётся и что-то принимается. Поэтому часто используют HAL_SPI_TransmitReceive.

    Пример: передать 1 байт команды и одновременно прочитать 1 байт ответа:

    Если вы вручную управляете CS через GPIO, то типичный шаблон такой:

  • опустить CS
  • обменяться байтами
  • поднять CS
  • Как выбирать интерфейс в проекте

    Ориентиры для начинающих:

  • Вывод логов в терминал, простая связь с ПК или модулем: UART
  • Несколько датчиков на двух проводах, невысокие скорости: I2C
  • Дисплеи, SD-карты, внешняя флеш-память, высокая скорость: SPI
  • Типичные проблемы и быстрые проверки

  • UART
  • - нет текста в терминале: проверьте скорость, формат 8N1, правильность TX/RX и общую землю - кракозябры: почти всегда неправильный baud rate или неверный источник тактирования
  • I2C
  • - постоянная ошибка/таймаут: проверьте подтяжки, адрес устройства, питание датчика, общую землю - “видит не тот адрес”: проверьте, 7-битный или 8-битный адрес использует ваша библиотека/пример
  • SPI
  • - данные не читаются: проверьте CPOL/CPHA (SPI mode), частоту SCK, правильность CS - несколько устройств мешают: убедитесь, что CS неактивных устройств поднят

    Полезные источники

  • UART (Wikipedia)
  • I2C (Wikipedia)
  • SPI (Wikipedia)
  • STM32CubeIDE (страница продукта ST)
  • STM32Cube MCU Packages (страница ST)
  • Итог

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

  • UART — простой асинхронный канал, удобен для отладки и связи точка-точка
  • I2C — двухпроводная шина с адресацией, требует подтяжек и аккуратного подключения
  • SPI — быстрый синхронный интерфейс, часто требует отдельный CS на устройство и правильный SPI mode
  • Эта база позволит вам дальше уверенно подключать реальные датчики и модули, а также строить обмен данными через прерывания, как вы уже делали для GPIO и таймеров.

    5. Отладка, энергосбережение и структура проекта

    Отладка, энергосбережение и структура проекта

    В предыдущих статьях вы настроили STM32CubeIDE, разобрались с архитектурой STM32, работой GPIO, прерываниями, таймерами и интерфейсами UART/I2C/SPI. Теперь перейдём к трём темам, которые превращают набор примеров в реальную прошивку: отладка, энергосбережение и структура проекта.

    Отладка на STM32: что это и какие инструменты вы используете

    Отладка в embedded отличается от отладки программ на ПК тем, что код выполняется на микроконтроллере, а вы управляете этим процессом с компьютера через отладчик.

    Обычно в платах Nucleo/Discovery отладчик уже встроен (ST-LINK). Он умеет:

  • прошивать микроконтроллер
  • ставить точки останова и выполнять код пошагово
  • читать/писать память и регистры
  • показывать состояние периферии в отладчике
  • !Общая схема: как IDE управляет выполнением прошивки на плате через ST-LINK

    Полезные официальные источники по инструментам:

  • STM32CubeIDE (страница продукта ST)
  • ST-LINK (страница продукта ST)
  • Базовые приёмы отладки в STM32CubeIDE

    Breakpoint, Step и Run

    Три базовых действия, которые вы будете делать чаще всего:

  • Breakpoint (точка останова): ставите остановку на строке, чтобы “поймать” момент.
  • Step Into/Over (пошаговое выполнение): идёте по коду медленно, наблюдая переменные.
  • Run/Resume: продолжаете выполнение до следующей точки останова.
  • Практический совет: ставьте breakpoint не в бесконечном while (1), а на событии, которое вы ждёте (например, после HAL_UART_RxCpltCallback, или в месте обработки флага от таймера).

    Watch, Variables и volatile

    Окна Variables и Expressions/Watch показывают значения переменных. В embedded важно помнить:

  • если переменная меняется в прерывании, она должна быть volatile, иначе отладка и поведение могут “обманывать”
  • некоторые значения меняются слишком быстро, и вы видите “случайное” значение в момент остановки
  • Связь с предыдущими статьями прямая: флаги button_event, tim_event, rx_flag из примеров должны быть volatile именно потому, что их меняет обработчик прерывания.

    Просмотр регистров и периферии

    Когда “железо не работает”, полезно смотреть не только переменные, но и состояние периферии.

    Типичные вещи, которые проверяют:

  • включено ли тактирование периферии (через RCC)
  • правильно ли настроен режим GPIO (Input/Output/AF)
  • пришёл ли флаг события (например, что UART реально получил байт)
  • В проектах STM32 CubeIDE часто помогает просмотр peripheral registers (регистров периферии) в debug-перспективе.

    Assert: быстрый способ поймать ошибку конфигурации

    В проектах STM32 HAL есть механизм assert, который может остановить программу, если вы передали неверные параметры или нарушили ожидания библиотеки.

    Практический смысл:

  • в режиме отладки assert помогает быстрее понять, что именно пошло не так
  • в релизной прошивке assert обычно отключают или превращают в логирование
  • Как отлаживать “зависания”: HardFault и бесконечные циклы

    Типичные причины зависаний

    Самые частые причины, почему прошивка “повисла”:

  • обращение по неверному указателю (ошибка адреса)
  • переполнение стека (слишком большие локальные массивы, глубокая рекурсия)
  • ожидание события, которое никогда не произойдёт (например, I2C без подтяжек)
  • блокирующие вызовы с большим таймаутом в неподходящем месте (например, в прерывании)
  • Из прошлой статьи про интерфейсы связи: I2C-ошибки из-за отсутствия подтяжек часто выглядят как таймаут и “зависание” в ожидании.

    Что делать на практике

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

  • Поставьте breakpoint в начале main() и убедитесь, что старт действительно проходит.
  • Поставьте breakpoint до и после подозрительной функции (например, HAL_I2C_Mem_Read).
  • Если зависание внутри HAL-функции, смотрите параметры и “условия работы” периферии: пины, тактирование, скорость, подключение.
  • Если падает в HardFault, проверьте последний выполненный участок кода и указатели.
  • Логирование: когда отладчик не подходит

    Отладчик отлично работает, пока вы можете остановить программу. Но иногда остановка ломает поведение (например, тайминги SPI или обмен по UART). Тогда используют логирование.

    Самый простой вариант для новичка:

  • лог в терминал через UART
  • Это удобно тем, что вы уже умеете настраивать UART, и можете печатать события: старт, ошибка, пришёл байт, состояние автомата.

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

  • лог должен быть коротким, чтобы не “забить” канал
  • не делайте длинный лог из обработчика прерывания, лучше ставьте флаг и печатайте в while(1)
  • Энергосбережение: почему важно и что реально можно сделать

    Даже если ваше устройство питается от USB, понимание энергосбережения важно по двум причинам:

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

    Что чаще всего “ест ток” в прошивке:

  • высокая частота тактирования CPU и шин
  • постоянно работающие периферийные блоки (UART, SPI, таймеры)
  • включённые подтяжки и неправильно настроенные GPIO (например, плавающий вход)
  • внешние устройства, которые вы забыли перевести в sleep
  • Low-power режимы в STM32 на уровне идеи

    В STM32 есть режимы, где ядро и часть периферии могут быть остановлены.

    В терминах “на пальцах”:

  • Sleep: CPU спит, периферия может работать, просыпаемся по прерыванию.
  • Stop: тактирование ядра сильно ограничено или остановлено, потребление заметно ниже, после пробуждения часто нужно восстановить тактирование.
  • Standby: максимально глубокий сон, состояние в основном теряется, пробуждение похоже на перезапуск.
  • Точные возможности зависят от конкретной серии STM32, но подход к проектированию один: “работаем быстро, спим долго”.

    !Сравнение режимов Sleep/Stop/Standby на концептуальном уровне

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

    Чтобы перейти от “крутится в while(1)” к энергосбережению, обычно делают так:

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

  • кнопка через EXTI даёт событие для пробуждения
  • таймер может будить раз в N миллисекунд для периодической задачи
  • UART RX может будить при приёме данных (в зависимости от режима и серии)
  • SysTick и HAL_Delay в low-power

    HAL_Delay() обычно опирается на SysTick, который “тикает” раз в миллисекунду. Для энергосбережения это может быть проблемой: CPU постоянно просыпается.

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

  • для “научебных” проектов HAL_Delay() нормален
  • для low-power лучше уходить от бесконечных задержек в пользу событий (таймер, EXTI, UART RX)
  • Минимальные практики энергосбережения без сложной настройки

    Даже без глубоких режимов можно заметно улучшить потребление и качество прошивки:

  • отключайте ненужные периферии в CubeMX
  • не оставляйте входы “плавающими” (выберите pull-up/pull-down или Analog)
  • не гоняйте CPU в пустом цикле, если можно ждать событие
  • уменьшайте частоту там, где не нужна высокая производительность
  • Документация, от которой обычно отталкиваются:

  • Документация STM32 на сайте ST (страница документации)
  • Структура проекта: как не утонуть в CubeMX и HAL

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

    Цель структуры проекта:

  • ваши файлы не затираются при Regenerate Code
  • код легко читать и расширять
  • периферия и бизнес-логика разделены
  • Как CubeMX генерирует код и где безопасно писать

    В сгенерированных файлах CubeMX оставляет специальные секции для пользовательского кода. Практическое правило:

  • пишите свой код только в user sections, иначе при перегенерации вы потеряете изменения
  • Если вы хотите более чистую архитектуру, лучший путь:

  • оставить main.c “тонким”
  • вынести свою логику в отдельные .c/.h файлы
  • Рекомендуемая модульная структура для начинающего

    Ниже пример простой структуры, которая хорошо масштабируется:

  • app/ содержит логику приложения (что устройство делает)
  • bsp/ содержит привязку к плате (Board Support Package): светодиоды, кнопки, питание датчиков
  • drivers/ содержит драйверы устройств по UART/I2C/SPI (что и как общаться с железом)
  • Пример содержания модулей:

  • bsp_led.c/.h: bsp_led_init(), bsp_led_set(), bsp_led_toggle()
  • bsp_button.c/.h: обработка кнопки, антидребезг, выдача событий
  • drv_eeprom_i2c.c/.h: чтение/запись EEPROM по I2C
  • app_main.c/.h: состояние приложения, обработка событий
  • Связь с прошлой статьёй про static/extern:

  • внутренние переменные модуля делайте static
  • наружу экспортируйте только функции модуля и минимально нужные типы
  • Паттерн “флаг из прерывания, работа в main” как основа архитектуры

    Вы уже применяли этот подход для EXTI и таймера. Он же полезен для UART RX и вообще для событийной прошивки.

    Правила:

  • в обработчиках прерываний делайте минимум (поставить флаг, положить байт в буфер)
  • в while(1) забирайте флаги и выполняйте “тяжёлую” логику
  • Это помогает одновременно:

  • упростить отладку (логика в одном месте)
  • улучшить стабильность (прерывания короткие)
  • подготовить проект к энергосбережению (между событиями можно спать)
  • Пример “тонкого main.c”

    Ниже упрощённый пример идеи: main.c только инициализирует HAL, периферию и запускает приложение.

    А внутри app_loop() вы делаете:

  • обработку событий
  • обмен по интерфейсам
  • переход в sleep, если делать нечего
  • Итог

    В этой статье вы собрали “инженерный минимум”, который отличает рабочий проект от набора примеров:

  • понимаете, как устроена отладка через ST-LINK и что проверять при зависаниях
  • знаете, когда нужен лог через UART, а когда лучше breakpoint
  • получили концептуальную картину энергосбережения и почему “событийная” прошивка помогает снижать потребление
  • научились проектировать структуру проекта, чтобы CubeMX не мешал развивать код, а прерывания оставались короткими и безопасными