Условно, код в Ардуино IDE можно разделить на четыре части.
Первая часть: Декларативная. В ней подключаются библиотеки, назначаются переменные и директивы.
Вторая часть: Функция setup(), она вызывается, когда стартует скетч. Используется для инициализации переменных, определения режимов работы выводов, запуска используемых библиотек и т.д.
Функция setup запускает только один раз, после каждой подачи питания или сброса платы Arduino.
Третья часть: Функция loop() которая крутится в бесконечном цикле, позволяя программе совершать вычисления и реагировать на них.
Функции setup() и loop() являются обязательными.
Четвертая часть: Вне обязательных функций можем создать свою функцию. И разместить её после функции loop().
Что такое Функция
Функция – часть программы, блок кода, имеющий своё название. Большая программа может строиться из нескольких функций, каждая из которых выполняет свою задачу, поэтому можно назвать функцию подпрограммой.
Использование функций очень сильно упрощает написание и чтение кода, и в большинстве случаев делает его оптимальным по объёму занимаемой памяти.
Функция должна быть описана, и после этого может вызываться.
Функция должна быть описана снаружи других функций!
В общем виде функция имеет следующую структуру:
тип_данных имя_функции (аргументы) {
тело_функции
}
Разбиение на сегменты кода функциями позволяет создавать части кода, которые выполняют определенные задания.
После выполнения происходит возврат в место, откуда была вызвана функция.
Причиной создания функции является необходимость выполнять одинаковое действие несколько раз.
Разделения кода на функции имеет ряд преимуществ:
- Функции позволяют организовать программу. Очень часто помогают заранее составить концепцию программы.
- Функции кодируют одно действие в одном месте программы. Далее необходимо только отладить код функции.
- Функции сокращают шансы на появление ошибки при необходимости изменения кода.
- Функции сокращают текст скетчей и делают его компактным, т.к. некоторые секции используются много раз.
- Функции облегчают использование кода в других программах делая его модульным. В этом случае функции обладают еще одним небольшим преимуществом, делая код программы легким для чтения.
Существуют две обязательные функции в скетчах Arduino setup() и loop().
Другие функции должны создаваться за скобками этих функций.
Пример создания своей функции setupPorts
void setup()
{
setupPorts();
}
void loop()
{
}
void setupPorts()
{
pinMode(7, OUTPUT);
pinMode(8, OUTPUT);
pinMode(10, INPUT);
}
В функции setup (которая как известно вызывается один раз), мы вызываем функцию setupPorts. Которую мы создали ниже.
Слово void перед названием функции setup, ничего не возвращает.
void setup()
{
setupPorts();
}
Чтобы вызвать вашу созданную функцию при необходимости, вы должны поместить на нее ссылку в «void loop()».
Вы вызываете ее, помещая имя своей функции в «void loop()».
void loop()
{
setupPorts();
}
Функция setupPorts() задает режимы работы используемых в скетче пинов.
void setupPorts()
{
pinMode(7, OUTPUT);
pinMode(8, OUTPUT);
pinMode(10, INPUT);
}
Параметры функции
Функция может принимать на вход некоторое количество переменных, называемых параметрами. Параметры пишутся в скобках (int pin).
Пример функций, принимающих параметры:
void lampOn(int pin)
{
digitalWrite(pin, HIGH);
}
void lampOff(int pin)
{
digitalWrite(pin, LOW);
}
Функция lampOn() зажигает светодиод, а функция lampOff() – гасит. Параметром является переменная pin типа int, в значении которой передается номер пина, к которому подключен светодиод.
Конечно, можно было сделать переменную pin глобальной. Но гораздо лучше использовать параметры, это делает код менее подверженным ошибкам и более понятным.
Пример функции c void
void MyFunction()
{ // вот код вашей функции, например:
// Serial.print (“This is my function”);
}
Эта функция ничего не возвращает (void), она только выполняет ряд действий.
Между фигурными скобками указано, что должна делать функция. Напоминаем: чтобы вызвать вашу созданную функцию, вы должны поместить на нее ссылку в «void loop()».
Вы вызываете ее, помещая имя своей функции в «void loop()».
void setup()
{
}
void loop()
{
MyFunction();
}
void MyFunction()
{
Serial.print(“This is my function”);
}
Возвращаемые значения функции
Функция также может возвращать значение. Тип возвращаемого значения указывается перед именем функции.
Если функция не возвращает значение, то перед ней указывается ключевое слово void.
Теперь, когда вы знакомы с типом «void», вы узнаете новый способ использования функций. Но не волнуйтесь! Это не так уж и сложно. В этом примере мы вычисляем значение с двумя переменными, которые уже объявлены.
int x = 51;
int y = 7;
int outcome;
void setup()
{
}
void loop()
{
outcome = MultyplyNumbers();
}
int MultyplyNumbers()
{
return x * y;
}
Вначале мы объявляем три переменные типа int. Первые две переменные называем x и y. И задаём значение 51 и 7.
В третьей переменной назначаем только тип переменной и название int outcome.
int x = 51;
int y = 7;
int outcome;
В функции void loop мы назначаем переменной outcome возвращаемое значение с функции MultyplyNumbers.
void loop()
{
outcome = MultyplyNumbers();
}
Ну и созданная функция под названием MultyplyNumbers
int MultyplyNumbers()
{
return x * y;
}
Итак, перечислим, чем это отличается от void-метода: вместо void здесь int, в конце метода есть “return”. Теперь возникает вопрос: что все это значит?
Так, “int” означает, что метод должен возвращать целое число, подробнее о “return” позже.
По сути, это означает, что функция будет целым числом. «результат» является целым числом, поэтому наша функция также должна «быть» целым числом.
Оператор return – это то, что у функции должен быть конечный результат, в данном случае 357.
Аргументы функции
В следующем примере мы умножили два числа, но что, если бы у нас было много чисел, которые нужно умножить, что ж, для этого есть решение: аргументы. Аргумент помещается в фигурные скобки в имени вашего метода функции (int x , int y).
int a = 1;
int b = 7;
int c = 5;
int d = 8;
int outcome1;
int outcome2;
int outcome3;
void setup()
{
}
void loop()
{
outcome1 = MultyplyNumbers(a , b); //outcome1 = 7
outcome2 = MultyplyNumbers(c , d); //outcome2 = 40
outcome3 = MultyplyNumbers(outcome1 , outcome2); //outcome3 = 280
}
int MultyplyNumbers(int x , int y)
{
return x * y;
}
Теперь мы добавили аргументы, поэтому нашу функцию можно использовать с разными числами. В методе x и y являются аргументами, они могут быть заполнены при вызове метода.
Разберём код.
Вначале создаём переменные. Все переменные типа int. Часть переменным сразу назначаем значения (для переменных с именами a,b,c и d). Три переменные не имеют назначенных значений (outcome1, outcome2 и outcome3).
int a = 1;
int b = 7;
int c = 5;
int d = 8;
int outcome1;
int outcome2;
int outcome3;
Функция под названием MultyplyNumbers должна возвращать int. Аргументы функции (int x , int y) имеют аргументы x и y с значением int.
В теле функции оператор return завершает выполнение функции и возвращает данные. Оператор return возвращает результат умножения x на y.
int MultyplyNumbers(int x , int y)
{
return x * y;
}
В функции void loop() при вызове функции под названием MultyplyNumbers мы меняем аргументы этой функции MultyplyNumbers(a , b) на переменные a и b.
void loop()
{
outcome1 = MultyplyNumbers(a , b); //outcome1 = 7
outcome2 = MultyplyNumbers(c , d); //outcome2 = 40
outcome3 = MultyplyNumbers(outcome1 , outcome2); //outcome3 = 280
}
То есть вместо x и y подменяем значениями переменных a и b MultyplyNumbers(int x , int y).
В самой функции производим умножение return x * y с уже полученными значениями от переменных a и b.
И результат заносим в переменные outcome1, outcome2 и outcome3.
Пример простейшей функции
Для начала возьмем очень простую задачу. Допустим, что в вашей программе часто, приходится использовать один и тот же фрагмент кода – например, зажигать несколько светодиодов и подавать сигнал в зависимости от состояния какого-либо пина.
Можно пойти по сложному пути и всякий раз писать лишние строки в те места кода, где это понадобится, а можно всего один раз написать функцию и затем вызывать ее в нужных местах – что мы сейчас и сделаем.
Простейшая функция выглядит следующим образом:
Она записывается вне основной функции loop и имеет такую же конструкцию.
Помните, я говорил, что функция может, как принимать какую-либо информацию, так и возвращать результат своей работы.
Вспомним что, ключевое слово void означает, что наша функция не будет возвращать никаких данных в результате своей работы, поскольку в ней мы не будем производить какие-либо вычисления или считывания и нам не нужна обратная связь с этой функцией. Так как функция не только не возвращает результат, но и не принимает информацию, то ее скобки мы оставляем пустыми, что эквивалентно тому, если бы мы написали в них void.
void function_name()
Итак, заготовка простейшей функции готова и в ней впишем необходимый нам код.
Давайте посмотрим, как же вызывать эту функцию в основном цикле программы.
На самом деле, все так же просто – вам нужно указать имя этой функции и обязательно не забыть про круглые скобки и точку с запятой.
Так как функция ничего не принимает, то мы так же оставляем скобки пустыми.
Теперь, если представить что мы программа, то алгоритм работы будет такой: сначала, как и обычно, происходит запуск микроконтроллера и его настройка в функции setup, затем мы переходим в функцию loop и затем, попадая на строчку вызова нашей функции, переходим в эту функцию.
В ней так же строка за строкой выполняется каждая команда и, по окончанию функции, осуществляется выход из нее обратно на то место, откуда был осуществлен вызов.
Мы можем объявить еще одну функцию и так же сделать ссылку на нее – как видите, основная программа теперь не загромождена лишними строками кода, а мы видим лишь названия функций, прочитав которые понимаем, что происходит в этой функции.
Названия функциям желательно давать исходя из того, какую задачу эта функция выполняет. Это поможет вам в будущем, да и другому человеку, читающему ваш код, понять, что делает эта функция без необходимости разбираться в ее коде.
Итак, с простейшими функциями мы разобрались. Давайте немного усложним нашу задачу.
Допустим, что нам необходимо сделать считывание не всегда одного и того же пина, как мы это делали раньше, а каждый раз разного.
В этом случае нам и пригодится способность функций принимать какие-то входные данные для работы.
Что бы использовать эту возможность, теперь в пустых скобках мы должны указать тип и имя переменной, которая будет отвечать за номер пина.
Назовем её, например, numberPin и дадим ей тип byte, который может нести в себе число от 0 до 255.
Теперь, обращаясь к функции, мы так же обязаны указать в круглых скобках число от 0 до 255 и если этого не сделать, то компилятор выдаст вам ошибку, так как функция теперь обязана принимать какое-либо значение. В нашем случае мы будем указывать номер пина, с которым хотим работать, поставим, например, цифру 4.
Чтобы код, написанный внутри нашей функции, стал таким же универсальным, на том месте, где до этого мы писали конкретный номер пина для считывания, теперь необходимо указать переменную nomberPin, которая подставит сюда такое число, какое мы передали при обращении к функции – в нашем случае это цифра 4, то есть 4-й пин.
Давайте еще раз повторим, что теперь у нас будет происходить при таком объявлении функции.
Программа, попадая на строку вызова функции, запоминает число, указанное в скобках, и передает это число функции, где значение этого числа присваивается указанной нами переменной numberPin.
Теперь, как и в обычном коде, на всех местах, где встретится эта переменная будет подставляться наше число, которое мы передали функции.
Таким образом, в основном коде программы мы можем вызывать одну и ту же функцию несколько раз, при этом указывая необходимый нам пин в ее круглых скобках.
Если мы хотим передать нашей функции не одно, а несколько значений, то мы просто указываем тип и имя переменной через запятую, и затем уже используем эти переменные в коде.
При вызове функции нам так же необходимо указывать значения через запятую, так как теперь функция будет ждать от нас столько значений, сколько указано в её круглых скобках.
Мы можем передавать в нее всё, что угодно, будь то показания с датчиков или текстовые сообщения, которые необходимо вывести на дисплей. Все зависит от кода, который вы укажете в теле функции.
Кстати, находясь внутри самой функции, мы можем вызывать еще сколько угодно функций, если это необходимо для упрощения и разгрузки кода.
Еще одним, очень важным плюсом функции является то, что если нам нужно изменить какой-либо фрагмент кода, то, в случае функции это нужно сделать всего лишь однажды, изменив что-то внутри нее.
Если бы мы копировали один и тот же код без использования функций, то нам бы пришлось вносить поправки во все места, где встречается этот код в программе.
Итак, мы научились сообщать функции какие-либо данные для работы. Давайте посмотрим, каким образом осуществляется возврат результата вычисления функцией.
При определении функции мы указали, что возвращаемый ею тип является void, то есть она ничего не возвращает. Изменим void, например, на тип byte и теперь в функции мы обязаны указать возврат какого-либо результата ее вычисления. Для того, чтобы функция вернула какое-то значение, существует оператор return, и запись, например, return 5 означает, что наша программа, дойдя до этого момента, сделает выход из функции, при этом запомнив результат равный 5.
Вместо конкретного числа вы можете подставлять значения различных переменных, которые использовались в функции, например, это может быть считанное состояние цифрового или аналогового пина микроконтроллера. Что бы в дальнейшем работать с этим значением, мы должны присвоить его какой-либо переменной в коде, а саму запись функции вы можете представлять себе как преображение в то число, которое она возвращает. Например, можно объявить переменную result, то есть результат, и написать, что result присвоить значение нашей функции.
Тогда программа, дойдя до этого момента, будет поступать так – она переходит в указанную нами функцию, выполняется там, доходит до оператора return и запоминает значение, которое мы указали.
Это значение она записывает по адресу переменной result. Теперь result равен тому числу, которое возвращает наша функция.
void setup()
{
}
void loop() {
/****Какая-либо программа*******/
// сохраняем в переменную resultзначение, переданное функцией
bool result = check_button(4);
// Продолжение программы
}
byte check_button(byte nomberPin)
{
bool button_On; // переменная используется только внутри функции
if(digitalRead(nomberPin) == 1)
{
button_On =1;
}
else
{
button_On =0;
}
return button_On; // возвращаем результат 0 или 1
}
Ещё один пример
Соберем светофор. В начале объявляем константы и назначаем пины 13, 12, и 11 с соответствующим цветовым названием.
#define RED 13
#define YELLOW 12
#define GREEN 11
В функции void setup() выполняем цикл. Начиная с GREEN (11 пин). Добавляем на единицу до трёх. Переводим пины на выход (OUTPUT).
for (int i = 0; i < 3; ++i)
pinMode(i + GREEN, OUTPUT);
В функции void loop вызываем созданные функции
void loop()
{
//Вызов функций.
riy_light();
green();
yellow();
}
Созданная функция riy_light создаёт две переменные main_delay и mini_delay и определяет интервал периода. А также создаёт цифровой пин с включением (HIGH).
void riy_light()
{
int main_delay = 5000;
int mini_delay = 1000;
digitalWrite(RED, HIGH);
delay(main_delay);
digitalWrite(YELLOW, HIGH);
delay(mini_delay);
}
Полный код:
// Основная часть
#define RED 13
#define YELLOW 12
#define GREEN 11
void setup()
{
// Объявление пинов.
for (int i = 0; i < 3; ++i)
pinMode(i + GREEN, OUTPUT);
}
void loop()
{
//Вызов функций.
riy_light();
green();
yellow();
}
// Началось описание функций.
void riy_light()
{
int main_delay = 5000;
int mini_delay = 1000;
digitalWrite(RED, HIGH);
delay(main_delay);
digitalWrite(YELLOW, HIGH);
delay(mini_delay);
}
void green()
{
int main_delay = 5000;
int blink_delay = 800;
digitalWrite(RED, LOW);
digitalWrite(YELLOW, LOW);
digitalWrite(GREEN, HIGH);
delay(main_delay);
digitalWrite(GREEN, LOW);
for(int i = 0; i < 3; i = i+1)
{
delay(blink_delay);
digitalWrite(GREEN, HIGH);
delay(blink_delay);
digitalWrite(GREEN, LOW);
}
}
void yellow()
{
int mini_delay = 1000;
digitalWrite(YELLOW, HIGH);
delay(mini_delay);
digitalWrite(YELLOW, LOW);
delay(mini_delay);
}
Принудительное включение функции
Иногда мы хотим, чтобы наш код запускался только тогда, когда пользователь выполняет какое-либо действие, например, нажимает кнопку, чтобы получить какое-то значение датчика.
В материале вы узнаете, как приостановить программу и возобновить ее только одним нажатием кнопки без использования функции задержки, поскольку мы не можем использовать функцию задержки, если программа должна возобновить работу после взаимодействия с пользователем.
Схема подключения показана на следующем рисунке, она основана на Arduino Nano, двух кнопочных переключателей, ЖК-дисплея и нескольких других пассивных электронных компонентов, таких как резистор. ЖК используется здесь только для демонстрации.
Программный код написан на языке программирования Arduino и скомпилирован с использованием Arduino IDE. В скетче мы будем использовать созданные функции CheckButton().
Функция CheckButton1() проверяет, нажата кнопка 1 или нет. При нажатии кнопки значение становится высоким для CheckButtonPress1, и программа возвращается к функции цикла.
// Подключаем библиотеку для работы с дисплеем.
#include <LiquidCrystal.h>
// Создаём переменные с типом данных int и определяем пин.
int Button1 = A0;
int Button2 = A1;
// Определяем пины для подключения дисплея.
const int rs = 7, en = 6, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
void setup() {
// Определяем тип дисплея LCD 1602.
lcd.begin(16, 2);
// Определяем пины на вход (кнопки).
pinMode(Button1, INPUT);
pinMode(Button2, INPUT);
}
void loop() {
// Инициализируем функции CheckButton1 и CheckButton2.
CheckButton1();
CheckButton2();
// Очищаем курсор дисплея и выставляем его на начало.
lcd.clear();
lcd.setCursor(0, 0);
// Пишем текст на дисплее.
lcd.print(“***Program is***”);
// Переводим курсор на строку ниже на дисплее.
lcd.setCursor(0, 1);
// Пишем текст на дисплее.
lcd.print(“***Resumed***”);
// Пауза 5 секунд.
delay(5000);
}
// Созданные функции.
void CheckButton1(){
// Переменная CheckButtonPress1 типа int. С значением 0.
int CheckButtonPress1 = 0;
/* Цикл while (англ. “пока”), он же “цикл с предусловием”, выполняется до тех пор, пока верно указанное условие. Если условие изначально неверно, цикл будет пропущен, не сделает ни одной итерации. Объявляется очень просто: ключевое слово while, далее условие в скобках, и вот уже тело цикла: */
while(1){
/* Если на кнопке Button1 появится 1, то выполняется цикл
while */
CheckButtonPress1 = digitalRead(Button1);
//lcd.clear();
lcd.setCursor(0, 0);
lcd.print(“Press 1st Button”);
lcd.setCursor(0, 1);
lcd.print(” to continue”);
if(CheckButtonPress1 == HIGH){
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(“1st But. Pressed”);
delay(2000);
return;
}
}
void CheckButton2(){
int CheckButtonPress2 = 0;
while(1){
CheckButtonPress2 = digitalRead(Button2);
//lcd.clear();
lcd.setCursor(0, 0);
lcd.print(“Press 2nd Button”);
lcd.setCursor(0, 1);
lcd.print(” to continue”);
if(CheckButtonPress2 == HIGH){
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(“2nd But. Pressed”);
delay(2000);
return;
}
}
Что такое многозадачность
Многозадачность означает выполнение более одной задачи/программы в одно и то же время. В настоящее время практически все операционные системы поддерживают многозадачность. К примеру, ваш персональный компьютер может в одно и то же время принимать электронную почту, открывать сайты в веб-браузере, выполнять какие-нибудь расчеты и т.п. Пользователю кажется что все эти процессы выполняются одновременно, однако у операционной системы на этот счет несколько иной взгляд. Давайте рассмотрим более подробно как выглядит многозадачность с точки зрения операционной системы.
Как показано на представленном рисунке, микропроцессор делит общее время на 3 равные части и в каждой части времени выполняет отдельную задачу. Таким образом реализуется концепция многозадачности в большинстве современных компьютерных систем.
Аналогичный подход мы применим и в нашем проекте для Arduino, однако распределение времени на разные задачи будет немного отличаться. Поскольку плата Arduino работает на значительно меньшей частоте чем современные персональные компьютеры и объем оперативной памяти у нее значительно меньше, то и время, выделяемое на каждую задачу, будет различным.
Но прежде чем мы перейдем к реализации многозадачности в Arduino давайте рассмотрим почему не стоит использовать функцию delay() абсолютно в каждом проекте на Arduino.
Почему стоит воздержаться от использования функции delay() в Arduino
В документации на Arduino указано, что в этой платформе существует две функции для организации задержек – delay() и delayMicroseconds().
По принципу действия обе эти функции абсолютно идентичны, только в функции delay() значение задержки указывается в миллисекундах, а в функции delayMicroseconds() – в микросекундах.
Обе эти функции останавливают работу платы Arduino на заданное время. К примеру, если мы сделаем задержку на 10 секунд, то плата Arduino не сможет выполнить никакую другую команду до тех пор, пока эти 10 секунд не истекут. Естественно, подобный подход сильно мешает рациональному использованию вычислительных ресурсов микроконтроллера.
Рассмотрим, к примеру, две кнопки. Допустим, нам нужно переключать состояние двух светодиодов с помощью этих двух кнопок. Например, при нажатии первой кнопки первый светодиод должен включаться на 2 секунды, а при нажатии второй кнопки второй светодиод должен включаться на 4 секунды. Если в данном случае мы будем использовать функцию delay(), то при нажатии первой кнопки программа остановится на 2 секунды и даже если в это время нажать вторую кнопку, то программа просто не сможет обработать нажатие этой кнопки.
В официальной документации на Arduino вы можете прочитать замечания и предупреждения по использованию функции delay().
Если вы найдете в себе силы их прочитать, то вы окончательно убедитесь в том, что использование функции delay() – далеко не лучший способ организации задержек в программах для Arduino.
Почему стоит использовать функцию millis()
Для преодоления проблем, описанных в предыдущем пункте данной статьи, вызванных использованием функции delay(), можно использовать функцию millis(). Использование данной функции позволит более гибко использовать вычислительные возможности платы Arduino, не прибегая к полному останову программы как в случае использования функции delay(). millis() – это функция, которая возвращает количество миллисекунд, прошедших с момента начала выполнения платой Arduino текущей программы. Никакого останова программы во время использования этой функции не происходит. Счетчик этой функции будет переполняться (то есть сбрасываться в ноль) примерно через 50 дней функционирования программы.
Также как и в случае с функцией delayMicroseconds(), в Arduino имеется аналогичная функция и для функции millis() – это функция micros(). Она работает так же как и millis(), но счет ведет в микросекундах.
Ее счетчик переполняется примерно через 70 минут функционирования программы.
Чтобы использовать функцию millis() для организации временных задержек в программе вам необходимо сохранять в переменной момент времени когда начало выполняться какое либо действие и затем непрерывно проверять сколько времени истекло с этого момента времени.
Для решения стоящей перед нами задачи вначале сохраним текущее время в соответствующей переменной:
unsigned long currentMillis = millis();
Но кроме этой переменной нам будут необходимы еще две переменные чтобы определить что заданный промежуток времени закончился.
Мы сохраняем текущее значение времени в переменной currentMillis, но кроме этого нам необходимо знать когда начался заданный временный промежуток и какой временной промежуток (задержка) нам необходим.
Для хранения этих значений объявим еще две переменные: previousMillis и period.
В переменной period будет храниться значение требуемого (заданного) временного интервала,
а в переменной previosMillis – последнее время, когда произошло заданное событие.
unsigned long previousMillis;
unsigned long period = 1000;
Для упрощения понимания этого способа организации временных задержек рассмотрим простой пример с миганием светодиода.
Значение переменной period = 1000 будет обозначать, что светодиод будет мигать с задержкой 1 секунда (1000 миллисекунд).
const int ledPin = 4; // контакт, к которому подключен светодиод
int ledState = LOW; // переменная, показывающая состояние светодиода
unsigned long previousMillis = 0; //в этой переменной будем хранить время, когда светодиод последний раз мигнул
const long period = 1000; //интервал времени в миллисекундах, через который светодиоду нужно мигать
void setup() {
pinMode(ledPin, OUTPUT); // ledpin – на вывод данных
}
void loop() {
unsigned long currentMillis = millis(); // сохраняем текущее время
if (currentMillis – previousMillis >= period) { // проверяем прошли ли 1000ms
previousMillis = currentMillis; // сохраняем последнее время когда мигнул светодиод
if (ledState == LOW) { //если светодиод выключен, то включаем его, и наоборот
ledState = HIGH;
} else {
ledState = LOW;
}
digitalWrite(ledPin, ledState);//устанавливаем светодиод в состояние, определяемое переменной ledState
}
}
В рассмотренной программе выражение <if (currentMillis – previousMillis >= period)> проверяет прошел ли заданный промежуток времени (1000 мс).
Если 1000 мс прошли, светодиод мигнет и затем снова возвратится в исходное состояние. И так будет продолжаться снова и снова пока выполняется программа.
Таким образом, мы смогли организовать временную задержку в программе с помощью использования функции millis() вместо использования функции delay(). При этом не происходит останова выполнения программы.
В этой статье мы рассмотрим реализацию многозадачности в плате Arduino на примере мигания двумя светодиодами. В нашем проекте два светодиода будут мигать с различными временными задержками, а состояние третьего светодиода будет переключаться в зависимости от нажатия кнопки.
Для реализации этой задачи мы будем использовать прерывания. Внешние прерывания в плате Arduino Uno можно задействовать на ее контактах 2 и 3. Более подробно об использовании прерываний в платах Arduino вы можете прочитать в данной статье.
millis()
Описание
Возвращает количество миллисекунд, прошедших с момента старта программы Ардуино. Возвращаемое число переполнится (сбросится в 0) спустя приблизительно 50 дней.
Параметры
Нет
Возвращаемые значения
Количество миллисекунд, прошедших с момента старта программы (unsigned long)
Пример
unsigned long time; void setup() { Serial.begin(9600); } void loop() { Serial.print("Time: "); time = millis(); //выводим время с момента старта программы Serial.println(time); // ждем 1 секунду, чтобы не отправлять большой массив данных delay(1000); } |
Совет:
Помните, что значение, возвращаемое функцией millis(), имеет тип unsigned long.
При попытке выполнения математических операций между этим значением и значениями другого типа (например, int) будет сгенерирована ошибка.
Важность применения принципа многозадачности
До этого момента для установки длительности какого-либо действия (время вращения мотора или время его паузы, время звучания зуммера и т.д.) мы использовали функцию delay().
Однако, мы сразу сказали, что её крайне нежелательно использовать в настоящих проектах для управления модулями.
Дело в том, что время, указанное в этой функции, контроллер расходует впустую — он не может выполнять никаких действий, он просто ждёт, пока это время закончится. А ведь он мог бы считывать показания датчиков, регулировать скорость двигателей и т.д. Поэтому сейчас мы рассмотрим принцип, позволяющий не останавливать работу контроллера.
Приведённый ниже скетч позволяет одновременно мигать светодиодом на плате и воспроизводить звук полицейской сирены с помощью зуммера. Зуммер, как и в предыдущих уроках, остаётся быть подключенным к 6 пину.
- long timeLed; // Переменная, хранящая время включения светодиода
- long zumStart; // Переменная, хранящая время последнего изменения частоты зуммера (для создания эффекта сирены)
- bool ledState = 0; // Состояние светодиода: 0 – выключен, 1 – включен
- int i = 0; // Начало отсчёта для увеличения частоты зуммера
- void setup(){
- pinMode (6, OUTPUT); // 6 пин – выход. К нему подключен зуммер
- pinMode (13, OUTPUT); // 13 пин – выход. К нему подключен светодиод (на плате)
- }
- void loop(){
- if (timeLed + 150 < millis()){ // Если с момента последнего изменения состояния прошло более 150 мс
- ledState = !ledState; // Инвертируем (меняем на противоположное) состояние светодиода
- timeLed = millis(); // Обновляем время смены состояния. В следующем цикле отсчёт будет идти уже от него
- }
- digitalWrite (13, ledState); // Записываем состояние (зажигаем или гасим светодиод).
- if (zumStart + 15 < millis()){ // Если с момента последнего изменения тона прошло больше 15 миллисекунд
- zumStart = millis(); // Обновляем время смены тона
- i++; // Увеличиваем значение для пересчёта тона
- if (i >= 255) i = 0; // Если произошло переполнение, сбрасываем в 0
- int res = (sin((2*PI*i)/128)+1)*45+160; // Формула для расчёта тона через синус
- tone(6, res*3); // Генерируем звуковой сигнал на 6 выводе
- }
- }
Как реализуется многозадачность
Функция millis() — функция, возвращающая значение времени в миллисекундах, прошедшее с момента запуска программы. Если задать переменной значение времени, полученное от этой функции, а потом сравнивать его с текущим временем, то можно узнать, сколько времени прошло с момента задания этого времени в переменную.
Например, инструкция timeLed = millis() (13 строка) записывает в переменную timeLed системное время момента, когда изменилось состояние светодиода (12 строка).
Далее, ориентируясь на текущее системное время, мы можем сделать вывод о том, сколько времени прошло (11 строка).
Если это время больше заданного значения ( в нашем случае это 150 мс), то состояние светодиода меняется.
Таким образом, можно организовать параллельное выполнение множества процессов. Посмотрите — одновременно со светодиодом мы управляем ещё и зуммером (17-23 строки).
При использовании функции delay() сделать это было бы невозможно.
Также, как Вы уже заметили, мы используем условие: if ( УСЛОВИЕ ) { ДЕЙСТВИЕ }.
ДЕЙСТВИЕ выполнится в том случае, если будет истинно УСЛОВИЕ, записанное в скобках.
Схема проекта
Схема проекта с демонстрацией многозадачности в Arduino представлена на следующем рисунке.
Объяснение программы для Arduino
Полный текст программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.
Программирование мультизадачности для платы Arduino будет основано на использовано функции millis() – подробно эти вопросы рассмотрены ранее в статье. Рекомендуем потренироваться с представленным выше в статье примером с мигающим светодиодом чтобы лучше понять принцип использования функции millis() в программе для организации задержек.
В этой программе мы будем использовать механизм прерываний – при нажатии кнопки в схеме будет срабатывать прерывание.
В начале программы необходимо объявить используемые контакты.
int led1 = 6;
int led2 = 7;
int toggleLed = 5;
int pushButton = 2;
Также объявим две переменные для хранения состояний светодиодов.
int ledState1 = LOW;
int ledState2 = LOW;
Как уже рассматривалось ранее в статье, для организации временных задержек с помощью функции millis() мы будем использовать дополнительные переменные для хранения момента времени начала события и требуемой продолжительности временного интервала.
Первый светодиод будет мигать с использованием задержки в 1 секунду,
а второй светодиод – с использованием задержки в 200 мс.
unsigned long previousMillis1 = 0;
const long period1 = 1000;
unsigned long previousMillis2 = 0;
const long period2 = 200;
Также мы будем использовать еще одну функцию millis() для формирования задержки, необходимой для устранения эффекта дребезга контактов (debounce delay) – чтобы предотвратить “множественные нажатия” при одиночном срабатывании кнопки.
Принцип организации этой задержки будет аналогичен рассмотренному подходу. Для формирования этой задержки также заведем еще две переменные.
int debouncePeriod = 20;
int debounceMillis = 0;
Также еще три переменные будут использованы для хранения статуса (состояния) кнопки – прерывание, переключение состояния светодиода и последнее состояние кнопки.
bool buttonPushed = false;
int ledChange = LOW;
int lastState = HIGH;
Далее зададим режимы работы используемых контактов – на ввод или вывод данных.
pinMode(led1, OUTPUT);
pinMode(led2, OUTPUT);
pinMode(toggleLed, OUTPUT);
pinMode(pushButton, INPUT);
После этого запишем правила обработки прерывания и определим контакт, с которого будет обрабатываться прерывание.
При возникновении прерывания будет вызываться функция pushButton_ISR (функция обработки прерывания). Контакт, на котором будет обрабатываться прерывание, будет передаваться в функцию attachInterrupt с помощью функции digitalPinToInterrupt(pin_number) – можно конечно и напрямую просто указывать контакт, но использование функции digitalPinToInterrupt(pin_number) – в данном случае это более рекомендуемый подход.
attachInterrupt(digitalPinToInterrupt(pushButton), pushButton_ISR, CHANGE);
Функция обработки прерывания в нашем случае будет очень простой – в ней нам будет необходимо всего лишь изменять флаг buttonPushed. Стоит отметить, что функция обработки прерывания должна быть по возможности как можно более короткой.
void pushButton_ISR()
{
buttonPushed = true;
}
Бесконечный цикл в нашей программе (Loop) начнется с сохранения значения функции millis() в переменной currentMillis.
unsigned long currentMillis = millis();
В рассматриваемом нами проекте многозадачности для Arduino будет три функции: мигание первого светодиода с частотой в 1 секунду, мигание второго светодиода с частотой 200 мс и, если нажата, кнопка, то включение/выключение третьего светодиода (toggleLed). Для каждой из этих задач будет своя часть кода в программе.
Первая задача – это переключение состояния первого светодиода каждую секунду. Эта секунда будет измеряться с помощью функции millis() – этот подход мы уже подробно рассмотрели ранее в статье.
if (currentMillis – previousMillis1 >= period1) {
previousMillis1 = currentMillis;
if (ledState1 == LOW) {
ledState1 = HIGH;
} else {
ledState1 = LOW;
}
digitalWrite(led1, ledState1);
}
Аналогичный подход мы будем использовать для переключения состояния второго светодиода каждые 200 мс.
if (currentMillis – previousMillis2 >= period2) {
previousMillis2 = currentMillis;
if (ledState2 == LOW) {
ledState2 = HIGH;
} else {
ledState2 = LOW;
}
digitalWrite(led2, ledState2);
}
И, наконец, для выполнения третьей задачи будет проверяться состояние флага buttonPushed и после организации задержки в 20 мс, необходимой для устранения эффекта дребезга контактов кнопки (debounce delay) будет переключаться состояние светодиода в зависимости от нажатия кнопки.
Нажатия кнопки будут обрабатываться с помощью механизма прерывания.
if (buttonPushed = true) // check if ISR is called
{
if ((currentMillis – debounceMillis) > debouncePeriod && buttonPushed) // generate 20ms debounce delay to avoid multiple presses
{
debounceMillis = currentMillis; // save the last debounce delay time
if (digitalRead(pushButton) == LOW && lastState == HIGH) // change the led after push button is pressed
{
ledChange = ! ledChange;
digitalWrite(toggleLed, ledChange);
lastState = LOW;
}
else if (digitalRead(pushButton) == HIGH && lastState == LOW)
{
lastState = HIGH;
}
buttonPushed = false;
}
}
На этом реализация нашего проекта многозадачности для Arduino заканчивается. Надеемся, после понимания данной статьи вы без проблем сможете применить описанный подход к многозадачности для управления сервомоторами, шаговыми двигателями, датчиками и другими периферийными устройствами.
Исходный код программы (скетча)
/* Multitasking using Arduino millis() function
Author : CircuitDigest (circuitdigest.com)
*/
int led1 = 6; // led1 connected at pin 6 (первый светодиод)
int led2 = 7; // led2 connected at pin 7 (второй светодиод)
int toggleLed = 5; // светодиод, состояние которого будет управляться с помощью нажатия кнопки. Подключен к контакту 5
int pushButton = 2; // кнопка подключена к контакту 2, который в нашем проекте также служит контактом, на котором производится обработка прерывания
int ledState1 = LOW; // переменные для управления состоянием 1-го и 2-го светодиодов
int ledState2 = LOW;
unsigned long previousMillis1 = 0; //переменная для хранения момента времени, в который последний раз производилось переключение состояния 1-го светодиода
const long period1 = 1000; // период в мс, с которым должен мигать 1-й светодиод
unsigned long previousMillis2 = 0; ////переменная для хранения момента времени, в который последний раз производилось переключение состояния 2-го светодиода
const long period2 = 200; // период в мс, с которым должен мигать 2-й светодиод
int debouncePeriod = 20; // debounce delay of 20ms (задержка для устранения дребезга контактов)
int debounceMillis = 0; // similar to previousMillis
bool buttonPushed = false; // interrupt routine button status
int ledChange = LOW; // переменная для отслеживания последнего состояния 3-го светодиода
int lastState = HIGH; // переменная для хранения последнего состояния кнопки
void setup() {
pinMode(led1, OUTPUT); // define pins as input or output
pinMode(led2, OUTPUT);
pinMode(toggleLed, OUTPUT);
pinMode(pushButton, INPUT);
attachInterrupt(digitalPinToInterrupt(pushButton), pushButton_ISR, CHANGE); // обработка прерывания будет производиться на контакте 2
}
void pushButton_ISR()
{
buttonPushed = true; // функция обработки прерывания должна быть по возможности максимально короткой
}
void loop() {
unsigned long currentMillis = millis(); // сохраняем текущее время
if (currentMillis – previousMillis1 >= period1) { // проверяем прошло ли 1000 мс
previousMillis1 = currentMillis; // сохраните последний раз, когда вы мигали светодиодом
if (ledState1 == LOW) { // если светодиод выключен, включите его и наоборот
ledState1 = HIGH; //изменяем состояние светодиода для следующей итерации
}
else {
ledState1 = LOW;
}
digitalWrite(led1, ledState1); // устанавливаем 1-й светодиод в состояние, определяемое переменной ledState1
}
if (currentMillis – previousMillis2 >= period2) { // проверяем прошло ли 200 мс
previousMillis2 = currentMillis; // сохраните последний раз, когда вы мигали светодиодом
if (ledState2 == LOW) { // если светодиод выключен, включите его и наоборот
ledState2 = HIGH;
} else {
ledState2 = LOW;
}
digitalWrite(led2, ledState2); // устанавливаем 2-й светодиод в состояние, определяемое переменной ledState2
}
if (buttonPushed = true) // проверяем вызывалась ли функция обработки прерывания (ISR)
{
if ((currentMillis – debounceMillis) > debouncePeriod && buttonPushed) // организация задержки в 20 мс для устранения эффекта дребезга контактов
{
debounceMillis = currentMillis; // сохранить время задержки последнего сброса
if (digitalRead(pushButton) == LOW && lastState == HIGH) // изменяем состояние светодиода если кнопка была нажата
{
ledChange = ! ledChange;
digitalWrite(toggleLed, ledChange);
lastState = LOW;
}
else if (digitalRead(pushButton) == HIGH && lastState == LOW)
{
lastState = HIGH;
}
buttonPushed = false;
}
}
}
Ссылка на статью
Ещё один пример:
Используем в Ардуино millis вместо delay
В последнем примере вывод счетчика на мониторе порта прерывается на время задержки в программе delay(1000);
— в этом заключается главное отличие этих функций.
При подключении датчиков к плате необходимо получать данные от них постоянно, поэтому команду delay заменяют на millis.
Как это сделать в скетче для Arduino IDE с мигающим светодиодом — продемонстрируем в следующем примере.
// пример замены delay на millis в программе unsigned long timer; // переменная времени timer boolean ledState = 0; // переменная состояния светодиода void setup() { pinMode(13, OUTPUT); Serial.begin(9600); // запускаем монитор порта timer = millis(); // запускаем отсчет времени впеременную timer } void loop() { if (millis() - timer > 1000) // Цикл Если. проверяем сколько прошло миллисекунд { ledState=!ledState; // меняем состояние светодиода на противоположное digitalWrite(13, ledState); timer = millis(); } // выводим количество миллисекунд прошедших с момента начала программы Serial.print("Time: "); Serial.println(timer); }
Заключение. Команда millis Arduino не останавливает выполнения программы, а начинает отсчет времени с начала запуска счетчика в миллисекундах. В отличии от этого, delay и delayMicroseconds останавливают выполнение программы на заданное количество миллисекунд или микросекунд соответственно.
Применение определенной задержки в скетче Ардуино микроконтроллеров зависит от поставленной задачи.
Еще пример:
Как ещё можно реализовать задержку, чтобы в это время, можно было делать другой код?
Самое лучшее решение – запустить условие проверки с некой переменной, которая постоянно увеличивает значение.
В ардуино уже реализована такая переменная millis() , которая увеличивается каждую миллисекунду на 1.
Её максимальное значение равно 4294967295мс, что приблизительно составляет почти 50 суток.
Пока не будем думать, о том, что она может переполнится (в этом ничего страшного нет), и посмотрим, как мы можем использовать эту переменную.
unsigned longcurrentTime; // Переменная текущего времени
if(millis() - currentTime > 1000) // Если время контроллера millis, больше переменной на 1000, то запускаем условие
if{currentTime = millis(); // Приравниваем переменную текущего времени к времени контроллера, чтобы через 1000мс опять сработал наш цикл. //здесь любое действие.}
Всё просто! Мы используем вспомогательную переменную текущего времени currentTime,
На старте её значение равно = 0, как и millis() = 0.
Но millis() у нас постоянно увеличивается, а currentTime – нет.
Поэтому через 1000мс, сработает условие
if (millis() – currentTime > 1000),
т.е. мы создали задержку в 1с, но код при этом выполнялся циклично в loop, и мы могли успеть обработать много периферии.
Потом мы просто обновляем нашу переменную currentTime, и она снова ждёт сравнения с millis(), когда она вырастет на 1000мс.
Попробуем сделать вышестоящую задачу, мигание двумя светодиодами с разной задержкой. но при этом вставим в loop какой нибудь код, пусть будет отправка в терминал, и проверим, как это работает.
Я сделал для наглядности вторую переменную currentTime2, для второго светодиода.
unsigned long currentTime; // Переменная времени диода 8
unsigned long currentTime2; // Переменная времени диода 9
bool ledState=0; // Переменная состояния диода 8
bool ledState2=0; // Переменная состояния диода 9
unsigned long a; // Переменная для проверки работоспособности кода в условиях задержки
void setup()
{
Serial.begin(9600); // Конфигурируем серийный порт
pinMode(8, OUTPUT); //Пины 8 и 9 конфигурируем на выход
pinMode(9, OUTPUT);
}
void loop()
{
if (millis() – currentTime > 1000) // Проверяем время для первого диода (1000мс)
{
currentTime = millis();
ledState=!ledState; // Меняем состояние первого диода на противоположное
digitalWrite(8, ledState);
}
if (millis() – currentTime2 > 200) // Проверяем время для второго диода (200мс)
{
currentTime2 = millis();
ledState2=!ledState2; // Меняем состояние второго диода на противоположное
digitalWrite(9, ledState2);
}
a++;
Serial.println (a);
}
Для состояний светодиодов, возьмём переменную bool ledState и ledState2.
Мы задали её тип как boolean, а это значит что она может принимать два значения – true и false, что в принципе равно 0 и 1.
Чтобы не писать по несколько раз HIGH, LOW, мы подставляем туда состояние светодиода ledState.
А меняем его каждый раз логическим отрицанием (!) на противоположное значение.
ledState=!ledState; |
В конце цикла loop, впишем команду инкрементирования переменной a++, и отправки значения в серийный порт, чтобы отследить, будет ли выполняться этот код в момент ожидания задержек.
a++; Serial.println (a); |