Прерывания — это основной аппаратный механизм, позволяющий периферийным устройствам уведомлять программное обеспечение о критических событиях. Например, мы можем сгенерировать аналоговый выходной сигнал с точными интервалами для воспроизведения аудиофайла. Одним из способов достижения этого является настройка таймера для генерации прерываний с точными интервалами. Когда происходит сконфигурированное прерывание, процессор переключает выполнение с прикладной программы на специальный обработчик прерываний, который затем может «обслуживать» событие прерывания, передавая выборку данных на аналоговый выход. Как только обработчик прерываний завершил свою задачу, процессор возобновляет выполнение прикладной программы. Прерывания также важны в обмене — например, уведомление процессора, когда значение прибыло в UART. В этой части мы обсудим, как работают прерывания в семействе микроконтроллеров STM32 (в более общем случае, в Cortex-M3), и представим несколько конкретных примеров, демонстрирующих их использование.
На самом деле мы использовали прерывания для реализации нашей функции задержки, как показано фрагментом кода в листинге. В рамках main мы настраиваем «SysTick» Cortex-M3 на запуск прерывания каждую миллисекунду. Кроме того, мы определяем обработчик прерываний, SysTick_Handler, который будет выполняться при возникновении прерывания SysTick. Этот обработчик уменьшает переменную TimingDelay и затем возвращает управление прикладной программе. Прикладная программа может ожидать точный период, вызывая процедуру Delay с интервалом, а затем ожидая истечения этого интервала (буквально отсчитывается обработчиком).
main(){
...
if (SysTick_Config(SystemCoreClock / 1000))
while (1);
...
}
static __IO uint32_t TimingDelay;
void Delay(uint32_t nTime){
TimingDelay = nTime;
while(TimingDelay != 0);
}
void SysTick_Handler(void){
if (TimingDelay != 0x00)
TimingDelay --;
Прерывания могут быть вызваны многими возможными событиями, включая системный таймер, ошибки доступа к памяти, внешние перезагрузки и все различные периферийные устройства, предоставляемые процессором STM32. Проще всего рассматривать каждый возможный источник прерывания как отдельный сигнал, который контролируется ядром процессора во время выполнения программы. Если ядро обнаруживает действительный сигнал прерывания и настроено на прием запросов на прерывание, оно реагирует путем сохранения состояния текущей исполняемой программы в стеке программ и выполнения обработчика (иногда называемого подпрограммой обработки прерывания), соответствующего принятому прерыванию. Когда обработчик завершается, сохраненное состояние программы восстанавливается, и нормальное выполнение программы возобновляется.
Понятие прерываний и, в более общем смысле, исключений может быть относительно трудным для понимания. Напомним, что программы на языке, таком как C, компилируются в ассемблерный код (символическое представление машинных инструкций), из которого генерируется машинный код. В STM32 этот машинный код копируется во флэш-память при программировании устройства и выполняется при перезагрузке процессора. Базовая модель исполнения:
while (1){
inst = *pc++;
eval(inst);
Процессор можно рассматривать как интерпретатор машинного кода, который считывает инструкции из памяти и оценивает их. Программный счетчик pc содержит адрес следующей инструкции для выполнения. Рассмотрим следующий оператор C из обработчика SysTick:
TimingDelay --;
Компилятор переводит этот оператор в три шага языка ассемблера, которые загружают (ldr) значение TimingDelay, уменьшают значение (subs) и записывают уменьшенное значение обратно (str). Этот список включает 6 байтов машинного кода (0x68a1, 0x3a01, 0x601a), сгенерированного ассемблером. Несмотря на то, что выходит за рамки этой книги, важно понимать, что язык ассемблера по сути является удобной для восприятия человеком формой двоичного машинного кода. Процесс связывания присваивает эти инструкции фиксированным адресам памяти.
681a ldr r2, [r3, #0] 3a01 subs r2, #1 601a str r2, [r3, #0]
Реализация прерываний в процессоре требует небольшого расширения этой модели:
while (1) {
if (interrupt_pending()) {
save_state();
pc = find_handler();
} else {
inst = *pc++;
eval(inst);
}
}
В каждом «цикле» выполняется проверка, чтобы определить, ожидает ли прерывание — это буквально соответствует проверке, является ли аппаратный ввод 1 или 0. Если это так, текущее состояние программы (включая счетчик программ) сохраняется, обработчик прерываний адрес найден, и выполнение продолжается с обработчиком. Когда обработчик завершает выполнение, прерванное состояние восстанавливается, и выполнение приложения продолжается с точки прерывания. Обратите внимание, например, что прерывания происходят на границах машинных инструкций, а не на границах оператора «C». Как мы видим, понимание этого является основополагающим для написания надежного кода прерывания.
В случае SysTick обратите внимание, что обработчик изменяет общие данные (TimingDelay) только тогда, когда они не равны нулю, а приложение только изменяет общие данные, когда они равны нулю. Подобные гарантии важны, потому что обработчики прерываний приводят к приостановке кода приложения в непредсказуемых местах, и поэтому структуры данных, которые используются для связи между обработчиками и кодом приложения, должны быть очень тщательно сконструированы для обеспечения их правильной работы. Рассмотрим хаос, который может возникнуть, если прикладная программа и обработчик прерываний одновременно получат доступ к связанному списку — обработчикам прерываний лучше не обращаться к ссылкам, которые находятся в процессе перемещения кодом приложения. Этот тип конфликта называется «условием гонки». Мы представляем пример использования обработчика прерываний для обслуживания UART, когда приложение и обработчик совместно используют две очереди данных — код тщательно разработан, чтобы избежать потенциальных конфликтов данных.
Пример SysTick демонстрирует некоторые типичные взаимодействия между обработчиками прерываний и приложениями. Обработчик выполняет очень кратко — всего несколько машинных инструкций — чтобы обновить некоторую информацию, передаваемую прикладной программе. Для достижения надежности обработчики прерываний должны удовлетворять трем свойствам:
- Они должны выполняться кратко.
- Время их выполнения должно быть предсказуемым (например, они никогда не должны ждать)
- Использование ими «общих данных» должно тщательно контролироваться
Эта модель, конечно, упрощена. Однако для последующего обсуждения прерываний этого достаточно. Ключевые вопросы, которые мы рассмотрим, — как мы позволяем периферийным устройствам генерировать ожидающие прерывания, как мы гарантируем, что наши обработчики выполняются, и как мы пишем надежные обработчики? В этой модели не проиллюстрировано понятие приоритета прерывания — когда запрашивается более двух прерываний, как осуществляется выбор. Также не рассматривается приоритетное прерывание — если выполняется обработчик прерывания, возможно ли, чтобы другие обработчики вызывались рекурсивно?
Далее мы начнем с обсуждения модели прерываний (исключений) Cortex-M3, включая различные типы исключений, стеки и привилегии. Затем обсудим таблицу векторов прерываний — механизм, с помощью которого ядро Cortex-M3 связывает обработчики с конкретными причинами прерываний, и Nested Vector Interrupt Controller (NVIC), который Cortex-M3 использует для выбора среди конкурирующих запросов прерываний.