Инструкции

Serial-порт: обмен данными между платой и компьютером

12 мин чтения·Август 2026·RoboArm R1

Serial — это одновременно канал прошивки, главный инструмент отладки и способ управлять платой с компьютера. Разобраться в нём стоит раньше, чем в чём-либо ещё: без вывода в порт микроконтроллер остаётся чёрным ящиком, который либо мигает, либо нет. Ниже — как настроить, как принимать команды не блокируя программу и что делать с кракозябрами.

Что это такое

Под именем Serial обычно скрывается UART — последовательный интерфейс, передающий байты по двум линиям: TX (передача) и RX (приём). На платах без собственного USB эти линии заведены на мост USB-UART, который показывается компьютеру как COM-порт.

Отсюда важное следствие: выводы UART0 заняты. На AVR-платах это пины 0 и 1, на классическом ESP32 — GPIO 1 и 3. Подключать к ним что-то постороннее нельзя: помешает и прошивке, и отладке.

ℹ️

У чипов с собственным USB — ESP32-S3, ESP32-C3, ESP32-C6, а также Arduino Leonardo и Micro — всё зависит от настройки Tools → USB CDC On Boot. Когда она включена, Serial работает через встроенный USB, а не через UART, и выводов не занимает; физический UART0 при этом доступен отдельно под именем Serial0. Когда выключена — Serial идёт через UART0 и занимает выводы: GPIO 43 и 44 у ESP32-S3, GPIO 21 и 20 у ESP32-C3.

Побочный эффект работы через USB: скорость в Serial.begin() для него формальна — данные идут по USB на полной скорости независимо от указанного числа.

Начало работы

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

void loop() {
  Serial.println("Работаю");
  delay(1000);
}

Аргумент begin() — скорость в бодах. Она должна совпадать со скоростью, выбранной в мониторе порта, иначе вместо текста появятся случайные символы. Практически всегда используют два значения: 9600 — исторический стандарт, ещё встречается в старых примерах, и 115200 — разумный современный выбор, вчетверо быстрее и так же надёжен.

ℹ️

На ESP32 первые строки после включения — это лог самого загрузчика, он печатается на скорости 115200 независимо от вашей настройки. Если выбрать 9600, начало вывода будет выглядеть мусором, а дальше пойдёт нормальный текст. Это не ошибка.

Три метода, которые постоянно путают:

int value = 65;

Serial.print(value);     // отправит текст "65" — два байта: '6' и '5'
Serial.println(value);   // то же плюс перевод строки
Serial.write(value);     // отправит один байт со значением 65 — это символ 'A'

print() превращает число в текст, write() отправляет сырой байт. Для человека, читающего монитор порта, нужен print(). Для бинарного протокола между двумя устройствами — write().

У print() есть второй аргумент — основание системы счисления, удобно при отладке регистров и адресов:

Serial.print(255, HEX);   // FF
Serial.print(255, BIN);   // 11111111

Для форматированного вывода на ESP32 есть printf(), работающий как в обычном C:

Serial.printf("Температура %.1f C, влажность %d%%\n", temperature, humidity);

На AVR-платах Serial.printf() отсутствует — там придётся собирать строку через snprintf() в буфер char[].

Приём данных без блокировки

Начинающие обычно пишут так:

// Плохо: блокирует loop() до перевода строки или таймаута в секунду
String command = Serial.readStringUntil('\n');

Проблема та же, что у delay(): пока метод ждёт данные, программа стоит. Плата не крутит моторы, не читает датчики, не отвечает по сети.

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

const uint8_t bufferSize = 64;
char commandBuffer[bufferSize];
uint8_t bufferLength = 0;

void loop() {
  while (Serial.available() > 0) {
    char incoming = Serial.read();

    if (incoming == '\n' || incoming == '\r') {
      if (bufferLength > 0) {
        commandBuffer[bufferLength] = '\0';
        handleCommand(commandBuffer);
        bufferLength = 0;
      }
    } else if (bufferLength < bufferSize - 1) {
      commandBuffer[bufferLength++] = incoming;
    }
    // символы сверх размера буфера отбрасываем, чтобы не выйти за границу
  }

  // остальная работа устройства продолжается на каждом проходе
}

void handleCommand(const char* command) {
  Serial.print("Получено: ");
  Serial.println(command);
}

Serial.available() возвращает количество принятых байтов, ждущих в буфере, — вызов не блокирует. Цикл while разбирает всё, что пришло, и управление возвращается в loop().

⚠️

Приёмный буфер невелик: на AVR-платах это 64 байта. Если программа надолго уходит в delay() или тяжёлые вычисления, пришедшие за это время данные просто потеряются — без всякого сообщения об ошибке. Ещё одна причина не блокировать loop().

Разбор команд

Когда строка собрана, её обычно надо разделить на команду и аргументы. На AVR это делают функциями стандартной библиотеки C, без String:

void handleCommand(char* command) {
  char* verb = strtok(command, " ");
  if (verb == nullptr) return;

  if (strcmp(verb, "led") == 0) {
    char* argument = strtok(nullptr, " ");
    if (argument != nullptr) {
      digitalWrite(LED_BUILTIN, atoi(argument) != 0);
    }
  } else if (strcmp(verb, "ping") == 0) {
    Serial.println("pong");
  } else {
    Serial.print("Неизвестная команда: ");
    Serial.println(verb);
  }
}

strtok() режет строку по разделителю, strcmp() сравнивает строки — обратите внимание, что сравнение через == для char* сравнивает адреса, а не текст. atoi() превращает текст в число.

Такой текстовый протокол удобен тем, что его можно отлаживать руками из монитора порта: набрал led 1 и увидел результат. Именно так устроены команды управления RoboArm R1.

Настройки монитора порта

В Arduino IDE монитор открывается кнопкой с лупой или сочетанием Ctrl+Shift+M (Cmd+Shift+M на macOS). Два выпадающих списка внизу окна определяют почти всё:

Скорость должна совпадать с указанной в Serial.begin().

Окончание строки (No line ending, Newline, Carriage return, Both NL & CR) определяет, какие символы добавятся к отправляемому тексту. Если плата ждёт команду до перевода строки, а в мониторе стоит No line ending, она не дождётся ничего — классическая причина «плата не отвечает на команды». Ставьте Newline, а в коде обрабатывайте и \n, и \r, как в примере выше.

ℹ️

Открытый монитор порта занимает порт. Пока он открыт, залить скетч не получится — IDE обычно закрывает его сама, но внешние программы и веб-инструменты в браузере этого не делают. Ошибка Access is denied при загрузке — почти всегда именно это.

Дополнительные UART на ESP32

У классического ESP32 три аппаратных UART. Serial занят прошивкой, а Serial1 и Serial2 свободны — на них вешают GPS-модули, датчики, вторую плату:

void setup() {
  Serial.begin(115200);                             // отладка через USB
  Serial2.begin(9600, SERIAL_8N1, 16, 17);          // RX на GPIO 16, TX на GPIO 17
}

void loop() {
  while (Serial2.available() > 0) {
    Serial.write(Serial2.read());                   // всё из Serial2 — в монитор порта
  }
}

Третий и четвёртый аргументы begin() — номера выводов RX и TX. Их можно назначить почти на любые свободные пины: внутри чипа стоит коммутационная матрица. Какие выводы свободны, разобрано в статье Пины ESP32.

Параметр SERIAL_8N1 — формат кадра: 8 бит данных, без контроля чётности, один стоповый бит. Это значение по умолчанию, менять его приходится редко.

На AVR-платах дополнительные UART есть только у Mega 2560 (Serial1Serial3). У UART у UNO и Nano всего один, и он занят.

Serial из браузера

Современные браузеры на движке Chromium умеют открывать последовательный порт напрямую через Web Serial API — без установки программ. Так работает наш веб-терминал: можно подключиться к плате, отправить команду и увидеть ответ, ничего не устанавливая.

Ограничения: нужен Chrome, Edge или другой браузер на Chromium, и страница должна быть открыта по HTTPS или с localhost. Safari и Firefox Web Serial не поддерживают.

Частые проблемы

В мониторе кракозябры. Скорость в мониторе не совпадает со скоростью в Serial.begin(). Если совпадает, но мусор идёт только в первых строках после сброса ESP32 — это лог загрузчика, так и должно быть.

Пусто, хотя скетч печатает. Проверьте, что вызван Serial.begin(), и что выбран правильный порт. На платах с нативным USB (ESP32-S3, ESP32-C3, Leonardo) порт исчезает и появляется заново при сбросе — монитор нужно переоткрыть.

Плата не отвечает на команды. В мониторе стоит No line ending, а код ждёт перевод строки. Или наоборот: код сравнивает строку целиком, а монитор дописал \r, которого код не ожидал.

Порт занят, скетч не заливается. Закройте монитор порта и все внешние программы, подключённые к плате, включая вкладки браузера с веб-терминалом.

Данные теряются. Программа блокируется в delay() дольше, чем заполняется приёмный буфер. Переписывайте на неблокирующую схему — см. Язык Arduino.

Что дальше

Рекомендуемые продукты

Связанные статьи