Метеостанция на подоконнике прожила у меня две недели в режиме «снимаю показания кабелем»: датчик CO2 MH-Z19 требует собственный UART, а на Uno он один. Пины D0 и D1 — это и есть Serial, тот самый, через который плата разговаривает с компьютером, и я воткнул TX датчика прямо в D0. Некоторое время я даже любовался цифрами в мониторе. Потом пришло время обновить скетч — и заливка упала. Не один раз, а пять попыток подряд: загрузчик рапортовал про таймауты, светодиод L нервно мигал. Датчик, сидящий на пинах прошивки, слушал загрузчик и лез в разговор со своими байтами.
Тогда я нашёл «умный» выход: перед каждой прошивкой выдёргивать два провода. Неделю я честно дёргал. Потом однажды забыл — и полчаса искал, почему плата «сломалась», хотя была цела и невредима, просто её порт снова был занят не тем, кем надо. Проблема в архитектуре Uno, а не в моих руках: аппаратный UART у неё один, и он же обслуживает USB-переводчик, через который грузится каждый скетч. Мы привыкли, что монитор порта читает всё, что печатает плата, — так вот, канал этот один, и делиться им с датчиком не хочется никому.
Ответ старше Arduino на десятилетия: второй порт, который не мешает прошивке. Аппаратный у Uno один, зато программный можно вырастить почти на любом пине: библиотека SoftwareSerial.h из комплекта IDE поднимает полноценный Serial на двух цифровых пинах по вашему выбору. Скорость — скромнее, дисциплина в скетче — строже, но для датчика, GPS-приёмника или Bluetooth-модуля этого хватает с запасом. В этом уроке ставим второй порт, учимся не путать RX и TX и честно считаем, сколько байт он успевает принять, пока loop делает свои дела.
Почему пины 0 и 1 — не вариант
Аппаратный UART Uno — это пины D0 (RX) и D1 (TX). Через них же работают USB-чип и загрузчик: при каждой заливке по линии летит около 115200 бод прошивки, и любой собеседник на этих пинах получает её фрагменты вместо ожидаемых данных, а загрузчик — чужие байты вместо своих. Отсюда правило, которое я вывел за тот вечер с таймаутами: периферию на D0/D1 не вешаем вообще. Ни датчик, ни GPS, ни дисплей с UART-интерфейсом. Два провода, выдёргиваемые перед прошивкой, — это не решение, а залог новых забытых проводов.
SoftwareSerial идёт другим путём: у него нет аппаратного передатчика, обычный цифровой пин переключается программно, с точностью до долей микросекунды. Приём ведётся по прерыванию от смены уровня на пине — механизм из той же семьи, что и прерывания attachInterrupt, только спрятан он внутри библиотеки. Вы пишете знакомые bt.available(), bt.read() и bt.println(), а библиотека щёлкает пинами сама. Ставится она одним include; если вы ещё не трогали сторонние библиотеки, порядок действий разложен в уроке про библиотеки в IDE, но SoftwareSerial к ним не относится — она уже в комплекте.
Объявление — одна строка: SoftwareSerial bt(10, 11). Порядок аргументов — RX, TX: сначала пин, на котором порт слушает, потом пин, на котором говорит. bt.begin(9600) — и дальше API совпадает с Serial один в один: print, println, read, available, readStringUntil — всё то же, что мы прошли в уроке про чтение данных из порта. Значит, командный словарь из урока про парсер пересаживается на второй порт почти без правок: поменять имя объекта, и всё.
Крест-накрест и скорости
Соединение — крест-накрест: TX одного конца идёт в RX другого. Мой личный тупик длился один вечер: я подключил RX модуля к пину 11 — «ну это же тоже передача, что-то общее» — и слушал тишину, пока не взял карандаш и не нарисовал два бокса со стрелками. Стрелки пересекаются. Всегда. TX модуля — в пин 10 (RX скетча), RX модуля — в пин 11 (TX скетча), земля — общая, без неё порт читает кашу. Пины выбираем любые цифровые от 2 до 12: 0 и 1 заняты аппаратным портом. Аналоговые входы A0–A5 тоже годятся — digitalWrite на них работает, хотя выкидывать аналоговые каналы ради порта расточительно; я держал второй порт на 10 и 11 годами и ни разу не пожалел об этой паре.
Со скоростями честнее сразу: чем выше бод, тем точнее должен щёлкать пин, а точность программного порта конечна. На передачу SoftwareSerial дотягивает до 115200, но приём выше 38400 уже сыплет битыми байтами — цифры в таблице проверены на Uno с Bluetooth-модулем и GPS-приёмником. Для HC-05 из следующего урока берите 9600: заводская скорость этого модуля в режиме данных именно такая.
| Скорость | Один байт | Вердикт для SoftwareSerial |
|---|---|---|
| 9600 | около 1 мс | рабочая скорость: модули, датчики, отладка |
| 19200 | около 0,5 мс | нормально, если в loop нет длинных пауз |
| 38400 | около 0,26 мс | предел для приёма: ещё ловит, но без запаса |
| 57600 | около 0,17 мс | передача работает, приём ловит мусор |
| 115200 | около 0,09 мс | только отправка: приём здесь уже не про данные |
#include <SoftwareSerial.h> SoftwareSerial bt(10, 11); // RX, TX: 10 - слушаем модуль, 11 - говорим ему void setup() { Serial.begin(9600); // порт компьютера: монитор и прошивка bt.begin(9600); // второй порт: датчик, GPS или Bluetooth Serial.println("Most: monitor <-> bt");} void loop() { if (Serial.available() > 0) { // вы набрали строку в мониторе bt.write(Serial.read()); // уходит во второй порт байт за байтом } if (bt.available() > 0) { // модуль ответил Serial.write(bt.read()); // ответ виден в мониторе }}Грабли: один слушатель, delay и дорогой TX
Первая грабля проявляется, когда портов становится три. SoftwareSerial умеет несколько экземпляров, но слушать в каждый момент может только один — тот, что вызвал listen(). Данные неслушающего порта теряются молча: без ошибок, без уведомлений. Я собрал GPS и Bluetooth на одной плате и полдня не понимал, почему GPS молчит, а телефон работает: оба порта были подключены, но слушал только тот, кто начал последним. listen() — это переключатель канала, а не разветвитель.
Вторая грабля — время. Буфер приёма у SoftwareSerial — 64 байта, те же, что мы считали в уроке про буфер UART. При 9600 бод буфер наполняется за 64 символа, то есть примерно за 67 мс. Мой delay(100) между опросами датчика гарантированно терял хвост строки: пока loop спал, порт принимал, а потом очередь переворачивалась. Лечение одно: миллисекундный таймер вместо delay и bt.read() в каждом обороте loop.
Третья грабля — на отправке. TX у SoftwareSerial не буферизуется: println из тридцати символов при 9600 бод занимает около 31 мс, и всё это время библиотека держит прерывания, чтобы не смазать фронты битов. millis() за эти тридцать одну миллисекунду отстаёт ровно на столько же. Для строки раз в секунду это незаметно; для печати в горячем цикле — уже тормоз. Отсюда моё правило: во второй порт пишем редко и осмысленно, в монитор — сколько влезет.
Практика: говорим с модулем
У симулятора этого самоучителя есть честное ограничение: SoftwareSerial в движке не поддержан, второй порт он не изображает. Поэтому тренажёр проверяет не пины, а то, ради чего SoftwareSerial вообще заводят: протокол «команда — ответ» с другим собеседником. Скетч ниже эмулирует модуль на втором порту: вы печатаете команду в монитор — как будто переслали её через мост из скетча выше, — а «модуль» отвечает строкой с парой имя=значение, латентностью ms= и счётчиком принятых байтов bajt=. На реальной плате между этими строчками и встаёт SoftwareSerial bt(10, 11).
Команды три: ping — проверка живости, temp — показание датчика, ver — версия прошивки модуля. Незнакомая строка получает err neizvestno — так же, как отвечал настоящий парсер. И посмотрите на ms=: на 9600 бод команда temp из четырёх символов едет до модуля около четырёх миллисекунд, ответ — столько же обратно; симулятор показывает меньше, потому что эфир в нём не эмулируется, зато пары имя=значение строятся в плоттере рядами без единой правки.
- Посчитайте на бумаге: строка ответа из симулятора — около 30 символов. Сколько миллисекунд она займёт на 9600 бод при отправке из SoftwareSerial и на сколько за это время отстанет millis()? Сверьте с третьей граблёй.
- Соберите мост из скетча-кода на реальной плате с любым текстовым модулем и проверьте, что байты ходят в обе стороны без правок: это базовая диагностика на все случаи жизни.
- Пересчитайте время переполнения буфера для 19200 бод: во сколько раз буфер на 64 символа наполняется быстрее и каким тогда может быть максимальный delay() в loop?
- Добавьте в kodKomandy() четвёртую команду power с ответом pitanie=5: проверьте в симуляторе и прикиньте, что изменится на реальной плате, где за этой строкой стоит SoftwareSerial.
- По таблице скоростей выберите скорость для GPS-приёмника, который шлёт строку в 70 символов раз в секунду, и обоснуйте выбор двумя числами из урока.
- На Uno один аппаратный UART, и он обслуживает USB и прошивку: периферию на D0/D1 не вешаем — второй порт ставим SoftwareSerial-ом на пины 2–12
- SoftwareSerial bt(10, 11): порядок аргументов — RX, TX; соединение всегда крест-накрест, земля общая
- Приём живёт до 38400 бод, дальше битые байты; 9600 — рабочая скорость для модулей и датчиков
- Слушает только один экземпляр порта: listen() — переключатель, а не разветвитель; буфер 64 байта переполняется за ~67 мс при 9600, поэтому delay() в loop теряет байты
- TX программного порта блокирует loop и прерывания: 30 символов при 9600 — около 31 мс отставания millis()
- 1. Почему датчик с UART нельзя сажать на пины D0/D1 Uno?
- 2. SoftwareSerial bt(10, 11) — что значит десятка?
- 3. На Uno два экземпляра SoftwareSerial. Что станет с данными того, что не слушает?
- 4. При 9600 бод в loop стоит delay(100). Что станет с входящим потоком?
- 5. Сколько займёт println из 30 символов на 9600 бод и что будет с millis()?
Частые вопросы по теме урока
- Почему датчик с UART нельзя сажать на пины D0/D1 Uno?
- Аппаратный UART один, и он же обслуживает USB-чип и загрузчик: при заливке по линии летит прошивка на ~115200 бод. Периферия на этих пинах ломает и заливку, и свой протокол. D0 и D1 обслуживают аппаратный Serial, USB и загрузчик: при прошивке линии заняты, датчик мешает заливке и получает мусор
- SoftwareSerial bt(10, 11) — что значит десятка?
- Порядок аргументов — RX, TX: сначала слушаем, потом говорим. Соединение с модулем при этом крест-накрест: TX модуля идёт именно в RX скетча. RX: пин, на котором скетч слушает
- На Uno два экземпляра SoftwareSerial. Что станет с данными того, что не слушает?
- Механика приёма у SoftwareSerial общая: слушать может только один экземпляр. listen() — переключатель канала; данные неслушающего порта гибнут без ошибок. Теряются молча: активный слушатель всегда один, переключает listen()
- При 9600 бод в loop стоит delay(100). Что станет с входящим потоком?
- Пока loop спит, прерывания копят байты в буфер на 64 символа. При 9600 бод это около 67 мс — delay(100) гарантирует переполнение и молчаливую потерю хвоста строки. Байты начнут теряться: буфер на 64 символа переполняется примерно за 67 мс
- Сколько займёт println из 30 символов на 9600 бод и что будет с millis()?
- TX программного порта не буферизуется и закрывает прерывания на время каждого байта. Тридцать символов по ~1 мс — это 31 мс блокировки и столько же отставания millis(). Около 31 мс, и всё это время прерывания закрыты: millis() отстаёт так же