Этот урок — мягкое знакомство с Serial (последовательным портом) на Arduino Uno. Разберём, что это такое, зачем нужен, как правильно читать и отправлять данные, какие тонкости и подводные камни есть у новичков. Сделаем серию мини‑проектов: от «эхо‑терминала» до управления светодиодом и простого парсера команд, плюс «живой» лог датчиков и графики в Serial Plotter.
begin(), available(), read(), readBytesUntil(), parseInt(), print()/println(), flush(), setTimeout().Serial.9600, современный минимум: 115200 (быстро и стабильно). Скорость должна совпадать и в Arduino, и в мониторе порта.// Мини-пример: проверка порта и «приветствие»
void setup() {
Serial.begin(115200);
delay(1500); // дать время Монитору порта подключиться после ресета
Serial.println(F("Hello, Serial! Uno ready."));
}
void loop() {
// пусто
}
Почему delay(1500)? На Uno команда while(!Serial) {} не даёт эффекта (это нужно для плат с «нативным USB»). Небольшая задержка — простой способ увидеть приветствие после открытия порта.
| Что | Зачем | Примечания |
|---|---|---|
Serial.begin(115200) |
Настройка порта | Скорость в бодах должна совпадать с монитором |
Serial.print()/println() |
Вывод текста/чисел | Числа можно печатать в BIN, HEX, DEC |
Serial.available() |
Сколько байт пришло | Проверяйте перед чтением, чтобы не зависать |
Serial.read()/peek() |
Считать байт / посмотреть не читая | Возвращают int (−1 если пусто) |
readBytesUntil(delim, buf, len) |
Читать в буфер до разделителя | Учитывает setTimeout() |
parseInt()/parseFloat() |
Быстро вытащить число из текста | Удобно, но будьте внимательны к тайм‑ауту |
Serial.setTimeout(ms) |
Тайм‑аут для методов чтения | По умолчанию ~1000 мс |
Serial.flush() |
Дождаться отправки данных | Полезно перед резетом/сном |
💡 Экономия ОЗУ: заворачивайте строковые литералы в
F("…"), чтобы хранить их во Flash.
Задача: безопасно собирать символы в строку до переноса \n или \r, защититься от переполнения буфера.
// Эхо-терминал. В Мониторе порта выставьте Newline или Both NL&CR.
const size_t LINE_BUF = 64;
char line[LINE_BUF];
size_t lineLen = 0;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println(F("Echo terminal ready. Type something:"));
}
void loop() {
while (Serial.available()) {
int b = Serial.read();
if (b < 0) break;
char c = (char)b;
if (c == '
') continue; // игнорируем CR
if (c == '
') { // строка готова
line[lineLen] = ' ';
Serial.print(F("You typed: "));
Serial.println(line);
lineLen = 0; // сброс
} else {
if (lineLen < LINE_BUF - 1) {
line[lineLen++] = c;
} else {
// переполнение — сбрасываем строку
lineLen = 0;
Serial.println(F("[warn] line too long, cleared"));
}
}
}
}
Что важно: мы никогда не ждём байт «впустую» — читаем только когда available()>0. Это делает код отзывчивым.
Команды: LED ON, LED OFF, PWM 9 128 (пин, значение 0–255).
// Простой парсер команд (на базе примера эхо-терминала)
const int LED_PIN = 13;
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
delay(1000);
Serial.println(F("Commands: LED ON|OFF, PWM <pin> <0..255>"));
}
void handleLine(const char* s) {
if (strcasecmp(s, "LED ON") == 0) {
digitalWrite(LED_PIN, HIGH);
Serial.println(F("OK: LED=ON"));
} else if (strcasecmp(s, "LED OFF") == 0) {
digitalWrite(LED_PIN, LOW);
Serial.println(F("OK: LED=OFF"));
} else if (strncasecmp(s, "PWM ", 4) == 0) {
int pin, val;
if (sscanf(s + 4, "%d %d", &pin, &val) == 2) {
val = constrain(val, 0, 255);
pinMode(pin, OUTPUT);
analogWrite(pin, val); // PWM на поддерживаемых пинах
Serial.print(F("OK: PWM ")); Serial.print(pin);
Serial.print(F(" = ")); Serial.println(val);
} else {
Serial.println(F("ERR: usage PWM <pin> <0..255>"));
}
} else {
Serial.println(F("ERR: unknown command"));
}
}
Вставьте вызов handleLine(line); туда, где мы заканчиваем строку в примере №1.
parseInt()), тайм‑аут и ловушкиИногда удобно набрать «1234↵» и прочитать сразу число.
void setup() {
Serial.begin(115200);
Serial.setTimeout(200); // не ждём секунду, если пользователь промахнулся
Serial.println(F("Type a number and press Enter:"));
}
void loop() {
if (Serial.available()) {
long n = Serial.parseInt(); // парсит первые цифры из входного буфера
if (n != 0 || Serial.peek() == '0') {
Serial.print(F("You entered: ")); Serial.println(n);
}
// очистим остатки до конца строки
while (Serial.available() && Serial.read() != '
') {}
}
}
⚠️
parseInt()и друзья блокируют до тайм‑аута, если не пришли цифры. Для интерактивных меню лучше строковый парсер (пример №1).
Открой Инструменты → Serial Plotter. Печатайте каждую строку как набор «имя:значение» или просто значения, разделённые табами.
Пример: читаем потенциометр и шлём значения для графика (вкладка Plotter):
const int AIN = A0;
void setup() {
Serial.begin(115200);
}
void loop() {
int x = analogRead(AIN);
// Формат для Plotter: "name value"
Serial.print("pot "); Serial.println(x);
delay(20);
}
Для нескольких линий печатайте: ch1 ch2 ch3 — Plotter нарисует несколько графиков.
Команды: RATE <мс> — шаг опроса; START/STOP — лог в одну строку для Plotter и в читаемый вид для Монитора.
// Логгер A0 с командами: RATE <ms>, START, STOP
const int AIN = A0;
unsigned long period = 50, last = 0;
bool running = true;
void setup() {
Serial.begin(115200);
pinMode(AIN, INPUT);
Serial.println(F("Logger: RATE <ms>, START, STOP"));
}
void handleCmd(const char* s) {
if (strncasecmp(s, "RATE ", 5) == 0) {
unsigned v;
if (sscanf(s + 5, "%u", &v) == 1 && v >= 5 && v <= 2000) {
period = v;
Serial.print(F("OK RATE=")); Serial.println(period);
} else {
Serial.println(F("ERR: 5..2000 ms"));
}
} else if (strcasecmp(s, "START") == 0) {
} else if (strcasecmp(s, "START") == 0) {
running = true; Serial.println(F("OK START"));
} else if (strcasecmp(s, "STOP") == 0) {
running = false; Serial.println(F("OK STOP"));
} else {
Serial.println(F("ERR"));
}
}
// вставьте обработчик строк из примера №1 и вызовите handleCmd(line);
void loop() {
// Ваша обработка строк из примера №1 здесь...
if (running && millis() - last >= period) {
last = millis();
int v = analogRead(AIN);
// Для Plotter:
Serial.print("a0 "); Serial.println(v);
// Для логов (удобно читать человеком):
// Serial.print(F("A0=")); Serial.print(v); Serial.print(F(" @ ")); Serial.println(last);
}
}
Почему плата перезагружается при открытии Монитора порта?
Это нормально для Uno: линия DTR «дёргает» Reset. Если это мешает, ищут аппаратные обходы (например, конденсатор 10 µF между RST и GND), но новичкам лучше так не делать.
while(!Serial) {} зависает?
На Uno «нативного USB» нет, поэтому конструкция бессмысленна. Используйте небольшую delay() после begin().
Можно ли использовать пины 0/1 для общения с другим устройством?
Можно, но во время прошивки и отладки это мешает (идёт конфликт с USB‑UART). Для второго порта берут SoftwareSerial/AltSoftSerial (ограничения по скорости).
Serial.flush() что делает?
Ждёт завершения передачи исходящих данных. Полезно перед сном/перезагрузкой.
Почему текст с русскими буквами «ломается»?
Монитор порта — не поддерживает кириллицу в момент выхода этой статьи. Для кириллицы используйте без BOM, совпадающие кодировки, либо печатайте английский.
Строки и String
Класс String удобен, но может фрагментировать ОЗУ на AVR. Для серьёзных проектов используйте статические буферы (как в наших примерах).
ADD 2 3, MUL 4 5 → печать результата. Обработайте ошибки.THR <0..1023> — порог срабатывания для A0; печатать «ON/OFF» при пересечении порога.A0 и A1) в Serial Plotter.No line ending (например, использовать readBytesUntil() с тайм‑аутом).Serial.available()?Serial.setTimeout() и на какие функции он влияет?print() отличается от println()?(Ответы: 1) Возвращает число байт во входном буфере. 2) Тайм‑аут для блокирующих чтений: readBytes, readBytesUntil, parseInt/Float. 3) println() добавляет перевод строки. 4) Из‑за сигнала DTR USB‑UART чипа, который вызывает RESET. 5) Проверять available(), читать в ограниченный буфер до \n/разделителя, обрабатывать переполнение и тайм‑ауты.)
Serial входит в стандарт Arduino Core для AVR.SoftwareSerial/AltSoftSerial для второго «мягкого» порта (ограничения по скорости/пинам).// Serial Starter Skeleton (безопасное чтение строк, обработчик команд)
const size_t LINE_BUF = 64;
char lineBuf[LINE_BUF];
size_t lineLen = 0;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println(F("Ready. Type help."));
}
void onLine(const char* s) {
if (strcasecmp(s, "help") == 0) {
Serial.println(F("Commands: help, ping, ver"));
} else if (strcasecmp(s, "ping") == 0) {
Serial.println(F("pong"));
} else if (strcasecmp(s, "ver") == 0) {
Serial.println(F("uno-serial-starter v1.0"));
} else {
Serial.println(F("ERR: unknown"));
}
}
void loop() {
while (Serial.available()) {
int b = Serial.read();
if (b < 0) break;
char c = (char)b;
if (c == '
') continue;
if (c == '
') {
lineBuf[lineLen] = ' ';
if (lineLen) onLine(lineBuf);
lineLen = 0;
} else if (lineLen < LINE_BUF - 1) {
lineBuf[lineLen++] = c;
} else {
lineLen = 0;
Serial.println(F("warn: overflow"));
}
}
// ваш основной цикл
}
Урок рассчитан на «самый первый раз»: просто подключите Uno по USB, откройте Инструменты → Монитор порта, поставьте 115200 бод и Both NL & CR, и идите по мини‑практикам сверху вниз. Удачи! ✨