AVR. Учебный Курс. Работа на прерываниях
Автор DI HALT
Опубликовано 19 Фев 2010
Рубрики: AVR. Учебный курс
Метки: UART, USART, Прерывания, Язык Си
Одним из серьезных достоинств контроллеров AVR является дикое количество прерываний. Фактически, каждое периферийное устройство имеет по вектору, а то и не по одному. Так что на прерываних можно замутить кучу параллельных процессов. Работа на прерываниях является одним из способов сделать псевдо многозадачную среду.
Идеально для передачи данных и обработки длительных процессов.
Для примера покажу буфферизированный вывод данных по USART на прерываниях.
В прошлых примерах был такой код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // Отправка строки void SendStr(char *string) { while (*string!='\0') { SendByte(*string); string++; } } // Отправка одного символа void SendByte(char byte) { while(!(UCSRA & (1<<UDRE))); UDR=byte; } |
Данный метод, очевидно, совершенно неэффективен. Дело в том, что у нас тут есть тупейшее ожидание события — поднятие флага готовности USART. А это зависит, в первую очередь, от скорости передачи данных. Например, на скорости 600 бод передача каких то 600 знаков будет длиться 9 секунд, блокируя работу всей программы, что ни в какие ворота не лезет.
Как быть?
Ну раз у нас отправка идет по аппаратному устройству, то большую часть времени мы впустую крутим цикл ожидания флага, хотя от процессора, собственно, тут ничего и не требуется и можно было бы заняться другими делами. Напрашивается мысль выбросить весь цикл ожидания в тело ядра/автомата/суперцикла и обрабатывать флаг на каждой итерации главного цикла/диспетчера. Но при этом у нас будет плавать время между байтами — ацтой!
Поэтому прерывания, пожалуй, будет единственным адекватным вариантов.
Итак, если брать в пример USART то у него есть три прерывания:
- RXT - прием байта. С этим понятно, мы его уже использовали
- TXC - завершение отправки
- UDRE - опустошение приемного буфера
Байты TXC и UDRE обычно вызывают путаницу. Поясню разницу.
Дело в том, что регистр передачи данных UDR в AVR на самом деле куда хитрей чем кажется, он двухэтажный. На первом ярусе, собственно UDR, а ниже находится конвейер сдвигового регистра. Первый байт, попавший в пустой регистр UDR тут же проваливается на конвейер, а UDR снова опустошается. После чего конвейер неторопливо, в соответствии с битрейтом, выплевывает данные в линию, а потом снова зажевывает байт из UDR. Поэтому, фактически, в UDR за короткое время влезает сразу два байта - первый тут же проваливается, а второй ждет.
Так вот,
- Флаг пустого регистра UDRE выставляется тогда, когда мы можем загнать байт в UDR,
- Флаг окончания передачи TXC появляется только тогда, когда у нас конвейер опустел, а новых данных в UDR нет.
Да, можно слать данные и по флагу TXC, но тогда у нас будет лишняя пауза между двумя разными байтами — время на опустошение буфера. Некошерно.
Вот как это можно сделать корректней.
Вначале выводим данные в массив, либо берем его из флеша — не важно. Для простоты запихну массив в ОЗУ. Код возьму из прошлой статьи:
1 2 3 | #define buffer_MAX 16 // Длина текстового буффера char buffer[buffer_MAX] = "0123456789ABCDEF"; // А вот и он сам u08 buffer_index=0; // Текущий элемент буффера |
Инициализация интерфейса выглядит стандартно:
1 2 3 4 5 6 7 8 | //InitUSART UBRRL = LO(bauddivider); UBRRH = HI(bauddivider); UCSRA = 0; UCSRB = 1<<RXEN|1<<TXEN|0<<RXCIE|0<<TXCIE; UCSRC = 1<<URSEL|1<<UCSZ0|1<<UCSZ1; sei(); // Разрешаем прерывания. |
Обратите внимание, что прерывания UDRE мы не разрешаем. Это делается потом. Иначе сразу же, на старте, контроллер ускачет на это прерывание, т.к. при пуске UDR пуст и мы получим черти что.
Отправка выглядит элементарно. Вначале пихаем первый байт нашего сообщения, не забыв выставить правильно индекс. А дальше разрешаем прерывание по UDRE оно само улетит куда надо, по прерываниям:
1 2 3 | buffer_index=0; // Сбрасываем индекс UDR = buffer[0]; // Отправляем первый байт UCSRB|=(1<<UDRIE); // Разрешаем прерывание UDRE |
Дальше можно затупить или делать вообще что угодно:
1 2 3 4 | while(1) { NOP(); } |
В течении нескольких тактов выскочит прерывание UDRE
Да, кстати, я один раз словил гадский баг который отловил только трассировкой ассемблерного листнига. У меня была такая последовательность:
1 2 3 UDR = X; UCSRB|=(1<<UDRIE); buffer_index = 1;И вот тут почему то первым байтом шел мусор. А дальше все нормально. Причем если менять уровень оптимизации, то баг то вылезал то нет. Причиной такого поведения являлось то, что я то надеялся на то, что прерывание UDRE выскочит гораздо поздней чем я присвою индексу буфера нужное значение (buffer_index = 1;) Но индюк тоже думал, а по факту я
пихаю байт в UDR, он в тот момент естественно пуст и уже следующим тактом, на выполнении команды UCSRB|=(1<<UDRIE) данные проваливались в сдвиговый регистр, а UDR тотчас пустел и выставлял бит прерывания.
А дальше, в зависимости от оптимизации, этот бит успевал выставиться к моменту когда я выставлял верный номер индекса либо не успевал.
Проблема решилась перестановкой строк:
1 2 3 UDR = X; buffer_index = 1; UCSRB|=(1<<UDRIE);Отсюда правило:
Готовь все необходимые данные ПЕРЕД разрешением прерываний.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Прерывание по опустошению буффера УАПП ISR (USART_UDRE_vect) { buffer_index ++; // Увеличиваем индекс if(buffer_index == buffer_MAX) // Вывели весь буффер? { UCSRB &=~(1<<UDRIE); // Запрещаем прерывание по опустошению - передача закончена } else { UDR = buffer[buffer_index]; // Берем данные из буффера. } } |
Все, автоматика! Каждый раз когда UDRE пустеет прерывание срабатывает и бросает туда новых дров. Когда же буфер пустеет и индекс достигает максимума, то мы просто запрещаем прерывание UDRE и успокаиваемся.
Осталось только дать понять головной программе, что мы отработали. Для этого и есть флаг TXC можно разрешить его прерывание и тогда он сбросится при обработке прерывания USART_TXC_vect, а в самом обработчике сделать заброс задачи на диспетчер или еще что нибудь умное. Либо периодически проверять главным циклом наличие флага TXC и вручную его стереть (записью единицы).
Вот полный код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | #define F_CPU 8000000L #include <avr/io.h> #include <util/delay.h> #include <avr/interrupt.h> #include <avrlibdefs.h> #include <avrlibtypes.h> #define buffer_MAX 16 // Длина текстового буффера char buffer[buffer_MAX] = "0123456789ABCDEF"; // А вот и он сам u08 buffer_index=0; //Прерывание по опустошению буффера УАПП ISR (USART_UDRE_vect) { buffer_index++; // Увеличиваем индекс if(buffer_index == buffer_MAX) // Вывели весь буффер? { UCSRB &=~(1<<UDRIE); // Запрещаем прерывание по опустошению - передача закончена } else { UDR = buffer[buffer_index]; // Берем данные из буффера. } } int main(void) { #define baudrate 9600L #define bauddivider (F_CPU/(16*baudrate)-1) #define HI(x) ((x)>>8) #define LO(x) ((x)& 0xFF) //Init UART UBRRL = LO(bauddivider); UBRRH = HI(bauddivider); UCSRA = 0; UCSRB = 1<<RXEN|1<<TXEN|0<<RXCIE|0<<TXCIE; UCSRC = 1<<URSEL|1<<UCSZ0|1<<UCSZ1; //Это так, просто помигать. #define LED1 4 #define LED_PORT PORTD #define LED_DDR DDRD LED_DDR = 1<<LED1; sei(); buffer_index=0; // Сбрасываем индекс UDR = buffer[0]; // Отправляем первый байт UCSRB|=(1<<UDRIE); // Разрешаем прерывание UDRE while(1) { LED_PORT=0<<LED1; _delay_ms(1000); LED_PORT=1<<LED1; _delay_ms(1000); } } |
Если грузануть его в Pinboard, предварительно подключив USART к FT232 и законнектиться терминалкой, то будет мигать наш LED4, а в терминалку от стрелятся байты ASCII кодов нашей строки. В это же время будет неторопливо тикать наш цикл с мигалкой.
![]() |
Ну и, как всегда, пример кода в архиве.
Комментарии
35 комментариев на «AVR. Учебный Курс. Работа на прерываниях»
Оставьте свой отзыв
Вы должны войти, чтобы оставлять комментарии.






UDRE - опустошение приемного буфера
Может “передающего” или имелось ввиду приёмный для отправки?
Да это и имелось в виду. Прием данных для последующей отправки.
buffer_index дожен быть помечен как volatile, если мы работам с ним из прерывания. Это очень важный момент. Если данная программа скорее всего будет нормально работать, то при работе с buffer_index вне прерывания при включенной оптимизации могут начаться косяки. ИМХО, стоит указать это в статье и объяснить, почему так. Нубы, которые только начинают изучать Си, часто с этим сталкиваются :)
В данном случае нет. Т.к. не меняется нигде кроме как в прерывании :)
так в этом то и фишка! в коде программы оно (buffer_index) грузицца в регистр и там и остается (оптимизация), а в это время оно, в прерывании, меняется, а значение, которое в регистре, осталось без изменений. вот чтоб этого не случилось и имеет смысл (акадэмический) объявлять переменные, изменяемые в прерывательских процедурах как volatile.
Нифига. Переменная глобальная. Так что любые модификации ее затрагивают именно ее изменения.
Мы вошли в прерывание, изменили и вышли. Компилятор не может ее сныкать в регистр, т.к. ему четко сказано записать ее в память - глобальную переменную. Она тут никак не останется только на уровне регистров. Волатиль тут не нужна!
А вот если бы была конструкция
ISR {I++ }
main { while(I<100){} }
То в этом случае волатиль нужна обязательно, т.к. оптимизатор цикл разложит по регистрам, а то что I меняется в ISR ему невдомек.
Ну в данном случае, может и покатит, но если вдруг захочется в main сделать что-то ещё с этой переменной могут вылезти неожиданные глюки =) Так что лучше сразу объявить её volatile и не бояться подземных грабель.
Самое смешное, что GCC оптимизирует даже регистровые переменные - если интересно, я как-то писал про то, как наступил на эти грабли: http://gremlinable.livejournal.com/6808.html
Да, забавно ещё заметить, что в IAR ключевое слово volatile обозначает вообще совершенно другое… =)
Захочу в майн ее вкорячить, то тогда и добавлю.
А на всякий случай можно вообще оптимизацию вырубить ;)
Ну что ты добавишь, я не сомневаюсь, а вот ньюб, который учится по твоим статьям - не факт, только если наши комменты читать будет, а на это не у каждого хватит времени и нервов… =)
Читать статьи надо по порядку ;) Вначале Сишного курса я рассказывал про эту фишку.
И мне кажется более красивым способ, если сделать buffer_index типа char*.
Тогда вначале мы присваиваем ему адрес буфера: buffer_index=buffer;
А потом делаем так:
ISR (USART_UDRE_vect)
{
if(*buffer_index) // Вывели весь буффер?
{
UDR = *buffer_index; // Берем данные из буффера.
buffer_index++; // Увеличиваем индекс
} else {
UCSRB &=~(1<<UDRIE); // Запрещаем прерывание по опустошению - передача закончена
}
}
Да, согласен. Но менее наглядно для нуба :)
И да, для данного случая это применимо. А если у нас не ASCIIZ? Или нуль в буффере вылезет?
Это применимо только для текста, да.
Объяснение базисов, что как работает это очень хорошо, но в жизни советую использовать что нибудь вроде usart из gcc-libavr
http://homepage.hispeed.ch/peterfleury/group__pfleury__uart.html
И чем оно лучше? Вряд ли будет компактней чем самописное минималистичное решение.
Зато код некрасивой платформозависимой грязюкой не заляпан.
Плюс часто позволяет иногда не наступать на неочевидные грабли
UCSRB|=(1<<UDRIE);
buffer_index = 1;
Попутно расставляя другие. Тем более данные функции, ЕМНИП, существуют только в avr-libc и при переносе на IAR или CVAVR вылезут в полный рост.
А все платформозависимые функции выносятся в HAL и там и остаются.
Ну на самом деле это дело вкуса/надобности:
Хочешь изучать платформу, и.т.д - пиши сам.
Нужно писать продукт (и как часто бывает - быстро писать) - юзай либы.
Спасибо за статью. В свое время очень нужна была эта информация, потом разобрался сам, писал на ассемблере.
а вот очень простая реализация буфера FIFO на асме. bufferstart и bufferend - адрес начала и конца буфера в СРАМ, buffervalue - регистр для обмена данными с буфером. буфер закольцован, когда он полон данные перестают записываться и выставляется флаг bufferfull, а когда пуст перестают считываться и выставляется bufferempty.
таким образом, при передаче пакета данных можно выделить буфер в памяти и в основном цикле после проверки флага “буфер полон” заполнять его необходимыми данными для отправки и разрешать прерывание UDR, а в прерывании по UDR считывать значение из буфера и если он опустошился - запрещать прерывание UDR. это позволит отправить пакет данных без пауз.
блин, как его отформатировать…
Щас сделаю.
спасибо:)
В серьезных системах возникает дополнительная проблема - сложность полного обработчика прерывания. Например, по прерыванию нужно выкачать из устройства пакет данных, или сделать с ним еще что-то, требующее сложного ввода-вывода. Размещение такого кода непосредственно в обработчике прерывания будет мешать принятию и обработке прерываний от других устройств. Поэтому, в ядре ОС есть специальная очередь задач обработки прерываний (в одной из известных ОС такие задачи называются IRP - Interrupt Request Packet). Приоритет исполнения задач в этой очереди ниже приоритета собственно обработчиков прерываний, но выше приоритета обычных пользовательских процессов. Собственно обработчик вектора прерывания в драйвере устройства очень прост с точки зрения нагрузки на процессор. Он уведомляет устройство и контроллер прерываний, что прерывание принято к сведению, и добавляет задачу обработки прерывания (IRP) в эту очередь. IRP - это довольно простая структура данных, содержащая базовые данные о том, какому драйверу и устройству оно соответствует, и указатели на функции в драйвере, которые нужно исполнить.
Наверное, даже в микроконтроллерах такая проблема может иногда возникать, поэтому имеет смысл иметь представление об этом стандартном подходе с дополнительной приоритетной очередью задач.
Гораздо удобнее система многоуровневых приоритетных прерываний, реализованная, например, в Интеловском контроллере прерываний I8059A еще в конце 70х годов. 8 векторов и 8 уровней приоритетов с возможностью наращивания каскадированием контроллеров, фиксированные или циклически меняющиеся приоритеты (например, уже обработанному прерыванию можно было присваивать самый низкий приоритет, или сдвигать приоритеты по кольцу), удобные маски, 4х или 8ми байтовый шаг таблицы векторов, и много еще всякого… С ним я не имел проблем с обработкой событий реального времени. Прерывание с более высоким приоритетом могло прервать обработку более низких, которая после будет опять продолжена, и само могло прерваться для обработки еще более высоких. Как я по ним скучаю…
Все, что сейчас реализовано в наиболее распространенных микроконтроллерах - лишь слабое подобие той системы. А ведь это сильно упрощало программы реального времени и снимало массу проблем… Городить же программно еще отдельный диспетчер для формирования и обработки очереди прерываний - только еще больше запутывать и тормозить программу.
Все равно эта конструкция никогда не заменит нормального аппаратно реализованного полноценного многоуровневого приоритетного контроллера прерываний… Не понимаю, почему бы не встроить такую штуку в современные микроконтроллеры. Схема его (по современным меркам) довольно проста, и сильно кристалл не усложнит.
Это все работает, если:
sei();
buffer_index=0; // Сбрасываем индекс
UDR = buffer[0]; // Отправляем первый байт
UCSRB|=(1<<UDRIE); // Разрешаем прерывание UDRE
находится до цикла WHILE
А если помести его за циклом - не работает. Как быть?
Ну так он и не выполняется. Цикл то бесконечный и за себя не пустит. Трассировать то код хоть пытался?
Ааа, вот оно что….
Да, смотрел код асм - и конечно все верно - он прыгает вечно от строчки к строчке, зацикливается (сомтрел на ассме, просто написал тут на си):
98: sei(); // Разрешаем прерывания
99: UDR = buffer[0]; // Отправляем первый байт
100: UCSRB|=(1<<UDRIE); // Разрешаем прерывание UDRE
100: UCSRB|=(1<<UDRIE); // Разрешаем прерывание UDRE
Но почему тогда выполняется передача первого числа - а только потом уже входит в цикл… Ведь и передача первого числа в цикле уже стоит…
Стоп, ты про какой вайл говоришь, про этот?
Напиши как ты сделал и что у тебя не получилось?
Да, наверное сразу надо было начинать с этого, чтобы не вводить в заблуждение.
Мне нужно проверить, замкнут ли контакт на землю, и если замкнут - отсылать сообщение по UART. Как только замыкание на землю исчезает - передача прекращается.
Сам только недавно начал копаться в контроллерах, и что успел прочитать - что основной код должен выполняться в цикле, а вся инициализация до цикла.
Очень понравился этот урок - и начал делать на его основе.
Для начала (чтобы не наделать ошибок) решил бесконечно отсылать по UART строку, ну только что добавил в обработчик прерывание проверку:
buffer_index++; // Увеличиваем индекс
if(buffer[buffer_index] == 0×00)
{
UCSRB &=~(1<<UDRIE); // UCSRB &=~(1<<UDRIE)- Установка бита: запрещаем прерывание по опустошению - передача закончена
}
Я как бы конечно буду знать заранее, что я отправляю, т.е. количество символов - но все же я одновременно и изучаю еще си (а с подачки этого сайта одновременно и асм :) и решил сделать такую проверку - буфер у нас 16, а мне нужно передать например 3 символа - после 3 символов начнется передача символа NULL (0×00) - и если такое замечаем - прекращаем передачу.
Все работало.
Но как я понял из кучи литературы (видимо неправильно понял) - основная программа должна выполняться в цикле, и я просто перенес строки:
sei(); // Разрешаем прерывания
UDR = buffer[0]; // Отправляем первый байт
UCSRB|=(1<<UDRIE); // Разрешаем прерывание UDRE
в цикл while (1)
Мигание диодами я убрал, не нужно оно мне пока что тут, в AVRStudio и так отлично видно, что получается.
И вот в симуляции получается так: доходит до цикла while, отправляет в UDR первый байт, т.е. buffer[0], перескакивает на обработчик, отрабатывает его 1 раз (!!!) и позвращается к циклу, т.е. фактически передается только первое число из строки.
Ну а в цикле от уже крутится бесконечно - прерывание не обрабатывается… А в цикле у нас передача первого символа - и он постоянно его пихает в UDR, но на обработчик прерывания не переходит…
Интуитивно я догадываюсь, создавать функцию отправки до бесконечного цикла, и вызывать ее в цикле.. Так?
ты не путай теплое с мягким.
Твой цикл вращается бешеное число раз, заваливая буффер UDR символами в то время как он не готов принять байт, т.к. прожевыает предыдущий.
Я потому и поставил там цикл с мигалками, чтобы код выполнился один раз, ну или хотя бы не чаще чем обработчик пошлет всю строку. Иначе получается косяк. В реальной же программе, где масса кода и вызов процедуры отправки идет не в цикле, а по событиям тогда, когда это реально нужно.
А прерывание у тебя не вызывается потому, что не успело еще. Отправка байта это весьма долгий процесс.