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

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 строка с ней не соберётся.

Байт приоритета IPR: у F1 и F4 значимы старшие четыре бита, NVIC_SetPriority с приоритетом 5 даёт 0x50, у M0 с приоритетом 1 — 0x40; PRIGROUP от 3 до 7 сдвигает границу между битами вытеснения и подприоритета

Практический совет: не раздавайте приоритеты «по важности задачи». Раздавайте по срочности реакции. Обработчик, который обязан уложиться в микросекунды (захват таймера, приём байта), получает высокий приоритет; тот, где важен сам факт события (нажатие кнопки), — низкий.

⚠️ Если вы работаете под 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.

Один детектор фронта и два его выхода: через маску прерывания в NVIC и обработчик, через маску события — прямо в ядро

Во что обходится обработчик. Перед входом в него ядро само кладёт на стек восемь слов — 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 Итоги

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

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».

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