Программирование, радиоэлектроника,
саморазвитие и частичка из моей жизни здесь...

Первая программа для AVR микроконтроллера на Ассемблере

Приведен и подробно разобран пример простой программы для AVR микроконтроллера на языке Ассемблер (Assembler). Собираем простую схему на микроконтроллере для мигания светодиодами, компилируем программу и прошиваем ее в микроконтроллер под ОС GNU Linux.

Содержание:

  1. Подготовка
  2. Принципиальная схема и макет
  3. Исходный код программы на Ассемблере
  4. Документация по Ассемблеру
  5. Работа с числами в Hex, Bin и Dec
  6. Компиляция и прошивка программы в МК
  7. Заключение

Подготовка

Итак, у нас уже есть настроенный и подключенный к микроконтроллеру программатор, также мы разобрались с программой 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) можно посмотреть на рисунке ниже:

LED мигалка на микроконтроллере

Рис. 2. Конструкция светодиодной мигалки на микроконтроллере ATmega8.

К микроконтроллеру подключен программатор USBAsp, используя ISP интерфейс, от него же и будет питаться наша экспериментальная конструкция. Если нужно запитать конструкцию от внешнего источника питания напряжением 5В то достаточно его подключить к + и - линиям питания панели.

Исходный код программы на Ассемблере

Разработанная нами программа будет попеременно зажигать и гасить два светодиода. Светодиоды подключены к двум пинам PD0 и PD1 микроконтроллера.

Ниже приведен исходный код программы на Ассебмлере(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.

По умолчанию все каналы (пины) порта настроены на ввод. При помощи двоичного числа 0b00000011, где последние биты установлены в 1, мы переводим каналы PD0 и PD1 в режим вывода. 

Начиная с метки "Start:" начинается основной рабочий цикл нашей программы, эта метка послужит нам для обозначения начального адреса основного цикла и позже будет использована для возврата.

При помощи инструкции "SBI" выполняем установку бита PORTD0 (предопределен в файле m8def.inc) в порте PORTD чем установим на пине PD0 высокий уровень. Используя инструкцию "CBI" выполняется очистка указанного (PORTD1) бита в порте PORTD и тем самым устанавливается низкий уровень на пине PD1.

Дальше с помощью инструкции RCALL выполняем относительный вызов подпрограммы которая начинается с метки "Wait:". Здесь для запоминания адреса возврата уже используется стек, который мы инициализировали в начале программы.

После завершения подпрограммы (в нашем случае ее функция - задержка по времени) программа вернется к позиции где был выполнен вызов подпрограммы (адрес возврата будет получен из стека) и с этого места продолжится выполнение последующих операторов.

После вызова подпрограммы задержки "Wait" следуют вызовы инструкций SBI и CBI в которых выполняется установка битов порта PORTD таким образом, что теперь на пине PD0 у нас будет низкий уровень, а на пине PD1 - высокий.

По завершению этих инструкций следует еще один вызов подпрограммы задержки "Wait", а дальше следует инструкция "RJMP" которая выполнит относительный переход к указанной метке - "Start", после чего программа снова начнет установку битов в порте с задержками по времени.

Таким образом выполняется реализация бесконечного цикла в котором будут "дергаться" пины порта PORTD микроконтроллера и поочередно зажигаться/гаснуть светодиоды которые подключены к каналам данного порта (пины PD0, PD1).

После основного цикла программы следует наша подпрограмма задержки по времени. Принцип ее работы заключается в выполнении трех вложенных циклов, в каждом из которых происходит вычитание (DEC) единички из числа которое хранится в отдельном регистре, и так до тех пор пока значение не достигнет нуля. Инструкция "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.

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.

Комментарии к публикации (8):
renych #1renych
16 Ноябрь 2016 13:07

Стэк работает по принципу FILO, а не FIFO/.

+1
ph0en1x #2ph0en1x
16 Ноябрь 2016 15:18

Благодарю за замечание, исправил вот так: LIFO (last in - first out, последним пришёл - первым вышел).

0
topsi #3topsi
05 Июль 2017 17:01

Спасибо за цикл статей!
Небольшое замечание: в комментарии говорится, что все пины ставятся на вывод, но по факту только младшие 2.

+1
ph0en1x #4ph0en1x
05 Июль 2017 20:26

Спасибо за замечание, исправил!

0
Вадим #5Вадим
03 Август 2017 11:16

Спасибо за статью, очень доходчиво. Но непонятно один момент:

DEC R17 ; уменьшаем значение в регистре R17 на 1
BRNE WLoop0 ; возврат к WLoop0 если значение в R18 не равно 0

наверно имелось ввиду R17?

0
ph0en1x #6ph0en1x
04 Август 2017 09:35

Да, в этом комментарии должно быть R17 - исправил.

0
Вадим #7Вадим
10 Август 2017 20:43

После выполнения инструкции OUT SPH,R16 программный счётчик попадает на область данных, где записано наименование программы Simple LEDs blinking program и начинает выполнять коды инструкций, которые соответствуют байтам этой надписи. Поведение программы при этом может быть непредсказуемым, т.к. теоретически может попасться, например, код перехода и т.п. Это так и задумывалось? Может быть строку, где резервируются данные поместить в конце или, если в начале, то по нулевому адресу разместить инструкцию перехода на начало программы?

0
ph0en1x #8ph0en1x
11 Август 2017 16:32

Вадим, вы правы. При подготовке кода упустил этот момент и так сложилось что байты в данной строчке не "запороли" ход выполнения программы.
Добавил исправления и уточнения в статью.
Спасибо!

0