- Назначение и принцип работы,
- Реализация в Ардуино,
- Примеры использования,
- Вывод,
- FAQ.
Назначение и принцип работы
У всех интерфейсов передачи данных, как легко догадаться из названия, одна цель - передача данных. Данными с контроллером могут обмениваться датчики, исполнительные устройства, индикаторы, дисплеи, компьютеры, смартфоны и другие контроллеры. Причем под обменом данных я имею в виду не только двухстороннюю связь, но и одностороннюю, когда, например, датчик температуры передает информацию на контроллер и ничего не ждет от него в ответ.
Если говорить применительно к Ардуино, то в аппаратном виде у всех плат присутствуют три интерфейса: UART, SPI и I2C. А у некоторых особо продвинутых имеется аж встроенный адаптер CAN-шины. У каждого из перечисленных интерфейсов есть свои преимущества и недостатки, о чем будет упомянуто отдельно, а также свои “клиенты”, то есть устройства, использующие только тот или иной интерфейс для связи с внешним миром. Потому обо всех вариантах надо знать и всеми уметь пользоваться.
В данной статье чуть подробнее разберем первый интерфейс из нашего списка - UART. Первый он и по другим параметрам: самый простой, понятный и распространенный. С него начинается знакомство начинающего DIY-мастера на тему “межмашинного” общения, даже если бы он выбрал другую последовательность. Все потому, что подключение Ардуино к компьютеру для прошивки программы осуществляется именно через него, родимого - UART. Благодаря специальному встроенному адаптеру он превращается на выходе с платы в более понятный для ПК порт USB. Через него на компьютер посылаются информационные и диагностические сообщения, а также принимаются команды и прочая полезная информация. Таким образом интерфейс этот будет преследовать нас везде и всегда, что, впрочем, и не так уж плохо.Обязательно к прочтению:Интерфейс передачи данных I2C
Итак, UART - Universal Asynchronous Receiver-Transmitter, что в переводе звучит как Универсальный Асинхронный Приемо-Передатчик. Насчет “универсальный” понятно, он широко распространен в электронном мире, его в разных видах используют и большие и малые компьютеры, контроллеры, датчики, средства коммуникации и прочие электронные устройства. Слово “асинхронный” означает то, что прием и передача отдельных битов не выравниваются так называемыми синхроимпульсами, что и хорошо, и плохо одновременно. Единица отличается от нуля исключительно по времени между перепадами уровней сигнала, которое заранее задается скоростью передачи. Для полноты картины следует сказать, что в природе существует версия UART в синхронизацией сигналов - USART, где буква S обозначает Synchronous, но здесь мы ее изучать пока не будем. Чем хороша асинхронная передача данных? Простотой протокола, минимумом проводов и занятых в процессе аппаратных средств, возможность полного дуплекса (одновременной передачи данных в обоих направлениях). В чем ее недостаток? Меньшая помехозащищенность и, как следствие, максимальная скорость и расстояние при тех же остальных условиях. Однако, для 99,5% наших задач скорости и устойчивости асинхронного варианта хватит с большим запасом.
С “хардовой” стороны UART использует два пина контроллера - RX и TX, где первые буквы обозначают, соответственно, Receiver и Transmitter. Что означают вторые Иксы, не спрашивайте, никто точно не знает, но все пишут именно так и нам советуют. Логично, что для связи двух устройств понадобятся два провода, причем соединять их следует крест-накрест RX первого в TX второго и наоборот. Куда один передает, там другой принимает.
Получается такой большой Икс. И тут нас должна осенить внезапная догадка, откуда в названии портов взялись Иксы! Возможно, что так оно и есть, версия действительно красивая и топологию так запомнить легче.
У разных плат Ардуино разное количество аппаратных портов UART, чаще всего один, а у огромной Меги аж четыре. Однако, простота этого интерфейса позволяет эмулировать порт программными методами, присваивая RX и TX почти любым пинам на выбор. То есть можно использовать “железные” порты на строго определенных пинах (рекомендуется) или - с некоторыми ограничениями - “программные”, а при необходимости и те, и другие одновременно.
С “софтовой” стороны, в общем случае, передача выглядит как цепочка сигналов, а именно битов, разделенных на байты, плюс (опционально) сигналов служебных.
Пока информация не передается, в линии сохраняется высокий уровень (для нашего случая с Ардуино это +5В, так называемая TTL-логика). Спад сигнала - это команда принимающей стороне, что сейчас начнется что-то интересное! И спустя определенное количество времени, зависящее от заранее заданной скорости, начинается обещанное интересное, а именно передача байта в виде ряда нулей и/или единиц с соблюдением тех же, заранее оговоренных, временных отрезков. После восьмого бита следует стоп-сигнал в виде высокого уровня и ситуация повторяется до тех пор, пока не будут переданы все нужные нам байты.
Так в реальности выглядит в пине TX снятый осциллографом фрагмент сообщения “Hello, World!”:
Когда я говорил про служебные сигналы, имелись в виду предусмотренные протоколом дополнительные меры борьбы с ошибками, которые вызываются помехами в проводах и контактах. Да, мир несовершенен и помехи случаются, причем чем тоньше и длиннее провода, тем чаще и сильнее. Если добавить в протокол так называемый бит четности, он будет передаваться в цепочке данных, сразу за последним битом информационного байта и перед стоповым битом.
Наличие или отсутствие такого бита тоже оговаривается в протоколе заранее, наряду со скоростью и прочими параметрами. Тут нужно понимать, что протокол имеет разновидности и может отличаться у разных устройств в зависимости от условий и предпочтений программистов. Основные параметры таковы: скорость, количество битов, паритет (четность), длина стоп-сигнала. Отсюда вытекает важное условие для работы UART: оба устройства должны быть настроены одинаково, иначе они друг друга не поймут, как случайно встретившиеся в Антарктиде китаец и швед. Опять же, истины ради, стоит упомянуть о возможностях некоторых особо умных устройств понимать, на каком “языке” говорит с ним собеседник и автоматически настраиваться под него, например GPS-модули типа SIM800, но это исключительно заслуга программистов, которые предусмотрели такую функцию и облегчили жизнь своим клиентам. Мы же пока таким заниматься не станем, поэтому будем просто задавать одинаковые параметры на обоих электронных “собеседниках”.
Применительно к Ардуино чаще всего бит четности не используется, данные байта состоят из восьми битов, а длина стоп-сигнала равна длине одного бита.
Пример - полный сигнал “Hello, World!” с переводом строки. Хорошо видны информационные биты, отмеченные точкой, и служебные “старт” и “стоп” в начале и в конце каждого байта.
Скорость передачи измеряется в “бодах”, то есть в битах в секунду округленно до сотни. Очень желательно выбирать из заранее кем-то сформулированных стандартных значений:
Чем выше скорость, тем быстрее бегают по проводам данные, но тем больше вероятность ошибок, поэтому рекомендуется не злоупотреблять и соблюдать принцип разумной достаточности. Для большинства случаев рекомендуется скорость 9600 бод, это довольно быстро и весьма надежно даже на приличных дистанциях, байт передается примерно за 1мс. Если этого недостаточно, всегда можно “поддать газу”. Предельно возможная скорость передачи данных на аппаратных UART для плат Ардуино - 250000 бод, на софтовых - 115200. Но, как говорилось выше, без нужды лучше до таких значений не разгоняться.
Данными наши устройства могут обмениваться в обе стороны, но несмотря на теоретическую возможность полного дуплекса, реально это происходит слегка поочередно, хоть и довольно прозрачно для пользователя благодаря буферизации порта.
Реализация в Arduino
В среде Ардуино общение с UART происходит с помощью класса Serial. Рассмотрим некоторые функции этого класса.
Serial.begin(long);
Запускает работу порта с заданной в параметре скоростью в бодах. У Меги 4 порта, запускаются командами эc номером, например, Serial1.begin(9600); и так далее. Все остальные команды к портам Меги тоже осуществляются с уточнением номера порта.
Serial.end();
Останавливает работу порта, если он был ранее запущен. На практике используется редко, но бывают случаи, когда необходимо освободить пины 0 и 1 хотя бы на время.
= Serial.available();
Возвращает в виде числа int количество принятых в буфер порта байт. Если возвращает 0, информации не поступало. Обычно используется как триггер для приема информации.
= Serial.read();
Возвращает байт из буфера приема. Следующий вызов возвращает следующий байт и так далее. Если буфер опустел, возвращает 0xFFFF.
Serial.print(xxx);
Предоставляет большое разнообразие вариантов передачи данных в порт, от байта до строки символов и числа с плавающей точкой. Очень удобная функция для отладки программ.
Serial.println(xxx);
Отличается от вышеупомянутой автоматической отправкой двух служебных символов переноса строки после информации из входящей в параметр информации. Следующее сообщение начнется с новой строки.
Serial.write(xxx);
Передает двоичные данные в порт. Возвращает число переданных байтов.
= Serial.read();
Возвращает принятые двоичные данные.
Список функций класса Serial далеко не полный, но с их помощью можно осуществлять почти любые операции с обменом данных. Посмотрим, как это делается на нескольких практических примерах.
Примеры использования
Начнем с простейшего. Передача информации из контроллера на компьютер с ее отображением в мониторе. Это очень важная возможность, позволяющая наблюдать происходящее в программе, если вставить в нее соответствующие строки. Можно выводить на экран содержимое переменных, метки прохождения каких-то точек и так далее.
Схему рисовать не будем, потому что ее нет. Достаточно просто подключить плату к USB компьютера и залить в нее такую программу:
void setup() { Serial.begin(9600); // запускаем порт } void loop() { Serial.print("timer: "); // пишем слово timer: Serial.print(millis()); // выводим кол-во миллисекунд с начала запуска программы Serial.println("ms"); // подписываем их ms, переводим строку на новую delay(1000); // задержка 1 сек }
После чего запускаем встроенный в среду Ардуино монитор порта при помощи кнопки с лупой в верхнем правом углу окна: И смотрим какие строки там побежали: Две строки Serial.print() последовательно выводят на экран текст, значение таймера millis(), третья строка Serial.println() выводит последний кусочек текста и переводит строку. Получается бесконечная цепочка сообщений, которые мы можем читать с экрана.
Пример второй, чуть посложнее, но тоже не требующий сборки схемы. Будем посылать данные в обратном направлении - от компьютера к контроллеру. Данные будут командами на включение или выключение встроенного светодиода на 13 пине Ардуино.
Заливаем программу:
void setup() { pinMode(13, OUTPUT); Serial.begin(9600); } void loop() { if (Serial.available() > 0) { // если пришла команда char incom = Serial.read(); // считываем, опознаем, реагируем if (incom == '1') { digitalWrite(13, HIGH); } else if (incom == '0') { digitalWrite(13, LOW); } } }
Теперь, если набрать в строке монитора единицу и отправить ее, нажав кнопку Enter, светодиод на плате включится, а если отправить ноль - выключится. Таким образом мы можем управлять программой непосредственно из монитора порта или отправлять в нее любые данные. Например время для часов, количество оборотов, которое нужно сделать мотору, или яркость светодиодной ленты. И это уже прямой путь к передаче данными между контроллерами напрямую, стоит лишь заменить ввод данных в мониторе на отправку их из программы второго контроллера.
Третий пример как раз про это. Для реализации нам потребуется две любых платы Ардуино, между которыми мы будем гонять данные. К каждой из них мы подключим две кнопки и два светодиода по одинаковой схеме, и соединим их RX-TX крест-накрест, как было описано выше.
Важное примечание. Если платы подключены к разным источникам питания, необходимо обязательно объединить их контакты GND, иначе для сигналов не будет опоры на соседней плате.
Что мы хотим увидеть на данной сборке. Кнопки, подключенные к одной плате, будут управлять светодиодами, подключенными к другой. И наоборот. Для этого каждая плата должна передавать информацию о том, что происходит на ее кнопках другой плате, одновременно принимая от нее такие же данные и управляя согласно им своими светодиодами. Сборки симметричны, функции тоже, значит и программы на обеих платах будут одинаковые.
Заливаем в обе:
#define LED_1 4 // светодиод 1 #define LED_2 5 // светодиод 2 #define BUT_1 2 // кнопка 1 #define BUT_2 3 // кнопка 2 byte but[2]; // переменные для отслеживания кнопок void setup() { // инициализируем пины, запускаем сирал порт pinMode(LED_1, OUTPUT); pinMode(LED_2, OUTPUT); pinMode(BUT_1, INPUT); pinMode(BUT_2, INPUT); Serial.begin(9600); } void loop() { if (get_but()) { // с кнопками что-то было Serial.write(but[1]); // отправляем в порт новое состояние кнопок } if (Serial.available() > 0) { // если пришли данные от соседних кнопок byte incom = Serial.read(); // считываем эти данные digitalWrite(LED_1, !(incom / 2)); // зажигаем или гасим светодиод 1 digitalWrite(LED_2, !(incom % 2)); // зажигаем или гасим светодиод 2 } } byte get_but() { static unsigned long timer; if (timer + 50 > millis()) return 0; // опрос каждые 50мс (антидребезг) timer = millis(); but[0] = but[1]; but[1] = digitalRead(BUT_1) + digitalRead(BUT_2) * 10; // данные с обеих кнопок в одну переменную (единицы и десятки) if (but[0] != but[1]) return 1; // если есть изменение, сигнализируем единицей return 0; // если нет, возвращаем 0 }
Нажимаем кнопки, видим, что все работает так, как задумано. Байты с командами бегают туда-обратно, причем в любой последовательности, в т.ч. одновременно.
Разумеется, это самый примитивный способ передачи данных, хоть и вполне рабочий. Полностью отсутствует проверка на ошибки и контроль исполнения, но это легко исправить, добавив в протокол контрольную сумму и обратную связь в виде подтверждения приема. Нет предела совершенству, все зависит лишь от требований и количества времени, затраченных на программу.
Вывод
UART бесспорно и заслуженно самый известный и широко применяемый интерфейс передачи данных. С его помощью к Ардуино подключаются датчики, исполнительные устройства, индикаторы и дисплеи, GPS и GPRS-модули. Через него осуществляется заливка программы в Ардуино и ее отладка. С его помощью можно легко и быстро организовать обмен команд и информацией с другим контроллером. Однако, у него есть и недостатки, например ограничение по скорости, относительно невысокая помехозащищенность, требование к точности тактовой частоты у передающей и принимающей платы, что особенно критично для контроллеров, работающих от внутреннего RC-генератора. Тем не менее он остается очень важным инструментом с огромными возможностями. Знать этот интерфейс и уметь им пользоваться должен любой DIY-мастер без исключения.
FAQ
Можно ли соединить с помощью UART более двух устройств и/или контроллеров?
Можно, однако придется слегка доработать шину передачи данных, чтобы все RX видели все TX. Для этого существуют адаптеры RS-485, позволяющие на физическом уровне объединить TX и RX всех устройств, входящих в сеть, в одно целое при помощи двух проводков. Но это тема отдельной статьи, а может, и не одной.
На какое максимальное расстояние возможно передать данные с помощью UART без потерь?
Расстояние сильно зависит от ряда параметров: толщины и материала провода, наличия экранирования, скорости. Однако даже в лучшем случае это не более пары десятков метров. Интерфейс не предназначен для дальних расстояний. Но, возвращаясь к предыдущему вопросу, стоит сказать, что преобразованный с помощью RS-485 сигнал может распространяться не только на несколько устройств, но и гораздо дальше, так как использует более высокое напряжение и противофазу сигналов.
Нужно ли соблюдать строгую очередность в передаче и приеме информации?
По умолчанию UART дуплексный, то есть может принимать и передавать данные одновременно. Однако важно в процессе передачи успевать забирать данные из буфера приема, иначе он переполнится и часть данных пропадет. По умолчанию размер буфера 64 байта, что довольно много и при правильном использовании более чем достаточно, но для особых случаев можно увеличить константу, например до 128 байт, при помощи команды препроцессору:
#define SERIAL_RX_BUFFER_SIZE 128
Что такое логические уровни, как и зачем их согласовывать?
Не все устройства и контроллеры работают на питании и логике 5В, есть те, которым противопоказано напряжение выше 3.3В, они могут вывести принимающий порт из строя и, возможно, все устройство. Если для передачи в обратную сторону 5 <-- 3.3 можно ничего не менять, принимающая сторона все поймет и не повредится, то в направлении 5 --> 3.3 следует обязательно понизить напряжение. Вариантов несколько, самый простой - резистивный делитель, намного лучше применить стабилитрон на 3.3 В, отсекающий лишнее напряжение, или специальную микросхему, особенно если каналов несколько.