В документации на микроконтроллеры AVR не приводится даже функциональной схемы контроллера прерываний. Максимум, что можно увидеть, прямоугольник с надписью "Interrupt Unit". Поэтому в сегодняшней статье некоторые иллюстрации будут нарисованы мной, на основании моего опыта, а не взяты из документации. Прошу это учитывать.
Аппаратная реализация
Принцип построения контроллера прерываний в AVR, в самых общих чертах, описан в статье
Микроконтроллеры для начинающих. Часть 57. Прерывания
в разделе "Несколько источников, несколько обработчиков". От более простого варианта "несколько источников, один обработчик", который мы подробно рассмотрели на примере PIC, основным отличием является добавление блока формирования адреса перехода (адреса вектора).
Однако, в отличии он рассмотренных в предыдущей статье микроконтроллеров PIC, в AVR управляющие прерываниями биты не собраны в регистры контроллера прерываний, а располагаются в регистрах соответствующих периферийных модулей, которые и могут вызывать прерывания.
Как и в большинстве других микроконтроллеров, в AVR есть два уровня разрешения/запрета прерываний. Самый верхний уровень, глобальный, управляется битом I (Global Interrupt Enable), который располагается в регистре SREG. Кроме того, можно запретить/разрешить прохождение запросов прерывания от отдельных источников.
Как видите, все очень похоже и на то, что мы уже рассматривали. Только биты флагов и масок не собраны воедино, а располагаются в регистрах тех модулей, за которые и отвечают. Какой вариант удобнее, PIC или AVR? Однозначного ответа у меня меня нет. С одной стороны, размещение масок и флагов в специально выделенных регистрах, как это сделано в PIC, позволяет более удобно выполнять над ними групповые операции. С другой стороны, в AVR расположение флагов и масок более семантически логично. Поэтому я считаю, что оба варианта примерно равны.
Необходимо пару слов сказать о прерывании по изменению состояния вывода порта. Флаг PCIF (для ATtiny13, который и показан на иллюстрации) устанавливается при изменении состояния любого вывода порта. А что бы реагировать только на нужные выводы, дополнительно устанавливаются соответствующие биты в регистре PCMSK. То есть, это уже третий уровень управления разрешением/запретом прохождения запросов прерываний. Если у микроконтроллера несколько портов, то и флагов запроса прерывания, и дополнительных регистров масок выводов, будет больше.
Формирование адреса вектора прерывания, с точки зрения аппаратной реализации, в документации на AVR не описано. Одни из вариантов мы видели в 57 статье цикла, я уже давал на нее ссылку. Однако, нужно определиться с количеством векторов и размером одного вектора. Это важные параметры.
Количество векторов зависит от количества периферийных устройств (модулей) входящих в состав микроконтроллера и количества формируемых каждым модулем запросов прерываний. Например, ATtiny13A имеет 10 векторов прерываний, ATtiny2313A имеет 21 вектор, а у ATmega328P уже 26 векторов. Причем в список векторов прерываний входит и "вектор сброса", который, по большому счету, вектором прерывания называть неверно.
Каждый вектор прерывания в микроконтроллерах AVR должен содержать команду безусловного перехода на собственно процедуру обработки прерывания. Таких команд две - RJMP и JMP. Для микроконтроллеров с малым объемом памяти программ достаточно 16-битной команды RJMP. Для микроконтроллеров с большим объемом памяти программ уже требуется использовать 32-битную JMP. Этим и определяется размер одного вектора прерывания - одно или два слова памяти программ.
Давайте, для примера, посмотрим на список векторов ATtiny13
Такой список есть в документации на каждый микроконтроллер AVR. Видно, что векторов 10 (мы это уже знаем), причем размер вектора одно слово (16 бит). Здесь же можно увидеть, что формирует данный запрос прерывания. При этом, будет ли сформирован запрос определяется настройками модуля, а это отдельная тема для разговора. В данном случае у нас, например, с модулем таймера связано три прерывания (три вектора) - 4, 7 и 8. А АЦП может формировать лишь один запрос.
В отличии от PIC, в AVR флаг запроса прерывания сбрасывается автоматически при переходе по вектору прерывания, аппаратно. Если переход по вектору не выполняется (прерывания запрещены) и требуется сбросить флаг запроса, то необходимо установить бит еще раз (записать логическую единицу). Мы с этим уже сталкивались.
При переходе по вектору автоматически, аппаратно, сбрасывается бит глобального разрешения/запрета прерываний в регистре SREG. Поэтому, по умолчанию, обработчики прерываний сами прерваны быть не могут. Однако, в отличии от PIC, стек в AVR является настоящим универсальным стеком, а его размер больше. Поэтому становится возможным гораздо более безопасно реализовать вложенность прерываний, разрешив прерывания в обработчике вручную. Это не тема для начинающих, но мы сегодня кратко поговорим об этом.
При выходе из обработчика прерывания на глобальном уровне будут автоматически, аппаратно, разрешены.
Приоритеты прерываний. Вложенные прерывания
В рассматриваемых в статье микроконтроллерах приоритеты прерываний заданы жестко, на аппаратном уровне. Если вы вспомните 57 статью цикла, то там я говорил, что это может быть реализовано приоритетным шифратором, который участвует в формировании адреса вектора.
Чем меньше номер вектора, тем выше приоритет запроса прерывания. Важно понимать, в каких случаях приоритет запроса прерывания работает. Контроллер прерываний работает непрерывно, но процессор уделяет внимание анализу запросов прерываний лишь в определенные моменты времени. У него и без прерываний работы хватает.
В документации на AVR не оговаривается точный момент анализа запросов прерываний процессоров. Однако, как вы помните, выполнение команды прервано быть не может. Поэтому будет вполне допустимым считать, что анализ запросов выполняется между выполнением команд.
После завершения выполнения команды процессор проверяет, есть ли запросы прерываний. Если нет, то начинается выборка и выполнение очередной команды. Если есть, и на глобальном уровне прерывания не запрещены, то процессор получает от контроллера прерываний адрес вектора и начинает выполнение внутренней процедуры перехода к обработчику прерывания.
Ключевым моментом здесь является "получение адреса вектора". Этот адрес вычисляется контроллером прерываний непрерывно и асинхронно. На основании поступающих запросов прерываний. Причем делается это независимо от состояния бита I регистра SREG. А вот биты маскирования запросов, на локальном уровне, на этот процесс влияют. Если запрос замаскирован, он просто не поступит на приоритетный шифратор.
В каждый момент времени может существовать несколько запросов. Запрос с меньшим номером (с наибольшим приоритетом) и сформирует на выходе приоритетного шифратора адрес вектора. Именно этот вектор и получит процессор, когда обратится к контроллеру прерываний.
То есть, приоритеты запросов работают в момент обращения процессора к контроллеру прерываний за адресом вектора. И только в этот момент. Когда адрес вектора процессором получен уже ничто не может повлиять на переход к соответствующему обработчику прерывания.
При этом все таки существует возможность немного вмешаться в этот аппаратный процесс и добавить еще один уровень. Точнее, подуровень. Мы можем искусственно понизить приоритет обработчика прерывания (именно обработчика, а не собственно прерывания!) сделав его прерываемым. Для этого надо разрешить прерывания в обработчике, которые были автоматически запрещены при переходе по вектору.
Таким образом, прерванным может быть не только выполнение основной программы, но и выполнение обработчика. Ситуация, когда новое прерывание возникаем во время выполнения обработчика другого прерывания и прерывает работу его, и называется вложенностью прерываний. Разрешение вложенности прерываний требует гораздо большей осторожности. И для новичков это не рекомендуется.
В микроконтроллерах AVR приоритет обработчика текущего прерывания не запоминается. Поэтому при разрешении прерываний внутри обработчика прерывания потенциально может возникнуть прерывание любого приоритета, даже ниже приоритета обрабатываемого прерывания. Более того, может быть вызван даже тот же самый обработчик, если еще раз возникло тоже самое прерывание.
Что бы как то упорядочить ситуацию, может потребоваться замаскировать отдельные запросы прерываний. Причем сделать это нужно до разрешения прерываний внутри обработчика. Кроме того, перед завершением обработчика потребуется запретить прерывания и восстановить первоначальное состояние масок прерываний.
Вот тут то и оказывается более удобным подход PIC, так как сохранить регистры PIE удобнее, чем состояние отдельных бит масок разбросанных по куче регистров.
Задержки при возникновении прерываний
Как и в случае PIC, в AVR прерывание не может быть обработано сразу в момент возникновения запроса. Необходимо дождаться завершения выполнения текущей команды, сохранить регистр PC, сбросить флаг запроса и флаг разрешения прерываний, выполнить переход по вектору и адресу обработчика.
В документации говорится, что минимально требуется 4 цикла для начала выполнения обработчика прерывания, если процессор находится в активном режиме. Вспомните, что ровно столько же циклов требовалось микроконтроллерам PIC. Выход из обработчика прерываний тоже требует 4 цикла. Эти аппаратные задержки иногда требуется учитывать.
Если микроконтроллер находился в режимах сна, то до начала выполнения обработчика прерывания он будет переведен в активный режим, что может потребовать довольно много времени. И время задержки перехода к обработчику будет добавлено к времени пробуждения.
Дополнительные возможности управления прерываниями
В некоторых микроконтроллерах AVR предусмотрена возможность использования загрузчика (BootLoader). Не смотря на то, что загрузчик в данном цикле статей не рассматривается, я кратко расскажу о дополнительных возможностях управления прерываниями в таких микроконтроллерах.
Во первых, загрузчик размещается в области верхних адресов памяти программ (в конце памяти). При этом загрузчику тоже может потребоваться обрабатывать прерывания. Проблема возникает, когда загрузчик выполняет перезапись информации в начале памяти программ. Ведь именно там располагается таблица векторов прерываний.
Для решения этой проблемы предусмотрена возможность переноса таблицы векторов в область памяти программ занимаемой загрузчиком. Это позволяет загрузчику безопасно работать с прерываниями при перезаписи области прикладной программы.
Для управления размещением таблицы прерываний используются биты IVSEL и IVCE регистра MCUCR (на примере ATmega328). Если IVSEL=0, то таблица векторов размещается в начале памяти программ, на своем стандартном месте. Если IVSEL=1, то таблица векторов располагается в начале области загрузчика. Расположение и размер области загрузчика определяется битами FUSE. Мы рассматривали конфигурирование микроконтроллера в статье
Микроконтроллеры для начинающих. Часть 41. Конфигурация AVR (Fuses, Lock Bits, и другие)
Бит IVCE используется для разблокирования возможности изменения бита IVSEL.
Во вторых, прерывания могут автоматически запрещаться для некоторых областей памяти программ. Это управляется битами BLB конфигурации. Про эту возможность я не буду рассказывать даже кратко, ограничусь лишь ее упоминанием.
Специальные случаи
Прерывания по изменению состояния выводов порта и внешние прерывания имеют некоторые особенности.
Вопреки ожиданию, даже если вывод порта сконфигурирован как выход функционал прерывания по изменению состояния остается активным (если он используется, конечно). То есть, прерывание возникнет даже при смене состояния вывода из программы. Это можно считать некоторым аналогом программных прерываний. Правда нужно учитывать, что такие "программные прерывания" действительно будут иметь влияние на "внешний мир", так как уровень на выводе будет изменяться.
Входы внешних прерываний (например, INT0) могут быть настроены для работы по фронту, спаду, и уровню. При работе "по уровню" триггер, фиксирующий возникновение запроса в виде установки флага, не используется. Другими словами, факт наступления события прерывания не запоминается. Например, если INT0 настроен на формирование запроса прерывания по низкому уровню на входе и такое событие наступило, но прерывание не может быть обработано сразу (например, выполняется обработчик другого прерывания), а к моменту появления возможности вызова обработчика уровень снова стал высоким, никакого прерывания не произойдет. Оно просто будет "потеряно".
При этом, если INT0 настроен, например, на прерывание по фронту, потери прерывания не произойдет. Такую особенность нужно учитывать, если прерывание по уровню используется для пробуждения микроконтроллера. Требуемый уровень должен удерживаться входе достаточно долго, что бы микроконтроллер не только проснулся, но и прерывание произошло. В противном случае микроконтроллер проснется, но прерывания не будет.
Программный аспект
Собственно говоря, мы уже знаем почти все, что нужно. Осталось добавить одну важную деталь - при вызове обработчика прерывания регистр SREG автоматически не сохраняется, это обязанность программиста. Теперь мы можем перейти к рассмотрению практических вопросов.
Обработка прерываний не только аппартно-зависима, но и компиляторо-зависима. В статье я буду использовать avr-gcc версии 10 (10.3, точнее) и avr-libc версии 2.0. Этот компилятор входит в состав многих популярных сред разработки для AVR и Arduino.
Для облегчения работы программиста в файл interrupt.h собраны некоторые полезные макросы. Однако, вас никто не заставляет их использовать. Если вы предпочитаете описывать регистры микроконтроллера самостоятельно, то использование interrupt.h может мешать. Но вы всегда можете написать все необходимые макросы самостоятельно. В статье я буду использовать interrupt.h
Необходимо обратить ваше внимание, что использование процедур и переменных одновременно и их основной программы, и из обработчиков прерываний требует обеспечения атомарности доступа. В сегодняшней статье я не буду рассматривать вопросы обеспечения атомарности.
Разрешение/запрет прерываний на глобальном уровне
Для разрешения прерываний необходимо установить бит I регистра SREG, а для запрета сбросить его. Проще всего для этого использовать команды SEI и CLI. В файле interrupt.h предусмотрены два макроса, которые непосредственно транслируются в эти команды
- sei() - разрешить прерывания (глобально)
- cli() - запретить прерывания (глобально)
Описание функции как обработчика прерывания
Что бы описать функцию как обработчик прерывания нужно использовать макрос
ISR ( vector, [attribute] )
Параметр attribute не является обязательным. Если он не указан, то по умолчанию подразумевается ISR_BLOCK (об этом чуть позже).
Параметр vector определяет собственно вектор прерывания. Есть предопределенный список имен векторов, причем он зависит от используемого микроконтроллера. И в качестве параметра vector можно использовать только одно из этих имен.
Например, что бы определить обработчик переполнения TIMER0 мы должны использовать имя TIMER0_OVF0_vect. Вот небольшой пример
ISR(TIMER0_OVF0_vect) {
. . .
}
То есть, описание обработчика очень похоже на описание функции. Полный список имен векторов можно найти в документации на avr_libc или напрямую в заголовочных файлах.
Поскольку имена векторов определяют и их адреса, которые зависят от длины вектора, необходимо обязательно задавать тип микроконтроллера. Это можно сделать определив символ MCU в тексте программы. Или через командную строку компилятора.
Второй параметр определяет дополнительные атрибуты обработчика:
- ISR_BLOCK - обычный обработчик прерываний. Этот атрибут можно просто не указывать, он подразумевается по умолчанию. Прерывания во время выполнения обработчика запрещены, компилятор автоматически генерирует весь дополнительный код для сохранения и восстановления регистров (пролог и эпилог). Это наиболее распространенный тип обработчиков. И именно его мы будет использовать в последующих статьях цикла.
- ISR_NOBLOCK - определяет прерываемый обработчик прерываний. То есть, разрешает вложенные прерывания при работе обработчика. Компилятор автоматически вставляет в генерируемый пролог команду SEI, а в эпилог команду CLI. Управлять расположением этих команд вручную мы не можем.
- ISR_NAKED - полностью определяемый программистов обработчик. Компилятор ничего не добавляет от себя. Весь код, включая сохранение регистра SREG и размещение команды RETI в конце обработчика, должен написать программист. Это наиболее гибкий вариант, но он требует от программиста наибольших усилий и полного понимания, как все это работает.
- ISR_ALIASOF(target_vector) - определяет, что данный обработчик прерывания является "синонимом" другого обработчика. Имя этого оригинального обработчика задается параметром атрибута target_vector. Собственно говоря, при этом весь код обработчика заменяется на команду перехода к оригинальному обработчику.
Если заглянуть внутрь макроса ISR и связанных с ним, то станет видно, что это просто обертка для модификатора __attribute__. Для обычных обработчиков используется атрибут signal, а для прерываемых атрибут interrupt. Более подробное описание можно найти в документации на avr-gcc.
Существует и старый способ определения обработчиков - макрос SIGNAL. Однако он является устаревшим и его использование настоятельно не рекомендуется. Как и использование старых имен векторов, которые начинаются с SIG_.
Специальные обработчики
Векторы неиспользуемых прерываний можно использовать для размещения кода основной программы. Однако, если такое прерывание все таки произойдет, например, по ошибке, то результат будет катастрофическим. Для избежания подобных ситуаций рекомендуется задавать полную таблицу векторов, даже если используется лишь малая ее часть. Именно так и поступает avr-gcc.
При этом возникает вопрос, что указывать в качестве обработчиков для неиспользуемых векторов? По умолчанию avr-gcc выполняет сброс, переход на вектор 1 (адрес 0), при возникновении прерывания, для которого обработчик не задан. Вы можете переопределить такое поведение написав обработчик для BADISR_vect. Переход на этот обработчик avr-gcc вставляет в каждый вектор прерывания, для которого не определен обработчик в явном виде.
ISR ( BADISR_vect) {
. . .
}
Если же для какого то прерывания специальная обработка не нужна, но само прерывание может возникать (и это не является ошибкой), то можно описать пустой обработчик с помощью макроса
EMPTY_INTERRUPT( vector );
Здесь vector задает имя вектора, а тело обработчика отсутствует. Компилятор заменяет такой обработчик на единственную команду RETI.
Заключение
На сегодня все. На первый взгляд кажется, что обработка прерываний в AVR кардинально отличается от PIC. Однако, при более внимательном рассмотрении становится видно, что в основе обеих реализаций лежат одни и те же принципы. Флаг запроса прерывания и маска. Глобальное разрешение/запрет прерываний. Но в AVR нам не нужно анализировать флаги в единственном обработчике, это делается на аппаратном уровне. Это может быть удобнее, но приводит к жестко заданным уровням приоритета, а для PIC это можно изменить изменив последовательность анализа флагов.
Нам осталось познакомиться в реализацией прерываний и их обработки в микроконтроллерах STM8. После чего мы сможем перейти к очередному практическому учебному проекту. В нем мы и сможем применить все изученное на практике.