cli, запрещающей прерывания (которая в начале программы лишняя, т. к. они все равно запрещены, но, видимо, поставлена на всякий случай). Следующая команда будет начинаться с байта $Е5, и первая тетрада в ней обозначает код команды ldi (1110 — проверьте!), а пятерка, очевидно, есть фрагмент адреса конца памяти (RAMEND), который в силу довольно сложного формата записи получается на самом деле равным $025F (см. значение младшего байта, равное $2F). Такое значение соответствует значению ramend, определенному в inc-файлах для МК с 512 байтами встроенного ОЗУ[12]. Все, как и должно быть. Если мы обратим внимание опять на первую-вторую строки с данными, то увидим повторяющийся фрагмент «1895», который, как легко догадаться из материала предыдущего раздела, должен быть командой reti (если проверите по справочнику, то так оно и окажется).
Как видите, разобраться довольно сложно, но при некотором навыке и наличии под рукой таблицы двоичных кодов команд вполне возможно. Именно так работает программа, которая превращает код обратно в текст — дизассемблер (он входит в AVR Studio). А зачем это может понадобиться на практике? Дело в том, что в памяти программ часто хранят те константы, которые предположительно не будут изменяться в процессе эксплуатации, например, устанавливаемые по умолчанию значения какой-то величины. Но, разумеется, по истечении некоторого времени эти константы обязательно захочется изменить. И если у вас текст программы по каким-то причинам отсутствует (например, программа взята из публикации в журнале или скачана с радиолюбительского сайта), а загрузочный hex-файл имеется, то всегда можно «хакнуть» исходный код, и немного подправить его под свои нужды.
Команды перехода (передачи управления)
В языках высокого уровня была всего одна команда перехода на метку (GOTO), и то Дейкстра на нее «набросился». А в ассемблере AVR таких команд пруд пруди, целых 33 шутки! Зачем? На самом деле без доброй половины из них, если не больше, можно обойтись во всех жизненных случаях, т. к. они в значительной степени взаимозаменяемы. Разнообразие это, если угодно, дань памяти великому программисту — для повышения читаемости программ. Мы рассмотрим только ключевые команды из этого перечня.
С командами безусловного перехода rjmp и jmp мы уже познакомились достаточно подробно, так что сразу перейдем к вызову процедур rcall и call (официальный язык фирменных руководств Atmel предпочитает вместо процедуры упоминавшийся нами консервативный термин — подпрограмма, subroutine). Синтаксис у них точно такой же, как у команд безусловного перехода, и, по сути, это тот же самый переход по метке. И разница между этими двумя командами аналогичная: call работает в пределах 64 К адресов памяти (или до 8 М в соответствующем контроллере, поддерживающем такой объем адресного пространства), занимает 4 байта и выполняется за 4 цикла, a rcall годится только для МК с объемом памяти не более 8 кбайт, но занимает 2 байта и выполняется за 3 цикла. Мы в дальнейшем будем пользоваться только командой rcall.
Отличаются они от команд безусловного перехода тем, что здесь в момент перехода к процедуре контроллер автоматически сохраняет в стеке адрес текущей команды для того, чтобы потом знать, куда вернуться (потому длительность выполнения этих команд на такт больше, чем для просто перехода). А как МК «узнает», когда именно нужно возвращаться? Для этого каждая процедура-подпрограмма оформляется специальным образом: не отличаясь поначалу ничем от любого другого участка программного кода, обозначенного меткой, в месте возврата она должна содержать специальную команду — ret (от return — «возврат»). По этой команде МК извлекает из стека сохраненное содержимое счетчика команд и продолжает выполнение прерванной основной программы.
Заметим, что стек — одно из самых употребительных понятий в программировании. Наличие программного стека позволяет, например, организовать привычное для языков высокого уровня разделение переменных на локальные и глобальные. Во всех последующих программах в этой книге мы будем пользоваться только глобальными переменными, и при нехватке регистров общего назначения для них мы будем использовать ячейки SRAM. (Привычку употреблять преимущественно глобальные переменные автор даже перенес в свой стиль программирования на Delphi, как вы увидите из главы 18.) Однако в больших проектах локальные переменные зачастую бывает просто необходимы (например, когда одна и та же процедура вызывается в разных местах с исходными данными, хранящимися в различных регистрах). Механизм организации локальных переменных с помощью стека приведен в листинге 13.2 (он в точности такой же, как это происходит при компиляции в «настоящих» языках программирования).
Листинг 13.2:
push var_1 ;переменная var_1 в программе помещается в стек
push var_2 ;переменная var_2 в программе помещается в стек
rcall procedure ;вызывается процедура
pop var_2 ;после нее результат извлекается из стека
pop var_1 ;второй результат извлекается из стека
…
procedure: ;в процедуре извлекается локальная
pop var_loc2 ;переменная var_loc2 со значением var_2
pop var_loc1 ;и переменная var_loc1 со значением var_1
… ;расчеты, расчеты…
push var_loc1 ;результат — в стек
push var_loc2 ;результат — в стек
ret ;возврат из процедуры
В качестве переменных var_1 и var_2 возможны любые регистры, при этом действия внутри процедуры всегда будут совершаться с заданными регистрами var_loc. Обратите внимание, что когда таких переменных несколько, важно соблюдать правильный порядок их помещения в стек и извлечения оттуда, согласно принципу «первым вошел — последним вышел» (в программах на языках высокого уровня за порядком переменных в стеке следят специальные форматы вызова функций типа stdcall и подобные).
Аналогично происходит обработка прерываний, только специальной команды, как вы знаете, там нет, вызов производится обычным переходом rjmp или jmp, но поскольку он осуществляется с определенного адреса (там, где стоит вектор прерывания), то контроллер делает то же самое: сохраняет в стеке адрес командного счетчика, на котором выполнение основной программы было грубо нарушено, начинает выполнять прерывание и ожидает команды возврата, здесь она записывается как reti (return interrupt).
Еще одна важнейшая группа команд ветвления программы — команды перехода по состоянию отдельного бита в указанном регистре (sbrs, sbic и т. п.). Они очень удобны для организации процедур, аналогичных оператору выбора CASE в языках высокого уровня, но, к сожалению, обладают непривычной логикой: «пропустить следующую команду, если условие выполняется» и для новичка могут показаться слишком заумными. В качестве примера приведу довольно сложную по логике работы, но характерную для микроконтроллерной техники процедуру, в которой задача формулируется так: при наступлении некоторого условия мигать попеременно зеленым и красным светодиодами (СД).
Предположим, что условие мигания задается состоянием бита 3 в некоем рабочем регистре, который назовем регистром флагов — Flag (не путать с «официальным» регистром флагов SREG, о котором далее). Если бит 3 регистра Flag равен единице (установлен), надо мигать, если нет (сброшен) — оба СД погашены.
Красный СД подсоединен к выходу порта В номер 5, а зеленый — к выходу порта С номер 7 (разумеется, это могут быть любые другие выводы других портов).
Текущее состояние СД задается битом 4 в том же регистре флагов Flag.
Алгоритм работы такой программы на языке Pascal (листинг 13.3) описывается типичным вложенным оператором выбора.
Листинг 13.3
case <бит 3 per. Flag> of
0: <погасить оба СД>
1: case <бит 4 per. Flag> of
1: <устанавливаем Port С, вых. 7>; //горим зеленым <сбрасываем
Port В, вых. 5>; //гасим красный
<сбрасываем бит 4 Flag>; {следующий раз горим красным}
0: устанавливаем Port В, вых. 5>; //горим красным
<сбрасываем Port С, вых. 7>; //гасим зеленый
<устанавливаем бит 4 Flag>; //следующий раз горим зеленым
end;
end;
Разобравшийся в алгоритме читатель уже, несомненно, задает вопрос — а как обеспечить цикличность? Для этого подобный код включают в обработчик события по таймеру с секундным, например, интервалом. Причем, что характерно, и в микроконтроллере, и в операционной системе Windows это происходит абсолютно одинаково: инициализируется системный таймер (в МК для этого надо разрешить соответствующее прерывание), задается интервал его срабатывания (в Windows это одна команда, в МК их несколько больше) и — вперед! Но с таймерами мы будем разбираться далее по ходу дела, а пока посмотрим, как тот же алгоритм реализовывается в МК (листинг 13.4).
Листинг 13.4
sbrs Flag,3 ;если флаг 3 стоит, будем мигать
rjmp dark ;иначе будем гасить
sbrs Flag,4 ;если флаг 4 стоит, будем гореть зеленым
rjmp set4 ;иначе красным
cbr Flag, 0Ь00010000 ;следующий раз горим красным
cbi PortB,5 ;гасим зеленый
sbi PortC,7 ;горим зеленым
rjmp continue ;все готово
set4: ;если флаг 4 не стоит, будем гореть красным
sbr Flag, 0Ь00010000 ;следующий раз горим зеленым
cbi PortC,7 ;гасим красный
sbi PortB,5