Освой Arduino играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Тернарный оператор
typedef: создание псевдонима для типа данных
Ключевое слово auto
Структуры
pair
Функции
Классы
Конструктор
Заголовочные файлы можно найти в папке ...\hardware\arduino\avr\cores\arduino\, в частности заголовочный файл Arduino.h. В данном файле можно узнать значения директив препроцессора, констант, сигнатуры функций и другую полезную информацию.
Файл main.cpp содержит код запускаемой программы на C++.
При работе с дробными числами в монитор порта выводятся только две цифры после запятой.
double longNumber = 12345.27071966;
void setup() {
Serial.begin(9600);
Serial.println(longNumber); // выводится 12345.27
}
void loop() {
}
Важно следить за диапазонами, чтобы не выйти за их пределы (overflow). Иначе можно получить непредсказуемые результаты. Напишем скетч с переполнением типа byte. Присвоим переменной значение, близкое к максимальному значению и будем прибавлять по единице в цикле.
byte number = 250;
void setup() {
Serial.begin(9600);
}
void loop() {
number++;
Serial.println(number);
delay(500);
}
Когда значение дойдёт до 255, то следующим значением станет 0 и т.д.
Замените тип на int и присвойте переменной значение 32760. Посмотрите, что произойдёт с числом после переполнения. Вы увидите отрицательные значения, которые будут затем уменьшаться на единицу.
Если присвоить слишком большое значение типу double и попробуем вывести его на экран, то увидим ovf.
double longNumber = 847982635862956999.27071966;
void setup() {
Serial.begin(9600);
Serial.println(longNumber);
}
void loop() {
}
Пример File | Examples | 04.Communication: ASCIITable из Arduino IDE демонстрирует вывод данных в Serial Monitor. Берётся таблица символов ASCII в виде значений int и выводится на экран в десятичном, шестнадцатеричном, восьмеричном и бинарном виде.
Для примера не требуется никаких компонентов и схем.
Код и результат вывода приводить нет смысла, вы это можете сделать самостоятельно.
Коллекцию однотипных переменных можно хранить в массиве и получать к ним доступ через индекс, который начинается с нуля. Варианты объявления и инициализации.
int ints[6];
int pins[] = {2, 4, 8, 3, 6};
int sensorVals[6] = {2, 4, -8, 3, 2};
char message[6] = "kitty";
Если вы укажете число за пределами индекса, то получите какое-то случайно число от сборщика мусора, а не ошибку.
Serial.println(sensorVals[15]);
Примеры использования массивов рассматривались в уроках про светодиоды.
Также можно создавать многомерные массивы. Визуально их можно представить в виде рядов и столбцов. Сначала указываем значения рядов, потом столбцов. Общее количество элементов можно узнать, перемножив число рядов и столбцов.
/*
2 5
6 9
*/
int twoDims[2][2] = {{2, 5}, {6, 9}};
Serial.print(twoDims[0][0]); // первый ряд и первый столбец, т.е. 2
Serial.println(twoDims[0][1]); // 5
Serial.println(twoDims[1][0]); // 6
Serial.println(twoDims[1][1]); // 9
Побитовый оператор AND обозначается символом &. Значение бита, полученное в результате выполнения побитового оператора AND, равно 1, если соответствующие биты в операндах также равны 1. Во всех остальных случаях значение результирующего бита равно 0.
Присвоим первой переменной значение 10, что равно двоичному значению 00001010. Второй переменной присвоим значение 3 (двоичное значение 00000011). При выполнение побитового оператора AND получим 00000010, что соответствует значению 2.
void setup() {
Serial.begin(9600);
int x = 10;
int y = 3;
int z = x & y;
Serial.println(z);
}
void loop() {}
// Result
2
Другие побитовые операторы: OR (|), XOR (^), NOT (~).
Оператор << сдвигает левый операнд влево на количество битов, определённое правым операндом.
Оператор >> сдвигает левый операнд вправо на количество битов, определённое правым операндом.
Когда мы сдвигаем влево на указанное число бит, то добавляем биты справа со значением 0. Таким образом, при сдвиге числа 00001100 на 2 бита, мы получаем 00110000.
Аналогично при сдвиге вправо. Число 00001100 при сдвиге вправо на 2 бита превращается в 00000011.
Подобные операции часто используются для быстрого умножения. Сдвиг на один бит влево фактически умножает число на 2.
Возьмём число 10 и будем сдвигать биты влево или вправо.
void setup() {
Serial.begin(9600);
int x = 10;
Serial.print("x >> 1: ");
Serial.println(x >> 1);
Serial.print("x >> 2: ");
Serial.println(x >> 2);
Serial.print("x << 1: ");
Serial.println(x << 1);
Serial.print("x << 2: ");
Serial.println(x << 2);
}
void loop() {}
// ответ
x >> 1: 5
x >> 2: 2
x << 1: 20
x << 2: 40
Условия и циклы (if, for и т.д.) работают аналогично, как в других языках, например, в Java.
Цикл for в С++ 11 поддерживает краткий синтаксис, когда нужно просто пройтись по всем элементам массива.
void setup() {
Serial.begin(115200);
// перебираем все 9 жизней кота
int lives[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int i : lives) {
Serial.println(i);
}
}
void loop() {}
Напомним стандартный способ. Тут главное не забыть, что индекс массива начинается с 0, поэтому следует правильно писать код, чтобы не получить ошибку.
void setup() {
Serial.begin(115200);
int lives[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int i = 0; i < 9; i++) {
Serial.print(lives[i]); // значения используем для индекса массива
// Serial.print(i); // просто выводим числа цикла, без привязки к массиву
Serial.print("|");
}
}
void loop() {}
Тернарный оператор использует для операнда. Синтаксис оператора:
expression1 ? expression2 : expression3
В первом операнде используется логическое выражение, которое возвращает второй или третий операнд в зависимости от значения первого операнда. Если в первом операнде используется true, то используется второе выражение. Если в первом операнде используется false, то используется третий операнд.
Тернарный оператор является компактным вариантом выражения if-else.
void setup() {
Serial.begin(9600);
Serial.println();
int cond = 9;
String result = cond > 5 ? "Больше чем 5" : "Меньше или равно 5";
Serial.print("Если cond = 9, то выводится: ");
Serial.println(result);
Serial.println("---");
cond = 3;
result = cond > 5 ? "Больше чем 5" : "Меньше или равно 5";
Serial.print("Если cond = 3, то выводится: ");
Serial.println(result);
Serial.println("---");
bool condition = false;
int x = 0, y = 0;
(condition ? x : y) = 10;
Serial.print("X: ");
Serial.println(x);
Serial.print("Y: ");
Serial.println(y);
Serial.println("---");
condition = true;
x = 0;
y = 0;
(condition ? x : y) = 10;
Serial.print("X: ");
Serial.println(x);
Serial.print("Y: ");
Serial.println(y);
Serial.println("---");
}
void loop() {}
// Результаты
Если cond = 9, то выводится: Больше чем 5
---
Если cond = 3, то выводится: Меньше или равно 5
---
X: 0
Y: 10
---
X: 10
Y: 0
---
Зарезервированное ключевое слово typedef используется для создания псевдонима для другого типа данных. Например, мы можем для типа данных int придумать псевдоним cat и использовать его в программе. Вроде смысла в этом нет, но коту будет приятно, что вы о нём думаете.
typedef int cat;
void setup() {
Serial.begin(9600);
cat i = 7;
int j = 27;
Serial.println(i + j);
}
void loop() {}
Ключевое слово auto появилось в C++ 11. Указывает, что тип переменной, который был объявлен, будет автоматически выведен из её инициализации. Для функций возвращаемый тип определяется также автоматически. Применимо к обычным типам, классам и функциям.
class Cat {
public:
int x;
Cat(int arg1) {
x = arg1;
}
};
int meow() {
return 5;
}
void setup() {
Serial.begin(9600);
auto murzik = Cat(10);
auto functionResult = meow();
auto myMouse = 20;
Serial.print("Object data member: ");
Serial.println(murzik.x);
Serial.print("Function result: ");
Serial.println(functionResult);
Serial.print("Int variable: ");
Serial.println(myMouse);
}
void loop() {}
// Result
Object data member: 10
Function result: 5
Int variable: 20
Структуры используются, когда надо объединить несколько переменных с разными типами под одним именем. Это делает программу гибкой для внесения изменений. Также структуры пригодятся, когда необходимо сгруппировать некоторые данные, например, запись из базы данных или координаты точки.
При объявлении структуры мы создаём составной тип данных, с помощью которого можно создавать переменные, которые сочетают в себе несколько значений. Внутри структуры каждому полю даётся имя, потом по его имени можно обращаться к этому значению.
struct sensor {
int deviceId;
int measurementType;
float value;
};
void setup() {
Serial.begin(115200);
struct sensor mySensor;
mySensor.deviceId = 944;
mySensor.measurementType = 1;
mySensor.value = 20.4;
Serial.println(mySensor.deviceId);
Serial.println(mySensor.measurementType);
Serial.println(mySensor.value);
}
void loop() {}
Контейнер pair позволяет сгруппировать парами элементы разных типов (можно и одинаковых). Доступ к элементам пары осуществляется через поля first и second. После создания пары можно изменить значения у любого элемента пары.
void setup() {
Serial.begin(115200);
std::pair <int, char *> testPair(9, "Hello Kitty");
Serial.println(testPair.first);
Serial.println(testPair.second);
// присваиваем новые значения
testPair.first = 20;
testPair.second = "Привет Барсик";
Serial.println("---------------");
Serial.println(testPair.first);
Serial.println(testPair.second);
}
void loop() {}
В любом скетче для Arduino есть две обязательные функции: setup() и loop(). Вы можете также создавать свои собственные функции. Создадим отдельную функцию для скетча Blink.
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
blink();
}
void blink() {
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
Мы перенесли часть кода для мигания светодиодом в отдельную функцию и вызываем её в loop(). В сложном проекте подобный подход делает код читабельным и удобным для редактирования.
Функция может иметь параметры, которые указываются в круглых скобках.
void setup() {
Serial.begin(9600);
helloCat("Барсик");
}
void loop() {}
void helloCat(String name) {
Serial.print("Привет, ");
Serial.print(name);
}
Можно создать вторую функцию с таким же именем, но они должны различаться по параметрам, чтобы система поняла, какую функцию следует вызывать. Это называется перегрузкой функции.
// добавить в предыдущий скетч
void helloCat(String greeting, String name) {
Serial.print(greeting);
Serial.print(", кот ");
Serial.print(name);
}
void setup() {
...
// Вызываем перегруженную версию функции
helloCat("Здравствуй", "Васька");
}
Функция может возвращать значения. Напишем функцию, которая будет возвращать длину имени кота.
void setup() {
Serial.begin(9600);
int number = getLengthCatName("Мурзик");
Serial.println(number);
}
void loop() {}
int getLengthCatName(String name) {
return name.length();
}
Учтите, что при использовании русских символов, функция будет возвращать в два раза больше, чем ожидается. Это связано с кодировкой Unicode, в которой используется два байта вместо одного.
В качестве параметра можно передавать ссылку на переменную. В этом случае к параметру прибавляется символ амперсанда. Создадим две переменные x и y и будем менять их значения через функцию. Сами переменные создаются в функции loop(), а наша функция будет иметь к ним доступ.
void setup() {
Serial.begin(9600);
}
void loop() {
int x = random(10); // pick some random numbers
int y = random(10);
Serial.print("The value of x and y before swapping are: ");
Serial.print(x);
Serial.print(",");
Serial.println(y);
swapRef(x, y);
Serial.print("The value of x and y after swapping are: ");
Serial.print(x);
Serial.print(",");
Serial.println(y);
Serial.println();
delay(1000);
}
// swap the two given values
void swapRef(int &value1, int &value2)
{
int temp;
temp = value1;
value1 = value2;
value2 = temp;
}
В общих чертах работа с классами аналогична с другими языками. Область видимости, объекты, инициализация... Но есть небольшие синтаксические особенности. Создадим объект Коробка с размерами. Создадим объект КоробкаДляКота и зададим ему ширину. Проверим значение полей класса сразу после создания объекта и после присвоения полю значения.
class Box {
public:
int width;
int height;
int length;
};
Box catBox;
void setup()
{
Serial.begin(9600);
catBox = Box();
// before
Serial.println(catBox.width);
catBox.width = 100;
// after
Serial.println(catBox.width);
}
void loop(){}
//
0
100
Как видите, по умолчанию поле width получило значение 0. Затем в коде мы явно присвоили другое значение, затерев предыдущее. Мы можем сразу присвоив другое значение по умолчанию.
int width = 5;
При создании класса мы можем задать конструктор, который позволить инициализировать необходимые поля класса при создании объекта класса. Доработаем предыдущий пример с коробкой.
class Box {
public:
int width;
int height;
int length;
Box(int arg1, int arg2, int arg3) {
width = arg1;
height = arg2;
length = arg3;
}
};
void setup()
{
Serial.begin(9600);
Box catBox(10, 20, 10);
Serial.print("Box width: ");
Serial.println(catBox.width);
Serial.print("Box height: ");
Serial.println(catBox.height);
Serial.print("Box length: ");
Serial.println(catBox.length);
}
void loop(){}
Кроме указанного выше способа присвоения в конструкторе значений полям класса, можно использовать список инициализации.
class Box {
public:
int width;
int height;
int length;
Box(int arg1, int arg2, int arg3): width(arg1), height(arg2), length(arg3) {}
};
void setup()
{
Serial.begin(9600);
Box catBox(23, 22, 11);
Serial.print("Box width: ");
Serial.println(catBox.width);
Serial.print("Box height: ");
Serial.println(catBox.height);
Serial.print("Box length: ");
Serial.println(catBox.length);
}
void loop(){}