На распутье — Ардуино, Cи или Ассемблер?
Сначала короткая предыстория появления этого поста. Относительно давно, помигав светодиодом, захотелось сделать что-то полезное. Так появился Беспроводной программируемый по Wi-Fi комнатный термостат с монитором качества воздуха и другими полезными функциями. Как назло, в это время перестал работать мой промышленный термостат. Меня выручил еще сырой макет, наспех спрятанный в картонную коробочку. За время отопительного сезона напрягал лишь один недостаток прототипа – это необходимость таскать по квартире удлинитель 220В и кабель, который всегда путался под шваброй ногами . Поэтому решил сделать нечто похожее, но автономное, притом, с питанием от батареек, как в серийном образце.
Понятно, что на потреблении устройства в целом сказывается слишком много факторов таких, как энергопотребление подключенных модулей, потребление самого контроллера, который управляет периферией, не последнюю роль тут играет оптимальное построение самого кода и алгоритм работы устройства.
Приступая к задаче, для меня было очевидно одно – вряд ли программы промышленных автономных устройств составлены на платформе Arduino IDE. Где все спрятано в громоздкие тяжеловесные библиотеки, а простые коды (скетчи) занимают в редакторе несколько десятков строк, делая работу в этой среде комфортной и не требующей особых усилий. Уточню сразу – дальше речь о выборе языка программирования между Ардуино, Си или Ассемблером. «Язык Ардуино»- это сленг для краткости. Нет такого языка программирования. Если увидите тут и дальше «язык Ардуино», то — это «Arduino IDE — интегрированная среда разработки для Windows, MacOS и Linux, разработанная на Си и C ++»(Википедия).
В начале пути меня оптимистично настроила статья Почему многие не любят Arduino. Ниже, для наглядности, картинка оттуда с кодом «мигалки».
Пример слева написан в платформе Arduino IDE, а справа — работа непосредственно с регистрами. Скетч выглядит несколько компактней, чем та же «мигалка», но с использованием регистров.
На изображении ниже — компиляция кода «мигалки» на Ассемблере. Как видно, былая компактность испарилась – количество строк в 3 раза больше, чем в Ардуино.

Итак, с 2 картинок выше видно – размер памяти, занимаемой в контроллере кодом «мигалки» одним светодиодом, написанным в платформе Ардуино, составляет 1030 байт, на Си – 176 байт, на Ассемблере – 42 байта.
Теперь взглянем на более сложный код. Поскольку в своих проектах использую модуль давления-температуры BMP280, составил код барометра-термометра на Си, чтобы заодно была какая-то польза.
барометр-термометр на Си
/* На распутье - Ардуино, Cи или Ассемблер? https://habr.com/ru/post/547752/ */ #include #include #include #include "bmp180/bmp180.c" #include "uart.c" #include #include #include #include "nokia/nokia5110.h" int main(void) < serial_init(); DDRD |= (1 return 0; >
В проект входят следующие компоненты: контроллер ATMEGA328P, модуль давления-температуры BMP180 и дисплей Nokia 3110. ATMEGA328P принимает инфу с датчика BMP180 и после преобразований отображает ее на дисплее Nokia 3110, затем спит. Сон задается сторожевым таймером Watchdog. Проект собирается в Atmel Studio 7 и эмулируется в Proteus 8 Pro. Этот проект Atmel Studio был создан для отладки кода в Proteus’e. В библиотеке Proteus 8 Pro модуля BMP280 нет, поэтому пришлось составить код с включением BMP180. Светодиод в коде — для наглядности, чтобы придать динамику статичной картинке.
Ниже — электрическая схема устройства. При монтаже схемы обращайте внимание на функциональное назначение выводов контроллера и модулей. Подключение кварца — XTAL1, XTAL2 (ATMEGA328P). Уточню, схему барометра-термометра на BMP180 я «в железе» не собирал, поэтому тут могут проявиться проблемы, которые не видны при эмуляции в Proteus’e.

Для скачивания zip-файла проекта в Atmel Studio 7 перейдите по ссылке – тут все виртуальные проекты и коды программ из этой публикации.
Файлы прошивок *.hex находятся в папках Debug соответствующего проекта Atmel Studio 7. В архиве есть проект барометра-термометра на BMP280. Его электрическая схема такая же, как и у барометра-термометра на BMP180. Проект успешно собирается в Atmel Studio 7 и работает «в железе». Для работы «в железе» пришлось внести изменения в строке #define BMP280_ADDR 0x77 файла библиотеки bmp280.c, а именно: заменить начальный адрес 0x77 на 0x76. Не забудьте сделать эту корректировку, если будете использовать в своих проектах код барометра-термометра на BMP280, с подключенной библиотекой bmp280.c.
Ниже — код этого же барометра-термометра в платформе Arduino IDE. Естественно, с другими библиотеками.
барометр-термометр в Arduino IDE
/* На распутье - Ардуино, Cи или Ассемблер? https://habr.com/ru/post/547752/ */ #include #include #include #include #include //https:esp8266.ru/forum/threads/esp8266-5110-nokia-lcd.1143/#post-16942 #include //https:esp8266.ru/forum/threads/esp8266-5110-nokia-lcd.1143/#post-16942 Adafruit_BMP280 bmp280; float Press, Tin; //давление, температура Adafruit_PCD8544 display = Adafruit_PCD8544(5, 7, 6); void setup() < Serial.begin(9600); display.begin(); // display.clearDisplay(); display.setContrast(60); // установка контраста while (!bmp280.begin(BMP280_ADDRESS - 1)) < Serial.println(F("Could not find a valid BMP280 sensor, check wiring!")); delay(100); >> void loop() < // измерение температуры, давления Tin = bmp280.readTemperature(); Press = bmp280.readPressure() / 133.3; Serial.println("Temperature: " + String(Tin) + "*C"); Serial.println("Pressure: " + String(Press) + "mm Hq"); display.clearDisplay(); //давление, мм рт.ст. < display.setTextSize(1); display.setCursor(20, 5); display.println (Press, 0); // нет знаков после запятой display.setCursor(41, 5); display.println("mmHq"); >//температура, *C < display.setTextSize(2); display.setCursor(15, 20); display.println (Tin, 1); // один знак после запятой display.setCursor(66, 20); display.println("C"); >display.display(); LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); >
Ресурсы, потребляемые программой барометра-термометра на Си и в Arduino IDE наглядно показаны на картинке:

Как видно, эти примеры потребляют 5954 байт (С) и 12956 байт (Arduino IDE) в Flash. Соотношение изменилось с 6-ти раз для «мигалки» до 2-х с небольшим. К сожалению, линейной зависимости нет – чем объемней код, тем меньше соотношение размеров памяти Ардуино к Си. В идеале на этой картинке должен присутствовать 3 столбец с кодом на Ассемблере, но такого кода в Интернете я не нашел, а составить код самому мне пока не под силу.
Попутно замечу, что использование компилируемых в Arduino IDE библиотек и функций на С/С++ особо имеет смысл в тех случаях, когда размер занимаемой памяти превышает или близок к размеру памяти контроллера. Мне, например, часто удается уходить от предупреждения: Недостаточно памяти, программа может работать нестабильно.

Теперь посмотрим еще один вариант – это цифровой термометр-гигрометр на AM2302 (DHT22), ATtiny13 и MAX7219, код которого составлен на Ассемблере.
Автор статьи задался целью разработать простой термометр-гигрометр выполненном на одном из самых «маленьких» микроконтроллеров — ATtiny13 с весьма скромными характеристиками – 1Кб программной памяти, 64 байтами ОЗУ и 5 интерфейсными выводами. Он решил эту непростую задачку, выбрав Ассемблер, заодно вспомнив те далекие времена, когда код можно было составлять на низкоуровневых языках, используя машины типа ZX-Spectrum.
Ниже скриншот со сборкой данного кода в Atmel Studio 7.

Код устройства на Ассемблере занимает 738 байт памяти в контроллере. Безусловно, программа барометра-термометра, о котором шла речь выше, будь ее код составлен на Ассемблере, заняла бы больше места. По нескольким причинам — в схеме реализовано управление дисплеем Nokia3110 по интерфейсу SPI (это 5 линий связи, тут – 3), связь с датчиком BMP280 осуществляется по протоколу I2C (2 линии, тут – 1) и дополнительные символы, которые позволяют не гадать – температура это или другой параметр.
Из того, что я нашел в Интернете, можно утверждать, Ассемблер даст выигрыш в размере кода для относительно больших проектов процентов 10-20 по сравнению с Си. Но надо учитывать, что в больших проектах Си может уменьшить размер кода за счёт лучшей оптимизации.
Код Ассемблера выполняется практически на машинном уровне: один цикл – одна команда. В качестве аргумента приведу пример из справочника по командам ассемблера AVR. Установка бита в регистре ввода/вывода — SBI A, b. Эта команда устанавливает заданный бит в регистре ввода-вывода. На выполнение этой операции контроллерами megaAVR потребуется 2 цикла и на tinyAVR, XMEGA — 1 цикл. Для схемы с контроллером ATtiny13 и резонатором 9,6 МГц выполнение команды займет один цикл, то есть 1/9600000 Гц = 0,104 мксек.
Выполнение похожей операции на языке Си, например, задать состояние порта — PORTB = 32; займет в этой же схеме не меньше времени. А о Ардуино и говорить нечего – там придется выполнить объемную функцию void digitalWrite(uint8_t pin, uint8_t val);. Подробно о размерах кода в Си и Ардуино читайте тут.
Поэтому разработчики простых в управлении серийных продуктов (холодильник, кофеварка без наворотов, другое — оглянитесь вокруг себя дома), как правило, пишут коды на низкоуровневых языках. С тем, чтобы разместить программу в контроллере с меньшей памятью. Тут работают законы экономики — контроллер с меньшими ресурсами стоит дешевле, следовательно себестоимость изделия становится ниже.
Теперь о энергосбережении немножко издали. Вспомним, что код Ассемблера выполняется на машинном уровне: один цикл – одна или несколько команд. в зависимости от типа контроллера. Это — десятые доли микросекунды. То есть, на выполнение программы с размером несколько десятков байт уйдут единицы-десятки микросекунд. Дальше контроллер бесконечно будет крутить этот набор «0» и «1», затрачивая энергию на перезаряд емкости затворов сотен полевых транзисторов, на которых построен кристалл контроллера, а также чтение и записи данных в его память. Длительность периода повтора будет зависеть только от размера кода в памяти контроллера, неважно на каком языке он составлен. Просто на Assembler’е он будет наименьшим, а в Arduino IDE – наибольшим. Соответственно, период цикла для кода на Assembler’е – наименьший, в Arduino IDE – наибольший.
Уменьшить эти затраты можно остановив процессор или программно уменьшив частоту его работы. В Ассемблере переход в «спящий» режим сна выполняет функция управления контроллером SLEEP. В других можно использовать функцию WDT (WatchDog Timer), а в Ардуино еще и функцию LowPower.powerDown (SLEEP_1S, ADC_OFF, BOD_OFF), заодно отключив все лишнее, что не используется в конкретной задаче. В эффективности этой функции сможет убедиться каждый, заменив в скетче «мигалки» (скетч — на картинке вначале статьи) функцию отсчета времени delay(1000); этой функцией и включив в разрыв питания контроллера амперметр. Да, не забудьте подключить библиотеку LowPower.h. На Си это сделал автор этой статьи. Ток в цепи питания attiny13a с паузой — 1,5мА, со сном — 240мкА. Потребление в 6(!) раз меньше.
Допустим, вы намерены собрать барометр-термометр и задумываетесь о энергосбережении. Понятно, что давление/температура в заданной разрядности не изменятся за несколько минут, которые для контроллера целая вечность. Ему можно выделить это время для сна. После сна он снова выполнит свою работу: примет информацию с датчика, преобразует в понятные для человека циферки и выведет все это на дисплей. И в таком режиме «работа-сон» он будет крутиться, пока не сядут батарейки. Объем «работы» контроллера, вернее время, которое контроллер будет занят выполнением работы, зависит от того, на каком языке составлена программа барометра-термометра. Если есть возможность загрузить в контроллер код на выбор – ArduinoIDE, C, Assembler, с одинаковым временем «сна», то в каком из трех предложенных вариантов батарейки сядут раньше (позже)? Мой ответ – ArduinoIDE (Assembler).
Так куда же идти? На мой взгляд, для любителей, как я, – это платформа Arduino IDE с низкоуровневыми вставками. Тем же, кому тесно в Arduino IDE, — в С. Хотя коды на С можно оптимизировать иногда до размеров не намного больше, чем в Assembler’е, все-таки для понимания работы контроллера стоит напрячься и освоить азы Assembler’а. Ведь полезность знаний – это аксиома.
Спасибо за внимание. Всего наилучшего!
Ссылки по теме
- Превращаем Arduino в полноценный AVRISP программатор, HWman
- Почему многие не любят Arduino, HWman
- Atmel Studio (видео)
- PROTEUS 8 для начинающих (видео)
- Код blink в формате *.asm на github
- Простой цифровой термометр/гигрометр на AM2302 (DHT22), ATtiny13 и MAX7219, kdekaluga
- Справочник по командам ассемблера AVR
- AVR — Power management или как правильно спать
- Реальная правда о Программистах ненавидящих Arduino, free_arduino
Как программировать Arduino на ассемблере
Попробуем на простом примере рассмотреть, как можно “хакнуть” Arduino Uno и начать писать программы в машинных кодах, т.е. на ассемблере для микроконтроллера ATmega328p. На данном микроконтроллере собственно и собрана большая часть недорогих «классических» плат «duino». Данный код также будет работать на практически любой demo плате на ATmega328p и после небольших возможных доработок на любой плате Arduino на Atmel AVR микроконтроллере. В примере я постарался подойти так близко к железу, как это только возможно. Для лучшего понимания того, как работает микроконтроллер не будем использовать какие-либо готовые библиотеки, а уж тем более Arduino IDE. В качестве учебно-тренировочной задачи попробуем сделать самое простое что только возможно — правильно и полезно подергать одной ногой микроконтроллера, ну то есть будем читать данные из датчика температуры и влажности DHT-11.
Arduino очень клевая штука, но многое из того что происходит с микроконтроллером специально спрятано в дебрях библиотек и среды Arduino для того чтобы не пугать новичков. Поигравшись с мигающим светодиодом я захотел понять, как микроконтроллер собственно работает. Помимо утоления чисто познавательного зуда, знание того как работает микроконтроллер и стандартные средства общения микроконтроллера с внешним миром — это называется «периферия», дает преимущество при написании кода как для Arduino так и при написания кода на С/Assembler для микроконтроллеров а также помогает создавать более эффективные программы. Итак, будем делать все наиболее близко к железу, у нас есть: плата совместимая с Arduino Uno, датчик DHT-11, три провода, Atmel Studio и машинные коды.
Для начало подготовим нужное оборудование.
Писать код будем в Atmel Studio 7 — бесплатно скачивается с сайта производителя микроконтроллера — Atmel.
Весь код запускался на клоне Arduino Uno — у меня это DFRduino Uno от DFRobot, на контроллере ATmega328p работающем на частоте 16 MHz — отличная надежная плата. Каких-либо отличий от стандартного Uno в процессе эксплуатации я не заметил. Похожая чорная плата от DFBobot, только “Mega” отлетала у меня 2 года в качестве управляющего контроллера квадрокоптера — куда ее только не заносило — проблем не было.
Для просмотра сигналов длительностью в микросекунды (а это на минутку 1 миллионная доля секунды), я использовал штуку, которая называется “логический анализатор”. Конкретно, я использовал клон восьмиканального USBEE AX Pro. Как смотреть для отладки такие быстрые процессы без осциллографа или логического анализатора — на самом деле даже не знаю, ничего посоветовать не могу.
Прежде всего я подключил свой клон Uno — как я говорил у меня это DFRduino Uno к Atmel Studio 7 и решил попробовать помигать светодиодиком на ассемблере. Как подключить описанно много где, один из примеров по ссылке в конце. Код пишется прямо в студии, прошивать плату можно через USB порт используя привычные возможности загрузчика Arduino -через AVRDude. Можно шить и через внешний программатор, я пробовал на китайском USBASP, по факту у меня оба способа работали. В обоих случаях надо только правильно настроить прошивальщик AVRDude, пример моих настроек на картинке
Полная строка аргументов:
-C “C:\avrdude\avrdude.conf” -p atmega328p -c arduino -P COM7 115200 -U flash:w:”$(ProjectDir)Debug\$(TargetName).hex:i
В итоге, для простоты я остановился на прошивке через USB порт — это стандартный способ для Arduio. На моей UNO стоит чип ATmega 328P, его и надо указать при создании проекта. Нужно также выбрать порт к которому подключаем Arduino — на моем компьютере это был COM7.
Для того, чтобы просто помигать светодиодом никаких дополнительных подключений не нужно, будем использовать светодиод, размещенный на плате и подключенный к порту Arduino D13 — напомню, что это 5-ая ножка порта «PORTB» контроллера.
Подключаем плату через USB кабель к компьютеру, пишем код в студии, прошиваем прямо из студии. Основная проблема здесь собственно увидеть это мигание, поскольку контроллер фигачит на частоте 16 MHz и, если включать и выключать светодиод такой же частотой мы увидим тускло горящий светодиод и собственно все.
Для того чтобы увидеть, когда он светится и когда он потушен, мы зажжем светодиод и займем процессор какой-либо бесполезной работой на примерно 1 секунду. Саму задержку можно рассчитать вручную зная частоту — одна команда выполняется за 1 такт или используя специальный калькулятор по ссылки внизу. После установки задержки, код выполняющий примерно то же что делает классический «Blink» Arduino может выглядеть примерно так:
Сигнальный вывод датчика подключен к ноге 2 (PIN2) порта PORTD контролера или (что то же самое) к выводу D2 Arduino. Он же через резистор 4.7 kOm “подтянут” на “плюс” питания. Плюс и минус датчика подключены — к соответствующим проводам питания. USB-TTL переходник подключен к выходу Tx USART порта Arduino, что значит PIN1 порта PORTD контроллера.
В собранном виде на breadboard:
Разбираемся с датчиком и смотрим datasheet. Сам по себе датчик несложный, и использует всего один сигнальный провод, который надо подтянуть через резистор к +5V — это будет базовый «высокий» уровень на линии. Если линия свободна — т.е. ни контроллер, ни датчик ничего не передают, на линии как раз и будет базовый «высокий» уровень. Когда датчик или контроллер что-то передают, то они занимают линию — устанавливают на линии «низкий» уровень на какое-то время. Всего датчик передает 5 байт. Байты датчик передает по очереди, сначала показатели влажности, потом температуры, завершает все контрольной суммой, это выглядит как “HHTTXX”, в общем смотрим datasheet. Пять байт — это 40 бит и каждый бит при передаче кодируется специальным образом.
Для упрощения, будет считать, что «высокий» уровень на линии — это «единица», а «низкий» соответственно «ноль». Согласно datasheet для начала работы с датчиком надо положить контроллером сигнальную линию на землю, т.е. получить «ноль» на линии и сделать это на период не менее чем 20 милсек (миллисекунд), а потом резко отпустить линию. В ответ — датчик должен выдать на сигнальную линию свою посылку, из сигналов высокого и низкого уровня разной длительности, которые кодируют нужные нам 40 бит. И, согласно datasheet, если мы удачно прочитаем эту посылку контроллером, то мы сразу поймем что: а) датчик собственно ответил, б) передал данные по влажности и температуре, с) передал контрольную сумму. В конце передачи датчик отпускает линию. Ну и в datasheet написано, что датчик можно опрашивать не чаще чем раз в секунду.
Итак, что должен сделать микроконтроллер, согласно datasheet, чтобы датчик ему ответил — нужно прижать линию на 20 миллисекунд, отпустить и быстро смотреть, что на линии:
Датчик должен ответить — положить линию в ноль на 80 микросекунд (мксек), потом отпустить на те же 80 мксек — это можно считать подтверждением того, что датчик на линии живой и откликается:
После этого, сразу же, по падению с высокого уровня на нижний датчик начинает передавать 40 отдельных бит. Каждый бит кодируются специальной посылкой, которая состоит из двух интервалов. Сначала датчик занимает линию (кладет ее в ноль) на определенное время — своего рода первый «полубит». Потом датчик отпускает линию (линия подтягивается к единице) тоже на определенное время — это типа второй «полубит». Длительность этих интервалов — «полубитов» в микросекундах кодирует что собственно пытается передать датчик: бит “ноль” или бит “единица”.
Рассмотрим описание битовой посылки: первый «полубит» всегда низкого уровня и фиксированной длительности — около 50 мксек. Длительность второго «полубита» определят, что датчик собственно передает.
Для передачи нуля используется сигнал высокого уровня длительностью 26–28 мксек:
Для передачи единицы, длительность сигнала высокого увеличивается до 70 микросекунд:
Мы не будет точно высчитывать длительность каждого интервала, нам вполне достаточно понимания, что если длительность второго «полубита» меньше чем первого — то закодирован ноль, если длительность второго «полубита» больше — то закодирована единица. Всего у нас 40 бит, каждый бит кодируется двумя импульсами, всего нам надо значит прочитать 80 интервалов. После того как прочитали 80 интервалов будем сравнить их попарно, первый “полубит” со вторым.
Вроде все просто, что же требуется от микроконтроллера для того чтобы прочитать данные с датчика? Получается нужно значит дернуть ногой в ноль, а потом просто считать всю длинную посылку с датчика на той же ноге. По ходу, будем разбирать посылку на «полу-биты», определяя где передается бит ноль, где единица. Потом соберем получившиеся биты, в байты, которые и будут ожидаемыми данными о влажности и температуре.
Ок, мы начали писать код и для начала попробуем проверить, а работает ли вообще датчик, для этого мы просто положим линию на 20 милсек и посмотрим на линии, что из этого получится логическим анализатором.
Растянем масштаб так чтобы увидеть окончание нашего импульса в 20 милсек и лучше увидеть начало посылки от датчика — смотрим все как в datasheet — сначала датчик выставил низкий/высокий уровень по 80 мксек, потом начал передавать биты — а данном случае во втором «полубите» передается «0»
Значит датчик работает и данные нам прислал, теперь надо эти данные правильно прочитать. Поскольку задача у нас учебная, то и решать ее будем тупо в лоб. В момент ответа датчика, т.е. в момент перехода с высокого уровня в низкий, мы запустим цикл с счетчиком числа повторов нашего цикла. Внутри цикла, будем постоянно следить за уровнем сигнала на ноге. Итого, в цикле будем ждать, когда сигнал на ноге перейдет обратно на высокий уровень — тем самым определив длительность сигнала первого «полубита». Наш микроконтроллер работает на частоте 16 MHz и за период например в 50 микросекунд контроллер успеет выполнить около 800 инструкций. Когда на линии появится высокий уровень — то мы из цикла аккуратно выходим, а число повторов цикла, которые мы отсчитали с использованием счетчика — запоминаем в переменную.
После перехода сигнальной линии уже на высокий уровень мы делаем такую же операцию– считаем циклы, до момента когда датчик начнет передавать следующий бит и положит линию в низкий уровень. К счастью, нам не надо знать точный временной интервал наших импульсов, нам достаточно понимать, что один интервал больше другого. Понятно, что если датчик передает бит «ноль» то длительность второго «полубита» и соответственно число циклов, которые мы отсчитали будет меньше чем длительность первого «полубита». Если же датчик передал бит «единица», то число циклов которые мы насчитаем во время второго полубита будет больше чем в первым.
И для того что бы мы не висели вечно, если вдруг датчик не ответил или засбоил, сам цикл мы будем запускать на какой-то временной период, но который гарантированно больше самой длинной посылки, чтоб если датчик не ответил, то мы смогли выйти по тайм-ауту.
В данном случае показан пример для ситуации, когда у нас на линии был ноль, и мы считаем сколько раз мы в цикле мы считали состояние ноги контроллера, пока датчик не переключил линию в единицу.
Для того, чтобы это измерять температуру регулярно добавляем вечный цикл с задержкой порядка 1200 миллисекунд, поскольку datasheet DHT11 говорит, что не рекомендуется опрашивать датчик чаще чем 1 раз в секунду.
Основной цикл после этого выглядит примерно так:
Прошиваем, подключаем USB-TTL кабель (преобразователь)к компьютеру, запускаем терминал, выбираем правильный виртуальный COM порта и наслаждаемся нашим новым цифровым термометром. Для проверки можно погреть датчик в руке — у меня температура при этом растет, а влажность как ни странно уменьшается.
Зажигаем светодиод в Arduino на ассемблере, изучая даташит на МК.
Правильно было бы изучать работу микронтроллера на языках более низкого уровня, чтобы немного иметь представление как логическая 1 прилетает на конретный pin нашей микросхемы. Современные среды разработки позволяют отправить логическую 1 на выход одной командой, но новички должны понимать, для чего это вообще нужно, ведь зажигать светодиоды — одно, а разбираться с библиотекой, которая отправляет 0 и 1 в огромном количестве на дисплей или датчик — другое дело.
Понимая работу команд на низком уровне, можно будет разобраться с ошибками, которые выйдут за пределы стандартных уроков, когда вы будете конструировать что-то своё. И так, наша подопытная плата будет Arduino Uno, а в её сердце находится atmega328, это 8-битный микроконтроллер AVR. Откроем даташит на неё, и посмотрим ключевые моменты, которые могут нам помочь начать ориентироваться в этой микросхеме.
Самые главные блоки, где хранятся данные в процессоре — это регистры. В нашем микронтроллере, регистров общего назначения (РОН) — 32 штуки, все они 8-битные, за некоторым исключением. 8 битные — значит можно временно хранить данные вида 0b01010101, что в десятеричном представлении будет от 0 до 255. Называются эти регистры R0 — R31, но программируя в arduino, никто и никогда не использует их, за вас это делает компилятор. Он сам решает, какие нужно задействовать, когда вы назначаете значение переменной или выполняете простые арифметические операции. Эти регистры мы используем для хранения данных.

Кстати, про исключения — последняя группа регистров R26-R31 можно использовать для указания адреса при косвенной адресации SRAM, группируются из 8 битных и называются X,Y,Z. Если проще, для них есть инструкции, чтобы делать операции смещения, декремента и инкремента, но также к ним можно обращаться, и как к 8-битным обычным регистрам.
Теперь поговорим о регистрах портов ввода-вывода, так как, чтобы зажечь светодиод, необходимо будет записать туда логическую единицу. Всего есть три важных регистра, отвечающих за работу порта ввода-вывода. Рассмотрим документацию, на примере Порта B.

1. DDR (Data Direction Register)— регистр направления данных — от его инициализации зависит, будет работать порт на ввод данных или вывод.

PIN (Port INput) — регистр для чтения состояния порта. Интересная деталь — считать логический уровень можно при любой конфигурации DDR и PORTB. Даже если порт настроен на выход — всё равно можно узнать его состояние.
Смотрите также: Переделываем корпус робота под монитор

PORT (Data Register) — регистр данных порта. Этот регистр позволяет принимать выходное значение порта в виде логической 1 или 0. Но при условии, что DDR у нас установлен на вывод данных. Если же порт сконфигурирован на ввод данных, то от значения регистра PORT будет зависеть, подтянута ли ножка сопротивлением к питанию, или контакт просто висит в воздухе для приёма данных (состояние HI-Z). Кстати в таком состоянии, без внешнего подтягивающего резистора, порт оставлять нежелательно, ибо на входе будет бесконечный поток 0 и 1, что может нежелательно сказаться на стабильности МК.

Попробуем зажечь светодиод, к примеру на PB1. Логично, что если мы хотим использовать порт на вывод данных (отправить логическую единицу, т.е. 5В на выход порта), то нам совсем не нужен регистр PIN, зато очень нужны DDR и PORTB. Смотрим на Arduino Uno, и ничего не понимаем. Где же PB1, который явно видно в даташите? Для удобства и создания своей экосистемы, компания Arduino сделала так сказать ремаппинг пинов, но конечно, исходный пин никуда не пропал, и его легко найти.

Подключаем катод светодиода к выводу GND, а анод через резистор к выводу с номером 9, что и будет являться PB1. Рассмотрим сначала код, который зажжет светодиод на чистом arduino
void setup() pinMode(9, OUTPUT);
digitalWrite(9, HIGH);
>
void loop() <>
Первой командой pinMode(9, OUTPUT); устанавливаем пин 9 в режим вывода данных, а второй digitalWrite(9, HIGH); переводим пин9 в состояние логической единицы, т.е. 5В, загружаем в плату, наблюдаем, что светодиод стал светиться. Всё понятно и просто

Теперь попробуем тоже самое проделать на ассемблере, используя ту же среду ардуино. В принципе arduino ide, использует в качестве компилятора avr-gcc, поэтому ничего нового здесь изобретать не нужно, все команды из даташита на atmega, нативно работают и тут. Правда для вставки асм-кода необходимо использовать конструкцию
asm volatile
(
» asm-code » «\n»
);
Попробуем поработать с командой LDI — Load Immediate , как легко понять из названия она загружает значение в выбранный нами регистр. Помните, схему Регистров общего назначения? Команда LDI, как и ряд других команд, может работать только со старшими регистрами под номерами r16-r31. Попробуем это проверить.
Смотрите также: Подключение дисплея ili9341 к ESP8266. Вывод JPG картинки

При попытке записать значение в регистр r10, мы получили ошибку от компилятора, причём она даже сообщил нам, что необходим регистр свыше номера 15. Кстати, почему я вообще пытаюсь загрузить число 0b00000010 в Бинарном представлении?, я его так записал для простоты понимания.

Помните мы выбрали PB.1 для нашего светодиода? Нумерация порта следует от 0 до 7, это и есть 8 бит данных порта, значит число 0b00000010, активирует бит номер 1, т.е. записав это значение в правильный регистр DDR, мы сделаем пин 1 порта B, активным на выход. Ещё раз внимательно посмотрим даташит, на странице с DDRB

Видим странную строчку, что при использовании порта, как порт ввода-вывода, происходит смещение адреса на 0x04. Значит именно туда нам нужно записать значение 0b00000010. Но ведь команда LDI работает только с регистрами от r16, а если посмотреть ещё раз даташит, то r16 это 0x10, значит компилятор опять выдаст ошибку.

Поэтому, наше значение мы записываем в любой регистр от r16 до r31, а в адрес 0x04, мы можем переместить значение другой командой OUT. Команда OUT — записывает данные из регистра в регистры I/O. Применяем команду OUT к регистру 0x04, и переносим туда значение из регистра r16.

Код скомпилировался, но светодиод ещё не горит. Ведь мы назначили направление порта на Вывод, но значение равно 0, т.е. логический ноль. Попробуем разобраться, что поменялось. Запустим proteus

Видим, что на PB1, образовался логический уровень = 0. Значит порт работает на выход. Теперь зажжём светодиод, поменяв уровень на 1.

Опять видим строчку, что при использованию команд ввода-вывода In и Out, нужно использовать адресацию 0х05. Установим значение PB1 равным 1. Для загрузки значения будем использовать тот же регистр, ведь значение 0b00000010 не поменялось для PB1

Теперь то светодиод загорелся

Теперь для наглядности поменяем конфигурацию порта, сделаем все пины порта B на выход, а в логическую 1, переведём PB1 и PB 3. Для двух переменных, задействуем ещё один регистр старше r16, например r17

Компилируем, и смотрим в Proteus.

Теперь у порта B, нет серых выходов — все синие (т.е. лог. 0), кроме PB1 и PB3. Видите, как точно пины порта повторили переменную 0b00001010. Теперь вернём всё как было, зажжём один светодиод, и сделаем его мигающим. Для этого нам нужно ввести задержку, и если на arduino это одна команда delay(1000); , то на ассемблере, конечно такой команды нет. Но так-как наш урок не об этом, то мы просто возьмём готовый код с Калькулятора задержек AVR и поместим его в наш. На этом сайте можно установить нужную задержку, например 500 миллисекунд. Готовый код содержит в себе три цикла с пустыми командами — естественно это самый простой тип задержки, во время выполнения которого, процессор ни на что не реагирует.
Смотрите также: Аренда офиса

Можно даже выбрать наш компилятор avr-gcc, и он заключит код в конструкцию asm volatile, что нам и нужно. В интернете много примеров, как использовать переход на метку rjmp или rcall, но так-как мы в среде arduino, и у нас уже есть бесконечный цикл main loop, то можно выйти из ситуации проще — например инвертировать переменную в конце программы, которую мы загружаем в регистр PORTB. Прямой команды инверсии нужного бита, как в С++ здесь нет, но сделать это можно разным путём — например использовать команду COM — выполняет дополнение до единицы (реализует обратный код) содержимого регистра. Но так мы затронем остальные биты порта, а нам нужно поменять только PB1, лучше использовать EOR — исключающее или, подобрав в дополнение нужную переменную маску. Пусть это будет R21 с таким же значением 0b00000010. Смотрим таблицу истинности

eor r17, r21 — в регистре r17, значение будет постоянно меняться с 0b00000010 на 0b00000000, потому-что изначально 1 с 1 даст 0, а потом 0 с 1, даст 1, и так по кругу. В итоге код должен получится такой

Загружаем в плату — диод мигает, что и требовалось получить.

Конечно, реальный случай использования асм- вставок в ардуино, наверное очень редкий, но если уже написана большая программа в этой среде, и есть какие то глюки или нужно ускорить быстродействие, то возможно это получится реализовать с помощью ассемблера. Можно найти много примеров, какой «грязный» код иногда делает компилятор ардуино. Здесь, я больше хотел показать любителям ардуино, как на деле происходит взаимодействие программы и МК, на примере документации портов ввода-вывода. В следующих статьях будем рассматривать реальные устройства, но не будем забывать обращаться к архитектуре МК.
Занимательная электроника

Почему важно изучать язык программирования Assembler.
В данном уроке мы с Вами рассмотрим несколько примеров работы одного и того же кода но на разных языках программирования, а так же плюсы и минусы каждого подхода.
И так, в начале мы рассмотрим программирование на С++ в системе Arduino Ide. Программный код будет очень простой, — мы будем по очереди включать и выключать один из выходов микроконтроллера. Результат выходного сигнала будем смотреть на осциллографе.
Программный код на С++ для микроконтроллера Atmega328P (плата Arduino Nano):
void setup() < pinMode(5, OUTPUT); digitalWrite(5, 0); digitalWrite(5, 1);
Запустив данный код мы видим на осциллограмме частоту 117кГц:
Но если у нас частота микроконтроллера 16МГц, с учетом работы микроконтроллера, у нас на выходе должно было быть 4МГц. Как мы видим данный код работает немного медленно. При этом код занимает 734байта.
Теперь давай напишем тот же программный код, но на языке программирования С:
void setup ()< DDRD = 0x 20 ; PORTD = 0x 20 ;
Как мы видим на осциллограмме, частота на выходе равна 4МГц, что равно уже расчетной, но как мы видим у нас нет меандра, то есть длительность логических уровней разная:

Сам код занимает 440 байт.
И теперь напишем программный код на Assembler, единственно, что возьмем микроконтроллер Atmega8, так как мы пока только начинаем изучать Assembler и нам надо минимально не сложный код для нашего примера. Причем мы напишем два примера кода, который выполняет одно и тоже действие.
.include «m8def.inc»
ldi r16, 0b00100000 ; регистр r16 = 0x20
out DDRD, r16 ; вывод PD5 на выход
сlr r17 ; регистр r17 = 0
Gcykle:
out PortD, r16 ; PD5 = 1 -такт
out PortD, r17 ; PD5 =0 1-такт
rjmp Gcykle ; переход обратно 2 такта
Данный программный код будет выполняться с частотой 1/4 от тактовой частоты, что можно проверить либо осциллографом либо частотомером. При этом весь программный код занимает 12 байтов, но при этом мы используем два регистра:
Как видим ситуация с переключением логических уровней тут уже получше.
И еще один вариант данного кода:
.include «m8def.inc»
ldi r16, 0b00100000 ; регистр r16 = 0x20
out DDRD, r16 ; вывод PD5 на выход
Gcykle:
sbi PortD, 5 ; PD5 = 2 -такт
cbi PortD, 5 ; PD5 =0 2-такт
rjmp Gcykle ; переход обратно 2 такта
Данный код не занимает регистры как предыдущий, а на прямую устанавливает/сбрасывает бит в порту D. Таким образом частота на выходе будет не 1/4, а 1/6 от тактовой. Но при этом мы занимаем меньше памяти 10 байт и экономим регистры:
Таким образом мы можем оптимизировать свой код как по скорости выполнения, так и по объему памяти.
Как мы видим, используя программный код на Assembler мы можем использовать максимально все свойства микроконтроллера и самим контролировать оптимизацию программного кода, о чес не скажешь о среде программирования Arduino IDE.
