Java am Microcontroller

Viele Entwickler erzittern vor dem Gedanken, Java in einem Embedded-System einzusetzen. Die Sprache sei angesichts der Ausführung in einer virtuellen Maschine langsam, aufgrund des Garbage Collector nur leidlich echtzeittauglich und für Steuerungsaufgaben sowieso ungeeignet. Wie so oft gilt auch hier, dass Schein und Sein weit auseinanderliegen.

Know-how  –  6 Kommentare
Java am Microcontroller
Anzeige

Im Laufe der letzten Jahre wurde der Markt mit verschiedenen Möglichkeiten konfrontiert, um Java auf Embedded-Hardware loszulassen. Die Palette der abgedeckten Systeme reicht vom 8- bis zum 32-Bitter. Angesichts der sinkenden Preise für Prozessrechner kann es zudem verlockend sein, das Problem beispielsweise mit einem OrangePi zu erschlagen: Die dahinterstehende Hoffnung ist, dass die immense Rechenleistung die Probleme "kaschiert".

Dieser Artikel möchte einige Systeme kurz vorstellen und ihre individuellen Stärken und Schwächen auch aus Hardwaresicht betrachten. Eines der wichtigsten Kriterien für die Auswahl war die Verfügbarkeit der als Basis dienenden Hostplattformen: Alle besprochenen Systeme sind entweder quelloffen oder am Markt auch in kleinen Stückzahlen erhältlich; die verwendeten Prozessoren sind weder exotisch noch besonders teuer.

Zum besseren Verständnis der folgenden Ausführungen ein paar Worte zu PWM-Wellenformen (Pulsweitenmodulation), die beispielsweise zur Ansteuerung von Motoren oder Leuchtmitteln Verwendung finden. Sowohl bei der PWM-Erzeugung als auch bei komplexeren Protokollen gibt es zwei Verfahren: zum einen die Implementierung in Hardware, zum anderen die Realisierung durch regelmäßiges Abfragen eines GPIO-Pins. Zweiteres stellt auch bei vergleichsweise langsamen Protokollen immense Ansprüche an die Hardware – mit Java ist Bit-Banging nicht oder nur schwer realisierbar.

Aufgaben, die sowohl harte Echtzeit als auch aufwendige Visualisierung verlangen, realisiert man gerne über das Konzept des kombinatorischen Prozessrechners. Das vom Arduino-Team mit dem Yun (s. Abb. 1) erstmals populär gemachte Verfahren lässt sich auch mit diversen anderen Prozessoren realisieren: Es ist lediglich darauf zu achten, dass SPI (Serial Peripheral Interface) oder I2C (Inter-Integrated Circuit) auf der Java-Seite in Form einer Hardware-Engine zur Verfügung stehen.

Der Arduino Yun verschaffte dem Paradigma des kombinatorischen Prozessrechners erstmals größere Aufmerksamkeit (Abb.1) (Bild: Arduino)

Der Nachteil kombinatorischer Prozessrechner sind die vergleichsweise hohen Hardwarekosten. Vierkernprozessoren verleiten Entwickler zum Versuch, die Probleme zu "durchtauchen" – ein Beispiel dafür findet sich beim RaspBerry Pi 3.

Java kann seit jeher native Funktionen aufrufen. Das zum Aktivieren betriebssystemspezifischer APIs vorgesehene Feature lässt sich ebenso zur Hardwaresteuerung verwenden. Im Folgenden kommt eine aktuelle Version von Debian "Jessie" zum Einsatz. Dafür müssen Nutzer die .img-Datei auf eine ausreichend große Speicherkarte ziehen – für die folgenden Experimente sind 32 GByte erforderlich – und das Betriebssystem danach starten.

Eclipse lässt sich komfortabel über apt-get installieren, eine Debian-Benutzerschnittstelle zur Verwaltung von Paketen. Dabei ist allerdings darauf zu achten, vor der eigentlichen Installation von Eclipse ein Update der Paketquellen durchzuführen: Das Raspberry-Pi-Team liefert auch aktuelle Versionen von Debian mit einer veralteten Paketliste aus:

pi@raspberrypi:~ $ sudo apt-get update 
pi@raspberrypi:~ $ sudo apt-get install eclipse

Dass Debian-Jessie-Image bringt sowohl GCC als auch eine Version der WiringPi-Bibliothek für das Schalten der GPIO-Ein-und -Ausgänge mit.

Im nächsten Schritt wird – zum Vergleich der Performance – ein kleines Programm erzeugt, das eine charakteristische Wellenform auf einem GPIO-Pin ausgibt. Im Fall der Programmierung mit C als alleinstehendes Programm sieht der Code dafür wie im folgenden Beispiel aus:

#include <wiringPi.h> 

int main()
{
wiringPiSetup();
pinMode(0,OUTPUT);
while(1==1)
{
digitalWrite(0, 1);
digitalWrite(0, 0);
digitalWrite(0, 1);
digitalWrite(0, 0);
}
}

Überraschendes findet sich dort nicht: Man setzt den GPIO-Pin als Ausgang und gibt sodann eine Wellenform aus, die aus zwei High- und zwei Low-Passagen besteht. Das Abschätzen der unterschiedlichen Länge der beiden Wellentäler ermöglicht Rückschlüsse über die Laufzeit der Umgebungslogik – zum Verständnis der Situation wird die folgende Abbildung herangezogen, die Teile der Wellenform den abzuarbeitenden Codestücken gegenüberstellt. Das Programm lässt sich problemlos kompilieren und auf dem Raspberry Pi 3 ausführen. Auf den ersten Blick interessant ist das Aussehen der generierten Wellenform, das sich zur Veranschaulichung mit einem Oszillographen oder einem Logikanalysator visualisieren lässt: Das Oszillogramm in Abbildung 2 zeigt, dass die Abarbeitung der while-Schleife nicht sonderlich viel Zeit in Anspruch nimmt.

Die beiden Wellentäler sind bei Abfrage durch ein C-Programm fast symmetrisch (Abb. 2).

Das Echtzeitverhalten von Debian Jessie ist alles andere als berühmt: Abbildung 3 zeigt das Histogramm, das das in C gehaltene Programm auf einem Modulationsdomänenanalysator erzeugt.

Ein C-Programm würde auf einem besser echtzeitgeeigneten Unix schönere Resultate liefern (Abb. 3).

Bei der Arbeit mit JNI ist es sinnvoll, zuerst den Java-Teil des Programms zu erzeugen. Der Start von Eclipse nimmt einige Zeit in Anspruch: Auf dem Raspberry Pi 3 dauerte es eineinhalb Minuten, bevor der Splashscreen der IDE zum ersten Mal am Bildschirm erschien. Nachfolgende Aufrufe erfolgen schneller, weil die Plug-ins beim erneuten Start indiziert sind.

Das soeben angelegte C-Beispiel soll nun in zwei Methoden aufgesplittet werden: erstens eine Initialisierungsmethode namens init; zweitens die Wellenformlogik in einer Methode namens run, die Java in einer Schleife permanent aufruft. Dafür muss man eine neue Datei namens GPIOWorker.java erzeugen und sie mit dem folgenden Code ausstatten:

public class GPIOWorker { 
static {
System.loadLibrary("HeiseGPIO");

}

private native void init();
private native void run();

public static void main(String[] args){ }
}

Der static-Teil der Klasse nutzt die Methode System.loadLibrary, die die Runtime zum Laden einer nativen Bibliothek anweist. Die beiden darunter folgenden Deklarationen nativer Methoden melden als geladen anzunehmende Funktionen an.

Der nächste Schritt nutzt diese Struktur zum Erzeugen einer C-Header-Datei. Das Kompilieren der Datei mit Eclipse läuft auch dann durch, wenn die Bibliothek nicht auffindbar ist. Das scheinbar widersinnige Verhalten des javac liegt darin begründet, dass das Suchen nativer Routinen in JNI eine Laufzeitangelegenheit ist. Alternativ dazu können Entwickler in das Arbeitsverzeichnis wechseln und das javac-Kommando verwenden: Das Terminalfenster benötigen sie ohnehin. Danach führen sie das javah-Kommando nach folgendem Schema aus:

pi@raspberrypi:~/workspace/HeiseGPIO/bin $ ls 
GPIOWorker.class
pi@raspberrypi:~/workspace/HeiseGPIO/bin $ javah -jni GPIOWorker

Interessanterweise erwartet das Werkzeug einen Klassennamen: Wer einen Dateinamen übergibt, wird mit einer IllegalArgumentException abgestraft.

Die miserable Reputation von JNI ist unter anderem auf die seltsamen Namensschemata zurückzuführen: Wenn es eine Methode gibt, um in einem Methodennamen eine Vielzahl von Unterstrichen unterzubringen, ist sie den JNI-Entwicklern mit Sicherheit bekannt. Die von javah erzeugte Datei präsentiert sich wie folgt:

/* DO NOT EDIT THIS FILE - it is machine generated */ 
#include <jni.h>
/* Header for class GPIOWorker */

#ifndef _Included_GPIOWorker
#define _Included_GPIOWorker
#ifdef __cplusplus
extern "C" {
#endif

/*
* Class: GPIOWorker
* Method: init
* Signature: ()V
*/

JNIEXPORT void JNICALL Java_GPIOWorker_init
(JNIEnv *, jobject);

/*
* Class: GPIOWorker
* Method: run
* Signature: ()V
*/

JNIEXPORT void JNICALL Java_GPIOWorker_run
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Die nächste Aufgabe besteht darin, den im vorigen Schritt erzeugten C-Code in diese vom Header vorgegebene Form zu "pressen". Das führt zu folgendem Ergebnis:

#include <jni.h> 
#include <wiringPi.h>
#include "GPIOWorker.h"

JNIEXPORT void JNICALL Java_GPIOWorker_init (JNIEnv * a, jobject b)
{
wiringPiSetup();
pinMode(0,OUTPUT);
}

JNIEXPORT void JNICALL Java_GPIOWorker_run (JNIEnv * a, jobject b)
{
digitalWrite(0, 1);
digitalWrite(0, 0);
digitalWrite(0, 1);
digitalWrite(0, 0);
}

Da das Eclipse-Paket die Variable JAVA_HOME nicht setzt, ist für das Kompilieren folgendes, etwas längeres Kommando erforderlich:

pi@raspberrypi:~/workspace/HeiseGPIO/bin $ gcc 
-fPIC -o libHeiseGPIO.so -shared
-I/usr/lib/jvm/java-1.7.0-openjdk-armhf/include
-I/usr/lib/jvm/java-1.7.0-openjdk-armhf/linux
-lwiringPi GPIOWorker.c

Der Aufruf weist GCC dazu an, eine Shared Library zu erzeugen und bei der Kompilierung zwei Pfade aus dem SDK zu berücksichtigen. Diese enthalten für JNI relevante Inhalte. Anschließend liegt die Datei libHeiseGPIO.so vor und lässt sich in das Java-Programm einbinden. Im nächsten Schritt muss man die main-Methode des Testharnisches anpassen, um die Wellenformgenerierung zu realisieren:

public static void main(String[] args){ 
GPIOWorker slave=new GPIOWorker();
slave.init();
while(1==1)
{
slave.run();
}
}

Da die C++-Methoden nicht als statisch deklariert wurden, ist zu ihrem Aufruf eine Instanz der GPIOWorker-Klasse erforderlich, die sich ohne großen Aufwand anlegen lässt. Wer in Eclipse auf den Run-Befehl klickt, wird mit der in Abbildung 4 gezeigten Fehlermeldung konfrontiert.

Java kann die Bibliothek zur Laufzeit nicht finden (Abb. 4).

Die Suche nach zu ladenden Bibliotheken erfolgt unter unixoiden Betriebssystemen über eine Umgebungsvariable. Da das Programm aus Eclipse heraus ausgeführt werden soll, muss man die Umgebungsvariable über die Ausführungskonfiguration festlegen. Zum Finden der relevanten Option öffnen Entwickler die Rubrik Java Build Path und expandieren den Knoten HeiseGPIO/src. Darunter findet sich die Option "Native library location", in der man den relevanten Pfad einpflegt. Da WiringPi Superuser-Rechte voraussetzt, müssen Entwickler Eclipse mit dem sudo-Befehl starten. Aktivieren sie anschließend das Programm und verbinden sie den betreffenden Pin mit dem Digitalspeicheroszillographen, ergeben sich zwei interessante Aspekte: Abbildung 5 zeigt das "kurze" Tal, während Abbildung 6 das längere Wellental zeigt.

Zum Ermitteln der für die einzelnen Teile des Programms erforderlichen Rechenzeit bietet sich die Nutzung von Cursoren an: Der hier verwendete LeCroy 9354AM blendet sie auf Wunsch automatisch ein.

Der im nativen Code ablaufende Wechsel zwischen High und Low erfolgt mit der von C bekannten Geschwindigkeit ... (Abb. 5)
... während der eigentliche Aufruf der JNI-Methode einiges an zusätzlicher Zeit in Anspruch nimmt (Abb. 6).

Für eine effiziente Bewertung der Wellenformstabilität mit dem Modulationsdomänenanalysator ist die soeben generierte Wellenform insofern ungünstig, als sie aus zwei Wellen mit stark unterschiedlicher Frequenz besteht. Zur Lösung des Problems bietet es sich an, die Methode run nach folgendem Schema zu vereinfachen:

JNIEXPORT void JNICALL Java_GPIOWorker_run (JNIEnv * a, jobject b) { 
digitalWrite(0, 1);
digitalWrite(0, 0);
}

Das Resultat des zweiten Durchlaufs ist in Abbildung 7 gezeigt.

JNI wirkt sich negativ auf die Wellenformstabilität aus (Abb. 7).
Anzeige