Rust ist eine momentan heiss diskutierte Programmiersprache. Sie findet under anderem auch in der Embedded-Welt Anklang, denn Rust kann Performance-Mässig in etwa mit C/C++ mithalten. Nur ist sie zusätzlich modern und so konzipiert, dass Entwickler gezwungen sind, Code so sicher wie möglich zu schreiben. Beispielsweise sind unerlaubte Speicherzugriffe wesentlich unwahrscheinlicher, als bei C/C++. Nun, wenn Rust auch auf Embedded Geräten eingesetzt werden kann, wäre es da nicht schön, Rust auf einem Mikrocontroller auszuprobieren?
Ich zeige euch in diesem Artikel, wie man mit etwa 15 CHF Budget bei einem regnerischen Sonntagnachmittag Rust vollständig auf einem Mikrocontroller zum Laufen bringt, inklusive Debugging. Wenn alles einwandfrei läuft, ist mit einem Zeitaufwand von ca. 2 -3 Stunden zu rechnen. Wir verwenden dafür das Raspberry Pi Pico, das erste Mikrocontrollerboard von der Raspberry Pi Foundation, mit hauseigenem Controller, dem RP2040.
Hardware und Tools
- Pico: Als Target wird wie oben erwähnt das Raspberry Pi Pico verwendet. Dabei handelt es sich um ein Mikrocontrollerboard, das perfekt in das Portfolio von Rapsberry Pi passt: preiswert, gut dokumentiert, grosse Community, relativ simpel, opensource. Als Debug-Probe verwenden wir: ebenfalls ein Pico. Dazu gibt es eine Firmware, die das Board mit sehr kleinem Aufwand in eine SWD Debug-Probe „verwandelt“. Wir benötigen also zwei Boards, die je ca. 5 – 7 CHF kosten.
- Rust: Obwohl dieser Blog zeigt, wie die Boards eingerichtet werden, um Rust darauf zu entwickeln, steht die Sprache selbst nicht im Vordergrund. Es wird also nicht auf die Eigenschaften der Sprache im Embedded-Bereich eingegangen. Wenn man mehr darüber wissen will, empfiehlt sich ein Blick in „The Embedded Rust Book“ (https://docs.rust-embedded.org/book/). Wir installieren Rust und verwenden Cargo als Packetmanager. Zudem binden wir den Hardware Abstraction Layer für das Pico als Paket ein und verwenden diesen in einem Beispielprojekt.
- OpenOCD: Der grösste Teil des Zeitaufwands investieren wir in OpenOCD, eine Applikation, die für die Kommunikation zur Debug-Probe zuständig ist. OpenOCD unterstützt bis jetzt den RP2040 offiziell nicht. Deshalb machen wir den Build davon selber, Raspberry Pi stellt dazu den Quellcode zur Verfügung.
- Die Arm GNU Toolchain wird für einzelne Build Schritte verwendet.
- Als Entwicklungsumgebung verwenden wir Visual Studio Code.
- Das ganze Setup ist für das Windows Betriebssystem gezeigt. Auf Linux Distributionen und macOS ist das Setup ähnlich, einzelne Schritte vereinfachen sich jedoch.
Zuerst installieren wir VSCode und Rust, danach kann man schon bald Programme übersetzten und flashen. In dem zweiten Teil wenden wir uns dem Debugging zu.
Rust mit dem Pico ohne Debugging
In diesem Teil schauen wir uns an, wie man Anwendungen übersetzt und dann auf das Target lädt. Dazu benötigen wir nur ein Raspberry Pi Pico.
Als erstes wird Visual Studio Code installiert, der Installer kann von https://code.visualstudio.com/ heruntergeladen werden.
Jetzt installieren wir Rust, hierzu gibt es ebenfalls einen Installer, der Rust Webseite zum Download bereit steht: https://www.rust-lang.org/learn/get-started
Sobald Rust erfolgreich installiert ist, können wir den Target Support für den RP2040 (ARM Cortex-M0+) installieren. Vorher bringen wir aber den Installer „rustup“ noch kurz auf den neusten Stand:
rustup self update rustup update stable rustup target add thumbv6m-none-eabi
Der RP2040 erwartet ein Binary in der Form einer .uf2-Datei. Dazu gibt es ein Package, das .elf Dateien in .uf2 Dateien übersetzt. Cargo ist der Paketmanager von Rust und wird verwendet, um genau solche Pakete zu installieren. Flip Link wird verwendet, um Stack Overflows zu verhindern und wird vom Beispielprojekt gebraucht. Wir installieren die Packete:
cargo install elf2uf2-rs --locked cargo install flip-link
(Bei mir funktioniert seit kurzer Zeit die Installation von elf2uf2-rs nicht, da offensichtlich ein Dependency-Error vorliegt. Das heisst, dass wir momentan nicht direkt Anwendungen übersetzten und flashen können. Aber keine Panik, sobald wir das Debugging eingerichtet haben, können wir das Target über die Debug-Probe flashen.)
Den Hardware Abstraction Layer für den Rp2040 wird mit Cargo installiert. Der Quellcode dazu kann unter https://github.com/rp-rs/rp-hal angesehen werden. Vom gleichen GitHub Account gibt es ein Beispielprojekt, bzw. ein Projekttemplate: https://github.com/rp-rs/rp2040-project-template.
Dieses laden wir jetzt herunter und öffnen es in VSCode. In VSCode starten wir dann ein Terminal und wir können die Anwendung, die sich in src/main.rs findet, mit „cargo build“ übersetzten.
Nach dem Übersetzten führt man die Anwendung Rust-typisch mit „cargo run“ aus. In der Datei .cargo/config.toml können wir verschiedene Runner auswählen. Runner sind Kommandos, die beim Ausführen von „cargo run“ ausgeführt werden. Jetzt kann der Runner „elf2uf2-rs -d“ ausgewählt werden.
# runner = "probe-run --chip RP2040" runner = "elf2uf2-rs -d" # runner = "probe-rs-cli run --chip RP2040 --protocol swd"
Was passiert jetzt? Mir „cargo build“ haben wir die Anwendung übersetzt und jetzt können wir mit „cargo run“ verschiedene Post Build Steps ausführen. In unserem Fall wollen wir die übersetzte Datei in eine .uf2 Datei umwandeln, die wir dann direkt auf das Target flashen wollen. Dazu wird ein Pico mit gedrücktem Bootselect-Button über USB an den Host angeschlossen. Nach dem Einstecken kann der Button losgelassen werden. Im File Explorer taucht das Pico nun als externen Datenträger, also wie ein USB Stick, auf und erwartet jetzt ein .uf2 Binary zum Ausführen.
Wenn wir beispielsweise ein .uf2 File haben, kann dieses (per Drag und Drop) auf das Pico kopiert werden, danach meldet sich das Pico ab und die Anwendung läuft auf dem Target. Beim Ausführen von „cargo run“ wird das .uf2 File erstellt und direkt auf das Pico geladen, sofern dies mit gedrücktem Button eingesteckt wurde. Das manuelle Kopieren der Datei wird uns also abgenommen. Nach „cargo run“ sollte sich das Pico vom Dateisystem abmelden und nun sollte die LED blinken. Sehr gut, die erste Rust Anwendung läuft jetzt auf unserem Mikrocontroller!
Jetzt können wir bereits Applikationen übersetzten und flashen. Weiter geht es mit dem Einrichten des Debug-Supports.
Debugging mit RP2040
In diesem Teil verwenden wir nun zwei Raspberry Pi Picos. Eines ist das Target, also auf dem soll die Anwendung laufen und eines dient als Debug-Probe. Dieser Teil befasst sich hauptsächlich damit, die Debug-Probe einzurichten.
Bis Debugging möglich ist, benötigen wir nochmals einige Tools und müssen auch das Template Projekt noch ein wenig anpassen. Für die Installation der folgenden Tools und Applikationen habe ich diese Orderstruktur gewählt:
Dies muss man natürlich nicht so machen. Ich habe mir den Order aber in C: angelegt, um möglichst kurze Pfade zu haben und damit alles an dem gleichen Ort installiert ist. Im Folgenden werden die Installationen gemäss dieser Orderstruktur ausgeführt und erklärt.
OpenOCD
OpenOCD ist ein Tool, das vom Host mit der Debug-Probe kommuniziert. Dabei unterstützt es viele Debug-Protokolle und Target-Mikrocontroller, jedoch leider (noch) nicht den RP2040. Deshalb gibt es eine Version von Raspberry Pi, die mit dem RP2040 kompatibel ist.
Diese Version müssen wir jedoch selbst kompilieren, und dafür verwenden wir „Git for Windows SDK“. Dieses muss installiert werden und danach werden noch zusätzliche Packages benötigt, die sich ebenfalls über Git for Windows SDK herunterladen lassen.
Git for Windows SDK laden wir von dieser Webseite https://gitforwindows.org/#download-sdk heruntergeladen und installieren es. Wir wählen die neuste Version aus und installieren sie mit allen Standardeinstellungen.
Um verschiedene Packages zu installieren, führen wir nun C:/git-sdk-64/git-bash.exe aus. Es ist darauf zu achten, dass exakt dieses Programm ausgeführt wird. Wichtig ist, dass wir das Terminal erst schliessen, nachdem alles installiert ist. Folgende Befehle führen wir nun nacheinander aus:
pacman -Syu pacman -Su pacman -S mingw-w64-x86_64-toolchain git make libtool pkg-config autoconf automake texinfo wget
Danach sind alle Meldungen, die ausgegeben werden, entweder mit Enter oder Y + Enter zu bestätigen. Nun benötigen wir noch weitere Packages, dazu sind folgende Kommandos auszuführen und wiederum mit Y + Enter zu bestätigen:
cd ~/Downloads wget http://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-libusb-1.0.26-1-any.pkg.tar.zst pacman -U mingw-w64-x86_64-libusb-1.0.26-1-any.pkg.tar.zst
Es kann vorkommen, dass in einiger Zeit diese Version veraltet ist und gar nicht mehr zum Download angeboten wird. Dann kann unter der Webseite http://repo.msys2.org/mingw/x86_64/ nach dem neusten Release gesucht und dann diese Version installiert werden.
Wenn dies alles eingerichtet ist, kann von GitHub die OpenOCD Version für den RP2040- Mikrocontroller nach C:/rp2040_tools (oder an einen sonst beliebigen Ort) heruntergeladen und übersetzt werden. Dazu werden im gleichen Terminal wie vorher die untenstehenden Kommandos eingegeben:
cd /c/rp2040_tools git clone https://github.com/raspberrypi/openocd.git --branch picoprobe --depth=1 cd openocd ./bootstrap ./configure --enable-picoprobe --disable-werror make
Jetzt wird OpenOCD übersetzt, was ca. 30 Minuten dauern sollte, ein perfekter Zeitpunkt für eine Kaffeepause.
Sobald der Vorgang abgeschlossen ist, muss man die vorher heruntergeladene libusb-1.0.dll in das gleiche Verzeichnis wie die openocd.exe kopieren. Dies erreichen wir durch den folgenden Befehl:
cp /c/git-sdk-64/mingw64/bin/libusb-1.0.dll src/libusb-1.0.dll
Nun ist OpenOCD als ausführbare Datei vorhanden und Git for Windows SDK kann man durch löschen des Orderns C:/git-sdk-64 wieder entfernen. Ich würde empfehlen, dass man den Pfad zu OpenOCD zu den Systemvariablen hinzufügt. Dies ist aber kein Muss, wenn z.B. bereits andere Versionen von OpenOCD auf dem Rechner installiert sind und man selber entscheiden möchte, welche man denn gerade verwenden will. Die Systemvariable ergänzt man folgendermassen:
- In der Windows-Suchleiste „env“ eingeben
- Umgebungsvariablen…
- Bei „Benutzervariablen“ die Variable „Path“ anwählen und dann bearbeiten
- Neue Variable erstellen: „C:\rp2040_tools\openocd\src“
- Alles mit OK bestätigen
Nun testen wir über eine Kommandozeile, ob das Betriebssystem OpenOCD findet. Dazu einfach den Befehl „openocd“ ausführen und schauen, ob eine Meldung zur Version ausgegeben wird. Ansonsten ist ein Neustart des Computers angebracht.
Picoprobe
Picoprobe ist eine Firmware von Raspberry Pi, mit der ein Pico Debug-Befehle ausführen kann, es wird also in eine Debug-Probe umfunktioniert. Das Binary (picoprobe.uf2) kann von der GitHub Seite von Raspberry Pi heruntergeladen werden: https://github.com/raspberrypi/picoprobe/releases.
Wie zu sehen ist, ist dies bereits im Richtigen Dateiformat .uf2, um es auf ein Pico zu laden. Wir wissen ja jetzt, wie wir ein Pico mit einer .uf2 Datei flashen können, also machen wir das. Jetzt müssen wir uns natürlich merken, auf welchem von unseren Boards die Picoprobe Firmware läuft, dies ist unsere Debug-Probe.
Nun müssen wir noch den korrekten USB-Treiber der Debug-Probe installieren. Dazu benötigen wir die Applikation „Zadig“, welche wir von https://zadig.akeo.ie/ herunterladen. Zadig ist eine Standalone-Applikation, ich habe diese dann ebenfalls in die angelegte Ordnerstruktur verschoben.
Nach dem Starten von Zadig wählen wir „Options“ und dann „List All Devices“. Das Pico, welches die Debug-Probe ist, muss über USB mit dem PC verbunden sein. Im Dropdown Menu sollten nun zwei Interfaces für Picoprobe erscheinen. Wir wählen das CMSIS-DAP Interface und ersetzen den aktuellen Treiber mit dem „libusb-win32“. Dieser kann mit den beiden Pfeilen ausgewählt und mit „Replace Driver“ bestätigt werden. Der neue Treiber erscheint dann in der linken Hälfte als „libusb0“.
GNU Toolchain
Wir laden nun die neuste Arm GNU Toolchain von https://developer.arm.com/downloads/-/gnu-rm herunter.
Die Installation erfolgt dann in den von uns angelegten Order, oder wiederum an einem anderen Ort. Wichtig ist auch, dass hier ebenfalls der Systempfad zur Toolchain ergänzt wird, dies kann bei der Installation beim letzten Schritt angewählt werden.
Auch hier ist allenfalls ein Neustart erforderlich.
Konfiguration der Hardware und von VSCode
Nun haben wir alles, was wir benötigen. Jetzt müssen wir noch die Hardware, VSCode und das Projekt konfigurieren, damit wir mit der IDE debuggen können.
Hardware: Die Verbindungen zwischen der Probe und dem Target sind in folgendem Bild dargestellt:
Quelle: https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf
Veröffentlicht von Raspberry Pi Ltd unter der „Creative Commons Attribution-NoDerivatives 4.0 International (CC BY-ND 4.0)“ Lizenz.
Links im Bild sehen wir die Probe, rechts das Target. Die Spannungsversorgung kommt von dem USB-Kabel, das am anderen Ende am Host angeschlossen ist. Damit wird dann auch über die roten und schwarzen Kabel das Target gespeist. Über das blaue und violette Kabel läuft das Single-Wire-Debug Protokoll und über das orange und gelbe Kabel läuft eine UART-Verbindung zur Kommunikation der beiden Picos. Bei mir sieht dies so aus:
Projekt: Im Template Projekt die Datei .vscode/launch.json abändern, dass nur folgender Inhalt darin enthalten ist:
{ "version": "0.2.0", "configurations": [ { "name": "Debug rp2040-project-template", "request": "launch", "type": "cortex-debug", "cwd": "${workspaceRoot}", "executable": "${workspaceFolder}/target/thumbv6m-none-eabi/debug/rp2040-project-template", "preLaunchTask": "Build binary", "servertype": "external", // This may need to be gdb-multiarch depending on your system (i.e. Linux vs Mac) "gdbPath": "arm-none-eabi-gdb", // Connect to an already running OpenOCD instance "gdbTarget": "localhost:3333", // If you have the Pico SDK installed, allows the // display of the RP2040 CPU registers in VS Code //"svdFile": "${env:PICO_SDK_PATH}/src/rp2040/hardware_regs/rp2040.svd", "runToMain": true, "preLaunchCommands": [ "monitor init", "monitor reset init", "monitor halt", ], // Work around for stopping at main on restart "postRestartCommands": [ "break main", "continue" ], } ] }
Hier definieren wir Kommandos und Informationen für den Debugger, beispielsweise wo sich das Binary befindet und dass es vor dem Debuggen übersetzt werden soll.
Dann erstellen wir im .vscode Ordner ein neues File, das den Name „tasks.json“ bekommt. Es hat folgenden Inhalt:
{ "version": "2.0.0", "tasks": [ { "label": "Cargo build", "type": "shell", "command": "cargo", "args": [ "build" ], "problemMatcher": [ "$rustc" ], "group": "build" }, { "label": "Build binary", "type": "shell", "command": "arm-none-eabi-objcopy", "args": [ "--output-target", "binary", # Reads from an ELF binary file "./target/thumbv6m-none-eabi/debug/rp2040-project-template", # Outputs a raw binary file "./target/thumbv6m-none-eabi/debug/rp2040-project-template.bin" ], "problemMatcher": [ "$rustc" ], "group": { "kind": "build", "isDefault": true }, "dependsOn": "Cargo build" } ] }
Diese Datei definiert den gesamten Build der Applikation.
VSCode: Nun brauchen wir noch eine Extension in VSCode, die „Cortex-Debug“ Extension. Diese lässt sich unter dem Extensions-Menu sehr einfach installieren.
Moment der Wahrheit: In VSCode öffnen wir ein Terminal und starten nun OpenOCD mit dem folgenden Kommando:
openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -s C:/rp2040_tools/openocd/tcl
Ich musste mit der Option -s den absoluten Pfad zu den Ordern „interface“ und „target“ angeben. Auch hier kann der Pfad variieren, falls ein anderer Installationsort gewählt wurde. Wenn OpenOCD nicht bei dem Systemvariablen ergänzt wurde, muss ebenfalls zur ausführbaren Datei der gesamte Pfad angegeben werden.
Folgende Meldung sollte erscheinen:
Open On-Chip Debugger 0.11.0-g4f2ae61 (2023-05-05-14:21) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "swd". To override use 'transport select <transport>'. Info : Hardware thread awareness created Info : Hardware thread awareness created Info : RP2040 Flash Bank Command adapter speed: 5000 kHz ... Info : CMSIS-DAP: Interface ready Info : clock speed 5000 kHz Info : SWD DPIDR 0x0bc12477 Info : SWD DLPIDR 0x00000001 Info : SWD DPIDR 0x0bc12477 Info : SWD DLPIDR 0x10000001 Info : rp2040.core0: hardware has 4 breakpoints, 2 watchpoints Info : rp2040.core1: hardware has 4 breakpoints, 2 watchpoints Info : starting gdb server for rp2040.core0 on 3333 Info : Listening on port 3333 for gdb connections
Nun können wir im Sourcecode nach unseren Wünschen Breakpoints setzten und in VSCode in dem Menu „Run and Debug“ mit dem Play-Knopf die Debug-Session starten.
Somit sollte nun alles funktionieren und man kann das Target wunderbar debuggen.
Fazit
Wir haben nun Rust vollständig auf dem Raspberry Pi Pico eingerichtet, dazu braucht es einiges an Tools und ein wenig Geduld. Nichtsdestotrotz lohnt sich der Aufwand, wenn man den eigenen Horizont erweitern möchte und selber mal „Hands-On“ Rust entwickeln möchte. Der Vorteil bei diesem Setup ist, dass beide Communities, die von Raspberry Pi und die von Rust, riesig sind und sich so der Einstieg in Embedded Rust deutlich erleichtern kann.
Nützliche Referenzen
Hier sind noch einige Referenzen, für alle, die mehr wissen wollen:
Pico & RP2040 Dokumentation: https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html
RP2040 HAL Dokumentation: https://docs.rs/rp2040-hal/latest/rp2040_hal/
Embedded Rust Book: https://docs.rust-embedded.org/book/
Quellen
Folgende Informationen für diesen Beitrag wurden den folgenden Quellen entnommen:
Embedded Rust Book: https://docs.rust-embedded.org/book/
Shawn Hymel: „How to Build OpenOCD and Picotool for the Raspberry Pi Pico on Windows“, https://shawnhymel.com/2168/how-to-build-openocd-and-picotool-for-the-raspberry-pi-pico-on-windows/
Jim Hodapp: „Getting Started with Rust on a Raspberry Pi Pico (Part 3)“, https://reltech.substack.com/p/getting-started-with-rust-on-a-raspberry-a88
Pingback: Noser Blog Start in die Rust-Welt auf einem Microcontroller - Noser Blog