Рад снова приветствовать постоянных читателей, ожидающих чего-то нового и интересного. В этот раз постараюсь оправдать ваши ожидания.
Давайте сегодня несколько нарушим регламент, и вместо теоретического введения я сразу напишу поставленную перед нами задачу, а затем попробуем подумать, как ее лучше выполнить.
Итак, нам предстоит выполнить следующее. Составить программу, которая бы автоматически выдавала при помощи светодиода LED1 сигнал SOS после нажатия на кнопку SB2.
Я нарочно не стал конкретизировать задачу, постараемся самостоятельно прийти к тому, как ее лучше выполнить.
Для начала разберемся с самим условием. Нам необходимо при помощи светодиода LED1 выдавать сигнал SOS. Сигнал этот в азбуке Морзе имеет следующий вид:
... --- ...
То есть три символа: три точки, три тире и снова три точки. При этом в телеграфной азбуке приняты следующие правила:
- длительность пауз между знаками внутри буквы равна длительности точки;
- длительность тире равно тройной длительности точки;
- длительность пауз между буквами равна тройной длительности точки.
Попытаемся с учетом сказанного написать алгоритм включения-выключения светодиода LED1. Длительность точки примем за единицу и будем ее отображать одним символом, а длительность остальных элементов составим с учетом вышеизложенных правил. Состояние включенного светодиода будем обозначать цифрой "0", а состояние выключенного - цифрой "1". Я так делаю специально, почему - потом объясню.
Алгоритм будет иметь следующий вид:
01010 111 00010001000 111 01010
S пауза O пауза S
Поскольку светодиод LED1 включается подачей на него "0", а выключается подачей "1", то последовательная выдача на него написанных выше нулей и единиц как раз и будет соответствовать поставленной задаче.
Переключение нулей и единиц должно происходить не мгновенно, поэтому в данном случае будет удобно использовать таймер 0, по прерыванию от которого будет осуществляться выдача очередного значения.
По поводу запуска выдачи сигнала SOS по нажатию на кнопку SB2 тоже есть некоторые нюансы. Во-первых, давайте обработку нажатия кнопки осуществим при помощи внешнего прерывания 0. Мы его еще не рассматривали, хотя оно имеет несколько интересных особенностей. Во-вторых, давайте проанализируем саму постановку задачи. Нам нужно при нажатии на кнопку запускать последовательность, выводимую на светодиод, а по окончании этой последовательности светодиод погасить и ожидать следующего нажатия кнопки. Тут можно задачу решить разными путями. И, возможно, сообразительный читатель увидит другое решение, я же предлагаю следующий вариант: при запуске программы в блоке инициализации не разрешать прерывание от таймера 0, а разрешать его только при нажатии на кнопку SB2. После этого выдать записанную последовательность, и после выдачи последнего знака снова запретить прерывание от таймера уже внутри самого обработчика прерывания таймера. Возможно, описание выглядит громоздко, однако на практике это все не так сложно.
И еще один интересный вопрос, состоящий в том, где и как удобней хранить вышеописанную последовательность, чтобы она занимала меньше места и была всегда доступна для считывания и обработки. Тут мы плавно переходим к вопросам, написанным в анонсе статьи.
Большинство контроллеров AVR (и ATtiny13 - не исключение) имеет три области памяти. При этом каждая из областей независима от остальных и имеет собственную адресацию и область применения. Я уже ранее упоминал о них вскользь, теперь пришло время разобраться подробнее. Итак, что же это за области?
1. Память программ (Program segment) представляет собой энергонезависимую память, в которой непосредственно располагается код исполняемой программы. То есть, когда мы выполняем запись полученного hex-файла в контроллер, то эти данные записываются именно в область памяти программ. Термин "энергонезависимый" обозначает, что содержимое этой области остается в целости, даже если выключается напряжение питания.
2. Память данных (Data segment) является энергозависимой памятью, то есть ее содержимое не сохраняется при отключении питания. Эта область памяти разбита на три участка, два из которых нам уже хорошо знакомы. Во-первых, это 32 регистра общего назначения. Во-вторых, это 64 регистра ввода-вывода (в каждом контроллере их может быть разное количество, однако под эту часть памяти во всех контроллерах выделено 64 байта). И в третьих, это оперативная память. Мы ее задействовали только для формирования стека, однако можно хранить в ней и переменные, и константы. Оперативная память доступна как для чтения, так и для записи. В скобках замечу, что хотя я уже написал не одну программу на ассемблере, но еще ни разу у меня не возникло необходимости применения оперативной памяти для других целей, кроме организации стека. Поэтому данный вопрос я в рамках этого цикла статей не рассматривал, и не планирую в дальнейшем. Важное замечание. Все три участка памяти данных имеют сквозную адресацию, при этом адреса располагаются именно в том порядке, в котором я их описал: с 0 по 31 адрес находятся РОН, с 32 по 95 - РВВ, с 96 и выше - оперативная память.
3. Электрически стираемая энергонезависимая память (EEPROM segment) как следует из названия, является энергонезависимой, предназначена для хранения каких-то величин, значения которых должны изменяться редко. В основном в ней хранятся настроечные значения, которые считываются при старте программы и служат для инициализации тех или иных модулей, и куда записываются данные, нужные для работы программы. Эта область памяти у некоторых конроллеро отсутствует.
В принципе, EEPROM вполне может подойти для хранения нашей последовательности, однако доступ к ней осуществляется при помощи специальных регистров, что никак не способствует уменьшению объема кода программ. Кроме того, данные для EEPROM памяти при ассемблировании записываются в отдельный файл с расширением "eep", который необходимо зашивать в контроллер отдельно. С этой областью памяти я довольно активно работал при написании программ на Си (собственно, как и с оперативной), поэтому, возможно, в дальнейшем я расскажу об использовании EEPROM.
Так к чему я пытаюсь вас подвести... EEPROM использовать мы не будем, в оперативной памяти хранить информацию тоже не удобно, она энергозависимая. Выходит, что придется записывать нужную последовательность в память программ. Но есть ли такая возможность в принципе? Оказывается, есть. Все современные контроллеры AVR поддерживают так называемое самопрограммирование. Под этим термином понимается изменение памяти программ самой программой. Таким образом по большому счету можно создавать программы с переменной структурой, самостоятельно изменяюще себя в зависимости от тех или иных условий. Также, с использованием этого принципа строятся так называемые загрузчики, именуемые в литературе бутлоадерами (bootloader). О том, что это такое, читайте в дополнительной литературе, вещь это весьма перспективная (рекомендую ознакомится со следующими проектами:
http://www.fischl.de/avrusbboot/ http://www.obdev.at/products/vusb/bootloadhid.htmlэто то, что я использовал сам, так что могу рекомендовать для изучения и повторения).
Мы же будем использовать память программ для хранения в ней только констант, задающих описанную выше последовательность. Давайте подумаем, как удобней ее хранить для экономии места. Если подсчитать количество нулей и единиц в последовательности, получится всего 27 цифр. Можно каждую из них хранить в отдельном байте, тогда нам понадобится аж 27 байт, что является непростительным расточительством.
Мы пойдем другим путем. Наша последовательность состоит только из нулей и единиц. А что, если и записать ее в виде двоичного кода. Тогда 27 значений можно разместить всего в четырех байтах (вообще в 4-х байтах можно разместить до 32 значений, но трех нам будет мало). Кроме того, нам нужно гасить светодиод после окончания последовательности, а, значит, в конце понадобится добавить еще одну единицу. Итого, 28 значений. Оставшиеся 4 бита равномерно распределим между началом и концом последовательности. В итоге получим следующий ряд:
11 01010 111 00010001000 111 01010 111
S пауза O пауза S
Разобьем его по 8 бит для записи в четыре байта:
11010101 11000100 01000111 01010111
1-й байт 2-й байт 3-й байт 4-й байт
Ну хорошо, мы разбили последовательность на байты, но каким же образом их можно использовать?
Запись констант в память программ осуществляется директивой .db после которой через запятую указываются записываемые байты. Количество этих байт должно быть четным, поскольку память программ имеет пословную организацию, то есть каждая ячейка такой памяти содержит два байта. Если количество байт будет нечетным, автоматически в конец последовательности добавится байт, равный 0.
Пример использования директивы .db рассмотрим уже при описании программы.
Итак, мы каким-то образом сохранили наши байты в память программ, но надо же их каким-то образом и считывать оттуда. Тут мы подходим еще к одному важному вопросу - способам адресации.
В контроллерах AVR применяется два основных способа адресации: прямая и косвенная. Каждый из этих способов имеет несколько разновидностей, но на них я останавливаться не буду, так как это не суть важно.
До сих пор все команды, используемые нами, имели прямую адресацию, хотя мы об этом и не подозревали. Рассмотрим конкретный пример, неоднократно нами применяемый:
ldi r16, 1
out DDRB,r16Мы ранее никогда не задумывались над тем, что же эти строки означают, принимая их как должное. Взглянем теперь на них под другим углом. Я всегда при описании команды ldi писал что-то типа "загрузка в r16 значения 1". Но о том, что есть какой-то r16 знаем мы, но не контроллер. Для него эта строка имеет приблизительно следующую интерпретацию: загрузить в память данных по адресу 16 число, равное 1. А вторую строку он воспринимает примерно так: взять значение из памяти данных по адресу 16 и записать его также в память данных по адресу 55 (именно там располагается регистр DDRB).
Вот это и есть прямая адресация. Мы в самой команде указываем адрес ячейки, а также значение, которое нужно считать из нее или записать в нее. В случае команды out мы осуществляем обмен значениями между ячейками памяти, адреса которых также явно указаны в самой команде.
Тут как-то все настолько очевидно, что мы над этим даже и не задумывались. Но вот представим себе такую задачу, которая, собственно и встает перед нами. Мы будем знать, по какому адресу расположены в памяти программ наши
константы, но с ними не будет ассоциировано никакого регистра, поэтому
прямая адресация тут не подойдет. Но мы можем записать в любой регистр адрес ячеек, где находятся наши константы. Тогда получается, что нам нужно будет считать значение не из самого регистра, а из той ячейки памяти, адрес которой указан в регистре. Вот это и называется косвенной адресацией.
Для косвенной адресации годится не любой регистр, а только строго определенные. Это РОН r26-r31, которые имеют даже специальные имена: X, Y, Z. При этом каждый из этих регистров является 16-битным, то есть содержит два РОН. Это сделано для расширения диапазона адресуемых ячеек памяти. Поскольку регистры состоят из двух байт, то каждый из них имеет в своем названии либо букву H (старший байт), либо букву L (младший байт). Тогда имеем следующее соответствие между именами адресных регистров и именами РОН:
XL - r26, XH - r27
YL - r28, YH - r29
ZL - r30, ZH - r31
Применение косвенной адресации лучше рассмотреть на конкретном примере. Таким образом мы плавно переходим к тому, что пора явить вам написанную мною программу, реализующую поставленную нами еще в самом начале задачу.
.include "F:\Prog\AVR\asm\Appnotes\tn13def.inc"
.org 0 ;Задание нулевого адреса старта программы
rjmp reset ;Безусловный переход к метке reset
.org 1 ;Адрес, по которому находится вектор внешнего прерывания 0
rjmp int_0 ;Безусловный переход к метке int0
.org 3 ;Адрес, по которому находится прерыв-е по переполнению таймера 0
rjmp tmr0 ;Безусловный переход к метке tmr0
.org 4 ;Адрес, с которого начинается расположение констант
.db 0b11010101, 0b11000100, 0b01000111, 0b01010111; Константы в памяти прогр.
reset: ;Начало раздела инициализации контроллера
ldi r16,RAMEND ;Загрузка в регистр r16 адреса верхней границы ОЗУ
out SPL, r16 ;Копирование значения из r16 в регистр указателя стека SPL
ldi r17,1 ;Загрузка в регистр r17 единицы
out DDRB, r17 ;Копирование из r17 в DDRB (РВ0 - выход)
ldi r18,1|(1<<1) ;загрузка в r18 единиц в нулевой и первый биты
out PORTB,r18 ;Включение подтягивающего резистора на входе РВ1
ldi r16,(1<<ISC01);Загрузка единицы в бит ISC01 регистра r16
out MCUCR,r16 ;Копирование r16 в MCUCR
ldi r16,(1<<INT0);Загрузка в регистр r16 единицы в разряд INT0
out GIMSK, r16 ;Разрешение прерывания по изменению состояния выводов
ldi r16,(1<<CS00)|(1<<CS02);Загрузка в регистр r16 единиц в CS00 и CS02
out TCCR0B,r16 ;Копирование значения из регистра r16 в регистр TCCR0B
sei ;Глобальное разрешение прерываний
main: ;Основной цикл программы
rjmp main ;Конец основного цикла программы
int_0: ;Начало обработчика внешнего прерывания 0
sbic PINB, 1 ;Если РВ1=0 (кнопка SB2 нажата), пропустить след. строку
rjmp exit_int_0 ;Переход к выходу из прерывания
rcall delay ;Вызов подпрограммы задержки на дребезг контактов
wait: ;Цикл ожидания, пока нажата кнопка
sbis PINB, 1 ;Если РВ1=1 (кнопка SB2 отпущена), пропустить след. строку
rjmp wait ;иначе перейти к началу цикла ожидания
rcall delay ;Вызов подпрограммы задержки на дребезг контактов
sbis PINB, 1 ;Если РВ1=0 (кнопка SB2 нажата), пропустить след. строку
rjmp exit_int_0 ;Переход к выходу из прерывания
ldi r16, (1<<TOIE0);Загрузка в регистр r16 единицы в TOIE0
out TIMSK0,r16 ;Копирование значения из регистра r16 в регистр TIMSK0
clr r21 ;Очистка регистра r21
ldi r24,3 ;Загрузка в r24 значения 3 (0-3 байты в памяти программ)
clr ZH ;Очистка старшего байта адреса
ldi ZL,8 ;Установка младшего байта адреса (4х2=8)
exit_int_0: ;Метка для перехода к возврату из прерывания
reti ;Возврат из подпрограммы обработки прерывания
tmr0: ;Начало обработчика прерывания по переполнению таймера 0
cpi r21,0 ;Сравнение регистра r21 с нулем
brne shift ;Если не равен, то перейти к сдвигу байта
lpm r22,Z+ ;Иначе загрузить в r22 значение из памяти программ
ldi r23,8 ;Загрузка в r23 числа 8 (8 бит в байте)
ser r21 ;Установка битов регистра r21 в единицы
shift: ;Смещение байта
sbrs r22,7 ;Если 7-й бит в регистре r22 равен 1, пропустить следующую строку
cbi PORTB,0 ;Сбросить 0-й бит PORTB (включить LED1)
sbrc r22,7 ;Если 7-й бит в регистре r22 равен 0, пропустить следующую строку
sbi PORTB,0 ;Установить 0-й бит PORTB (выключить LED1)
lsl r22 ;Логический сдвиг регистра r22 влево
subi r23,1 ;Вычитание из r23 единицы
breq next_dig ;Если полученное значение равно 0, то загрузить следующий байт
rjmp exit_tmr0;иначе перейти к выходу из прерывания
next_dig: ;Загрузка следующего байта
clr r21 ;Очистка регистра r21
subi r24,1 ;Вычитание единицы из r24
brcc exit_tmr0;Если результат равен 0, то выйти из прерывания
clr r16 ;Очистка регистра r16
out TIMSK0,r16;Копирование значения из регистра r16 в регистр TIMSK0
exit_tmr0: ;Метка для перехода к возврату из прерывания
reti ;Возврат из подпрограммы обработки прерывания
delay: ;Начало подпрограммы задержки
ldi r19, 255 ;Загрузка значения в регистр r18
ldi r20, 63 ;Загрузка значения в регистр r19
del: ;Цикл задержки
subi r19, 1 ;Вычитание 1 из регистра r18
sbci r20, 0 ;Вычитание 0 из регистра r19 с учетом переноса
brcc del ;Если не было переноса вернуться к метке del
ret ;Возврат из подпрограммыОбъем программы, конечно, поначалу вызывает оторопь, ну а после изучения ее оторопь станет вызывать сам алгоритм. Но сначала, как уже было заведено, рассмотрим новые и неизведанные команды.
Команда ser имеет всего один операнд - РОН. По своему действию она противоположна команде clr. В результате ее выполнения все биты указанного РОН устанавливаются в единицу, тем самым в регистре устанавливается значение 255.
Команда brne имеет один операнд - метку. Это еще одна команда условного перехода. Переход к указанной метке осуществляется в том случае, если результат предыдущей операции был не равен нулю. Кроме того, эта команда еще называется "переход, если не равно", поскольку обычно используется после команды сравнения.
Команда breq также имеет своим операндом метку. Эта команда противоположна предыдущей. Переход к указанной метке осуществляется, если результат предыдущей операции был равен нулю. Еще эта команда называется "переход, если равно".
Команда lsl имеет один операнд - РОН. В результате ее выполнения осуществляется логический сдвиг указанного РОН на один бит влево. При этом старший бит сохраняется в бите С регистра SREG, а в младший бит записывается нуль.
Команда lpm является довольно хитрой командой. В зависимости от применения она может иметь три разных варианта синтаксиса. Рассмотрим их все, но чуть позже. А пока о ее назначении. Команда lpm как раз и предназначена для копирования байта из памяти программ в РОН. При этом адрес, откуда будет производиться копирование, должен быть записан только в регистр Z. Данная команда может либо не иметь операндов, либо иметь два операнда. Вот теперь обещанные подробности.
Если команда lpm записана без операндов, то производится копирование значения из памяти программ из адреса, указанного в регистре Z в регистр r0. Если мы хотим считать значение не в r0, а в какой-то другой РОН, то используется другой синтаксис команды. В этом случае команда имеет два операнда: первый - это РОН, в который будет производиться копирование, а второй - регистр Z, в который предварительно должен быть загружен адрес. Кроме того, есть еще третий вариант записи этой команды. Допустим, что нам необходимо считать несколько последовательно записанных байт (как в нашем случае). Тогда мы должны после каждого считывания увеличивать адрес на единицу. И при этом нужно использовать дополнительную команду, что приведет к дополнительному увеличению программы. Оказывается есть возможность одной командой считать значение из указанного адреса, а затем увеличить адрес на единицу. Для этого в качестве второго операнда надо указать не просто "Z", а "Z+". Ассемблер поймет такую запись правильно, не переживайте.
Вот, теперь описание команд можно считать оконченным, и пришло время переходить к самому сложному (ну, как для меня) - описанию алгоритма и логики работы программы. Ну что же, приступаем...
Таблица векторов прерываний в нашей программе больше, чем была до этого, поскольку мы решили задействовать два прерывания - внешнее прерывание 0, находящееся по адресу 1, и прерывание по переполнению таймера 0, находящееся по адресу 3.
9, 10 строки. В них как раз осуществляется запись констант в память программ. При этом в строке 9 мы указываем начальный адрес, по которому будет располагаться первый записанный байт. Этот адрес равен 4. Но тут нужно учитывать, что директивой .org задается адрес в словах, а при использовании команд типа lpm адресация ведется побайтно. Так что в байтах адрес первого элемента составляет не 4, а 8. В 10-й строке записаны уже полученные нами выше четыре байта при помощи директивы .db.
19, 20 строки. В регистр MCUCR записываем единицу в бит ISC01. В регистре MCUCR два бита: ISC01 и ISC00 - определяют условие генерации внешнего прерывания 0 в соответствии со следующей таблицей:
ISC01
| ISC00
| Условие генерации прерывания
|
0
| 0
| По низкому уровню на выводе INT0
|
0
| 1
| При любом изменении сигнала на выводе INT0
|
1
| 0
| По спадающему фронту сигнала на выводе INT0
|
1
| 1
| По нарастающему фронту сигнала на выводе INT0 |
Поскольку нажатие кнопки вызывает спадающий фронт сигнала, то мы и установили только бит ISC01. Следует быть внимательным при использовании данного прерывания, поскольку если оба бита равны нулю, то генерация прерывания будет осуществляться постоянно, пока на выводе INT0 будет присутствовать низкий уровень, а поскольку это прерывание имеет наивысший приоритет, то выполнение программы на это время просто остановится.
21, 22 строки. В регистр GIMSK записывается единица в бит INT0. Я уже упоминал об этом регистре и об этом бите в шестом шаге. Этот бит разрешает генерацию внешнего прерывания 0.
23, 24 строки. Здесь мы задали коэффициент деления тактовой частоты таймера 0 равным 1024.
25 строка. Разрешение механизма работы прерываний.
Обратите внимание, что в разделе инициализации отсутствует разрешение прерывания по переполнению таймера 0. Причины этого я описал выше.
30 строка. Начало обработчика внешнего прерывания 0.
31-39 строки. Стандартная процедура опроса кнопки. Не останавливаюсь на ней.
40-45 строки. Действия, выполняемые при отпускании кнопки. На них остановимся подробнее, поскольку они представляют интерес.
40, 41 строки. Вот тут и происходит разрешение прерывания по переполнению таймера 0.
42 строка. Очистка регистра r21. Этот регистр у нас будет выполнять роль своеобразного флага. Если он равен нулю, то нужно загружать из памяти программ следующий байт, а если равен 255, то нужно сдвигать уже загруженный. Поскольку сначала у нас ничего не загружено, то мы очищаем указанный регистр.
43 строка. Загрузка в регистр r24 числа 3. Этот регистр у нас будет выполнять роль счетчика считанных из памяти программ байт. Нам нужно считать 4 байта, а загрузили мы число 3. Тут нет противоречия, поскольку счет начинается с 0 (0, 1, 2, 3 - как раз четыре байта).
44, 45 строки. Загрузка в регистр Z начального адреса, из которого будет происходить считывание из памяти программ. Мы уже договорились ранее, что адрес должен быть указан в байтах, поэтому он равен 8 (строка 45). В строке 44 происходит очистка старшего байта адреса ZH. Делать это нужно обязательно, поскольку для всех двухбайтовых регистров первым всегда должен записываться старший байт, а затем младший. Считывание происходит в обратном порядке - сначала младший байт, затем - старший.
49 строка. Начало обработчика прерывания по переполнению таймера 0.
50, 51 строка. Сравнивается содержимое регистра r21 с нулем (строка 50), и если r21 не равен 0, то происходит переход к метке shift (строка 51), с которой начинается цикл сдвига загруженного из памяти программ байта. Если же r21 = 0, то происходит переход к строке 52.
52 строка. Считывание в регистр r22 значения из памяти программ из адреса, указанного в регистре Z, а также увеличение содержимого регистра Z на единицу.
53 строка. Загрузка в регистр r23 числа 8. Этот регистр будет выполнять роль счетчика количества сдвигов загруженного в регистр r22 байта. Поскольку в байте 8 бит, то мы и загрузили в r23 число 8. Почему же так? Ведь не так давно я в такой же счетчик r24 загрузил число на единицу меньше нужного, говоря, что счет ведется с нуля. Да, все правильно. Но мы будем использовать разные проверки для окончания счета, поэтому никакой ошибки тут нет. Да и вы должны уметь пользоваться разными командами, и понимать, когда какую лучше использовать.
54 строка. Установка всех битов регистра r21 в единицы командой ser. Таким образом мы задаем признак того, что байт уже загружен, и новый загружать не нужно, пока не будут обработаны все 8 бит загруженного байта.
55 строка. Метка shift, обозначающая начало цикла сдвига загруженного в регистр r22 байта.
56 - 59 строки. Уже неоднократно применявшаяся нами конструкция. Проверяется старший (7-й) бит регистра r22, и если он равен "1", то устанавливается единица в нулевом бите регистра PORTB (гашение светодиода LED1), а если равен "0", то устанавливается нуль в этом бите (включение светодиода LED1). Почему мы проверяем именно 7-й бит? Тут все дело в том порядке, в котором мы сохранили нашу последовательность, разбив ее на четыре байта. Мы записали ее слева направо. А поскольку самый левый бит является самым старшим, то мы и осуществляем его проверку.
60 строка. Логический сдвиг регистра r22 влево командой lsl. Теперь старшим битом становится тот, который до этого был шестым, а седьмой бит для нас теряется, но он нам уже и не нужен. Таким образом при следующем проходе цикла мы уже установим состояние светодиода, соответствующее шестому биту, при третьем проходе - пятому и т.д. Вот так будет осуществляться проверка всех битов загруженного в регистр r22 байта.
61 строка. Вычитание из регистра r23 единицы. Уменьшаем счетчик битов на каждом проходе цикла до тех пор, пока он не станет равным нулю.
62 строка. Тут проверяем, не стал ли регистр r23 равным нулю, командой breq. Обратите внимание, что ей в данном случае не предшествует команда сравнения cpi. Она тут не нужна, поскольку выполняет те же действия, что и команда subi в строке 61, только без сохранения результата. Поэтому в данном случае отсутствие команды сравнения вполне оправдано и даже желательно. Итак, если регистр r23 стал равным нулю, происходит переход к метке next_dig, где устанавливается признак того, что нужно бы загрузить новый байт, так как старый уже кончился. Иначе происходит переход к строке 63.
63 строка. Безусловный переход к метке exit_tmr0, которая отправляет нас к выходу из прерывания. Почему же мы выходим из прерывания, а не возвращаемся к началу цикла (метка shift)? На самом деле тут все просто. Мы должны изменять состояние светодиода только один раз за прерывание. При следующем входе в прерывание в строке 50 мы убеждаемся, что r21 не равен нулю и в строке 51 осуществляем переход к метке shift. Таким вот образом мы избегаем ненужного зацикливания программы на одном месте. Кроме того, такое зацикливание потребовало бы введения дополнительных длительных задержек, а это уже вовсе не комильфо.
64 строка. Метка next_dig. С нее начинается участок программы, в котором устанавливается признак того, что нужно загрузить новый байт, а также происходит проверка количества уже загруженных байт.
65 строка. Очистка регистра r21. Теперь при очередном входе в прерывание условие в строках 50-51 не будет выполняться, и произойдет загрузка очередного байта из памяти программ. Но это будет только при следующем входе в обработчик прерывания по таймеру 0!
66 строка. Вычитание единицы из r24. Уменьшение счетчика уже загруженных и обработанных байт.
67 строка. Тут нас поджидает команда brcc. Напомню еще раз ее назначение. Она проверяет, не произошло ли переноса в старший разряд или заема из старшего разряда. Если не произошло, то мы переходим к метке exit_tmr0, выходя из подпрограммы обработки прерывания. Если же заем произошел, то есть содержимое регистра r24 изменилось с 0 на 255, то происходит переход к строке 68.
68-69 строки. Очистка регистра TIMSK. После выполнения этих строк прерывание по переполнению таймера 0 будет запрещено. То есть здесь мы выполняем условие нашей задачи - после окончания цикла передачи сигнала SOS светодиод гаснет в ожидании следующего нажатия кнопки SB2.
Все остальные строки должны быть понятны читателю, внимательно следящему за данным циклом статей, поэтому я опускаю их описание.
Не знаю, насколько доступно мне удалось изложить этот в общем-то не самый простой для понимания материал. Но в любом случае я всегда на связи, и вы можете задать свои вопросы на форуме или же прямо тут.
Кстати, обратите внимание, что при асссемблировании данной программы объем полученного программного кода составляет всего 60 слов, или 120 байт. Это составляет чуть больше 10% от и без того довольно маленькой памяти программы контроллера ATtiny13. Данный факт показывает, насколько компактным является код, написанный на ассемблере.
Ну и, наконец, задание для самостоятельного выполнения. Оно будет состоять в расширении уже написанной программы.
В исходном состоянии при старте питания светодиоды LED1 и LED2 должны быть погашены. При нажатии на кнопку SB1 при помощи светодиода LED1 выдавать слово "asm" (.- ... --), а при нажатии на кнопку SB2 при помощи светодиода LED2 выдавать слово "AVR" (.- ...- .-.). Обработку кнопки SB2 осуществить при помощи внешнего прерывания 0, а обработку кнопки SB1 - при помощи прерывания по изменению состояния выводов. Работа обоих кнопок должна быть независимой, то есть в любой момент времени можно запустить как выдачу слова asm, так и выдачу слова AVR. При этом остановка таймера должна осуществляться только после окончания последнего знака последнего из выводимых в данный момент слов.
Понимаю, что задание это намного сложнее, чем все, что мы до этого писали, но я верю, что вы его осилите, потому что мы уже практически добрались до вершины, а там нет места слабым!
Если у вас возникнут вопросы, задавайте их на форуме или здесь в виде комментариев к статье.
Желаю успехов!