Fühler für die Hardware

Android Things – die große Einführung: Teil 1 – Sensor mit I2C
Keine Kommentare

Android Things ist – sowohl unter Java als auch unter C – in Sachen Hardwareinteraktion alles andere als schnell. Unser Raspberry Pi bringt mit seinen hardwarebeschleunigten Bus-Interfaces eine alternative Möglichkeit mit, die uns die Arbeit erleichtert. Als Erstes wollen wir uns mit I2C beschäftigen – einem von Philips Semiconductor entwickelten Bus, der heute bei Sensoranbietern große Popularität genießt.

Android Things – die große Einführung

In der Praxis findet man Verweise auf den Namen TWI oder Two-Wire Interface – Philips hatte ein Patent auf den Namen, weshalb insbesondere Unternehmen wie Atmel einen alternativen Begriff bevorzugten. Aus technischer Sicht ist I2C einfach: Vom Master zu den Slaves laufen zwei Leitungen. Die erste der beiden, als SDA bezeichnet, ist für die eigentliche Übertragung der Daten verantwortlich. Über SCL, vulgo Serial Clock, liefert der Master einen Arbeitstakt. In den Spezifikationen findet man hier verschiedene Werte – 100 und 400 kHz sind besonders populär. Die meisten Sensoren legen keinen großen Wert auf die Genauigkeit ihrer Arbeitsfrequenz.


Artikelserie: Einführung in Android Things

  • Teil 1: Sensor mit I2C in Android Things
  • Teil 2: Schwarz-weiß-Displays mit SPI anschließen
  • Teil 3: Farbdisplays anschließen

Von besonderem Interesse ist, wie Master und Slaves gemeinsam über eine Leitung kommunizieren können. Der Grund dafür sind die Pull-up-Widerstände. Sowohl SCL als auch SDA werden über einen Widerstand – er liegt im Bereich von 5-10 kOhm – leicht in Richtung der Versorgungsspannung gezogen. Sowohl Master als auch Slave ziehen diese Leitungen nach unten, das Hochschweben erfolgt nur über die Pull-ups. Zum Funktionieren eines I2C-Busses muss für jede Leitung ein solcher Widerstand vorhanden sein. Unser Raspberry Pi bringt allerdings schon einen mit, wie auch viele andere Einplatinencomputer. Ansonsten ist die Situation einfach. Das Kommunikationsprotokoll sieht 127 verschiedene Adressen vor, weshalb der Master vor jedem Befehl festlegt, welcher Slave für die Entgegennahme verantwortlich zeichnet.

Android Things: Einrichtung des Prozessrechners

Googles unter [1] bereitstehende Entwicklerkonsole ist nach wie vor nur in Chrome lauffähig – wer die Website mit Firefox aufruft, steht vor einem niemals endenden Ladebalken.

Klicken Sie im ersten Schritt auf Add a Product, um ein neues Projekt anzulegen. Der Autor verwendet SUSHardwareBus als Name, das Vergeben eines alternativen Namens ist ebenfalls möglich. Öffnen Sie danach das von Google erzeugte Modell, und legen Sie wie gewohnt eine Build Configuration an. Als Betriebssystemversion verwendet der Autor 1.0.8.5206609 – wer eine frühere oder spätere Variante nutzt, muss mit (kleinen) Änderungen im Bereich der Hardwaretreiber rechnen.

Diesmal wollen wir die Konfiguration der Hardware teilweise im Backend vornehmen. Im fünften Schritt klicken wir auf die Option Add a Peripheral, um den Hardwarekonfigurationsassistenten zu öffnen. Wählen Sie im Feld Choose a peripheral die Option PIO I2C aus, um einen I2C-Bus anzulegen. Im Feld Bus entscheiden wir uns für I2C1, im Feld Frequency wählen wir 400kHz. Verlassen Sie den Assistenten durch Klick auf Save – das Resultat präsentiert sich wie in Abbildung 1 zu sehen.

Abb. 1: Der I2C-Bus ist fertig konfiguriert

Abb. 1: Der I2C-Bus ist fertig konfiguriert

Die restlichen Voreinstellungen übernehmen wir 1:1, da wir mit einer 8 GB großen SD-Karte arbeiten. Nach dem Kompilierungsprozess bietet Google ein Image zum Download an, das wie gewohnt – als Entwicklerversion – auf den Raspberry Pi wandert.

Wichtiger Hinweis: Android Things unterstützt derzeit nur das normale Modell 3B – das neuere 3B+ kann das Betriebssystem nicht starten.

An dieser Stelle sollten Sie den Prozessrechner mit Internet, Monitor und Stromversorgung verbinden und starten. Je nach Größe und Geschwindigkeit ihrer SD-Karte dauert das erstmalige Hochfahren bis zu zehn Minuten.

Das Anschließen von Tastatur und Maus ist nicht unbedingt erforderlich – insbesondere dann nicht, wenn Sie die Internetverbindung direkt ohne WLAN herstellen. Das erweist sich in der Praxis als hochgradig empfehlenswert. Debuggingprozesse dauern erfahrungsgemäß sehr lange, wenn die durch WLAN verursachte zusätzliche Latenz hinzukommt. Öffnen Sie im nächsten Schritt eine Version von Android Studio 3.2 und starten Sie den Assistenten zur Generierung eines neuen Projektskeletts. In der Plattformauswahl entscheiden Sie sich für Android Things und entfernen die Checkbox vor der Mobilversion des Betriebssystems. In der Activity-Auswahl entscheiden Sie sich für Android Things Empty Activity und bestätigen auch diese Auswahl mit Next. Die restlichen Einstellungen übernehmen Sie eins zu eins, das eventuell erforderliche Herunterladen einiger Komponenten ist ebenfalls in Ordnung.

Falls Sie die gleiche Installation von Android Studio verwenden, die Sie zuvor schon für C++-Experimente eingespannt haben, sollten Sie darauf achten, die C++-Unterstützung zu deaktivieren. Wir arbeiten in den folgenden Schritten ausschließlich mit Java – die minimalen Performancevorteile von C++ sind ja, wie weiter oben festgestellt, an dieser Stelle ohne Relevanz. Im nächsten Schritt müssen wir die Deklaration für die Berechtigung zum Hardwarezugriff zum Projekt hinzufügen. Öffnen Sie die Manifestdatei und aktivieren Sie sie:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tamoggemon.susthermodemo">
  <uses-permission android:name="com.google.android.things.permission.USE_PERIPHERAL_IO" />

 

Auch I2C-Busse sind unter Android Things über einen Devicestring ansprechbar. Unsere erste Amtshandlung besteht deshalb darin, diesen herauszufinden. Die im Backend durchgeführten Konfigurationseinstellungen betreffen uns an dieser Stelle insofern nicht, als Android Studio davon nichts erfährt. Erfreulicherweise ist die Problemlösung einfach. Ergänzen Sie onCreate im ersten Schritt um ein wenig Zusatzcode, den der Inhalt des I2C-Bus-Arrays in die Debuggerkonsole ausgibt (Listing 1).

@Override
protected void onCreate(Bundle savedInstanceState) {
  . . .
  PeripheralManager manager = PeripheralManager.getInstance();
  List<String> deviceList = manager.getI2CBusList();
  if (deviceList.isEmpty()) {
    Log.i("SUS", "No I2C bus available on this device.");
  } else {
    Log.i("SUS", "List of available devices: " + deviceList);
  }
}

Android Studio ist nicht in der Lage, im Netz herumwirkende Endgeräte automatisch zu erfassen und als Debuggingziele zu erkennen. Öffnen Sie ein Konsolenfenster, in dem sie den Raspberry Pi durch Eingabe des passenden ADB-Befehls als gültiges Ziel deklarieren:

tamhan@TAMHAN14:~/Android/Sdk/platform-tools$ ./adb connect 192.168.1.103
* daemon not running; starting now at tcp:5037
* daemon started successfully
connected to 192.168.1.103:5555

Nach Eingabe des Kommandos sollte unser Prozessrechner in der Zielliste von Android Studio aufscheinen. Befehlen Sie die Programmausführung und überwachen Sie die LogCat-Ausgabe. Auf einem Raspberry Pi 3 präsentiert sich das Ergebnis folgendermaßen:

I/SUS: List of available devices: [I2C1]

Eine Frage der Konfiguration

Die in der Einleitung erfolgte allgemeine Besprechung des I2C-Busstandards ist für uns nun insofern von Relevanz, als sich die Frage stellt, wie Android Things bzw. das Betriebssystem I2C-Geräte auf API-Seite exponieren. In der Welt der Embedded-Betriebssysteme hat sich die Arbeit mit Geräteabstraktionsklassen als Standard etabliert – es handelt sich dabei um eine Klasse, die ein mit einem bestimmten Bus verbundenes Gerät eindeutig anhand seiner Adresse identifiziert. Die in der Theorie insgesamt 127 Adressen stehen am HDC2010 nicht zur Verfügung – wer das Datenblatt öffnet, sieht das in Abbildung 2 dargestellte Layout.

Abb. 2: Texas Instruments beschränkt den Adressbereich des Sensors

Abb. 2: Texas Instruments beschränkt den Adressbereich des Sensors

Es handelt sich hierbei übrigens nicht um eine mutwillige Handlung seitens TI. Der Sensor befindet sich in einem Gehäuse, das nur 6 Pins umfasst – wer mehr als zwei Sensoren an einem I2C-Bus betreiben möchte, muss auf eine Mux setzen. Hier gibt es eine Vielzahl verschiedener Implementierungsmöglichkeiten – die unter [2] und [3] zu verfolgende Diskussion bietet einen Überblick über die Möglichkeiten.

Das geradezu winzige Gehäuse ist für uns nicht direkt zu verarbeiten. Wenn Sie es – in der Praxis kommt das häufig vor – mit einem für Sie manuell nicht zu verarbeitenden Gehäuse zu tun bekommen, empfiehlt sich die Suche nach einem Entwicklungskit. Die offiziellen Produkte sind dabei meist sehr teuer, weshalb Drittanbieter wie MikroElektronika eine attraktive Alternative sind. Wir wollen in den folgenden Schritten mit dem MIKROE-2937 arbeiten, das wesentlich preiswerter ist als das offizielle Produkt aus dem Hause TI (Abb. 3).

Abb. 3: Texas Instruments ruft für das offizielle HDC2010EVM 26 Euro auf, MikroElektronika für sein MIKROE-2937 nur 11 Euro

Abb. 3: Texas Instruments ruft für das offizielle HDC2010EVM 26 Euro auf, MikroElektronika für sein MIKROE-2937 nur 11 Euro

Wer das Board aufmerksam betrachtet, findet einen kleinen SMD-Jumper, der die Auswahl zwischen 3,3 V und 5 V Versorgungsspannung ermöglicht. Das in Abbildung 4 gezeigte Schaltbild zeigt, dass diese Auswahl für uns irrelevant ist – es geht nur darum, ob die Versorgungsspannung am Pin 3V3 oder am Pin 5V anliegt. Normalerweise ist das Board für 3,3 V konfiguriert; falls Sie es mit dem 5-V-Jumper zu tun bekommen, müssen Sie nur den Versorgungspin austauschen. Die resultierende Schaltung präsentiert sich jedenfalls wie in Abbildung 5 gezeigt.

Abb. 4: Die Schaltung unseres Evaluationsboards ist nicht sonderlich kompliziert

Abb. 4: Die Schaltung unseres Evaluationsboards ist nicht sonderlich kompliziert

Abb. 5: Das Anschließen von I2C-Peripheriegeräten an den Raspberry Pi ist denkbar einfach

Abb. 5: Das Anschließen von I2C-Peripheriegeräten an den Raspberry Pi ist denkbar einfach

Wichtig ist beim in Abbildung 5 gezeigten Schaltbild nur, dass Sie den CS-Pin von Hand auf Low ziehen müssen. Der HDC2010 wartet dann nämlich auf Adresse 64. Würde das Signal schweben, könnte sich die Adresse im laufenden Betrieb verändern oder es könnten sonstige Instabilitäten auftreten.

Das Vorhandensein zusätzlicher Pull-up-Widerstände ist hier kein Problem, da der Wert von 4K7 für den Raspberry Pi nicht kritisch ist. Unsere nächste Amtshandlung ist das Anlegen der I2CDevice-Repräsentationsklasse, die auf globaler Ebene zu liegen kommt:
public class MainActivity extends Activity {
I2CDevice mySensor;

Die eigentliche Initialisierung erfolgt in onCreate. Im Interesse der Kompaktheit drucken wir hier die Beschaffung des PeripheralManager nicht ab, sondern beschränken uns auf die Einrichtung von I2CDevice (Listing 2).

@Override
protected void onCreate(Bundle savedInstanceState) { . . .
  try {
    mySensor = manager.openI2CDevice("I2C1", 64);
  }catch (IOException e) {
    Log.w("SUS", "I2C-Zugriffsfehler!", e);
  }
}

Im Interesse des Housekeepings implementieren wir onDestroy, das eine beschaffte Instanz freigibt. Das ist nicht nur aus akademischer Sicht vernünftig, sondern sorgt dafür, dass Sie den Prozessrechner beim Debuggen nicht permanent neu starten müssen (Listing 3). Die Try-Catch-Blöcke sind erforderlich, weil so gut wie alle Android Things APIs eine Exception werfen können.

@Override
protected void onDestroy() {
  super.onDestroy();
  if (mySensor != null) {
    try {
      mySensor.close();
      mySensor = null;
    } catch (IOException e) {

Android Things: Interaktion mit Hardware

Das Erzeugen eines Deviceobjekts ist nur die halbe Miete. Wirklich interessant ist die Situation erst, wenn die in unserem Sensor vorliegenden Informationen in Richtung des Raspberry Pi wandern.

Google implementiert in Android Things einen vollwertigen I2C-Stack. Daraus folgt, dass er sowohl SMBus-Transaktionen als auch gewöhnliche serielle Transaktionen durchführen kann. Im Datenblatt des Chips finden Sie auf S. 6 die in Abbildung 6 gezeigte Wellenform.

Abb. 6: Dieses Diagramm liefert Informationen über die Busschnittstelle

Abb. 6: Dieses Diagramm liefert Informationen über die Busschnittstelle

Das Aufscheinen eines Repeated-Start-Ereignisses informiert uns darüber, dass unser Chip auf dem Prinzip des SMBus arbeitet. Wir können die fortgeschrittenen Routinen benutzen und müssen uns nicht mit dem direkten Senden von Bytepaketen auseinandersetzen.

Im nächsten Schritt ist es – wie das auch bei jeder anderen unbekannten Hardware der Fall ist – empfehlenswert, eine Beispielimplementierung zu suchen. Im Fall des HDC2010 öffnen wir hierzu den unter [4] zu findenden URL und suchen dann nach dem String snac075, um ein Beispiel für den Arduino herunterzuladen. Hardwarehersteller liefern bei der Implementierung von Beispieltreibern gerne extrem komplizierten Code ab.

Als Erstes müssen wir die diversen numerischen Konstanten übernehmen, die in der .cpp– und der der .h-Datei vorliegen. Wie Sie die Defines in eine für Java verständliche Form umwandeln, bleibt Ihrer Fantasie überlassen – der Autor dieser Zeilen arbeitet gerne mit Member-Variablen. Im nächsten Akt suchen wir nach dem Einsprungpunkt, der zum Start des Arduino-Sketches dient. Im Fall des HDC2010 handelt es sich dabei um Reset, in C++ aufgebaut wie in Listing 4 dargestellt.

void HDC2010::reset(void) {
  uint8_t configContents;
  configContents = readReg(CONFIG);
  configContents = (configContents | 0x80);
  writeReg(CONFIG, configContents);
  delay(50);
}

Wir sehen, dass TI die Inhalte des Konfigurationsregisters im ersten Schritt als Ganzes einliest, um danach das Resetbit zu setzen und das Resultat in Richtung des Sensors zurückzuschreiben.

Da der Arduino von Haus aus vom SMBus nichts weiß, implementiert TI mit den Funktionen readReg und writeReg ein Analogon. Im Fall unseres Raspberry Pi setzen wir stattdessen auf die in Listing 5 dargestellte, einfachere Routine.

private void reset() {
  try {
    int myByte = mySensor.readRegByte(CONFIG);
    myByte = (myByte | 0x80);
    mySensor.writeRegByte(CONFIG, (byte)myByte);
    Thread.sleep(50);
  }catch (Exception e) {
    Log.w("SUS", "I2C-Resetfehler!", e);
  }
}

Die Java-Version der Resetfunktion ist ob des Vorhandenseins dedizierter SMBus-Interaktionsbefehle wesentlich kompakter. Zum Testen des Programms bietet sich das Auslesen der ID-Register an, die von Texas Instruments mit bekannten Werten bevölkert werden (Listing 6).

protected void onCreate(Bundle savedInstanceState) { . . .
  try {
    mySensor = manager.openI2CDevice("I2C1", 64);
    reset();
    byte lower = mySensor.readRegByte(DEVICE_ID_L);
    byte higher = mySensor.readRegByte(DEVICE_ID_H);
    higher=higher;

Wer auf der tautologischen Anweisung einen Breakpoint setzt, sieht das in Abbildung 7 gezeigte Verhalten. Dieses verwirrende Aussehen ist dadurch bedingt, dass die per I2C zurückgelieferten Bytes aus Sicht von Texas Instruments vorzeichenlos sind – Java kennt keinen unsigned Bytetyp. Hier ist das kein Problem, weil wir -48 als korrekten Wert annehmen und uns am Funktionieren der Kommunikation erfreuen.

Abb. 7: Ein Teil der Chip-IC ist negativ

Abb. 7: Ein Teil der Chip-IC ist negativ

Genauere Parametrierung

Mit Bus-Interfaces ausgestattete Sensoren bringen meist eine Vielzahl von Konfigurationseinstellungen mit. So ist es beispielsweise so gut wie immer möglich, nicht benötigte Teile abzuschalten – im Fall unseres HDC2010 können wir nur Temperatur- oder nur Feuchtigkeitswerte anfordern. Zudem dürfen wir die Genauigkeit beeinflussen; das erlaubt die Abstimmung zwischen höherer Messgenauigkeit und damit einhergehendem höheren Rechenaufwand einerseits, und geringerer Genauigkeit mit kürzerer Zykluszeit und weniger Rechenaufwand andererseits. Was auf unserem Raspberry Pi lustig klingt, kann in der Praxis ein echtes Problem sein – denken Sie daran, dass der Sensor in der Praxis oft mit einem 8-Bit-Mikrocontroller betrieben wird.

Der Autor kombiniert die beiden Methoden setTempRes und setMeasurementMode im Sketch in eine gemeinsame Schreibmethode, die sich wie in Listing 7 dargestellt präsentiert.

try {
  mySensor = manager.openI2CDevice("I2C1", 64);
  reset();
  //sensor.setMeasurementMode(TEMP_ONLY);
  int mySensorVal = mySensor.readRegByte(MEASUREMENT_CONFIG);
  mySensorVal = (mySensorVal & 0xFC);
  mySensorVal = (mySensorVal | 0x02);
  mySensorVal = (mySensorVal & 0x3F);
  mySensor.writeRegByte(MEASUREMENT_CONFIG, (byte)mySensorVal);

Auch diese Schreibweise liest im ersten Schritt den Wert des betroffenen Registers, um ihn danach durch eine Gruppe von Bitoperationen auf den gewünschten Wert zu setzen. Dieser wandert zurück in den Sensor, womit die Parametrierung abgeschlossen ist.

Eine Frage der Auslösung

Sensoren haben die Eigenschaft, ihre Werte im Interesse des geringeren Energieverbrauchs nur dann zu aktualisieren, wenn der Entwickler eine diesbezügliche Willensäußerung tätigt. Im Fall unseres HDC2010 müssen wir ein charakteristisches Byte schreiben, danach 50 ms warten und die Werte abernten. Da wir die Auswertung permanent erledigen möchten, die Funktion onCreate aber am GUI-Thread abläuft, ist die im Arduino-Beispiel verwendete Endlosschleife keine gangbare Lösung.

Wir wollen in den folgenden Schritten auf einen Handler setzen, dem regelmäßig eine Payload eingeschrieben wird. Die erste Amtshandlung besteht darin, den Handler als globales Mitglied der MainActivity anzulegen. Achten Sie darauf, eine Klasse aus dem Paket android.os zu importieren – Android Studio bietet Ihnen oft auch den Import einer im Java-Paket liegenden Klasse an, die die von uns geforderten Aufgaben naturgemäß nicht erledigen kann:

public class MainActivity extends Activity {
  private Handler handler;
  TextView myView;

Im nächsten Schritt benötigen wir ein Runnable, das für die Auswertung der Daten verantwortlich ist. Es entsteht ebenfalls als Member von MainActivity und beginnt mit dem Einlesen des Werts des Registers MEASUREMENT_CONFIG. In ihm wird ein Bit gesetzt, das den Sensor zur Durchführung einer Konversion animiert. Zu guter Letzt schreiben wir das Resultat noch in Richtung des Sensors (Listing 8).

private Runnable runnable = new Runnable(){
  public void run() {
    try {
      int configContents = mySensor.readRegByte(MEASUREMENT_CONFIG);
      configContents = (configContents | 0x01);
      mySensor.writeRegByte(MEASUREMENT_CONFIG, (byte)configContents);
      Thread.sleep(950);

Im nächsten Akt müssen wir ein wenig warten, um dem Sensor genügend Zeit zur Bereitstellung der Informationen zu gewähren. Diese werden gemäß einer im Datenblatt genannten Formel zusammengefasst, um einen Float-Wert zu erhalten. Dieser wandert in Richtung einer Textbox (Listing 9).

      int higher=java.lang.Byte.toUnsignedInt(mySensor.readRegByte(TEMP_HIGH));
      int lower=java.lang.Byte.toUnsignedInt(mySensor.readRegByte(TEMP_LOW));
      float tempF;
      int temp = higher << 8 | lower;
      tempF = (float)(temp) * 165 / 65536 - 40;
      tempF = tempF;
      Float c = tempF;
      myView.setText(c.toString() +  " °C");
      handler.postDelayed(runnable, 1000);
    }catch (Exception e) {
      Log.w("SUS", "Runnable-Fehler!", e);
    }
  }
};

Interessant ist, dass wir die zurückgelieferten Werte über die Funktion java.lang.Byte.toUnsignedInt konvertieren. Das ist erforderlich, weil die von Texas Instruments im Datenblatt vorgegebene Formel davon ausgeht, dass die angelieferten Werte vorzeichenlos sind. Zur Abfeuerung der neuen Routine müssen wir sie in onCreate anmelden:

@Override
protected void onCreate(Bundle savedInstanceState) { . . .
  try {
    . . .
    handler.postDelayed(runnable, 1000);

postDelayed befiehlt dem Handler die einmalige Ausführung der Payload, nachdem die Totzeit verstrichen ist. Da sich die Payload immer wieder anmeldet, ist die permanente Abarbeitung der Temperaturleseroutine sichergestellt. An dieser Stelle können Sie das Programm in Richtung des Prozessrechners jagen. Aufgrund einer Besonderheit des Sensors liefert der erste Durchlauf -39°, danach sehen Sie – mehr oder weniger – die Umgebungstemperatur. Interessant ist an dieser Situation, dass sich der gemessene Wert langsam, aber sicher um einige Zehntel Grad erhöht. Das ist ein bei Temperatursensoren häufig zu beobachtendes Verhalten – sowohl die Messwertwandlung als auch die Kommunikation setzen Energie voraus, die am Chip in Wärme umgewandelt wird und das Substrat dementsprechend aufheizt.

Android Things – Einführung: Zwischenfazit

Die Verwendung des I2C-Busses ermöglicht uns, Android-Things-Hardware bequem mit Sensoren auszustatten – das Auslesen eines analogen Temperatursensors hätte wesentlich mehr Arbeit bedeutet. Leider sind wir an dieser Stelle noch nicht am Ziel. Die Ausgabe der Werte erfolgt im Labor des Autors über einen Studiomonitor, der wesentlich mehr kostet als der ganze Raspberry Pi. Im nächsten Artikel zu diesem Thema werden wir uns Displays im Kleinformat zuwenden, um mehr über den SPI-Bus zu erfahren. Bis dahin wünschen wir Ihnen viel Freude.


Unsere Redaktion empfiehlt:

Relevante Beiträge

Abonnieren
Benachrichtige mich bei
guest
0 Comments
Inline Feedbacks
View all comments
X
- Gib Deinen Standort ein -
- or -