vendredi 27 avril 2018

Chapitre 27 : Logique floue et GPIO du Raspberry


Le chapitre 2 du livre précédemment cité sur l’Intelligence Artificielle propose un exemple de logique floue. Cela m’a donnée l’idée de l’appliquer au GPIO du Raspberry. Pour cela, je voudrais gérer la luminosité d’une LED en fonction de la lumière reçue par 2 cellules photoélectriques.
C’est un projet complexe car outre l’adaptation de la logique floue à l’assembleur, il faut aussi arriver à récupérer les intensités lumineuses de chaque photo résistance puis moduler la luminosité de la LED au travers des instructions du GPIO.
Donc je vais le décomposer en 3 parties : un programmepour tester la récupération de la luminosité et connaitre l’échelle de variation possible. Un deuxième programme pour tester la variation de la lumière émise par la LED et là aussi connaitre l’échelle des valeurs possibles et enfin un 3ième programme pour gérer les ensembles et règlesflous.
Comme pour le programme sur le système expert, je vous renvoie pour les concepts théoriques au livre en question.
En premier, recherche sur Internet du montage pour lire les valeurs d’une photo résistance et d’un exemple de programme associé pour comprendre le principe. Le montage est assez simple. J’adapte le programme déjà écrit précédemment pour intégrer la lecture de l’état de la photo résistance.
Pour la Led, je recherche comment gérer sa luminosité. C’est moins simple car il faut alternativement l’allumer et l’éteindre avec des durées différentes. Et il va falloir donc trouver une solution pour garder la luminosité constante pendant un laps de temps jusqu’au calcul suivant.
Enfin je traduis en assembleur les routines données en Java dans le livre précédent. Pour simplifier la saisie, je développe toute une partie pour récupérer les variables d’entrées, de sortie et les règles dans un fichier de configuration. Les routines de gestion seront rassemblées dans un fichier objet qui sera lié à un programme maitre. Seul celui-là sera à modifié et à recompiler en fonction des valeurs d’entrées nécessaires à la résolution.
Les routines de gestion des ensembles floues sont assez compliquées. En plus, j’ai choisi d’utiliser des listes chainées pour gérer toutes les données nécessaires. Cela oblige d’utiliser un grand nombre de pointeurs ce qui ne facilite pas la lecture du programme. Puis c’est du brut de fonderie, je n’ai pas réfléchi à toutes les améliorations possibles.
Au début du programme, nous trouvons les descriptions des structures nécessaires : une pour les points, une pour les ensembles flous, une pour le contrôleur, une pour les variables et enfin la dernière pour les valeurs. Vous remarquerez qu’en plus des données propres, des pointeurs vers la structure suivante ont été ajoutés pour gérer les listes chainées.
Ensuite nous trouvons les descriptions des libellés nécessaires puis les réservations de place des variables. Peu de variables sont déclarées car toutes les données seront créées dans un tas et accessibles seulement par des pointeurs.Dans ce programme nous utiliserons le tas du système (heap) en récupérant l’adresse de début du tas par la fonction call system brk de Linux. Nous ajouterons notre taille nécessaire et nous fixerons la nouvelle taille du tas par le même call system brk. Cette solution permet de créer plusieurs contrôleurs avec des configurations différentes dans le même programme ( cas de figure non testé à ce jour).
La première routine FLOUcreationcontroleur concerne la création du contrôleur à partir du nom du fichier de configuration passé en paramètre dans le registre r0. Nous commençons par lire le fichier de configuration pour vérifier s’il existe bien. Puis nous créons dans le tas, le contrôleur à l’aide de la description de sa structure. Pour tout ce qui concerne les pointeurs de débuts de liste chainées, nous les initialisons avec une adresse du tas contenant une valeur sentinelle (cad que le pointeur contenant l’entité suivante contient en fait sa propre adresse). Nous aurions pu nous contenter de mettre une valeur nulle dans le pointeur mais cette solution permet de s’assurer que les listes sont correctement alimentées et terminées par une sentinelle de fin. Une valeur à zéro pouvant être aussi une erreur de programmation.
Puis nous passons à une sous routine d’analyse du fichier de configuration pour créer les variables d’entrées, et leurs valeurs associées, la variable de sortie et ses valeurs associés, et les règles de détermination. Cette programmation n’est pas le top et a été un peu améliorée par rapport au programme sur le petit système expert (voir chapitre 21). J’ai écrit une sous sous routine pour boucler sur les blancs présents dans le buffer de lecture et une autre pour analyser chaque mot, mettre un 0 binaire de fin de chaine et avancer jusqu’au prochain mot valide (ou la fin de fichier) en éliminant les blancs, le caractère « : » et les caractères de fin de ligne (soit 0D0A mais à vérifier suivant votre système et votre éditeur de texte adoré). Chaque variable, valeur ou règle est stockée dans la liste chainée adéquate du contrôleur.
Après mise à jour en mémoire du pointeur du tas, la routine retourne l’adresse du contrôleur au programme appelant.
Une autre routine concerne l’ajout de valeurs déterminant le problème à résoudre. Les paramètres en entree sont l’adresse du contrôleur, le nom de la variable concernée et la valeur associée. Si la variable n’est pas trouvée dans les variables d’entrée données par le fichier de configuration, un libellé d’erreur est affiché.
Puis nous trouvons la sous routine résoudre qui est l’exact reflet des routines décrites dans le livre de MME MATHIVET. Quelques petites différences quand même : pour éviter des calculs en virgule flottante, la valeur Y de chaque point est 100 et non pas 1. Ce qui entraine qu’un résultat de 77 par exemple correspond à un résultat de 0,77 du livre. Les sous routines semblent complexes, mais j’ai mis le maximum de commentaires pour expliquer les calculs et opérations réalisées. A ce jour, j’ai testé ces routines avec les données du livre, et il y a une petite différence dans les calculs pour un des cas. Malgré mes recherches, je n’ai pas trouvé l’erreur et je suspecte qu’une entrée du livre est incorrecte !!!
Enfin la dernière sous routine concerne la réinitialisation des données du problème pour effectuer un nouveau calcul. Et pour éviter tout accroissement du tas, cette routine remet l’adresse du tas à sa valeur après l’analyse du fichier de configuration.
J’ai ajouté d’autres sous routines pour afficher les variables d’entrées, de sortie et les règles, ce qui permet de vérifier la bonne prise en compte du fichier de configuration.
Après compilation, le fichier objet sera linké avec le programme appelant ainsi que le fichier des sousroutines de gestion du gpio

Ici dans notre cas, le programme appelant va appeler la routine de création du contrôleur en lui passant le nom du fichier de configuration récupère dans la ligne de commande comme déjà vu dans des chapitres précédents. Puis va appeler dans une boucle, la routine de lecture de la photorésistance 1 la routine de lecture de la photo résistance 2, passer les 2 valeurs au contrôleur par la sous routine FLOUajoutervaleur,, appeler la résolution, puis appeler la routine d’allumage de la LED avec le résultat précèdent en paramètre. Puis attendre un certain temps avant de boucler à nouveau après avoir réinitialiser l’état du contrôleur a son état après la prise en compte des règles.
Le jour où j’écris ces lignes, je ne sais pas encore si tout cela va fonctionner correctement !! Je pense que cela va nécessiter plusieurs ajustements tant au niveau des valeurs qu’au niveau des règles. Bon je vais déjà aller tester la partie GPIO avec les montages proposés.
Quelques jours après : après quelques erreurs de montage, les 2 premiers programmes ont bien fonctionné. J’ai pu obtenir les variations de la cellule photoélectrique ( de 5000 à 80000) et j’ai donc appliqué un facteur de réduction de 1000 pour rester dans une plage de valeur de 5 à 800. Pour le réglage de la luminosité de la LED, les réglages ont été plus délicats et ne sont pas encore tout à fait satisfaisants. Mais cela suffit pour effectuer un test final.
J’effectue soigneusement le montage des 2 cellules photorésistantes (attention au sens des condensateurs) l’ajout de la led (attention aussi à son sens), je modifie le programme principal pour que les pins du GPIO correspondent bien à ceux du programme (18 pour la LED, 4 et 22 pour les cellules) J’adapte le fichier de configuration pour déterminer les différentes valeurs et je crée quelques règles de génération de la luminosité (voir plus bas la syntaxe du fichier de configuration à respecter). Et cela fonctionne !!! En passant la main devant les cellules, la luminosité de la LED varie (imparfaitement mais tout est dans le fignolage des réglages).
Pour lancer le programme il faut taper :  pilotageLedFinal configFlGpio.txt

Et maintenant ?   restent à fignoler les règles et réglages, à réécrire les routines pour améliorer l’utilisation des registres et la lisibilité.

Syntaxe du fichier de configuration (fichier texte) :
1ère ligne : nom du contrôleur ( ne sert à rien !!!mais est obligatoire)
2ième ligne : libellé Entrées:     indique le début de la liste des variables d’entrées
Ligne Variable:nom_de_la variable:valeur mini:valeur maxi
Pour chaque variable plusieurs lignes valeurs
Valeur:nom_de_la valeur :type_ensemble_flou :valeur1 :valeur2 :valeurn :
Avec type d’ensemble flou : EFTG   : Ensemble Flou Trapèze Gauche suivi de 4 valeurs en ordre croissant dont la première est égale à la valeur mini et la dernière à la valeur maxi
                                               EFTD   : Ensemble Flou Trapèze Droit suivi de 4 valeurs en ordre croissant dont la première est égale à la valeur mini et la dernière à la valeur maxi
EFTRA   : Ensemble Flou TRApèze suivi de 5 valeurs en ordre croissant dont la première est égale à la valeur mini et la dernière à la valeur maxi
EFTRI   : Ensemble Flou TRIangle suivi de 5 valeurs en ordre croissant dont la première est égale à la valeur mini et la dernière à la valeur maxi
Puis le libellé Sortie : avec une seule variable et ses valeurs comme une variable d’entrée.
Puis le libellé Règles :
Puis chaque règle codifiée comme suit
SI nom_variable_entrée EST nom_de la valeur [ET nom_variable_entrée EST nom_de la valeur…] ALORS nom_variable_sortie EST nom_d’une_valeur_de_sortie
Attention : les contrôles de cette syntaxe sont très légers !!! et donc vérifier bien l’affichage lors de l’execution pour contrôler la prise en compte.


mercredi 25 avril 2018

Chapitre 26: accès au Firmware du GPU du Raspberry


Lors du démarrage du raspberry, ce n’est pas un bios habituel qui s’exécute mais un programme particulier exécuté par le GPU (c’est le processeur graphique). Celui-ci prépare le raspberry puis lance l’Os désiré stocké et qui peut être un Linux comme Raspbian ou un OS plus exotique ou éventuellement le vôtre !!.
Ce programme appelé firmware continue à être présent et dialogue avec le système d’exploitation au travers d’un mécanisme de mailboxes (voir documentation sur le site : https://github.com/raspberrypi/firmware/wiki). Nous allons voir que nous pouvons à partir d’un programme assembleur récupérer des informations sur le raspberry. Il est bien sûr aussi possible d’afficher ces infos dans une console Linux avec l’utilitaire et les options suivantes  « vcgencmd commands » : mais la récupération par le programme assembleur permet de les utiliser comme nous le voulons. Source du programme
Comme les programmes auparavant sur le GPIO ou l’accès au Timer, Linux limite les accès à la mémoire des périphériques et nous allons donc devoir ouvrir le périphérique /dev/vcio et lancer des commandes avec la fonction call système ioctl. Le plus difficile c’est de trouver les codes pour communiquer avec le firmware et la description des réponses. Une partie se trouve dans la documentation indiquée plus haut. Nous devons donc renseigner les données du message à transmettre dans un buffer suivant la description de la structure msg mailbox.
Après l’ouverture du périphérique, nous cherchons la valeur du code série du raspberry en passant à la fonction système IOCTL la zone message contenant les informations pour la requête et dans laquelle nous récupérons les infos retournées par le firmware. Cette zone doit contenir sur 4 octets sa longueur, puis 4 octets à zero pour le code retour puis le code correspondant à l’information recherché ici 0x10004 puis la longueur de la réponse.
En retour, nous trouverons un code retour égal à 0x80000000 si la requête s’est bien passée et la valeur du N) de série dans la zone réponse. Il faut faire attention à l’endroit où commence la valeur. J’ai laissé en commentaire l’appel à mon vidage des zones mémoire qui me permet de vérifier le contenu.
Ensuite le programme effectue une deuxième requête pour chercher la température du Raspberry , température qui sera retourné en degré * 1000.
Maintenant il faut creuser pour rechercher et utiliser les autres données et fonctions disponibles.

Remarque 1 : d’après certaines documentations, la réponse à la requête peut durer un certain temps et donc à la place de tester le code retour à 0x80000000, il faut mettre une boucle d’attente qui se terminera quand le code retour est différent de zéro puis tester la valeur 0x8000000 pour savoir si la réponse est OK.

Remarque 2 : la fonction système IOCTL utilise des codes particuliers (ici variable IOWR) pour accéder à chaque périphérique. J'ai trouvé cette fois ci le code pour accéder au vcio mais je n'ai pas trouvé les autres codes disponibles. 

vendredi 13 avril 2018

Chapitre 25 : utilisation du timer du BCM2835


En écrivant les programmes précédents sur le GPIO, j’avais trouvé que l’on pouvait aussi accéder aux périphériques en effectuant un mapping de la mémoire à partir du fichier /dev/mem. Mais restriction importante, le programme ne peut être lancé que par sudo alors que l’utilisation de /dev/gpiomem permet le lancement par un utilisateur quelconque.
Pour voir quand même les possibilités, j’ai écrit ceprogramme qui exploite le timer disponible sur le BCM2835. La mise au point de ce programme m’a donné beaucoup de mal et j’ai passé de nombreuses heures sur internet à chercher les causes des différents problèmes que j’ai rencontrés. Enfin aujourd’hui ce programme fonctionne correctement et à part un petit truc bizarre, j’ai progressé dans la compréhension des périphériques.
Comme pour l’utilisation du GPIO, le programme commence par ouvrir le fichier /dev/mem et effectue le mapping dans la mémoire par l’instruction mmap. Vous remarquerez que les flags n’autorisent que la lecture des données mappées, limite imposée par la sécurisation de la mémoire par Linux. Et là j’ai eu beaucoup de mal à exécuter correctement cette fonction. En fait, j’ai utilisé le code call system 192 pour cette fonction mmap qui correspond dans la documentation linux à mmap2. Et pour cette fonction, l’adresse à passer dans le registre r5 doit être indiquée en nombre de pages de 4096 caractères et ne doit pas être l’adresse directe contrairement à tous les exemples que l’on trouve sur Internet.
Donc ici la documentation du BCM2835 indique à la page 172 que l’adresse du timer est 0x7E003000. Ce qui correspond pour le type de raspberry que j’utilise à l’adresse virtuelle 0x20003000 soit à un nombre de pages de 0x2003 valeur que j’ai donnée à la constante ST_BASE. Pour d’autres modèles du Raspberry, il faudra adapter cette valeur.
La deuxième erreur que j’avais faite pour l’appel de mmap, c’est que je testais une valeur négative en cas d’erreur mais la fonction retourne une adresse résultante du mapping qui peut être négative en notation signée et donc à l’exécution, le mapping était correct mais mon programme se terminait avec une erreur. J’ai donc modifié le test du retour pour ne détecter que les erreurs comprises entre -125 et 0.
Ensuite le programme récupère la valeur du timer à l’offset +4 pour effectuer un calcul du nombre de micro secondes données par le timer pour chaque seconde (appel de la fonction sleep avec r0 egal à 1 et r1 à zéro). Dans un premier temps, mon programme affichait la zone mémoire située à cette adresse et je ne comprenais pas les données affichées car cela ne correspondait pas à la documentation. Et d’ailleurs c’est toujours le cas, le programme fonctionne correctement il affiche des valeurs proches de 1000145 par seconde bien que l’affichage de la zone mémoire ne soit pas correcte. Je suspecte que la sécurisation de l’accès à la mémoire par le Kernel Linux n’autorise pas (ou autorise mal) certains instructions à la mémoire (en particulier les accès octet par octet).
En fin de boucle, le programme se termine par la fermeture de la zone mappée et du fichier du périphérique.
L’utilisation de ce timer peut servir pour commander des composants comme les LEDS du GPIO et c’est une solution intéressante pour les programmes bare metal qui n’auront pas de fonction sleep du système d’exploitation pour contrôler les durées. Et dans ce cas, pas besoin de mapping puisque le programme pourra accéder directement à l’adresse du timer.
Je laisse le soin aux spécialistes des périphériques, d’étudier les autres possibilités d’utiliser les données issues de /dev/mem.

mercredi 4 avril 2018

Chapitre 24 : et les macros ?


Une macro permet d’inclure dans un programme assembleur, une série d’instructions qui seront compilées en même temps que le programme (donc différent d’un objet qui est inclus au moment du link).
Les macros évitent de retaper plusieurs fois les mêmes instructions et il est possible de leur passer des paramètres.
Par exemple dans les premiers chapitres nous avons écrit une routine pour afficher un message dans la console, routine que nous avons inclus dans un autre programme contenant toutes les routines. Mais nous aurions pu écrire cette fonction sous forme de macro :

.macro  afficheMessage          @ définition de la macro
@ r0 contient l’adresse du message et il faut d’abord calculer sa longueu
              mov r1,r0   @ save adresse
    mov r2,#0   @ compteur de caractère
    1\@:         @ boucle de comptage de caractères
    ldrb r3,[r1,r2]   @ lecture un octet du message
    cmp r3,#0          @ c'est la fin ?
    addne r2,#1       @ non ajout de 1 au compteur
    bne 1\@b        @ et boucle
                @ appel systeme fonction affichage
    mov r0, #1                  @ r0 ← 1
    mov r7, #4                  @ write code 4  
    swi #0    
.endm   @ fin de la macro

L’appel de la macro dans le programme s’effectue simplement par le nom : afficheMessage
Les instructions utilisées sont les mêmes que l’assembleur sauf le label 1\@ qui est particulier. En effet une macro peut être insérée plusieurs fois et si nous avions mis un label comme 1 : ou Boucle1 : , la compilation aurait produit une erreur car elle aurait trouvé plusieurs labels identiques. Vous remarquerez que nous utilisons des registres sans les sauvegarder donc il faudra faire attention et les sauvegarder si nécessaire
Nous pouvons passer un ou plusieurs paramètres à une macro : dans la macro comparVal nous passons 2 adresses de message et une valeur à tester dont la valeur par défaut sera 0 (en respectant la syntaxe #0). L’utilisation de ces données dans le corps de la macro s’effectue en mettant un \ devant le nom. Nous pouvons appeler une autre macro dans la macro, ici en fonction du résultat de la comparaison le message affiché sera différent.
L’appel de la macro s’effectue simplement par comparVal r1, r2, #4 , Les registres r1 et r2 contiendront les 2 adresses nécessaires et la valeur de test sera passée comme une valeur immédiate.
Remarque : les macros ne sont pas à utiliser pour des actions trop compliquées !! Mais si vous aimez mes macros, vous pouvez vous constituer une collection et les grouper dans un fichier externe que vous appellerez avec l’instruction .include « nomdufichier ».
Voici un autre exemple : pour l’affichage des registres que j’utilisais depuis le début, j’affichais comme libellé l’adresse de l’instruction appelante car il n’est pas possible avec cet assembleur de récupérer le N° de ligne du source ce qui aurait été bien pratique. Avec une macro je peux donc récupérer un libellé comme paramètre, le stocker sur la pile pour l’afficher sans modifier les valeurs des registres. Bon il faut un peu bidouiller avec un registre puisque le push exige l’utilisation d’un registre. Voici le corps de la macro :
/* macro d'enrobage du vidage des registres  avec étiquette */
.macro vidregtit str
    push {r12}    @ save r12
                mrs r12,cpsr  /* copie du registre d'état  dans r12 */
                push {r12}   @ save du registre d’état sur la pile
                ldr r12,=lib1\@  @ recup libellé passé dans str
                push {r12}    @ passage argument sur la pile
                ldr r12,[sp,#8]   @ on remet en état r12 pour l'afficher correctement
                bl affregistres  @ affichage des registres  (cette routine restaure l’état de la pile)
                pop {r12}     @ on récupére le save du registre d’état
                msr cpsr,r12    /*restaur registre d'état */
                pop {r12}         @ on restaure R12 pour avoir une pile réalignée
                b smacro1vidregtit\@   @ pour sauter le stockage de la chaine.
lib1\@:  .asciz "\str"            @ stockage dans le corps du programme du libellé passé en paramètre
.align 4                       @ il faut aligner l’étiquette suivante car le libellé précedent n’est pas un multiple de 4
smacro1vidregtit\@:     @ pour continuer le reste du programme
.endm   @ fin de la macro
J’en ai aussi profité pour sauvegarder le registre d’état avant l’appel de la routine de vidage et le restauré à la fin pour que cette routine reste neutre.
L’appel se fait simplement par vidregtit  debut   ou vidregtit  Après_appel_compar_3
Et le résultat est le suivant :


Vous remarquerez que le registre r2 contient 0xD alors que l’on s’attendrait à trouver une adresse comme dans r1.
Et d’ailleurs r0 contient aussi 0xD !!  Donc il faut remonter les appels pour s’apercevoir que ComparVal utilise en dernier afficheMessage qui utilise r2 pour calculer la longueur du dernier message  donc ici 12 caractères (ne pas oublier de compter le \n final !!!). Et l’appel système Write (code 4) retourne dans le registre r0 la longueur du message affiché (revoir la doc des call system Linux). Donc c’est logique !!!

Remarque avril 2018: en utilisant cette macro sur un gros programme, le compilateur signale une erreur sur l'instruction  ldr r12,=lib1\@  @ recup libellé passé dans str . En effet dans un gros programme l'instruction de chargement d'une adresse avec le signe égal ne fonctionne pas. J'ai eu du mal à comprendre pourquoi puisque l'adresse du libellé ne se trouve que quelques instructions plus loin dans la même section. En fait l'instruction ldr r12,=lib1 est une pseudo instruction qui crée en fin du code programme, une variable supplémentaire contenant l'adresse de lib1. Et cette variable pour un gros programme peut se trouver au delà des 4K mots fatidiques. Donc j'ai remplacé cette instruction par l'autre pseudo instruction : adr r12,lib1   (et sans le =) qui fonctionne dans tous les cas. Cette instruction signifie de charger dans r12 l'adresse du libellé lib1.