Перейти к содержимому

Чем отличаются команды ret и reti

  • автор:

Прерывания

Это аппаратные события. Ведь у микроконтроллера кроме ядра есть еще дофига периферии. И она работает параллельно с контроллером. Пока контроллер занимается вычислением или гоняет байтики по памяти — АЦП может яростно оцифровывать входное напряжение, USART меланхолично передавать или принимайть байтик, а EEPROMка неспеша записывать в свои тормозные ячейки очередной байт.

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

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

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

Как это работает

Когда случается прерывание, то процессор тут же завершает текущую команду, пихает следующий адрес в стек (точно также как и при CALL) и переходит… А куда, собственно, он переходит?

А переходит он на фиксированный вектор прерывания. За каждым аппаратным прерыванием закреплен свой именной адрес. Все вместе они образуют таблицу векторов прерывания. Расположена она в самом начале памяти программ. Для Атмега16, используемой в Pinboard таблица прерываний выглядит так:

RESET 0x0000 ; Reset Vector

INT0addr 0x0002 ; External Interrupt Request 0

INT1addr 0x0004 ; External Interrupt Request 1

OC2addr 0x0006 ; Timer/Counter2 Compare Match

OVF2addr 0x0008 ; Timer/Counter2 Overflow

ICP1addr 0x000a ; Timer/Counter1 Capture Event

OC1Aaddr 0x000c ; Timer/Counter1 Compare Match A

OC1Baddr 0x000e ; Timer/Counter1 Compare Match B

OVF1addr 0x0010 ; Timer/Counter1 Overflow

OVF0addr 0x0012 ; Timer/Counter0 Overflow

SPIaddr 0x0014 ; Serial Transfer Complete

URXCaddr 0x0016 ; USART, Rx Complete

UDREaddr 0x0018 ; USART Data Register Empty

UTXCaddr 0x001a ; USART, Tx Complete

ADCCaddr 0x001c ; ADC Conversion Complete

ERDYaddr 0x001e ; EEPROM Ready

ACIaddr 0x0020 ; Analog Comparator

TWIaddr 0x0022 ; 2-wire Serial Interface

INT2addr 0x0024 ; External Interrupt Request 2

OC0addr 0x0026 ; Timer/Counter0 Compare Match

SPMRaddr 0x0028 ; Store Program Memory Ready

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

Запишем эту бодягу в цивильной форме, через ORG и добавим немного кода.

.ORG $000 ; (RESET)

RJMP Reset

RETI ; (INT0) External Interrupt Request 0

RETI ; (INT1) External Interrupt Request 1

RETI ; (TIMER2 COMP) Timer/Counter2 Compare Match

RETI ; (TIMER2 OVF) Timer/Counter2 Overflow

RETI ; (TIMER1 CAPT) Timer/Counter1 Capture Event

RETI ; (TIMER1 COMPA) Timer/Counter1 Compare Match A

RETI ; (TIMER1 COMPB) Timer/Counter1 Compare Match B

RETI ; (TIMER1 OVF) Timer/Counter1 Overflow

RETI ; (TIMER0 OVF) Timer/Counter0 Overflow

RETI ; (SPI,STC) Serial Transfer Complete

RJMP RX_OK ; (USART,RXC) USART, Rx Complete

RETI ; (USART,UDRE) USART Data Register Empty

RETI ; (USART,TXC) USART, Tx Complete

RETI ; (ADC) ADC Conversion Complete

RETI ; (EE_RDY) EEPROM Ready

RETI ; (ANA_COMP) Analog Comparator

RETI ; (TWI) 2-wire Serial Interface

RETI ; (INT2) External Interrupt Request 2

RETI ; (TIMER0 COMP) Timer/Counter0 Compare Match

RETI ; (SPM_RDY) Store Program Memory Ready

.ORG INT_VECTORS_SIZE ; Конец таблицы прерываний

; Это обработчик прерывания. Тут, на просторе, можно наворотить сколько

; угодно кода.

RX_OK: IN R16,UDR ; Тут мы делаем что то нужное и полезное

RETI ; Прерывание завершается командой RETI

Reset: LDI R16,Low(RAMEND) ; Инициализация стека

OUT SPL,R16 ; Обязательно.

LDI R16,High(RAMEND)

OUT SPH,R16

SEI ; Разрешаем прерывания глобально

OUT UCSRB,R17

Теперь разберем эту портянку. Контроллер стартует с адреса 0000, это точка входа. Там мы сразу же делаем бросок на метку RESET. Если это не сделать, то контроллер пойдет выполнять команды из таблицы векторов, а они не для того там посажены. Да и не далеко он ускачет — без инициализации стека и наличия адреса возврата в нем первый же RETI вызовет коллапс. Ведь RETI работает почти также как и RET.

Поэтому сразу уносим оттуда ноги на RESET. Где первым делом инициализируем стек. А потом, командой SEI, разрешаем прерывания. И установкой бита в регистре периферии UCSRB включаем прерывание по приему байта.

Дальше зацикливаемся и ждем когда в приемный буффер USART извне свалится байт. Запускай это дело в эмуляцию и начинай трассировать по одной команде. Сначала, как я и говорил, проц прыгнет на метку Reset, потом выставит нужные значения и наглухо зациклится на

До прихода байта. Но как же нам осуществить этот приход байта если весь наш эксперимент не более чем симуляция виртуального процессора в отладчике? А очень просто!

Вручную вписать этот байт в регистр и вручную же протыкать флаг, словно байт пришел. За прием байта отвечает флаг RXC в регистре периферии UCSRA, раздел USART. Найди там бит RXC и тыкни его, чтобы закрасился. Все, прерывание вроде как наступило.

Нажми F11, чтобы сделать еще один шаг по программе… Опа, стрелочка улетела в таблицу векторов, как раз на вектор

RJMP RX_OK ; (USART,RXC) USART, Rx Complete

А оттуда уже прыжок сразу же на метку RX_OK, где мы забираем данные из регистра UDR в R17 и выходим из прерывания, по команде RETI.

При этом процессор вернется в наш бесконечный цикл, в то место откуда прерывался.

Вот, как это было, если по коду:

Вот, вроде теперь вопросов по выполнению быть не должно.

Разрешение и запрещение прерываний Прерываний много, но по умолчанию они все запрещены. Во-первых, глобально — есть в процессоре в регистре SREG (о нем чуть позже) флаг I (interrupt) когда он равен 0, то все прерывания запрещены вообще, глобально, все без исключения.

Когда он равен 1, то прерывания глобально разрешены, но могут быть запрещены локально.

Устанавливается и сбрасывается этот флаг командами

  • SEI — разрешить прерывания
  • CLI — запретить прерывания (Да, по поводу моего ника, DI это, кстати, то же самое что и CLI, но для процессора Z80 😉 )
1 IN R16,UDR

Либо флаг скидывают вручную — записью в этот флаг единицы. Не нуля! Единицы! Подробней в даташите на конкретную периферию. Очередность прерываний Но что будет если произошло одно прерывание и процессор ушел на обработчик, а в этот момент произошло другое прерывание? А ничего не будет, при уходе на прерывание происходит аппаратный глобальный запрет всех других прерываний– просто сбрасывается флаг I, а по команде RETI, при возврате, этот флаг устанавливается в 1. В этом, кстати, отличие RET от RETI Но! Никто не запрещает нам внутри обработчика поставить SEI и разрешить прерывания. При этом мы получим вложенные прерывания. Можно, но это опасно. Черевато переполнением стека и прочими гадостями. Так что это надо делать с твердым осознанием последствий. Что тогда? Прерывание которое пришло во время обработки первого потеряется? Нет, не потеряется. Флаг то его события никто сам не снимет, так что только процессор выйдет из первого обработчика (разреша при этом глобальные прерывания), как это не снятый флаг сгенерирует новое прерывание и процессор его обработает. А теперь такая ситуация — прерывания запрещены, неважно по какой причине, и тут приходит два прерывания. Поднимает каждая свой флажок и оба ждут глобального разрешения. SEI. Кто пойдет первым? А первым пойдет то прерывание, чей вектор меньше по адресу, ближе к началу памяти. За ним второй. В случае когда пришло несколько прерываний одного типа. Скажем, пока мы там ковырялись в обработчике, нам сбоку тут еще таймер три раза тикнул своим флагом, то обработается только одно событие, остальные могут потеряться. Бег по граблям Прерывания штука мощная, но опасная. С их помощью плодятся такие глюки, по сравнению с которыми стековые срывы так — семечки. Вся засада багов из-за кривых прерываниях в том, что их практически невозможно отследить в отладчике. Возникает плавающий глюк, появление которого зависит от того в каком именно месте кода произойдет вызов прерывания. Что поймать, сам понимаешь, почти невозможно. Так что если у МК то понос, то золотуха, то программа петухом поет, а то молчит как рыба — знай, в 95% копать собаку надо в районе прерываний и их обработчиков. Но если правильно написать прерывание, то багов оно не даст. Главное понимать в чем его опасность. Грабли первые — спасай регистры. Прерывание, когда оно разрешено, вызывается ВНЕЗАПНО, между двумя произвольными инструкциями кода. Поэтому очень важно, чтобы к моменту когда мы из прерывания вернемся все осталось как было. Все регистры, используемые в обработчике прерываний, должны быть предварительно сохранены. Также должен быть сохранен регистр флагов SREG, в котором хранится результат логических операций. Результаты проще всего сохранять в стеке. Приведу пример: Вот есть у нас обработчик прерывания который сравнивает байт на входе в USART и если он равен 10, выдает обратно отклик ‘t’ (ten в смысле).

1 2 3 4 5 6 7 8 9 10 11 RX_OK: IN R16,UDR CPI R16,10 BREQ Ten RJMP Exit Ten: LDI R17,’t’ OUT UDR,R17 Exit:

И у нас есть код. Код скопипащен от балды из какого то проекта, даже пояснять не буду, не в нем суть.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 . . . LPM R18,Z CPI R18,0 BREQ ExitStr SUBI R16,65 LSL R16 LDI ZL,Low(Ltrs*2) LDI ZH,High(Ltrs*2) ADD ZL,R16 ADC ZH,R1 . . .

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

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 . . . LPM R18,Z CPI R18,0 BREQ ExitStr SUBI R16,65 >>>>>>>>>>>Прерывание >>>>>>>>>>> RX_OK: IN R16,UDR CPI R16,10 BREQ Ten RJMP Exit Ten: LDI R17,’t’ OUT UDR,R17 Exit: RETI LSL R16 LDI ZL,Low(Ltrs*2) LDI ZH,High(Ltrs*2) ADD ZL,R16 ADC ZH,R1 . . .

До входа в прерывание, после команды SUBI R16,65 в R16 было какое то число из которого вычли 65. И дальше с ним должны были провернуть операцию логического сдвига LSL R16, но тут вклинился обработчик, где в R16 записалось значение из UDR. И мы выпали из обработчика перед командой LSL R16, но в R16 уже мусор какой то. Совершенно не те данные, что мы планировали. Естественно вся логика работы от такого издевательства порушилась и возник глюк. Но стоит прерыванию прийти чуть раньше, на одну микросекунду — как глюк исчезнет, т.к. SUBI R16,65 будет уже после прерывания и логика не будет порушена, но может возникнуть другой глюк. Полная лотерея, повезет не повезет. Может вылезти сразу, а может и через год идеальной работы, а потом также сгинет и сиди чеши репу что же это было — сбой по питанию, бага, или таракан по плате пробежал неудачно. Чтобы такого не было в обязательно порядке надо сохранять регистры и SREG на входе в прерывание и доставать на выходе. У нас тут, в обработчике, используется R17 и R16 и SREG (в него помещается результат работы команды CPI). Вот их и сохраним. Выглдеть это будет так:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 RX_OK: PUSH R16 ; Сохранили R16 IN R16,SREG ; Достали SREG в R16 PUSH R16 ; Утопили его в стеке PUSH R17 ; Туда же утопили R17 ; Теперь можно со спокойной совестью работу работать. IN R16,UDR CPI R16,10 BREQ Ten RJMP Exit Ten: LDI R17,’t’ OUT UDR,R17 ; А на выходе вернем все как было. ; Достаем в обратном порядке Exit: POP R17 POP R16 OUT SREG R16 POP R16 RETI ; Спокойно выходим. Регистры вернул как было.

Как же выносить данные из прерываний? Да по разному, можно в память сохранять, можно для этого спец регистр заиметь и знать, что он может в любой момент измениться в прерывании. Грабли вторые — не тормози. Прерывания отвлекают процессор от основных дел, более того, они блокируют другие прерывания. Поэтому в прерывании главное все сделать максимально быстро и свалить. Никаких циклов задержки, никаких долгоиграющих процедур. Никаких ожиданий аппаратного события. СКОРОСТЬ! СКОРОСТЬ! СКОРОСТЬ! Вот что должно тобой руководить при написании обработчика. Заскочил — сделал — выскочил! Но такая красивая схема возможна далеко не всегда. Иногда бывает надо по прерыванию делать и медленные вещи. Например, прием байтов из интерфейса и запись их в какую нибудь медленную память, вроде EEPROM. Как тогда быть? А тут делают проще. Цель прерывания — во что бы то ни стало среагировать на событие именно в тот момент, когда оно пришло, чтобы не прозевать. А вот обрабатывать его прямо сейчас обычно и не обязательно. Заскочил в обработчик, схватил быстро тухнущие данные, перекинул их в буффер где им ничего не угрожает, поставил где-нибудь флажок отметку “мол там байт обработать надо” и вышел. Все! Остальное пусть сделает фоновая программа в порядке общей очереди. Либо, если иначе никак нельзя, разрешай прерывания внутри обработчика, но смотри чтобы не возникли рекурсивные прерывания, когда один и тот же обработчик разрешает прерывания сам себе, образуя множественные вложенные вызовы с зарыванием в стек. Грабли третьи — атомарный доступ Есть ряд операций которые должны выполняться неразрывно. Например, чтение 16ти разрядных регистров таймера. Ядро то у нас восьми-разрядное, поэтому 16ти разрядный регистр таймера считывается в два приема сначала младший байт, а потом старший. И вот в этом месте нельзя ни в коем случае допускать, чтобы между считыванием младшего и старшего было прерывание. Т.к. после выхода из прерывания таймер уже может много чего натикать и информация уже будет не актуальная. Поэтому перед чтением делаем CLI, а после SEI. Также нельзя разрешать прерывания если одна и та же многоходовая операция делается и в прерывании и в главном цикле. Например, прерывание хватает байты из АЦП и пишет их в буффер (ОЗУ), образуя связные цепочки данных. Главный же цикл периодически из этого буффера данные читает. Так вот, на момент чтения буффера из памяти, надо запрещать прерывания, чтобы никто не посмел в этот буффер что-нибудь записать. Иначе мы можем получить невалидные данные — половина байтов из старого замера, половина из нового. Причем, во многих случаях не обязательно глобально блокировать все прерывания вообще, достаточно заблокировать то локальное прерывание, которое может нам нагадить. Грабли четвертые — не блокируй. Третие грабли нужно внимательно обходить, но в паранойю впадать тоже не следует. Тупо запрещать прерывания везде, где мерещится бага, не стоит. Иначе можно прозевать события, а это плохо. Нет, обработчик то выполнится, но будет это уже не актуально — хороша ложка к обеду. Обязательно погоняй в отладчике код (да хотя бы кучу NOP) с разными прерываниями. Чтобы понять и прочувствовать как ведут себя прерывания. Старая версия статьи. Чисто поржать 🙂 AVR. Учебный курс. Флаги и условные переходы Автор DI HALT Опубликовано 08 июля 2008 Рубрики: AVR. Учебный курс Метки: Assembler, AVR, Программирование, Флаги Есть в AVR (да и, пожалуй, во всех остальных процессорах) особый регистр SREG. О нем я несколько раз упоминал в прошлых статьях, но не вдавался в подробности. Чтож, пришло время рассказать, что же это жеSREG такой и зачем он нужен. SREG это регистр состояния ядра. Он так называется Status Register. В этом регистре находится независимых битов — флажков. Которые могут быть либо 1 либо 0, в зависимости от выполненных в прошлом операций. И вот по тому какие флаги стоят, можно понять что произошло с процессором и что нам дальше делать. Например, если флаг Z (Zero) выставлен в 1, значит в ходе вычисления предыдущей математической операции в результате образовался ноль. А если выставлен флаг С (Carry — заем, перенос), то мы из меньшего числа отняли большее, или же прибавили такое число, что результат стал больше 255. А теперь подробней по каждому флагу.

  • I — флаг разрешения прерываний. Когда установлен в 1 — прерывания разрешены.
  • T — пользовательский флаг. Можно юзать по своему назначению.

Кроме того, есть две команды которые позволяют в этот бит записать любой бит любого из 32 регистров общего назначения R0-R31 (далее буду их звать РОН). Это команды BLD Rn,bit и BST Rn,bit

  • H — флаг полупереноса. Это если произошел заем бита из старшей половины байта в младшую. То есть когда из числа 0001 0111 пытаются вычести 0000 1000, то происходит заем бита из более старшего разряда, так как младшая тетрада уменьшаемого меньше чем младшей тетрады вычитаемого. Используется этот флаг в некоторых математических операциях.
  • S — флаг знака. 1 — значит минус. При вычислении чисел со знаком он возникает если после арифметической операции возник отрицательный результат. Флаг S = V XOR N.
  • V — Флаг переполнения дополнительного кода. Это если мы считаем число в дополнительном коде со знаком и оно вылезло за пределы регистра.

Число в дополнительном коде с заемом — самая естественная форма представления числа во вселенной! Вот возьмем, например, число -61 как его получить? Ну не знаем мы про отрицательные числа! Просто! Вычтем его из нуля 00 — 61 = 39 Во как! Заняли из старшего разряда! Не похоже, да? Хорошо, проверим столбиком: 61 + 39 – 00 А единичка не влезла в разрядность! ЧТД. (с) Лохов С.П. (Наш преподаватель по ассемблеру в универе) Вот двоичный доп код работает точно по такому же принципу. А в процессоре есть команды для перевода числа из в доп код за одну команду.

  • N — флаг отрицательного значения. Если в результате арифметической операции 7 бит результата стал 1, то этот флаг тоже станет 1.
  • Z — флаг нуля. Если в результате какой либо операции получился ноль, то вылазит этот флаг. Чертовски удобно для всех целей!
  • С — флаг переноса. Если в результате операции произошел выход за границы байта, то выставляется этот флаг. Вообще самый юзаемый флаг после Z и I. Что только с ним не творят, как только не юзают.

Флаги, кроме автоматической установки, можно устанавливать и сбрасывать вручную. Для этого есть команды SE* для установки и CL* для сброса. Вместо звездочки подставляется нужный флаг, например, CLI — запрет прерываний. В даташите, в разделе Instruction Set Summary, написано какая команда что делает и на какие флаги влияет. Пример:

1 2 3 4 5 INC Rd Increment Rd = Rd + 1 Z,N,V DEC Rd Decrement Rd = Rd − 1 Z,N,V TST Rd Test for Zero or Minus Rd = Rd AND Rd Z,N,V CLR Rd Clear Register Rd = Rd XOR Rd Z,N,V SER Rd Set Register Rd = $FF None

Команда INC прибавляет к регистру 1, но несмотря на то, что она может добить регистр до переполнения, флаг переполнения С она не ставит. Такая особенность. Зато она ставит флаги нуля, отрицательного значения и переполнения доп кода. Как инкремент может дать нуль? Элементарно, прибавив к -1 +1. Минус 1 в двоичной знаковой арифметике = FF. FF+1=1 00 ,но 1 не влезла в разрядность, поэтому 00 Логично же? 🙂 Или хотим мы, например, узнать в каком регистре число больше в R17 или R18 Нет ничего проще — к нашим услугам команда CP (нет, не детское порно, а Compare). Эта команда, у себя в уме, из R17 вычитает R18 при этом содержимое регистров не меняется, а меняются только флаги. И если, например, выскочил флаг С, значит при R17-R18 произошел заем, а значит R18 больше R17. Если флаг С не появился, то значит R17 больше чем R18. А коли у нас выскочил нуль, то значения равны. Есть еще команда CPI Rn,### работает также, но сравнивает регистр (только старшую группу) с произвольным числом. Используется чаще.

1 CP R17,R18

А потом смотрим флаги. И дальше в ход вступают команды условных переходов из группы BRANCH (ветвление). BR** И вот в этом месте товарищей из ATMEL надо хватать за ноги и бить головой об стену. Потому что я не понимаю на кой хрен было так все запутывать, изобретая команды которых реально нет? Суди сам. Флагов у нас 8, соответственно должно быть 16 возможных бранчей. 8 по условию — флаг есть, 8 по условию — флага нет. Бранчевых команд же у нас аж 20 штук.

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 BRBC # переход если бит # в регистре SREG=0 BRBS # переход если бит # в регистре SREG=1 BRCS переход если С=1 BRCC переход если С=0 BREQ переход если Z=1 BRNE переход если Z=0 BRSH переход если С=0 BRLO переход если С=1 BRMI переход если N=1 BRPL переход если N=0 BRGE переход если S=0 BRLT переход если S=1 BRHC переход если H=0 BRHS переход если H=1 BRTC переход если T=0 BRTS переход если T=1 BRVS переход если V=1 BRVC переход если V=0 BRID переход если I=0 BRIE переход если I=1
  • Если значение = 1, то делаем действие А
  • Если значение = 2, то делаем действие Б
  • А если значение меньше 13, то делаем действие ЦЭ
  • Во всех остальных случаях не делаем ничего
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 CPI R16,1 ; Сравниваем R16 с 1 BREQ ActionA ; Переход если равно (EQual, Z=1) ; Если не равно, то идем дальше CPI R16,2 ; Сравниваем R16 с 2 BREQ ActionB ; Переход если равно ; Если не равно, то идем дальше CPI R16,13 ; Сравниваем R16. т.е. R16-13 BRCS ActionC ; Если возник перенос, значит R16 меньше 13 ; Если не возник — идем дальше RJMP NoAction ; Переход на выход ActionA: NOP NOP ; Делаем наш экшн NOP RJMP NoAction ; Выходим, чтобы не влезть в ActionB ActionB: NOP NOP ; Делаем наш экшн NOP RJMP NoAction ; Выходим, чтобы не влезть в ActionC ActionC: NOP NOP ; Делаем наш экшн NOP NoAction: NOP ; Вместо NOP, разумеется, должны быть какие-нибудь полезные команды.

Сложно? Вот и я думаю, что делать нефиг :))))) Ничуть не сложней чем на Си забабахать из if then конструкцию или switch-case какой-нибудь. Бег по граблям У команд BR*** есть правда весьма ощутимое западло. Дальнобойность их составляет всего 63 команды. Т.е. это относительный переход. Поначалу ты этого не заметишь — короткая программа обычно не допускает переходов дальше 63. Но вот, со временем, твоя программа распухнет, появится дофига кода и дальности бранча перестанет хватать. Вроде бы все работало, а добавил пару команд — и компилятор начал ругаться злыми словами — Error out of range или что то в этом духе. Как быть? Перехватываться через промежуточные переходы. Т.е. находим ближайший к нам безусловный переход за который контроллер, как бы не старался залезть не сможет.

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 ActionA: NOP NOP NOP RJMP NoAction NOP ; То есть если я вот тут поставлю островок ; то он никогда не выполнится. Т.к. сверху ; Процессор перепрыгнет сразу по RJMP ; А снизу вход будет сразу же на ActionB ActionB: NOP NOP NOP RJMP NoAction И вот, в этом островке, мы создаем капсулу телепортер такого вида: ActionA: NOP NOP NOP RJMP NoAction ;———————————— Near: JMP FarFar_away ;———————————— ActionB: NOP NOP NOP RJMP NoAction

Т.е. бранчу теперь достаточно дострелить до островка Near, а там дальнобойный JMP зашлет его хоть в бутсектор на другой конец флеша. В случае большой программы, чтобы не плодить островки, его лучше сделать один, сразу после пачки CP. Test & Skip Кроме Branch‘ей есть еще один тип команд: проверка — пропуск. Работает она по принципу “Проверяем условие, если справедливо — пропускаем следующую команду” Запомнить просто, первая буква это Skip — пропуск. Вторая условие — С=Clear, т.е. 0 S=Set, т.е. 1. Соответственно S**C пропуск если сброшено. S**S пропуск если установлено. Команды SBRC/SBRS Указываешь ей какой РОН, и какой номер бита в этом регистре надо проверить. И если условие верное, то следующая команда будет пропущена. Пример:

1 2 3 4 5 6 7 8 9 10 11 12 SBRC R16,3 ; Если бит 3 в регистре R16 = 0, то прыжок через команду, на NOP RJMP bit_3_of_R16_Not_Zer0 NOP SBRS R16,3 ; Если бит 3 в регистре R16 = 1, то прыжок через команду, на NOP RJMP bit_3_of_R16_Zer0 NOP

Аналогичная команда SBIC/SBIS Но проверяет она не биты регистров РОН, а биты регистров периферийных устройств. Те самые,что идут в памяти между блоком РОН и оперативкой. Но есть у ней ограничение — она может проверить только первые 1F регистров ввода вывода. Если заглянешь в m16def.inc (для мега16, для других МК в их файл дефайнов) То увидишь там это:

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 .equ UBRRH = 0x20 .equ UCSRC = 0x20 .equ EEARL = 0x1e ( .equ EEARH = 0x1f

Вот все это богатство твое. Проверяй не хочу :)))) А мне мало! Я хочу дальше. Что делать. А дальше жизнь наша трудна и опасна. Чуть вправо,чуть влево — взрыв и ошметки. Страшно? На самом деле ничего такого. Просто нам придется заюзать битмаски. И будет это не в одну команду, а в три. Например, надо нам проверить состояние третьего бита в регистре UCSRC. Как видишь, это выше чем 1F Так что тест-скип не прокатит. Не беда, делай как я!

1 2 3 4 5 6 7 8 IN R16, UCSRC ; Хватаем байт из регистра, целиком. ANDI R16,1; Накладываем на него маску 00001000 ; В результате, все байты кроме третьего ; Обнулятся. А третий каким был таким и ; останется. Был нулем — будет нуль и будет Z ; Не был нулем, не будет Z флага. =)))) BREQ Bit_3_of_R16_Equal_Zero

Говорил же в три команды уложимся =) А можно проверять и не по одному биту сразу. Накладывай нужные маски, оставляй только нужные биты. А дальше смотри какие числа эти биты могут образовать и сравнивай напрямую с этими числами через CPI. Красота. Ладно, к битмаскам мы еще не раз вернемся. Это мощнейшее средство, но пока, не влезая в периферию, его не прочуешь. А до периферии мы еще не до росли. Всему свое время. В следующей статье я тебе буду взрывать мозг индексными табличными переходами. Сходи лучше воздухом подыши предварительно. Такие вещи надо раскуривать со свежими мозгами AVR. Учебный курс. Ветвления на индексных переходах Автор DI HALT Опубликовано 08 июля 2008 Рубрики: AVR. Учебный курс Метки: Assembler, AVR, Таблица переходов, Трюки Таблицы переходов Вот представь, что нам надо сделать мега CASE, когда на вход валится число от 1 до 100 и нам надо сто вариантов действий. Как будешь делать? Если лепить сто штук CPI с последующими переходами, то можно дальше убиться головой об стену. У тебя только эти CPI/BR** сожрут половину памяти кристалла. Учитывая, что каждая CPI это два байта, а каждый BR** еще байт. А о том сколько тактов эта шняга будет выполняться я даже не упоминаю. Делается это все круче. Помнишь я тебе рассказывал в прошлых уроках о таких командах как ICALL и IJMP. Нет, это не новомодная яблочная истерия, а индексный переход. Прикол в том, что переход (или вызов подпрограммы, не важно) осуществляется тут не по метке, а по адресу в регистре Z (о том что Z это пара R30:R31 я пожалуй больше напоминать не буду, пора бы запомнить). Итак, вначале пишем дофига вариантов наших действий. Те самые сто путей, у меня будет не сотня, а всего пять, но это не важно.

1 2 3 4 5 Way0: NOP Way1: NOP Way2: NOP Way3: NOP Way4: NOP

Дальше, где нибудь посреди кода мы херачим таблицу переходов.

1 Table: .dw Way0, Way1, Way2, Way3, Way4

Распологать ее можно где угодно. Хоть прямо тутже, хоть в конце кода. Главное, чтобы программа при исполнении не выполнила эту строку как команды, иначе будет ошибка. Для этого таблицы либо перепрыгивают с помощью команды RJMP

1 2 3 4 5 6 7 RJMP Kudato ; Программа сюда никогда не попадет. Table: .dw Way0, Way1, Way2, Way3, Way4 Kudato: NOP NOP

Или (предпочтительней) размещать в слепых тупиках кода, где выполнение не будет по алгоритму. Например между подпрограммами. Так:

1 2 3 4 5 6 7 8 9 10 . NOP RET ; Точка выхода ; Программа сюда никогда не попадет Table: .dw Way0, Way1, Way2, Way3, Way4 SomeProc: NOP ; Точка входа NOP .

Таблица переходов это всего лишь строка данных в памяти, содержащая адреса Way0…Way4. Обратите внимание на то, что данные у нас двубайтные слова dw. Если же мы хотим адресовать расположенные данные, например вложенную таблицу адресов переходов, то адреса нужно умножать на два. Вообще тут проще при написании по быстрому скомпилить кусок, открыть дамп с map файлом и посмотреть что лежит в памяти, куда что ссылается. Сразу станет понятно надо умножать на два или нет. Допустим у нас данные появляются в регистре R20 и нам, на основании числа там, нужно выбрать по какому пути переходить. Регистр R21 временный, под всякую хрень. А теперь делаем финт ушами.

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 LSL R20 ; Сдвигом влево умножаем содержимое R20 на два. ; Было, например, 011=3 стало 110=6 Круто да? 😉 ; опять же изза того, что у нас адреса двубайтные ; НЕ ПУТАТЬ С ТЕМ ЧТО КОМПИЛЕР ОБСЧИТЫВАЕТ ; ПАМЯТЬ В СЛОВАХ, тут несколько иное. Если непонятно ; то в комменты пишите, обьясню. LDI ZL, low(Table*2) ; Загружаем адрес нашей таблицы. Компилятор сам посчитает LDI ZH, High(Table*2) ; Умножение и запишет уже результат.Старший и младший байты. CLR R21 ; Сбрасываем регистр R21 — нам нужен ноль. ADD ZL, R20 ; Складываем младший байт адреса. Если возникнет переполнение ADC ZH, R21 ; То вылезет флаг переноса. Вторая команда складывает ; с учетом переноса. Поскольку у нас R21=0, то по сути ; мы прибавляем только флаг переноса. Вопросы — в комменты! ; Таким образом складываются многобайтные числа. ; Позже освещу тему ; Математики на ассемблере. LPM R20,Z+ ; Загрузили в R20 адрес из таблицы LPM R21,Z ; Старший и младший байт MOVW ZH:ZL,r21:r20 ; забросили адрес в Z /* Что это было? А все просто! У нас наше число образовало смещение по таблице переходов. Т.е. когда оно равно 0, то смещение тоже нулевое равное адресу Table, а значит выбирается адрес ячейки где лежит адрес на Way0. Следом за ней в памяти сразу же находится адрес Way1, Разница между ними два байта (так как сами адреса двубайтные). Таким образом, если в R20 будет число 1, то команда LSL умножит его на 2 и будет указан на адрес ячейки с Way1 и так далее. А что же дальше? А дальше, конечно же: */ IJMP ; Обана и переход на наш выбранный Way. Понравилось? ; То ли еще будет 🙂
  • Поднять резец
  • Опустить резец
  • Включить подачу
  • Включить привод
  • Подать на один шаг вперед
  • Подать на один шаг назад
  • Подать влево
  • Подать вправо.
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 BladeUP: NOP NOP NOP NOP RET BladeDN: NOP NOP NOP NOP RET DriveON: NOP NOP NOP NOP RET DriveOFF: NOP NOP NOP NOP RET Forward: NOP NOP NOP NOP RET Back: NOP NOP NOP NOP RET

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

1 Index: .dw BladeUP, BladeDN, DriveON, DriveOFF, Forward, Back

А в памяти ОЗУ заводится очередь задач. Обычный строковый массив нужной длины:

1 TaskList: .byte 30

Потом создается процедура которая извне загружает нашу очередь действий, в виде последовательности кодов этих действий (всего лишь смещение по таблице! Очень быстрое и компактное!) и вызывается диспетчер очереди. Диспетчер будет в цикле перебирать нашу строку TaskList, выковыривать из нее номера подпрограмм, по таблице, смещением, вычислять адреса переходов на реальные процедуры, переходить по ним, делать полезное действие, возвращаться обратно и брать новое значение из таблицы. Чуете куда я клоню? Получается своеобразная виртуальная машина. Процессор в процессоре. Более того, сами задачи, вызываемые из очереди, не обязательно должны быть тупо выполняющими конкретное действие. Они же тоже могут набрасывать в эту самую очередь новых кодов, сортировать и на ходу их переделывать. Получается мощный полиморфный алгоритм, который может сам менять свою логику исходя из условий. Позже, я на этом принципе покажу прообраз операционной системы. С диспетчером задач и кучей потоков, вызывающих друг друга по цепочке. А еще на индексных переходах можно делать очень компактные конечные автоматы. Причем даже адрес не надо будет вычислять т.к. в виде текущего состояния можно смело использовать прямые адреса других состояний автомата. Если не понял о чем идет речь, то не забивай пока голову. Дальше будет на это дело отдельная статья =) Или покури интернет на предмет того, что такое конечные автоматы и как они реализуются. AVR. Учебный Курс. Типовые конструкции Автор DI HALT Опубликовано 09 июля 2008 Рубрики: AVR. Учебный курс Метки: Assembler, Макро язык, Начинающим, Оптимизация, Трюки При написании программ постоянно приходится использовать разные стандартные конструкции вроде циклов, операторов выбора, перехода, сравнения. Всякие там if-then-else или case-switch. В высокоуровневых языках это все уже готово, а на ассемблере приходится изобретать каждый раз заново. Впрочем, такие вещи пишутся один раз, а дальше просто по наезженной тропинке применяются везде, где потребуется. Заодно и оптимизируются по ходу процесса. Условие if-then-else Тут проще всего методом последовательной проверки проложить маршрут до нужного блока кода. Приведу пример:

1 2 3 4 5 6 7 8 9 if (А>=B) < action_a >else < action_b >next_action

Как это проще всего сделать на ассемблере? Считаем, что А в R16,B в R17, а полезные действия, которые могли бы быть там, заменяем на NOP,NOP,NOP. Сравнение делается путем вычитания. По итогам вычитания выставляются флаги. Заем (когда A стало меньше B) означает, что условие не выполнилось.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 CP R16,R17 ; Сравниваем два значения BRCS action_b ; когда A>=B флага С не будет ; Перехода не произойдет action_a: NOP ; и выполнится действие А NOP NOP RJMP next_action ; Но чтобы действие B не произошло ; в ходе естественного выполнения ; кода — его надо перепрыгнуть. action_b: NOP ; Действие B NOP NOP next_action: NOP NOP NOP

Но надо учитывать тот момент, что в случае А=B флаг С не вылезет, зато будет флаг Z. Но переход то у нас исходя из какого-нибудь одного флага (по С=0 или по С=1). И, исходя из выбора команды для построения конструкции if-then-else (BRCC или BRCS), будет разная трактовка результата условия if (А>=B) в случае А=B. В одном случае (нашем), где переход идет на else по С=1, А=B будет эквивалентно А>B — флага C не будет. И в том и другом случае услове if (А>=B) даст True и переход по BRCS на then. Если перестроить конструкцию наизнанку, через команду BRCC то условия (А>=B) уже не получится. При A=B не будет флага С и произойдет переход по BRCC на else. Для создания строгих неравенств нужна проверка на ноль. Теперь А больше B:

1 2 3 4 5 6 7 8 9 if (А>B) < action_a >else < action_b >next_action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CP R16,R17 ; Сравниваем два значения BREQ action_b ; Если равно (флаг Z), то переход сразу. ; Потом проверяем второе условие. BRCS action_b ; когда A>B флага С не будет ; Перехода не произойдет action_a: NOP ; и выполнится действие А NOP NOP RJMP next_action ; Но чтобы действие B не произошло ; в ходе естественного выполнения ; кода — его надо перепрыгнуть. action_b: NOP ; Действие B NOP NOP next_action: NOP NOP NOP

Сложно? Думаю нет. Усложним еще раз наше условие:

1 2 3 4 5 6 7 8 9 if (C else < action_b >next_action

Считаем, что A=R16, B=R17, С=R18

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 ;IF CP R16,R18 ; Сравниваем С и А BREQ action_b ; Переход сразу на ELSE если С=А BRCS action_b ; Если А оказалось меньше, то будет С ; и сразу выходим отсюда на else ; Но если C ; Второе условие (A CP R16,R17 ; Сравниваем два значения ; BREQ action_b ; Переход на ELSE если B=А. Команда BREQ специально ; закомментирована — она тут не нужна. Я ее поставил ; для наглядности. Ведь в случае А=B ; флага С не будет и однозначно будет переход по BRCC. BRCC action_b ; Когда A>B флага С не будет ; А переход по условию С clear сработает. ;THEN action_a: NOP ; Выполнится действие А NOP NOP RJMP next_action ; Но чтобы действие B не произошло ; в ходе естественного выполнения ; кода — его надо перепрыгнуть. ;ELSE action_b: NOP ; Действие B NOP NOP next_action: NOP NOP NOP

Просто же! Вот так вот, комбинируя условия, можно размотать любую логическую конструкцию. Битовые операции Но, пожалуй, самые распространенные операции в контроллере — битовые. Включить выключить какой-нибудь параметр, инвертировать его, проверить есть или нет. Да масса случаев где это нужно. Основная масса операций идет через битовые маски. Есть замечательные команды SBI и CBI первая ставит указанный бит в порту, вторая сбрасывает. Например: CBI PORT,7 — обнулить 7й бит в регистре ввода-вывода PORT SBI PORT,6 — выставить 6й бит в регистре ввода-вывода PORT Все замечательно, но работают эти команды только в пределах первых 31 адресов в пространстве регистров ввода вывода. Для мега16 это:

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 .equ EEARH = 0x1f .equ EEDR = 0x1d .equ EECR = 0x1c .equ PORTA = 0x1b .equ DDRA = 0x1a .equ PINA = 0x19 .equ PORTB = 0x18 .equ DDRB = 0x17 .equ PINB = 0x16 .equ PORTC = 0x15 .equ DDRC = 0x14 .equ PINC = 0x13 .equ PORTD = 0x12 .equ DDRD = 0x11 .equ PIND = 0x10 .equ SPDR = 0x0f .equ SPSR = 0x0e .equ SPCR = 0x0d .equ UDR = 0x0c .equ UCSRA = 0x0b .equ UCSRB = 0x0a .equ UBRRL = 0x09 .equ ACSR = 0x08 .equ ADMUX = 0x07 .equ ADCSRA = 0x06 .equ ADCH = 0x05 .equ ADCL = 0x04 .equ TWDR = 0x03 .equ TWAR = 0x02 .equ TWSR = 0x01 .equ TWBR = 0x00

И ни байтом дальше. А с более старшими адресами — облом. Ну ничего, что нам мешает взять и записать в регистр ввода-вывода (далее РВВ, а то я уже задолбался). Этот бит самому? Только то, что доступ там к порту идет не побитный, а сразу целыми байтами. Казалось бы, в чем проблема? Надо выставить биты 1 и 3 в регистре TWCR, например, взял да записал двоичное число 00001010 и выставил.

1 LDI R16,1
  • Взять старое значение
  • Подправить в нем биты
  • Записать обратно
1 IN R16,TWCR

А установить нужные биты нам поможет битовая маска и операция OR. В результате этой операции, там где в битовой маске были нули будут те же значения, что и раньше. А где в маске 1 возникнут единички.

1 ORI R16,1; Битовая маска 00001010

А затем записать уже измененный байт обратно

1 OUT TWCR,R16

Сброс битов тоже можно сделать через битовую маску, но уже операция AND. И там где мы хотим сбросить нам надо поставить в маске 0, а где не хотим менять 1 Удобней делать инверсную маску. Для инверсии маски применяется операция побитового НЕ ~ Выглядеть будет так:

1 2 3 IN R16,TWCR ANDI R16,~(1<<3|1<<1) OUT TWCR,R16

Обрати внимание, для сброса у нас внутри конструкции используется 1. Ведь байт то потом инвертируется! Для инверсии битов применяется маска по XOR. Нули в этой маске не меняют биты, а единичка по XOR бит инвертирует. Смотри сам, вверху произвольное число, внизу маска:

1 2 3 4 1100 1100 XOR 1111 0000 _________ 0011 1100

Правда есть одно ограничение — нет команды XOR маски по числу, поэтому через регистр.

1 2 3 4 IN R17,TWCR LDI R16, 3; Маска EOR R17,R16 ; Ксорим OUT TWCR,R17 ; Сгружаем обратно

Можно увязать всю ботву в макросы и тогда будет совсем хорошо 🙂 Единственно, при использовании макросов, особенно когда они содержат внутри промежуточные регистры, нельзя забывать про эти регистры. А то в ходе исполнения макроса содержимое этих регистров меняется и если про это забыть можно получить кучу глюков 🙂 Но от такой напасти можно и защититься, например стеком. К примеру, изобретем мы свою команду XRI — XOR регистра с числом. Макрос может быть таким:

1 2 3 4 .MACRO XRI LDI @2,@1 EOR @0,@2 .ENDM

А вызов макроса

1 XRI R16,mask,R17

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

1 2 3 4 5 6 .MACRO XRI PUSH R16 LDI R16,@1 EOR @0,R16 POP R16 .ENDM

Вызов, в этом случае

1 XRI R17,Mask

Но нельзя будет поксорить R16, т.к. его значение все равно затрет при выгрузке из стека. Также масками можно смело отрезать неиспользованные биты. Например, в регистре TWSR первые два бита это настройки, а остальные — код состояние интерфейса TWI. Так зачем нам эти два бита настройки каждый раз учитывать? Отдавили их по AND через маску 11111100 да дело в шляпе. Или сдвинули вправо. Сдвиги Ну тут просто — можно двигать биты в РОН влево и вправо. Причем сдвиг бывает двух типов. LSR Rn — логический вправо, при этом слева в байт лезут нули. LSL Rn — логический влево, при этом справа в байт лезут нули. и через перенос. ROL и ROR При этом байт уходящий за край попадает в флаг С, а флаг С вылезает с другого края байта. Получается как бы 9 битная циклическая карусель. Где одна команда дает один шаг влево или вправо. Как применить? Ну способы разные, например, посчитать число единиц в байте. Смотри как просто:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CLR R18 ; Сюда будем считать единички. Сбросим пока LDI R17,9 ; Счетчиком циклов будет LDI R16,0xAA ; В этом байте будем считать единички CLC ; Сбросим флаг С, чтобы не мешался. Loop: DEC R17 ; уменьшим счетчик BREQ End ; если весь байт дотикали — выход. ROL R16 ; Сдвигаем байт BRCC Loop ; если нет 1 в С, еще одну итерацию INC R18 ; Сосчтитали единичку RJMP Loop End: NOP

Еще через флаг С удобно проверять крайние биты, по быстрому, чтобы не заморачиваться с масками (хотя выигрыша ни по времени, ни по коду не будет).

1 2 ROL R16 BRCS Label ; Переход если бит 7 в R16 есть.
1 2 ROR R16 BRCS Laber ; Переход если бит 0 в R16 есть.

Циклы Ну тут все просто — либо сначала, либо потом условие, а затем тело цикла и все окольцовано переходами. Например, выполнить цикл 20 раз.

1 2 3 4 5 6 7 8 9 10 LDI R17,20 ; Счетный регистр Loop: NOP NOP NOP NOP NOP DEC R17 ; Уменьшаем счетчик BRNE Loop

; Переход если не ноль (нет флага Z) Главное следить за тем, чтобы счетный регистр нигде не запоролся в теле цикла. Иначе получим глюк. Такой глюк, конечно, легко вылавливается трассировкой, но далеко не факт, что он всплывает сразу. Еще один пример цикла — тупые задержки. Почему я их называю тупыми? Да потому, что они сами ничего не делают и другим не дают. Крайне не рекомендую их использовать, но для простеньких программ подойдет. Основная идея такой задержки загрузить контроллер бессмысленной работой, чтобы нащелкать как можно больше тактов. У каждого такта есть своя длительность, так что длительность задержки можно формировать от микросекунд, до десятков лет. Вот решение в лоб:

1 2 3 LDI R16, Delay Loop: DEC R16 BRNE loop

Процессор потратит примерно Delay*2 тактов на бессмысленные операции. При длительности такта (на 8мгц) 1.25Е-7 секунд максимальная выдержка будет 6.4E-5 секунды. Немного, но, например, хватит на то чтобы дисплей прожевал отданый ему байт и не подавился следующим. Если надо больше задержки, то делаются вложенные циклы с несколькими счетчиками. Тогда суммарная длительность выдержки перемножается и уже на трех вложенных циклах можно получить около 2 секунд. Ну, а на четырех уже 536 секунд. Но есть более красивое решение, нежели вложенные циклы — вычитание многобайтного числа с переносом.

1 2 3 4 5 6 7 8 9 LDI R16,LowByte ; Грузим три байта LDI R17,MidleByte ; Нашей выдержки LDI R18,HighByte loop: SUBI R16,1 ; Вычитаем 1 SBCI R17,0 ; Вычитаем только С SBCI R18,0 ; Вычитаем только С BRCC Loop ; Если нет переноса — переход.
  • Делаем раз
  • Делаем два
  • Делаем три
  • Тупим
  • Делаем то, ради чего тупили
  • Делаем четыре
  • Делай раз
  • Делай два
  • Делай три
  • Тик!
  • Натикало? -да- Делаем то, ради чего тупили сбрасываем счетчик.
  • Делай четыре
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 .DSEG Counter: .byte 3 ; Наш счетчик циклов. Три байта. .CSEG MainLoop: Do_One: NOP Do_Two: NOP Do_Three: NOP LDS R16,Counter ; Грузим наш счетчик LDS R17,Counter+1 LDS R18,Counter+2 SUBI R16,1 ; Вычитаем 1 SBCI R17,0 ; Вычитаем только С SBCI R18,0 ; Вычитаем только С BRCC DoNothing ; Не натикало? Переход ; Натикало! YES: NOP ; Делаем то, ради чего тупили NOP NOP LDI R16,LowByte ; Грузим три байта LDI R17,MidleByte ; Нашей выдержки LDI R18,HighByte DoNothing: STS Counter,R16 ; Сохраняем обратно в память STS Counter+1,R17 STS Counter+2,R18 RJMP MainLoop

Обрати внимание, что другие операции Do_one, Do_Two, Do_Three не ждут когда натикают, они выполняются в каждую итерацию. А операция требующая задержку ее получает, не тормозя всю программу! И таких таймеров можно налепить сколько угодно, пока оперативка не кончится. Более того, такой цикловой счетчик сам может быть алгоритмозадающим механизмом. Счетчик тикает каждый прогон главного цикла, а каждая операция в этом главном цикле ждет именно своего “номера” в главном счетчике. Получается этакая программная “шарманка”, где в качестве барабана с гвоздями выступает наш счетчик. Но это уже конечные автоматы. К ним я, возможно, вернусь позже. Еще не решил стоит освещать эту тему. А то могу и увлечься. люблю я их 🙂 Горыныч Писал я тут библиотечку для подключения LCD к AVR. А чтобы не вкуривать в команды контроллера дисплея я распотрошил Сишный код, дабы подглядеть какими байтами и в каком порядке надо кормить контроллер, чтобы он вышел на нужный режим. Попутно сварганил один прикольный трюк, который я называю Горыныч. Это когда мы несколько функций объединяем в одну многоголовую — с несколькими точками входа и одной точкой выхода. Итак, мы имеем две функции обращения к LCD — запись данных и запись команд. О! Вот эти ребята:

1 2 3 4 5 6 7 8 9 10 11 12 13 CMD_WR: CLI RCALL BusyWait CBI CMD_PORT,RS CBI CMD_PORT,RW SBI CMD_PORT,E LCD_PORT_OUT OUT DATA_PORT,R17 RCALL LCD_Delay CBI CMD_PORT,E LCD_PORT_IN SEI RET

Нененененене Девид Блейн.

1 2 3 4 5 6 7 8 9 10 11 12 13 DATA_WR: CLI RCALL BusyWait SBI CMD_PORT,RS CBI CMD_PORT,RW SBI CMD_PORT,E LCD_PORT_OUT OUT DATA_PORT,R17 RCALL LCD_Delay CBI CMD_PORT,E LCD_PORT_IN SEI RET

Сейчас я вам покажу особую, оптимизаторскую, магию!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 CMD_WR: CLI RCALL BusyWait CBI CMD_PORT,RS RJMP WR_END DATA_WR: CLI RCALL BusyWait SBI CMD_PORT,RS WR_END: CBI CMD_PORT,RW SBI CMD_PORT,E LCD_PORT_OUT OUT DATA_PORT,R17 RCALL LCD_Delay CBI CMD_PORT,E LCD_PORT_IN SEI RET

Ты что наделал Блейн. Ты зачем функцию скукожил. Нука раскукожь ее обратно! У меня теперь функция беби сайз. Ну, а теперь, для сравнения, поглядим как это сделано в LCD.c весь исходник я сюда копировать не буду, только то, что сделано у меня, ну и всю условную компиляцию я тоже выкину.

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 void lcdControlWrite(u08 data) < lcdBusyWait(); cbi(LCD_CTRL_PORT, LCD_CTRL_RS); cbi(LCD_CTRL_PORT, LCD_CTRL_RW); sbi(LCD_CTRL_PORT, LCD_CTRL_E); outb(LCD_DATA_DDR, 0xFF); outb(LCD_DATA_POUT, data); LCD_DELAY; LCD_DELAY; cbi(LCD_CTRL_PORT, LCD_CTRL_E); outb(LCD_DATA_DDR, 0x00); outb(LCD_DATA_POUT, 0xFF); >void lcdDataWrite(u08 data)

Собственно, строчка в строчку — те же яйца только в профиль. Что подтверждает, сказанный мной в первом посте, тезис, что не важно на каком языке писать, на си или на ассемблере — главное знать что в какие порты пихать. Но на асме я могу свернуть свою прогу почти вдвое, не потеряв ни байта и ни такта, а на сях придется для этого вводить внутреннию функцию, которая захавает оперативку на стек, потребует кучу тактов на переходы и вообще будет выглядеть убого. И финальная процедура инициализации на сях и на макроасме:

1 2 3 4 5 6 7 8 9 10 void lcdInit()

Ну и у меня:

1 2 3 4 5 6 7 8 .MACRO INIT_LCD RCALL InitHW WR_CMD 0x38 WR_CMD 0x01 WR_CMD 0x06 WR_CMD 0x0F WR_CMD 0x02 .ENDM

Конструкции вида (1<без снижения читабельности. Конечно что либо крупное, вроде того же USB я на Асме делать не буду, тут проще взять готовый код. Хотя… чем черт не шутит, может заморочусь и напишу свой крошечный USB драйвер. AVR. Учебный курс. Стартовая инициализация Автор DI HALT Опубликовано 09 июля 2008 Рубрики: AVR. Учебный курс Метки: Assembler, Мелочи, Программирование Инициализация памяти Мало кто подозревает о том, что при включении в оперативке далеко не всегда все байты равны 0xFF. Они могут, но не обязаны. Равно как и регистры РОН не всегда равны нулю при запуске. Обычно да, все обнулено, но я несколько раз сталкивался со случаями когда после перезапуска и/или включения-выключения питания, микроконтроллер начинал творить не пойми что. Особнно часто возникает когда питание выключаешь, а потом, спустя некоторое время, пара минут, не больше, включаешь. А всему виной остаточные значения в регистрах. Итак, возьмите себе за правило после каждого включения, в разделе инициализации, еще даже до инициализации стека, делать зануление памяти и очистку всех регистров. Разумеется делается это все в цикле. Вот примерный вариант кода:

1 2 3 4 5 6 7 8 9 10 11 12 RAM_Flush: LDI ZL,Low(SRAM_START) ; Адрес начала ОЗУ в индекс LDI ZH,High(SRAM_START) CLR R16 ; Очищаем R16 Flush: ST Z+,R16 ; Сохраняем 0 в ячейку памяти CPI ZH,High(RAMEND+1) ; Достигли конца оперативки? BRNE Flush ; Нет? Крутимся дальше! CPI ZL,Low(RAMEND+1) ; А младший байт достиг конца? BRNE Flush CLR ZL ; Очищаем индекс CLR ZH

Поскольку адрес оперативки у нас двубайтный, то мы вначале смотрим, чтобы старший байт совпал с концом, а потом добиваем оставшиеся 255 байт в младшем байте адреса. Далее убиваем все регистры от первого до последнего. Все, контроллер готов к работе.

1 2 3 4 5 LDI ZL, 30 ; Адрес самого старшего регистра CLR ZH ; А тут у нас будет ноль DEC ZL ; Уменьшая адрес ST Z, ZH ; Записываем в регистр 0 BRNE PC-2 ; Пока не перебрали все не успокоились

За процедурку зануления регистров спасибо Testicq Либо значения сразу же инициализируются нужными величинами. Но, обычно, я от нуля всегда пляшу. Поэтому зануляю все. З.Ы. Кстати, о оперативке. Нашел я недавно планку оперативной памяти на 1килобайт, древнюю как говно мамонта, еще на ферромагнитных кольцах. AVR. Учебный курс. Скелет программы Автор DI HALT Опубликовано 10 июля 2008 Рубрики: AVR. Учебный курс Метки: Assembler, AVR, Макро язык, Программирование, Трюки, Флаги При написании прошивки надо очень внимательно подходить к процессу организации архитектуры будущей программы. Программа должна быть быстрой, не допускать задержек главного цикла и легко расширяться. Оптимально использовать аппаратные ресурсы и стараться выжать максимум возможного из имеющихся ресурсов. Вообще, архитектура программ это отдельная тема и ближе к концу курса, в его Сишной части я подробней рассказываю о разных типах организации прошивки. Можешь забежать вперед и поглядеть, что да как. В ассемблерной же части, я расскажу о одном из самых простых вариантов — флаговом автомате, а позже, когда ты уже будешь вовсю ориентироваться в моем коде, дам пример на основе конвейерного диспетчера, с подробным описанием его работы. Суперцикл Все программы на микроконтроллерах обычно зацикленные. Т.е. у нас есть какой то главный цикл, который вращается непрерывно. Структура же программы при этом следующая:

  • Макросы и макроопредения
  • Сегмент ОЗУ
  • Точка входа — ORG 0000
  • Таблица векторов — и вектора, ведущие в секцию обработчиков прерываний
  • Обработчики прерываний — тела обработчиков, возврат отсюда только по RETI
  • Инициализация памяти — а вот уже отсюда начинается активная часть программы
  • Инициализация стека
  • Инициализация внутренней периферии — программирование и запуск в работу всяких таймеров, интерфейсов, выставление портов ввода-вывода в нужные уровни. Разрешение прерываний.
  • Инициализация внешней периферии — инициализация дисплеев, внешней памяти, разных аппаратных примочек, что подключены к микроконтроллеру извне.
  • Запуск фоновых процессов — процессы работающие непрерывно, вне зависимости от условий. Такие как сканирование клавиатуры, обновление экрана и так далее.
  • Главный цикл — тут уже идет вся управляющая логика программы.
  • Сегмент ЕЕПРОМ

Начинается все с макросов, их пока не много, если что по ходу добавим.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .include «m16def.inc» ; Используем ATMega16 ;= Start macro.inc ======================================== .macro OUTI LDI R16,@1 .if @0 < 0x40 OUT @0,R16 .else STS @0,R16 .endif .endm .macro UOUT .if @0 < 0x40 OUT @0,@1 .else STS @0,@1 .endif .endm ;= End macro.inc =======================================

В оперативке пока ничего не размечаем. Нечего.

1 2 3 ; RAM =================================================== .DSEG ; END RAM ===============================================

С точкой входа и таблицей векторов все понятно, следуя нашему давнему шаблону, берем его оттуда:

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 ; FLASH ====================================================== .CSEG .ORG $000 ; (RESET) RJMP Reset .ORG $002 RETI ; (INT0) External Interrupt Request 0 .ORG $004 RETI ; (INT1) External Interrupt Request 1 .ORG $006 RETI ; (TIMER2 COMP) Timer/Counter2 Compare Match .ORG $008 RETI ; (TIMER2 OVF) Timer/Counter2 Overflow .ORG $00A RETI ; (TIMER1 CAPT) Timer/Counter1 Capture Event .ORG $00C RETI ; (TIMER1 COMPA) Timer/Counter1 Compare Match A .ORG $00E RETI ; (TIMER1 COMPB) Timer/Counter1 Compare Match B .ORG $010 RETI ; (TIMER1 OVF) Timer/Counter1 Overflow .ORG $012 RETI ; (TIMER0 OVF) Timer/Counter0 Overflow .ORG $014 RETI ; (SPI,STC) Serial Transfer Complete .ORG $016 RETI ; (USART,RXC) USART, Rx Complete .ORG $018 RETI ; (USART,UDRE) USART Data Register Empty .ORG $01A RETI ; (USART,TXC) USART, Tx Complete .ORG $01C RETI ; (ADC) ADC Conversion Complete .ORG $01E RETI ; (EE_RDY) EEPROM Ready .ORG $020 RETI ; (ANA_COMP) Analog Comparator .ORG $022 RETI ; (TWI) 2-wire Serial Interface .ORG $024 RETI ; (INT2) External Interrupt Request 2 .ORG $026 RETI ; (TIMER0 COMP) Timer/Counter0 Compare Match .ORG $028 RETI ; (SPM_RDY) Store Program Memory Ready .ORG INT_VECTORS_SIZE ; Конец таблицы прерываний

Обработчики пока тоже пусты, но потом добавим

1 2 ; Interrupts ============================================== ; End Interrupts ==========================================

Инициализация ядра. Память, стек, регистры:

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 Reset: LDI R16,Low(RAMEND) ; Инициализация стека OUT SPL,R16 ; Обязательно. LDI R16,High(RAMEND) OUT SPH,R16 ; Start coreinit.inc RAM_Flush: LDI ZL,Low(SRAM_START) ; Адрес начала ОЗУ в индекс LDI ZH,High(SRAM_START) CLR R16 ; Очищаем R16 Flush: ST Z+,R16 ; Сохраняем 0 в ячейку памяти CPI ZH,High(RAMEND) ; Достигли конца оперативки? BRNE Flush ; Нет? Крутимся дальше! CPI ZL,Low(RAMEND) ; А младший байт достиг конца? BRNE Flush CLR ZL ; Очищаем индекс CLR ZH CLR R0 CLR R1 CLR R2 CLR R3 CLR R4 CLR R5 CLR R6 CLR R7 CLR R8 CLR R9 CLR R10 CLR R11 CLR R12 CLR R13 CLR R14 CLR R15 CLR R16 CLR R17 CLR R18 CLR R19 CLR R20 CLR R21 CLR R22 CLR R23 CLR R24 CLR R25 CLR R26 CLR R27 CLR R28 CLR R29 ; End coreinit.inc

Всю эту портянку можно и нужно спрятать в inc файл и больше не трогать. Секции внешней и внутренней инициализации переферии пока пусты, но ненадолго. Равно как и запуск фоновых программ. Потом я просто буду говорить, что мол добавьте эту ботву в секцию Internal Hardware Init и все 🙂

1 2 3 4 5 6 7 8 9 10 11 ; Internal Hardware Init ====================================== ; End Internal Hardware Init =================================== ; External Hardware Init ====================================== ; End Internal Hardware Init =================================== ; Run ========================================================== ; End Run ======================================================

А теперь, собственно, сам главный цикл.

1 2 3 4 5 ; Main ========================================================= Main: JMP Main ; End Main =====================================================

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

1 2 3 ; Procedure ==================================================== ; End Procedure ================================================

Ну и вот тебе файлик с уже готовым проектом под этот шаблон AVR. Учебный курс. Операционная система. Пример. Автор DI HALT Опубликовано 10 июля 2008 Рубрики: AVR. Учебный курс Метки: Assembler, AVR, RTOS, Программирование, Трюки Отлично, с теорией работы ОС ознакомил. Устанавливать научил, осталось научить использовать весь этот конвеерно таймерный шухер. Чем я сейчас и займусь. Сразу берем быка за рога и формулируем учебно-боевую программу. Тестовое задание: Пусть у нас будет ATMega8, с несколькими кнопками. АЦП и подключеним к компу через UART. На меге будет три светодиода.

  • Девайс должен при включении начинать мигать зеленым диодом, мол работаю.
  • При этом раз в секунду сканировать показания АЦП и если показания ниже порога — Моргать красным диодом.
  • По сигналу с UARТ с целью защиты от ошибок сделать по байту ‘R’ установку флага готовности, а потом, в течении 10ms если не придет байт ‘A’ сбросить флаг готовности и игнорировать все входящие байты кроме ‘R’. Если ‘A’ придет в течении 10мс после ‘R’, то отправить в UART ответ и зажечь белый диод на 1 секунду.

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

  • Мигать зеленым диодом
  • Проверять показания с АЦП

И цепочки:

  • Прерывание АЦП — Проверка условия — зажечь диод — погасить диод.
  • Прерывание UART- Проверить на R — взвести флаг (ждать ‘А’ — зажеть диод — погасить диод) — снять флаг

Выполнение команд ветвления и перехода

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

Команды ветвления дают возможность изменять ход выполнения программы в зависимости от выполнения или невыполнения определенных условий.

Рассмотрим, как выполняются команды перехода на примере микроконтроллеров семейства AVR (рис. 4).

Выборка команды РС=РС+1

Декодирование команды

Выборка адреса перехода РС=РС+1

Выполнение команды РС=А

Рис. 4. Пример выполнения команды перехода

Общий принцип выполнения команд уже описан, поэтому остановимся только на особенностях команд ветвления и перехода.

В составе команд микроконтроллеров семейства AVR имеются команды ветвления (или условного перехода) и команды безусловного перехода.

Команды безусловного перехода всегда изменяют значение регистра PC на заданное, начиная выполнение команды с указанного нового адреса.

На рис. 4 приведен пример выполнения команды безусловного перехода JMP по адресу 6. Как видим, единственное отличие от уже описанного способа выполнения команд (см. рис. 3) состоит в том, что па этапе выполнения команды изменяется значение регистра-счетчика команд PC.

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

Более подробно о командах ветвления микроконтроллеров семейства AVR можно узнать в [1,2].

Выполнение команд вызова подпрограмм

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

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

Стек — это структура памяти, работающая по принципу «последний пришел — первый ушел». Для организации стека возвратов используется регистр-указатель стека SR

Имеются также специальные машинные команды работы со стеком — PUSH и POP.

Команда PUSH — поместить содержимое указанного регистра на стек. В процессе выполнения данной команды значение регистра-указателя стека SP уменьшается на единицу. Затем содержимое указанного регистра помещается в память данных по адресу, значение которого хранится в SP.

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

Рассмотрим, как происходит вызов подпрограммы (команда CALL) и возврат из подпрограммы (команда RET).

Команда CALL в качестве операнда использует абсолютный адрес подпрограммы, имеющий размер 16 бит (2 байта). К началу этапа выполнения команды CALL регистр PC содержит адрес следующей выполняемой команды, т. е. адрес возврата из вызываемой подпрограммы (см. рис. 3). Команда CALL во время этапа выполнения производит следующие действия:

  • • уменьшает значение регистра-указателя стека SP на 2, т. к. регистр PC имеет размер 2 байта;
  • • помещает на стек содержимое регистра PC (адреса возврата подпрограммы);
  • • заносит в регистр PC адрес подпрограммы (полностью аналогично команде JMP).

Следующая команда, которая будет выполнена за командой вызова подпрограммы, будет первая команда вызываемой подпрограммы.

Кроме команды CALL, в микроконтроллерах семейства AVR имеются и другие команды вызова подпрограмм — ICALL и RCALL. Эти команды отличаются от команды CALL только способом вычисления адреса подпрограммы:

  • • команда CALL использует в качестве адреса подпрограммы константу, размером 2 байта. При этом размер команды CALL — 4 байта: 2 байта — КОП и 2 байта — адрес подпрограммы;
  • • команда RCALL использует в качестве адреса подпрограммы константу, размером 12 бит, входящую в КОП. Адрес вызова подпрограммы вычисляется путем сложения содержимого регистра PC с 12-битной константой, интерпретируемой как знаковое целое число. Такая команда позволяет осуществить вызов подпрограмм в пределах от +2047 до -2048 слов относительно адреса расположения команды вызова. При этом размер команды RCALL — 2 байта;
  • • команда ICALL использует в качестве адреса подпрограммы содержимое регистра Z (т. е. пары регистров R30:R31). Данная команда удобна при наличии таблицы подпрограмм, когда адрес вызываемой подпрограммы вычисляется, а не задается константой.

Команда RET не имеет аргументов и, во время этапа выполнения, производит следующие действия:

  • • снимает со стека адрес возврата из подпрограммы (2 байта) и заносит этот адрес в регистр PC;
  • • увеличивает значение регистра-указателя стека SP на 2.

По сути, команда возврата из подпрограммы RET осуществляет передачу управления по адресу, который хранился на вершине стека к моменту выполнения команды RET.

Кроме команды RET, в микроконтроллерах семейства AVR имеется команда RETI, используемая для возврата из процедуры обработки прерывания. Отличие команды RETI от команды RET состоит в том, что команда RETI во время выполнения устанавливает глобальный флаг разрешения прерываний. В остальном команды RET и RETI полностью аналогичны. Подробнее о прерываниях рассказано в соответствующем разделе.

Следует помнить, что если мы выполним команду RET, не вызвав предварительно подпрограмму командой CALL, то произойдет переход по произвольному адресу и программа может дать сбой.

Кроме того, если мы используем команды PUSH и POP для сохранения на стеке или извлечения со стека данных — то непарное использование этих команд в подпрограмме может привести к неверной работе команды RET.

Поэтому следует помнить несколько правил обращения к подпрограммам в машинном коде, помогающих избегать краха программы из-за повреждения содержимого стека:

  • • когда вы используете подпрограмму, то для ее вызова следует использовать только команды CALL, ICALL или RCALL;
  • • возврат из подпрограммы выполняется командой RET;
  • • внутри подпрограммы (т. е. от точки входа в подпрограмму до команды RET) количество команд PUSH должно быть равно количеству команд POP;
  • • во время выполнения подпрограммы количество команд PUSH ни в какой момент времени не должно быть меньше количества команд POP.

Команды переходов

Команды группы переходов можно подразделить на 3 подгруппы. Первая подгруппа — безусловные переходы. Список команд этой подгруппы приведён в табл. 7.7.

Команды безусловных переходов

Переход по адресу

Переход по @A+DPIR

Вызов подпрограммы по адресу

Возврат из подпрограммы

Возврат из прерывания

Под одним мнемоническим обозначением JMP скрыты три команды перехода: SJMP, LJMP и AJMP, которые отличаются форматом адреса перехода. JMP является общим обозначением, применяемым в тех случаях, когда программисту безразлично, какую команду перехода использовать.

Для короткого перехода (Short JMP, Short — короткий) адрес перехода определяется смещением, в качестве которого выступает второй байт команды. Этот байт интерпретируется как число со знаком, что обеспечивает переход на величину от -128 до +127 байт относительно адреса команды, следующей за инструкцией S JMP.

Инструкция длинного перехода L JMP (Long JMP, Long — длинный) имеет длину в три байта, причём два последних байта представляют адрес. Эта команда дает возможность перехода к любому адресу из 64 Кбайт памяти программ.

Команда среднего перехода (Average JMP, Average — средний) является двухбайтной. Первый байт помимо кода операции содержит 3 бита адреса перехода, а второй байт — остальные 8 бит адреса перехода. При выполнении команды AJMP эти 11 бит подставляются в 11 младших разрядов PC, а старшие 5 бит остаются без изменений. Следовательно, переход возможен в пределах блока в 2 Кбайт, в котором расположена команда AJMP.

Во всех случаях программисту следует описывать адрес перехода или в виде метки, или в виде 16-битной константы. Ассемблер сам подставит корректный формат адреса.

Инструкция JMP @A+DPTR предназначена для выполнения переходов по таблице. В момент выполнения команды адрес перехода вычисляется как сумма содержимого шестнадцатибитного DPTR и содержимого аккумулятора. Обычно DPTR устанавливается на адрес таблицы переходов, а в аккумуляторе находится смещение (индекс) относительно начала таблицы. Практический пример использования этой команды приведен ниже:

MOV DPTR, #JUMP_TABLE

MOV A, INDEX_NUMBER

RL А /обеспечивается двухбайтовый интервал

;при последовательных значениях номера /индекса (0,1,2,3. )

По аналогии с командой JMP команда CALL addr имеет две формы: короткую и среднюю: LCALL и ACALL. Все замечания относительно форматов JMP верны и для CALL.

Единственное различие команд RET и RETI заключается в том, что при выполнении команды RETI система организации прерываний уведомляется об окончании подпрограммы прерываний.

Вторая подгруппа — условные переходы. Их характеристики приведены в табл.7.8. Все команды этой подгруппы выполняют короткий переход и содержат байт смещения, который из-за отсутствия места в табл. 7.8 не показан. Отметим, что переход по нулевому/ненулевому условию производится на основе содержимого аккумулятора, а флаг Z в слове состояния программы отсутствует.

Команды условных переходов

Система команд микроконтроллеров AVR

Микроконтроллеры (далее — МК) серии AVR имеют от 90 до 120 команд в зависимости от модели. Здесь рассмотрены только те команды, которые использованы в нашей программе.

ADD

Синтаксис: ADD reg1, reg2

Эта команда складывает содержимое регистров reg1 и reg2, сохраняя результат в регистре reg1. Изменяет состояние флагов H, S, V, N, Z, C.

AND

Синтаксис: AND reg1, reg2

Эта команда выполняет операцию «Логическое И» между регистрами reg1 и reg2, сохраняя результат в регистре reg1. Изменяет состояние флагов S, V, N, Z.

ANDI

Синтаксис: ANDI hreg, number

Эта команда выполняет операцию «Логическое И» между содержимым старшего регистра hreg и константой number (0…255), сохраняя результат в регистре. Изменяет состояние флагов S, V, N, Z.

BRBC

Синтаксис: BRBC bit, label

Эта команда проверяет состояние бита регистра SREG ($3F) и переходит к метке label, если бит сброшен. Метка должна находиться в пределах 63 команд от команды brbc.

BRBS

Синтаксис: BRBS bit, label

Эта команда проверяет состояние бита регистра SREG ($3F) и переходит к метке label, если бит установлен. Метка должна находиться в пределах 63 команд от команды brbs.

BRCC

Синтаксис: BRCC bit, label

Эта команда проверяет флаг переноса (С) и переходит к метке label, если он сброшен.

BRCS

Синтаксис: BRCS bit, label

Эта команда проверяет флаг переноса (С) и переходит к метке label, если он установлен.

BREQ

Синтаксис: BREQ bit, label

Эта команда проверяет флаг нуля (Z) и переходит к метке label, если он установлен.

BRNE

Синтаксис: BRNE bit, label

Эта команда проверяет флаг нуля (Z) и переходит к метке label, если он сброшен.

CBI

Синтаксис: CBI ioreg, bit

Эта команда сбрасывает в 0 бит регистра ввода-вывода ioreg ($00…$1F). Номер бита определяет операнд bit.

CLR

Синтаксис: CLR reg

Эта команда сбрасывает все биты регистра reg в 0. Изменяет состояние флагов S, V, N, Z.

CP

Синтаксис: CP reg1, reg2

Эта команда сравнивает содержимое регистров reg1 и reg2 путем вычитания reg2 из reg1. Содержимое регистров не изменяется. Изменяет состояние флагов H, S, V, N, Z, C.

CPI

Синтаксис: CPI hreg, number

Эта команда сравнивает содержимое регистра hreg с константой number путем вычитания константы из регистра. Содержимое регистров не изменяется. Изменяет состояние флагов H, S, V, N, Z, C. Команда работает только с регистрами R16…R31.

DEC

Синтаксис: DEC reg

Эта команда уменьшает на единицу содержимое регистра reg, записывая результат обратно в регистр. Изменяет состояние флагов S, V, N, Z.

IN

Синтаксис: IN reg, ioreg

Эта команда пересылает содержимое регистра ввода-вывода ioreg в РОН reg.

INC

Синтаксис: INC reg

Эта команда увеличивает на единицу содержимое регистра reg, записывая результат обратно в регистр. Изменяет состояние флагов S, V, N, Z.

LD

Синтаксис: LD reg, longreg

Эта команда загружает один байт из памяти данных (адрес ячейки памяти содержится в регистре longer) в регистр reg (для модели 1200).

LDI

Синтаксис: LDI hreg, number

Эта команда загружает непосредственное значение number в старший регистр hreg (команда может работать только с регистрами R16…R31). Число number должно находиться в пределах 0…255.

LSR

Синтаксис: LSR reg

Эта команда выполняет логический сдвиг вправо содержимого регистра reg. При этом 0-й бит копируется в флаг C, 7-й бит сбрасывается в 0. Изменяет состояние флагов S, V, N, Z, C.

ORI

Синтаксис: ORI hreg, number

Эта команда выполняет операцию «Логическое ИЛИ» между содержимым старшего регистра hreg и константой number (0…255), сохраняя результат в регистре. Изменяет состояние флагов S, V, N, Z.

OUT

Синтаксис: OUT ioreg, reg

Эта команда пересылает содержимое регистра общего назначения reg в регистр ввода-вывода ioreg.

RCALL

Синтаксис: RCALL label

Эта команда вызывает подпрограмму, обозначенную меткой label, которая должна находиться в пределах 2048 команд от команды rcall (относительный вызов).

RET

Синтаксис: RET

Эта команда выполняет возврат из подпрограммы, переходя на команду, которая следует за исходной командой call.

RETI

Синтаксис: RETI

Эта команда выполняет возврат из подпрограммы и устанавливает флаг общего разрешения прерываний.

RJMP

Синтаксис: RJMP label

Относительный переход. Выполняет переход к участку программы, обозначенному меткой label, которая должна находиться в пределах 2048 от команды RJMP.

SBI

Синтаксис: SBI ioreg, bit

Эта команда устанавливает в 1 бит регистра ввода-вывода ioreg ($00…$1F). Номер бита определяет операнд bit.

SBIC

Синтаксис: SBIC ioreg, bit

Эта команда проверяет бит регистра ввода-вывода ioreg ($00…$1F) и пропускает следующую команду, если этот бит сброшен. Номер бита определяет операнд bit.

SBIS

Синтаксис: SBIS ioreg, bit

Эта команда проверяет бит регистра ввода-вывода ioreg ($00…$1F) и пропускает следующую команду, если этот бит установлен. Номер бита определяет операнд bit.

SEI

Синтаксис: SEI

Эта команда устанавливает флаг общего разрешения прерываний.

SER

Синтаксис: SER reg

Эта команда устанавливает все биты регистра reg в 1. Изменяет состояние флагов S, V, N, Z. Команда может работать только с регистрами R16…R31.

ST

Синтаксис: ST reg, longreg

Для модели 1200 эта команда сохраняет содержимое регистра reg в памяти данных (адрес ячейки памяти содержится в регистре longer), т.е. косвенно пересылает содержимое РОН reg по адресу, хранящемуся в регистре longer.

SUBI

Синтаксис: SUBI hreg, number

Вычитает из старшего регистра hreg константу number. Результат сохраняется в этом же регистре. Изменяет состояние флагов H, S, V, N, Z, C. Команда может работать только с регистрами R16…R31.

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *