Курс Arduino с нуля · Кнопки

Урок 12. Монитор порта: программа разговаривает с вами

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

Урок 12 из 31⏱ примерно 45 минутОбновлено: 26 июля 2026

Что понадобится

  • Плата Uno из набора
  • Кабель USB
  • Схема из прошлого урока (кнопка и светодиод)

Все детали есть в обучающем наборе, по которому идёт курс.

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

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

Как плата разговаривает

Ничего подключать не нужно: канал связи уже есть. Это тот же USB-кабель, по которому вы прошиваете плату. Он работает в обе стороны — просто до сих пор мы использовали только одну.

светодиоды TX и RX мигают,когда по кабелю идут данныеПлата на связи!нажатие № 1нажатие № 2нажатие № 3Монитор портатот же кабель USB, по которому вы прошиваете платуSerial.print — плата рассказываетSerial.read — плата слушает
По одному кабелю плата и компьютер и прошивают, и переписываются текстом.

Канал называется последовательным портом (по-английски serial — «последовательный»): данные бегут по одному проводу друг за другом, бит за битом. Отсюда и название всех команд — они начинаются со слова Serial. А окно на компьютере, где видно эти строки, называют монитором порта.

Два светодиода, о которых мы молчали. На плате рядом с разъёмом USB есть крошечные светодиоды TX и RX — «передача» и «приём». Они мигают, когда по кабелю идут данные. Раньше вы видели их вспышки только при прошивке; сегодня они замигают от вашей программы.

Шаг 1. Первое слово

Чтобы плата заговорила, нужны две вещи: один раз открыть связь в setup() и дальше печатать что угодно.

void setup() {
  // Открываем связь с компьютером. 9600 — скорость обмена.
  Serial.begin(9600);

  // Всё, что напечатано, уходит по USB-кабелю в монитор порта.
  Serial.println("Плата на связи!");
}

void loop() {
}

Число 9600 — скорость обмена в битах в секунду (её называют «бод»). Само по себе оно ничем не примечательно: важно, чтобы такая же скорость была выбрана в мониторе порта. Иначе компьютер услышит поток символов, но не разберёт ни слова.

Прошейте плату и откройте монитор порта. В Trema IDE это значок с уголком >_ на левой панели или сочетание Ctrl+Shift+M. Убедитесь, что справа сверху выбрана скорость 9600 бод.

Монитор порта в Trema IDE: выбран порт COM11, скорость 9600 бод, в окне идут строки от платы
Монитор порта: сверху выбор порта и скорости, посередине — то, что говорит плата, внизу — поле для ваших команд.

В окне появится строка «Плата на связи!». Она напечаталась ровно один раз, потому что стоит в setup(), а setup() выполняется однократно.

Почему строка появилась именно сейчас? Открытие монитора перезапускает плату — она начинает программу заново, как после включения питания. Это важно помнить: если приветствие печатается в setup(), а монитор открыт позже, вы его не увидите. Нажмите кнопку RES на плате — и строка появится снова.

Вместо текста непонятные значки? Значит, скорость в мониторе не совпадает со скоростью в программе. В Serial.begin(9600) и в окне монитора должно стоять одно и то же число. В Trema IDE есть кнопка «Подобрать скорость» — она переберёт варианты сама.

Шаг 2. Поток строк

Перенесём печать в loop() — и увидим, что программа живёт:

int seconds = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("Считаю секунды:");
}

void loop() {
  seconds = seconds + 1;
  Serial.println(seconds);
  delay(1000);
}

В окне побегут числа: 1, 2, 3… Обратите внимание на светодиоды TX и RX на плате — теперь они подмигивают каждую секунду. Это и есть ваши строки, летящие по кабелю.

Шаг 3. print и println: две почти одинаковые команды

Голые числа мало о чём говорят. Гораздо полезнее подпись — что именно за число мы видим. Для этого есть две команды.

Только printSerial.print(1);Serial.print(2);Serial.print(3);123монитор портаprintlnSerial.println(1);Serial.println(2);Serial.println(3);123монитор порта
print печатает подряд, println после печати переводит строку.

Разница только в переводе строки: println — это «print line», «напечатать строку целиком». Сочетая их, собираем осмысленное сообщение:

int seconds = 0;

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

void loop() {
  seconds = seconds + 1;

  Serial.print("Прошло секунд: ");   // print — курсор остаётся в строке
  Serial.println(seconds);           // println — и переводит строку

  delay(1000);
}

Сначала print печатает подпись и оставляет курсор на месте, потом println дописывает число и переходит на новую строку. Получается «Прошло секунд: 7». Именно так выглядят почти все отладочные сообщения.

Кавычки имеют значение. Serial.println("5") напечатает символ «пять» — то, что в кавычках, выводится как есть. А Serial.println(seconds) без кавычек напечатает значение переменной. Перепутать легко, а результат совсем разный: во втором случае вы увидите слово «seconds», если поставите кавычки по ошибке.

Шаг 4. Заглядываем в кнопку

Схема остаётся ровно той же, что в прошлом уроке, — ничего перекладывать не нужно.

++abcdefghij120121517810123451Кнопка — верхом на канавке: ножки в столбиках 8 и 10, по обе стороны.2Жёлтый провод: вывод 2 платы → столбик 8 сверху от канавки.3Синяя перемычка: столбик 8 снизу от канавки → нижняя шина «−».4Светодиод: вывод 10 → резистор 470 Ом → длинная ножка; короткая — в шину «−».5Синий провод: GND платы → нижняя шина «−».
Та же схема, что и в одиннадцатом уроке: кнопка на выводе 2, светодиод на 10.

Теперь пусть плата докладывает, что происходит с кнопкой. Печатать на каждом проходе loop() нельзя — окно захлебнётся сотнями тысяч строк в секунду. Печатаем только в момент изменения:

const int BUTTON = 2;

int wasState = HIGH;

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON, INPUT_PULLUP);
  Serial.println("Нажимайте кнопку.");
}

void loop() {
  int nowState = digitalRead(BUTTON);

  if (nowState != wasState) {
    if (nowState == LOW) {
      Serial.println("нажата");
    } else {
      Serial.println("отпущена");
    }
    wasState = nowState;
  }
}

Нажимайте кнопку и смотрите в окно: «нажата», «отпущена», «нажата»… Вы впервые видите внутреннее состояние программы, а не его последствия. Дальше в курсе так мы будем проверять каждый новый датчик: сначала печатаем то, что он выдаёт, и только потом что-то с этим делаем.

Шаг 5. Видим дребезг своими глазами

В прошлом уроке мы поверили на слово, что кнопка дребезжит. Теперь можно убедиться. Будем печатать каждое изменение сигнала и время, прошедшее с предыдущего:

const int BUTTON = 2;

int wasState = HIGH;
unsigned long lastTime = 0;

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON, INPUT_PULLUP);
  Serial.println("Каждое изменение сигнала — отдельная строка:");
}

void loop() {
  int nowState = digitalRead(BUTTON);

  if (nowState != wasState) {
    Serial.print("уровень ");
    Serial.print(nowState);
    Serial.print("   прошло мс: ");
    Serial.println(millis() - lastTime);

    lastTime = millis();
    wasState = nowState;
  }
}

Нажмите кнопку один раз. В мониторе с большой вероятностью появится не две строки, а четыре, шесть или восемь — и между ними будет по 1–5 миллисекунд. Вот он, дребезг, в цифрах:

уровень 0   прошло мс: 2411
уровень 1   прошло мс: 2
уровень 0   прошло мс: 3
уровень 1   прошло мс: 1
уровень 0   прошло мс: 4
уровень 1   прошло мс: 587

Первая строка — настоящее нажатие (с прошлого события прошло больше двух секунд). Дальше идут скачки по 1–4 мс — это пружинит контакт. И только последняя строка с большим числом — настоящее отпускание. Ровно от этих быстрых строк нас и защищает антидребезг.

Это и есть отладка. Мы не гадали, что происходит внутри, — мы попросили программу рассказать. Запомните приём: если устройство ведёт себя странно, не вглядывайтесь в код, а напечатайте в порт то, что программа видит на самом деле. В девяти случаях из десяти ответ находится за минуту.

Шаг 6. Счётчик нажатий

Соберём вместе антидребезг из прошлого урока и печать из этого. Заодно у нас появится первый настоящий счётчик:

const int BUTTON = 2;
const int LED = 10;
const unsigned long BOUNCE_MS = 30;

int wasState = HIGH;
unsigned long lastChange = 0;

int count = 0;
bool ledOn = false;

bool buttonPressed() {
  int nowState = digitalRead(BUTTON);
  bool pressed = false;

  if (nowState != wasState) {
    if (millis() - lastChange > BOUNCE_MS && nowState == LOW) {
      pressed = true;
    }
    lastChange = millis();
    wasState = nowState;
  }

  return pressed;
}

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON, INPUT_PULLUP);
  pinMode(LED, OUTPUT);
  Serial.println("Считаю нажатия:");
}

void loop() {
  if (buttonPressed()) {
    count = count + 1;
    ledOn = !ledOn;
    digitalWrite(LED, ledOn);

    Serial.print("нажатие № ");
    Serial.print(count);
    Serial.print(", светодиод ");
    if (ledOn) {
      Serial.println("включён");
    } else {
      Serial.println("выключен");
    }
  }
}

Теперь одно нажатие пальцем — ровно одна строка в мониторе и одно переключение светодиода. Понажимайте два десятка раз и убедитесь, что счётчик не забегает вперёд: это лучшая проверка того, что антидребезг работает.

Шаг 7. Плата слушает компьютер

Связь двусторонняя, и до сих пор мы использовали только половину. Внизу окна монитора есть поле ввода — то, что вы туда напишете и отправите, прилетит в плату. Научим её принимать команды:

const int LED = 10;

void setup() {
  Serial.begin(9600);
  pinMode(LED, OUTPUT);
  Serial.println("Введите 1 — включить, 0 — выключить.");
}

void loop() {
  // Есть ли что-то, что прислал компьютер?
  if (Serial.available() > 0) {
    // Читаем один присланный символ. Символ пишут в одинарных кавычках.
    char command = Serial.read();

    if (command == '1') {
      digitalWrite(LED, HIGH);
      Serial.println("включил");
    }

    if (command == '0') {
      digitalWrite(LED, LOW);
      Serial.println("выключил");
    }
  }
}

Прошейте, наберите в поле ввода 1 и нажмите Enter — светодиод загорится. Наберите 0 — погаснет. Ваш компьютер только что стал пультом.

Здесь три новых слова, разберём их по порядку:

  • Serial.available() — сколько символов уже прилетело и ждёт, когда их прочитают. Если ноль, читать нечего.
  • Serial.read() — забирает один символ из очереди.
  • char — тип переменной для одного символа. Символы пишут в одинарных кавычках: '1'. Двойные кавычки "1" означали бы текст, а не символ, и сравнение не сработало бы.

Символ «1» — это не число 1. Для платы '1' — это код знака, которым мы рисуем единицу (число 49, если совсем честно). Поэтому сравнивать нужно именно с '1' в кавычках. Позже, когда понадобится принимать настоящие числа, мы возьмём другую команду — Serial.parseInt().

Бонус: вкладка «График»

Рядом с монитором в Trema IDE есть вкладка График. Если программа печатает просто числа, по одному в строке, она нарисует из них живой график. Пока нам нечего рисовать, но в блоке про датчики это станет любимым инструментом: температура, освещённость, положение крутилки — всё сразу видно кривой.

Вкладка График в Trema IDE: числа из монитора порта нарисованы кривой во времени
Те же числа, что и в мониторе, — только нарисованные. Удобно ловить, как меняется датчик.

Не работает? Разбираемся

  • Окно пустое, ничего не появляется. Проверьте, есть ли в setup() строка Serial.begin(9600) — без неё плата молчит. И выбран ли в мониторе тот порт, к которому подключена плата.
  • Вместо текста непонятные значки. Скорости не совпадают: в программе одно число, в мониторе другое. Поставьте 9600 в обоих местах.
  • Строка из setup() не видна. Она напечаталась до того, как вы открыли монитор. Нажмите RES на плате — программа начнётся заново.
  • Строк слишком много, ничего не разобрать. Печать стоит в loop() без всяких условий. Печатайте только при изменении или не чаще раза в секунду по часам.
  • Плата перестала прошиваться. Проверьте, не заняли ли вы выводы 0 и 1. Именно через них идёт связь с компьютером: если к ним что-то подключено, и прошивка, и монитор порта начнут сбоить.
  • Команды из поля ввода не срабатывают. Убедитесь, что сравниваете с '1' в одинарных кавычках, и что справа от поля ввода выбрано «Новая строка» или «Нет конца строки» — лишние символы могут мешать.

Задания для тренировки

  • РазминкаПусть плата раз в секунду печатает, сколько секунд она работает. Подсказка: millis() / 1000 — это и есть секунды с момента включения.
  • ПосложнееСделайте секундомер: первое нажатие кнопки — старт, второе — стоп и печать результата в секундах с сотыми долями. Понадобится запомнить millis() в момент старта.
  • Со звёздочкой ⭐Измерьте, сколько раз в секунду на самом деле выполняется loop(). В прошлом уроке мы говорили «сотни тысяч» — проверьте это числом. Разбор ниже.
Показать разбор «сколько раз в секунду работает loop()»

Идея простая: считаем проходы в переменной, а раз в секунду печатаем накопленное число и обнуляем счётчик. Задержек в программе быть не должно — иначе мы измерим не скорость платы, а собственное delay.

unsigned long lastReport = 0;
unsigned long passes = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("Считаю проходы loop() за секунду:");
}

void loop() {
  // Каждый проход — плюс один к счётчику.
  passes = passes + 1;

  // Раз в секунду печатаем результат и начинаем считать заново.
  if (millis() - lastReport >= 1000) {
    Serial.println(passes);
    passes = 0;
    lastReport = millis();
  }
}

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

Полезный побочный эффект: добавьте в loop() одну строку с digitalRead или Serial.print — и посмотрите, как упадёт число. Так можно измерять цену любой команды.

Что мы узнали

  • Serial.begin(9600) в setup() открывает связь с компьютером; та же скорость должна стоять в мониторе порта.
  • Serial.print печатает и остаётся в строке, Serial.println — переводит строку. Вместе они собирают сообщение с подписью.
  • В кавычках печатается текст как есть, без кавычек — значение переменной.
  • Печать в порт — главный инструмент отладки: не гадайте, что внутри программы, — попросите её рассказать.
  • Связь двусторонняя: Serial.available() и Serial.read() принимают команды с клавиатуры компьютера.
  • Выводы 0 и 1 заняты этой связью — не подключайте к ним детали.

У нас есть кнопка, есть светодиод и есть голос. Пора собрать из этого настоящее устройство: в следующем уроке появятся три кнопки и три светодиода, а вместе с ними — приём, который позволяет управлять хоть десятком кнопок, не превращая программу в кашу.

Частые вопросы

Почему в мониторе порта вместо текста непонятные значки?

Скорость в мониторе не совпадает со скоростью в программе. Число в Serial.begin и число в окне монитора должны быть одинаковыми — обычно 9600. В Trema IDE есть кнопка Подобрать скорость, она переберёт варианты сама.

Что означает 9600 бод?

Это скорость обмена: примерно 9600 бит в секунду. Само число ничем не примечательно, важно лишь, чтобы оно совпадало у платы и у монитора. Для текстовых сообщений этой скорости хватает с запасом.

Почему при открытии монитора программа начинается заново?

Открытие монитора порта перезапускает плату — так устроена связь через USB. Поэтому строки из setup появляются каждый раз при открытии окна, а увидеть их можно ещё и нажатием кнопки RES на плате.

Можно ли использовать выводы 0 и 1, если работает монитор порта?

Нет. Именно через выводы 0 и 1 плата общается с компьютером. Если подключить к ним детали, начнёт сбоить и прошивка, и монитор порта. Для своих схем берите выводы со 2 по 13.

← Все уроки курса