
Ассемблер AVR для начинающих
.doc
Ассемблер AVR для начинающих (шестой шаг)
Ну что же, дорогие читатели, мы с вами освоили (надеюсь) уже достаточно для написания программ на ассемблере. Сегодня я не буду рассказывать вам о каких-то принципиально новых приемах работы. Будем закреплять уже имеющиеся знания и пополнять арсенал ассемблерных команд. В этот раз, как написано в аннотации, рассмотрим, как использовать контроллер для цифро-аналогового преобразования посредством ШИМ. Разберем по очереди эти понятия. Цифро-аналоговое преобразование, как следует из названия, предназначено для преобразования цифрового кода в аналоговую величину. Применительно к нашим контроллерам, можно сказать, что преобразуется число, записанное в определенном регистре, в пропорциональное ему значение напряжения на определенном выводе. На первый взгляд, возможно, эта фраза кажется абракадаброй, но в дальнейшем она обретет для вас смысл. Теперь более сложное понятие широтно-импульсной модуляции. Опять же будем рассматривать его не абстрактно, а применительно к контроллерам. Как вы помните из предыдущего шага, в микроконтроллерах, в частности, в ATtiny13 есть таймеры. Я упоминал уже, что режимы работы их могут быть различными. Но прошлом шаге мы рассмотрели самый простой из них, который именуется Normal. При этом счетчик таймера при каждом тактовом импульсе увеличивается на единицу, а при достижении значения 255 на следующем шаге сбрасывается в 0. При этом генерируется прерывание по переполнению таймера, которое и было нами использовано ранее. Однако оказывается, что при помощи таймера 0 можно осуществлять цифро-аналоговое преобразование. Для этого таймер должен работать в одном из режимов ШИМ. Мы рассмотрим простейший из них - Fast PWM (быстрый ШИМ). Таймер может самостоятельно без участия центрального процессора управлять определенными выводами, устанавливая их в "1" или "0" при достижении счетчиком таймера определенных значений. В классическом случае в момент перехода таймера от 255 к 0 на выводе устанавливается "1", которая будет там до тех пор, пока счет таймера не дойдет до указанного значения, и в этот момент на выводе устанавливается "0". Чем больше будет указанное значение, тем большую часть периода на выходе будет напряжение, и тем большим будет среднее напряжение. Таким образом, величина импульса может варьироваться от нуля (тогда выходное напряжение равно 0) до полного периода (тогда выходное напряжение равно напряжению питания). За подробностями и графическими иллюстрациями обращайтесь к рекомендованной литературе. Так вот, таймер 0 может управлять двумя выводами: OC0A и OC0B, совмещенными с РВ0 и РВ1 соответственно. Для управления этими выводами служат уже упоминавшиеся в предыдущем шаге регистры OCR0A и OCR0B. Именно значение, записанное в эти регистры, будет определять величину выходного напряжения. Оно может изменяться от 0 до 255, при этом напряжение на выходе будет пропорционально изменяться от нуля до напряжения питания. Таким образом, в контроллерах AVR реализован так называемый аппаратный ШИМ: пользователь избавлен от необходимости самостоятельно управлять выводами, все за него делает таймер, нужно лишь один раз его настроить, а потом, по мере необходимости, изменять значение регистров OCR0A и OCR0B - и величина выходного напряжения сразу же автоматически изменится. Возможно, проницательный читатель обратит внимание, что фактически меняется не величина выходного напряжения, а длительность импульса при постоянной его амплитуде. Да, все верно. Но при достаточно высокой частоте переключения вывода (порядка нескольких кГц) пульсации не будут заметны глазу. Для более чувствительных устройств, чем светодиод, на выходе следует устанавливать ВЧ-фильтры, но в нашем случае сойдет и так. Напишем программу для управления яркостью светодиода LED1 при помощи кнопок SB1 и SB2. Нажатие на кнопку SB1 уменьшает яркость на 1 шаг, а нажатие на кнопку SB2 - увеличивает на 1 шаг. Весь диапазон изменения яркости разбит на 10 шагов. При достижении крайних значений яркости (максимальной или минимальной) дальнейшее изменение яркости не происходит. Обработка нажатия кнопок осуществляется при помощи прерывания по изменению состояния выводов. Понимаю, задание звучит более громоздко, чем это было раньше. Хотя и программа также будет самой большой из всех рассмотренных нами ранее. В связи с этим я отныне несколько изменю порядок описания. Вначале по традиции я буду рассказывать о новых командах, но затем описание программы буду осуществлять не построчно, а поблочно с пропуском одинаковых конструкций. Все таки вы уже достаточно знаете, чтобы разжевывать в очередной раз очевидные вещи.
Текст программы, реализующей поставленную задачу, приведен ниже: .include "F:\Prog\AVR\asm\Appnotes\tn13def.inc" .org 0 ;Задание нулевого адреса старта программы rjmp reset ;Безусловный переход к метке reset .org 2 ;Адрес прерывания по изменению состояния выводов rjmp pin_change;Безусловный переход к метке pin_change reset: ;Начало раздела инициализации контроллера ldi r16,RAMEND ;Загрузка в регистр r16 адреса верхней границы ОЗУ out SPL, r16 ;Копирование значения из r16 в регистр указателя стека SPL sbi DDRB, 0 ;Установка 0-го бита в регистре DDRB в "1" (РВ0 - выход) ldi r16,(3<<1) ;Загрузка в r16 двух "1", смещенных на 1 разряд влево out PORTB,r16 ;Включение подтягивающих резисторов на входах РВ1 и РВ2 out PCMSK,r16;Разрешение прерываний по изм. сост. выводов для РВ1 и РВ2 ldi r16,(1<<PCIE) ;Загрузка в регистр r16 единицы в разряд PCIE out GIMSK, r16 ;Разрешение прерывания по изменению состояния выводов 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 ldi r16,125 ;Загрузка в регистр r16 значения 125 (половинная яркость) out OCR0A,r16 ;Задание начальной яркости светодиода LED1 sei ;Глобальное разрешение прерываний main: ;Основной цикл программы rjmp main ;Конец основного цикла программы pin_change: ;Начало обработки прерывания по изменению сост. выводов cpi r16,25 ;Сравниваем значение регистра r16 c 25 brlo sb2 ;Если меньше, то переходим к проверке следующей кнопки sbic PINB, 1 ;Если РВ1=0 (кнопка SB1 нажата), пропустить след. строку rjmp sb2 ;Переход в опросу следующей кнопки rcall delay ;Вызов подпрограммы задержки на дребезг контактов wait1: ;Цикл ожидания, пока нажата кнопка sbis PINB, 1 ;Если РВ1=1 (кнопка SB1 отпущена), пропустить след. строку rjmp wait1 ;иначе перейти к началу цикла ожидания rcall delay ;Вызов подпрограммы задержки на дребезг контактов sbic PINB, 1 ;Если РВ1=0 (кнопка SB1 нажата), пропустить след. строку subi r16,25 ;Вычитание из регистра r16 числа 25 sb2: ;Опрос копки SB2 cpi r16,230 ;Сравниваем значение регистра r16 с 225 brsh set_led;Если больше или равно, то переходим к уст. яркости светодиода sbic PINB, 2 ;Если РВ2=0 (кнопка SB2 нажата), пропустить след. строку rjmp set_led;Переход к установке яркости светодиода rcall delay ;Вызов подпрограммы задержки на дребезг контактов wait2: ;Цикл ожидания, пока нажата кнопка sbis PINB, 2 ;Если РВ2=1 (кнопка SB2 отпущена), пропустить след. строку rjmp wait2 ;иначе перейти к началу цикла ожидания rcall delay ;Вызов подпрограммы задержки на дребезг контактов sbic PINB, 2 ;Если РВ2=0 (кнопка SB2 нажата), пропустить след. строку subi r16,-25 ;Вычитание из регистра r16 числа -25 (прибавление 25) set_led: ;Изменение яркости светодиода out OCR0A,r16;Задание яркости светодиода LED1 reti ;Возврат из подпрограммы обработки прерывания delay: ;Начало подпрограммы задержки ldi r18, 255 ;Загрузка значения в регистр r18 ldi r19, 31 ;Загрузка значения в регистр r19 del: ;Цикл задержки subi r18, 1 ;Вычитание 1 из регистра r18 sbci r19, 0 ;Вычитание 0 из регистра r19 с учетом переноса brcc del ;Если не было переноса вернуться к метке del ret ;Возврат из подпрограммы Объем программы поначалу весьма впечатляет, но, если разобраться в ней, то ничего особо сложного она собой не представляет. Внимательный читатель мог насчитать всего четыре неизвестные команды, с которыми я его с радостью и познакомлю. Команда subi имеет два операнда: РОН и константу. В результате ее выполнения из указанного РОН вычитается указанная константа, и результат записывается в тот же РОН. Надо сказать, что в ассемблере AVR отсутствует аналогичная команда для прибавления константы к РОН, однако хитрые программисты нашли выход из ситуации: они вычитают отрицательную константу, что, как мы знаем из средней школы, равносильно сложению. В приведенной программе также встречается указанный прием. Команда cpi также имеет два операнда: опять же РОН и константу. В результате ее выполнения также происходит вычитание указанной константы из указанного РОН, но в отличие от предыдущей команды результат никуда не записывается. "Зачем же нам нужна команда, результат которой нигде не сохраняется?" - спросит въедливый читатель. А тут все просто. Я уже говорил ранее, что многие из выполняемых в контроллере операций изменяют те или иные биты так называемого статусного регистра SREG. И данная команда не исключение. Ведь в результате ее выполнения разность может оказаться большей нуля, меньшей нуля, равной нулю либо не равной нулю. Каждый из этих этих результатов влияет на регистр SREG. Обычно за данной командой следует какая-либо операция условного перехода, типа уже известной нам brcc. Команда brlo имеет один операнд - метку. После ее выполнения происходит переход к указанной метке в том случае, если результат предыдущей операции был отрицательный. Это еще одна команда условного перехода, которая может следовать за cpi (как у нас в программе, собственно, и есть). Результат cpi будет отрицательный, если значение, записанное в РОН меньше указанной константы. Таким образом, команда brlo имеет название "переход, если меньше". Команда brsh тоже имеет один операнд - метку. По своему назначению она противоположна предыдущей команде. После ее выполнения происходит переход к указанной метке, если результат предыдущей операции был неотрицательный. Команда brsh еще называется "переход, если больше или равно". Вот, собственно, и все новые команды. Фактически из более, чем сотенного набора команд, имеющихся в распоряжении AVR-контроллеров, наиболее часто используется едва ли треть из них. Теперь рассмотрим непосредственно программу. Как я уже сказал выше, знакомые строки я буду опускать из описания. В самом начале известная нам с прошлого раза таблица векторов прерываний. Она отличается тем, что в строках 4 и 5 указан другой адрес, и, соответственно, другой вектор. На этот раз мы включаем прерывание по изменению состояния выводов, вектор которого находится по адресу 2, что мы и указали в строке 4. Кроме того, изменили название метки начала обработчика прерывания на pin_change, что отражает сущность задействованного прерывания. Далее с метки reset начинается раздел инициализации. На нем остановлюсь подробнее, поскольку в нем много новшеств. 11 строка. В ней командой ldi в регистр r16 загружается конструкция (3<<1). Ранее мы использовали только конструкции вида (1<<х). Но такие конструкции позволяли изменить за один раз только один бит. Что же мы имеем в данном случае? Тут необходимо вспомнить, что число 3 имеет в двоичном представлении значение 0b11, то есть две подряд идущие единицы. Тогда все становится понятным: операцией сдвига мы смещаем две подряд идущие единицы на один бит влево, получая в результате единицы в первом и втором битах (не забываем, что счет битов в байте начинается в нуля). 12 строка. Копируем содержимое регистра r16 в регистр PORTB. Поскольку, как мы уже договорились, в r16 находятся единицы в битах 1 и 2, а выводы РВ1 и РВ2 определены как входы, то данной командой включаются подтягивающие резисторы на этих входах. В скобках напомню, для забывчивых читателей, что именно к этим выводам у нас подключены кнопки SB1 и SB2 соответственно. 13 строка. Аналогична предыдущей по структуре, только содержимое регистра r16 копируется в регистр PCMSK. Я уже говорил в предыдущий раз, что прерывание по изменению состояния выводов может вызываться любым изменением на указанных выводах. Так вот, регистр PCMSK и определяет, какие же выводы смогут вызвать прерывание. Поскольку нам необходимо задействовать обе кнопки как источники прерываний, то неудивительно, что мы записали в PCMSK то же значение, что и в PORTB. 14 и 15 строки. В регистр GIMSK записывается единица в бит PCIE. Регистр GIMSK по своему назначению схож с рассмотренным на предыдущем шаге регистром TIMSK, только если последний разрешал прерывания, связанные с таймером 0, то GIMSK разрешает внешние прерывания. Бит PCIE разрешает прерывание по изменению состояния выводов, а бит INT0 - внешнее прерывание 0. Итак, мы настроили входы РВ1 и РВ2 и разрешили прерывание по изменению их состояния. 16 и 17 строки. В регистр TCCR0A записываются единицы в биты WGM00, WGM01, COM0A1 и COM0A0. Разберемся по порядку. Биты WGM0x определяют режим работы таймера. Если они равны 0, то устанавливается режим Normal (именно поэтому в прошлый раз мы о них не говорили). Установка же в единицу битов WGM00 и WGM01 приводит к переключению в режим Fast PWM, то есть быстрый ШИМ. Остальные комбинации этих битов можете посмотреть в литературе или подождать, пока нам понадобятся новые режимы, и тогда я о них сам сообщу. Биты COM0A0 и COM0A1 определяют как раз связь таймера 0 с выводом ОС0А. Связь эта приведена в таблице ниже:
Поскольку мы установили в "1" оба бита, то у нас включается инвертированный ШИМ-сигнал. Почему же именно инвертированный? Это опять же связано со схемным решением. Я думаю, вы помните (а если не помните, то напоминаю), что светодиоды LED1 и LED2 включаются тогда, когда на соответствующих им выходах присутствует логический "0". Именно с этим и связана инверсия ШИМ-сигнала. В нашем случае яркость светодиода будет тем больше, чем большую часть периода на выходе будет 0. Ну и еще раз напомню на всякий случай, что TCCR0A - это второй регистр из пары (TCCR0A, TCCR0B), применяющейся для настройки таймера 0. 18 и 19 строки. В регистр TCCR0B записывается "1" в бит CS01, тем самым задавая коэффициент деления тактовой частоты для таймера равным 8. Следовательно частота переключения светодиода будет составлять 1000000/(8 х 256) = 488 Гц, что глазу абсолютно незаметно. 20 и 21 стоки. В регистр OCR0A записывается число 125. Мы договорились в условии задачи, что изменение яркости должно происходить в 10 шагов. Поскольку максимально возможное значение регистра OCR0A равно 255, то шаг должен быть равен 255/10 = 25,5. Отбрасываем дробную часть и получаем величину шага, равную 25. При таком шаге максимальное число в регистре OCR0A будет равно 250, а не 255, но такая малая разница в яркости будет глазу не заметна. Итак, мы записали в регистр OCR0A начальное значение, равное 125, тем самым задав при старте программы половинную яркость свечения. С 27 строки начинается подпрограмма обработки прерывания по изменению состояния выводов (а для нас - по нажатию на кнопки). Обратите внимание, что на обе кнопки у нас одно прерывание, так что внутри обработчика придется проверять и SB1, и SB2. 28 и 29 строки. Сравнивается значение в регистре r16 с константой 25, и, если r16 < 25, то осуществляется переход к опросу кнопки SB2 (метка sb2). В противном же случае происходит обработка кнопки SB1, начинающаяся с 30-й строки (как вы помните из условия задачи, по нажатию на SB1 яркость должна уменьшаться на один шаг). Цель проверки в строках 28 и 29 - исключить вычитание из регистра r16, если его значение может стать отрицательным. Таким образом реализуется указанное в задании ограничение при минимальном значении яркости. 30-38 строки. Обработка нажатия кнопки SB1. Полное описание я опущу, поскольку аналогичная конструкция встречалась в шаге 4. Несколько нюансов. В строке 31 команда безусловного перехода отправляет нас не к началу цикла, а к обработке кнопки SB2. Оно и логично. Если не нажата кнопка SB1, то прерывание вызвано кнопкой SB2, и переходим к ее обработке. В строке 38 выполняется вычитание из регистра r16 шага изменения яркости, равного 25. В принципе можно было бы после этой строки сделать безусловный переход к метке set_led, в которой происходит копирование регистра r16 в OCR0A, но мы этого не делаем, поскольку данный переход осуществляется несколькими строками ниже. По большому счету так делать неправильно, но мы считаем изначально, что пользователь окажется благоразумным и не будет нажимать на две кнопки одновременно. 40-51 строки. Обработка нажатия кнопки SB2. Отличия от строк 28-38 следующие: - в строке 41 происходит сравнение содержимого регистра r16 с константой 230, чтобы не выйти за верхний предел яркости; - в строке 42 осуществляется переход к метке set_led, если r16 >= 230. То есть в данном случае уже наращивать яркость некуда и обрабатывать нажатие кнопки SB2 не нужно; - в строке 44 также осуществляется переход к метке set_led, если кнопка SB2 не нажата. Именно об этом переходе я говорил при описании 38 строки. - в строке 51 происходит вычитание константы -25 из регистра r16, что равносильно прибавлению к нему 25. Таким образом, в этой строке происходит увеличение яркости на 1 шаг, что от нас и требовалось по заданию. 53 и 54 строки. Уже не раз упоминавшийся участок программы для задания яркости светодиода. В строке 54 командой out происходит копирование содержимого регистра r16 в OCR0A, при этом практически сразу же меняется яркость светодиода. 55 строка. Возврат из прерывания по изменению состояния выводов командой reti. 57-64 строки. Подпрограмма задержки на дребезг контактов. Абсолютно аналогична таковой в четвертом шаге, поэтому на ней я не останавливаюсь. Вот, собственно, и все, что касается вышеприведенной программы. Как я уже упоминал, чем дальше мы движемся, тем более сложные и объемные будут наши программы. Но я уверен, что это вас не остановит, поскольку нет предела совершенству. Вообще я изначально планировал раздел "для начинающих" завершить на десятом шаге, так что мы с вами уже благополучно прошли больше половины пути. Надеюсь, написанное оказалось вам хоть немного полезным. По традиции в конце задания для самостоятельного решения. 1. Изменить программу таким образом, чтобы изменение яркости происходило не один раз за нажатие, а постоянно, пока соответствующая кнопка удерживается нажатой, с частотой 2 шага в секунду. 2. Изменить программу таким образом, чтобы задействовать неинвертирущий ШИМ вместо инвертирующего. При этом конечный пользователь не должен заметить никакой разницы в работе программ. |
Ассемблер AVR для начинающих (седьмой шаг)
Этот шаг можно считать релаксирующим по сравнению с предыдущим. И программа попроще, и новых команд поменьше, да и сам модуль АЦП легче для понимания. Итак, начнем с рассмотрения самого модуля АЦП. Для тех, кто совершенно не в курсе (хотя, думаю, таких на седьмом шаге уже нет, но все же...), расшифровываю: АЦП - аналого-цифровое преобразование. Тут ситуация, обратная ЦАП. Модуль АЦП преобразует напряжение, поступающее на вход, в пропорциональное ему число, записываемое в определенный регистр. В микроконтроллерах 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 следующим образом:
Поскольку мы уже выше договорились, что движок переменного резистора подключен ко входу 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) Определяют коэффициент деления тактовой частоты контроллера для тактирования модуля АЦП. Этот коэффициент зависит от указанных битов следующим образом:
Мы установили в "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 максимального, то должны гореть оба светодиода. Засим разрешите откланяться до следующего шага. Он обещает быть не в пример труднее предыдущих, так что собирайтесь с силами и с духом. |