Linux Embedded

Le blog des technologies libres et embarquées

GPIO sur AOSP

Introduction

Nous avons déjà évoqué l'architecture d'AOSP et les modifications possibles lors de précédents articles. Android présente de nombreux avantages, à commencer par la relative facilité de développer une application en Java. Un inconvénient d'AOSP est qu'il ne supporte pas les interfaces tels que I2C, GPIO, SPI. Nous allons voir dans cet article comment il est possible d'utiliser un port GPIO sous Android.

Pour utiliser des appels bas niveau en C sur Android, il est possible d'utiliser le NDK ou bien de modifier directement le framework. Cette seconde approche implique de compiler sa propre ROM. Linaro, qui travaille sur le projet de téléphone modulaire ARA avec l'aide de Google, à déjà travaillé sur le sujet. Les développeurs d'applications pour ARA doivent en effet pouvoir intéragir avec les différents modules qui constituent le téléphone via GPIO et I2C.

Les sources d'Android 5.1 pour ARA sont disponibles sur le dépot git suivant : https://git-ara-mdk.linaro.org. Nous allons passer en revue les modifications importantes effectuées dans le framework. Les manipulations seront réalisées sur une Beaglebone Black sous Android 6.0. Elles peuvent être adaptées pour toute carte de développement fonctionnant sur Android. L'objectif n'est pas d'explorer en détail le code source. Nous allons nous concentrer uniquement sur les points importants. Le but est de démontrer qu'ajouter le support d'interfaces matérielles à Android est tout à fait réalisable.

Configuration sous Linux

Avant toute chose, le noyau doit être configuré pour supporter les GPIO et le système de fichiers virtuel SYSFS. On active donc CONFIG_SYSFS et CONFIG_GPIO_SYSFS dans la configuration du kernel android. De cette manière, les ports GPIO seront contrôlables depuis les fichiers contenus dans le répertoire "/sys/class/gpio/".

Pour nos tests, nous allons utiliser la pin 12 (GPIO_60) d'une beaglebone black. Celle-ci est reliée à la "USER_LED" d'un écran 4DCAPE-43T. Nous avons désactivé la LED dans le "Device Tree" (fichier .dts) afin de la contrôler nous-même.

Nous pouvons déjà nous assurer que l'on est en mesure de contrôler le GPIO manuellement:
echo 60 > /sys/class/gpio/export
echo out >/sys/class/gpio/gpio60/direction
echo 1 > /sys/class/gpio/gpio60/value #la led s'allume!

Modifications dans AOSP

Nous allons commencer du plus bas niveau (service écrit en C) vers le plus haut niveau (le Manager). On commence avec l'implémentation des fonctions natives en C. Le but est d'ouvrir le fichier "/sys/class/gpio/gpio60/value" et de renvoyer un ParcelFileDescriptor, afin d'écrire dans le fichier depuis l'application en Java.
frameworks/base/services/core/jni/com_android_server_GpioService.cpp
static jobject android_GpioService_openDevice(JNIEnv *env, jobject thiz,
    jint gpio, jstring direction,jstring pid,jstring vid) {
 
    // exporter le gpio si besoin
    // Ecrire dans direction soit in soit out
    [...]
    char buf[BUFFER_SIZE];
 
    memset(buf,'',sizeof(buf));
    sprintf(buf, "/sys/class/gpio/gpio%d/value", gpio);
    //check permissions
 
    fd = open(buf, O_WRONLY);
    if(fd < 0){
        ALOGE("%s", "Error opening value file in write mode");
        return NULL;
    }
 
    //creating one duplicate file pointer
    int newFD = dup(fd);
    close(fd);
 
    // on va retourner un ParcelFileDescriptor à l'application Java
    // pour qu'elle manipule le fichier value.
    jobject fileDescriptor = jniCreateFileDescriptor(env, newFD);
    if (fileDescriptor == NULL) {
        ALOGE("%s fileDescriptor ",fileDescriptor);
        return NULL;
    }
    jclass clazz = env->FindClass("android/os/ParcelFileDescriptor");
    LOG_FATAL_IF(clazz == NULL, "Unable to find class android.os.ParcelFileDescriptor");
    gParcelFileDescriptorOffsets.mClass = (jclass) env->NewGlobalRef(clazz);
    gParcelFileDescriptorOffsets.mConstructor = env->GetMethodID(clazz, "<init>",
                                                 "(Ljava/io/FileDescriptor;)V");
    LOG_FATAL_IF(gParcelFileDescriptorOffsets.mConstructor == NULL,
                 "Unable to find constructor for android.os.ParcelFileDescriptor");
 
    return env->NewObject(gParcelFileDescriptorOffsets.mClass,
    gParcelFileDescriptorOffsets.mConstructor, fileDescriptor);
}
Désormais, nous pouvons coder le service en Java qui sera utilisé par le Manager que l'on écrira plus tard.
Ce service fait appel aux fonctions natives précédemment écrites en C, grâce à JNI (Java Native Interface).
# frameworks/base/services/core/java/com/android/server/GpioService.java
public class GpioService extends IGpioManager.Stub {
 
    public GpioService(Context context) {
        super();
        mContext = context;
        mPackageManager = context.getPackageManager();
        Log.i(TAG, "GpioManager Service Started");
    }
 
    /* Ouvre le GPIO et retourne un ParcelFileDescriptor à l'application android */
    @Override
    public ParcelFileDescriptor openGpio(int gpio ,String direction,String pkgName) {
 
        Log.i(TAG,"OpenGPIO, Package Name: " + pkgName);
        GpioDeviceFilter filter = getPkgInfo(pkgName);
        if (filter != null) {
            return open_gpiodevice(gpio ,direction,filter.mProductID,filter.mVendorID);
        }
        Log.e(TAG, "Product name not found in package!!" + pkgName);
        return null;
    }
 
    /* Close the specified gpio device */
    @Override
    public void closeGpio(int gpio ) {
        Log.i(TAG, "GpioManager Service CLOSE GPIO");
        close_gpiodevice(gpio );
    }
 
    private native ParcelFileDescriptor open_gpiodevice(int gpio ,String direction,String pid,String vid);
    private native void close_gpiodevice(int gpio );
 
    [...]
 
    static JNINativeMethod sMethods[] = {
        /* name, signature, funcPtr */
        { "open_gpiodevice", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;",
        (void*)android_GpioService_openDevice },
        { "close_gpiodevice", "(I)V",(void*)android_GpioService_closeDevice },
    };
 
    int register_android_server_GpioService(JNIEnv* env) {
        return jniRegisterNativeMethods(env, "com/android/server/GpioService",
                                        sMethods, NELEM(sMethods));
    }
}
Continuons en codant l'interface du Manager en AIDL (Android Interface Description Language).
Les méthodes déclarées ici seront exposées au développeur d'application.
 # frameworks/base/core/java/android/hardware/gpio/IGpioManager.aidl
interface IGpioManager {
    /* retourne un descriptor pour ecrire/lire des données */
    ParcelFileDescriptor openGpio(int gpio ,String direction,String packageName);
    void closeGpio(int gpio);
}
Ecrivons le Manager correspondant. Il s'agit cette fois-ci de l'implémentation des méthodes de GpioManager, qui seront utilisées par les développeurs d'applications.
Cette implémentation fait appel aux méthodes du service GpioService.
# frameworks/base/core/java/android/hardware/gpio/GpioManager.java
public final class GpioManager {
    [...]
    private final IGpioManager mGpio;
    private final Context mContext;
 
    /* Ouvre le GPIO et retourne un ParcelFileDescriptor à l'application android */
    public ParcelFileDescriptor openGpio(int gpio , String direction) {
        try {
            return mGpio.openGpio(gpio,direction,mContext.getPackageName());
        } catch (RemoteException e) {
            return null;
        }
    }
 
    public void closeGpio(int gpio) {
        try {
            mGpio.closeGpio(gpio);
        } catch (RemoteException e) {
            Slog.e("GpioManager", "Unable to contact the remote Gpio Service");
        }
    }
 
    public GpioManager(Context context,IGpioManager service) {
        mContext = context;
        mGpio = service;
    }
}

Ajout du service au démarrage

Pour rappel, Android démarre de nombreux services au démarrage. Nous pouvons lister les services fonctionnant actuellement avec la commande "service list".
Pour ajouter le service, nous déclarons le nom de notre service :
frameworks/base/core/java/android/content/Context.java
public static final String GPIO_SERVICE = "gpio";
Nous enregistrons notre service pour qu'il soit démarré :
# frameworks/base/core/java/android/app/SystemServiceRegistry.java
registerService(Context.GPIO_SERVICE, GpioManager.class, new CachedServiceFetcher<GpioManager>() {
    @Override
    public GpioManager createService(ContextImpl ctx) {
        IBinder b = ServiceManager.getService(Context.GPIO_SERVICE);
        return new GpioManager(ctx, IGpioManager.Stub.asInterface(b));
    }});
}
Pour lancer notre service au démarrage, nous devons modifier le code du processus system_server.
frameworks/base/services/java/com/android/server/SystemServer.java
Slog.i(TAG, "GPIO Service");
ServiceManager.addService(Context.GPIO_SERVICE, new GpioService(context));
Enfin, on appelle register_android_server_GpioService() depuis onload.cpp.
frameworks/base/services/core/jni/onload.cpp
int register_android_server_GpioService(JNIEnv* env);
[...]
register_android_server_GpioService(env);

Compilation image et SDK

Maintenant que nous avons modifié le framework d'Android, recompilons une image système pour notre cible. (voir http://www.linuxembedded.fr/2014/02/introduction-a-aosp-2/)

cd ~/aosp #répertoire des sources d'AOSP
source build/envsetup
lunch #sélection de la cible
make
On "flashe" alors la partition système avec le fichier image présent dans ~/aosp/out/target/<board>/system.img. Après redémarrage, nous devons remarquer la présence de notre nouveau service lorsque l'on éxécute "service list | grep gpio". Pour créer notre application de test, nous devons générer un nouveau SDK:
make update-api #MAJ de l'api
lunch sdk-eng
make [-j N] sdk #choisir N en fonction du nombre de threads du processeur
A la fin de la (longue) compilation, le sdk sera disponible dans "out/host/linux-x86/sdk/sdk/". Il faudra configurer le chemin du nouveau SDK dans Android Studio afin de développer des applications.

Application

Il est temps de coder notre petite application de démonstration. L'idée est d'avoir un unique bouton qui active/désactive notre LED.
On trouvera dans la méthode onStart():
gpioManager = (GpioManager) getSystemService(GPIO_SERVICE);
mPfd = gpioManager.openGpio(GPIO, "out");
fd = mPfd.getFileDescriptor();
fos = new FileOutputStream(fd);
et notre méthode toggleLED(), qui sera appelée lors du clic sur le bouton :
public void toggleLED(View view) {
    private static final String ON = "1";
    private static final String OFF = "0";
    Log.d(TAG, "toggleLED");
    try {
        if(toggled) {
            fos.write(OFF.getBytes());
            toggled = false;
            button.setText("Enable LED");
        }
        else {
            fos.write(ON.getBytes());
            toggled = true;
            button.setText("Disable LED");
        }
        fos.flush();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
Testons ensuite cette application via ADB. Nous devrions pouvoir contrôler la LED!

Conclusion

Cet article démontre qu'il est possible d'ajouter le support d'interfaces tels que les GPIO au framework d'Android. Nous avons utilisé le sysfs de linux afin de contrôler un GPIO depuis un service Android. On expose au développeur Android une classe GpioManager grace à laquelle il pourra contrôler un GPIO dans son application. Cette méthode implique de recompiler le SDK, mais il est possible de distribuer un simple add-on qui s'ajoutera au SDK officiel Android.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.