Монитор порта


Как мы с вами знаем из урока “О платформе“, на платах Ардуино стоит USB-TTL конвертер, позволяющий микроконтроллеру в текстовом режиме “консоли” общаться с компьютером по последовательному интерфейсу, Serial. На компьютере создаётся виртуальный COM порт, к которому можно подключиться при помощи программ-терминалов порта, и принимать-отправлять текстовые данные. Через этот же порт загружается прошивка, т.к. поддержка Serial является встроенной в микроконтроллер на “железном” уровне, и USB-TTL преобразователь подключен именно к этим выводам микроконтроллера. На плате Arduino Nano это кстати пины D0 и D1.

Именно поэтому пины D0 и D1 на платах Nano/Uno нельзя занимать датчиками или подтягивать к питанию/земле на момент прошивки: микроконтроллер не сможет получить данные и вы получите ошибку загрузки

К этим же пинам можно подключаться при помощи отдельных плат “программаторов”, например на чипах CP2102 или том же CH340 с целью загрузки прошивки или просто общения с платой.

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

Окно монитора порта

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

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

Объект Serial


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

Serial.begin(speed)

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

  • 300
  • 1200
  • 2400
  • 4800
  • 9600 чаще всего используется, можно назвать стандартной для большинства устройств с связью через TTL
  • 19200
  • 38400
  • 57600
  • 115200 тоже часто встречается
  • 230400
  • 250000
  • 500000
  • 1000000
  • 2000000
Serial.end()
Прекратить связь по Serial. Для УНО/НАНО (ATmega328) это позволяет освободить пины 0 и 1 для обычных целей (чтение/запись).
Serial.available()
Возвращает количество байт, хранящихся в буфере (объём буфера 64 байта) и доступных для чтения.
Serial.availableForWrite()
Возвращает количество байт, которые можно записать в буфер последовательного порта, не блокируя при этом функцию записи.
Serial.write(val), Serial.write(buf, len)
Отправляет в порт val численное значение или строку, или отправляет количество len байт из буфера buf. Важно! Отправляет данные как байт (см. таблицу ASCII), то есть отправив 88 вы получите букву X: Serial.write(88); выведет букву X.
Serial.print(val), Serial.print(val, format)

Отправляет в порт значение val – число или строку. В отличие от write выводит именно символы, т.е. отправив 88 вы получите 88: Serial.print(88); выведет 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 позволяет настраивать вывод данных: BIN, OCT, DEC, HEX выведут число в соответствующей системе исчисления, а цифра после вывода 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(val, format)

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

Serial.flush()
Ожидает окончания передачи данных
Serial.peek()
Возвращает текущий байт с края буфера, не убирая его из буфера. При вызове Serial.read() будет считан тот же байт, но из буфера уже уберётся
Serial.read()
Читает и возвращает байт как код символа из таблицы ASCII. Если нужно вернуть цифру, делаем Serial.read() – ‘0’;
Serial.setTimeout(time)
Устанавливает time (миллисекунды) таймаут ожидания приёма данных для следующих ниже функций. По умолчанию равен 1000 мс (1 секунда)
Serial.find(target), Serial.find(target, length)

Читает данные из буфера и ищет набор символов 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(target, terminal)
Читает данные из буфера и ищет набор символов target (тип char) либо терминальную строку terminal. Ожидает окончания передачи по таймауту, либо завершает приём после чтения terminal
Serial.readBytes(buffer, length)
Читает данные из порта и закидывает их в буфер buffer (массив char [] или byte []), также указывается количество байт, который нужно записать – length (чтобы не переполнить буфер)
Serial.readBytesUntil(character, buffer, length)
Читает данные из порта и закидывает их в буфер 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 пробел_или_запятая значениеN перенос_строки, то есть значения выводятся в одну строку, одно за другим по порядку, разделяются пробелом или запятой, и в конце обязательно перенос строки. Давайте выведем несколько случайных величин:

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

byte val1, val2, val3;
uint32_t timer;

void loop() {
  // каждые 300 мс
  if (millis() - timer >= 300) {
    timer = millis();
    val1 = random(100);
    val2 = random(100);
    val3 = random(100);
  }

  // вывод каждые 10 мс
  Serial.print(val1);
  Serial.print(' ');
  Serial.print(val2);
  Serial.print(' ');
  Serial.print(val3);
  Serial.println(' ');
  delay(10);
}

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

Подписи графиков

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

Использование пинов


Как я писал выше, аппаратный Serial имеет выводы на ноги микроконтроллера, для Nano/Uno/Mini это выводы D0 и D1. Можно ли работать с этими пинами, как с обычными цифровыми пинами? При отключенном Serial – можно, при включенном – нет. После вызова Serial.begin() ноги перестают функционировать как цифровые пины в ручном режиме, но после вызова Serial.end() можно снова ими пользоваться!

Отправка и парсинг


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

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

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(" ");
  }
  Serial.println();
}

void loop() {

}

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

Проблемы возникают при попытке принять данные в порт. Дело в том, что метод read() читает один символ, даже если вы отправите длинное число – программа получит его по одной цифре, и составлять число из цифр придётся вручную. Проблема усугубляется тем, что read() читает именно символ, то есть код символа в таблице ASCII.

Посмотрим вот такой пример, в котором в порт отправляются принятые в него данные (так называемое эхо):

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

void loop() {
  if (Serial.available()) {
    Serial.println(Serial.read());
    // выведет в порт 49, если отправить 1
    // выведет 97, если отправить а (англ.)
  }
}

Так как же принять именно цифру? Есть хитрость – вычитать из полученного кода символа код цифры 0, либо сам 0 в виде символа: ‘0’

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

void loop() {
  if (Serial.available()) {
    Serial.println(Serial.read() - '0');
    // выведет в порт 1, если отправить 1
    // выведет в порт 7, если отправить 7
  }
}
Если при парсинге у вас появляются лишние цифры – отключите перевод строки при отправке. Нижнее меню монитора порта, выбрать “Нет конца строки” в селекторе. Смотри самое начало урока.

Также для принятия одиночных чисел у нас есть готовый метод – parseInt()/parseFloat() – для целочисленных и рациональных чисел соответственно. Процесс приёма и расшифровки данных называется парсинг (parsing). Давайте примем в порт число 1234, используя готовый метод парсинга.

void setup() {
  Serial.begin(9600); // открыть порт для связи
  Serial.println("Hello World!"); // отправить
}

void loop() {
  if (Serial.available()) {       // есть что на вход?
    int buff = Serial.parseInt(); // принять в переменную buff
    if (buff == 1234) {           // если приняли 1234
      Serial.println("OK");       // успех
    } else {
      Serial.println("error");    // ошибка 
    }
  }
}

Итак, мы используем конструкцию if (Serial.available()) {} чтобы опрашивать порт только в том случае, если в него что-то пришло. Отправив в порт число 1234 мы получим ответ ОК, отправив любое другое – error. Также вы заметите, что после отправки проходит секунда, прежде чем плата ответит. Эта секунда спрятана внутри метода parseInt, программа ждёт секунду после принятия данных, чтобы все данные успели прийти. Секунда это очень много, достаточно было ждать, скажем, 50 миллисекунд. Это можно сделать при помощи метода setTimeout().

void setup() {
  Serial.begin(9600); // открыть порт для связи
  Serial.setTimeout(50); // установить таймаут
  Serial.println("Hello World!"); // отправить
}

void loop() {
  if (Serial.available()) {       // есть что на вход?
    int buff = Serial.parseInt(); // принять в переменную buff
    if (buff == 1234) {           // если приняли 1234
      Serial.println("OK");       // успех
    } else {
      Serial.println("error");    // ошибка
    }
  }
}

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

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

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


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

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

Также если для вывода вы захотите использовать одинарные или двойные кавычки, или обратный слэш \ – нужно выводить их при помощи соответствующего спецсимвола, иначе ваш вывод “поломается”, я думаю, не нужно объяснять, почему:

  • \" – двойные кавычки
  • \' – апостроф
  • \\ – обратный слэш
  • \0 – нулевой символ
  • \? – знак вопроса

Как использовать? Просто писать в вывод. Например комбинация \r\n переведёт строку и вернёт курсор в левое положение:

Serial.print("Hello, World!\r\nArduino Forever");
// выведет
// Hello, World!
// Arduino Forever
// (на разных строках!)

Именно так кстати и работает функция println(), она просто добавляет вывод \r\nпосле print() =)

Символы табуляции позволят удобно отправлять данные для последующей вставки в excel или другие таблицы. Например выведем несколько степеней двойки в виде таблицы, используя символ табуляции \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

Удобно!

Видео


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