
Dies ist der erste Teil der Ruuvi-Firmware-Serie, in dem beschrieben wird, wie du eine Sensor-Beacon-Software auf dem RuuviTag mit Segger Embedded Studio erstellst.
Der finale Code dieses Blogposts kann auf Ruuvi GitHub im ruuviblog-Branch, Tag 3.1.1-alpha.1 heruntergeladen werden. Du kannst den Code mit Segger Embedded Studio kompilieren, nachdem du
git clone https://github.com/ruuvi/ruuvi.firmware.c
cd ruuvi.firmware.c
git checkout 3.1.1-alpha.1
git submodule update --init --recursiveStell sicher, dass das Nordic Semiconductor nRF15 SDK zusammen mit dem Repository installiert ist. Folge diesen Anweisungen, um Segger Embedded Studio zu installieren. Nachdem du das Repository geklont hast, findest du die Projektdatei in targets/ruuvitag_b/ses.

Hinweis: Wenn du die Beispiele auf dem RuuviTag ausführst, musst du den Tag möglicherweise komplett löschen, um den Bootloader zu entfernen, der mit dem RuuviTag ausgeliefert wird. Die aktuelle Factory-Firmware zum Wiederherstellen der Original-Firmware findest du auf Ruuvi GitHub.
Das Wichtigste zuerst
Wenn jemand ein neues Gerät bekommt, um darauf eine Anwendung zu entwickeln, ist der erste Schritt meistens, eine LED blinken zu lassen. Schließlich heißt es: Wenn du eine LED zum Blinken bringen kannst, kannst du alles damit machen.
Wir gehen hier allerdings anders vor. Da wir ein batteriebetriebenes Gerät verwenden, legen wir zunächst eine Basis fest, wie niedrig der Stromverbrauch werden kann. Das erreichen wir, indem wir in einen Low-Power-Zustand wechseln und dort bleiben, bis etwas das Gerät aufweckt. In diesem Fall bleiben wir für immer im Low-Power-Zustand, da unser Programm noch keine weitere Funktionalität hat.
Eines der schwierigsten Dinge in der Informatik ist das Benennen von Dingen. Unsere enter-low-power-state-Funktion nennen wir yield statt sleep. Damit signalisieren wir, dass es keinen festen Zeitpunkt gibt, wann die Programmausführung weitergeht: Der Programmablauf wartet, bis ein Ereignis eintritt. In einer Multithreading-Umgebung würden wir in der Zwischenzeit andere Tasks ausführen, aber da unsere Umgebung Single-Threaded ist, warten wir einfach, bis etwas passiert ist, bevor wir mit der Ausführung fortfahren.
Schauen wir uns unsere kommende Architektur an:

Anwendung
An diesem Punkt ist unsere Anwendung extrem simpel. Wir müssen die externen Sensoren abschalten und die GPIO-Leitungen in klar definierte Zustände bringen und dann in den Sleep-Modus wechseln.

Um unsere Anwendung zu erstellen, brauchen wir ein paar Dinge. Wir brauchen Board-Definitionen, die das Pinout enthalten. Sobald die Pinouts definiert sind, brauchen wir einen GPIO-Treiber, um sie zu steuern. Und schließlich brauchen wir die Yield-Funktion, die in den Sleep-Modus geht, bis das nächste Ereignis eintritt. Der Programmablauf ist nichts Besonderes: Wir führen Init-Funktionen aus und richten die GPIOs ein, und dann schlafen wir in der while(1)-Schleife.
Boards
Wir beginnen damit, das Board-Pinout zu definieren. Um unsere Anwendung nicht von Anfang an an RuuviTag_B zu binden, orientieren wir uns am Nordic SDK und erstellen einen Header ruuvi_boards.h.

Als Erstes fällt auf, dass wir keine Header #includen; die Definition muss direkt als Flag an den Compiler übergeben werden. Das liegt daran, dass wir in unserem Anwendungscode nicht gezwungen sein wollen, die verfügbaren Targets nachzuverfolgen, sondern stattdessen BOARD_RUUVITAG_B in unseren Target-Skripten definieren.
Das gibt dir außerdem die Flexibilität, ein eigenes Boards-Repository zu pflegen, z. B. proprietary.boards.c, und deine Board-Definitionen dort zu verwalten. Übergib einfach BOARD_PROPRIETARY und BOARD_NAME im Target-Skript an den Compiler und implementiere deine Board-Liste in proprietary_boards.h. Darauf schauen wir im Abschnitt targets genauer.

Wie der Header vermuten lässt, orientieren wir uns erneut am Nordic Semiconductor SDK. Wir verwenden PCA10040, auch bekannt als nRF52-DK, als Basis, entfernen alles, was noch nicht nötig ist, und benennen die Pin-Definitionen in RUUVI_BOARD_PIN um.
Yield
Yield-Schnittstelle
Definieren wir die Yield-Funktionen. Wir brauchen die Initialisierung und Yield selbst, und wenn wir schon dabei sind, fügen wir auch Delay-Funktionen hinzu. Die Funktionen geben Statuscodes zurück, aber die lassen wir vorerst außen vor und kommen im nächsten Teil der Serie darauf zurück.

Nichts Besonderes. Wir übergeben keine Parameter an die Yield-Funktionen, da diese Parameter vermutlich implementationsspezifisch wären und die Anwendung mit Implementierungsdetails unnötig verkomplizieren würden.
Yield-Implementierung
Die Implementierung der Funktionen ist einfach: Wir leiten die Aufrufe an das Nordic SDK weiter und lassen es den Rest erledigen. Statuscodes, die vom SDK zurückgegeben werden, werden in Ruuvi-Statuscodes umgewandelt – darauf schauen wir im nächsten Teil der Blogserie.
Als Plausibilitätscheck prüfen wir die Definition NRF5_SDK15_YIELD_ENABLED. Wenn diese Funktionen in unser Programm kompiliert werden sollen, muss sie zu „nicht false“ auswerten. Unser Treiber kann nicht wissen, ob die Anwendung diese konkreten Implementierungen nutzen will, daher fügen wir ein Include ruuvi_platform_external_includes.h hinzu, das die Konfiguration aus der Anwendung einbindet. Wir könnten C-Preprocessor-Flags verwenden, wie wir es bei ruuvi_boards.h gemacht haben, aber im Verlauf des Projekts ist mit vielen Konfigurationsoptionen zu rechnen. Explizite Includes erlauben es uns, die Bedeutung jedes Flags und jeder Option sowie gültige Werte zu kommentieren.

GPIO
GPIO-Schnittstelle
Unsere GPIO-Definitionen sind an diesem Punkt schlicht und einfach. Wir haben den High-Impedance-Modus, Input ohne Pull-Widerstände, Input mit Pull-up oder Pull-down, Standard-Output für IO und High-Drive-Output zum Ansteuern von LEDs.
Die Funktionen sind ebenso schlicht: init, configure, toggle, write und read. Wichtig ist, dass die Funktion read einen Pointer auf ruuvi_interface_state_t als Input nimmt; der Pointer-Wert wird auf den Zustand des Pins gesetzt. So können wir den Statuscode aus der Read-Funktion zurückgeben.

GPIO-Implementierung
Der Start der GPIO-Implementierung ist ähnlich wie bei der yield-Implementierung. Wir prüfen, ob NRF15_SDK15_GPIO_ENABLED „nicht false“ ist, bevor wir das Programm kompilieren. Wie bei Yield leiten wir die Funktionsaufrufe an nRF5 SDK15 weiter und geben Statuscodes aus der Funktion zurück.

Es gibt ein paar Stilentscheidungen, die vielleicht Stirnrunzeln auslösen, daher gehe ich hier darauf ein. Die erste betrifft Rückgabewerte: Es ist eine gute Praxis, in einer Funktion nur eine mögliche Stelle zu haben, an der ein Wert zurückgegeben wird. Die Begründung für mehrere mögliche Return-Points hängt mit der Bedeutung des Rückgabewerts zusammen: Wir geben kein Ergebnis einer Berechnung zurück, sondern einen Statuscode. Die Return-Werte werden eher wie geworfene Exceptions behandelt als etwas, das das Programm akzeptieren und weiterverarbeiten sollte.
Die zweite betrifft die If-Else-Strukturen. Viele Leser würden die Funktion ruuvi_platform_gpio_read so schreiben:
ruuvi_driver_status_t ruuvi_platform_gpio_read(uint8_t pin, ruuvi_interface_gpio_state_t* state)
{
if(state == NULL)
{
return RUUVI_DRIVER_ERROR_NULL;
} bool high = nrf_gpio_pin_read(pin);
if(high)
{
*state = RUUVI_INTERFACE_GPIO_HIGH;
}
else
{
*state = RUUVI_INTERFACE_GPIO_LOW;
}
return RUUVI_DRIVER_SUCCESS;
}Erstens: Meine Gewohnheit, die Konstante links im Vergleich zu platzieren, stammt von dem einen Mal, als ich versehentlich eine Zuweisung statt eines Vergleichs geschrieben habe – und das Chaos war perfekt. Mir ist auch klar, dass der Compiler warnen sollte, wenn in einer If-Klausel eine Zuweisung steht, und dass Warnungen als Fehler behandelt werden sollten. Leider gilt das nicht in allen Sprachen, mit denen ich arbeite.
Zweitens: Prüfen, ob ein Boolean-Wert true ist, statt den Boolean-Wert einfach so zu verwenden. Das ist ebenfalls eine Gewohnheit und auch ein bisschen Pythonic Thinking: Explizit ist besser als implizit.
Drittens: Zwei gegenseitig ausschließende If-Klauseln statt If-Else. Das hängt mit meiner Denkweise zusammen: Ich will den Zustand auf LOW setzen, wenn der Zustand LOW war. Welche Logik vorher passiert ist, bevor der Zustand auf LOW gesetzt wird, ist hier irrelevant. Die Zeile
if(false == high) { *state = RUUVI_INTERFACE_GPIO_LOW; }ist in sich geschlossen; wir müssen nichts anderes wissen, um sie zu beurteilen. Wenn wir schreiben
else
{
*state = RUUVI_INTERFACE_GPIO_LOW;
}wissen wir nicht wirklich, was hier passiert. Wir müssten den vorherigen Kontext prüfen, um zu verstehen, welche Logikbedingung in diese Klausel führt. Natürlich könnte man argumentieren, dass das für die erste Klausel genauso gilt, wenn die folgenden Klauseln erreicht werden können. Vielleicht sollte die ganze Funktion
ruuvi_driver_status_t ruuvi_platform_gpio_read(uint8_t pin, ruuvi_interface_gpio_state_t* state)
{
if(state == NULL){ return RUUVI_DRIVER_ERROR_NULL; } bool high = nrf_gpio_pin_read(pin);
if(true == high)
{
*state = RUUVI_INTERFACE_GPIO_HIGH;
return RUUVI_DRIVER_SUCCESS;
}
if(false == high)
{
*state = RUUVI_INTERFACE_GPIO_LOW;
return RUUVI_DRIVER_SUCCESS;
}
return RUUVI_DRIVER_ERROR_INTERNAL;
}Ich sehe das als Geschmackssache: Wenn nichts schiefgelaufen ist, wurde die Funktion erfolgreich ausgeführt. Jemand anderes würde den Code vielleicht lieber so strukturieren: „Wenn die Funktion nicht erfolgreich zurückgegeben hat, ist ein Fehler aufgetreten“.
Konfiguration
Um den Code tatsächlich zu kompilieren, müssen wir eine sdk_configuration.h-Vorlage zu unserem nrf5_sdk15_platform hinzufügen. Wir verwenden die Konfiguration von Nordics ble_app_buttonless_dfu als Basis. Um anwendungsspezifische Overrides zu ermöglichen, binden wir ruuvi_platform_external_includes.h in sdk_config.h ein.
Als Nächstes erstellen wir ruuvi_platform_external_includes.h und binden Header ein, um unseren Platform-Code sowie das Nordic SDK zu konfigurieren.

An diesem Punkt brauchen wir keine Nicht-Standard-Konfiguration für das SDK, aber wir müssen die GPIO– und yield-Funktionen in ruuvi_platform_nrf5_sdk15_config.h aktivieren. Zusätzlich definieren wir NRF5_SDK15_PLATFORM_ENABLED.

Target
Das Einrichten von SEGGER Embedded Studio (SES), um das Projekt zu kompilieren, wäre ein eigenes Tutorial wert – daher gehen wir hier nicht ins Detail. Kurz gesagt: Das SES-Projekt des Nordic SDK 15 ble_app_buttonless_dfu wurde als Basis genommen und Projektdateien wurden hinzugefügt. Außerdem müssen wir die Definition BOARD_RUUVITAG_B für den Preprocessor hinzufügen.

Flashen
Zeit, unsere neue Firmware auf das Board zu flashen und zu sehen, wie sie sich schlägt! Beachte, dass SES beim Flashen nicht den gesamten Tag löscht – es könnte also noch ein Bootloader auf dem Tag sein. Führe im SES-Connect-Menü „Erase all“ aus, um eine saubere Installation zu bekommen.

Nun, das ist nicht so gut. Ruuvi Firmware 1.2.12, die tatsächlich nützliche Dinge tut, verbraucht im Schnitt weniger als 25 uA – wir liegen also schon etwa 18-mal über dem Benchmark, bevor wir überhaupt etwas tun. Aus einer Vermutung heraus lassen wir die Interrupt-Leitungen im High-Z-Modus.

Wie sieht es diesmal aus?

Nach dem Entfernen der Pull-ups liegen wir bei 4 uA Verbrauch. Früher, beim Power-Profiling der FW im 1.x-Branch, sind wir zu dem Schluss gekommen, dass unsere Baseline 8 uA war – das heißt, wir liegen bei der Hälfte des vorherigen Levels. Bis hierhin: sehr gut!
Fazit
Im Moment sind wir funktional ungefähr dort, wo wir Anfang 2016 waren: Wir können Software für den RuuviTag kompilieren und flashen und verifizieren, dass wir im Sleep-Modus einen sehr niedrigen Stromverbrauch erreichen. Wir haben eine Basisstruktur, auf der wir aufbauen können.
