Warum Einfaches die Dinge verkompliziert
oder
„Debugging für Anfänger“.

2025-09-24

Wovon ist die Rede?

Eigentlich sollte dies ein kurzer Artikel werden.
Er ist aber leider etwas länger geraten.

Ich benutze der Einfachheit halber das Arduino IDE 2.x.y. mit verschiedenen Linuxvarianten, aber generell Linux.

Ist frei verfügbar und es gibt eine Unzahl Bibliotheken und Beispiele.

Die neueste Version der integrierten Enwicklungsumgebung, Integrated Development Environment (IDE), bietet auch die Möglichkeit ein C-Programm Schrittweise durchzuspielen, genannt „debugging“.

Voraussetzung ist ein kleines Gerät, „Pico Debug Probe“.

Ein zweiter Pico kann ebenfalls als Debugger verwendete werden, ist aber hier kein Thema.

Ich habe meinen „Pico Debug Probe“ etwas modifiziert.

Ich habe eine weitere Verbindung auf dem Teil angebracht um vom USB-Stecker die Vusb über eine Schottkydiode an den Ziel-Pico weiterzuleiten auf Vsys.

Vorteil: ich brauche nur eine USB-Verbindung.

Hier ein Beispiel um den großen Bruder für Demos zu nutzen:

Hier noch ein anderes Beispiel:

Standardmäßig werden von dem „Pico Debug Probe“ zwei Kommunikationsverbindungen zum Ziel-Pico hergestellt.

  1. Verbindung zu UART0 mit GP0 und GP1, Pin1 und Pin2
  2. Die Debugverbindung mit SWC und SWD
    Diese Verbindungen können zum Laden des Programmes und zum Anzeigen von Meldungen im Arduino Monitor.
    Statt „Serial.println()“ muß aber „Serial1.println()“ benutzt werden, der durchgeleitete UART0.

Diese Möglichkeit hat sich als stabiler herausgestellt statt der Standardlösung per direkter USB-Verbindung.

Auf Linux müssen mehrere Dinge vorher eingestellt werden.


Der Benutzer muß Mitglied von „plugdev“ sein und eine „udev rule“ erstellt werden.

Kommandos:

Ob das alles wirklich nötig ist, habe ich nicht verifiziert.


Die größere Herausforderung war zu verstehen wie Arduino nun überredet werden kann den Code in Einzelschritten zu durchforsten.

Nach vielen frustrierenden Versuchen und auch Suchen im Internet habe ich dann Erfolg gehabt.

Hier die kurze Erklärung.

Die Verwirrung kommt von den identischen Symbolen.

Das obere Symbol startet den Debuggerprozeß.

Das linke Symbol öffnet die Debugleiste.

Das sieht dann wie folgt aus:

Die Symbole bedeuten:

Die Funktionen können auch per Tastatur ausgeführt werden:

$$
\begin{array}
{ c | c }
Funktion & Tasten\\
\hline
Pause/Continue & F5 \\
Stop Debugging & Shift+F5 \\
Step Over & F10 \\
Step In & F11 \\
Step Out & Shift+F11 \\
Restart & Ctrl+Shift+F5 \\
\end{array}
$$


Breakpoints können dann per Mausklick erzeugt werden, aber auch hier an der richtigen Stelle.

Wie man sieht links von der Zeilennummer.

Wichtig!
Der RP2040/RP2350 hat nur vier Hardware-Breakpoints.


Markiert man mehr als diese vier bekommt man sehr merkwürdige Fehlermeldungen, die alles beschreiben nur nicht den Fehler.

Aber das ist ja eine der typischen Eigenschaften von Fehlermeldungen.

Ist wahrscheinlich Absicht, um uns geistig flexibel zu halten. 😎

Was mir jetzt noch fehlt ist, wie kann ich den Wert von Variablen sehen?

Variablen per Debug untersuchen.

Wer glaubt, daß Variable in der Kategorie „VARIABLES“ zu finden sind, sieht sich getäuscht.
Nein, die müssen unter „WATCH“ hinzugefügt werden.

Um dies zu demonstrieren erweitere ich das „Blink“ Programm.

  1. Blinken wird nicht mit delay() realisiert, sondern mit einer Funktion, die Millisekunden zählt.
  2. Statt der „LED_BUILTIN“ benutze ich eine, die auf dem Display vorhanden sind.

Da ich dies schon mehrere Male benutzt habe, sind dies notwendigen Dateien schon in meinem „Common“ Ordner.
Das sieht bei mir wie folgt aus:

Die Datei „flashled.ino“:

Die Datei „pico_display_2_8_pins.h“:

Das Programm „blink_debug_test.ino“:

Die LEDs auf dem Display sind mit der Anode an +3V3 verbunden und nicht wie die LED_BUILTIN mit der Kathode gegen GND.

Daraus ergibt sich, das die Werte für LED_LOW_TIME und LED_HIGH_TIME getauscht werden müssen.

In der setup() Funktion müssen die LEDs mit digitalWrite(LED_xx, HIGH) abgeschaltet werden.

Das eigentliche Program, die Endloschleife, wird recht übersichtlich.
Aus alten Tagen habe ich mir noch angewöhnt mit NOPs gezielt Breakpoints einzusetzen.

Bei den Prozessoren in der Vergangenheit haben einige Debugger vor dem Breakpoint angehalten und andere danach.

Als ersten Versuch mit einer Variablen habe ich „int cnt“ lokal in loop() deklariert und im WATCH per „+“ hinzugefügt.

Jetzt schlägt der Optimizer zu.

Nun die Variable als „ volatile int cnt“ deklariert.

Jetzt wird zwar ein Wert angezeigt, wo immer der herkommt, aber nicht inkrementiert.
Also die Variable außerhalb von loop() global deklarieren und auf Null initialisieren.

Jetzt wird der Wert nach einem Neustart mit Null angezeigt und mit jedem „CONTINUE“ inkrementiert.

Das ist zwar so nicht gewünscht, aber während der Test-Phase akzeptabel.

Nun zum digitalen Ausgang mit welchem die LED angesteuert wird.

Wenn ein neuer Eintrag im WATCH hinzugefügt wird erscheint ein PopUp-Fenster:

Nun sollte man den Begriff „Expression“ nicht überbewerten.

Wer hier von komplexen Möglichkeiten träumt wie unter „Vscode“, muß dies auch weiterhin tun.

Hier einige Beispiele.

Als erstes die Anweisung digitalRead(LED_BL).

Warum?

Weil der Zustand des Pins per Eingang immer zur Verfügung steht.

Gesetzt wird ein Pin über ein spezielles Single Cycle register (sio_xyz).

Diese kann man sich natürlich auch ansehen, wird aber etwas später getan.

Der Eintrag digitalRead(LED_BL) führt nicht zum gewünschten Ergebnis.

Ändert man jedoch den Eintrag und benutzt die entsprechende GPIO Nummer, 28, dann wird der Zustand angezeigt.

Das führt zum nächsten Test.

Statt per #define den Wert vorzugeben, deklarieren einer Variablen und mit 28 initialisieren.

int v_LED_BL = 28; und digitalRead(v_LED_BL) im WATCH führen zum Ziel.

Ich habe bis jetzt bewußt so getan als ob diese Ausdrücke nur ins WATCH eingetragen werden müssen und dann sieht man die Werte.
Leider genügt dies nicht.

Auch im Programm müssen diese Anweisungen irgendwie ausgeführt werden.

Dazu habe ich mir zwei Variable deklariert, die dann per readDigital() gefüllt werden.

Also zusätzlich noch bool led_state = false; und bool led_state_2 = false; deklarieren und im Programm laden.

Um die LED mal hell mal dunkel zu sehen ist natürlich die flash() Funktion unsinnig.

Wer will schon 20ms und 500ms per mausclick durchklappern.

Aber hier geht es ja om Moment „nur“ um Variable.

Jetzt zu einem mir persönlich wichtigem Punkt.
Ich möcht den Inhalt eines Registers in seiner Gesamtheit sehen.

Die Pico Register sind 32 Bit weit.

Ich hätte gerne die Darstellung als Dezimalzahl, 32 Einzelbits, byteweise Dezimal und als „Nibbles“ hexadezimal.

Ähnliches habe ich schon in der Vergangenheit immer gemacht mit der Hilfe einer „union“, die die verschiedenen Daten auf einen gemeinsamen Speicherplatz abbildet.

Ich habe mir auch die Version mit Pointern angesehen, fand sie aber verwirrend.

Hier also die Lösung für schlichte Gemüter.

standard_unions.h

Diese Datei per Link in den Ordner einbinden und schon kann es losgehen.

Um die Beziehung zwischen sio_hw und gpio_out/gpio_in zu verstehen muß man folgendes studieren:

  • Raspberry Pi Pico-series C/C++ SDK, 745 Seiten!
    (https://datasheets.raspberrypi.com/pico/raspberry-pi-pico-c-sdk.pdf)
  • hardware/regs/sio.h
  • hardware/structs/sio.h

Man kann es aber auch sein lassen. 😇

Die kurze Fassung.

„sio“ steht für „Singlecycle Input Output“ und repräsentiert eine Adresse im Speicherraum des Pico, da dieser sogenanntes „Memory Mapped IO“ nutzt.

Das heißt Register verhalten sich wie Speicherzellen, man kann sie beschreiben und lesen.

Innerhalb des „sio“ Blocks sind nun irgendwo die Register „gpio_out/gpio_in“ angesiedelt und sind relativ zur „sio“ Startadresse definiert.

„sio_hw->gpio_in“ ist also nichts anderes als eine Adresse die sich aus Startadresse plus relativer Adresse (offset) errechnet.

So sind alle Register definiert.

Kennt man eines, kennt man alle. Na ja, nach Studium der 750 Seiten und der header Dateien.

Ich habe mich auf die zwei genannten beschränkt.

Nun zur Darstellung der Inhalte.

Im Vorspann sind werden zwei Variablen „in“ und „out“ deklariert und mit den Werten der Register zur Einschaltzeit initialisiert.

Diese Variablen benutze ich um den 32 bit Wert der „union“ zu laden und dann die einzelnen Mitglieder zu drucken.

„print_value.ino“ existiert in meinem Common Ordner und wird per Link nutzbar gemacht.

Jetzt kann mit dieser Routine der Zustand von „out“ oder „in“ am Ende von setup() überprüft werden.

Innerhalb der Schleife sollten dann aber frische Werte gelesen werden.

Um nun auch etwas zu sehen sollte die ursprüngliche Variante von „Blink“ benutzt werden und dann jeweils die aufgefrischten Registerinhalte angezeigt werden.

Im Moment wird wieder per print entflöht.

Geduld, die WATCH Einträge kommen noch.

Eine generalisierte Lösung

Ich benutze gerne generalisierte Hardware um dann spezialisierte Software zu erstellen.

Der Raspberry Pi Pico ist wunderbar geeignet schnell mal eine Lösung zu bekommen.

Man muß jedoch einige Einschränkungen in Kauf nehmen.

Für ein Endprodukt mit eigenem Layout sind die Einschränkungen zu groß.

Ich habe mir daher „schnell mal“ eine Lösung zusammengestrickt und ein festes Gehäuse drumherum gedruckt.

Eigenschaften

  • RP2350 80 Pin
  • USB C auf USB Micro Kabel
  • +3V3 LDO für SPI, I2C und UART
  • LST XH 2,5mm für SPI1/2, I2C1,2 und UART1
  • 1:1 Anschlüsse für Pico Debug Probe
  • 2 x 20 Pin Sockel mit Belegung wie Pico jedoch größerer Abstand
  • 2 x 12 Anschlüsse für die zusätzlichen IOs

Im Bild ist schon eine LED zu sehen, die mit GP25 verbunden ist, da die PGA Lösung keine „LED_BUILTIN“ besitzt.

Wie schon mehrfach erwähnt ist meine Entwicklungswelt Linux.

Statt Vscode die einfache Variante Arduino IDE v2, im moment 2.3.6.

SDK ist installiert mit dem „earlephilhower“ core, der auch den Debugger stellt, auf Linux – wichtig – „gdb“.

Daraus folgen alle nachstehenden Bemerkungen.

Sicherstellen, daß der Pico Debugger Probe erkannt wird folgende udev rule:

Einige Webseiten zeigen für idProduct 0003.

Um den richtigen Wert zu finden muß „lsusb“

konsultiert werden.

Und auch folgendes Überprüfen:

  • Board Select: Pimoroni PGA2350 (in meinem Fall)
  • Upload Method: “Picoprobe/Debugprobe (CMSIS-DAP)`”
  • Port: “/dev/ttyACM0”
  • Monitor Baud Rate: 115200

Zum Programm

Auch hier wird das „Blink“ Beispiel abgewandelt.

„tree“ zeigt folgende Struktur:

Das Programm „blink_debug.ino“:

Wenn Debug enabled (deunglisch) ist sollte im Monitor folgendes angezeigt werden:
(meine flatpak variante druckt einen Zeitstempel ?)

Der RP2350 80 pin hat mehr als die normalen 29 GPIOs.

Ein kompletter Satz wird mit zwei Registern verwirklicht.

Ich drucke im Monitor exemplarisch vier Register: out, out_h, in, in_h.

Für jedes Register den Wert decimal, hexadecimal und binär.

Nun Will man ja nicht unbedingt immer den Monitor bemühen, weil die ganze Druckerei ja auch Programmspeicher und Zeit kostet.

Also jetzt den Debugger konfigurieren um die Werte im WATCH Teil darzustellen.

Die gewählten Variablen werden alle decimal dargestellt.

Das ist bei einzelnen Bits wie LEDs kein problem mit 0 und 1.

Jedoch bei den Registern möchte ich auch die einzelnen Bits sehen.

Nach längerem Suchen im Internet habe ich folgende Lösung gefunden für „gdb“:

„out,b“ zeigt die Bits, allerdings ohne Führungsnullen. Aber immerhin.

Die Werte in hex mit „out,h“.

Wenn jetzt noch Debug disabled wird, werden die Variablen immer noch über #else gefüllt, aber im Monitor sollte nur noch das Spinningsymbol angezeigt werden.

Hier im Monitor auf einzelnen Zeilen und auf einem TFT Display immer in derselben als Animation.

Die WATCH Werte sollten jetzt bei Einzelschritten die Bits anzeigen.
Dazu habe ich och zwei zusätzliche Breakpoints gesetzt auf die beiden
digitalWrite(LED_BL, LOW);
Zeilen.

Werte am ersten Breakpoint:

Ein mal Step Over:

Zweites mal Step Over:

Continue zu Breakpoint drei:

Ein mal Step Over:

Zweites mal Step Over:

Continue zu Breakpoint zwei:

Das ist natürlich sehr informativ, aber auch nicht wirklich übersichtlich.

Die einzelnen Bits aus der union können hier helfen.

Nach dem setzen per digitalWrite() sehe ich mir mal die einzelnen Bits an.

Hierzu benötige ich wieder zwei Variable bit_25_out und bit_25_in.

Dann sollten Continue Steps zeigen:

und

Und damit kann man leben.

Résumé

Arduino IDE 2 kann mit Debugger genutzt werden.

Es muß nicht alles in eine Datei gepackt werden.

Software-Projekte können, wenn auch primitiv, in Ordnerstrukturen gespeichert werden.

Es kann auf Serial.println() verzichtet werden, jedoch werden Variable für WATCH benötigt.

Nach diesem etwas längeren Artikel werde ich mich nun mal wieder der Lösung eines Projektes annehmen.

Wir haben uns eine Pause verdient.