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
Толерантность к 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, порты 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 и значений сброса
Блокировка конфигурации (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:
- Порты на другой шине. У F1 такт включается в
RCC->APB2ENR(IOPxEN), у F4 — вRCC->AHB1ENR(GPIOxEN). Скопировали строку сAPB2ENR— порт молчит. - Другие базовые адреса.
GPIOAу F1 —0x4001 0800, у F4 —0x4002 0000, шаг между портами0x400. СмещениеODRу F1 —0x0C, у F4 —0x14. - Альтернативные функции по-настоящему. У 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.
#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++) { }
}
}
Разбор:
PC13— вывод старшей половины порта, поэтомуCRH, а неCRL.- Такт на порт включается всегда и отдельно, даже если ядро уже тактируется
на 72 МГц. Забыть
RCC->APB2ENR— самая частая причина «код правильный, а ножка молчит». IOPCENрасшифровывается как Input/Output Port C ENable.- Задержка пустым циклом — заглушка: реальную дают SysTick или таймер (разделы 7 и 13).
Счётчик объявлен
volatile: без него оптимизатор выбросит пустой цикл целиком (раздел 3.4), и светодиод вместо мигания будет гореть вполсилы.
⚠️ Светодиод 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-цепочкой на плате:
Обработка кнопки по прерыванию — раздел 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, без чтения порта.
- «Бегущий огонёк»: один светодиод, слева направо.
- «Бегущий огонёк» из трёх светодиодов подряд.
- «Накопление» слева направо: зажигаются по одному и остаются гореть.
- «Схлопывание»: пары с краёв по очереди сходятся к середине (
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 Итоги
- Порт не работает, пока не включён его такт в
RCC. - После сброса все выводы — плавающие входы,
CRL/CRH=0x44444444. - Меняйте состояние выхода через
BSRR/BRR, а не черезODR. - У F1 конфигурация в
CRL/CRH(4 бита на вывод), у остальных семейств — вMODER+OTYPER+OSPEEDR+PUPDR. Код не переносится между ними без правок. - Скорость вывода — про электромагнитную совместимость, а не про производительность.
PA13/PA14заняты SWD; освобождая их, вы теряете отладку.
Полные исходники
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)».