Эта глава (и несколько следующих) посвящены программированию 8-битных микроконтроллеров семейства ATMega328, выпускаемых фирмой Atmel и использующихся повсеместно во всевозможных преобразователях сигналов и системах управления. Архитектура AVR, на которой основаны упомянутые микроконтроллеры, достаточно проста для использования в учебных целях и достаточно репрезентативна для того, чтобы составить представление об устройстве ЭВМ и об их программировании на уровне машинных кодов.
Для начала перечислим основные характеристики микроконтроллера ATMega328 и кратко прокомментируем их смысл.
Тактовая частота: регулируемая, не более 20 МГц. Частота (в герцах) – количество тактов, подаваемых на микросхему за одну секунду. Исполнение одной команды занимает один или несколько тактов (это утверждение, вообще говоря, не верно для более современных архитектур; например, для x86_64, используемой на персональных компьютерах).
Оперативная память: 2048 байт. Используется для хранения промежуточных данных, используемых для нужд решаемой задачи. Хранит данные только пока устройство включено. При выключении или перезагрузке устройства данные из оперативной памяти теряются.
EEPROM: 1024 байта. Хранит данные даже при выключенном питании. Используется в первую очередь для параметризации решаемой устройством задачи.
Память программы: 32768 байт. Хранит программу в виде последовательности команд. Большинство команд занимают 2 байта. Незначительное количество команд занимают 4 байта. Обычно последние 1-2 килобайта заняты программой-загрузчиком, которая позволяет обновлять программу, записанную в устройство, при помощи последовательного входа.
Первые 256 байт оперативной памяти ATMega328 имеют специальную роль. Байты с 0го по 31й (нумерация байт традиционно начинается с нуля) называются рабочими регистрами. Рабочие регистры могут являться операндами для команд процессора. Всем командам, использующим рабочие регистры, доступны регистры с 16го по 31й. Некоторым командам доступны и первые 16 рабочих регистров. Рабочие регистры имеют традиционные названия: r0
, r1
, r2
, … r31
. Пара регистров r26
и r27
известна под названием X
. Пара регистров r28
и r29
называется Y
. Последняя пара регистров (r30
и r31
) называется Z
.
Остальные 224 байта называются регистрами ввода-вывода и используются для общения с периферией.
Ассемблером называется набор человекочитаемых названий для команд процессора; язык программирования, основанный на этих названиях; а также – приложение, предназначенное для перевода человекочитаемого текста программы на этом языке в кодировку, понимаемую процессором, для которого эта программа написана.
Для трансляции программ мы будем использовать приложение-ассемблер GNU as. Для Windows это приложение доступно в составе приложения WinAVR. Для POSIX-систем это приложение входит в состав GNU binutils.
Сборка программы происходит в четыре этапа.
Текст программы записывается в текстовый файл (далее – foobar.S
).
Текст программы скармливается приложению avr-as
командой
avr-as foobar.S -o foobar.o
Полученный промежуточный файл скармливается приложению avr-ld
командой
avr-ld foobar.o -o foobar.elf
Полученный файл скармливается приложению avr-objcopy
командой
avr-objcopy -O ihex foobar.elf foobar.hex
Итоговый файл foobar.hex
содержит машинный код программы в формате Intel Hex. Этот формат де факто является стандартным для приложений, записывающих программу в память микроконтроллера.
На сайте доступен эмулятор, который позволяет пошагово исполнять программу, записанную в формате Intel Hex, и иллюстрирует некоторые процессы, происходящие при исполнении этой программы.
Приведём пример программы, складывающей числа 12 и 21.
LDI r16, 12
LDI r17, 21
ADD r16, r17
barrier:
RJMP barrier
В ней мы видим 4 команды и метку barrier
. Метка не соответствует никакой команде процессора и используется только как аргумент для команд перехода.
Собрав эту программу так, как описано в предыдущем разделе, и выполнив её в эмуляторе, можно наблюдать как сначала в регистр r16
попадает число 12, затем в регистр r17
попадает число 21, затем сумма этих чисел записывается в регистр r16
, после чего программа зацикливается на последней инструкции.
Здесь перечислены те команды, которые нам понадобятся на первых порах.
Для перемещения данных используются следующие команды:
LDI куда, что
– перемещение указанного числа в указанный регистрLD куда, откуда
– перемещение байта из оперативной памяти в указанный регистр; второй аргумент – либо X, либо Y, либо Z – задаёт пару регистров, в которой записан адрес ячейки памятиMOV куда, откуда
– перемещение байта из регистра в регистрST куда, откуда
– перемещение байта из регистра в оперативную память; первый аргумент – X, Y или ZАрифметические операции (наиболее важные):
ADD куда, откуда
– прибавление числа из указанного регистра к числу в указанном регистреSUB куда, откуда
– вычитание числа из указанного регистра из числа в указанном регистреSUBI куда, что
– вычитаение указанного числа из числа в указанном регистре (обратите внимание, что команды ADDI
нет; SUBI
для большинства целей заменяет ADDI
)NEG что
– изменение числа в указанном регистре на противоположноеAND куда, откуда
– поразрядная конъюнкция чисел в указанных регистрахOR куда, откуда
– поразрядная дизъюнкция чисел в указанных регистрахEOR куда, откуда
– поразрядная исключающая дизъюнкция чисел в указанных регистрахANDI куда, что
– поразрядная конъюнкция указанного числа и числа в указанном регистреORI куда, что
– поразрядная дизъюнкция указанного числа и числа в указанном регистре (обратите внимание, что команды EORI
нет)Сложение и вычитание производятся в арифметике по модулю 256, т.е с «переходом» через 0. Если такой переход произошёл, то этот факт запоминается выставлением 1 в специальный бит переноса (carry flag). Если перехода не было, в бит переноса попадает 0.
ADC куда, откуда
– прибавление суммы числа из указанного регистра и бита переноса к числу в указанном регистреSBC куда, откуда
– вычитание суммы числа из указанного регистра и бита переноса из числа в указанном регистреINC что
– увеличение значения в указанном регистре на 1; бит переноса не затрагиваетсяDEC что
– уменьшение значения в указанном регистре на 1; бит переноса не затрагиваетсяТакже результат арифметических операций отражается на бите нуля: он выставляется, если результат операции равен нулю.
Эти биты можно исползовать для условных переходов:
BREQ метка
– переход по метке, если выставлен бит нуляBRNE метка
– переход по метке, если не выставлен бит нуляBRCS метка
– переход по метке, если выставлен бит переносаBRCC метка
– переход по метке, если не выставлен бит переносаТакже есть безусловный переход:
RJMP метка
– переход по меткеСкажем напоследок, что микроконтроллеры архитектуры AVR поддерживают ещё несколько более сложных арифметических операций, о которых мы поговорим позже.
При выполнении заданий не забывайте ставить в конце программы барьер вида RJMP
на себя же. Постарайтесь выполнить задания, пользуясь только инструкциями, описанными в предыдущем разделе.
r0
единицу, если число, записанное в r16
, больше числа, записанного в r17
, и 0 – в противном случае.;; Решение. Здесь и далее считается, что входные данные уже заданы.
start: ;; часть меток используется просто в качестве комментариев
SUB r17, r16
BRCC greater_or_equal
less:
LDI r16, 1
MOV r0, r16
RJMP end
greater_or_equal:
LDI r16, 0
MOV r0, r16
end:
RJMP end
r0
единицу, если число, записанное в r16
, не меньше числа, записанного в r17
, и 0 – в противном случае.start:
SUB r16, r17
BRCC greater_or_equal
less:
LDI r16, 0
MOV r0, r16
RJMP end
greater_or_equal:
LDI r16, 1
MOV r0, r16
end:
RJMP end
r0
и r1
сумму чисел, записанных в регистры r16
и r17
. Младшие 8 бит суммы должны оказаться в регистре r0
.start:
LDI r18,0
MOV r1, r18
MOV r0, r16
ADD r0, r17
ADC r1, r18
end:
RJMP end
r0
и r1
произведение чисел, записанных в регистры r16
и r17
. Младшие 8 бит произведения должны оказаться в регистре r0
.start:
LDI r18, 0
MOV r0, r18
MOV r1, r18
;; это довольно стандартная проверка на равенство:
SUB r17, r18
BREQ end
loop:
ADD r0, r16
ADC r1, r18
DEC r17
BRNE loop
end:
RJMP end
Программа получает на вход два числа (в регистрах r16
и r17
). На выходе (в регистре r0
) должен оказаться остаток от деления r16
на r17
. Если r17=0
, то в регистре r1
должен оказаться код ошибки 1. В противном случае код ошибки должен быть равен 0.
Программа получает на вход два числа (в регистрах r16
и r17
). На выходе (в регистре r0
) должно оказаться неполное частное от деления r16
на r17
. Если r17=0
, то в регистре r1
должен оказаться код ошибки 1. В противном случае код ошибки должен быть равен 0.
Программа получает на вход число (в регистре r16
). На выходе (в регистрах r0
и r1
) должна оказаться сумма всех целых чисел от 1 до r16
. Разряд r0
– младший.
Программа получает на вход число (в регистре r16
). На выходе (в регистрах r0
и r1
) должно оказаться произведение всех целых чисел от 1 до r16
(взятое по модулю 65536). Разряд r0
– младший.
Программа получает на вход два числа (в регистрах r16
и r17
). На выходе (в регистре r0
) должен оказаться их НОД. Считается, что НОД пары нулей равен нулю.
@ 2016 arbrk1, all rights reversed