Libraries und Software für das Elecrow LoRaWAN LR1262 Dev.-Board mit Raspi-Pico 2040

In diesem Artikel hatte ich ja schon das LoRaWAN LR1262 Development Board mit RP2040 und 1.8 “ LCD für LoRa-Anwendungen von der Hardwareseite her vorgestellt.

Heute soll es darum gehen, das Board softwaretechnisch auszustatten. Fangen wir mit der Auswahl der Libraries an. Die müssen zum einen zu den Hardware-Komponenten passen und zum anderen auf dem verbauten RP2040 Raspberry Pi Pico Chip kompatibel sein.

Die Integration richtig sich natürlich wieder nach der verwendeten Entwicklungsumgebung (auch IDE genannt). Ich setze wieder Visual Studio Code mit Platform IO ein, weil mir diese am besten gefällt und diese mich am besten unterstützt. Siehe dazu auch mein Artikel Taugt Visual Studio Code mit Platform IO als Ersatz für die die Arduino IDE?.

Umgebung in platform.io definieren

Zuerst muss unsere IDE mal wissen, mit welchem Board sie es zu tun hat, damit sie auch für die CPU passende Maschinensprachebefehle in die Firmware kompiliert. Das geschieht über die platformio.ini, die normalerweise so aussehen würde:

Core: mbed

[env:pico] platform = raspberrypi board = pico framework = arduino upload_protocol = mbed upload_port = G: monitor_port = COM15 monitor_speed = 115200 monitor_dtr = 1 monitor_rts = 0
Damit weiß PlatformIO, dass ein RP2040 auf dem Board ist. Außerdem gebe ich als Upload-Protokoll mbed an. Das voreingestellte pico-tool, also der komfortable Upload via seriellem Interface über USB mit automatischem Boot-Select funktioniert nämlich leider nicht mit dem Board.

upload_protocol = mbed bewirkt, dass die Firmware auf das dann auftauchende Laufwerk kopiert wird, wenn man links unter dem USB-Port BOOT hält und dann kurz RESET über dem USB-Port drückt. Und leider funktioniert auch das automatische Kopieren nicht richtig. Die Firmware wollte sich damit nicht bei mir aktualisieren. Ich musste immer per Hand kopieren, aber dafür habe ich mir ein kleines _upload_firmware.cmd geschrieben, das ich dann nur noch kurz anklicken muss:
copy .pio\build\pico\firmware.uf2 g:\
Im Prinzip lasse ich dann PlatformIO nur noch kompilieren (Hotkey CTRL+ALT+B für Build) und wenn der Compiler-Durchgang fehlerfrei ist, versetze ich den RP2040 in den Boot-Modus, und klicke dann doppelt auf meine _upload_firmware.cmd und lade damit die Firmware hoch. Danach macht das Board dann auch gleich einen Reset und legt mit der neuen Firmware los.

Da die Schalter für BOOT und RESET auf dem Board wirklich winzig sind und ich bei dem Spagat BOOT zu halten, während man kurz RESET drückt, ständig von den Knöpfchen abrutsche - was extrem nervt - habe ich nach einer Lösung dafür gesucht. Und es gibt eine: Man kann auch nur den RESET-Taster benutzen: Erst einmal ein wenig länger drücken, vielleicht eine halbe Sekunde und dann ganz schnell danach noch einmal kurz - et voila: ebenfalls Boot-Modus. Klappt eigentlich zuverlässig nach ein bisschen Übung und man erkennt den Boot-Modus a) an dem USB-Einstöpsel-Sound von Windows und b) und einem kurzen Flackern der blauen LED auf dem Board.

Core: earlephilhower und maxgerhardt

Ich habe oben "normalerweise" geschrieben, weil ich feststellen musste, dass es mit der obigen Konfiguration nicht wirklich funktioniert und ich auf Board-Features verzichten müsste. Spätestens, wenn ich den I2C-Bus ansprechen will, was ich schon allein für die LED-Ampeln muss, scheitert der mbed-Core.

Darum sieht meine platformio.ini nun so aus:
[env:pico] ;platform = raspberrypi platform = https://github.com/maxgerhardt/platform-raspberrypi.git board = pico board_build.core = earlephilhower framework = arduino ;upload_protocol = picotool upload_port = COM13 ;upload_speed = 230400 monitor_port = COM13 monitor_speed = 115200 monitor_dtr = 1 monitor_rts = 0
Näheres, warum das so sein muss, findet ihr in meinem Artikel Installation des Core von Earle Philhower für Platform IO.

Damit klappt es auch mit dem I2C-Bus und auch der Firmware-Upload gelingt (ab dem zweiten mal) ohne Boot- und Reset-Knöpgchen-Gefummel.

Die Hardware-Komponenten und die dazu passenden Libraries

Okay, wir brauchen Libraries, die auch für den RP2040 bzw. Pico geeignet sind. Um nach passendem zu suchen können wir im Library Manager (in PIO Home Libraries anklicken) unseren Suchterm durch platform:rp2040 ergänzen, also zum Beispiel nach ST7735 platform:rp2040 suchen. Zur Sicherheit gucken wir bei den einzelnen Libs noch nach, ob sie auch den Tag "Raspberry Pi RP2040" in den Tags stehen haben.

Der Buzzer

Fangen wir mit etwas ganz einfachem an, für das wir keine Library brauchen: dem Buzzer. Auf der Platine ist die passive Ausführung verbaut. Das heißt, wir müssen das Signal am Buzzer-Pin mit der gewünschten Frequenz oszillieren lassen. Das erledigt aber der Befehl tone() zuverlässig für uns.

Warum mit dem Buzzer anfangen? Nun, er bietet die wenigsten Fehlerquellen neben der eingebauten LED. So können wir schnell erkennen, ob das Hochladen der Firmware geklappt haben und hören zumindest etwas. Beim TFT könnte es schon schwerer werden, ihm ein Bild zu entlocken.

Hier der Source-Code für eine kleine Melodie, die wir beim Einschalten abspielen wollen:
#include ... #define PIN_BUZZER 28 ... void playOnMelody() { tone(PIN_BUZZER, 440); delay (200); tone(PIN_BUZZER, 440); delay (200); tone(PIN_BUZZER, 880); delay (600); noTone(PIN_BUZZER); } void setup() { playOnMelody(); }
Alles ganz einfach. Frei nach dem Motto "Keep it simple". Erst einmal muss es laufen, dann können wir es erweitern. Zwischendurch immer wieder testen. Dann weiß man, wenn etwas schief läuft, dass der Fehler in den neuen Code-Zeilen liegen muss. Auf Mikrocontrollern ist es nämlich mitunter schwierig Fehler einzugrenzen, weil die Debugging-Möglichkeiten meistens nur rudimentär sind.

Weitere Code- und Anwendungsbeispiele und Melodien für den Buzzer findet ihr in meinen Artikeln Okay, das hat funktioniert. Nach dem Einschalten bzw. nach einem Reset spielt das Dev.-Board eine kleine Melodie. Wir hören also schon einmal was. Als nächstes möchten wir auch etwa sehen.

Das ST7735 128x160px Farb-Display

Ahja, der gute alte Bekannte, der ST7735 Controller für TFT-Displays. Das 128x160 Pixel Farb-TFT-Display war wegen seines günstigen Preises in China sehr beliebt.

Wer die Hardware näher kennenlernen will: Mehr Infos zu ST7735-TFT-Displays findet ihr in meinen Artikeln: Die Hersteller versuchten, hier und da noch ein paar Cents beim ST7735 einzusparen, was dazu führte, dass es bald eine ganze Reihe von unterschiedlichen Displays gab, die sich alle "ST7735 128x160px Farb-Display" schimpften, aber nicht hundertprozentig zueinander kompatibel waren.

Man fing dann damit an, in den Treibern angebbar zu machen, welche Display-Variante das wohl war und nahm dazu die Farbe des kleinen überstehenden Tabs an der Displayschutzfolie als Indikator. Bei meinem Board ist der Tab dunkelgrün.

Der ST7735 ist nicht sonderlich schwierig über SPI anzusprechen. Eigentlich könnte man einen eigenen Treiber dafür schreiben. Das ist nicht das Problem. Das Problem sind die "höheren" Funktionen wie die Darstellung von gut aussehender Schrift auf dem Display oder das Anzeigen von Grafiken. Das zu implementieren bedeutet jede Menge Arbeit. Und diese haben adafruit, OliKraus mit u8g2 oder Bodmer mit TFT_eSPI bereits schon erledigt. Warum also das Rad neu erfinden.

Ich habe in meinen CYD-Projekten mit TouchScreen schon positive Erfahrungen mit der TFT_eSPI-Library gesammelt. Vor allen die schönen Schriften und die LVGL-Kompatibilität haben mich überzeugt.

Also habe ich mich für diese Library entschieden und mache sie in der platformio.ini bekannt, um sie verwenden zu können:
[env:pico] ... lib_deps = bodmer/TFT_eSPI@^2.5.43
Die TFT_eSPI-Library bietet Unterstützung für wirklich sehr viele Displays und deswegen ist man vielleicht ein bisschen erschlagen davon, wie man sein Display konfigurieren soll. Das zu soll man die User_Setup.h editieren, die man im Projektpfad \Projekt\.pio\libdeps\pico\TFT_eSPI findet. Weil die so lang ist, kopiere ich mir gerne die entsprechenden Zeile in die main.cpp, um eine Übersicht über meine relevanten Änderungen zu behalten.

Meine TFT_eSPI-Config für das Display sehen so aus:
// SPI: TFT #define ST7735_DRIVER #define TFT_WIDTH 128 #define TFT_HEIGHT 160 #define TFT_MOSI 19 // Automatically assigned with ESP8266 if not defined #define TFT_SCLK 18 // Automatically assigned with ESP8266 if not defined #define TFT_CS 17 // Chip select control pin D8 #define TFT_DC 16 // Data Command control pin #define TFT_RST 22 // Reset pin (could connect to NodeMCU RST, see next line) #define TFT_BL 23 // LED back-light (only for ST7789 with backlight control pin) // das hier bringt alles nicht wirklich was in Sachen Versatz // #define ST7735_GREENTAB3 // weitere Anpassungen in ST7735_Init.h#L162 // auch die Farbfehler bekomme ich selbst besser mit Werten ins ST7735 Register schreiben hin // #define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue // #define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red // #define TFT_INVERSION_ON // #define TFT_INVERSION_OFF // auf den Versatz muss ich dann halt selbst acht geben, dafür ein paar defines // der beschreibbare Bildschirm ist damit minimal kleiner #define TFT_XOFF 2 #define TFT_XMAX 127 #define TFT_YOFF 1 #define TFT_YMAX 159 //ST7735-TFT init. und auf Variable mappen TFT_eSPI tft = TFT_eSPI();
Die Hardware-Pins findet ihr alle in der Tabelle in meinem Artikel Vorstellung des Elecrow LoRaWAN LR1262 Dev.-Board mit Raspi-Pico 2040.

Wie ihr meinen auskommentierten Zeilen entnehmen könnt, bietet die Library schon einiges an Versionen für unterschiedliche ST7735-Varianten, nämlich GREENTAB GREENTAB2, GREENTAB3, GREENTAB128 (128x128), GREENTAB160x80, (128x160), REDTAB, BLACKTAB und REDTAB160x80.

Die unterschiedlichen Varianten haben eine andere Farb-Konfiguration, also Byte-Reihenfolge für die Farben, die nicht unbedingt RGB sein muss und häufig auch einen Pixel-Versatz, was wohl daran liegt, dass die Start-Adresse für die Pixel nicht immer bei 0,0 beginnt.

Ich habe alle Tabs durchprobiert und entweder passten die Farben oder der Pixel-Offset nicht. Darum habe ich beschlossen, die Farben selbst Hardcore per ST7735-Register in setup() zu setzen:
// MADCTL: ST7735-TFT richtig einstellen: Farben und Orientation tft.writecommand(0x36); tft.writedata(0xc0); tft.fillScreen(TFT_BLACK);
Dann wollte ich auch den PixelOffset per Register CASET und RASET einstellen, aber habe festgestellt, dass schon ein einfaches tft.fillScreen(TFT_BLACK); der TFT_eSPI diese Einstellungen überschreibt. Also hieß es: auf TFT_eSPI verzichten und alles neu programmieren oder TFT_eSPI benutzen und sich um den Versatz selbst kümmern. Ich habe mich dann für zweites entschieden und die Defines
#define TFT_XOFF 2 #define TFT_XMAX 128 #define TFT_YOFF 1 #define TFT_YMAX 160
für mich festgelegt. Wie man sieht, fängt das Display für mich bei X=2 an. Würde ich auf X=0 schreiben, würden links 2 Pixel abgeschnitten. Das gleiche gilt für oben und unten (Y). Ich muss bei den TFT_eSPI-Befehlen also immer drauf achten, diese Defines zu beachten.

Trotzdem habe ich noch ein Problem: wenn ich den Bildschirm löschen will mit tft.fillScreen(TFT_BLACK) dann bleibt immer ein bunter Pixel-Rand rechts und unten. Das liegt daran, dass der ST7735 einen Cache-Speicher hat, um schneller zu sein. Der übersteht sogar einen Reset des Boards. Am Anfang steht im Cache-Speicher natürlich nur zufälliger "Mist". Daher kommt das bunte Muster.

Und da ich ja nicht den vollen Bildschirm beschreiben kann, eben wegen des Versatzes und weil für TFT_eSPI bei 127 Schluss ist (0...127) löscht er nicht in voller Breite (2...129), sondern eben nur so, dass 2 Pixel rechts stehen bleiben (2...127).

Um den Bildschirm aber doch komplett zu löschen, habe ich mit den folgenden Trick ausgedacht: Ich drehe den Bildschirm einfach alle Richtungen durch und lösche von da aus. Damit wandert die 0,0 Koordinaten: oben links, oben rechts, unten rechts, unten links. Und füllen jeweils von da aus den Bildschirm. So wird jede Ecke erreicht und der Bildschirm ist komplett schwarz:
// Trick, um bunten Pixelrand loszuwerden, der Offset muss aber trotzdem noch bei Texten etc. berücksichtigt werden for (int i=0; i < 4; i++) { tft.setRotation(i); tft.fillScreen(TFT_BLACK); }
Das heißt aber auch, dass wir das jedesmal machen müssen, wenn wir eine andere Hintergrundfarbe wollen. Und dass wir 2 Pixel rechts und unten verlieren. Aber unter dem Strich haben wir so eine leistungsfähige Engine, nämlich TFT_eSPI, mit der wir das Display betreiben können.

Die Buttons K1 bis K4


Oberhalb des Displays finden sich zwei LED-Ampeln, da kommen wir später zu.

Und unter dem Display befinden sich vier Knöpfe, die man auch ganz gut drücken kann und die nicht so winzig wie Boot und Reset-Knopf sind.

Das ist praktisch für eine Menüführung, dass man einblenden kann und dass dann so in etwa über den jeweiligen Knöpfen positioniert ist. Ich habe mich für folgende Funktionen für die Buttons im Menu entschieden:

K1: Cursor runter
K2: Cursor rauf
K3: OK / auswählen
K4: Exit / zurück

Das wird durch entsprechende Symbole am unteren Display-Rand angezeigt.

Was die Knöpfe / Funktionen tun, dürfte klar sein. Der Cursor, also die Auswahlmarkierung wird ebenfalls durch ein Dreieck angezeigt.

Hardwaretechnisch hängen alle vier Knöpfe an nur einem (analogen) Pin:
#define PIN_KEYS_ADC 29
Dieser wird analog mit analogRead() ausgewertet und liefert dann je nach gedrücktem Key einen anderen Wert, der ungefähr ist:

K1: 4
K2: 512
K3: 680
K4: 766
nichts gedrückt: 1024

Dies geschieht durch Widerstände mit unterschiedlichen Ohm-Werte, die in einer Spannungsteiler-Schaltung jeweils eine andere Spannung an den Pin legt, die dann wiederum in einem anderen Messwert zwischen 0 und 1024 resultiert.

Mit Checks auf die mittleren Werte zwischen den Grenzwerten können wir so abfragen, welcher Button gedrückt wurde:
while (1) { int keysval = analogRead(PIN_KEYS_ADC); if (keysval < 400) { key=1; break;} else if (keysval < 600) { key=2; break;} else if (keysval < 720) { key=3; break;} else if (keysval < 900) { key=4; break;} delay(10); }
Im Zusammenspiel zwischen Anzeige und Tastendrücken kann man sich so durch die Menüs hangeln und die gewünschte Funktion auswählen. Ich gebe zudem noch Feedback über den Buzzer, indem ich einen kurzen Ton bei Tastendruck erzeuge.

Die zwei LED-Ampeln

Oberhalb des Displays finden sich zwei LED-Ampeln mit je drei LEDs in den Farben grün, gelb und rot. Damit kann man wunderbar einen Status anzeigen, ohne das Display dafür zu bemühen.

Nun ist es aber nicht so, dass man nicht einfach 6 GPIO-Pins für die 6 LEDs benutzen würde, das wäre Verschwendung an GPIO-Pins. Stattdessen hat man einen IO-Expander mit 8 Bit, nämlich den PCA9557 auf die Platine gelötet, der sozusagen 8 weitere IO-Pins zur Verfügung stellt, die dann allerdings über ein I2C-Protokoll angesprochen werden.

Ich persönlich hätte zwar eher einen 74HC595 verbaut, um Steuerleitungen einzusparen, ein Seriell-zu-Parallel-Schieberegister, aber das ist wohl Geschmackssache. Und der bräuchte auch 2 Leitungen bzw. GPIOs.

Für den PCA9557 gibt es eine extra Library, die wir in unsere platformio.ini inkludieren können, wenn wir sie benutzen möchten:
lib_deps = maxpromer/PCA9557-arduino@^1.0.0
Entsprechender Beispiel-Code ist unter https://github.com/maxpromer/PCA9557-arduino zu finden.

Ich habe mich aber dafür entschieden, das selbst in die Hand zu nehmen, und die I2C-Befehle selbst zu managen. Auch weil später bestimmt noch mehr I2C-Bedarf bestehen wird, etwa für einen Sensor.

Zuerst wieder ein paar Defines:
// I2C-Adressen und Defines //// LEDs: PCA9557-IO-Expander #define ADDR_LEDS 0x18 #define LED_G1 0b01111111 #define LED_Y1 0b10111111 #define LED_R1 0b11011111 #define LED_G2 0b11101111 #define LED_Y2 0b11110111 #define LED_R2 0b11111011 #define LED_NONE 0xff
Man sieht schon: der PCA9557, der übrigens rechts vom Display auf dem Dev.-Board zu finden ist, wird über die I2C-Adressen 0x18 angesprochen. Er braucht deshalb keinen eigenen Pin (das Schieberegister hätte mehrere Pins benötigt) und ist damit sehr Pin-sparend, da über den I2C-Bus mehrere Geräte angeschlossen werden können. Nähere zum I2C-Bus in meinem Artikeln Über den I2C-Bus des Raspberry Pi einen Analog-Digital-Wandler (PCF8591) ansteuern und Der I2C-Bus auf dem Arduino.

Und die LEDs sind als Bitmuster definiert. IO-Port 2 (gezählt ab 0) ist für LED_R2 (lese: Rot 2, also die rechte, rote LED), das geht weiter bis IO-Port 7 für LED_G1. Also von rechts nach links auf dem Board. Soll eine LED an sein, muss das entsprechende Bit auf 0 sein. Die beiden letzten Bits bleiben immer 1.

Will ich beispielsweise also die ersten drei LEDs anmachen, so wäre das Bitmuster 0b00011111. Ganz einfach, oder? Ich kann auch schreiben: "LED_G1 & LED_Y1 & LED_R1" und ein binäres AND benutzen. Das löscht die Bits an den entsprechenden Stellen und gibt unterm Strich auch 0b00011111. Und wenn ich nur eine LED anmachen will, wird es noch einfacher: dann schicke ich einfach nur z. B. LED_G2.

Die LEDs leuchten übrigens alle gleich hell, was sehr schön ist und nur mit der Wahl der richtigen Vorwiderstände zu bewerkstelligen ist. Ich habe übrigens mal einen Vorwiderstandsrechner programmiert, der dabei hilft.

Im setup() teile ich dem PCA9557 mit, dass ich alle IOs auf Ausgang haben will. Weil wir ja nichts messen wollen, sondern etwas ein- und ausschalten:
// Alle 8 Pins des PCA9557 für die farbigen LEDs als Ausgang konfigurieren (0 = Ausgang) i2cWrite(ADDR_LEDS, 3, 0); delay (10); // LEDs ausschalten i2cWrite (ADDR_LEDS, 1, LED_NONE);
Danach schalte ich auch gleich alle LEDs aus, weil anfangs 0x00 im Register steht, was "alles an!" bedeutet. "Alles aus" hingegen ist 0xff, also alle Bits gesetzt, definiert als LED_NONE. Die weitere Steuerung ist denkbar einfach, man muss statt LED_NONE nur das Bitmuster schicken, wie eben erklärt. Ich habe mir das in zwei Funktionen "gegossen":
void ledOn(byte ledBits) { i2cWrite(ADDR_LEDS, 1, ledBits); } void ledOffAll() { i2cWrite(ADDR_LEDS, 1, LED_NONE); }
Für die Funktion i2cwrite schaut bitte in meinem Artikel 8x8-LED-Matrix mit Beschleunigungssensor / Kompass und 5 Funktionen nach.

Und damit hätten wir auch schon alles an Grund-Hardware besprochen, was mich an dem Board interessiert. Alles, außer dem LoRa-Chip. Aber dem widme ich ein extra Artikel.

Der Rest an Hardware

Da wären die weiteren Schnittstellen: I2C, SPI, UART, Analog über Header und Grove-Konnektoren.

Über die Grove-Konnektoren kann man sicher wunderbar I2C-Sensoren anschließen, um deren Messwerte dann über LoRa zu versenden. Oder ein analoges Thermometer (Thermistor). Oder ein Foto-Widerstand. GPIO 14 und 15 stehen als digitale Grove-Verbindungen zur Verfügung wie auch UART1

Alternativ kann man Header über DuPont-Kabel benutzen. Es sind auch noch einige digitalen GPIO-Ports (2, 3, 6...15) über Header zugänglich.

Über UART (RS232) lassen sich GPS-Module für Empfang der aktuellen Geo-Koordinaten, GSM-Module zum Versenden von SMS über Mobilfunk und ähnliches anschließen.

Und es ist sogar eine RS485-Schnittstelle über Schraubanschluss vorhanden. RS485 ist ein Industriestandard für die Datenkommunikation, der eine zuverlässige, differentielle Signalübertragung über lange Strecken und in rauen Umgebungen ermöglicht.

Video

Die Board-Tests (Buzzer, Screen, LEDs, Buttons) sind soweit fertig und ich habe ein kleines Demo-Video davon aufgenommen:



Weitere Aussichten

Im nächsten Teil zum Elecrow LoRaWAN LR1262 Dev.-Board werden wir uns damit beschäftigen, wie wir den I2C-Bus ansteuern und damit Sensoren auslesen können, dessen Messwerte später über LoRaWAN übertragen werden sollen.



Quellen, Literaturverweise und weiterführende Links