Этот шаг можно считать релаксирующим по сравнению с предыдущим. И программа попроще, и новых команд поменьше, да и сам модуль АЦП легче для понимания. Итак, начнем с рассмотрения самого модуля АЦП. Для тех, кто совершенно не в курсе (хотя, думаю, таких на седьмом шаге уже нет, но все же...), расшифровываю: АЦП - аналого-цифровое преобразование. Тут ситуация, обратная ЦАП. Модуль АЦП преобразует напряжение, поступающее на вход, в пропорциональное ему число, записываемое в определенный регистр. В микроконтроллерах AVR применяются 10-битные АЦП. Это значит, что максимально возможное число в регистре АЦП равно 1023. Как же вместить это значение в один регистр? Вот тут и кроется военная хитрость. Регистр АЦП - двухбайтный, и значение записывается одновременно в два байта, и должно считываться и обрабатываться также как двухбайтное. Однако, если нет необходимости в использовании 10-битной точности, а достаточно лишь 8-битной, то есть возможность считывать результат всего из одного регистра. В нашей сегодняшней программе мы именно так и поступим. Далее возникает еще один вопрос. А какому же напряжению будет соответствовать максимальное число в регистре АЦП? В контроллере ATtiny13 на этот вопрос есть два варианта ответа. Максимальное напряжение, подаваемое на вход АЦП, определяется величиной так называемого опорного напряжения. Для ATtiny13 в качестве источника опорного напряжения может выступать либо источник питания контроллера (в нашем случае это 5 В), либо встроенный источник опорного напряжения величиной 1,1 В. Откуда именно будет поступать опорное напряжение для модуля АЦП, определяется при помощи специального бита. Об этом чуть позднее. В качестве входов АЦП можно использовать один из четырех выводов, в названии которых есть обозначение ADCх (где х = 0, 1, 2, 3). Если посмотреть на схему нашей платы, а потом на обозначение выводов контроллера, можно видеть, что мы подключили движок переменного резистора к входу ADC3. Вывод, с которого в данный момент будет считываться напряжение, определяется опять же при помощи специальных битов. Следующий нюанс. Один цикл преобразования АЦП занимает 13 тактов. Рекомендуется для повышения точности преобразования использовать в качестве тактового сигнала АЦП источник с частотой 50...200 кГц. Для этого, как и в рассмотренных ранее таймерах используется делитель частоты. Коэффициент деления также задается при помощи соответствующих битов. Ну и последняя, пожалуй, тонкость. Модуль АЦП может работать как в режиме одиночного преобразования, так и непрерывного. В первом случае модуль АЦП инициализируется и разрешается однократное преобразование, по завершении которого модуль снова переходит в ждущий режим до следующего разрешения. В режиме непрерывного преобразования модуль АЦП также инициализируется, и разрешется первое преобразование. По завершении его модуль АЦП может либо сразу автоматически начать новое преобразование, либо ожидать разрешения от какого-либо периферийного модуля (таймера, внешнего прерывания, компаратора и др.). Как вы уже, наверное, догадались, все эти режимы задаются установкой нужных битов. Собственно, на этом теоретическое введение можно считать оконченным. Перейдем к практике. Напишем программу, которая бы в непрерывно считывала напряжение с движка переменного резистора и преобразовывала его в 8-битное число. В качестве источника опорного напряжения АЦП использовать источник питания 5 В. Величину считанного напряжения индицировать при помощи светодиодов LED1 и LED2 таким образом: - при помощи LED2 дискретно: если напряжение на входе АЦП меньше 2,5 В светодиод LED2 погашен, если больше, то зажжен; - при помощи LED1 непрерывно: яркость свечения светодиода должна быть пропорциональна входному напряжению. Задание хоть и выглядит громоздким, но реализация его не так уж сложна. Текст программы, выполняющей поставленную задачу, представлен ниже. .include "F:\Prog\AVR\asm\Appnotes\tn13def.inc" .org 0 ;Задание нулевого адреса старта программы rjmp reset ;Безусловный переход к метке reset .org 9 ;Задание адреса прерывания по окончанию преобразования АЦП rjmp ADC_complete;Безусловный переход к метке ADC_complete reset: ;Начало раздела инициализации контроллера ldi r16,RAMEND ;Загрузка в регистр r16 адреса верхней границы ОЗУ out SPL, r16 ;Копирование значения из r16 в регистр указателя стека SPL ldi r16, 1|(1<<4);Загрузка в r16 единиц в нулевой и четвертый биты out DDRB,r16 ;Переключение выводов PB0 и PB4 на выход ldi r16,(1<<ADLAR)|(1<<MUX0)|(1<<MUX1);См. описание программы out ADMUX,r16 ;Копирование из r16 в ADMUX ldi r16,(1<<ADEN)|(1<<ADSC)|(1<<ADATE)|(1<<ADIE)|(1<<ADPS2);См.опис. out ADCSRA,r16 ;Копирование из r16 в ADCSRA ldi r16,(1<<WGM00)|(1<<WGM01)|(1<<COM0A1)|(1<<COM0A0);Fast PWM out TCCR0A,r16 ;с включением OC0A при совпадении с регистром OCR0A ldi r16,(1<<CS01);Загрузка в регистра r16 единицы в бит CS01 out TCCR0B,r16 ;Установка делителя тактовой частоты таймера 0 равным 8 sei ;Глобальное разрешение прерываний main: ;Основной цикл программы rjmp main ;Конец основного цикла программы ADC_complete:;Начало обработчика прерывания от АЦП in r16,ADCH ;Копирование в регистр r16 результата преобразования out OCR0A,r16 ;Копирование из r16 в регистр OCR0A sbrs r16,7 ;Если 7-й бит в регистре r16 равен 1, пропустить след. строку sbi PORTB,4 ;Установить 4-й бит PORTB (выключить LED2) sbrc r16,7 ;Если 7-й бит в регистре r16 равен 0, пропустить след. строку cbi PORTB,4 ;Сбросить 4-й бит в регистре PORTB (включить LED2) reti ;Возврат из прерывания Как видите, тут гораздо больше места занимает блок инициализации, нежели собственно рабочий блок программы. Итак, очередные новые команды, встречающиеся в данной программе. Команда in имеет два операнда: РОН и РВВ. В результате ее выполнения содержимое РВВ копируется в РОН. Таким образом, эта команда противоположна по назначению команде out (собственно, это видно даже из их названия). Команда sbrs имеет два операнда: РОН и номер бита в этом РОН (от 0 до 7). Если бит с указанным номером в данном регистре равен "1", то следующая строка пропускается. Эта команда аналогична команде sbis, только предназначена для проверки не РВВ, а РОН. Команда sbrc является противоположностью предыдущей. Если указанный бит в указанном регистре равен "0", то следующая строка пропускается. Опять же по своему назначению она аналогична sbic, и опять же, в отличие от sbic, проверяющей только РОН, применяется только для РВВ. Остальные команды уже встречались ранее. Кстати, замечу в скобках, что в нашем распоряжении уже 23 ассемблерные команды. Теперь перейдем к рассмотрению программы. Как и на прошлом шаге, я буду это делать с пропуском уже известных конструкций. Начинается программа как всегда с таблицы векторов прерываний. На этот раз нам необходимо задействовать прерывание по окончанию цикла преобразования АЦП, находящееся по адресу 9, что и указано в 4-й строке. Далее блок инициализации модулей контроллера. На нем придется остановиться поподробней, поскольку многое в нем непонятно. 10 строка. Командой ldi в регистр r16 загружается конструкция 1|(1<<4). Возможно она покажется непонятной с первого взгляда, хотя в ней ничего сложного нет. Загружается несмещенная "1" и "1", смещенная на 4 бита влево. Команда "Побитовое ИЛИ" (|) указывает, что нужно суммировать эти единицы. 11 строка. Командой out содержимое r16 копируется в регистр DDRB тем самым переводя на вход выводы РВ0 (светодиод LED1) и РВ4 (светодиод LED2). 12, 13 строки. В регистре ADMUX устанавливаются едиицы в битах ADLAR, MUX0 и MUX1. Регистр ADMUX предназначен для управления мультиплексором модуля АЦП, то есть для выбора того входа, с которого в данный момент нужно считывать напряжение. Этот выбор как раз задается битами MUXx следующим образом:
MUX1
MUX0
Вход АЦП
ADC0
ADC1
ADC2
ADC3
Поскольку мы уже выше договорились, что движок переменного резистора подключен ко входу ADC3, то мы и установили в единицу оба бита мультиплексора.
Назначение бита ADLAR не столь очевидно. Попытаюсь описать как можно понятней. Итак, я уже говорил ранее, что по умолчанию точность АЦП составляет 10 бит, и результат преобразования хранится в двух байтах. Но полная вместимость двух байт составляет 16 бит. Получается, что шесть бит остаются незадействованными. По умолчанию эти 6 бит - это шесть старших бит старшего байта, то есть результат представляется в следующем виде: 0b000000хххххххххх, где "х" - это любое значение (0 или 1). При таком хранении результата нам обязательно нужно считывать и обрабатывать оба байта. Если же нам достаточно 8-битной точности, то можно выравнять результат по левой границе слова, оставив незадействованными младшие 6 бит младшего байта: 0bхххххххххх000000. В этом случае нам достаточно считать только старший байт, а младший вовсе не трогать. Потерянные младшие два бита результата, конечно, несколько сократят нам точность, но для нашего задания как раз и нужна именно 8-битная точность. Собственно, о чем я... Так вот, бит ADLAR, как уже многие читатели догадались, как раз и предназначен для задания выравнивания результата. Если он равен нулю, то результат равняется по правой границе, а если единице, то по левой. Так как нам нужно равнение именно по левой границе, мы и устанавливаем данный бит.
Еще в регистре ADMUX есть бит REFS0. Мы его оставили равным нулю. Однако, я считаю, что о нем стоит упомянуть. Если он равен "0", то в качестве источника опорного напряжения для модуля АЦП используется источник питания, а если равен "1", то внутренний источник величиной 1,1 В.
14, 15 строки. Тут все еще более весело. В регистре ADCSRA устанавливаются единицы в битах ADEN, ADSC, ADATE, ADIE, ADPS2. Рассмотрим их по порядку.
Бит ADEN разрешает функционирование модуля АЦП. Если он установлен, то модуль активен, если сброшен, то, соответственно, неактивен.
Бит ADSC запускает преобразование. В режиме одиночного преобразования именно установка этого бита в "1" стартует преобразование, и далее модуль АЦП ожидает очередной установки его в "1". В режиме непрерывного преобразования установка этого бита определяет старт первого преобразования, а все последующие уже не зависят от состояния бита ADSC.
Бит ADATE Как раз и определяет, в каком режиме будет работать модуль АЦП. Если он равен "0", то устанавливается режим одиночного преобразования, а если "1", то режим непрерывного преобразования.
Бит ADIE разрешает генерацию прерывания по завершению цикла преобразования АЦП. Поскольку мы собираемся использовать именно это прерывание, то в программе мы и устанавливаем данный бит.
Биты ADPSx (х = 0, 1, 2) Определяют коэффициент деления тактовой частоты контроллера для тактирования модуля АЦП. Этот коэффициент зависит от указанных битов следующим образом:
ADPS2
ADPS1
ADPS0
Коэффициент деления
Мы установили в "1" только бит ADPS2, тем самым задав коэффициент деления равным 16. При этом тактовая частота модуля АЦП составит 1000000/16=62500Гц, что вполне укладывается в рекомендуемые границы 50-200 кГц.
Как я уже говорил, эти биты относятся к регистру ADCSRA. Но раз есть А, значит есть и B. И таки да, есть регистр ADCSRB. Его биты определяют источник запуска нового преобразования АЦП в режиме непрерывного преобразования. У нас преобразования должны следовать непосредственно одно за другим, а этот режим устанавливается, если все биты ADCSRB равны 0, поэтому мы данный регистр и не задействовали в программе.
16-20 строки. Полностью аналогичны таковым в программе шестого шага, поэтому на них я не останавливаюсь.
25 строка. Метка ADC_complete определяет начало обработчика прерывания по завершению цикла преобразования АЦП.
26-27 строки. Копирование содержимого РВВ ADCH в РВВ OCR0A через промежуточный РОН r16. Как я уже говорил ранее, нельзя непосредственно скопировать содержимое одного РВВ в другой РВВ. Для этого нужно использовать промежуточный РОН и команды in и out. Регистр ADCH - это старший регистр результата преобразования АЦП, младший называется, как нетрудно догадаться, ADCL, но мы его не используем по причинам, описанным выше. Итак, что же происходит в результате выполнения этих строк? Значение, записанное в ADCH, пропорционально напряжению на входе ADC3, а яркость свечения светодиода пропорциональна значению, записанному в OCR0A. Таким образом, путем копирования одного регистра в другой мы получим, что яркость светодиода будет пропорциональна входному напряжению, что и требовалось по заданию.
28-31 строки. Они должны что-то напоминать внимательному читателю, что-то давно забытое и простое... Если не вспомнили, поведаю, что похожая конструкция использовалась нами в самой первой нашей программе (во втором шаге), только там мы проверяли нажатие кнопки. Что же происходит тут? В строках 28 и 30 мы проверяем 7-й бит регистра r16 (в 28 строке на "1", а в 30 - на "0"). Почему так? По заданию нам нужно включать светодиод LED2 если напряжение на входе больше 2,5 В. 2,5 В - это половина напряжения питания. При этом в регистре ADCH будет записано число 256/2=128. В двоичной форме это число выглядит как 0b10000000. А число 127 имеет представление 0b01111111. Таким образом, если в ADCH значение больше или равно 128, то в старшем (седьмом) бите будет "1", а если меньше, то "0". Именно эту проверку мы и совершаем в строках 28 и 30. Для особо невнимательных читателей, недоумевающих, почему я рассказываю о регистре ADCH, а проверяем мы регистр r16, поясню: в 26 строке мы копируем значение из ADCH в r16, так что в дальнейшем нет никакой разницы, какой из них проверять. В строках 29 и 31 происходит установка (строка 29) или сброс (строка 31) четвертого бита в регистре PORTB, при этом происходит соответственно выключение либо включение светодиода LED2. Логику работы смотрите во втором шаге. Она нисколько не изменилась.
Да вот, как бы и все, что я имел сказать по поводу АЦП. По большому счету я, сам того не желая, практически полностью расписал работу модуля АЦП, хотя поначалу планировал описывать только работу с ассемблером. Ну да ладно. С меня не убудет, а вам меньше лазить по даташитам.
Ну и напоследок задание для самостоятельного выполнения.
Написать программу графической индикации величины напряжения, поступающего на вход АЦП при помощи светодиодов LED1 и LED2. Если напряжение меньше 1/3 от максимального, оба светодиода должны быть погашены, если напряжение находится в пределах от 1/3 до 2/3 от максимального, должен гореть светодиод LED1, а если напряжение больше 2/3 максимального, то должны гореть оба светодиода.
Засим разрешите откланяться до следующего шага. Он обещает быть не в пример труднее предыдущих, так что собирайтесь с силами и с духом.
Ассемблер 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
Условие генерации прерывания
По низкому уровню на выводе INT0
При любом изменении сигнала на выводе INT0
По спадающему фронту сигнала на выводе INT0
По нарастающему фронту сигнала на выводе 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. При этом остановка таймера должна осуществляться только после окончания последнего знака последнего из выводимых в данный момент слов.
Понимаю, что задание это намного сложнее, чем все, что мы до этого писали, но я верю, что вы его осилите, потому что мы уже практически добрались до вершины, а там нет места слабым!