8L-Курс, Часть 3 - Прерывания, EXTI

← Часть 2 — GPIO Содержание Часть 4 — Тактирование →

Мы уже умеем работать с GPIO. Выставлять нужные уровни и читать состояния ножек, настроенных на вход. Но что, если нужно не просто прочитать состояние ножки, а быстро отреагировать на смену уровня? А если таких ножек несколько? Тут нам на помощь приходит система внешних прерываний — EXTI (EXTernal Interrupts). Она довольно гибко настраивается и позволяет получить прерывание с любой ножки МК, что дико удобно — не нужно подстраивать разводку платы под пару специальных ножек INT, как это было в большинстве AVR.

Перед тем, как разбираться с внешними прерываниями, надо-бы понять как вообще устроена подсистема прерываний в STM8.

Контроллер прерываний (ITC — InTerrupt Controller) в стмках поддерживает вложенные прерывания и разделяет их по приоритетам. Это означает, что если у нас сейчас выполняется обработчик какого-то не очень важного прерывания (например от таймера), и вдруг случилось прерывание с более высоким приоритетом, то прерывание от таймера будет прервано и МК немедленно бросится обрабатывать другое. А если приоритеты одинаковы (или приоритет нового ниже того, что сейчас обрабатывается) — новое прерывание начнет обрабатываться только после завершения предыдущего.

А что будет, если два прерывания возникли одновременно? Или несколько прерываний произошли во время обработки более важного? А тогда первым пойдет обрабатываться то, у которого выше приоритет. А если и приоритеты одинаковы? — спросит дотошный читатель. Тогда первым будет выполняться то прерывание, которое стоит ближе к началу в таблице прерываний.

Всего есть 29 векторов прерываний + два без номера: RESET — сброс МК, и TRAP — программное прерывание (вызывается ассемблерной командой TRAP). Строго говоря, есть два типа приоритетов — программный (который мы можем настраивать для каждого прерывания) и аппаратный — это как раз порядок векторов в таблице прерываний. По табличке видно, что самый высокий аппаратный приоритет у прерывания FLASH памяти, а прерывание от I2C вообще обделено вниманием, находясь в самом конце таблицы. Впрочем, никто не запрещает сделать его самым важным, выставив высокий программный приоритет — он важнее, чем аппаратный.

Прерывание TRAP вообще плюет на все программные приоритеты. Независимо от того, какой приоритет имеет текущее прерывание, выполнение команды TRAP тут-же уносит нас в обработчик.

А для других прерываний можно настроить приоритет через регистры ITC_SPR1 — ITC_SPR8.

Каждому вектору тут выделено по два бита, что дает 4 возможных значения. Но на самом деле программных приоритетов всего 3:
01 - Самый низкий
00 - Среднй
11 - Самый высокий

Значение 10, которого нет в списке, соотвествует приоритету основной программы, и установить его нельзя. Да и бессмысленно, если вспомнить как контроллер разруливает очередь прерываний: прерывание с таким-же приоритетом как у основной программы, никогда ее не прервет.

После того, как нужные прерывания разрешены и настроены, надо глобально разрешить обработку прерываний. В STM8 для этого служит ассемблерная команда RIM (А для запрета прерываний — SIM) Причем работают они весьма интересным образом. Вместо установки какого-либо флага разрешения/запрета прерываний (как было например в AVR) тут они меняют приоритет кода, который сейчас выполняется. Команда RIM выставляет низкий приоритет (10, тот самый который нельзя выставить для прерываний) и теперь любое прерывание может прервать выполнение программы. А команда SIM ставит текущему коду наивысший приоритет и ни одно прерывание уже не может помешать выполнению кода.

У каждого прерывания есть свой флаг, который обозначает, что событие случилось и пора бежать в обработчик. В STM8 эти флаги, как правило, сами не сбрасываются и их приходится опускать вручную. Иначе мы после выхода из обработчика снова попадем в него-же. А что? Флаг поднят, значит надо обработать прерывание.

Правда, сброс флагов в разных случаях организован по-разному. Иногда для этого нужно сбросить флаг самому, иногда — прочитать определенный регистр (например в прерывании от АЦП — прочитать результат измерения). Далее, при описании разной периферии я буду давать шаблоны обработчиков, и указывать каким именно образом там сбрасываются флажки прерываний.

Кстати, а как быстро МК перескакивает в обработчик прерывания?
А вот:

По такому сценарию происходит обработка прерывания. Любого.
Сначала ядро завершает выполнение текущей команды. На это требуется от 1 до 6 тактов.
Затем адрес для возврата из прерывания и процессорные регистры пихаются в стек, чтобы после выхода из обработчика вернуть их изначальное значение. На это уходит еще 9 тактов. После чего происходит переход по адресу, хранящемуся в таблице прерываний. Сколько тактов уходит на это — в документации не сказано. Но если измерить время от возникновения события, которое приводит к прерыванию, до перехода в обработчик, то выходит вот это:

Здесь красный канал прицеплен к пину, который поднимается по событию от таймера, а желный — к пину, который поднимается в прерывании по этому событию (самой первой командой). МК работает на частоте 1МГц, и задержка 15 мкс соотвествует 15 тактам.

Время входа в прерывание можно уменьшить, если использовать режим WFI — Wait For Interrupt. Это энергосберегающий режим, который предназначен для замены задержек в ожидании прерывания. В WFI ядро отключается, а остальная периферия и тактовый генератор продолжают работать (включенный тактовый генератор позволяет быстро выйти из спящего режима — не тратится время на его запуск). При этом, перед отключением ядра происходит сохранение в стеке текущего адреса и регистров процессора — подготовка к входу в прерывание.

Когда прерывание происходит в режиме WFI, МК переходит в обработчик без лишних задержек. На практике, время входа в прерывание сокращается на 5 тактов. Вот:

Для входа в WFI надо выполнить ассемблерную команду WFI (Капитан Очевидность активно помогал писать статью)
asm("WFI");

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

Ну вот, теперь некоторое представление о том, как работают прерывания в STM8 у нас есть. Плавно переходим к EXTIEXTernal Interrupts, то есть внешним прерываниям.

Если посмотреть на таблицу прерываний, видно, что на EXTI выделено 11 векторов.
Три из них имеют в названии букву, подозрительно напоминающую название порта (EXTIE/F, EXTIB...). А остальные 8 — цифру, не менее подозрительно похожую на номер пина. RM0031 развеивает все наши сомнения и подтверждает подозрения — из-за того что векторов прерываний банально не хватит на все пины, система организована весьма замысловатым образом.

Начнем ее разбирать с самого начала. Для каждого пина можно индивидуально разрешить или запретить прерывание. Это делается, если помните предыдущую часть, через регистр Px_CR2, где x — буква порта (перед этим надо настроить пин на вход). Таким образом можно разрешить прерывания только для нужных пинов, а остальные никак не будут мешаться.

Далее, для каждой половины порта (то есть для 4 младших ножек и для 4 старших) мы можем выбрать, куда пойдет сигнал — на прерывания пинов (EXTI0, EXTI1...), или портов (EXTIB, EXTID...). В первом случае мы получаем индивидуальное прерывание для каждого пина. Но прерывания с других портов будут приходить сюда-же. Во втором случае у нас будет один обработчик для сигнала со всех четырех пинов (если мы конечно разрешим прерывания со всех), но зато индивидуальный для каждого порта. Все это настраивается через регистры EXTI_CONF1 и EXTI_CONF2:


Каждый бит тут соответсвует старшей или младшей половине какого-либо порта. Например, PDLIS (Port D Low Interrupt select) отвечает за пины 0..3 порта D.

Установка бита в единицу означает, что эта половина порта будет давать одно прерывание на все пины (например EXTID). А ноль соответсвенно значит, что каждый из четырех пинов будет давать свое прерывание из группы EXTI0..EXTI7.

Кроме того, для каждого вектора (именно вектора, а не отдельного пина) можно настроить фронт по которому сработает прерывание. Делается это через регистры EXTI_CR1 — EXTI_CR4.


Каждому прерыванию тут отводится по два бита. Соотвественно, доступно 4 режима работы прерывания:
00 — Срабатывает по переходу из 1 в 0 (задний фронт) и по низкому уровню. То есть обработчик будет вызываться постоянно, пока пине низкий уровень.
01 — По переходу из 0 в 1 (передний фронт)
10 — Опять по заднему фронту, но уже без низкого уровня
11 — По обоим фронтам. Иначе говоря, срабатывает при любом изменении уровня.

Названия для битов формируются просто. P1IS — Pin 1 Interrupt Sensitivity, PBIS — Port B Interrupt Sensitivity и так далее.

Флаги прерываний (про которые я выше писал, что их надо сбрасывать) лежат в регистрах EXTI_SR1 и EXTI_SR2.


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

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

Что касается STM8S, то там с прерываниями не так запутанно (но и возможностей меньше). По сути, есть только прерывания от портов. Как PCINT в AVR. На один вектор сходятся сигналы с каждого из пинов данного порта. Разрешать прерывания можно индивидуально для каждого пина, а вот фронт настраивается только для каждого вектора — т.е. для целого порта.

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

Если зажать первую кнопку, МК зависнет в обработчике прерывания и начнет перебирать цифры. Но при нажатии на вторую кнопку, первое прерывание прервется более важным, и начнет мигать светодиод. Если отпустить вторую кнопку, то индикатор продолжит перебирать цифры с той, на которой остановился — обработчик первого прерывания продолжит выполняться. Просто, наглядно.

Для начала нужно настроить пины. Пусть первая кнопка висит на D6, а вторая — на D7 (эти пины удобнее всего подключать к кнопкам на Pinboard).
PD_CR1_bit.C16 = 1; //На вход..
  PD_CR2_bit.C26 = 1; //Прерывание разрешено

  PD_CR1_bit.C17 = 1;
  PD_CR2_bit.C27 = 1;

Регистр DDR мы не трогаем — он и так обнулен при старте МК.

В регистре EXTI_CONF1 (который распределяет сигналы на прерывания пинов/портов) при старте тоже нули, значит каждый пин будет давать нам отдельное прерывание. Так и оставим.

Еще нужно настроить фронт, по которому сработает прерывание. Кнопки при нажатии замыкаются на землю, значит ловить мы должны задний фронт, которому соответсвует значение 2 (10):
EXTI_CR2_bit.P6IS = 2;
  EXTI_CR2_bit.P7IS = 2;

И наконец, надо понизить приоритет у прерывания от первой кнопки (D6), чтобы прерывание от второй могло прерывать обработчик первого:
ITC_SPR4_bit.VECT14SPR = 0;

Смотрите в таблицу прерываний — вектор EXTI6 у нас идет под номером 14 и ему соотвествует группа VECT14SPR. Значение 0, напомню, означает «средний» приоритет.

Настройка индикатора и светодиода перекочевала из прошлых примеров, поэтому я ее тут приводить не стану — смотрите исходники.

Осталось только глобально разрешить прерывания. Делается это ассемблерной командой RIM
asm("RIM");

На этом с main() закончили и переходим к обработчикам прерываний. В IAR обработчик объявляется вот таким хитрым образом:
#pragma vector=[Номер вектора]
__interrupt void [Название обработчика](void)

Чтобы каждый раз не лезть в даташит за номером нужного вектора, можно взять дефайны из iostm8l151k6.h. В самом конце файла прописаны все векторы. Вот кусок для примера:
#define EXTI6_vector                         0x10
#define EXTI7_vector                         0x11
#define CLK_CSS_vector                       0x13
#define CLK_SWITCH_vector                    0x13
#define TIM1_BIF_vector                      0x13
#define COMP_EF1_vector                      0x14
#define COMP_EF2_vector                      0x14
#define TIM2_OVR_UIF_vector                  0x15

Ой, а почему вектору EXTI6 соответствует номер 0x10 (16), а не 14, как в табличке из даташита? А просто тут учитываются первые два прерывания — RESET и TRAP. А в таблице они идут без номеров.

А название обработчика — это просто название функции, и никаких особых требований к нему нет.

Получается, что обработчик прерывания EXTI6 мы можем обозначить так:
#pragma vector=EXTI6_vector 
__interrupt void Pin6_interrupt(void) 
{
... 
}

Любители лаконичности могут, если захотят, свернуть это в дефайн:
#define STRINGVECTOR(x) #x
 #define ISR( a, b )  \
 _Pragma( STRINGVECTOR( vector = (b) ) )        \
 __interrupt void (a)( void )

Тогда прерывания будут обозначаться так:
ISR(Pin6_interrupt, EXTI6_vector)
{
...
};

Что, согласитесь, выглядит приятнее.

Обработчики прерываний в нашем примере будут почти одинаковые (за исключением того, что в первом переключаются цифры, а во втором мигает диод). Поэтому покажу только обработчик первой кнопки:
ISR(Pin6_interrupt, EXTI6_vector)
{
 while (PD_IDR_bit.IDR6 == 0)
 {
  value++; //Инкрементируем счетчик
  if (value==10) value=0; //Проверяем - не ушел ли он за предел
  PB_ODR = numbers[value];   //Выводим число на индикатор
  SomeDelay();
 };

  EXTI_SR1_bit.P6F = 1; //Перед выходом из прерывания необходимо вручную сбросить флаг,
  //Иначе тут-же вернемся обратно в обработчик.
};

Думаю тут все должно быть ясно. Во втором обработчике мы мигаем светодиодом на D5, до тех пор пока не появится высокий уровень на пине D7.

Вот вроде бы и все касательно внешних прерываний.



На последок расскажу про интересную фичу по имени Activation Level, которая тоже имеет отношение к прерываниям.
Довольно часто получается так, что вся программа (ну кроме начальной настройки) разбросана по обработчикам прерываний. А в main красуется один жалкий бесконечный цикл-затычка. У нас как-раз такой случай. А если наше устройство питается от батарей и ему необходимо экономить энергию, то между прерываниями оно будет находиться в спящем режиме и выходить из него только для выполнения обработчиков.

Ребята из STM решили облегчить нам жизнь при построении подобных алгоритмов и запилили бит AL. Находится он в регистре CFG_GCR (Configiration — General Configuration Register)

Если записать в него единичку, и уйти в спящий режим, то после того как МК проснется по прерыванию и выполнит обработчик, он не попадет в main() а сразу улетит обратно в спящий режим. Про спящие режимы мы еще ничего толком не знаем, но попробовать эту фичу хочется.

Давайте заменим цикл-заглушку в конце на такую конструкцию:
CPU_CFG_GCR_bit.AL = 1;

  asm("halt");

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

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

Исходники примера

← Часть 2 — GPIO Содержание Часть 4 — Тактирование →
  • +11
  • 09 февраля 2013, 14:35
  • dcoder
  • 1
Файлы в топике: 3_GPIO_Interrupts.zip

Комментарии (4)

RSS свернуть / развернуть
Ой, а почему вектору EXTI6 соответствует номер 0x10 (16), а не 14, как в табличке из даташита? А просто тут учитываются первые два прерывания — RESET и TRAP. А в таблице они идут без номеров.
когда впервые на это напоролся — очень сильно матюкался… В чем кроется тайный смысл оного?
Еще убивает то, что в определениях ИАРа многие векторы названы так, что фиг найдешь нужное прерывание. Для ADC, например. Проще самому обьявить, или просто писать магическую цифирь.
0
Тогда «кто первый встал — того и тапки»
Ошибочная фраза.
а прерывание от I2C вообще обделено вниманием
Ага, в вашей таблице его вообще нет :)
Сначала может показаться, что она не очень удобна в работе.
Мне сначала показалось, что она очень укуренна. Потом тоже, впрочем.

P.S.: Надо ещё приписать, что бы не пытались это повторить на STM8S-серии :)
0
Табличку пофиксил (там продолжение на другой странице было, я его не захватил сразу), про STM8S написал, даже тапки убрал :)

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