Simulink 2 Lustre 2 nxtOSEK


dernière modif le 16/05/2014

Introduction


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


Environnement de travail

Développement

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

/user/5/raymond/mdl2lus2osek


Pour accéder simplement aux outils, ajouter la ligne suivante à votre .bashrc :
source /user/5/raymond/mdl2lus2osek/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  devez :

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) :
Deux exemples plus complets sont aussi disponibles dans le répertoire mdl2lus2osek/examples :
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.
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 :
 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 :

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) :

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 : 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 on l'a 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 :
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ést 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ón sort du type flotant 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 :