Universal Asynchronous Receiver-Transmitter (UART)
Последовательный протокол связи для передачи данных через USB или пины TX/RX.
Автор: Hannes Siebeneicher
В этой статье вы познакомитесь с основами Universal Asynchronous Receiver-Transmitter (UART) — последовательного протокола связи, который используется для передачи данных между платой Arduino и другими устройствами. Именно этот протокол задействуется при отправке данных с Arduino на компьютер с помощью классического метода:
Serial.print()
UART — один из наиболее распространённых протоколов последовательной (device-to-device) связи. Он используется платами Arduino для взаимодействия с компьютером. Протокол обеспечивает асинхронную последовательную передачу данных, при которой формат данных и скорость передачи настраиваются гибко. UART является одним из старейших последовательных протоколов, и хотя во многих областях его уже вытеснили SPI и I2C, он по-прежнему широко применяется в приложениях с невысокой скоростью и небольшим потоком данных, поскольку отличается простотой, низкой стоимостью и лёгкостью реализации.
Связь через UART обеспечивается классом Serial, который предоставляет множество методов для чтения и записи данных.
Совет
Если вы хотите сразу перейти к примерам, прокрутите страницу до раздела Примеры Serial через USB.
Класс Serial
С помощью класса Serial можно отправлять и получать данные между Arduino и компьютером по USB, а также с устройствами, подключёнными к пинам RX/TX платы.
При передаче данных через USB используется
Serial. Эти данные можно просматривать в Мониторе порта в Arduino IDE.При передаче данных через пины RX/TX используется
Serial1.Платы GIGA R1 WiFi, Mega 2560 и Due также поддерживают
Serial2иSerial3.
Класс Serial предоставляет несколько методов, среди которых основными являются:
begin()— инициализирует последовательную связь с заданной скоростью (скоростью в бодах); в большинстве примеров используются значения9600или115200.print()— выводит данные в Монитор порта.println()— выводит данные в Монитор порта и добавляет символ новой строки.available()— проверяет наличие доступных данных в последовательном буфере (например, команды, отправленной из Монитора порта).read()— считывает данные из последовательного порта.write()— записывает данные в последовательный порт.
Например, для инициализации последовательной связи на обоих портах нужно написать следующее:
Serial.begin(9600); // инициализация связи через USB
Serial1.begin(9600); // связь через пины RX/TX
Важно
Класс Serial поддерживается на всех платах Arduino.
Пины UART на Arduino
Пины TX/RX по умолчанию на платах Arduino — это D0 (RX) и D1 (TX). Некоторые платы имеют дополнительные последовательные порты, как показано в таблице ниже:
Форм-фактор |
RX |
TX |
RX1 |
TX1 |
RX2 |
TX2 |
RX3 |
TX3 |
|---|---|---|---|---|---|---|---|---|
MKR |
D0 |
D1 |
||||||
UNO |
D0 |
D1 |
||||||
Nano |
D0 |
D1 |
||||||
Mega |
D0 |
D1 |
D19 |
D18 |
D17 |
D16 |
D15 |
D14 |
Технические характеристики
Принцип работы UART
UART передаёт данные в виде последовательности битов, включающей стартовый бит, биты данных, необязательный бит чётности и стоповый бит (или биты). В отличие от параллельной передачи данных, при которой несколько битов передаются одновременно, UART отправляет данные последовательно — по одному биту за раз. Как следует из названия, протокол работает асинхронно, то есть не опирается на общий тактовый сигнал. Вместо этого он использует заранее согласованные скорости передачи (baud rate) для определения временны́х интервалов битов данных.
Последовательная и параллельная передача данных
Как видно на рисунке выше, при параллельной передаче 8-битного сообщения потребуется восемь кабелей, тогда как последовательная передача требует лишь один кабель для отправки сообщений и один для их приёма.
Важно
Не забудьте подключить общий потенциал (GND) между устройствами — он необходим для определения высоких и низких сигналов в UART-связи. Без общего потенциала устройства могут неправильно интерпретировать передаваемые данные.
Компоненты
Ключевые компоненты UART — передатчик, приёмник и скорость передачи (baud rate). Передатчик собирает данные из источника, форматирует их в последовательные биты и отправляет через пин TX (Transmit). Приёмник принимает данные через пин RX (Receive), обрабатывает входящие последовательные данные и преобразует их в параллельную форму для хост-системы. Скорость передачи определяет быстроту обмена данными.
Синхронизация и тайминг
Синхронизация и тайминг — важнейшие аспекты UART-связи. В отличие от синхронных протоколов последовательной связи, таких как SPI и I2C, UART работает асинхронно: он не опирается на общий тактовый сигнал для координации передачи данных. Вместо этого для определения временны́х интервалов битов используются заранее согласованные скорости передачи.
Скорость передачи (Baud Rate)
Скорость передачи — фундаментальный параметр UART-связи. Он определяет скорость передачи данных по каналу связи. Baud rate задаётся в битах в секунду (бит/с) и представляет количество битов, передаваемых за одну секунду. В UART оба устройства — передающее и принимающее — должны использовать одинаковую скорость передачи для успешной связи.
Значимость baud rate состоит в его прямом влиянии на скорость передачи данных. Более высокая скорость позволяет передавать данные быстрее, но при этом требует более точной синхронизации между отправителем и получателем. Более низкая скорость может быть достаточной для приложений, где точность синхронизации менее критична, однако приводит к медленной передаче данных. При программировании Arduino наиболее распространёнными значениями baud rate являются 9600, 115200, 4800 и 57600. В коде скорость задаётся следующим образом:
Serial.begin(9600);
Управление потоком данных (Flow Control) в UART
Управление потоком данных (Flow Control) в UART — это метод, позволяющий медленным и быстрым устройствам обмениваться данными через UART без риска потери данных. Рассмотрим случай, когда два устройства обмениваются данными через UART. Передатчик T отправляет длинный поток байтов получателю R. R — более медленное устройство, чем T, и R не успевает обрабатывать данные. Ему нужно либо обработать полученные данные, либо освободить буферы, прежде чем продолжать приём.
R должен сообщить T о необходимости приостановить передачу. Именно для этого и существует управление потоком данных. Flow Control предоставляет дополнительные сигналы, информирующие передатчик о необходимости остановить (приостановить) или возобновить передачу.
Существует несколько видов управления потоком данных. Например, аппаратное управление потоком использует дополнительные провода, логический уровень на которых определяет, должен ли передатчик продолжать отправку данных или нет. При программном управлении потоком по обычным линиям данных передаются специальные символы для запуска или остановки передачи.
Подробнее об управлении потоком данных UART можно прочитать здесь.
Сообщения UART
В UART-связи каждый кадр данных заключён между стартовым и стоповым битами. Эти биты играют ключевую роль в определении границ передачи данных и обеспечении синхронизации между отправителем и получателем.
Формат кадра
Формат кадра
Стартовый бит
Один стартовый бит передаётся в начале каждого UART-кадра. Основная цель стартового бита — указать на начало передачи данных и подготовить приёмник к получению данных.
Стартовый бит в UART всегда имеет логический низкий уровень (0). Это означает, что стартовый бит передаётся как уровень напряжения ниже порога логического высокого уровня, как правило, на стороне приёмника.
Когда приёмник обнаруживает стартовый бит, он знает, что начинается новый кадр данных, и готовится к приёму входящих битов.
Стартовый бит
Биты данных
Биты данных — фундаментальный компонент UART-связи, поскольку они несут фактическую информацию для передачи. Количество битов данных в кадре UART может варьироваться, однако наиболее распространённой и широко используемой конфигурацией является 8 бит. Тем не менее UART поддерживает различные размеры символов, включая 7-битную и 6-битную конфигурации, в зависимости от конкретных требований приложения.
Биты данных
Размер символа (Character Size)
Размер символа в UART-связи определяется количеством битов данных в кадре. Важно выбрать подходящий размер символа в соответствии с требованиями к передаваемым данным. Ниже приведены наиболее распространённые конфигурации размера символа:
8 бит: Это наиболее распространённый размер символа в UART-связи. Он позволяет передавать один байт данных, который может представлять широкий диапазон значений, включая символы ASCII, числовые значения и другие.
7 бит: В случаях, когда требуется уменьшить размер данных, используется 7-битный размер символа. Он подходит для приложений, требующих меньших накладных расходов на данные, и позволяет представить 128 различных значений.
6 бит: Для ещё более компактного представления данных можно использовать 6-битный размер символа. Эта конфигурация позволяет представить 64 различных значения.
Кодирование данных
Биты данных представляют символы или данные с помощью двоичного кодирования, при котором каждый бит соответствует степени числа 2. Каждый бит в байте данных занимает определённую позицию и имеет определённый вес в двоичном представлении. Такое кодирование позволяет передавать широкий диапазон информации, делая UART универсальным для различных типов данных — от простых текстовых символов до сложных двоичных данных.
Целостность данных
Точность передачи данных в UART-связи зависит от правильной настройки битов данных. Важно, чтобы передатчик и приёмник договорились о количестве битов данных и способе их кодирования. При неправильной настройке возможно искажение данных. Например, если передатчик отправляет данные как 8-битные символы, а приёмник настроен на ожидание 7-битных символов, данные могут быть интерпретированы неверно, что приведёт к ошибкам в полученной информации.
Бит чётности (Parity)
Помимо битов данных, кадр UART может включать бит чётности. Бит чётности — это механизм проверки ошибок, помогающий обнаружить ошибки при передаче данных. Чётность может быть установлена как «нечётная» (odd) или «чётная» (even), и она гарантирует, что общее количество битов со значением логической «1» в символе будет чётным или нечётным в зависимости от выбранного типа чётности. Наличие бита чётности позволяет приёмнику проверить целостность полученных данных. Если количество битов «1» не соответствует ожидаемой чётности, обнаруживается ошибка.
Бит чётности
Стоповые биты
Один или несколько стоповых битов передаются после битов данных в каждом UART-кадре. Стоповый бит (или биты) сигнализирует об окончании байта данных и завершении передачи. Наиболее распространённая конфигурация — один стоповый бит, однако в ситуациях, где требуется дополнительная надёжность, можно использовать два стоповых бита.
Полярность стопового бита (или битов) может варьироваться: некоторые системы используют высокий стоповый бит, другие — низкий, в зависимости от конкретной конфигурации UART.
Стоповые биты
Примеры Serial через USB
Для передачи данных между Arduino и компьютером необходимо подключить плату к компьютеру с помощью USB-кабеля.
Базовый пример вывода (Print)
Этот пример отправляет строку Hello World! с Arduino на компьютер с помощью функции Serial.println(). Данные будут отправляться каждую секунду.
void setup(){
Serial.begin(9600); // инициализация последовательной связи на скорости 9600 бод
}
void loop(){
Serial.println("Hello world!");
delay(1000);
}
Serial.print() / Serial.println() используется почти во всех скетчах Arduino, поскольку позволяет понять, что происходит на плате, и разрабатывать программы, предоставляющие информацию о конкретных событиях.
Пример чтения (Read)
Для отправки данных с компьютера на Arduino (из Монитора порта) можно воспользоваться функциями Serial.available() и Serial.read(). Сначала проверяется наличие доступных данных, и если они есть — данные считываются и выводятся.
Этот пример по сути выводит всё, что вы вводите в Монитор порта (потому что мы отправляем данные на плату, а плата, в свою очередь, отправляет их обратно).
int incomingByte = 0; // для входящих данных
void setup() {
Serial.begin(9600); // инициализация последовательной связи на скорости 9600 бод
}
void loop() {
// отправляем данные только при их получении:
if (Serial.available() > 0) {
// считываем входящий байт:
incomingByte = Serial.read();
// выводим полученное:
Serial.print("I received: ");
Serial.println(incomingByte, DEC);
}
}
Примеры с пинами RX/TX
В этом разделе представлены базовые примеры UART, в которых данные передаются между двумя платами Arduino. Для подключения соедините пины TX и RX обеих плат согласно схеме ниже:
Подключение двух плат Arduino через UART
Отправка и приём сообщений
Этот пример позволяет отправлять и получать сообщения (строки) между устройствами. Загрузите следующий скетч на обе платы:
String sendMessage;
String receivedMessage;
void setup() {
Serial.begin(9600); // инициализация Монитора порта для отладки
Serial1.begin(9600); // инициализация Serial1 для отправки данных
}
void loop() {
while (Serial1.available() > 0) {
char receivedChar = Serial1.read();
if (receivedChar == '\n') {
Serial.println(receivedMessage); // вывод полученного сообщения в Монитор порта
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 бод, устанавливая соединение с компьютером и другой передающей платой Arduino.
Serial.begin(9600);
Serial1.begin(9600);
Чтение сообщений
Основной код находится в функции loop(). Если принят новый байт, то есть Serial1.available() больше 0, мы проверяем полученное сообщение.
while (Serial1.available() > 0) {
...
}
Считываем один символ из буфера входящих данных Serial1:
char receivedChar = Serial1.read();
Если обнаружен символ новой строки '\n', принятое сообщение обрабатывается и выводится в Монитор порта. Переменная receivedMessage затем сбрасывается в пустую строку для подготовки к следующему входящему сообщению.
Примечание
В программировании символ новой строки (“\n“) аналогичен нажатию клавиши «Enter». Это специальный символ, который сообщает компьютеру: «Перейди на следующую строку». В нашем случае мы знаем, что сообщение отправляется после нажатия Enter, что соответствует символу новой строки (“\n“).
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); // настройка пина светодиода на вывод
digitalWrite(LED_BUILTIN, LOW); // выключение светодиода
Serial1.begin(9600); // инициализация UART со скоростью 9600 бод
}
void loop() {
while (Serial1.available() >= 0) {
char receivedData = Serial1.read(); // считываем один байт из серийного буфера
if (receivedData == '1') {
digitalWrite(LED_BUILTIN, HIGH); // включить светодиод
}
else if (receivedData == '0') {
digitalWrite(LED_BUILTIN, LOW); // выключить светодиод
}
}
}
Передатчик (Transmitter)
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // настройка пина светодиода на вывод
digitalWrite(LED_BUILTIN, LOW); // выключение светодиода
Serial.begin(9600); // инициализация последовательной связи на скорости 9600 бод
Serial1.begin(9600); // инициализация UART со скоростью 9600 бод
}
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-соединение, вызывая Serial1 со скоростью 9600 бод.
Serial1.begin(9600);
Если принят новый байт, то есть Serial1.available() больше 0, мы проверяем полученное сообщение.
while (Serial1.available() > 0) {
...
}
Далее считываем принятый байт и проверяем, равен ли он „1“. Если условие выполнено, включаем встроенный светодиод, записывая HIGH.
char receivedData = Serial1.read(); // считываем один байт из буфера
if (receivedData == '1') {
digitalWrite(LED_BUILTIN, HIGH); // включить светодиод
}
Если принятый байт равен „0“, записываем LOW во встроенный светодиод.
else if (receivedData == '0') {
digitalWrite(LED_BUILTIN, LOW); // выключить светодиод
}
Передатчик — разбор кода
Начнём с настройки устройств: устанавливаем встроенный светодиод как OUTPUT и записываем начальное значение LOW.
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
В функции setup() инициализируем оба порта — Serial и Serial1 — со скоростью 9600 бод, устанавливая соединение с компьютером и другой передающей платой Arduino. Это необходимо, поскольку только передатчик должен быть подключён к Монитору порта.
Serial.begin(9600);
Serial1.begin(9600);
В функции loop() проверяем, равно ли сообщение, введённое в Мониторе порта IDE (Serial), значению „1“, и если да — отправляем то же сообщение через Serial1, включая встроенный светодиод.
if (Serial.read() == '1'){
Serial1.println('1');
digitalWrite(LED_BUILTIN, HIGH);
Serial.println("LEDS ON");
}
Если сообщение, введённое в Мониторе порта IDE (Serial), равно „0“, отправляем то же сообщение через Serial1, выключая встроенный светодиод.