Что такое прерывание в микроконтроллере
Перейти к содержимому

Что такое прерывание в микроконтроллере

  • автор:

8. Прерывания, вектор прерываний.

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

[Можно ли обрабатывать эти события без использования прерываний?

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

Чем неудобен подобный подход?

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

2) Некоторые события в принципе не могут долго ждать, например, обработка приёма данных по USART – если пропустить хоть один байт, логика работы программы будет нарушена, а повторная пересылка данных может быть не предусмотрена.

Именно поэтому для организации более эффективного использования ресурсов микроконтроллера и используют прерывания.]

Как работают прерывания?

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

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

Контроллер стартует с адреса 0x00 (нулевого адреса памяти программ). Далее мы делаем безусловный переход на метку RESET. Если этого не сделать, то контроллер начнёт выполнять команды из таблицы векторов прерываний, что нам совершенно не нужно.

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

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

// инициализация стека и инициализация периферии

Что происходит в том случае, если одновременно возникает не одно прерывание, а несколько?

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

Обработка прерываний

[ВОПРОС ИЗ КОНТРОЛЬНОЙ – краткое описание того, что ниже.

В билете можно писать это, а потом читать остальное для пояснения]

Выберите правильную последовательность действий при обработке прерывания:

1) Для включения прерывания программа должна установить флаг I регистра SREG в единицу и записать в регистры маски такой код, который разрешит лишь нужные в данный момент прерывания

2) При поступлении запроса на прерывание устанавливается флаг соответствующего прерывания

3) Флаг I автоматически сбрасывается, запрещая обработку других прерываний. Флаг, соответствующий вызванному прерыванию, также сбрасывается, сигнализируя о том, что МК уже приступил к его обработке.

4) После окончания обработки очередного прерывания происходит проверка остальных флагов, и если имеется хоть одно не обработанное прерывание, МК переходит к его обработке.

Для глобального разрешения или запрещения прерываний предназначен флаг I регистра состояния SREG. Для разрешения работы прерываний он должен быть установлен в единицу (это делается с помощью ассемблерной команды SEI), а для запрещения сброшен (командой CLI). По умолчанию (после сброса микроконтроллера) этот флаг сброшен, и все прерывания микроконтроллера запрещены.

Для каждого блока периферии существует собственный регистр для разрешения локальных прерываний (E/TIMSK). При возникновении прерывания флаг I регистра SREG сбрасывается на аппаратном уровне, запрещая тем самым обработку следующих прерываний. При возврате из подпрограммы обработки прерывания (при выполнении команды RETI) флаг I устанавливается обратно.

Все доступные для работы прерывания можно разделить на два типа.

I тип) возникают при наступлении некоторого события, в результате которого устанавливается флаг прерывания.

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

II тип) Не имеют флагов прерываний и возникают в течение всего времени, пока присутствуют условия, необходимые для их возникновения.

Если условия, вызывающие прерывание, исчезнут до разрешения прерывания, генерации прерывания не произойдет.

Поскольку прерывание нарушает стандартный ход выполнения программы и может возникнуть в любой момент времени, необходимо сохранить текущие данные. Для этого необходимо использовать стек, туда нужно переместить все регистры, используемые в обработчике прерывания, чтобы не потерять хранящиеся там данные. Также необходимо сохранять регистр флагов SREG, в котором хранится результат логических операций:

PUSH R16 // Сохраняем регистр R16

IN R16, SREG // Перемещаем содержимое SREG в R16

PUSH R16 // Сохраняем всё в стек

PUSH R17 // R17 сохраняем туда же

…………… // Выполнение кода обработчика прерывания

POP R17 // Перед выходом из прерывания извлекаем

POP R16 // сохранённые данные

OUT SREG, R16 // Действуем при этом в обратном порядке

RETI // Выходим из прерывания

Также существует ряд операций, которые должны выполняться неразрывно, например, чтение 16-разрядного счетного регистра таймера.

16-разрядный регистр считывается в два приема: сначала считывается младший байт, а потом старший. (ЗАПИСЬ ПРОИСХОДИТ В ОБРАТНОМ ПОРЯДКЕ)

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

Прерывания

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

Прерывания

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

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

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

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

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

Время реакции на прерывание

tp – время реакции системы на прерывание;
tз – время запоминания состояния прерываемой программы;
tппр – время собственно прерывающей программы;
tв – время восстановления состояния прерванной программы

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

Глубина прерываний

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

Характеристиками системы прерывания являются:

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

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

Начинаем изучать Cortex-M на примере STM32, часть 2

Данная статья является продолжением цикла по программированию микроконтроллеров на базе ядра Cortex-M.
Первую статью можно прочитать здесь:
Начинаем изучать Cortex-M на примере STM32
Задачей статей является подробное описание особенностей, возникающих при программировании МК. Материал не предназначен для желающих за 10 минут запустить пример мигания светодиодом. Я постараюсь подробно описать то, что часто скрывают от новичков, чтобы их не напугать.

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

Основной акцент сделан на изучение документации на ядро Cortex-M и документации на конкретный контроллер.
На этот раз речь пойдет про прерывания, а так же будут затронуты некоторые вопросы архитектуры памяти и структуры прошивки МК.

Несколько слов про документацию ARM

По не совсем ясным для меня причинам, нельзя зайти на сайт ARM и скачать полную документацию на ядро Cortex-M4. Да и на Cortex-M3 тоже нельзя.
Придется почитать несколько документов.
1. Изучение придется начать с Cortex ™-M3 TechnicalReference Manual Revision: r1p1 — самой первой ревизии технической спецификации на ядро Cortex-M3
2. Во всех дальнейших ревизиях и описании Cortex ™-M4 TechnicalReference Manual описаны лишь общие данные и изменения относительно предыдущего документа.
Так что прошу не удивляться ссылкам на спецификации другого ядра.

Interrupt and Events

Прежде всего необходимо разобраться с тем, что такое прерывания.
В МК Cortex-M есть два понятия, которые часто путают Interrupt и Event.
Event — это событие (аппаратное или программное), на которое могут реагировать ядро или периферийные блоки. Одним из вариантов реакции может быть — прерывание.
Interrupt — это прерывание работы программы и переход управления в специализированный участок обработчик прерывания.

Взаимосвязь между Event и Interrupt заключается в следующем:
Каждый Interrupt вызывается Event, но не каждый Event вызывает Interrupt.
Помимо прерываний, события могут активировать и другие возможности МК.

Управление и обработка прерываниями производится контроллером приоритетных векторных прерываний NVIC (Nested Vectored Interrupt Controller). Контроллер прерываний часть ядра Cortex-M. Документацию на этот контроллер необходимо начинать изучать с Cortex ™-M3 TechnicalReference Manual Revision: r1p1

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

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

Как это работает?

Данный пост не претендует на абсолютную полноту, я советую изучить раздел прерываний в Cortex™-M3 Technical Reference Manual. Поскольку эта часть ядра не претерпела изменений, ее описание дано в первой ревизии r1p1 на ядро Cortex-M3.

Вход в прерывание и выход из него

При инициации прерывания NVIC переключает ядро в режим обработки прерывания. После перехода в режим обработки прерывания регистры ядра помещаются в стек. Непосредственно во время записи значения регистров в стек осуществляется выборка начального адреса функции обработки прерывания.

В стек перемещается регистр регистр статуса программы ( Program Status Register (PSR)), счетчик программы (Program Counter (PC)) и регистр связи (Link Register (LR) ). Описание регистров ядра приведено в Cortex-M4 Generic User Guide. Благодаря этому, запоминается состояние, в котором находилось ядро перед переходом в режим обработки прерываний.

Также сохраняются регистры R0 — R3 и R12. Эти регистры используются в инструкциях для передачи параметров, поэтому, помещение в стек делает возможным их использование в функции обработки прерывания, а R12 часто выступает в роли рабочего регистра программы.

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

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

Вложенность прерываний

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

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

2. Непрерывная обработка прерываний
Эта ситуация может возникнуть в двух случаях: если два прерывания имеют одинаковый приоритет и возникают одновременно, если низкоприоритетное прерывание возникает во время обработки высокоприоритетного.
В этом случае, промежуточные операции над стеком не производятся. Происходит только загрузка адреса обработчика низкоприоритетного прерывания и переход к его выполнению. Отказ от операций над стеком экономит 6 тактов. Переход к следующему прерыванию происходит не за 12 тактов, а всего за 6.

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

Приоритеты прерываний

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

Значение приоритета прерывания задается в регистрах Interrupt Priority Registers (см. Cortex-M4 Generic User Guide). При этом, часть бит отвечает за приоритет группы, в которой находится прерывание, а часть — за приоритет внутри группы.
Настройка распределение бит на приоритет группы или приоритет внутри группы осуществляется с помощью регистра Application Interrupt and Reset Control Register (ВНИМАТЕЛЬНО. см. Cortex-M4 Generic User Guide).

Как вы, наверно, заметили, в Cortex-M4 Generic User Guide сказано, что настройка приоритетов и группировки приоритетов зависят от конкретной реализации implementation defined.
А вот дальше не очень приятная вещь. В Reference manual к МК STM32F407 про NVIC почти нет информации. Но есть ссылка на отдельный документ. Для того, чтобы разобраться с реализацией NVIC в STM32 придется прочитать еще один документ — STM32F3xxx and STM32F4xxx Cortex-M4 programming manual. Вообще говоря, я советую внимательно изучить данный документ и по всем другим вопросам, в нем работа ядра расписана более подробно, чем в документации от ARM.
В нем, уже можно найти:

Из возможных 8 бит приоритета используются только 4. Но этого вполне достаточно для большинства задач.

Маскирование прерываний

Предположим, что у нас стоит задача запуска ракеты-носителя при нажатии на красную кнопку, но только при условии, что повернут ключ.
Нет совершенно ни какого смысла генерировать прерывание на поворот ключа. А вот прерывание на нажатие красной копки нам понадобится. Для того, чтобы включать/выключать различные вектора прерываний, существует маскирование прерываний.
Маскирование прерывания осуществляется с помощью регистров Interrupt Set-enable Registers.
Если прерывание замаскировано, это не означает, что периферия не генерирует события! Просто NVIC не вызывает обработчик этого события.

Таблица векторов прерываний

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

  • Reset
  • NMI
  • HardFault
  • MemManage
  • BusFault
  • UsageFault
  • SVCall
  • PendSV
  • SysTick
Расположение векторов прерываний и загрузка МК

Из начала флеш памяти ядро считывает значение SP (stack top addres) и PC (reset routine location). Таким образом, автоматически начинает выполняться функция, с адресом считанным в регистр PC. Это может быть, например main.

После обязательных четырех компонентов, может находиться дальнейшая таблица векторов прерываний. Главное сохранить порядок.
При желании, можно разместить таблицу векторов прерываний в другой области памяти, но тогда, необходимо сообщить NVIC, куда мы передвинули таблицу. За это смещение таблицы векторов отвечает регистр Vector Table Offset Register (см. Cortex-M4 Technical Reference Manual. Это может понадобиться для написание встроенного загрузчика нового ПО (bootloader), но об этом как-нибудь в другой раз.

От теории к практике
ТЗ второго проекта

Пример создается для отладочной платы STM32F4Discovery.
При нажатии на кнопку должен загореться светодиод LED3. При замыкании контактов PC6 и GND загорается светодиод LED5.
В процессе программирования поиграемся с приоритетами прерываний и посмотрим к чему это приведет.

Железная часть

Найдем в документации к плате кнопку и светодиод:

При ненажатой кнопке на пине PA0 будет логический ноль, при нажатии на кнопку на кнопке появится логическая 1 (3.3В).
Светодиод LED3 подключен к пину PD13.
Светодиод LED5 подключен к пину PD14.
Интересней всего с контактом PC6 — он напрямую выведен на штыревой разъем. Нам будет необходимо обеспечить регистрацию логической 1, когда он не закорочен с контактом GND. О том, как это сделать пойдет речь ниже.

Настройка GPIO

Для нашей задачи необходимо настроить пины PD13 и PD14 как выходные. О том, как это делать можно прочитать в предыдущей статье.
С настройкой пина PA0 тоже все достаточно просто — его нужно настроить на вход. Не смотря на то, что после ресчета МК почти все пины настроены на вход, крайне желательно явно прописать эту инициализацию.
С пином PC7 все несколько интереснее. Поскольку он «висит в воздухе», его состояние не определено. Нам же необходимо, чтобы при этом его состояние всегда было «1». Для этого, в блоке GPIO активировать подтяжку. В нашем случае, необходима подтяжка к питанию — PULL UP.
Активация подтяжки осуществляется с помощью регистра GPIO port pull-up/pull-down register.

Прерывания EXTI

Для выполнения нашего «ТЗ» с использованием прерываний, нам необходимо настроить прерывания, которые будут срабатывать при переходе контакта PA0 из состояния «0» в состояние «1», и прерывание при переходе контакта PC6 из состояния «1» в состояние «0».

В МК STM32F4xx для этой цели служит контроллер внешних прерываний/событий — EXTI (External interrupt/event controller). Я настоятельно рекомендую ознакомиться с его функционалом в Reference manual. Нам необходимо поступить в соответствии с описанным:

Нам понадобятся 0 и 6 линии EXTI. Для размаскирования соответствующих линий прерываний необходимо записать в регистр EXTI_IMR значение 0x9.
Для линии PA0, необходима генерация события прерывания по переходу из состояния «0» в состояние «1» — по возрастающему фронту. То есть, необходимо записать 1 в нулевой бит регистра EXTI_RTSR.
Для линии PC6, наоборот, необходима генерация события прерывания по переходу из состояния «1» в состояние «0» — по падающему фронту. То есть, необходимо записать 1 в шестой бит регистра EXTI_FTSR.
На этом настройка блока EXTI закончена. Последний пункт будет реализован при настойке NVIC.

По мимо этого, необходимо определиться, пин с какого порта подключается к определенной линии EXTI. Делается это с помощью регистров SYSCFG external interrupt configuration register (Reference manual). Эти регистры находятся в System configuration controller, что мне кажется не очень логичным (почему было не включить эту насторойку в EXTI?), но оставим сей факт на совести ST.

Настройка NVIC

Активация обработки определенного вектора прерывания осуществляется с помощью регистров Interrupt set-enable registers (NVIC_ISERx). Описание регистров приведено в Cortex-M4 Generic User Guide. Сама таблицу векторов прерываний для нашего МК приведена в Reference manual (см. Table 61).

Из таблицы можно увидеть, что для 0 линии есть отдельное прерывание, а вот линии с 5 по 9 генерируют одно прерывание на всех.
Кроме того, из таблицы мы узнали номера векторов, необходимых нам прерываний. Теперь нужно записать «1» в 6 бит (активация прерываний линии 0 EXTI) регистра NVIC_ISER0 (адрес 0xE000E100) и в 23 бит того же регистра (активация прерываний линий 5-9).

Настройка приоритетов

Для того, чтобы можно было побаловаться с приоритетами прерываний настроим группы приоритетов так, чтобы 2 бита отвечали за приоритет внутри группы, и 2 бита — за приоритет самой группы. Для этого необходимо записать значение 0х05FA0500 в регистр Application interrupt and reset control register (STM32F3xxx and STM32F4xxx Cortex-M4 programming manual).
Настройка приоритетов осуществляется с помощью регистров Interrupt Priority Registers (STM32F3xxx and STM32F4xxx Cortex-M4 programming manual). Нас будут интересовать регистры Interrupt Priority Register 2 (0xE000E4008) и регистр Interrupt Priority Register 5(0xE000E401C).
Пока не будем изменять приоритеты. Пусть будут одинаковы для обоих прерываний.

Обработка прерываний

Функции обработчики прерываний — ни что иное, как просто функции языка C, который ни чего не получают и не возвращают (и правильно — не от кого и не кому).
Главное правило — обработка прерываний должна осуществляться как можно быстрее. Иначе низкоприоритетные прерывания могут слишком долго ждать.

После окончания обработки прерывания необходимо сбросить активность события, вызвавшего прерывание — «очистить прерывание». Очистка прерывания EXTI производится с помощью регистра EXTI_PR . Обратите внимание: запись «1» очищает прерывание, запись «0» не имеет ни какого воздействия.

Если с обработкой прерывания линии 0 EXTI все достаточно просто, то с группой линий 5-9 возникает вопрос — как определить какая линия вызвала прерывание. Узнать это можно проверкой бит регистра Pending register (EXTI_PR) — Reference manual.

Создаем таблицу векторов и располагаем ее в правильном месте

Использование
Располагает таблицу __vector_table в начале секции, объявленной в файле линкера. Сам файл можно посмотреть тут:

Сама секция задается в начале ROM памяти. Адреса можно посмотреть тут (документ, в котором описана адресация флеш памяти STM32):

Комбинация директивы IAR и спецфункции IAR:
Записывает в начале флеша указатель на верхушку стека.

Саму таблицу заполняют адреса функций, реализующий вечный цикл. Исключение сделано только для интересующих нас функций:

В функции, вызываемой при старте, просто производится переход к
Это функция — main(). Сам символ можно переопределить, если возникнет желание:

Переходим к основному файлу

Обратите внимание на то, что значения спецрегистров МК объявлены как volatile. Это необходимо, чтобы компилятор не пытался оптимизировать операции обращения к ним, поскольку это не просто участки памяти и их значения могут изменяться без участия ядра.

Настраиваем группирование приоритетов

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

Включение тактирование используемой периферии

Напомню, что перед началом работы с периферийными блоками необходимо включить их тактирование:

Работать сразу с SYSCFG нельзя, нужно подождать несколько тактов. Но мы и не будем. Займемся инициализацией GPIO.

Инициализация GPIO

Светодиоды инициализируются так же как и в прошлый раз:

Кнопка PA0 и контакт PC7 инициализируются как входные:

Вот только для контакта PC6 необходимо включить подтяжку питания. Активация подтяжки производится с помощью регистра GPIOC_PUPDR:

Настройка EXTI

И так, на нужно настроить следующие параметры — включить прерывания для линий 0 и 6, для линии 0 прерывание по растущему фронту, для линии 6 — прерывание по падающему фронту:

Осталось настроить пины каких портов подключены к линии EXTI (странное решение, например МК stellaris могут генерировать прерывание при любой комбинации пинов, у STM32 с этим сложнее):

Настройка NVIC

Осталось настроить приоритеты прерываний и маскировать их для инициации обработки. Обратите внимание, что регистры NVIC_IPR доступны для побайтового обращения, что значительно упрощает доступ только к необходимым байтам приоритетов отдельных векторов прерываний. Достаточно только сделать сдвиг на величину номера вектора прерывания (см. листинг определений). Еще раз напомним, что EXTI Line 0 имеет 6 номер в таблице векторов, а EXTI line 5_9 — номер 23. У STM32 значение имеют только старшие 4 бита приоритета:

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

С этого момента нажатие на кнопку и закоротки PC6 и GND будет приводить к вызову функций обработчиков прерываний EXTI_Line0_IntHandler и EXTI_Line6_IntHandler соответственно.

Обработка прерываний

В функциях обработки прерываний в первую очередь необходимо очистить прерывание, после этого можно зажечь светодиоды. Для демонстрации приоритетов прерываний в один из обработчиков добавлен вечный цикл. Если приоритет прерывания с вечным циклом ниже приоритета второго — то оно не сможет быть вызвано. Иначе, оно сможет прервать первое. Я предлагаю вам самим попробовать различные знчения приоритетов прерываний и наглядно увидеть к чему это приводит (ВНИМАНИЕ — не забудьте про группы прерываний!).

6. Прерывания

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

Для перехода на обработчик прерывания должны быть выполнены все связанные с ним условия:

  • Прерывание от данного события должно быть разрешено. Если в устройстве используется только UART, возникновение прерываний от всяких таймеров нам не интересно. Более того, некоторые устройства (в основном, ножки ввода-вывода) генерируют прерывания не импульсно (один раз на событие), а непрерывно. Например, все время пока на ножке высокий уровень. Без возможности запрета таких прерываний контроллер будет постоянно висеть в обработчике.
  • Прерывание должно быть разрешено глобально. Дело в том, что некоторые группы команд прерывать нельзя — собьются тайминги, возникнут нежелаельные импульсы или что-то в этом роде. Такие операции называются атомарными (неделимыми) и в простейшем случае реализуются именно сбросом глобального разрешения прерываний и его последующим восстановлением. Также глобальное разрешение сбрасывается при заходе в прерывание, чтобы не было циклической обработки.
  • Контроллер прерываний вообще-то должен быть настроен. Выставлен адрес обработчика, разрешения и т.п.
  • При наличии системы приоритетов прерываний (в некоторых контроллерах, например AVR, ее нет, но в нашем gd32vf103 — есть, хотя работать с ней мы пока не будем) приоритет пытающегося запуститься прерывания должен быть выше того, что обрабатывается сейчас. “Приоритет” основного кода, естественно, ниже, чем у любого прерывания. Таким способом можно обойти предыдущий пункт и все-таки реализовать обработку прерывания в обработчике прерывания.

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

Как писать код, если все регистры заняты? В общем-то, примерно так же, как и для обычных подпрограмм: нужные регистры сохраняются на стеке, а перед выходом из обработчика восстанавливаются. В простейшем случае (на котором мы остановимся) стек будет общий как для пользовательского кода, так и для прерываний. Но вообще-то RISC-V поддерживает переключение стеков при смене привилений. Скажем, основной код выполняется в U-mode (пользовательский с низкими привилегиями), а прерывания — в M-mode (машинный уровень с максимальными привилегиями). Для этого служит интересный CSR регистр mscratchcsw, но его мы проверить не сможем, пока не доберемся до режимов работы контроллера.

Откуда брать адрес возврата из прерывания, если ra использовать нельзя? Для этого существует специальный CSR-регистр mepc. Возврат по его значению осуществляется специальной инструкцией mret. Помимо прочего она умеет переключать уровни привилегий, если контроллер настроен правильно.

6.1. ECLIC и его настройка

Контроллер прерываний в нашем микроконтроллере называется ECLIC (Enhanced Core Local Interrupt Controller). Управляется он частично через CSR-регистры, частично через MMIO. Регистры у него следующие:

регистр размер смещение описание
cliccfg 4(8) 0x0 Глобальные настройки приоритетов
clicinfo 25(32) 0x4 Разнообразная информация о прерываниях конкретного контроллера
mth 8(8) 0xB Порог срабатывания прерываний
clicintip[i] 1(8) 0x1000+4*i Флаг ожидающего прерывания
clicintie[i] 1(8) 0x1001+4*i Флаг разрешения прерывания
clicintattr[i] 3(8) 0x1002+4*i Настрока фронта прерывания и режим
clicintctl[i] 8(8) 0x1003+4*i Приоритет

Тут же встает вопрос относительно чего расчитывается смещение и где про это написано. И тут у меня ответа к сожалению нет: я не нашел упоминаний этого адреса ни в одной документации. Только изучая примеры кода от производителя, был обнаружен базовый адрес 0xD200’0000.

Регистры clicintip, clicintie, clicintattr и clicintctl привязаны каждый к своему прерыванию, поэтому и объединены в массив. В нашем случае используется прерывание от USART0, за которым производителем закреплен номер 56, соответственно использоваться будут clicintip[56], clicintie[56], clicintattr[56] и clicintctl[56] с адресами (0x1000 + 4*56 =) 0x10E0, 0x10E1, 0x10E2 и 0x10E3. Посмотреть номера и список всех доступных в данном конкретном контроллере прерываний можно в его User Manual’е в разделе, посвященном прерываниям. В нашем случае это здоровенная табличка из 86 элементов.

Немного расшифрую что написано в таблице. Регистр mth: я пока точно не знаю за что он отвечает, поэтому подробностей не будет, изучайте документацию. Регистр clicintip содержит всего один значащий бит. Если прерывание уже готово выполниться, он выставляется в 1, что, при выполнении остальных условий, приводит к собственно переходу на прерывание. Насколько я понял, он оставлен только для совместимости со старыми версиями контроллеров векторов прерываний и в ECLIC практически не используется. Регистр clicattr: некоторая периферия (особенно ножки ввода-вывода GPIO) умеют генерировать прерывание по высокому уровню (все время пока на ножке лог.1 будет вызываться прерывание), по нарастающему фронту (лог.0 -> лог.1) и по спадающему фронту (лог.1 -> лог.0). Для большей же части периферии эта настройка бесполезна. Что такое режим (векторный / не-векторный) рассмотрим чуть позже.

Собственно настройка ECLIC для простейшего случая работы с прерываниями сводится всего лишь к разрешению прерываний от интересующей нас периферии, то есть выставлении clicintie[56] в 1:

Обратите внимание, что для записи использована инструкция sb: размер регистров 8 бит, и трогать соседние мы не хотим.

6.2. Настрока периферии

Периферии мы пока изучили немного, поэтому работать будем с UART. Как мы уже выяснили, за все события, происходящие с UART’ом отвечает одно прерывание, с номером 56. Самих же событий может быть несколько, и прописаны они в регистре USART_CTL0:

USART_CTL0

PERRIE, Parity error interrupt enable — прерывание по ошибке приема. У UART есть простенькая система защиты от сбоев при обмене, и это прерывание возникает при ее срабатывании.

TBEIE, Transmitter buffer empty interrupt enable — прерывание по опустошению буфера передачи

TCIE, Transmission complete interrupt enable — прерывание по фактическому окончанию передачи

RBNEIE, Read data buffer not empty interrupt and overrun error interrupt enable — прерывание по приему байта

IDLEIE, IDLE line detected interrupt enable — прерывание по таймауту. Если данные не приходили слишком долго.

Продемонстрировать работу прерываний будет проще всего на передаче. Контроллер передаст байт, после чего должно произойти событие и мы окажемся в обработчике. Но в регистре USART_CTL0 этих прерываний два. Дело в том, что передача байта происходит в два этапа: сначала байт записывается в USART_DATA, потом автоматически копируется во внутренний буфер, из которого бит за битом передается в линию TX. И пока он передается, в регистр USART_DATA можно положить еще один байт, он там будет лежать, пока предыдущий не освоболит место во внутреннем регистре. Так вот, прерывание TBEIE возникает когда байт покинул регистр USART_DATA и начал передаваться. А TCIE — когда покинул внутренний регистр, и передача полностью завершилась. Соответственно, TBEIE надо использовать когда передаются байт за байтом, чтобы не было задержки между фактическим окончанием передачи, пока отработает прерывание, пока положат следующий байт и т.д. А TCIE — когда надо отключить модуль UART, то есть дождаться фактического окончания передачи. Поскольку отключать UART мы не будем, воспользуемся TBEIEб его нужно добавить к прочим флагам USART_CTL0.

6.3. Настройка контроллера

Итак, модуль ECLIC мы настроили, периферию настроили. Осталось написать собственно обработчик прерывания, положить его адрес в какой-нибудь регистр и разрешить прерывания глобально. Начнем, как ни странно, с регистра хранения адреса обработчика, mtvec:

устройство mtvec

Как видно из таблицы, младшие 6 битов отвечают за режим работы. Нас интересует режим ECLIC, которому соответствует комбинация 0b000011. Но из-за аж шести занятых битов, данный регистр не может хранить шесть младших битов адреса. Поэтому придется обработчик прерывания располагать с выравниванием на 64:

Из кода довольно очевидно, что прерывание всего лишь мигает зеленым светодиодом и возвращается по адресу mepc при помощи команды mret. Вот именно адрес этой подпрограммы надо записать в mtvec:

И разрешить прерывания глобально. Очевидно, что делать это надо когда периферия и прерывания уже настроены, то есть обычно перед бесконечным рабочим циклом. За глобальное разрешение прерываний отвечает регистр mstatus, а точнее его бит MIE:

Вот теперь прерывание работает: после выполнения кода передачи строки начинает мигать зеленый светодиод, а управление в основную программу не возвращается. В чем дело?

В том, что с прерыванием мы ничего не сделали: оно как ждало обработки, так и продолжает ждать. Вот и тыкается в обработчик в надежде, что хоть теперь его обработают. Но полноценно мы его обрабатывать пока не будем, просто скажем “хорошо, мы поняли, что данные переданы, можешь больше не следить за UART’ом”. То есть просто запретим данное прерывание:

…ну и плюсик выведем, почему бы и нет. Вот теперь прерывание работает правильно: срабатывает, отключает само себя и возвращает управление основному коду.

6.4. Исключения

В микроконтроллерах источником неожиданных событий почти всегда оказывается именно периферия. Но в компьютерах, где программы куда больше и сложнее, чаще бывают и чисто программные ошибки. Плюс при наличии операционной системы появляется и необходимость к ней обращаться из пространства пользователя. Эти задачи также решаются контроллером прерываний. Тут я сразу вынужден уточнить терминологию: прерывания это события от внешних устройств; исключения — от выполнения определенных инструкций кода; исключительные ситуации — от обоих. Разница в том, что прерывание возникает когда ему хочется, и к коду не привязано. Поэтому после обработки надо вернуться в то же самое место, на котором оно случилось. Исключение же возникает в строго отведенных местах: код попытался выполнить несуществующую инструкцию; попытался обратиться к недоступному адресу; попытался работать с невыровненными данными (кстати, наш контроллер к невыровненным данным равнодушен, и ошибок не выдает); попытался сделать системный вызов. Следовательно, при обработке исключений надо сначала определиться что же собственно произошло — штатное событие или ошибка. Если ошибка, то можно ли ее обработать или лучше прибить процесс, пока хуже не стало.

Для примера напишем три инструкции, приводящие к исключениям:

При выполнении такого кода контроллер начинает яростно мигать зеленым светодиодом и спамить плюсики в UART. Логично, ведь при выполнении инструкции 0xFFFFFFFF возникает исключительная ситуация, обрабатывается нашим trap_entry, после чего управление передается опять на 0xFFFFFFFF. Что снова приводит к исключению.

В первую очередь надо отделить исключения от прерываний. Для этого служит регистр mcause, а точнее, его 31-й бит. Если он сброшен в 0, то перед нами исключение, а если выставлен в 1 — прерывание. Соответственно обработчик прерываний остается неизменным, но при обнаружении нуля в 31-м бите mcause надо перейти на обработчик исключения. Отличаться он будет тем, что мигать в нем будем красным диодом, а возвращаться не на ту же инструкцию, которая привела к исключению, а на следующую. Просто-напросто считаем mepc, увеличим на 4 (размер инструкции) и запишем обратно:

Но тут из-под воды возникают интересные грабли: контроллер наш поддерживает расширение C (Compressed) — сжатые инструкции. То есть часть инструкций у него 32-битная, а часть — 16-битная. А перепрыгивать 16-битную инструкцию через 4 байта это плохая идея. К счастью, разработчики RISC-V предусмотрели замечательный способ определить длину инструкции. У 32-битных два младших бита всегда равны 0b11, а в 16-битных — любому другому числу. То есть нам надо всего лишь проверить эти два бита и в зависимости от этого решить, прибавлять 4 или все же 2:

Как я говорил в самом начале, исключения в контроллерах используются достаточно редко, поэтому пока на этом и остановимся.

6.5. Разделение прерываний и исключений

Но если исключения штука редкая, но возможная, можно ли убрать проверку mcause из обработчика прерываний? Оказывается, можно. Для этого используется еще один CSR-регистр mtvt2, который в стандарт RISC-V не входит, и является специфичным для нашего контроллера. Его младший бит отвечает за то, использоать ли его вообще, а оставшиеся биты хранят адрес обработчика прерываний. То есть в mtvec будет адрес обработчика исключений, а в mtvt2 — прерываний:

Естественно, раз уж обработчиков теперь стало два, нужно каждый из них оформить как обработчик — персональная точка входа, работа со стеком, mret.

6.6. Векторный режим

Специфика контроллера вынуждает пойти еще дальше и вместо одного обработчика на все прерывания, в котором нужно было анализировать младшие биты mcause чтобы выяснить какое именно устройство вызвало прерывание, был придуман еще более хитрый механизм. Он заключается в том, что для каждого устройства пишут свой, персональный обработчик прерывания, а их адреса (иногда — прямо команды перехода по адресам) сводят в специальную таблицу — таблицу векторов прерываний. Например, если нас интересует 56-е прерывание, то в 56-ю ячейку надо записать адрес обработчика. Как и с отдельными обработчиками, для хранения адреса таблицы выделен отдельный CSR-регистр mtvt. Причем работа с таблицей реализована очень разумно: в регистре хранится старшая часть адреса таблицы, а вместо младшей подставляется номер прерывания. То есть если адрес самой таблицы равен 0x2000’1000 (где-то в оперативной памяти), и произошло прерывание 56 (поскольку инструкция 4-байтная, то смещение будет 224, оно же 0x0000’00E0), то адрес будет взят из ячейки (0x2000’1000 OR 0x0000’00E0) = 0x2000’10E0. Из этой реализации следует ограничение на выравнивание таблицы. В нашем случае, когда прерываний 86, фактический размер таблицы составляет 344 байта, что помещается в 512-байтную область. Это соответствует выравниванию .align 9.

Здесь надо не забыть, что векторный / не-векторный режим настраивается в регистре clicattr[i], причем для каждого прерывания независимо.

Прописывать простыню из 86 адресов прерываний я здесь не буду, кому интересно может посмотреть в примерах кода.

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

6.7. Расположение таблицы векторов прерываний

Как мы увидели раньше, расположить ее можно где угодно, лишь бы выравнивание соблюдалось. Можно даже хранить несколько таблиц по разным адресам и переключать их по желанию левой пятки. Но проще всего все же выделить для таблицы постоянное место — в начале прошивки. Мы ведь точно знаем, что адрес 0x0000’0000 (и даже реальный адрес 0x0800’0000) совершенно точно выровнены по 512-байтной границе. Разработчики даже сделали нам подарок, не став использовать 0-й адрес вектора прерываний, на который попадает управление при старте контроллера. В него можно записать безусловный переход на начало основного кода. Ну а чтобы таблица располагалась именно там, где надо, для нее можно выделить специальную секцию памяти, а в *.ld файле указать, что размещается она в самом начале.

6.8. Системные вызовы

Теперь, когда с самой сложной частью закончили, можно вернуься к исключениям. Например, реализовать системные вызовы из стандарта RARS вроде ввода-вывода чисел, строк, символов и прочего.

Как говорит нам документация, причина исключения хранится в младших 12 битах регистра mcause. За выполнением ecall зарезервировано два кода: 11 (если вызов произошел на M-mode) и 4 (если на U-mode). Пока что будем обрабатывать только ecall, а остальные исключения игнорировать.

Номер ecall‘а хранится в регистра a7, и из списка вызовов RARS’а (напоминаю, в других средах системные вызовы другие!) нас интересуют 1 и 4 — вывод числа и вывод строки. Вот так может выглядеть обработчик исключений, поддерживающий эти два системных вызова:

В результате наконец-то начали корректно работать участки кода

Заключение

Вот так в микроконтроллер gd32vf103 настраиваются прерывания и исключения. Возможно, на таком простом примере, как UART, их польза не очевидна, но желающие могут изучить и другую периферию, поработать через polling и прерывания и оценить, что выгоднее для их задачи. Это, кстати, не шутка: не надо пихать прерывания куда попало, в ряде случаев именно опрос регистра является оптимальным решением.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *