Зачем использовать функции
Функция, использованная в приведенном выше примере, очень проста, поэтому все преимущества использования функций будут видны не сразу.
Одним из преимуществ использования функций является то, что они избавляют от необходимости многократно писать один и тот же код в скетче, что экономит время и память. Каждый раз, когда вызывается функция, мы просто повторно используем код, который был написан нами один раз.
Если функцию необходимо изменить, это нужно сделать только один раз, и изменения вступят в силу в каждом месте скетча, в котором вызывается функция. Если функция не использовалась, необходимо было бы найти и изменить каждое место в программе, где находятся операторы для выполнения определенной задачи.
С помощью функций можно разбить скетч на части, что сделает его более модульным и понятным. Функции можно повторно использовать в других программах.
Аппаратная часть Arduino
Для начала стоит уяснить, что собой представляет микроконтроллер. По логике, это небольшое устройство, к которому подключаются все остальные элементы системы. Ардуино должен координировать их работу при помощи прописанных в нём скриптов, выдавая соответствующие электрические сигналы. Для стандартного МК Ардуино сигналом является 5 вольт – это единичка, а отсутствие сигнала – нолик.
Именно на таком принципе построено программирование двоичным кодом. Но от такой системы мы уже давно ушли, и потому к устройству можно подключать трансформаторы переменного тока и дополнительные резисторы, ведь некоторым модулям требуется ток в 3.2-4.7 Вольт.
Соответственно, аппаратная часть Ардуино в стандартной комплектации представлена чипом с постоянной памятью, набором из резисторов и транзисторов, а также несколькими пинами. Такая простая конструкция позволяет пользователю самому навешивать «улучшения» по необходимости.
С «коробки» в микроконтроллер устанавливается стандартная прошивка, способная распознавать базовые АТ команды. Пользователь может переустановить её или перепрошить Ардуино по желанию, но стоит учитывать, что без должного опыта вы можете получить бесполезную и неработающую плату.
Как несложно догадаться, изначально Ардуино – это лишь инструмент, который позволяет координировать работу всей системы. А делает он это при помощи встроенных в него библиотек, которые можно устанавливать в систему дополнительно, по необходимости. Вплоть до того, что вы можете поставить вспомогательную карту памяти, если не хватает места. А сами же библиотеки написаны на низкоуровневом C++, который обеспечивает полный контроль над работой микроконтроллера, но имеет и ряд весомых недостатков, о которых мы и поговорим ниже.
Первая программа
Для того, чтоб лучше понять принцип работы платформы, давайте напишем первую программу. Эту простейшую программу (Blink) мы выполним в двух вариантах. Разница между ними только в сборке.
int Led = 13; // объявляем переменную Led на 13 пин (выход) void setup(){ pinMode(Led, OUTPUT); // определяем переменную } void loop(){ digitalWrite(Led, HIGH); // подаём напряжение на 13 пин delay(1000); // ожидаем 1 секунду digitalWrite(Led, LOW); // не подаём напряжение на 13 пин delay(1000); // ожидаем 1 секунду }
Принцип работы этой программы достаточно простой: светодиод загорается на 1 секунду и тухнет на 1 секунду. Для первого варианта нам не понадобиться собирать макет. Так как в платформе Arduino к 13 пину подключён встроенный светодиод.
Случайные числа Ардуино randomSeed
Обратите внимание, что во всех примерах, представленных выше, при каждом перезапуске программы случайная последовательность чисел повторяется. Этого можно избежать, используя функцию randomSeed Arduino
randomSeed позволяет поместить переменную в функцию random. Для этого используют millis, analogRead или другие варианты, позволяющие сделать генерацию чисел более случайной.
int data; void setup() { Serial.begin(9600); randomSeed(analogRead(A1)); // к пину A1 ничего не подключается } void loop() { data = random(100, 200); // ардуино рандомное число в диапазоне Serial.println(data); delay(250); }
При каждом перезапуске программы генератор псевдослучайных чисел будет инициализироваться функцией randomSeed(analogRead(A1)); со случайного значения из-за «шума» на порту. Можно использовать любой свободный порт для этого, так как все аналоговые выводы воспринимают случайный шум из окружающей среды — радиоволны, электромагнитные помехи от компьютеров, сотовых телефонов и т.д.
3Параллельные процессы без оператора «delay()»
Вариант, при котором Arduino будет выполнять задачи псевдо-параллельно, предложен разработчиками Ардуино. Суть метода в том, что при каждом повторении цикла loop() мы проверяем, настало ли время мигать светодиодом (выполнять фоновую задачу) или нет. И если настало, то инвертируем состояние светодиода. Это своеобразный вариант обхода оператора delay().
const int soundPin = 3; // переменная с номером пина пьезоэлемента const int ledPin = 13; // переменная с номером пина светодиода const long ledInterval = 200; // интервал мигания светодиодом, мсек. int ledState = LOW; // начальное состояние светодиода unsigned long previousMillis = 0; // храним время предыдущего срабатывания светодиода void setup() { pinMode(soundPin, OUTPUT); // задаём пин 3 как выход. pinMode(ledPin, OUTPUT); // задаём пин 13 как выход. } void loop() { // Управление звуком: tone(soundPin, 700); delay(200); tone(soundPin, 500); delay(200); tone(soundPin, 300); delay(200); tone(soundPin, 200); delay(200); // Мигание светодиодом: // время с момента включения Arduino, мсек: unsigned long currentMillis = millis(); // Если время мигать пришло, if (currentMillis - previousMillis >= ledInterval) { previousMillis = currentMillis; // то запоминаем текущее время if (ledState == LOW) { // и инвертируем состояние светодиода ledState = HIGH; } else { ledState = LOW; } digitalWrite(ledPin, ledState); // переключаем состояние светодиода } }
Существенным недостатком данного метода является то, что участок кода перед блоком управления светодиодом должен выполняться быстрее, чем интервал времени мигания светодиода «ledInterval». В противном случае мигание будет происходить реже, чем нужно, и эффекта параллельного выполнения задач мы не получим. В частности, в нашем скетче длительность изменения звука сирены составляет 200+200+200+200 = 800 мсек, а интервал мигания светодиодом мы задали 200 мсек. Но светодиод будет мигать с периодом 800 мсек, что в 4 раза больше того, что мы задали.
Вообще, если в коде используется оператор delay(), в таком случае трудно сымитировать псевдо-параллельность, поэтому желательно его избегать.
В данном случае нужно было бы для блока управления звуком сирены также проверять, пришло время или нет, а не использовать delay(). Но это бы увеличило количество кода и ухудшило читаемость программы.
Оператор switch case
Оператор if позволяет проверить только одно условие. Иногда необходимо выполнить одно из действий в зависимости от возвращаемого или прочитанного значения. Для этого идеально подходит оператор множественного выбора switch. Ниже показан синтаксис команды switch:
switch (var) { case 1: //инструкция для var=1 break; case 2: // инструкция для var=2 break; default: // инструкция по умолчанию (если var отличается от 1 и 2) }
В зависимости от значения переменной var выполняются инструкции в определенных блоках. Метка case означает начало блока для указанного значения. Например, case 1: означает, что данный блок будет выполнен для значения переменной var, равной один.
Каждый блок должен быть завершен с помощью команды break. Он прерывает дальнейшее выполнение оператора switch. Если команду break пропустить, то инструкции будут выполняться и в последующих блоках до команды break. Метка default не является обязательной, как и else в команде if. Инструкции, расположенные в блоке default выполняются только тогда, когда значение переменной var не подходит ни к одному шаблону.
Часто бывает так, что одни и те же инструкции должны быть выполнены для одного из нескольких значений. Это можно достичь следующим образом:
switch (x) { case 1: //инструкция для x=1 break; case 2: case 3: case 5: // инструкция для x=2 или 3 или 4 break; case 4: // инструкция для x=4 break; case 6: // инструкция для x=6 break; default: // инструкция по умолчанию (если х отличается от 1,2,3,4,5,6) }
В зависимости от значения переменной x будет выполнен соответствующий блок инструкций. Повторение case 2: case 3: case 5: информирует компилятор о том, что если переменная x имеет значение 2 или 3 или 5, то будет выполнен один и тот же фрагмент кода.
Теперь поинтереснее
Давайте объединим потенциометр и диод. И у нас выйдет плавное управление яркостью светодиода. Подключаем всё по следующей схеме:
После подключения давайте напишем код к нашему импровизированному светильнику:
int pot = A0; // потенциометр подключён к А0 int val; // переменная для хранения значений int LED = 3; // светодиод подключён к 3 пину void setup() { Serial.begin(9600); // настраиваем скорость обмена данных на 9600 бит в секунду pinMode(pot, INPUT); pinMode(LED, OUTPUT); } void loop() { val = analogRead(pot); // считываем данные с потенциометра Serial.println(val); // с новой строки выводим значения val = val / 4; // делим значения с потенциометра на 4 analogWrite(LED, val); // выводим значение переменной, которое получаем после деления на 4 }
Короткие объяснения по коду. Деление на 4 необходимо для следующего. Потенциометр может принимать значения от 0 до 1023. А вот аналоговый вход/выход передаёт значения только в диапазоне от 0 до 255. Поэтому деление нам в данном случае просто необходимо.
Общие рекомендации по написанию обработчиков прерываний
В заключение хочу привести несколько рекомендаций по написанию обработчиков прерываний.
- Во-первых, делайте обработчики предельно короткими. Ведь они прерывают выполнение основной программы, а также блокируют обработку других прерываний. По возможности обработчик должен фиксировать только факт возникновения события, изменяя значение переменной. А сама реакция на событие должна выполняться в основной программе при анализе этой переменной.
- Как уже было сказано, при входе в обработчик устанавливается глобальный запрет на обработку других прерываний. А это в свою очередь влияет на работу функций, использующих прерывания. Будьте с ними осторожнее. Если не уверены в безопасности их вызова, то лучше откажитесь от их использования в обработчике.
- Возьмите за правило объявлять разделяемые между основной программой и обработчиком переменные как volatile. И не забывайте, что этого квалификатора недостаточно в случае многобайтных переменных — используйте при работе с ними атомарно исполняемые блоки или interrupts/noInterrupts
следующей части
Из чего состоит программа
Для начала стоит понять, что программу нельзя читать и писать как книгу:
от корки до корки, сверху вниз, строку за строкой. Любая программа состоит
из отдельных блоков. Начало блока кода в C/C++ обозначается левой фигурной
скобкой , его конец — правой фигурной скобкой .
Блоки бывают разных видов и какой из них когда будет исполняться зависит от внешних
условий. В примере минимальной программы вы можете видеть 2 блока. В этом
примере блоки называются определением функции. Функция — это просто
блок кода с заданным именем, которым кто-то затем может пользоваться из-вне.
В данном случае у нас 2 функции с именами и . Их присутствие обязательно
в любой программе на C++ для Arduino. Они могут ничего и не делать, как в нашем случае,
но должны быть написаны. Иначе на стадии компиляции вы получите ошибку.
attachInterrupt
Функция attachInterrupt сообщает микроконтроллеру, на какие события он должен реагировать и какой обработчик им соответствует. Функция имеет следующий синтаксис:
attachInterrupt(interrupt, function, mode)
- interrupt — номер внешнего прерывания. Его можно указать явно (таблица соответствия номеров прерываний выводам Ардуино приведена ниже) или воспользоваться функцией digitalPinToInterrupt(pin) — она вернет номер прерывания по номеру пина.
- function — функция-обработчик прерывания. Эта функция будет вызываться каждый раз при появлении запроса прерывания.
-
mode — определяет события какого типа будут рассматриваться как запрос прерывания. Для данного параметра предусмотрены следующие значения:
- LOW — генерировать запрос прерывания при наличии сигнала низкого уровня;
- RISING — генерировать запрос прерывания при изменении уровня сигнала от низкого к высокому;
- FALLING — генерировать запрос прерывания при изменении уровня сигнала от высокому к низкого;
- CHANGE — генерировать запрос прерывания при любом изменении уровня сигнала;
- для DUE и Zero также доступно значение HIGH.
Плата | INT0 | INT1 | INT2 | INT3 | INT4 | INT5 |
UNO и другие на базе ATmega328/P | 2 | 3 | ||||
Leonardo | 3 | 2 | 1 | 7 | ||
Mega2560 | 2 | 3 | 21 | 20 | 19 | 16 |
digitalPinToInterruptattachInterruptattachInterruptmode
#define ledPin 13 #define interruptPin 2 volatile byte state = LOW; void setup() { pinMode(ledPin, OUTPUT); pinMode(interruptPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(interruptPin), blink, FALLING); } void loop() { digitalWrite(ledPin, state); } void blink() { state = !state; }
setupattachInterruptblinkFALLINGblinkstateloopstate
Оператор if…else
Расширенным оператором if является оператор if….else. Он обеспечивает выполнение одного фрагмента кода, когда условие выполняется (true), и выполнение второй фрагмент кода, если условие не выполняется (false). Синтаксис операторf if….else выглядит следующим образом:
if (условие) { // команда A } else { // команда B }
Команды «A» будут выполняться только в том случае, если условие выполнено, команда «B» будет выполняться, когда условие не выполнено. Одновременное выполнение команды «A» и «B» невозможно. Следующий пример показывает, как использовать синтаксис if…else:
Электрический паяльник с регулировкой температуры
Мощность: 60/80 Вт, температура: 200’C-450’C, высококачествен…
Подробнее
if (init()) { Serial.print(«ок»); } else { Serial.print(«ошибка»); }
Подобным образом можно проверить правильность выполнения функции и информировать об этом пользователя.
Обычной практикой является отрицание условия. Это связано с тем, что функция, которая выполнена правильно возвращает значение 0, а функция, которая отработала неверно по какой-то причине, возвращает ненулевое значение.
Объяснение такого «усложнения жизни» — просто. Если функция выполнена правильно, то это единственная информация, которая нам нужна. В случае же ошибки, стоит иногда понять, что пошло не так, почему функция не выполнена правильно. И здесь на помощь приходят числа, отличающиеся от нуля, т. е. с помощью цифровых кодов мы можем определить тип ошибки. Например, 1 — проблема с чтением какого-то значения, 2 — нет места в памяти или на диске и т. д.
В последнем измененном примере показано, как вызвать функцию, которая возвращает ноль при правильном выполнении:
if (!init()) { Serial.print(«ок»); } else { Serial.print(«ошибка»); }
Программирование Arduino
Теперь, когда необходимая нам схема собрана, мы можем начать программирование платы Arduino UNO. Полный текст программы будет приведен в конце статьи, в этом разделе будет дано объяснение некоторых участков кода этой программы.
В каждой программе для Arduino должны обязательно присутствовать две функции – это функции void setup () и void loop (), иногда их называют «абсолютным минимумом», необходимым для написания программы. Все операции, которые мы запишем внутри void setup (), исполнятся только один раз, а операции, которые мы запишем внутри void loop () – будут исполняться снова и снова. Пример этих функций показан в коде ниже – именно в таком виде они создаются когда вы выбираете пункт меню File -> New.
Arduino
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
1 |
voidsetup(){ // put your setup code here, to run once: } voidloop(){ // put your main code here, to run repeatedly: } |
Начнем писать программу в функции setup (). Обычно в этой функции объявляются названия пинов (контактов). В нашей программе нам необходимо объявить всего два контакта: контакт 2 в качестве входного контакта и контакт 3 в качестве выходного контакта. Это можно сделать с помощью следующих строчек кода:
Arduino
pinMode(2,INPUT);
pinMode (3,OUTPUT);
1 |
pinMode(2,INPUT); pinMode(3,OUTPUT); |
Но здесь необходимо внести небольшое изменение в программу – нам желательно чтобы контакт 2, который мы объявили в качестве входного контакта, никогда не был бы в «плавающем» состоянии. Это означает что входной контакт должен быть всегда подсоединен либо к +5 В, либо к земле. А в нашем случае при нажатии кнопки он будет подсоединен к земле, а при отжатой кнопке он будет находиться в плавающем состоянии. Чтобы исключить это нам необходимо задействовать внутренний подтягивающий резистор, который находится внутри микроконтроллера ATmega 328 (то есть снаружи мы этот резистор не видим). Для его задействования необходимо написать соответствующую строчку кода в программе.
С помощью этой строчки кода контакт 2 будет подключаться через подтягивающий резистор к напряжению +5 В всегда когда он не подсоединен к земле. То есть мы должны в одной из написанных нами строчек кода изменить слово INPUT на слово INPUT_PULLUP как показано ниже.
Arduino
pinMode(2,INPUT_PULLUP);
1 | pinMode(2,INPUT_PULLUP); |
Теперь, когда мы закончили с функцией setup (), перейдем к функции loop (). В этой функции мы должны проверять не подсоединен ли контакт 2 к земле (то есть на его входе низкий уровень – LOW) и если он подсоединен в земле, то мы должны зажечь светодиод при помощи подачи на контакт 3 высокого уровня (HIGH). А если контакт 2 не подсоединен к земле (то есть кнопка не нажата), то мы должны держать светодиод в выключенном состоянии при помощи подачи на контакт 3 низкого уровня (LOW). В программе это будет выглядеть следующим образом:
Arduino
if (digitalRead(2) == LOW)
{
digitalWrite(3,HIGH);
}
else
{
digitalWrite(3,LOW);
}
1 |
if(digitalRead(2)==LOW) { digitalWrite(3,HIGH); } else { digitalWrite(3,LOW); } |
В этих строчках кода оператор digitalRead() используется для проверки статуса (состояния) входного контакта. Если контакт подсоединен к земле, то оператор digitalRead() возвратит значение LOW, а если оператор подсоединен к +5 В, то оператор возвратит значение HIGH.
Аналогично, оператор digitalWrite() используется для установки состояния выходного контакта. Если мы установим контакт в состояние HIGH, то на его выходе будет напряжение +5 В, а если мы установим контакт в LOW, то на его выходе будет 0 В.
Таким образом в нашей программе когда мы нажимаем кнопку на контакт 2 будет подана земля и, соответственно, на контакт 3 мы подаем высокий уровень +5 В (HIGH) чтобы зажечь светодиод. Если условие не выполняется – то есть на контакт 2 не подана земля, то мы на контакт 3 подаем низкий уровень 0 В (LOW) чтобы выключить светодиод.
На этом наша программа закончена, теперь загрузим код программы на нашу плату Arduino таким же образом как ранее мы загружали код программы мигания светодиодом.
Операторы if else Arduino примеры
Пример конструкции if else Arduino при подключении фоторезистора:
#define SENSOR A0 #define LED 9 unsigned int value = 0; void setup() { pinMode(LED, OUTPUT); pinMode(SENSOR, INPUT); } void loop() { // Считываем значение с фоторезистора на аналоговом входе A0 value = analogRead(SENSOR); // Если значение value на входе A0 меньше 500, включаем светодиод if (value<500) { digitalWrite(LED, HIGH); } // В противном случае (если value>500), выключаем светодиод if (value>500) { digitalWrite(LED, LOW); } }
Операторы сравнения Arduino
x == y (x равно y)x != y (x не равно y)x < y (x меньше y)x > y (x больше y)x <= y (x меньше или равно y)x >= y (x больше или равно y)
Не путайте знак равенства ‘=‘ и оператор сравнения ‘==‘. Использование знака равенства в условном операторе if может дать другой результат при выполнении программы (скетча). Например, if (y = 100) не тоже самое, что if (y==100). Знак равенства – это оператор присваивания, который устанавливает значение переменной равным 40 , а не проверяет значение переменной на равенство 100.
Логические операторы (not and or) Arduino
Для связи нескольких логических величин используются логические операторы:
! (not) логическое НЕ, отрицание&& (and) логическое И|| (or) логическое ИЛИ
if (water > 100 && value > 100) { digitalWrite(12, HIGH); digitalWrite(10, LOW); } else { digitalWrite(12, LOW); digitalWrite(10, HIGH); }
Порядок условий
Порядок условий играет важную роль при оптимизации кода и попытке сделать скетч более быстрым. Дело в том, что логические выражения/величины проверяются слева направо, и если первая проверка делает выражение неверным, то дальнейшая проверка условий прекращается. В примере, если условие water > 100 является ложным, то следующее выражение value > 100 уже не проверяется.
Язык программирования Ардуино
Когда у вас есть на руках плата микроконтроллера и на компьютере установлена среда разработки, вы можете приступать к написанию своих первых скетчей (прошивок). Для этого необходимо ознакомиться с языком программирования.
Для программирования Arduino используется упрощенная версия языка C++ с предопределенными функциями. Как и в других Cи-подобных языках программирования есть ряд правил написания кода. Вот самые базовые из них:
- После каждой инструкции необходимо ставить знак точки с запятой (;)
- Перед объявлением функции необходимо указать тип данных, возвращаемый функцией или void если функция не возвращает значение.
- Так же необходимо указывать тип данных перед объявлением переменной.
- Комментарии обозначаются: // Строчный и /* блочный */
Подробнее о типах данных, функциях, переменных, операторах и языковых конструкциях вы можете узнать на странице по программированию Arduino. Вам не нужно заучивать и запоминать всю эту информацию. Вы всегда можете зайти в справочник и посмотреть синтаксис той или иной функции.
Все прошивки для Arduino должны содержать минимум 2 функции. Это setup() и loop().
Функция setup
Функция setup() выполняется в самом начале и только 1 раз сразу после включения или перезагрузки вашего устройства. Обычно в этой функции декларируют режимы пинов, открывают необходимые протоколы связи, устанавливают соединения с дополнительными модулями и настраивают подключенные библиотеки. Если для вашей прошивки ничего подобного делать не нужно, то функция все равно должна быть объявлена. Вот стандартный пример функции setup():
Функция loop
Функция loop() выполняется после функции setup(). Loop в переводе с английского значит «петля». Это говорит о том что функция зациклена, то есть будет выполняться снова и снова. Например микроконтроллер ATmega328, который установлен в большинстве плат Arduino, будет выполнять функцию loop около 10 000 раз в секунду (если не используются задержки и сложные вычисления). Благодаря этому у нас есть большие возможности.
Подключение вашей платы Arduino к компьютеру
После того как вы установили Arduino IDE на свой компьютер следующим логичным шагом будет подключение платы Arduino UNO к компьютеру. Чтобы сделать это просто используйте кабель для программирования (синего цвета) и соедините его с платой Arduino и USB портом вашего компьютера.
Синий кабель для программирования может выполнять следующие три функции:
- Он запитывает плату Arduino UNO, то есть чтобы обеспечить выполнение программ на ней необходимо просто запитать ее с помощью USB кабеля.
- Через него программируется микроконтроллер ATmega328, находящийся на плате Arduino UNO. То есть код программы пересылается из компьютера в микроконтроллер именно по этому кабелю.
- Он может функционировать в качестве кабеля для последовательной связи, то есть с его помощью можно передавать данные с Arduino UNO в компьютер – это полезно для целей отладки программы.
После того как вы подадите питание на плату Arduino UNO на ней загорится маленький светодиод – это свидетельствует о том, что на плату подано питание. Также вы можете заметить как мигает другой светодиод – это результат работы программы по управлению миганием светодиода, которая по умолчанию загружена в вашу плату ее производителем.
Поскольку вы подключаете плату Arduino в первый раз к компьютеру необходимо некоторое время чтобы драйвера для нее успешно установились. Чтобы проверить правильно ли все установилось и определилось откройте «Диспетчер устройств (Device manager)» на вашем компьютере.
В диспетчере устройств откройте опцию «Порты» “Ports (COM & LPT)”, кликните на ней и посмотрите правильно ли отображается там ваша плата.
При этом стоит отметить, что не стоит обращать внимание на то, какой номер порта отобразился у вашей платы Arduino – он может, к примеру, выглядеть как CCH450 или что то подобное. Этот номер порта просто определяется производителем платы и больше ни на что не влияет
Если вы не можете в диспетчере устройств найти опцию “Ports (COM & LPT)”, то это означает, что ваша плата не корректно определилась компьютером. В большинстве случает это означает проблему с драйверами – по какой то причине они автоматически не установились для вашей платы. В этом случае вы должны будете вручную установить необходимые драйверы.
В некоторых случаях в указанной опции диспетчера устройств может отобразиться два COM порта для вашей платы и вы не будете знать какой из них правильный. В этой ситуации отключите и снова подключите плату Arduino к компьютеру – какой из COM портов при этом будет появляться и исчезать, значит тот и правильный порт.
Следует помнить о том, что номер COM порта будет изменяться при каждом новом подключении вашей платы к компьютеру – не пугайтесь, в этом нет ничего страшного.