lundi 18 décembre 2017

Chapitre 13 : dessiner en assembleur avec le FrameBuffer -1-



Dans la première partie, nous avons découvert les principes pour réaliser des petits programmes en assembleur avec des données en entrée et des données en sortie dans la console ou dans des fichiers. Maintenant nous allons essayer de découvrir des utilisations plus poussées sur le Raspberry : dessin, gestion de fenêtres, gpio.
Déjà, nous avons pu remarquer que dans les instructions assembleur ARM, il n’y a pas d’instructions d’entrée-sorties pour accéder à des interfaces divers : écran, clavier, gpio, usb , réseau. Et donc tout me reste à découvrir.
Voyons déjà comment dessiner sur l’écran du Raspberry. Les premières recherches pour lire et écrire directement dans les adresses mémoires réservées à l’affichage sont des échecs. En effet, un mécanisme de protection géré par Linux interdit à un utilisateur non habilité à accéder aux adresses mémoire importantes pour le fonctionnement de l’ordinateur. Mais une autre solution est proposée, c’est d’utiliser une notion appelée frameBuffer  qui permet d’accéder à ces adresses en effectuant un mapping (mappage ?) entre les adresses physiques et des adresses en mémoire dont l’accès est autorisé. Voyons le source du programme.
Nous allons commencer par afficher quelques caractéristiques de l’affichage et pour cela nous décrivons 2 structures de données différentes, une pour les informations fixes de l’écran, l’autre pour les informations variables.
Dans la partie .data, nous décrivons les messages pour afficher les informations principales et dans la section .bss, nous réservons la place pour le stockage des 2 structures définies plus haut. En effet les structures ne servent qu’à attribuer des noms et des déplacements à des zones en fonction de leur emplacement et il faut donc réserver la place en mémoire pour attribuer un pointeur qui nous servira lors des appels.
Dans la partie code, nous ouvrons le périphérique concernant notre écran d’affichage. En fait c’est comme ouvrir un fichier mais avec un nom standard : /dev/fb0. Et pour nous permettre de travailler dessus, nous lui attribuons les autorisations de lecture écriture. Ensuite nous allons utiliser l’appel system IOCTL qui permet d’obtenir les informations sur un périphérique à l’aide de codes fonction spécifiques, d’une part les données fixes qui vont alimenter la structure fix_info puis les données variables dans la structure var_info. Comme exemple, nous affichons quelques données : identification, largeur et hauteur de l’affichage et nombre de bits par pixel.
Puis nous allons effectuer un mapping du périphérique dans une zone mémoire en utilisant l’appel systeme MMAP. Il faut lui fournir plusieurs paramètres dont la taille en octet nécessaire. Nous trouvons cette information dans les données fixes récupérées auparavant (zone FBFIXSCinfo_smem_len). Vous remarquerez que pour toutes ces opérations, nous testons le code retour des appels pour vérifier si tout se passe bien, cela évite de chercher trop longtemps en cas de dysfonctionnement. Le mapping permet de lire et modifier le contenu de l’affichage dans la mémoire et les modifications sont répercutées dans le périphérique. (il faudrait approfondir ce mécanisme et voir aussi le rôle de l’appel système SYNC).
Enfin pour vérifier si tout cela permet d’intervenir sur l’affichage, nous nous contentions de modifier les pixels sur un huitième de l’écran avec une couleur bleue (code rgb 0xFF). Évidement quand vous lancez ce programme depuis la console de la connexion ssh, rien ne se passe. Donc dans un premier temps, j’ai installé le serveur tightvncserve sur le raspberry et sur mon ordinateur portable pour afficher les images écran en mode graphique.
Cela n’a rien donné, j’ai donc vérifié plusieurs fois le programme, et j’étais désespéré de ne rien obtenir. Puis j’ai fait un dernier test en branchant directement un écran sur le Raspberry et là miracle !! les pixels bleu sont apparus. Je pensais que cette méthode d’affichage ne fonctionnait que sur un écran connecté au Raspberry par la prise HDMI. Mais en rédigeant ce post, je me suis repenché sur le problème. En fait le serveur tightvncserver utilise un port différent (display :1) de celui utilisé pour l’affichage direct (display :0) ce qui explique le dysfonctionnement. Mais j’ai trouvé sur le blog de framboise14 un post sur l’utilisation du serveur X11vnc qui permet d’afficher le display :0 à distance et cela fonctionne correctement.
Et maintenant quelle suite à donner : et bien vous savez modifier des pixels !! donc il ne reste plus qu’à écrire toutes les fonctions d’affichage classiques ( pixels, droites, cercles, polygones etc etc.) car aucune fonction du système n’est disponible pour cela !!!!   ou alors il faut utiliser les fonctions d’une librairie graphique.
Vous trouverez des exemples d’affichage par cette méthode en langage C sur le site : http://raspberrycompote.blogspot.fr/2013/ et il faudra adapter les programmes à l 'assembleur.
Exercices : Modifier le programme pour afficher des trames de couleurs différentes.
                 Pour ceux qui aiment les algorithmes, écrire les fonctions pour tracer une droite, un cercle une ellipse, un rectangle etc etc.

mardi 5 décembre 2017

Réflexions sur cette première partie



Ces premiers chapitres nous ont permis de découvrir les instructions de l’assembleur et son intégration dans le système Linux. Cela aussi nous permet de dresser quelques consignes pour faciliter la programmation :
-          Commenter chaque instruction car il est difficile quelques temps après de comprendre le fonctionnement.
-          Programmer des routines d’une cinquantaine de lignes et sinon la décomposer en plusieurs sous routines
-          Utiliser les registres r0 à r4 pour les calculs courants, les registres r5 à r7 pour les indices de boucles et les registres plus élevés pour des sauvegardes de registre ou pour des données utilisées tout le long du programme.
-          Préparer des squelettes de programmes qui serviront de modèles.
 Et nous avons découvert quelques erreurs dont les plus fréquentes sont :
Confondre la récupération d’une adresse avec sa valeur : ldr r0,=donnée  et ldr r0,donnée.
Oublier le b derrière le ldr ou le str pour travailler sur un seul octet
Oublier le s pour mettre à jour les flags après une opération.
Utiliser un registre déjà utilisé par ailleurs.
Ne pas gérer correctement la pile lors du retour de fonctions.
Ne pas sauvegarder (ou restaurer) le registre lr dans une sous routine.
Oublier la partie lsl #2 dans une instruction de stockage ou de lecture d’un poste dans une table de pointeurs.
Confondre les codes conditions après un test (blt, bgt, ble, bge)
Confondre le branchement à une sous fonction (bl) avec le branchement après un test (ble)
Essayer de modifier une zone située dans la partie code (Il n’est possible que d’y mettre des constantes).
Oublier que les octets d’un registre sont stockés dans l’ordre inverse en mémoire.
Confondre l'affectation d'une constante avec une reservation de place :
toto: .int  200 * 4    @ affecte la valeur 800 à l'adresse toto sur 4 octets.
truc: .skip 200 * 4  @ réserve 800 octets de mémoire commençant à l'adresse truc 


Dans la première partie, nous avons découvert les principes pour réaliser des petits programmes en assembleur avec des données en entrée et des données en sortie dans la console ou dans des fichiers. Maintenant nous allons essayer de découvrir des utilisations plus poussées sur le Raspberry : dessin, gestion de fenêtres, gpio.
Déjà, nous avons pu remarquer que dans les instructions assembleur ARM, il n’y a pas d’instructions d’entrée-sorties pour accéder à des interfaces divers : écran, clavier, gpio, usb , réseau. Et donc tout est à découvrir.
 



samedi 25 novembre 2017

Chapitre 12 :Utilisation données locales sur la pile et exemple de tri



Nous avons utilisé la pile pour passer des paramètres dans des routines et nous allons voir l’utilisation de la pile pour stocker des données locales, cela permettant d’appeler une routine dans elle-même (appel récursif). Nous allons utiliser le générateur de nombres aléatoires du chapitre précédent légèrement modifié pour générer 100 nombres que nous stockerons dans une table puis nous trierons cette table en utilisant une routine de tri rapide.
Voyons le source du programme :
Nous commençons par générer 100 nombres aléatoires que nous plaçons dans une table. Le générateur calcule un nombre aléatoire comme précédemment mais nous prenons les chiffres à gauche en fonction de la plage à la place des chiffres à droite (ce qui devrait produire des nombres plus aléatoires).
Pour mesurer le temps du tri, nous allons effectuer un call system pour positionner le temps avant le tri et nous effectuerons la même opération après le tri et nous calculerons la différence. Sur mon raspberry, l’appel system TIME (code 0x0D ) n’est pas implémenté et j’ai donc utilisé l’appel GETTIMEOFDAY (code 0x4E).
Nous appelons ensuite la routine de tri et nous terminons par une vérification que la table est bien triée. Nous balayons la table en comparant 2 postes successifs et nous signalons une erreur si le premier est plus grand que le second.
Dans la routine de tri, nous nous contentons de sauvegarder les registres qui vont être utilisés et nous appelons la sous routine tri. Dans celle-ci après la sauvegarde habituelle des 2 registres fp et lr sur la pile, nous réservons sur la pile 16 octets par l’instruction sub sp,#16 (rappel ; les adresses de la pile décroissent). Et nous positionnons le registre fp (frame pointer) à l’adresse de la pile. Cela nous permet de manipuler les données de la zone réservée facilement.
Le tri rapide utilise un algorithme de partitionnement de la table à trier en 2 sous tables qui seront-elles mêmes partitionner en deux en utilisant un appel récursif de la même sous routine. Avant chaque appel, les pointeurs gauche, droite et courant et l’adresse de la table sont sauvegardé sur la pile dans la zone réservée. Le registre fp permet d’utiliser ces adresses sans difficulté.
Le début de la table est affiché avant et après le tri pour vérification ainsi que le temps mis pour le tri. Ce tri est efficace puisque sur mon raspberry il trie 1000000 de nombres en 1,5 secondes. Attention, ce tri peut utiliser une grande partie de la pile si les données sont mal réparties (voir la théorie sur ce type de tri).
Exercice : Adapter le tri pour trier dans l’ordre décroissant
                Écrire d’autres routines de tri et comparer le temps d’exécution.
                   Modifier le tri pour trier des chaines de caractères.

mercredi 22 novembre 2017

Chapitre 11 : Calcul en virgule flottante et nombres pseudo aléatoires



Pour effectuer des calculs en virgule flottante en utilisant les instructions assembleur, le programme ci après va consister à générer des nombres pseudo aléatoires et à calculer la moyenne, l’écart type et le khi afin de vérifier la validité du générateur.
Je dois vous avouer que ce générateur est très simple et donc que les nombres générées ne sont pas très aléatoires (voir la théorie sur la génération de ces nombres et les livres de Knuth).
Le programme vous semble aussi très long mais j’ai voulu afficher les résultats en écrivant une routine d’affichage des nombres en virgule flottante simple précision et c’est assez compliqué (et certainement cette routine peut être améliorée).
ATTENTION remarque du 10/09/2020 : la routine d'affichage est fausse pour de grands exposants (certainement > 32). Une nouvelle routine pour les nombres en double précision est en cours de test et sera publiée dans le chapitre 90. Le chapitre 88 donne une routine qui semble plus exacte à ce jour pour l'assembleur 64 bits.
  
Lors de la première compilation de ce programme, le compilateur signale que les instructions en virgule flottante ne sont pas autorisées sur le processeur de mon Raspberry. Alors que la documentation précise bien que c’est possible et donc après recherche, j’ai découvert qu’il fallait ajouter la directive suivante

-mcpu=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard

dans le script d’assemblage. Donc j’ai écrit un nouveau script de compilation et j’ai aussi modifié le link pour qu’il s’exécute par gcc car le programme fait aussi appel à la fonction printf du C.
Après les déclarations habituelles des messages et des zones nécessaires, nous initialisons les registres nécessaires au calcul. En virgule flottante simple précision, nous trouvons de nouveaux registres s0 à s31 et en double précision les registres d0 à d15 ( mais attentions les registres d recouvrent les registres s. Voir le chapitre 13 sur http://thinkingeek.com/arm-assembler-raspberry-pi/). Nous allons effectuer les calculs en simple précision et donc nous utiliserons les registres s. Pour alimenter s3, il suffit de copier le registre r0 par l’instruction vmov s3,r0  puis il faut effectuer une conversion par l’instruction vcvt.f32.s32 s3, s3  (f32 et s32 indique que nous travaillons en simple précision) car les nombres ont une structure très particulière : norme IEEE754.
En simple précision, les nombres font 32 bits : le bit à gauche donne le signe puis les 8 bits suivants représentent l’exposant et les 23 derniers bits la mantisse (pour plus de précisions voir la norme IEEE754)
Après avoir initialisé les 3 registres s2, s3 et s4 (surtout de jamais oublier d’effectuer la conversion). Nous préparons une table qui va compter le nombre de tirage pour un chiffre aléatoire tiré, table nécessaire pour le calcul du KHI.
Puis nous avons une boucle qui va effectuer MAXI tirages en appelant la fonction de génération. Nous passons à cette fonction la valeur PLAGE et nous aurons en retour dans r0 un nombre pseudo aléatoire compris entre 0 et PLAGE. La fonction de génération est simple puisque nous nous contentons de multiplier une graine de départ par un nombre donné et d’y ajouter un autre nombre pour calculer une nouvelle graine. Ensuite nous effectuons une division par la plage demandée et nous retournons le reste comme nombre aléatoire.
 Nous copions ce nombre dans le registre s1, et effectuons sa conversion pour le diviser par le nombre de tirage et l’ajouter dans le registre de totalisation de la moyenne avec les instructions de calcul en virgule flottante (vdiv, fmul, fadd ). Nous calculons aussi l’écart type en virgule flottante. Et pour chaque tirage, nous l’ajoutons aussi dans la table des tirages.
En fin de boucle, nous calculons la moyenne en nombre entier (en fait simplement pour voir la différence avec le calcul en virgule flottante) et nous l’affichons par notre fonction d’affichage, nous terminons le calcul de l’écart type (élévation au carré et calcul de la racine carrée) et nous l’affichons avec la fonction printf du C. Mais cette fonction n’admet que les nombres en double précision passés dans les registres r2 et r3. Il nous faut donc convertir notre résultat par l’instruction vcvt.f64.f32  d5, s3  (noter le f64 et l’utilisation du registre d5) puis alimenter les registres par vmov r2,r3,d5 pour appeler la fonction du C printf. Cette fonction utilise le format %f pour éditer correctement le nombre indiqué dans la chaine szFormat.
Ensuite nous appelons la routine conversionFloatSP pour convertit le contenu du registre r0 en chaine affichable. J’ai écrit cette routine pour ne pas faire appel à des fonctions du C. Comme je ne suis pas encore un expert en assembleur, cette routine n’est certainement pas l’idéal et il reste des points à améliorer (en particulier les zéros inutiles après la virgule. Elle est aussi compliquée car pour calculer le résultat qui dépasse la taille d’un registre, je suis passé par des zones en mémoire pour effectuer les multiplications et divisions nécessaires. Sur Internet, je n’ai pas trouvé d’exemples de routines en assembleur qui effectuent cette conversion et j’ai dû écrire la mienne à partir de la documentation du format IEEE754.
La routine commence par extraire l’exposant et la mantisse du registre r0 et en fonction de l’exposant va effectuer 4  traitements différents : l’exposant est égal à 255 , l’exposant est égal à 0, l’exposant est supérieur à 127 et l’exposant est inférieur à 127. Pour la suite de la routine, je vous suggère d’aller voir directement le code et les commentaires.
Revenons dans le programme principal pour calculer le coefficient khi  qui permet de juger de la pertinence du générateur aléatoire. Nous repartons de la table dans laquelle nous avons enregistré les comptages de chaque tirage  pour calculer le total des carrés de chaque compteur. Ensuite nous effectuons le reste des calculs en virgule flottante pour afficher le résultat avec la fonction printf du C et avec la routine en assembleur.
Voici le résultat sur mon raspberry :

Comme vous le voyez, la moyenne, l’écart type et le khi sont proches de ceux attendus (50 , 25 et 100).
Pour les calculs en virgule flottante, les instructions à utiliser ne sont pas très complexes, il faut surtout veiller à bien mettre les bonnes extensions .f32.s32  et pensez à effectuer les conversions entier vers float.
Et dans ce programme, je n’ai pas fait appel à des données en virgule flottante. Si c’est nécessaire il faut les coder comme ceci :
pi: .float 0E-3141592E-6

et l’utiliser par ldr r1,=pi
                           vldr s1,[r1]         @ s1 contient pi

et dans ce cas il est inutile d’effectuer la conversion par l’instruction vcvt.
Exercice : tester  un générateur diffèrent pour avoir un khi meilleur
                Améliorer la routine de conversion du nombre en simple précision
              Et plus difficile, écrire une routine de conversion pour les nombres en double précision.
               Et aussi une routine pour saisir dans la console des nombres en virgule flottante.