Эта глава (и несколько следующих) посвящены программированию 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 endr0 единицу, если число, записанное в 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 endr0 и r1 сумму чисел, записанных в регистры r16 и r17. Младшие 8 бит суммы должны оказаться в регистре r0.start:
LDI r18,0
MOV r1, r18
MOV r0, r16
ADD r0, r17
ADC r1, r18
end:
RJMP endr0 и 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