LoRa mit dem ESP32 und Semtech SX1262
In meinem Artikel über die theoretischen Grundlagen von LoRa habe ich die Funktionsweise der CSS-Modulation und die Bedeutung von Parametern wie Spreading Factor und Bandbreite beleuchtet, die LoRa seine extreme Reichweite verleihen. In diesem Artikel geht es um die Praxis - welche Hardware gibt es und wie implementiert man damit einen einfachen Sender/Empfänger.
Zunächst gebe ich einen Überblick über die aktuell verfügbaren Semtech Transceiver-Chips und die WiFi LoRa 32 Boards von Heltec. Anhand dieser Boards, die einen ESP32 und ein LoRa-Modul integrieren, zeige ich abschließend, wie man einen einfachen, aber voll funktionsfähigen LoRa-Sender und -Empfänger implementiert.
Semtech Transceiver im Überblick
Die gesamte LoRa-Hardware basiert im Wesentlichen auf den Transceiver-Chips der Firma Semtech. Für Einsteiger ist das Angebot jedoch oft verwirrend, da sowohl veraltete als auch moderne Chips parallel auf dem Markt erhältlich sind. Während die erste Generation (SX127x) über Jahre den Standard definierte und noch auf vielen günstigen Modulen zu finden ist, hat sich die zweite Generation (SX126x) technisch längst durchgesetzt. Sie ist kleiner, effizienter und leistungsstärker. Die folgende Tabelle hilft, die verschiedenen Familien und ihren Einsatzzweck richtig einzuordnen.
| Chip | Frequenzbereich | Max. TX Power | Spreading Factors (SF) | RX Strom | Besonderheiten |
| Generation 1 | |||||
| SX1272 | 860-1020 MHz | +20 dBm | SF6 - SF12 | ~10.8 mA | SF6 erfordert spezielle Register-Einstellungen ("Implicit Header Mode"). |
| SX1276 | 137-1020 MHz | +20 dBm | SF6 - SF12 | ~10.8 mA | Wie 1272. SF6 ist möglich, aber oft instabiler als bei Gen 2. |
| SX1278 | 137-525 MHz | +20 dBm | SF6 - SF12 | ~10.8 mA | Die 433MHz-Variante des SX1276. Gleiche SF-Grenzen. |
| Generation 2 (LoRa Core) | |||||
| SX1261 | 150-960 MHz | +15 dBm | SF5 - SF12 | 4.6 mA | Unterstützt offiziell SF5. Deutlich bessere Performance bei SF6 als Gen 1. |
| SX1262 | 150-960 MHz | +22 dBm | SF5 - SF12 | 4.6 mA | Der Standard-Chip für alle SF-Bereiche. |
| SX1268 | 410-810 MHz | +22 dBm | SF5 - SF12 | 4.6 mA | China/433MHz Version des SX1262. |
| LLCC68 | 150-960 MHz | +22 dBm | SF5 - SF11 | 4.6 mA | Bei 125 kHz nur SF7-SF9, bei 250 kHz SF7-SF10 und bei 500 kHz SF7-SF11. Ideal für Smart Home. |
| SX1280 | 2.4 GHz | +12.5 | SF5 - SF12 | 5.5 mA | Bietet LoRa, FLRC und (G)FSK. Unterstützt Time-of-Flight (ToF) Distanzmessung zwischen zwei Modulen. |
| SX1281 | 2.4 GHz | +12.5 | SF5 - SF12 | 5.5 mA | Baugleich zum SX1280, aber die Distanzmess-Funktion ist deaktiviert. |
| Generation 3 & 4 | |||||
| LR1121 | Multi-Band | +22 / +11.5 dBm | SF5 - SF12 | 5.4 mA | Global Connectivity. Pin-kompatibel zu LR1110/LR1120. Ermöglicht denselben PCB-Footprint, spart aber Kosten, wenn keine Ortungsfunktion benötigt wird. |
| LR2021 | Multi-Band | +22 dBm | SF5 - SF12 | 5.5 mA | High-Speed. Unterstützt FLRC (bis 2.6 Mbps) für Bild/Audio. 4. Gen LoRa IP. |
| LoRa Edge (Geolocation) | |||||
| LR1110 | 150-960 MHz | +22 dBm | SF5 - SF12 | 4.6 mA | GNSS (GPS/BeiDou) Scanner + Wi-Fi Passiv-Scanner. |
| LR1120 | Multi-Band | +22 dBm | SF5 - SF12 | 5 mA | Wie LR1110, aber Multi-Band. Weltweites Tracking (auch 2.4 GHz) & Satelliten-Anbindung. |
Die veralteten Chips der ersten Generation gelten heute als „Not Recommended for New Designs“. Man findet sie noch oft auf günstigen Import-Modulen (z. B. RFM95). Sie verbrauchen im Empfangsmodus fast doppelt so viel Strom wie ihre Nachfolger. Ihre Daseinsberechtigung haben sie heute nur noch durch die große Menge an verfügbaren Tutorials und alten Bibliotheken für Arduino und Co.
Wer heute ein zuverlässiges, batteriebetriebenes Gerät baut, greift zum SX1262. Er bietet das beste Verhältnis aus Reichweite und Energieeffizienz. Der günstigere LLCC68 ist in der Reichweite beschnitten (keine hohen Spreading Factors bei Standard-Bandbreite) und für weit entfernte LoRaWAN-Knoten oft ungeeignet. Chips wie der LR1110 (mit Geolocation) oder der SX1280 (2.4 GHz) sind hochspezialisiert. Aufgrund ihrer Komplexität (z. B. Cloud-Zwang für GPS-Berechnung beim LR1110) spielen sie in typischen Maker-Projekten kaum eine Rolle und richten sich primär an industrielle Asset-Tracking-Lösungen.
Heltec WiFi LoRa 32 (V3.x & V4)
Ursprünglich hatte ich geplant, die Hardware für dieses Projekt selbst zusammenzustellen und einfach einen ESP32 mit einem Semtech LoRa-Transceiver-Modul zu verbinden. Diese Idee habe ich jedoch verworfen, als ich die Heltec WiFi LoRa 32 Boards entdeckte. Diese Boards integrieren einen Mikrocontroller, den LoRa-Chip sowie ein Display und sind für rund 20 Euro erhältlich.
Für aktuelle LoRa-Anwendungen sind nur die Boards der V3- und V4-Serie relevant. Beide Generationen teilen sich die gleiche Architektur: Einen ESP32-S3 Mikrocontroller gekoppelt mit dem aktuellen Semtech SX1262 Transceiver und ein kleines monochromes Display. Dennoch gibt es im Detail einige Unterschiede.
Version 3.x
Mit der V3 vollzog Heltec den Wechsel von der alten Architektur hin zum ESP32-S3/SX1262-Design. Innerhalb dieser Serie gab es mehrere Hardware-Revisionen:
- V3.0: Die Basis-Version. Solide Leistung, litt jedoch teilweise unter erhöhtem Stromverbrauch im Deep-Sleep.
- V3.1: Hier wurde das Energiemanagement durch optimierte Spannungsregler und LDOs verbessert, was die Batterielaufzeit im Ruhezustand deutlich verlängerte.
- V3.2: Diese Revision führte einen TCXO (Temperaturkompensierter Oszillator) ein. Dieser sorgt dafür, dass die Frequenz auch bei extremen Temperaturschwankungen (z. B. im Winter draußen) stabil bleibt, was für LoRaWAN und geringe Bandbreiten essenziell ist.
Version 4
Die 2025 eingeführte V4 behält den Formfaktor weitgehend bei, bringt aber einige entscheidende Verbesserungen für den autarken Einsatz:
- Sendeleistung: Durch einen neuen HF-Leistungsverstärker sind nun bis zu +28 dBm (ca. 500 mW) möglich (V3: ca. +21 dBm). Das bietet massive Reserven für Reichweite (unter Beachtung regionaler Limits).
- Solar-Management: Die V4 besitzt einen dedizierten Solar-Eingang, der den Akku direkt lädt, so dass keine separate Ladeelektronik nötig ist.
- Ausstattung: Ein dedizierter Port für GNSS-Module (GPS) ist nun direkt an Bord. Zudem wurde der Speicher auf 16 MB Flash + 2 MB PSRAM aufgestockt, um komplexe Anwendungen zu ermöglichen.
Softwareseitig sind V3 und V4 weitgehend kompatibel (beide nutzen den ESP32-S3). Leider wird bei den V3.x Boards die genaue Version bei den Händlern meistens nicht angegeben.
Semtech SX1262
Der SX1262 ist ein integrierter Sub-GHz-Transceiver, der den Frequenzbereich von 150 bis 960 MHz abdeckt und Modulationen für LoRa sowie (G)FSK unterstützt. Im Gegensatz zu älteren Generationen, die primär registerbasiert arbeiteten, wird der SX1262 über eine interne Zustandsmaschine gesteuert, die Befehle (OpCodes) über SPI entgegennimmt.
Um einen Treiber für diesen Chip zu implementieren, muss man zunächst die interne Logik, die Besonderheiten der Hardware-Schnittstelle und die notwendigen Schritte betrachten. Für genauere Informationen empfehle ich, das Datenblatt des SX1262 zu lesen.
1. Die Kommunikationsschnittstelle
Die Basis der Kommunikation bildet eine klassische SPI-Schnittstelle, die jedoch um wichtige Handshake-Mechanismen erweitert wurde. Der SX1262 agiert dabei als Slave und unterstützt eine Taktfrequenzen von bis zu 16 MHz. Im Gegensatz zu klassischen Registern-Transceivern, bei denen eine Transaktion meist nur aus Adresse + Daten besteht, nutzt der SX1262 eine Befehls-Architektur. Jede Interaktion beginnt mit einem 8-Bit OpCode, der definiert, was getan werden soll (z. B. "Schreibe Register", "Setze Modus", "Lese Buffer").
Die Struktur einer Transaktion unterscheidet sich je nach Operationstyp:
Schreib-Transaktionen (Commands & Write Register)
Dies ist der einfachste Fall. Der Host sendet den Befehl und direkt im Anschluss die Parameter oder Daten:
- NSS Low: Der Host initiiert die Transaktion.
- OpCode: Der Host sendet das Befehls-Byte (z. B. 0x80 für SetStandby).
- Parameter: Der Host sendet n Bytes an Parametern (z. B. 0x00 für STDBY_RC).
- NSS High: Die steigende Flanke signalisiert dem Chip, dass der Befehl vollständig ist und verarbeitet werden soll.
Lese-Transaktionen (Read Register & Buffer)
Der SX1262 benötigt Zeit, um die angeforderten Daten intern bereitzustellen. Daher muss der Host nach der Adresse ein NOP (No Operation) bzw. Dummy-Byte senden, bevor er die eigentlichen Daten lesen kann:
- NSS Low
- OpCode: Host sendet 0x1D (ReadRegister).
- Adresse: Host sendet 2 Bytes Adresse (z. B. 0x0740 für Sync Word).
- NOP: Host sendet 0x00. Während dieses Taktes bereitet der SX1262 die Daten vor.
- Daten: Host sendet weitere Dummy-Bytes (0x00), um den Takt zu generieren, während der SX1262 auf der MISO-Leitung die Daten ausgibt.
- NSS High
Status-Byte (MISO Rückkanal)
Während der Host auf der MOSI-Leitung den OpCode und die Parameter sendet, ist die MISO-Leitung nicht stumm. Der SX1262 sendet bei jeder Transaktion (auch beim Schreiben) zeitgleich ein Status-Byte zurück. Dieses Byte enthält wichtige Diagnose-Informationen, wie den aktuellen Chip-Modus (z. B. 0x2 für STDBY_RC) und den Command-Status (z. B. 0x4 für Command Error).
BUSY-Pin
Das wichtigste Element für einen stabilen Treiber ist das BUSY-Pin. Da der SX1262 über einen internen Prozessor verfügt, werden Befehle nicht immer sofort in Echtzeit verarbeitet. Der BUSY-Pin fungiert als Indikator für den Zustand dieser internen State Machine.
- Zustand HIGH: Der interne Prozessor ist ausgelastet. Dies geschieht beispielsweise während des Aufwachens aus dem Schlafmodus, beim Kalibrieren der PLL oder während der Verarbeitung eines komplexen Befehls. In diesem Zustand ist der SPI-Bus für Schreibbefehle blockiert.
- Zustand LOW: Der Chip ist bereit (Idle) und kann neue Befehle entgegennehmen.
Das beduetet, dass vor jeder SPI-Transaktion (mit Ausnahme bestimmter Statusabfragen) der Treiber den Zustand des BUSY-Pins prüfen muss. Wird ein Befehl gesendet, während BUSY aktiv ist, wird dieser ignoriert oder führt zu undefiniertem Verhalten.
2. Multifunktions-Pins (DIO)
Um die Anzahl der benötigten Leitungen zum Mikrocontroller zu minimieren, bietet der SX1262 drei flexible Digital-I/O-Pins (DIO). Diese sind nicht fest verdrahtet, sondern können per Software konfiguriert werden, um verschiedene Aufgaben im HF-Design zu übernehmen.
DIO1: Zentrale Interrupt-Quelle
Für die Treiberentwicklung ist DIO1 der wichtigste Pin. Er fungiert als universeller Interrupt-Ausgang. Egal ob ein Paket erfolgreich gesendet wurde (TxDone), Daten empfangen wurden (RxDone) oder ein Timeout auftrat – all diese Ereignisse werden im Interrupt-Controller des SX1262 gesammelt und können über eine Maskierung auf den DIO1-Pin gelegt werden. Der Befehl SetDioIrqParams (0x08) steuert diese Zuordnung.
DIO2: Automatische RF-Switch-Steuerung
In einem HF-Design ist meist ein Antennenschalter (RF Switch) nötig, um zwischen Sende- und Empfangspfad zu wechseln. Anstatt diesen Schalter über einen GPIO des Mikrocontrollers manuell zu steuern, kann der SX1262 diese Aufgabe übernehmen. Durch den Befehl SetDio2AsRfSwitchCtrl (0x9D) wird DIO2 so konfiguriert, dass er automatisch auf High-Pegel schaltet, sobald der Transceiver in den Sendemodus (TX) wechselt. In allen anderen Modi (RX, Sleep) fällt er auf Low zurück. Dies garantiert ein präzises Timing des Antennenschalters ohne Latenz durch den Host-Controller.
DIO3 und die Frequenzstabilität (TCXO)
Der Pin DIO3 nimmt eine Sonderrolle ein, die besonders für LoRa-Anwendungen mit geringer Bandbreite kritisch ist: die Steuerung eines TCXO (Temperature Compensated Crystal Oscillator).
Ein herkömmlicher Quarz (XTAL) unterliegt bei Temperaturschwankungen einer Frequenzdrift. Da LoRa-Signale oft sehr schmalbandig sind (z. B. 125 kHz oder niedriger), kann bereits eine geringe Temperaturänderung dazu führen, dass der Empfänger das Signal verliert. Moderne Module (wie das Heltec V3) nutzen einen TCXO, der dies aktiv kompensiert, benötigen dafür aber eine aktive Stromversorgung und eine Einschwingzeit.
Um Energie zu sparen, kann der SX1262 den TCXO über den Pin DIO3 mit Strom versorgen. Der Treiber nutzt dazu den Befehl SetDIO3AsTcxoCtrl (0x97). Dieser konfiguriert die Ausgangsspannung (z. B. 1,8 V) und eine Verzögerungszeit (Delay). Der SX1262 schaltet die Spannung am DIO3 nur dann ein, wenn er den Takt tatsächlich benötigt (z. B. im RX- oder TX-Modus). Im Sleep-Modus wird der TCXO automatisch abgeschaltet. Ohne korrekte Konfiguration dieses Pins bleibt ein Board mit TCXO funktionslos, da der Chip keinen Basistakt erhält.
3. Speicherarchitektur: Der Data Buffer
Der SX1262 verfügt über einen 256 Byte großen RAM-Bereich. Im Gegensatz zu einem FIFO wird dieser Speicher nicht automatisch geleert, sondern behält seinen Inhalt (außer im Sleep-Modus) und muss vom Treiber aktiv adressiert werden.
Da Sende- und Empfangsoperationen auf denselben physischen Speicher zugreifen, liegt es in der Verantwortung des Treibers, diesen Bereich so aufzuteilen, dass sich TX- und RX-Daten nicht ungewollt überschreiben.
Speicher-Partitionierung (SetBufferBaseAddress)
Der Zugriff der internen State Machine auf den Speicher wird über zwei Zeiger gesteuert, die mit dem Befehl SetBufferBaseAddress (0x8F) konfiguriert werden :
- TxBaseAddress: Definiert den Offset im RAM, an dem der Chip beginnt, Daten für die Modulation zu lesen.
- RxBaseAddress: Definiert den Offset im RAM, an dem der Chip beginnt, empfangene Daten zu schreiben.
Durch unterschiedliche Offsets kann der Speicher logisch unterteilwerden (z. B. TX-Bereich im unteren Teil, RX-Bereich im oberen Teil), um Konflikte zu vermeiden.
Der Sendevorgang (TX)
Beim Senden schreibt der Treiber die Daten an eine von ihm gewählte Adresse und teilt dem Chip mit, dass dies der Startpunkt für die Übertragung ist.
- Zeiger setzen: Der Treiber sendet SetBufferBaseAddress und setzt TxBaseAddress auf den gewünschten Offset (z. B. 0x00 oder 0x80).
- Daten schreiben: Mit dem Befehl WriteBuffer (0x0E) werden die Nutzdaten an diesen Offset in den Speicher kopiert.
- Länge definieren: Der Befehl SetPacketParams (0x8C) konfiguriert die exakte Payload-Länge. Dies ist zwingend erforderlich, damit der Chip weiß, wie viele Bytes ab der TxBaseAddress gesendet werden sollen.
- Start: Der Befehl SetTx startet die Übertragung. Der Chip liest nun ab TxBaseAddress.
Der Empfangsvorgang (RX)
Beim Empfangen weist der Treiber dem Chip einen Speicherbereich zu. Das Auslesen erfolgt jedoch dynamisch basierend auf Rückmeldungen des Chips, nicht starr über den Basis-Zeiger.
- Speicher zuweisen: Vor dem Wechsel in den RX-Modus setzt der Treiber mittels SetBufferBaseAddress die RxBaseAddress auf einen freien Speicherbereich.
- Empfang: Der Chip schreibt eingehende Daten ab dieser Adresse in den RAM.
- Metadaten abfragen: Nach dem Interrupt RxDone darf der Treiber nicht einfach blind ab RxBaseAddress lesen. Stattdessen muss der Befehl GetRxBufferStatus (0x13) ausgeführt werden. Dieser liefert zwei Werte:
- PayloadLengthRx: Die tatsächliche Anzahl der empfangenen Bytes.
- RxStartBufferPointer: Die exakte Startadresse des Pakets im Speicher.
- Daten auslesen: Der Treiber liest nun mittels ReadBuffer (0x1E) die Daten aus, wobei er als Startadresse zwingend den vom Chip gemeldeten RxStartBufferPointer verwendet.
Der 256-Byte-Speicher verhält sich zirkulär. Wenn eine Lese- oder Schreiboperation die Adresse 255 überschreitet, wird automatisch bei Adresse 0 fortgesetzt. Der Treiber muss sicherstellen, dass Pakete, die über das Ende des Speichers hinausgehen, korrekt behandelt werden, falls man den oberen Adressbereich nutzt.
4. Initialisierung und Konfiguration
Die Initialisierung des SX1262 erfordert eine strikte Reihenfolge von Befehlen, da die Interpretation bestimmter Parameter vom gewählten Betriebsmodus abhängt. Nach einem Power-On-Reset befindet sich der Chip im STDBY_RC-Modus.
Schritt 1: Definition der Funk-Topologie
Der allererste Befehl muss zwingend SetPacketType (0x8A) sein. Hier entscheidet der Treiber, ob der Chip im LoRa-Modus (0x01) oder im FSK-Modus (0x00) arbeiten soll. Alle nachfolgenden Befehle zur Modulation und Paketstruktur werden vom Chip basierend auf dieser Auswahl interpretiert.
Schritt 2: Konfiguration des Leistungsverstärkers (PA)
Der SX1262 benötigt je nach Sendeleistung eine spezifische Ansteuerung der PA-Transistoren. Der Befehl SetPaConfig (0x95) erwartet vier Parameter:
- paDutyCycle: Steuert den Arbeitszyklus der PA. Für +22 dBm wird hier meist 0x04 verwendet.
- hpMax: Definiert die Größe der PA. Für den SX1262 muss dieser Wert zwingend auf 0x07 gesetzt werden, um die volle Leistung abzurufen.
- deviceSel: Wählt den Chip-Typ. Für den SX1262 ist dies 0x00 (beim SX1261 wäre es 0x01).
- paLut: Ein Reservierter Wert, der immer auf 0x01 gesetzt wird.
Schritt 3: RF-Parameter und Modulation
Anschließend werden die Frequenz (SetRfFrequency 0x86) und die Modulationsparameter (SetModulationParams 0x8B) gesetzt. Bei LoRa umfasst dies den Spreading Factor (SF), die Bandbreite (BW) und die Coding Rate (CR). Ein wichtiges Detail ist hier das "LowDataRateOptimize"-Bit: Bei sehr langsamen Übertragungen (hoher SF, niedrige BW) muss dieses Bit aktiviert werden, um die Synchronisation des Empfängers zu gewährleisten.
5. Senden und Empfangen
Die Interaktion im laufenden Betrieb folgt einem klaren Muster aus Befehl, Wartezeit und Interrupt-Handling.
Der Sende-Zyklus (TX)
Um ein Paket zu senden, schreibt der Treiber zunächst die Nutzdaten mittels WriteBuffer (0x0E) in den internen Speicher. Anschließend werden die Interrupt-Parameter (SetDioIrqParams) konfiguriert, um festzulegen, welche Ereignisse (z. B. TxDone) signalisiert werden sollen. Der eigentliche Sendevorgang wird durch SetTx (0x83) ausgelöst, wobei optional ein Timeout übergeben werden kann. Der Chip fährt nun die PA hoch, sendet die Präambel sowie die Daten.
Die Erkennung, dass das Senden abgeschlossen ist, kann auf zwei Arten erfolgen:
- Interrupt: Der Chip löst einen Flankenwechsel am konfigurierten DIO1-Pin aus. Dies ist effizient, da der Host-Mikrocontroller während des Sendens schlafen kann.
- Polling: Alternativ fragt der Treiber zyklisch das interne Statusregister mittels GetIrqStatus (0x12) ab. Sobald das TxDone-Bit im Rückgabewert gesetzt ist, ist der Vorgang beendet.
In beiden Fällen muss der Treiber abschließend das Interrupt-Flag mittels ClearIrqStatus (0x02) bereinigen, um den Chip für den nächsten Zyklus freizugeben.
Der Empfangs-Zyklus (RX)
Im Empfangsmodus (SetRx 0x82) wartet der Chip auf eine gültige Präambel. Wird diese erkannt, synchronisiert er sich und empfängt das Paket.
Die Signalisierung eines erfolgreichen Empfangs oder eines Fehlers erfolgt analog zum Senden:
- Interrupt: Der RxDone-Interrupt wird an DIO1 ausgelöst, sobald ein Paket vollständig und (bei aktiver CRC) fehlerfrei empfangen wurde.
- Polling: Der Treiber liest periodisch den IrqStatus aus und prüft, ob das RxDone-Flag (oder CrcErr) gesetzt wurde.
Sobald ein Paket bereitsteht, muss der Treiber zwei Schritte ausführen:
- Status abrufen: Der Befehl GetRxBufferStatus (0x13) liefert die Länge des Pakets und den Start-Offset im Buffer.
- Daten lesen: Mittels ReadBuffer (0x1E) werden die Daten an der zuvor ermittelten Adresse ausgelesen.
Auch Fehlerfälle (z. B. CRC-Fehler) werden über Flags im Statusregister (CrcErr) abgebildet.
SX1262 Treiber für den ESP32
Basierend auf diesen Informationen habe ich damit begonnen, einen SX1262 Treiber für den ESP32 zu implementieren. Der Treiber ist sehr einfach gehalten, beherrscht aber alle grundlegenden Funktionen zur LoRa-Kommunikation. Der Code iwurde mit Heltec WiFi LoRa 32 V3.x und V4 Boards entwickelt und getestet, sollte aber bei entsprechender Konfiguration mit jedem SX1262 Board funktionieren. Natürlich hätte ich auch einfach einen fertigen Treiber verwenden können, aber der Lerneffekt ist bei eigener Entwicklung deutlich höher.
Architektur des Treibers
Der Treiber besteht grundsätzlich aus drei Teilen:
- Hardware-Initialisierung - SPI-Bus und GPIO-Konfiguration
- Radio-Konfiguration - LoRa-Parameter und Modem-Einstellungen
- Kommunikation - Senden und Empfangen von Paketen
Diese Trennung macht den Code übersichtlich und ermöglicht es, den Treiber schrittweise zu initialisieren und zwischen verschiedenen Betriebsmodi zu wechseln. Die Implementierung orientiert sich eng am SX1262 Datenblatt und den Application Notes von Semtech.
Das API im Detail
Initialisierung und Konfiguration
sx1262_init_bus()
Diese Funktion initialisiert den SPI-Bus und konfiguriert die GPIO-Pins für die Kommunikation mit dem SX1262.
Funktionalität:
- Konfiguriert die GPIO-Pins für BUSY, DIO1 (als Eingang) und RESET (als Ausgang)
- Initialisiert den SPI2-Bus
- Registriert das SX1262-Gerät am SPI-Bus
Diese Funktion muss als erste aufgerufen werden, bevor jegliche andere Treiber-Funktion verwendet wird
sx1262_init_radio()
Nach der Bus-Initialisierung muss das Radio selbst konfiguriert werden. Diese Funktion führt einen vollständigen Hardware-Reset durch und bringt den SX1262 in einen definierten Ausgangszustand.
Funktionalität:
- Führt einen Hardware-Reset aus (über RESET-Pin)
- Wartet auf BUSY-Signal
- Setzt den Chip in Standby-Modus (STDBY_XOSC)
- Konfiguriert den TCXO (Temperature Compensated Crystal Oscillator) für 3.3V und 5ms Timeout
- Konfiguriert DIO2 als RF-Switch (für automatisches TX/RX-Umschalten)
- Setzt den Spannungsregler-Modus (DC-DC + LDO)
- Führt eine vollständige Kalibrierung durch (0x7F = alle Blöcke)
- Konfiguriert den Fallback-Modus (Chip geht nach TX/RX zurück in STDBY_XOSC)
Die Initialisierung des SX1262 ist deshalb in einer eigenen Funktion, da sie nur bei einem Kaltstart aufgerufen werden muss.
sx1262_wakeup()
Wenn der SX1262 im Sleep-Modus war und die Konfiguration erhalten bleiben soll (Warmstart), kann diese Funktion verwendet werden.
Funktionalität:
- Weckt den Chip durch Anlegen von NSS Low
- Wartet auf BUSY-Signal
- Überprüft, ob die Konfiguration erhalten geblieben ist (durch Auslesen des Packet Type)
Diese Funktion ist besonders für Geräte wichtig, die zwischen den Übertragungen in den Deep Sleep gehen. Der Warmstart spart Zeit und Energie gegenüber einer vollständigen Neu-Initialisierung.
sx1262_configure()
Dies ist die zentrale Konfigurationsfunktion, die alle LoRa-Parameter setzt.
Parameter: Pointer auf sx1262_config_t Struktur
typedef struct { sx1262_modem_mode_t modem_mode; // LoRa or FSK uint32_t frequency; // Frequency in Hz int8_t tx_power; // TX Power in dBm // LoRa specific parameters sx1262_bandwidth_t bandwidth; // Bandwidth (LoRa only) uint8_t spreading_factor; // Spreading Factor 5-12 (LoRa only) sx1262_coding_rate_t coding_rate;// Coding Rate (LoRa only) bool iq_inverted; // IQ inverted (LoRa only) bool rx_gain_boosted; // true = Boosted RX Gain (+3dB sensitivity) // FSK specific parameters uint32_t fsk_bitrate; // Bitrate in bps (FSK only) uint32_t fsk_fdev; // Frequency deviation in Hz (FSK only) sx1262_fsk_rx_bw_t fsk_rx_bw; // RX Bandwidth (FSK only) sx1262_fsk_mod_shaping_t fsk_shaping; // Pulse shaping (FSK only) // Common parameters uint16_t preamble_length; // Preamble length uint8_t payload_length; // Payload length (0 = variable) bool crc_on; // CRC enabled uint16_t sync_word; // LoRa Sync Word (0x1424 = public, 0x3444 = private) } sx1262_config_t;
Funktionalität: Die Funktion führt eine umfassende Konfiguration durch:
- Paket-Typ setzen: LoRa oder FSK
- Frequenz setzen: Berechnet den Register-Wert aus der Frequenz in Hz
- Image-Kalibrierung: Kalibriert das Frequenzband für optimale Performance
- PA-Konfiguration: Stellt die Power Amplifier Parameter basierend auf der gewünschten Sendeleistung ein
- OCP-Konfiguration: Setzt den Over Current Protection entsprechend der Sendeleistung
- TX-Parameter: Konfiguriert Sendeleistung und Ramp-Time
- Modulations-Parameter: Setzt SF, BW, CR und Low Data Rate Optimization
- Paket-Parameter: Konfiguriert Präambel, Payload-Länge, CRC und IQ
- Workarounds: Wendet Herstellerempfohlene Korrekturen an (IQ-Polarität, 500kHz BW)
- Sync Word: Setzt das LoRa Sync Word für Netzwerk-Trennung
- RX Gain: Wählt zwischen Power Save und Boosted Mode
Die Funktion setzt den Chip in Standby-Modus. Nach sx1262_configure() ist das Radio bereit zum Senden oder Empfangen.
Senden und Empfangen
sx1262_send()
Sendet ein LoRa-Paket.
Parameter:
data: Pointer auf zu sendende Datenlen: Länge der Daten in Bytes (max. 255)
Funktionalität:
- Setzt Chip in Standby
- Schreibt Daten in den TX-Buffer des SX1262
- Konfiguriert TX-Timeout (0xFFFFFF = keine Timeout)
- Startet die Übertragung
- Wartet auf TX_DONE Interrupt (Polling)
- Geht automatisch in Standby zurück (Fallback Mode)
Die Funktion blockiert bis die Übertragung abgeschlossen ist. Die Daten werden intern kopiert, der Aufrufer kann den Buffer sofort wiederverwenden.
sx1262_receive()
Empfängt ein LoRa-Paket im Polling-Modus (blockierend).
Parameter:
data: Pointer auf Empfangsbufferlen: Pointer auf Buffer-Größe (Input) / empfangene Bytes (Output)timeout_ms: Timeout in Millisekunden (0 = Continuous RX mit 60s Limit)
Funktionalität:
- Setzt Chip in Standby
- Konfiguriert RX-Timeout
- Startet RX-Modus
- Polled kontinuierlich den IRQ-Status (alle 1ms)
- Bei RX_DONE: Liest Buffer Status und Daten
- Bei TIMEOUT oder CRC_ERROR: Bricht ab mit entsprechendem Fehler
Die Funktion blockiert den Task bis ein Paket empfangen wurde oder der Timeout abläuft. Der Buffer muss groß genug für das erwartete Paket sein - es gibt keine Überlaufprüfung!
sx1262_start_receive_async()
Startet den asynchronen Empfangsmodus mit Interrupt-Verarbeitung.
Parameter:
callback: Callback-Funktion, die bei Paketempfang aufgerufen wird
typedef void (*sx1262_rx_callback_t)(uint8_t *data, uint8_t len, sx1262_packet_status_t *status);
Die Callback-Funktion erhält:
data: Pointer auf empfangene Daten (Buffer wird wiederverwendet!)len: Anzahl empfangener Bytesstatus: Pointer auf Paket-Status (RSSI und SNR)
Funktionalität:
- Setzt Chip in Standby
- Konfiguriert DIO1 als GPIO-Interrupt (Positive Edge)
- Erstellt einen FreeRTOS Task mit hoher Priorität (10)
- Installiert ISR-Handler für DIO1
- Konfiguriert IRQ-Maske (RX_DONE, CRC_ERROR, HEADER_ERROR)
- Startet Continuous RX Mode (0xFFFFFF)
Ablauf:
- ISR wird bei DIO1 High getriggert
- ISR sendet Notification an RX-Task
- RX-Task liest IRQ-Status über SPI
- Bei RX_DONE: Liest Daten und ruft Callback auf
- RX-Task aktiviert RX-Modus wieder (Continuous Loop)
Der Callback läuft im Task-Kontext (nicht ISR!), daher sind blockierende Operationen erlaubt. Der Daten-Buffer wird wiederverwendet - bei Bedarf kopieren!
sx1262_stop_receive_async()
Stoppt den asynchronen Empfangsmodus.
Funktionalität:
- Entfernt den ISR-Handler von DIO1
- Deaktiviert GPIO-Interrupt
- Löscht den RX-Task
- Setzt Chip in Standby
Nach dem Stoppen muss die Empfangsfunktion erneut aufgerufen werden, um wieder empfangen zu können.
Helper-Funktionen
sx1262_sleep()
Versetzt den SX1262 in den Sleep-Modus (Warmstart Konfiguration).
Funktionalität:
- Sendet Sleep-Command mit Config 0x04 (Warmstart)
- Im Warmstart bleiben die Register erhalten
- Stromverbrauch: ~1.6 µA
Wird typischerweise vor dem ESP32 Deep Sleep aufgerufen. Nach dem Aufwachen kann mit sx1262_wakeup() fortgesetzt werden.
sx1262_standby()
Setzt den Chip in Standby-Modus (STDBY_XOSC).
Funktionalität:
- Stoppt alle Radio-Operationen
- TCXO bleibt aktiv
- Stromverbrauch: ~1.2 mA
Wird automatisch von vielen Funktionen aufgerufen. Kann manuell verwendet werden, um laufende Operationen zu unterbrechen.
sx1262_get_rssi()
Liest den aktuellen RSSI-Wert (Received Signal Strength Indicator).
Rückgabewert:
- RSSI in dBm (typisch -120 bis 0 dBm)
- -999 bei Fehler
Sinnvoll nur im RX-Modus oder direkt nach Paketempfang.
sx1262_get_packet_status()
Liest detaillierte Informationen über das letzte empfangene Paket.
Parameter:
status: Pointer aufsx1262_packet_status_tStruktur
typedef struct { int16_t rssi; // RSSI in dBm int8_t snr; // SNR in dB } sx1262_packet_status_t;
sx1262_get_chip_info()
Gibt Chip-Informationen über den ESP-Logger aus (Debug-Funktion).
Funktionalität:
- Liest Status-Register
- Gibt Informationen zur Konsole aus
Code-Beispiele
Beispiel 1: Grundlegende Initialisierung
#include "sx1262.h" void app_main(void){ // Phase 1: Initialize bus esp_err_t ret = sx1262_init_bus(); if (ret != ESP_OK) { ESP_LOGE("APP", "Bus initialization failed"); return; } // Phase 2: Initialize radio ret = sx1262_init_radio(); if (ret != ESP_OK) { ESP_LOGE("APP", "Radio initialization failed"); return; } ESP_LOGI("APP", "SX1262 initialization complete"); }
Beispiel 2: LoRa-Konfiguration
void configure_lora(void){ sx1262_config_t config = { .modem_mode = SX1262_MODEM_LORA, .frequency = 868000000, // 868 MHz (EU) .tx_power = 14, // 14 dBm TX power // LoRa parameters .bandwidth = LORA_BW_125, // 125 kHz bandwidth .spreading_factor = 7, // SF7 .coding_rate = LORA_CR_4_5, // CR 4/5 .iq_inverted = false, // Standard IQ .rx_gain_boosted = false, // Power Save RX Gain // Packet parameters .preamble_length = 8, // 8 symbol preamble .payload_length = 0, // Variable length .crc_on = true, // CRC enabled .sync_word = 0x3444 // Private network }; esp_err_t ret = sx1262_configure(&config); if (ret != ESP_OK) { ESP_LOGE("APP", "Configuration failed"); return; } ESP_LOGI("APP", "LoRa configured: SF7, BW125, CR4/5, 868MHz"); }
Erklärung der Parameter:
- frequency: Die ISM-Frequenz, in Deutschland typisch 868 MHz
- tx_power: Die Sendeleistung in dBm. Der SX1262 unterstützt -9 bis +22 dBm, praktisch sinnvoll sind 2-14 dBm für kurze Distanzen und 14-22 dBm für lange Reichweiten
- bandwidth: Kleinere Bandbreite = höhere Empfindlichkeit, aber längere Sendezeit
- spreading_factor: SF7 = schnell/kurze Reichweite, SF12 = langsam/große Reichweite
- coding_rate: CR 4/5 = weniger Overhead, CR 4/8 = mehr Fehlerkorrektur
- rx_gain_boosted: false = ~1.2mA Stromverbrauch, true = ~1.8mA aber +3dB Empfindlichkeit
- sync_word: 0x3444 für private Netzwerke, 0x1424 für LoRaWAN öffentliche Netzwerke
Beispiel 3: Einfaches Senden
void send_message(void) { char message[] = "Hello LoRa!"; esp_err_t ret = sx1262_send((uint8_t*)message, strlen(message)); if (ret == ESP_OK) { ESP_LOGI("APP", "Message sent successfully"); } else if (ret == ESP_ERR_TIMEOUT) { ESP_LOGW("APP", "TX Timeout"); } else { ESP_LOGE("APP", "TX Failed"); } }
Beispiel 4: Blockierender Empfang (Polling)
void receive_message_blocking(void){ uint8_t rx_buffer[255]; uint8_t rx_len = sizeof(rx_buffer); ESP_LOGI("APP", "Waiting for packet (5s timeout)..."); esp_err_t ret = sx1262_receive(rx_buffer, &rx_len, 5000); if (ret == ESP_OK) { ESP_LOGI("APP", "Received %d bytes", rx_len); // Print data as string (if it is text) rx_buffer[rx_len] = '\0'; ESP_LOGI("APP", "Data: %s", rx_buffer); // Read RSSI and SNR sx1262_packet_status_t status; sx1262_get_packet_status(&status); ESP_LOGI("APP", "RSSI: %d dBm, SNR: %d dB", status.rssi, status.snr); } else if (ret == ESP_ERR_TIMEOUT) { ESP_LOGW("APP", "RX Timeout - no packet received"); } else if (ret == ESP_ERR_CRC_ERROR) { ESP_LOGW("APP", "CRC Error"); } else { ESP_LOGE("APP", "RX Failed"); } }
Ideal für einfache Empfänger oder wenn der ESP32 nur sporadisch empfängt. Der Task blockiert während des Wartens.
Beispiel 5: Asynchroner Empfang mit Interrupts
// Callback function for received packets
void on_packet_received(uint8_t *data, uint8_t len,
sx1262_packet_status_t *status){
ESP_LOGI("RX", "Packet received: %d bytes", len);
ESP_LOGI("RX", "RSSI: %d dBm, SNR: %d dB", status->rssi, status->snr);
// Process data
// IMPORTANT: Copy data if needed later!
char message[256];
memcpy(message, data, len);
message[len] = '\0';
ESP_LOGI("RX", "Message: %s", message);
// Further processing...
}
void start_async_receiver(void){
esp_err_t ret = sx1262_start_receive_async(on_packet_received);
if (ret == ESP_OK) {
ESP_LOGI("APP", "Async RX mode started");
ESP_LOGI("APP", "Receiver is now listening continuously");
} else {
ESP_LOGE("APP", "Failed to start async RX");
}
}
void stop_async_receiver(void){
sx1262_stop_receive_async();
ESP_LOGI("APP", "Async RX mode stopped");
}
Ideal für Gateways oder Empfänger, die kontinuierlich hören müssen. Der ESP32 kann in der Zwischenzeit andere Aufgaben erledigen.Der Callback läuft in einem eigenen Task mit hoher Priorität. Zeitkritische Verarbeitung ist möglich, aber längere Operationen sollten in einen separaten Task ausgelagert werden.
Beispiel 6: Deep Sleep Sensor-Node
Dieses Beispiel zeigt einen energieeffizienten Sensor-Node, der periodisch Messwerte sendet und zwischen den Übertragungen in Deep Sleep geht.
#include "sx1262.h" #include "esp_sleep.h" #include "esp_log.h" #define SLEEP_DURATION_SEC 300 // 5 minutes between measurements // Structure for sensor data typedef struct { float temperature; float humidity; uint32_t battery_mv; } sensor_data_t; // Simulated sensor measurement sensor_data_t read_sensors(void) { sensor_data_t data; data.temperature = 22.5; // In real application: BME280, DHT22, etc. data.humidity = 65.0; data.battery_mv = 3700; return data; } RTC_DATA_ATTR bool sx1262_is_configured = false; void app_main(void) { // Initialize bus (must always be done) esp_err_t ret = sx1262_init_bus(); if (ret != ESP_OK) { ESP_LOGE("APP", "Bus init failed"); return; } bool radio_ready = false; // Was the ESP32 in deep sleep? if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TIMER && sx1262_is_configured) { ESP_LOGI("APP", "Woke up from deep sleep. Attempting warm start..."); // Wake up SX1262 (NSS Toggle) if (sx1262_wakeup() == ESP_OK) { ESP_LOGI("APP", "Warm start successful! Configuration retained."); radio_ready = true; } else { ESP_LOGW("APP", "Warm start failed (SX1262 has amnesia)."); // radio_ready remains false -> Fallback to cold start } } // Fallback / Cold start logic if (!radio_ready) { ESP_LOGI("APP", "Performing cold start (Hard-Reset & Calibration)..."); // The expensive, slow part: sx1262_init_radio(); // Resend configuration sx1262_config_t cfg = { .modem_mode = SX1262_MODEM_LORA, .frequency = 869525000, .tx_power = 22, .spreading_factor = 7, .bandwidth = LORA_BW_125, .coding_rate = LORA_CR_4_5, .preamble_length = 8, .crc_on = true }; sx1262_configure(&cfg); // Remember for next time! sx1262_is_configured = true; } // --- Chip is ready from here on --- // Read sensors sensor_data_t sensor_data = read_sensors(); ESP_LOGI("APP", "Temperature: %.1f°C", sensor_data.temperature); ESP_LOGI("APP", "Humidity: %.1f%%", sensor_data.humidity); ESP_LOGI("APP", "Battery: %lu mV", sensor_data.battery_mv); // Pack data into packet char payload[64]; snprintf(payload, sizeof(payload), "T:%.1f,H:%.1f,BAT:%lu", sensor_data.temperature, sensor_data.humidity, sensor_data.battery_mv); // Send LoRa packet ESP_LOGI("APP", "Sending data..."); ret = sx1262_send((uint8_t*)payload, strlen(payload)); if (ret == ESP_OK) { ESP_LOGI("APP", "Data sent successfully"); } else { ESP_LOGE("APP", "Send failed: %d", ret); } // Radio in sleep mode (Warm Start) sx1262_sleep(); // ESP32 in deep sleep ESP_LOGI("APP", "Going to sleep for %d seconds", SLEEP_DURATION_SEC); esp_deep_sleep(SLEEP_DURATION_SEC * 1000000ULL); }
Hinweise:
- Nach Deep Sleep und erfolgreichem Warmstart spart man ~50ms und ~0.5mA
- Für noch längere Laufzeit: SF erhöhen (SF12), TX Power reduzieren (z.B. 10dBm)
- Bei schlechter Verbindung: SF12 verwenden, erhöht aber die Air Time deutlich
Beispiel 6: Thread-Safety
Der Treiber ist NICHT thread-safe. Wenn mehrere Tasks gleichzeitig auf das Radio zugreifen, müssen Sie dies mit einem Mutex serialisieren:
static SemaphoreHandle_t lora_mutex = NULL;
void init_lora_mutex(void)
{
lora_mutex = xSemaphoreCreateMutex();
}
void safe_send(uint8_t *data, uint8_t len)
{
if (xSemaphoreTake(lora_mutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
sx1262_send(data, len);
xSemaphoreGive(lora_mutex);
}
}
LoRa Test-Tool für Heltec WiFi LoRa 32
Um den Treiber zu testen, habe ich ein kleines Tool für die Heltec Boards entwickelt, mit dem man die Kommunikation zwischen den Boards testen kann. Über das kleine Display und die PRG-Taste können damit die wichtigsten Parameter eingestellt werden:
- Modus: Senden/Empfangen
- Spreading Factor: 5 - 12
- Bandbreite: 125kHz, 250 kHz, 500 kHz
- Coding Rate: 4/5, 4/6, 4/7, 4/8
- Sendleistung: -9 dBm bis 22 dBm
Die Navigation hat folgende Struktur:
┌────────────────────────────┐ │ >Mode: Send [*] │ ← Active menu item (>) │ SF: 7 │ Edit mode ([inverted]) │ BW: 125 │ │ CR: 4/5 │ │ Pwr: 14 │ │ ────────────────────────── │ │ TX: 42 * │ ← Status: Packets + transmit indicator └────────────────────────────┘ In receive mode: ┌────────────────────────────┐ │ Mode: Recv │ │ SF: 7 │ │ BW: 125 │ │ CR: 4/5 │ │ Pwr: 14 │ │ ────────────────────────── │ │ RX:15 RSSI:-85 │ ← Received packets + RSSI └────────────────────────────┘
Die Navigation erfolgt über die PRG-Taste auf dem Board:
Linke Seite (Menünavigation)
- Kurzer Klick
- Springt zum nächsten Menüpunkt
- Zyklisch: Mode → SF → BW → CR → Power → Mode
- Langer Druck
- Wechselt in den Bearbeitungsmodus (rechte Seite)
- Aktueller Wert wird invertiert angezeigt
Rechte Seite (Werte bearbeiten)
- Kurzer Klick
- Wechselt zum nächsten Wert (durchläuft die Optionen)
- Aktualisiert die LoRa-Konfiguration sofort
- Langer Druck
- Verlässt den Bearbeitungsmodus
- Kehrt zur Menünavigation zurück (linke Seite)
Mit diesem Tool kann man beispielsweise sehr einfach die Reichweite von LoRa testen. Da keine Verbindung zum PC nötig ist, lässt sich das Setup mobil mit einer Powerbank betreiben. Man platziert einen Sender stationär und läuft mit dem Empfänger die Umgebung ab.
Dabei lässt sich live beobachten, wie sich die Parameter auswirken: Ein höherer Spreading Factor (SF) erhöht zwar die Reichweite, verlängert aber auch die Sendezeit drastisch. Das Tool vermittelt so ein direktes Gefühl für die Balance zwischen Reichweite und Geschwindigkeit.
Zusätzlich können damit verschiedene Antennen oder Positionen ausprobiert werden. Im Empfangsmodus (Recv) zeigt das Display den RSSI-Wert (Signalstärke) jedes empfangenen Pakets an und die eingebaute LED leuchtet kurz auf. So kann man direkt ablesen, ob eine neue Antenne tatsächlich einen Gewinn bringt oder ob eine ungünstige Positionierung das Signal zu stark dämpft.
Der Code für das Test-Tool inklusive SX1262 Treiber ist wie immer auf GitHub verfügbar.
Fazit
Es war wieder einmal sehr spannend, sich in ein völlig neues Thema einzuarbeiten. Der Hintergedanke dabei war die Option, LoRa auch für die Wetterstation einzusetzen. Durch die Heltec Boards wären die Sensoren sehr einfach aufzubauen. Mit einem CrowPanel Advance und dem LoRa Modul gäbe es auch schon eine gute Lösung für den Empfang der Sensordaten. Das Problem ist eher, dass ich der Stadt eigentlich keinen Bedarf für so hohe Reichweiten habe. Ich meiner Wohnung reicht ESP-NOW völlig aus. Aber vielleicht fällt mir ja noch etwas Sinnvolles ein...





