Прерывания: NVIC, EXTI и приём USART
Опрос флагов в цикле годится для первых программ, но тратит время ядра и теряет события. Научимся получать прерывания от кнопки через контроллер EXTI и от приёмника USART1, разберём NVIC и приоритеты и напишем обработчики, которые не зацикливаются и не теряют байты.
Обновлено Редакция GAW
STM32 с нуляурок 6 из 840 мин
Цель урока: Настроить внешнее прерывание EXTI от кнопки и прерывание приёма USART1, включить их в NVIC с приоритетами и правильно сбрасывать флаги в обработчиках.
Прерывание — способ сообщить ядру о событии без постоянного опроса. В Cortex-M3 прерываниями заведует NVIC (Nested Vectored Interrupt Controller) — контроллер в составе ядра, описанный в руководстве Arm DUI 0552A. У STM32F103C8 к нему подведено до 43 маскируемых каналов прерываний, кроме 16 линий ядра (DS5319, разд. 2.3.5), — это IRQ 0–42 нашей таблицы векторов, — и 16 уровней приоритета: реализованы 4 бита (раздел 10.1 RM0008, с. 197). Число 68 в том же разделе RM0008 относится ко всей серии, включая connectivity line. Подробный разбор входа в прерывание, хвостовых вызовов и масок — в статье «Ядро Cortex-M3», а здесь — то, что нужно для двух конкретных прерываний.
Что нужно, чтобы прерывание сработало
Цепочка всегда одна и та же:
- Источник в периферии ставит флаг и, если его прерывание разрешено битом в регистре периферии, выставляет запрос.
- NVIC пропускает запрос, если этот канал разрешён (регистры
NVIC_ISERс адреса 0xE000E100) и его приоритет выше текущего. - Ядро берёт адрес обработчика из таблицы векторов — нашей структуры из урока 2, — сохраняет регистры и вызывает функцию.
- Обработчик сбрасывает флаг источника и делает минимум работы.
Пропуск любого шага даёт одну из двух классических ошибок: прерывание не приходит вовсе (не разрешено в периферии или в NVIC, имя обработчика с опечаткой) или приходит бесконечно (флаг не сброшен). Второе объясняет раздел 4.2.9 DUI 0552A: при входе в обработчик ядро снимает с прерывания состояние «ожидает», но если сигнал запроса от периферии не снят к моменту возврата, прерывание снова становится ожидающим и обработчик вызывается опять.
NVIC: разрешение и приоритеты
Функции CMSIS из core_cm3.h делают всё нужное одной строкой. NVIC_EnableIRQ(USART1_IRQn) записывает единицу в бит 37 mod 32 = 5 регистра ISER[1]: на каждые 32 прерывания — один регистр. NVIC_SetPriority(IRQn, p) записывает байт приоритета в NVIC_IPR (с адреса 0xE000E400), сдвинув число на 4 бита влево: поле приоритета 8-битное, а у STM32F1 реализованы только 4 старших бита, младшие читаются как ноль (DUI 0552A, разд. 4.2.7). Поэтому в функцию передают 0–15, и меньшее число — более срочное прерывание: обработчик с приоритетом 1 может прервать обработчик с приоритетом 2, но не наоборот.
Номера IRQn и имена обработчиков должны совпадать с таблицей векторов: EXTI15_10_IRQn = 40, USART1_IRQn = 37 в stm32f103xb.h — это позиции 40 и 37 таблицы 63 RM0008. Как 4 бита делятся между группой и подприоритетом и что означает поле PRIGROUP, покажет калькулятор приоритетов NVIC.
EXTI: прерывание от вывода
Контроллер внешних прерываний EXTI у STM32F103 средней плотности имеет 19 линий (разд. 10.2, с. 207). Линии 0–15 подключены к выводам, остальные — к PVD, будильнику RTC и выходу USB из сна. Подключение к выводам устроено как мультиплексор: линия n — это вывод с номером n одного из портов, а какого именно, выбирают поля EXTIn[3:0] регистров AFIO_EXTICR1–AFIO_EXTICR4 (разд. 10.2.5, с. 209–210): 0000 — порт A, 0001 — порт B и так далее. Отсюда главное ограничение: PA12 и PB12 одновременно на прерывания не повесить.
Регистры AFIO доступны, только если включено тактирование AFIO битом AFIOEN в RCC_APB2ENR (разд. 9.4, с. 183) — без него запись в AFIO_EXTICR4 молча пропадёт, и прерывание придёт не от того порта. Вывод при этом должен быть настроен входом (разд. 9.1.3).
Дальше всё по разделу 10.2.4: биты EXTI_RTSR и EXTI_FTSR выбирают нарастающий и спадающий фронт (можно оба), бит EXTI_IMR снимает маску прерывания. Пришедший фронт ставит бит в EXTI_PR, и он держит запрос к NVIC, пока программа его не сбросит — записью единицы (разд. 10.3.6, с. 213). Запись нуля ничего не делает, а EXTI->PR |= … сбросит заодно и другие ожидающие линии, поэтому пишут ровно один бит: EXTI->PR = EXTI_PR_PR12;.
Линии 0–4 имеют свои обработчики EXTI0_IRQHandler…EXTI4_IRQHandler, линии 5–9 делят EXTI9_5_IRQHandler, линии 10–15 — EXTI15_10_IRQHandler. В общем обработчике линии различают по битам EXTI_PR.
Дребезг кнопки
Механический контакт при нажатии и отпускании несколько миллисекунд замыкается и размыкается, и каждый такой отскок — новый фронт и новое прерывание. Как выглядит дребезг и какие бывают способы с ним бороться, разобрано в уроке «Кнопки, подтяжка и дребезг» трека AVR и в статье глоссария «Дребезг контактов». Читать вывод прямо в обработчике бесполезно: обработчик срабатывает через доли микросекунды после фронта, когда контакт ещё прыгает, и уровень на выводе в этот момент случаен. Поэтому разделим работу. Прерывание по обоим фронтам только запоминает время последнего фронта. Основная программа, которую SysTick будит каждую миллисекунду, читает вывод, лишь когда фронтов не было 30 мс, — к этому времени уровень устоялся, — и сравнивает его с последним устойчивым состоянием кнопки: переход из «отпущена» в «нажата» и есть нажатие. Порог 30 мс — наш выбор с запасом, а не число из документации.
Приём USART1 в прерывании
Бит RXNEIE регистра USART1->CR1 разрешает прерывание USART1 по флагу RXNE. По разделу 27.6.1 RM0008 то же прерывание вызывает и ORE — переполнение приёмника, — поэтому обработчик реагирует на оба флага. Чтение SR, а затем DR сбрасывает RXNE и ORE одновременно. Обработчик кладёт байт в кольцевой буфер, а разбирает байты основная программа. У буфера один писатель — прерывание, которое меняет только rx_head, — и один читатель — main, которая меняет только rx_tail, поэтому запрещать прерывания при чтении не нужно. Размер — степень двойки, чтобы переход через конец массива делался маской. Почему такой буфер безопасен и как сделать его на передачу, подробно рассказано в статье «Кольцевые буферы для UART».
Программа
Кнопка — между PB12 и GND, светодиод — на PC13, преобразователь USB-UART — как в уроке 5. В Makefile ничего не меняется: SRCS = startup.c main.c clock.c uart.c.
/* main.c, lesson 6: button on PB12 via EXTI12, USART1 receive interrupt */
#include "stm32f1xx.h"
#include "clock.h"
#include "uart.h"
/* ---- button: EXTI line 12, both edges; the handler only stamps the time ---- */
static volatile uint32_t last_edge_ms; /* written only by the handler */
void EXTI15_10_IRQHandler(void)
{
if (EXTI->PR & EXTI_PR_PR12) {
EXTI->PR = EXTI_PR_PR12; /* write 1 to clear, first thing */
last_edge_ms = g_ticks; /* the contact may still bounce */
}
}
/* ---- USART1 receive: the interrupt fills a ring buffer ---- */
#define RX_SIZE 64u /* power of two */
static volatile uint8_t rx_buf[RX_SIZE];
static volatile uint32_t rx_head; /* written only by the handler */
static volatile uint32_t rx_tail; /* written only by main() */
void USART1_IRQHandler(void)
{
uint32_t sr = USART1->SR;
if (sr & (USART_SR_RXNE | USART_SR_ORE)) {
uint8_t byte = (uint8_t)USART1->DR; /* SR then DR: clears RXNE and ORE */
uint32_t next = (rx_head + 1u) & (RX_SIZE - 1u);
if (next != rx_tail) { /* full: drop the byte */
rx_buf[rx_head] = byte;
rx_head = next;
}
}
}
static int rx_get(void)
{
if (rx_tail == rx_head)
return -1;
int c = rx_buf[rx_tail];
rx_tail = (rx_tail + 1u) & (RX_SIZE - 1u);
return c;
}
int main(void)
{
uint32_t hclk = clock_init();
systick_init(hclk);
uart1_init(hclk, 115200u);
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;
/* PC13: LED, push-pull, 2 MHz, off */
GPIOC->CRH = (GPIOC->CRH & ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13))
| (0x2u << GPIO_CRH_MODE13_Pos);
GPIOC->BSRR = GPIO_BSRR_BS13;
/* PB12: input with pull-up, the button shorts it to GND */
GPIOB->CRH = (GPIOB->CRH & ~(GPIO_CRH_MODE12 | GPIO_CRH_CNF12))
| (0x2u << GPIO_CRH_CNF12_Pos);
GPIOB->BSRR = GPIO_BSRR_BS12;
/* EXTI12 <- port B, both edges, unmasked */
AFIO->EXTICR[3] = (AFIO->EXTICR[3] & ~AFIO_EXTICR4_EXTI12) | AFIO_EXTICR4_EXTI12_PB;
EXTI->RTSR |= EXTI_RTSR_TR12;
EXTI->FTSR |= EXTI_FTSR_TR12;
EXTI->PR = EXTI_PR_PR12; /* drop a request left from setup */
EXTI->IMR |= EXTI_IMR_MR12;
USART1->CR1 |= USART_CR1_RXNEIE;
NVIC_SetPriority(USART1_IRQn, 1u); /* bytes cannot wait */
NVIC_SetPriority(EXTI15_10_IRQn, 2u);
NVIC_EnableIRQ(USART1_IRQn);
NVIC_EnableIRQ(EXTI15_10_IRQn);
uint32_t presses = 0;
int pressed = 0; /* debounced state of the button */
for (;;) {
int c;
while ((c = rx_get()) >= 0) {
if (c == 't') { /* 't' toggles the LED */
if (GPIOC->ODR & GPIO_ODR_ODR13)
GPIOC->BSRR = GPIO_BSRR_BR13;
else
GPIOC->BSRR = GPIO_BSRR_BS13;
}
uart1_putc((char)c); /* echo */
}
/* Read the pin only after 30 ms without edges: the level has settled */
if ((uint32_t)(g_ticks - last_edge_ms) >= 30u) {
int low = !(GPIOB->IDR & GPIO_IDR_IDR12);
if (low && !pressed) { /* released -> pressed */
presses++;
uart1_puts("presses: ");
uart1_put_uint(presses);
uart1_puts("\n");
}
pressed = low;
}
__WFI(); /* sleep until the next interrupt */
}
}
EXTI->PR сбрасывается перед EXTI->IMR: пока настраивались фронты, линия могла поймать фронт, а NVIC по разд. 4.2.10 DUI 0552A запоминает запрос и для ещё запрещённого прерывания. Порядок приоритетов выбран так: USART1 (1) прерывает обработчик кнопки (2), а оба прерывают SysTick (15 из урока 4). Переменные g_ticks и last_edge_ms — по одному 32-битному слову, и основная программа читает каждую одной командой, поэтому запрещать прерывания вокруг сравнения не нужно. Если фронт придёт между сравнением и чтением вывода, программа прочитает уровень в начале дребезга, но это реальный переход кнопки: в худшем случае он будет засчитан через следующие 30 мс тишины, а не сразу.
Команда __WFI() останавливает ядро до следующего прерывания. SysTick будит его каждую миллисекунду, так что даже если байт пришёл между проверкой буфера и __WFI(), он будет обработан не позже чем через 1 мс (расчёт наш). Как избежать и этой задержки и что такое критическая секция, объясняет статья «Прерывания и общие данные».
На STM32F4
NVIC и функции CMSIS у Cortex-M4 те же, и у STM32F411 номера нужных прерываний совпадают с F1: USART1_IRQn = 37, EXTI15_10_IRQn = 40. Отличается выбор порта для линии EXTI: у F4 нет блока AFIO, вместо него регистры SYSCFG_EXTICR1–SYSCFG_EXTICR4 с теми же кодами портов (RM0090, разд. 9.2.3; в заголовке F411 — SYSCFG->EXTICR[3] и SYSCFG_EXTICR4_EXTI12_PB). Тактирование SYSCFG включает бит RCC_APB2ENR_SYSCFGEN. Подтяжку у F4 задаёт регистр PUPDR, а не бит ODR.
Задание
- Что изменится, если в обработчике кнопки сбрасывать флаг строкой
EXTI->PR &= ~EXTI_PR_PR12;? - Добавьте вторую кнопку на PB13 так, чтобы она уменьшала счётчик. Что нужно изменить в настройке и в обработчике?
Ответ и решение
-
Флаг не сбросится: биты EXTI_PR сбрасываются записью единицы, а
&= ~пишет в бит 12 ноль. Зато в остальные биты запишется то, что из них прочиталось, — то есть единицы во все прочие ожидающие линии, и их запросы пропадут. Бит 12 останется, запрос к NVIC не снимется, и после выхода обработчик будет вызываться снова и снова (DUI 0552A, разд. 4.2.9) — основная программа остановится. -
PB13 — линия EXTI13, поле EXTI13 того же регистра AFIO_EXTICR4 и тот же обработчик EXTI15_10_IRQHandler. В настройке: вход с подтяжкой для PB13 (поля CNF13 и MODE13 регистра
GPIOB->CRH, бит BS13 в BSRR),AFIO_EXTICR4_EXTI13_PB, биты 13 вEXTI->RTSR,EXTI->FTSRиEXTI->IMR. В обработчике — вторая веткаif (EXTI->PR & EXTI_PR_PR13), которая сбрасывает только свой бит и записывает время во вторую переменную, напримерlast_edge13_ms. В основном цикле — второй такой же блок проверки 30 мс тишины со своим устойчивым состояниемpressed13, который при переходе в «нажата» уменьшает счётчик.
Что дальше
В уроке 7 таймер TIM2 будет вызывать прерывание по переполнению, а TIM3 — выдавать ШИМ на светодиод без участия ядра. Таблица прерываний STM32F1 и F4, приоритеты и типичные ошибки — в статье «Прерывания STM32: NVIC, EXTI», регистры EXTI и AFIO — на страницах атласа регистров EXTI STM32F1 и регистров AFIO STM32F1. Термины урока — в статьях глоссария NVIC, EXTI, «Кольцевой буфер» и «Задержка прерывания».
Частые вопросы
Почему обработчик прерывания вызывается снова и снова?
Не сброшен флаг источника. Для EXTI это бит регистра EXTI_PR, и сбрасывают его записью единицы, а не нуля. Пока флаг стоит, запрос к NVIC остаётся активным, и по DUI 0552A после возврата из обработчика прерывание сразу снова становится ожидающим. Сбрасывайте флаг в начале обработчика, а не в конце.
Можно ли повесить прерывания на PA12 и PB12 одновременно?
Нет. Линия EXTI12 одна на все выводы с номером 12, и поле EXTI12 регистра AFIO_EXTICR4 выбирает только один порт. Две кнопки нужно разнести по выводам с разными номерами, например PB12 и PB13: они попадут на линии 12 и 13 и на общий обработчик EXTI15_10_IRQHandler, где их различают по битам EXTI_PR.
Зачем нужен кольцевой буфер для приёма UART?
Обработчик прерывания должен быть коротким: забрать байт из USART1->DR и выйти. Обрабатывать строку прямо в нём нельзя — пока он работает, следующий байт может прийти и вытеснить ещё не прочитанный, а остальные прерывания будут ждать. Кольцевой буфер развязывает их: прерывание кладёт байты в голову, основная программа забирает из хвоста, когда успевает.
Источники
- STMicroelectronics. RM0008 Rev 21. Reference manual STM32F101xx–STM32F107xx — разд. 9.1.3: для внешнего прерывания вывод должен быть входом; разд. 9.4 (с. 183): регистры AFIO_EXTICRx доступны только при включённом тактировании AFIO; разд. 10.1 (с. 197): 16 уровней приоритета (4 бита); разд. 10.2 (с. 207): 19 детекторов фронтов в кристаллах, кроме connectivity line; разд. 10.2.4: настройка — маска EXTI_IMR, фронты EXTI_RTSR и EXTI_FTSR, затем NVIC; запрос сбрасывается записью 1 в EXTI_PR; разд. 10.2.5 (с. 209–210): линии 0–15 — выводы Px0–Px15, порт выбирают поля EXTIx[3:0] регистров AFIO_EXTICR1–4; разд. 10.3.6 (с. 213): EXTI_PR, бит сбрасывается записью 1; табл. 63: EXTI15_10 — позиция 40, USART1 — 37; разд. 27.6.1: при RXNEIE = 1 прерывание вызывают RXNE и ORE
- STMicroelectronics. DS5319 Rev 20. STM32F103x8, STM32F103xB datasheet — разд. 2.3.5: NVIC STM32F103xx — до 43 маскируемых каналов прерываний, кроме 16 линий ядра Cortex-M3, и 16 уровней приоритета
- Arm. Cortex-M3 Devices Generic User Guide, DUI 0552A — разд. 4.2: NVIC_ISER с 0xE000E100, NVIC_IPR с 0xE000E400; разд. 4.2.7: поля приоритета по 8 бит, нереализованные младшие биты читаются как ноль; разд. 4.2.9: при входе в обработчик ядро снимает состояние pending; если запрос уровня не снят до возврата, прерывание снова становится pending и обработчик вызывается ещё раз; разд. 4.2.10: прерывание может стать pending и когда оно запрещено; __enable_irq и __disable_irq — команды CPSIE I и CPSID I
- Arm CMSIS_5 5.9.0: core_cm3.h; STMicroelectronics cmsis-device-f1 v4.3.5: stm32f103xb.h — NVIC_EnableIRQ пишет 1 << (IRQn & 0x1F) в ISER[IRQn >> 5]; NVIC_SetPriority сдвигает приоритет на 8 − __NVIC_PRIO_BITS = 4 бита влево; __WFI — команда wfi; EXTI15_10_IRQn = 40, USART1_IRQn = 37; AFIO->EXTICR[4], AFIO_EXTICR4_EXTI12_PB = 0x1; EXTI_PR_PR12, EXTI_FTSR_TR12, EXTI_RTSR_TR12, EXTI_IMR_MR12
- STMicroelectronics. RM0090 Rev 21 (STM32F405/407), разд. 9.2.3; cmsis-device-f4 v2.6.11, stm32f411xe.h — у F4 порт линии EXTI выбирает SYSCFG_EXTICRx (0000 — PA, 0001 — PB…); в заголовке F411 — SYSCFG->EXTICR[4], RCC_APB2ENR_SYSCFGEN, SYSCFG_EXTICR4_EXTI12_PB, USART1_IRQn = 37 и EXTI15_10_IRQn = 40, как у F1
Нашли ошибку?
Смотрите также
- Прерывания STM32Векторы и IRQn, приоритеты и HAL, EXTI и общие векторы, флаги, события, примеры
- Приоритеты прерываний NVICPRIGROUP, вытеснение и подприоритет, NVIC_IPR, AIRCR, bit-band
- EXTI — внешние прерывания STM32Линии, которые превращают фронт на выводе в прерывание или событие
- NVIC — контроллер прерыванийКонтроллер прерываний Cortex-M: приоритеты и вытеснение
- Кольцевой буфер UARTБуфер SPSC с маской, ISR приёма и передачи на AVR и STM32, ORE и DOR, размер