Прерывания в STM32

В этой статье я планирую поделиться с читателями своими скромными познаниями в области прерываний. Начать следует с того, что же представляют собой прерывания. Прерывание - это событие как правило связанное с каким-либо блоком периферии микроконтроллера STM32. Событий которые могут породить прерывание может быть множество. Например если речь о таком блоке периферии как UART, то там могут быть такие события: передача завершена, приём завершен, возникла ошибка чётности итд. Использование прерываний позволит нашей программе мгновенно регировать на подобные события. Сам термин прерывание говорит о том, что что-то должно прерваться и в нашем случае прервется выполнение основного кода вашей программы и управление будет передано некоторому другому куску кода который называется обработчиком прерывания. Таких обработчиков достаточно много, ибо периферийных устройств в STM32 предостаточно. Стоит отметить важный момент: В случае возникновения двух разных прерываний от одного блока периферии возникает одно и тоже прерывание. Например если произойдет прерывание по приёму байта через UART и прерывание по завершению передачи через тот же UART, то в обоих случаях будет вызван один и тот же обработчик. Для того чтоб определить какое из возможных прерываний произошло нужно смотреть на флаги состояния. И само собой очищать их перед выходом из прерывания. Когда обработчик прерывания отработает, управление будет передано той самой строчке кода, во время выполнения которой наступило прерывание. То есть основная программа продолжит работать дальше как ни в чем не бывало.

Что нужно знать о прерываниях

  1. Они все независимо включаются/выключаются
  2. Имеют приоритет
  3. Могут быть вызваны программно
  4. Если для прерывания нет обработчика, а оно возникло, то будет вызван обработчик по умолчанию

С первым пунктом всё более-менее понятно, но есть один нюанс. Прерывание не возникнет, если оно запрещено (а оно по умолчанию запрещено) для какого-либо конкретного блока периферии. Чтоб его включить существует функция NVIC_EnableIRQ, ну а для выключения соответственно NVIC_DisableIRQ. В качестве параметра этим двум функциям надо передать переменную типа IRQn. Этот тип является перечислением и соответственно переменная может принимать одно значение из списка который описан в заголовочном файле для вашего контроллера (у меня этот файл называется stm32f10x.h). Пример использования: 

 NVIC_EnableIRQ (USART1_IRQn); // Разрешаем прерывания от USART1
 NVIC_EnableIRQ (ADC1_IRQn); // Разрешаем прерывания от АЦП
 NVIC_DisableIRQ (USART1_IRQn); // Запрещаем обратно
 NVIC_DisableIRQ (ADC1_IRQn); // Для АЦП запрещаем прерывания тоже

Следующий важный момент это приоритеты прерываний. Предположим, что у нас возникло прерывание с приоритетом 10 и управление было передано в его обработчик. Если в ходе исполнения кода обработчика возникнет прерывание приоритет которого меньше 10, то исполнение прервется и управление будет передано обработчику более высокоприоритетного прерывания. Чем меньше приоритет - тем прерывание более приоритетное. Если ничего не менять, то все прерывания имеют такие приоритеты, как это написано в референс мануале (см. таблицу Interrupt and exception vectors). Интересно, что будет если назначить одинаковые приоритеты всем? Судя по той табличке о которй я писал выше, в случае одновременного возникновения двух прерываний с одинаковыми приоритетами, будет вызван обработчик того прерывания которое имеет наименьшее значение в столбце "Position". Но это всего лишь моё предположение и точно я ничего сказать не могу.  Прерывания могут быть вызваны вашим кодом, просто поставьте нужный флаг и оно случится (если конечно его ни кто не запрещал). Хотя ни кто не запрещает вам вызвать обработчик просто как обычную функцию без всех этих извратов с флагами. И наконец последнее: Если вдруг каким-то чудом у вас случается прерывание для которого нет обработчика, то управление передается в обработчик по умолчанию, в теле которого находится пустой бесконечный цикл. Ну а теперь еще немного теории и попробуем написать несложную программу которая обрабатывает прерывания от обыкновенного порта ввода/вывода. Такие прерывания могут возникать в случае если на ноге: 1) появилась лог.1, 2) появился лог.0, 3) лог. уровень сменился на противоположный. Прерывание может генерировать любая ножка любого порта, но таких ног может быть не более 16 штук. Вот тут-то STM32 страшно рулит по сравнению с большинством AVRок у которых подобных прерываний всего два и они жестко привязаны к определённым ногам. Всего может существовать семь обработчиков внешних прерываний: EXTI0, EXTI1, EXTI2, EXTI3, EXTI4, EXTI9_5, EXTI15_10 а что касается выводов, то их у нас куда больше. Прерывание от нулевой ноги любого порта всегда будет обрабатываться при помощи обработчика EXTI0, прерывание второй ноги будет вызывать обработчик EXTI2 и так далее. Что касает ножек с 5-й по 9-ю то для любой из них будет вызван обработчик EXTI9_5, аналогичная ситуация и с EXTI15_10. Существует ограничение: нельзя настроить прерывание таким образом чтоб оно возникало от двух ног с одинаковым номером. Например нельзя PA1 и PB1 одновременно настроить. Для того чтоб определить ноги каких портов будут вызывать возникновение прерываний, служат 4 регистра AFIO_EXTICR1...AFIO_EXTICR4. Они все похожи друг на друга, покажу тут только первый:

AFIO EXTICR1В каждом таком регистре есть 4 группы по 4 бита в каждой. Каждая группа соответствует ноге от нулевой до пятнадцатой. Изменяя биты группы можно задать какому порту соответствует эта нога. Возможны такие комбинации: 
 

EXTI3

EXTI2

EXTI1

EXTI0

Порт

0

0

0

0

PA

0

0

0

1

PB

0

0

1

0

PC

0

0

1

1

PD

0

1

0

0

PE

0

1

0

1

PF

0

1

1

0

PG

Если мы хотим чтоб ноги PB3, PC2, PA1 и PD0 могли порождать прерывания то нужно записать в регистр AFIO_EXTICR1 такую комбинацию бит: 0001 0010 0000 0011. По умолчанию во всех 4-х регистрах AFIO_EXTICR записаны нули, а то означает, что каждая нога порта А готова генерировать прерывания. Но чтоб это происходило нужно явно разрешить это при помощи регистра EXTI_IMR

EXTI IMR


Нам интересны первые 16 бит, каждый бит соответствует ноге, если он установлен то нога может генерить прерывания. Как я уже писал выше, прерывание может возникнуть по нарастающему или спадающему фронту а так же при любом изменении лог. уровня на ноге. Настраивается это в регистрах EXTI_RTSR и EXTI_FTSR. В них нас так же интересуют первые 16 бит. Если мы установим бит в регистре EXTI_RTSR то соответствующая нога при   появлении на ней лог. 1 вызовет прерывание. Ну а соответственно установив бит в EXTI_FTSR мы разрешим ноге генерить прерывание в момент появления лог 0. Если для одной ноги стоят биты в обоих этих регистрах, то прерывание будет возникать всякий раз когда происходит изменение логического уровня. Еще есть регистр EXTI_SWIER при помощи которого вы можете программно вызвать прерывание от имени любой ноги. Достаточно просто записать единицу в интересующий нас бит. И последний важный регистр EXTI_PR. Это регистр флагов, флаги устанавливаются в зависимости от того какая нога вызвала прерывание. Находясь в обработчике прерывания EXTI9_5, при помощи этого регистра легко проверить какая из ног (от 5 до 9) вызвала прерывание. Важный момент: Флаг надо сбрасывать самому в обработчике прерывания. Сбрасывается он записью единицы в соответствующий бит. Теперь попробуем сделать небольшой практический пример. Есть две кнопки и два светодиода. Нажимаем кнопку и светодиод меняет своё состояние на противоположное. Я использовал как обычно свою дискавери прикрутив к ней еще одну кнопку с резистором: 

interrupts

А вот собственно код который вдохнет жизнь в железку: 

 

 

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
int main() {
  GPIO_InitTypeDef PORT;
  //Затактируем все три порта
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , ENABLE);
  //Прерывания - это альтернативная функция порта
  //поэтому надо установить бит Alternate function I/O clock enable
  //в регистре RCC_APB2ENR
  RCC_APB2PeriphClockCmd(RCC_APB2ENR_AFIOEN , ENABLE);
  // Настроим ноги со светодиодами на выход
  PORT.GPIO_Pin = (GPIO_Pin_9 | GPIO_Pin_8);
  PORT.GPIO_Mode = GPIO_Mode_Out_PP;
  PORT.GPIO_Speed = GPIO_Speed_2MHz;
  GPIO_Init(GPIOC, &PORT);
  //Наша задача получить в регистре EXTICR[0] такую комбинацию бит
  // 0000 0000 0001 0000
  // по умолчанию там ноли, поэтому установим только 1 бит
  AFIO->EXTICR[0]|=AFIO_EXTICR1_EXTI1_PB;
  //Прерывания от нулевой и первой ноги разрешены
  EXTI->IMR|=(EXTI_IMR_MR0 | EXTI_IMR_MR1);
  //Прерывания на обоих ногах по нарастающему фронту
  EXTI->RTSR|=(EXTI_RTSR_TR0 | EXTI_RTSR_TR1);
  //Разрешаем оба прерывания
  NVIC_EnableIRQ (EXTI0_IRQn);
  NVIC_EnableIRQ (EXTI1_IRQn);
  while(1)
  {
     //Программа ничего не делает в пустом цикле
  }
}
// Обработчик прерывания EXTI0
void EXTI0_IRQHandler(void)
{
  GPIOC->ODR^=GPIO_Pin_8; //Инвертируем состояние светодиода
  EXTI->PR|=0x01; //Очищаем флаг
}
// Обработчик прерывания EXTI1
void EXTI1_IRQHandler(void)
{
  GPIOC->ODR^=GPIO_Pin_9; //Инвертируем состояние светодиода
  EXTI->PR|=0x02; //Очищаем флаг
}

 

Как обычно я написал пояснения к программе в самом коде, но если есть вопросы то пишите ниже. 

P.S. если я что-то не так написал, то тем более пишите.