* Главы с пометкой Pro необязательны для изучения, это информация для продвинутых и любознательных.

Типы данных Arduino


Начнём с типов памяти микроконтроллера, их целых три:

  • Flash или ПЗУ – постоянное запоминающее устройство, энергонезависимая память микроконтроллера, в которой хранится программный код, прошивка. Функции, процедуры, операции, всё это сидит там и не меняется в процессе работы прошивки (можно конечно туда залезть, но это мы делать не будем, да и не надо оно). Во время загрузки прошивки загрузчик (bootloader) записывает прошивку именно сюда.
  • SRAM или ОЗУ – оперативное запоминающее устройство, оперативная память. В ней хранятся переменные, значения которых могут изменяться во время работы, к этой памяти мы имеем более чем свободный доступ и будем активно этим пользоваться. После перезагрузки микроконтроллера память полностью очищается.
  • EEPROM – англ. Electrically Erasable Programmable Read-Only Memory — электрически стираемое перепрограммируемое ПЗУ, энергонезависимая память, отведённая пользователю для хранения значений, которые можно менять, но они не будут сбрасываться после перезагрузки. Штука удобная, о ней есть отдельный урок.

В ближайшее время нас будет интересовать только SRAM память, в которой хранятся переменные, именно о них дальше и пойдёт речь.

Двоичная система


В цифровом мире, к которому относится также микроконтроллер, информация хранится, преобразуется и передается в цифровом виде, то есть в виде нулей и единиц. Соответственно элементарная ячейка памяти, которая может запомнить 0 или 1, называется бит (bit). Таким образом мы плавно переходим к двоичной системе исчисления. Ну же, вспоминайте школьную информатику! Не вдаваясь в подробности “как это работает”, просто попробуем рассмотреть закономерность

Двоичная Десятичная
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
10000 16

И так далее. Помимо закономерности увеличения разрядов и чисел есть ещё одна: приглядитесь к числам в двоичной системе со всеми нулями справа от единицы:

10 2
100 4
1000 8
10000 16

Именно, степень двойки! Именно на степенях двойки в цифровом мире завязано очень много. Чтобы получить количество десятичных чисел, которые могут быть закодированы заданным количеством бит, нужно возвести 2 в степень количества бит. Смотрим на таблицу выше и продолжаем:

  • 5 бит – 32
  • 6 бит – 64
  • 7 бит – 128
  • 8 бит – 256
  • 9 бит – 512
  • 10 бит – 1024

И так далее. Сразу нужно запомнить, что в программировании счёт начинается с нуля, то есть 5ю битами мы можем закодировать десятичное число от 0 до 31, 8-ю битами – от 0 до 255, 10-ю битами – от 0 до 1023. Очень важно понять и запомнить это, дальше очень пригодится.

Следующая по величине единица измерения в цифровом мире – байт (byte), состоит из 8 бит. Почему 8? Исторически сложилось, что шины первых микропроцессоров имели разрядность 8 бит, возможно поэтому это количество приняли за более старшую единицу памяти. Также 8 это 2 в степени 3, что очень символично и удобно. А ещё, для кодирования всех латинских букв, знаков препинания, математических знаков и просто символов (всех что на клавиатуре) раньше хватало 7-ми бит (128 символов), но потом их стало мало, и ввели дополнительный бит, восьмой. То есть 8 бит это также размер таблицы символов, которая называется ASCII. К ней мы вернёмся уже в этой главе. Так что вопрос почему в одном байте 8 бит четкого ответа не имеет, ведь бывает и 6-ти битный байт, и 9-ти битный… Но это исключения старых процессоров, в современных цифровых устройствах в одном байте обычно содержится 8 бит (на отличных от AVR архитектурах может быть иначе), что позволяет закодировать 256 десятичных чисел от 0 до 255 соответственно. Дальше вы уже точно знаете:

  • 1 кБ = 1024 Б
  • 1 МБ = 1024 кБ
  • 1 ГБ = 1024 МБ
  • И так далее

Двоичная система является родной для микроконтроллера, и для работы с отдельными битами существует целый ряд инструментов, о них мы поговорим в уроке о битовых операциях из раздела продвинутых уроков.

Другие системы исчисления


Данные в памяти микроконтроллера хранятся в двоичном представлении, но помимо него существуют и другие системы исчисления, в которых мы можем работать. Постарайтесь сразу запомнить и понять, что переводить числа из одной системы исчисления в другую не нужно, Ардуино абсолютно всё равно, в каком формате вы скармливаете значение переменной, они автоматически будут интерпретированы в двоичный вид. Разные системы исчисления введены в первую очередь для удобства программиста.

Теперь по сути: ардуино поддерживает (да в целом другого и не нужно) четыре классических системы исчисления: двоичную, восьмеричную, десятичную и шестнадцатеричную. Да, и до неё добрались. Краткая напоминалка: 16-ричная система имеет 16 значений на один разряд, первые 10 как у десятичной, остальные – первые буквы латинского алфавита: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f.

С десятичной системой всё просто, пишем числа так, как они выглядят. 10 это десять, 25 это двадцать пять, и так далее. Двоичная (Binary) имеет префикс 0b (ноль бэ) или B, то есть двоичное число 101 запишется как 0b101 ИЛИ B101. Восьмеричная (Octal) имеет префикс 0 (ноль), например 012. Шестнадцатеричная (hexadecimal) имеет префикс 0x (ноль икс), FF19 запишется как 0xFF19.

Базис Префикс Пример Особенности
2 (двоичная) B или 0b (ноль бэ) B1101001 цифры 0 и 1
8 (восьмеричная) 0 (ноль) 0175 цифры 0 – 7
10 (десятичная) нет 100500 цифры 0 – 9
16 (шестнадцатеричная) 0x (ноль икс) 0xFF21A цифры 0-9, буквы A-F

Основная фишка 16-ричной системы в том, что она позволяет записывать длинные десятиричные числа короче, например один байт (255) запишется как 0xFF, два байта (65 535) как 0xFFFF, а жуткие три байта (16 777 215) как 0xFFFFFF. Вы не представляете (или уже имеете представление), насколько удобно и понятно это позволяет работать с цветами и оттенками.

Двоичная же система обычно используется для наглядного представления данных и низкоуровневых конфигураций различного железа. Например конфиг кодируется одним байтом, каждый бит в нём отвечает за отдельную настройку (вкл/выкл), и передав один байт вида 0b10110100 можно сразу кучу всего настроить, к этому мы вернёмся в уроке работа с регистрами из раздела продвинутых уроков. В документации по этому поводу пишут в стиле “первый бит отвечает за это, второй за то” и так далее.

Переменные


Переменная – это ячейка SRAM памяти, которая имеет своё уникальное название и хранит числа соответственно своему размеру. К переменной мы можем обратиться по её имени и получить значение, либо изменить его.

Степень двойки преследует нас и дальше, ведь объём одной ячейки памяти в микроконтроллере тоже ей кратен:

  • 1 байт = 8 бит = 256
  • 2 байта = 16 бит = 65 536
  • 4 байта = 32 бита = 4 294 967 296

Да, больше четырёх байт в ардуино (точнее в МК от AVR) уже не влезет, при использовании обычных типов данных. Для работы с разными диапазонами значений используются разные типы данных (переменных). По сути можно использовать 4 байта для хранения чего угодно, но это не оптимально. Это как знать, что вам нужно будет унести максимум 200 мл воды (меньше 1 байта), но вы всё равно берёте 19 литровую бутыль (2 байта). Или железнодорожную цистерну на 120 тонн (4 байта). Если хотите писать красивый и оптимальный код, используйте соответствующие типы данных. Кстати, вот они:

Типы данных


Название Альт. название Вес Диапазон Особенность
boolean bool 1 байт 0 или 1, true или false Логическая переменная. bool на Arduino тоже занимает 1 байт, а не бит!
char 1 байт -128… 127 Хранит номер символа из таблицы символов ASCII
int8_t 1 байт -128… 127 Целочисленный тип
byte uint8_t 1 байт 0… 255 Целочисленный тип
int int16_t, short 2 байта -32 768… 32 767 Целочисленный тип
unsigned int uint16_t, word 2 байта 0… 65 535 Целочисленный тип
long int32_t 4 байта -2 147 483 648…    2 147 483 647 Целочисленный тип
unsigned long uint32_t 4 байта 0… 4 294 967 295 Целочисленный тип
float 4 байта -3.4028235E+38… 3.4028235E+38 Хранит числа с плавающей точкой (десятичные дроби). Точность: 6-7 знаков
double 4 байта   Для AVR то же самое, что float. А так он 8 байт на более взрослом железе
int64_t 8 байт -(2^64)/2… (2^64)/2-1 *Очень большие числа. Стандартный Serial не умеет такие выводить
uint64_t 8 байт 2^64-1 *Очень большие числа. Стандартный Serial не умеет такие выводить

*Не встречал упоминания об этом в официальных источниках, но Ардуино (точнее компилятор) также поддерживает 64 битные числа, соответственно тип данных int64_t и uint64_t

Максимальный размер всех типов данных хранится в константах, и его можно использовать в коде по надобности:

  • UINT8_MAX – вернёт 255
  • INT8_MAX – вернёт 127
  • UINT16_MAX – вернёт 65 535
  • INT16_MAX – вернёт 32 767
  • UINT32_MAX– вернёт 4 294 967 295
  • INT32_MAX – вернёт 2 147 483 647
  • UINT64_MAX – вернёт 18 446 744 073 709 551 615
  • INT64_MAX – вернёт ‭9 223 372 036 854 775 807

Помимо целочисленных типов (byte, int, long) есть более интересные:

  • bool (boolean) – логический тип данных, принимает значения 0 и 1, или true и false (правда и ложь). По сути ведёт себя как бит, но занимает 8 бит. Экая несправедливость! Есть несколько путей хранить логические переменные так, чтобы они занимали 1 байт, но об этом поговорим позже. Также переменная типа boolean принимает значение true, если присвоить ей значение? отличное от нуля, то есть boolean a = 50;  a будет true, и boolean b = -20; тоже будет true! Пример: boolean flag = true;
  • char – символьный тип данных, в численном эквиваленте принимает значения от -128 до 127. В случае с char эти значения являются кодами символов в стандартной таблице символов ASCII. Может принимать и хранить данные в символьном формате (буква или символ в одиночных кавычках), например char var = 'a';
  • float – тип данных с плавающей точкой (англ. float – плавающий), т.е. десятичная дробь. float var = 3.1415;

Есть ещё несколько нестандартных типов, которые иногда встречаются в чужом коде:

  • size_t – “синоним” uint16_t, предназначен для отображения размера объекта в байтах. Например этот тип возвращает функция sizeof() и некоторые другие.
  • Расширенный char: char16_t, char32_t и wchar_t. Нужен для хранения больших символьных данных для алфавитов разных стран, а не только английского языка.

Объявление и инициализация переменных


Объявление переменной – резервирование имени под данные указанного типа. Инициализация – присвоение переменной начального значения при помощи оператора =.

  • тип_данных имя; // объявление
  • тип_данных имя = значение; // объявление и инициализация
  • Также можно объявить и инициализировать несколько переменных через запятую: int a = 0, b, с = 10;
byte myVal;
int sensorRead = 10;
byte val1, val2, val3 = 10;

Важные моменты:

  • Переменная должна бывать объявлена до обращения к себе, буквально находиться выше по коду. Иначе вы получите ошибку “переменная не объявлена” – Not declared in this scope
  • Глобальные переменные при объявлении имеют значение 0 по умолчанию (если не инициализировать)
  • Локальные переменные (создаваемые внутри функций в процессе работы программы) при объявлении могут иметь случайное значение, т.к. выделяются из динамической памяти. Крайне рекомендуется их инициализировать, если в дальнейшем коде они используются как нулевое значение (пример ниже)
// читаем среднее значение с АЦП
int averRead() {
  long sum = 0;  // инициализируем 0
  // если этого не сделать - будет беда

  for (int i = 0; i < 10; i++) {
    sum += analogRead(0);
    // если sum не инициализирована нулём -
    // вместо суммы значений получим мусор
  }
  sum = (long)sum / 10;
  return sum;
}

Преобразование типов


Иногда требуется преобразовать один тип данных в другой: например, функция принимает int, а вы хотите передать ей byte. В большинстве случаев компилятор сам разберётся и преобразует byte в int, но иногда вылетает ошибка в стиле “попытка передать byte туда, где ждут int“. В таком случае можно преобразовать тип данных, для этого достаточно указать нужный тип данных в скобках перед преобразуемой переменной (тип_данных)переменная, иногда можно встретить запись тип_данных(переменная). Результат вернёт переменную с новым типом данных, сам же тип данной у переменной не изменится (работает в рамках одного действия). Например:

// переменная типа byte
byte val = 10;

// передаём какой-то функции, которая ожидает int
sendVal( (int)val );

И всё! (int)val будет обрабатываться как int, а не как byte.

Преобразование _cast (Pro)


Иногда можно встретить преобразование типов через оператор cast. Отличную статью можно глянуть на Хабре, а я кратко опишу 4 основных каста:

  • reinterpret_cast– приведение типов без проверки, непосредственное указание компилятору. Применяется только в случае полной уверенности программиста в собственных действиях. Не снимает const и volatile, применяется для приведения указателя к указателю, указателя к целому и наоборот;
  • static_cast– преобразует выражения одного статического типа в объекты и значения другого статического типа. Поддерживается преобразование численных типов, указателей и ссылок по иерархии наследования как вверх, так и вниз. Преобразование проверяется на уровне компиляции и в случае ошибки приведения типов будет выдано сообщение;
  • dynamic_cast– используется для динамического приведения типов во время выполнения. В случае неправильного приведения типов для ссылок вызывается исключительная ситуация std::bad_cast, а для указателей будет возвращен 0;
  • const_cast– самое простое приведение типов. Снимает const и volatile, то есть константность и отказ от оптимизации компилятором переменной. Это преобразование проверяется на уровне компиляции и в случае ошибки приведения типов будет выдано сообщение.

Как пользоваться: на примере предыдущего примера

// переменная типа byte
byte val = 10;

// передаём какой-то функции, которая ожидает int
sendVal( static_cast<int>(val) );

Константы


Что такое константа понятно из её названия – что-то, значение чего мы можем только прочитать и не можем изменить. Задать (объявить) константу можно двумя способами:

  • Точно так же как переменную, указав перед типом данных слово const. Если значение переменной не будет изменяться в процессе выполнения программы – рекомендуется объявить её как константу, это позволит компилятору лучше оптимизировать код и в большинстве случаев он будет чуточку легче и быстрее.
    const byte myConst = 10;  // объявить константу типа byte
  • При помощи директивы препроцессору #define, которая делает следующее: на этапе компиляции кода препроцессор заменяет указанные все последовательности символов в текущем документе (напомню, что вкладки Arduino IDE являются одним документом) на соответствующие им значения. Константа, определённая при помощи #define не занимает места в оперативной памяти, а хранится как код программы во Flash памяти, это самый большой плюс данного способа. Синтаксис: #define имя значение. Точка запятой не ставится. Таким способом обычно указывают пины подключения, настройки, различные величины и всё такое. Пример:
    #define BTN_PIN 10
    #define DEFAULT_VALUE 3423

Ещё пару слов о константах и переменных: если обычная переменная нигде не изменяется в процессе выполнения программы – компилятор может самостоятельно сделать её константой и она не займёт места в оперативной памяти, т.е. будет помещена во Flash.

Область видимости


Переменные, константы и другие типы данных (структуры и перечисления) имеют такое важное понятие, как область видимости. Она бывает

  • Глобальной
  • Локальной
  • Формальной (параметр)

Глобальная


Глобальная переменная объявляется вне функций и доступна для чтения и записи в любом месте программы, в любой её функции.

byte var;
void setup() {
  // спокойно меняем глобальную переменную
  var = 50;
}
void loop() {
  // спокойно меняем глобальную переменную
  var = 70;
}

Локальная


Локальная переменная живёт внутри функции или внутри любого блока кода, заключённого в { фигурные скобки }, доступна для чтения и записи только внутри него. При попытке обратиться к локальной переменной из другой функции (за пределами её { блока } ) вы получите ошибку, потому что локальная переменная создаётся заново при выполнении содержащего её блока кода (или функции) и удаляется из памяти при завершении выполнения этого блока (или функции):

void setup() {
  byte var;  // локальная для setup переменная
  // спокойно меняем локальную переменную
  var = 50;
}
void loop() {
  // приведёт к ошибке, потому что в этом блоке кода var не объявлена
  var = 70;
  
  // сделаем тут отдельный блок кода
  {
    byte var2 = 10;
    // var2 существует только внутри этого блока!
  }
  // вот тут var2 уже будет удалена из памяти
}

Важный момент: если имя локальной переменной совпадает с глобальной, то приоритет обращения по имени в функции отдаётся локальной переменной:

byte var;   // глобальная var
void setup() {
  byte var; // создаём локальную var
  // меняем локальную var
  var = 50;
}
void loop() {
  // а тут меняем уже глобальную var!
  var = 70;
}

Формальная (параметр)


Формальная переменная, она же параметр, передаваемый в функцию, ведёт себя как обыкновенная локальная переменная, но появляется при немного других условиях: при вызове функции. Эту переменную можно читать и менять внутри её функции. Также читайте отдельный урок про функции.

void setup() {
  // передаём 10 как аргумент
  myFunc(10);
}

void loop() {
}

void myFunc(byte var) {
  // тут var является "локальной"
  // прибавим к ней 20
  var += 20;
  // после } переменная будет удалена из памяти!
}

Структуры (Pro)


Структура struct – очень интересный тип данных: это совокупность разнотипных переменных, объединённых одним именем. В некоторых случаях структуры позволяют очень сильно упростить написание кода, сделать его более логичным и легко модифицируемым. Тип данных структура объявляется вот по такой схеме:

struct ярлык {
  тип_данных имя_переменной_1;
  тип_данных имя_переменной_2;
  тип_данных имя_переменной_3;
};

Ярлык будет являться новым типом данных, и, используя этот ярлык, можно объявлять уже непосредственно саму структуру:

ярлык имя_структуры;  // объявить одну структуру
ярлык имя_структуры_1, имя_структуры_2;  // объявить две структуры типа ярлык
ярлык имя_структуры[5];  // объявить массив структур

Также есть вариант объявления структуры без создания ярлыка, т.е. создаём структуру, не объявляя её как тип данных со своим именем.

struct {
  тип_данных имя_переменной_1;
  тип_данных имя_переменной_2;
  тип_данных имя_переменной_3;
} имя_структуры;
  • Обращение к члену структуры производится вот по такой схеме: имя_структуры.имя_переменной и позволяет менять или читать значение.
  • Если две структуры имеют одинаковую структуру (объявлены одним ярлыком) то можно одну структуру просто приравнять к другой, все переменные запишутся соответственно на свои места.
  • Ещё одним удобным вариантом является присваивание значения вот таким образом: имя_структуры = (ярлык) {значение_переменной_1, значение_переменной_2, значение_переменной_3};

Рассмотрим большой пример, где показано всё вышеописанное

struct myStruct { // создаём ярлык myStruct
  boolean a;
  byte b;
  int c;
  long d;
  byte e[5];
} kek;            // и сразу создаём структуру kek

// создаём массив структур cheburek типа myStruct
myStruct cheburek[3];

void setup() {  
  // присвоим членам структуры значения вручную
  kek.a = true;
  kek.b = 10;
  kek.c = 1200;
  kek.d = 789456;
  kek.e[0] = 10;    // e у нас массив!
  kek.e[1] = 20;
  kek.e[2] = 30;

  // присвоим структуру kek структуре cheburek номер 0
  cheburek[0] = kek;

  // присвоим элемент массива из структуры kek 
  // структуре cheburek номер 1
  cheburek[0].e[1] = kek.e[1];

  // забьём данными структуру cheburek номер 2
  cheburek[2] = (myStruct) {
    false, 30, 3200, 321654, {1, 2, 3, 4, 5}
  };
}

Для чего нужны структуры? В большинстве примеров в интернете приводится использование структур для хранения адресных данных, т.е. создания базы данных адресов: имя, фамилия, номер телефона, итд. На моей практике структуры оказались очень удобными для создания меню с большим количеством режимов и настроек (скажем несколько каналов, у каждого одинаковый набор настроек). Структуры очень удобно передавать и получать например при помощи RF24 модулей, т.е. вместо массивов удобнее использовать структуры и передавать одной строчкой сразу кучу типов данных. Также структуру можно целиком записать в eeprom одной строчкой (командой put) и одной строчкой оттуда же её считать, не заморачиваясь с номерами ячеек, как это происходит при записи данных вручную.

Размер элемента структуры


Структуры позволяют делать одну очень интересную вещь для оптимизации памяти: указывать максимальный вес элемента в битах. Таким образом можно делать даже однобитные флаги (обычный bool/boolean занимает в памяти 8 бит). Делается это при помощи оператора двоеточие :

// создаём и пакуем структуру однобитных флагов
struct MyFlags {
  bool button: 1;
  bool state: 1;
  bool position: 1;
  bool flag_3: 1;
  bool flag_4: 1;
};

// объявляем структуру типа MyFlags
MyFlags flags;

void setup() {
  Serial.begin(9600);
  // по умолчанию все флаги false
  // обращаемся как к структуре
  // можем менять и работать как с обычными флагами
  Serial.println(flags.button);
  Serial.println(flags.state);
  flags.position = true;
  Serial.println(flags.position);
}

void loop() {}

Точно так же можно паковать целочисленные типы данных, например знаем, что значение переменной не превысит 50, можем объявить её внутри структуры как byte val : 6, что сделает её 6-битной в памяти. Для небольшого набора переменных это не имеет смысла, а вот если их реально много – стоит запаковать их в структуру!

Вложенные структуры


Структуры также могут быть вложенными друг в друга, доступ к нужному элементу осуществляется так же при помощи оператора “точка”, смотрите простой пример:

struct Values {
  int value1;
  float value2;
};

struct BigStruct {
  Values values;  // элемент типа Values
  int otherValue;
};

BigStruct myStruct;
myStruct.values.value2 = 3.14;

Перечисления (Pro)


Перечисления (enum – enumeration) – тип данных, представляющий собой набор именованных констант, нужен в первую очередь для удобства программиста. Сразу пример из опыта: допустим у нас есть переменная mode, отвечающая за номер режима работы устройства. Мы для себя запоминаем, какому значению переменной какой режим будет соответствовать, и где-нибудь себе записываем, например 0 – обычный режим, 1 – режим ожидания, 2 – режим настройки_1, 3 – режим настройки_2, 4 – калибровка, 5 – аварийный режим, ошибка. При написании или чтении программы часто придётся обращаться к этому списку, чтобы не запутаться. Можно сделать первый шаг по оптимизации: обозвать каждый режим при помощи дефайна:

#define NORMAL 0
#define WAITING 1
#define SETTINGS_1 2
#define SETTINGS_2 3
#define CALIBRATION 4
#define ERROR_MODE 5

Таким образом вместо цифры можно будет использовать понятные слова и ориентироваться в коде будет гораздо проще. Использование enum ещё немного упрощает эту конструкцию: перечисление позволяет создать переменную (по умолчанию типа int), которая может принимать только те “названия”, которые для неё указаны. Это удобно тем, что в одной программе могут находиться разные хранители режимов с одинаковыми названиями, и в отличие от #define это не будет приводить к ошибкам.

Объявление перечисления чем-то похоже на объявление структуры:

enum ярлык {имя1, имя2, имя3, имя4, имя5};

Таким образом мы объявили ярлык. Теперь, используя этот ярлык, можно объявить само перечисление:

ярлык имя_перечисления;

Также как и у структур, можно объявить перечисление без создания ярлыка (зачем нам лишняя строчка?):

enum {имя1, имя2, имя3, имя4, имя5} имя перечисления;

Созданное таким образом перечисление является переменной, которая может принимать указанные для неё имена, также с этими именами её можно сравнивать. Теперь самое главное: имена для программы являются числами, начиная с 0 и далее по порядку увеличиваясь на 1. В абстрактном примере выше имя1 равно 0, имя2 равно 1, имя3 равно 2, и так далее. Помимо указанных имён, перечислению можно приравнять и число напрямую, но как бы зачем. Рассмотрим пример!

// создаём перечисление modes
// не создавая ярлык
enum {
  NORMAL,
  WAITING,
  SETTINGS_1,
  SETTINGS_2,
  CALIBRATION,
  ERROR_MODE,
} modes;

void setup() {
  Serial.begin(9600);   // для отладки
  modes = CALIBRATION;  // присваивание значения

  // можем сравнивать
  if (modes == CALIBRATION) {
    Serial.println("calibr");
  } else if (modes == ERROR_MODE) {
    Serial.println("error");
  }

  // присваиваем числом
  modes = 3;  // по нашему порядку это будет SETTINGS_2
}

Также порядок автонумерации перечислений можно изменить, задав начальное значение. От него всё будет и дальше изменяться на единицу:

enum {SET1 = 1, SET2, SET3, SET4, SET5} settings;

Таким образом SET1 имеет значение 1, SET2 будет 2 и так далее по порядку.

Пользовательские типы (Pro)


В языке есть инструмент typedef, позволяющий создать свой тип данных, основанный на другом стандартном. Зачем? С одной стороны для удобства программиста, с другой – чтобы его запутать =) Я никогда им не пользовался, но для разбора чужих кодов из инторнета это знать нужно! Итак, typedef работает следующим образом: typedef тип имя; – создать новый тип данных имя на основе типа тип. Пример:

typedef byte color;

Создаёт тип данных под названием color, который будет абсолютно идентичен типу byte (то есть принимать 0-255). Теперь с этим типом можно создавать переменные:

color R, G, B;

Создали три переменные типа color, который тот же byte, только в профиль. Это всё!

Есть ещё один важный момент, касающийся структур struct и перечислений enum – в кодах из интернета вы часто встретите использование typedef перед struct и enum. В чистом языке Си это имеет большой смысл, но в C++ это наоборот является проблемой. typedef в ардуино-прошивках используют программисты, которые пришли с Си и по привычке используют typedef, на плюсах делать этого не нужно.

В языке Си (без ++) при создании структуры/перечисления по ярлыку нужно писать слово struct/enum перед ярлыком, иначе будет ошибка. Либо саму структуру нужно объявить как typedef

// НА СИ, НЕ ++
// делаем так
struct myStruct {
  byte x;
  byte y;
};
struct myStruct kek;  // создать структуру kek

// или так
typedef struct myStruct {
  byte x;
  byte y;
};
myStruct kek;  // создать структуру kek

В С++ (и на Ардуино) этого делать не нужно! Наоборот, typedef в этом применении может приводить к ошибкам. Например:

// используя typedef вы не можете
// объявить структуру сразу.
// Вот это приведёт к ошибке при
// попытке обратиться к члену структуры
typedef struct {
  byte x;
  byte y;
} kek;

// вот это тоже приведёт к ошибке
typedef struct myStruct {
  byte x;
  byte y;
} kek;

// Единственным вариантом останется
// работа по ярлыку
typedef struct myStruct {
  byte x;
  byte y;
};

myStruct kek;
// вот серьёзно, нужны ли вам такие проблемы?

Пространство имён (Pro)


Пространство имён – очень удобная возможность языка, с её помощью можно разделить функции или переменные с одинаковыми именами друг от друга, то есть защитить свой набор данных инструментов от конфликтов имён с другими именами. “Именная область” определяется при помощи оператора namespace:

namespace mySpace {
  // функции или данные
};

Чтобы использовать содержимое из пространства имён, нужно обратиться через его название и оператор разрешения области видимости ::

mySpace::имя_функции

Более подробный пример:

namespace mySpace {
byte val;
void printKek() {
  Serial.println("kek");
}
};

void setup() {
  Serial.begin(9600);
  // printKek(); // приведёт к ошибке
  mySpace::printKek();
}

Также есть оператор using, позволяющий не использовать каждый раз обращение к пространству имён. Например, в отдельном файле у нас есть пространство имён с различными функциями. Чтобы в основном файле программы каждый раз не писать ярлык пространства имён с ::, можно написать

using имя_пространства_имён;

И ниже по коду можно будет пользоваться содержимым пространства имён без обращения через имя::

Спецификаторы (Pro)


Помимо возможности сделать переменную константой при помощи спецификатора const у нас есть ещё несколько интересных инструментов по работе с переменной.

static


static – делает переменную (или константу) статичной. Что это значит?

Статичная локальная

Для начала вспомним, как работает обычная локальная переменная: при вызове функции локальная переменная создаётся заново и получает нулевое значение, если не указано иначе. Если локальная переменная объявлена как static – она будет хранить своё значение от вызова к вызову функции, то есть станет грубо говоря глобально-локальной. Пример:

Обычная локальная:

void setup() {
  myFunc(); // вернёт 20
  myFunc(); // вернёт 20
  myFunc(); // вернёт 20
  myFunc(); // вернёт 20
}

void loop() {
}

byte myFunc() {
  byte var = 10;
  var += 10;
  return var;
}

Статическая локальная:

void setup() {
  myFunc(); // вернёт 20
  myFunc(); // вернёт 30
  myFunc(); // вернёт 40
  myFunc(); // вернёт 50
}

void loop() {
}

byte myFunc() {
  static byte var = 10;
  var += 10;
  return var;
}

Статичная глобальная

Статичная глобальная переменная становится доступной только в данном файле, спецификатор static позволяет спрятать её от воздействий из других файлов программы.

extern


extern – указывает компилятору, что переменная объявлена где-то в другом файле программы, и при компиляции он её найдёт и будет использовать. А если не найдёт – ошибки не будет. Например при помощи данного кода можно сбросить счётчик millis()

// указываем, что хотим использовать
// переменную timer0_millis,
// которая объявлена где-то далеко
// в файлах Arduino
extern volatile unsigned long timer0_millis;

void setup() {
  timer0_millis = 0;  // сброс mills()
}
void loop() {
  
}

volatile


volatile – данный спецификатор указывает компилятору, что данную переменную не нужно оптимизировать и её значение может быть изменено откуда-то извне. Обычно переменные с таким спецификатором используются в обработчиках прерываний. Вычисления с такими переменными также не оптимизируются и занимают больше процессорного времени.

Видео


Важные страницы