Embedded HandbookПриложения
Все разделы › II. Ядро и системные механизмы
STM32F103, STM32F407, STM32F401, STM32F072 · уровень базовый · 20 мин чтения
Открыть в интерактивном справочнике

8. Порты ввода-вывода (GPIO)

GPIO (General-Purpose Input/Output) — то, чем микроконтроллер касается внешнего мира. Всё остальное — таймеры, UART, SPI — в конечном счёте выходит наружу через те же выводы. Поэтому GPIO изучают первым: не поняв его, нельзя зажечь даже светодиод.

Порты именуются буквами: GPIOA, GPIOB, GPIOC… Каждый порт — до 16 выводов, каждый вывод настраивается независимо от соседей.

Сколько их? Число выводов зависит от корпуса (LQFP48, BGA176…), семейства (F0, F1, F4…) и от того, заняты ли выводы кварцами HSE/LSE.


8.1 Как устроено

Структура одного вывода

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

Базовая структурная схема вывода порта I/O Базовая структурная схема вывода порта I/O

Толерантность к 5 В. На схеме у защитного диода стоит маркировка VDD_FT. Она означает, что вывод терпит входной сигнал 5 В при питании МК 3,3 В. Если такой маркировки в даташите у вывода нет — подавать выше питания нельзя (типовой запас всего ~0,3 В). Обычно нетолерантны выводы, заведённые на АЦП. И у толерантного вывода две оговорки: 5 В он терпит только при поданном питании — предел VDD + 4 В, так что на камень без питания 5 В подавать нельзя; а выше VDD + 0,3 В его внутренние подтяжки должны быть выключены. У F4 толерантность пропадает и в аналоговом режиме.

Сколько тока держит вывод. Абсолютный предел у F1 и F4 — 25 мА на вывод, втекающих или вытекающих, но уровни 0 и 1 даташит гарантирует только до 8 мА (до 20 мА — с просевшими уровнями). Сумма токов всех выводов вместе с потреблением самого камня — не больше 150 мА у F103 и 240 мА у F407, отдельно через VDD и через VSS. PC13–PC15 — не больше 3 мА и не источником тока (раздел 2.6). Светодиод подключают через резистор: (3,3 − 2,0) В / 330 Ом ≈ 4 мА; реле, мотор и длинную светодиодную ленту — только через транзистор.

Восемь режимов вывода

Как это называется в документации

Оригинал Перевод
port bit configuration настройка бита порта
output push-pull двухтактный выход
output open-drain выход с открытым стоком
floating input плавающий вход
input with pull-up / pull-down вход с подтяжкой вверх / вниз
Schmitt trigger триггер Шмитта
output driving current выходной ток
maximum output speed максимальная скорость вывода
five-volt tolerant (FT) толерантный к 5 В
alternate function альтернативная функция
atomic bit set/reset атомарная установка и сброс бита
locking mechanism механизм блокировки конфигурации
Режим Что делает
Input floating вход, подтяжки отключены (состояние после сброса)
Input pull-up вход, подтяжка к питанию
Input pull-down вход, подтяжка к земле
Analog вход АЦП/выход ЦАП; цифровая обвязка отключена
Output push-pull выход «тяни-толкай»: 1 → VDD, 0 → GND
Output open-drain выход с открытым стоком: 0 → GND, 1 → Hi-Z
AF push-pull выводом управляет периферия, выход двухтактный
AF open-drain выводом управляет периферия, выход с открытым стоком

Что происходит с железом в каждом режиме — по рисункам ниже:

Вход — выходной буфер отключён, триггер Шмитта включён, подтяжки по настройке, состояние вывода защёлкивается в IDR каждый такт шины.

Конфигурация входа Вход: плавающий, подтянутый вверх/вниз

Выход — работает выходной буфер; чтение IDR даёт реальный уровень на выводе, чтение ODR — последнее записанное значение. Разница важна: в режиме open-drain внешняя цепь может «пересилить» и на выводе окажется не то, что вы писали.

Конфигурация выхода Выход

Альтернативная функция — буфер управляется сигналом от периферии (USART, SPI, I²C). Если настроить вывод на AF, но саму периферию не включить, состояние выхода не определено.

Конфигурация альтернативной функции Альтернативная функция

Аналоговый — выходной буфер отключён, триггер Шмитта выключен (иначе он потреблял бы ток на промежуточных уровнях), подтяжки отключены, чтение IDR даёт 0.

Аналоговый вход Высокоимпедансный аналоговый вход

Атомарный доступ

Классическая ошибка — менять состояние вывода через ODR:

GPIOC->ODR |= (1 << 13);   // читать → изменить → записать: ТРИ операции

Между чтением и записью может прийти прерывание или переключиться задача RTOS — и соседний бит, изменённый там, потеряется. Поэтому у GPIO есть отдельные регистры BSRR и BRR: запись в них меняет выбранные биты ODR за одну шинную операцию, не трогая остальные.


8.2 Регистры

⚠️ Главная развилка всего раздела. У STM32F1 и у всех остальных семейств (F0, F3, F4, F7, L0, L4, G0, G4, H7…) устройство регистров GPIO разное. Код с MODER не соберётся под F1, код с CRL/CRH — под F4. Встречая противоречащие друг другу примеры, смотрите не на «правильность», а на то, для какого поколения периферии они написаны.

Что настраиваем STM32F1 Остальные семейства
режим вывода CRL / CRH — по 4 бита (MODE[1:0]+CNF[1:0]) MODER — по 2 бита
push-pull / open-drain там же, в CNF OTYPER
скорость фронта там же, в MODE OSPEEDR
подтяжки CNF=10 + бит в ODR PUPDR
номер альт. функции нет (только грубый remap через AFIO_MAPR) AFRL / AFRH
чтение / запись IDR / ODR то же
атомарная установка BSRR / BRR BSRR (BRR есть не везде)

Регистры STM32F1

Регистр Смещ. После сброса Назначение
GPIOx_CRL 0x00 0x4444 4444 конфигурация выводов 0–7
GPIOx_CRH 0x04 0x4444 4444 конфигурация выводов 8–15
GPIOx_IDR 0x08 0x0000 XXXX входные данные, только чтение
GPIOx_ODR 0x0C 0x0000 0000 выходные данные
GPIOx_BSRR 0x10 0x0000 0000 атомарно: установить (BS) / сбросить (BR)
GPIOx_BRR 0x14 0x0000 0000 атомарно: только сбросить
GPIOx_LCKR 0x18 0x0000 0000 блокировка конфигурации до сброса МК

Значение после сброса 0x44444444 означает CNF=01, MODE=00 для каждого вывода — то есть все выводы после сброса — плавающие входы.

Биты регистра GPIOx_CRL Биты регистра GPIOx_CRL, порты x=A..G

По 4 бита на вывод. Для вывода y: биты [4y+1:4y] — это MODE, биты [4y+3:4y+2] — CNF.

MODE[1:0] — вход или выход, и с какой скоростью:

MODE Значение
00 вход (после сброса)
01 выход, максимум 10 МГц
10 выход, максимум 2 МГц
11 выход, максимум 50 МГц

CNF[1:0] читается по-разному в зависимости от MODE:

CNF если MODE=00 (вход) если MODE≠00 (выход)
00 аналоговый push-pull
01 плавающий вход (после сброса) open-drain
10 вход с подтяжкой AF push-pull
11 зарезервировано AF open-drain

⚠️ Малоизвестная деталь. В режиме входа с подтяжкой (CNF=10) направление подтяжки задаётся не отдельным регистром, а битом в ODR: 1 — вверх, 0 — вниз. Если вы настроили CNF=10 и забыли про ODR, получите подтяжку вниз, хотя в коде об этом ни слова. У F4 и остальных для этого есть честный PUPDR.

Скорость — это не «быстрее лучше». Настройка ограничивает крутизну фронта. Ставить 50 МГц там, где мигает светодиод, — значит без нужды поднять электромагнитные помехи от платы. Для медленных сигналов берите 2 МГц.

BSRR и BRR

BSRR — 32 бита: младшие 16 (BS0..BS15) устанавливают биты ODR, старшие 16 (BR0..BR15) сбрасывают. Если для одного вывода взведены и BS, и BR, побеждает BS.

BRR — только сброс, младшие 16 бит.

Оба регистра — только для записи, чтение возвращает нули.

Карта регистров GPIO Карта регистров GPIO и значений сброса

Блокировка конфигурации (LCKR)

Позволяет «заморозить» настройку выводов до следующего сброса МК — защита от того, что чужой код случайно перенастроит критичную ножку. Требует строгой последовательности записи ключа LCKK (бит 16): записать 1 → записать 0 → записать 1 → прочитать 0 → прочитать 1. Любое отклонение отменяет блокировку.

Что занято сразу после сброса

Выводы отладочного интерфейса JTAG/SWD поднимаются не плавающими, а в своих режимах: PA15 (JTDI, pull-up), PA14 (JTCK, pull-down), PA13 (JTMS, pull-up), PB4 (JNTRST, pull-up).

⚠️ Заняв PA13/PA14 под свои нужды, вы теряете SWD — и вместе с ним возможность перепрошить плату обычным способом. Отсюда закомментированные строки с AFIO_MAPR_SWJ_CFG_JTAGDISABLE в примере с кнопкой ниже: их включают осознанно и только когда выводов совсем не хватает.

Регистры STM32F4

У F4 та же периферия решена принципиально иначе: вместо двух регистров с полями по 4 бита — пять раздельных регистров, каждый со своим смыслом. Это удобнее: меняете скорость — трогаете только OSPEEDR, не рискуя задеть режим.

Регистр Смещ. Бит на вывод Назначение
MODER 0x00 2 режим: вход / выход / альт. функция / аналог
OTYPER 0x04 1 0 — push-pull, 1 — open-drain
OSPEEDR 0x08 2 скорость фронта: low / medium / high / very high
PUPDR 0x0C 2 00 — нет, 01 — вверх, 10 — вниз
IDR 0x10 1 входные данные, только чтение
ODR 0x14 1 выходные данные
BSRR 0x18 1 атомарная установка (BS) и сброс (BR)
LCKR 0x1C 1 блокировка конфигурации
AFR[0], AFR[1] 0x20, 0x24 4 номер альтернативной функции AF0…AF15

MODER, поля по два бита:

MODER Режим
00 вход (после сброса, кроме выводов SWD)
01 выход общего назначения
10 альтернативная функция
11 аналоговый

Три отличия, из-за которых код F1 не переносится на F4:

  1. Порты на другой шине. У F1 такт включается в RCC->APB2ENR (IOPxEN), у F4 — в RCC->AHB1ENR (GPIOxEN). Скопировали строку с APB2ENR — порт молчит.
  2. Другие базовые адреса. GPIOA у F1 — 0x4001 0800, у F4 — 0x4002 0000, шаг между портами 0x400. Смещение ODR у F1 — 0x0C, у F4 — 0x14.
  3. Альтернативные функции по-настоящему. У F4 каждый вывод может получить любую из шестнадцати альтернативных функций через AFR[0] (выводы 0–7) и AFR[1] (8–15), по 4 бита на вывод. У F1 такого регистра нет вовсе — там лишь грубый «ремап» готовыми комбинациями через AFIO_MAPR. Это, пожалуй, самое заметное улучшение F4 для разводки платы: периферию можно посадить почти на любые ноги.
/* Назначить PA9 функцию AF7 (USART1_TX) — так это делается только у F4 */
GPIOA->MODER  &= ~GPIO_MODER_MODER9;
GPIOA->MODER  |=  GPIO_MODER_MODER9_1;          /* 10 -> альтернативная функция */
GPIOA->AFR[1] &= ~GPIO_AFRH_AFSEL9;             /* вывод 9 -> AFR[1], биты 7:4 */
GPIOA->AFR[1] |=  (7u << GPIO_AFRH_AFSEL9_Pos); /* AF7 */

⚠️ Номер альтернативной функции не универсален: AF7 — это USART1/2/3 у F407, но у другого камня под тем же номером может быть иная периферия. Таблицу альтернативных функций смотрите в даташите конкретного МК (не в reference manual — там её нет).


8.3 HAL, LL и CubeMX

HAL прячет различия семейств за одной структурой.

typedef struct {
  uint32_t Pin;        // маска выводов: GPIO_PIN_5, или GPIO_PIN_1|GPIO_PIN_5
  uint32_t Mode;       // GPIO_MODE_OUTPUT_PP, GPIO_MODE_INPUT, ...
  uint32_t Pull;       // GPIO_NOPULL | GPIO_PULLUP | GPIO_PULLDOWN
  uint32_t Speed;      // GPIO_SPEED_FREQ_LOW ... _VERY_HIGH
  uint32_t Alternate;  // номер альтернативной функции
} GPIO_InitTypeDef;

Pin — это маска, а не номер: GPIO_PIN_5 равно 0x0020, а не 5. Поле Alternate отсутствует в CubeF1 — у F1 нет регистров AFRL/AFRH, ремап делается через AFIO_MAPR.

Значения Mode, помимо очевидных, включают режимы прерываний и событий: GPIO_MODE_IT_RISING, GPIO_MODE_IT_FALLING, GPIO_MODE_IT_RISING_FALLING (разбираются в разделе 7 про EXTI) и GPIO_MODE_EVT_* — пробуждение ядра по инструкции WFE без вызова обработчика.

Пять функций на всю работу с выводом:

GPIO_PinState HAL_GPIO_ReadPin (GPIO_TypeDef *GPIOx, uint16_t Pin);
void          HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t Pin, GPIO_PinState St);
void          HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t Pin);
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef *GPIOx, uint16_t Pin);
void          HAL_GPIO_DeInit  (GPIO_TypeDef *GPIOx, uint32_t Pin);

ReadPin возвращает GPIO_PIN_RESET или GPIO_PIN_SET. DeInit возвращает вывод в состояние по сбросу (плавающий вход) — полезно перед уходом в сон, чтобы не тратить ток.

Одно и то же тремя способами — зажечь PA5:

/* регистры, F1 */          GPIOA->BSRR = GPIO_BSRR_BS5;
/* LL */                    LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
/* HAL */                   HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);

Все три компилируются в запись в BSRR. HAL добавляет вызов функции, LL — инлайн-обёртку. Выбор — вопрос читаемости и переносимости, а не скорости.


8.4 🔧 Рецепт: мигаем светодиодом

Плата BluePill (STM32F103C8T6), светодиод на PC13.

Светодиод на PC13 подключён катодом к выводу микроконтроллера

#include "stm32f10x.h"

void PORTC_13_INIT (void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;      // такт на порт C — без него порт мёртв

    GPIOC->CRH &= ~GPIO_CRH_MODE13;          // сброс MODE13
    GPIOC->CRH |=  GPIO_CRH_MODE13_1;        // MODE13 = 10 -> выход 2 МГц
    GPIOC->CRH &= ~GPIO_CRH_CNF13;           // CNF13  = 00 -> push-pull
}

int main (void)
{
    PORTC_13_INIT();

    while (1)
    {
        GPIOC->BSRR = GPIO_BSRR_BR13;        // зажечь: светодиод включён катодом

        for (volatile int i = 0; i < 1000000; i++) { }

        GPIOC->BSRR = GPIO_BSRR_BS13;        // погасить

        for (volatile int i = 0; i < 1000000; i++) { }
    }
}

Разбор:

⚠️ Светодиод BluePill горит от нуля. PC13 включён катодом к выводу: BR13 (ноль на выходе) его зажигает, BS13 гасит. А BSRR — регистр только для записи, поэтому в него пишут =, а не |=: чтение даёт 0, и |= лишь добавляет ненужное обращение к шине.

То же на HAL:

__HAL_RCC_GPIOC_CLK_ENABLE();

GPIO_InitTypeDef g = {0};
g.Pin   = GPIO_PIN_13;
g.Mode  = GPIO_MODE_OUTPUT_PP;
g.Pull  = GPIO_NOPULL;
g.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &g);

while (1) {
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    HAL_Delay(500);
}

То же на STM32F4 — Discovery F407, зелёный светодиод LD4 на PD12:

#include "stm32f4xx.h"

void PD12_INIT(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;        /* AHB1, а не APB2 */

    GPIOD->MODER   &= ~GPIO_MODER_MODER12;
    GPIOD->MODER   |=  GPIO_MODER_MODER12_0;    /* 01 -> выход */
    GPIOD->OTYPER  &= ~GPIO_OTYPER_OT12;        /* push-pull */
    GPIOD->OSPEEDR &= ~GPIO_OSPEEDR_OSPEED12;   /* низкая скорость */
    GPIOD->PUPDR   &= ~GPIO_PUPDR_PUPD12;       /* без подтяжек */
}

int main(void)
{
    PD12_INIT();

    while (1)
    {
        GPIOD->BSRR = GPIO_BSRR_BS12;           /* только запись, без |= */
        for (volatile int i = 0; i < 1000000; i++) { }
        GPIOD->BSRR = GPIO_BSRR_BR12;
        for (volatile int i = 0; i < 1000000; i++) { }
    }
}

Для Nucleo-F401RE/F411RE светодиод LD2 сидит на PA5 — меняются только порт и номер вывода, структура кода та же.

Сравните с версией для F1 выше: пять раздельных регистров вместо CRH, AHB1ENR вместо APB2ENR. Логика при этом идентична — включить такт, задать режим, писать в BSRR. Это и есть практический смысл разделения «ядро против периферии» из раздела 1.


8.5 🔧 Рецепт: опрос кнопки

Кнопка на PA0, светодиод на PB2.

Кнопка с подтягивающим резистором к питанию

uint32_t Button_State = 0;

#define LED_PB2_ON()   (GPIOB->BSRR = GPIO_BSRR_BS2)
#define LED_PB2_OFF()  (GPIOB->BSRR = GPIO_BSRR_BR2)

void PINA_0_INIT (void)                      // кнопка PA0
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    GPIOA->CRL &= ~GPIO_CRL_MODE0_0;         // MODE0 = 00 -> вход
    GPIOA->CRL &= ~GPIO_CRL_MODE0_1;

    GPIOA->CRL |=  GPIO_CRL_CNF0_0;          // CNF0 = 01 -> плавающий вход:
    GPIOA->CRL &= ~GPIO_CRL_CNF0_1;          // подтяжка стоит на плате
}

void PINB_2_INIT (void)                      // светодиод PB2
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    GPIOB->CRL &= ~GPIO_CRL_MODE2_0;         // MODE2 = 10 -> выход 2 МГц
    GPIOB->CRL |=  GPIO_CRL_MODE2_1;

    GPIOB->CRL &= ~GPIO_CRL_CNF2_0;          // CNF2 = 00 -> push-pull
    GPIOB->CRL &= ~GPIO_CRL_CNF2_1;
}

int main (void)
{
    PINB_2_INIT();
    PINA_0_INIT();

//  SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);
//  CLEAR_BIT(AFIO->MAPR, AFIO_MAPR_SWJ_CFG);
//  SET_BIT(AFIO->MAPR, AFIO_MAPR_SWJ_CFG_JTAGDISABLE);

    while (1)
    {
        Button_State = READ_BIT(GPIOA->IDR, GPIO_IDR_IDR0);

        if (Button_State == 1)
            {
             LED_PB2_ON();
            }
        else
            {
               LED_PB2_OFF();
            }
    }
}

⚠️ READ_BIT возвращает маску, а не единицу. Здесь сравнение с 1 работает только потому, что GPIO_IDR_IDR0 равен 0x0001. Перенесите кнопку на PA5 — и READ_BIT вернёт 0x0020, условие == 1 никогда не выполнится, а код будет выглядеть правильным. И не кладите маску в uint8_t: для выводов 8–15 она обрежется до нуля, и не сработает даже != 0 — поэтому Button_State здесь 32-битная. Надёжнее так:

c if (READ_BIT(GPIOA->IDR, GPIO_IDR_IDR5) != 0) { ... }

⚠️ Плавающий вход под кнопкой держит неопределённый уровень, пока кнопка не нажата, — вывод ловит наводки. Нужна подтяжка: внешним резистором либо CNF0 = 10 плюс соответствующий бит в ODR (см. граблю в 8.2).

Дребезг. Механический контакт при нажатии и отпускании даёт пачку импульсов длиной единицы–десятки миллисекунд. В опросном цикле это видно как многократное срабатывание. Лечится программной задержкой после первого фронта, фиксацией состояния по двум совпавшим замерам через 10–20 мс либо RC-цепочкой на плате:

Подавление дребезга RC-цепочкой

Обработка кнопки по прерыванию — раздел 7 (EXTI).


8.6 F0 и G0: регистры как у F4, плюс BRR

Порты F0 и G0 устроены как у F4: MODER, OTYPER, OSPEEDR, PUPDR, AFR, и BSRR на том же смещении 0x18. Сверху добавлен регистр BRR на 0x28 — сброс вывода записью единицы, отдельно от BSRR. Отличий три, и каждое ломает перенос кода:

F0 G0
такт порта RCC->AHBENR RCC->IOPENR
MODER порта A после сброса 0x28000000 — входы 0xEBFFFFFF — аналоговые
имена битов GPIO_MODER_MODER5, GPIO_BSRR_BS_5, GPIO_IDR_0 GPIO_MODER_MODE5, GPIO_BSRR_BS5, GPIO_IDR_ID0

Значения после сброса — из файлов описания периферии (SVD) в паках. У порта B на G0 MODER после сброса — 0xFFFFFFFF: все выводы аналоговые.

Вход на G0 надо включить явно. Аналоговый режим (MODER = 11) бережёт ток, но цифровой вход в нём отключён — IDR читает ноль, что бы ни было на выводе. Код F4, который полагался на «после сброса — вход», на G0 видит кнопку всегда нажатой:

RCC->IOPENR |= RCC_IOPENR_GPIOAEN;                          /* порты у G0 — регистр IOPENR */
GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODE5) | GPIO_MODER_MODE5_0;     /* 01 — выход */
GPIOA->MODER &= ~GPIO_MODER_MODE0;                          /* 00 — вход: после сброса там 11 */
GPIOA->PUPDR |= GPIO_PUPDR_PUPD0_0;

Имена битов разные. Заголовок G0 называет поля по-новому, и код для F0 на нём не собирается: «use of undeclared identifier 'GPIO_MODER_MODER5'». Тот же светодиод на F0:

RCC->AHBENR |= RCC_AHBENR_GPIOAEN;                          /* порты у F0 — на шине AHB */
GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODER5) | GPIO_MODER_MODER5_0;   /* 01 — выход */
GPIOA->BSRR = GPIO_BSRR_BS_5;
GPIOA->BRR = GPIO_BRR_BR_5;                                 /* то же, что BSRR = BR_5 */

Переносимый путь — не имена отдельных бит, а маски по номеру вывода (3u << (2u * pin) для MODER) или библиотека LL: её функции называются одинаково на обеих сериях.


8.7 Тот же вывод на 1986ВЕ9х

Те же задачи на российском Cortex-M3 «Миландр» решаются идентично по смыслу: включить тактирование порта, задать режим, писать в регистр данных. Отличаются имена регистров и то, что настройка вывода задаётся не парой MODE/CNF, а отдельными регистрами OE, FUNC, ANALOG, PULL, PWR.

Практическая ценность здесь не в различиях API, а в том, что схема рассуждения переносится между разными Cortex-M без изменений.


8.8 Упражнения

Восемь светодиодов LED1..LED8 на выводах одного порта BluePill. Каждый шаг любого упражнения — одна запись в ODR или BSRR, без чтения порта.

  1. «Бегущий огонёк»: один светодиод, слева направо.
  2. «Бегущий огонёк» из трёх светодиодов подряд.
  3. «Накопление» слева направо: зажигаются по одному и остаются гореть.
  4. «Схлопывание»: пары с краёв по очереди сходятся к середине (LED1+LED8, LED2+LED7, …), затем в обратном порядке гаснут.

Ещё одно — на логические выражения: реализовать вывод, состояние которого равно логической функции от двух кнопок (И, ИЛИ, исключающее ИЛИ), не используя ветвления — только битовые операции над IDR и BSRR.


8.9 Грабли

Пример с MODER не собирается под F1, пример с CRL — под F4. GPIO у F1 и у остальных семейств устроены по-разному (8.2).

Вход с подтяжкой тянет вниз, хотя об этом ни слова. У F1 направление подтяжки задаёт бит в ODR (8.2).

После «освобождения» PA13/PA14 плата не прошивается. Выводы SWD отданы под другое — отключать JTAG/SWD только осознанно (8.2).

Альтернативная функция не та. Номер AF у разных камней значит разное; таблица — в даташите конкретного кристалла (8.2).

Светодиод BluePill горит наоборот. Он включён катодом к PC13: BS13 гасит, BR13 зажигает (8.4).

Кнопка на другом выводе перестала читаться. READ_BIT вернул маску, а код сравнивал с 1 — или маску положили в uint8_t, и у выводов 8–15 она обрезалась до нуля (8.5).

Кнопка «нажимается» сама. Плавающий вход без подтяжки ловит наводки (8.5).


8.10 Итоги

Полные исходники

gpio_f0.c — светодиод PA5 и кнопка PA0 на STM32F072.

/* Раздел 8.6: светодиод PA5 и кнопка PA0 на STM32F072.
 * Регистры — как у F4 (MODER, OTYPER, OSPEEDR, PUPDR) плюс BRR; такт портов — на AHB. */
#include <stddef.h>
#include "stm32f0xx.h"

_Static_assert(offsetof(GPIO_TypeDef, MODER) == 0x00u && offsetof(GPIO_TypeDef, BSRR) == 0x18u, "как у F4");
_Static_assert(offsetof(GPIO_TypeDef, BRR) == 0x28u, "BRR — сброс вывода записью единицы, у F4 его нет");

void led_init_f0(void)
{
    RCC->AHBENR |= RCC_AHBENR_GPIOAEN;                          /* порты у F0 — на шине AHB */
    GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODER5) | GPIO_MODER_MODER5_0;   /* 01 — выход */
    GPIOA->PUPDR |= GPIO_PUPDR_PUPDR0_0;                        /* PA0 — вход с подтяжкой вверх */
}

void led_on_f0(void)
{
    GPIOA->BSRR = GPIO_BSRR_BS_5;
}

void led_off_f0(void)
{
    GPIOA->BRR = GPIO_BRR_BR_5;                                 /* то же, что BSRR = BR_5 */
}

uint32_t button_f0(void)
{
    return (GPIOA->IDR & GPIO_IDR_0) == 0u;                     /* нажата — вывод на земле */
}

gpio_g0.c — то же на STM32G071: вход после сброса аналоговый, его включают явно.

/* Раздел 8.6: светодиод PA5 и кнопка PA0 на STM32G071.
 * После сброса выводы G0 — аналоговые (MODER = 11): вход надо включить явно. */
#include <stddef.h>
#include "stm32g0xx.h"

_Static_assert(offsetof(GPIO_TypeDef, BRR) == 0x28u, "BRR на том же месте, что у F0");

void led_init_g0(void)
{
    RCC->IOPENR |= RCC_IOPENR_GPIOAEN;                          /* порты у G0 — регистр IOPENR */
    GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODE5) | GPIO_MODER_MODE5_0;     /* 01 — выход */
    GPIOA->MODER &= ~GPIO_MODER_MODE0;                          /* 00 — вход: после сброса там 11 */
    GPIOA->PUPDR |= GPIO_PUPDR_PUPD0_0;
}

void led_on_g0(void)
{
    GPIOA->BSRR = GPIO_BSRR_BS5;
}

void led_off_g0(void)
{
    GPIOA->BRR = GPIO_BRR_BR5;
}

uint32_t button_g0(void)
{
    return (GPIOA->IDR & GPIO_IDR_ID0) == 0u;
}

Дальше: раздел 9 «Прямой доступ к памяти (DMA)».

Открыть в интерактивном справочнике