Weltweiter kostenloser Versand ab 120 € Bestellwert – Zahlung mit PayPal und Stripe – Hergestellt in Finnland

Ruuvi Firmware – Teil 2: Debugging

Ruuvi Firmware Debugging

Dies ist der zweite Teil der Ruuvi-Firmware-Serie, die detailliert beschreibt, wie man eine Sensor-Beacon-Software erstellt, die auf dem RuuviTag mit Segger Embedded Studio läuft. Den finalen Code dieses Blogbeitrags kannst du bei Ruuvi GitHub im ruuviblog-Branch, Tag 3.2.0-alpha, herunterladen. Folge bitte Teil 1 der Serie für Details zum Klonen des Repositorys und Kompilieren des Codes.

Die Lücken füllen

Im letzten Teil der Serie hatte unsere Codebasis bereits einige Stubs zum Zurückgeben von Statuscodes vom Treiber sowie zum Kompilieren des Projekts mit ARMGCC ohne SES. Lass uns diese Abschnitte des Codes ausfüllen und Logging-Funktionalität hinzufügen.

Ruuvi Firmware.c Architektur 3.2.0
Teile in Blau wurden bereits zuvor eingeführt, wir konzentrieren uns auf die Teile in Grün. Verlaufsblöcke werden Änderungen aufweisen.

Logging

Logging-Schnittstelle

Das Nordic SDK verfügt über ausgezeichnete Debug-Log-Makros, allerdings ist das Übergeben von Argumenten an Makros durch Funktionsaufrufe etwas knifflig. Daher machen wir einige Kompromisse und implementieren Logging als Funktionsaufrufe und rufen die Makros in der Plattformimplementierung auf.

Wir definieren vier Schweregrade: Error, Warning, Info und Debug. Unsere init-Funktion nimmt den gewünschten Mindestschweregrad als Parameter. Wir implementieren auch ein flush, das die Logs aus allen Puffern schreibt, sowie eine Fehlercode-zu-String-Funktion, die eine menschenlesbare Beschreibung der Fehler liefert.

ruuvi_interface_log.h
ruuvi_interface_log.h

Logging-Implementierung

Zuerst füllen wir die plattformunabhängige Funktion ruuvi_platform_error_to_string aus. Ich halte die Implementierung für ziemlich unelegant – wir haben ein festes Switch-Case von ruuvi_driver_status_t-Labels und füllen die Werte ein. Falls jemand eine Implementierung entwickeln kann, die die Labels aus der enum-Definition liest und sie mit snprintf zum Pointer schreibt, lass es mich bitte wissen – oder noch besser: Öffne einen Pull Request.

ruuvi_interface_log.c – Dateianfang
ruuvi_interface_log.c – Dateianfang

Zweitens tauchen wir in die Nordic-SDK-Implementierung der Logging-Funktionen ein. Das Nordic SDK verfügt über eine ganze Reihe von Makros, die Zeitstempel, Dateinamen, Modulnamen einfügen und eine großartige Granularität für die Fehlerberichterstattung bieten können. Um die Sache noch besser zu machen, sind sie als Makros implementiert, die nicht kompiliert werden, wenn Logging deaktiviert ist.

Unsere Implementierung ist viel einfacher – wir nehmen einen Funktionsaufruf entgegen und leiten ihn an das Nordic-Makro weiter. Theoretisch könnte unser Compiler bemerken, dass unsere ruuvi_platform_log-Funktion nichts zurückgibt und keine Seiteneffekte hat, was bedeutet, dass sie als NOP optimiert werden könnte, wenn wir Logging deaktivieren. In der Praxis würden wir Linking-Fehler bekommen, da unsere Funktionen Funktionen und keine Makros sind. Vielleicht implementieren wir später ein Dummy-Logging-Backend, das eine leere Funktion sein wird, um das Deaktivieren des Loggings zu ermöglichen.

ruuvi_platform_log.c der nRF5-SDK15-Plattform
ruuvi_platform_log.c der nRF5-SDK15-Plattform

Unsere Logging-Funktion ruft das Nordic-RAW-Makro auf, das keine weitere Präfixierung oder Zeitstempelung der Nachricht vornimmt. Falls die Nachricht aus irgendeinem Grund null war, geben wir eine Warnung aus und lassen den Programmfluss fortsetzen.

Statuscodes

Fehler-Schnittstelle

Wir möchten Fehler so früh wie möglich abfangen, sowohl während der Entwicklung als auch in der Produktion. Daher geben wir den Status von den Funktionen zurück und prüfen den Status im Programm. Wir erlauben auch, einige Fehler als nicht-fatal zu definieren: Wenn beispielsweise ein Umgebungssensor nicht gefunden wird, können wir den Status akzeptieren und fortfahren. Wenn jedoch ein Umgebungssensor gefunden wird und den Selbsttest nicht besteht, sollten wir den Tag als fehlerhaft betrachten und das Programm stoppen.

ruuvi_driver_error.h
ruuvi_driver_error.h

Die Fehlercodes sind als Bit-Flags definiert, und wir verwenden int32_t statt uint32_t als Container, um dem höchsten Bit eine besondere Bedeutung fatal zu geben.

Fehler-Implementierung

Fehlerprüfung und -konvertierung haben nichts Komplexes an sich. Wir ändern die Werte vom Nordic SDK zu Ruuvi-Werten in der Konvertierungsfunktion. Die Fehlerprüfungsfunktion vergleicht den Fehlercode mit der Nicht-Fatal-Maske, und wenn ein Fehler aufgetreten ist, der als fatal betrachtet wird, loggen wir einen Fehler und setzen zurück. Wenn ein nicht-fataler Fehler aufgetreten ist, loggen wir eine Warnung und fahren fort. Wenn kein Fehler aufgetreten ist, fahren wir fort, ohne etwas zu loggen.

ruuvi_platform_error.c in der Nordic-SDK15-Plattformimplementierung
ruuvi_platform_error.c in der Nordic-SDK15-Plattformimplementierung

Konfiguration der Logs

Da wir das Nordic-Backend verwenden, müssen wir einige Konfigurationen hinzufügen. Wir beginnen damit, die Anwendungskonfiguration application_config.h hinzuzufügen und dort Logging-Konstanten zu definieren. Während wir dabei sind, fügen wir auch die GPIO– und yield-Aktivierungen zu unserer Anwendungskonfiguration hinzu und verweisen in ruuvi_platform_nrf5_sdk15_config.h darauf.

application_config.h
application_config.h

Dann aktualisieren wir die nrf5_sdk15_application_config.h. Da das Nordic SDK erwartet, dass die Log-Puffergröße mindestens 128 Bytes und eine Zweierpotenz ist, fügen wir eine Plausibilitätsprüfung für die Puffergröße hinzu.

nrf5_sdk15_application_config.h
nrf5_sdk15_application_config.h

Logging und Fehlerprüfung zur Anwendung hinzufügen

Wir integrieren den neuen Code in unsere Hauptanwendung, indem wir den Statuscode von den Initialisierungsfunktionen sammeln. Da unser Statuscode ein Bit-Flag ist, können wir neue Fehler mit OR hinzufügen, während wir im Programm fortschreiten. Wir fügen auch etwas Logging hinzu, um die Log-Funktionen zu demonstrieren.

Code von main.c
main.c

Zeile 57 zeigt, wie man Laufzeitwerte im Log mit snprintf ausgibt. Lass uns den Code ausführen und sehen, was passiert!

Code ausführen – fataler Fehler bei der Initialisierung und Neustartzyklus.
Code ausführen – fataler Fehler bei der Initialisierung und Neustartzyklus.

Die gute Nachricht ist: Unsere Fehlerprüfung funktioniert wie beabsichtigt. Die andere gute Nachricht ist, dass wir einen Bug im Code von Teil 1 gefunden haben – aus irgendeinem Grund gibt die yield-Initialisierung INVALID_STATE zurück.

Tiefer graben, um die Fehlerquelle zu finden

Jetzt wissen wir, dass es einen Fehler in unserem yield-Code gibt, wir wissen jedoch nicht, was der Fehler ist oder wie er uns betrifft. Schließlich hatten wir am Ende von Teil 1 einen perfekt funktionierenden Sleep-Modus. Unser Logging hat den Fehler zwischen den Zeilen 21 und 25 von main.c lokalisiert, und dort gibt es nichts anderes als die Yield-Initialisierung. Setzen wir einen Breakpoint bei yield und gehen mit Step In in die Funktion.

Unser Programm wurde in yield init angehalten (jetzt Zeile 25) und wir sind bereit, mit F11 / Klick auf das erhobene Symbol hineinzugehen.
ruuvi_platform_yield.c – lass uns ins nRF SDK gehen
nrf_pwr_mgmt.c – Was macht dieses PWR_MGNT_TIMER_CREATE?
Wie der Name vermuten lässt, versuchen wir, einen Timer in der Timer-Erstellungsfunktion zu erstellen.
app_timer.c – wir haben den Übeltäter gefunden.

An diesem Punkt haben wir den Fehler lokalisiert. Unsere yield-Initialisierung prüft, ob wir das timer-Modul initialisiert haben, und das haben wir nicht getan. Warum braucht unser Sleep Timer? Kehren wir zu nrf_pwr_mgmt.c zurück, um das herauszufinden.

nrf_pwr_mgmt.c

Ein paar Zeilen vor dem Fehler sehen wir eine Prüfung für NRF_PWR_MGMT_CONFIG_AUTO_SHUTDOWN_RETRY, die das PWR_MGMT_TIMER_REQUIRED definiert. Wir haben an diesem Punkt keinen Bedarf für irgendwelche automatischen Shutdown-Wiederholungen, also deaktivieren wir es in nrf5_sdk15_application_config.h. Jetzt sind wir bereit für einen weiteren Versuch mit unserer Anwendung.

Finale main.c

Jetzt sind wir näher an dem, was man erwarten könnte. Unsere Initialisierung endet in RUUVI_DRIVER_SUCCESS und der Tag geht in den Sleep-Modus. Merkwürdigerweise wachen wir einmal auf und gehen dann wieder schlafen. Vielleicht haben wir einen Interrupt, wenn das Log-Printing beendet ist?

Letzte Feinheiten

Stromverbrauch

Wie immer ist der Stromverbrauch der treibende Faktor in unserem Code. Es besteht eine gute Chance, dass wir nach dem Aktivieren der Logging-Peripherie etwas zusätzlich verbrauchen, also lass uns den finalen Stromverbrauch überprüfen.

Wir verbrauchen 3,6 μA

Im vorherigen Teil kamen wir zu dem Schluss, dass unser Stromverbrauch 4,0 μA betrug, sodass es scheint, dass wir eine Verbesserung von 10 % ohne ersichtlichen Grund haben. Persönlich vermute ich, dass der Unterschied durch die Umgebungstemperatur verursacht wird – letzte Woche hatten wir eine Hitzewelle in Finnland, während die aktuelle Temperatur viel kühler ist. Nichts im Leistungsprofil gibt uns Anlass zur Sorge, da wir nicht mehr verbrauchen als zuvor.

ARMGCC & Jenkins.

Wir richten auch ein ARMGCC-Ziel für Continuous Integration mit Jenkins ein. Nach diesem Video-Tutorial müssen wir ARMGCC 6-2017-q2-update in /usr/local unseres Jenkins-Servers installieren, nRF5 SDK15 herunterladen und nach /var/lib/jenkins/workspace/ entpacken.

Jetzt wird der Code jedes Mal, wenn ich Code in meinen persönlichen Branch pushe, von GitHub gezogen und kompiliert. Dies stellt sicher, dass jeder Zugriff auf eine funktionierende Version mit allen aktuellen Submodulen hat. Es ermöglicht auch jedem, den Build-Status auf einen Blick in der README zu sehen. Schließlich speichern wir die von ARMGCC erstellte kompilierte .hex-Datei und lassen jeden sie von http://jenkins.ruuvi.com/job/ruuvi.firmware.c/ herunterladen.

Wenn du lieber ARMGCC anstelle von SES verwendest, kannst du jetzt „make“ im Stammverzeichnis des Projekts ausführen, um das Projekt zu kompilieren – vorausgesetzt, du hast die Build-Umgebung eingerichtet.

Fazit

Nach diesem Tutorial haben wir eine grundlegende automatisierte und manuelle Qualitätskontrolle für unser Projekt implementiert. Unsere Treiber geben Fehlercodes zurück, die geprüft und geloggt werden, falls es Anomalien gibt, und wir kompilieren den zu GitHub gepushten Code und speichern die resultierenden Ausgaben. Du solltest beachten, dass ARMGCC und SES unterschiedliche Linker-Skripte verwenden, was bedeutet, dass die resultierenden Hexes nicht unbedingt funktional identisch sind. Vielleicht verbessern wir dies in zukünftigen automatisierten Tests.

In der Zwischenzeit bleib dran und folge @ojousima und @ruuvicom auf Twitter für #FirmwareFriday-Posts!