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, но если нужна вдруг меньшая дискретность или высокая скорость реакции на событие (а не пока главный конвеер дотащит задачу до исполнения), то механим управляемых прерываний, ИМХО, то что доктор прописал.

51 thoughts on “AVR. Учебный Курс. Управляемый вектор прерывания”

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

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

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

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

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

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

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

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

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

            1. Или эмулировать приоритеты…
              Конечно, это лишний код и такты, но…
              Например отдать под флаг приоритета один регистр или ячейку оперативки.
              А в каждый обработчик добавить сравнение:
              Timer0_Overflow.
              if r20>05 then goto exit \\сравниваем флаг приоритета с нашим приоритетом(05). Если работает более приоритетное прерывание, не мешаем ему, выходим. Можно > заменить на >=
              push r20 \\Сохраним приоритет прерванного прерывания
              r20:=05 \\пишем во флаг свой приоритет(чтоб и нас могли прервать еще более важные прерывания)
              CLI \\разрешим нас прервать
              код обработчика \\работаем
              SEI \\запретим нас прерывать(а то не сможем правильно восстановить прерванное прерывание)
              pop r20 \\восстановим приоритет прерванного прерывания
              if r20>0 then goto exit \\Если мы кого-то прервали, выходим
              r20:=00 \\Иначе, если мы никого не прервали, очистим флаг
              exit: reti \\и выйдем

              Недостатки есть конечно:
              Все прерывания с более низким приоритетом при работе более приоритетного не откладываются, а завершаются — это главный недостаток…
              Прерывать мы будем даже более приоритетные прерывания, но это его замедлит всего на несколько тактов…

              Это всего лишь идея, и ее можно доработать…
              В идеале надо как-то сохранять состояние менее приоритетных прерываний, чтоб они не завершались а откладывались… но это куча кода и стека

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

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

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

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

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

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

  4. косьяк.
    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 на входе во фрагмент возможно ненулевой, в нем флаги. А тут его пользуем как базу для проверки. Айайай. Почистить нада, насяйника!

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

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

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

  6. Не сочтите за занудство, но команды выполняются за 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 циклов, и если не важно СТРОГО считать время, а важнее БЫСТРО обработать прерывание, использовать её смысла не имеет.

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

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

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

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

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

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

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

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

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

            CLI
            RCALL Vector

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

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

  8. Спасибо за статью! очень полезной оказалась, щас так и переделаю свою прогу. Вот только вопрос а как быть если памяти больше 128К? Нужно использовать уже не команду ijmp а EIJMP. Так вот как загрузить регистр EIND?

    c ijmp тут все понятно:
    PC(15:0) ←Z
    PC(21:16) ← 0

    а EIJMP
    PC(15:0) ←Z
    PC(21:16) ←EIND

    Есть что то подобное типа
    LDI R16,»самый старший из трех байт»(Timer_01)
    STS EIND,R16

    1. Честно говоря не скажу вот так сразу. НА асме я под процы больше 32к не писал, т.к. там масштаб задачи уже подразумевает Си.

        1. в справке к AVR studio 4.19 (help->Assembler help) написано:

          Functions
          The following functions are defined:
          LOW(expression) returns the low byte of an expression
          HIGH(expression) returns the second byte of an expression
          BYTE2(expression) is the same function as HIGH
          BYTE3(expression) returns the third byte of an expression
          BYTE4(expression) returns the fourth byte of an expression
          LWRD(expression) returns bits 0-15 of an expression
          HWRD(expression) returns bits 16-31 of an expression
          PAGE(expression) returns bits 16-21 of an expression
          EXP2(expression) returns 2 to the power of expression
          LOG2(expression) returns the integer part of log2(expression)

  9. Не знал к какой теме прилепить вопрос,
    Привет Dihalt, помоги разобраться почему не срабатывает прерывание по переполнению T/C0 на атмеге 16, на ассемблере. Каким образом работает .org в этом примере, для чего она здесь необходима, пример брал с Ревич, только там tiny2313. Через отладчик таймер считает, ставиться флаг TOV0, но на метку TIM0 не переходит.
    /////////////////////
    .def count = r16 // Счетчик
    /////////////////////
    .def timeS1 = r17

    .def asdf = r20
    ////////////////////
    .def temp = r18 // Временная

    .def temp2 = r21

    rjmp Init
    .org $026 // адрес прерывания переполнения T/C O
    rjmp TIM0

    TIM0:
    inc temp2
    out PORTD, temp2
    /* // программа управления SERVO 1
    ldi timeS1, 12 //угол поворота сервы
    cp count, timeS1
    brne tim0_compA_control_mark1
    cbi portb, 0

    tim0_compA_control_mark1:

    cpi count, 158
    brne TIM0

    sbr r23, 0b11111111
    out PORTB, r23
    */
    ldi asdf,0
    out TCNT0, asdf
    reti

    Init:
    ldi temp, low(RAMEND)
    out SPL, temp
    ldi temp, 0b11111111
    out DDRD, temp
    out DDRB, temp
    out PORTD, temp
    out PORTB, temp

    ldi temp, 10
    out OCR0, temp //регистр сравнения

    clr temp
    ldi temp, (1<<TOIE0)
    out TIMSK,temp
    ldi asdf,98
    out TCNT0,asdf
    ldi temp, 1<<CS00
    out TCCR0, temp //предделитель частоты

    Cykle:
    rjmp cykle

  10. Специально зарегистрировался на этом сайте. )) Очень понравился.
    Читаю по порядку все по курсу обучения АВР для начинающих. Не могу сказать, что вообще ничего не знаю. Мало того, читаю уже раз в третий…
    Однако, эта статья никак не доходит. не совсем она для начинающих, или я такой тупой…
    Просто после всего самого фундаментального, по работе МК и подключению его, пошла первая статья про программирование, где сразу такое количество кода..
    А где можно посмотреть, синтаксис операторов в ассемблере, их работу, за что отвечают? Правило написания шапки программы и т.д.???? Да еще и так, чтобы тупому было понятно…
    Боюсь, что сразу с РТОС не разберусь. Мне бы сначала про то, какие операторы что конфигурируют, как работать с таймерами и прерываниями.
    Или я просто не туда залез? Пните пожалуйста.

    1. Список операторов в даташите, плюс справка в AVR Studio + книгу найди Название: Микроконтроллеры AVR семейств Tiny и Mega фирмы ATMEL + CD. Электронное издание
      Автор: Евстифеев А.В.

  11. И можно ли пронумеровать уроки в какой последовательности их читать (изучать)? Типа урок № 1, урок № 2 и т.д. А то, я путаюсь. Спасибо. =)

    1. Да нет у них какой либо внятной последовательности. Это не линейный список, а облако, где каждая статья решение какой-либо задачи с контроллером.

  12. А! все разобрался… По датам посмотрел добавления тем. Читать нужно просто снизу вверх….
    Все равно удобнее было бы подписать уроки по номерам.

  13. А можно адрес перехода в обработчике прерывания загружать напрямую в регистровую пару Z, а не загружать в RAM, а потом из нее вытаскивать?

        1. Регистр может пригодиться, потому забивать его адресом глупо. Особенно если адрес будет нужен не скоро.

Добавить комментарий

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha