AVR blinking - el-pths/w GitHub Wiki
AVR ‐ начало ‐ мигание светодиодом
Ассемблер - язык, который позволяет непосредственно записать команды для процессора. Конечно, у разных процессоров и набор команд может быть разный. Микроконтроллеры архитектуры AVR все используют похожий набор, с которым мы и познакомимся.
Мы будем иногда использовать числа в 16-ричной системе. Не стоит их пугаться и не надо все время их переводить в 10-чную, достаточно просто понимать что они записываются цифрами 0123456789ABCDEF (до F) и впереди мы пишем префикс 0x чтобы их отличать. Двухзначное число в 16-ричной системе как раз помещается точно в 1 байт (каждая цифра занимает ровно полбайта или 4 бита).
Кроме того нам нужно освоиться с памятью и другими средствами контроллера, на которые эти команды влияют.
Оперативная память контроллера имеет несколько разных "зон" или областей. Сейчас нам понадобятся две.
Первая область содержит 32 однобайтовых ячейки, называемых "регистры общего назначения". Это вроде переменных в обычном языке программирования, в каждый такой регистр можно записать число от 0 до 255 и выполнить над ним какие-то операции. Регистры
называются именами от R0
до R31
. При этом с регистрами от 16-го до 31-го могут работать почти все команды, а с 0 по 15й не все. Поэтому
удобнее обычно начинать с 16-го.
Посмотрим примеры команд, что можно с этими регистрами сделать.
CLR R16
(от слова "clear") - очистить регистр, записав в него 0INC R16
(от слова "increment") - увеличить значение в регистре на единицуMOV R19, R16
(от слова "move") - скопировать значение из R16 в R19 (обратите внимание на порядок записи)ADD R16, R19
(понятно от какого слова) - прибавить к R16 значение из R19 (опять заметим что "целевой" регистр записан первым)
Что, например, будет результатом всех этих четырех инструкций, если их выполнить по порядку? Сначала мы записали в R16 ноль, потом увеличили его - стала единица. Скопировали эту единицу в R19 и наконец прибавили R19 обратно к R16 - очевидно в нём окажется 2.
Есть и команда которая сразу записывает в регистр требуемую константу:
LDI R16, 5
(от слова "load immediate") - загрузить "непосредственное" значение (то есть константу, а не число из другого регистра)
Что произойдёт, если в регистре записано максимальное значение 255 а мы ещё раз прибавим к нему единицу? Значение превратится в 0, но факт того что случилось "переполнение" или точнее "перенос" как бы в следующий разряд - его процессор заметит. Существует специальный "флаг" (можно считать что это однобитовый регистр) под названием Carry (перенос) - туда и будет записана единичка. Этот же эффект случится если прибавить к 200 например ещё 100 - результатом будет 44 и единичка во флаге переноса. Если же результат сложения не превысил 255 то во флаге переноса после сложения запишется 0.
Как использовать этот флаг переноса при сложении чисел состоящих из нескольких байт? Есть специальная версия команды сложения (ADC), которая прибавляет не только регистр к регистру но ещё и добавляет единичку из флага переноса (если она там установилась предыдущей операцией). Рассмотрим пример, пусть некое число записано в паре регистров R17:R16 и мы хотим прибавить к нему число из пары R21:R20:
LDI R17, 3
LDI R16, 200
LDI R21, 0
LDI R20, 100
ADD R16, R20
ADC R17, R21
Первые две команды записали 3 и 200 в R17:R16, можно считать что это число 0x3C8 если записать его в шестнадцатеричном виде - 3 ушло в "старший" байт и 0xC8 = 200 в "младший". В десятичном виде это 968 что совершенно не важно, т.к. контроллер сам по себе нам ни в десятичном ни в шестнадцтеричном, ни в двоичном виде чисел показывать не может, у него и экрана-то нет.
Следующие две команды записали в R21:R20 пару чисел 0:100 (ну это и в африке 100, т.к. в старшем байте ноль)
После этого мы складываем младшие байты. ADD R16, R20
вычислит результат 200+100=300 - что в шестнадцатеричном виде выглядит как 0x12C. Из
них 2C
влезают обратно в младший байт (в R16 - это как раз 44 по нашему) - а вот единичка уходит во флаг переноса.
Следующая команда ADC R17, R21
складывает 3 + 0
и прибавляет флаг переноса, в котором удачно оказалась единица - получается 4. В общем
это очень похоже на сложение в столбик, только цифры не от 0 до 9 а от 0 до 255.
Мы уже почти готовы мигать светодиодом. Но как контроллер работает с "внешними устройствами"? Мы пока видели только манипуляции над регистрами - но переписывание чисел из переменной в переменную никакого внешнего эффекта не даст.
Для этого есть другая область памяти, называемая "регистры ввода-вывода". Таких ячеек 61 штуки, они тоже однобайтовые. Интересно в них то, что они на самом деле не память - а с этими адресами связаны различные устройства контроллера. И запись каких-либо значений (ноликов и единиц) в эти ячейки вызывает изменени состояний этих устройств.
В частности ноги контроллера под названиями PB0, PB1,... PB7
связаны с ячейкой номер 0x18 (и не только) в этой области памяти - как только
в какой-либо из 8 бит этой ячейки записывается единичка, то соответствующая нога подключается внутренним резистором к плюсу питания - на
ней появляется "электрическая" единица, хотя и слабенькая (из-за внутреннего резистора). А нам очень удобно - подключим между ногой и GND светодиод - и он сможет работать без дополнительного резистора (т.к. есть внутренний).
Команда для записи в эти "волшебные" регистры выглядит так
OUT 0x18, R16
Здесь первым идёт адрес выбранного регистра ввода-вывода, а вторым регистр общего назначения. Конечно, в нём уже должно быть подготовлено нужное число, например:
LDI R16, 5
OUT 0x18, R16
При выполнении второй команды мы записываем число 5 (которое в двоичном виде выглядит как 00000101) в ячейку управляющую ногами (совокупно она называется PORTB) - если рассмотреть записанное число как двоичное, мы поймём, какие ноги "подключились" к плюсу - считать биты надо начиная с младшего (правого) который считается нулевым, а старший (левый) седьмым. То есть в данном примере мы подключили ноги PB0 и PB2 к плюсу, а остальные (PB1, PB3...PB7) остались не подключены никуда. Если на ноге PB0 есть светодиод до GND - он начнет светиться.
Удобно вместо 0x18 завести константу с помощью "директивы" .equ
(это никак не влияет на контроллер, все такие константы просто
незаметно заменяются числами перед компиляцией).
.equ PORTB = 0x18
LDI R16, 5
OUT PORTB, R16
Гениально! Мы напишем простую программу которая увеличивает некое число пока оно не переполнится и потом все происходит снова. А число это будем каждый раз отправлять в PORTB.
Есть одна проблема - микроконтроллер работает довольно быстро, исполняя каждую (почти) инструкцию за 1 микросекунду - то есть, миллион раз в секунду. Если мы будем переключать светодиод слишком быстро, мы не заметим мигания. Решение этой проблемы заключается в том, чтобы увеличивать число не однобайтовое а более длинное, например, записанное в R18:R17:R16. Будем прибавлять небольшую величину каждый раз к R16, остальные регистры увеличивать только за счет переноса (если он есть), как будто мы инкрементируем потихоньку 3-байтовое число. А в PORTB будем записывать число из R18.
Обратите внимание на шаблон программы, который появляется когда вы открываете нашу страничку программирования
.device atmega8
.org 0
start:
nop
rjmp start
Что здесь написано? В первой строке директива которая подсказывает компилятору, какой именно контроллер мы хотим использовать. На контроллер это никак не повлияет, но ассемблер сможет предупредить нас, например, если мы используем команду, которая для данного типа контроллера не работает (например, деления в нашем AtMega8 нет).
Вторая строчка - тоже директива - она говорит ассемблеру с какого адреса мы хотим записать свою программу во флеш-память. С нуля - оттуда все стартует.
Дальше идёт метка "start" - что видно из двоеточия в конце. Метка это не инструкция, это просто удобный способ как-то отметить точку в программе на которую в дальнейшем нужно будет сделать переход (например, для повторения цикла).
Команда NOP
это действительно инструкция процессора. Она от слова "no operation" то есть, ничего не делает. Её можно будет стереть.
Последняя строчка - команда RJMP
(от слова "relative jump") - это как раз переход. Мы хотим перейти в начало программы чтобы она повторилась. После RJMP
можно записать адрес нужной инструкции, прямо числом, но это неудобно. Мы ставим в нужном месте метку (видели это раньше) - а здесь метку просто упоминаем. Компилятор вычислит какой адрес у метки - и этот адрес здесь сам подставит.
Итак переходим к написанию программы. В ней должно быть две части - сначала нужно подготовить разные полезные значения в регистрах - а во второй части нужно заниматься прибавлением к регистрам и записью в PORTB - причем делать это в цикле.
Таким образом начало цикла будет меткой start
(или можете лучше назвать её loop
или again
). Подготовку регистров надо выполнить до
метки, а повторяющуюся часть - после (между меткой и финальным RJMP).
Подготовка заключается в следующем:
- очистите регистры R16, R17, R18
- запишите в R20 какое-нибудь число которое мы будем прибавлять к остальным на каждой итерации, напримре 5
- также запишите в R21 число 0, его тоже надо будет прибавлять при операции ADC (т.к. мы хотим добавить только перенос)
Повторяющаяся часть выглядит так:
- Прибавьте (без переноса) R20 к R16
- Прибавьте с переносом 0 (из R21) к R17
- Проделайте то же самое с R18 (не перепутайте очередь! - перенос, если появился при увеличении R16 добавится к R17, а если появился при добавлении к R17 то добавится и к R18 (если появится перенос и при увеличении R18 мы его игнорируем)
- запишите значение из R18 в PORTB
- выполните переход на начало цикла.
Скомпилируйте и залейте программу. Если все получилось правильно, светодиод начнёт равномерно мигать. Мигать он будет тем чаще, чем больше число в R20 (то которое мы добавляем на каждом шаге). Попробуйте менять это значение и перезаливать программу.
P.S. наша страничка программирования не умеет сохранять программу куда-либо (пока), поэтому можете переписать её в тетрадку или сделать copy-paste в заметки или куда-либо ещё.