Урок #2 – работа с последовательным портом

Монитор порта, отладка

Как мы с вами знаем из урока "Что умеет микроконтроллер", у многих микроконтроллеров есть интерфейс UART, позволяющий передавать и принимать различные данные. У интерфейса есть два вывода на плате - пины TX и RX. На большинстве Arduino-плат к этим пинам подключен USB-UART преобразователь (расположен на плате), при помощи которого плата может определяться компьютером при подключении USB кабеля и обмениваться с ним информацией. На компьютере создаётся виртуальный COM порт (последовательный порт), к которому можно подключиться при помощи программ-терминалов и принимать-отправлять текстовые данные. Таким же образом кстати работают некоторые принтеры и большинство станков с ЧПУ.

В самой Arduino IDE есть встроенная "консоль" - монитор порта, кнопка с иконкой лупы в правом верхнем углу программы. Нажав на эту кнопку мы откроем сам монитор порта, в котором будут настройки:

Если с отправкойавтопрокруткойотметками времени и кнопкой очистить вывод всё и так понятно, то конец строки и скорость мы рассмотрим подробнее:

  • Конец строки: тут есть несколько вариантов на выбор, чуть позже вы поймёте, на что они влияют. Лучше поставить нет конца строки, так как это позволит избежать непонятных ошибок на первых этапах знакомства с платформой.
    • Нет конца строки - никаких дополнительных символов в конце введённых символов после нажатия на кнопку отправка или клавишу Enter.
    • NL - символ переноса строки в конце отправленных данных.
    • CR - символ возврата каретки в конце отправленных данных.
    • NL+CR - и то и то.
  • Скорость - тут на выбор нам даётся целый список скоростей, т.к. общение по Serial может осуществляться на разных скоростях, измеряемых в бод (baud), и если скорости приёма и отправки не совпадают - данные будут получены некорректно. По умолчанию скорость стоит 9600, её и оставим.

Объект Serial


Начнём знакомство с одним из самых полезных инструментов Arduino-разработчика - Serial, который идёт в комплекте со стандартными библиотеками. Serial позволяет как просто принимать и отправлять данные через последовательный порт, так и наследует из класса Stream кучу интересных возможностей и фишек, давайте сразу их все рассмотрим, а потом перейдём к конкретным примерам.

Serial.begin(speed)

Запустить связь по Serial на скорости speed (измеряется в baud, бит в секунду). Скорость можно поставить любую, но есть несколько "стандартных" значений. Список скоростей для монитора порта Arduino IDE:

  • 300
  • 1200
  • 2400
  • 4800
  • 9600 чаще всего используется, можно назвать стандартной
  • 19200
  • 38400
  • 57600
  • 115200 тоже часто встречается
  • 230400
  • 250000
  • 500000
  • 1000000
  • 2000000 - максимальная скорость, не работает на некоторых китайских платах
Serial.end()
Прекратить связь по Serial. Также освобождает пины RX и TX.
Serial.available()
Возвращает количество байт, находящихся в буфере приёма и доступных для чтения.
Serial.availableForWrite()
Возвращает количество байт, которые можно записать в буфер последовательного порта, не блокируя при этом функцию записи.
Serial.write(val), Serial.write(buflen)
Отправляет в порт val численное значение или строку, или отправляет количество len байт из буфера buf. Важно! Отправляет данные как байт (см. таблицу ASCII), то есть отправив 88 вы получите букву X: 
Serial.write(88);
.
Serial.print(val), Serial.print(valformat)

Отправляет в порт значение val - число или строку, фактически "печатает". В отличие от write выводит именно текст, т.е. отправив 88, вы получите 88: 

Serial.print(88);
Отправляет любые стандартные типы данных: численные, символьные, строковые. Также методы print()/println() имеют несколько настроек для разных данных, что делает их очень удобным инструментом отладки:
Serial.print(78); // выведет 78
Serial.print(1.23456); // 1.23 (по умолч. 2 знака)
Serial.print('N'); // выведет N
Serial.print("Hello world."); // Hello world.
 
// можно сделать форматированный вывод в стиле
Serial.print("i have " + String(50) + " apples");
// выведет строку i have 50 apples
 
// вместо чисел можно пихать переменные
byte appls = 50;
Serial.print("i have " + String(appls) + " apples");
// выведет то же самое

format позволяет настраивать вывод данных: BINOCTDECHEX выведут число в соответствующей системе счисления: двоичная, восьмеричная, десятичная (по умолчанию) и 16-ричная. Цифра после вывода float позволяет настраивать выводимое количество знаков после точки:

Serial.print(78, BIN); // вывод "1001110"
Serial.print(78, OCT); // вывод "116"
Serial.print(78, DEC); // вывод "78"
Serial.print(78, HEX); // вывод "4E"
Serial.print(1.23456, 0); // вывод "1"
Serial.print(1.23456, 2); // вывод "1.23"
Serial.print(1.23456, 4); // вывод "1.2345"
Serial.println(), Serial.println(val), Serial.println(valformat)
Полный аналог 
print()
, но автоматически переводит строку после вывода. Позволяет также вызываться без аргументов (с пустыми скобками) просто для перевода курсора на новую строку.
Serial.flush()
Ожидает окончания передачи данных.
Serial.peek()
Возвращает текущий байт с края буфера, не убирая его из буфера. При вызове 
Serial.read()
 будет считан тот же байт, но из буфера уже уберётся.
Serial.read()
Читает и возвращает крайний символ из буфера.
Serial.setTimeout(time)
Устанавливает time (миллисекунды) таймаут ожидания приёма данных для следующих ниже функций. По умолчанию равен 1000 мс (1 секунда).
Serial.find(target), Serial.find(targetlength)

Читает данные из буфера и ищет набор символов target (тип 

char
), опционально можно указать длину length. Возвращает 
true
, если находит указанные символы. Ожидает передачу по таймауту.
// будем искать слово hello
char target[] = "hello";
 
void setup() {
Serial.begin(9600);
}
 
void loop() {
if (Serial.available() > 0) {
if (Serial.find(target))
Serial.println("found");
// вывести found, если было послано
}
}
Serial.findUntil(targetterminal)
Читает данные из буфера и ищет набор символов target (тип 
char
) либо терминальную строку terminal. Ожидает окончания передачи по таймауту, либо завершает приём после чтения terminal.
Serial.readBytes(bufferlength)
Читает данные из порта и закидывает их в буфер buffer (массив 
char[]
 или 
byte[]
). Также указывается количество байт, который нужно записать - length (чтобы не переполнить буфер).
Serial.readBytesUntil(characterbufferlength)
Читает данные из порта и закидывает их в буфер buffer (массив 
char[]
 или 
byte[]
), также указывается количество байт, который нужно записать - length (чтобы не переполнить буфер) и терминальный символ character. Окончание приёма в buffer происходит при достижении заданного количества length, при приёме терминального символа character (он в буфер не идёт) или по таймауту
Serial.readString()
Читает порт, формирует из данных строку 
String
, и возвращает её (урок про стринги). Заканчивает работу по таймауту.
Serial.readStringUntil(terminator)
Читает порт, формирует из данных строку 
String
, и возвращает её (урок про стринги). Заканчивает работу по таймауту или после приёма символа terminator (символ 
char
).
Serial.parseInt(), Serial.parseInt(skipChar)
Читает целочисленное значение из порта и возвращает его (тип 
long
). Заканчивает работу по таймауту. Прерывает чтение на всех знаках, кроме знака - (минус). Можно также отдельно указать символ skipChar, который нужно пропустить, например кавычку-разделитель тысяч (10'325'685), чтобы принять такое число.
Serial.parseFloat()
Читает значение с плавающей точкой из порта и возвращает его. Заканчивает работу по таймауту.

Плоттер


Помимо монитора последовательного порта, в Arduino IDE есть плоттер - построитель графиков в реальном времени по данным из последовательного порта. Достаточно отправлять значение при помощи команды 

Serial.println(значение)
 и открыть плоттер по последовательному соединению, например построим график значения с аналогового пина A0:
void setup() {
Serial.begin(9600);
}
 
void loop() {
Serial.println(analogRead(0));
delay(10);
}

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

 

значение1
 
пробел_или_запятая
 
значение2
 
пробел_или_запятая
 
значение3
 
пробел_или_запятая
 
перенос_строки

Давайте выведем значения с аналоговых пинов A0A1 и A2:

void setup() {
Serial.begin(9600);
}
 
void loop() {
Serial.print(analogRead(0));
Serial.print(',');
Serial.print(analogRead(1));
Serial.print(',');
Serial.print(analogRead(2));
Serial.println();
delay(5);
}

Получим вот такие графики:


В Arduino IDE с версии 1.8.10 добавили возможность подписать графики, для этого перед выводом нужно отправить названия в виде 

название 1, название 2, название n
 с переносом строки, и дальше просто выводить данные:

Отправка в порт


Рассмотрим самый классический пример для всех языков программирования: Hello World!

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

Давайте вспомним урок циклы и массивы и выведем в порт элементы массива:

void setup() {
Serial.begin(9600);
 
byte arr[] = {0, 50, 68, 85, 15, 214, 63, 254};
for (byte i = 0; i < 8; i++) {
Serial.print(arr[i]);
Serial.print(' ');
}
}
 
void loop() {
}

Вывод: 0 50 68 85 15 214 63 254 - элементы массива, разделённые пробелами.

Чтение из порта


Проблемы возникают при попытке принять данные в порт. Дело в том, что метод 

read()
 читает один символ, а если вы отправите длинное число или строку - программа получит его по одному символу. Чтение сложных данных называется парсинг. Его можно делать вручную, об этом мы поговорим в отдельном уроке из блока "Алгоритмы". В рамках этого урока рассмотрим встроенные инструменты для парсинга Serial.

Чтобы не нагружать программу чтением пустого буфера, нужно использовать конструкцию

if (Serial.available()) {
// тут читаем
}

Таким образом чтение будет осуществляться только в том случае, если в буфере есть какие-то данные.

Парсинг цифр


Для чтения целых цифр используем 
Serial.parseInt()
, для дробных - 
Serial.parseFloat()
. Пример, который читает целое число и отправляет его обратно:
void setup() {
Serial.begin(9600);
}
 
void loop() {
if (Serial.available()) {
int val = Serial.parseInt();
Serial.println(val);
}
}
Если при парсинге у вас появляются лишние цифры - поставьте "Нет конца строки" в настройках монитора порта

Вы заметите, что после отправки проходит секунда, прежде чем плата ответит в порт. Эта секунда является таймаутом, о котором мы говорили чуть выше. Программа ждёт секунду после принятия последнего символа, чтобы все данные успели прийти. Секунда это очень много, ожидать, скажем, 50 миллисекунд. Это можно сделать при помощи метода 

setTimeout()
.
void setup() {
Serial.begin(9600);
Serial.setTimeout(50);
}
 
void loop() {
if (Serial.available()) {
int val = Serial.parseInt();
Serial.println(val);
}
}

Теперь после отправки цифры программа будет ждать всего 50 мс и ответит гораздо быстрее!

Парсинг текста


Проще всего прочитать текст в String-строку (урок про них). Это максимально не оптимально, но зато довольно просто для восприятия:

void setup() {
Serial.begin(9600);
Serial.setTimeout(50);
}
 
void loop() {
if (Serial.available()) {
String str = Serial.readString();
Serial.println(str);
}
}

Данный пример выводит любой текст, который был отправлен в монитор порта.

Управляющие символы


Существуют так называемые управляющие символы, позволяющие форматировать вывод. Их около десятка, но вот самые полезные из них

  • \n
     - новая строка
  • \r
     - возврат каретки
  • \v
     - вертикальная табуляция
  • \t
     - горизонтальная табуляция

Также если в тексте вы захотите использовать одинарные кавычки 

'
, двойные кавычки 
"
, обратный слэш 
\
 и некоторые другие символы - их нужно экранировать при помощи обратного слэша, он просто ставится перед символом:
  • \"
     - двойные кавычки
  • \'
     - апостроф
  • \\
     - обратный слэш
  • \0
     - нулевой символ
  • \?
     - знак вопроса

Выведем строку с кавычками:

Serial.println("\"Hello, World!\""); // выведет "Hello, World!"

Комбинация 

\r\n
 переведёт строку и вернёт курсор в левое положение:
Serial.print("Hello, World!\r\nArduino Forever");
// выведет
// Hello, World!
// Arduino Forever

Символы табуляции позволят удобно отправлять данные для последующей вставки в таблицу. Например выведем несколько степеней двойки в виде таблицы, используя символ табуляции 

\t
:
for (byte i = 0; i < 16; i++) { // i от 0 до 16
Serial.print(i); // степень
Serial.print("\t"); // табуляция
Serial.println(round(pow(2, i))); // 2 в степени i
}

Результат скопируем и вставим в excel  Удобно!

Видео

Урок #1.1 – операции с переменными и константами

Математические операции

Математика


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

  • =
     присваивание
  • +
     сложение
  • -
     вычитание
  • *
     умножение
  • /
     деление
  • %
     остаток от деления

Рассмотрим простой пример:

int a = 10;
int b = 20;
int c = a + b; // c = 30
int d = a * b; // d = 200
 
// так тоже можно
d = d / a; // d = 20
c = c * d; // c = 600

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

  • +=
     составное сложение: 
    a += 10
     равносильно 
    a = a + 10
  • -=
     составное вычитание: 
    a -= 10
     равносильно 
    a = a - 10
  • *=
     составное умножение: 
    a *= 10
     равносильно 
    a = a * 10
  • /=
     составное деление: 
    a /= 10
     равносильно 
    a = a / 10
  • %=
     остаток от деления: 
    a %= 10
     равносильно 
    a = a % 10

С их использованием можно сократить запись последних двух строчек из предыдущего примера:

d /= a;
c *= d;

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

  • ++
     (плюс плюс) инкремент: 
    a++
     равносильно 
    a = a + 1
  • --
     (минус минус) декремент: 
    a--
     равносильно 
    a = a - 1

Порядок записи инкремента играет очень большую роль: пост-инкремент 

var++
 возвращает значение переменной до выполнения инкремента. Операция пре-инкремента 
++var
 возвращает значение уже изменённой переменной. Пример:
byte a, b;
 
a = 10;
b = a++;
// a получит значение 11
// b получит значение 10
 
a = 10;
b = ++a;
// a получит значение 11
// b получит значение 11

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

{
byte a; // просто объявляем
byte b = 0; // инициализируем 0
 
a++; // результат непредсказуем
b++; // результат 1
}

Порядок вычислений


Порядок вычисления выражений подчиняется обычным математическим правилам: сначала выполняются действия в скобках, затем умножение и деление, и в конце - сложение и вычитание.

Скорость вычислений


Математические вычисления выполняются процессором некоторое время, оно зависит от типа данных и типа операции. Вот время выполнения (в микросекундах) не оптимизированных компилятором вычислений для Arduino Nano 16 МГц:

Тип данных Время выполнения, мкс
Сложение и вычитание Умножение Деление, остаток
int8_t
0.44 0.625 14.25
uint8_t
0.44 0.625 5.38
       
int16_t
0.89 1.375 14.25
uint16_t
0.89 1.375 13.12
int32_t
1.75 6.06 38.3
uint32_t
1.75 6.06 37.5
       
float
8.125 10 31.5
  • Нужно понимать, что не все во всех случаях математические операции занимают ровно столько времени, так как компилятор их оптимизирует. Можно помочь ему в этом, подробнее читайте в уроке по оптимизации кода.
  • Операции с 
    float
     выполняются гораздо дольше целочисленных, потому что в AVR нет аппаратной поддержки чисел с плавающей точкой и она реализована программно как сложная библиотека. В некоторых микроконтроллерах есть FPU - специальный аппаратный блок для вычислений с 
    float
    .
  • Операции целочисленного деления на AVR выполняются дольше по той же причине - они реализованы программно, а вот умножение и сложение с вычитанием МК делает аппаратно и очень быстро.

Целочисленное деление


При целочисленном делении результат не округляется по "математическим" правилам, дробная часть просто отсекается, фактически это округление вниз: и 

9/10
 и 
1/10
 дадут 
0
. При использовании 
float
 само собой получится 
0.9
 и 
0.1
. Если нужно целочисленное деление с округлением вверх, его можно реализовать так: вместо 
x / y
 записать 
(x + y - 1) / y
. Рассмотренные выше примеры деления на 10 дадут результат 
1
.

Для округления по обычным математическим правилам можно использовать функцию 

round()
, но она довольно тяжёлая, так работает с 
float
.

Переполнение переменной


Вспомним предыдущий урок о типах данных: что будет с переменной, если её значение выйдет из допустимого диапазона? Тут всё весьма просто: при переполнении в бОльшую сторону из нового значения вычитается максимальное значение переменной, и у неё остаётся только остаток. Для сравнения представим переменную как ведро. Будем считать, что при наливании воды и заполнении ведра мы скажем стоп, выльем из него всю воду, а затем дольём остаток. Вот так и с переменной, что останется - то останется. Если переполнение будет несколько раз - несколько раз опорожним наше "ведро" и всё равно оставим остаток. Ещё один хороший пример - кружка Пифагора.

При переполнении в обратную сторону (выливаем воду из ведра), будем считать, что ведро полностью заполнилось. Да, именно так =) Посмотрим пример:

// тип данных byte (0.. 255)
byte val = 255;
 
// тут val станет равным 0
val++;
 
// а тут из нуля станет 246
val -= 10;
 
// переполним! Останется 13
val = 525;
 
// и обратно: val равна 236
val = -20;

Особенность больших вычислений


Для сложения и вычитания по умолчанию используется ячейка 4 байта (

long
), но для умножения и деления - 2 байта (
int
). Если при умножении или делении в текущем действии результат превысит 
32768
 - ячейка переполнится и мы получим некорректный результат. Для исправления ситуации нужно привести тип переменной к 
long
 перед вычислением, что заставит МК выделить дополнительную память. Например  
a = (long)b * c;

Для цифр существуют модификаторы, делающие то же самое:

  • U
     или 
    u
     - перевод в 
    uint16_t
     (от 0 до 65'535). Пример: 
    36000u
  • L
     или 
    l
     - перевод в 
    int32_t
     (-2 147 483 648… 2 147 483 647). Пример: 
    325646L
  • UL
     или 
    ul
     - перевод в 
    uint32_t
     (от 0 до 4 294 967 295). Пример: 
    361341ul

Посмотрим, как это работает на практике:

long val;
val = 2000000000 + 6000000; // посчитает корректно (т.к. сложение)
val = 25 * 1000; // посчитает корректно (умножение, меньше 32'768)
val = 35 * 1000; // посчитает НЕКОРРЕКТНО! (умножение, больше 32'768)
val = (long)35 * 1000; // посчитает корректно (выделяем память (long) )
val = 35 * 1000L; // посчитает корректно (модификатор L)
val = 35 * 1000u; // посчитает корректно (модификатор u)
val = 70 * 1000u; // посчитает НЕКОРРЕКТНО (модификатор u, результат > 65535)
val = 1000 + 35 * 10 * 100; // посчитает НЕКОРРЕКТНО! (в умножении больше 32'768)
val = 1000 + 35 * 10 * 100L; // посчитает корректно! (модификатор L)
val = (long)35 * 1000 + 35 * 1000; // посчитает НЕКОРРЕКТНО! Второе умножение всё портит
val = (long)35 * 1000 + (long)35 * 1000; // посчитает корректно (выделяем память (long) )
val = 35 * 1000L + 35 * 1000L; // посчитает корректно (модификатор L)

Особенности float


Помимо медленных вычислений, поддержка работы с 

float
 занимает много памяти, т.к. реализована в виде "библиотеки". Использование математических операций с 
float
 однократно добавляет примерно 1.5 кБ в память программы.

С вычислениями есть такая особенность: если в выражении нет 

float
 чисел, то вычисления будут иметь целый результат (дробная часть отсекается). Для получения правильного результата нужно писать преобразование 
(float)
 перед действием, использовать 
float
 числа или 
float
 переменные. Также есть модификатор 
f
, который можно применять только к цифрам 
float
Смысла в нём нет, но такую запись можно встретить. Смотрим:
float val; // далее будем присваивать 100/3, ожидаем результат 33.3333
val = 100 / 3; // посчитает НЕПРАВИЛЬНО (результат 33.0)
 
int val1 = 100; // целочисленная переменная
val = val1 / 3; // посчитает НЕПРАВИЛЬНО (результат 33.0)
 
float val2 = 100; // float переменная
val = val2 / 3; // посчитает правильно (есть переменная float)
 
val = (float)100 / 3; // посчитает правильно (указываем (float) )
val = 100.0 / 3; // посчитает правильно (есть число float)
val = 100 / 3.0f; // посчитает правильно (есть число float и модификатор)

При присваивании 

float
 числа целочисленному типу данных дробная часть отсекается. Если хотите математическое округление - его нужно использовать отдельно:
int val;
val = 3.25; // val станет 3
val = 3.92; // val станет 3
val = round(3.25); // val станет 3
val = round(3.92); // val станет 4

Следующий важный момент: из за особенности самой модели "чисел с плавающей точкой" - вычисления иногда производятся с небольшой погрешностью. Смотрите (значения выведены через порт):

float val2 = 1.1 - 1.0;
// результат 0.100000023 !!!
 
float val4 = 1.5 - 1.0;
// результат 0.500000000

Казалось бы, 

val2
 должна стать ровно 
0.1
 после вычитания, но в 8-ом знаке вылезла погрешность! Будьте очень внимательны при сравнении 
float
 чисел, особенно со строгими операциями 
<=
: результат может быть некорректным и нелогичным.

Список математических функций


Математических функций в Arduino довольно много, часть из них являются макросами, идущими в библиотеке Arduino.h, все остальные же наследуются из мощной C++ библиотеки math.h

[su_spoiler title="Математические функции из math.h" open="no" style="fancy" icon="arrow"]

Функция Описание
cos (x)
Косинус (радианы)
sin (x)
Синус (радианы)
tan (x)
Тангенс (радианы)
fabs (x)
Модуль для float чисел
fmod (x, y)
Остаток деления x на у для float
modf (x, *iptr)
Возвращает дробную часть, целую хранит по адресу iptr http://cppstudio.com/post/1137/
modff (x, *iptr)
То же самое, но для float
sqrt (x)
Корень квадратный
sqrtf (x)
Корень квадратный для float чисел
cbrt (x)
Кубический корень
hypot (x, y)
Гипотенуза ( корень(x*x + y*y) )
square (x)
Квадрат ( x*x )
floor (x)
Округление до целого вниз
ceil (x)
Округление до целого вверх
frexp (x, *pexp)
http://cppstudio.com/post/1121/
ldexp (x, exp)
x*2^exp http://cppstudio.com/post/1125/
exp (x)
Экспонента (e^x)
cosh (x)
Косинус гиперболический (радианы)
sinh (x)
Синус гиперболический (радианы)
tanh (x)
Тангенс гиперболический (радианы)
acos (x)
Арккосинус (радианы)
asin (x)
Арксинус (радианы)
atan (x)
Арктангенс (радианы)
atan2 (y, x)
Арктангенс (y / x) (позволяет найти квадрант, в котором находится точка)
log (x)
Натуральный логарифм х ( ln(x) )
log10 (x)
Десятичный логарифм x ( log_10 x)
pow (x, y)
Степень ( x^y )
isnan (x)
Проверка на nan (1 да, 0 нет)
isinf (x)
Возвр. 1 если x +бесконечность, 0 если нет
isfinite (x)
Возвращает ненулевое значение только в том случае, если аргумент имеет конечное значение
copysign (x, y)
Возвращает x со знаком y (знак имеется в виду + -)
signbit (x)
Возвращает ненулевое значение только в том случае, если _X имеет отрицательное значение
fdim (x, y)
Возвращает разницу между x и y, если x больше y, в противном случае 0
fma (x, y, z)
Возвращает x*y + z
fmax (x, y)
Возвращает большее из чисел
fmin (x, y)
Возвращает меньшее из чисел
trunc (x)
Возвращает целую часть числа с дробной точкой
round (x)
Математическое округление
lround (x)
Математическое округление (для больших чисел)
lrint (x)
Округляет указанное значение с плавающей запятой до ближайшего целого значения, используя текущий режим округления и направление

[/su_spoiler] [su_spoiler title="Arduino - функции" open="no" style="fancy" icon="arrow"]

Функция Значение
min(a, b)
Возвращает меньшее из чисел 
a
 и 
b
max(a, b)
Возвращает большее из чисел
abs(x)
Модуль числа
constrain(val, min, max)
Ограничить диапазон числа 
val
 между 
min
 и 
max
map(val, min, max, newMin, newMax)
Перевести диапазон числа 
val
 (от 
min
 до 
max
) в новый диапазон (от 
newMin
 до 
newMax
). 
val = map(analogRead(0), 0, 1023, 0, 100);
 - получить с аналогового входа значения 0-100 вместо 0-1023. Работает только с целыми числами!
round(x)
Математическое округление
radians(deg)
Перевод градусов в радианы
degrees(rad)
Перевод радиан в градусы
sq(x)
Квадрат числа

[/su_spoiler][su_spoiler title="Математические константы" open="no" style="fancy" icon="arrow"]

Константа Значение Описание
INT8_MAX
127 Макс. значение char, int8_t
UINT8_MAX
255 Макс. значение byte, uint8_t
INT16_MAX
32767 Макс. значение int, int16_t
UINT16_MAX
65535 Макс. значение unsigned int, uint16_t
INT32_MAX
2147483647 Макс. значение long, int32_t
UINT32_MAX
4294967295 Макс. значение unsigned long, uint32_t
M_E
2.718281828 Число e
M_LOG2E
1.442695041 log_2 e
M_LOG10E
0.434294482 log_10 e
M_LN2
0.693147181 log_e 2
M_LN10
2.302585093 log_e 10
M_PI
3.141592654 pi
M_PI_2
1.570796327 pi/2
M_PI_4
0.785398163 pi/4
M_1_PI
0.318309886 1/pi
M_2_PI
0.636619772 2/pi
M_2_SQRTPI
1.128379167 2/корень(pi)
M_SQRT2
1.414213562 корень(2)
M_SQRT1_2
0.707106781 1/корень(2)
NAN
__builtin_nan(“”) nan
INFINITY
__builtin_inf() infinity
PI
3.141592654 Пи
HALF_PI
1.570796326 пол Пи
TWO_PI
6.283185307 два Пи
EULER
2.718281828 Число Эйлера е
DEG_TO_RAD
0.01745329 Константа перевода град в рад
RAD_TO_DEG
57.2957786 Константа перевода рад в град

[/su_spoiler]

Видео

Урок #1 – структура скетча и типы данных

Синтаксис и структура кода

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

Синтаксис


  • Тела функций заключаются в фигурные скобки 
    { }
    . Код внутри фигурных скобок иногда называют блоком кода.
  • Каждая команда заканчивается точкой с запятой 
    ;
  • Метод применяется к объекту через точку. Пример: 
    Serial.begin();
  • Вызов функции или метода всегда заканчивается скобками, даже если функция не принимает параметров. Пример: 
    loop()
  • Разделитель десятичных дробей - точка. Пример: 
    0.25
     У запятой тут другое применение.
  • Запятыми перечисляются аргументы функций и методов, члены массива, также через запятую можно выполнить несколько действий в одну строчку. Пример: 
    digitalWrite(3, HIGH);
     массив - 
    int myArray[] = {3, 4, 5 ,6};
  • Одиночный символ заключается в одиночные кавычки 
    'а'
  • Строка и массив символов заключается в двойные кавычки 
    "строка"
  • Имена переменных могут содержать латинские буквы в верхнем и нижнем регистре (большие и маленькие), цифры и подчеркивание. Пример: 
    myVal_35
     .
  • Имена переменных не могут начинаться с цифры. Только с буквы или подчёркивания.
  • Регистр имеет значение, т.е. большая буква отличается от маленькой. Пример: имена 
    val
     и 
    Val
     - не одно и то же.

К синтаксису также можно отнести комментарии, т.к. в разных языках они выделяются по-разному. Комментарий это обычный текст, который игнорируется на этапе компиляции и не попадает в итоговую программу для МК. Комментарии нужны для пояснения кода, как себе самому, так и другим возможным его читателям. В C++ у нас два типа комментариев:

  • Однострочный комментарий
    // однострочный комментарий
    // компилятор меня игнорирует =(
  • Многострочный комментарий
    /* Многострочный
    комментарий */

Оформление


Есть такое понятие, как форматирование (выравнивание) кода, то есть соблюдение пробелов и интервалов. Чисто для примера, сравните эти два куска кода. Какой смотрится более понятно?
Не бойтесь, во всех серьезных средах разработки есть автоформатирование кода, оно работает как в процессе написания, так и "по кнопке". Arduino IDE - не исключение, в ней код форматируется комбинацией клавиш Ctrl+T:

  • Между математическими действиями, знаками сравнения, присваивания и всем подобным ставится пробел.
  • Как и в обычном тексте, пробел ставится после и не ставится перед запятой, двоеточием, точкой с запятой.
  • Отступ от левого края экрана - знак табуляции, код сдвигается вправо и на одном расстоянии формируются команды из одного блока кода. В Arduino IDE одна табуляция равна двум пробелам. Можно использовать клавишу Tab.
  • Каждое действие выполняется с новой строки (автоформатирование это не исправляет).
  • Имена функций и переменных принято называть с маленькой буквы. Пример: 
    value
  • Если имя состоит из двух и более слов, то их принято разделять. Есть два способа:
    • camelCase (верблюжий стиль): первая буква маленькая, каждая первая буква следующего слова - большая.
    • under_score (подчёркивание): все буквы маленькие, разделитель - подчёркивание.

  • Имена типов данных и классов принято писать с большой буквы. Пример: 
    Signal
    Servo
  • Имена констант принято писать в верхнем регистре, разделение - подчеркивание. Пример: 
    MOTOR_SPEED
  • При написании библиотек и классов, имена внутренних (приватных) переменных принято писать, начиная со знака подчёркивания. Пример: 
    _position
  • Несколько общепринятых сокращений для названий переменных, вы часто будете встречать их в чужих прошивках и библиотеках:
    • button - btn, кнопка
    • index - idx - i, индекс
    • buffer - buf, буфер
    • value - val, значение
    • variable - var, переменная
    • pointer - ptr, указатель
  • Имена функций и методов принято начинать с глагола, кратко описывающего действие функции. Вот те из них, которые вы будете встречать постоянно:
    • get - получить значение (getValue)
    • set - установить значение (setTime)
    • printshow - показать что-то
    • read - прочитать
    • write - записать
    • change - изменить
    • clear - очистить
    • beginstart - начать
    • endstop - закончить, остановить
Частый вопрос: влияет ли длина названия переменной на занимаемую прошивкой память? На вес файла с программой на компьютере - влияет. На вес загруженной в микроконтроллер прошивки - не влияет, потому что код преобразуется в машинный, в котором нет имён.

Структура кода


Прежде чем переходить к структуре и порядку частей кода, нужно кое-что запомнить:

  • Переменная любого типа должна вызываться только после своего объявления. Иначе будет ошибка
  • Объявление и использование классов или типов данных из библиотеки/файла должно быть после подключения библиотеки/файла
  • Функция может вызываться как до, так и после объявления, потому что C++ компилируемый язык, компиляция проходит в несколько этапов, и функции "выделяются" отдельно, поэтому могут вызываться в любом месте программы

При запуске Arduino IDE даёт нам заготовку в виде двух обязательных функций: setup() и loop()

 

Код в блоке 

setup()
 выполняется один раз при каждом запуске МК. Код в блоке 
loop()
 выполняется "по кругу" на всём протяжении работы программы, начиная с момента завершения выполнения 
setup()
Для любознательных: если вы уже знакомы с языком C++, то вероятно спросите "а где же 
int main()
 и вообще файл main.cpp?". Всё очень просто: 
int main()
 за вас уже написали внутри файла main.cpp, который лежит глубоко в файлах "ядра", а 
setup()
 и 
loop()
 встроены в него следующим образом:
// main.cpp
// где-то в глубинах ядра Arduino
int main() {
setup();
for (;;) {
loop();
}
return 0;
}

На протяжении нескольких лет работы с Arduino я сформировал для себя следующую структуру скетча:

  1. Описание прошивки, ссылки, заметки
  2. Константы-настройки (define и обычные)
  3. Служебные константы (которые следует менять только с полным осознанием дела)
  4. Подключаемые библиотеки и внешние файлы, объявление соответствующих им типов данных и классов
  5. Глобальные переменные
  6. setup()
  7. loop()
  8. Свои функции

[su_spoiler title="Пример кода" open="no" style="fancy" icon="arrow"]

/*
Данный скетч плавно крутит
сервопривод туда-обратно
между мин. и макс. углами
by AlexGyver
*/
 
// -------- НАСТРОЙКИ ---------
#define SERVO_PIN 13 // сюда подключена серво
#define SERVO_SPEED 3 // скорость серво
#define MIN_ANGLE 50 // мин. угол
#define MAX_ANGLE 120 // макс. угол
 
// ------- БИБЛИОТЕКИ -------
#include <Servo.h>
Servo myservo;
 
// ------- ПЕРЕМЕННЫЕ -------
uint32_t servoTimer;
boolean servoDirection;
int servoAngle;
 
// --------- SETUP ----------
void setup() {
myservo.attach(SERVO_PIN);
}
 
// ---------- LOOP ----------
void loop() {
turnServo();
}
 
// --------- ФУНКЦИИ --------
void turnServo() {
if (millis() - servoTimer >= 50) { // каждые 50 мс
servoTimer = millis();
if (servoDirection) {
servoAngle += SERVO_SPEED;
if (servoAngle >= MAX_ANGLE) {
servoAngle = MAX_ANGLE;
servoDirection = false;
}
} else {
servoAngle -= SERVO_SPEED;
if (servoAngle <= MIN_ANGLE) {
servoAngle = MIN_ANGLE;
servoDirection = true;
}
}
myservo.write(servoAngle);
}
}

[/su_spoiler]

Это удобная структура для "скетча", крупные проекты так писать не рекомендуется и следует приучать себя к более взрослым подходам, описанным в уроке по разработке крупных проектов.

Подключение библиотек и файлов


В реальной работе вы очень часто будете использовать библиотеки или просто внешние файлы, они подключаются в главный файл (файлу прошивки) при помощи директивы 

#include
. Данная команда сообщает компилятору, что нужно найти и добавить в программу указанный файл. Этот файл может содержать свои 
#include
 и тянуть за собой и другие файлы, таким образом программа может быть разбита на множество независимых файлов. Рассмотрим пример:
#include <Servo.h> // подключает библиотеку Servo.h
 
#include “Servo.h” // тоже подключает библиотеку Servo.h

В чём отличие 

<>
 и 
""
? Когда указываем название 
"в кавычках"
, компилятор сначала ищет файл в папке со скетчем, а затем в папке с библиотеками. При использовании 
<галочек>
 компилятор ищет файл только в папке с библиотеками! К слову о папках с библиотеками: их две, в обеих будет производиться поиск библиотек.
  • Пользовательская папка: Документы/Arduino/libraries. Сюда библиотеки попадают при добавлении их через "подключить .zip библиотеку" и при установке из менеджера библиотек.
  • Папка с программой: C:/Program Files (x86)/Arduino/libraries (или C:/Program Files/Arduino/libraries для 32-разрядной Windows). Здесь хранятся встроенные стандартные библиотеки.

Не используйте мышку!


Вы наверняка замечали, как в фильмах программисты и хакеры делают свою работу, барабаня по клавиатуре и особо не трогая мышку. Это действительно так, чем больше вы программируете, тем меньше будете использовать мышку для установки курсора в нужное место и выделения слов и строк, потому что делать это с клавиатуры можно гораздо быстрее!

  • Ctrl+← , Ctrl+→ – переместить курсор влево/вправо НА ОДНО СЛОВО
  • Home , End – переместить курсор в начало/конец строки
  • Shift+← , Shift+→ – выделить символ слева/справа от курсора
  • Shift+Ctrl+← , Shift+Ctrl+→ – выделить слово слева/справа от курсора
  • Shift+Home , Shift+End – выделить все символы от текущего положения курсора до начала/конца строки
  • Ctrl+Z – отменить последнее действие
  • Ctrl+Y – повторить отменённое действие
  • Ctrl+C – копировать выделенный текст
  • Ctrl+X – вырезать выделенный текст
  • Ctrl+V – вставить текст из буфера обмена

Местные сочетания:

  • Ctrl+U – загрузить прошивку в Arduino
  • Ctrl+R – скомпилировать (проверить)
  • Ctrl+Shift+M – открыть монитор порта
  • Ctrl+T - автоформатирование

Для отодвигания комментариев в правую часть кода используйте TAB, а не ПРОБЕЛ. Нажатие TAB перемещает курсор по некоторой таблице, из-за чего ваши комментарии будут установлены на одинаковом расстоянии.

Видео

EcoMonitoring

ЛІЧІЛЬНИК ВІДВІДУВАННЬ

Сьогодні 68
Вчора171
Цього тижня 730
Минулого тижня 1980
Цей місяць 3689
Минулий місяць 9302
За весь час 177019
Ваш IP: 18.221.185.110
Сегодня: 2025-05-14
Пользователей на сайте: 0
Гостей на сайте: 21