Микроконтроллеры без использования прерываний практически ничего не стоят. Прерывание (interrupt) получило свое название из того факта, что при срабатывании прерывания по какому-нибудь событию нормальное выполнение программы прерывается, и выполняется особый код - код обработчика прерывания (interrupt handler). Прерывания можно рассматривать просто как подпрограммы, которые приостанавливают нормальный процесс выполнения программы до тех пор, пока код подпрограммы обработчика прерывания не будет завершен. После того, как обработчик прерывания завершит свою работу, ядро микроконтроллера вернется к исполнению основной программы точно в той точке, где она была прервана прерыванием. События прерывания обычно происходят от каких-то внешних асинхронных воздействий на микроконтроллер - например, переключение логического уровня на внешнем выводе порта, либо переполнение регистра таймера, что означает истечение заданного интервала времени, и т. д.
Почему же прерывания так важны? Например, мы могли бы обойтись без прерываний, в бесконечном цикле опрашивая возникновение интересующих нас событий. Такое функционирование программы называется опросом (polling). Но опрос имеет много недостатков и неудобств - к примеру, программа будет тратить на циклы ресурс ядра, который мог быть потрачен на выполнение других действий. Это одна из причин, почему у микроконтроллера есть много источников прерывания, которые могут использоваться при необходимости. Вместо проверки событий на возникновение микроконтроллер имеет возможность прервать нормальный поток программы при возникновении события и переключиться на действия обработчика события прерывания (ISR, interrupt service routine), затем вернуться обратно и продолжить выполнение основной программы.
[Векторы прерывания]
Векторами прерывания называют адреса перехода на обработчик прерывания. Список таких адресов называется таблицей векторов прерываний, и он находится в памяти программ по заранее известному адресу. У микроконтроллеров AVR таблица векторов прерываний находится в самом начале памяти программ FLASH по адресу 0. Содержимое таблицы векторов прерываний определяет программист, когда ему нужно реализовать обработку прерываний.
Каждый вектор прерывания AVR занимает в памяти 2 байта (1 слово кода инструкций AVR), и представляет из себя команду rjmp относительный_адрес. Вот так, например, выглядит на языке ассемблера полностью заполненная таблица прерываний микроконтроллера ATmega16:
$000 rjmp RESET ;сброс (начало программы)
$001 rjmp INT0_vect ;внешнее прерывание 0
$002 rjmp INT1_vect ;внешнее прерывание 1
$003 rjmp TIMER2_COMP_vect ;прерывание совпадения сравнения таймера 2
$004 rjmp TIMER2_OVF_vect ;прерывание переполнения таймера 2
$005 rjmp TIMER1_CAPT_vect ;прерывание события захвата таймера 1
$006 rjmp TIMER1_COMPA_vect ;прерывание совпадения сравнения A таймера 1
$007 rjmp TIMER1_COMPB_vect ;прерывание совпадения сравнения B таймера 1
$008 rjmp TIMER1_OVF_vect ;прерывание переполнения таймера 1
$009 rjmp TIMER0_OVF_vect ;прерывание переполнения таймера 0
$00A rjmp SPI_STC_vect ;прерывание завершения передачи SPI
$00B rjmp USART_RXC_vect ;прерывание завершения приема байта UART
$00C rjmp USART_UDRE_vect ;прерывание опустошения регистра передачи UART
$00D rjmp USART_TXC_vect ;прерывание завершения передачи байта UART
$00E rjmp ADC_vect ;прерывание завершения преобразования АЦП
$00F rjmp EE_RDY_vect ;прерывание готовности EEPROM
$010 rjmp ANA_COMP_vect ;прерывание изменения сигнала на выходе компаратора
$011 rjmp TWI_vect ;прерывание двухпроводного интерфейса TWI (I2C)
$012 rjmp INT2_vect ;внешнее прерывание 2
$013 rjmp TIMER0_COMP_vect ;прерывание совпадения сравнения таймера 0
$014 rjmp SPM_RDY_vect ;прерывание готовности к записи памяти программ
Такая полностью заполненная векторами таблица никогда не применяется. На практике обычно используется только 1..4 прерывания, в этом случае не используемые адреса векторов могут остаться не инициализированными. Обратите внимание, что в левом столбце метками обозначены шестнадцатеричные адреса инструкций, соответствующие байтовые адреса будут в 2 раза больше (потому что инструкция rjmp занимает 2 байта).
В этой статье на примере обработчика прерывания таймера 1 для ATmega16 рассказывается, как организовать обработчик прерывания в проекте GCC. Показаны два варианта реализации - на языке C и ассемблера. В примерах алгоритм работы таймера отличается, но это не важно для рассмотрения методов организации обработчика прерывания.
[Обработчик прерывания на C]
Это самый простой вариант. В данном примере используется следующий алгоритм - основная программа настраивает таймер 1 и запускает обработчик прерывания. Этот обработчик срабатывает раз в секунду и заботится о себе сам, подстраивая величину счетчика TCNT1 (чтобы прерывания происходили точно раз в секунду). Обработчик раз в секунду также декрементирует счетчик времени timer, который устанавливается и отслеживается в основной программе. Прерывание таймера разрешено постоянно (разрешается при старте программы). Таким образом, основная программа может получить любую задержку времени в интервале от 1 до 255 секунд.
Процесс по шагам:
1. Настраиваем таймер и разрешаем прерывание для него. Этот код должен вызываться однократно, при старте программы. Для этого можно написать отдельную процедуру, например:
#include <avr/io.h>
..
void SetupTIMER1 (void){
//На частоте тактов 16 МГц прерывание переполнения T/C1
// произойдет через (счет до 65535):
// 1 << CS10 4096 mkS (нет прескалера Fclk)
// 1 << CS11 32.768 mS (Fclk/8)
// (1 << CS11)|(1 << CS10) 262.144 mS (Fclk/64)
// 1 << CS12 1048.576 mS (Fclk/256)
TCCR1B = (1 << CS12);
TCNT1 = 65536-62439; //коррекция счетчика, чтобы время было ровно 1 секунда
/* Разрешение прерывания overflow таймера 1. */
TIMSK = (1 << TOIE1);}
2. В любом из модулей проекта (можно в общем, где функция main, а можно в отдельном, например timer.c) пишем код обработчика прерывания таймера. Вот пример такого кода:
#include <avr/interrupt.h>#include <avr/io.h>
..
u8 timer;
ISR (TIMER1_OVF_vect){
//теперь прерывание будет происходить через 62439 тиков
// таймера 1, что на частоте 16 МГц составит 1 сек.
TCNT1 = 65536-62439;
//Далее идет код, который будет работать каждую секунду.
//Желательно, чтобы этот код был короче.
if (timer)
timer--;}
3. В нужном месте разрешаем прерывания программы. Это делается также однократно, после того как сделаны все приготовления:
sei();
[Обработчик прерывания на ASM]
Этот вариант не многим сложнее, просто организован по-другому. Я его сделал на основе отдельного файла, который содержит только код на языке ассемблера. Алгоритм тут тоже другой - обработчик прерывания срабатывает раз в секунду и сам себя запрещает. Основная программа отслеживает это событие и меняет секундные счетчики (выполняет все действия, которые нужно выполнять раз в секунду), и нова разрешает прерывание. Такой алгоритм позволяет ускорить обработку прерывания, что может быть критично для некоторых задач (например, только так можно организовать точный отсчет времени при использовании библиотеки V-USB). Процесс по шагам:
1. Настраиваем таймер. Это может делать код на C. Все точно так же, как и с обработчиком прерывания на C (см. шаг 1).
2. Готовим файл с нашим кодом обработчика на языке ассемблера. Вот пример кода:
#include <avr/io.h>
.text
.global TIMER1_OVF_vect
TIMER1_OVF_vect:
push R24
ldi R24, 0
out _SFR_IO_ADDR(TIMSK), R24
pop R24
reti
Этот код будет работать очень быстро, поскольку короткий. Он почти ничего не делает, только запрещает прерывание от таймера 1 (в регистре TIMSK сбрасываются все флаги, в том числе и нужный нам флаг TOIE1). Запускать прерывание будет основная программа, как только обнаружит, что прерывание запрещено (путем анализа состояния флага TOIE1).
3. В нужном месте разрешаем прерывания программы. Это делается также однократно, после того как сделаны все приготовления:
sei();
4. В основной программе, в главном цикле main, должен максимально часто вызываться следующий код:
..void main (void){
..
while (1)
{
..
if (0==(TIMSK & (1 << TOIE1)))
{
TCNT1 = ONE_SECOND;
TIMSK = (1 << TOIE1);
//далее действия, которые будут происходить
// раз в секунду
..
}
..
}}
[Общие замечания]
Можно заметить, что в обоих примерах использовалась именованная константа TIMER1_OVF_vect, которая задает адрес вектора прерывания таймера 1. Имена констант можно узнать во включаемом файле процессора. Для ATmega16, например, это будет файл c:\WinAVR-20080610\avr\include\avr\iom16.h. Чтобы имена стали доступны, не нужно добавлять именно этот файл в проект директивой #include, достаточно добавить #include <avr/io.h> и задать макроопределение, задающее тип процессора (например, MCU = atmega16. Это можно сделать либо в Makefile, либо в свойствах проекта).
При использовании одновременно нескольких прерываний в AVR важно помнить, что прерывания имеют фиксированный, ненастраиваемый приоритет. Чем меньше адрес вектора прерывания, тем приоритет у прерывания выше. Этот приоритет срабатывает, если при выходе из прерывания имеется несколько необработанных флагов прерывания. Прерывание с более высоким приоритетом НЕ может временно приостановить уже работающий обработчик прерывания с меньшим приоритетом, чтобы немедленно выполнить свой код. Работает система примерно так:
- когда общие прерывания разрешены (установлен бит I в регистре SREG, этот бит называют Global Interrupt Enable), то может быть вызвано любое разрешенное прерывание с любым приоритетом. Бит Global Interrupt Enable может быть принудительно сброшен или установлен командами CLI или SEI соответственно. - для того, чтобы прерывание могло сработать и вызвать свой обработчик, кроме установки бита Global Interrupt Enable необходимо также установить бит разрешения соответствующего прерывания. Для таймеров-счетчиков это биты регистра TIMSK, для интерфейса SPI - бит SPIE в регистре SPCR, и т. д. - когда срабатывает любое прерывание, то сразу очищается флаг I (Global Interrupt Enable), и автоматически запрещаются все прерывания, пока не произойдет выход из обработчика прерывания. Если во время работы обработчика прерывания возникали условия, при которых должны были сработать другие прерывания, то эти другие прерывания не вызываются, а просто запоминаются соответствующие им флаги (прерывания "откладываются" на будущее). При выходе из обработчика прерывания запомненные флаги прерывания запустят нужный обработчик прерывания в соответствии с назначенным ему приоритетом (если на данный момент имеется несколько отложенных прерываний). - разработчик может в обработчике прерывания вызвать команду SEI (которая установит флаг Global Interrupt Enable), тем самым разрешив выполнение других прерываний во время работы этого обработчика прерывания. Тогда, если произойдет новое другое прерывание до завершения текущего обработчика (в котором уже была вызвана команда SEI), текущий обработчик прерывания будет приостановлен, адрес возврата в него будет сохранен в стеке и будет вызвано новое прерывание. Таким способом можно обеспечить некое подобие соблюдения приоритета - в низкоприоритетных обработчиках прерывания должна первой стоять команда SEI, а в высокоприоритетных обработчиках команда SEI должна отсутствовать, что обеспечит выполнение этого обработчика полностью.
Отсутствие возможности четко настроить приоритет - довольно серьезный недостаток платформы AVR.
[Ссылки]
1. Как комбинировать программу на Си (C) с кодом ассемблера (ASM). |
Комментарии
microsin: все тупо и просто для тех, кто окончил 3-й класс средней школы. Частота тактирования таймера равна 16000000/256=62500 Гц. Следовательно, чтобы прошла 1 секунда, счетчик должен досчитать до 62500. Поскольку надо также учитывать время отработки кода обработчика прерывания, это число оказывается меньше. В реальной жизни тупо берется 62500, и далее подбором это число уменьшается до тех пор, пока время между прерываниями не окажется требуемым (для данного примера 1 секунда). В итоге получается число 62439.
microsin: ИМХО поскольку нормальной вложенности прерываний и настройки уровней приоритетов у AVR нет, то единственное, что Вы можете сделать - это проверить в высокоуровневом обработчике прерывания, что возникло событие низкоуровневого прерывания, и нужно его обработать. Рациональнее всего обработку выполнить тут же, и затем перейти к обработке других задач прерывания. Чтобы такой ситуации не возникло, рационально подбирать аппаратную конфигурацию схемы так, чтобы фиксированные приоритеты прерываний совпадали с теми приоритетами, которые Вам реально нужны. Тогда достаточно в низкоуровневом обработчике прерываний вызвать команду SEI, чтобы разрешить обработку прерывания более высокого (аппаратно) уровня.
out _SFR_IO_ADDR(TI MSK), R24
заменил на
sts TIMSK1, R24
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=printview&t=78203&start=0
При запуске первым вызовется прерывание с меньшим адресом вектора. Внутри подпрограммы обработки прерывания другие прерывания(глоб ально) запрещены. Т. е. до конца прерывания оно не будет прервано. Если их разрешить, внутри прерывания, то сработает первое разрешённое прерывание(прио ритет не важен. Или я ошибаюсь?
microsin: да, действительно, Вы правы. Оказывается, что приоритет работает только в случае нескольких запомненных (во время выполнения прерывания, когда другие прерывания не могут быть вызваны) флагов прерывания. Благодарю Вас, мне надо исправить старый текст.
RSS лента комментариев этой записи