Ассемблер AVR для начинающих
.doc
Ассемблер AVR для начинающих (четвертый шаг)
Ну что же, мои уважаемые читатели, вы уже научились писать простые программы на ассемблере, не зная, впрочем, еще довольно многих его команд и особенностей. Все программы, написанные мной, да и вами, если вы делали задания, выполнялись исключительно в рамках основного цикла программы. Те, кто имеет опыт программирования на языках высокого уровня, наверняка знает, что для лучшей структурированности и читабельности программ удобно использовать подпрограммы. Ассемблер также имеет возможность применения подпрограмм. Для того, чтобы понять, как работают подпрограммы в языке низкого уровня, необходимо обогатить ваш багаж знаний такими понятиями, как программный счетчик и стек. Что же это такое? Программный счетчик (в литературе он сокращенно называется РС - Program Counter) представляет собой, ну назовем это так, некоторую переменную, содержащую текущий адрес памяти, в которой находится выполняемая в данный момент команда. Я думаю, проницательный читатель уже догадался, что написанные нами программы при ассемблировании преобразуются в специальные коды, которые процессор считывает, расшифровывает и выполняет. Так вот эти коды записываются в так называемую память программ (CSEG - Code Segment). Каждая команда занимает от 2-х до 4-х байт в этой области. Все команды записываются последовательно одна за другой. И, соответственно, каждая из них имеет свой адрес. Если не используется никаких команд перехода, то выполнение их осуществляется по очереди - от первой к последующим. При выполнении последней снова выполняется первая. Программный счетчик отслеживает адрес каждой выполняемой команды, так что процессор знает, откуда он в данный момент берет данные. При использовании команд перехода мы, как вы помните по предыдущему шагу, используем метки. Так вот, контроллер абсолютно не знает ни о каких метках. Зато о них знает транслятор языка ассемблера. Они ему нужны для того, чтоб отсчитать разность между текущим положением команды перехода и команды, следующей за меткой. Эта разность и записывается в контроллер. Итак, при выполнении команды перехода программный счетчик прерывает свое непрерывное наращивание и изменяет свое текущее значение на величину, являющуюся аргументом команды перехода. Для любопытных читателей в скобках отмечу, что некоторые команды перехода работают по другому, и в них положение счетчика может задаваться как в виде абсолютного адреса, так и адреса, указанного в специальном регистре. Но они используются для контроллеров, объем памяти которых превышает 16 кБ, так что пока их применение нам не грозит. Теперь разберемся с понятием стека. Чтобы представить себе, что такое стек, обратимся к бытовой аналогии. Представим себе детскую пирамидку. Мы кладем сначала большой диск, затем поменьше, затем еще меньше, и в конце концов самый маленький. Если же нам надо разобрать эту самую пирамидку, мы снимаем сначала самый маленький диск, затем второй по величине, и последним снимаем самый большой. Так вот пирамидка представляет собой классический стек - это структура, в которой элемент, который добавляется первым, удаляется последним, и наоборот: элемент, добавленный последним, удаляется первым. В литературе еще используется англоязычное сокращение LIFO (Last In - First Out, последний вошел - первый вышел). Какое же отношение имеет пресловутый стек к тому, что делается в нашем контроллере? Оказывается, самое непосредственное. Именно благодаря ему есть возможность применять подпрограммы. Рассмотрим этот процесс подробнее. В чем отличие вызова подпрограммы от обычной команды перехода? В том, что команда перехода не требует возвращения к тому месту, откуда был совершен переход, а вызов подпрограммы подразумевает, что после ее выполнение и выхода из подпрограммы, выполнение основной программы продолжится со следующей за вызовом строки. А поскольку одна и та же подпрограмма может быть вызвана из разных мест основной программы, или других подпрограмм, то адрес возврата каждый раз будет разным. Тут нам на помощь и приходит стек. В момент вызова подпрограммы текущее положение программного счетчика сохраняется в верхушке стека, а в сам программный счетчик записывается адрес первой команды подпрограммы. После завершения подпрограммы выполняется специальная команда возврата, в результате которой в программный счетчик записывается значение из верхушки стека. И таким образом продолжается выполнение программы с того места, откуда было совершен вызов подпрограммы. Возможно, интересующийся читатель спросит, а зачем тут стек, хватило бы просто какой-то переменной, куда бы сохранялось текущее значение программного счетчика. Отвечу. Переменной хватило бы в том случае, если бы подпрограммы можно было вызывать только из основной программы. А в случае вызова подпрограммы из другой подпрограммы в стеке уже сохраняется оба адреса возврата. Если вторая подпрограмма вызывает третью, то в стеке хранится три адреса и т.д. Так каков же размер этой матрешки? Сколько вложенных друг в друга подпрограмм может быть? Размер стека ограничен только объемом оперативной памяти. Поскольку в контроллере ATtiny13 имеется 64 байта ОЗУ, то, соответственно, глубина стека может достигать 64 адресов, что более чем достаточно для нужд большинства пользователей. Но это все слова. Давайте, наконец, перейдем к практике. Напишем программу, которая бы переключала состояние светодиода при помощи одной кнопки. То есть при первом нажатии и отпускании кнопки светодиод зажигается, при повторном - гасится и т.д. По традиции сначала приведу текст программы, затем объясню новые команды, потом расскажу о назначении каждой строки и общем алгоритме работы. Итак, вот текст программы: .include "F:\Prog\AVR\asm\Appnotes\tn13def.inc" ldi r16, RAMEND ;Загрузка в регистр r16 адреса верхней границы ОЗУ out SPL, r16 ;Копирование значения из r16 в регистр указателя стека SPL ldi r17,(1<<4) ;Загрузка в регистр r17 "1", смещенной на 4 разряда влево out DDRB, r17 ;Копирование из r17 в DDRB (РВ4 - выход) ldi r16,(1<<2) ;Загрузка в регистр r16 "1", смещенной на 2 разряда влево main: ;Основной цикл программы out PORTB,r16 ;Копирование из r16 в PORTB sbic PINB, 2 ;Если РВ2=0 (кнопка нажата), пропустить след. строку rjmp main ;Возврат к началу цикла rcall delay ;Вызов подпрограммы задержки на дребезг контактов wait: ;Цикл ожидания, пока нажата кнопка sbis PINB, 2 ;Если РВ2=1 (кнопка отпущена), пропустить след. строку rjmp wait ;иначе перейти к началу цикла ожидания rcall delay ;Вызов подпрограммы задержки на дребезг контактов sbic PINB, 2 ;Если РВ2=0 (кнопка нажата), пропустить след. строку eor r16,r17 ;Исключающее ИЛИ регистров r16 и r17 rjmp main ;Возврат к метке main delay: ;Начало подпрограммы задержки ldi r18, 255 ;Загрузка значения в регистр r18 ldi r19, 31 ;Загрузка значения в регистр r19 del: ;Цикл задержки subi r18, 1 ;Вычитание 1 из регистра r18 sbci r19, 0 ;Вычитание 0 из регистра r19 с учетом переноса brcc del ;Если не было переноса вернуться к метке del ret ;Возврат из подпрограммы Очередное пополнение списка команд: Команда out имеет два операнда: первый - РВВ, и второй - РОН. В результате выполнения этой команды содержимое РОН копируется в РВВ. Следует учесть, что операнды должны быть именно такими и именно в такой последовательности. То есть напрямую скопировать из РВВ в другой РВВ нельзя, а только с использованием промежуточного РОН. Для копирования же из РОН в РОН используется другая команда, но о ней расскажу позже по мере необходимости. Команда eor имеет также два операнда: оба - регистры общего назначения. В результате ее выполнения осуществляется побитовая операция "Исключающее ИЛИ" между содержимым обоих регистров, и результат записывается в первый регистр. Вообще операция "исключающее ИЛИ" используется в микропроцессорной технике довольно часто. С ее помощью осуществляется переключение бита или группы битов в противоположное состояние. Не буду глубоко вдаваться в подробности. Любопытный читатель может почерпнуть дополнительную информацию в интернете. Команда rcall имеет единственный операнд - метку. В результате ее выполнения осуществляется вызов подпрограммы, начинающейся с указанной метки. Механизм вызова описан выше, и здесь я на нем останавливаться не буду. Команда ret не имеет операндов. Ею должна оканчиваться любая подпрограмма. Именно она восстанавливает прежнее состояние программного счетчика из стека. Таким образом, подпрограмма начинается с метки, по имени которой мы переходим командой rcall, и оканчивается командой ret. Теперь, когда все новые команды обрели для вас смысл, переходим непосредственно к описанию программы, опять же, опуская подробное описание уже встречающихся нам строк. 1 строка. Подключение файла tn13def.inc к нашему ассемблерному файлу. 2 строка. Тут нас ожидает нечто новое. При помощи известной нам команды ldi в регистр r16 загружается некоторое странное значение RAMEND, нигде ранее не встречавшееся. Откуда же оно берется? Оно прописано все в том же подключенном нами файле tn13def.inc и содержит в себе адрес верхней границы оперативной памяти. Этот адрес нам пригодится для указания вершины стека. 3 строка. Тут происходит копирование содержимого регистра r16 в регистр SPL, являющийся ни чем иным, как указателем стека. Таким образом, пара операций ldi и out служит для записи какой-нибудь константы в регистр ввода вывода, поскольку напрямую записать эту константу в РВВ нельзя, а только с использованием промежуточного РОН. Также следует заметить, что инициализировать указатель стека нужно ВСЕГДА, если планируется использование подпрограмм, иначе работа программы может стать непредсказуемой. 4 строка. В регистр r17 происходит копирование странной конструкции (1<<4). Я не упоминал ранее, но теперь пришло время рассказать об одном нюансе. Пр использовании констант командой ldi или подобными ею, в качестве константы можно указать не только число, но и целое математическое выражение, состоящее из набора математических и логических операций, а также некоторых функций. Операции по синтаксису и составу практически полностью соответствуют таковым на языке Си. Набор функций же сильно ограничен, и о них я буду упоминать по мере надобности. С учетом вышесказанного теперь можно понять, что в регистр r17 записывается единица, сдвинутая на 4 бита влево (операция сдвига <<). Таким образом, в регистр r17 будет записано число 0b00010000 (префикс 0b означает, что число записано в двоичной форме). 5 строка. Значение из регистра r17 копируется в регистр DDRB. При этом в 4-м бите регистра будет установлена логическая "1", а во всех остальных - логический "0". Это значит, что вывод РВ4 будет работать как выход, а все остальные - как входы. Строки 4 и 5 показывают еще один способ инициализации портов ввода-вывода. В данном случае мы не получили никакой выгоды в объеме кода, и даже увеличили объем программы на 2 байта, так как инициализировали всего один бит. Но если необходимо перевести на выход сразу несколько выводов, то такая конструкция значительно сократит код. 6 строка. Аналогична по записи с 4-й. В ней в регистр r16 записывается единица, сдвинутая на два бита влево. 7 строка. Пустая для лучшей читабельности программы. Она разделяет блок инициализации контроллера с блоком основного цикла программы. 8 строка. Метка main. Служит границей основного цикла программы. 9 строка. Аналогична по структуре 5-й. В ней содержимое регистра r16 копируется в регистр PORTB, включая подтягивающий резистор на выводе РВ2. Почему же мы ее записали в основном цикле программы, а не в разделе инициализации? На самом деле она выполняет двоякую функцию. Я уже упоминал, что регистр PORTB отвечает за включение подтягивающих резисторов, если вывод работает как вход, и за состояние вывода, если он работает как выход. Так вот, этой же строкой мы будем переключать и светодиод LED2, находящийся на РВ4. Но об этом позже. 10 строка. Командой sbic проверяем, нажата ли кнопка SB1, и, если нажата, то пропускаем следующую строку. 11 строка. Команда безусловного перехода rcall на метку main. Таким образом, пока не нажата кнопка, программа не выполняет никаких действий, разве что постоянно перезаписывает регистр r16 в регистр PORTB. Этого можно было бы избежать, если перенести строку 9 в конец основного цикла, и продублировать ее в разделе инициализации. 12 строка. Вот тут как раз и встречается первый вызов подпрограммы. Это подпрограмма delay, которая описана ниже по тексту. Назначение ее - задержать выполнение программы приблизительно на 30 мс. Зачем это нужно? Дело в том, что кнопки, используемые нами, не являются идеальными, и в них имеет место так называемый дребезг контактов. Это означает, что в момент нажатия и отпускания кнопки может происходить многократное количество размыканий и замыканий контакта с высокой частотой. Если не вводить эту задержку, то контроллер может однократное нажатие расценить как многократное и отреагировать соответствующим образом. Поэтому запомните, что при использовании кнопок необходимо вводить задержку на дребезг контактов. Возможно, кое-кто из читателей воскликнет: "Позвольте, а как же наша первая программа? Мы же использовали там кнопки, но ни о каких дребезгах тогда речь не шла!" Да, использовали, и да, не шла. Но тогда это было неважно. У нас при нажатии на кнопку выполнялось только включение светодиода, а при отпускании - только выключение, и в этом случае нам было совершенно безразлично, сколько раз в момент коммутации произойдет установка "0" или "1" на выводе светодиода, поскольку человеческий глаз не в силах уловить мигание с такой частотой. В нынешнем же случае ситуация другая. Если не вводить задержки на дребезг контактов, то за одно нажатие может произойти многократное переключение светодиода, и после отпускания кнопки он установится в неопределенном состоянии, а оно нам надо? 13 строка. Тут располагается еще одна метка: wait. О ее назначении расскажу при описании следующих строк. 14 строка. Командой sbis проверяется, отпущена ли кнопка, и если отпущена, то пропускается следующая строка. 15 строка. Команда безусловного перехода к метке wait. Итак, давайте теперь разберем работу строк 13-15 в комплексе. Они выполняют задержку выполнения программы на то время, пока нажата кнопка. И действительно, если в строке 14 не выполняется условие, что кнопка отпущена, то следующая за ней строка 15 возвращает нас снова к строке 13 и к новой проверке. Зачем это надо? Это один из классических приемов опроса кнопки. Если нам необходимо выполнить всего одно действие за одно нажатие, то наиболее часто это действие выполняют не при нажатии, а именно при отпускании кнопки. Таким образом, мы ожидаем, пока кнопка нажата, ничего не выполняя, и только затем, когда ее отпускают, выполняем необходимое действие. 16 строка. Снова вызов подпрограммы delay. Это также задержка на дребезг контактов, но теперь при отпускании кнопки. Назначение ее аналогично вышеописанному. 17 строка. Снова проверка, нажата ли кнопка, и если нажата, то пропуск следующей строки. По большому счету ее можно было бы уже и не делать, и она относится к перестраховочным. Назначение ее - проверить, а действительно ли мы уже отпустили кнопку, и только затем осуществить необходимое действие. Повторюсь, можно было бы и без нее, но с ней спокойней. 18 строка. Это та строка, ради которой все и затевалось. В ней командой eor осуществляется выполнение операции "Исключающее ИЛИ" между регистрами r16 и r17 с сохранением результата в r16. Вот тут и кроется смысл на первый взгляд расточительной инициализации портов ввода-вывода контроллера. Итак, в регистре r17 у нас находится "1" только в разряде, соответствующем выводу РВ4. Если выполнить операцию "Исключающее ИЛИ" с этим регистром, то бит, соответствующий РВ4, изменит свое состояние на противоположное. То есть, если там была единица, то станет ноль, и наоборот. Таким образом, в этой строке происходит переключение бита, соответствующего РВ4, в противоположное состояние и сохранение результата в регистре r16. А по возвращении к началу цикла этот регистр в строке 9 копируется в PORTB, тем самым изменяя состояние светодиода. 19 строка. Аналогична 11-й. В ней осуществляется безусловный переход к метке main. Но в отличие от строки 11, данная строка определяет конец основного цикла. Все, что идет ниже по тексту, к основному циклу не относится. 20 строка. Пустая, служит для визуального отделения границы основного цикла от подпрограммы delay. 21 строка. Метка delay. Именно с нее начинается подпрограмма задержки на дребезг контактов. Одновременно она задает и имя этой подпрограммы, по которому будет осуществляться ее вызов. 22 строка. Загрузка в регистр r18 константы 255. 23 строка. Загрузка в регистр r19 константы 31. Мы использовали здесь регистры r18 и r19 в отличие от предыдущей программы, поскольку r16 и r17 уже заняты для выполнения иных функций. 24 строка. Метка del. Служит границей цикла вычитания из регистров r18 и r19. 25 строка. Простое вычитание единицы из регистра r18. 26 строка. Вычитание нуля с учетом заема из младшего разряда из регистра r19. 27 строка. Проверка флага переноса в регистре r19, и если переноса не было, то переход к началу цикла вычитания (метка del). 28 строка. Команда ret, означающая конец подпрограммы, и возвращающая программный счетчик к месту вызова подпрограммы. Я умышленно не стал подробно останавливаться на строках 22-27, поскольку они уже были достаточно полно описаны в предыдущем шаге. Собственно, на этом уже можно считать четвертый шаг сделанным. В заключение традиционные уже задания для самостоятельного выполнения: 1. Изменить программу таким образом, чтобы одна кнопка управляла двумя светодиодами, работающими в противофазе. То есть вначале горит светодиод LED1, а LED2 погашен. При нажатии на кнопку гасится LED1 и включается LED2, при повторном нажатии светодиоды переключаются и т.д. 2. Добавить в программу обработку нажатия другой кнопки таким образом, чтобы каждая кнопка отвечала за свой светодиод. То есть кнопка SB1 переключала светодиод LED1, а кнопка SB2 - светодиод LED2. Переключение каждого светодиода должно осуществляться независимо от другого. 3. Выполнить задание из предыдущего шага с вынесением цикла задержки в отдельную подпрограмму. |
Ассемблер AVR для начинающих (пятый шаг)
Наверняка любознательный читатель где-то да слышал о том, что в микроконтроллерах существует механизм прерываний. Что же это такое и зачем это нам нужно? Все написанные нами до сих пор программы выполнялись по жестко заданному алгоритму без возможности хоть как-то его скорректировать. При этом те или иные действия осуществлялись только в тот момент времени, когда до них доходила очередь. А если нам нужно на какие-нибудь события реагировать мгновенно, а не ждать, пока до них дойдет очередь? Или, например, нужно дождаться реакции от какого-либо периферийного модуля контроллера? Глупо было бы по всей программе ставить проверки в надежде поймать нужные нам события. Вот здесь-то нам и помогут прерывания. Само название их уже говорит за себя. Это специальные подпрограммы, не вызываемые явно из основной программы, а прерывающие ход ее выполнения при наступлении какого-либо события (например, изменение состояния входа, переполнение таймера, окончание цикла преобразования АЦП). Как же так получается, что ни с того ни с сего выполняется какая-то подпрограмма, которую никто не вызывал, да, к тому же, и прерывающая в любом месте ход выполнения основной программы? Суть тут вот в чем. В каждом контроллере определен изначально список событий, которые могут вызвать прерывание, причем в каждом контроллере этот список свой в зависимости от количества и функциональных возможностей периферийных модулей. События, вызывающие прерывания, могут быть как внешними (например, изменение состояния входа, прием байта по интерфейсу), так и внутренними (например, переполнение таймера, окончание цикла записи АЦП). Рассмотрим такой список применительно к нашему контроллеру ATtiny13, и посмотрим, какие прерывания нам могут пригодиться, а какие вряд ли. Вообще, список этот имеется в самом конце каждого файла с расширением inc, так что можно легко узнать, какие могут присутствовать прерывания у того или иного контроллера. Приведу здесь выписку из файла tn13def.inc с некоторыми пояснениями для тех, кто не понимает по-аглицки. Заодно и рассмотрим еще несколько особенностей языка ассемблера. ;**** Interrupt Vectors **** .equ INT0addr =$001 ;External Interrupt0 .equ PCINT0addr =$002 ;Pin Change Interrupt0 .equ TIM0_OVF0addr =$003 ;Overflow0 Interrupt .equ EE_RDYaddr =$004 ;EEPROM write complete .equ ANA_COMPaddr =$005 ;Analog Comparator Interrupt .equ TIM0_COMPAaddr =$006 ;Timer/Counter0 Compare Match A .equ TIM0_COMPBaddr =$007 ;Timer/Counter0 Compare Match B .equ WDTaddr =$008 ;Watchdog Timeout .equ ADCaddr =$009 ;ADC Conversion Complete Handle Итак, заголовок гласит "Interrupt Vectors", что означает "Векторы прерываний". Почему именно векторы, расскажу попозже. Все последующие строки начинаются с одной и той же директивы equ. Она имеет следующий синтаксис. Первым аргументом ее является символическое имя. Любое, на вкус пользователя (типа объявления имени переменных или констант в языках высокого уровня). Далее ставится знак равенства, а за ним значение, присваиваемое указанному имени. По своему действию эта директива похожа на объявление константы в языках высокого уровня. Значение, присвоенное указанному имени, нельзя изменить в дальнейшем. Еще одно новшество в синтаксисе - знак $ перед числом. Знак этот указывает, что последующее за ним число записано не в десятичной, а в шестнадцатиричной форме. Для указания этой же формы записи в языке ассемблера есть еще один префикс 0x. То есть записи $4E и 0x4E для ассемблера аналогичны и означают шестнадцатиричное число 4Е. Для записи двоичных чисел, как я упоминал уже ранее в одном из предыдущих шагов, используется префикс 0b (например, 0b000100101). Но вернемся все же к прерываниям. Сообразительный читатель уже смог подсчитать, что контроллер ATtiny13 имеет 9 различных источников прерываний. Рассмотрим их вкратце. Более подробно о них я расскажу, когда до них дойдет очередь в наших программах. 1. External Interrupt0 - Внешнее прерывание 0. Это прерывание генерируется, если произойдет одно из предустановленных изменений состояния входа, обозначенного, как int0. Если посмотреть на список выводов контроллера (второй шаг), то можно увидеть, что этот вход объединен с выводом РВ1. В зависимости от настроек это прерывание может генерироваться при изменении состояния входа РВ1 из "0" в "1", из "1" в "0", или на протяжении всего времени, пока на входе РВ1 будет "0". 2. Pin Change Interrupt0 - Прерывание по изменению состояния выводов. В отличие от предыдущего, это прерывание может быть вызвано любым изменением состояния любого из выводов, обозначенного как PCINTx (где х может быть от 0 до 5). В настройках задается, какие именно из входов могут вызвать данное прерывание. Рассмотренные прерывания относятся к внешним, так как реагируют на наступление событий извне. Все остальные прерывания - внутренние, они генерируются какими-либо периферийными модулями. 3. Overflow Interrupt0 - Прерывание по переполнению таймера 0. В контроллере ATtiny13 присутствует всего один 8-битный таймер, называемый "таймер 0". О режимах его работы я расскажу чуть ниже, а пока лишь замечу, что данное прерывание возникает при переполнении счетчика таймера, то есть при изменении его значения от 255 к 0. 4. EEPROM Write Complete - Прерывание по завершению записи в энергонезависимую память. 5. Analog Comparator Interrupt - Прерывание от аналогового компаратора. В принципе это прерывание тоже можно отнести к внешним. У контроллера ATtiny13, как и всех остальных, есть аналоговый компаратор, имеющий два входа - неинвертирующий (AIN0 - РВ0) и инвертирующий (AIN1 - РВ1). Когда величина напряжения на неинвертирующем входе становится больше напряжения на инвертирующем входе, генерируется данное прерывание. 6. Timer/Counter0 Compare Match A - Прерывание по совпадению значения счетчика таймера 0 со значением, записанным в специальном регистре OCR0A. 7. Timer/Counter0 Compare Match B - Прерывание по совпадению значения счетчика таймера 0 со значением, записанным в регистре OCR0B. 8. Watchdog Timeout - Прерывание по срабатыванию сторожевого таймера. Вообще сторожевой таймер - это довольно интересная штука. Она служит для отслеживания зависания программы и автоматического сброса контроллера при его обнаружении. При этом генерируется вот такое прерывание, чтоб пользователь мог знать, что же именно вызвало рестарт программы. 9. ADC Convertion Complete Handle - Прерывание по окончанию цикла преобразования АЦП. Модуль АПЦ, используемый в контроллерах, преобразует аналоговый сигнал в цифровой код не мгновенно, на это ему требуется некоторое время. Так вот по окончании этого самого времени и генерируется данное прерывание. Наиболее часто по своему опыту я использовал прерывания 1, 2, 3 и 6. Прерывания 4, 5, 7 и 8 не довелось применять ни разу, возможно, ввиду специфики разрабатываемых устройств. Теперь рассмотрим, каким же образом работает сам механизм прерываний в микроконтроллере. Мы уже договорились, что прерывания не вызываются из самой программы, а генерируются при наступлении определенных событий. Но обработчики прерываний-то нужно писать нам, и именно в тексте программы. Как же сказать контроллеру, что написанный нами кусок кода должен выполняться именно при наступлении какого-либо прерывания? Именно для этого и служит так называемая таблица векторов прерываний. Эта таблица располагается в начале памяти программ, причем номер прерывания соответствует адресу, по которому должен располагаться вызов подпрограммы обработки прерывания. Попробую объяснить попроще на конкретном примере. Допустим, мы задумали выполнять какое-либо действие по переполнению счетчика таймера 0. По вышеприведенной таблице находим, что данное прерывание находится под номером 3. Это значит, что в памяти программ в ячейке с адресом 3 должна находиться команда перехода на подпрограмму, которая будет выполняться при наступлении данного прерывания. Обычно используется команда безусловного перехода rjmp. Каким же образом указать контроллеру, что мы хотим поместить данную команду именно по этому адресу? Для этого используется еще одна директива org. Ее аргументом является число, соответствующее адресу, с которого последующая команда будет занесена в память контроллера. Мы тут уже достаточно много понаписывали, давайте теперь разберем конкретную задачу. Задание оставим то же, что и в третьем шаге. Заставим мигать светодиод LED2, но теперь с использованием не задержек, а прерывания от таймера 0. Текст программы представлен ниже. .include "F:\Prog\AVR\asm\Appnotes\tn13def.inc" .org 0 ;Задание нулевого адреса старта программы rjmp reset ;Безусловный переход к метке reset .org 3 ;Задание адреса прерывания по переполнению таймера 0 rjmp timer0_ovf ;Безусловный переход к метке timer0_ovf reset: ldi r16, RAMEND ;Загрузка в регистр r16 адреса верхней границы ОЗУ out SPL, r16 ;Копирование значения из r16 в регистр указателя стека SPL ldi r16, (1<<TOIE0) ;Загрузка в регистр r16 "1", смещенной на TOIE0 out TIMSK0,r16 ;Копирование значения из регистра r16 в регистр TIMSK0 ldi r16, (1<<CS00)|(1<<CS02);Загрузка двух "1", смещенных на CS00 и CS02 out TCCR0B,r16 ;Копирование значения из регистра r16 в регистр TCCR0B ldi r17,(1<<4) ;Загрузка в регистр r17 "1", смещенной на 4 разр. влево out DDRB, r17 ;Копирование из r17 в DDRB (РВ4 - выход) clr r16 ;Очистка регистра r16 sei ;Глобальное разрешение прерываний main: ;Основной цикл программы rjmp main ;Вернуться к метке main timer0_ovf: ;Прерывание по переполнению таймера 0 eor r16,r17 ;Исключающее ИЛИ регистров r16 и r17 out PORTB,r16 ;Копирование из r16 в PORTB reti ;Возврат из прерывания Как видите, объем наших программ все более возрастает, а количество неизвестных команд все более уменьшается. Как обычно, рассмотрим не встречавшиеся нам ранее. Команда clr имеет один операнд - РОН. Результатом ее выполнения является очистка указанного РОН, то есть запись во все его биты значения "0". По своему действию аналогична записи "ldi r16, 0". Команда sei не имеет операндов. Она разрешает включение механизма обработки прерываний в микроконтроллере. По умолчанию этот механизм отключен, и контроллеру совершенно одинаково, по какому адресу располагается какая команда. Если же его включить, то активируется таблица векторов прерываний, и теперь при наступлении какого-либо прерывания будет выполняться обращение к соответствующему адресу. Команда reti также не имеет операндов. Этак команда аналогична команде ret по своему назначению, только командой ret завершалась обычная подпрограмма, а reti завершает подпрограмму обработки прерывания. В чем же отличие между ними? Дело в том, что при наступлении прерывания в статусном регистре SREG (о котором я уже упоминал ранее) сбрасывается флаг, разрешающий прерывания. Таким образом, пока происходит обработка уже произошедшего прерывания, запрещается наступление новых прерываний. Так вот, команда reti, помимо восстановления счетчика программ из стека, снова устанавливает флаг разрешения прерываний. Так все хитро тут устроено. Теперь рассмотрим работу каждой строки. 1 строка. Подключение файла tn13def.inc к нашему ассемблерному файлу. 2 строка. Директивой org задается нулевой адрес в памяти программ, по которому будет располагаться следующая команда. 3 строка. Командой rjmp осуществляется переход на метку reset. Зачем это нужно? На самом деле во всех контроллерах присутствует еще одно прерывание, которое не требует отдельного разрешения и выполняется всегда после выхода контроллера из состояния сброса. Вектор обработки этого прерывания находится как раз по нулевому адресу, поэтому именно с этого адреса мы даем команду безусловного перехода к основной части программы. Ранее мы этого не делали, потому что и так всегда первой выполнялась команда, стоящая первой по тексту. Но теперь в условиях задействования прерываний нам придется каждый раз поступать именно так. 4 строка. Директивой org задается адрес 3. Если посмотреть на таблицу векторов прерываний, то можно видеть, что по этому адресу находится прерывание по переполнению таймера 0. 5 строка. Командой rjmp осуществляется переход на метку timer0_ovf. Именно с этой метки в нашей программе начинается обработчик прерывания по переполнению таймера 0. Таким образом, в результате выполнения строк 4 и 5, контроллер будет знать, в какую часть программы ему переходить при переполнении таймера 0. 6 строка. Пустая для отделения таблицы векторов прерываний от основной программы 7 строка. Метка reset. Отсюда будет стартовать наша программа при включении контроллера. 8 строка. Командой ldi в регистр r16 загружается адрес верхней границы ОЗУ. 9 строка. Копирование содержимого регистра r16 в регистр указателя стека SPL 10 строка. Здесь мы встречаем уже знакомую нам по предыдущему шагу конструкцию загрузки в регистр единицы, смещенной влево. Только вот величина смещения, названная TOIE0, выглядит несколько непонятно. Поясню ее чуть позднее, наберитесь терпения. 11 строка. Копирование содержимого r16 в опять же неизвестный нам регистр с именем TIMSK0. Итак, после выполнения строк 10 и 11 в неизвестном нам РВВ TIMSK0 оказывается единица в бите с опять же неизвестным нам названием TOIE0. Начнем разбираться, что оно такое. TIMSK0 - это регистр ввода-вывода, который содержит в себе биты, разрешающие или запрещающие различные прерывания от таймера 0. Тут мы подходим к одному важному моменту. Даже если мы задействовали механизм прерываний в контроллере, не все возможные источники прерываний смогут их вызвать. Помимо глобального разрешения всех прерываний нужно еще индивидуально разрешать те прерывания, которые нам необходимы. Поскольку в программе мы решили использовать только одно прерывание по переполнению таймера 0, то только его мы и разрешим, а все остальные оставим неактивными. Так вот, к чему я веду... Установка в единицу бита TOIE0 регистра TIMSK0 как раз и разрешает именно это прерывание. Кроме того, с помощью регистра TIMSK0 можно установить и остальные прерывания, связанные с таймером 0: бит OCIE0A разрешает прерывание по совпадению с регистром OCR0A, а бита OCIE0B - прерывание по совпадению с регистром OCR0B. Подытожим. В результате выполнения строк 10-11 разрешается прерывание по переполнению таймера 0. 12 строка. Снова имеем знакомую конструкцию сдвига, но теперь аж двух единиц. При этом одна единица смещается на бит CS00, а вторая - на бит CS02, и между полученными значениями выполняется операция побитового ИЛИ "|", что равносильно сложению в обычной алгебре. Таким образом, имеем в регистре r16 не одну, а две единицы, каждую в нужном месте. 13 строка. Копирование содержимого регистра r16 в регистр TCCR0B. Теперь снова разъяснение смысла того, что мы сделали. Таймер 0 может работать в различных режимах и иметь разную частоту тактирования. Для его настройки служат два регистра TCCR0A и TCCR0B. Сейчас я не буду расписывать что для чего служит. По мере необходимости я буду снабжать вас новыми знаниями. Пока же нам нужно знать только одно - как задать частоту работы таймера. Для этого имеется так называемый предделитель. Коэффициент деления предделителя задается тремя битами CS00, CS01 и CS02, расположенными в регистре TCCR0B. Зависимость его от значений этих битов представлена в таблице:
Поскольку мы установили в единицу биты CS02 и CS00, то тактовая частота контроллера будет разделена на 1024. Попробуем теперь посчитать частоту мигания светодиода. Тактовая частота контроллера равно 1 МГц, делитель частоты для таймера равен 1024, тогда тактовая частота таймера будет составлять 1000000/1024 = 977 Гц. Это значит, что 977 раз в секунду будет происходить наращивание счетчика таймера на 1. Переполнение наступает при достижении счетчиком значения 255, при этом он сбрасывается в 0. Значит, прерывание будет генерироваться 977 / 256 = 3,8 раза в секунду. Поскольку при каждом генерировании прерывания светодиод изменяет свое состояние на противоположное, то полная частота мигания составит 3,8 / 2 = 1,9 Гц. То есть светодиод будет мигать приблизительно 2 раза в секунду. Если нужна большая частота, то можно уменьшить множитель. А вот для уменьшения частоты нужно применять разные ухищрения, типа введения дополнительной инкрементируемой переменной. Может быть, если будет настроение, поведаю об этом в одном из последующих шагов. А пока вернемся к дальнейшему рассмотрению программы. Подытожим еще раз. В строках 12-13 мы задали тактовую частоту для таймера 0, равную 1/1024 частоты процессора. Таким образом, в строках 10-13 мы настроили таймер 0 и разрешили прерывание по его переполнению. 14 строка. Загрузка в регистр r17 единицы, смещенной на 4 разряда влево. 15 строка. Копирование регистра r17 в регистр DDRB для инициализации вывода РВ4 как цифрового выхода. 16 строка. Очистка регистра r16. Регистр этот будет использоваться для переключения светодиода LED2, и в принципе его можно было бы не очищать, поскольку на выход настроен всего один вывод. Но тогда бы происходило постоянное включение-выключение подтягивающих резисторов на тех входах, где осталась бы "1" в регистре r16, а это уже не комильфо. Так что очищаем, это правила хорошего тона в программировании. 17 строка. Команда sei разрешает глобально механизм обработки прерываний. Даже если мы уже разрешили прерывание, задав бит TOIE0 в регистре TIMSK0, оно все равно останется неактивным, пока не будет установлен флаг прерываний в регистре SREG командой sei. 18 строка. Пустая для отделения блока инициализации от основного цикла программы 19 строка. Метка main - начало основного цикла программы 20 строка. Команда безусловного перехода к метке main. Окончание основного цикла программы. "Как же так?" - спросит удивленный читатель - "Неужели в главном цикле ничего не делается?!". Да, ничего. Именно так должна выглядеть правильно составленная программа для контроллера. Сначала инициализация всех необходимых модулей, а затем пустой цикл с прерываниями. Все нужные действия будут выполняться по мере возникновения тех или иных событий, в нашем случае - при переполнении счетчика таймера 0. 21 строка. Пустая для отделения основного цикла программы от обработчика прерывания. 22 строка. Метка timer0_ovf - начало обработчика прерывания по переполнению таймера 0. Именно сюда нас отправляла команда безусловного перехода в пятой строке. 23 строка. Уже знакомая нам операция исключающего ИЛИ между регистрами r16 и r17. Поскольку в регистр r17 мы в строке 14 загрузили единицу только в четвертый разряд, то в регистре r16 будет переключаться именно этот бит, а он, как мы помним, соответствует выводу РВ4, на котором находится светодиод LED2. 24 строка. Копирование содержимого r16 в PORTB для непосредственного управления светодиодом LED2. 25 строка. Команда reti возвращает нас из прерывания снова к бесконечному циклу, который будет крутиться в ожидании следующего прерывания. Вот, собственно, и все, на этот раз. Я стал замечать, что с каждым разом статьи мои становятся все больше и пространее. Но что поделать: сложность материала тоже растет. В заключение традиционные уже задания для самостоятельного выполнения. 1. Изменить программу таким образом, чтобы мигал не только светодиод LED2, но и LED1, притом, чтобы мигание осуществлялось в противофазе. Но с одним условием. Объем hex-файла не должен измениться. Думайте, данных для выполнения этого задания вам достаточно. 2. Попытайтесь изменить программу так, чтобы светодиод мигал с частотой 0,5 Гц. Я уже говорил, что для этого придется использовать некоторые ухищрения, но, в принципе, вы тоже можете сообразить, как это сделать. 3. Добавить в программу обработку кнопок SB1 и SB2. При нажатии кнопки SB1 должно начинаться мигание светодиода LED2, а при нажатии кнопки SB2 - прекращаться. Дам небольшую подсказку. Обработку нажатия кнопок можно осуществлять в основном цикле программы, а начало или прекращение мигания производить путем установки или очистки бита TOIE0 в регистре TIMSK0. |
