Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ассемблер AVR для начинающих

.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
847.87 Кб
Скачать

Ассемблер AVR для начинающих (восьмой шаг)

Рад снова приветствовать постоянных читателей, ожидающих чего-то нового и интересного. В этот раз постараюсь оправдать ваши ожидания. Давайте сегодня несколько нарушим регламент, и вместо теоретического введения я сразу напишу поставленную перед нами задачу, а затем попробуем подумать, как ее лучше выполнить. Итак, нам предстоит выполнить следующее. Составить программу, которая бы автоматически выдавала при помощи светодиода 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. При этом остановка таймера должна осуществляться только после окончания последнего знака последнего из выводимых в данный момент слов. Понимаю, что задание это намного сложнее, чем все, что мы до этого писали, но я верю, что вы его осилите, потому что мы уже практически добрались до вершины, а там нет места слабым!

Ассемблер AVR для начинающих (девятый шаг)

Надеюсь, дорогие мои читатели, вы благополучно справились с предложенным мною в предыдущем шаге заданием и спешите к новым свершениям. Не будем же медлить!

Давайте в этот раз поступим так же, как и в прошлый. Сразу поставим себе задачу, а потом будем ее решать. Однако, в отличие от прошлого раза, сегодня задание сформулируем максимально конкретно. Необходимо по прерыванию от переполнения таймера 0 непрерывно и автоматически изменять содержимое регистра r16 от 0 до 255, а затем в обратном порядке с шагом 1. Кроме того, необходимо изменять яркость свечения светодиода LED1 при помощи ШИМ таким образом, чтобы эта яркость была пропорциональна квадрату содержимого регистра r16. Итак, как мы видим, можно поставленную задачу разбить на две подзадачи: управление содержимым регистра r16 и математическая обработка этого самого содержимого. И если с первой подзадачей все более-менее ясно, то над второй нужно немного подумать. Нам нужно изменять яркость светодиода пропорционально квадрату r16. Но значение r16 изменяется в пределах от 0 до 255, а, следовательно, его квадрат - от 0 до 65025. Это значение выходит за пределы однобайтового числа, которое может быть записано в регистр OCR0A. То есть мы должны записать в OCR0A 255, а не 65025, при r16=255. Таким образом, перед нами встает задача масштабирования, решая которую мы должны получить коэффициент пропорциональности между r162 и OCR0A.  По заданию минимальная яркость должна соответствовать нулевому значению в r16, поэтому если r16=0, то и OCR0A=0. А чтобы получить коэффициент масштабирования, нам, как большинство читателей уже догадалось, нужно разделить 65025 на 255. При этом получается число 255 (что, собственно, неудивительно).  Таким образом, чтобы вложиться в однобайтовое число, нам нужно вычислять значение OCR0A по следующей формуле: OCR0A = r162 / 255 То есть для выполнения поставленной задачи нам потребуется умножить содержимое r16 само на себя и разделить полученное значение на 255. Именно в такой последовательности, иначе для всех значений r16, кроме 255, OCR0A будет равно 0 (поскольку деление целочисленное, и дробная часть просто отсекается). Такая задача на Си решается одной строкой без малейших раздумий. Но в ассемблере она осложняется рядом фактов. Во-первых, здесь отсутствуют команды умножения и деления, поэтому нам придется разрабатывать алгоритмы, реализующие эти действия на основании имеющихся - сложения и вычитания.  Во-вторых, даже элементарные операции: сложение и вычитание - нам придется производить над двухбайтовыми числами, что тоже имеет свои нюансы.  Но давайте не будем долго рассуждать, а сразу приступим к рассмотрению программы, и по ходу описания я расскажу, что к чему.  .include "F:\Prog\AVR\asm\Appnotes\tn13def.inc" .org 0        ;Задание нулевого адреса старта программы rjmp reset  ;Безусловный переход к метке reset .org 3         ;Задание адреса прерывания по переполнению таймера 0 rjmp tmr0   ;Безусловный переход к метке tmr0 reset:                    ;Начало раздела инициализации контроллера ldi r16,RAMEND      ;Загрузка в регистр r16 адреса верхней границы ОЗУ out SPL, r16           ;Копирование значения из r16 в регистр указателя стека SPL sbi DDRB, 0           ;Установка 0-го бита в регистре DDRB в единицу (РВ0 - выход) ldi r16, (1<<TOIE0);Загрузка в регистра r16 единицы, смещенной на TOIE0 out TIMSK0,r16      ;Копирование значения из регистра r16 в регистр TIMSK0 ldi r16,(1<<WGM00)|(1<<WGM01)|(1<<COM0A1)|(1<<COM0A0); Fast PWM out TCCR0A,r16     ;с включением OC0A при совпадении с регистром OCR0A ldi r16,(1<<CS01)|(1<<CS00);Загрузка в регистра r16 единицы в бит CS01 out TCCR0B,r16    ;Установка делителя тактовой частоты таймера 0 равным 8 ser r17                 ;Регистр направления изменения яркости clr r18                  ;Регистр старшего байта для множителя и делителя ser r22                 ;Регистр младшего байта делителя sei                       ;Глобальное разрешение прерываний  main:                   ;Основной цикл программы rjmp main            ;Конец основного цикла программы tmr0:                   ;Обработка прерывания по переполнению таймера 0 cpi r16,0              ;Сравнение регистра r16 с 0 breq step_up        ;Если он равен 0,то перейти к метке step_up cpi r16,255           ;Сравнение регистра r16 с 255 brne set               ;Если не равен 255, то перейти к метке set ser r17                 ;Иначе установить регистр r17 rjmp set               ;И перейти к метке set step_up:              ;Метка step_up - признак приращения r16 clr r17                  ;Очистка r17 set:                     ;Метка set - изменение яркости светодиода sbrs r17,0            ;Если нулевой бит r17 установлен, пропустить след. строку inc r16                 ;Увеличить r16 на 1 sbrc r17,0            ;Если нулевой бит r17 сброшен, пропустить след. строку dec r16                ;Уменьшить r16 на 1 rcall multiply         ;Вызов подпрограммы умножения rcall divide            ;Вызов подпрограммы деления out OCR0A,r23      ;Копирование в OCR0A значения r23 - результата операций reti                      ;Возврат из подпрограммы обработки прерывания multiply:              ;Подпрограмма умножения clr r19                 ;Очистка младшего байта результата clr r20                 ;Очистка старшего байта результата mov r21,r16         ;Копирование в r21 значения из r16 sum:                   ;Цикл суммирования add r19,r16         ;Прибавление к младшему биту результата содержимого r16 adc r20,r18         ;Прибавление к старшему биту результата 0 с учетом переноса dec r21               ;Вычитание единицы из регистра r21 brne sum            ;Если результат не равен 0, то вернуться к метке sum ret                     ;Возврат из подпрограммы divide:               ;Подпрограмма деления clr r23                ;Очищаем регистр r23 - частное minus:               ;Цикл вычитания делителя из делимого cp r19,r22          ;Сравниваем младшие байты делимого и делителя cpc r20,r18        ;Сравниваем старшие байты делимого и делителя brlo exit            ;Если делимое уже меньше, то выходим из цикла sub r19,r22       ;Иначе вычитаем из делимого делитель (младшие байты) sbc r20,r18        ;и старшие байты inc r23              ;Прибавляем к частному единицу rjmp minus       ;Возвращаемся к началу цикла exit:                 ;Метка выхода из цикла ret                   ;Возврат из подпрограммы Программа тоже получилась немаленькая, но я намеренно увеличил ее объем ради выделения алгоритмов умножения и деления в отдельные подпрограммы. Кроме того, в вышеприведенном тексте встречается рекордное количество новых команд. Они довольно простые и часто используемые, просто раньше в них не было необходимости. Рассмотрим же их по порядку. Команда inc имеет один операнд - РОН. В результате ее выполнения содержимое  указанного РОН увеличивается на единицу.  Команда dec также имеет один операнд - РОН. По назначению она противоположна предыдущей: уменьшает содержимое регистра на единицу. Команда mov имеет два операнда, оба - регистры общего назначения. В результате ее выполнения в регистр, записанный первым, копируется значение, содержащееся в регистре, записанном вторым. Команда предназначена для работы только с РОН, но никак не с РВВ. Напомню, что для копирования их содержимого в РОН и обратно используются команды in и out соответственно. Все рассмотренные далее команды будут также иметь два операнда, и оба также только РОН. При этом следует запомнить, что результат выполнения команды всегда записывается в регистр, указанный первым (кроме команд cp  и cpc). Я написал эту общую часть потому, что иначе пришлось бы ее дублировать для всех нижеследующих команд, а это как-то скучно. Далее опишу только результаты выполнения команд. Команда add выполняет простое суммирование указанных РОН. Если в результате операции происходит перенос в старший разряд, он записывается в бит С статусного регистра SREG. Команда adc выполняет арифметическое суммирование с учетом переноса из младшего разряда. То есть он суммирует указанные регистры и прибавляет к результату значение бита С. Если в результате снова происходит перенос в старший разряд, он снова сохраняется в бите С. Команда sub выполняет простое вычитание второго регистра из первого. Если для выполнения операции требуется заем из старшего разряда, то снова устанавливается в единицу бит С регистра SREG. Команда sbc выполняет арифметическое вычитание второго регистра из первого с учетом переноса. То есть из первого РОН вычитается второй, а также вычитается значение бита С. При выполнении этой команды также возможен заем из старшего разряда с автоматической установкой бита С. Команда cp выполняет сравнение содержимого указанных РОН. По своему смыслу она похожа на команду cpi, только сравнивает она не РОН и константу (как cpi), а два РОН. Команда cp вычитает из первого регистра второй, но не осуществляет сохранения результата в первом регистре. Она только устанавливает флаги выполнения операций в статусном регистре SREG. При выполнении этой команды так же, как и в командах вычитания, может происходить заем из старшего разряда через бит С. Команда cpc выполняет сравнение содержимого указанных РОН с учетом переноса. Из первого регистра вычитается второй, а также бит переноса С. И так же, как и для предыдущих операций, в результате ее выполнения может устанавливаться бит С. Ну вот, как-то так. Разберем теперь применение этих команд на практике, в написанной нами программе. 2-5 строки. Таблица векторов прерываний. Тут все уже известно из прошлых шагов. 6-20 строки. Раздел инициализации. В нем тоже все команды и используемые регистры контроллера были описаны ранее. Остановлюсь лишь на назначении регистров, указанных в строках 16,17,18.  Регистр r17 является флагом, определяющим направление изменения яркости. Если он равен 0, то происходит уменьшение яркости, если 255, то увеличение. Вначале мы командой ser записываем в него 255, тем самым задавая, что при старте программы яркость будет увеличиваться.  Регистр r18 командой clr обнуляется. О его назначении расскажу при описании алгоритмов умножения и деления. В регистр r22 командой ser записывается число 255. Этот регистр будет играть роль делителя в формуле масштабирования, записанной выше. 25 строка. Метка tmr0 - начало подпрограммы обработки прерывания по переполнению таймера 0.  26-33 строки. В них происходит установка и сброс флага r17 при достижении регистром r16 крайних значений (0 или 255). Рассмотрим подробнее, как это работает.  В строках 26-27 происходит сравнение регистра r16 с нулем, и если это равенство выполняется, то осуществляется переход к метке step_up (строка 32), за которой следует команда ser, записывающая в r17 число 255 (строка 33). Таким образом получается, что при достижении регистром r16 значения "0" мы устанавливаем в r17 число 255, определяя, что теперь содержимое регистра r16 должно увеличиваться.  В строках 28 и 29 происходит сравнение регистра r16 с 255, и если равенство не выполняется (команда brcc), происходит переход к метке set (строка 34), а иначе происходит переход к следующей строке (30), в которой осуществляется сброс флага r17 (командой clr). Таким образом, при достижении регистром r16 значения "255" в r17 устанавливается значение "0", определяющее, что далее следует уменьшать содержимое регистра r16.  Если в r16 будет записано любое другое число, то после выполнения строки 29 сразу произойдет переход к метке set без изменения содержимого регистра r17 (почему именно так, подумайте сами, алгоритм довольно прост). 35-38 строки. Конструкция знакомая по прошлым шагам. Мы проверяем младший бит регистра r17 (строки 35, 37), и в зависимости от его значения выполняем увеличение  содержимого регистра r16 на 1, если этот бит равен 1 (строка 36), либо уменьшение на 1, если он равен 0 (строка 38). По большому счету можно было бы проверять абсолютно любой бит регистра r17, так как мы устанавливаем и сбрасываем все восемь бит сразу. 39 строка. Вызов подпрограммы умножения, выполняющей возведение содержимого регистра r16 в квадрат 40 строка. Вызов подпрограммы деления, выполняющей деление полученного результата на 255. 41 строка. Копирование результата двух предыдущих операций (регистр r23) в регистр OCR0A для непосредственного изменения яркости светодиода. 44 строка. Метка multiply - начало подпрограммы умножения. Для начала опишу алгоритм работы этой подпрограммы, а затем уже рассмотрим его реализацию.  Собственно, алгоритм весьма прост. Он основан на многократном прибавлении к произведению одного из множителей внутри цикла. Количество проходов цикла равно второму множителю. Таким образом, умножение сводится к многократному суммированию, причем, чем больше второй множитель, тем больше времени потребуется контроллеру на данную операцию. Теперь обещанные подробности. 45, 46 строки. Очистка регистров r19, r20. В этих регистрах мы и будем накапливать квадрат содержимого r16. Я уже говорил в самом начале, что для хранения квадрата однобайтового числа потребуется два байта. Будем считать, что r19 содержит в себе младший байт результата, а r20 - старший. 47 строка. Копирование в РОН r21 содержимого r16. Регистр r21 будет выполнять роль второго множителя. Поскольку нам нужно возвести r16 во вторую степень, то логично, что оба множителя одинаковые. 48 строка. Метка sum обозначает начало цикла суммирования. 49,50 строки. Таким образом осуществляется сложение двух двухбайтовых чисел. Вначале производится суммирование младших байт командой add (строка 49), а затем - старших байт с учетом переноса командой adc (строка 50). Сложение должно осуществляться именно этими командами и именно в такой последовательности. Собственно, это должно быть понятно из обычной математики - мы всегда суммируем, начиная с младшего разряда, и если осуществляется перенос из младшего разряда, мы его учитываем и добавляем к старшему. Тут как раз пришло время объяснить назначение регистра r18. Несмотря на то, что мы прибавляем только младшие разряды, а в старшем накапливаем только переносы, все равно из-за синтаксиса команды adc нужно использовать два регистра. И если мы хотим прибавлять только бит переноса, то второй регистр (r18) нужно сделать равным нулю, что мы и совершили в 17 строке. 51 строка. Вычитание единицы из регистра r21. Тут, думаю, тоже должно быть понятно. Мы сначала загрузили в него нужный нам множитель, а затем на каждом проходе цикла вычитаем из него по "1". Когда в r21 останется 0, значит мы выполнили все итерации, и цикл можно завершить. 52 строка. Собственно, здесь мы и проверяем, не получилось ли в результате предыдущей операции нуля, и если не получилось (команда brne), то переходим к метке sum - началу цикла. 53 строка. Возврат из подпрограммы умножения.  Хочу еще заметить, что при сложении чисел большей разрядности (3, 4 и т.д. байт) алгоритм не меняется. Нужно прибавлять младшие байты командой add, а все последующие - в порядке возрастания командой adc. 55 строка. Метка divide - начало подпрограммы деления. Как и в случае с предыдущей подпрограммой сначала рассмотрим алгоритм ее работы.  В качестве лирического отступления поведаю историю создания этого алгоритма. При изучении ассемблера я для себя решил не пользоваться чужими программами, а каждый раз изобретать велосипед заново. Такую же задачу я поставил и перед своими студентами, которые начали учить этот язык почти одновременно со мной. Когда возникла необходимость написать подпрограмму деления, я сотворил замечательный, как мне тогда казалось, алгоритм. В чем-то он и есть замечательный. Его достоинством является то, что количество итераций  не зависит от величины делителя и делимого, а только от разрядности чисел. Но он получился достаточно громоздким и сложным для понимания и описания, поэтому я его здесь не привожу. Гордый собой, я задал своим студентам написать собственные алгоритмы деления. На следующий вечер мне был явлен алгоритм, поразивший меня до глубины души своей простотой и логичностью. Я не мог понять, как не додумался до такого простого и очевидного решения. Мне он настолько понравился, что я стал его использовать в своих программах. И здесь я привожу именно его с любезного разрешения автора - Марии Забильской. Понимаю, что скорее всего этот алгоритм уже много раз описан в разных учебниках, но в рамках нашего небольшого коллектива я соблюдаю авторские права. Но это так, к слову. Теперь наконец-то рассмотрим сам алгоритм. Смысл его заключается в многократном вычитании делителя из делимого внутри цикла до тех пор, пока делимое не станет меньше делителя. Одновременно с этим на каждом проходе цикла к частному прибавляется единица. Таким образом после выхода из цикла мы получаем частное, а в регистре, в котором было делимое - остаток от деления. Рассмотрим теперь реализацию алгоритма в программе. 56 строка. Очистка регистра r23. В нем мы будем накапливать частное. 57 строка. Метка minus - начало цикла вычитания делителя из делимого и наращивания частного. 58-59 строки. Сравнение двух двубайтовых чисел: делимого (r19,r20) и делителя (r18,r22). Тут логика та же, что и при суммировании: сначала сравниваются младшие байты при помощи обычной команды равнения (ср), а затем старшие с учетом переноса (срс). Тут тоже стоит вспомнить математику - вычитание так же, как и сложение, начинается с младшего разряда с возможностью заема единицы из старшего. Обратите внимание, что для сравнения старших разрядов мы используем в делителе все тот же регистр r18, равный 0, поскольку наш делитель однобайтный (r22=255), а для синтаксиса команды срс нужно сравнивать два регистра. 60 строка. Если делимое меньше делителя (команда brlo), то выходим из цикла (метка exit), иначе переходим к следующей строке. 61-62 строки. Вычитаем из делимого делитель. Логика тут абсолютно такая же, как и для строк 58-59, единственное отличие в том, что после выполнения команд sub и sbc происходит изменение регистров r19,r20 63 строка. Прибавление к частному (r23) единицы.  64 строка. Безусловный переход к началу цикла (метка minus). Таким образом из цикла мы можем выйти только через строку 60. 65 строка. Метка exit служит для указания места выхода из цикла для строки 60. 66 строка. Возврат из подпрограммы деления.  После этой строки мы попадаем в 41 строку, где частное (r23) копируется в регистр OCR0A. Ну вот как бы и все, что касается работы программы. Что тут можно еще добавить... При загрузке программы в контроллер светодиод начнет плавно менять свою яркость от минимальной до максимальной и обратно. Возможно, внимательный читатель спросит: "Ну и где тут видно, что оно пропорционально квадрату r16?". Тут должен признаться честно, это не очень отличимо невооруженным глазом. Но для сравнения попробуйте в строке 41 в OCR0A копировать не r23, а r16, и вы увидите, что характер изменения яркости стал несколько иным. Напоследок традиционное задание для самостоятельного решения. Написать программу индикации величины напряжения, снимаемого с движка переменного резистора при помощи светодиода LED1. При этом яркость светодиода должна быть пропорциональна кубу считываемого напряжения. Тут будет иметь место такой себе симбиоз программ из нынешнего и из 7-го шагов, так что особых затруднений возникнуть не должно. Впереди нас ждет десятый шаг, которым я планирую завершить текущий цикл. Если он покажется интересным для читателей, то у меня есть уже задумки по его продолжению. Так что жду отзывов.

Ассемблер для начинающих (десятый шаг)

Я долго думал, как бы получше завершить начатый мною цикл. У меня было несколько вариантов. Сначала я планировал написать очередную рядовую программу, потом хотел сотворить какой-то большой и фееричный проект. Но в итоге остановился на том, что собственно вы сейчас и прочтете.

А решил я на этот раз заняться просто разговорами без всяких программ. Будем считать, что мы одолели хоть и маленькую, но вершину и можем теперь немного передохнуть и расслабиться. Все дело в том, что возможности используемой нами платы уже почти полностью исчерпаны, а высасывать из пальца никому не нужные задачи тоже неохота. На протяжении всех девяти шагов я потихоньку сообщал вам новые директивы ассемблера, описывал необходимые нам команды. Настало время узнать вам ВСЁ. На самом деле полное описание всех команд и директив ассемблера с примерами занимает всего несколько страниц текста. Чтобы вам долго не искать это описание на просторах интернета, я выкладываю его здесь. Вы можете ознакомиться с ним, перейдя по этой ссылке.  Возможно, оно покажется вам слишком сокращенным либо наоборот более понятным, чем мои многословные и пространные описания - тут дело зависит от субъективного восприятия. Давайте же оглянемся назад и подытожим то, что мы уже изучили за это время.  Во-первых, несмотря на мое изначальное намерение не останавливаться на рассмотрении модулей и узлов контроллера, я не удержался и довольно немалую часть времени уделил описанию работы с ними. Хочу заметить, что все вышесказанное на 100% соответствует только контроллеру ATtiny13. В других контроллерах названия регистров или расположение битов в них может отличаться, поэтому перед использованием их необходимо свериться либо с даташитом, либо с рекомендованными мною книгами Евстифеева. Во-вторых, мы рассмотрели довольно большое количество команд ассемблера, которых будет достаточно в 70-80 процентах случаев для написания программ.  Команды эти охватывают: - работу с битами регистров ввода-вывода (cbi, sbi, cbis, sbis, sei); - работу с битами регистров общего назначения (clr, ser, sbrc, sbrs); - обмен информацией между РВВ и РОН (mov, in, out); - безусловный и условный переход (rjmp, rcall, ret, reti; brcc, brlo, brsh, brne, breq); - логические и математические операции между РОН (eor, lsl, inc, dec, add, adc, sub, sbc, cp, cpc); - логические и математические операции между РОН и константам (ldi, subi, sbci, cpi); - чтение данных из памяти программ (lpm). Не рассмотренными оказались команды косвенного чтения и записи в память данных, команды установки и сброса битов статусного регистра, большинство команд логических операций. С ними вы можете ознакомиться в упомянутом выше описании. Там же вы можете прочитать и об оставленных за пределами нашего внимания директивах ассемблера.  Что же ожидает нас в будущем? Если данный цикл покажется интересным читателям, у меня уже есть задумки по его продолжению, но уже на другом контроллере (ATmega8) и с рассмотрением более продвинутых алгоритмов, таких как: динамическая индикация с использованием 7-сегментых светодиодных индикаторов, работа с цифровыми интерфейсами 1-wire, I2C, SPI, UART, работа с ЖК-дисплеем. Кроме того, есть задумка написать похожие циклы статйе, только для контроллеров PIC и MSP430. Поэтому, не ленитесь оставлять комментарии, задавать вопросы и писать замечания - от этого будет зависеть, появятся ли последующие статьи или нет, потому как тратить уйму и без того краткого свободного времени на написание никому не нужных статей как-то печально. Но не будем слишком пессимистичными. Если хоть кому-то этот цикл помог приобщиться к программированию микроконтроллеров, значит моя цель достигнута и время потрачено не зря. Вот, собственно, и все, что я хотел поведать в этот раз. Что-то я сегодня немногословен вопреки обыкновению, но отсутствие обратной связи как-то сильно поубавило энтузиазма.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]