7. Прерывания и исключения
Опрашивать кнопку в цикле — рабочий способ ровно до тех пор, пока в программе одна кнопка. Как только дел становится несколько, опрос превращается в клубок проверок, каждая из которых крадёт время у остальных. Прерывания решают это иначе: процессор занимается своим делом, а железо само дёргает его, когда случилось событие.
7.1 Опрос против прерывания
/* опрос: проверяем сами, тратим такты, можем пропустить короткое событие */
while (1) {
if (GPIOA->IDR & GPIO_IDR_IDR0) { /* ... */ }
if (USART1->SR & USART_SR_RXNE) { /* ... */ }
if (ADC1->SR & ADC_SR_EOC) { /* ... */ }
}
/* прерывание: железо позовёт само, ровно когда нужно */
void EXTI0_IRQHandler(void) { /* ... */ }
void USART1_IRQHandler(void) { /* ... */ }
Прерывание — асинхронное событие, которое останавливает текущий код и передаёт управление обработчику. Сохранением контекста (регистры, адрес возврата) занимается само железо: у Cortex-M3 и M4 вход в обработчик занимает 12 тактов (плюс ожидание Flash), у M0 — 16, у M0+ — 15, и ни одной команды программы за это время не выполняется.
Терминология ARM. Прерывание приходит от железа, исключение порождается самим процессором — например, обращением по несуществующему адресу. В ARM прерывание считается частным случаем исключения.
Именно на прерываниях построена вытесняющая многозадачность: без аппаратной поддержки невозможно переключать контексты, не теряя ход выполнения. Отсюда и ОСРВ (часть IV).
7.2 Таблица векторов и NVIC
В самом начале Flash лежит таблица векторов — массив адресов обработчиков.
Первая запись не адрес, а начальное значение указателя стека; дальше идут
Reset_Handler, обработчики системных исключений, а за ними — векторы
периферии.
Когда происходит событие, процессор берёт из таблицы адрес нужного обработчика и передаёт ему управление. Связывание происходит по имени функции: стартовый файл объявляет слабые заглушки, а ваша функция с тем же именем их замещает.
⚠️ Опечатка в имени обработчика не вызывает ошибки компиляции. Напишете
EXTI0_IRQHanderвместоEXTI0_IRQHandler— проект соберётся, но прерывание уйдёт в пустую заглушку, обычно в бесконечный цикл. Никакой диагностики вы не получите; сверяйте имена со стартовым файлом.
NVIC (Nested Vectored Interrupt Controller) — блок внутри ядра, который решает, какое прерывание пустить первым и можно ли прервать текущий обработчик. «Nested» означает вложенность: обработчик с высоким приоритетом прерывает обработчик с низким.
7.3 Приоритеты
Как это называется в документации
Оригинал Перевод nested vectored interrupt controller контроллер вложенных прерываний interrupt service routine (ISR) обработчик прерывания vector table таблица векторов pending flag флаг ожидания обслуживания pre-emption priority приоритет вытеснения subpriority подприоритет to mask / unmask an interrupt замаскировать / размаскировать rising / falling edge trigger запуск по нарастающему / спадающему фронту interrupt mask register ( IMR)регистр маски прерываний tail-chaining сцепление обработчиков без выхода в основной код
У STM32 под приоритет отведено 4 бита — 16 уровней, причём меньшее число означает более высокий приоритет. Ноль — самый срочный.
Четыре бита делятся на две части:
- приоритет вытеснения — может ли это прерывание прервать выполняющееся;
- подприоритет — кого пустить первым, если два события ждут одновременно и приоритет вытеснения у них одинаковый.
Соотношение задаётся группировкой:
NVIC_SetPriorityGrouping(3); /* PRIGROUP = 3: все 4 бита — вытеснение; в HAL это NVIC_PRIORITYGROUP_4 */
NVIC_SetPriority(EXTI0_IRQn, 5);
NVIC_EnableIRQ(EXTI0_IRQn);
Функция CMSIS принимает номер поля PRIGROUP, а не число бит вытеснения:
3 — четыре бита вытеснения, 4 — три бита вытеснения и один бит подприоритета.
Константа NVIC_PRIORITYGROUP_4 (её значение 3) объявлена в заголовке HAL,
и в проекте на одних заголовках CMSIS строка с ней не соберётся.
Практический совет: не раздавайте приоритеты «по важности задачи». Раздавайте по срочности реакции. Обработчик, который обязан уложиться в микросекунды (захват таймера, приём байта), получает высокий приоритет; тот, где важен сам факт события (нажатие кнопки), — низкий.
⚠️ Если вы работаете под FreeRTOS, у прерываний, из которых вызываются функции ОС, приоритет обязан быть численно не меньше порога (то есть логически не срочнее его). Порог в
FreeRTOSConfig.hзаписан дважды:configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(например, 5) — это число дляNVIC_SetPriority, аconfigMAX_SYSCALL_INTERRUPT_PRIORITY— то же число, сдвинутое в старшие биты байта приоритета:5 << 4=0x50. Нарушение приводит к редким и трудновоспроизводимым отказам. Подробно — в разделе 28.
7.4 SysTick
Единственный таймер, встроенный в само ядро, а не в периферию ST. Поэтому он есть у любого Cortex-M и код с ним переносится без правок.
SysTick_Config(SystemCoreClock / 1000u); /* прерывание каждую миллисекунду */
volatile uint32_t ms_ticks = 0;
void SysTick_Handler(void)
{
ms_ticks++;
}
void delay_ms(uint32_t ms)
{
uint32_t start = ms_ticks;
while ((ms_ticks - start) < ms) { }
}
Именно на нём построен HAL_Delay, и он же обычно даёт такт планировщику ОСРВ.
Вычитание
ms_ticks - startвместо сравненияms_ticks < start + msвыбрано не случайно: беззнаковая арифметика корректно переживает переполнение счётчика, а сравнение с суммой — нет.
Что делает SysTick_Config. Это несколько записей в регистры ядра:
LOAD — с какого числа считать вниз (на единицу меньше периода), VAL —
текущее значение (любая запись обнуляет счётчик), CTRL — управление:
ENABLE (бит 0) запускает счёт, TICKINT (бит 1) разрешает прерывание при
переходе через ноль, CLKSOURCE (бит 2) берёт такт ядра, а не делённый на 8.
Для 1 мс при 72 МГц функция пишет LOAD = 71999 и CTRL = 7 — все три бита.
Счётчик 24-разрядный. В LOAD помещается не больше 0xFFFFFF, то есть
период не длиннее 16 777 216 тактов: при 72 МГц это 233 мс, при 168 МГц —
99,9 мс. Секундный тик так не получить, и функция об этом не кричит — она
просто возвращает 1 и ничего не включает. В прогоне на ядре Cortex-M4 с
настоящей SysTick_Config из CMSIS число 16 777 216 прошло, 16 777 217 и
72 000 000 вернули 1, а CTRL остался нулём. Результат стоит проверять:
if (SysTick_Config(SystemCoreClock / 1000u) != 0u) {
/* период не влез в 24 бита — SysTick не запущен */
}
Ту же арифметику делает вкладка «SysTick» в калькуляторах приложения:
такт и частота прерываний — LOAD, период на деле и самый долгий период,
который помещается в 24 бита при этом такте.
COUNTFLAG сбрасывается чтением. Бит 16 регистра CTRL взводится, когда
счётчик прошёл через ноль, и сбрасывается при чтении CTRL программой и при
записи в VAL. Два чтения подряд в прогоне дали 1 и 0. Чтение отладчиком
флаг обычно не трогает: отладочный порт помечает свои обращения битом
MasterType, и такое чтение COUNTFLAG не сбрасывает. А вот любая лишняя
строка программы, читающая CTRL, флаг съедает.
7.5 EXTI: как сигнал доходит до обработчика
Внешние прерывания от выводов проходят через отдельный блок — контроллер EXTI. Путь состоит из нескольких ступеней, и пропуск любой означает, что прерывание просто не придёт.
Важное ограничение. Для выводов у EXTI шестнадцать линий — по числу
выводов в порту (остальные линии внутренние: PVD, будильник RTC, пробуждение USB).
Линия с номером n обслуживает вывод n одного из портов: EXTI0 — это
PA0, или PB0, или PC0, но не два сразу.
Вторая особенность — векторов меньше, чем линий:
| Линии | Вектор |
|---|---|
| 0, 1, 2, 3, 4 | свой у каждой: EXTI0_IRQn…EXTI4_IRQn |
| 5…9 | один общий: EXTI9_5_IRQn |
| 10…15 | один общий: EXTI15_10_IRQn |
В общем обработчике придётся выяснять, какая именно линия сработала, читая
флаги EXTI->PR.
Два выхода одного детектора. Фронт на выводе ловит одна схема, а выходов у неё два, и разрешаются они разными регистрами:
| Регистр | Куда уходит сигнал | Что происходит |
|---|---|---|
EXTI->IMR |
в NVIC | прерывание встаёт в очередь, ядро входит в обработчик |
EXTI->EMR |
прямо в ядро | ядро получает событие: обработчика нет, программа просто продолжается |
Биты независимы — можно включить обе маски сразу: в опыте ниже после
IMR |= 1 и EMR |= 1 оба регистра читаются как 0x00000001.
Во что обходится обработчик. Перед входом в него ядро само кладёт на стек
восемь слов — R0–R3, R12, LR, адрес возврата и xPSR. Это видно прямо
в программе: главная запоминает указатель стека, обработчик — свой, разница
получается 32 байта. Ни одной команды обработчика к этому моменту ещё не
выполнено.
Зачем нужно событие. Ядро умеет ждать двумя командами: WFI — до
прерывания, WFE — до события. Событие будит ядро, не открывая обработчика:
программа идёт дальше со следующей команды после WFE. Ни стека, ни вектора,
ни сброса флага — поэтому «проснуться и продолжить свой цикл» стоит дешевле,
чем «войти в обработчик, выйти и вернуться к тому же месту».
Тот же эффект другим путём. Прерывание, разрешённое в NVIC, но закрытое
PRIMASK, никуда не пропадает: оно остаётся в очереди. В том же опыте после
cpsid i счётчик входов не изменился — обработчик не выполнялся, — а в
NVIC->ISPR горел бит 6 (0x00000040), то есть прерывание ждёт. После
cpsie i обработчик вошёл, счётчик стал 2, очередь очистилась. Висящее
прерывание будит WFI даже при закрытом PRIMASK, а WFE — только если
в SCB->SCR поднят бит SEVONPEND (раздел 10.2): тогда каждое прерывание,
ставшее ожидающим, считается событием. Этим и пользуются, когда нужно
проснуться, но обработать не сразу и в своём порядке.
| Что нужно | Чем сделать |
|---|---|
| отреагировать на фронт кодом | прерывание: IMR |
| проснуться и продолжить свой цикл | событие: EMR и WFE |
| разбудить, но разобрать позже и в своём порядке | прерывание, закрытое PRIMASK, и WFI (или WFE с SEVONPEND) |
⚠️ Один
WFE— не ожидание. У ядра есть признак «событие было», и если он уже взведён, команда возвращается сразу, ничего не подождав. ПоэтомуWFEставят в цикл, который проверяет настоящее условие — флаг, счётчик, очередь, — а не считают, что послеWFEусловие непременно выполнено.
7.6 🔧 Рецепт: кнопка по прерыванию
Та же задача, что решалась опросом в разделе 8, — теперь без цикла ожидания.
STM32F103:
volatile uint8_t button_pressed = 0; /* volatile обязателен */
void EXTI0_Config(void)
{
/* Такт на порт A и на AFIO — без AFIO выбор линии не запишется. */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
/* PA0 — вход с подтяжкой вверх (у F1 направление задаётся битом в ODR). */
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
GPIOA->CRL |= GPIO_CRL_CNF0_1;
GPIOA->ODR |= GPIO_ODR_ODR0;
/* Привязка линии EXTI0 к порту A. */
AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;
AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI0_PA;
/* Спадающий фронт: кнопка замыкает вывод на землю. */
EXTI->IMR |= EXTI_IMR_MR0;
EXTI->FTSR |= EXTI_FTSR_TR0;
EXTI->RTSR &= ~EXTI_RTSR_TR0;
NVIC_SetPriority(EXTI0_IRQn, 5);
NVIC_EnableIRQ(EXTI0_IRQn);
}
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & EXTI_PR_PR0) {
EXTI->PR = EXTI_PR_PR0; /* сброс флага — записью ЕДИНИЦЫ */
button_pressed = 1; /* вся работа — в основном цикле */
}
}
STM32F407 — отличие в двух местах:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; /* порты на AHB1, а не APB2 */
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; /* SYSCFG вместо AFIO */
GPIOA->MODER &= ~GPIO_MODER_MODER0;
GPIOA->PUPDR &= ~GPIO_PUPDR_PUPD0;
GPIOA->PUPDR |= GPIO_PUPDR_PUPD0_0; /* честный регистр подтяжек */
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
Остальное — маски, фронты, NVIC, обработчик — совпадает дословно.
⚠️ Самая частая ошибка при переносе с F1 на F4 — убрать строку с
AFIO(под F4 она и не соберётся: такого блока нет) и не добавить тактSYSCFG. Без него запись вEXTICRпросто не проходит. Линия остаётся привязанной к порту A по умолчанию: кнопка наPB0молчит, аPA0срабатывает от наводок.
7.7 Каким должен быть обработчик
Коротким. Пока работает обработчик, всё остальное ждёт. Правило простое: внутри — только то, что нельзя отложить. Взвели флаг, положили байт в буфер, переключили ножку — и вышли. Разбор данных, вычисления, работа с дисплеем делаются в основном цикле.
Без задержек. HAL_Delay внутри обработчика — верный способ подвесить
систему: он сам ждёт прерывания SysTick, а оно может иметь более низкий
приоритет и не получить управление никогда.
Общие переменные — только volatile. Флаг, который взводится в обработчике
и проверяется в main, обязан быть volatile, иначе компилятор оптимизирует
проверку (разбор с ассемблером — в разделе 3).
Флаг сбрасывать обязательно. Не сбросили EXTI->PR — обработчик будет
вызываться сразу после выхода, снова и снова, и программа больше никогда
не вернётся в основной цикл. Причём сброс делается записью единицы, а не
нуля: запись нуля не делает ничего. Сбрасывают флаг в начале обработчика,
а не последней строкой: запись доходит до периферии за несколько тактов, и если
ядро выйдет раньше, NVIC увидит тот же запрос и вызовет обработчик ещё раз.
Если сброс всё-таки стоит в конце, после него ставят __DSB() или читают
регистр обратно.
Атомарность. Если основной код и обработчик меняют одну переменную,
одного volatile мало: counter++ состоит из трёх операций, и прерывание
может вклиниться посередине. Нужна критическая секция или атомарная операция
(раздел 5).
7.8 F0 и G0: ядро Cortex-M0 и M0+
У STM32F0 ядро Cortex-M0, у G0 — Cortex-M0+. Векторы, EXTI и правила для обработчиков из этого раздела верны и для них, но ядро проще, и при переносе кода с F103 или F407 натыкаются на одни и те же отличия:
| F1, F4 (M3, M4) | F0 (M0) | G0 (M0+) | |
|---|---|---|---|
| бит приоритета | 4 — 16 уровней | 2 — 4 уровня | 2 — 4 уровня |
| подприоритеты | есть | нет | нет |
BASEPRI |
есть | нет | нет |
перенос таблицы, VTOR |
есть | нет | есть |
деление UDIV |
есть | нет | нет |
| выбор порта для линии | AFIO (F1), SYSCFG (F4) |
SYSCFG |
в самом EXTI |
| флаг линии | PR |
PR |
RPR1 — фронт, FPR1 — спад |
Приоритеты. Уровней четыре, от 0 до 3, и группировки на вытеснение
и подприоритет нет вовсе. NVIC_SetPriority(EXTI0_1_IRQn, 1) пишет в байт
приоритета 0x40: значимы только два старших бита. Вызов
NVIC_SetPriorityGrouping из кода для F4 соберётся, но ничего не сделает:
в заголовке ядра M0 это пустая заглушка (void)(X).
Критическая секция — только целиком. BASEPRI, которым M3 и M4 закрывают
лишь младшие приоритеты, у M0 и M0+ нет, и __set_BASEPRI компилятор для F0
просто не знает. Остаётся PRIMASK — выключить все прерывания разом:
uint32_t primask = __get_PRIMASK();
__disable_irq();
uint32_t n = presses;
presses = 0;
__set_PRIMASK(primask);
В объектном файле это MRS PRIMASK, CPSID i и MSR PRIMASK — три команды.
Порт FreeRTOS для Cortex-M0 устроен так же: прерывания в критической секции
закрываются все, настройка configMAX_SYSCALL_INTERRUPT_PRIORITY на нём не
действует.
Общие векторы EXTI. Линии 0 и 1 делят вектор EXTI0_1_IRQn (5), 2 и 3 —
EXTI2_3_IRQn (6), а линии 4…15 — один EXTI4_15_IRQn (7). В обработчике
общего вектора проверяют флаг каждой линии.
F0: порт выбирается в SYSCFG, как у F4. Кнопка на PA0:
RCC->AHBENR |= RCC_AHBENR_GPIOAEN; /* порты у F0 — на AHB */
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGCOMPEN; /* такт SYSCFG — без него выбор порта не запишется */
SYSCFG->EXTICR[0] = (SYSCFG->EXTICR[0] & ~SYSCFG_EXTICR1_EXTI0) | SYSCFG_EXTICR1_EXTI0_PA;
EXTI->FTSR |= EXTI_FTSR_TR0; /* по спаду */
EXTI->IMR |= EXTI_IMR_MR0;
NVIC_SetPriority(EXTI0_1_IRQn, 1); /* уровни 0…3 */
G0: порт выбирается в самом EXTI, и флагов два. SYSCFG для этого не
нужен; поле порта — 3 бита, но идёт через каждые 8. Спад и фронт отмечаются
в разных регистрах, и обработчик должен смотреть в тот, который включён:
RCC->IOPENR |= RCC_IOPENR_GPIOAEN; /* порты у G0 — отдельный регистр IOPENR */
EXTI->EXTICR[0] &= ~EXTI_EXTICR1_EXTI0; /* 0 — порт A; выбор порта живёт в EXTI */
EXTI->FTSR1 |= EXTI_FTSR1_FT0;
EXTI->IMR1 |= EXTI_IMR1_IM0;
void EXTI0_1_IRQHandler(void)
{
if (EXTI->FPR1 & EXTI_FPR1_FPIF0) { /* спад — свой регистр FPR1, фронт — RPR1 */
EXTI->FPR1 = EXTI_FPR1_FPIF0;
presses++;
}
}
Таблица векторов в ОЗУ. У G0 (M0+) её переносит SCB->VTOR, как у F4.
У F0 такого регистра нет — SCB->VTOR не собирается: «no member named
'VTOR'». Загрузчик на F0 копирует таблицу в начало ОЗУ и отображает ОЗУ
на нулевой адрес битами MEM_MODE в SYSCFG->CFGR1.
Деления нет в железе. sum / count на M0 и M0+ — вызов библиотечной
__aeabi_uidiv, а не одна команда UDIV, как на M3. В обработчике
прерывания делят на степень двойки — сдвигом.
7.9 Грабли
Прерывание не приходит. Проверьте по цепочке: включён ли такт порта;
включён ли такт AFIO (F1) или SYSCFG (F4); привязана ли линия к нужному
порту; размаскирована ли линия в IMR; выбран ли фронт в RTSR/FTSR;
разрешено ли прерывание в NVIC. Пропуск любого шага даёт один и тот же
результат — тишину.
Обработчик вызывается бесконечно. Забыт сброс флага EXTI->PR.
EXTI->PR |= EXTI_PR_PR0 теряет чужие прерывания. Биты PR
сбрасываются записью единицы, а запись нуля их не трогает. Оператор |=
сначала читает регистр целиком: если в этот момент ждёт ещё, скажем,
линия 2, её бит прочитается единицей, единицей же запишется обратно — и
флаг линии 2 сбросится, хотя её обработчик ещё не вызывался. Прерывание
пропадает без следа. Сбрасывать нужно присваиванием, только свой бит:
EXTI->PR = EXTI_PR_PR0; /* верно: сбрасывается только линия 0 */
EXTI->PR |= EXTI_PR_PR0; /* ошибка: сбрасываются все ждущие линии */
В модели регистра ждут линии 0 и 2: после |= в нём остаётся 0x00,
после присваивания — 0x04, и линия 2 дожидается своего обработчика.
В документации такие биты помечены rc_w1 — «читается, сбрасывается
записью единицы»; с ними |= не годится никогда.
Дребезг превращается в десяток прерываний. Механическая кнопка на фронте даёт пачку импульсов, и обработчик отработает на каждом. Лечится так же, как при опросе: программной выдержкой, проверкой состояния через 10–20 мс или RC-цепочкой (схема — в разделе 8).
HardFault. Обращение по несуществующему адресу, неверный указатель или
переход по чётному адресу приводят к исключению HardFault, и по умолчанию —
в бесконечный цикл. Деление на ноль у M3 и M4 исключения не даёт: SDIV
и UDIV молча возвращают 0, пока не включён бит DIV_0_TRP (7б.7).
Переполнение стека сначала тихо портит соседние переменные (раздел 11),
а отказ бывает, только когда указатель стека уходит за пределы ОЗУ.
Полезно с самого начала написать
собственный обработчик, который хотя бы мигает светодиодом:
void HardFault_Handler(void)
{
while (1) {
GPIOC->ODR ^= GPIO_ODR_ODR13;
for (volatile int i = 0; i < 200000; i++) { }
}
}
Так вы отличите «зависло где-то в логике» от «упало в исключение» — разбор причин HardFault по содержимому стека разбирается в разделе 35.
Приоритет обработчика выше системного. Обработчик с приоритетом 0, который
работает долго, блокирует SysTick — и HAL_Delay в основном коде перестаёт
считать время.
7.10 Итоги
- Прерывание позволяет не опрашивать железо: процессор занят делом, а событие приходит само.
- Связывание обработчика — по имени функции; опечатка не вызывает ошибки компиляции, прерывание просто уходит в пустую заглушку.
- У STM32 16 уровней приоритета, меньшее число — срочнее. Раздавайте их по требуемой скорости реакции, а не по важности задачи.
- SysTick встроен в ядро и переносится между любыми Cortex-M.
- Линий EXTI шестнадцать, и линия n обслуживает вывод n только одного порта. Линии 5…9 и 10…15 делят общий вектор.
- У F1 порт для линии выбирается через
AFIO, у F4 — черезSYSCFG; такт на этот блок нужен обязательно. - Флаг
EXTI->PRсбрасывается записью единицы. Иначе обработчик зациклится. - В обработчике — минимум работы, никаких задержек, общие переменные
volatile. - F0 и G0: 4 уровня приоритета, нет
BASEPRIи деления в железе; у F0 нетVTOR, у G0 порт линии выбирается в самомEXTI, флаги —RPR1иFPR1.
Полные исходники
irq_f0.c — кнопка PA0 по прерыванию на STM32F072 (Cortex-M0).
/* Раздел 7.8: кнопка PA0 по прерыванию на STM32F072 (Cortex-M0).
* Порт для линии EXTI выбирается в SYSCFG, как у F4; такт — SYSCFGCOMPEN. */
#include "stm32f0xx.h"
_Static_assert(__NVIC_PRIO_BITS == 2, "у Cortex-M0 два бита приоритета — 4 уровня");
_Static_assert(((1u << (8u - __NVIC_PRIO_BITS)) & 0xFFu) == 0x40u, "приоритет 1 — это 0x40 в байте IPR");
_Static_assert(EXTI0_1_IRQn == 5 && EXTI2_3_IRQn == 6 && EXTI4_15_IRQn == 7, "общие векторы EXTI");
_Static_assert(SYSCFG_EXTICR1_EXTI0 == 0xFu && SYSCFG_EXTICR1_EXTI0_PA == 0u, "порт — поле из 4 бит, A = 0");
volatile uint32_t presses;
void button_irq_init_f0(void)
{
RCC->AHBENR |= RCC_AHBENR_GPIOAEN; /* порты у F0 — на AHB */
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGCOMPEN; /* такт SYSCFG — без него выбор порта не запишется */
GPIOA->PUPDR |= GPIO_PUPDR_PUPDR0_0; /* подтяжка вверх, кнопка на землю */
SYSCFG->EXTICR[0] = (SYSCFG->EXTICR[0] & ~SYSCFG_EXTICR1_EXTI0) | SYSCFG_EXTICR1_EXTI0_PA;
EXTI->FTSR |= EXTI_FTSR_TR0; /* по спаду */
EXTI->IMR |= EXTI_IMR_MR0;
NVIC_SetPriority(EXTI0_1_IRQn, 1); /* уровни 0…3 */
NVIC_EnableIRQ(EXTI0_1_IRQn);
}
void EXTI0_1_IRQHandler(void) /* один вектор на линии 0 и 1 */
{
if (EXTI->PR & EXTI_PR_PR0) {
EXTI->PR = EXTI_PR_PR0; /* сброс записью единицы */
presses++;
}
}
/* Критическая секция: BASEPRI у Cortex-M0 нет — только PRIMASK, то есть все прерывания сразу. */
uint32_t take_presses(void)
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
uint32_t n = presses;
presses = 0;
__set_PRIMASK(primask);
return n;
}
/* Деление: у Cortex-M0 и M0+ нет команды UDIV — компилятор зовёт библиотечную функцию. */
uint32_t average(uint32_t sum, uint32_t count)
{
return sum / count;
}
irq_g0.c — кнопка PA0 по прерыванию на STM32G071 (Cortex-M0+).
/* Раздел 7.8: кнопка PA0 по прерыванию на STM32G071 (Cortex-M0+).
* Порт выбирается в самом EXTI, SYSCFG не нужен; флаги спада и фронта раздельные. */
#include "stm32g0xx.h"
_Static_assert(__NVIC_PRIO_BITS == 2 && __VTOR_PRESENT == 1, "M0+: 4 уровня приоритета, но VTOR есть");
_Static_assert(EXTI0_1_IRQn == 5 && EXTI2_3_IRQn == 6 && EXTI4_15_IRQn == 7, "общие векторы EXTI — как у F0");
_Static_assert(EXTI_EXTICR1_EXTI0 == 0x7u && EXTI_EXTICR1_EXTI1_Pos == 8u, "порт — поле из 3 бит через каждые 8");
volatile uint32_t presses;
void button_irq_init_g0(void)
{
RCC->IOPENR |= RCC_IOPENR_GPIOAEN; /* порты у G0 — отдельный регистр IOPENR */
GPIOA->PUPDR |= GPIO_PUPDR_PUPD0_0;
EXTI->EXTICR[0] &= ~EXTI_EXTICR1_EXTI0; /* 0 — порт A; выбор порта живёт в EXTI */
EXTI->FTSR1 |= EXTI_FTSR1_FT0;
EXTI->IMR1 |= EXTI_IMR1_IM0;
NVIC_SetPriority(EXTI0_1_IRQn, 1);
NVIC_EnableIRQ(EXTI0_1_IRQn);
}
void EXTI0_1_IRQHandler(void)
{
if (EXTI->FPR1 & EXTI_FPR1_FPIF0) { /* спад — свой регистр FPR1, фронт — RPR1 */
EXTI->FPR1 = EXTI_FPR1_FPIF0;
presses++;
}
}
/* У M0+ таблицу векторов можно перенести в ОЗУ регистром VTOR — как у M3/M4. */
void vectors_to_ram(const uint32_t *table)
{
SCB->VTOR = (uint32_t)table;
}
Дальше: раздел 7а «Вход в исключение изнутри: кадр стека, EXC_RETURN и FPU».