Ну что же, дорогие читатели, мы с вами освоили (надеюсь) уже достаточно для написания программ на ассемблере. Сегодня я не буду рассказывать вам о каких-то принципиально новых приемах работы. Будем закреплять уже имеющиеся знания и пополнять арсенал ассемблерных команд.
В этот раз, как написано в аннотации, рассмотрим, как использовать контроллер для цифро-аналогового преобразования посредством ШИМ. Разберем по очереди эти понятия.
Цифро-аналоговое преобразование, как следует из названия, предназначено для преобразования цифрового кода в аналоговую величину. Применительно к нашим контроллерам, можно сказать, что преобразуется число, записанное в определенном регистре, в пропорциональное ему значение напряжения на определенном выводе. На первый взгляд, возможно, эта фраза кажется абракадаброй, но в дальнейшем она обретет для вас смысл.
Теперь более сложное понятие широтно-импульсной модуляции. Опять же будем рассматривать его не абстрактно, а применительно к контроллерам. Как вы помните из предыдущего шага, в микроконтроллерах, в частности, в 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А. Связь эта приведена в таблице ниже:
COM0A1 | COM0A0 | Описание |
0 | 0
| Таймер 0 отключен от вывода ОС0А
|
0 | 1 | Если WGM02 = 0, то таймер 0 отключен от вывода ОС0А Если WGM02 = 1, то состояние вывода меняется на противоположное при равенстве счетчика таймера и содержимого регистра OCR0A
|
1 | 0 | Сбрасывается в 0 при равенстве счетчика таймера и содержимого регистра OCR0A. Устанавливается в 1 при достижении счетчика максимального значения (неинвертированный ШИМ)
|
1 | 1 | Устанавливается в 1 при равенстве счетчика таймера и содержимого регистра
OCR0A. Сбрасывается в 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. Изменить программу таким образом, чтобы задействовать неинвертирущий ШИМ вместо инвертирующего. При этом конечный пользователь не должен заметить никакой разницы в работе программ.
Если у вас возникнут вопросы, задавайте их на форуме или здесь в виде комментариев к статье.
Желаю успехов!