но форматом команды) между наименованиями инструкций, переменных, регистров и т. п. должен быть.
Имена меток, конечно, можно присваивать произвольно. Как вы уже поняли, после точки с запятой идет комментарий — произвольная строка, на которую компилятор не обращает внимания. На новую строку комментарий не переходит: в случае длинного комментария в начале каждой следующей строки также надо ставить точку с запятой.
Но постойте, мы что, обязаны использовать все прерывания? Конечно, нет. Для неиспользуемых прерываний вместо команды rjmp <метка> следует поставить reti — выход из прерывания («return interrupt»). На самом деле здесь возможна и команда пор — пустая операция («по operation»). Я пробовал из любопытства, — все отлично работает. Но руководство рекомендует именно reti, т. к. при случайной инициализации прерывания оно все равно не будет выполняться, а команда пор в этом случае, очень вероятно, привела бы к «зависанию» программы либо выполнению совсем других, не предусмотренных нами, операций.
Лично я оформляю начало программы следующим образом: копирую таблицу в указанном ранее виде, а затем в начале каждой строки (кроме первой) автоматически меняю с помощью функции поиска-замены редактора «rjmp» на «reti; rjmp», вот так (старую команду я закомментировал):
rjmp RESET ;Reset Handler
reti; rjmp EXT_INT0 ;IRQ0 Handler
reti; rjmp EXT_INT1 ;IRQ1 Handler
…
Теперь заготовка начала программы готова: при необходимости в дальнейшем какого-нибудь прерывания, мы удаляем из нужной строки фрагмент reti;, а затем где-то в программе перед началом процедуры обработки этого прерывания ставим нужную метку, например:
rjmp RESET ;Reset Handler
rjmp EXT_INT0 ;IRQ0 Handler
…
EXT_INT0: ;процедура обработки прерывания INTO
…
reti ;окончание процедуры обработки прерывания INTO
Обратите внимание, что после метки в программе (но не внутри команды, которая на нее ссылается!) должно идти двоеточие, иначе компилятор не «поймет», что перед ним именно метка. Кроме того, меток не касается правило написания операторов в отдельных строках (потому что метка не оператор, а адрес), после метки и двоеточия в программе сразу может идти оператор.
Процедура RESET
Теперь обратимся к процедуре RESET, т. е. к истинному началу программы, что там должно быть? Когда контроллер доходит до команды вызова подпрограммы (call или rcall, см. далее), или в нем происходит прерывание, он должен сохранить состояние программного счетчика с тем, чтобы потом «знать», куда вернуться. Это происходит в специальной области памяти, которая называется стеком (stack). Потому в любой программе на AVR-ассемблере для семейств Mega и Classic первыми после метки reset должны идти следующие строки:
RESET:
ldi temp, low(RAMEND) ;загрузка указателя стека
out SPL, temp
ldi temp, high(RAMEND) ;загрузка указателя стека
out SPH, temp
Этими операторами вы указываете, где компилятору расположить программный стек, а именно в конце SRAM (что обозначается константой RAMEND, объявленной в соответствующем inc-файле, загляните). Служебные слова low и high означают, что от ramend нужно взять, соответственно, младший и старший байты. Для тех моделей, у которых объем SRAM не превышает 256 байт (из доступных в настоящее время это только модель 2313 во всех ее версиях), запись сокращается:
RESET:
ldi temp, RAMEND ;загрузка указателя стека
out SPL, temp
Заметки на полях
У многих представителей семейства Tuny (кроме, насколько я знаю, Tuny2313), в том числе тех, у которых объем SRAM также не превышает 256 байт, стек аппаратный, а не программный, потому там его расположение специально указывать не требуется (отметим, что в [2] в этой части допущена ошибка). По этой причине, в частности, для семейства Tuny не определены команды push и pop, которые для остальных моделей позволяют временно сохранять в стеке значение какого-либо регистра (а для некоторых процессоров, вроде i8086, в которых регистров общего назначения мало, это вообще одни из самых употребительных команд). Размер же аппаратного стека строго фиксирован, и в нем умещается только адрес возврата из подпрограммы или обработчика прерывания.
После задания стека желательно поставить следующие строки:
ldi temp,1<
out ACSR,temp ;выкл. аналог, компаратор
Почему желательно, а не обязательно? Потому что по умолчанию аналоговый компаратор всегда включен и, соответственно, расходует питание. Если вы его не используете, то зачем лишнее потребление? Правда, это практически не скажется на потреблении в нормальном режиме работы, т. к. доля компаратора здесь очень мала. Вставка этих строк становится критичной только в том случае, если применяются режимы энергосбережения. С другой стороны, если компаратор нужен (см. главу 14), то, конечно, эти строки следует исключить.
После всего этого в процедуре reset обычно идет секция инициализации, где разрешаются прерывания, устанавливаются состояния выводов портов, инициализируются начальные значения переменных и т. п. Примеры мы еще встретим в тексте этой книги неоднократно. Эта секция обязательно должна заканчиваться командой sei — общим разрешением прерываний, т. к. по умолчанию они запрещены.
Теперь вроде бы все готово к работе, но что будет делать контроллер в ожидании прерываний? Ведь основная функциональность большинства микропрограмм сосредоточена именно в их обработчиках, и в простейшем случае контроллер попросту ничего не должен делать, пока не придет сигнал очередного прерывания. Поэтому простейшая программа должна заканчиваться пустым циклом:
…
sei ;разрешаем прерывания
STOP:
rjmp STOP
На самом деле в этом цикле можно делать и что-то полезное, например, войти в один из режимов энергосбережения, или отслеживать изменение состояния какого-то вывода, или момент прихода байта через UART и т. п. — в дальнейшем мы увидим примеры подобных действий. Именно внутри такого цикла работают немногочисленные программы, вообще не использующие прерываний.
Определения переменных, констант и подключение внешних файлов
Определения играют в процессе написания ассемблерной программы важную роль, не менее важную, чем секция объявления переменных в программе на языке высокого уровня. Сравните две записи:
….
mov r16,r11
cpi r1l6,$11
…
и
…
mov temp,counter
cpi temp,max_value
…
И та, и другая запись верна, но вторая значительно более «читабельна», правда? Одно дело иметь программу, в которой фигурируют регистры (еще слава богу, что не их абсолютные адреса) и числа, совсем другое — работать с «говорящими» именами переменных и констант. Идентификатором temp (от слова «temporary» — временный) обычно обозначают рабочую переменную, которая не предназначена для долговременного хранения данных, counter — в переводе «счетчик», max_vaiue — «максимальное значение». Но если команды компилятор «знает» (мнемоника команд фиксирована и их смысл мы расшифруем немного далее), то как ему узнать, что мы имели имели в виду под этими самыми temp и counter?
Для этого служит секция определений имен переменных и констант, которую чаще всего располагают в самом начале текста программы, еще до векторов прерываний, или в отдельном внешнем файле, если определений много, и они очень загромождают текст. Именно такие макроопределения и содержат те самые файлы с расширением inc, которые мы копировали из AVR Studio, — для каждого контроллера свои. В результате чего мы можем писать mov GIMSK, temp вместо mov $3F,r16.
Для того чтобы компилятор нашел эти определения, inc-файл нужно подключить к нашей программе. Это делается с помощью директивы include, как в языке С, например (обратите внимание на точку перед include):
.include «2313def.inc»
В данном случае подключается файл с макроопределениями для контроллера AT90S2313. Подобной строкой должен начинаться текст любой программы. Причем файл должен соответствовать выбранному контроллеру, иначе программа может и не заработать: иногда на ошибку вам укажут при компиляции, иногда это выявится только во время исполнения.
Отметим, что имя файла здесь может быть абсолютно произвольным, — по директиве include компилятор, «не думая», разыщет файл с указанным именем (разумеется, он должен находиться в текущем каталоге, или для него должен быть указан полный путь), скопирует из него текст и вставит этот текст в том месте вашей программы, где расположена директива. И только потом начнет «разбираться». С директивами include надо быть аккуратным, если файл содержит команды, а не только определения, то вставлять его нужно уже не в начале текста, а после всех векторов прерываний, иначе выполнение программы начнется с него.
Но имени temp (не говоря уж о counter) вы в inc-файлах не найдете — это нестандартное определение и относится уже к нашей «епархии». Создать свои определения можно с помощью директив def (definitions) для переменных и equ (equvalent) для констант вот так:
.equ max_value = $11 ;максимальное значение = 17
.def temp = r16 ;регистр r16 есть переменная temp
.def counter = r05 ;регистр r05 есть переменная counter
Эти определения также обычно располагают в начале текста программы. Теперь компилятор разберется, что к чему в нашем примере ранее. Только учтите, что никаких проверок, кроме синтаксических, тут не выполняется, потому можно объявить два разных имени для одного регистра, и они будут восприниматься как синонимы: