Структура программы на языке C++ для Arduino
Рассмотрим пример минимально возможной программы на C++ для Arduino, которая ничего не делает:
Разберёмся что здесь написано и почему это обязательно: почему нельзя обойтись просто пустым файлом.
Из чего состоит программа
Блоки бывают разных видов и какой из них когда будет исполняться зависит от внешних условий. В примере минимальной программы вы можете видеть 2 блока. В этом примере блоки называются определением функции. Функция — это просто блок кода с заданным именем, которым кто-то затем может пользоваться из-вне.
Классика жанра: мигающий светодиод
Давайте теперь дополним нашу программу так, чтобы происходило хоть что-то. На Arduino, к 13-му пину подключён светодиод. Им можно управлять, чем мы и займёмся.
Скомпилируйте, загрузите программу. Вы увидите, что каждую секунду светодиод на плате помигивает. Разберёмся почему этот код приводит к ежесекундному миганию.
Каждое выражение — это приказ процессору сделать нечто. Выражения в рамках одного блока исполняются одно за другим, строго по порядку без всяких пауз и переключений. То есть, если мы говорим об одном конкретном блоке кода, его можно читать сверху вниз, чтобы понять что делается.
Если пронумеровать выражения по порядку, как они исполняются, получится:
Результат от этого не изменится ни на йоту: после компиляции вы получите абсолютно эквивалентный бинарный файл.
Что делают выражения
Теперь давайте попробуем понять почему написанная программа приводит в итоге к миганию светодиода.
Как известно, пины Arduino могут работать и как выходы и как входы. Когда мы хотим чем-то управлять, то есть выдавать сигнал, нам нужно перевести управляющий пин в состояние работы на выход. В нашем примере мы управляем светодиодом на 13-м пине, поэтому 13-й пин перед использованием нужно сделать выходом.
Это делается выражением в функции setup :
Уточняющие значения, такие как 13 и OUTPUT называются аргументами функции. Совершенно не обязательно, что у всех функций должно быть по 2 аргумента. Сколько у функции аргументов зависит от сути функции, от того как её написал автор. Могут быть функции с одним аргументом, тремя, двадцатью; функции могут быть без аргументов вовсе. Тогда для их вызова круглые скобка открывается и тут же закрывается:
На самом деле, вы могли заметить, наши функции setup и loop также не принимают никакие аргументы. И загадочное «нечто» точно так же вызывает их с пустыми скобками в нужный момент.
Вернёмся к нашему коду. Итак, поскольку мы планируем вечно мигать светодиодом, управляющий пин должен один раз быть сделан выходом и затем мы не хотим вспоминать об этом. Для этого идеологически и предназначена функция setup : настроить плату как нужно, чтобы затем с ней работать.
Перейдём к функции loop :
Как только сон окончен, функция loop завершается. По факту завершения «нечто» тут же вызывает её ещё раз и всё происходит снова: светодиод поджигается, горит, гаснет, ждёт и т.д.
Если перевести написанное на русский, получится следующий алгоритм:
Arduino для начинающих. Часть 1
Предисловие
Доброго времени суток, Хабр. Запускаю цикл статей, которые помогут Вам в знакомстве с Arduino. Но это не значит, что, если Вы не новичок в этом деле – Вы не найдёте ничего для себя интересного.
Введение
Было бы не плохо начать со знакомства с Arduino. Arduino – аппаратно-программные средства для построения систем автоматики и робототехники. Главным достоинством есть то, что платформа ориентирована на непрофессиональных пользователей. То есть любой может создать своего робота вне зависимости от знаний программирования и собственных навыков.
Начало
Создание проекта на Arduino состоит из 3 главных этапов: написание кода, прототипирование (макетирование) и прошивка. Для того, чтоб написать код а потом прошить плату нам необходима среда разработки. На самом деле их есть немало, но мы будем программировать в оригинальной среде – Arduino IDE. Сам код будем писать на С++, адаптированным под Arduino. Скачать можно на официальном сайте. Скетч (набросок) – программа, написанная на Arduino. Давайте посмотрим на структуру кода:
Важно заметить, что обязательную в С++ функцию main() процессор Arduino создаёт сам. И результатом того, что видит программист есть:
Давайте разберёмся с двумя обязательными функциями. Функция setup() вызывается только один раз при старте микроконтроллера. Именно она выставляет все базовые настройки. Функция loop() — циклическая. Она вызывается в бесконечном цикле на протяжении всего времени работы микроконтроллера.
Первая программа
Для того, чтоб лучше понять принцип работы платформы, давайте напишем первую программу. Эту простейшую программу (Blink) мы выполним в двух вариантах. Разница между ними только в сборке.
Принцип работы этой программы достаточно простой: светодиод загорается на 1 секунду и тухнет на 1 секунду. Для первого варианта нам не понадобиться собирать макет. Так как в платформе Arduino к 13 пину подключён встроенный светодиод.
Прошивка Arduino
Для того, чтоб залить скетч на Arduino нам необходимо сначала просто сохранить его. Далее, во избежание проблем при загрузке, необходимо проверить настройки программатора. Для этого на верхней панели выбираем вкладку «Инструменты». В разделе «Плата», выберете Вашу плату. Это может быть Arduino Uno, Arduino Nano, Arduino Mega, Arduino Leonardo или другие. Также в разделе «Порт» необходимо выбрать Ваш порт подключения (тот порт, к которому вы подключили Вашу платформу). После этих действий, можете загружать скетч. Для этого нажмите на стрелочку или во вкладке «Скетч» выберете «Загрузка» (также можно воспользоваться сочетанием клавиш “Ctrl + U”). Прошивка платы завершена успешно.
Прототипирование/макетирование
Для сборки макета нам необходимы следующие элементы: светодиод, резистор, проводки (перемычки), макетная плата(Breadboard). Для того, чтоб ничего не спалить, и для того, чтоб всё успешно работало, надо разобраться со светодиодом. У него есть две «лапки». Короткая – минус, длинная – плюс. На короткую мы будем подключать «землю» (GND) и резистор (для того, чтоб уменьшить силу тока, которая поступает на светодиод, чтоб не спалить его), а на длинную мы будем подавать питание (подключим к 13 пину). После подключения, загрузите на плату скетч, если вы ранее этого не сделали. Код остаётся тот же самый.
На этом у нас конец первой части. Спасибо за внимание.
Программируем Arduino на чистом Си
В жизни ардуинщика рано или поздно наступает момент, когда в штатной среде разработки становится тесно. Если скетчам перестает хватать памяти, требуется жесткий реалтайм и работа с прерываниями или просто хочется быть ближе к железу — значит пришло время переходить на C. Бывалые электронщики при упоминании Arduino презрительно поморщатся и отправят новичка в радиомагазин за паяльником. Возможно, это не самый плохой совет, но мы пока не будем ему следовать. Если отбросить Arduino IDE и язык wiring/processing, у нас в руках останется прекрасная отладочная плата, уже оснащенная всем необходимым для работы микроконтроллера. И, что немаловажно, в память контроллера уже зашит бутлоадер, позволяющий загружать прошивку без использования программатора.
Для программирования на языке C нам понадобится AVR GCC Toolchain.
Windows:
Устанавливаем WinAVR, который содержит все необходимое.
Debian и Ubuntu:
sudo apt-get install gcc-avr binutils-avr avr-libc
Также нам потребуется установленная Arduino IDE, т.к. она содержит утилиту avrdude, которая нужна для загрузки прошивки в контроллер. CrossPack тоже содержит avrdude, но версия, идущая с ним, не умеет работать с Arduino.
После того, как все установлено, создадим наш первый проект. Для начала напишем Makefile. Он позволит нам избежать ввода длинных команд вручную при каждой компиляции и загрузке прошивки.
В этом файле нам нужно вписать свою команду для запуска avrdude. На разных системах она будет выглядеть по разному. Чтобы узнать свой вариант, запускаем Arduino IDE и в настройках ставим галочку «Show verbose output during upload».
Небольшое замечание: все отступы в Makefile делаются символами табуляции (клавишей Tab). Если ваш текстовый редактор заменяет эти символы пробелами, команда make откажется собирать проект.
Теперь создадим файл main.c — собственно текст нашей программы, в которой традиционно помигаем светодиодом.
Наш проект готов. Откроем консоль в директории нашего проекта и введем команду «make»:
Как видим, размер получившейся прошивки составляет всего 180 байт. Аналогичный ардуиновский скетч занимает 1116 байт в памяти контроллера.
Теперь вернемся к консоли и введем «make flash» чтобы загрузить скомпилированный файл в контроллер:
Если загрузка прошла без ошибок, то светодиод, подключенный к 13 контакту платы, радостно замигает. Иногда avrdude не может найти плату или отваливается по таймауту — в этом случае может помочь передегивание USB кабеля. Также, во избежание конфликтов доступа к плате, не забудьте закрыть Arduino IDE перед командой «make flash».
Возможно многие вещи, описанные в этой статье, покажутся очевидными матерым разработчикам. Я постарался описать процесс максимально понятным для начинающего ардуинщика языком и собрать в одном месте информацию, которую мне удалось добыть в различных источниках, и проверенную опытным путем. Может быть кому-то эта статья сэкономит пару часов времени.
Создание крупных проектов
Написание программы
В этом уроке мы поговорим о том, чем ардуинщики отличаются от программистов, почему они друг друга не понимают и не любят, а также чем “скетч” отличается от программы. Начнем издалека: с языков программирования. Микроконтроллер сам по себе программируется на языке ассемблер (assembler, asm), причем у каждого МК есть свой ограниченный набор команд. Каждая команда выполняется за один такт процессора, есть программируя на асме мы имеем максимальный контроль за скоростью выполнения кода, его размером и вообще происходящими внутри МК процессами: именно поэтому он называется низкоуровневым языком. Программировать на ассемблере очень непросто, потому что набор команд очень небольшой, команды эти элементарные (не в плане использования, а в самой сути) и многие стандартные для других языков вещи приходится буквально изобретать заново и описывать вручную: вручную перемещаться по адресному пространству, манипулировать памятью и работать в обнимку с документацией на конкретный МК и знанием его архитектуры. Кода получается очень много и он выглядит мягко говоря непонятно:
Прочитать и понять незнакомый код на асме без комментариев разработчика и знания документации на микроконтроллер в некоторых случаях вообще практически невозможно. Именно поэтому программирование микроконтроллеров раньше было очень сложным занятием, и как “хобби” было недоступно человеку без соответствующей специальности. Сейчас ассемблерный код генерируется компилятором из языков более высокого уровня, писать на которых легко и приятно: ту же Ардуину можно программировать на C, C++, Basic, а также на целой куче оболочек визуального программирования. Мы используем готовые и понятные функции и инструменты языка, пишем на них пару десятков или сотен строчек, а компилятор потом преобразует их в десятки тысяч строк уже ассемблерного кода, который будет работать на МК. В Arduino IDE мы программируем на C++… или на C? Хороший вопрос, потому что фактически мы пишем на обоих: язык С++ является языком С, в который добавили классы, объекты, наследование и всё что с ними связано, С++ изначально даже назывался как “С с классами“. С++ позволяет использовать все прелести ООП – объектно ориентированного программирования, благодаря которому огромную программу можно организовать очень красиво, читаемо, можно создавать различные независимые друг от друга модули и библиотеки, выстроить струкруру проекта наиболее эффективно, а также обеспечить его удобную доработку и редактирование. А вот теперь мы переходим к части, где я скажу “вспомните все примеры из предыдущих уроков и исходники моих проектов. Так делать нельзя.“ В общем и целом можно выделить два подхода к разработке программы: процедурный и объектно-ориентированный (есть ещё третий – Ардуино-стайл, когда все функции и переменные намешаны в одном файле). Если описывать в общих чертах, то процедурный подход представляет собой нагромождение функций и переменных, иногда раскиданных по отдельным файлам, а ООП – оборачивание всего кода в классы, которые взаимодействуют между собой и основной программой. В Ардуино-сообществе практически всегда встречается первый тип, потому что новичку так работать гораздо проще, программы в целом небольшие и несложные: сама основная программа пишется процедурно, но использует “библиотеки”, которые в основном являются классами. Все официальные и неофициальные примеры построены таким образом, да и сама Arduino IDE называет свои документы скетчами (от англ. sketch – набросок), потому что Ардуино задумана как платформа для обучения и быстрого прототипирования, а не для разработки крупных и серьезных проектов. В Arduino IDE вместо нормального менеджера документов у нас есть вкладки, которые работают не самым очевидным образом и явно сделаны для процедурного подхода. Давайте более подробно разберём особенности подходов.
ООП и процедурный подход
Порог вхождения
Мы начинаем мыслить и писать процедурно с самых первых уроков, потому что это просто. Работа с ООП требует гораздо более глубоких знаний С++, а эффективная работа – максимальных.
Объем кода
Оборачивание всего подряд в классы приводит к генерации огромного объема кода как текста: его просто приходится писать гораздо больше, соответственно читать тоже. Делать это в родной IDE к слову очень неприятно по сравнению со взрослыми средами разработки, в которых есть автоподстановка слов и членов классов, а также “дерево” проекта и его файлов.
Вес и скорость выполнения кода
Компиляторы все время обновляются и улучшаются, даже встроенный в Arduino IDE avr-gcc: новые версии всё лучше оптимизируют код, делая его легче и быстрее. Тем не менее, большое количество вложенных друг в друга классов и длинные цепочки передачи данных будут неизбежно работать чуть медленнее и занимать больше места, чем при более компактном процедурном подходе.
Область определения и названия
В процедурном подходе приходится постоянно следить за использованием имён переменных и функций и вынужденно избегать повторений, по возможности прятать их внутри отдельных документов и всегда держать в голове. Маленькая ошибка, например использование глобальной переменной вместо локальной может привести к багам, которые трудно отследить. Оборачивая части кода в классы мы получаем грубо говоря отдельные программы, у которых названия функций и переменных отделены от остального кода и им все равно, что у других классов или в основной программе есть такие же.
Крупные проекты
Большую программу с кучей подпрограмм гораздо приятнее писать с ООП, причем не только писать, но и улучшать в будущем.
Мелкие проекты и прототипы
Написание небольшой программы в процедурном стиле гораздо легче и быстрее классического оопшного программирования на C++, именно поэтому Ардуино IDE это просто блокнот, который сохраняет скетч в своём расширении .ino, вместо взрослых .h и .cpp: нам не нужно думать о файловой структуре проекта, мы просто пишем код и всё. Для быстрого прототипирования и отладки небольших алгоритмов это работает отлично, но с крупным проектом могут начаться проблемы и неудобства.
Библиотеки и совместимость
Что делать и как писать дальше?
Пример 1
Давайте рассмотрим пример превращения ужасного “винигретного” кода с кучей глобальных переменных и бардаком в главном цикле в понятную программу с отдельными независимыми подпрограммами. В этом примере у нас подключены две кнопки (на пины D2 и D3) и светодиод (используем бортовой на пине D13). Напишем программу, которая будет мигать светодиодом и асинхронно опрашивать кнопки с программным гашением дребезга контактов. При помощи кнопок можно будет изменять частоту мигания светодиода. Подробно комментировать код нет смысла, потому что все используемые конструкции мы не раз разбирали в цикле уроков.
Как написать скетч?
Послесловие к базовым урокам
Вот и закончился базовый курс уроков программирования Arduino. Мы с вами изучили самые базовые понятия, вспомнили (или изучили) часть школьной программы по информатике, изучили большую часть синтаксиса и инструментов языка C++, и вроде бы весь набор Ардуино-функций, который предлагает нам платформа. Подчеркну – мы изучили C++ и функции Ардуино, потому что никакого “языка Arduino” нет, это ложное понятие. Arduino программируется на C или ассемблере, а платформа предоставляет нам всего лишь несколько десятков удобных функций для работы с микроконтроллером, именно функций, а не язык. Теперь перед нами чистый лист блокнота Arduino IDE и желание творить и программировать, давайте попробуем!
Структура программы
Прежде, чем переходить к реальным задачам, нужно поговорить о некоторых фундаментальных вещах. Микроконтроллер, как мы обсуждали в самом начале пути, это комплексное устройство, состоящее из вычислительного ядра, постоянной и оперативной памяти и различных периферийных устройств (таймеры/счётчики, АЦП и проч.). Обработкой нашего с вами кода занимается именно ядро микроконтроллера, оно раздаёт команды остальным “железкам”, которые в дальнейшем могут работать самостоятельно. Ядро выполняет различные команды, подгоняемое тактовым генератором: на большинстве плат Arduino стоит генератор с частотой 16 МГц. Каждый толчок тактового генератора заставляет вычислительное ядро выполнить следующую команду, таким образом Ардуино выполняет 16 миллионов операций в секунду. Много ли это? Для большинства задач более чем достаточно, главное использовать эту скорость с умом. Зачем я об этом рассказываю: микроконтроллер может выполнить только одну задачу в один момент времени, так как у него только одно вычислительное ядро, поэтому реальной “многозадачности” нет и быть не может, но за счёт большой скорости выполнения ядро может выполнять задачи по очереди, и для человека это будет казаться многозадачностью, ведь что для нас “раз Миссисипи“, для микроконтроллера – 16 миллионов действий! Есть всего два варианта организации кода:
“Многозадачность” с yield()
Таким же образом можно опрашивать энкодер или другие датчики, которые требуют максимально частого опроса. Не менее жизненным будет пример со сценарием движения шагового мотора или плавного движения сервопривода, которые требуют максимально частого вызова функций опроса. Рассмотрим абстрактный пример движения мотора по нескольким заданным точкам, функция вращения мотора должна вызываться как можно чаще (так сделано почти во всех библиотеках для шаговых моторов):
Таким образом мы быстро и просто расписали “траекторию” движения для шагового мотора по времени, не используя какие-то таймеры или библиотеки таймеров. Для более сложных программ, например с движением двух моторов, такой фокус уже может не пройти и проще работать с таймером.
“Многозадачность” с millis()
Первым делом внесём такую оптимизацию: сократим код вдвое и избавимся от одной задержки, используя флаг:
Хитрый ход, запомните его! Такой алгоритм позволяет переключать состояние при каждом вызове. Сейчас наш код всё ещё заторможен задержкой в 1 секунду, давайте от неё избавимся:
Что здесь происходит: цикл loop() выполняется несколько сотен тысяч раз в секунду, как ему и положено, потому что мы убрали задержку. Каждую свою итерацию мы проверяем, не настало ли время переключить светодиод, не прошла ли секунда? При помощи этой конструкции и создаётся нужная многозадачность, которой хватит для 99% всех мыслимых проектов, ведь таких “таймеров” можно создать очень много!
Данный код всё ещё мигает светодиодом раз в секунду, но помимо этого он с разными промежутками времени отправляет сообщения в последовательный порт. Если открыть его, можно увидеть следующий текст:
Это означает, что у нас спокойно работают 4 таймера с разным периодом срабатывания, работают “параллельно”, обеспечивая нам многозадачность: мы можем выводить данные на дисплей раз в секунду, и заодно опрашивать датчик 10 раз в секунду и усреднять его показания. Хороший пример для первого проекта! Обязательно вернитесь к уроку о функциях времени, там мы разобрали несколько конструкций на таймере аптайма!
“Многозадачность” с прерываниями таймера
Для критичных по времени задач можно использовать выполнение по прерыванию таймера. Какие это могут быть задачи:
Настройка таймера на нужную частоту и режим работы – непосильная для новичка задача, хоть и решается в 2-3 строчки кода, поэтому предлагаю использовать библиотеки. Для настройки прерываний по таймеру 1 и 2 есть библиотеки TimerOne и TimerTwo. Мы сделали свою библиотеку, GyverTimers, в которой есть также таймер 0 (для программирования без использования Arduino.h), а также все таймеры на Arduino MEGA, а их там целых 6 штук. Ознакомиться с документацией и примерами можно на странице библиотеки. Сейчас рассмотрим простой пример, в котором “параллельно” выполняющемуся Blink будут отправляться данные в порт. Пример оторван от реальности, так делать нельзя, но он важен для понимания самой сути: код в прерывании выполнится в любом случае, ему безразличны задержки и мёртвые циклы в основном коде.
Переключение задач
Важнейшим инструментом по организации логики работы программы является так называемый конечный автомат (англ. State Machine) – значение, которое имеет заранее известный набор состояний. Звучит сложно, но на самом деле речь идёт об операторе swith и переменной, которая переключается кнопкой или по таймеру. Например:
Таким образом организуется выбор и выполнение выбранных участков кода. Переключение переменной mode тоже должно быть сделано не просто так, как в примере выше, тут есть варианты:
Ограничить диапазон при увеличении можно несколькими способами. Способы абсолютно одинаковые по своей сути, но записать можно по разному:
Аналогично при уменьшении:
Переключение с первой на последнюю и обратно делается точно так же:
Рассмотрим несколько готовых примеров на базе библиотеки GyverButton:
Флаги
Логические переменные, или флаги, являются очень важным инструментом организации логики работы программы. В глобальном флаге можно хранить “состояние” составляющих программы, и они будут известны во всей программе, и во всей же программе могут быть изменены. Немного утрированный пример:
Состояние глобального флага может быть прочитано в любых других функциях и местах программы, таким образом можно сильно упростить код и избавиться от лишних вызовов. При помощи флага можно организовать однократное выполнение блока кода по какому-то событию:
Также флаг можно инвертировать, что позволяет генерировать последовательность 10101010 для переключения каких-то двух состояний:
Флаги – очень мощный инструмент, не забывайте о них!
Избавляемся от циклов и задержек
Как мигать светодиодом без задержки мы обсуждали выше. А как избавиться от цикла? Очень просто – цикл заменяется на счётчик и условие. Пусть у нас есть цикл for, выводящий значение счётчика:
Для избавления от цикла нам нужно сделать свою переменную-счётчик, поместить всё это дело в другой цикл (например, в loop) и самостоятельно увеличивать переменную и проверять условие:
И всё. А как быть, если в цикле была задержка? Вот пример
Как соединить несколько скетчей?
Чтобы соединить несколько проектов в один, нужно разобраться со всеми возможными конфликтами:
Можно внести все правки в схемы и программы объединяемых проектов, чтобы они не конфликтовали. Далее приступаем к сборке общей программы:
Раньше у нас было два (или больше) отдельно работающих проекта. Теперь наша задача как программиста – продумать и запрограммировать работу этих нескольких проектов в одном, и тут ситуаций уже бесконечное множество:
В большинстве случаев нельзя просто так взять и объединить содержимое loop() из разных программ, я надеюсь все это понимают. Даже мигалку и пищалку таким образом объединить не получится, если изначально код был написан с задержками или замкнутыми циклами.
Пример “Метеостанция”
Сила Arduino как конструктора заключается в том, что абсолютно по любой железке вы сможете найти в Гугле подробное описание, библиотеку, схему подключения и пример работы: полностью готовый набор для интеграции в свой проект! Вернёмся к нашим метео-часам и попробуем “собрать” такой проект из скетчей-примеров, ведь именно для этого примеры и нужны! Нам понадобится:
Начинаем гуглить информацию по подключению и примеру для каждой железки:
Из уроков из Гугла мы узнаём такую важную информацию, как схемы подключения: дисплей и часы подключаются к шине i2c, а датчик ds18b20 можно подключить в любой другой пин. Схема нашего проекта: Качаем библиотеки для наших модулей и устанавливаем. Библиотеку дисплея нам дают прямо в статье: https://iarduino.ru/file/134.html, библиотеку для часов по своему опыту советую RTClib (та, что в статье – не очень удобная). В статье про датчик температуры нам рассказали про библиотеку DallasTemperature.h, ссылку – не дали. Ну чтож, поищем сами “DallasTemperature.h”, найдём по первой ссылке. Для неё нужна ещё библиотека OneWire, ссылку на неё дали в статье про термометр. Итого у нас должны быть установлены 4 библиотеки. Сейчас наша цель – найти рабочие примеры для каждой железки, убедиться в их работоспособности и выделить для себя минимальный набор кода для управления модулем, это бывает непросто – в статьях бывают ошибки и просто нерабочий код: эти статьи чаще всего являются копипастой от людей, далёких от темы. Я взял пример работы с дисплеем из статьи, а вот часы и термометр пришлось смотреть в примерах библиотеки. Немного причешем примеры, оставим только нужные нам функции получения значений или вывода, я оставил всё что мне нужно в setup() :