Simulink 2 Lustre 2 nxtOSEK

dernière modif le 11/10/2017

Introduction

Ce document détaille les aspects techniques, pour des informations plus générales, voir les notes de cours :

Environnement de travail

Développement

Tout les outils nécessaires sont disponibles sur les PC/linux, dans :

/user/5/raymondp/mdl2lus2osek et /user/5/raymondp/lustre

Pour accéder simplement aux outils, ajouter la ligne suivante à votre .bashrc :

source /user/5/raymondp/mdl2lus2osek/SETENV.sh
source /user/5/raymondp/lustre/setenv.sh

Chargement des programmes sur la brique

La méthode nxt-OSEK produit 3 types de binaires (ram.bin, rom.bin et .rxe), avec chacun sa méthode de chargement.

Pour simplifier la démarche, il est conseillé d’utiliser les binaires .rxe. Sous les PC linux  vous devez :

  • Vous assurez que la brique fonctionne avec le firmware Jon Hansen. Son interface est la même que celle du firmware lego standard, il faut aller dans “Settings/NXT Version” pour voir la différence : la première ligne doit commencer par “FW NBC/NXC” (au lieu de simplement “FW”).
  • Si ce firmware n’est pas installé, il faut :
    • faire un reset de la brique (appuyer 3 secondes sur le bouton caché au fond du trou sous la prise USB), ce qui la met en mode “tic” (elle s’éteint et émet des sons “tic”),
    • connecterla brique au port USB du PC, et lancer la commande flashfirm.
  • Pour charger un programme .rxe, vous pouvez utiliser le script loadrxe (basé sur l’outil windows NextTool) :

loadrxe nom-du-fichier.rxe

ou l’outil (natif linux) t2n :

t2n -put  nom-du-fichier.rxe

NOTE : utiliser de préférence t2n, car il est maintenu.

Si vous voulez en savoir plus sur les différents types de programme, les firmware et le chargement, voir :

Programmation des périphériques
NXT

IMPORTANT. Les accès aux périphériques (capteurs, moteurs, écran) se font via l’API nxtOSEK, qu’on n’étudie pas en cours. Vous devez vous familiariser avec cette librairie d’interface en consultant la documentation officielle :

http://lejos-osek.sourceforge.net/ecrobot_c_api.htm

Schéma général

 

schema

 

Exemples

Le répertoire  mdl2lus2osek
contient deux squelettes d’applications (qui ne font
rien du tout) :

  • sample1task est un squelette générique pour une unique tâche périodique.
  • sample2tasks est un squelette générique avec deux tâches périodiques concurrentes.

Deux exemples plus complets sont aussi disponibles dans le répertoire mdl2lus2osek/examples :

  • testlight est un exemple typique avec une unique tâche périodique.
  • compteurs est un exemple typique avec deux tâches périodiques concurrentes.

Pour tester, copier le répertoire et tapper make pour construire les binaires, make clear pour nettoyer.

Les fichiers du projet

Un projet (répertoire de travail) contient un certain nombre de fichiers obligatoires qu’il faut adapter. Encore une fois, il est recommendé de partir d’un des exemples fournis pour se faire un idée précise de leurs rôles.

  • Makefile est un “wapper” autour du makefile générique OSEK (generic.mak, qu’on n’est normalement pas censé modifier, ni même comprendre en détails !).  Le Makefile du projet rajoute simplement les informations spécifiques du projet, qu’il faut adapter au cas par cas (essentiellement le nom du projet, la liste des fichiers C utilisateur avec leurs règles de fabrication).
  • kernel_cfg.h est le fichier qui définit tous les paramètres OSEK (noms et nombres de tâches, priorités, périodes etc). Ces fichiers sont normalement créé à partir d’une descrition de type “architecture système”, écrite dans un langage ad hoc (appelé oil) et compilée par un outil spécifique. Pour ce cours, on n’a pas besoin d’une telle sophistication : on se contentera d’utiliser un des deux  kernel_cfg.h prédéfinis :
    • testlight/kernel_cfg.h est un fichier de configuration valable pour tout projet avec une tâche périodique unique,
    • compteurs/kernel_cfg.h est un fichier de configuration valable pour tout projet avec deux tâches périodiques.
  • Les fichiers utilisateur .mdl
  • Un fichier glue.c principal, qui doit contenir la définiton des tâches, le code d’interface Lustre/Osek, et des initialisations Lustre et OSEK obligatoires (suivre l’exemple et les commentaires des fichiers glue fournis).
  • Éventuellement, tous les autres fichiers .c/.h nécessaire au projet.

Une fois le projet en place, il suffit normalement de taper make pour fabriquer les binaires nxt, make clear pour nettoyer. La suite détaille les choses à savoir si et quand ça se passe mal !

Préparer vos fichiers mdl et les compiler en lustre

N.B. A partir de 2014/2015 on utilise une nouvelle version du compilateur Simulink vers lustre (mdl2lus au lieu de sim2lus) : on n’est plus obligé d’avoir des fichiers séparés pour chaque tâche.

Ce fichier sera ensuite traduit en Lustre, puis en code c.

Le traducteur Simulink to Lustre est par nature très contraignant. Il faut donc prendre des précautions particulières quand on développe en Simulink pour produire du “vrai” code :

    • <!–

    • obsolete avec mdl2lus : chaque programme doit être tout
      seul dans un fichier
      mdl
    • obsolete avec mdl2lus : L‘option “Simulation/solver
      doit être mise à “
      fixed-step,
      discrete
    • obsolete avec mdl2lus : L’option “Data Import/Export/Format
      doit être mise à “
      Structure
      with time

–>

  • Les types de données doivent être renseignés autant que possible : il faut typiquement forcer le type de toutes les entrées et des constantes (boolean, int, float etc), le type des autres variables en découlera.

Essayer la commande mdl2lus file.mdl pour compiler le système principal,   ou la commande mdl2lus file.mdl -system foo  pour compiler un (sous) système particulier.

Compilation des fichiers lustre en C

Le compilateur Lustre propose des options qui modifient la manière
dont le code produit va s’interfacer avec la glue, et qu’il faut
donc maîtriser. Pour ce cours, on peut utiliser deux options :

  • lus2c foo.lus foo -ctx-static produit un programme réactif avec mémoire statique, et le code est donc utilisable pour une seule et unique instance. Dans ce cas, le code est assez simple à interfacer : voir l’exemple de testlight. Ce mode est suffisant pour n’i­mporte qu’elle application mono-tâche, mais aussi pour les applications multi-tâches qui utilisent des fichiers mdl différents.
  • lus2c foo.lus foo -ctx-global produit un programme réactif instantiable. Dans ce cas, l’interfaçage est plus compliqué car toutes les procédures sont paramétrées par une instance particulière (c’est du code “orienté objet”). Ce mode n’est a priori pas nécessaire pour ce cours, mais au cas où vous pouvez voir ce que ca donne sur l’exemple compteurs.

Communication entre les tâches

Comme expliqué dans  le cours sur l’Implantation sûre de systèmes contrôle/commande temps-réel, on peut mettre en oeuvre, si on s’y prend bien, une application  multi-tâches aussi sûre qu’une application mono-tâche, c’est-à-dire où les communications sont déterministes et non-blocantes. On considère le schéma conceptuel suivant :

  • une tâche rapide prioritaire H, de période ph
  • une tâche lente non-prioritaire L, de période pl (ph > pl)
  • H communique une valeur
    H2L à L, immédiatement
    (i.e. L utilise, quand elle est activée, la dernière
    valeur calculée par H)
  • L communique une valeur L2H à H, avec un retard logique
    (i.e. H utilise toujours la valeur calculée
    au cycle pl
    strictement passé, et non pas la dernière valeur
    calculée, ce qui serait indéterministe).

Remarques :

  • ce schéma de
    communication peut être programmé et simulé en Simulink
  • le code glue
    correspondant (à base de buffers) pourrait être
    généré automatiquement, mais l’outil actuel sim2lus
    n’est pas encore tout à fait opérationnel : on doit donc
    écrire ce code directement en C à la main,
  • pour que ça fonctionne,
    on a impŕativement besoin d’un buffer  H2L et un
    “double-buffer” pour L2H/L2Hnext :

    • si H est active, mais pas L, elle reçoit la valeur
      L2H, H2L doit être mémorisé au cas où L en ait besoin
      plus tard
    • si L est active, mais pas H, elle reçoit la valeur
      H2L, la valeur de L2Hnext est recopiée dans L2H, et L
      commence  à calculer une nouvelle valeur qu’elle
      stockera dans L2H,
    • si les deux sont actives en “même temps”, la valeur
      de L2Hnext est recopiée dans L2H, et H calcule avec
      cette nouvelle valeur L2H, comme elle est prioritaire,
      elle calcule un nouveau H2L, puis, seulement L
      commence à calculer avec ce nouveau H2L, pour produire
      une nouvelle valeur L2Hnext.
  • En étant un peu malin, on n’a jamais
    besoin de “recopier”
    L2Hnext dans L2H, ce qui peut
    être pénalisant pour des données de grande taille. On
    utilise un tableau à deux éléments, plus un index (0
    ou 1) qui indique qui est le
    L2Hnext courant ; L écrit via
    cet index, et H lit dans l’index opposé. Le changement
    de buffer est juste un “swap” de l’index.
  • Il existe plusieurs
    manières d’implémenter en C/Osek ce schéma de
    communication, on en détaille deux ci-dessous.

 

Implémentation dans le cas harmonique

Les  tâche sont  harmoniques si pl = n * ph. Intuitivement, dans ce cas. la tâche rapide (ph) sait exactement quand l’autre va s’exécuter et donc quand des communications sont nécessaires. De plus, comme elle est prioritaire, elle peut gérer les communication sans risque d’être interrompue (pas besoin de mutex) :

  • H gère un compteur cpt modulo n ; chaque fois que cpt=0, elle “sait” que L va être activée juste après.
  • Quand elle est activée, H lit L2H, fait un step et écrit dans H2l, puis, si cpt=0, elle “swape” le double-buffer (L2H = L2Hnext)
  • Quand elle est activée, L lit toujours dans H2L, et écrit dans L2Hnext.

Le répertoire exemple/com-harmo contient un exemple complet.

Implémentation dans le cas général
Le cas non-harmonique est plus difficile car on doit considérer des instants où H est activée seule, où H et L sont toutes deux activées, mais aussi où L est activée seule. Pour chacun de ces cas, des opérations nécessaires aux communications doivent être faites en priorité et/ou de manière ininterruptible. Pour y arriver on doit donc faire intervenir une dose d’exclution mutuelle et/ou une tâche  additionelle  ultra-prioritaire et suffisemment  fréquente pour gérer les communications. Le répertoire exemple/com-synchro contient un exemple où on utilise une tâche de synchro :
  • La tâche de synchro est ultra-prioritaire, donc non-interuptible, sa période est pgcd(pu, pf) (e.g. pgcd(20,50) = 10)
  • Elle calcule si H et/ou L vont être activées dans l’instant, et positionne les entrés en conséquences
  • Il n’y a qu’une chose qu’elle ne peut pas faire : le swap du double-buffer, qui doit avoir lieu après H. C’est donc H qui s’en charge : mais pas de problème puisque qu’une fois la
    Synchro effectuée, elle est elle-même priotitaire et ininterruptible jusqu’au prochain instant.

On peut imaginer plein d’optimisation, ne pas hésiter a essayer des variantes !

Fonctions externes et édition de liens

De plus, il est possible (et même probable) que le Lustre produit depuis depuis Simulink contienne des fonctions externes : vous pouvez le vérifier en regardant le début du code Lustre. Ce cas survient dès que le fichier Simulink contient des opérateurs qui ne sont pas “built-in” en Lustre, comme typiquement l’opérateur “cosinus”. Dans ce cas, l’opérateur reste “abstrait” tout au long de la compilation, jusqu’à devenir un appel de procédure externe dans le code c. Il faudra donc naturellement implémenter cette procédure en C dans le code glue.

Le compilateur Lustre/c adopte des convention très précise sur le profil de ces procédures C externe : il faut consulter le code c produit pour voir le profil attendu. Par exemple, pour calculer l’opérateur “cosinus” de  Simulink, le code C produit au bout de la chaîne mdl/lus/c attend la définition d’une procédure dont le profil est (le nom peut varier selon la version de Simulink) :

extern void Trigonometric_cos(_float* res, _float arg);

Il vous faut alors programmer cette procédure dans le code glue. Comme on s’appuie sur un compilateur gnu avec une librairie C (quasi) complète, le programmation est très simple : on peut utiliser la fonction cos de la librairie mathématique standard :

#include <math.h>

….

void Trigonometric_cos(_float* res, _float arg) { *res = cos(arg); }

L’implémentation des fonctions externes peut faire appel à des librairies externes. Les librairies externes supplémentaires doivent être spécifiées dans le Makefile, via la liste USR_LIBS (typiquement, -ltoto). Pour le “cos” (et plus généralement pour toutes les fonctions de la librairie mathématique standard), il est inutile de rajouter l’option -lm, car elle est prise en compte par défaut dans le makefile générique.

Problème des blocs Simulink à mémoire non reconnus par sim2lus

Comme expliqué ci-dessus, les blocs Simulink non built-in en Lustre sont traduits par des appels de procédures externes. Ce mécanisme est un peu contraignant puisqu’il faut programmer à la main les procédures manquantes, mais il a l’avantage de “marcher” pour tout les blocs Simulink combinatoires. Par contre, ce principe ne marche plus si le bloc Simulink est en fait un bloc à mémoire, comme par exemple le bloc “dérivé”

(1-z/z).  Dans sa version actuelle, sim2lus ne sait pas distinguer les blocs combinatoire (i.e. les fonctions) des blocs à mémoire. Pour faire face à ce problème, il y a en gros deux solutions :

  • s’obliger à ne programmer en Simulink qu’avec des blocs à mémoire de base, reconnus pas sim2lus (donc, en gros uniquement le retard “1/z”) ce qui est un peu lourd, mais ne nécessite aucune technique sophistiquée,
  • patcher le code Lustre produit pour remplacer les “fausses” fonctions externe par des “vrais” programmes à mémoire, programmés en Lustre, ce qui est un peu technique et nécessite de savoir programmer un minimum en Lustre.

On conseille vivement la première solution !

Note sur les types de données

En Simulink, toutes les données sont par défaut des flottants, ce qui n’est pas toujours acceptable pour génerer du bon code embarqué.
C’ est pourquoi il est fortement conseillé de renseigner dans Simulink les types de donnée qu’on veut avoir dan le code final : boolean, int ou flotant.

Dès qu’ on sort du type flottant par défaut, Simulink offre un choix pléthorique, en particulier pour les entiers (signés/non signés, 8, 16 bits et +).

Dans la chaîne mdl/Lustre/OSEK, une telle précision ne sert à rien puisque Lustre ne connait qu’un type “entier” et un type “real”. Le choix du sous-type entier dans Simulink n’a donc aucune importance (on peut prendre systématiquement int16 par exemple).

Au final, l’application peut comporte trois types de données :

  • les “boolean” Simulink sont traduits en “bool” Lustre puis en booléens C standard, c’est-à-dire en entiers natifs (des unsigned int 16 bits dans le cas du processeur ARM de la brique).
  • les “int” Simulink sont traduits en “int” Lustre puis en entiers natif.
  • les “float” Simulink sont traduits en “real” Lustre, puis en “double” C standard. Notez que le type float (a fortiori double) n’est pas natif sur le processeur ARM, qui ne possède qu’une simple unité arithmétique. Les doubles sont donc supportés “en soft”, via la libc de gnu. Le coût de la moindre opération “double” est donc ENORME (facteur 100 ou plus) par rapport à une opération entière. Ça ne devrait pas poser de problème pour ce cours, où les programmes sont relativement simples, mais c’est un critère important quand on programme de vraies applications embarquées temps-réel !