Щупаем CDEBYTE E32 (LoRa)

Наврядли я ошибусь, утверждая то, что каждый искавший информацию по использованию связки Arduino+LoRa, встречал статьи и видеобзоры модулей от CDEBYTE (ранее - Ebyte).

Компактные модули, более популярные среди DIY-обзорщиков в исполнении UART, обмеряны, наверное, уже со всех сторон. Так что я не буду писать очередной пост на тему настройки параметров связи через 0xC2 или о способе легко и просто пульнуть millis() на 8 км. Вместо этого предлагаю слегка занырнуть в тёмные глубины разума разработчиков CDEBYTE и узнать, как и о чем чирпают их модули, а так же почирпать в унисон с ними.

Всё, мной разведанное и проверенное, относится к модулю E32-900T20D v8, а так же, полагаю, в той или иной мере, ко всей серии E32. Скорее всего и к остальным - E22/E220/тд, но таковых модулей у меня нет, поэтому возможность проверки гипотезы отсутствует.

Второй стороной радиообмена в моём эксперименте будет выступать плата на базе ESP32 - TTGO T3 v1.6.1 с модулем Lora32 (чип Semtech SX1276), подключенным по SPI.

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

И в реальности (модель прошлых сезонов, E32-TTL-100):

image

Модуль E32 способен работать в прозрачном режиме, передавая полученные через UART данные другим таким же (или совместимым) модулям, из которых всё полученное выпадает прямо в UART микроконтроллеров. Проверка адреса получателя производится самим E32, поэтому сторонний LoRa-абонент, после несложных манипуляций, захватывает все передаваемые данные.

Настройки радиочасти чипа SX1276, находящегося внутри E32, напрямую недоступны и задаются опосредованно через параметр “Air datarate” - скорость радиобмена. Исследование выявило следующее соответствие:

  • 300 Bps : BW = 500000, SF = 11, CR4 = 6 (*);
  • 1200 Bps : BW = 500000, SF = 11, CR4 = 6 (*);
  • 2400 Bps: BW = 500000, SF = 11, CR4 = 5 (*);
  • 4800 Bps: BW = 250000, SF = 8, CR4 = 5;
  • 9600 Bps: BW = 500000, SF = 8, CR4 = 5;
  • 19200 Bps: BW = 500000, SF = 7, CR4 = 5.

(*) Примечательно, что при настройках, соответствующих 2400 Bps, TTGO T3 ловит пакеты, отправленные через E32 на 300 Bps и 1200 Bps, но вот второй E32 их принимает неустойчиво (хотя и принимает). К сожалению, я не обладаю инструментарием, который позволили бы прояснить данную особенность. Поэтому точные значения BW/SF/CR4 на данный момент под вопросом.

Передаваемый в радиоэфир пакет Lora на физическом уровне имеет тип Explicit Header. Размер преамбулы и sync word стандартные - 0x08 и 0x12 соответственно. CRC включён.

Инкапсулированный пакет данных CDEBYTE может быть как “простым”, так и “защищённым” с помощью технологии FEC - Forward Error Correction. Автоопределения типа пакета модули не осуществляют.

Структура “простого” пакета (FEC disabled):

| Header - 4 byte | User data - 1..58 bytes | Checksum - 1 byte |

Структура заголовка (header) “простого” пакета:

    | Data length - 1 byte | Channel No  - 1 byte | Target address high byte - 1 byte | Target address low byte  - 1 byte |
  • Data length - размер пользовательских данных. Формируется следующим образом: 0x40 + N, где N = 1…58;
  • Channel No - номер канала. Используется для вычисления ключа шифрования, допускает значения: 0x00…0x45 (*);
  • Target address - адрес получателя. Широковещательным адресом, согласно документации, является значения 0xFFFF и 0x0000. При этом должен быть жестко задан канал 0x04 (**);
  • User data - пользовательские данные. Зашифрованы однобайтным ключом методом XOR: data ^ key;
  • Checksum - Проверочная сумма. Является дополнительным кодом восьмиразрядной контрольной суммы всех байт пакета.

(*) Примечание: установленный в настройках E32 номер канала дополнительно смещает рабочую частоту радиопередачи. Для модуля на 868MHz результирующая частота такова: 862MHz + Channel No.
(**) Бесстыдное враньё. Гадание по картинкам в даташите завело не в ту степь.

Функция вычисления контрольной суммы:

static uint8_t calcCDEByteChecksum(const uint8_t* _data, uint8_t _size) {
  uint8_t checksum = 0x00;
  while (_size--) { checksum += *_data++; }
  checksum = 0x100 - checksum;
  return checksum;
}

Функция вычисления ключа шифрования так же частично использует таблицу подстановок:

static uint8_t getCDEByteKey(const uint8_t _channelNo) {
  const uint8_t keyLUT[] = {0x0A, 0x09, 0x08, 0x0F, 0x0E, 0x0D, 0x0C, 0x03, 0x02, 0x01, 0x00, 0x07, 0x06, 0x05, 0x04, 0x0B};	
  return ((_channelNo + 0x99) & 0xF0) |  keyLUT[_channelNo & 0x0F]);
}

Пример захвата “простого” пакета:

[*] Received packet w/RSSI = -17
[*] Packet raw data:  42 06 00 00 9C AD 6F
[*] Decoded OK: 
	 channelNo: 06
	 targetAddress 0000
	 size: 02
	 payload:  00 31

Структура “защищённого” пакета (FEC enabled):

| FEC-coded data - 12..126 bytes | CRC - 2 byte |
  • FEC-coded data - “простой” пакет, закодированный с помощью таблицы (см. ниже) подстановок полубайтов, которая используется следующим образом: нечётный байт FEC-данных, через подстановку трансформируется в старший полубайт байта “простого” пакета, чётный байт после тех же операций становится младшим полубайтом. Например 0x65 0x59 => 0x04 0x02 => 0x42;
  • CRC - 16-битная контрольная сумма, вычисленная по алгоритму CRC16/KERMIT, параметры: Poly 0x1021, Init 0x99C0, Final XOR 0x0000, Reverse input, Reverse output.

Таблица подстановок FEC:

static const uint8_t fecLUT[] = {0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA};

Пример захвата “защищённого” пакета:

[*] Received packet w/RSSI = -23
[*] Packet raw data:  65 59 55 69 55 56 55 5A 96 A5 99 A6 69 9A F5 D1
[*] Decoded OK: 
	 channelNo: 06
	 targetAddress 0103
	 size: 02
	 payload:  00 31

Однако, всё это может не заработать без последнего ингридиента: флага LDO. Его следует выставить в true. Мне для этого пришлось внести модификацию в библиотеку arduino-LoRa by sandeepmistry, из issues которой я и получил стартовую информацию о перехвате траффика.

Итак, теперь, приложив полученные знания к чипу SX1278/SX1276 и знакомому микроконтроллеру, вы, как и я, сможете не только подслушать разговор модулей CDEBYTE, но и послать им горячий привет.

Продолжение следует. Надеюсь.

6 лайков

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

В руководстве по модулю указано, что он работает как с целевой (unicast) адресацией, так и с широковещательной (broadcast). Так же имеется два режима пересылки данных - прозрачый и фиксированный, который упоминается в документе как “similar to MODBUS”. Придумщики знатные, конечно. Всё это пояснено картинками и полунамёками, поэтому потребовался натурный эксперимент и изучение руководств от других модулей.

Итак, адрес назначения (target address) передаётся в заголовке пакета CDEBYTE. В заголовок он попадает разными способами - в зависимости от установленного бита Fixed transmission enabling bit в байте OPTIONS (поле Fixed mode в утилите RF Settings).

Если модуль находится в прозрачном режиме (Transparent transmission mode), то адрес назначения берётся из настроек самого модуля. См. скриншот или описание команд 0xC0 и 0xC2.

image

Если модуль находится в фиксированном режиме (Fixed transmission mode), то канал и адрес задаются в первых трёх байтах пользовательских данных, а не берутся из настроек модуля. Например: при посылке строки “111” в канале 0x16 абоненту 0x4512, необходимо сформировать следующий пакет { 0x45, 0x12, 0x16, 0x31, 0x31, 0x31 }. После этого модуль переключится на радиоканал 0x16, даже если он находился на другом, и осуществит передачу. Абонент 0x4512, слушающий канал 0x16 получит “111”. Переключение канала без дёрганья входов M0 и M1 - основная фича данного режима.

Далее - установив адрес назначения в 0000 или FFFF тем или иным способом, и, соответственно, поместив его в пакет, мы инициируем широковещательную передачу, которую все модули, слушающие на совпадающем с вещателем канале (см. поле Channel), обрабатывают вне зависимости от их собственных адресов. Например: отправитель 0xFFFF , получатель 0x4512 - пакет будет получен и переправлен МК; отправитель 0x1234 , получатель 0x4512 - пакет будет дропнут; отправитель 0x1234 , получатель 0xFFFF - пакет будет получен и переправлен МК.

UPD:
На моём модуле вот этот пример из документации не работает так, как написано. Широковещательный пакет проходит только с адресом 0xFFFF, а 0x0000 работает как обычный адрес:

5.3 Broadcasting address

  • For example: Set the address of module A as 0xFFFF or 0x0000, and the channel as 0x04;

Такая эквилибристика.

3 лайка

Кстати, я был бы рад, если на форуме нашлись любители или профессионалы, которым показалось бы знакомым применёное FEC-кодирование нибла данных.

На простой код Хемминга вроде не похоже, но, судя по обещаниям из документации, должно давать возможность как-то обнаруживать и корректировать хотя бы единичные ошибки приёма.

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

Не то, чтобы это незнание меня сильно беспокоило, но всё же…

HEX FEC BIN FEC DEC BIN
55 01010101 0 0000
56 01010110 1 0001
59 01011001 2 0010
5A 01011010 3 0011
65 01100101 4 0100
66 01100110 5 0101
69 01101001 6 0110
6A 01101010 7 0111
95 10010101 8 1000
96 10010110 9 1001
99 10011001 10 1010
9A 10011010 11 1011
A5 10100101 12 1100
A6 10100110 13 1101
A9 10101001 14 1110
AA 10101010 15 1111

Видно что пара бит содержит прямое и инверсное значение бита из нибла. Вот тебе и защита.)

Это я уже под вечер понял - обнаружение ошибок доступно. Но оно доступно и по CRC16, которое, к слову, не покрывается FEC и подвержено искажению не хуже “защищенной” части. Поэтому от меня ускользает смысл дополнительного перемешивания битов для коррекции чего-либо.

Завтра хочу проверить - действительно ли будет восстановлена специально внесенная в пакет ошибка или же в документации допущена очередная ошибка и FEC на самом деле расшифровывается как Foolish Error Correction.

UPD: если принять во внимание физический уровень LoRa, на котором существует как минимум одна проверка CRC и FEC, заключающийся в добавлении избыточной информации (Coding Rate), то, похоже, CDEBYTE бессмысленно увеличивает объём передаваемых данных при активации своей опции FEC.

Не трудно заметить, что при передаче не может идти подряд больше двух единиц либо нулей. Длинные цепочки одинаковых значений - трудная для определения последовательность для канала передачи, который не может передать постоянную составляющую.
Т.е. при таком кодировании спектр частот жестко ограничен не только сверху, но и снизу. В итоге речь идет не столько о коррекции ошибок, сколько о том, чтобы они вообще не появлялись.

Стало быть толковать документ от CDEBYTE следует так: при передаче данных совокупность применённых технологий обеспечивает помехозащищенность - коррекцией ошибок на уровне LoRa и её предупреждением на уровне пакета CDEBYTE. Верно понимаю?

Производитель классически преувеличил свой вклад в FEC, уведя уровень PHY в тень, выходит.

…но, удивительно, что CRC16 не подверглось той же прополке, что и остальной пакет. Решили, что вероятность возникновения ошибок и так снижена прилично, а в CRC не возникнет нулей/единиц на секунду передачи и можно не париться?

UPD: опять же, как я вспомнил, - технологически в PHY LoRa символы передаются чирпом, а он всегда имеет перепад, нет одного уровня ни при каком символе. Получается, что постоянной составляющей в канале не бывает.

Я не вникал в детали (и, честно говоря, желания такого нет). Поэтому все, что я могу в данной теме написать - это досужий вымысел. Соответственно, прошу к этому так и относиться. Т.е. - максимум, предположения из общих соображений.
По поводу crc16 рискну предположить, что автор посчитал ее вообще излишней, но где-то она формально требуется, поэтому присутствует. Остается, правда, неясным, что делать, если пакет пришел, вроде, без ошибок, а в crc16 - ошибка возникла.

Продолжаем бесчеловечные опыты. На этот раз стороне TTGO вносил искажения в подготовленные к передаче пакеты и отсылал на E32. Результаты удивляют.

“Простой” пакет

Неискаженный принимает.

[*] Sending data (size = 3):  31 31 31
[*] Encode packet:  43 06 00 00 AD AD AD B0
[*] Modify packet:  43 06 00 00 AD AD AD B0

Искаженный не принимает

[*] Sending data (size = 3):  31 31 31
[*] Encode packet:  43 06 00 00 AD AD AD B0
[*] Modify packet:  43 06 00 00 AD AD A5 B0

Модифицировал так:

bitWrite(cdeBytePacketSend.payload[0x06], 0x03, !bitRead(cdeBytePacketSend.payload[0x06], 0x03));

“Защищённый” пакет

Неискажённый принимает:

[*] Sending data (size = 3):  31 31 31
[*] Encode packet:  65 5A 55 69 55 55 55 55 99 A6 99 A6 99 A6 9A 55 FA DB
[*] Modify packet:  65 5A 55 69 55 55 55 55 99 A6 99 A6 99 A6 9A 55 FA DB

Искаженно тело пакета. Принимает:

[*] Sending data (size = 3):  31 31 31
[*] Encode packet:  65 5A 55 69 55 55 55 55 99 A6 99 A6 99 A6 9A 55 FA DB
[*] Modify packet:  CF 1F 55 69 55 55 55 55 99 A6 99 A6 99 A6 9A 55 FA DB

Модифицировал так:

//bitWrite(cdeBytePacketSend.payload[0x00], 0x00, !bitRead(cdeBytePacketSend.payload[0x00], 0x00));
bitWrite(cdeBytePacketSend.payload[0x00], 0x01, !bitRead(cdeBytePacketSend.payload[0x00], 0x01));
//bitWrite(cdeBytePacketSend.payload[0x00], 0x02, !bitRead(cdeBytePacketSend.payload[0x00], 0x02));
bitWrite(cdeBytePacketSend.payload[0x00], 0x03, !bitRead(cdeBytePacketSend.payload[0x00], 0x03));
//bitWrite(cdeBytePacketSend.payload[0x00], 0x04, !bitRead(cdeBytePacketSend.payload[0x00], 0x04));
bitWrite(cdeBytePacketSend.payload[0x00], 0x05, !bitRead(cdeBytePacketSend.payload[0x00], 0x05));
//bitWrite(cdeBytePacketSend.payload[0x00], 0x06, !bitRead(cdeBytePacketSend.payload[0x00], 0x06));
bitWrite(cdeBytePacketSend.payload[0x00], 0x07, !bitRead(cdeBytePacketSend.payload[0x00], 0x07));

bitWrite(cdeBytePacketSend.payload[0x01], 0x00, !bitRead(cdeBytePacketSend.payload[0x01], 0x00));
//bitWrite(cdeBytePacketSend.payload[0x01], 0x01, !bitRead(cdeBytePacketSend.payload[0x01], 0x01));
bitWrite(cdeBytePacketSend.payload[0x01], 0x02, !bitRead(cdeBytePacketSend.payload[0x01], 0x02));
//bitWrite(cdeBytePacketSend.payload[0x01], 0x03, !bitRead(cdeBytePacketSend.payload[0x01], 0x03));
bitWrite(cdeBytePacketSend.payload[0x01], 0x04, !bitRead(cdeBytePacketSend.payload[0x00], 0x04));
//bitWrite(cdeBytePacketSend.payload[0x01], 0x05, !bitRead(cdeBytePacketSend.payload[0x01], 0x05));
bitWrite(cdeBytePacketSend.payload[0x01], 0x06, !bitRead(cdeBytePacketSend.payload[0x01], 0x06));
//bitWrite(cdeBytePacketSend.payload[0x01], 0x07, !bitRead(cdeBytePacketSend.payload[0x01], 0x07));

Искажена CRC16. Не принимает:

[*] Sending data (size = 3):  32 32 32
[*] Encode packet:  65 5A 55 69 55 55 55 55 99 A9 99 A9 99 A9 99 A6 2C 68
[*] Modify packet:  65 5A 55 69 55 55 55 55 99 A9 99 A9 99 A9 99 A6 2C 69

Модифицировал так:

bitWrite(cdeBytePacketSend.payload[cdeBytePacketSend.size-0x01], 0x00, !bitRead(cdeBytePacketSend.payload[cdeBytePacketSend.size-0x01], 0x00));

Итоги:

  1. Восстанавливаются данные при ошибке в любых двух и более непоследовательных битах (например: 0, 3, 7) тела пакета. При искажении двух последовательно идущих битах (например 2, 3) пакет отбрасывается.
    Восстанавливаются данные при одной ошибке в паре бит (например: 0-1, 2-3, …) тела пакета. Наличие ошибки в смежных битах пары так же исправляется (например: 1-2). При искажении двух битов в паре (например 2-3) пакет отбрасывается.
  2. При искажение любого бита CRC16 пакет отбрасывается.

Выходит, что FEC, как ни странно, есть но какой-то недоделанный, китайский. Что мешало CRC16 покрыть FEC-ом - непонятно.

И, всё же, пока неясно, как из 11 или 00 однозначно восстанавливается 01 и 10.

2 лайка

Пришла мне в голову ещё одна идейка по запыталову: раз E32 так легко из 11 и 00 восстанавливает истинные значения 01 и 10, то не сунуть ли ему под пилу рельсу… Возьму, думаю, FEC B01010101 (55) и FEC B10101010 (AA), да изменю в каждой паре бит по одной штуке, коли он это нормально перепиливал. Сделаю два одинаковых FEC байта B11111111 (FF) из первых двух и пусть разбирается.

И что бы вы думали - восстановил, мерзавец. Приём идет.

55 FF FF AA восстановил до 55 55 AA AA
55 55 FF FF восстановил до 55 55 AA AA
FF 55 FF AA восстановил до 55 55 AA AA

А вот при искажении третьего байта - всё, не может.

55 FF FF FF не восстановил до 55 55 AA AA

image

Т.е. исправляет в FEC-кодировке ошибки размером до двух байт при условии, что в этих байтах не искажены полностью пары бит. Если во всех парах бит двух любых (?) байт искажен только один бит - всё ок, возвращает биты на место, CRC сходится и пакет декодируется. Сдаётся мне, что к делу привлечён CRC. Избыточность, похоже, в теле пакета не прячется.

Снимаю шляпу перед товарищами китайцами. Не зря рис едят.

2 лайка

А мы - перед тобой. :slight_smile: Не зря налоги плотишь.

Шикарный обзор. :+1:

хм… а почему в “Общем” разделе?

Так проект целиком я не готов пока предоставить, чисто аппаратного тут нет. Чисто программного тоже. Так, все скопом - общее.

Приведу пару фотографий волнующегося эфира, захваченного через SDR# с RTL-SDR тюнером и обычной телескопической антенной.

Переключение каналов E32 на ходу в Fixed Mode - поочередные посылки в каналы 0x06 и 0x07:

LoRa-пакеты в анфас (bitrate 7800, sender on TTGO)

Преамбула 0:

Преамбула 4:

К сожалению не могу понять, как мне на скорости, которую выдаёт E32 получить такую же картинку и сравнить 300bps с 2400bps, чтобы понять - разные параметры SF и CR или нет.

Если кто-то подскажет, как сделать захват эфира и зазумить “пилу” по времени - буду благодарен.

Вот не понимаю зачем вы щупаете это готовое изделие (НИОКР проводите). А какова цель всей этой работы? Ведь всё равно в аппаратной части ни чего не измените. Или вы это всё затеяли ради собственного любопытства? Вы задаёте нам вопросы, ответы на котрые наверно можно найти потратив кучу времени. Я могу понять, если человек сделав реверсинженеринг придумает изделие лучше, и не просто придумает, а начнет его производство и продажу. Я двумя руками за. Вот любой из нас с высоты своего опыта изобретал железку и планировал выпустить её на рынок, он бы не выжал из неё максимум? Сомневаюсь. Может я не прав и это IMHO

импортозачещение видимо

Все верно.

1 лайк

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

3 лайка

Да я только за. Просто вегда удивляют люди, которые тратят время на исследования, ради собственного интереса. По этому и задал вопрос ТС. К тому же скорее всего данные исследования могут так же оказатся из недавних историй нужен ли конденсатор. И по этому вряд ли что то будет помещено как дополнение к китайскому даташиту. Конечно это всё IMHO. Садмана уважаю, как грамотного и отзывчивого программиста.

Здравствуйте.
Посмотрела видео про Lora E22

Видео

https://www.youtube.com/watch?v=-VVtaB0fOcE
https://www.youtube.com/watch?v=7E53laRrsjA

В продаже на Али E32
Есть ли разница между E22 и E32 ?
Потом есть более дешёвый модуль E220
Разница в мощности?
А еще дешевле 400M22S