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

Ruuvi Firmware – Teil 5: Umweltsensorik

Introbild zur Ruuvi-Firmware-Serie, Teil 5

In diesem Teil des Tutorials entwickeln wir eine Sensorschnittstelle, die jedes Backend zum Auslesen von Sensordaten nutzen kann, und implementieren das Backend mit dem integrierten Temperatursensor des nRF52. Danach schreiben wir SPI-Treiber zum Auslesen externer Sensoren und implementieren das Sensor-Backend mit dem Bosch BME280. Den finalen Code findest du auf Ruuvi GitHub im ruuviblog-Branch, Tag 3.5.0-alpha. Bitte folge Teil 1 der Serie für Details dazu, wie du das Repository klonst und den Code kompilierst. Das finale Hex dieses Tutorials kannst du von Ruuvi Jenkins herunterladen.

Die Ruuvi-Firmware-Architektur 3.5.0

Sensorschnittstelle

Im Allgemeinen hat jeder Sensor, den wir verwenden, einige gemeinsame Parameter, wie Auflösung, Abtastrate, Skalierung, Digital-Signal-Processing-(DSP)-Funktion und Parameter für das DSP. Außerdem könnten wir daran interessiert sein, den Sensor zu starten, zu stoppen oder eine Single-Shot-Messung durchzuführen.

Wir wollen außerdem ein paar feste Bedeutungen für unsere Sensoren definieren, wie RESOLUTION_MAX oder SCALE_MIN. So können wir eine Vielzahl von Sensoren einrichten, ohne die genauen Fähigkeiten des zugrunde liegenden Geräts zu kennen. Außerdem verwenden wir 0 als sinnvollen Standard-Wert. Dadurch können wir die Konfiguration per memset auf lauter Nullen setzen und erhalten am Ende sinnvolle Werte, ohne Parameter explizit setzen zu müssen, die uns eigentlich nicht interessieren.

Da wir die Konfiguration später per BLE übertragen wollen, sparen wir in unserer Schnittstelle Platz. Wir verwenden uint8_t-Werte, um die Sensoren zu konfigurieren. DSP-Funktionen sind Bit-Flags, damit sich mehrere Optionen kombinieren lassen.

Konstanten für die Sensorkonfiguration
Konstanten für die Sensorkonfiguration

Da wir jeden verfügbaren Sensor nutzen wollen, definieren wir eine gemeinsame Schnittstelle zum zugrunde liegenden Sensor über Funktionszeiger. So können wir denselben Code mit dem nRF52-Temperatursensor und dem BME280-Umweltsensor verwenden, um Umweltdaten zu erhalten; nRF52 liefert bei Druck und Luftfeuchtigkeit einfach „ungültige“ Werte zurück. Man könnte argumentieren, dass C++ die bessere Wahl für Schnittstellen wäre, allerdings werden wir die beiden nicht mischen, außer es ist aus irgendeinem Grund unbedingt nötig.

ruuvi_driver_sensor.h, Schnittstellenfunktionen
ruuvi_driver_sensor.h — Schnittstellenfunktionen

Unsere Initialisierung erlaubt es, den Bus zu definieren; damit können wir unterstützen, ob der Sensortreiber mit dem jeweiligen Sensor I2C oder SPI verwenden soll. Außerdem übergeben wir ein uint8_t handle, das dem Programm hilft, den richtigen Sensor auszuwählen. Bei I2C ist dieses Handle die Adresse des Sensors, bei SPI ist es der GPIO-Pin, der zum Auswählen des Sensors verwendet wird.

Wir versuchen, unsere Schnittstelle einfach zu halten. Die Abtastrate ist in Hz und die Auflösung in Bits. Werte von 1 … 200 sind erlaubt, wobei 0 die besondere Bedeutung Standard hat und Werte über 200 für Konstanten mit spezieller Bedeutung reserviert sind. Wenn unser Sensor den exakt angegebenen Wert nicht unterstützt, interpretieren wir den Wert als mindestens. Wenn zum Beispiel eine Auflösung von 9 Bit angefordert wird, aber 8 und 10 Bit unterstützt werden, wählen wir 10 Bit Auflösung.

Eine schwierigere Frage ist, was Auflösung und Skalierung darstellen sollen, wenn sie nicht MIN, MAX oder DEFAULT sind. Skalierung ist mit uint8_t als Typ schwerer darzustellen als Auflösung. Für etwas wie die Beschleunigungssensor-Skalierung funktioniert es gut, weil wir problemlos 2, 4, 8, 16 G haben können. Einheiten wie Pascal sind anspruchsvoller: Wir könnten so etwas wie 100 000 Pa haben, das durch die Skalierungszahl repräsentiert werden sollte.

Um unsere Schnittstelle einfach zu halten, wenden wir dasselbe Prinzip an wie bei Abtastrate und Auflösung. Die Skalierung ist in einer sinnvollen physikalischen Einheit wie G. Gleiches gilt für die Abtastrate: Ein Beschleunigungssensor könnte im Low-Resolution-Modus 5 kHz erreichen, aber jeder Wert außerhalb des Bereichs wird mit MAX dargestellt. Das schränkt zwar einige Sonderfälle ein, aber wir akzeptieren, dass jeder spezielle Use Case mehr Arbeit erfordert, und versuchen, die allgemeinen Bedürfnisse zu erfüllen.

Digital-Signal-Processing-Funktionen sind ein noch gemischteres Feld, da es unterschiedliche gewünschte Funktionskombinationen geben kann, die komplett andere Parameter erfordern. Um beim Prinzip „einfach halten“ zu bleiben, erlauben wir ein Bit-Flag der hardwareunterstützten DSP-Funktionen und übergeben einen einzelnen, gemeinsamen Parameter dafür.

Die Sensorschnittstelle definiert Funktionszeiger zum Setzen und Auslesen jedes Parameters. Die Schnittstelle passt die übergebenen Werte an den tatsächlich geschriebenen Wert an oder an default , wenn der Sensor den angegebenen Parameter nicht unterstützt. Der Rückgabecode ist SUCCESS, wenn eine sinnvolle Aktion möglich war, oder ein Fehlercode wie NOT_SUPPORTED, wenn es keine Möglichkeit gibt, die angeforderte Einstellung zu erfüllen.

Umweltsensor

Schnittstelle für Umweltsensordaten

Wir verwenden Floats in unserem Sensordatenformat, da wir damit genügend Skalierung in sinnvollen SI-Einheiten haben. Außerdem fügen wir den Daten einen Zeitstempel in Millisekunden hinzu, auch wenn wir noch kein RTC implementiert haben.

ruuvi_interface_environmental.h
ruuvi_interface_environmental.h

Variablen bekommen als Suffix den Namen der Einheit, um jede Mehrdeutigkeit darüber zu vermeiden, um welche Art von Werten es im Code geht.

Implementierung des nRF52-Umweltsensors

Die Umgebungs-Implementierung ist beim nRF52 einfach, da wir dort nur eine einfache, einzelne synchrone data_get-Funktion in nRF52 zur Verfügung haben. Unsere Initialisierungsfunktion richtet das Offset-Kompensationsregister des nRF52 und die Funktionszeiger ein. Die Definitionen von Bus und handle können hier ignoriert werden. Die Deinitialisierung setzt die Funktionszeiger auf NULL, weitere Aktionen sind nicht nötig.

ruuvi_platform_environmental_mcu.c — Initialisierung
ruuvi_platform_environmental_mcu.c — Initialisierung

Die Set-Funktionen geben RUUVI_DRIVER_ERROR_NOT_SUPPORTED zurück, außer der Nutzer gibt zufällig den Standardwert der Einstellung ein, z. B. eine Auflösung von 10 Bit. Außerdem erlauben wir DEFAULT, MIN, MAX und NO_CHANGE. Getter markieren den Wert, auf den gezeigt wird, als default und geben RUUVI_DRIVER_SUCCESS zurück.

ruuvi_platform_environmental_mcu.c — Setter und Getter für Auflösung
ruuvi_platform_environmental_mcu.c — Setter und Getter für Auflösung

Wir ahmen die kontinuierliche Sampling-Funktionalität nach, indem wir ein Flag setzen, um die Daten bei data_get. zu aktualisieren. So können Programme, die beim Umweltsensor auf den Continuous Mode angewiesen sind, auch mit dem nRF52-Temperatursensor sinnvoll arbeiten.

Mode-Setter und -Getter, Datenlesefunktion
ruuvi_platform_environmental_mcu.c — Mode-Setter und -Getter, Datenlesefunktion

Implementierung des BME280-Umweltsensors

Die BME280-Implementierung ist deutlich komplexer, weil wir über den SPI-Bus mit einem externen Sensor kommunizieren müssen. Zum Glück stellt Bosch einen plattformunabhängigen C-Treiber für den BME280 bereit, und wir müssen nur ein paar grundlegende Funktionen bereitstellen, um ihn zu nutzen.

Die Initialisierung ist ähnlich wie bei der nRF52 -Implementierung. Wir prüfen, ob der ausgewählte Bus SPI war, richten das Handle sowie die read/write/delay -Funktionen ein, rufen den Bosch-Treiber zur Initialisierung des BME280 auf, führen einen Self-Test aus und machen einen Soft Reset. Zum Schluss setzen wir das Oversampling für jedes Sensorelement auf 1, um den Stolperstein zu vermeiden, dass der BME280 nicht sampelt, wenn das Oversampling nicht mindestens 1 ist. Wenn keine Fehler aufgetreten sind, richten wir die Funktionszeiger ein und geben SUCCESS zurück.

ruuvi_interface_bme280.c — Initialisierung
ruuvi_interface_bme280.c — Initialisierung

Der BME280 hat natürlich deutlich mehr Umweltsensor-Features als der nRF52, aber die meisten davon zu nutzen ist ziemlich geradlinig über Switch-Cases zur Auswahl der passenden Optionen. Die einzigen Stolpersteine sind: Das Oversampling jeder Sensorkomponente muss mindestens 1 sein, es gibt keine feste Abtastrate, sondern eine Verzögerung zwischen Samples, und die Sampling-Zeit hängt von der Oversampling-Einstellung ab. Wir akzeptieren eine gewisse Ungenauigkeit bei der Abtastrate und nehmen einfach an, dass wir die gewünschte Rate erreichen, indem wir die Verzögerung wählen, die die gewünschte Abtastrate ergibt.

ruuvi_interface_bme280.h — Setter und Getter für Abtastrate.
ruuvi_interface_bme280.h — Setter und Getter für Abtastrate.

Auflösung und Skalierung sind schwer als einzelne Zahl zu definieren, da wir es mit drei verschiedenen Maßeinheiten zu tun haben. Daher geben wir default als Parameterwert zurück und liefern einen Fehler, wenn der Setter etwas anderes als MIN, MAX, DEFAULT oder NO_CHANGE. erhalten hat.

ruuvi_interface_bme280.h — Setter und Getter für Skalierung.
ruuvi_interface_bme280.h — Setter und Getter für Skalierung.

SPI-Treiber

Schnittstelle

Unsere Schnittstelle hat zwei Funktionen: Initialization und einen blockierenden Full-Duplex-transfer. Unsere anderen Treiber für Peripherie implementieren Wrapper, die die Transfer-Funktion verwenden.

Die Schnittstelle definiert SPI-Mode- und Frequenz-Enumerationen sowie eine Initialisierungsstruktur. So vermeiden wir, den Treiber an die Board-Definitionen zu binden, aber die Anwendung muss die Board-Einstellungen passend zum Treiber konfigurieren.

ruuvi_interface_spi.h
ruuvi_interface_spi.h

Implementierung

Die Implementierung selbst enthält nichts, was wir nicht schon gesehen hätten: Wir prüfen die Eingabeparameter, reichen sie an das Nordic SDK weiter und geben eventuelle Fehlercodes zurück. Das Nordic SDK würde viel Feinschliff mit Direct Memory Access (DMA) und Peripheral-Peripheral Interconnect (PPI) unterstützen, aber wir entscheiden uns vorerst für einfache blockierende Transfer-Funktionen. Vielleicht schauen wir uns die Implementierungen später noch einmal an, wenn das Power-Profiling zeigt, dass die funktionale Firmware bei den SPI-Reads viel Strom verbraucht – aber im Moment wäre das verfrühte Optimierung.

BME280-Wrapper

Der Bosch-BME280-Treiber erwartet read-, write– und delay-Funktionen mit den folgenden Signaturen:

int8_t ruuvi_interface_spi_bme280_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len);int8_t ruuvi_interface_spi_bme280_read (uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len);void bosch_delay_ms(uint32_t time_ms);

Delay ist einfach: Wir reichen den Aufruf an ruuvi_platform_delay_ms weiter und ignorieren den Rückgabewert. Read und write erfordern etwas mehr Arbeit: Unsere SPI-Transfer-Funktion ist Full-Duplex mit zwei Buffern, während die Bosch-Funktion Adresse + R/W-Daten verwendet.

Wir implementieren die BME280SPI-Funktionen in der Schnittstelle, also als plattformunabhängig. Im Wrapper rufen wir die vom Platform bereitgestellten GPIO-Steuerfunktionen und die SPI-Transfer-Funktion auf.

ruuvi_interface_spi_bme280.c
ruuvi_interface_spi_bme280.c

Im Wesentlichen teilen wir hier einen Funktionsaufruf aus dem Bosch-Treiber in vier Aufrufe auf:

  • Das angegebene SPI-Gerät auswählen, indem die GPIO-Leitung auf low gesetzt wird
  • Zielregister schreiben
  • Daten aus dem Register lesen/schreiben
  • Das SPI-Gerät abwählen, indem die GPIO-Leitung auf high gesetzt wird

SPI-Treiber initialisieren

Die Initialisierung von SPI erfordert etwas mehr Arbeit als die bisherigen GPIO– oder log-Implementierungen, da unsere Boards unterschiedliche Pinbelegungen oder maximale SPI-Geschwindigkeiten haben können. Wir erstellen eine dedizierte Task für SPI, die die Board-Definitionen liest und an den Treiber weitergibt.

task_spi.c
task_spi.c

Wir nehmen hier eine Abkürzung und gehen davon aus, dass wir den SPI-Mode 0 verwenden. Die Frequenz wird per Switch-Case behandelt, und die Pinbelegung wird unverändert übernommen.

Den Umweltsensor beim Boot auswählen

Idealerweise müssten wir nur eine einzelne Zeile new_environmental_sensor_present 1 zu unserer Board-Definition hinzufügen, und unsere Schnittstelle würde versuchen, den neuen Sensor zu initialisieren, wenn der Treiber implementiert wurde. Das würde jedoch eine Abhängigkeit zwischen Treibern und Boards einführen, und solche Abhängigkeiten vermeiden wir so weit wie möglich. Daher müssen wir die möglichen Sensoren in unserer Initialisierungs-Task für den Umweltsensor hinzufügen. Das bedeutet zwar etwas mehr Arbeit beim Portieren der Anwendung auf ein neues Board mit einem neuen Umweltsensor, aber wir akzeptieren das und machen die zusätzliche Arbeit bei Bedarf für ein neues Ziel-Board.

task_environmental.c — Initialisierung
task_environmental.c — Initialisierung

Die Definitionen RUUVI_BOARD_ENVIRONMENTAL_BME280_PRESENT und RUUVI_BOARD_ENVIRONMENTAL_MCU_PRESENT sind in der Board-Datei definiert. Wenn das Board beide Sensoren haben könnte, kompilieren wir den Code zum Testen beider mit. Beim BME280 erlauben wir den Fehler NOT_FOUND, weil wir diesen Code auf einem RuuviTag B Basic ohne Sensoren ausführen könnten. Wenn der BME280 gefunden wird, kehren wir zurück und der Umweltsensor ist initialisiert und konfiguriert. Wenn der BME280 nicht gefunden wird, versuchen wir, den nRF52-Temperatursensor als Umweltsensor zu initialisieren. Unsere Konfigurationsfunktion übernimmt Konstanten aus der application_config.h

application_config.h — Einstellungen für den Umweltsensor
application_config.h — Einstellungen für den Umweltsensor
task_environmental.c — Konfiguration
task_environmental.c — Konfiguration

Unsere Konfiguration verwendet standardmäßig den Single-Shot-Modus, da wir die Umgebungs-Messung per Tastendruck auslösen wollen. Weil wir davon ausgehen, dass Tastendrücke relativ selten sind, oversamplen wir so stark, wie es die Hardware erlaubt, um möglichst wenig Rauschen in der Messung zu haben.

Den Umweltsensor verwenden

Jetzt können wir mit unserem Board etwas Interessanteres machen, als nur LEDs blinken zu lassen. Lass uns eine on_button-Funktion erstellen, die ein Umgebungs-Sample nimmt, die Sensordaten ausliest und ausgibt.

task_environmental.c — Sensor auslesen und Daten ausgeben
task_environmental.c — Sensor auslesen und Daten ausgeben

Ein bemerkenswerter Punkt ist, dass das Ausgeben von uint64_t und float in Bezug auf die Codegröße ziemlich teuer ist. Nach dem Aktivieren der Compiler-Flags ist unser Code um 3,3 kB gewachsen! Es gibt noch ein weiteres ernstes Problem mit den Compiler-Flags: Unser Jenkins-Build wird sich anders verhalten, da der Code mit einem anderen Makefile und anderen Compiler-Flags kompiliert wird. Wir werden den Jenkins-Build in einem späteren Tutorial fixen, denn dieser Teil ist ohnehin schon lang genug.

Wir fangen an, die RuuviFW-Funktionalität nachzuahmen, indem wir die ROTE LED während der Aktivität einschalten. Ersetzen wir die bisherige task_led_cycle-Task durch task_environmental_on_button

main.c — main
main.c — main

Wir implementieren hier außerdem ein weiteres RuuviFW-Feature: nRF52 gibt ERROR_NOT_SUPPORTED zurück, wenn wir versuchen, das Oversampling zu konfigurieren. Der Status wird am Ende der Initialisierung nicht RUUVI_DRIVER_SUCCESS sein, und wir werden die GRÜNE LED nicht einschalten. Das ist an dieser Stelle ziemlich hacky, weil eine andere Konfiguration in application_config.h keinen solchen Fehler ausgeben würde. Im nächsten Teil, wenn der Beschleunigungssensor hinzugefügt wird, haben wir beim Basic-Modell jedoch immer ERROR_NOT_FOUND, und die GRÜNE LED funktioniert wie vorgesehen.

Testen

Funktionalität

Wenn alles wie vorgesehen funktioniert, haben wir ein Programm, das die Sensoren initialisiert und intelligent zwischen BME280 und nRF52 auswählt – je nachdem, was verfügbar ist. Bei Tastendruck werden die Daten im Terminal ausgegeben.

Ausgabe auf RuuviTag B Modell +
Ausgabe auf RuuviTag B Modell +

Die Sensorwerte sehen beim Modell + plausibel aus, der Zeitstempel ist UINT64_MAX, also der ungültige Wert, weil wir noch kein RTC implementiert haben. Schauen wir, wie es beim Basic-Modell ohne BME280 aussieht.

Ausgabe auf RuuviTag B Modell Basic
Ausgabe auf RuuviTag B Modell Basic

Beim Basic-Modell bekommen wir Warnungen, weil der nRF52 kein integriertes Hardware-Oversampling hat. Die Temperaturmessungen unterscheiden sich stark zwischen den Geräten; eine mögliche Erklärung ist, dass das nRF52-DK unter meinem Modell Plus das Board aufheizt. Das Basic-Modell liefert ungültige Druck- und Luftfeuchtigkeitswerte, weil der nRF52 diese nicht messen kann. Unsere Treiber scheinen wie vorgesehen zu funktionieren.

Stromverbrauch

Wie immer ist ein niedriger Stromverbrauch für uns entscheidend. Wenn unser Code „funktioniert“, aber die Batterie innerhalb weniger Wochen leer saugt, müssen wir zurück ans Reißbrett und die Probleme beheben, bevor wir weitermachen. Schauen wir, wie der Verbrauch beim RuuviTag+ aussieht.

RuuviTag+ im Idle
RuuviTag+ im Idle
BME280-Sampling
BME280-Sampling

Diesmal scheint unser Sleep-Strom bei 3,4 μA zu liegen, runter von 4,1 μA im letzten Teil der Serie. Das könnte mit der anhaltenden Hitzewelle in Europa zusammenhängen – letzte Woche war es deutlich heißer als jetzt. Oder es ist einfach Messungenauigkeit. Wie auch immer: Im Moment müssen wir uns keine Sorgen machen.

Schauen wir, wie es bei einem RuuviTag Basic ohne Sensoren aussieht.

nRF52-Sampling
nRF52-Sampling
nRF52 im Idle
nRF52 im Idle

Beim RuuviTag Basic liegen wir bei 1,4 μA, da wir uns nicht um den Standby-Strom der Sensoren kümmern müssen. Praktisch macht das keinen Unterschied, da beide Tags so wenig verbrauchen, dass die Batterie lange vorher abläuft, bevor der Verbrauch sie leerzieht. Bis hierhin sieht’s gut aus.

Fazit

Das war der bisher längste Beitrag, und wir sind weit vorangekommen. Jetzt haben wir:

  • Eine generische Schnittstelle für Sensoren
  • Intelligente Auswahl des Sensor-Backends für unsere Anwendung
  • Einen generischen SPI-Treiber für externe Sensoren und BME280-spezifische Wrapper dafür
  • Integration des Bosch-BME280-Treibers
  • nRF52 als Fallback für BME280

Die nächsten Tutorial-Teile werden einfacher, da wir auf dieser Grundlage aufbauen und der Firmware Beschleunigungssensor, RTC und ADC-Batteriemessungen hinzufügen.

Bleib dran und folge @ojousima und @ruuvicom auf Twitter für #FirmwareFriday-Beiträge!