Глядя на обилие дешевых ESP32 модулей, захотелось мне сделать из них что нибудь полезное. Для работы мне нужен был BLE адаптер с последовательным интерфейсом пригодный для разных применений вроде организации беспроводного канала связи между железками или сбора телеметрии с нескольких устройств. Ну а для большей радости от процесса была выбрана платформа Ардуино. Эта статья - о том, что получилось.
Недостатки существующих решений
На али есть масса готовых решений вроде JDY-08 или HM-10, но они закрытые, не позволяют соединяться одновременно с несколькими устройствами и не предоставляют средств управления потоком данных. Еще об одном недостатке речь пойдет ниже.
Важность управления потоком данных
Любой канал связи имеет ограниченную пропускную способность. В случае BLE соединения она довольно ограничена и сильно зависит от условий приема. Что же произойдет при попытке передать больше данных, чем канал связи способен пропустить через себя? То же, что при попытке наливать в ванну больше, чем из нее вытекает - вода выльется на пол, а данные потеряются. Если они потеряются в канале связи, адаптер может хотя бы отследить, какие конкретно фрагменты потерялись. А если потеря происходит в последовательном канале связи, то как то контролировать масштаб этой потери просто невозможно. Чтобы этого не допускать, полезно использовать аппаратное управление потоком - сигналы RTS/CTS. Особенно важен RTS на стороне адаптера, поскольку он предотвращает переполнение его приемного буфера. Этот сигнал должен соединяться со входом CTS на другой стороне последовательного канала связи. В конфигурации адаптера сигнал RTS активен по умолчанию при использовании аппаратного порта. Вы можете не соединять его физически, если в нем нет необходимости.
Как это работает - передача данных посредством BLE
BLE устройство может работать в двух принципиально разных ролях, выполняя функцию периферийного или центрального устройства. Периферийное устройство имеет богатую внутреннюю структуру, показанную на рисунке ниже.
![Роли и потоки данных между BLE устройствами Роли и потоки данных между BLE устройствами](https://habrastorage.org/getpro/habr/upload_files/9fb/8ef/7fb/9fb8ef7fb800987546be8c1a30ebdaa2.png)
Периферийное устройство содержит набор сервисов. Сервис представляет собой коллекцию характеристик. Характеристика фактически является буфером данных размером до 512 байт. Характеристики могут иметь дескрипторы, которые описывают их свойства и тоже являются буфером данных, только меньшего размера (бритва Оккама скучает без дела). Центральное устройство лишено всей этой внутренней структуры, оно лишь может устанавливать соединение с периферийным устройством, записывать данные в его характеристики и подписываться на обновления. Адаптер имеет единственную характеристику, через нее и происходит обмен данными. При этом адаптер может работать как в роли центрального устройства, так и в роли периферийного, так и в двух ролях одновременно. В качестве центрального устройства он может устанавливать соединение с периферийными устройствами в количестве до 4 одновременно (это ограничение реализации BLE стэка). При использовании процессора первой версии ESP32 количество одновременно работающих соединений ограничено двумя.
Смысл соединения
Тут полезно задаться вопросом - в чем смысл соединения между центральным и периферийным устройством? Например, в классическом BT есть последовательный канал SPP, он гарантирует целостность потока данных и либо доставляет их в потоке, либо рвет соединение. Аналогичную семантику имеет TCP/IP соединение. Оказывается, что BLE соединение ничего не гарантирует вообще, оно нужно просто чтобы хранить контекст связи двух устройств. Но рваться по собственной инициативе оно тоже может. Обновления характеристик могут как угодно повреждаться, теряться и переупорядочиваться в процессе передачи.
Прозрачная передача против пакетной
И здесь мы подходим ко второму фундаментальному недостатку существующих решений - они ориентированы на прозрачную передачу потока данных. Это конечно удобно для пользователя, но правильно ли? Ведь BLE не может гарантировать целостность этого потока. Адаптер делит его на фрагменты, с которыми в канале связи может произойти все что угодно. В результате поток данных может быть произвольно модифицирован. Единственный известный человечеству способ обеспечить целостность данных при передаче по такому ненадежному каналу - это делить его на фрагменты и добавлять к ним средства проверки целостности (контрольные суммы). А если нужен поток с гарантией целостности, то добавлять средства контроля доставки в нужном порядке, подтверждение доставки с приемной стороны и повтор передачи на передающей стороне. Мы не пойдем настолько далеко в нашем адаптере. Но деление данных на пакеты в нем предусмотрено. Он получает их в виде пакетов на передающей стороне и доставляет пакеты с сохранением границ на приемную сторону. Так что пользователь лишен необходимости делить поток на пакеты самостоятельно. И последнее, но не менее важное обстоятельство, - с прозрачной передачей потоковых данных невозможно реализовать передачу данных в несколько соединений одновременно.
Протокол управления
Для управления адаптером и передачи данных он реализует простой асинхронный протокол, схематически показанный на следующем рисунке.
![Протокол управления адаптером Протокол управления адаптером](https://habrastorage.org/getpro/habr/upload_files/338/234/6d8/3382346d87410cee855ae0e3cc68975e.png)
В качестве управляющей подсистемы может выступать сколь угодно простое устройство, имеющее последовательный порт. От него адаптер получает команды и данные для передачи. Ему он в свою очередь передает данные, полученные от подключенных устройств, нотификацию о своем состоянии и отладочные сообщения. Основных команд всего две - сброс и подключение списка устройств. Устройства идентифицируются по их адресу. Идентификация по имени не предусмотрена, поскольку она требует дополнительной процедуры поиска устройства, к тому же имена не являются уникальными. Чтобы узнать адрес устройства, можно, например, воспользоваться программой nRF Connect.
Реализация протокола на языке python содержится в файле python/ble_multi_adapter.py
Передача бинарных данных
![Кодирование бинарных данных Кодирование бинарных данных](https://habrastorage.org/getpro/habr/upload_files/162/25a/fda/16225afda12b00daf983dcaa6b3fad62.png)
Поскольку протокол использует определенные байты как маркеры начала и конца сообщения (белые и черные кружки на рисунке выше), наличие этих байт в передаваемых данных нарушает протокол обмена. Для передачи произвольных бинарных данных их необходимо закодировать в base64 перед отправкой адаптеру. Байт со значением 2 добавляется в начало блока данных в качестве маркера закодированных бинарных данных. Адаптер раскодирует данные, отправит их на приемную сторону, где они будут снова закодированы в base64.
Расширенный пакетный режим
Адаптер рассчитывает, что полученный им пакет данных можно записать в характеристику, что ограничивает его размер. Адаптер использует фрагменты данных до 244 байт, которые теоретически должны передаваться без дальнейшей фрагментации. Чтобы передавать пакеты данных большего размера адаптер реализует расширенный пакетный режим, в котором пакеты делятся на фрагменты, каждый из которых снабжен однобайтным заголовком и трехбайтовой контрольной суммой, как показано на рисунке ниже.
![Расширенный пакетный режим Расширенный пакетный режим](https://habrastorage.org/getpro/habr/upload_files/c60/abb/04a/c60abb04a6be0a2c9f17f3b298727af6.png)
Использование расширенного пакетного режима совершенно прозрачно для пользователя и рекомендуется для всех случаев, кроме тех, когда нужно взаимодействие с другими BLE устройствами, которые не имеют возможности обрабатывать фрагментированные пакеты расширенного режима. В конфигурационном файле расширенный режим включен по умолчанию (EXT_FRAMES). По умолчанию максимальный размер пакета данных в расширенном режиме равен 2160 байт. При необходимости его можно увеличить, изменив параметр MAX_CHUNKS в файле конфигурации.
Обработка ошибок
Адаптер использует простую, но чрезвычайно эффективную стратегию обработки ошибок - он просто рестартует заново. В том числе, это происходит при разрыве соединения.
Проблемы
В ходе работы над проектом было обнаружено немало проблем и даже багов в библиотеках и Ардуино классах на их основе.
Как уже отмечалось, использование управления потоком последовательного порта это хорошо и правильно. Но есть один нюанс. При использовании двухстороннего управления RTS/CTS возможна взаимоблокировка передатчика и приемника, когда они оба пытаются записать что то в последовательный канал, при том, что у обоих буфер приема переполнен. Взаимоблокировка возникает от того, что и передатчик и приемник могут либо передавать, либо принимать данные, но не могут это делать одновременно. Есть казалось бы простое и очевидное решение - не добавлять данные в буфер передачи, если в нем нет для них места. К несчастью библиотека ESP32 не позволяет достоверно узнать размер свободного места в буфере передатчика. Так что на данный момент рекомендация сводится к тому, чтобы использовать RTS, чтобы исключить переполнение приемного буфера адаптера, но не использовать CTS.
Следует иметь ввиду, что при работе адаптера через USB CDC нет никакого управления потоком вообще. Новые данные просто перетирают старые.
Неожиданностью стало то, что метод BLERemoteCharacteristic::writeValue, который зовется, чтобы записать данные в периферийное устройство, непригоден для использования вообще. По замыслу авторов он должен дожидаться завершения предыдущей записи. Для этого он берет семафор, но не отдает его в случае ошибки, так что следующий вызов повисает навсегда. Пришлось дублировать эту функциональность в коде адаптера.
Интересно, что запись без подтверждения, которая выполняется в библиотеке по умолчанию, приводит к нестабильности соединений в случае, если их больше одного. Запись с подтверждением не имеет такой проблемы. Она и используется в адаптере.
Как выяснилось, работа адаптера одновременно в двух ролях хотя и возможна, но может приводить к нестабильности соединения. Возможно, это даже фича, а не баг. Центральное устройство управляет периодами активности в эфире как для себя, так и для подключенных периферийных устройств. Значит, устройство с двумя ролями одновременно должно как само управлять периодами активности своего радио модуля, так и управляться другим центральным устройством. Возможно, это противоречие и приводит к проблемам при одновременном использовании двух ролей.
Компиляция, настройки
Для компиляции не потребуется ничего, кроме Ардуино. В настройках (Additional board manager URLs) добавьте ссылку на пакет поддержки ESP32:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
Загрузите пакет esp32 by Espressif Systems в менеджере плат. Выберите ESP32C3 Dev Module, ESP32S3 Dev Module или ESP32C6 Dev Module в зависимости от того, какой процессор у вас на плате. С другими возможно тоже работает, я тестировал эти Разрешите в настройках платы USB CDC On Boot. Установите правильный COM порт, соответствующий подключенной плате, и плату можно прошивать. Имейте ввиду, что платы вообще без прошивки при подключении входят в цикл перезагрузки. Чтобы прошить такую плату, ее нужно перевести в режим загрузки. Для этого нажмите и удерживайте кнопку BOOT, потом нажмите и отпустите кнопку RST, затем отпустите BOOT. После прошивки нужно нажать RST, чтобы выйти из режима загрузки.
Проект имеет множество настроек времени компиляции, которые вынесены в заголовочный файл mx_config.h. Он включает другой заголовочный файл (по умолчанию default_config.h), который вы можете скопировать, переименовать и настроить по своему вкусу. В настройках вы можете выбрать имя устройства, режим его работы, адрес устройства для автоматического соединения, если вам нужен канал связи, который устанавливается автоматически, и многое другое.
Производительность
Максимальная скорость передачи данных, полученная в эхо-тесте для одного соединения, составляет около 4kБ/сек в одну сторону (+ столько же в другую). Для четырех соединений в каждом из них скорость падает до 1.5kБ/сек, что видимо является ограничением последовательного порта. При желании его скорость можно увеличить в настройках (UART_BAUD_RATE).
Передача данных от периферийного устройства к центральному при перегрузке канала демонстрирует рост потерянных пакетов. Фактически доставляется лишь столько пакетов, сколько канал способен пропустить, что ожидаемо т.к. они используют полностью асинхронный механизм нотификации. Запись в обратном направлении от центрального устройства к периферийному более устойчива к перегрузке, т.к. она использует запись с подтверждением, и при перегрузке начинает тормозить прием данных из последовательного порта.
Энергопотребление
Модуль на основе ESP32C3 при работе на максимальной тактовой частоте 160MHz в покое потребляет около 60мА. Модуль на основе ESP32S3 при работе на максимальной тактовой частоте 240MHz в покое потребляет около 95мА. Видимо, сказывается наличие двух процессорных ядер. Модуль на основе ESP32C6 при работе на максимальной тактовой частоте 160MHz в покое потребляет 68мА. При работе на пониженной частоте 80MHz потребление составляет 50мА для ESP32C3, 65мА для ESP32S3 и 60мА для ESP32C6.
Потребление под нагрузкой измерялось в следующем тесте. Центральное устройство устанавливало соединения к трем периферийным. Те передавали ему по 50 коротких сообщений в секунду каждое. В одном варианте теста центральное устройство никак их не обрабатывало, в другом посылало каждое полученное сообщение обратно периферийному устройству. Результаты измерения энергопотребления в этом тесте показаны на рисунке ниже. Центральное устройство изображено сверху, периферийные - снизу.
![Энергопотребление в нагрузочном тесте Энергопотребление в нагрузочном тесте](https://habrastorage.org/getpro/habr/upload_files/369/9c0/b79/3699c0b79b6690761907ea12394ed589.png)
Как видим, передача данных от центрального устройства к периферийному наиболее затратна для центрального устройства, но почти никак не сказывается на энергопотреблении периферийного. Видимо, BLE вообще ориентирован на минимизацию энергопотребления именно периферийных устройств. В целом ESP32С3 показывает наилучшие результаты по энергопотреблению, но есть один нюанс.
Проблема брака
Как показали тесты, покупая самые дешевые ESP32C3 модули вроде Super Mini вы с высокой вероятностью можете получить брак. Типичные случаи брака, с которыми столкнулся я, это неправильно запаянные светодиоды (самый безобидный), запаянный процессор без встроенной флеш памяти и неработающий радио модуль процессора. Среди модулей, сделанных под брендом WeAct, брака обнаружено не было.
Дальность связи
Сильно зависит от антенны. Наихудший вариант - чип антенна, вроде тех, что стоят на модулях ESP32C3 Super Mini. Она превращает электрическую энергию преимущественно в тепло. Не стоит ожидать от нее дальности больше 10 метров. Печатная антенна уже значительно лучше. Наилучшие результаты дают внешние антенны, даже самые простые. Если на плате стоит чип антенна, и нет разъема для внешней, - просто удалите чип антенну и припаяйте внешнюю, как показано на фото.
![Внешняя антенна припаянная вместо чип антенны к ESP32C3 Super Mini Внешняя антенна припаянная вместо чип антенны к ESP32C3 Super Mini](https://habrastorage.org/getpro/habr/upload_files/5f1/8e2/fcb/5f18e2fcb660bb6436a697c7e5c796f7.jpg)
При этом есть важный нюанс. Если вы думаете, что нижняя (на фото) площадка, к которой была припаяна чип антенна, соединена с землей, то это не так. Я тоже так думал. Но, как выяснилось, она не соединена ни с чем. Поэтому, припаивая внешнюю антенну, важно соединить оплетку кабеля с землей. На фото она соединена с земляной площадкой расположенного рядом кварцевого резонатора. Если этого не сделать, то весь кабель будет работать как одна большая антенна. Мощность излучаемого сигнала при этом может даже вырасти. Но вот что точно вырастет, так это энергопотребление радио модуля. Я наблюдал рост потребления в нагрузочных тестах до 140мА и более. Со временем в таких условиях постоянной перегрузки выход радио модуля может деградировать.
Дополнительно увеличить дальность можно за счет программного увеличения мощности передатчика. Эта опция включена по умолчанию в файле конфигурации (TX_BOOST). В таком варианте дальность работы на открытой местности составляет 100м. В помещении возможна устойчивая работа между соседними этажами через железобетонное перекрытие.
Совместимость
Код адаптера протестирован на процессорах ESP32, ESP32C3, ESP32S3, ESP32C6.
Адаптер может работать совместно с JDY-08 и им подобным адаптерам при условии, что вы передаете не более 20 байт данных за один раз. Он также полностью совместим с Web BLE приложениями, например с этим. Приложение по ссылке удобно использовать для тестирования.
Исходный код
Лежит тут https://github.com/olegv142/esp32-ble
Директория проекта для Ардуино ble_uart_mx
Комментарии (24)
NeoCode
27.08.2024 16:43У меня сломался (протек) чайник, и перед тем как его выбросить я вытащил из него плату с BLE адаптером NRF51822, также на ней двухразрядный семисегментный индикатор, 4 кнопки, зуммер, датчик температуры на проводе и выход для управления реле. Питается от 5 вольт, в общем могла бы быть неплохая игрушка для того чтобы вывести бегущую надпись "HELLO HAbr", но вот как ее программировать?
oleg_v Автор
27.08.2024 16:43Продвинутый чайник, однако. Я давно с NRF игрался, программировал их и в IAR, его еще найти надо и вылечить от жадности, и программатор нужен. До сих пор валяются. С STM32 и ESP32 как то попроще
gudvinr
27.08.2024 16:43+1https://www.nordicsemi.com/Products/nRF51822/GetStarted
Буквально первая ссылка по запросу "NRF51822 programming"
Но:
Там могут не быть разведены JTAG и UART, придется напрямую к ногам паяться
Возможно включен secure boot, тогда неподписанную прошивку нельзя залить в принципе
NeoCode
27.08.2024 16:43Сбоку на плате есть 4 контактных отверстия. Вполне может быть или UART или SWD, но без осциллографа не посмотреть:)
Скрытый текст
m0tral
27.08.2024 16:43SWD вполне себе смотрится без осциллографа, а вот Secure Boot вполне вероятно что включен.
Kudriavyi
27.08.2024 16:43+1nRF51 устаревшая платформа, ее сам нордик уже не поддерживает. Возьмите лучше nRF52840 на али за 300 рублей и китайский j-link, который все равно придется купить и для этой платы.
okhsunrog
27.08.2024 16:43Очень просто, ничуть не сложнее, чем STM32. Возьмите любой STLink, там же обычный ARM. Софт OpenOCD для программирования и отладки. Насчёт фреймворков - можно взять Zephyr и писать на Си, можно взять Embassy и писать на Rust (тогда прошивать через софтинку probe.rs нужно будет, тоже удобнейший инструмент)
shadrap
27.08.2024 16:43Кстати эспрессив рекомендует тщательно проверять импеданс внешней антенны - 50 ом , ибо обвязка настроена именно так , иначе возникают шумы и искажения что приводит к большому количеству ошибок. Конечно энергопотребление в 120ма это жесть .... зачем нужно Бле в таком случае..
slog2
27.08.2024 16:43Если уже есть ESP32 то ещё какая-то дополнительная слабая ардуинка тут как пятое колесо в телеге. Основное преимущество ради чего стоит использовать BLE - минимальное потребление и оно тут утеряно.
oleg_v Автор
27.08.2024 16:43Это скорее чтобы прикрутить к готовой железке, которая питается не от батарей.
oleg_v Автор
27.08.2024 16:43Почему дополнительная ардуинка? Тут про то, что код ESP32 под ардуино компилится. Дополнительной ардуинки не требуется.
oleg_v Автор
27.08.2024 16:43Как выяснилось, аномальное энергопотребление было связано с неправильным соединением с внешней антенной. Добавил в текст про это.
gudvinr
Зачем тут arduino нужен? В ESP-IDF уже есть BLE стек, а arduino итак работает поверх IDF.
По большому счёту, вся Arduino вообще не используется в этом проекте.
oleg_v Автор
Исключительно чтобы компилить и прошивать было проще
r1000ru
Рекомендую попробовать связку VSCode+Platformio. Ещё более удобно.
Так же замечу, что Espressif рекомендует использовать BLE стек NimBLE, а не их собственный, если нет необходимости в одновременной работе BLE и классических профилей (например SPP): https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/index.html
oleg_v Автор
Я пробовал nimBLE, он показался мне заметно менее надежным. Ну и вообще странная идея - рекомендовать левый стэк вместо того, чтобы чинить свой. Вы уж тогда и баги в нем чините.
okhsunrog
Я всё же рекомендую CLion/VSCode + esp-idf. У esp-idf очень удобное расширения для VS Code, например