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 :
-
Cours 1, Implantation sûre de systèmes contrôle/commande temps-réel
-
Cours 2, Mdl vers Lustre vers Osek
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”),
- connecter la brique au port USB du PC, et lancer la commande bash 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</big></span></p>
- le script loadrxe (basé sur l’outil windows NextTool) :
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 firmwares 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 :
Schéma général
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éfinition 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 et .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 :
- 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 Lustre vers 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 :
- mémoire statique:
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’importe qu’elle application mono-tâche, mais aussi pour les applications multi-tâches qui utilisent des fichiers mdl différents.
- mémoire allouée globalement:
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 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 :
|
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.
exemple/com-harmo
contient un exemple complet.
Implémentation dans le cas général
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.
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ée” (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 deux solutions :
- (conseillée) 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.
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énérer 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 flottant.
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 comporter trois types de données :
- Les boolean Simulink sont traduits en bool Lustre puis en “booléens” C standard (int natifs)~; pour info, dans le cas du processeur ARM de la brique les int natifs sont des uint16.
- les int Simulink sont traduits en int Lustre puis en entiers natif.
- les float Simulink sont traduits en real Lustre, puis en double C. 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 implémentée logiciellement dans la ’libc’ de gnui. Le coût de la moindre opération flottante est donc énorme (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.