Главная chevron_right Уроки chevron_right Связь chevron_right UART (Universal Asynchronous Receiver-Transmitter) — последовательная связь на Arduino

UART (Universal Asynchronous Receiver-Transmitter) — последовательная связь на Arduino

Подробный разбор протокола UART: как работает последовательная передача данных, формат кадра, скорость обмена и практические примеры для Arduino.

В этой статье разберём основы протокола UART (Universal Asynchronous Receiver-Transmitter) — одного из самых распространённых способов последовательной передачи данных. Именно UART работает «под капотом», когда вы отправляете данные с Arduino на компьютер через метод Serial.print().

UART — один из старейших последовательных протоколов, и несмотря на то что во многих применениях его вытеснили SPI и I2C, он по-прежнему активно используется там, где важны простота, низкая стоимость и лёгкость реализации. Протокол позволяет организовать асинхронную последовательную связь с настраиваемым форматом данных и скоростью передачи.

Для работы с UART в Arduino используется класс Serial, предоставляющий методы чтения и записи данных.

Если хотите сразу перейти к примерам — нажмите here или листайте в конец статьи.

Класс Serial

С помощью класса Serial можно отправлять и принимать данные через USB (соединение с компьютером) или через аппаратные пины RX/TX для связи с другими устройствами.

  • Для отправки данных через USB используется Serial — их можно наблюдать в Serial Monitor среды Arduino IDE.
  • Для отправки данных через пины RX/TX используется Serial1.
  • Платы GIGA R1 WiFi, Mega 2560 и Due также имеют Serial2 и Serial3.

Основные методы класса Serial:

  • begin() — инициализирует последовательный порт с указанной скоростью (baud rate). Чаще всего используют 9600 или 115200.
  • print() — выводит данные в Serial Monitor.
  • println() — выводит данные в Serial Monitor с переводом строки в конце.
  • available() — проверяет, есть ли входящие данные (например, команда из Serial Monitor).
  • read() — считывает данные из последовательного порта.
  • write() — записывает данные в последовательный порт.

Пример инициализации обоих последовательных портов:

Serial.begin(9600); //инициализация связи через USB
Serial1.begin(9600); //связь через пины RX/TX

Класс Serial поддерживается на всех платах Arduino.

Пины UART на Arduino

По умолчанию пины TX/RX на Arduino — это D0(RX) и D1(TX). Некоторые платы имеют дополнительные последовательные порты:

Form FactorRXTXRX1TX1RX2TX2RX3TX3
MKRD0D1
UNOD0D1
NanoD0D1
MegaD0D1D19D18D17D16D15D14

Техническое описание

Как работает UART

UART передаёт данные побитно в виде последовательности: стартовый бит, биты данных, опциональный бит чётности и стоповый бит. В отличие от параллельной передачи, где несколько бит идут одновременно по разным проводам, UART отправляет один бит за раз по одному проводу. Протокол асинхронный — он не использует общий тактовый сигнал, а вместо этого обе стороны заранее договариваются о скорости (baud rate).

Serial / Parallel Communication

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

Как видно на схеме, параллельная передача 8-битного сообщения требует восемь проводов, тогда как последовательная обходится одним кабелем для отправки и одним для приёма.

Важно: для корректной работы UART между устройствами обязательно нужен общий провод GND. Без него устройства не смогут правильно различить уровни HIGH и LOW.

Ключевые компоненты

  • Передатчик — собирает данные, формирует из них последовательный поток и отправляет через пин TX.
  • Приёмник — получает данные через пин RX и преобразует последовательный поток обратно в параллельный формат для хост-системы.
  • Baud rate — определяет скорость передачи данных.

Тайминг и синхронизация

В отличие от синхронных протоколов вроде SPI и I2C, UART работает асинхронно — без общего тактового сигнала. Синхронизация достигается за счёт заранее согласованного baud rate.

Baud Rate

Baud rate — это скорость передачи данных, измеряемая в битах в секунду (bps). Оба устройства — и передатчик, и приёмник — должны работать на одинаковом baud rate, иначе данные будут приняты с ошибками.

Чем выше baud rate, тем быстрее передача, но тем точнее должна быть синхронизация. Слишком высокая скорость при длинных линиях связи или некачественных соединениях приводит к ошибкам. Типичные значения baud rate для Arduino: 9600, 115200, 4800, 57600.

Установка baud rate в коде:

Serial.begin(9600);

Управление потоком (Flow Control)

Flow Control — механизм, позволяющий медленному устройству сигнализировать быстрому о необходимости приостановить передачу, чтобы не потерять данные.

Представьте: передатчик T отправляет непрерывный поток байт приёмнику R, который работает медленнее и не успевает обрабатывать данные. R должен уметь попросить T «подождать».

Существует два основных вида flow control:

  • Аппаратный (Hardware Flow Control) — использует дополнительные провода; уровень сигнала на них указывает, продолжать или остановить передачу.
  • Программный (Software Flow Control) — специальные символы передаются по обычным линиям данных для управления потоком.

Подробнее об управлении потоком UART читайте here.

Формат кадра UART

Каждый кадр UART включает стартовый бит, биты данных, опциональный бит чётности и стоповый бит.

Frame Format

Формат кадра UART: idle → start → data bits → parity → stop

Стартовый бит

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

Start Bit

Стартовый бит в кадре UART — всегда логический 0

Биты данных

Биты данных несут полезную информацию. Наиболее распространённая конфигурация — 8 бит (один байт), но UART поддерживает и другие варианты:

  • 8 бит — самый популярный вариант; позволяет передавать байт данных, включая ASCII-символы и числа.
  • 7 бит — меньше накладных расходов, 128 возможных значений.
  • 6 бит — компактный формат, 64 возможных значения.

Data Bits

Биты данных в кадре UART

Данные кодируются в двоичном формате: каждый бит соответствует степени числа 2. Это делает UART универсальным — от текстовых символов до произвольных бинарных данных.

Важно: передатчик и приёмник должны быть настроены на одинаковое количество бит данных. Несоответствие конфигурации — одна из самых частых причин ошибок в UART-коммуникации.

Бит чётности (Parity)

Опциональный бит чётности используется для обнаружения ошибок передачи. Он обеспечивает, чтобы суммарное количество единичных битов в кадре было чётным (even) или нечётным (odd) — в зависимости от выбранного режима. Если принятое значение не совпадает с ожидаемым, фиксируется ошибка.

Parity Bit

Бит чётности в кадре UART

Стоповый бит

Один или два стоповых бита завершают каждый кадр. Они сигнализируют об окончании передачи байта. Обычно используется один стоповый бит; два применяют там, где нужна повышенная надёжность. Полярность стопового бита зависит от конкретной конфигурации UART.

Stop Bits

Стоповый бит в кадре UART

Serial USB: Примеры

Для передачи данных между Arduino и компьютером подключите плату кабелем USB.

Базовый пример: вывод данных

Этот пример каждую секунду отправляет строку Hello World! с Arduino на компьютер с помощью функции Serial.println().

void setup(){
  Serial.begin(9600); //инициализация последовательной связи на скорости 9600 baud
}

void loop(){
  Serial.println("Hello world!");
  delay(1000);
}

Serial.print() / Serial.println() — основа отладки практически любого Arduino-скетча. Понимая, что происходит на плате, вы можете проектировать программы, которые сообщают о важных событиях.

Чтение данных

Чтобы отправить данные с компьютера на Arduino (из Serial Monitor), используем функции Serial.available() и Serial.read(). Сначала проверяем, есть ли входящие данные, затем считываем и выводим их обратно.

Этот скетч «эхом» возвращает всё, что вы вводите в Serial Monitor.

int incomingByte = 0; // ожидание входящих данных

void setup() {
  Serial.begin(9600); //инициализация последовательной связи на скорости 9600 baud
}

void loop() {
  // отправлять данные только при получении данных:
  if (Serial.available() > 0) {
    // читаем входящий байт:
    incomingByte = Serial.read();

    // выводим полученное:
    Serial.print("I received: ");
    Serial.println(incomingByte, DEC);
  }
}

Практическое замечание: если в Serial Monitor ничего не отображается, убедитесь, что выбранный baud rate в выпадающем меню монитора совпадает со значением в begin(). Это одна из самых частых ошибок новичков.

Примеры с пинами RX/TX

В этом разделе рассмотрим примеры обмена данными между двумя платами Arduino. Соедините TX одной платы с RX другой (и наоборот) по схеме ниже:

Connecting two Arduino boards via UART.

Соединение двух плат Arduino по UART: TX ↔ RX, RX ↔ TX, GND ↔ GND

Передача и приём сообщений

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

String sendMessage;
String receivedMessage;

void setup() {
  Serial.begin(9600);    // инициализация Serial монитора для отладки
  Serial1.begin(9600);   // инициализация Serial1 для отправки данных
}

void loop() {
  while (Serial1.available() > 0) {
    char receivedChar = Serial1.read();
    if (receivedChar == '\n') {
      Serial.println(receivedMessage);  // вывод полученного сообщения в Serial монитор
      receivedMessage = "";  // сброс полученного сообщения
    } else {
      receivedMessage += receivedChar;  // добавление символов к полученному сообщению
    }
  }

  if (Serial.available() > 0) {
    char inputChar = Serial.read();
    if (inputChar == '\n') {
      Serial1.println(sendMessage);  // отправка сообщения через Serial1 с символом новой строки
      sendMessage = "";  // сброс сообщения
    } else {
      sendMessage += inputChar;  // добавление символов к сообщению
    }
  }
}

Для хранения входящих и исходящих сообщений объявляем две переменные типа String:

String sendMessage;
String receivedMessage;

Внутри setup() инициализируем оба порта — Serial и Serial1 — со скоростью 9600 baud:

Serial.begin(9600); 
Serial1.begin(9600);

Чтение сообщений

Основная логика находится в loop(). Если получен новый байт — то есть Serial1.available() больше 0 — начинаем его обработку:

   while (Serial1.available() > 0) {
    ...
  }

Считываем один символ из буфера Serial1:

char receivedChar = Serial1.read();

Если встречается символ новой строки '\n', полученное сообщение выводится в Serial Monitor и переменная receivedMessage сбрасывается в пустую строку для следующего сообщения.

Символ '\n' — это «нажатие Enter». Он сигнализирует о конце введённой строки. Именно по нему мы понимаем, что сообщение отправлено целиком.

 if (receivedChar == '\n') {
   Serial.println(receivedMessage);
   receivedMessage = "";
 }

Отправка сообщений

Проверяем буфер Serial — есть ли там введённый текст (Serial.available() > 0):

if (Serial.available() > 0) {
  ...
}

Когда встречается символ новой строки '\n', сообщение отправляется через Serial1, а переменная sendMessage сбрасывается:

char inputChar = Serial.read();
if (inputChar == '\n') {
  Serial1.println(sendMessage);
  sendMessage = "";
}

Если введённый символ не является символом новой строки, он добавляется к текущему сообщению:

else {
  sendMessage += inputChar;  // добавление символов к сообщению
}

Управление встроенным светодиодом

Следующий пример позволяет управлять встроенным светодиодом через UART-сообщения.

Приёмник (Receiver)

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);      // настройка пина LED как выход
  digitalWrite(LED_BUILTIN, LOW);    // выключить LED

  Serial1.begin(9600);            // инициализация UART со скоростью 9600 baud
}
void loop() {
  while (Serial1.available() >= 0) {
    char receivedData = Serial1.read();   // читаем один байт из буфера и сохраняем в receivedData
    if (receivedData == '1') {
      digitalWrite(LED_BUILTIN, HIGH); // включить LED
    }
    else if (receivedData == '0') {
      digitalWrite(LED_BUILTIN, LOW);  // выключить LED
    }
  }
}

Передатчик (Transmitter)

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);      // настройка пина LED как выход
  digitalWrite(LED_BUILTIN, LOW);    // выключить LED

  Serial.begin(9600);               // инициализация последовательной связи на скорости 9600 baud:
  Serial1.begin(9600);            // инициализация UART со скоростью 9600 baud
}
void loop() {
  if (Serial.read() == '1'){
    Serial1.println('1');
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("LEDS ON");
  }
  else if (Serial.read() == '0'){
    Serial1.println('0');
    digitalWrite(LED_BUILTIN, LOW);
    Serial.print("LEDS OFF");
  }
}

Разбор кода приёмника

Настраиваем встроенный светодиод как OUTPUT и задаём начальное значение LOW:

pinMode(LED_BUILTIN, OUTPUT);  
digitalWrite(LED_BUILTIN, LOW);

Внутри setup() инициализируем UART-соединение с baud rate 9600:

Serial1.begin(9600);

Если получен новый байт — Serial1.available() больше 0 — проверяем его содержимое:

    while (Serial1.available() > 0) {
      ...
    }

Считываем байт через read и проверяем, равен ли он '1'. Если да — включаем светодиод, записывая HIGH:

   char receivedData = Serial1.read();   // читаем один байт из буфера и сохраняем в receivedData
   if (receivedData == '1') {
     digitalWrite(LED_BUILTIN, HIGH); // включить LED
   }

Если байт равен '0' — записываем LOW, выключая светодиод:

   else if (receivedData == '0') {
     digitalWrite(LED_BUILTIN, LOW);  // выключить LED
   }

Разбор кода передатчика

Настраиваем встроенный светодиод как OUTPUT с начальным значением LOW:

pinMode(LED_BUILTIN, OUTPUT);  
digitalWrite(LED_BUILTIN, LOW);

Внутри setup() инициализируем оба порта — Serial и Serial1 — со скоростью 9600 baud. Передатчику нужно соединение с Serial Monitor для ввода команд:

Serial.begin(9600); 
Serial1.begin(9600);

Внутри loop() проверяем, ввёл ли пользователь '1' в Serial Monitor (Serial). Если да — отправляем '1' через Serial1, включая светодиод на приёмнике:

   if (Serial.read() == '1'){
    Serial1.println('1');
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("LEDS ON");
  }

Если введено '0' (Serial) — отправляем '0' через Serial1, выключая светодиод:

   if (Serial.read() == '1'){
    Serial1.println('1');
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("LEDS ON");
  }

Типичные ошибки при работе с UART

  • Несовпадение baud rate — самая частая причина «мусора» в Serial Monitor. Убедитесь, что скорость в коде и в настройках монитора одинакова.
  • Нет общего GND — устройства не смогут правильно интерпретировать уровни сигналов. Всегда соединяйте GND обеих плат.
  • TX к TX, RX к RX — неправильная распиновка. Правило: TX одной платы идёт в RX другой, и наоборот.
  • Использование D0/D1 при загрузке скетча — пины D0 и D1 разделяются с USB-UART преобразователем. Если к ним подключено внешнее устройство, загрузка скетча может не работать. Отключайте внешние устройства перед прошивкой.
  • Переполнение буфера — если приёмник не успевает читать данные, буфер переполняется и данные теряются. Используйте flow control или снижайте скорость передачи.