Kendi LoRa düğümümü zor yoldan çözmek
Bir ESP32, EBYTE LoRa modülü üzerinden nem gönderiyor. Bunu HackRF ile havadan geri okumak olması gerekenden iki gün uzun sürdü — çünkü doğru bir ölçüm, sormadığım bir soruyu yanıtlıyordu.
Tezgâhımda, üstüne EBYTE E32-900T20D vidalanmış bir ESP32 duruyor ve saniyede bir nem ölçümü gönderiyor. Ne dediğini biliyorum, çünkü firmware'i ben yazdım. Asıl merak ettiğim soru başkaydı: vericiyi bir kara kutu gibi ele alıp, bunu HackRF One ile havadan okuyabilir miyim?
Soru "LoRa alabilir miyim" değil — gr-lora_sdr var ve çalışıyor. İşin ilginç
tarafı, E32'nin veri sayfasının işe yarar neredeyse hiçbir şey söylememesi. kbps
cinsinden bir "hava veri hızı" ve bir kanal numarası veriyor. Yayılma faktörünü,
bant genişliğini, sync word'ü ya da modülün baytlarına çıkışta ne yaptığını
söylemiyor. Bunların hepsinin havadan gelmesi gerekti.
Beklediğimden epey uzun sürdü ve zamanın çoğu, iki gün boyunca doğru görünen tek bir yanlış varsayıma gitti.

PortaPack'in buradaki tek katkısı kasası — o ekranda hiçbir şey çözülmüyor. Örnekler USB kablosundan çıkıyor ve aşağıdaki her adım, kablonun öbür ucundaki GNU Radio'da oluyor.
Düğüm aynı masanın bir metre ötesinde duruyor ve bu, kulağa geldiğinden daha önemli. O mesafede sorun hiçbir zaman zayıf sinyal değil; yani bu yazıdaki bütün hatalar, sinyal fazlasıyla varken yapıldı.
LoRa gerçekte neye benziyor
LoRa, cıvıltı yayılı spektrum (chirp spread spectrum). Bir sembol, kanal boyunca doğrusal bir frekans süpürmesi: bandın bir yerinden başla, sabit hızla yukarı tırman, tepeye varınca başa sar, bant genişliğini bir kez süpürünce dur. Veri süpürmenin içinde değil. Veri, süpürmenin nereden başladığında.
Kaydedince yapı hiçbir çözümlemeye gerek kalmadan görünüyor:

Önbaşlık (preamble), birbirinin aynısı yukarı-cıvıltılardan oluşuyor; alıcı paketi bulmak ve zamanlamayı kilitlemek için bunu kullanıyor. Sonra süpürme yönü iki çeyrek sembol boyunca ters dönüyor — çerçeve ayracı. Ondan sonrası yük (payload) ve sembollerin farklı yüksekliklerden başladığı gözle görülüyor. O merdiven, verinin ta kendisi.
Yanlış dönüş
Demodülasyon için iki sayı gerekiyor: bant genişliği BW ve yayılma faktörü
SF. İkisi birlikte sembol süresini sabitliyor: Tsym = 2^SF / BW.
Akla gelen ilk hamle cıvıltı eğimini ölçmek. Ölçtüm, 122 MHz/s çıktı, olası yapılandırmalarla eşleştirdim ve SF9 + 250 kHz bant genişliğinde karar kıldım. Sonraki her şey, insanı devam ettiren o sinsi biçimde yarı çalıştı: de-chirp korelasyonu gerçek bir tepe verdi, paket dedektörü tetiklendi. Hiçbir şey çözülmedi ama sorun hiçbir zaman modülasyonda görünmedi.
Yanlıştı. LoRa cıvıltısının eğimi BW / Tsym, yani açılımı BW² / 2^SF. Bant
genişliğini iki katına çıkarıp yayılma faktörünü iki artırırsanız eğim aynı
kalıyor:
- SF9, BW 250 kHz → 250 kHz / 2.048 ms = 122 MHz/s
- SF11, BW 500 kHz → 500 kHz / 4.096 ms = 122 MHz/s
Eğim tek başına bunları ayıramaz. Zaten hiçbir zaman ayıramazdı.
Böylece eğim uydurmayı bırakıp önbaşlık boyunca tepe frekansını kare kare takip ettim. Bu, cıvıltıyı doğrudan okunabilen bir testere dişine çeviriyor:

Yükseklik 501 kHz, periyot 4.11 ms; dolayısıyla 2^SF = 500 kHz × 4.096 ms = 2048 ve SF = 11. İki bağımsız kontrol de aynı sonucu verdi:
- De-chirp keskinliği. Bant genişliğinin iki katına yeniden örnekleyip her iki hipotezle korele ettiğimde tepe-ortalama oranı SF11/500k için 231, SF9/250k için 115 çıktı. Düşük değer, 250 kHz varsayımının hiç istemediği sinyalin yarısını atmasından doğan bir yarı-bant artefaktı.
- Bit hızı.
SF × BW / 2^SF= 11 × 500000 / 2048 = 2686 bps; bu da E32'nin "2.4 kbps" hava hızı ayarı. SF9/250k olsaydı 4.4 kbps çıkardı — modülün "4.8k" diye sunduğu ve benim ayarlamadığım bir hız.
Elime geçen bir dış araştırma SF9 + 125 kHz diyordu. O, bant genişliği sabit tutularak bir veri sayfası çıpasından yapılmış bir dışdeğerlemeydi ve doğrudan eğim ölçümü onu anında eliyor: SF9/BW125 30.5 MHz/s süpürür, telsizin yaptığının dörtte biri.
Aynı anda doğru olması gereken üç şey
SF11/BW500k elde olunca demodülatör kilitlendi, başlığı okudu ve çöp üretti. Başlığın kendisi temizdi ve alıntılamaya değer, çünkü ikinci bir yanlış inancı da bitirdi — EBYTE'ın yükü tescilli bir şeye sardığı inancını:
Header checksum VALID!
Payload length: 32
Coding rate: 1 (4/5)
CRC presence: 1
Standart açık başlıklı (explicit header) LoRa. PHY katmanında hiç özel çerçeveleme yok. Yükü bozan üç ayrı şey vardı ve herhangi biri tek başına onu okunamaz kılıyordu; işin bu kadar uzun sürmesinin sebebi tam olarak bu — üçünden ikisini düzeltmek, hiçbirini düzeltmemekle birebir aynı görünüyor.
Düşük Veri Hızı Optimizasyonu (LDRO) açık olmalıydı. LDRO normalde sembol süresi 16 ms'yi aştığında devreye girer; burada 4 ms, yani her "otomatik" ayar onu kapatıyordu. E32 ise ne olursa olsun açık gönderiyor. Alıcı aynı fikirde olmayınca yükün tamamı sistematik olarak karışıyor — gürültülü değil, karışık; bu da insana yapılandırma sorunundan çok şifre çözme sorunu gibi geliyor.
Sync word atlanmalıydı. En çok saati bu yedi, en az içgörüyü bu verdi.
LoRa'nın varsayılan sync word'ü 0x12 ve frame_sync bunu taşımayan her
önbaşlığı reddediyor. E32 kendi ağ kimliğini kullanıyor ve o da ne 0x12'ye ne
0x34'e denk düşüyor. Belirtisi acımasız: ders kitabı temizliğinde bir kayıt,
248 korelasyon tepesi ve sıfır paket kilidi — blok her paketi sessizce geri
çeviriyor. Sync word'ü [0] yapmak kontrolü devre dışı bırakıyor ve anında
kilitleniyor.
Saat kayması düzeltilmeliydi. HackRF ile E32 ortak bir referans paylaşmıyor; ~15 ppm'lik fark paketin başında hiçbir şey, 256 ms içindeyse öldürücü. İpucu şuydu: hatalar her yükün sonunda kümeleniyor, ilk baytlar sorunsuz geliyordu. Demodülatörün önüne konan kesirli bir yeniden örnekleyici bunu çözüyor:
# ppm burada negatif: kayıt, göndericiye göre bir tık hızlı akıyor.
grfilter.mmse_resampler_cc(0, 1 / (1 + ppm * 1e-6))
−10 ile −20 ppm arasındaki her değer 9 paketin 9'unu sıfır bit hatasıyla verdi. Düzeltilmediğinde her paketin kuyruğu gürültüydü.
Üçü de bir kez bulunup akılda tutulacak cinsten şeyler değil; o yüzden üçü birden, sonunda flowgraph'ın önüne koyduğum tarayıcı arayüzünde birer düğmeye dönüştü — hepsini E32'nin istediği hâle getiren tek bir ön ayarın arkasında:

Bir süre de, HackRF'in merkez tepesinden kaçmak için sinyalin DC dışına alınması gerektiğine kanaat getirmiştim. Gerekmiyormuş. Yukarıdaki üç düzeltme yerine oturunca, kanal tam DC'nin üstündeyken de paket kusursuz çözülüyor — LoRa 500 kHz'e yayılıyor ve birkaç kHz genişliğindeki bir çentiği hiç umursamıyor. O sapak, LDRO ve saat kayması hâlâ bozukken hata ayıklayıp görünürdeki en yakın artefaktı suçlamaktan doğdu.
EBYTE'ın üste eklediği katman
CRC'ler temiz ve 32 baytlık yük hâlâ metin değil. Bu kısım gerçekten EBYTE'ın işi ve standart LoRa'nın üstünde oturuyor:
yük = [ 8 baytlık başlık ][ girdi baytı başına 2 bayt ][ 4 baytlık kuyruk ]
Modüle verdiğiniz her bayt iki bayt olarak çıkıyor. İlk tahminim tekrarlayan bir
XOR maskesiydi — sabit bir BBBB test dizisinden e4 eb türetmiştim ve
yanlıştı; çünkü sabit bir girdi, maske ile kod tablosunu birbirinden ayıramaz.
Çözüm tahmini bırakıp bilinen veri üretmekti. 0x00'dan 0xFF'ye kadar her
değeri n × 0x11 sabitleri hâlinde yayan bir kalibrasyon eskizi yükledim, tam
bir turu kaydettim ve eşlemeyi doğrudan okudum. Her girdi baytı iki nibble'a
ayrılıyor; çift konum yüksek nibble'ı 0x33 ile XOR'lanmış hâlde, tek konum
düşük nibble'ı taşıyor ve ikisi de tek bir 16 girdilik kod tablosundan geçiyor:
| kod | değer | kod | değer | kod | değer | kod | değer |
|---|---|---|---|---|---|---|---|
a5 | 0 | 95 | 4 | 65 | 8 | 55 | 12 |
a6 | 1 | 96 | 5 | 66 | 9 | 56 | 13 |
a9 | 2 | 99 | 6 | 69 | 10 | 59 | 14 |
aa | 3 | 9a | 7 | 6a | 11 | 5a | 15 |
hi = INVG[even ^ 0x33]
lo = INVG[odd]
byte = (hi << 4) | lo
Tabloda olmayan bir kod baytı, bozuk çerçeve demek; bu da tabloyu CRC'nin üstüne bedava bir bütünlük kontrolüne çeviriyor — bozuk çerçeveler gürültü olarak basılmak yerine atılıyor.
Son bir pratik pürüz: crc_verif, CRC başarısız olduğunda boş bir PDU
yayınlıyor, yani hatalı paket ile hiç paket olmaması birbirinden ayırt
edilemiyor. Paketleri her hâlükârda göstermek için de-whitening bayt akışını
doğrudan okuyup, her çerçeve başında offset, yük uzunluğu, kodlama oranı ve CRC
bayrağıyla yayılan frame_info etiketleriyle akışı çerçevelere böldüm.
Vardığı yer
$ humidity:41.2
$ humidity:41.3
$ humidity:40.9
ESP32'nin en başından beri seri portuna bastığı şeyin ta kendisi; ama artık
868 MHz, bir HackRF ve şu alıcı zinciri üzerinden geliyor:
frame_sync → fft_demod → gray_mapping → deinterleaver → hamming_dec → header_decoder → dewhitening → crc_verif, sonuna EBYTE nibble çözümü
eklenmiş hâlde.

- Modülasyon
- SF11 / 500k
- Cıvıltı eğimi
- 122 MHz/s
- Saat kayması
- −15 ppm
- Temiz paket
- 9 / 9
Aklımda tutacağım ders ilki. Elimde tamamen doğru bir ölçüm vardı — 122 MHz/s — ve onu yalnızca bir kısıt olduğu hâlde cevap diye okudum. İki yapılandırma onu sağlıyordu, ben ise başka bir şeyin de sağlayıp sağlamadığına hiç bakmadım; çünkü sayı temiz çıkmıştı ve bir sayıyı tutturmak insana bilmek gibi geliyor. Çözüm daha iyi bir ölçüm değildi. İkinci ve bağımsız bir niceliği ölçüp ikisini kesiştirmekti.
Gerisi — LDRO, sync word, saat kayması — dinlenilmek üzere tasarlanmamış donanımla konuşmanın olağan vergisi. Bir kez ödemeye değer; bir daha ödememek için de yazmaya.