AVR. Учебный курс. Подпрограммы и прерывания

AVR. Учебный курс. Подпрограммы и прерывания

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

Вот, например, кусок кода, передающий в регистр UDR байты с некоторой выдержкой, выдержка делается за счет вращения бесконечного цикла:

.CSEG LDI R16,Low(RAMEND) ; Инициализация стека OUT SPL,R16 ; Обязательно. LDI R16,High(RAMEND) OUT SPH,R16 .equ Byte = 50 .equ Delay = 20 LDI R16,Byte ; Загрузили значение Start: OUT UDR,R16 ; Выдали его в порт LDI R17,Delay ; Загрузили длительность задержки M1: DEC R17 ; Уменьшили на 1 NOP ; Пустая операция BRNE M1 ; Длительность не равна 0? Переход если не 0 OUT UDR,R16 ; Выдали значение в порт LDI R17,Delay ; Аналогично M2: DEC R17 NOP BRNE M2 OUT UDR,R16 LDI R17,Delay M3: DEC R17 NOP BRNE M3 RJMP Start ; Зациклим программу

Сразу напрашивается повторяющийся участок кода вынести за скобки.

LDI R17,Delay M2: DEC R17 NOP BRNE M2

Для этих целей есть группа команд перехода к подпрограмме CALL (ICALL, RCALL, CALL) И команда возврата из подпрограммы RET В результате получается такой код:

.CSEG LDI R16,Low(RAMEND) ; Инициализация стека OUT SPL,R16 ; Обязательно. LDI R16,High(RAMEND) OUT SPH,R16 .equ Byte = 50 .equ Delay = 20 LDI R16,Byte ; Загрузили значение Start: OUT UDR,R16 ; Выдали его в порт RCALL Wait OUT UDR,R16 RCALL Wait OUT UDR,R16 RCALL Wait OUT UDR,R16 RCALL Wait RJMP Start ; Зациклим программу. Wait: LDI R17,Delay M1: DEC R17 NOP BRNE M1 RET

Как видишь, программа резко сократилась в размерах. Теперь скопируй это в студию, скомпилируй и запусти на трассировку. Я хочу показать как работает команда RCALL и RET и при чем тут стек.

Вначале программа, как обычно, инициализирует стек. Потом загружает наши данные в регистры R16 и выдает первый байт в UDR… А потом по команде RCALL перейдет по адресу который мы присвоили нашей процедуре, поставив метку Wait в ее начале. Это понятно и логично, гораздо интересней то, что произойдет в этот момент со стеком.

До выполнения RCALL

Увеличить Адрес команды RCALL в памяти, по данным PC = 0x000006, адрес следующей команды (OUT UDR,R16), очевидно, будет 0x000007. Указатель стека SP = 0x045F — конец памяти, где ему и положено быть в этот момент.

Увеличить Смотри, в стек пихнулось число 0x000007, указатель сместился на два байта и стал 0x045D, а контроллер сделал прыжок на адрес Wait.

Наша процедура спокойно выполняется, как ей и положено, а по команде RET процессор достанет из стека наш заныченный адрес 0x000007 и прыгнет сразу же на команду OUT UDR,R16

Таким образом, где бы мы не вызвали нашу процедуру Wait — мы всегда вернемся к тому же месту откуда вызвали, точнее на шаг вперед. Так как при переходах в стеке сохраняется адрес возврата. А если испортить стек? Взять и засунуть туда еще что нибудь? Подправь процедуру Wait и добавь туда немного бреда, например, такого

Wait: LDI R17,Delay M1: DEC R17 NOP BRNE M1 PUSH R17 ; Ой, я не специально! RET

Перекомпиль и посмотри что будет =) Заметь, компилятор тебе даже слова не скажет. Мол все путем, дерзай :)

До команды PUSH R17 в стеке будет адрес возврата 00 07, так как в регистре R17 ,в данный момент, ноль, и этот ноль попадет в стек, то там будет уже 00 00 07.

А потом идет команда RET… Она глупая, ей все равно! RET тупо возьмет два первых верхних байта из стека и запихает их в Programm Counter.

И куда мы перейдем? Правильно — по адресу 00 00, в самое начало проги, а не туда откуда мы ушли по RCALL. А будь в R17 не 00, а что нибудь другое и попади это что-то в стек, то мы бы перешли вообще черт знает куда с непредсказуемыми последствиями. Это и называется срыв стека.

Но это не значит, что в подпрограммах нельзя пользоваться стеком в своих грязных целях. Можно. Но делать это надо с умом. Класть туда данные и доставать их перед выходом. Следуя железному правилу «Сколько положил в стек — столько и достань!», чтобы на выходе из процедуры для команды RET лежал адрес возврата, а не черти что.

Мозговзрывной кодинг Да, а еще тут возможны стековые извраты. Кто сказал, что мы должны вернуться именно туда откуда были вызываны? =))) А если условия изменились и по итогам вычислений в процедуре нам ВНЕЗАПНО туда стало не надо? Никто не запрещает тебе нужным образом подправить данные в стеке, а потом сделать RET и процессор, как миленький, забросит тебя туда куда надо. Легко!

Более того, я когда учился в универе и сдавал лабы по ассемблеру, то лихо взрывал мозги нашему преподу такими конструкциями (там, правда, был 8080, но разница не велика, привожу пример для AVR):

LDI R17,low(M1*2) PUSH R17 LDI R17,High(M1*2) PUSH R17 ; потом дофига дофига другого кода. для отвлечения ; внимания, а затем, в нужном месте, ВНЕЗАПНО RET

И происходил переход на метку M1, своего рода извратский аналог RJMP M1. А точнее IJMP, только вместо Z пары мы используем данные адреса загруженные в стек из любого другого регистра, иногда пригождается. Но без особой нужды таким извратом заниматься не рекомендую — запутывает программу будь здоров.

Но побалуйся обязательно, чтобы во всей красе прочувствовать стековые переходы.

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

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

Подпрограммы vs Макросы Но не стоит маникально все повторяющиеся участки заворачивать в подпрограммы. Дело в том, что переход и возврат добавляют две команды, а еще у нас идет прогрузка стека на 2 байта. Что тоже не есть гуд. И если заменяется три-четыре команды, то овчинка с CALL-RET не стоит выделки и лучше запихать все в макрос.

Это аппаратные события. Ведь у микроконтроллера кроме ядра есть еще дофига периферии. И она работает параллельно с контроллером. Пока контроллер занимается вычислением или гоняет байтики по памяти — АЦП может яростно оцифровывать входное напряжение, 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 и добавим немного кода.

.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 RJMP RX_OK ; (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 ; Конец таблицы прерываний ;---------------------------------------------------------------------- ; Это обработчик прерывания. Тут, на просторе, можно наворотить сколько ; угодно кода. RX_OK: IN R16,UDR ; Тут мы делаем что то нужное и полезное RETI ; Прерывание завершается командой RETI ;---------------------------------------------------------------------- Reset: LDI R16,Low(RAMEND) ; Инициализация стека OUT SPL,R16 ; Обязательно. LDI R16,High(RAMEND) OUT SPH,R16 SEI ; Разрешаем прерывания глобально LDI R17,(1<<RXCIE) ; Разрешаем прерывания по приему байта OUT UCSRB,R17 M1: NOP NOP NOP NOP RJMP M1

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

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

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

M1: NOP NOP NOP NOP RJMP M1

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

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

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

.ORG $016 RJMP RX_OK ; (USART,RXC) USART, Rx Complete

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

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

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

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

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

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

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

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

Кроме того, у каждого прерывания есть еще свой собственный бит локального разрешения. В примере с UDR это бит RXCIE (Receive Complete Interrupt Enable) и расположены они в портах ввода вывода

По дефолту, после старта, все прерывания запрещены локально и глобально. И их надо разрешать вручную, не забыв прописать обработчик прерывания.

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

Если все неиспользуемые прерывания заглушены командой RETI то ничего не пройзойдет — как пришел так и вернется обратно. Если же там ничего нет, то процессор будет выполнять эту пустоту пока не доберется до живого кода. Это может быть как переход на обработчик другого прерывания (ниже по таблице векторов) так и начало основного кода, тело обработчика прерывания, какая либо процедура, записанная до метки start. Да что угодно, что первой попадется.

Сам понимаешь, что в таком случае ни о какой корректной работе говорить не придется. Кстати, почти все сишные компиляторы, неиспользуемые обработчики глушат на RESET т.е. случайный вызов несуществующего прерывания приводит к сбросу.

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

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

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

Флаг этого события сбрасывается либо сам, при переходе к обработчику прерывания, либо при совершении какого-либо действия. Например, чтобы сбросить флаг события прерывания RxC надо считать байт из UDR. Протрассируй программу и сам увидишь, что сброс флага RxC происходит после выполнения команды

Либо флаг скидывают вручную — записью в этот флаг единицы. Не нуля! Единицы! Подробней в даташите на конкретную периферию.

Очередность прерываний Но что будет если произошло одно прерывание и процессор ушел на обработчик, а в этот момент произошло другое прерывание?

А ничего не будет, при уходе на прерывание происходит аппаратный глобальный запрет всех других прерываний— просто сбрасывается флаг I, а по команде RETI, при возврате, этот флаг устанавливается в 1. В этом, кстати, отличие RET от RETI

Но! Никто не запрещает нам внутри обработчика поставить SEI и разрешить прерывания. При этом мы получим вложенные прерывания. Можно, но это опасно. Черевато переполнением стека и прочими гадостями. Так что это надо делать с твердым осознанием последствий.

Что тогда? Прерывание которое пришло во время обработки первого потеряется?

Нет, не потеряется. Флаг то его события никто сам не снимет, так что только процессор выйдет из первого обработчика (разреша при этом глобальные прерывания), как это не снятый флаг сгенерирует новое прерывание и процессор его обработает.

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

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

Бег по граблям Прерывания штука мощная, но опасная. С их помощью плодятся такие глюки, по сравнению с которыми стековые срывы так — семечки.

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

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

Так что если у МК то понос, то золотуха, то программа петухом поет, а то молчит как рыба — знай, в 95% копать собаку надо в районе прерываний и их обработчиков.

Но если правильно написать прерывание, то багов оно не даст. Главное понимать в чем его опасность.

Грабли первые — спасай регистры. Прерывание, когда оно разрешено, вызывается ВНЕЗАПНО, между двумя произвольными инструкциями кода. Поэтому очень важно, чтобы к моменту когда мы из прерывания вернемся все осталось как было.

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

Приведу пример: Вот есть у нас обработчик прерывания который сравнивает байт на входе в USART и если он равен 10, выдает обратно отклик ‘t’ (ten в смысле).

RX_OK: IN R16,UDR CPI R16,10 BREQ Ten RJMP Exit Ten: LDI R17,'t' OUT UDR,R17 Exit:

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

. . . 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 . . .

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

. . . 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). Вот их и сохраним.

Выглдеть это будет так:

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) с разными прерываниями. Чтобы понять и прочувствовать как ведут себя прерывания.

Спасибо. Вы потрясающие! Всего за месяц мы собрали нужную сумму в 500000 на хоккейную коробку для детского дома Аистенок. Из которых 125000+ было от вас, читателей EasyElectronics. Были даже переводы на 25000+ и просто поток платежей на 251 рубль. Это невероятно круто. Сейчас идет заключение договора и подготовка к строительству!

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

279 thoughts on “AVR. Учебный курс. Подпрограммы и прерывания”

А есть в данной вариации ассемблера аналог команд PUSHAD/POPAD, сохраняющих в стек и достающих из него значения всех регистров общего назначения?

Нету. Тут же 32 регистра. Никакой оперативки не напасешься пихать такую массу.

Начал изучать прерывания опять же на С. И вот какое дело, обработчик прерываний у меня срабатывает когда на линии PB5 меняется уровень, в обработчеке у меня простой код если PB5 = 0 и PB6= 0 то пишу ноль, исли PB6=1 пишу еденицу. Так вот за 100 ms у меня проходит 100 едениц и нулей. Суть в чем если я просто посылаю по UART, то принимаю все биты. Но стоит мне записать биты в массив, а потом перевести их в число, как на выходе я получаю какую то биллиберду. Я вот думаю успевает ли выполнится обработчик прерывания, до возникновения другого прерывания? Или может я что не так делаю.

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

В строке 3 проц уходит на обработку по метке loc_Control_VUART2UART_05 (при T=0). Там (строка 35)он Вопрос 1: обнуляет бит VUART_RECEIVE регистра Status_VUART. Я прав? Вопрос 2: что значит 1<<? после чего: выходит из loc_Control_VUART2UART_05 и Вопрос 3: продолжает «копать дальше» строку 4 и далее следующие строки? ИЛИ Вопрос 4: выходит из Control_VUART2UART в строке 1? Еще общий вопрос: если рассматривать call и brxx. Обе они переходят по метке. Call переходит по метке процедуры, сохраняя в стек адрес команды, к которой надо будет вернуться после выполнения процедуры. Branch(как и jmp) тоже переходит по метке, но в стек ничего не сейвит. Я прав? Если так, то на вопрос 4 получается ответ ДА.

И еще вопрос-для закрепления знаний =): Впорос 5: Если я , например, по строке 22 перешел в лок_3 в строке 33, то далее проц «копает» 34-ю, 35-ю, 36-ю и далее по 37-й строке выходит либо из главной процедуры либо в строку 23(если branch сейвит в stack, надеюсь что он так не делает)))

1 Control_VUART2UART: 2 rcall ChkUART_RTS 3 brtc loc_Control_VUART2UART_05 4 sbrs Status_VUART,VUART_RECEIVE 5 ret 6 out UDR,RX_VUART 7 #ifdef TestMode 8 mov tstUART,RX_VUART 9 sbr Status_VUART,1<<TST_UART_RX 10 #endif 11 tst NextBaudRate 12 breq loc_Control_VUART2UART_05 13 tst Count_ChkSumm 14 brne loc_Control_VUART2UART_01 15 mov ChkSumm,RX_VUART 16 cpi RX_VUART,0x82 17 brne loc_Control_VUART2UART_05 18 rjmp loc_Control_VUART2UART_04 19 loc_Control_VUART2UART_01: 20 ldi temp0,0x05 21 cp Count_ChkSumm,temp0 22 brne loc_Control_VUART2UART_03 23 cpi RX_VUART,0x54 24 brne loc_Control_VUART2UART_02 25 cp ChkSumm,RX_VUART 26 brne loc_Control_VUART2UART_02 27 mov CurrBaudRate,NextBaudRate 28 loc_Control_VUART2UART_02: 29 clr NextBaudRate 30 rjmp loc_Control_VUART2UART_05 31 loc_Control_VUART2UART_03: 32 add ChkSumm,RX_VUART 33 loc_Control_VUART2UART_04: 34 inc Count_ChkSumm 35 loc_Control_VUART2UART_05: 36 cbr Status_VUART,1<<VUART_RECEIVE ; 37 ret

1<<? это макрооператор языка, не команда процессора — задвинуть 1 влево на число «?», то есть мы делаем число в котором все нули, но один бит с номером «?» установлен в 1. Ищи чему равно «?» (это должно быть макроопределение каке нибудь из серии .equ)

В строке 36 происходит обнуление бита VUART_RECEIVE в регистре Status_VUART

А в строке 37 происходит возврат к точке откуда это подпрограмму вызвали и это не строка 4, а где то явно выше ее. В строке 3 проц просто прыгает (это не CALL. ) на метку. Выйдет по строке 37 вообще из процедуры «Control_VUART2UART:» выше, в вызвающую прогу. Не на строку 1, а куда то туда, где последний раз был «RCALL Control_VUART2UART»

Разницу между CALL и JMP/BRanch ты понял правильно. Так и есть.

Вопрос 5: После бранча проц продолжает копать оттуда куда его послали. Возврата нет, ибо бранч не сохраняет адреса возврата.

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

А что это за хрень? Судя по меткам это гонит из виртуального уарта (VUART судя повсему это виртуальный) в реальный. Из Ethernet делаем UART? или чо?

Вообще вся схема — это преобразователь интерфейса диагностического порта автомобиля k-line в интерфейс Bluetooth. Сам бы рад разобратся что там куда гонит. Вся прога вместе с назначениями типа .def .equ около 650 строк. Надо разобратся за 3 дня, и потом по возможности еще в протеусе смоделить(((. AVR я изучаю ровно неделю)))), контроллеры всякие — месяц))). А VUART это как я пока думаю — UART со стороны блютусника.

650 строк эт немного :))))) В протеусе вряд ли смоделишь. Слишком кривая среда для моделинга сложных прог. Разве что МК будет точно такой же какие там есть. В протеусе хорошо куски программ отлаживать. Какие то узлы проверять.

3 дня это сильно. Если чо стучись в аську. Оперативней будет. Номер в инфо есть.

Оки, вообще говоря, моя задача сейчас — просто нарисовать некое подобие блок-схем логики работы устройства. Комменты есть к каждой строке, так что с этим, думаю справлюсь,понимания особого это не требует. А вот с протоколами k-line и BT, AT-командами придется разобраться))

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

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

про стек: на своём втором компе Z80 (это проц) со стеком делал так, ставил его на конец видеопамяти в аккумулятор = 0 и циклом пробегал push A. очень быстро очищал экран, а затем стек на место и работаем дальше.

тут правда ещё не придумал как использовать!

есть вопрос мнемоники команд асма для АВР написанны везде и количество тактов есть, а машинный код можно гдето глянуть? чисто из любопытства посмотреть, дизасемблер ведь както работает? например команда jmp XX в памяти занимает два байта или три? чет совсем уже все забыл :(

Легкий косячек: «В случае ассемблерной процедуры, тебе бы пришлось вначале нужно было подсунуть гуталин вместо зубной пасты» лишнее «нужно было»

📎📎📎📎📎📎📎📎📎📎