AVR Studio: как написать обработчик прерывания |
![]() |
Добавил(а) microsin |
Микроконтроллеры без использования прерываний практически ничего не стоят. Прерывание (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 соответственно. Отсутствие возможности четко настроить приоритет - довольно серьезный недостаток платформы AVR. [Ссылки] 1. Как комбинировать программу на Си (C) с кодом ассемблера (ASM). |