Site logo

Triceraprog
La programmation depuis le Crétacé

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


  • Scripter MAME pour explorer la machine ()

    Puisque je suis actuellement dans l'utilisation de LUA pour scripter MAME, faisons un petit détour pour explorer quelques possibilités.

    Les fonctions accessibles aux scripts LUA sont assez nombreuses. Je n'en connais pas de documentation complète si ce n'est dans les sources du programme lui-même à cet endroit : mame/src/frontend/mame/luaengine.cpp. Des groupes de commentaires indiquent les fonctions et leur usage. Reste à utiliser la console pour expérimenter un peu la façon de les appeler.

    L'une des possibilités intéressantes est de pouvoir agir sur l'affichage de l'émulateur. Cela peut être assez pratique pour suivre certaines valeurs en mémoire, pour afficher des informations sur le comportement de la machine, voire d'ajouter des fonctionnalités interactives à l'émulateur facilement.

    Affichage à l'écran

    La première chose à faire pour afficher quelque chose à l'écran est de récupérer un objet permettant de le manipuler. Pour cela, en passant par l'objet manager, on récupère la machine, puis un écran qui dans notre cas se nomme :screen.

    Grâce à cet objet, il est ensuite possible d'afficher du texte, de tracer des lignes et des boites. C'est limité, mais déjà pas mal.

    Ce qui suit est un exemple d'utilisation dans lequel je vais chercher directement en mémoire les coordonnées X et Y du curseur du VG5000µ, qui sont situées aux emplacement mémoire fixes $4805 et $4806. Par un objet donnant accès à la mémoire et l'utilisation des fonctions read_u8 (comme lecture d'un entier non signé sur 8 bits), je récupère les valeurs et je les affiche.

    Simple et efficace. Je peux à présent, si le script est dans un fichier nommé vgdisplay.lua, lancer la commande suivant pour suivre les évolutions des coordonnées du curseur.

    mame64 vg5k -ramsize 48k -nomax -window -autoboot_delay 0 -autoboot_script vgdisplay.lua

    Le script

    -- Script for MAME that launches the vg5k emulator
    -- and continuously displays the position of the cursor
    
    -- Get the only screen of the VG-5000µ
    local screen = manager:machine().screens[":screen"]
    
    -- Get access to the memory
    local cpu = manager:machine().devices[":maincpu"]
    local memory = cpu.spaces["program"]
    
    function draw_hud()
        local cursor_x = memory:read_u8(0x4805)
        local cursor_y = memory:read_u8(0x4806)
        screen:draw_text(300, 40, "X: " .. cursor_x, 0xffff0000);
        screen:draw_text(300, 50, "Y: " .. cursor_y, 0xffff0000);
    end
    
    emu.register_frame_done(draw_hud)
    

    Le résultat

    Affichage en surimpression dans MAME


  • Injecter un programme dans un émulateur VG5000µ, partie II ()

    La fois dernière était consacrée au lancement d'un programme sur VG5000µ avec l'émulateur MAME de manière à injecter un programme binaire directement en mémoire puis à l'appeler. Tout cela automatiquement. Pour rappel, l'idée derrière cela est d'éviter les erreurs de manipulations qui arrivent de temps en temps.

    La première option utilisée dans l'article précédent était d'utiliser un script pour le debuggeur d'une part, et la capacité de MAME à entrer au clavier de la machine émulée une suite de frappes de touches.

    Cette méthode fonctionne mais a ses limites : il faut régler le délai avant la frappe de touches de manière empirique, et le script est linéaire. Dans cet article, je vais utiliser une autre possibilité de MAME pour résoudre ces deux écueils.

    La console

    Commençons par une première manipulation.

    mame64 vg5k -ramsize 48k -nomax -window -debug -debugger none -console

    Dans le shell depuis lequel vous avez entré cette commande s'affiche un bandeau et une invite [MAME]>. Vous avez à présent accès à une console qui comprend le LUA. C'est bien entendu le paramètre -console qui a permis cela.

    Le couple -debug -debugger none est plus surprenant. Il indique que le debuggeur est activé, mais sans interface graphique. Le debuggeur est donc utilisable depuis la console, et c'est parfait. Au passage, vous remarquerez que, en l'absence d'interface graphique pour le debuggeur, l'émulateur ne s'est pas mis en pause.

    Depuis la console, vous pouvez essayer ceci :

    m = manager:machine()
    md = m:debugger()
    print(md)
    

    Vous devez voir quelque chose comme sol.debugger_manager*: 0xf9de9a8 qui indique que md contient bien un debuggeur (l'adresse sera différente). Si vous obtenez nil, c'est que le debuggeur n'est pas actif.

    L'object md permet de s'adresser au debuggueur.

    Par exemple :

    md:command("help")
    

    Et l'aide du debuggeur s'affiche. Nous communiquons bien avec le debuggeur. C'est pratique.

    Script LUA

    Mais si une console est pratique pour essayer des choses, ce n'est pas ce que je recherche au final. Ce que je recherche à faire est toujours automatiser le démarrage d'un programme pour sa mise au point.

    Et MAME a la commande qu'il me faut : -autoboot_script. Cette commande, suivie par un nom de fichier, va exécuter un script écrit en LUA qui aura accès au fonctionnement de l'émulateur.

    La commande pourra ressembler à quelque chose comme ça :

    mame64 vg5k -ramsize 48k -nomax -window -debug -debugger none -autoboot_delay 0 -autoboot_script vgboot.lua

    -autoboot_delay 0 indique que le script doit être lancé dès le démarrage de l'émulation de la machine. La première chose (ou presque) à faire dans le script si vous voulez contrôler la machine cible dès le début est alors un emu.pause() que vous pourrez balancer avec un emu.unpause() lorsque votre script sera prêt.

    Le script que j'utilise est trop long pour s'afficher sur cette page. Vous pouvez le trouver ici.

    Des choses intéressantes à savoir :

    • manager:machine() renvoie l'objet contrôlant machine émulée,
    • manager:machine():debugger() renvoie l'objet de contrôle du debuggeur,
    • manager:machine():debugger().consolelog est le log du debuggeur, pratique pour vérifier les retours de commandes,
    • manager:machine().devices[":maincpu"] renvoie l'objet contrôlant le CPU principal de la machine (le Z80 pour le VG5000µ),
    • manager:machine().devices[":maincpu"]:debug() renvoie l'objet de commande du debuggeur sur ce CPU (qui offre des facilités sur les commandes du debuggeur pour ce processeur en particulier).

    Sur l'objet renvoyé par debug() sur le CPU, par exemple, on peut appeler bpset() pour placer un point d'arrêt. Sur l'objet renvoyé par debugger(), on peut envoyer tous les commandes que l'on entrerait dans l'interface graphique avec command().

    Ainsi ...:debugger():command("go") a le même effet, sur cette machine, que ...:debug():go().

    L'objet emu qui permet de faire pause() et unpause() permet aussi de simuler les appuis sur les touches du clavier émulé. Par exemple emu.keypost('CALL &"7000"\n').

    C'est plus complexe !

    Oui, cette méthode est plus complexe, plus longue à écrire. Elle permet aussi un meilleur contrôle et des choses que ne permettent pas un script de debuggeur simple. À vous de choisir !

    La console LUA dans MAME


  • Injecter un programme dans un émulateur VG5000µ ()

    Reprenons sur la programmation en assembleur sur VG5000µ depuis un ordinateur actuel. Car s'il est possible et tout à fait légitime, par hobby, de programmer directement sur la machine, à l'ancienne, pour « retro programmer » comme il y a du « retro gaming » en éprouvant les sensations originelles, ce n'est pas mon but ici.

    Mon but, c'est de pouvoir programmer depuis un éditeur de texte simple, mais pas trop, et de pouvoir mettre au point avec certaines facilités comme un retour d'erreur d'assemblage et l'accès à un debuggeur. Et tout ceci avec une chaîne qui minimise les manipulations répétitives.

    Minimiser les manipulations répétitives permet de rester concentré au maximum sur ce que je fais. Je n'ai pas envie, à chaque lancement, de taper des commandes particulières ou de faire quelques clics souris, toujours les mêmes, avec un risque de me tromper et de chasser un bug que je crois être là alors que je me suis juste trompé dans une manipulation.

    Il y a de nombreuses manière d'automatiser une chaîne de construction de programme. Certains choix sont contraints par les outils que l'on utilise. D'autres sont des choix par préférences.

    Après avoir fait mon travail de reflexion, lorsque je passe à la partie d'entrée au clavier, les données sont les suivantes :

    • j'entre le code source dans un éditeur,
    • j'effectue une action qui produit un fichier binaire,
    • ce fichier binaire doit se lancer dans un émulateur et/ou produire un fichier pour chargement sur machine réelle,
    • s'il y a des erreurs, les afficher,
    • s'il n'y a pas d'erreurs l'émulateur se lance automatiquement et lance à son tour le programme.

    Cette dernière opération est mon point de départ et le sujet de cet article. Vous aurez compris au passage, et suite à l'article précédent, que j'utiliserai un émulateur pour la mise au point, avant de passer sur machine réelle pour confirmation du fonctionnement.

    Choix de l'émulateur

    Au niveau VG5000µ, au moment de l'écriture, il n'y a pas beaucoup de choix : soit DCVG5K, soit MAME. Les deux possèdent un debuggeur et le moyen d'injecter un binaire en mémoire.

    Je vois en MAME deux avantages pour moi : la possibilité de scripter le debuggeur et le lancement en natif sous Linux, qui est derrière le système d'exploitation que j'utilise (Ubuntu). DCVG5K a l'avantage de la légèreté dû au fait qu'il est dédié à une seule machine, ainsi que de la simplicité d'utilisation.

    Pour savoir comment utiliser MAME, c'est très simple, il suffit de taper mame -showusage en ligne de commande... Ooops... 462 lignes d'explication de paramètres... Et ce n'est qu'un résumé.

    MAME est un gros morceau et j'espère qu'avec les renseignements de cet article, je vous aurait débrousaillé le terrain.

    Lancement de l'émulation

    Avant toute chose : tout ce que j'explique est du lancement en ligne de commande. Il est possible de faire la même chose à travers des fichiers de configuration ou des raccourcis Windows avec paramètres mais le but final est d'inclure ça à un script de lancement. La ligne de commande est aussi la manière la plus simple de jouer avec les paramètres (pour peu qu'on utilise un shell moderne).

    Pour lancer l'émulation du VG5000µ, c'est tout simplement :

    mame64 vg5k

    Note : l'exécutable s'appelle mame64 pour quoi car je compile la version depuis les sources. Suivant les distributions de MAME, il peut avoir un autre nom.

    Note : Si MAME vous annonce qu'il vous manque une ROM, revenez à mon article précédent, dumpez votre ROM, placez-là dans le répertoire attendu par MAME et c'est reparti.

    Le lancement de l'émulateur dans cette configuration lance la machine après un bandeau la présentant et, à l'heure actuelle, un bandeau rouge indiquant que l'émulation ne fonctionne pas. En effet, l'émulation n'est pas considérée comme terminée ; mais en ce qui nous concerne, cela sera suffisant.

    J'ai évoqué un debuggeur. Il est accessible sous plusieurs formes et je vais laisser celle par défaut :

    mame64 vg5k -ramsize 48k -nomax -window -debug

    J'ai ajouté au passage quelques options supplémentaires :

    • -ramsize 48k indique que je veux l'extension RAM maximale disponible sur le VG5000µ (l'autre option est 32k, et par défaut, la machine aura ses 16k)
    • -nomax indique que je ne veux pas maximiser la fenêtre
    • -window indique que je veux lancer l'émulateur dans une fenêtre et non en plein écran
    • -debug, bien entendu, lance le debugger

    En mode debugger par défaut, l'émulation ne se lance pas tout de suite. Elle est immédiatement mise en pause. Sur l'image suivante, vous pouvez voir que le PC est à $0000, sur la première instruction qui sera exécutée par le processeur de la machine jp $1000.

    Debugger par défaut (Qt) sur MAME

    Ce debugger accepte des raccourcis clavier mais peut aussi accepter des commandes. Le debugger peut aussi prendre ses commandes depuis un fichier texte grace à la commande source.

    Je vous invite à entrer le texte suivant dans un fichier debug.txt.

    go $2adf
    load hello.bin,$7000
    go
    

    Note : go $2adf indique de lancer l'émulation avec un point d'arrêt temporaire à l'adresse $2adf, qui est une addresse qui, lorsqu'elle est atteinte, garanti que le VG5000µ a terminé son initialisation.

    Puis d'avoir un fichier binaire au nom de hello.bin de mettre ces deux fichiers dans le répertoire de travail de MAME.

    Et enfin, depuis le debuggeur, taper source debug.txt

    Le debuggeur devrait afficher :

    >go $2adf
    Stopped at temporary breakpoint 2ADF on CPU ':maincpu'
    >load hello.bin,$7000
    Data loaded successfully to memory : 0x7000 to 0x7018
    >go
    

    Suivant votre fichier, l'addresse 0x7018 peut varier. Dans mon cas, le fichier provient du résultat de l'assembleur d'un article précédent.

    Dans le debuggeur, vous pouvez ouvrir une fenêtre de visualisation de mémoire pour vérifier que le fichier binaire a été injecté. Et si votre fichier est le même que le mien, vous pouvez entrer CALL &"7000" dans la machine émulée pour voir apparaître Bonjour! à l'écran.

    C'est déjà bien, non ?

    Automatique on a dit !

    C'est bien... mais taper source à chaque lancement, c'est long, on peut se tromper et cela serait mieux si cela pouvait être automatique.

    Et c'est tout à fait possible :

    mame64 vg5k -ramsize 48k -window -nomax -debug -debugscript debug.txt

    -debugscript, comme son nom l'indique, fourni un nom de script pour le debugger qui sera exécuté au démarrage. Juste après le lancement de l'émulateur, le fichier binaire est donc injecté sans manipulation.

    D'accord, mais il faut encore taper la commande CALL &"7000" pour lancer le programme, et avec la disposition d'un clavier qui change entre la machine émulée et la machine hôte, personnellement, je trouve cela fastidieux (surtout pour la touche ", qui est une touche morte dans ma configuration par défaut).

    Pas de problème :

    mame64 vg5k -ramsize 48k -window -nomax -debug -debugscript debug.txt -autoboot_command 'CALL &"7000"\n' -autoboot_delay 2

    • -autoboot_command indique la chaîne à passer à l'émulateur comme si les touches étaient appuyées,
    • -autoboot_delay pose un délai de 2 secondes avant de lancer les commandes, ce qui laisse le temps au script de s'exécuter.

    À vrai dire, le délai de 2 secondes est la valeur par défaut, mais l'idée était de montrer la commande, pour pouvoir ajuster le délai.

    Là, ça commence à être pas mal.

    Mais...

    Mais il y a deux choses encore qui me chiffonnent. Tout d'abord, la fenêtre du debuggeur se place au-dessus de ma fenêtre d'émulation, et je dois systématiquement la bouger pour voir le résultat. Il y a bien la possibilité d'utiliser la commande -debugger none, mais alors le script n'est plus exécuté.

    Il y a la possibilité d'utiliser un debuggeur qui utilise la bibliothèque imgui et qui s'affiche dans la fenêtre d'émulation. Je vous suggère alors de passer -nomax en -max. Possibilité aussi pour le debugger par défaut (mais imgui est semi-transparent, le debugger par défaut opaque).

    mame64 vg5k -ramsize 48k -window -max -debug -debugscript debug.txt -autoboot_command 'CALL &"7000"\n' -autoboot_delay 2 -debugger imgui -video bgfx

    Cela donne déjà de quoi s'amuser et automatiser la chaîne de développement. Il y a cependant moyen d'aller encore plus loin avec MAME. Ça sera pour la prochaine fois.

    Debugger utilisant ImGUI sur MAME


« (précédent) Page 15 / 22 (suivant) »

Tous les tags

3d (14), 6809 (1), 8bits (1), Affichage (24), AgonLight (2), Altaïr (1), Amstrad CPC (1), Apple (1), Aquarius (2), ASM (30), Atari (1), Atari 800 (1), Atari ST (2), Automatisation (4), BASIC (30), BASIC-80 (4), C (3), Calculs (1), CDC (1), Clion (1), cmake (1), Commodore (1), Commodore PET (1), CPU (1), Debug (5), Dithering (2), Divers (1), EF9345 (1), Émulation (7), Forth (3), Game Jam (1), Hector (3), Histoire (1), Hooks (4), i8008 (1), Image (16), Jeu (14), Jeu Vidéo (4), Livre (1), Logo (2), Machine virtuelle (2), Magazine (1), MAME (1), Matra Alice (3), MDLC (7), Micral (2), Motorola (1), MSX (1), Musée (2), Nintendo Switch (1), Nombres (3), Optimisation (1), Outils (3), Pascaline (1), Peertube (1), Photo (2), Programmation (3), Python (1), ROM (15), RPUfOS (5), Salon (1), SC-3000 (1), Schéma (5), Synthèse (14), Tortue (1), Triceraprog (1), VG5000 (62), VIC-20 (1), Vidéo (1), Z80 (20), z88dk (1)

Les derniers articles

Instance Peertube pour Triceraprog
Environnement de développement pour Picthorix
Un jeu en Forth pour Hector HRX : Picthorix
Yeno SC-3000 et condensateurs
Suite de tests pour VG5000µ
Un peu d'Atari ST
Le Forth sur Hector HRX
J'MSX 24 et un micro jeu
Récréation 3D, Matra Alice
Tuiles des plus très-curieuses

Atom Feed

Réseaux