Консоль #
Самый простой способ получить обратную связь от программы - вывод данных в консоль - окно ввода-вывода текстовых данных для взаимодействия с программой. Это самый простой способ отладки кода - отправлять из программы данные на разных участках её выполнения, выводить ошибки, показания датчиков и прочее. Если речь идёт о компьютере, то программа выполняется на компьютере и консоль расположена там же, выполнением программы и выводом в консоль занимается один и тот же процессор.
Для примера можно открыть онлайн-компилятор OneCompiler, в нём будет минимальный пример, который выводит в консоль строку "Hello, World!". Если запустить программу - можно увидеть эту строку в консоли:

UART #
С Arduino ситуация немного иная - программа выполняется на отдельном устройстве со своим процессором, к которому нет такого прямого доступа. Arduino подключается к компьютеру по USB, на самой плате USB гнездо подключено не напрямую к микроконтроллеру - он не умеет с ним работать - а к USB-UART преобразователю. UART - это универсальный приёмник-передатчик, почти все микроконтроллеры имеют такой интерфейс для связи с внешними устройствами. UART представлен двумя пинами RX (Receive, приём) и TX (Transmit, передача) - именно к ним и подключен преобразователь. Взглянем на плату Arduino Nano:
С его помощью Arduino может принимать и отправлять данные по USB проводу.
COM порт #
Компьютер взаимодействует именно с USB-UART преобразователем, он не знает, что это плата Arduino. При подключении Arduino на стороне компьютера создаётся виртуальный COM порт (последовательный порт) - именно так компьютер видит нашу плату:

То есть на стороне микроконтроллера для связи используется UART, а на стороне компьютера - COM порт.
Монитор порта #
Чтобы работать с портом на стороне компьютера, понадобится монитор порта - программа, которая умеет отправлять и читать данные с COM порта. Таких программ много и подойдёт любая, но в Arduino IDE есть свой встроенный монитор порта, что очень удобно. Чтобы начать работу, нужно выбрать порт (тот же, что был выбран для загрузки прошивки) и открыть монитор:

Монитор порта в Arduino IDE v1

Монитор порта в Arduino IDE v2
В окне монитора порта есть несколько важных настроек:
- Скорость - скорость связи. Должна совпадать со скоростью UART, установленной в программе на микроконтроллере. Обычно
9600или115200бод - Конечный символ или терминирующий символ, конец строки - будет добавляться в конце текста при отправке из окна монитора порта
Serial #
Для работы с портом на стороне Arduino используется системный объект Serial, он позволяет читать и отправлять данные.
Все возможности Serial можно посмотреть в документации на класс Stream - Serial его наследует
Перед началом работы нужно запустить связь с указанием скорости - метод begin:
void setup() {
Serial.begin(115200); // запустить связь на скорости 115200 бод
}
Также можно остановить общение по Serial:
Serial.end();
Отправка #
Serial может отправлять данные из программы в монитор порта или на другие МК и микросхемы, для этого используются методы print(любые данные) и println(любые данные) - первый просто печатает данные, второй печатает и переносит строку. Давайте напишем классический пример, который выводит строку "Hello, World!" в консоль:
void setup() {
Serial.begin(115200);
Serial.println("Hello, World!");
}
void loop() {
}
Загрузите программу и откройте монитор порта на скорости 115200:

Данный код выполнится один раз при запуске МК. Нажмите на плате кнопку Reset (перезагрузка) несколько раз - после каждого нажатия в порт снова выведется строка
Ещё несколько примеров:
void setup() {
Serial.begin(115200);
Serial.print("Hello"); // строка
Serial.print(','); // символ
Serial.println(" World!"); // строка с переносом
Serial.println(12345); // число по основанию 10 (по умолчанию)
Serial.println(12345, BIN); // число по основанию 2
Serial.println(3.1415); // десятичная дробь, точность 2 знака по умолчанию
Serial.println(3.1415, 4); // десятичная дробь, точность 4 знака
Serial.println(); // просто перевод строки
}
void loop() {
}

В дальнейших уроках мы будем пользоваться выводом в Serial для визуализации того, что происходит в программе.
Табуляция #
Символ табуляции '\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:
Байты и символы #
Сам механизм передачи UART отправляет по 1 байту данных за раз. Здесь нужно понимать, что передаются просто бинарные данные, то есть буквально числа от 0 до 255. Символы в программе - это тоже для процессора просто числа. Чтобы человек мог "общаться" с машиной, была придумана таблица ASCII и другие кодировки (подробнее - в уроке про строки), они ставят в соответствие число для машины и символ для человека. Таким образом, символ 'f' для процессора это просто число 102 - он про символы ничего не знает:
Serial позволяет отправлять как символы, так и бинарные данные, а вот монитор порта выводит все поступающие данные как символы - читаемый текст:
int i = 'f'; // здесь i == 102
Serial.write(i); // отправить бинарные данные
Serial.write(102); // отправить бинарные данные
Serial.write('f'); // отправить бинарные данные
Serial.print('f'); // напечатать символ
Во всех случаях монитор порта выведет символ f, т.е. получится ffff. И наоборот:
int i = 'f'; // i == 102
Serial.print(i); // напечатать
В порт будет выведено число 102, потому что метод print() именно печатает данные - он буквально разделяет число на символы, в данном случае '1', '0' и '2', и отправляет их по одному через write(). Таким же образом print() выводит строки и другие данные - посимвольно. Это очень важный момент, многие новички в этом путаются.
Чтение #
В порт также можно отправлять данные извне и читать их из программы. Отправителем в Serial может быть окно монитора порта или другой МК/микросхема, например GPS или GSM модем. Эта глава немного забегает вперёд, но должна быть здесь. Возвращайтесь к ней позже, если что-то будет непонятно.
Для проверки наличия входящих данных используется метод available() - он вернёт количество входящих байт. В таком условии можно проверить, что есть хотя бы один байт для чтения:
void loop() {
if (Serial.available()) {
// есть входящие данные
}
}
И именно в этом условии нужно читать данные с порта.
Пока данные не прочитаны, они так и будут ожидать в приёмном буфере на стороне МК. Если буфер переполнится - новые входящие данные будут теряться!
Символы #
Давайте отправим обратно в порт принятые символы, для этого их сначала нужно прочитать - метод read(). Он возвращает тип int, поэтому "печатать" его напрямую нельзя - будут выведены коды символов, как объяснялось выше:
void setup() {
Serial.begin(115200);
}
void loop() {
if (Serial.available()) {
Serial.println(Serial.read()); // печать int!
}
}
Если прочитать данные, когда их никто не отправил (приёмный буфер пуст, available() равен 0) - read() вернёт -1. Именно для этого здесь используется тип int - проверить корректность данных
Отправьте текст abc в поле ввода монитора порта, он вернётся как коды символов 97, 98, 99 - соответствуют кодам символов из таблицы ASCII.
Здесь важна настройка конца строки в окне монитора порта - если он включен, то в конце данных будет выведен его код. Например у переноса строки NL это 10, т.е. после отправки abc получим в мониторе 97, 98, 99, 10
Чтобы отправить обратно именно символы, нужно преобразовать чтение к символьному типу - (char)Serial.read(), либо отправлять через write():
void setup() {
Serial.begin(115200);
}
void loop() {
if (Serial.available()) {
// отправка обратно как символ
Serial.print((char)Serial.read());
// или так
// Serial.write(Serial.read())
}
}
Отправьте что-нибудь в поле ввода монитора порта - этот текст придёт обратно.
Здесь важна настройка конца строки в окне монитора порта - если включить NL (New Line), то каждая отправка будет выводиться на новой строке, т.к. символ переноса будет отправляться обратно в монитор и переносить строку. Если отключить - всё отправленное будет выводиться в одну строку
Строки #
Текст в монитор порта отправляется и принимается посимвольно - в примере выше мы его посимвольно принимаем и выводим. Можно прочитать весь отправленный текст как String-строку (читай урок про String):
void setup() {
Serial.begin(115200);
}
void loop() {
if (Serial.available()) {
String s = Serial.readString();
Serial.println(s);
}
}
Отправьте текст в поле ввода - он придёт обратно, но с ощутимой задержкой. Метод readString() является блокирующим - он собирает строку посимвольно, ожидая поступления новых символов. Если после получения символа проходит тайм-аут - передача считается завершённой и возвращается строка. Тайм-аут можно настроить - Serial.setTimeout(миллисекунды), по умолчанию установлен 1000 мс. Поставьте например 50:
void setup() {
Serial.begin(115200);
Serial.setTimeout(50);
}
void loop() {
if (Serial.available()) {
String s = Serial.readString();
Serial.println(s);
}
}
Теперь между отправкой в монитор и получением текста обратно не будет такой заметной паузы.
Ещё один вариант - терминирующий символ. Он отправляется в конце текста и будет сигналом конца строки, чтобы МК не ждал тайм-аут. Чтение строки такого формата выполняется методом readStringUntil(символ), пусть таким символом будет точка с запятой - ';':
void setup() {
Serial.begin(115200);
// стандартный тайм-аут
}
void loop() {
if (Serial.available()) {
String s = Serial.readStringUntil(';');
Serial.println(s);
}
}
Отправьте на МК какой-нибудь текст, например test - он вернётся с задержкой. А если отправить test; - без задержки.
Не забываем, что в мониторе порта можно настроить "конец строки" - поставьте NL, а в качестве терминирующего символа в программе - '\n' (Serial.readStringUntil('\n');). Теперь любой отправленный текст будет приниматься без задержки.
Числа #
Когда мы отправляем в монитор порта число 1234, то МК получит по очереди символы '1', '2', '3', '4'. Как преобразовать их обратно в численный тип?
- Можно принять символы и собрать их в число вручную
- Можно прочитать строку через
readString()как выше, затем вывести из строки черезtoInt()илиtoFloat():
void setup() {
Serial.begin(115200);
Serial.setTimeout(50);
}
void loop() {
if (Serial.available()) {
String s = Serial.readString();
int i = s.toInt();
Serial.println(i);
}
}
- Можно использовать встроенный парсер Serial -
parseInt()илиparseFloat():
void setup() {
Serial.begin(115200);
Serial.setTimeout(50);
}
void loop() {
if (Serial.available()) {
// int числа
int i = Serial.parseInt();
Serial.println(i);
// float числа
// float f = Serial.parseFloat();
// Serial.println(f);
}
}
Для отправки и парсинга чисел отключите "конец строки" в мониторе порта, иначе лишние символы будут прочитаны как дополнительно отправленное число 0
Какой из способов оптимальнее? Если числа отправляются человеком из монитора порта, то эффективнее будет настроить в мониторе конец строки NL, в программе читать в строку через readStringUntil('\n'), а затем выводить в число через toInt()/toFloat(). Таким образом мы не будем ждать таймаута, как в случае с parseInt()/parseFloat().
Парсинг пакетов* #
При помощи Stream-инструментов можно очень просто разбирать несложные текстовые протоколы связи, например пакеты вида "ключ:значение<перенос строки>": сначала читаем до двоеточия, затем до символа переноса строки:
void loop() {
if (Serial.available()) {
String key = Serial.readStringUntil(':');
String val = Serial.readStringUntil('\n');
Serial.println(key);
Serial.println(val);
Serial.println();
}
}
Нужно включить отправку NL в настройках монитора порта
Такой протокол можно использовать для управления устройством через монитор порта по проводу с компьютера или по Bluetooth со смартфона, например для включения/выключения светодиода на пине 13 (бортовой светодиод на Arduino Nano) и настройки яркости светодиода на пине 3:
void setup() {
Serial.begin(115200);
pinMode(3, OUTPUT);
pinMode(13, OUTPUT);
}
void loop() {
if (Serial.available()) {
String key = Serial.readStringUntil(':');
String val = Serial.readStringUntil('\n');
if (key == "led") {
digitalWrite(13, val.toInt());
} else if (key == "pwm") {
analogWrite(3, val.toInt());
} else if (key == "echo") {
Serial.println(val);
}
}
}
led:1- светодиод включится,led:0- выключитсяpwm:100- запустит ШИМ на пине 3 со значением100echo:hello!- отправит обратно в порт текстhello!
Получился очень простой инструмент для тестирования программы или электронного устройства без нужды в кнопках и крутилках.
Продолжение темы с этим протоколом читай в уроке про хэш-строки, там рассказано об оптимизации сравнения строк
Для эффективного и асинхронного обмена данными между устройствами можно использовать мою библиотеку StreamPacket - она позволяет отправлять и принимать бинарные данные (не строки) в виде пакетов, а также контролирует целостность передачи
Дополнительно #
Дополнительный контент доступен владельцам набора GyverKIT и по подписке, подробнее читай здесь. Блок содержит:
- Тезисы, Примеры, Задания
- 2 блоков кода
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками




