Club robotique de Sophia-Antipolis

Accueil > POBOTpedia > Lego Mindstorms > Découverte des Lego Mindstorms EV3 > Java, Python... et le C ?

Java, Python... et le C ?

lundi 11 novembre 2013, par Eric P.

Dans un précédent article nous avons vu comment programmer l’EV3 en Java grâce à l’excellent environnement leJOS. Il a même été question de Python, mais celui-ci n’en est pour le moment qu’au stade de Proof Of Concept (POC) et il n’y a pour le moment aucune bibliothèque permettant de s’interfacer facilement avec la brique et ses accessoires.

Cela est fort bien, mais nous avons pu constater dans les deux cas de figure que le démarrage du programme est assez long, ce qui en dit beaucoup sur les ressources mises en oeuvre. C’est marrant d’ailleurs, car il faudra que je chronomètre le NXT avec sa version de leJOS car je n’ai pas souvenir de temps aussi importants. Bon, ma mémoire me joue peut-être des tours.

Tout cela pour dire que si nous pouvions développer nos programmes en pur C/C++ compilé en natif pour le processeur ARM, nous devrions y gagner pas mal, sur le démarrage d’une part, mais plus important sur les performances d’exécution ensuite. Or nous sommes maintenant en présence d’un environnement Linux en bonne et due forme [1]. Et cerise sur le gâteau, les sources complets de tout cela, y compris du système de base sont librement disponibles pour les plus aventureux (ou plus prosaïquement pour rechercher une information disponible nulle part).

On se lance

Pour faire du C/C++ pour l’EV3, nous avons besoin... d’un cross-compilateur C/C++ pour ARM. Ca tombe bien, il y a un qui nous est suggéré sur la page même du repository des sources de LMS [2] : CodeSourcery Lite. La version référencée est la 2009q1-203. Pas très récent si on en juge par l’année incluse dans l’intitulé de la version, mais si c’est celle qui est indiquée par les auteurs du firmware de l’EV3, pas la peine de chercher les ennuis.

Vous la récupérez via le lien inclus dans le texte de la page, sous forme d’un exécutable de 129M, qui est en fait un installeur InstallAnyWhere écrit en Java pouvant de ce fait de tourner sur n’importe quelle cible.

Rien de spécial à dire du côté de l’installation, en tout cas sur ma config Ubuntu 12.04.

Premier programme en C

Je vous passe le "Hello world" qui s’écrit comme d’hab et s’exécute sans histoire.

Faisons donc quelque chose de plus intéressant, et surtout plus en rapport avec le contexte. Comme souvent dans notre domaine, le premier programme consiste à faire clignoter des LEDs. Ca tombe bien, la brique en a des bicolores, qu’on peut d’ailleurs faire clignoter selon plusieurs patterns. Le cahier des charges est donc le suivant :

  • Au démarrage, on éteint les LEDs.
  • L’appui sur le bouton Up (1) fait passer au pattern suivant.
  • L’appui sur le bouton Down (3) fait passer au pattern précédent.
  • On cycle sur les 10 patterns disponibles (détaillés plus loin) en cas de "débordement".
  • La sortie du programme est obtenue via le bouton Escape (6).

Les numéros de bouton n’ont pas été choisis au hasard et renvoient à l’illustration ci-dessous :

L’interfaçage avec la brique

L’interface avec le hardware suit la philosophie Unix et se base sur des pseudo-fichiers gérés par le noyau et ses modules, en l’occurrence la série des /dev/lms_*.

Les LEDs et les boutons sont accessibles via le fichier /dev/lms_ui. On sélectionne le pattern d’allumage des LEDs en écrivant une chaîne composée d’un seul caractère pris dans la séquence 0-9, suivi de l’habituel caractère nul pour la terminer.

La lecture de l’état des boutons ne se fait par contre pas en lisant ce même fichier, mais via un mapping mémoire des 6 premiers bytes, qui représentent chacun l’état du bouton correspondant (d’où le choix précédent des numéros).

Comment on sait tout cela ? Très simple :

  • en lisant nos articles,
  • en lisant les sources de leJOS
  • en lisant ceux du LMS dont on vous a donné les coordonnées en début d’article

Le code maintenant

Et bien le voici :

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
  
#define MSECS 1000
  
int main(void) {
    int fd;
    char buf[2] = "0";
    char *btns;
    char led_pattern = 0;
 
    fd = open("/dev/lms_ui", O_RDWR | O_SYNC);
    printf("device opened\n");
    write(fd, buf, 2);
 
    btns = mmap(0, 6, PROT_READ, MAP_SHARED, fd, 0);
 
    char last_states = 0xff;
 
    while (1) {
        char states = 0 ;
        for (int i = 0 ; i < 6 ; i++)
            states = (states << 1) | (btns[i] & 0x01);
 
        if (states & 0x01)
            break;
 
        if (states != last_states) {
            if (states) {
                if (states & 0x20) { // up
                    led_pattern = (led_pattern + 1) % 10;
                } else if (states & 0x08) { // down
                    if (--led_pattern < 0)
                        led_pattern = 9;
                } 
 
                printf("setting LED pattern to %d\n", led_pattern);
                buf[0] = '0' + led_pattern;
                write(fd, buf, 2);
            }
 
            last_states = states ;
        }
        usleep(100*MSECS);
    }
 
    buf[0] = '1';
    write(fd, buf, 2);
    close(fd);
    printf("Bye bye.\n");
 
    return 0;
}

A noter :

  • les options d’ouverture du pseudo-fichier
  • la définition du memory mapping
  • l’utilisation d’une pause en fin de boucle, faute de quoi la CPU est à 100% et paradoxalement la brique est moins réactive (on perturbe le rafraîchissement du contenu du pseudo-fichier ?)

Pour le reste ça se passe de commentaires. Et ça tombe bien, j’ai eu la flemme d’en mettre :)

Les codes des patterns des LEDs sont définis comme suit :

Code Couleur  Motif d’allumage
0 éteint -
1 vert fixe
2 rouge fixe
3 orange fixe
4 vert clignotement simple
5 rouge clignotement simple
6 orange clignotement simple
7 vert clignotement double
8 rouge clignotement double
9 orange clignotement double

Pour la bonne bouche le makefile qui va avec :

# change next line to match your configuration
BIN=/home/eric/CodeSourcery/Sourcery_G++_Lite/bin
 
GCC=$(BIN)/arm-none-linux-gnueabi-gcc
COPT=-O3 -std=c99 -lpthread -D_REENTRANT
 
build: hello.c
	@echo "--- Compiling..."
	$(GCC) $(COPT) hello.c -o hello
 
clean:
	rm -f hello
 
upload: build
	@echo "--- Uploading binary..."
	scp hello root@ev3:
 
run: upload
	@echo "--- Starting execution on target..."
	ssh -t root@ev3 ./hello

Comme vous pouvez le constater, tout y est prévu, y compris lancer le programme à distance, directement depuis la machine de développement.

L’option -lpthread -D_REENTRANT est là du fait de l’utilisation de la fonction usleep et donc du threading.

En tapant make run depuis votre machine de dev, le programme va être compilé, téléchargé sur l’EV3 [3] et rien de spectaculaire ne va se passer (si ce n’est l’extinction des LEDs si elles étaient allumées), hormis un message de progression sur votre console. Mais si vous jouez avec les boutons up et down, vous allez modifier l’allumage des LEDs, en termes de couleur et de pattern de clignotement. En appuyant sur Escape le programme se termine, en allumant la LED en vert fixe. Tout baigne par conséquent.

A noter l’option -t de ssh dont le rôle est de forcer l’allocation d’un pseudo-tty et d’éviter la bufferisation des outputs du programme remote. La manifestation du problème est qu’on ne voit pas les messages au fur et à mesure, mais souvent à la fin seulement de l’exécution.

On essaye le C++ maintenant ?

Ben pourquoi pas. Voici ce que donne le nouveau code :

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
 
#define MSECS 1000
 
// To avoid the link error. It does not matter as long as you don't
// use exceptions in your code.
void *__gxx_personality_v0;
 
class UI {
    private:
        int device_fd;
        char* buttons_state;
        unsigned char last_btns_state;
         
    public:
        UI();
        ~UI();
        unsigned char get_buttons_state();
        unsigned char wait_for_buttons();
        void set_led_pattern(unsigned char pattern);
};
 
UI::UI() {
    this->device_fd = open("/dev/lms_ui", O_RDWR | O_SYNC);
    printf("UI device opened\n");
 
    this->buttons_state = (char *)mmap(0, 6, PROT_READ, MAP_SHARED, this->device_fd, 0);
    this->last_btns_state = get_buttons_state();
}
 
UI::~UI() {
    close(this->device_fd);
}
 
unsigned char UI::get_buttons_state() {
    register unsigned char states = 0 ;
    for (int i = 0 ; i < 6 ; i++)
        states = (states << 1) | (this->buttons_state[i] & 0x01);
    return states;
}
 
unsigned char UI::wait_for_buttons() {
    unsigned char old_state = this->last_btns_state;
    while (true) {
        unsigned char new_state = this->last_btns_state = get_buttons_state();
        unsigned char pressed = new_state & (~old_state);
        if (pressed) 
            return pressed;
        old_state = new_state;
        usleep(10*MSECS);
    }
}
 
void UI::set_led_pattern(unsigned char pattern) {
    static char buf[2] = "0";
    buf[0] = '0' + (pattern % 10);
    write(this->device_fd, buf, 2);
}
 
int main(void) {
    unsigned char led_pattern = 0;
    UI ui;
    ui.set_led_pattern(0);
 
    while (1) {
        unsigned char states = ui.wait_for_buttons();
 
        if (states & 0x01)
            break;
 
        if (states & 0x20) { // up
            led_pattern = (led_pattern + 1) % 10;
        } else if (states & 0x08) { // down
            if (--led_pattern < 0)
                led_pattern = 9;
        } 
 
        printf("setting LED pattern to %d\n", led_pattern);
        ui.set_led_pattern(led_pattern);
    }
 
    ui.set_led_pattern(1);
    printf("Bye bye.\n");
 
    return 0;
}

C’est un peu plus long globalement, mais la classe UI en fait un peu plus, avec notamment une méthode d’attente de l’appui sur les boutons. Le code du main s’en trouve par contre bien simplifié, et le serait encore plus dans le cas où nous aurions eu besoin de lire les boutons à plusieurs endroits.

L’incantation en tête de source relative à __gxx_personality_v0 concerne une histoire de handling d’exceptions. Le plus simple est que vous fassiez quelques recherches sur Google comme moi pour avoir tous les détails. Faut bien que vous bossiez un peu quand même :)

Le makefile modifié maintenant :

# change next line to match your configuration
BIN=/home/eric/CodeSourcery/Sourcery_G++_Lite/bin
 
GCC=$(BIN)/arm-none-linux-gnueabi-g++
COPT=-O3 -lpthread -D_REENTRANT
 
build: hello.c
	@echo "--- Compiling..."
	$(GCC) $(COPT) hello.cpp -o hello
 
clean:
	rm -f hello
 
upload: build
	@echo "--- Uploading binary..."
	scp hello ev3:
 
run: upload
	@echo "--- Starting execution on target..."
	ssh -t ev3 ./hello

On note le changement de compilateur et c’est tout.

Enfin, presque... Si vous essayez de faire tourner le binaire sur l’EV3, vous allez obtenir une erreur de chargement de la librairie libstdc++.so.6. Et pour cause, car elle n’est pas incluse dans le file system leJOS (vu qu’ils ne s’en servent pas). Il suffit de la récupérer dans l’arborescence des sources de l’EV3 que vous avez récupérée depuis le repository Git, dans le répertoire extra/linux-devkit/arm-none-linux-gnueabi/usr/lib, sous le nom libstdc++.so.6.0.10. Copiez-la simplement dans /usr/lib sur l’EV3, et créez au même endroit le lien symbolique conventionnel vers elle, nommé libstdc++.so.6. Le tour est joué, et votre hello est content maintenant.

Et pour conclure

Bon, ne nous enflammons pas : on ne va pas aller très loin avec cela. Il nous manque en effet une librairie sympa pour pouvoir programmer la brique avec une abstraction comme celle de leJOS par exemple. Et l’écrire est un sacré boulot, même si on peut justement s’en inspirer et la porter en C++ par exemple. Des volontaires ?

Mais cela nous montre que l’EV3 est totalement accessible et ouvert et qu’il n’y a que l’embarras du choix pour lui donner vie.

En tout cas j’ai noté un avantage notoire par rapport à la filière Java : le temps de turnaround est infiniment plus réduit, car le démarrage du binaire est immédiat maintenant. Et comme la compilation C/C++ ne coûte gère plus cher que la compilation Java, ce n’est pas négligeable.

Have fun ;)


[1distribution sur base Angström pour ne rien vous cacher

[2LEGO Minstorms System

[3que vous avez pensé à connecter et mettre sous tension au préalable

Un message, un commentaire ?

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.