Освой Arduino играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Экспресс-курс по C++

Тернарный оператор
typedef: создание псевдонима для типа данных
Ключевое слово auto
Структуры
pair
Функции
Классы
Конструктор

Заголовочные файлы можно найти в папке ...\hardware\arduino\avr\cores\arduino\, в частности заголовочный файл Arduino.h. В данном файле можно узнать значения директив препроцессора, констант, сигнатуры функций и другую полезную информацию.

Файл main.cpp содержит код запускаемой программы на C++.

Типы данных

  • boolean - занимает 1 байт. Принимает значения true или false (1 или 0), документация рекомендует его вместо bool
  • bool - аналог boolean
  • char - занимает 1 байт. Используется для кодов символов ASCII, имеет числовые значения от -128 до +128
  • unsigned char - занимает 1 байт. Похож на byte (рекомендуется его и использовать). Значения от 0 до 255
  • byte - занимает 1 байт. Значения от 0 до 255
  • int - занимает 2 байта. Целые 16-битные значения со знаком. Диапазон от –32 768 до 32 767. На некоторых платах может работать как 32-битное число и занимать 4 байта памяти
  • short - аналог int
  • unsigned int - занимает 2 байта. Если не нужны отрицательные значения. Диапазон от 0 до 65 535
  • word - аналог unsigned int
  • long - занимает 4 байта. Для очень больших чисел. –2 147 483 648 … + 2 147 483 647
  • unsigned long - занимает 4 байта. Если не нужны отрицательные значения. Диапазон от 0 до 4 294 967 295
  • float - занимает 4 байта. Для дробных чисел
  • double - занимает 4 байта. Для дробных чисел. Для Arduino аналогичен float. Но в Arduino Due данный тип имеет 8 байт (64 бит)
  • String (строка) заслуживает отдельной статьи. Строка может быть в двойных кавычках (массив символов), одиночным символом в одинарных кавычках, экземпляром объекта String.

При работе с дробными числами в монитор порта выводятся только две цифры после запятой.


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 и т.д.

Overflow

Замените тип на int и присвойте переменной значение 32760. Посмотрите, что произойдёт с числом после переполнения. Вы увидите отрицательные значения, которые будут затем уменьшаться на единицу.

Если присвоить слишком большое значение типу double и попробуем вывести его на экран, то увидим ovf.


double longNumber = 847982635862956999.27071966;

void setup() {
  Serial.begin(9600);
  Serial.println(longNumber);
}

void loop() {
}

Overflow

04.Communication: ASCIITable

Пример 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: создание псевдонима для типа данных

Зарезервированное ключевое слово typedef используется для создания псевдонима для другого типа данных. Например, мы можем для типа данных int придумать псевдоним cat и использовать его в программе. Вроде смысла в этом нет, но коту будет приятно, что вы о нём думаете.


typedef int cat;

void setup() {
  Serial.begin(9600);

  cat i = 7;
  int j = 27;

  Serial.println(i + j);
}

void loop() {}

Ключевое слово auto

Ключевое слово 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

Контейнер 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(){}
Реклама