СВЯЗЬ НЕСКОЛЬКИХ АРДУИНО ПО ПРОВОДУ

Не нашёл нормальных способов связать несколько Ардуинок в проводную сеть, решил сделать свой. Да, существуют библиотеки на базе Wire (например EasyTransfer), но там всё очень криво, по двум проводам и на слишком высокой частоте, т.е. на большом расстояние отправить не получится. В рассмотренной библиотеке предлагается использовать программный аналог интерфейса UART с двухсторонней связью по одному проводу, встроенным протоколом передачи, адресацией, упаковщиком и распаковщиком данных, контролем целостности и прочими приколами. Скорость интерфейса невысокая, что позволяет отправлять данные по проводам на очень большие расстояния и с высокой надёжностью, а лёгкая реализация вмещается даже в ATtiny13 на приём и на отправку. Расскажу немного о том, как работает интерфейс.

Подключение производится по одному пину, земля (GND) должна быть общая. В сети могут находиться любые Ардуино – совместимые платы и МК, подключение – к любому GPIO (цифровому пину):

Сам интерфейс имеет такую же суть, как UART:

Чтение производится в ожидаемых серединах фреймов, начало отсчёта – стартовый LOW:

В начальный момент времени все устройства держат пин в подтянутом к питанию состоянии (INPUT_PULLUP). Передавать данные может только одно устройство. Все устройства в сети принимают данные одновременно, но в пакет входит адрес приёмника, поэтому приёмник поймёт, что данные предназначены ему. Остальные устройства проигнорируют принятый пакет.

Применение


Зачем нужен данный интерфейс? Можно собрать устройство с центральной базой (например на Arduino Nano) и несколькими удалёнными, например на копеечных ATtiny13. База будет общаться с удалёнными устройствами и отправлять им команды на исполнение (включить/выключить нагрузку, увеличить/уменьшить тепловую мощность, яркость и т.д.), а также может запрашивать данные с их датчиков. Связь по медленному цифровому протоколу получится очень надёжной даже на большом расстоянии (сотни метров), что избавляет от применения дополнительных конвертеров интерфейсов. Та же тинька 13 может отправлять на базу данные с датчиков температуры/влажности/освещённости и так далее.

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

БИБЛИОТЕКА GYVERBUS

  • Очень простой, надёжный, устойчивый к помехам и задержкам, но медленный интерфейс связи на базе UART
  • Двухсторонняя связь по одному проводу
  • Асинхронная отправка и чтение (на базе millis())
  • Двухсторонняя совместимость с аппаратным UART
  • Возможность принимать и отправлять данные внутри сети Ардуинок
  • Адресация до 254 устройств в сети (от 1 до 255)
  • Всеядная функция отправки и приёма (ест переменные, структуры, массивы)
  • Встроенная проверка CRC (контроль целостности) пакета данных
  • Возможность отправки и чтения короткого “запроса”
  • Сама библиотека предоставляет возможности по отладке (коды ошибок)
    • В примерах есть компактные варианты чтения и отправки данных, влезет даже в ATtiny

Поддерживаемые платформы: все Arduino (используются стандартные Wiring-функции)

Версии:

  • Версия 1.1: добавлена waitAck() и исправлена ошибочка
  • Версия 1.2: улучшена стабильность, функции оптимизированы, уменьшен вес
  • Версия 1.3: добавлен CRC в запрос и ответ в обеих библиотеках
  • Версия 2.0: всё переделано, читай доку =)

ДОКУМЕНТАЦИЯ


Особенности новой версии


В новой версии библиотеки протокол GBUS был полностью отделён от интерфейса: сейчас GBUS – это протокол связи, то есть порядок байтов в посылке (адрес, CRC и прочее). Общаться по GBUS можно при помощи любой Serial библиотеки, а точнее – любого объекта класса Stream (родная библиотека аппаратного Serial, включая Serial1, Serial2 и прочие на Arduino Mega, SoftwareSerial на любой Ардуино-совместимой платформе, а также встроенный в GyverBus однопроводной softUART). Это позволяет удобно, надёжно и не блокируя выполнение остального кода передавать наборы данных любых типов между Ардуино-совместимыми платами (Arduino, esp8266, esp32) при помощи программного или аппаратного UART’a.

Особенности библиотеки


Библиотека содержит в себе несколько наборов инструментов:

  • GyverBus.h – базовый набор инструментов для работы с протоколом GBUS: функции для упаковки, распаковки и проверки байтовых массивов для передачи и приёма данных. Функции можно использовать отдельно от всего остального для упаковки-распаковки и проверки данных для передачи по любому интерфейсу связи, просто передавая любые данные как байтовый массив.
  • GBUS.h – класс GBUS, который позволяет удобно общаться по протоколу GBUS через любой “Serial”. В отличие от родных инструментов класса Stream не блокирует выполнение кода на приём и отправку, что очень критично на невысоких скоростях.
  • softUART.h – однопроводной UART, работающий на приём и отправку, не блокирующий выполнение кода. Может использоваться для библиотеки GBUS, так как наследует класс Stream.
  • GBUSmini.h – набор отдельных функций для общения по GBUS. Все функции блокируют выполнение кода на время приёма/отправки (сделаны на базе delay()), но реализованы максимально легко и позволяют работать с шиной даже на ATtiny13. Для работы нужно подключить библиотеку и просто использовать нужные функции из доступных.

Подключение для softUART


Платы/микроконтроллеры объединяются в шину одним дата-проводом и должны иметь общую землю GND. Пин шины – любой обычный GPIO (цифровой пин входа-выхода). Пин должен быть подтянут к питанию внутренним (режим INPUT_PULLUP) или внешним резистором (5-100 кОм). При использовании GyverBus.h внутренняя подтяжка включается автоматически, а для GBUSmini.h её нужно прописать самому (см. примеры). Схема подключения – на самой первой картинке этой страницы сайта.

Для удалённого подключения рекомендуется использовать экранированный провод, экран нужно подключить на GND. Если подключение производится двумя обычными проводами (GND и дата) – провода рекомендуется скрутить в косичку для защиты от наводок.

Подключение для Serial


Для одностороннего общения по классическому Serial достаточно соединить TX передатчика и RX приёмника. Для двухсторонней связи нужно будет соединить ещё и RX передатчика и TX приёмника. Например для использования функций “достукивания” до приёмника, чтобы удостовериться в том, что он получил данные.

Общение по шине


Шина двухсторонняя, любое из устройств в сети может быть отправителем и получателем, но в один момент времени может осуществляться только одна передача: один передаёт – все слушают. Такая логика работы позволяет всем устройствам общаться по очереди между собой без ограничений.

Совместимость softUART


softUART имеет такие же контрольные биты и тайминги, как у UART (в ардуиновской среде – Serial), и полностью с ним совместим, то есть можно отправлять данные по Serial (с ноги TX) и принимать на softUART, либо отправлять c softUART и принимать по Serial (на ногу RX). Скорость Serial нужно выставлять выше 250 бод, softUART соответственно тоже.

Скорость


  • softUART – скорость задаётся при создании объекта.
  • GBUS – скорость задаётся у самого интерфейса, например у “Serial’ов” это begin.
  • GBUSmini – скорость задаётся в GBUSmini.h, параметром GBUS_DEFAULT_SPEED, либо дефайном GBUS_SPEED в скетче перед подключением библиотеки.

Скорость задаётся в бодах (baud rate) и совпадает с бодрейтом UART’а. Время передачи одного байта равно 10 / baud секунд, соответственно скорость передачи составляет baud / 10 байт в секунду (например при скорости 300 бод – 30 байт в секунду).

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

Максимальная скорость


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

При использовании в качестве интерфейса аппаратного Serial скорости могут быть очень большие, вплоть до 1000000 бод.

При использовании в качестве интерфейса программного Serial читайте описание к нему. Ардуиновский софтсериал обещает стабильную передачу на скоростях вплоть до 115200. В то же время он блокирует код и запрещает прерывания, что может быть очень плохо.

При использовании в качестве интерфейса softUART очень важен частый вызов tick(): рекомендуется делать это не реже, чем каждые 1 000 000 / baud / 4 микросекунд. При наличии в остальном коде задержек или блокирующих выполнение участков на время, превышающее четверть времени бита, передача на высоких скоростях может происходить с ошибками. Можно вызывать tick() по прерыванию таймера. Ещё можно сделать так:

void loop() {
  if (rx.tick() != RECEIVING) {
    // Потенциально "тяжёлый" код.
    // Не выполняем, пока идёт приём!
  }
  // "лёгкий" код
}

При использовании библиотеки GBUSmini.h максимальная скорость ограничена частотой опроса читающих функций (вызывать не реже, чем каждые 1 000 000 / baud / 4 микросекунд) и точностью настройки коррекции: в GBUSmini.h в секции настроек есть параметры GBUS_DEFAULT_WRITE и GBUS_DEFAULT_READ. Они отвечают за коррекцию задержки в микросекундах на отправку и чтение одного бита. Это значение зависит от частоты тактирования МК (пропорционально!), модели самого МК, версии “ядра” Arduino и подбирается вручную. Также значение коррекции может меняться в зависимости от скорости шины! Привожу некоторые известные:

МК Ядро GBUS_DEFAULT_WRITE GBUS_DEFAULT_READ
ATmega328p (Arduino Nano) Стандартное версии 1.8.3 8 (при 16 MHz) 5 (при 16 MHz)

Максимальная скорость с учётом “пустого” скетча и откалиброванными значениями коррекции задержки, платы соединены на бредборде проводами длиной 10см:

Отправитель – приёмник Макс. скорость
softUART – softUART 25’000
GBUSmini – GBUSmini 10’000
softUART – GBUSmini 15’000
GBUSmini – GBUSmini 10’000

Контроль целостности


В библиотеку встроен контроль целостности данных (включен по умолчанию): последним байтом в пакете передаётся crc (очевидно 8-ми битный). Приёмник считает CRCвсего пакета, и если он совпадает – приём считается успешным. CRC повышает надёжность передачи данных: даже если один бит будет передан неправильно – приёмник отбракует посылку.

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

Проверку CRC можно отключить и в основной библиотеке, и в мини-версии. Зачем отключать? Позволит сэкономить чутка памяти. Лучше не отключать CRC, надёжность передачи данных сильно снизится.

Протокол связи


Протоколом здесь назван порядок и значение байтов в пакете:

Тип посылки Байт 1 Байт 2 Байт 3 Байт 4 Байт 4+n Байт 4+n+1
Данные Количество байт Адрес получателя Адрес отправителя Байт даты 1 Байт даты n CRC
Тип посылки Байт 1 Байт 2 Байт 3 Байт 4
Запрос (request) 0 Адрес получателя Адрес отправителя CRC
Тип посылки Байт 1 Байт 2 Байт 3 Байт 4
Ответ (ack) 1 Адрес получателя Адрес отправителя CRC

Таким образом например пакет [5, 3, 8, 123, 456, 12] содержит 6 байт, предназначен для устройства с адресом 3 и отправлен с адреса 8. Байты данных имеют значение 123 и 456, а CRC – 12.

Оба способа взаимодействия с GBUS работают по этому протоколу и совместимы между собой. Если стоит задача сформировать пакет вручную, отправить и принять его через функции из библиотеки – пакет должен быть сформирован согласно протоколу.

Адресация


В GBUS используется 8-ми битная адресация, но адрес 0 зарезервирован как код ошибки чтения в GBUSmini.h. Таким образом адресовать устройства можно с 1 по 255 адрес.

Если не работает


Самая нестабильная передача – между softUART и GBUSmini, а также между устройствами на базе разных микроконтроллеров и/или с разной частотой тактирования. Первым делом стоит попробовать чуть увеличить или чуть уменьшить значения коррекции задержки.

Совместимость


  • Вариант с GBUS поверх родного Serial или софтсериал совместим абсолютно между всеми платформами.
  • softUART лично мне не удалось завести на esp8266, возможно виной тому кривая распиновка и неудачно выбранные пины
  • А так по сути Serial он везде Serial, то есть общаться можно между разными МК и платформами

Инициализация


GBUS объект(обработчик, адрес, размер буфера), где:

  • обработчик – адрес объекта-обработчика интерфейса (например &Serial, &mySerial)
  • адрес – адрес этого устройства в сети (1-255)
  • размер буфера – размер внутреннего буфера в количестве байт (больше или равен отправляемому объёму данных)

Метод tick()


Метод tick() занимается отправкой и приёмом данных по шине, то есть измеряет время и слушает/дёргает пин, что обеспечивает “асинхронную” работу интерфейса. Вызывать тик нужно как можно чаще, желательно как минимум в 4 раза чаще, чем 1'000'000 / скорость микросекунд. То есть например для скорости 300 это будет 1000000/300/4 ~ 830 микросекунд. Можно оставить его в loop() и обеспечить его “прозрачное” выполнение без задержек, либо дополнительно положить в прерывание таймера с таким периодом. Логика работы tick() автоматически переключается в зависимости от текущего режима: во всё время кроме отправки мы “слушаем” шину.

Также tick() возвращает текущее состояние шины.

Коды статусов и ошибок


Помимо тика, текущий статус можно узнать из функции getStatus(), она возвращает то же самое, что тик:

Код Название Описание
0 GBUS_IDLE Ожидание
1 TRANSMITTING Передача
2 TX_OVERFLOW Буфер переполнен
3 TX_COMPLETE Передача завершена
4 RECEIVING Приём
5 RX_ERROR Ошибка приёма
6 RX_ABORT Ошибка. Приём прерван
7 RX_OVERFLOW Ошибка. Буфер или пакет переполнен
8 RX_ADDRESS_ERROR Ошибка. Не наш адрес
9 RX_CRC_ERROR Ошибка. Не совпадает CRC
10 RX_REQUEST Успешное получение запроса
11 RX_COMPLETE Успешный приём данных
12 RX_ACK Успешное получение подтверждения

Статус можно опрашивать как по коду, так и по имени (константа подсветится синим).

Отправка данных


Для отправки данных нужно взывать sendData(адрес, дата), где адрес – адрес принимающего устройства, а дата – любой тип данных (переменная любого типа, массив переменных любого типа, включая char array, структура). Данные будут автоматически разбиты на байты и отправлены внутри tick().

Пример отправки структуры:

struct myStruct {
  byte val_b;
  int val_i;
  float val_f;
};
myStruct tx_data;

void setup() {
  // отправляем на адрес 2
  tx.sendData(2, tx_data);
}

void loop() {
  tx.tick();
}

Приём данных


На принимающей стороне опрашиваем функцию gotData(), она однократно вернёт true при получении корректного пакета данных (внутри tick() автоматически проводится проверка ошибок передачи, переполнения, совпадения адреса и контроль целостности данных, если он включен).

Данные читаются при помощи readData(дата), где дата – переменная с таким же типом данных, какой был отправлен. Функция  сама запишет данные в указанную переменную, то есть соберёт из встроенного буфера. Пример:

struct myStruct {
  byte val_b;
  int val_i;
  float val_f;
};
myStruct rx_data;

void setup() {
}

void loop() {
  rx.tick();
  if (rx.gotData()) {
    rx.readData(rx_data);
  }
}

Адрес отправителя запоминается при получении, его можно прочитать из getTXaddress().

Отправка и приём запроса


В библиотеке также реализована отправка короткого “запроса” при помощи sendRequest(адрес). При получении такого запроса на текущее устройство метод gotRequest() вернёт true.

Структура запроса для работы с шиной вручную: [0, адрес получателя, адрес отправителя]

Отправка и приём подтверждения


В библиотеке также есть отправка и приём “подтверждения” – ack. Отправить можно при помощи sendAck(адрес) и принять в gotAck(), логика такая же как у запроса.

Структура подтверждения для работы с шиной вручную: [1, адрес получателя, адрес отправителя]

Отправка запроса с подтверждением


В библиотеке реализован блокирующий метод sendRequestAck(адрес, кол-во попыток, таймаут), который отправляет запрос по указанному адресу и ждёт подтверждения приёма (ack) или отправленные в ответ данные. Если в течение указанного таймаута данные или ack не получены – запрос будет отправлен ещё раз, и так до тех пор, пока количество попыток не достигнет указанного. Это позволяет “достучаться” до приёмника и понять, получил ли он запрос, или дождаться от него данные в ответ на запрос. Смотри пример call_response_ack.

Сам метод sendRequestAck() возвращает статус:

Код Название Описание
2 ACK_ERROR Ответ не получен
3 ACK_ONLY Получено подтверждение
4 ACK_DATA Получены данные

Ожидание ответа (v1.1)


Метод waitAck(адрес, кол-во попыток, таймаут) работает по такой же логике, как предыдущий, но не блокирует выполнение кода. Логика такая: вручную отправляем реквест sendRequest(адрес) и при помощи waitAck() можем дождаться ответа и попытаться достучаться до приёмника, автоматически отправляя новые запросы через таймаут миллисекунд. Количество попыток ограничено заданным. Метод возвращает статусы:

Код Название Описание
0 ACK_IDLE Ничего не делаем
1 ACK_WAIT Ждём ответа
2 ACK_ERROR Ответ не получен
3 ACK_ONLY Получено подтверждение
4 ACK_DATA Получены данные

Примеры находятся в examples/GBUS/wait_ack. В примере wait_ack_rx можно закомментировать отправку ответа и в мониторе порта увидеть, как передатчик отправляет несколько запросов перед тем, как выдать ошибку.

Отправка и приём сырых данных


Сырые данные (просто байтовый массив) можно отправить по шине при помощи sendRaw(байтовый массив, размер), где размер можно передать как sizeof(массив). Метод gotRaw() вернёт true, если приёмник корректно принял какие-то данные. Размер принятого пакета можно узнать в rawSize(), а прочитать его – обратившись напрямую к члену класса buffer, как к массиву.

Инициализация


Объект создаётся через шаблон, а также принимает скорость:

softUART<пин, режим> объект(скорость), где:

  • пин – любой GPIO пин. Автоматически будет настроен в INPUT_PULLUP. Если встроенной подтяжки нет – тянуть вручную резистором.
  • режим – режим работы объекта:
    • GBUS_FULL – двухсторонняя связь (активен по умолчанию, можно не указывать)
    • GBUS_TX – только отправка (экономит память)
    • GBUS_RX – только приём (экономит память)
  • скорость – скорость интерфейса в бодах

Использование с GBUS


Для общения по GBUS через однопроводной UART достаточно передать его в GBUS при создании:

// подключаем софт юарт
#include "softUART.h"
softUART<4> myUART(1000); // пин 4, скорость 1000

// подключаем GBUS
#include "GBUS.h"
GBUS bus(&myUART, 5, 20); // обработчик UART, адрес 5, буфер 20 байт

Буфер


softUART имеет свой программный буфер на отправку, его размер можно настроить в файле softUART.h, параметр SOFTUART_BUF_SIZE

Особенности


GBUSmini.h содержит набор функций для работы с шиной, все функции (за исключением GBUS_is_busy(пин)) – блокирующие, то есть блокируют выполнение кода на время отправки или чтения пакета.

Функции чтения должны работать в таких же условиях, как и tick(): вызываться как можно чаще, чтобы не пропустить начало передачи.

Все функции работают только с массивом байтов (в описании ниже – дата), также в функции передаётся его размер (например через sizeof(data)). Для работы с другими типами данных используй упаковщик и распаковщик данных из “Утилиты“.

Выключение CRC


CRC включается и выключается в файле GBUSmini.h, параметр GBUS_CRC в секции настроек

Отправка и чтение данных


“Сырые” данные (без протокола) можно отправить и принять при помощи

  • GBUS_send_raw(пин, дата, размер)
  • GBUS_read_raw(пин, дата, размер) – возвращает количество принятых байт при успешном завершении приёма

Для работы по протоколу используются

  • GBUS_send(пин, адрес получателя, адрес отправителя, дата, размер)
  • GBUS_read(пин, наш адрес, дата, размер) – возвращает адрес отправителя при успешном завершении приёма. При ошибке возвращает 0

Отправка и чтение запроса


  • GBUS_send_request(пин, адрес получателя, адрес отправителя)
  • GBUS_read_request(пин, наш адрес) – возвращает адрес отправителя при успешном завершении приёма. При ошибке возвращает 0

Отправка и чтение подтверждения


  • GBUS_send_ack(пин, адрес получателя, адрес отправителя)
  • GBUS_read_ack(пин, наш адрес) – возвращает адрес отправителя при успешном завершении приёма. При ошибке возвращает 0

Запрос с ожиданием


GBUS_send_request_ack(пин, адрес получателя, адрес отправителя, кол-во попыток, таймаут между попытками) – отправить запрос и ждать подтверждения приёма, т.е. пытаться “достучаться” до приёмника. Возвращает 0 при таймауте, 1 при успехе (получили ack). Смотри примеры call response_ack

// =========== GBUS =========== 
// проверить статус принятых данных (буфер, его размер, кол-во принятых байтов, наш адрес)
// вернёт статус GBUSstatus
GBUSstatus checkGBUS(uint8_t* buffer, byte bufSize, byte amount, byte addr);


// ====== УПАКОВЩИК GBUS ====== 
// запаковать данные для отправки (буфер, его размер, дата, адрес получателя, адрес отправителя)
// вернёт количество упакованных байт
// запакует согласно протоколу [суммарное количество байт, адрес получателя, адрес отправителя, ...байты даты..., CRC]
template  byte packGBUSdata(uint8_t* buffer, byte bufSize, T &data, byte to, byte from);

// распаковать данные, минуя служебные байты (буфер, его размер, дата)
// при успехе вернёт true. Вернёт false, если буфер слишком мал для даты
template  bool unpackGBUSdata(uint8_t* buffer, byte bufSize, T &data);

// запаковать команду в буфер (буфер, команда, кому, от кого)
// команды: запрос (0), ответ (1)
// запакует согласно протоколу [команда, адрес получателя, адрес отправителя, CRC]
byte packGBUScmd(uint8_t* buffer, byte cmd, byte to, byte from);


// ====== УПАКОВЩИК БАЙТОВ ====== 
// пакуем любой тип данных в байтовый буфер (буфер, дата)
template  void packDataBytes(byte *buffer, T &data);

// распаковываем из байтового буфера обратно (буфер, дата)
template  void unpackDataBytes(byte *buffer, T &data);


// ============= CRC =============
// обновить CRC байта (crc, байт)
void GBUS_crc_update(uint8_t &crc, uint8_t data);

// расчёт crc для буфера (буфер, количество байт для проверки)
byte GBUS_crc_bytes(byte *data, byte size);

ПРИМЕРЫ


В библиотеке очень много примеров, смотри их в папке examples =) Онлайн-версия на GitHub

УСТАНОВКА БИБЛИОТЕКИ

Если вы не знаете, как установить библиотеку – читайте отдельный урок по работе с библиотеками!

БАГИ И ОШИБКИ

Если вы нашли баг или ошибку в исходнике или примерах, или у вас есть идеи по доработке библиотеки – пишите пожалуйста на почту alex@alexgyver.ru. В комментарии на страницах я заглядываю очень редко, на форум – ещё реже.

ОСТАЛЬНЫЕ БИБЛИОТЕКИ

У меня есть ещё очень много всего интересного! Смотрите полный список библиотек вот здесь.