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”),
    • 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>
      

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

image schéma

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 :

  • 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.

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.