Содержание
- 1 Спецификаторы (Pro)
- 2 Первая прошивка
- 3 Некоторое железо
- 4 Beaglebone Black
- 5 Подключение библиотек и файлов
- 6 #include – подключить файл
- 7 Операторы сравнения
- 8 Сравнение
- 9 Важные страницы
- 10 Условные директивы #if #else
- 11 Пространство имён (Pro)
- 12 Передача массива в функцию (Pro)
- 13 Что такое протокол I2C и как он работает
- 14 Важные страницы
Спецификаторы (Pro)
Помимо возможности сделать переменную константой при помощи спецификатора у нас есть ещё несколько интересных инструментов по работе с переменной.
static
– делает переменную (или константу) статичной. Что это значит?
Статичная локальная
Для начала вспомним, как работает обычная локальная переменная: при вызове функции локальная переменная создаётся заново и получает нулевое значение, если не указано иначе. Если локальная переменная объявлена как – она будет хранить своё значение от вызова к вызову функции, то есть станет грубо говоря глобально-локальной. Пример:
Обычная локальная:
void setup() { myFunc(); // вернёт 20 myFunc(); // вернёт 20 myFunc(); // вернёт 20 myFunc(); // вернёт 20 } void loop() { } byte myFunc() { byte var = 10; var += 10; return var; }
Статическая локальная:
void setup() { myFunc(); // вернёт 20 myFunc(); // вернёт 30 myFunc(); // вернёт 40 myFunc(); // вернёт 50 } void loop() { } byte myFunc() { static byte var = 10; var += 10; return var; }
Статичная глобальная
Статичная глобальная переменная становится доступной только в данном файле, спецификатор позволяет спрятать её от воздействий из других файлов программы.
extern
– указывает компилятору, что переменная объявлена где-то в другом файле программы, и при компиляции он её найдёт и будет использовать. А если не найдёт – ошибки не будет. Например при помощи данного кода можно сбросить счётчик
// указываем, что хотим использовать // переменную timer0_millis, // которая объявлена где-то далеко // в файлах Arduino extern volatile unsigned long timer0_millis; void setup() { timer0_millis = 0; // сброс mills() } void loop() { }
volatile
– данный спецификатор указывает компилятору, что данную переменную не нужно оптимизировать и её значение может быть изменено откуда-то извне. Обычно переменные с таким спецификатором используются в обработчиках прерываний. Вычисления с такими переменными также не оптимизируются и занимают больше процессорного времени.
Первая прошивка
Итак, разобрались со средой разработки, теперь можно загрузить первую прошивку. Можно загрузить пустую прошивку, чтобы просто убедиться, что все драйвера установились и платы вообще прошиваются. Рекомендуется делать это с новой платой, к которой никогда не подключались датчики и модули, чтобы исключить выход платы из строя по вине пользователя.
1. Плата подключается к компьютеру по USB, на ней должны замигать светодиоды. Если этого не произошло:
- Неисправен USB кабель
- Неисправен USB порт компьютера
- Неисправен USB порт Arduino
- Попробуйте другой компьютер, чтобы исключить часть проблем из списка
- Попробуйте другую плату (желательно новую), чтобы исключить часть проблем из списка
- На плате Arduino сгорел входной диод по линии USB из-за короткого замыкания, устроенного пользователем при сборке схемы
- Плата Arduino сгорела полностью из-за неправильного подключения пользователем внешнего питания или короткого замыкания
2. Компьютер издаст характерный сигнал подключения нового оборудования, а при первом подключении появится окошко “Установка нового оборудования”. Если этого не произошло:
- См. предыдущий список неисправностей
- Кабель должен быть data-кабелем, а не “зарядным”
- Кабель желательно втыкать напрямую в компьютер, а не через USB-хаб
- Не установлены драйверы Arduino (во время установки IDE или из папки с программой), вернитесь к установке.
3. В списке портов (Arduino IDE/Инструменты/Порт) появится новый порт, обычно COM3. Если этого не произошло:
- См. предыдущий список неисправностей
- Некорректно установлен драйвер CH341 из предыдущего урока
- Если список портов вообще неактивен – драйвер Arduino установлен некорректно, вернитесь к установке
- Возникла системная ошибка, обратитесь к знакомому компьютерщику
4. Выбираем свою плату. Если это Arduino Nano, выбираем в Инструменты\Плата\Arduino Nano. Если другая – выбираем другую. Нажимаем стрелочку в левом верхнем углу (загрузить прошивку). Да, загружаем пустую прошивку.
Если появилась надпись “Загрузка завершена” – значит всё в порядке и можно прошивать другие скетчи. В любом случае на вашем пути встретятся другие два варианта событий, происходящих после нажатия на кнопку “Загрузка” – это ошибка компиляции и ошибка загрузки. Вот их давайте рассмотрим более подробно.
Некоторое железо
- GyverStepper – высокопроизводительная библиотека для управления шаговым мотором
- AccelStepper – более интересная и качественная замена стандартной библиотеке Stepper для контроля шаговых моторчиков. Скачать можно со страницы разработчика, или вот прямая ссылка на архив.
- AccelMotor – моя библиотека для управления мотором с энкодером (превращает обычный мотор в “шаговый” или сервомотор)
- ServoSmooth – моё дополнение к стандартной библиотеке Servo, позволяющее управлять сервоприводом с настройкой максимальной скорости движения и разгона/торможения (как в AccelStepper, только для серво). Must have любого любителя серво манипуляторов!
- CapacitiveSensor – библиотека для создания сенсорных кнопок (из пары компонентов рассыпухи). Описание
- ADCTouchSensor – ещё одна версия библиотеки для создания сенсорных кнопок. Есть ещё одна, так, на всякий случай
- TouchWheel – библиотека для создания сенсорных слайдеров и колец
- Buzz – детектор присутствия на основе всего лишь одного провода! (измеряет ЭМ волны)
- Bounce – библиотека антидребезга для кнопок и всего такого. Сомнительная полезность, но почитайте описание
- oneButton – библиотека для расширенной работы с кнопкой. На мой взгляд неудобная
- GyverButton – моя библиотека для расширенной работы с кнопкой. Очень много возможностей!
- AdaEncoder – библиотека для работы с энкодерами
- GyverEncoder – моя библиотека для энкодеров с кучей возможностей, поддерживает разные типы энкодеров
- RTCLib – лёгкая библиотека, поддерживающая большинство RTC модулей
- OV7670 – библиотека для работы с камерой на OV7670
- IRremote – базовая библиотека для работы с ИК пультами и излучателями
- IRLib – более расширенная версия для работы с ИК устройствами
- IRLremote – самая чёткая библиотека для ИК пультов, работает через прерывания. 100% отработка пульта
- keySweeper – почти готовый проект для перехвата нажатий с беспроводных клавиатур
- USB_Host_Shield – позволяет Ардуине работать с геймпадами (PS, XBOX) и другими USB устройствами
- Brain – библиотека для работы с NeuroSky ЭЭГ модулями
- TinyGPS – шустрая библиотека для работы с GPS модулями
- GyverRGB – моя библиотека для работы с RGB светодиодами и лентами
- FadeLED – библиотека для плавного (ШИМ) мигания светодиодами с разными периодами
- CurrentTransformer – измерение силы тока при помощи трансформатора (катушки) на проводе. Читай: токовые клещи
- LiquidCrystal-I2C – библиотека для LCD дисплеев с I2C контроллером. Разработчик – fdebrabander
- LiquidCrystal-I2C – библиотека для LCD дисплеев с I2C контроллером. Разработчик – johnrickman. Предыдущая вроде бы лучше
- LiquidTWI2 – быстрая библиотека для LCD дисплеев на контроллерах MCP23008 или MCP23017
- LCD_1602_RUS – библиотека русского шрифта для LCD дисплеев
- LCD_1602_RUS_ALL – новая версия предыдущей библиотеки с поддержкой украинского языка
- u8glib – библиотека для работы с монохромными LCD и OLED дисплеями
- ucglib – библиотека для работы с цветными LCD и OLED дисплеями
- Adafruit_SSD1306 – ещё одна библиотека для OLED дисплеев
- Adafruit-GFX-Library – дополнение для adafruit библиотек дисплеев, позволяет выводить графику
- SSD1306Ascii – самодостаточная и очень лёгкая библиотека для вывода текста на OLEDы
- NeoPixelBus – библиотека для работы с адресной светодиодной лентой, адаптированная под esp8266 (NodeMCU, Wemos и др.).
- microLED – лёгкая и простая библиотека для работы с адресной лентой
- – лёгкая библиотека для отправки любых данных через радио модули 433 МГц
- rc-switch – библиотека для работы с радио модулями 433 МГц и разными протоколами связи
Beaglebone Black
Подключение библиотек и файлов
В реальной работе вы очень часто будете использовать библиотеки или просто внешние файлы, они подключаются к главному файлу (файлу прошивки) при помощи директивы , данная директива сообщает препроцессору, что нужно найти и включить в компиляцию указанный файл. Указанный файл может тянуть за собой и другие файлы, но там оно уже всё прописано и подключается автоматически. Рассмотрим пример:
#include <Servo.h> // подключает библиотеку Servo.h #include “Servo.h” // тоже подключает библиотеку Servo.h
В чём отличие и ? Когда указываем название , компилятор сначала ищет файл в папке со скетчем, а затем в папке с библиотеками. При использовании компилятор ищет файл только в папке с библиотеками!
К слову о папках с библиотеками: их две, в обеих будет производиться поиск библиотек.
- Мои Документы/Arduino/libraries
- C:/Program Files (x86)/Arduino/libraries (или C:/Program Files/Arduino/libraries для 32-разрядной Windows)
В первую папку (в документах) библиотеки попадают при подключении их при помощи команды “подключить .zip библиотеку”. Подключать библиотеки таким способом не рекомендуется, потому что не всегда библиотека попадает к вам в архиве, и проще будет скопировать её вручную в Program files. Также если в обеих папках будут одинаковые по названию библиотеки, это приведёт к конфликту, поэтому библиотеки просто копируем в папку libraries в Program files/Arduino.
Важное замечание: папка с библиотекой, находящаяся в C:/Program Files (x86)/Arduino/libraries, должна содержать файлы и папки библиотеки, а не одну папку с таким же названием, как сама библиотека. Это приведёт к ошибке, сборщик не сможет найти файлы!
#include – подключить файл
С подключением файлов мы уже знакомы: директива подключает новый документ в текущий, например библиотеку. После нужно указать имя файла, который подключается. Указать можно в , а можно в . В чём разница? Файл, имя которого указано в двойных кавычках, компилятор будет искать в папке с основным документом, если не найдёт – будет искать в папке с библиотеками. Если указать в скобках – будет сразу искать в папке с библиотеками, путь к которой обычно можно настроить.
#include "mylib.h" // подключить mylib.h, сначала поискать в папке со скетчем #include <mylib.h> // подключить mylib.h из папки с библиотеками
Также можно указать путь к файлу, который нужно подключить. Например у нас в папке со скетчем есть папка libs, а в ней – файл mylib.h. Чтобы подключить такой файл, пишем:
#include "libs/mylib.h"
Компилятор будет искать его в папке со скетчем, в подпапке libs.
Операторы сравнения
Предположим, что переменная A содержит 10, а переменная B содержит 20, тогда —
Имя оператора | Оператор простой | Описание | пример |
---|---|---|---|
равно | == | Проверяет, равно ли значение двух операндов или нет, если да, тогда условие становится истинным. | (A == B) не соответствует действительности |
не равно | знак равно | Проверяет, является ли значение двух операндов равным или нет, если значения не равны, тогда условие становится истинным. | (A! = B) верно |
меньше, чем | < | Проверяет, меньше ли значение левого операнда, чем значение правого операнда, если да, тогда условие становится истинным. | (A <B) верно |
больше чем | > | Проверяет, больше ли значение левого операнда, чем значение правого операнда, если да, тогда условие становится истинным. | (A> B) не соответствует действительности |
меньше или равно | <= | Проверяет, меньше ли значение левого операнда или равно значению правого операнда, если да, тогда условие становится истинным. | (A <= B) верно |
больше или равно | > = | Проверяет, больше ли значение левого операнда или равно значению правого операнда, если да, тогда условие становится истинным. | (A> = B) не соответствует действительности |
Сравнение
В языке C++ (как и пожалуй во всех языках) есть такое понятие, как логическая величина, которая принимает два значения: правда и ложь, и , 1 и 0. В качестве типа данных по работе с логическими величинами у нас есть (синоним – ), который может принимать значения 0 () или 1 (). Точно такое же значение возвращает результат сравнения двух чисел или переменных, для сравнения у нас есть несколько операторов сравнения:
- == равенство (a == b)
- != неравенство (a != b)
- >= больше или равно (a >= b)
- <= меньше или равно (a <= b)
- > больше (a > b)
- < меньше (a < b)
В рассмотренных выше абстрактных примерах с и происходит следующее: скобка “возвращает” логическое значение, которое является результатом сравнения чисел. Например если у нас и , то скобка вернёт значение , потому что меньше . А например вернёт , т.к. действительно не равно . Для связи нескольких логических величин используются логические операторы:
- ! логическое НЕ, отрицание. Есть аналог – оператор
- && логическое И. Есть аналог – оператор
- || логическое ИЛИ. Есть аналог – оператор
byte a = 10, b = 20; (a > b); // false (a != b); // true boolean flag = true; flag; // true !flag; // false !инверт !(a > b); // true //flagA = true, flagB = false; (flagA && flagB); // false, т.к. B false //flagA = true, flagB = true; (flagA and flagB); // true, т.к. оба true //flagA = true, flagB = false; (flagA || flagB); // true, т.к. хотя бы А true //flagA = false, flagB = false; (flagA or flagB); // false, т.к. ни А ни В не true
Результаты операторов сравнения можно использовать для работы с условиями, а также для цикла (речь о нём пойдёт в следующем уроке)
Сравнение float
Со сравнением чисел всё не так просто из за особенности самой модели “чисел с плавающей точкой” – вычисления иногда производятся с небольшой погрешностью, из за этого сравнение может работать неверно! Пример из:
float val1 = 0.1; // val1 == 0.100000000 float val2 = 1.1 - 1.0; // val2 == 0.100000023 !!! // казалось бы, val1 == val2 // но сравнение вернёт false if (val1 == val2); // false
Будьте внимательны при сравнении float чисел, особенно со строгими операциями ==, >= и <=: результат может быть некорректным и нелогичным!
Важные страницы
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
Условные директивы #if #else
Помимо директивы , сообщающей препроцессору о необходимости замены набора символов набором символов, есть ещё условные директивы, позволяющие заниматься так называемой условной компиляцией: обладая такой же логикой, как if-else, данные конструкции позволяют делать некоторый выбор перед компиляцией самого кода. Отличным примером является само “ядро” Ардуино – большинство функций написаны со спецификой каждого процессора, и перед компиляцией кода из множества вариантов реализации функции выбирается тот, который соответствует текущему выбранному микроконтроллеру. Проще говоря, условная компиляция позволяет по условиям включать или исключать тот или иной код из основной компиляции, т.е. сначала препроцессор анализирует код, что-то в него включается, что-то нет, и затем проходит компиляция.
Также например мы не можем объявить какую-либо константу или макро через более одного раза, это приведёт к ошибке. Условная компиляция позволяет сделать ветвящуюся конструкцию, где такое возможно. Для условной компиляции нам доступны директивы , , , , ,
- – аналог в логической конструкции
- – аналог в логической конструкции
- – аналог в логической конструкции
- – директива, завершающая условную конструкцию
- – если “определено”
- – если “не определено”
- – данный оператор возвращает если указанное слово “определено” через , и – если нет. Используется для конструкций условной компиляции.
Как ими пользоваться давайте посмотрим на примере:
#define TEST 1 // определяем TEST как 1 #if (TEST == 1) // если TEST 1 #define VALUE 10 // определить VALUE как 10 #elif (TEST == 0) // TEST 0 #define VALUE 20 // определить VALUE как 20 #else // если нет #define VALUE 30 // определить VALUE как 30 #endif // конец условия
Таким образом мы получили задефайненную константу , которая зависит от “настройки” . Конструкция позволяет включать или исключать куски кода перед компиляцией, вот например кусочек про отладку:
#define DEBUG 1 void setup() { #if (DEBUG == 1) Serial.begin(9600); Serial.println("Hello!"); #endif }
Таким образом при помощи настройки можно включить или исключить любой кусок кода.
Есть у препроцессора ещё две директивы: и , они позволяют включать или исключать участки кода по условию: – определено ли? – не определено ли? Определено или не определено – речь идёт конечно же о
#define TEST // определяем TEST #ifdef TEST // если TEST определено #define VALUE 10 // определить VALUE как 10 #else // если закоммент. #define TEST #define VALUE 20 // определить VALUE как 20 #endif // конец условия
Именно на условной компиляции строится вся универсальность библиотек для Arduino, ведь при выборе платы автоматически “создаётся” дефайн на название микроконтроллера, выглядят они так:
- И далее в этом стиле
Это позволяет создавать универсальный код, используя конструкцию с или :
#if defined(__AVR_ATmega32U4__) // код для Leonardo/Micro/Pro Micro #elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) // код для Mega (1280 или 2560) #elif defined(__AVR_ATmega328P__) // код для UNO/Nano/Pro Mini #endif
Таким образом микроконтроллерам (платам Arduino) разных моделей будет доступен персональный кусок кода, который будет передан компилятору при выборе этой платы из списка плат.
Читай продвинутый урок по директивам препроцессора, если хочешь узнать больше!
Пространство имён (Pro)
Пространство имён – очень удобная возможность языка, с её помощью можно разделить функции или переменные с одинаковыми именами друг от друга, то есть защитить свой набор данных инструментов от конфликтов имён с другими именами. “Именная область” определяется при помощи оператора :
namespace mySpace { // функции или данные };
Чтобы использовать содержимое из пространства имён, нужно обратиться через его название и оператор разрешения области видимости
mySpace::имя_функции
Более подробный пример:
namespace mySpace { byte val; void printKek() { Serial.println("kek"); } }; void setup() { Serial.begin(9600); // printKek(); // приведёт к ошибке mySpace::printKek(); }
Также есть оператор , позволяющий не использовать каждый раз обращение к пространству имён. Например, в отдельном файле у нас есть пространство имён с различными функциями. Чтобы в основном файле программы каждый раз не писать ярлык пространства имён с , можно написать
using имя_пространства_имён;
И ниже по коду можно будет пользоваться содержимым пространства имён без обращения через
Передача массива в функцию (Pro)
Иногда бывает нужно передать в функцию массив (мы о них уже говорили), передать именно массив целиком, а не отдельный его элемент. В этом случае уже не обойтись без указателей (читай урок про указатели). В следующем примере наша функция будет суммировать элементы массива, который в неё передаётся. Функция заранее знает, сколько в массиве элементов, потому что я явно цифрой указал количество в цикле .
int c; int myArray[] = {100, 30, 890, 645, 251}; void setup() { c = sumFunction(myArray); // результат 1916 } void loop() { } int sumFunction(int *intArray) { int sum = 0; // переменная для сложения for (byte i = 0; i < 5; i++) { sum += intArray; } return sum; }
Что из этого нужно запомнить: при описании функции параметр массива указывается со звёздочкой, т.е. . При вызове массив передаётся как . И в целом всё.
Давайте покажу как сделать универсальную функцию, которая суммирует массив любого размера. Для этого нам поможет оператор , возвращающий размер в байтах. Этот размер нам нужно будет передать как аргумент функции:
int c; int myArray[] = {100, 30, 890, 645, 251, 645, 821, 325}; void setup() { // передаём сам массив и его размер в БАЙТАХ c = sumFunction(myArray, sizeof(myArray)); } void loop() { } int sumFunction(int *intArray, int arrSize) { // переменная для суммирования int sum = 0; // находим размер массива, разделив его вес // на вес одного элемента (тут у нас int) arrSize = arrSize / sizeof(int); for (byte i = 0; i < arrSize; i++) { sum += intArray; } return sum; }
И вот мы получили функцию, которая суммирует массив типа данных любой длины и возвращает результат.
Важно! Переданный в функцию массив не дублирует исходный массив! Любые действия, совершённые с переданным массивом, влияют на “оригинальный” массив!
Что такое протокол I2C и как он работает
Термин IIC расшифровывается как “Inter Integrated Circuits” и часто обозначается как I2C или даже как TWI (2-wire interface protocol), но во всех случаях за этими обозначениями скрывается один и тот же протокол. I2C представляет собой протокол синхронной связи – это значит что оба устройства, которые обмениваются информацией с помощью данного протокола должны использовать общий сигнал синхронизации. Поскольку в этом протоколе используются всего 2 линии (провода), то по одной из них должен передаваться сигнал синхронизации, а по другой – полезная информация.
Впервые протокол I2C был предложен фирмой Phillips. Протокол в самом простом случае соединяет с помощью 2-х линий 2 устройства, одно из устройств должно быть ведущим, а другое – ведомым. Связь возможна только между ведущим и ведомым. Преимуществом протокола (интерфейса) I2C является то, что к одному ведущему можно подключить несколько ведомых.
Схема связи с помощью протокола I2C представлена на следующем рисунке.
Назначение линий данного интерфейса:
- Serial Clock (SCL): по ней передается общий сигнал синхронизации, генерируемый ведущим устройством (master);
- Serial Data (SDA): по ней осуществляется передача данных между ведущим и ведомым.
В любой момент времени только ведущий может инициировать процесс обмена данными. Поскольку в этом протоколе допускается несколько ведомых, то ведущий должен обращаться к ним, используя различные адреса. То есть только ведомый с заданным (указанным) адресом должен отвечать на сигнал ведущего, а все остальные ведомые в это время должны «хранить молчание». Таким образом, мы можем использовать одну и ту же шину (линию) для обмена данными с несколькими устройствами.
Уровни напряжений для передаваемых сигналов в интерфейсе I2C жестко не определены. В этом плане I2C является достаточно гибким, то есть если устройство запитывается от напряжения 5v, оно для связи с помощью протокола I2C может использовать уровень 5v, а если устройство запитывается от напряжения 3.3v, то оно для связи с помощью протокола I2C может использовать уровень 3v. Но что делать если с помощью данного протокола необходимо связать между собой устройства, работающие от различных питающих напряжений? В этом случае используются преобразователи/переключатели напряжения (voltage shifters).
Существует несколько условий для осуществления передачи данных в протоколе I2C. Инициализация передачи начинается с падения уровня на линии SDA, которое определяется как условие для начала передачи (‘START’ condition) на представленной ниже диаграмме. Как видно из этого рисунка, в то время как на линии SDA происходит падение уровня, в это же самое время на линии SCL ведущий поддерживает напряжение высокого уровня (high).
То есть, как следует из рисунка, падение уровня на линии SDA является аппаратным триггером для условия начала передачи. После этого все устройства на этой шине переключаются в режим прослушивания.
Аналогичным образом, повышение уровня на линии SDA останавливает передачу данных, что на представленной диаграмме обозначено как условие окончания передачи данных (‘STOP’ condition). В это же самое время ведущим на линии SCL поддерживается напряжение высокого уровня (high).
На следующем рисунке представлена структура адреса ведомого в протоколе I2C.
Бит R/W показывает направление передачи следующих за ним байт, если он установлен в HIGH – это значит что будет передавать ведомый (slave), а если он установлен в low – это значит что будет передавать ведущий (master).
Каждый бит передается в своем временном цикле, то есть нужно 8 временных циклов чтобы передать байт информации. После каждого переданного или принятого байта 9-й временной цикл используется для подтверждения/не подтверждения (ACK/NACK) приема информации. Этот бит подтверждения (ACK bit) формируется либо ведомым, либо ведущим в зависимости от ситуации. Для подтверждения приема информации (ACK) на линии SDA ведущим или ведомым устанавливается низкий уровень (low) в 9 временном цикле, в противном случае происходит не подтверждение приема информации (NACK).
На следующем рисунке представлена структура передаваемого сообщения в протоколе I2C.
Важные страницы
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])