Triceraprog
La programmation depuis le Crétacé

  • Visite au Computer History Museum ()

    La semaine dernière, profitant d'un voyage à San Francisco, j'ai allongé un peu mon séjour pour pousser jusqu'à Mountain View et aller visiter un musée qui semblait fort intéressant. Le Musée de l'Histoire des Ordinateurs, ou dans le texte « Computer History Museum »

    Dépliant du musée

    Première chose à savoir si vous êtes à San Francisco même, que vous choisissiez le train (pas cher, mais long, avec changement) ou la voiture (plus cher, un peu moins long), prévoyez le trajet pour ne pas arriver trop tard. En effet, le musée ferme assez tôt : 17h.

    En voiture, tant qu'à visiter, vous pourrez faire un détour par la 280 pour passer par des endroits sympas ou la 1 pour des endroits encore plus sympa, à l'aller ou au retour. Ou opter par la 101 pour aller au plus direct (mais potentiellement aussi plus encombrée, vérifiez avant de partir).

    De notre côté (puisque nous étions deux), nous avons opté par un peu de balade sur le chemin.

    La plage

    Arrivé au musée et ayant acquitté notre droit d'entrée, après une petite explication de l'agencement à l'accueil et la remise d'un plan, nous voici parti pour une aventure d'environ quatre heures.

    Coup de bol ? Ce jour là, des étudiants du coin faisaient une démonstration à l'entrée d'appareils fonctionnels et d'artefacts divers (comme un reste de Silicon après découpage de Wafers). Les visiteurs pouvaient donc goûter au plaisir de perforation d'une carte (c'est un plaisir quand c'est dans un musée...).

    Performation de carte

    Puis la visite commence. Un petit film d'introduction peut être visionné, mais je ne l'ai pas fait, j'avais hâte de voir du concret.

    L'exposition principale est divisée en 20 zones thématiques et globalement chronologique. Globalement car les époques recouvertes par les thèmes se superposent dans l'histoire des ordinateurs. La première zone est consacrée au calcul, le besoin initial à la base de tout ça. Règles à calcul, bouliers (que l'on peut manipuler), métier de « calculateur » permettant de dresser les tables de fonctions.

    Règle à calcul

    Mais encore calculatrices, mécaniques tout d'abord puis électroniques (mais non programmables). Et que serait cet endroit sans la réplique de la sous-partie de démonstration de la Machine à Différences de Charles Babbage ? Près de là, les plans de la Machine Analytique du même inventeur, bien entendu avec mention des travaux d'Ada Lovelace.

    Machine à Différences

    Suit un très intéressant reportage sur la culture d'entreprise d'IBM au début du XXième siècle. Le musée est comme cela ponctué de nombreux reportages ou extraits de films, interviews et reportages d'époque. Il y en a vraiment beaucoup et malgré les quatre heures de visite, je n'ai pas tout pu regarder.

    Ce passage est dans la zone 2, consacrée aux cartes perforées, dont IBM fut un grand fournisseur.

    IBM, think

    De là, on passe en zone 3, consacrée aux calculateurs analogiques et des « batailles » qui s'ensuivent entre les modèles numériques et les modèles analogiques, ainsi que des fusions entre les deux systèmes.

    Une machine analogique câblée nous montre bien la similitude avec l'état de certaines bases de code actuelles...

    Calculateur Analogique


    Peut-être préférez-vous le câblage suivant.

    Autre calculateur Analogique


    Les zones suivantes sont consacrées à la naissance des ordinateurs ainsi qu'à l'histoire des premières compagnies à se lancer dans le secteur. On y trouve quelques ordinateurs à lampes de dimensions conséquentes, comme des morceaux d'ENIAC.

    Le musée présente aussi des blocs de bases d'un ordinateur dans leurs formes d'alors, et rappellent que des pistes initiales se tournaient vers la mécanique. Sur l'image suivante, une porte logique mécanique à gauche (a priori un inverseur), et une additionneur/soustracteur à lampe à droite.

    Portes


    La zone suivante, la 6, nous amène aux besoins de calculs en temps réel et nous rappelle les besoins de l'armée, toujours très gourmande en calculs pour la balistique. Et très avide d'information à des fins de surveillance dans une époque de guerre froide. Des éléments du système SAGE sont exposés.

    Système SAGE


    Tout cela demande de la puissance de calcul grandissante, et nous entrons à présent dans la zone 7, dédiée aux Mainframes. L'IBM 360 y trône en bonne place, avec ses beaux dérouleurs de bande rouge et bleus.

    IBM 360


    Et avec des possibilités de calculs grandissante vient les besoins en stockage grandissants eux aussi. La zone 8 s'intéresse à la mémoire, qu'elle soit persistante ou non. En partant de quelques artefacts très lointains, comme un système de stockage d'information Inca utilisant des cordes, on arrive sur des systèmes plus récents. Un mur de stockage présente de nombreux systèmes : lecteurs de disquettes, cartouches, cassettes, disques magnétiques...

    Tout ça, c'est bien beau, mais sans logiciel, cela ne va pas bien loin. C'est la zone 9 qui présente la programmation. Le musée est principalement focalisé sur le matériel, et si le logiciel est toujours dans l'air, ce n'est pas sont point central (voir plus loin pour la section déportée du musée). À un mur, une fresque généalogique des différents langages de programmation s’étale sur quelques mètres. C'est une version cependant très simplifiée, même si les jalons essentiels sont là.

    Une film explicatif constitue la principale attraction de la zone. Il faut dire que ce n'est pas si évident de montrer du logiciel dans un contexte de vieilles machines dont la plupart ne fonctionnent pas et qui étaient utilisées principalement pour des calculs scientifiques, militaires et des traitements administratifs.

    Ah si, tout de même, un badge DECUS rappelle que le principe du partage de logiciel via des sources de code ouvertes ne date pas d'hier, puisque le groupe a été créé en 1961 et que la pratique le précédait.

    DECUS


    Si les Mainframes, c'était du trop petit pour vous, la zone 10 en remet une couche avec une présentation de Super Ordinateurs. Forcément, vu la place que cela prend, il n'y a pas beaucoup de pièces. On y trouve aussi un cluster de PCs. Bien entendu, que serait une section Super Ordinateurs sans Cray.

    Un film rapide retrace au passage la vie de Seymour Cray et des particularités de ses ordinateurs.

    Cray


    Ils ont plein de petites LEDs qui clignotent et des petits leviers tous sympas, ils sont tout minis se sont les... Minis Ordinateurs de la zone 11. Des PDP-8, PDP-11, du HP, du CDC, un modèle de PDP-8 qui a servi pour des opérations chirurgicales du cerveau et a permis d'effectuer celles-ci avec endormissement du patient (car donc, apparemment, auparavant, le patient devait rester réveillé...)

    PDP-8

    Bien évidemment, la section évoque UNIX et présente un manuel d'époque.

    Puis l'on passe en zone 12, consacré à la logique numérique au cœur du fonctionnement de toutes ces machines jusqu'à nos tablettes et téléphones actuels. Quelques portes logiques peuvent être actionnées avec des interrupteurs (dommage, le Flip Flop ne fonctionnait pas à cause d'un bouton défectueux). Un film présente les étapes de fabrication d'un circuit intégré, et l'on peut regarder à la loupe plusieurs générations de ces puces. Quelques wafers de différentes tailles accompagnent le tout.

    La zone 13 est consacrée à la robotique et à l'intelligence artificielle, avec de nombreux robots vintage en exposition. Mes photos de cette section étant toutes floues, passons.

    En zone 14, on découvre la thématique de la création artistique à travers l'outil informatique. Ici, à côté d'un cube de PIXAR, de système SUN, de tables traçantes et de synthétiseurs, on trouve une machine au logo qui aura marqué une époque en image numérique.

    Silicon Graphic


    J'y ai découvert aussi un accessoire pour Commodore 64 que je ne connaissais pas du tout. Le Incredible Music Keyboard, qui se place au dessus de la coque d'un C64 pour le transformer en synthétiseur. J'aurais du mieux regarder la chaîne 8-Bit Keys, qui en fait une présentation.

    Incredible Music Keyboard


    La zone 15 s'intéresse au périphériques d'entrées et sorties. Le coin est plutôt fourni en périphériques exotiques, dont beaucoup étaient des idées... intéressantes. Et dont d'autres ont eu plus de succès. Et quoi de mieux pour accueillir le visiteur dans cette zone que le très à propos Xerox Alto.

    Xerox Alto


    La zone 16 est consacrée aux jeux vidéo. Pas de trucs renversants à ce niveau-là, beaucoup de musées de vieilleries informatiques se focalisant essentiellement sur le matériel de jeu, il est difficile de faire original. Trois pods de démonstrations permettent de jouer à des jeux assez différents : un jeu d'aventure textuel, Spacewar! et PAC-MAN. Les trois en « reproduction » (plutôt des recréations que de l'émulation j'ai l'impression... mais je n'en sais rien).

    Bref, passons ici aussi.

    La zone 17 présente l'ordinateur personnel. Le Micro, ça y est, l'ère grand public (ou presque) commence. C'est un IBM PC qui nous reçoit dans la zone. Vu la taille de ces nouvelles machines, de nombreuses pièces différentes sont présentées. Apple I et II, TRS-80, Commodore PET, Atari 800XL, Lisa,...

    IBM PC

    Mais aussi de l'exotisme (vu des Etats-Unis), avec un Smaky, un Amstrad 464, du Spectrum,... Un clone russe de ZX Spectrum.

    Et un Thomson TO7-70 ! Malheureusement un peu dans l'ombre.

    Thomson TO7-70


    Et puisqu'on est dans l'ordinateur français, un petit Micral.

    Micral

    Toute une partie est à l'honneur du Do It Yourself de l'époque : ces machines que l'on pouvait recevoir en kit, ou bien dont on ne recevait que des parties essentielles, à compléter ensuite, en suivant éventuellement un plan dans un magazine.

    Ici, un Altaïr 8800 côtoie (mais de pas trop près) un Imsai 8080. Mais aussi un EDUC-8 Australien (que je ne connaissais pas), une carte KIM-1 et de nombreux autres.

    Altair 8800


    De la zone 17 on passe à la zone... 18 ! Celle-ci présente l'informatique mobile. En partant des portatifs initiaux (et de publicités où l'on voit des utilisateurs suer avec le sourire en traînant une mallette de plomb signe de leur modernité), on passe par les ordinateurs portables de différentes époques et les organiseurs personnels.

    Le pôle interactif offre de soulever un Osborne 1 dans sa malette, pour juger du temps pendant lequel nous aurions pu garder le sourire en se baladant avec. On est loin des quelques centaines de grammes de tablettes actuelles.

    La zone 19 présente la mise en réseau, en commençant par le commencement : la mise en relation de personnes à distance. Télégraphe, puis téléphone, avec des cartes de l'évolution des lignes reliant les continents.

    Un petit bouton permet de retrouver le délicieux son d'une communication entre ordinateurs via Modem.

    Modem


    Est exposée aussi une des premières armoire à serveurs de Google, tout un tas de modems, de routeurs et autres matériels de communication. Des initiatives aussi, comme les ordinateurs communautaires hippies.

    Côtés ordinateurs associés à cette thématique, un NeXT Cube, mais aussi un Minitel, expliquant que bien avant l'essor grand public d'Internet, en France, il était possible d'aller chercher des renseignements en ligne ou de réserver un train.

    Minitel

    La dernière zone, la vingtième est une zone tournée vers l'avenir. Principalement un film interviewant quelques personnalités de la Silicon Valley. J'avoue ne pas l'avoir regardé. on sort avec cette zone de l'Histoire pour entrer dans l'actuel, et l'actuel est tous les jours.

    C'est ainsi que se termine la visite de l'exposition principale. Mais pas du musée.


    PDP-1 et Spacewars!

    Si la quasi totalité des machines exposées ne sont pas en fonctionnement, le musée possède deux salles consacrées à deux machines emblématiques restaurées et en état de fonctionnement.

    La première salle est celle du PDP-1.

    La vénérable machine n'est cependant allumée que deux fois par mois à un horaire bien précis... Et le jour de ma visite n'était pas un jour de démonstration. C'est dommage, mais tant pis.

    La démonstration consiste, entre autre, à lancer le jeu Spacewars! Il est même possible d'y jouer, avec des contrôles déportés dans la zone visiteur. Le stylo optique est aussi fonctionnel.

    PDP-1


    IBM 1401

    La seconde salle est encore plus vaste et est utilisée pour une installation d'IBM 1401. Un film explique l'histoire de sa restauration, qui aura été longue et fastidieuse.

    Là encore, il faut tomber le bon jour et la bonne heure pour la démonstration. C'était le bon jour, mais nous sommes arrivés après la mise en route, qui a lieu le matin.

    Mais même éteinte, la machine est impressionnante.

    IBM 1401


    Le reste

    Et ce n'est pas fini. Mais le musée allait fermer 30 minutes après et j'ai parcouru le reste un peu plus rapidement. J'ai dédié la majeure partie du temps restant à l'exposition sur Ada Lovelace, qui présente quelques manuscrits de sa main et lettres de Charles Babbage à son attention. Qui retrace aussi en quelques panneaux sa vie.

    À côté, une mini expo sur les transports automatisés, qui me semble est surtout une excuse pour présenter la voiture toute ronde de chez Google.

    Le dernier grand espace est dédié au logiciel sous différents aspects : jeu, musique, image, connaissance, simulation, industrie textile. Chaque poste montre l'évolution du travail dans ces domaines avec l'arrivé de l'informatique. Par exemple en juxtaposant le développement de pellicule en chambre noir et Photoshop.

    Exposition plutôt bien faite, mais que j'ai traversée un peu en coup de vent. Je n'étais pas venu pour cela.

    Conclusion

    Ce musée est fantastique. Les quatre heures passées, nullement suffisante pour tout voir dans les détails, sont passées très rapidement. Beaucoup de pièces intéressantes, des explications, des films d'archive, des reportages,...

    Si vous passez dans le coin, n'hésitez-pas !


  • VG5000µ, SetPoint en ASM, diviser ()

    À présent que j'ai un garde fou pour vérifier que je ne fais pas d'erreur d'inattention, me voilà près à diviser des nombres. Pour rappel j'ai besoin de diviser des nombres afin de faire les calculs permettant d'affiche le bon pixel à l'écran.

    Pour second rappel, le Z80, au cœur du VG5000µ (et de beaucoup d'autres ordinateurs de l'époque) n'a pas d'instruction de division.

    La division

    Lorsque je divise de manière entière a par b, je veux trouver le nombre c tel quel \(c * b = a\). Comme la division ne tombe pas toujours juste, j'ai aussi un reste r tel que \(c * b + r = a\).

    Autrement dit, combien de fois dois-je additionner b pour obtenir a (au reste près). Une manière de trouver le résultat est de soustraire b à a autant de fois que l'on peut sans passer sous 0.

    Par exemple, \(\frac{21}{7}\) se trouve comme ceci :

    21 - 7 = 14
    14 - 7 =  7
     7 - 7 =  0
    

    Il y a trois soustractions, et donc \(\frac{21}{7} = 3\)

    Un autre exemple avec \(\frac{17}{3}\) :

    17 - 3 = 14
    14 - 3 = 11
    11 - 3 = 8
     8 - 3 = 5
     5 - 3 = 2
    

    Il y a cinq soustractions, et il reste 2 à la fin, dont la soustraction de 5 donnerait -3. On s'arrête donc et \(\frac{17}{3} = 5 + \frac{2}{3}\)

    Un problème majeur de cette façon de faire est que plus le résultat est grand, plus il a fallu de calculs pour le trouver. Si on divise de grands chiffres par de petits, cet algorithme est lent.

    Il est par contre très facile à programmer. La séquence donne ceci :

    • Mettre le dividende (le nombre à diviser) dans A
    • Mettre le diviseur dans C
    • Mettre 0 dans B
    • Tant que A est supérieur à C
      • Soustraire C à A et mettre le résultat dans A
      • Ajouter 1 à B
    • Le quotient (le résultat de la division entière) est dans B
    • Le reste de la division entière est dans A

    Implémentation par soustractions

    Il y a des limitations bien entendu à faire cette division sur des registres 8 bits. Je ne pourrai pas traiter des nombres supérieurs à 255. Pour afficher un point, je n'en ai pas besoin, c'est donc très bien.

    Tout d'abord, je renseigne mon système de tests avec mes nouvelles informations :

    div3_input_data:
            defb    0,2,10,21,255
    div3_reference_data:
            defb    0,0,3,7,85
    
    div3_params:
            defw    div3_input_data
            defw    div3_reference_data
            defw    div3
            defm    "DIV3\0"
    

    Et j'ajoute le test à la liste :

    test_suite:
            ld      hl,id_params
            call    prepare_test
    
            ld      hl,div2_params
            call    prepare_test
    
            ld      hl,div3_params
            call    prepare_test
    
            ret
    

    Ce qui me permet de mettre au point le code et le vérifier :

    div3:                   ; Entrée: registre A, Sortie: valeur divisée par 3, dans A
            push    bc              ; J'utilise les registres B et C, je les préserve
                                    ; Le temps de l'opération
            ld      c,3             ; Le registre C contient le diviseur
            ld      b,0             ; Le compteur B est préparé pour retenir
                                    ; Le nombre de soustractions effectuées
    
    div3_loop:
            cp      c               ; A et C sont comparés
            jr      c,div3_end      ; Si A était plus petit que C, alors une retenue a eu lieu
                                    ; le calcul est terminé
                                    ; Attention, ici C signifie Carry (retenue)
    
            sub     c               ; C est soustrait de A
            inc     b               ; B est augmenté de 1
            jr      div3_loop       ; La boucle est relancée
    
    div3_end:
            ld      a,b             ; Le résultat de la division est dans B, il est placé dans A
                                    ; À noter que A contenait le reste de la division.
                                    ; Cela pourra être intéressant pour plus tard.
    
            pop     bc              ; Les registres B et C sont restaurés
            ret                     ; Et la fonction est terminée
    


    Les instructions utilisées

    Les instructions déjà utilisées dans les articles précédents ne sont pas répétées ici, les nouvelles sont :

    • cp: permet de comparer le registre A (qui est implicite et donc non noté) et le registre indiqué (ici, C). La comparaison se fait par soustraction de A et du registre, mais le résultat n'est pas retenu, seuls les drapeaux de résultat de l'opération le sont. Ainsi, en cas d'égalité par exemple, le drapeau Z sera à 1 (Zéro, car la soustraction de deux nombres égaux donne zéro). Ici, on cherche s'il y a eu une retenue, ce qui indique que le nombre dans le registre spécifié était plus grand que le nombre dans A, et donc le résultat de la soustraction était négatif.
    • sub: effectue une soustraction simple. On avait précédemment vu sbc, qui faisait une soustraction en tenant compte de la retenue du calcul précédent. sub n'en tient pas compte et soustrait simplement le registre mentionné du registre A, qui est implicite.

    Résultat

    Voici donc une première implémentation de division par 3, qui peut être généralisée à une division par n'importe quel nombre (inférieur à 255)

    Pour référence futur, le code de la fonction nécessite 15 octets de langage machine. L'exécution de la fonction nécessite \(15 + 8 * q\) cycles processeur, où \(q\) est le quotient, résultat de la division. Au pire, pour \(\frac{255}{3}\), la fonction prend donc 695 cycles. Ce qui n'est pas négligeable.

    Nous verrons plus tard si l'on peut faire mieux.

    Affichage des résultats de tests dans MAME avec la division par 3


  • VG5000µ, SetPoint en ASM, vérifier les résultats ()

    Après avoir mis en place une vérification (légère) de l'intégrité de la pile, je passe à la vérification de la validité de l'appel d'une fonction.

    Le fonctionnement du test est assez simple : je prends une suite de nombres, j'appelle une fonction avec en paramètre chacun de ces nombres, je vérifie que le résultat est conforme à ce que j'attendais.

    Par exemple, si je veux tester une fonction diviser par 2 (division entière), je peux utiliser la suite de nombre 0, 10, 32, 255 et comparer les résultats respectifs avec 0, 5, 16, 127 (255 étant impair, le résultat de la division entière est 127, avec un reste égal à 1).

    Encore plus simple qu'une division par 2, il y a la fonction identité : celle qui renvoie le paramètre sans le toucher. Tester cette fonction permet de se concentrer sur le développement du test.

    La fonction en elle-même est très simple :

    identity:               ; Entrée: registre A, Sortie : registre A, inchangé
            ret             ; Retour immédiat, on ne touche à rien
    


    La boucle de test

    La boucle de test initialise deux pointeurs de données qui vont être augmentés en parallèle. La donnée source sera envoyée à la fonction, via le registre A, puis le résultat, mis dans le registre A aussi, sera comparé à la valeur attendue.

    Il nous faut donc en premier lieu la liste de ces valeurs :

    identity_input_data:
            defb    0,10,32,255
    identity_reference_data:
            defb    0,10,32,255
    

    La boucle en elle-même ressemble à cela :

            ; Fonction de test
    test:
            ld      hl,identity_reference_data      ; HL pointe sur les résultats de références
            ld      de,identity_input_data          ; DE pointe sur les données en entrées
    
            or      a,a         ; Effacement du drapeau de retenue (voir article précédent)
            sbc     hl,de       ; Par soustraction des deux valeurs, on obtient le nombre de valeurs
                                ; de la série.
    
            ld      b,h
            ld      c,l     ; BC contient le nombre de valeurs à tester
    
            ld      hl,identity_reference_data      ; HL est pointe à nouveau sur le résultat de référence
    
    test_loop:
            ld      a,(de)      ; Chargement dans l'accumulateur de la valeur pointée par DE
            call    identity    ; Appel de la fonction
                                ; Au retour de la fonction, A contient le résultat de la fonction
    
            cpi             ; Compare A avec (HL), incrémente HL et décrémente BC
                            ; Si BC passe à 0, le bit d'overflow (V) est mis à 0 ; 1 sinon
                            ; Si A et (HL) sont identique, le flag Zero est mis à 1
    
            jr      nz,test_failed  ; Si A et (HL) étaient différent, saute à test_failed
    
            inc     de              ; Sinon, incrémente DE manuellement
    
            jp      v,test_loop     ; S'il reste des valeurs (BC > 0), on boucle
                                    ; DE et HL pointant à présent sur la paire de valeurs suivantes
    
            ld      hl,test_pass_msg        ; Arrivée ici, toutes les paires de valeurs ont été
                                            ; vérifiée avec succès. HL pointe donc sur le message
                                            ; de succès.
            jr      print_test_result_msg   ; Et on saute à l'affichage.
    
    test_failed:
            ld      hl,test_fail_msg        ; Arrivée ici, une comparaison a échouée, HL pointe
                                            ; donc sur le message d'échec.
    
    print_test_result_msg:
            call    print_str               ; On affiche le message contenu dans HL
            ret                             ; Le test est fini !
    
    test_pass_msg:
            defm "Pass!\r\0"
    
    test_fail_msg:
            defm "Fail!\r\0"
    


    Les instructions utilisées

    Les instructions déjà utilisées dans l'article précédent ne sont pas répétées ici, les nouvelles sont :

    • defb : qui est une directive pour l'assembleur, indiquant de réserver de la place mémoire et de l'initialiser avec les octets qui suivent,
    • cpi : instruction de comparaison qui effectue plusieurs actions d'un coup, comme indiqué dans le commentaire ci-dessus. Cela contraint l'utilisation des registres HL, BC et A, qui sont spécialisés ainsi (HL pour un pointeur de mémoire, BC comme compteur et A pour l'accumulateur).
    • inc : incrémente la valeur du registre en paramètre, c'est-à-dire lui ajoute 1.
    • jp : saut (jump) au label indiqué. La différence avec l'utilisation de jr est dans l'encodage de l'adresse de destination. Sans entrer dans le détail, jr est plus condensé que jp, car il n'encode pas l'adresse complète mais seulement un déplacement court. Cependant, il n'est pas possible d'utiliser le drapeau de dépassement de capacité (V) n'est pas utilisable avec jr.


    Et la division par 2 ?

    À présent, il devient facile de tester différentes fonctions. Il suffit de la fonction elle-même, de la paire de liste de valeurs, et de remplacer l'appel de le fonction dans le test.

    div2:                   ; Entrée: registre A, Sortie : valeur divisée par 2, dans A
            or      a       ; Effacement du drapeau de retenue
            rra             ; Rotation du registre A vers la droite, en passant par la retenue
            ret
    
    div2_input_data:
            defb    0,10,32,255
    div2_reference_data:
            defb    0,5,16,127
    

    Et par exemple, si vous aviez, par étourderie comme moi, utilisé rrca plutôt que rra, le test échoue sur la division par 255.


    Généralisation

    Mais changer les pointeurs à chaque test de fonction, ça n'est pas pratique. C'est la grande différence entre des tests automatisés, qui peuvent rester à demeure et que l'on peut lancer régulièrement pour s'assurer que l'on construit un programme sur des fondations solides, et le test manuel, de temps en temps, pour s'assurer du fonctionnement en un point donné, et que l'on doit remettre en place manuellement à chaque fois.

    Bref, il me faut généraliser ça avec, par exemple, la boucle de test qui prendrait en entrée les pointeurs nécessaires. Et pourquoi pas, même, un nom explicatif de la fonction testée sur le moment.

    Ce que je voudrais, c'est quelque chose comme ceci :

    test_suite:
            ld      hl,id_params
            call    prepare_test
    
            ld      hl,div2_params
            call    prepare_test
    
            ret
    
    id_params:
            defw    identity_input_data
            defw    identity_reference_data
            defm    "IDENTITY\0"
    
    div2_params:
            defw    div2_input_data
            defw    div2_reference_data
            defm    "DIV2\0"
    

    Il faut pour cela adapter un peut la routine test pour aller piocher les valeurs depuis HL, qui devient le paramètre d'entrée.

    Tout d'abord, la préparation des paramètres du test va mettre sur la pile les paramètres indiqués.

    Note : il y a de multiples choix pour passer les paramètres des tests à la fonction. Mais aussi beaucoup de contraintes sur les instructions disponibles. Passer par la pile grâce à une fonction d'aide est assez simple à implémenter et lisible. Mais loin d'être le plus rapide.

    prepare_test:
            ld      b,3                 ; B sert de compteur, on va mettre les trois premières adresses sur la pile
    prepare_test_loop:
            ld      e,(hl)              ; Récupération de la première partie de l'adresse
            inc     hl
            ld      d,(hl)              ; Récupération de la seconde partie de l'adresse
            inc     hl
    
            push    de                  ; DE contient l'adresse, qui est poussée sur la pile
    
            djnz    prepare_test_loop   ; DJNZ décrémente B et, si B n'est pas égal à zéro, retourne au label indiqué
                                        ; C'est la manière canonique d'effectuer des boucles
    
            push    hl                  ; La dernière adresse est poussée directement, car elle pointe sur la chaîne de caractères,
                                        ; sans indirection.
    
            jp      test                ; ici, on devrait faire un CALL à la routine de test. Mais ce CALL serait immédiatement
                                        ; suivi d'un RET. Dans ce cas-ci, on peut remplacer le CALL par un JP.
                                        ; Si vous avez bien compris ce que font CALL, RET et JP, alors vous devriez comprendre
                                        ; pourquoi.
    

    À présent, à l'appel de la routine test, il y a sur la pile, dans l'autre du plus « haut » vers le plus « bas » : l'identifiant sous forme de chaîne de caractères, l'adresse de la fonction à appeler, le pointeur de données de références, le pointeur de données en entrée.

    Il s'agit de récupérer tout cela.

    Voici le début de la routine modifiée, le reste ne change pas :

    test_sep_msg:
            defm ": \0"                 ; Une chaîne de caractère, voir plus loin
    test:
            pop     hl                  ; La première opération consiste à afficher l'identifiant
            call    print_str
    
            ld      hl,test_sep_msg     ; Suivi de la nouvelle chaîne de caractère, pour afficher les deux points
            call    print_str
    
            pop     hl                  ; La valeur suivante récupérée est l'adresse d'appel de la fonction
                                        ; Les appels indirects sur un Z80 ne sont pas naturels, il n'existe pas de CALL
                                        ; à une adresse non préalablement fixée.
    
            ld      (call_func+1), hl   ; Du coup, on profite du fait d'être en RAM pour modifier le code à la volée
                                        ; en modifiant directement l'adresse du CALL à la fonction.
                                        ; Cela ne serait pas possible avec un programme en ROM par exemple, mais il
                                        ; existe plusieurs autres possibilités (utilisation de vecteurs et
                                        ; modification manuelle de la pile par exemple)
    
                                        ; Ce genre de manipulation vient avec des contraintes, mais qui dans notre cas
                                        ; sont tout à fait acceptables.
    
            pop     hl                  ; Récupération de l'adresse des données de référence
            pop     de                  ; Récupération de l'adresse des données en entrée
    
            push    hl                  ; Sauvegarde temporaire de HL
    
            or      a,a                 ; Le calcul du nombre de données, comment avant
            sbc     hl,de
    
            ld      b,h
            ld      c,l
    
            pop     hl                  ; Récupération de la sauvegarde temporaire de HL
    
    test_loop:
            ld      a,(de)
    
    call_func:
            call    $0000               ; Ici, le CALL à l'adresse $0000 sera modifié dynamiquement par
                                        ; la manipulation décrite ci-dessus. Lors de l'exécution de cette instruction,
                                        ; c'est donc bien la fonction spécifiée qui sera appelée.
    


    Résultats

    En situation réelle, il est très peu probable que j'utilise des fonctions identité ou division par 2. Les calculs seront faits sur place. Cependant, tester ces fonctions m'ont permis de développer mon petit framework de tests, assez minimaliste, et cela va m'être bien utile pour la suite, pour attaquer la division.

    Affichage des résultats de tests dans MAME


  • VG5000µ, SetPoint en ASM, vérifier la pile ()

    Il y a maintenant pas mal de temps, j'avais implémenté, en BASIC, une routine pour afficher un point à l'écran. Puis de là, une routine pour tracer une ligne, puis un cercle. Le constat était que c'était très lent. Le BASIC interprété est déjà plutôt lent de manière générale, et celui du VG5000µ n'est pas particulièrement rapide.

    Il y a plusieurs raisons à cela, et ce sera peut-être le contenu d'articles futurs.

    Mais en attendant, et après tous les efforts pour se confectionner un environnement de travail pour développer en assembleur, je repars sur l'implémentation du tracé d'un point à l'écran, et cette fois-ci, en assembleur.

    L'avantage d'un programme écrit dans un langage de « haut niveau », comme le BASIC, est de simplifier bien des choses. Faire une division d'une variable A par 3 par exemple, peut s'écrire B = A/3. C'est simple, concis, lisible.

    Traduire cela en assembleur n'est pas toujours simple. Diviser par un nombre quelconque n'est pas direct car le processeur Z80 n'a pas d'instruction de division. Le processeur possède des instructions pour additionner et soustraire, mais pas pour multiplier, ni de diviser.

    Diviser par des nombres en particuliers est parfois simple, comme diviser par 2, qui consiste à décaler tous les bits d'un nombre binaire « vers la droite », tout comme diviser par 10 dans notre arithmétique courante consiste à décaler tous les chiffres vers la droite (ou supprimer l'unité, si vous préférez).

    Pour 30 par exemple, en décimal, une vision par 10 donne 3. En binaire, le même nombre s'écrit 00011110 (sur 8 bits) et sa division par 2 donne 00001111, c'est-à-dire 15 en décimal. Diviser (ou multiplier) par des multiples de la base dans laquelle on représente les nombres est simple.

    Cependant, dans le calcul du pixel à afficher, j'avais une division par 3. Et là... c'est plus compliqué.

    La première étape, puisque je pars de zéro est d'implémenter une division. Puisque le calcul n'a besoin que de diviser par 2 et par 3, et que la division par 2 est simple, je vais me contenter d'une division par 3.

    Mais STOP. Si vous suivez ce blog, vous avez peut-être vu que j'aime essayer de transposer des techniques modernes sur d'anciennes machines. C'est de la rétro-programmation anachronique, mais qui peut convenir au fait que, de toute façon, programmer ce genre de machines depuis un ordinateur actuel est anachronique.

    Une technique moderne de développement (qui a ses détracteurs) est de guider sa programmation à travers des tests qui peuvent, à chaque instant, indiquer si une erreur apparaît. La méthode est globalement de : 1/ écrire un test... qui échoue 2/ écrire le minimum pour faire passer ce test 3/ améliorer (sans ajouter de nouvelle fonctionnalité).

    Le point 3/ en particulier, permet de tester des choses en étant certain que ce que l'on a programmé ne « casse » pas. Suite à une optimisation un peu trop cavalière par exemple. Je ne suis pas un spécialiste de l'écriture de programmes en assembleur Z80, et un outil qui me permet de vérifier que mon changement ne casse pas tout m'intéresse.

    Puisque je vais implémenter une fonction, mon environnement de test prendra en entrée une suite de nombres, y appliquera la fonction puis, testera que les résultats sont ceux que j'attends. J'affiche ensuite le résultat de la comparaison.

    Techniquement, un tel système de tests devrait lui-même être testé... mais à si bas niveau, c'est aller un peu trop loin pour ce que je veux faire.

    Le premier test

    La première chose dont je veux m'assurer, c'est que la pile à la sortie de mon traitement soit dans le même état qu'au début. En effet, si ce n'est pas le cas, il va se passer des choses probablement à classer dans le domaine du « mal ». Dans le meilleur des cas une erreur bizarre, dans le pire (et souvent), un reboot de la machine.

    Aparté: je crois n'avoir jamais parlé de la pile dans un article précédent. Très rapidement, c'est un endroit en mémoire où l'on peut stocker des informations sous forme de pile (imaginez une pile d'assiettes). La dernière donnée mise sur le pile est aussi la première que l'on lira par la suite. Cette pile est entre autre la moyen lors de l'appelle d'une routine d'en revenir. À l'appel, l'adresse du code appelant est mis sur la pile. Pour retrouver cette adresse, il faut donc que l'état de la pile soit le même en entrée et en sortie de fonction.

    Voilà la première partie de la vérification de l'état de la pile.

            ld      hl,0        ; Il n'est pas possible sur Z80 de prendre le pointeur de pile pour le mettre dans un registre
            add     hl,sp       ; l'astuce est donc d'ajouter à 0 la valeur du pointeur de pile, en deux étapes.
            push    hl          ; Et je pousse la valeur du pointeur de pile dans la pile
    

    Il y a à présent en haut de la pile une valeur arbitraire suivi de l'adresse de la pile en début de fonction.

    Voici ensuite la seconde partie de la vérification de l'état de la pile. C'est un peu plus long car il y a la vérification ainsi que l'affichage du résultat.

            pop     hl                  ; Récupération de la valeur depuis la pile
            or      a,a                 ; Reset de la retenue
            sbc     hl,sp               ; Soustraction de cette valeur avec le pointeur de pile
            jr      nz,print_stk_fail   ; Si le résultat n'est pas zéro, c'est qu'on n'a pas trouvé la bonne valeur dans la pile
                                        ; Dans ce cas, saut à print_stk_fail
    
                                        ; Si tout ce passe bien, c'est à dire que la pile n'a pas été corrompue et qu'elle
                                        ; est au même « niveau » qu'au début, alors...
            ld      hl,stack_ok         ; on charge dans HL le message de succès
            call    $36aa               ; et on l'affiche
    
            ; [...]                     ; On verra plus tard ce qui est ici.
    
            ret
    
    print_stk_fail:                     ; On arrive ici en cas d'échec
            ld      hl,stack_fail       ; On charge dans HL le message d’échec
            call    $36aa               ; et on l'affiche
    
                                        ; Mais on ne peut pas sortir comme ça de la fonction. Puisque la pile n'est pas dans
                                        ; le même état qu'au début, l'instruction `RET` ne va pas trouver l'adresse ramenant
                                        ; au programme appelant, et cela ne va rien amener de bon (un reboot très souvent)
    endless_loop:
            halt                        ; Alors on arrête tout... ou presque. L'instruction HALT va arrêter le système jusqu'à
                                        ; la prochaine interruption, qui est, sur VG5000µ, l'interruption d'affichage.
            jr      endless_loop        ; Puis à la fin de l'affichage, ou boucle à l'infinie.
                                        ; Cela permet de voir le message d'erreur.
    
    stack_ok:
        defm "Stack Pass!\0"
    
    stack_fail:
        defm "Stack Fail!\0"
    

    Les instructions utilisées

    Lire de l'assembleur peut être un peu déroutant au premier abord. Les explications fonctionnelles sont disponibles en commentaire dans le code ci-dessous, voici à présent les instructions utilisées dans l'ordre de leur apparition :

    • ld : abréviation de load, c'est-à-dire charge. La valeur à droite de la virgule est chargée dans le registre à gauche. Par exemple, après ld hl,0, le registre HL contiendra la valeur 0,
    • add : la valeur de droite (ou le contenu du registre à droite) de la virgule est additionné (add) dans le registre à gauche. Arès add hl,sp, le contenu de HL sera augmenté de la valeur de SP,
    • push : pousse la valeur contenu dans le registre sur la pile, après push hl, le contenu de HL est en haut de la pile, HL n'est pas modifié,
    • pop : est le contraire de push. La valeur en haut de la pile est chargée dans le registre en paramètre, puis la pile est positionné sur l'élément au-dessous. Après pop hl, HL contient le contenu qui était en haut de la pile,
    • or : permet d'effectuer une opération ou. Ici, cependant, cette instruction est utilisée uniquement pour mettre le drapeau de retenue à zéro, à cause de l'instruction suivante,
    • sbc : soustraction avec retenue, la valeur du registre de droite est soustraite de la valeur du registre de gauche. Il n'est pas possible sur Z80 de faire une soustraction sans retenue entre deux registres 16 bits. De là vient la nécessité d'effacer la retenue, au cas où, avec le or précédent. Le résultat est chargé dans le registre de gauche.
    • jr : saut relatif, un branchement, c'est-à-dire une modification d'ordre d'exécution, va être effectué au label indiqué. Dans le code ci-dessus, nz à gauche de la virgule indique que le saut sera conditionné par le résultat du calcul précédent si celui-ci n'était pas nul (not zero). Après jr nz,print_stk_fail, le processeur continuera son exécution à l'emplacement indiqué par print_stk_fail si le résultat n'est pas 0. Dans le cas contraire, le branchement n'a pas lieu et l'exécution continue à l'instruction suivante,
    • call : est un appel de sous-routine. L'emplacement de l'instruction suivante est mise sur la pile (équivalent d'un push) puis un branchement est fait au label indiqué,
    • ret : est le pendant de call, la valeur en haut de la pile est prise pour prochaine instruction grâce à l'équivalent d'un pop. L'exécution continue donc là d'où avait été appelée la routine,
    • halt : arrête l'exécution du processeur. Lorsqu'une interruption matérielle est reçue, le processeur se remet en route.
    • defm : ce n'est pas une instruction du processeur mais une directive pour l'assembleur. Une zone mémoire est réservée et initialisée avec la chaîne de caractères qui suit.

    Garder le contexte

    Cette routine de vérification de la pile modifie quelques registres. Pour laisser les choses dans l'était où elles étaient lorsque ce code sera appelé, il est de bon ton de sauvegarder l'état des registres et de les restituer à la fin. Cela peut se faire par une série de push et de pop. Ce qui donne au final :

            ; Vérification de pile
            org     $7000
    
            defc    print_str = $36aa
    
            ; Sauve le contexte
            push    hl
            push    bc
            push    af
            push    de
    
            ; Enregistre la pile
            ld      hl,$0
            add     hl,sp
            push    hl
            ;
    
            ; Opérations futures...
    
            ; Vérification de la pile
            pop     hl
            or      a,a
            sbc     hl,sp
            jr      nz,print_stk_fail
    
            ; Message en cas de succès
            ld      hl,stack_ok
            call    print_str
    
            ; Restitution du contexte
            pop de
            pop af
            pop bc
            pop hl
    
            ret
    
    print_stk_fail:
            ld      hl,stack_fail
            call    print_str
    loop:
            halt
            jr      loop
    
    stack_ok:
        defm "Stack Pass!\0"
    
    stack_fail:
        defm "Stack Fail!\0"
    

    Résultat

    Pour le moment, pas grand chose, tout se passe bien et un message est affiché indiquant que... la pile est ok.

    Affichage du test de pile dans MAME


  • Automatisation : utilisation de Sublime Text 3 ()

    Dans les articles précédents sur l'automatisation au niveau des outils de développement, j'avais vu comment injecter un programme dans MAME en émulation VG5000µ d'abord grâce au debuggeur manuellement, puis avec un script LUA grâce aux possibilité d'extensions de MAME.

    Ce n'est toujours pas suffisant pour moi. Comme je l'ai déjà écrit dans ces autres articles, je ne veux pas faire ce qu'une automatisation peut faire bien plus facilement, et sans se tromper. Lancer l'assembleur en ligne de commande, puis lancer MAME avec les bons paramètres, cela n'est pas bien compliqué avec un shell moderne (comme fish).

    Un des principaux problèmes que j'y vois, outre que ce sont des opérations manuelles, c'est que ce sont des opérations non documentées. Si je fais une pause dans un projet et que j'y reviens deux mois après (ça arrive bien souvent, surtout pour du hobby), il y a de bonnes chances que je ne me souvienne plus des opérations exactes à faire. J'ai pu prendre des notes, laisser des instructions, écrire un article même. Mais quoi de mieux comme instructions que de laisser des outils qui fonctionneront et seront une source de documentation si besoin ?

    Un premier script assez simple serait de regrouper l'assemblage et le lancement dans un même script. Ou pourquoi pas dans un Makefile.

    J'ai choisi d'ajouter les outils à un environnement Sublime Text 3, qui est l'un des deux éditeurs de texte que j'utilise le plus souvent.

    De la couleur

    Et puisque j'en étais à créer un petit environnement confortable pour mettre au point des programmes en assembleurs pour Z80, j'ai fait un petit détour par la colorisation syntaxique. N'ayant pas trouvé de colorisation adéquat à de l'assembleur tel qu'attendu par z80asm, j'ai écrit une description simpliste que vous trouverez dans le package en fin d'article.

    Il ne gère pas tous les cas, et je pense l'améliorer en fonction des besoins.

    Au passage, Sublime Text 3 a un système de tests pour la colorisation syntaxique qui est extrêmement pratique pour mettre au point le fichier. Le fichier de test est disponible dans le paquetage au côté des autres, pour référence.

    Un système de build

    Deuxième étape, la plus importante, celle qui permet de construire un code objet à partir d'un code source en assembleur. La mise en place de la colorisation syntaxique n'a pas amené que des couleurs à l'affichage. Elle a aussi permis de déclarer l'existence d'un format spécifique, que j'ai appelé z80asm.

    Lorsque l'éditeur de texte ouvrira un fichier .asm, il l'associera par défaut à ce format (sauf si vous avez d'autres associations pour .asm, auquel cas cela sera un choix possible).

    Pour créer un nouveau système de build avec Sublime Text 3, il suffit d'aller dans Tools -> Build System -> New Build System. Un nouveau fichier de description, en JSON, permettra d'indiquer à l'éditeur les actions qu'il devra effectuer lors de la construction du fichier.

    Pour une construction simple, qui appel l'assembleur z80asm se trouvant dans vos chemin de recherche d'exécutables, cela peut donner ceci :

    {
        "selector": "source.asm.z80",
        "cmd": ["z80asm", "-b", "-v", "$file"],
        "file_patterns": ["*.asm"],
        "file_regex": "^Error at file '([^']+)' line (\\d+): ()(.+)"
    }
    
    • selector: indique que ce type de construction est valable pour le type de fichier associé au format source.asm.z80.
    • cmd: indique la commande à exécuter. $file indiquant le fichier actif au moment du lancement de la construction.
    • file_patterns: indique le type de fichier valable pour ce type de construction aussi, au cas où le format de selector n'aurait pas été appliqué
    • file_regex: est une expression régulière identifiant les erreurs de construction, et permettant à l'éditeur de texte d'indiquer ces erreurs directement dans le fichier édité.

    Avec ceci, un Ctrl-B (sous Linux et Windows) transforme votre fichier .asm en fichier du même nom .bin.

    Lancer MAME après le build

    Je voulais aller un peu plus loin et avoir la possibilité de lancer MAME depuis Sublime Text si l'assemblage était un succès. Pour cela, il m'a fallu écrire un script python un peu plus complexe qui lance l'assemblage de la même manière que ce qui est indiqué au paragraphe précédent, et en cas de succès, lance MAME avec les bons paramètres.

    Il aurait été aussi tout à fait possible, et probablement plus simple, d'appeler un fichier script shell (ou batch). Mais j'avais depuis quelques temps envie de comprendre comment écrire un plugin de build pour Sublime Text, j'ai donc fait un détour.

    Je ne rentrerai pas dans le détail du fonctionnement du plugin de build que vous pourrez trouver dans le paquetage à la fin de l'article.

    Le système est améliorable. Pour le moment, l'adresse de démarrage du code objet est fixé par le script vgboot.lua. Il faudra donc changer cette adresse dans le script à l'endroit de l'injection du code et au moment du CALL si votre code objet est situé à une autre adresse mémoire.

    Dans le fichier README.md, vous trouverez les instructions pour créer le système de build correspondant. Je le reprends ici pour l'expliquer :

    {
        "selector": "source.asm.z80",
        "cmd": ["z80asm", "-b", "-v", "$file"],
        "file_patterns": ["*.asm"],
        "file_regex": "^Error at file '([^']+)' line (\\d+): ()(.+)",
    
        "variants": [
            {
                "target": "z80_asm",
                "name": "run",
                "mame_path": "mame",
                "script": "vgboot.lua"
            }
        ]
    }
    

    Outre le début, qui est identique à la version du paragraphe précédent, une section variants a fait son apparition. Cette section indique des versions alternatives de construction. Le paramètre target indique quel est le plugin à invoquer. Les autres paramètres sont passés tel quel au plugin.

    Les variantes hérites aussi des paramètres principaux, mais mon plugin se contente de les ignorer pour tout gérer seul.

    Pour sélectionner une variante dans Sublime Text 3, le raccourci est Ctrl-Shift-B (sous Linux et Windows).

    Le paquetage

    Les sources du paquetages sont disponibles sur Github mais aussi sous forme de paquetage Sublime Text 3 (qui est en fait un fichier zip) directement ici (à la version lors de la publication de l'article).

    Le résultat

    À présent, je peux écrire de l'assembleur dans mon éditeur de texte et, par un seul raccourci, lancer l'assemblage, avoir un retour d'erreur annoté directement dans le code source, et en cas de réussite, le programme lancé directement dans l'émulateur.

    On pourrait aller encore plus loin, avec le script MAME qui ouvrirait un canal de communication afin d'injecter le code sans relancer l'émulateur... Mais je vais m'arrêter ici et revenir sur le sujet qui a entraîné tout cela.


« (précédent) Page 2 / 10 (suivant) »