AVR. Учебный Курс. Управляемый вектор прерыванияPrint This Post

Автор DI HALT
Опубликовано 12 июня 2009 
Рубрики: AVR. Учебный курс
Метки: , , ,

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

Простой пример — таймер и его прерывание по переполнению.
Мы можем задавать выдержку и по прерыванию делать какие-нибудь операции. Но если в один момент времени мы хотим чтобы таймер по прерванию сделал одну операцию, а потом другую, третью. Да сколько угодно, в зависимости от состояния. А вектор один.

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

Конечно, можно добавить в обработчик прерывания switch-case конструкцию и по выбору режима перейти на нужный участок кода, но это довольно громоздко, а самое главное — время перехода будет разное, в зависимости от того в каком порядке будет идти опрос-сравнение switch-case структуры.

То есть в свитче вида:

1
2
3
4
5
6
7
switch(x)
	{
	1: Действие 1
	2: Действие 2
	3: Действие 3
	4: Действие 4
	}

Будет последовательное сравнение х вначале с 1, потом с 2, потом с 3 и так до перебора всех вариантов. А в таком случае реакция на Действие 1 будет быстрей чем реакция на Действие 4. Особо важно это при расчете точных временных интервалов на таймере.

Но есть простое решение этой проблемы — индексный переход. Достаточно перед тем как мы начнем ожидать прерывание предварительно загрузить в переменные (а можно и сразу в индексный регистр Z) направление куда нам надо перенаправить наш вектор и воткнуть в обработчик прерывания индексный переход. И вуаля! Переход будет туда куда нужно, без всякого сравнения вариантов.

В памяти создаем переменные под плавающий вектор:

1
2
Timer0_Vect_L:	.byte	1 	; Два байта адреса, старший и младший
Timer0_Vect_H: 	.byte	1

Подготовка к ожиданию прерывания проста, мы берем и загружаем в нашу переменную нужным адресом

1
2
3
4
5
6
7
		CLI 				; Критическая часть. Прерывания OFF 
		LDI	R16,low(Timer_01)	; Берем адрес и сохраняем
		STS	Timer0_Vect_L,R16	; его в ячейку памяти.
 
		LDI	R16,High(Timer_01)	; Аналогично, но уже со старшим вектором
		STS	Timer0_Vect_H,R16
		SEI				; Прерывания ON

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

А обработчик получается вида:

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
;=============================
; Вход в прерывание по переполнению от Timer0
;=============================
TIMER_0:	PUSH	ZL		; сохраняем индексный регистр в стек
		PUSH	ZH		; т.к. мы его используем
		PUSH	R2		; сохраняем R2, т.к. мы его тоже портим
		IN	R2,SREG		; Извлекем и сохраняем флаговый регистр
		PUSH	R2		; Если не сделать это, то 100% получим глюки
 
		LDS	ZL,Timer0_Vect_L		; загружаем адрес нового вектора
		LDS	ZH,Timer0_Vect_H		; оба байта. 
 
		CLR 	R2		; Очищаем R2
		OR	R2,ZL		; Проверяем вектор на ноль. Иначе схватим аналог
		OR	R2,ZH		; reset'a. Проверка идет через операцию OR 
		BREQ	Exit_Tm0	; с накоплением результата в R2
					; так мы не портим содержимое Z и нам не придется
					; загружать его снова
		IJMP			; Уходим по новому вектору
 
; Выход из прерывания.		
Exit_Tm0:	POP 	R2		; Достаем и восстанавливаем регистр флагов
		OUT	SREG,R2		
		POP	R2		; восстанавливаем R2
		POP	ZH		; Восстанавливаем Z
		POP	ZL
		RETI
 
; Дополнительный вектор 1
Timer_01:	NOP			; Это наши новые вектора
		NOP			; тут мы можем творить что угодно
		NOP			; желательно недолго - в прерывании же 
		NOP			; как никак. Если используем какие другие
		NOP			; регистры, то их тоже в стеке сохраняем
		RJMP	Exit_Tm0	; Это переход на выход из прерывания
					; специально сделал через RJMP чтобы 
; Дополнительный вектор 2		; сэкономить десяток байт на коде возврата :)))
Timer_02:	NOP
		NOP
		NOP
		NOP
		NOP
		RJMP	Exit_Tm0
; Дополнительный вектор 3
Timer_03:	NOP
		NOP
		NOP
		NOP
		NOP
		RJMP	Exit_Tm0

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

В моей RTOS механизмов взаимоисключений нет, но их можно реализовать. По крайней мере сделать некоторое минимальное подобие. Полноценную реализацию всего этого барахла я делать не хочу, т.к. моей целью является удержания размера ядра на уровне 500-800 байт.
Проще всего зарезервировать в памяти еще один байт — переменную занятости. И когда один процесс захватывает ресурс, то в эту переменную он записывает время когда ориентировочно он его освободит. Время идет в тиках системного таймера которое у меня 1ms.
Если какой либо другой процесс попытается обратиться к этому же аппаратному ресурсу, то он вначале посмотрит на состояние его занятости, считает время в течении которого будет занято и уйдет покурить на этот период — загрузит сам себя в очередь по таймеру. Там снова проверит и так далее. Это простейший вариант.

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

Решение проблемы — добавление еще одной очередной цепочки, на этот раз уже на доступ к ресурсу. Чтобы он не простаивал вообще. Т.е. один выскочил, тут же второй, третий и так далее пока все процессы не справят свою нужду в какой нибудь там USART.
Недостаток очевиден — еще одна очередь это дополнительная память, дополнительный код, дополнительное время. Можно, конечно, извратиться и на очередь к вектору натравить код диспетчера основной цепи. Но тут надо все внимательно отлаживать, ведь вызываться он будет по прерыванию! Да и громоздко, требуется лишь тогда, когда у нас много желающих.

Второе решение — выкинуть переменную времени занятости, оставив только флаг “Занято!”. А процесс который пытается обратиться не убегает покурить, а отскакивает на пару шагов назад — на конец очереди задач и сразу же ломится обратно. Народ вокруг сортира не вокруг бегает, а толкется локтями у входа по принципу кто первый пролезет.
Недостаток другой — большая нагрузка на главный конвеер, куча запросов на постановку в очередь так недолго распухнуть на всю оперативку и повстречаться со стеком, а это черевато глобальным апокалипсисом.

Разумеется таймер тут приведен для примера, большую часть задач можно решить системным таймером RTOS, но если нужна вдруг меньшая дискретность или высокая скорость реакции на событие (а не пока главный конвеер дотащит задачу до исполнения), то механим управляемых прерываний, ИМХО, то что доктор прописал.

Комментарии

33 комментариев на «AVR. Учебный Курс. Управляемый вектор прерывания»


  1. SWG 12 июня 2009 2:39

    Все же нет ничего лучше старой, прекрасной системы приоритетных прерываний. Все предельно просто, реакция мгновенная, более приоритетное событие прервет обработку прерывания от другого, потом она будет продолжена… 8 уровней приоритета. Куча режимов, задается все программно, очень гибко настраивается. Тоскую по ним уже 20 лет, с тех пор как перестал делать контроллеры на КР580ВМ80А с контроллером прерываний КР580ВН59А…
    Можно было как угодно переназначать программно приоритеты, или сделать автоматическую циклическую смену, когда обработанное прерывание становится самым младшим, и еще много всего… Кстати, КР580ВН59А легко прицепить к любой меге, имеющей внешнюю шину адреса и данных, или I8048, I8051. Конечно, корпус крупноват, (DIP28), и потребление милиампер 100… И внутренние прерывания через него не пустишь, только внешние. Удивляюсь, в последнее время в контроллеры чего только не суют, а сделать систему прерываний приоритетной - западло… А ведь это намного проще, чем USART, АЦП, не говоря уж о USB. Просто немного простой логики. Как мне их не хватает… К хорошему привыкаешь быстро, но кто не пробовал, тому этого не понять, какая это была прелесть для задач реального времени… Кому интересно, почитайте о режимах и их настройке в КР580ВН59А. (Intel 8059).

    DI HALT

    Тоже удивляюсь. Ведь такой рулез был! На С51 кстати был простейший двухуровневый поллинг запросов, хоть что то. А на AVR это уже зарубили на корню :(

    SWG

    В MCS 48 тоже было 2 вектора - от таймера и внешнее. А вот про приоритет в них уже не помню… Помню, что чтобы не заморачиваться с сохранением в прерываниях, просто использовал для прерываний регистры другого банка, неиспользуемые в программах. Там индексные регистры для косвенной адресации тоже в обоих банках были.

    ex0-planet.livejournal.com

    Ну вложенные прерывания в AVR таки есть, так что прерывать прерывания можно.

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

    SWG

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

    ex0-planet.livejournal.com

    > Она гарантирует, что наиболее приоритетные прерывания всегда будут обрабатываться оперативно, независимо от состояния программы и обработки прерываний более низкого уровня.
    Как я уже сказал, AVR умеет вкладывать прерывания. Этого достаточно для организации описанного поведения, ибо время реакции в любом случае будет определяться длиной максимальной критической секции (нам же неинтересно, если нас прервут например, во время сохранения SREG), а она зависит более от структуры программы, чем от аппаратуры.

    Более того, даже требование вкладываемости прерываний не является обязательным, ибо давным давно известна техника деления обработчика прерываний на top (непрерываемую) и bottom (прерываемую) поповины. Достаточно только уметь переключать контексты и выполнение нижних половинок можно переложить на шедулер.

    SWG

    При желании можно вообще обойтись без прерываний (постоянно проверяя их источники). Я же говорил, не попробовав - не поймешь. Это как описывать оттенки цвета слепому…

    ex0-planet.livejournal.com

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

    Segler

    И всё-таки сделали трех уровневую систему прерываний. Xmega - новое детище атмела. Да и появились ещё кое-какие полезные вещички. Правда по какой цене и где покупается… :) Но, главное, есть! где-то.
    http://atmel.com/dyn/resources/prod_documents/doc8068.pdf стр 24

    SWG

    Лучше поздно, чем никогда… Правда, Xmega для меня (для реально возможных у меня применений) несколько избыточна… все, для чего не хватит Меги 128, проще переложить на нормальный комп, обеспечив лишь канал связи.

    youmych

    Я сейчас работаю с СС2510 (Chipcon). Приоритеты прерываний имеются. Только заданы они жестко. Не уверен насчет того же в MSP430 - писал под него достаточно болшой софт, но там приоритеты прерываний (как ни странно) не понадобились


  2. Cyber_RAT 12 июня 2009 10:40

    по моему не
    LDI ZL,Timer0_Vect_L ; загружаем адрес нового вектора
    LDI ZH,Timer0_Vect_H ; оба байта.

    а LDS если мы в памяти сохраняли….

    DI HALT

    Точно! Спасибо. Копипаст зло! =)

    semak

    LDI R16,low(Timer_01) ; прикольно, не знал что адрес(по меткам) мона туда загружать :)

    DI HALT

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


  3. _riko_ 12 июня 2009 13:40

    Где-то я уже видел сию функцию приоритетов….. ))
    не в журнале ли “Хакер”, на мобильном трояне ;) ?
    кстати, прикольная весч ! (=

    DI HALT

    Нет, там было просто выгребание данных по куче разных таблиц переходов из ПЗУ


  4. ArgusB 13 июня 2009 11:47

    косьяк.
    IN R2,SREG ; Извлекем и сохраняем флаговый регистр
    PUSH R2 ; Если не сделать это, то 100% получим глюки
    LDS ZL,Timer0_Vect_L ; загружаем адрес нового вектора
    LDS ZH,Timer0_Vect_H ; оба байта.
    OR R2,ZL ; Проверяем его на ноль. Иначе схватим аналог
    OR R2,ZH ; reset’a. Проверка идет через операцию OR
    BREQ Exit_Tm0 ;

    Внимательно смотрим… r2 на входе во фрагмент возможно ненулевой, в нем флаги. А тут его пользуем как базу для проверки. Айайай. Почистить нада, насяйника!

    DI HALT

    О точно.


  5. ToRmoZZ 14 июня 2009 19:39

    Прошу прощения за офф.
    НО мой пост в теме AVR. Учебный курс. Передача данных через UART был попросту проигнорирован (что очень обидно).Ведь вопрос актуален организация Многопроцессорного режима.Ещё раз прошу прощения но хотелось бы получить ответ.

    DI HALT

    Для многопроцессорного режима куда лучше использовать I2C. ТАм и адресация и разрешение коллизий.

    У USART можно только на базе архитектуры Token Ring что то сделать. Что медленно и неуклюже. В промышленности ЕМНИП на этом принципе работает RS485


  6. SAWushka 25 июня 2009 20:46

    Не сочтите за занудство, но команды выполняются за 1-3 такта, поэтому если мы хотим, именно
    >точных временных интервалов на таймере

    я имею в виду с точностью до такта, то необходимо еще учесть какая команда была во время наступления прерывания

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    TIM0_OVF:
    		nop  ; сохраняем контекст программы
    		nop  ;
    		nop  ;
     
    		in   temp,tcnt0
    		subi temp,(7+n)	; сдесь под n понимается число тактов,
    				; необходимое для сохранения контекста
    		brmi align1
    align1:		dec  temp3
    		brmi align2
    align2:		nop		; эта операция будет выполняться через 
    		nop		; точное колличество циклов
    		nop		;
     
    		reti

    такая контрукция сьест 5-7 циклов, и если не важно СТРОГО считать время, а важнее БЫСТРО обработать прерывание, использовать её смысла не имеет.

    DI HALT

    Тоже верно, только работает лишь на таймерах. ГДе есть чем замерить прошедшее с момента события время.


  7. glukerr.livejournal.com 29 июня 2009 14:29

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

    DI HALT

    Перед тем как ждать прерывание мы заносим доп вектор в переменную.

    glukerr.livejournal.com

    CLI ; Критическая часть. Прерывания OFF
    LDI R16,low(Timer_01) ; Берем адрес и сохраняем
    STS Timer0_Vect_L,R16 ; его в ячейку памяти.

    LDI R16,High(Timer_01) ; Аналогично, но уже со старшим вектором
    STS Timer0_Vect_H,R16
    SEI ; Прерывания ON

    вот тут загружается адрес первого доп вектора. верно?
    при получении прерывания, происходит переход по этому адресу. так?)

    glukerr.livejournal.com

    т.е. выбор куда пойти необходимо сделать _до_ наступления прерывания?

    glukerr.livejournal.com

    пересчитал.
    все стало ясно)


  8. dima_m 16 июня 2010 15:27

    DIHALT привет, а почему ты использовал аж два байта из SRAMа для адресов векторов. Я попробовал использовать один, все работает также. И с одним байтом можно забабахать целых 255 векторов на одно прерывание. Это ж за глаза хватит. Кажется что второй байт абсолютно лишний и он никогда не будет использоваться. И без него код проще и меньше.

    DI HALT

    Адрес двубайтный, а на авось (”никогда не будет использоваться”) даст сбой. Тем более никогда не знаешь как далеко окажется реальный обработчик от вектора и какой длины будет адрес. Он может быть и один байт (старший ноль) и все два.

    dima_m

    Ну да что-то я об этом не подумал. Ведь и вправду когда прога распухнет адрес может стать уже двубайтным.

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

    dima_m

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

    DI HALT

    Есть одна разница. При приходе прерывания еще и аппаратно они запрещаются. Т.е. полный аналог аппаратного вызова будет выглядеть как

    CLI
    RCALL Vector

    А по RETI прерывания вновь включаются. Несмотря ни на что.

    Плюс надо учитывать тот факт, что аппаратные вызовы снимают флаги прерываний, чего не будет по RCALL

Оставьте свой отзыв

Вы должны войти, чтобы оставлять комментарии.


Материалы сайта являются авторскими. Копирование и публикация материалов без активной ссылки на первоисточник запрещено.

Реклама: