Порты ввода-вывода, прерывания и контроллер прерываний NVIC … ↩
mod. Суббота, 17 октября 2026 22:14
. См. 83
BeeFC · Урок S5 из 33 · Уровень «Старт»
Чем прерывание отличается от опроса, как настроить внешнее прерывание по фронту сигнала, как распределить приоритеты между обработчиками и как отфильтровать дребезг контактов кнопки. В конце урока задержка обработки прерывания измеряется логическим анализатором.
В этом уроке в BeeFC появляется обработчик линии готовности данных гироскопа. Сам гироскоп будет настроен в уроке P1, но прерывание от него уже проверяется в программной имитации.
Содержание
1. Цели урока
Продолжительность урока — три часа: один час теории и два часа практики. После урока слушатель:
- выбирает между опросом и прерыванием для конкретного сигнала;
- настраивает внешнее прерывание по фронту на любом выводе и задаёт его приоритет;
- безопасно передаёт данные из обработчика прерывания в основную программу;
- реализует фильтр дребезга контактов как конечный автомат и проверяет его модульными тестами;
- измеряет задержку обработки прерывания логическим анализатором.
2. Термины и сокращения
| Термин | Определение |
|---|---|
| прерывание | приостановка основной программы для выполнения обработчика по событию |
| обработчик прерывания | функция, которую ядро вызывает при возникновении события |
| контроллер вложенных векторных прерываний (NVIC) | блок ядра Cortex-M, который разрешает прерывания и выбирает порядок их обслуживания |
| контроллер внешних прерываний (EXTI) | блок STM32, который формирует прерывание по фронту сигнала на выводе |
| линия готовности данных (DRDY) | выход датчика, сообщающий о появлении нового измерения |
| дребезг контактов | многократное замыкание и размыкание механического контакта в момент переключения |
| конечный автомат | модель, в которой поведение определяется текущим состоянием и входным событием |
| критическая секция | участок кода, на время выполнения которого прерывания запрещены |
3. Опрос и прерывания
Программа может узнать о событии двумя способами. При опросе она периодически читает состояние вывода или флага. При прерывании ядро само приостанавливает текущий код и вызывает обработчик.
| Признак | Опрос | Прерывание |
|---|---|---|
| задержка реакции | до одного периода опроса | десятки наносекунд |
| нагрузка на процессор | постоянная, даже без событий | только при событии |
| поведение при дребезге | фильтруется естественным образом | каждое переключение вызывает обработчик |
| пример в BeeFC | кнопка, состояние батареи | линия готовности данных гироскопа, приём байта |
Ядро Cortex-M4 начинает выполнять обработчик через 12 тактов после события, то есть примерно через 71 нс на частоте 168 МГц. Такты ожидания флеш-памяти и выполняющиеся в этот момент обработчики с более высоким приоритетом увеличивают задержку.
↑ К оглавлению4. Контроллер внешних прерываний
Контроллер EXTI STM32F405 имеет шестнадцать линий для выводов: линия с номером n подключается к выводу с тем же номером в одном из портов. Линия EXTI4 может обслуживать PA4, PB4 или PC4, но только один из них. Выбор порта для линии задаётся в регистрах SYSCFG_EXTICR.
| Линии | Обработчик в таблице векторов |
|---|---|
| EXTI0–EXTI4 | отдельный обработчик для каждой линии |
| EXTI5–EXTI9 | общий обработчик EXTI9_5_IRQHandler |
| EXTI10–EXTI15 | общий обработчик EXTI15_10_IRQHandler |
Настройка прерывания по фронту
Файл hal/stm32f4/exti_stm32.c
void gyro_drdy_exti_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; /* SYSCFG выбирает порт для линии EXTI */
(void)RCC->APB2ENR;
/* Линия EXTI4 подключается к порту C: регистр EXTICR[1], поле EXTI4. */
SYSCFG->EXTICR[1] = (SYSCFG->EXTICR[1] & ~SYSCFG_EXTICR2_EXTI4)
| SYSCFG_EXTICR2_EXTI4_PC;
EXTI->RTSR |= EXTI_RTSR_TR4; /* по переднему фронту */
EXTI->FTSR &= ~EXTI_FTSR_TR4;
EXTI->PR = EXTI_PR_PR4; /* сбросить старый запрос (запись 1) */
EXTI->IMR |= EXTI_IMR_MR4; /* разрешить линию */
NVIC_SetPriority(EXTI4_IRQn, 1); /* 0 — самый высокий из 16 уровней */
NVIC_EnableIRQ(EXTI4_IRQn);
}
void EXTI4_IRQHandler(void)
{
EXTI->PR = EXTI_PR_PR4; /* сбросить флаг первым делом, иначе — повторный вход */
g_gyro_drdy_cycles = DWT->CYCCNT; /* отметка времени для замера задержки */
g_gyro_drdy_count++; /* в обработчике — только короткие действия */
}
Флаг запроса в регистре EXTI->PR сбрасывается записью единицы. Если его не сбросить, ядро вызовет обработчик снова сразу после выхода из него, и основная программа перестанет выполняться.
5. Приоритеты прерываний
В STM32F405 контроллер NVIC использует четыре бита приоритета, то есть шестнадцать уровней. Уровень 0 — самый высокий. Обработчик с более высоким приоритетом прерывает обработчик с более низким. Функция NVIC_SetPriority из CMSIS сама сдвигает значение в старшие биты регистра.
| Источник | Приоритет в BeeFC | Обоснование |
|---|---|---|
| линия готовности данных гироскопа | 1 | от неё зависит частота цикла управления |
| таймер выходов на двигатели | 2 | урок S8 |
| приём по последовательному порту | 5 | байт можно обработать в течение нескольких микросекунд |
| системный таймер SysTick | 15 | служебные задачи |

Общие данные обработчика и основной программы
Переменная, которую изменяет обработчик, объявляется с квалификатором volatile. Чтение и запись выровненного 32-битного значения на Cortex-M4 выполняются одной инструкцией и не могут быть прерваны. Для значений типа uint64_t и для структур это не так, и их чтение выполняется в критической секции.
uint32_t primask = __get_PRIMASK();
__disable_irq(); /* начало критической секции */
gyro_sample_t copy = g_gyro_sample; /* копия структуры целиком */
__set_PRIMASK(primask); /* восстановить прежнее состояние */
Сохранение и восстановление регистра PRIMASK вместо безусловного разрешения прерываний позволяет вызывать такой код и там, где прерывания уже запрещены.
6. Дребезг контактов
При нажатии кнопки контакты замыкаются и размыкаются несколько раз в течение нескольких миллисекунд. Если повесить на кнопку внешнее прерывание, одно нажатие вызовет несколько обработчиков. Поэтому кнопка в BeeFC опрашивается задачей планировщика, а решение принимает конечный автомат.
Файл core/debounce.h
typedef enum { BTN_EVENT_NONE, BTN_EVENT_PRESSED, BTN_EVENT_RELEASED } btn_event_t;
typedef struct {
bool stable; /* подтверждённое состояние: true — нажата */
uint8_t count; /* сколько опросов подряд сырое значение отличается */
uint8_t threshold; /* сколько опросов нужно для смены состояния */
} debounce_t;
void debounce_init(debounce_t *d, uint8_t threshold);
btn_event_t debounce_update(debounce_t *d, bool raw_pressed);
Файл core/debounce.c
void debounce_init(debounce_t *d, uint8_t threshold)
{
d->stable = false;
d->count = 0;
d->threshold = threshold;
}
btn_event_t debounce_update(debounce_t *d, bool raw_pressed)
{
if (raw_pressed == d->stable) {
d->count = 0; /* дребезг закончился, счёт сначала */
return BTN_EVENT_NONE;
}
if (++d->count != d->threshold) {
return BTN_EVENT_NONE; /* изменение ещё не подтверждено */
}
d->count = 0;
d->stable = raw_pressed;
return raw_pressed ? BTN_EVENT_PRESSED : BTN_EVENT_RELEASED;
}
Автомат меняет состояние только после того, как новое значение повторилось заданное число опросов подряд. При опросе с частотой 1 кГц и пороге в десять опросов изменение подтверждается через 10 мс.
Файл tests/unit/test_level1.c
void test_debounce_ignores_bounce_and_reports_one_press(void)
{
debounce_t d;
debounce_init(&d, 3);
const bool raw[] = { 1, 0, 1, 0, 1, 1, 1, 1, 1 }; /* дребезг, затем нажатие */
int presses = 0;
for (unsigned i = 0; i != sizeof raw; i++)
if (debounce_update(&d, raw[i]) == BTN_EVENT_PRESSED) presses++;
TEST_ASSERT_EQUAL_INT(1, presses);
TEST_ASSERT_TRUE(d.stable);
}
↑ К оглавлению7. Замер задержки прерывания
Задержка измеряется логическим анализатором с двумя каналами. Первый канал подключается к выводу кнопки, второй — к свободному выводу, который обработчик переключает первой же инструкцией. Время между фронтом на первом канале и фронтом на втором — задержка обработки прерывания вместе с временем записи в порт.
На том же снимке анализатора виден дребезг: после нажатия на первом канале появляется серия коротких импульсов, и каждый из них вызывает обработчик.
8. Практика
Задание S5.1. Фильтр дребезга
Продолжительность — 30 минут, выполняется на компьютере.
Реализовать модуль core/debounce и добавить тест на отпускание кнопки с дребезгом.
Критерий автоматической проверки: тесты test_debounce_* и скрытые тесты пройдены; скрытые тесты подают на вход записи дребезга реальных кнопок.
Задание S5.2. Счётчик прерываний от кнопки
Продолжительность — 40 минут.
Подключить кнопку между свободным выводом и общим проводом, включить подтягивающий резистор и настроить внешнее прерывание по спаду. Обработчик увеличивает счётчик. Выполнить двадцать нажатий и записать в отчёт docs/s5_report.md, сколько раз был вызван обработчик.
Задание S5.3. Замер задержки
Продолжительность — 30 минут, требуется логический анализатор.
Измерить задержку между фронтом на выводе кнопки и переключением отладочного вывода в обработчике. Приложить к отчёту снимок экрана анализатора и сравнить результат с расчётным значением.
Задание S5.4. Линия готовности данных гироскопа
Продолжительность — 20 минут.
Настроить прерывание по фронту на выводе линии готовности данных своей платы функцией gyro_drdy_exti_init. Обработчик сохраняет значение счётчика тактов DWT и увеличивает счётчик прерываний.
Критерий автоматической проверки: в модели Renode на вывод подаётся сто фронтов, после чего значение g_gyro_drdy_count равно 100.
9. Как это сделано в больших прошивках
| Прошивка | Версия | Файл в исходном коде |
|---|---|---|
| Betaflight | 2026.6.2 |
src/main/drivers/exti.h, src/platform/STM32/exti.c |
| INAV | 9.1.0 |
src/main/drivers/exti.c |
| ArduPilot | Copter-4.7.2 |
libraries/AP_HAL_ChibiOS/GPIO.cpp |
| PX4 | v1.17.0 |
src/drivers/imu/invensense/icm42688p/ICM42688P.cpp |
- В Betaflight драйвер датчика регистрирует обработчик функциями
EXTIHandlerInitиEXTIConfig, объявленными вexti.h. Реализация вsrc/platform/STM32/exti.cсама обслуживает общие обработчики линий 5–9 и 10–15 и вызывает нужную функцию драйвера по номеру линии. - В INAV устройство модуля
exti.cунаследовано от общего предка и похоже на Betaflight. - В ArduPilot метод
attach_interruptв файлеGPIO.cppподключает обработчик через события линий ввода-вывода операционной системы ChibiOS. - Драйвер
ICM42688P.cppв PX4 по прерыванию готовности данных запускает чтение очереди измерений датчика. Одновременно драйвер планирует резервный запуск через удвоенный интервал. Если прерывание не пришло, срабатывает резервный запуск: драйвер увеличивает счётчик «DRDY missed» и читает количество накопленных измерений из датчика напрямую. Так пропущенное прерывание не останавливает цикл управления.
10. Проверьте себя
- Почему кнопку не подключают к внешнему прерыванию напрямую?
- Можно ли одновременно использовать внешние прерывания на выводах PA4 и PC4?
- Что произойдёт, если в обработчике не сбросить флаг в регистре
EXTI->PR? - Когда для чтения общей переменной нужна критическая секция?
- Что делает драйвер ICM-42688-P в PX4, если прерывание готовности данных не пришло?
11. Чек-лист самопроверки
12. Источники
- Справочное руководство RM0090, раздел 12, контроллер прерываний и событий.
- Руководство программиста PM0214, разделы о контроллере NVIC и регистре PRIMASK.
- Описание платы MAMBAF405_2022B для Betaflight.
- Драйвер ICM-42688-P в PX4.
13. Что дальше
- Следующий урок: S6. Системное время и планировщик задач.
- Полный перечень уроков приведён в статье «Курс BeeFC: содержание уровня „Старт“».
Проверено: 13.10.2026
↑ К оглавлению- мод. : Суббота, 17 октября 2026