Теперь создадим файл 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: нам не нужно думать о файловой структуре проекта, мы просто пишем код и всё. Для быстрого прототипирования и отладки небольших алгоритмов это работает отлично, но с крупным проектом могут начаться проблемы и неудобства.
Давайте рассмотрим пример превращения ужасного “винигретного” кода с кучей глобальных переменных и бардаком в главном цикле в понятную программу с отдельными независимыми подпрограммами. В этом примере у нас подключены две кнопки (на пины D2 и D3) и светодиод (используем бортовой на пине D13). Напишем программу, которая будет мигать светодиодом и асинхронно опрашивать кнопки с программным гашением дребезга контактов. При помощи кнопок можно будет изменять частоту мигания светодиода. Подробно комментировать код нет смысла, потому что все используемые конструкции мы не раз разбирали в цикле уроков.
Вот и закончился базовый курс уроков программирования Arduino. Мы с вами изучили самые базовые понятия, вспомнили (или изучили) часть школьной программы по информатике, изучили большую часть синтаксиса и инструментов языка C++, и вроде бы весь набор Ардуино-функций, который предлагает нам платформа. Подчеркну – мы изучили C++ и функции Ардуино, потому что никакого “языка Arduino” нет, это ложное понятие. Arduino программируется на C или ассемблере, а платформа предоставляет нам всего лишь несколько десятков удобных функций для работы с микроконтроллером, именно функций, а не язык. Теперь перед нами чистый лист блокнота Arduino IDE и желание творить и программировать, давайте попробуем!
Прежде, чем переходить к реальным задачам, нужно поговорить о некоторых фундаментальных вещах. Микроконтроллер, как мы обсуждали в самом начале пути, это комплексное устройство, состоящее из вычислительного ядра, постоянной и оперативной памяти и различных периферийных устройств (таймеры/счётчики, АЦП и проч.). Обработкой нашего с вами кода занимается именно ядро микроконтроллера, оно раздаёт команды остальным “железкам”, которые в дальнейшем могут работать самостоятельно. Ядро выполняет различные команды, подгоняемое тактовым генератором: на большинстве плат Arduino стоит генератор с частотой 16 МГц. Каждый толчок тактового генератора заставляет вычислительное ядро выполнить следующую команду, таким образом Ардуино выполняет 16 миллионов операций в секунду. Много ли это? Для большинства задач более чем достаточно, главное использовать эту скорость с умом. Зачем я об этом рассказываю: микроконтроллер может выполнить только одну задачу в один момент времени, так как у него только одно вычислительное ядро, поэтому реальной “многозадачности” нет и быть не может, но за счёт большой скорости выполнения ядро может выполнять задачи по очереди, и для человека это будет казаться многозадачностью, ведь что для нас “раз Миссисипи“, для микроконтроллера – 16 миллионов действий! Есть всего два варианта организации кода:
Таким же образом можно опрашивать энкодер или другие датчики, которые требуют максимально частого опроса. Не менее жизненным будет пример со сценарием движения шагового мотора или плавного движения сервопривода, которые требуют максимально частого вызова функций опроса. Рассмотрим абстрактный пример движения мотора по нескольким заданным точкам, функция вращения мотора должна вызываться как можно чаще (так сделано почти во всех библиотеках для шаговых моторов):
Таким образом мы быстро и просто расписали “траекторию” движения для шагового мотора по времени, не используя какие-то таймеры или библиотеки таймеров. Для более сложных программ, например с движением двух моторов, такой фокус уже может не пройти и проще работать с таймером.
Первым делом внесём такую оптимизацию: сократим код вдвое и избавимся от одной задержки, используя флаг:
Хитрый ход, запомните его! Такой алгоритм позволяет переключать состояние при каждом вызове. Сейчас наш код всё ещё заторможен задержкой в 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() :
Теперь вы знаете какие однокоренные слова подходят к слову Как написать программу для ардуино самому, а так же какой у него корень, приставка, суффикс и окончание. Вы можете дополнить список однокоренных слов к слову "Как написать программу для ардуино самому", предложив свой вариант в комментариях ниже, а также выразить свое несогласие проведенным с морфемным разбором.