- •Медицинские микропроцессорные системы
- •Анисимов а.А.
- •Isbn © сПбГэту «лэти», 2019 введение
- •1. Общая структура микроконтроллеров avr
- •2. Программирование микроконтроллеров на языке ассемблер
- •3. Работа с отладочной платой attiny104-xnano
- •4. Устройство портов ввода-вывода
- •5. Таймеры-счётчики
- •6. Широтно-импульсная модуляция
- •7. Использование аналого-цифрового преобразователя
- •8. Передача данных по uart
- •9. Последовательный интерфейс spi
- •Приложение 1. Основные команды языка assembler для микроконтроллера attiny104
- •Список литературы
- •Оглавление
- •Медицинские микропроцессорные системы
- •197376, С.-Петербург, ул. Проф. Попова, 5
2. Программирование микроконтроллеров на языке ассемблер
Написание программ на ассемблере может вызвать у неподготовленных пользователей, которые не сталкивались с этим языком ранее, определенные сложности. Язык ассемблера – система обозначений, используемая для представления в удобно читаемой форме программ, записанных в машинном коде. Команды языка ассемблера один к одному соответствуют командам процессора. Фактически, они и представляют собой более удобную для человека символьную форму записи – мнемокоды – команд и их аргументов. Кроме того, язык ассемблера позволяет использовать символические метки вместо адресов ячеек памяти, которые при ассемблировании заменяются на вычисляемые ассемблером абсолютные или относительные адреса, а также так называемые директивы (команды ассемблера, не переводимые в машинные команды процессора, а выполняемые самим ассемблером). Директивы ассемблера позволяют, в частности, включать блоки данных, задать ассемблирование фрагмента программы по условию, задать значения меток, использовать макрокоманды с параметрами. Каждая модель (или семейство) процессоров имеет свой набор команд и соответствующий ему язык ассемблера, микроконтроллеры семейства AVR не являются исключением. Краткий список команд с описанием для микроконтроллера Attiny104, который мы будем программировать на практических занятиях, представлен в Приложении 1.
Перевод программы на языке ассемблера в исполнимый машинный код (вычисление выражений, раскрытие макрокоманд, замена мнемоник собственно машинными кодами и символьных адресов на абсолютные или относительные адреса) производится ассемблером — программой-транслятором, которая и дала языку ассемблера его название.
Чтобы немного исправить положение с написанием программ на языке низкого уровня, существует макроассемблер. В чём же его суть? Макроассемблер - программа, выполняющая преобразование входного текста в выходной при помощи задаваемых ей правил замены последовательностей символов, называемых правилами макроподстановки. Наиболее простое и часто используемое правило макроподстановки сводится к замене определённой строки (называемой макрос или макрокоманда) другой строкой, возможно, с использованием параметров. Также правила макроподстановки могут иметь более сложный характер, включая определение процедур и функций, вычислительные алгоритмы и пр.
Теперь рассмотрим подробнее синтаксис ассемблера AVR Studio 7.
Комментарии. Общее правило – чем подробнее описаны выполняемые команды, тем лучше. Самый крайний случай – может быть прокомментирована каждая строчка программного кода, в целом такой подход является избыточным, но на первое время для начинающих лучше действовать именно так. Комментарии можно использовать при защите своих программ, что может сильно облегчить жизнь. Комментарии обозначаются двумя способами – с помощью символа ; (точка с запятой) или // (двойная косая черта). Для объёмных комментариев, занимающих несколько строчек, целесообразно использовать следующую структуру: /* текст комментария */ (подобная нотация взята из языка С)
Оператор .def позволяет присвоить любому регистру микроконтроллера некоторое осмысленное символьное имя. Это делается исключительно для удобства работы – например, если мы используем в качестве промежуточного хранилища регистр R16, намного проще присвоить ему имя temp и в дальнейшем использовать уже его:
.def temp = R16
Одному и тому же регистру можно присвоить несколько имён, обратная же операция недопустима. В целом, присваивать регистрам осмысленные имена считается хорошим стилем программирования и рекомендуется при написании даже простых программ, для наработки этого самого стиля.
Оператор .equ позволяет присвоить выражению или константе некоторую символьную метку. Этот оператор помогает нам избежать так называемых магических чисел, которые представляют собой встречающиеся в программе числа, происхождение которых неочевидно. Проанализировать такой код достаточно сложно, поэтому каждому числу необходимо присваивать осмысленную метку (к тому же, если какое-то число встречается в программе часто, и нам понадобится его изменить, мы один раз меняем присвоенную ему метку, вместо изменения всех этих чисел в тексте программы).
Также с помощью оператора. equ можно присвоить выражение, которое в процессе компиляции будет посчитано препроцессором, и далее передано в код в виде уже готового значения. Необходимо учитывать, что деление тут исключительно целочисленное, с отбрасыванием дробной части, без какого-либо округления: 1/2 = 0 и 5/2 = 2. В качестве примера можно привести выражение для расчёта скорости передачи данных по послеждовательному интерфейсу UART:
.equ XTAL = 4000000 // определяем тактовую частоту контроллера в МГц
.equ baudrate = 9600 // задаём скорость передачи данных, бит/с
.equ bauddivider = XTAL/(16*baudrate)-1 // вычисляем значение делителя
Оператор .include позволяет подключать в тело программы часть кода из другого текстового файла. Это позволяет разбить большую программу на отдельные функциональные части и повысить тем самым читаемость кода. Увидев оператор .include, компилятор подставляет на это место кусок кода из указанного файла. Если необходимо подключить не весь файл, а только его часть, то нужно воспользоваться директивой .exit, дойдя до которой компилятор попросту выйдет из файла, забрав с собой только необходимый участок кода.
.macro – оператор макроподстановки. В языках ассемблера, а также в некоторых других языках программирования, макрос — символьное имя, заменяемое при обработке препроцессором на последовательность программных инструкций. Для каждого интерпретатора существует специальный синтаксис объявления и вызова макросов. Макрос может «разворачиваться» в различные последовательности инструкций при каждом вызове, в зависимости от сработавших разветвлений внутри макроса и переданных ему аргументов. Перед тем как использовать макрос, необходимо его объявить. Часто стандартные макросы уже находятся в готовом виде в стандартных подключаемых файлах. Для их использования в программе необходимо просто подключить нужный файл.
По сути, макрос представляет собой последовательность команд, которые подставляются в нашу программу при вызове этого самого макроса. Это позволяет значительно сократить количество строчек кода в нашей программе, улучшить её читаемость и структурированность.
Рассмотрим в качестве примера простейший макрос, записывающий восьмибитную константу (т.е. значение от 0 до 255) в регистр ввода-вывода:
.macro OUTI // имя макроса, по которому мы будем к нему обращаться
ldi R16, @1 // записываем нашу константу в регистр R16
.if @0 < 0x40 // если адрес регистра меньше 0x40
out @0, R16 // выводим константу в искомый регистр первым методом
.else // если адрес больше 0x40
sts @0, R16 // выводим константу в искомый регистр вторым методом
.endif // оператор окончания условного цикла
endm // знак окончания макроса
Вследствие ограничений, накладываемых компилятором, мы не можем напрямую записывать константу в регистр, только с использованием промежуточного регистра общего назначения, поэтому подобный макрос позволяет нам значительно упростить написание программ.
@0, @1 представляют собой параметры макроса, они нумеруются строго по порядку, а при вызове подставляются в код. Почему же строчка, выводящая константу в регистр, оказалась продублирована? К регистрам периферии можно обратиться двумя разными способами: через команды IN/OUT по короткому адресу в пространстве адресов ввода-вывода, и через группу команд LOAD/STORE по полному адресу в пространстве адресов RAM. Оба метода дают идентичные результаты, но первый работает только с первыми 3F (в шестнадцатеричном исчислении) адресами, а со всеми остальными регистрами, не поместившимися в это адресное пространство, работает только вторая группа команд. Поэтому макрос проверяет адрес регистра, в который нужно записать константу, и, исходя из результатов проверки, подставляет необходимую команду. Теперь мы можем не думать об адресации регистров, макрос позаботится об этом за нас.
Некоторые дополнительные подробности о синтаксисе языка ассемблер и написании макросов для микроконтроллеров семейства AVR можно почерпнуть из фирменной документации (требуется знание английского языка): AVR Assembler и AVR Instruction Set Manual.
Рассмотрим поподробнее систему команд микроконтроллеров производства фирмы Microchip. Система команд микроконтроллеров AVR семейств Tiny и Mega весьма развита и насчитывает в различных моделях от 90 до 133 различных инструкций. Несмотря на то, что микроконтроллеры AVR являются микроконтроллерами с RISC архитектурой (процессор с сокращенным набором команд), по количеству реализованных инструкций и их разнообразию они больше похожи на микроконтроллеры с CISC архитектурой (процессор с полным набором команд).
Практически каждая из команд (за исключением команд, у которых одним из операндов является 16-разрядный адрес) занимает только одну ячейку памяти программ. Причем это достигнуто не за счет сокращения количества команд процессора, а за счет увеличения разрядности памяти программ.
Операнды
Программа для любого микроконтроллера представляет собой последовательность команд, записанных в памяти программ. Большинство команд при выполнении изменяют содержимое одного или нескольких регистров общего назначения, регистров ввода/вывода или ячеек ОЗУ. Для обращения к различным областям адресного пространства памяти данных используются различные команды, реализующие, в свою очередь, различные способы адресации. Доступ к регистрам ввода/вывода осуществляется по их адресам, являющимися операндами команды. Вместе с тем при написании ассемблерных программ гораздо удобнее обращаться к регистрам, используя вместо числовых значений адресов их стандартные, принятые в фирменной документации, символические имена. Чтобы задать соответствие этих имен реальным адресам необходимо подключить в начале программы (при помощи директивы ассемблера .include) файл определения адресов регистров ввода/вывода. Помимо всего прочего, такое решение облегчит перенос программного обеспечения с одного типа микроконтроллера на другой. Эти файлы (для каждой модели микроконтроллеров семейства) свободно распространяются фирмой Microchip вместе с документацией на микроконтроллеры. Названия этих файлов унифицированы и определяются следующим образом:
<номер_модели> def.inc
Например, программа для микроконтроллера ATtiny15L должна содержать следующую директиву ассемблера:
.include "tn15def.inc"
Как уже было упомянуто, в микроконтроллерах семейства Tiny и Mega память программ является 16разрядной. Соответственно большинство команд описываются 16-разрядным словом, которое называется также кодом операции (КОП). Код операции — это число, расположенное в памяти программ и определяющее действие, которое необходимо произвести между источником и приемником. Некоторые команды, у которых один из операндов является 16-разрядным адресом, занимают две ячейки памяти программ. Соответственно, код операции таких команд является 4-байтным числом. В ряде случаев значение операнда источника может содержаться непосредственно в коде операции, а не в регистре. Это происходит в том случае, когда операндом-источником является константа.
Некоторые константы, которые могут быть полезны при написании программ, определены в упомянутых включаемых файлах:
RAMEND — значение верхнего адреса внутреннего ОЗУ;
XRAMEND — значение верхнего адреса внешнего ОЗУ;
E2END — значение верхнего адреса EEPROM;
FLASHEND — значение верхнего адреса памяти программ.
Особенности мнемонической записи команд в AVR-ассемблере не сильно отличается от ассемблеров других фирм-производителей. Сначала идёт непосредственно сама команда (обычно трёх или четырёхбуквенная), затем через пробел (один или несколько) следуют операнды. Некоторые команды не имеют операндов (RETI, SEI, CLI, NOP), в других присутствует только один операнд (например, команда инкремента/декремента INC/DEC). Если же команда имеет два операнда, сначала указывают приёмник, затем источник (так называемая прямая польская запись), причём между ними обязательно должна стоять запятая. Так выражение sub R16, R17 означает, что из содержимого регистра R16 нужно вычесть содержимое регистра R17, причём результат окажется в регистре R16.
Следует помнить, что AVR-Ассемблер не различает буквенный регистр, причём не только при записи команд, но и в именах переменных, констант, меток и тому подобного (то есть формы записи Reset, RESET или RESet будут выглядеть для компилятора абсолютно одинаково). Поскольку все имена должны быть уникальными, стоит обращать на это особое внимание.
Каждая команда должна занимать отдельную строку, разбивать команду на части разрывом строки также запрещено. Кроме команды, единая строка может содержать метки и примечания. Метка (Label) – идентификатор произвольной длины, который задается программистом и заканчивается двоеточием без пробела перед ним (Metka:). Метку можно располагать в отдельной строке, они могут служить указанием на адрес подпрограмм и заодно выступать в качестве их названия.
В некоторые команды можно включать числовые значения. Числа по умолчанию считаются десятичными, шестнадцатеричные числа можно записывать двумя способами: как в языке Си (0хAB) или Паскале ($AB). Двоичные числа записываются как в языке Си – 0b00010101.
Также в команды можно включать алгебраические и логические выражения, их полный перечень можно найти в фирменном описании ассемблера фирмы Atmel.
Типы ассемблерных команд
Все множество команд микроконтроллеров AVR можно разбить на несколько групп:
команды логических операций;
команды арифметических операций и команды сдвига;
команды операций с битами;
команды пересылки данных;
команды передачи управления;
команды управления системой.
Команды логических операций
Команды логических операций позволяют выполнять стандартные логические операции над байтами, такие, как логическое умножение (И), логическое сложение (ИЛИ), операцию «исключающее ИЛИ», а также вычисление обратного (дополнение до единицы) и дополнительного (дополнение до двух) кодов числа. К этой группе можно отнести также команды очистки/установки регистров. Операции производятся между регистрами общего назначения либо между регистром и константой; результат сохраняется в РОН. Все команды из этой группы выполняются за один машинный цикл.
Команды арифметических операций и команды сдвига
К данной группе относятся команды, позволяющие выполнять такие базовые операции, как сложение, вычитание, сдвиг (вправо и влево), инкремент и декремент. В микроконтроллерах семейства Mega также имеются команды, позволяющие осуществлять умножение 8-разрядных значений. Все операции производятся только над регистрами общего назначения. При этом микроконтроллеры AVR позволяют легко оперировать как знаковыми, так и беззнаковыми числами, а также работать с числами, представленными в дополнительном коде. Почти все команды рассматриваемой группы выполняются за один машинный цикл. Команды умножения и команды, оперирующие двухбайтовыми значениями, выполняются за два цикла.
Команды операций с битами
К данной группе относятся команды, выполняющие установку или сброс заданного разряда РОН или РВВ. Причем для изменения разрядов регистра состояния SREG имеются также дополнительные команды (точнее говоря, эквивалентные мнемонические обозначения общих команд), т. к. проверка состояния разрядов именно этого регистра производится чаще всего. Условно к этой группе можно отнести также две команды передачи управления типа «проверка/пропуск», которые пропускают следующую команду в зависимости от состояния разряда РОН или РВВ. Все задействованные разряды РВВ имеют свои символические имена. Определения этих имен описаны в том же включаемом файле, что и определения символических имен адресов регистров. Таким образом, после включения в программу указанного файла в командах вместо числовых значений номеров разрядов можно будет указывать их символические имена
Команды пересылки данных
Команды этой группы предназначены для пересылки содержимого ячеек, находящихся в адресном пространстве памяти данных. Разделение адресного пространства на три части (РОН, РВВ, ОЗУ) предопределило разнообразие команд данной группы. Пересылка данных, выполняемая командами группы, может производиться в следующих направлениях:
РОН ⇔ РОН;
РОН ⇔ РВВ;
РОН ⇔ память данных.
Также к данной группе можно отнести стековые команды PUSH и POP, позволяющие сохранять в стеке и восстанавливать из стека содержимое РОН. На выполнение команд данной группы требуется в зависимости от команды от одного до трех машинных циклов.
Команды передачи управления
В эту группу входят команды перехода, вызова подпрограмм и возврата из них и команды типа «проверка/пропуск», пропускающие следующую за ними команду при выполнении некоторого условия. Также к этой группе относятся команды сравнения, формирующие флаги регистра SREG и предназначенные, как правило, для работы совместно с командами условного перехода. В системе команд микроконтроллеров семейства имеются команды как безусловного, так и условного переходов. Команды относительного перехода (RJMP), а в микроконтроллерах семейства Mega также косвенного (IJMP) и абсолютного (JMP) безусловного перехода являются самыми простыми в этой группе. Их функция заключается только в записи нового адреса в счетчик команд. Команды условного перехода также изменяют содержимое счетчика команд, однако это изменение происходит только при выполнении некоторого условия или, точнее, при определенном состоянии различных флагов регистра SREG.
Команды вызова подпрограммы (ICALL, RCALL и CALL) работают практически так же, как и команды безусловного перехода. Отличие заключается в том, что, перед тем как выполнить переход, значение счетчика команд сохраняется в стеке. Кроме того, подпрограмма должна заканчиваться командой возврата RET , как показано в следующем примере:
RCALL test //Вызов подпрограммы «test»
... // Текст основной программы
TEST //Метка подпрограммы
... // Выполнение подпрограммы
RET //Возврат из подпрограммы
В приведенном примере команда RET заменяет адрес, находящийся в счетчике команд, адресом команды, следующей за командой RCALL. Очевидно, что команды передачи управления нарушают нормальное (линейное) выполнение основной программы. Каждый раз, когда выполняется команда из этой группы (кроме команд сравнения), нормальное функционирование конвейера нарушается. Перед загрузкой в конвейер нового адреса производится остановка и очистка выполняемой последовательности команд. Соответственно, реинициализация конвейера приводит к необходимости использования нескольких машинных циклов для выполнения таких команд.
Команды управления системой
В эту группу входят всего 3 команды:
NOP — пустая команда-заглушка;
SLEEP — перевод микроконтроллера в режим пониженного энергопотребления;
WDR — сброс сторожевого таймера.
Все команды этой группы выполняются за один машинный цикл.
