ГлавнаяРегистрацияВход Сайт Сокола Сергея Среда, 01.05.2024, 05:49
  Каталог статей Приветствую Вас Гость | RSS

 
 
Главная » Статьи » Мои статьи

Ассемблер 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. Выполнить задание из предыдущего шага с вынесением цикла задержки в отдельную подпрограмму.

Если у вас возникнут вопросы, задавайте их на форуме или здесь в виде комментариев к статье.

Желаю успехов!
Категория: Мои статьи | Добавил: mimino (25.12.2011)
Просмотров: 15019 | Комментарии: 22 | Рейтинг: 2.7/6
Всего комментариев: 10
10 Savage  
0
Прочитал основную задачу и начал ваять программу на основе предыдущих уроков. Вот что вышло:

.include "tn13def.inc"

.equ LED1 = PB0
.equ SB1 = PB3

ldi r16, RAMEND
out SPL, r16

sbi DDRB, LED1 ;Линия РВ4 - выход
cbi DDRB, SB1 ; Вход - кнопка
sbi PORTB, SB1 ; Подтягивающий резистор на кнопку
sbi PORTB, LED1 ;Установка РВ4 в 1 (выключение светодиода)

main:
sbic PINB, SB1
rjmp main
rcall delay

wait:
sbis PINB, SB1
rjmp wait

sbic PINB, LED1 ;Если РВ4=0 (светодиод зажжен), то пропустить след. строку
cbi PORTB, LED1 ;Установка РВ4 в 0 (включение светодиода)
sbis PINB, LED1 ;Если РВ4=1 (светодиод погашен), то пропустить след. строку
sbi PORTB, LED1 ;Установка РВ4 в 1 (выключение светодиода)

rjmp main

delay:
push r16

ldi r16, 100 ;Загрузка значения в регистр r16

count:
subi r16, 1 ;Вычитание 1 из регистра r16
brcc count ;Если не было переноса вернуться к метке delay

pop r16
ret

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

7 Владимир  
0
Сергей!
В каком направлении двигаться со второй кнопкой ?

8 mimino  
0
Тут можно написать два обработчика кнопок, просто один за другим. И в каждом - выполнять операции над своим светодиодом.

6 Владимир  
0
Получилось !!!
При загрузке в r16 применил 2 И 3, третий бит установился при запуске, а далее ИЛИ между битом 3, 4 . r16/
Довольно просто ! Несколько дней подбирал команду все без успешно.
Спасибо Сергей за идею.

5 Владимир  
0
Сейчас попробую

3 Владимир  
0
Что-бы работала в инверсии, не могу команду подобрать для этого задания.

4 mimino  
0
Владимир, тут же все просто. Надо задать начальные условия так, чтобы при старте один светодиод горел, а второй - нет. А потом их переключать одновременно, тогда все будет как надо

1 Владимир  
0
Сергей, есть ли команда противоположная EOR ?

2 mimino  
0
В каком смысле "противоположная"?

Имя *:
Email *:
Код *:
 
 
Категории раздела
Мои статьи [20]

Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

Вход на сайт

Поиск

Посетители

Погода
GISMETEO: Погода по г.Мариуполь

 

Copyright MyCorp © 2024
Бесплатный конструктор сайтов - uCoz