NVIC и приоритеты прерываний
Мы видели, что прерывания заставляют процессор мгновенно перейти к другой функции в коде, но что происходит за кулисами, чтобы это стало возможным? В этом разделе рассказаны технические детали, которые не понадобятся для остальной части книги, так что смело пропускайте его, если вам это не интересно.
Контроллер прерываний
Прерывания позволяют процессору реагировать на события периферии, например на изменение состояния входного вывода GPIO, на завершение цикла таймера или на получение нового байта по UART. Периферия содержит схемы, которые замечают событие и сообщают о нём специальной периферии для обработки прерываний. В процессорах Arm эта периферия называется NVIC — Nested Vector Interrupt Controller.
ПРИМЕЧАНИЕ В других архитектурах микроконтроллеров, например RISC-V, названия и детали, описанные здесь, будут отличаться, но базовые принципы, как правило, очень похожи.
NVIC может получать запросы на инициирование прерываний от многих периферийных устройств. Более того, у одной периферии часто бывает несколько возможных прерываний: например, у порта GPIO — прерывание для каждого вывода, а у UART — и прерывание «данные получены», и прерывание «передача завершена». Задача NVIC — расставить эти прерывания по приоритету, запомнить, какие из них ещё нужно обработать, и заставить процессор выполнить соответствующий код обработчика.
Приоритеты прерываний
У каждого прерывания в NVIC есть настраиваемый «приоритет». В зависимости от настроек NVIC может гарантировать, что текущее прерывание полностью обработается, прежде чем начнётся новое, или может «вытеснить» процессор из середины одного прерывания, чтобы обработать другое, с более высоким приоритетом.
Вытеснение позволяет процессорам очень быстро реагировать на критические события. Например, контроллер робота может использовать прерывания низкого приоритета для отправки статусной информации оператору, но принять прерывание высокого приоритета, когда датчик обнаружит неминуемое столкновение, чтобы немедленно остановить моторы. Вы же не хотите, чтобы робот дождался окончания отправки пакета данных, прежде чем дойти до остановки!
Если во время ISR происходит прерывание того же или более низкого приоритета, оно «откладывается» (pended): NVIC запомнит новое прерывание и запустит его ISR где-то после завершения текущей ISR. Когда функция ISR возвращает управление, NVIC проверяет, не произошли ли за время её работы другие прерывания, которые нужно обработать. Если да, NVIC смотрит в таблицу прерываний и вызывает ту ISR, у которой наивысший приоритет из записанных там. Иначе процессор возвращается к выполняемой программе.
Обратите внимание: если прерывания полностью отключены, все входящие прерывания будут отложены. Отложенные прерывания обработаются, когда прерывания снова будут включены.
В embedded Rust NVIC можно программировать с помощью крейта cortex-m, который предоставляет методы для разрешения и запрета прерываний (их называют unmask и mask), для задания приоритетов прерываний и для инициирования прерываний программно. Такие фреймворки, как RTIC, могут сами настраивать NVIC за вас, используя его гибкость для удобного разделения ресурсов и управления задачами.
Подробнее о NVIC можно прочитать в документации Arm.
Таблица векторов
Когда мы описывали NVIC, я сказал, что он может «заставить процессор выполнить код соответствующего обработчика прерывания». Но как это на самом деле работает?
Во-первых, процессору нужен способ узнать, какой код выполнять для каждого прерывания. На процессорах Cortex-M в этом участвует область памяти, которая называется таблицей векторов. Обычно она расположена в самом начале флеш-памяти с нашим кодом (эта память перепрограммируется при каждой загрузке нового кода) и содержит список адресов — расположений в памяти каждой функции прерывания. Конкретная раскладка начала памяти определена Arm в Architecture Reference Manual; для наших целей важно, что байты с 64 по 256 содержат адреса всех 48 обработчиков прерываний для используемого нами процессора nRF, по четыре байта на адрес. У каждого прерывания есть номер от 0 до 47. Например, TIMER0 — прерывание номер 8, поэтому байты с 96 по 100 содержат четырёхбайтовый адрес его обработчика. Когда NVIC велит процессору обработать прерывание номер 8, процессор читает адрес, сохранённый в этих байтах, и переходит к нему.
Как эта таблица векторов генерируется в нашем коде? За это отвечает крейт cortex-m-rt, который делает всё необходимое. Он задаёт прерывание по умолчанию для каждой неиспользуемой позиции (поскольку каждая позиция должна быть заполнена) и позволяет нашему коду переопределить это значение, когда нам нужно указать собственный обработчик прерывания. Для этого мы используем макрос #[interrupt], который требует, чтобы функция имела определённое имя, связанное с обрабатываемым прерыванием. Затем крейт cortex-m-rt использует свой скрипт компоновщика, чтобы адрес этой функции оказался в нужной части памяти.
Подробнее о том, как в Rust управляются эти обработчики прерываний, см. главы об исключениях и прерываниях во встраиваемой книге Rust.