[Home] [< Prev: GPIO: порты ввода/вывода (зажигаем светодиод)] [Next: SysTick: системный таймер >]

Исключения и прерывания

Введение
Исключения в Cortex-M микроконтроллерах
Приоритеты исключений
Больше об исключениях
Пример обработчика исключения
// Счётчик количества исключений от системного таймера.
volatile uint32_t cnt=0;

// Обработчик исключения от системного таймера.
extern "C" void SysTick_Handler()
{
    cnt++;
}


Введение

Под исключениями здесь мы будем подразумевать исключения микроконтроллера, а не C++ исключения.

Во время выполнения программы могут возникнуть ситуации, требующие немедленной реакции со стороны процессора, такие как ошибки в программе, сбои в работе аппаратуры, запросы от внешних устройств. Для обработки таких событий используется механизм исключений. Если рассматривать процесс упрощённо, то когда происходит исключение, если оно разрешено, работа программы прерывается, в стеке сохраняется текущий контекст, происходит переход на обработчик исключения - функцию, выполняющую необходимые действия. После возврата из обработчика исключения, восстанавливается контекст и выполнение программы возобновляется так, как будто ничего не произошло. Обработка одного исключения также может быть прервана для обработки другого исключения с более высоким приоритетом.

Исключения в Cortex-M микроконтроллерах

Как уже сказано выше, исключение - ситуация, когда изменяется обычный ход выполнения программы. Можно выделить две стадии в реакции процессора на событие, ведущее к исключению: собственно генерация исключения, а затем - обработка (активация) исключения. Обработка включает в себя вход в обработчик, выполнение кода обработчика и возврат. Переход к обработке может произойти как немедленно после генерации исключения, так и позже, если обработка в данный момент невозможна (исключение становится "отложенным" и будет обработано, когда появится возможность).

На форумах часто обсуждается вопрос: что такое прерывания и чем они отличаются от исключений? Этот вопрос об исключениях касается исключительно терминологии и исчерпывающий ответ на него содержится в ARM Architecture Reference Manual (ARM ARM) для профиля ARMv7-M. В документе определяются следующие категории исключений: Reset - сброс; Supervisor call (SVCall) - вызов супервизора; Fault - ошибка и Interrupt - прерывание. Далее указывается, что прерывание - это любое исключение, отличное от сброса, вызова супервизора или ошибки. Все прерывания асинхронны по отношению к потоку инструкций. Обычно прерывания используются компонентами системы, которые должны взаимодействовать с процессором (внешнее по отношению к ядру устройство микроконтроллера делает запрос на прерывание, сообщая о необходимости обслуживания).

Табл. 1. Исключения Cortex-M3
Номер исключения Номер прерывания в CMSIS Имя константы - номера в CMSIS Приоритет Название
0 - - - Значение для инициализации SP
1 - - -3 Reset
2 -14 NonMaskableInt_IRQn -2 NMI
3 -13 HardFault_IRQn -1 HardFault
4 -12 MemoryManagement_IRQn var* MemManage
5 -11 BusFault_IRQn var BusFault
6 -10 UsageFault_IRQn var UsageFault
7..10 - - var Зарезервировано
11 -5 SVCall_IRQn var SVC
12 -4 DebugMonitor_IRQn var DebugMon
13 - - var Зарезервировано
14 -2 PendSV_IRQn var PendSV
15 -1 SysTick_IRQn var SysTick
16 0 WWDG_STM_IRQn var WWDG
17 1 PVD_STM_IRQn var PVD
... ... ... var ...

* var - программируемый приоритет.

По адресу 0x0 в адресном пространстве микроконтроллера должна быть расположена таблица с векторами прерываний - с адресами обработчиков исключений (смотрите также "Минимальное приложение"). Позже таблица может быть программно перемещена в другое место, но после сброса микроконтроллер ожидает обнаружить её по нулевому адресу.

Исключения с номером 0 не существует - соответствующее место в таблице векторов занимает начальное значение для указателя стека SP (это значение помещается в SP сразу после сброса - так осуществляется ранняя инициализация стека, необходимая для того, чтобы микроконтроллер сразу же был готов к обработке исключений).

Исключения с номерами 1..15 являются системными, они определены на уровне архитектуры ARMv7-M и одинаковы для всех микроконтроллеров с этой архитектурой.

Исключения с номерами, начиная с 16 и далее, отведены под прерывания от периферийных устройств (внешние прерывания - внешние по отношению к ядру микроконтроллера). Их количество и назначение определяется разработчиком микроконтроллера. Описание смотрите в документации на устройство.

В CMSIS определены константы с номерами исключений и прерываний для микроконтроллера. Нумерация внешних прерываний в CMSIS начинается с 0, а номера системных исключений являются отрицательными, т.е. в CMSIS нумерация смещена относительно нумерации в таблице векторов на 16.

Каждое прерывание и некоторые из исключений могут быть программно разрешены или запрещены в индивидуальном порядке (Reset, NMI, HardFault всегда разрешены). Кроме того, исключения могут быть разрешены или запрещены на глобальном уровне. В CMSIS для этого предусмотрены специальные функции из core_cm0.h / core_cm3.h

// Разрешить исключения.
void __enable_irq(void);

// Запретить все исключения.
void __disable_irq(void);             

// Разрешить прерывание с номером IRQn,
// IRQn>=0 - только для внешних прерываний.
void NVIC_EnableIRQ(IRQn_Type IRQn);

// Запретить прерывание с номером IRQn,
// IRQn>=0 - только для внешних прерываний.
void NVIC_DisableIRQ(IRQn_Type IRQn); 

После сброса исключения разрешены на глобальном уровне (но запрещены в индивидуальном порядке те, для которых предусмотрена возможность запрета).

Стартовый код из CMSIS обеспечивает реализацию обработчика по умолчанию для всех исключений микроконтроллера. Для того, чтобы использовать собственный обработчик, нужно написать свою функцию-обработчик исключений. Благодаря особенностям архитектуры процессоров Cortex-M, обработчик - это обычная функция языка C. Обработчик не принимает аргументов и не возвращает значения. Имя функции должно быть в точности тем, какое указано в файле стартового кода или в документации. Для системных исключений это будет название исключения из Табл. 1 с приписанным справа "_Handler", а для внешних прерываний - название с приписанным справа "_IRQHandler". Если пишем обработчик на C++, функция должна быть объявлена со спецификатором extern "C" для правильной компоновки. При компоновке адрес обработчика будет правильным образом размещён в таблице векторов, за это отвечает файл стартового кода из CMSIS (в стартовом коде используется "слабое" связывание для функций-обработчиков по умолчанию, поэтому, если при компоновке обнаруживается "сильно" связываемая функция с тем же именем, будет использоваться она). После того, как в коде пользователя соответствующее исключение будет разрешено, обработчик начнёт получать управление при возникновении исключения.

Пример функции-обработчика исключения:

//Обработчик прерывания от сторожевого таймера.
extern "C" void WWDG_IRQHandler(void)
{
    //Код обработчика...
}

Пример кода: обработка исключения вызова супервизора.

//(!) VECT_TAB_SRAM

#include <stm32f10x.h>

//Обработчик исключения SVC - вызов супервизора.
//Зажигает светодиод, подключённый к PC9.
extern "C" void SVC_Handler(void)
{
    GPIOC->BSRR=GPIO_BSRR_BS9;
}

int main()
{
    //Включаем тактирование порта GPIOC.
    RCC->APB2ENR|=RCC_APB2ENR_IOPCEN;

    //Конфигурируем PC9 как двухтактный выход.
    GPIOC->CRH=GPIO_CRH_MODE9_1;

    //Выполняем инструкцию, которая
    //генерирует исключение SVC (после сброса 
    //исключения на глобальном уровне разрешены).
    __asm("svc 0");

    while(true) {}
}

В приведённом примере главная функция сначала выполняет конфигурирование периферии (настраивает вывод PC9, к которому подключён светодиод как выход), после чего генерирует исключение SVC (SVCall) с помощью специально предназначенной для этого инструкции svc n. Здесь не настраивается приоритет исключения, используются значения по умолчанию. Но так, как используется только одно исключение с настраиваемым приоритетом, то его уровень совершенно ни на что не влияет. Обработчик исключения зажигает светодиод, это позволит нам узнать, что обработчик был вызван.

Вообще-то вызов супервизора предназначен для организации взаимодействия между программой и операционной системой и если операционная система не используются, то использовать это исключение особого смысла нет. Но в качестве простейшего примера работы с исключениями, по-моему, вполне годится.

Приоритеты исключений

Каждое исключение имеет определённый приоритет, который задаётся уровнем приоритета. Три исключения имеют фиксированные приоритеты, приоритеты всех остальных могут динамически настраиваться программой, выполняемой на привилегированном уровне.

Выполняемый процессором поток инструкций также имеет определённый приоритет - приоритет исполнения (execution priority). Сразу после сброса процессор начинает выполнять код с базовом уровнем приоритета, это самый низкий возможный приоритет. Базовый уровень приоритета может иметь только код, выполняющийся в режиме треда. Любое исключение имеет более высокий приоритет.

Когда возникает исключение, приоритет которого значимо выше (т.е. в части группы приоритета - см. далее) приоритета исполнения текущего потока инструкций, то исключение становится активным. Выполнение текущего потока прерывается. Процессор автоматически сохраняет контекст в стеке (кроме исключения Reset, сброс обрабатывается совершенно иначе). После этого управление передаётся на код, указываемый соответствующим вектором в таблице векторов исключений. Обработчик исключения всегда запускается в режиме Handler mode - режиме обработчика. Приоритет исполнения повышается до приоритета обрабатываемого исключения.

После завершения выполнения кода обработчика происходит возврат. Если исключение в этот момент имеет состояние активного и отложенного (это значит, что во время обработки это же исключение было сгенерировано опять), и имеет достаточный приоритет, сразу происходит повторный вход в обработчик, а если есть более приоритетные исключения, данное откладывается. При выходе из обработчика, исключение из активного состояния переходит в неактивное. Сохранённый в стеке контекст восстанавливается, если прерванный поток инструкций выполнялся в режиме треда, происходит переключение из режима обработчика в режим треда. Затем выполнение прерванного потока инструкций возобновляется с его приоритетом исполнения.

Программа может повысить приоритет исполнения, используя предназначенные для этого регистры. Если не использовать повышение приоритета, то приоритетом исполнения будет наивысший приоритет из приоритетов всех активных в данный момент исключений или базовый, если нет активных исключений.

Определяющий порядок обработки исключений приоритет задаётся 8-битовым числом, старшая часть которого содержит значение уровня приоритета группы, младшая - субприоритет. Способ разделения приоритета на эти две части зависит от настройки NVIC, разделение можно изменить программно. По умолчанию старшие 7 битов отведены под уровень приоритета группы и один младший бит - под субприоритет. Приоритет группы определяет возможность прерывания обработчика одного исключения для обработки другого. Если в данный момент выполняется обработчик какого-то исключения (исключение активно), то он может быть прерван для обработки исключения с более высоким приоритетом группы (более высокий приоритет задаётся меньшим значением уровня приоритета, нулевой - самый высокий).

Субприоритет определяет порядок обработки исключений при их одновременном возникновении. Если одновременно возникнут несколько исключений с одинаковым приоритетом группы, первым будет обработано то из них, которое имеет меньшую величину субприоритета (более высокий субприоритет). Если же одновременно возникают исключения с одинаковым приоритетом (одинаковым приоритетом группы и субприоритетом), то они будут обрабатываться в порядке нумерации исключений.

Задаётся способ разделения приоритета на уровень приоритета группы и уровень субприоритета с помощью регистра SCB->AIRCR. Биты PRIGROUP регистра содержат позицию старшего бита субприоритета в байте приоритета. Значение этого трёхбитового поля может находиться в пределах 0..7. По умолчанию, после сброса PRIGROUP=0, т.е. один самый младший бит определяет субприоритет, а остальные 1..7 биты (всего 7 битов) задают уровень приоритета группы. Если, например, задать PRIGROUP=1, то два младших бита 0..1 в приоритете будут задавать субприоритет, а 6 старших 2..7 - приоритет группы. Если задать максимальное значение PRIGROUP=7, то весь байт будет задавать субприоритет - это означает, что все исключения будут иметь одинаковый приоритет группы и отличаться только субприоритетом (ни одно исключение с программируемым приоритетом тогда не сможет прервать обработку другого).

Разработчик микроконтроллера может реализовать не все 8 разрядов, задающих приоритет исключения. У него есть возможность отключить несколько младших разрядов. Это уменьшает количество вентилей процессора, соответственно уменьшается занимаемое пространство на кристалле и уменьшается энергопотребление. При чтении из регистра для нереализованных разрядов всегда будет возвращаться 0, а запись в эти разряды игнорируется. Например, в серии микроконтроллеров STM32F100xx реализовано 4 разряда для приоритета (т.е. может быть 16 программируемых уровней приоритета: 0x00, 0x10, 0x20, ..., 0xF0). Если в таком случае задавать значения PRIGROUP от 0 до 3, то все 4 разряда будут кодировать уровень приоритета, а уровней субприоритета не будет - соответствующие биты попадают на нереализованные разряды регистра и всегда содержат 0.

В устройствах Cortex-M3, Cortex-M4 и Cortex-M7 минимальное количество реализованных разрядов в регистрах приоритета - три, т.е. в зависимости от решения производителя может составлять от 3 до 8 разрядов.

Обработка исключения откладывается, если оно генерируется, когда происходит обработка исключения с таким же или большим приоритетом группы, или если одновременно с ним генерируется исключение с таким же уровнем группы и большим субприоритетом. Отложенным становится также то исключение, которое не может быть обработано в данный момент, потому что оно запрещено или если запрещена обработка всех исключений на глобальном уровне. Если во время обработки исключения, это исключение генерируется снова, оно также откладывается. Регистры, в которых исключения отмечаются как отложенные, доступны для чтения и записи, так что можно программно как снять так и установить пометку. Если пометку для данного отложенного исключения снять, процессор "забудет" про это исключение, оно будет пропущено и не будет обрабатываться, даже когда появиться возможность. Если пометку поставить, то будет "отложено" исключение, которое не генерировалось.

Для работы с исключениями в CMSIS предусмотрено несколько удобных функций, избавляющих от необходимости работать напрямую с регистрами. Кроме названных ранее функций для включения/выключения исключений void __enable_irq(void); void __disable_irq(void); void NVIC_EnableIRQ(IRQn_Type IRQn); void NVIC_DisableIRQ(IRQn_Type IRQn); это:

// Возвращает битовое поле PRIGROUP регистра SCB->AIRCR,
// количество битов субприоритета - 1 (или позиция старшего
// бита субприоритета в байте приоритета).
uint32_t NVIC_GetPriorityGrouping(void);

// Функция устанавливает разделение приоритета на
// приоритет группы и субприоритет, допустимые значения
// для PriorityGroup: 0..7.
void NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

// Функция возвращает приоритет прерывания или исключения IRQn.
// Важно! Функция сдвигает значение вправо на количество неиспользуемых
// разрядов приоритета (выравнивает по реализованным битам)!
uint32_t NVIC_GetPriority(IRQn_Type IRQn);

// Функция задаёт приоритет для прерывания или исключения IRQn.
// Меньшее значение соответствует большему приоритету; наивысший
// доступный приоритет - 0.
// Не может быть задан приоритет исключений с фиксированным приоритетом.
// Как и для NVIC_GetPriority, используется смещённое значение.
void NVIC_SetPriority(IRQn_Type IRQn,
                      uint32_t  priority); 

// Выделяет из заданного значения приоритета Priority
// уровень приоритета группы и субприоритет, полученные значения
// помещает по указателям pPreemptPriority, pSubPriority.
// Приоритет группы и субприоритет выравниваются по 0-му биту и
// могут принимать значения 0, 1, и т.д. до максимальных значений.
// Требуется задать параметр PriorityGroup, который имеет тот же
// смысл, что и биты PRIGROUP регистра SCB->AIRCR.
void NVIC_DecodePriority(uint32_t Priority,
                         uint32_t PriorityGroup,  
                         uint32_t *pPreemptPriority,  
                         uint32_t *pSubPriority);
                         
// Функция возвращает значение приоритета для данных значений
// PRIGROUP, приоритета группы, субприоритета.
// Смысл величин тот же, что и в предыдущей функции.
uint32_t NVIC_EncodePriority(uint32_t PriorityGroup,
                             uint32_t PreemptPriority,  
                             uint32_t SubPriority);

// Возвращает бит активности прерывания IRQn, если возвращается:
// 0 - прерывание не активно; 1 - активно.
// Прерывание становится активным, когда начинается его обработка;
// если один обработчик прерывается другим, предыдущий по-прежнему
// остаётся активным.
uint32_t NVIC_GetActive(IRQn_Type IRQn);

// Функция определяет, является ли прерывание IRQn отложенным,
// возвращаемое значение:
// 0 - прерывание не является отложенным; 1 - отложено.
uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn);

// Удаляет пометку об отложенном состоянии для прерывания IRQn,
// IRQn>=0 - только для внешних прерываний.
void NVIC_ClearPendingIRQ(IRQn_Type IRQn);

// Устанавливает бит отложенного прерывания для прерывания IRQn,
// IRQn>=0 - только для внешних прерываний.
void NVIC_SetPendingIRQ(IRQn_Type IRQn);

Функции CMSIS смещают значение приоритета вправо на количество нереализованных битов в регистрах приоритетов. Поэтому нумерация уровней становится последовательной: 0, 1, 2, ..., - до максимального значения, например 15 - для STM32F100xx.

Больше об исключениях

Для более детального рассмотрения работы с исключениями будет полезным знакомство с программной моделью процессора.

Профиль ARMv7-M описывает ряд регистров, тесно связанных с процессором.

Процессоры имеют 13 регистров общего назначения, обозначаемых R0..R12 (из них R0..R7 называют младшими, R8..R12 - старшими) и дополнительно 3 регистра ядра с предопределёнными функциями:

Кроме того, имеется несколько регистров специального назначения. Доступ к ним осуществляется с помощью специальных инструкций (mrs, msr), отличных от инструкций для доступа к регистрам R0..R15. Это регистры PSR, PRIMASK, BASEPRI, FAULTMASK и CONTROL.

Табл. 2. Регистры процессора
R0..R7 Младшие РОН. Регистры общего назначения.
R8..R12 Старшие РОН.
R13 (SP) Указатель на активный стек: основной MSP или стек процесса PSP.
R14 (LR) Регистр связи.
R15 (PC) Счётчик команд.
PSR Флаги состояния и номер исключения, если происходит обработка исключения.
PRIMASK Однобитовый регистр маскирования исключений. Установка в 1 повышает текущий приоритет исполнения кода до 0, т.е. запрещаются все исключения кроме Reset, NMI, HardFault, которые имеют фиксированный отрицательный приоритет. По умолчанию имеет значение 0, исключения разрешены.
BASEPRI Регистр определяет уровень приоритета маскируемых прерываний. Если в регистр записано отличное от 0 значение, то исключения с таким приоритетом и более низким запрещены. Значение 0 (значение по умолчанию) отключает маскирование по уровню приоритета.
FAULTMASK Однобитовый регистр маскирования ошибок. Установка в 1 повышает текущий приоритет исполнения кода до -1, что означает запрет всех исключений, кроме Reset и NMI (запрещается даже HardFault, в отличии от PRIMASK=1). Значение по умолчанию 0, что разрешает исключения.
CONTROL Задаёт уровень привилегий и используемый стек для режима треда.

Для доступа к регистрам ядра микроконтроллера PRIMASK, FAULTMASK, BASEPRI, CONTROL, PSP, MSP можно использовать функции CMSIS (определены в файле core_cm3.h):

Табл. 3. Функции для доступа регистрам ядра
Функция Действие
void __enable_irq(void) PRIMASK=0 (инструкция cpsie i), разрешить прерывания на глобальном уровне.
void __disable_irq(void) PRIMASK=1 (инструкция cpsid i), запретить прерывания на глобальном уровне.
void __set_PRIMASK(uint32_t value) PRIMASK=value (используется интсрукция msr).
uint32_t __get_PRIMASK(void) Возвращает значение PRIMASK (используется mrs).
void __enable_fault_irq(void) FAULTMASK=0 (cpsie f)
void __disable_fault_irq(void) FAULTMASK=1 (cpsid f)
void __set_FAULTMASK(uint32_t value) FAULTMASK=value (используется интсрукция msr).
uint32_t __get_FAULTMASK(void) Возвращает значение FAULTMASK (используется mrs).
void __set_BASEPRI(uint32_t value) BASEPRI=value
uint32_t __get_BASEPRI(void) Возвращает значение BASEPRI.
void __set_CONTROL(uint32_t value) CONTROL=value
uint32_t __get_CONTROL(void) Возвращает значение CONTROL.
void __set_PSP(uint32_t TopOfProcStack) PSP=TopOfProcStack
uint32_t __get_PSP(void) Возвращает значение PSP.
void __set_MSP(uint32_t TopOfMainStack) MSP=TopOfMainStack
uint32_t __get_MSP(void) Возвращает значение MSP.

Процессоры с архитектурой ARMv7-M имеют два режима работы: режим треда (Thread mode, так же имеет название "режим потока") и режим обработчика. В режиме треда процессор может находиться на одном из уровней привилегий: на привилегированном или непривилегированном. Кроме того, в режиме треда может использоваться как основной стек (тот же, который используется в режиме обработчика) или стек процесса. Всё это интересно в большей степени для разработчиков операционных систем, чем разработчикам приложений, но обсуждение исключений будет неполным, если хотя бы очень кратко не затронуть названные вопросы.

После сброса процессор начинает выполнять код в режиме треда с высоким уровнем привилегий. Основное назначение режима треда - выполнение кода программ.

Работая в режиме треда, процессор может находится на привилегированном или непривилегированном уровне. После старта процессор начинает выполнение программы на привилегированном уровне. На привилегированном уровне нет ограничений на доступ к ресурсам процессора. Установив младший бит регистра CONTROL в 1, можно переключится на непривилегированный уровень. На непривилегированном уровне имеется ряд ограничений. Так, запрещён доступ к регистрам SCS (System Control Space), запрещён доступ регистрам специального назначения, в том числе к регистру CONTROL. Таким образом, в режиме треда можно переключиться с привилегированного на непривилегированный уровень, а обратно уже не удастся - регистр CONTROL становится недоступным. Обратное переключение возможно только через исключение.

Обработчики исключений всегда выполняются на привилегированном уровне. После возврата из обработчика и переключения в режим треда происходит переход на уровень привилегий, определяемый регистром CONTROL. Если установить CONTROL[0]=0, то в режиме треда процессор перейдёт на привилегированный уровень.

Обычно непривилегированный уровень используется для запуска программы под управлением операционной системы, код ядра самой ОС выполняется на привилегированном уровне. Ограничение прав доступа программы к аппаратуре позволяет построить более безопасную систему. Программа вынуждена работать с критически важными ресурсами через функции, предоставляемые операционной системы и таким образом находится под её контролем и не может нарушить работу других программ и самой ОС.

Кроме того, для того, чтобы некорректная работа программы со стеком не нарушила работу операционной системы, предусмотрена возможность в режиме треда использовать отдельный стек процесса. В процессоре есть два указателя стека: основной MSP и указатель стека процесса PSP. С помощью SP (R13) осуществляется доступ к активному в данный момент указателю (с помощью команд доступа к специальным регистрам, можно работать непосредственно с MSP и PSP).

В режиме обработчика активным всегда является MSP. В режиме треда активным может быть MSP или PSP, в зависимости от бита CONTROL[1]. По умолчанию CONTROL[1]=0 и в режиме треда, как и в режиме обработчика используется основной стек. Для переключения на использование стека процесса в режиме треда есть два способа. Во-первых, можно в режиме треда на привилегированном уровне установить CONTROL[1] в 1. Во-вторых, в режиме обработчика можно изменить второй бит регистра LR, тогда после возврата из обработчика произойдёт установка CONTROL[1] в 1. Непосредственно в обработчике изменить бит нельзя, что вполне логично, так как в режиме обработчика может использоваться только основной стек и бит CONTROL[1] всегда сброшен.

В своих программах, запускаемых без операционных систем, мы сами полностью контролируем свой код. В таких условиях нет смысла использовать непривилегированный уровень и стек процесса, это не добавит надёжности программе, а только усложнит её. Поэтому имеет смысл оставить используемый по умолчанию основной стек и привилегированный уровень выполнения кода в режиме треда.

author: hamper; date: 2015-12-19
  @Mail.ru