Первая программа для AVR микроконтроллера на Ассемблере
Приведен и подробно разобран пример простой программы для AVR микроконтроллера на языке Ассемблер (Assembler). Собираем простую схему на микроконтроллере для мигания светодиодами, компилируем программу и прошиваем ее в микроконтроллер под ОС GNU Linux.
Содержание:
- Подготовка
- Принципиальная схема и макет
- Исходный код программы на Ассемблере
- Документация по Ассемблеру
- Работа с числами в Hex, Bin и Dec
- Компиляция и прошивка программы в МК
- Заключение
Подготовка
Итак, у нас уже есть настроенный и подключенный к микроконтроллеру программатор, также мы разобрались с программой avrdude, изучили ее настройки и примеры использования. Пришло время разработать свою первую программу, которая будет выполнять какие-то реальные действия с AVR микроконтроллером (МК).
Писать программу мы будем на языке программирования Ассемблер (Assembler, Asm). Основной ее задачей будет заставить поочередно и с установленной задержкой мигать два разноцветных светодиода (красный и синий), имитируя таким образом полицейскую мигалку.
В результате у вас получится простая электронная схема, которую можно вмонтировать в какой-то пластмассовый макет полицейского автомобиля и подарить ребенку для забавы.
Понятное дело что подобную мигалку можно реализовать на основе простого мультивибратора на двух транзисторах с конденсаторами. Микроконтроллер же вам предоставляет намного больше возможностей.
Используя один чип можно оживить полицейскую мигалку + заставить раз в несколько секунд мигать модель авто фарами, добавить различные звуковые эффекты, научить модельку ездить реагируя на препятствия и многое другое.
Первый инструмент, который нам понадобится - редактор исходного кода, здесь можно использовать любой текстовый редактор. В одной из прошлых статей мы рассматривали настройку среды разработки программ Geany для программирования AVR микроконтроллеров с использованием языков Ассемблера и Си.
В принципе там уже все готово, останется написать код программы и поочередным нажатием двух кнопок (Compile-Flash) скомпилировать и прошить программу в микроконтроллер.
Несмотря на то что у вас уже может быть настроена среда Geany, я приведу все консольные команды которые необходимы для компиляции и прошивки нашей программы в МК.
Принципиальная схема и макет
Для понимания того что из себя представляет наша конструкция, для которой мы будем писать программу, приведу ниже принципиальную схему устройства.
Рис. 1. Принципиальная схема мигалки на светодиодах и микроконтроллере ATmega8.
Примечание: принципиальная схема нарисована за несколько минут в программе Eeschema, которая входит в комплекс программ EDA(Electronic Design Automation) KiCAD (для Linux, FreeBSD, Solaris, Windows). Очень мощный профессиональный инструмент, и что не мало важно - свободный!
Схема устройства состоит из микроконтроллера ATmega8 и двух светодиодов, которые подключены через гасящие резисторы. К микроконтроллеру подключен ISP-коннектор для осуществления программирования через программатор. Также предусмотрены клеммы для подключения внешнего источника питания напряжением 5В.
То как выглядит данная схема в сборе на макетной баспаечной панели (BreadBoard) можно посмотреть на рисунке ниже:
Рис. 2. Конструкция светодиодной мигалки на микроконтроллере ATmega8.
К микроконтроллеру подключен программатор USBAsp, используя ISP интерфейс, от него же и будет питаться наша экспериментальная конструкция. Если нужно запитать конструкцию от внешнего источника питания напряжением 5В то достаточно его подключить к + и - линиям питания панели.
Исходный код программы на Ассемблере
Разработанная нами программа будет попеременно зажигать и гасить два светодиода. Светодиоды подключены к двум пинам микроконтроллера, которые соответствуют каналам с названиями PD0 и PD1 (порт PORTD, D).
Ниже приведен исходный код программы на Ассебмлере(Assembler, Asm) для микроконтроллера ATmega8. Сохраните этот код в файл под названием leds_blinking.asm для последующей работы.
; Светодиодная мигалка на микроконтроллере ATmega8
; https://ph0en1x.net
.INCLUDEPATH "/usr/share/avra/" ; путь для подгрузки INC файлов
.INCLUDE "m8def.inc" ; загрузка предопределений для ATmega8
.LIST ; включить генерацию листинга
.CSEG ; начало сегмента кода
.ORG 0x0000 ; начальное значение для адресации
; -- инициализация стека --
LDI R16, Low(RAMEND) ; младший байт конечного адреса ОЗУ в R16
OUT SPL, R16 ; установка младшего байта указателя стека
LDI R16, High(RAMEND) ; старший байт конечного адреса ОЗУ в R16
OUT SPH, R16 ; установка старшего байта указателя стека
.equ Delay = 5 ; установка константы времени задержки
; -- устанавливаем каналы PD0 и PD1 порта PORTD (PD) на вывод --
LDI R16, 0b00000011 ; поместим в регистр R16 число 3 (0x3)
OUT DDRD, R16 ; загрузим значение из регистра R16 в порт DDRD
; -- основной цикл программы --
Start:
SBI PORTD, PORTD0 ; подача на пин с каналом PD0 высокого уровня
CBI PORTD, PORTD1 ; подача на пин сканалом PD1 низкого уровня
RCALL Wait ; вызываем подпрограмму задержки по времени
SBI PORTD, PORTD1 ; подача на пин с каналом PD1 высокого уровня
CBI PORTD, PORTD0
RCALL Wait
RJMP Start ; возврат к метке Start, повторяем все в цикле
; -- подпрограмма задержки по времени --
Wait:
LDI R17, Delay ; загрузка константы для задержки в регистр R17
WLoop0:
LDI R18, 50 ; загружаем число 50 (0x32) в регистр R18
WLoop1:
LDI R19, 0xC8 ; загружаем число 200 (0xC8, $C8) в регистр R19
WLoop2:
DEC R19 ; уменьшаем значение в регистре R19 на 1
BRNE WLoop2 ; возврат к WLoop2 если значение в R19 не равно 0
DEC R18 ; уменьшаем значение в регистре R18 на 1
BRNE WLoop1 ; возврат к WLoop1 если значение в R18 не равно 0
DEC R17 ; уменьшаем значение в регистре R17 на 1
BRNE WLoop0 ; возврат к WLoop0 если значение в R17 не равно 0
RET ; возврат из подпрограммы Wait
Program_name: .DB "Simple LEDs blinking program"
Кратко рассмотрим приведенный выше код и построчно разберем его структуру. Выполнение программы происходит по порядку - с верху кода и к низу, учитывая при этом метки, переходы с возвратами и условия.
Все строки и части строк, которые начинаются с символа ";" - это комментарии. При компиляции и выполнении программы такие строчки игнорируются, они служат для документирования и примечаний.
При помощи директивы ".INCLUDEPATH" мы указываем путь "/usr/share/avra/", по которому компилятору нужно искать файлы для включения их в текущий файл с использованием директив ".INCLUDE". В нашем примере подключается файл, полный путь к которому будет выглядеть вот так: "/usr/share/avra/m8def.inc".
Директива ".LIST" указывает компилятору о необходимости генерирования листинга с текущего места в коде, отключить генерирование можно директивой ".NOLIST". Листинг представляет собой файл в котором содержится комбинация ассемблерного кода, адресов и кодов операций. Используется для отладки и других полезных нужд.
Директива ".CSEG" (CodeSEGment) определяет начало программного сегмента (код программы что записан во флешь-память) - сегмента кода. Соответственно все что размещено ниже этой директивы относится к программному коду.
Для определения сегмента данных (RAM, оперативная память) или памяти EEPROM используются директивы ".DSEG" и ".ESEG" соответственно. Таким образом выполняется распределение памяти по сегментам.
Каждый из сегментов может использоваться в программном коде только раз, по умолчанию если не указана ни одна из директив используется сегмент кода (CSEG).
При помощи директивы ".ORG" компилятору указывается начальный адрес "0x0000" сегмента, в данном случае мы указали начальный адрес сегмента кода. В данной программе эту директиву можно было бы и не использовать, поскольку по умолчанию адрес программного кода всегда 0x0000.
Дальше в коде происходит инициализация стека. Стек (Stack) - это область памяти (как правило у всех AVR чипов размещается в SRAM), которая используется микропроцессором для хранения и последующего считывания адресов возврата из подпрограмм, а также для других пользовательских нужд.
При вызове подпрограммы flhtc nt записывается в стек и начинается выполнение кода подпрограммы. По завершению подпрограммы (директива RET)
Стек работает по принципу LIFO (Last In - First Out, последним пришёл - первым вышел). Для адресации вершины стека используется указатель стека - SP (Stack Pointer), это может быть однобайтовое или двухбайтовое значение в зависимости от доступного количества SRAM памяти в МК.
При помощи инструкции "LDI" мы загружаем в регистр R16 значение младшего байта конечного адреса ОЗУ "Low(RAMEND)" (предопределенная константа в файле m8def.inc что содержит адрес последней ячейки SRAM), а потом при помощи инструкции OUT выполняем загрузку данного значения из регистра R16 в порт SPL (Stack Pointer Low). Таким же образом производится инициализация старшего байта адреса в указателе стека SPH.
Инструкция LDI используется для загрузки старшего и младшего значений из константы в регистр общего назначения. А инструкция OUT позволяет выполнить операцию загрузки с немного иной спецификой - из регистра общего назначения в регистр периферийного устройства МК, порт ввода-вывода и т.п.
Если не произвести инициализацию стека то возврат из подпрограмм станет невозможным, к примеру в приведенном коде после выполнения инструкции перехода к подпрограмме "RCALL Wait" возврат не будет выполнен и программа не будет работать как нужно.
Директива ".equ" выполняет присвоение указанному символьному имени "Delay" числового значения "5", по сути мы объявили константу. Имя константы должно быть уникальным, а присвоенное значение не может быть изменено в процессе работы программы.
Дальше мы устанавливает каналы PD0 и PD1 порта DDRD (PortD) на вывод, делается это загрузкой двоичного значения 0b00000011 (0x3, число 3) в регистр R16 с последующим выводом этого значения из него в порт DDRD при помощи команды OUT.
По умолчанию все каналы (и соответствующие им пины) порта настроены на ввод (input). При помощи двоичного числа 0b00000011, где последние биты установлены в 1, мы переводим каналы PD0 и PD1 в режим вывода.
Начиная с метки "Start:" начинается основной рабочий цикл нашей программы, эта метка послужит нам для обозначения начального адреса основного цикла и позже будет использована для возврата.
При помощи инструкции "SBI" (Set Bit in I/O register) выполняем установку бита PORTD0 (предопределен в файле m8def.inc) в порте PORTD чем установим на пине с каналом PD0 высокий уровень напряжения.
Используя инструкцию "CBI" (Clear Bit in I/o register) выполняется очистка бита PORTD1 в байте регистра для порта PORTD и тем самым устанавливается низкий уровень напряжения на пине с каналом PD1.
Дальше с помощью инструкции RCALL выполняем относительный вызов подпрограммы которая начинается с метки "Wait:". Здесь для запоминания адреса возврата уже используется стек, который мы инициализировали в начале программы.
После завершения подпрограммы (в нашем случае ее функция - задержка по времени) программа вернется к позиции где был выполнен вызов подпрограммы (адрес возврата будет получен из стека) и с этого места продолжится выполнение последующих операторов.
После вызова подпрограммы задержки "Wait" следуют вызовы инструкций SBI и CBI в которых выполняется установка битов в байте регистра для порта PORTD таким образом, что теперь на пине с каналом PD0 у нас будет низкий уровень, а на пине с каналом PD1 - высокий.
По завершению этих инструкций следует еще один вызов подпрограммы задержки "Wait", а дальше следует инструкция "RJMP" которая выполнит относительный переход к указанной метке - "Start", после чего программа снова начнет установку битов в порте с задержками по времени.
Таким образом выполняется реализация бесконечного цикла в котором будут изменяться состояния битов в байте регистра для порта PORTD микроконтроллера и поочередно зажигаться/гаснуть светодиоды которые подключены к пинам что соответствуют каналам данного порта (PD0, PD1).
После основного цикла программы следует наша подпрограмма задержки по времени. Принцип ее работы заключается в выполнении трех вложенных циклов, в каждом из которых происходит вычитание (DEC, decrement) единички из числа которое хранится в отдельном регистре, и так до тех пор пока значение не достигнет нуля. Инструкция "DEC" декрементирует значение указанного регистра и требует для этого 1 рабочий такт процессора.
При помощи инструкций "BRNE" (условный переход) выполняется анализ нулевого бита статусных флагов процессора (Zero Flag, ZF). Переход на указанную в инструкции метку будет выполнен если после выполнения предыдущей команды нулевой флаг был установлен.
В данном случае проверяется значение нулевого флага после выполнения команд "DEC" над значениями которые хранится в регистрах общего назначения (R17, R18, R19). Инструкция "BRNE" требует 1/2 такта процессора.
Таким образом, использовав несколько вложенных циклов, ми заберем у ЦПУ некоторое количество тактов и реализуем нужную задержку по времени, которая будет зависеть от количества итераций в каждом цикле и от установленной частоты микропроцессора.
По умолчанию, без установки фьюзов что задают источник и частоту тактового генератора, в микроконтроллере ATmega8 используется откалиброванный внутренний RC-генератор с частотой 1МГц. Если же мы изменим частоту МК на 4Мгц то наши светодиоды начнут мигать в 4 раза быстрее, поскольку на каждую операцию вычитания и сравнения будет тратиться в 4 раза меньше времени.
Завершается подпрограмма инструкцией "RET", которая выполняет возврат из подпрограммы и продолжение выполнения инструкций с того места, с которого эта подпрограмма была вызвана (на основе сохраненного адреса возвращения, который сохранился в стеке при вызове инструкции "RCALL").
При помощи директивы ".DB" в памяти программ (флешь) резервируется цепочка из байтов под строчку данных "Simple LEDs blinking program", эти данные являются статичными и их нельзя изменять в ходе работы программы. Для резервирования слов (Double Word) нужно использовать директиву ".DW".
В данном случае, у нас во FLASH-память вместе с программным кодом будет записана строка "Simple LEDs blinking program", которая содержит название программы. Данные из этой строчки нигде в программе не используются и приведены в качестве примера.
При каждом резервировании данных с использованием директивы ".DB" или ".DW" должна предшествовать уникальная метка, которая пригодится нам когда нужно будет получить адрес размещаемых данных в памяти для дальнейшего их использования, в нашем случае это "Program_name:".
При построении программы важно чтобы счетчик выполняемых команд не добрался до адреса с зарезервированными данными, иначе процессор начнет выполнять эти строчки как программный код (поскольку они размещены в сегменте кода). В примере моей программы байты под название программы зарезервированы в конце сегмента кода и за пределами рабочих циклов программы, так что все ОК.
Эти данные можно разместить и в начале кода, использовав операторы перехода для изоляции этих байтов от выполнения:
RJMP DataEnd
Program_name: .DB "Simple LEDs blinking program"
DataEnd:
Документация по Ассемблеру
Разобраться с основами языка программирования Ассемблер в пределах одной статьи достаточно сложно, без практики здесь никак, но тем не менее на начальном этапе и для нашего эксперимента приведенных знаний вполне достаточно. У вас уже будет базовое представление что такое программа на Ассемблере и как используются директивы и инструкции.
Процесс дальнейшего изучения Ассемблера для AVR микроконтроллеров полностью в ваших руках. Есть достаточно много полезных ресурсов в интернете, книг и материалов с примерами и пояснениями.
Приведу несколько полезных документов, которые вы можете скачать и использовать для справки при разработке программ на AVR ASM.
Справка по Ассемблеру для Atmel AVR (перевод Руслана Шимкевича): atmel-avr-assembler-quick-doc-ru.zip (16Кб, HTML, RU).
Справка по инструкциям Atmel Assembler: atmel-avr-instruction-set-manual-en.pdf.zip (700Кб, PDF, EN, 2015).
Работа с числами в Hex, Bin и Dec
В коде программы для загрузки значений в регистры используются числа и в скобках приведены их значения в шестнадцатеричной системе счисления, например: "50 (0x32, )". В двоичной системе счисления числа указываются в формате "0b00000011".
Для удобной переконвертации чисел из шестнадцатеричной системы счисления в десятичную, двоичную и наоборот отлично подходит программный калькулятор из среды рабочего окружения KDE - KCalc.
Рис. 3. KCalc - простое и эффективное решение для пересчета между разными системами счисления.
В настройках (Settings) нужно выбрать режим (Numeral System Mode), после чего программа приобретет вид что на рисунке выше. Переключаться между системами счисления можно устанавливая флажки в полях "Dec", "Hex", "Bin".
Для примера: переключаемся в Hex и набираем "FF", потом переключаемся в Dec и видим число в десятичной системе счисления - 255, просто и удобно.
В операционной системе GNU Linux с рабочей средой GNOME (например Ubuntu) также есть подобный калькулятор, это программа - galculator.
Компиляция и прошивка программы в МК
Итак, у нас уже есть полный код программы, который мы сохранили в файл с именем "leds_blinking.asm". Теперь самое время скомпилировать его, делается это нажатием кнопки "Compile" в предварительно настроенной среде Geany или же отдельной командой в консоли:
avra --includepath /usr/share/avra/ leds_blinking.asm
Если результат выполнения будет без ошибок то мы получим файл прошивки в формате Intel HEX - "leds_blinking.hex", который уже можно прошивать во флешь-память микроконтроллера.
Примечание: опцию "--includepath /usr/share/avra/" можно и не указывать, поскольку в файле с исходным кодом уже была указана директива ".INCLUDEPATH" для поиска файлов с предопределениями для разных моделей МК.
Осталось прошить микроконтроллер используя полученный файл "leds_blinking.hex". В примере я использую программатор USBAsp и микроконтроллер ATmega8, вот так выглядит команда для записи получившегося файла во флешь-память МК:
avrdude -p m8 -c usbasp -P usb -U flash:w:leds_blinking.hex
Примечание: в команде используется относительный путь к файлу leds_blinking.hex, поэтому для успешного выполнения команды нужно перейти в терминале(консоли) в директорию где находится данный файл.
Сразу же после прошивки флешь-памяти на микроконтроллер поступит команда сброса (RESET) и программа начнет выполняться, об єтом будут свидетельствовать два попеременно мелькающих светодиода.
Если же светодиоды не подают признаков жизни, значит что-то пошло не так. Посмотрите внимательно вывод команды для компиляции и прошивки МК, возможно что там увидите сообщения об ошибках которые нужно исправить.
Заключение
Увеличив значение константы "Delay" можно уменьшить частоту мерцания светодиодов, а уменьшив - увеличить частоту. Также можете попробовать добавить несколько светодиодов к свободным каналам порта (PD2-PD7) и модифицировать программу таким образом чтобы получить бегущий огонь из светодиодов.
В заключение приведу краткое видео работы рассмотренной схемы на двух светодиодах:
В следующей статье мы разберем программу с похожим функционалом, используя тот-же макет, только выполним ее на языке программирования Си.
Начало цикла статей: Программирование AVR микроконтроллеров в Linux на языках Asembler и C.
Стэк работает по принципу FILO, а не FIFO/.
Благодарю за замечание, исправил вот так: LIFO (last in - first out, последним пришёл - первым вышел).
Спасибо за цикл статей!
Небольшое замечание: в комментарии говорится, что все пины ставятся на вывод, но по факту только младшие 2.
Спасибо за замечание, исправил!
Спасибо за статью, очень доходчиво. Но непонятно один момент:
наверно имелось ввиду R17?
Да, в этом комментарии должно быть R17 - исправил.
После выполнения инструкции OUT SPH,R16 программный счётчик попадает на область данных, где записано наименование программы Simple LEDs blinking program и начинает выполнять коды инструкций, которые соответствуют байтам этой надписи. Поведение программы при этом может быть непредсказуемым, т.к. теоретически может попасться, например, код перехода и т.п. Это так и задумывалось? Может быть строку, где резервируются данные поместить в конце или, если в начале, то по нулевому адресу разместить инструкцию перехода на начало программы?
Вадим, вы правы. При подготовке кода упустил этот момент и так сложилось что байты в данной строчке не "запороли" ход выполнения программы.
Добавил исправления и уточнения в статью.
Спасибо!
Добрый день! Очень полезная и интересная статья! Хотелось бы узнать, а возможно ли к этому программному коду написать алгоритм в виде блок-схемы? Или такие алгоритмы используются только для программ, написанных на языке Си.
Добрый день. Алгоритмы в виде блок-схем можно рисовать для любых программ, в не зависимости от используемого языка программирования.
В программе из этой статьи есть основной вечный цикл с несколькими действиями (установка значений на выходах двух портов) и вызовом подпрограммы задержки по времени. Последняя содержит в себе несколько вложенных циклов и условие для выхода из них.
Для рисования алгоритмических диаграмм можно использовать следующие программы:
Сделал набросок алгоритма в Dia:
Спасибо большое за статьи.
возникла небольшая проблема. при компиляции программы через avra идут вот такие ошибки:
"/usr/share/avra/tn2313def.inc(619) : PRAGMA directives currently ignored"
Как это исправить? версия avra 1.3.0
И еще возник вопрос, чем avrasm2 который лежит в файлах atmel studio отличается от avra версии 1.3.0?
Здравствуйте.
Такие строчки являются предупреждениями, которые говорят о том что одна из директив для транслятора была проигнорирована. Причиной может быть неполная официальная поддержка чипа, устаревшие версии библиотек или что-то другое.
Старайтесь использовать последнюю доступную версию GNU/Linux для вашего дистрибутива, а также обновить пакеты, в том числе и avra, последняя версия которого на данный момент - 1.3.0 (как и в вашем случае).
Компиляция с такими предупреждениями зачастую завершиться без ошибок (в выводе сказано что 'Assembly complete with no errors.') и зашитый в МК ранее полученный HEX-код скорее всего что будет работать нормально. Проверяйте весь функционал на реальном примере.
Насчет отличий avra и avrasm2 (atmel studio) - в последнем могут быть свои оптимизации кода и какие-то отличия в поддержке некоторых чипов. Детального изучения и сравнения на этот счет не проводил.
Доброго дня!
Не подскажете можно ли применить данную программу на Асемблере для младших версий
ATtiny 13 в частности, вероятно потребуется изменить порты к которым подключаются диоды
как поставитьзаголовок инициализации я понял, надо ли прописывать настройках dude программатор STk200-300 или достаточно просто параллельный порт по умолчанию.
Приветствую!
Приведенную здесь простую программу можно запустить практически на любом AVR микроконтроллере. Для этого понадобится изменить имя файла с предопределенными константами для используемого МК в строчке:
А также, используя документацию (даташит), найти на корпусе микросхемы соответствующие выводы PD0 и PD1 для порта PORTD и подключить к ним светодиоды (как на схеме).
В начале я ссылался на статью по avrdude - там описаны параметры запуска и приведены примеры использования. Также была приведена ссылка на предыдущую статью по программаторам.
При запуске прошивки через avrdude нужно будет указать тип микроконтроллера, а также тип используемого программатора, при необходимости порт.
Для некоторых типов программаторов по умолчанию уже выставлен порт, поэтому его можно и не указывать.
Примерно вот так будут выглядеть варианты команд для теста подключения к МК ATtiny13 через параллельный интерфейс (LPT1):
Если все хорошо, то для прошивки заменяем ключ "-n" (режим без записи, тест) на "-U flash:w:leds_blinking.hex" (запись прошивки из файла во flash).