Итак, мои добросовестные читатели, надеюсь, вы благополучно освоили изложенный ранее материал и справились с предложенными мною заданиями. Настало время обогатить себя новыми знаниями.
В прошлый раз я поведал о том, что все действия, которые выполняет контроллер, определяются так называемыми регистрами, и даже рассказал о трех из них: PORTB, DDRB, PINB.
На самом деле этих регистров намного больше. Кроме того, существует две их основные разновидности. Рассмотренные ранее относятся к так называемым регистрам ввода-вывода (РВВ). Этот тип регистров определяет функционирование различных узлов и модулей контроллера: портов ввода/вывода, таймеров, прерываний, энергонезависимой памяти, АЦП, компаратора, цифровых интерфейсов и др. Для каждого из этих модулей определено по одному, а чаще по несколько РВВ. По мере изложения материала я буду рассказывать о них подробнее. Сегодня же нам понадобятся только уже известные нам по прошлому разу РВВ.
Кроме РВВ, количество и состав которых отличается у разных микроконтроллеров, имеются еще так называемые регистры общего назначения (РОН). Все микроконтроллеры AVR имеют по 32 РОН, которые называются очень просто: r0, r1, r2, ... r31. Забегая вперед, скажу, что не все эти регистры равнозначны, но большей части команд нет разницы, какие из них задействовать. Следует уяснить и запомнить, что большинство операций в контроллере выполняется именно над РОН. Фактически, рассмотренные нами в прошлый раз команды составляют большую часть доступных команд для работы непосредственно с РВВ. Все РОН представляют собой однобайтовые регистры, к которым в ассемблере можно обращаться, указывая их имя.
Но это все слова. Перейдем теперь к практике. Напишем программу, которая будет самостоятельно мигать светодиодом LED2 с определенной частотой. Создаем новую папку с именем step3 и изменяем файл build.bat для ассемблирования из этой папки. Открываем ASM_Ed и пишем в нем следующий текст:
.include "F:\Prog\AVR\asm\Appnotes\tn13def.inc" sbi DDRB, 4 ;Линия РВ4 - выход main: ;Основной цикл программы sbic PINB, 4 ;Если РВ4=0 (светодиод зажжен), то пропустить след. строку cbi PORTB,4 ;Установка РВ4 в 0 (включение светодиода) sbis PINB, 4 ;Если РВ4=1 (светодиод погашен), то пропустить след. строку sbi PORTB,4 ;Установка РВ4 в 1 (выключение светодиода) ldi r16, 255 ;Загрузка значения в регистр r16 ldi r17, 255 ;Загрузка значения в регистр r17 delay: ;Цикл задержки subi r16, 1 ;Вычитание 1 из регистра r16 sbci r17, 0 ;Вычитание с переносом из регистра r17 brcc delay ;Если не было переноса вернуться к метке delay rjmp main ;Вернуться к метке main
Эта программа бесспорно уже побольше предыдущей. Внимательный читатель может увидеть в ней как знакомые ранее команды, так и совершенно новые. Вначале по традиции расскажу о нововведениях в программе, а затем об алгоритме работы программы.
Можно увидеть, что помимо команд и комментариев, в программе появились метки. Это main в 3 строке и delay в 11 строке. Их можно узнать по двоеточию после них. Метки не занимают памяти контроллера, а лишь служат некоторым другим командам для навигации по программе.
Теперь о новых командах.
Команда ldi имеет два операнда: первый - РОН, а второй - любая численная константа в диапазоне от 0 до 255 (поскольку все РОН однобайтовые, то в них можно записывать только числа в указанном диапазоне, при попытке записать большее число, транслятор выдаст предупреждение). В результате выполнения этой команды в указанный РОН загружается указанная константа. То есть если провести аналогию с языками высокого уровня, эта команда сходна с операцией присвоения (РОН = k).
Команда subi по синтаксису аналогична вышеприведенной. В результате ее выполнения из указанного РОН вычитается указанная константа, а результат заносится в тот же регистр (РОН = РОН - k).
Команда sbci также аналогична по синтаксису, и сходна по функциональному назначению с предыдущей. Она также вычитает из указанного РОН указанную константу, но с учетом переноса. Что это означает? По сути практически любая логическая либо математическая операция устанавливает либо сбрасывает те или иные биты в так называемом статусном регистре SREG. Доступ к нему имеется только с использованием специальных ассемблерных команд. Обо всех битах я напишу в последующих статьях, пока же скажу, что есть в нем так называемый бит переноса С. Этот бит устанавливается в "1", если в результате предыдущей операции произошло переполнение в старший разряд при сложении либо заем из старшего разряда при вычитании (то есть при попытке прибавить число к 255 либо вычесть число из 0). Итак, команда sbic вычитает из РОН указанную константу и бит переноса и записывает результат в тот же РОН (РОН = РОН - k - C).
Команда brcc относится к командам условного перехода. Единственным ее аргументом является метка, на которую перемещается программный счетчик, если в результате выполнения предыдущей операции не произошло переноса, то есть, если был сброшен бит С статусного регистра. Если же перенос произошел, выполняется следующая за ней команда (Если С=0, то перейти к метке, иначе перейти к следующей команде).
Команда rjmp является командой безусловного перехода. Ее единственным аргументом также является метка, на который перемещается программный счетчик, однако перемещение выполняется всегда, без каких-либо условий. Таким образом, следующая за rjmp команда никогда не выполнится, если там не будет установлена другая метка, на которую будет совершен переход при помощи какой-то иной команды перехода.
Ну что же, наш багаж знаний обогатился еще на 5 команд. Теперь в нашем распоряжении их целых 9 штук.
Рассмотрим алгоритм работы программы. Те строки, которые содержат уже известные нам команды, я буду описывать кратко, считая, что читатель с ними знаком.
1 строка. Подключение файла tn13def.inc к нашему ассемблерному файлу.
2 строка. Запись "1" в 4-й бит регистра DDRB для перевода линии РВ4 на выход
3 строка. Метка "main". В принципе, эту метку можно было бы и не ставить, но ее использование является правилом хорошего тона, потому что незачем каждый раз выполнять инициализацию портов ввода-вывода. Пока у нас она состоит всего из одной команды - это не так страшно, но ведь в реальных программах это десятки команд, и некоторые из них сбрасывают используемые модули к исходному состоянию. А зачем нам оно надо? Поэтому возьмите себе за правило. Сначала - инициализация, затем - зацикливание программы за пределами раздела инициализации.
4 и 5 строки. Если бит 4 в регистре PINB сброшен (то есть светодиод погашен, команда sbic), то переходим к строке 6, а иначе сбрасываем этот бит в регистре PORTB (команда cbi).
6 и 7 строки. Если бит 4 в регистре PINB установлен (то есть светодиод зажжен, команда
sbis), то переходим к строке 8, а иначе устанавливаем этот бит в регистре PORTB (команда sbi).
Таким образом, строки 4 - 7 выполняют самостоятельное переключение светодиода (проверяется состояние светодиода и меняется на противоположное). Если так и оставить программу, то окажется, что это переключение будет происходить с очень высокой скоростью, и глаз его не увидит, разве что заметит, что яркость светодиода стала меньше. Чтобы заставить светодиод мигать с приемлемой частотой, необходимо осуществить задержку выполнения программы. Обычно это делается путем многократного вычитания либо сложения. Поскольку каждая операция требует определенного времени, то выполнение однотипных команд несколько тысяч раз подряд как раз и обеспечит нам требуемую задержку. Рассмотрим теперь, как это реализовано у нас.
Строка 8. При помощи команды ldi в РОН r16 загружается максимально возможное число 255. Почему именно r16 а не r0? Дело в том, что команды, имеющие вторым операндом константу, не работают с регистрами r0-r15. При попытке указать в качестве первого операнда такой регистр транслятор выдаст ошибку и откажется работать далее.
Строка 9. Аналогична строке 8, только теперь загружается 255 в регистр r17. От значений, загруженных в эти регистры, будет зависеть наша задержка.
Строка 10. Метка "delay". Это начало цикла вычитания. Мы загрузили в два регистра числа 255. Теперь наша задача вычитать из них единцы, пока оба не станут равными 0. Когда это произойдет, мы сможем покинуть этот цикл.
Строка 11. Командой subi мы вычитаем единицу из регистра r16. Вычитание производится просто, без всяких условий и переносов.
Строка 12. На первый взгляд она кажется бессмысленной. Мы вычитаем из регистра r17 нуль. Однако, не стоит забывать, что команда sbci вычитает число с учетом переноса. Как это все работает в комплексе, я расскажу далее.
Строка 13. Проверка бита переноса С. Поскольку эта команда идет следом за командой вычитания из регистра r17, то проверяется перенос именно из этого регистра. Пока содержимое регистра r17 будет больше либо равно 0, команда brcc будет отправлять нас к метке delay. Как только произойдет попытка вычитания из нуля в r17, мы сразу же переходим к строке 14.
Теперь рассмотрим работу строк 10-13 в комплексе. Сначала мы вычитаем 1 из регистра r16. В следующей строке вычитаем 0 с учетом переноса из регистра r17. Пока содержимое регистра r16 больше или равно 0, содержимое регистра r17 не изменится. При вычитании единицы из нуля в регистре r16 происходят следующие события: в самом r16 устанавливается значение 255, кроме того, устанавливается флаг переноса С. Теперь при выполнении команды sbci из r17 вычтется 1. Таким образом, вычитание единицы из r17 происходит только тогда, когда в регистре r16 происходит попытка вычесть единицу из нуля. Когда же в регистре r17 произойдет вычитание единицы из нуля, также установится флаг переноса, который, будучи отловленным командой brcc, завершит цикл.
За счет такой нехитрой конструкции получается, что весь цикл будет выполняться 256 х 256 = 65536 раз. Если учесть, что каждая команда выполняется один такт контроллера, и на переход к началу цикла тратится еще один такт, то каждый проход цикла выполняется за четыре такта. Таким образом имеем задержку в 4 х 65536 = 262144 тактов. Если учесть, что частота контроллера по умолчанию составляет 1 Мгц, то задержка будет равна 262144/1000000 = 0,26 с. Таким образом, светодиод будет менять свое состояние каждую четверть секунды, что даст нам частоту мигания приблизительно равную 2 Гц.
Ну и наконец строка 14, о которой мы уже было позабыли. В ней происходит безусловный переход к метке main, таким образом мы исключаем повторную инициализацию вывода РВ4.
Вот таким образом работает написанная нами программа. Замечу, что задавая различные значения, записываемые в регистры r16 - r17 можно получать разные задержки, меньшие 0,26, используя вышеизложенные расчеты. А как же быть, если необходимо сделать большую задержку? Тогда нужно добавить еще один регистр, например r18. И реализация задержки станет выглядеть так:
ldi r16, 255 ;Загрузка значения в регистр r16 ldi r17, 255 ;Загрузка значения в регистр r17 ldi r18, 2 ;Загрузка значения в регистр r18 delay: ;Цикл задержки subi r16, 1 ;Вычитание 1 из регистра r16 sbci r17, 0 ;Вычитание с переносом из регистра r17 sbci r18, 0 ;Вычитание с переносом из регистра r18 brcc delay ;Если не было переноса вернуться к метке delay
Общее число проходов цикла теперь станет равно 256х256х3 = 196608, а с учетом того, что в цикле появилась еще одна команда, теперь он будет выполняться за 5 тактов. Общее число тактов задержки составит 196608х5=983040, а период задержки: 983040/1000000 = 0,98 с.
Возможно, внимательный читатель спросит, а почему же мы загружаем 255 и 2, а умножаем 256 и 3. Тут все дело в том, что счет введется не с 1, как мы привыкли, а с 0, и за счет этого происходит один лишний проход цикла. Вот так просто это все объясняется.
Надеюсь, все мои рассуждения и пояснения понятны читателю. На этом я считаю, что шаг 3 можно считать сделанным. Мы узнали новые команды, ознакомились с разными видами регистров контроллера.
В качестве самостоятельно работы предлагаю следующую задачу: организовать поочередное переключение светодиодов LED1 и LED2 с частотой, максимально близкой к 0,5 Гц, то есть с периодом в 2 с.
Если у вас возникнут вопросы, задавайте их на форуме или здесь в виде комментариев к статье.
sbic PINB, LED2 ;Если РВ4=0 (светодиод зажжен), то пропустить след. строку cbi PORTB, LED2 ;Установка РВ4 в 0 (включение светодиода) sbis PINB, LED2 ;Если РВ4=1 (светодиод погашен), то пропустить след. строку sbi PORTB, LED2 ;Установка РВ4 в 1 (выключение светодиода)
ldi r16, 255 ;Загрузка значения в регистр r16 ldi r17, 255 ;Загрузка значения в регистр r17 ldi r18, 2 delay: subi r16, 1 ;Вычитание 1 из регистра r16 sbci r17, 0 ;Вычитание с переносом из регистра r17 sbci r18, 0 ;Вычитание с переносом из регистра r17 brcc delay ;Если не было переноса вернуться к метке delay rjmp main
Вот в таком виде заработало в протеусе. Теперь изменение значения всех трех регистров влечет изменение задержки на величину измененной величины. Установлено на 1 секунду ровно sbi DDRB, 4 ;Линия РВ4 - выход
main: ;Основной цикл программы cbi PORTB,4 ;Установка РВ4 в 0 (включение светодиода) rcall delay sbi PORTB,4 ;Установка РВ4 в 1 (выключение светодиода) rcall delay ;Создаем задержку rjmp main ;Вернуться к метке main
delay: ldi r18, 23 ;Загрузка значения в регистр r18 rjmp del1 ;Переходим к метке del1 ret
del3: ldi r17, 255 ;Загрузка значения в регистр r17 sbci r18, 1 ;Вычитание единицы с переносом из r18 brcc del1 ;Если не было переноса вернуться к метке del1 ret
del2: sbci r17, 1 ;Вычитание единицы с переносом из r17 brcc del1 ;Если не было переноса вернуться к метке del1 rjmp del3 ;Если перенос всё же произошел то прыгаем на del3 ret
del1: ldi r16, 255 ;Загрузка значения в регистр r16 rcall pd1 ;Запускаем цикл уменьшения младшего регистра на единицу rjmp del2 ;Когда младший регистр исчерпан переходим к уменьшению среднего регистра на единицу ret
pd1: sbci r16, 1 ;Вычитание 1 из регистра r16 brcc pd1 ;Если переноса не было повторяем процедуру, иначе возвращаемся к del1 где перейдем к уменьшению среднего регистра ret
Правда пока делал понял что нахрен это не нужно)) при необходимости точной задержки надо использовать таймер)) а этот вариант весит 48 байт вместо 28 что на сайте
1) Комментарии в самом листинге неправильные. Вместо выключение написано включение и наоборот 2) по сути мы выключаем диод, затем тут же его включаем затем делаем задержку, и по новой, если sbi и cbi поменять местами то будет наоборот кратковременно включаем а потом выключаем и ждем. для глаза по сути получится постоянно последнее состояние диода, тут приводили листинг с задержками после каждого изменения диода, со словами что так работает в протеусе, но я не понимаю почему в виде как здесь на сайте написано оно должно работать в реальности?!!! Всмысле не просто гореть или не гореть, а именно мигать. 3)А теперь собственно мой вопрос: делал различные варианты реализации задержки, в том числе и ваш вариант, Почему то задержка реагирует только на изменение значения того регистра который последним вычисляется, в вашем примере 17-й. Изменение значений любых других регистров при входе в процедуру задержки не дают результатов вообще. при этом их отсутствие дает ожидаемый результат, т.е. задержка резко становится меньше(разумеется удаление из кода одного из регистров сопровождается и удалением из кода его уменьшение на единицу), мат части пока нету, поэтому работаю в протеусе, может сталкивались? глюк протеуса или я чего то не понимаю?
1) Комментарии в самом листинге неправильные. Вместо выключение написано включение и наоборот 2) по сути мы выключаем диод, затем тут же его включаем затем делаем задержку, и по новой, если sbi и cbi поменять местами то будет наоборот кратковременно включаем а потом выключаем и ждем. для глаза по сути получится постоянно последнее состояние диода, тут приводили листинг с задержками после каждого изменения диода, со словами что так работает в протеусе, но я не понимаю почему оно должно в таком виде работать в реальности?!!! 3)А теперь собственно мой вопрос: делал различные варианты реализации задержки, в том числе и ваш вариант, Почему то задержка реагирует только на изменение значения того регистра который последним вычисляется, в вашем примере 17-й. Изменение значений любых других регистров при входе в процедуру задержки не дают результатов вообще. при этом их отсутствие дает ожидаемый результат, т.е. задержка резко становится меньше(разумеется удаление из кода одного из регистров сопровождается и удалением из кода его уменьшение на единицу), мат части пока нету, поэтому работаю в протеусе, может сталкивались? глюк протеуса или я чего то не понимаю?
Спасибо за данные уроки, как и многие здесь неделями копался в статьях и ничего не понял - тут совсем другая история. А вот в таком виде программа работает в Proteus.
Очень познавательный урок! Подскажите, а кто-то пробовал живьём эту программу? Или только на эмуляторе?... Все дело в том, что эта программа возможно действительно рабочая на некоторых типах микроконтроллеров!!! И тут дело не в тонкостях языка Ассемблера, а в структуре микроконтроллера. Т.е., некоторые типы микроконтроллеров используют синхронный обмен с портами, т.е., обмен состоянием между регистрами портов (PORT, DDR, PIN) и физическими выводами микроконтроллера происходит по фронту тактового сигнала. А, из этого следует, что установка нового состояния произойдёт после выполнения такта соответствующей команды, а данные для выполнения проверки будут взяты перед тактом команды выполнения проверки состояния, т.е., фактический обмен информацией произойдёт в одно и то же мгновение времени!!! Ну, а если учесть, что сигнал мгновенно не меняется, то... ...в некоторых типах микроконтроллеров правильная работа приведённой программы вполне возможна...
Роман, Я все уроки проверял только на реальном микроконтроллере, и там действительно все работает очень четко. Видимо, действительно складываются перечисленные Вами факторы.