Das Bild in diesem Blogpost stammt vom Benutzer18526052 auf Freepik.

Die Wire-Bibliothek verbindet deinen Arduino mit Sensoren und Aktoren, die über das I2C-Protokoll kommunizieren. Leider hat diese Bibliothek viele Unzulänglichkeiten, und oft möchtest du sie durch eine andere I2C-Bibliothek ersetzen. Die Wire-Bibliothek projektbezogen (also nur für einen Sketch) zu ersetzen, erweist sich als komplizierter, als man erwarten würde. In diesem Blogbeitrag beschreibe ich eine einfache Methode, um das zu erreichen.

Was stimmt nicht mit der Standard-Wire-Bibliothek?

Zunächst einmal gibt es nicht nur eine Wire-Bibliothek, sondern viele davon. Für jede unterschiedliche Architektur gibt es eine eigene Wire-Bibliothek, weil sich die I2C-Hardware für die verschiedenen Architekturen unterscheidet. Allerdings folgen sie alle mehr oder weniger den Spezifikationen, die in der Arduino-Referenz angegeben sind. Hier werde ich mich auf die AVR-Version der Bibliothek konzentrieren.

Die besonderen Implementierungsentscheidungen, die für die AVR-Version der Wire-Bibliothek getroffen wurden, sind nicht ohne Weiteres nachzuvollziehen – um es höflich auszudrücken. Erstens wird die Ein-/Ausgae mit fünf (!) Puffern zu je 32 Byte gepuffert, was 160 Byte wertvollen SRAM beansprucht. Das ist eine totale Speicherverschwendung, denn du kannst die I2C-Kommunikation auch ohne Puffer durchführen, ohne dass es offensichtliche Nachteile gibt. Zweitens: Da der Puffer auf 32 Byte begrenzt ist, kannst du nur 32-Byte-Datenpakete senden und empfangen, was den Durchsatz erheblich verringert. Drittens musst du die GPIO-Pins, die mit dem I2C-Peripheriegerät verbunden sind, für die I2C-Kommunikation verwenden. Viertens kannst du aus diesem Grund keine anderen Pins für I2C-Busse verwenden oder mehr als einen I2C-Bus einrichten. Es gab noch einige andere Unzulänglichkeiten, z. B. Clock-Stretching und die fehlende Unterstützung für wiederholte Starts. Diese Probleme wurden jedoch behoben.

Alternative I2C-Bibliotheken

Aus den oben genannten Gründen gibt es mehrere alternative I2C-Bibliotheken, die versuchen, einige der Probleme zu entschärfen und oft nur die Master-Funktionalität implementieren. Das reicht jedoch aus, um mit I2C-Peripheriegeräten zu kommunizieren.

Eine dieser Alternativen ist die Bibliothek SoftI2CMaster, die Peter Fleurys I2C-Softwarebibliothek für AVR-MCUs im Arduino-Framework mit Inline-Assembler implementiert. Im Vergleich zur Standardbibliothek Wire verwendet sie keinen Puffer und der Flash-Speicherplatz ist um den Faktor vier kleiner (500 statt 2000 Byte). Und auf klassischen AVR-Chips, die mit 1 MHz laufen, bietet sie immer noch etwa 30 kHz Busgeschwindigkeit.

Anstelle der API der Wire-Bibliothek bietet SoftI2CMaster jedoch eine prozedurale Schnittstelle in C. Glücklicherweise gibt es einen C-Wrapper, der die gesamte Funktionalität der Wire-Bibliothek im Master-Modus bereitstellt (mit nur einem Puffer von 32 Byte). Die größten Nachteile sind, dass du die für den I2C-Bus verwendeten GPIOs zur Kompilierzeit festlegen musst und du nicht mehrere Instanzen dieser Klasse haben kannst.

Eine weitere Alternative zur Wire-Bibliothek ist SoftwareWire. Sie ermöglicht es, die GPIO-Pins für den I2C-Bus zur Laufzeit auszuwählen, und ist trotzdem relativ klein und schnell. Sie funktioniert allerdings nur für AVR MCUs.

SoftWire und SlowSoftWire sind beides Bibliotheken, die nur die Arduino-Bausteine digitalRead, digitalWrite und pinMode verwenden, um die GPIOs zu steuern, und aus diesem Grund vollständig portabel sind. Allerdings sind sie auch ein wenig langsam.

Diese alternativen Bibliotheken verwenden normalerweise nur einen Puffer von 32 Byte zum Lesen, was in den meisten Fällen ausreicht. Man kann sich jedoch Situationen vorstellen, in denen ein Puffer nicht ausreicht. Wenn man eine Schreibtransaktion mit beginTransmission startet, gefolgt von einer Lesetransaktion mit requestFrom, bevor die Schreibtransaktion mit endTransmission beendet wurde, dann funktioniert es nicht, wenn nur ein Lesepuffer vorhanden ist.

Wie können wir die Wire-Bibliothek ersetzen?

Die oben erwähnten I2C-Bibliotheken bieten zwar (fast) die gleiche Schnittstelle wie die Wire-Bibliothek (beschränkt auf den Master-Modus), aber es ist schwierig, sie als direkten Ersatz für die Wire-Bibliothek auf einer Sketch-Basis zu verwenden. Die Gründe dafür liegen in der Implementierung der Wire-Bibliothek, in der Nutzung der Bibliothek durch Bibliotheken von Drittanbietern und in der Art und Weise, wie die Arduino IDE einen Sketch erstellt und Bibliotheksabhängigkeiten auflöst.

Wenn du einen Sketch schreibst, der mit einem I2C-Peripheriegerät kommunizieren muss, und es gibt bereits eine Bibliothek dafür, z. B. die Sensor-Bibliothek, dann benutzt du die Bibliothek, indem du die folgende Anweisung in deinen Sketch einfügst:

#include <sensor.h>

Die Sensorbibliothek verwendet wiederum die Wire-Bibliothek, indem sie eine weitere Include-Direktive verwendet, wie folgt:

#include <Wire.h>

Oft verwendet auch die Sketch-Datei die Wire-Bibliothek und enthält ebenfalls eine solche Include-Anweisung. Im Allgemeinen haben wir also die folgende Struktur, wobei Pfeile die Verwendungsbeziehungen symbolisieren.

Wenn du nun die Verwendung von Wire in der Sensorbibliothek für einen bestimmten Sketch ersetzen willst, kannst du den Kompilierbefehl von arduino-cli verwenden. Angenommen, du hast eine alternative Wire-Bibliothek im Ordner /AlternativeLibs/Wire/ vorbereitet, dann wird der folgende Befehl diese alternative Bibliothek anstelle der Standardbibliothek verwenden:

arduino-cli compile -b arduino:avr:uno --library "/AlternativeLibs/Wire"

Wenn du die Arduino IDE verwendest, sind die Dinge weniger einfach. Du hast eine Reihe von Möglichkeiten, die von umständlich über unmöglich bis hin zu hässlich reichen, wobei ich die letzte bevorzuge.

Umständlich: Die Sensorbibliothek ändern

Die sauberste Lösung ist wahrscheinlich, die Sensorbibliothek anzupassen und sie in den Unterordner src im Sketch-Ordner zu kopieren. Dann kannst du die Sensorbibliothek in deinem Sketch verwenden, indem du die folgende include-Anweisung verwendest:

#include "src/sensor.h"

Wenn wir davon ausgehen, dass wir die SoftwareWire-Bibliothek verwenden, musst du die folgenden Änderungen in der Sensorbibliothek vornehmen:

  1. Ersetze #include <Wire.h> durch #include <SoftwareWire.h>
  2. Ersetze jede Erwähnung von TwoWire durch SoftwareWire
  3. Wenn die globale Wire-Instanz verwendet wird, musst du sie in der Header-Datei der Sensorbibliothek einfügen: extern SoftwareWire Wire;

In der Sketchdatei musst du eine SoftwareWire-Instanz mit dem Namen Wire erstellen, z. B.:

SoftwareWire Wire(A4, A5);

Wenn die in der Sensorbibliothek deklarierte Klasse einen Parameter hat, bei dem eine SoftwareWire-Instanz erwartet wird, kannst du dieses Objekt als Parameter übergeben. Wenn die Sensorbibliothek nur die globale Wire-Instanz verwendet, wird alles durch die externe Deklaration in der oben erwähnten Header-Datei geregelt.

Damit ist alles eingerichtet und die Sensorbibliothek kann mit der SoftwareWire-Bibliothek verwendet werden. Diese Lösung bedeutet jedoch, dass du von der weiteren Entwicklung der Sensorbibliothek abgeschnitten bist. Das kann schlecht sein, da du nicht von Fehlerkorrekturen profitieren wirst. Es ist aber immer noch möglich, die Entwicklung der Bibliothek manuell nachzuhalten, indem du den oben beschriebenen Prozess für neue Versionen der Bibliothek wiederholst.

Unmöglich: Vererbung und Methoden-Überschreibung verwenden

Ein eleganterer Weg scheint die Nutzung des Vererbungsmechanismus von C++ zu sein. Da Bibliotheken im Grunde C++-Klassen sind, sollte es möglich sein, die in der Wire-Bibliothek definierte Klasse TwoWire objektorientiert auf eine Unterklasse zu spezialisieren, die das I2C-Protokoll auf eine andere Weise implementiert. Tatsächlich wurde in einer Version der SoftwareWire-Bibliothek die SoftwareWire-Klasse so definiert, dass sie von der TwoWire-Klasse abgeleitet wurde. Das Überschreiben der Methoden hat allerdings nicht geklappt.

Eine notwendige Voraussetzung dafür wäre, dass die Sensorbibliothek nicht mehr das globale Wire-Objekt verwendet, sondern einen zusätzlichen Parameter vom Typ TwoWire akzeptiert, was einige Bibliotheken bereits tun. Aber auch dann gibt es unüberwindbare Hindernisse.

Man (d.h. Arduino.cc) müsste alle TwoWire-Methoden als virtuell deklarieren, was bedeutet, dass Methoden in abgeleiteten Klassen sie überschreiben können. Das würde jedoch implizieren, dass der Code der Wire-Bibliothek nicht durch Link-Time-Optimierung entfernt werden könnte. Schlimmer noch: Man müsste immer noch die globale Wire-Instanz instanziieren, um zu verhindern, dass Sketches, die auf die Wire-Bibliothek angewiesen sind, nicht mehr kompiliert werden. Alles in allem: eine totale Verschwendung von Flash- und SRAM-Speicher. Das ist also nicht der richtige Weg.

Übler Hack: Sich auf die Arduino-IDE zur Auflösung von Abhängigkeiten verlassen

Wenn man den Prozess analysiert, wie die Arduino-IDE Bibliotheksabhängigkeiten auflöst, stellt sich natürlich die Frage, an welchem Punkt man die Suche nach Bibliotheken außer Kraft setzen kann. Es gibt eine längliche Beschreibung, wie Abhängigkeiten aufgelöst werden. Aber selbst, nachdem ich die Beschreibung zweimal gelesen hatte, kam ich nur zu dem Schluss, dass die oben beschriebene --library Option funktionieren würde. Dann entdeckte ich in einer langen Diskussion über ein Problem mit der SoftwareWire-Bibliothek eine interessante Information.

Offenbar wird der Prozess der Abhängigkeitsauflösung schrittweise ausgeführt. Wenn ein Schritt eine Lösung für eine Abhängigkeit liefert, die später aufgelöst werden soll, wird diese Abhängigkeit als bereits aufgelöst betrachtet. Wenn also während der rekursiven Suche nach Importabhängigkeiten die SoftwareWire-Bibliothek (oder eine andere Wire-kompatible Bibliothek) einbezogen wird und der Bibliotheksordner auch eine Datei namens Wire.h enthält, wird jede Abhängigkeit von der Wire-Bibliothek als bereits gelöst betrachtet.

Das bedeutet, dass das Einbinden der folgenden Header-Datei als Wire.h in den Bibliotheksordner unser Problem lösen wird.

#ifndef Wire_h
#define Wire_h

#include <SoftwareWire.h>
extern SoftwareWire Wire;
typedef SoftwareWire TwoWire;
#endif 

Das funktioniert in der Tat wunderbar! Und der Nachteil, den der Autor des oben genannten Beitrags erwähnt, nämlich dass die ursprüngliche Wire-Bibliothek kompiliert und gelinkt wird, tritt nicht ein!

Außerdem sollte es keine negativen Nebeneffekte geben, wenn wir andere Konfigurationen zum Einbinden von Header-Dateien haben. Wenn die Wire-Bibliothek überhaupt nicht verwendet wird, gibt es kein Problem. Wire.h wird nicht eingebunden. Wenn nur die Wire-Bibliothek verwendet wird, aber nicht die SoftwareWire-Bibliothek, dann wird letztere als Kandidat für die Wire-Bibliothek betrachtet (wegen der Header-Datei Wire.h ). Aufgrund der Priorität des Ordnernamens gewinnt jedoch immer die Standardbibliothek. Wenn schließlich die Inlcude-Anweisung der Wire-Bibliothek vor der Inlcude-Anweisung der SoftwareWire-Bibliothek steht, erhält man mehrere Fehlermeldungen, darunter die Meldung Mehrere Bibliotheken wurden für "Wire.h" gefunden. Mit anderen Worten: Du solltest SoftwareWire immer als erstes einbinden, bevor du die Sensorbibliotheken einbindest, die die Wire-Bibliothek verwenden.

Alles in allem heißt das, dass es ausreicht, die SoftwareWire-Bibliothek in die Sketchnote einzubinden, nachdem du die Header-Datei Wire.h in den Bibliotheksordner von SoftwareWire gelegt hast. Alle importierten Sensorbibliotheken, die Wire.h enthalten, verwenden dann stattdessen die SoftwareWire-Bibliothek. Zauberei!

Es gibt, wie immer, einige Dinge, auf die man achten muss. Erstens sind SoftwareWire (und alle anderen Ersatzbibliotheken) nicht zu 100 % mit Wire kompatibel und es kann Situationen geben, in denen sich das zeigt. Zweitens haben alle Ersatzbibliotheken normalerweise eine viel niedrigere Kommunikationsgeschwindigkeit. Drittens: Wenn du versuchst, dies zu kompensieren, indem du die ursprüngliche Wire-Bibliothek für ein I2C-Gerät und z. B. Softwarewire für alle anderen verwendest, wirst du feststellen, dass du nicht sowohl die Wire-Bibliothek (mit hoher Geschwindigkeit) als auch die SoftwareWire-Bibliothek (mit mäßiger Geschwindigkeit) parallel nutzen kannst. Wenn du eine Hochgeschwindigkeitskommunikation benötigst, ist es in diesem Fall besser, SPI statt I2C zu verwenden.

Zusammenfassung und Ausblick

Wenn du die Wire-Bibliothek projekt- oder sketchbezogen ersetzen willst, hast du verschiedene Möglichkeiten. Diejenige, die am wenigsten Änderungen erfordert, ist ein hässlicher Hack, der sich auf Arduinos Mechanismus zur Auflösung von Abhängigkeiten stützt. Aber er funktioniert wunderbar. Ich habe vor, eine eigene Version einer Ersatzbibliothek namens FlexWire zu entwickeln, die diesen Hack sowie eine Möglichkeit zum dynamischen Wechseln der GPIO-Pins enthält, um einfach zwischen verschiedenen I2C-Bussen wechseln zu können. Damit wird es möglich sein, einen Software-I2C-Multiplexer zu implementieren, über den ich im nächsten Blogpost berichten werde.

Views: 6