Site logo

Triceraprog
La programmation depuis le Crétacé

  • Forth sur 6502, épisode 12 ()

    La reprise

    L'écriture de cet article a commencé le 3 mars 2026... et je le reprends le 9 juillet de la même année. Heureusement que j'avais déjà laissé pas mal de note car la reprise est difficile !

    Interpréter une ligne

    Depuis l'article précédent, la boucle principale du système Forth est capable de récupérer une ligne de texte entrée par l'utilisateur. L'étape suivante est bien entendu d'en faire quelque chose : de l'interpréter.

    INTERPRET est le mot Forth qui s'occupe de cela. Et ce que fait ce mot est assez simple : prendre le prochain morceau de texte entouré d'espace disponible dans la ligne, essayer de le trouver dans le dictionnaire, et si c'est le cas, l'exécuter. Si le mot n'est pas trouvé, la boucle tente de l'interpréter comme un nombre, et si c'est un nombre valide, elle le pousse sur la pile. Et si ce n'est pas un nombre, alors c'est une erreur.

    Du moins, c'est le fonctionnement de INTERPRET en mode d'interprétation directe. INTERPRET peut aussi fonctionner en mode de compilation. Mais nous verrons cela plus tard. Pour le moment, je veux juste une interprétation directe afin de pouvoir executer la ligne Forth suivante : HEX 7FF FF C!.

    Voilà à quoi ressemble INTERPRET (avec un nommage FIG-Forth) avec uniquement le mode interprétation directe :

    : INTERPRET
        BEGIN
            -FIND IF
                CFA EXECUTE
            ELSE
                HERE NUMBER
            ENDIF
        AGAIN
    

    Il y a quelques mots ici que je vais devoir définir. Tout d'abord -FIND, qui à la charge de prendre le prochain groupe de caractères depuis le TIB (Terminal Input Buffer) et de trouver s'il y a un mot correspondant dans le dictionnaire. Si un mot est trouvé, alors le PFA (Parameter Field Address) est laissé sur la pile.

    CFA est un mot qui trouve le CFA (Code Field Address) d'un mot à partir de son PFA. Et EXECUTE est un mot qui exécute le code à l'adresse donnée sur la pile. On voit donc comment l'enchaînement de ces trois mots permet d'exécuter un mot trouvé dans le dictionnaire.

    -FIND a un effet de bord : lors du parsing du mot, il laisse celui-ci dans la zone de données pointée par HERE. Cette zone de donnée est une zone libre en RAM. Incidemment celle où serait construit un mot si on était en mode de compilation. Mais dans notre cas, c'est une zone libre d'usage. Ainsi, dans le cas où -FIND ne trouve pas de mot correspondant dans le dictionnaire, il laisse les caractères à interpréter dans cette zone de données. NUMBER prend une adresse et tente d'interpréter les caractères à partir de cette adresse comme un nombre. Si c'est un nombre valide, il le pousse sur la pile. Sinon, c'est une erreur qui fera sortir de la boucle d'interprétation.

    Sortir d'une boucle infinie

    Mais comment est-ce que l'on sort de cette boucle ? En effet, INTERPRET doit redonner la main une fois la ligne complètement interprétée. L'astuce de Fig-Forth est d'ajouter au dictionnaire un mot nommé X, dont le nom est de longueur 1 et de valeur 0. Ainsi, lorsque -FIND arrive en fin de ligne, il trouve (à travers WORD) ce mot X et l'exécute. Or, X est défini de manière à faire sortir de la boucle d'interprétation et enlevant l'adresse de retour qui est au sommet de la pile des retours.

    Je ne sais pas encore si je vais utiliser cette astuce. Elle a une sorte d'élégance pratique, mais aussi un côté « hack ». Voici ce qu'en dit la co-autrice de Forth, Elizabeth Rather dans cette discussions sur comp.lang.forth le 5 octobre 2011 :

    *Sigh* I remember that trick. It was in very early Forths, probably as
    long ago as NRAO. Awful. Excessively "cute" and obscure. Once you're
    done enjoying your "eureka!" moment, forget you ever saw that!
    
    Traduction française : *soupir* Je me souviens de cette astuce. Elle était dans les tout premiers Forth, probablement aussi vieille que le NRAO. C'est affreux. Excessivement « mignon » et obscur. Une fois que vous avez fini de profiter de votre moment « eureka ! », oubliez que vous avez jamais vu ça !
    

    Et poursuit en donnant une solution alternative :

    A much cleaner solution is to have BEGIN ... WHILE ... REPEAT loops that
    compile or interpret depending on STATE, with the loops terminating when
    the current input source is exhausted, whereupon the system (or TERMINAL
    task) simply waits for more input in the BEGIN ... AGAIN loop in QUIT.
    
    Traduction française : une solution beaucoup plus propre est d'avoir des boucles BEGIN ... WHILE ... REPEAT qui compilent ou interprètent selon l'état, avec les boucles se terminant lorsque la source d'entrée actuelle est épuisée, auquel cas le système (ou la tâche TERMINAL) attend simplement plus d'entrée dans la boucle BEGIN ... AGAIN de QUIT.
    

    Trouver un mot dans le dictionnaire

    -FIND est le mot le plus complexe à implémenter pour faire fonctionner INTERPRET. Et lui-même appelle plusieurs autres mots. Voici une implémentation possible de -FIND :

    : -FIND ( -- addr len flag )
        BL WORD
        HERE COUNT CAPITAL
        HERE LATEST (FIND)
    

    Le mot agit en trois étapes. Tout d'abord WORD prend le prochain groupe de caractères terminé par le délimiteur spécifié, ici BL, qui représente l'espace. Comme indiqué plus haut, WORD laisse les caractères traités dans la zone de données pointée par HERE, précédés de leur longueur de la chaîne.

    Ensuite, CAPITAL convertit les caractères en majuscules. Ce mot prend en entrée l'adresse de la chaîne à convertir ainsi que sa longueur. C'est le rôle de COUNT que de transformer une chaîne de caractères précédée de sa longueur en une adresse de cette chaîne et une longueur séparées sur la pile.

    Enfin, l'appel à (FIND) cherche dans le dictionnaire un mot dont le nom correspond à la chaîne de caractères donnée en partant de LATEST, qui est une variable qui pointe vers le dernier mot ajouté au dictionnaire. On se souvient que dans cette implémentation, le dictionnaire est une liste chaînée de mots. (FIND) partira donc du dernier ajouté puis va remonter toute la chaîne jusqu'à trouver un mot... ou pas.

    Si le mot n'est pas trouvé, le flag de retour est 0 et il n'y aura ni l'adresse ni la longueur de la chaîne sur la pile.

    Pas de vocabulaire pour le moment

    En plus de la simplification du mot INTERPRET pour lequel je ne traite pas le mode de compilation, je fais une autre simplification : je n'implémente pas les vocabulaire. Un vocabulaire est un mécanisme de Forth qui permet de regrouper des mots dans des espaces de noms. Cela permet de travailler dans un certain contexte avec des mots ayant une certaine signification, et de changer de contexte au besoin.


  • Compression de données, la vidéo ()

    Après ma série de quatre articles sur la compression de données sans pertes commencée l'année dernière, j'ai eu envie de faire une petite vidéo illustrative de ces informations. C'est chose faite, avec un angle assez similaire : donner une intuition, une compréhension générale, de comment fonctionnent ces algorithmes. Mais cette fois avec des images animées, ce que je trouve complémentaire dans les articles.

    Pour rappel, les article sont les suivants :

    Après avoir écrit un premier script incluant la partie ZX0, j'ai finalement décidé de retirer cette partie afin de rester dans la compréhension globale. Parler de ZX0, c'est rentrer dans le détail d'implémentation, ça fait intervenir de nombreux concepts. Ça alourdissait beaucoup la vidéo.

    Et voici la vidéo :


  • TextooM, un jeu pour CP/M ()

    Une fois Micreversi terminé, et avant de revenir sur Family Forth, j'ai eu envie de faire un petit détour par une idée qui me trottait dans la tête depuis un bon moment. Depuis le moment du live de la restauration Micral N où on avait affiché « DOOM » en BASIC pour affirmer : Doom tourne sur Micral N. C'était une blague bien entendu, mais pourquoi pas continuer la blague.

    L'idée était donc de faire un Doom façon aventure textuelle, initialement pour Micral N. Mais après réflexion, comme ce n'est pas une machine très courante et puisque mon émulateur n'est pas très pratique à utiliser, je me suis dit qu'une version CP/M serait plus accessible. Et aussi plus facile à produire.

    Et voici ainsi TextooM, un jeu d'aventure textuelle pour CP/M qui s'inspire du premier niveau de Doom, E1M1. C'est plus un jouet qu'un vrai jeu, il ne faut pas en attendre beaucoup. Mais il y a des secrets à trouver, avec un pourcentage à la fin, et il est possible de rusher, ou de tenter le nettoyage complet du niveau (qui n'est pas si simple).

    De quoi amuser 15 minutes...

    TextooM n'est pas le premier jeu de ce type. Au début du développement, j'ai cherché si cela avait été déjà fait et, oui, cela a été fait. En 1996 même ! Ce jeu est Foom et utilise le moteur de fiction interactive TADS. N'ayant pas envie d'entreprendre un portage de TADS, et Foom ayant beaucoup de texte, peut-être beaucoup trop pour une machine CP/M, j'ai décidé de faire mon propre moteur, plus simple, et de faire un jeu plus court. Ah, et en français.

    Le code source du projet est disponible sur GitLab. Et je vous fournis ici l'exécutable pour CP/M.

    Écran de jeu de TextooM


  • Micreversi, un micro-jeu pour Canon X07 ()

    La session Canon X07 de la Game Jam « Retro Programmers United for Obscure Systems » se termine (officiellement, elle est terminée, mais cette game jam est flexible sur les délais), il est temps de revenir sur ma contribution, un Reversi (ou Othello, mais c'est une marque déposée).

    Le Reversi

    Tout d'abord, qu'est-ce que le Reversi ? C'est un jeu de plateau pour deux joueurs, joué sur une grille de 8x8 cases avec des pions réversibles, généralement noirs d'un côté et blancs de l'autre. Le but du jeu est d'avoir le plus de pions à sa couleur à la fin de la partie.

    Pour retourner des pions adverses, il faut les entourer de ses propres pions. Lors de la pose d'un pion, tous les pions adverses qui sont sur une ligne droite (horizontale, verticale ou diagonale) entre le pion posé et un autre pion de la même couleur sont retournés.

    C'est simple à comprendre, mais pas forcément si simple à jouer. Être trop gourmand au début risque de se retourner contre soi.

    Le Canon X07

    Le Canon X07 est un micro-ordinateur sous forme d'une grosse calculatrice, la version « imposante » des calculatrices programmables des années 80. Il est équipé d'un processeur compatible Z80, mais le gros du boulot graphique et sonore est assuré par un coprocesseur dédié. Il a pas mal de fonctionnalités et de périphériques. C'est une machine que j'aime bien, mais que je n'avais jamais eu l'occasion de programmer.

    Comme toujours avec ces machines, l'état des émulateurs n'est pas glorieux. Quelques participants, dont je fais partie, ont envoyés quelques patchs à MAME, mais il reste encore beaucoup de boulot pour avoir quelque chose d'à peu près fiable. Utiliser la vraie machine était quasi indispensable. Mais en contrepartie, mon programme final ne tourne avec MAME qu'avec une version très récente.

    Merci d'ailleurs à pR-0000 pour son câble série et son chargeur qui a permis des transferts très pratiques vers la machine.

    La source du jeu

    J'ai pas mal tourné en rond avant de trouver une idée qui me plaise et soit adapté à la machine. Je me dirigeais vers un jeu de pions, ce qui a l'avantage de ne pas nécessiter de grosses capacités graphiques, et je me suis souvenu quand dans ma pile de notes, j'avais référencé un programme de Reversi en BASIC publié dans le premier numéro du magazine l’Ordinateur Individuel en Octobre 1978 (pages 50 à 52, signé Jean-Marie Donat).

    Ce programme est assez court, plutôt bien expliqué avec un but pédagogique : apprendre à programmer en BASIC.

    L'idée a donc été d'en faire un portage pour le Canon X07, en ajoutant une interface graphique pour la sélection des pions avec un curseur, puisque le Canon X07 a 4 flèches directionnelles bien visibles. Le programme BASIC d'origine, lui, demande des coordonnées et fait un affichage en mode texte, en affichant à chaque tour la grille de jeu.

    Le BASIC, c'est lent !

    En tout cas, un BASIC Microsoft interprété sur une console construire autour de l'économie d'énergie... La première version complète du jeu, entièrement en BASIC, était injouable. La réflexion de l'ordinateur plusieurs secondes, mais à la rigueur, ce n'était pas le plus gênant. Le déplacement du curseur, prenant plus d'une seconde à chaque mouvement, était lui vraiment injouable.

    J'ai donc décidé de transcrire le tout en assembleur.

    L'assembleur, c'est plus rapide !

    Nettement, nettement plus rapide. Ça a pris un peu de temps à convertir, le temps de retrouver la bonne manière de s'adresser au coprocesseur pour la plupart des opérations. Là-dessus, les actifs du Discord ont été d'une grande aide, merci à eux !

    Partant d'un programme entièrement fonctionnel en BASIC, le reste de la transcription a été relativement automatique. Relativement.

    C'est fun ?

    Selon moi, le jeu se tient à peu près. Il est jouable, j'ai fait quelques parties. Le niveau de l'ordinateur est très basique. Il pourra battre un débutant, mais on peu rapidement prendre le dessus. Sans être un bon joueur de Reversi, je gagne à présent toutes les parties.

    C'était d'ailleurs une proposition de l'article dans le magazine : améliorer le programme. Je l'ai amélioré d'un point de vue utilisation et graphiquement, mais pas du tout d'un point de vue intelligence artificielle. C'est à la base pour cela que je l'avais pris en note. Pour un article potentiel sur ce site. L'idée est toujours là.

    Autre Reversi

    Contrairement à d'autres machines utilisées dans les « Retro Programmers United for Obscure Systems », le Canon X07 a eu droit a beaucoup de listings de jeux en BASIC. Il rentre donc dans la liste des machines avec peu de jeux officiellement exploité durant sa durée de vie, tout en ayant une ludothèque pas si maigre.

    Entre autre, il y a un jeu de Reversi publié page 138 du livre « Applications en assembleur dans l'univers du Canon X-07 ». J'ai tenté de le recopier, malheureusement, le scan auquel j'ai accès est très abîmé sur certaines parties, et le programme est listé comme un code machine en hexadécimal. Cependant, ce que j'ai pu récupérer à l'air de promettre un jeu graphique avec une IA plus développée que celle de l'article de l'Ordinateur Individuel, le tout en 3073 octets (mon programme fait 2460 octets).

    J'espère pouvoir le voir tourner un jour.

    Page du jeu

    Le jeu est disponible sur la page itch.io dédiée.

    J'ai aussi publié le code source de la version assembleur.

    Écran de jeu de Micreversi


  • Comparaisons 8 bits en Z80 ()

    Il y a bien quelque chose avec le Z80 qui me ralenti à chaque fois, ce sont les comparaisons. L'égalité, c'est facile. Mais dès qu'il s'agit de comparer deux octets pour savoir si l'un est plus grand que l'autre, strictement ou pas, et encore pire, si ce sont des octets signés, j'y passe du temps... pour souvent me tromper.

    Et donc, voici un petit tableau récapitulatif pour m'aider à m'y retrouver, et peut-être que ça pourra aussi vous aider.

    Table des matières

    Comparaisons non signées (8 bits)

    Un octet non signé est positif et dans l'intervalle [0, 255]. Après cp b (qui calcule A−B sans stocker le résultat), on a : Z = 1 si A = B et C = 1 si A < B.

    A = B

        cp b
        jr z,egal
        ; cas 1 : A ≠ B
        jr next
    egal:
        ; cas 2 : A = B
    next:
    

    A ≠ B

    Le même code que pour A = B, mais en privilégiant le cas A ≠ B.

        cp b
        jr nz,inegal
        ; cas 1 : A = B
        jr next
    inegal:
        ; cas 2 : A ≠ B
    next:
    

    A < B et A ≥ B

    Les deux cas sont identiques, c'est juste une question de sémantique.

        cp b
        jr c,inf_strict
        ; cas 1 : A < B (ou A ≥ B)
        jr next
    inf_strict:
        ; cas 2 : A ≥ B (ou A < B)
    next:
    

    A ≤ B et A > B

    Les deux cas sont identiques, c'est juste une question de sémantique.

        cp b
        jr z,inf_ou_egal
        jr c,inf_ou_egal
        ; cas 1 : A > B (ou A ≤ B)
        jr next
    inf_ou_egal:
        ; cas 2 : A ≤ B (ou A > B)
    next:
    

    Comparaisons signées (8 bits)

    En signé, les deux octets sont dans l'intervalle [−128, +127]. La condition A < B est plus complexe à évaluer, car il faut aussi prendre en compte le flag de débordement (overflow) qui peut se produire lors de la soustraction A−B.

    Si on est assuré qu'aucun overflow ne se produit (si A et B sont tous deux dans un intervalle [−64, +63]), voir la section suivante.

    En signé ou pas, l'égalité est identique à celle des non signés, elle n'est pas reproduite ici.

    Note : jr ne supporte pas les conditions sur S et V, il faut donc utiliser jp pour ces cas.

    A > B ou A ≤ B (signé)

        cp b
        jr z,inf_ou_egal    ; A et B égaux
        jp pe,ov_gt         ; Overflow, il faut inverser la condition
        jp p,sup_strict     ; S=0 et pas d'overflow, donc A > B (strict)
        jp inf_ou_egal      ; S=1 et pas d'overflow, donc A ≤ B
    ov_gt:                  ; Cas de l'overflow
        jp m,sup_strict     ; S=1 et overflow, donc A > B
        jp inf_ou_egal      ; S=0 et overflow, donc A ≤ B
    sup_strict:
        ; cas 1 : A > B (signé)
        jp next
    inf_ou_egal:
        ; cas 2 : A ≤ B (signé)
    next:
    

    A < B ou A ≥ B (signé)

        cp b
        jp pe,ov_lt         ; Overflow, il faut inverser la condition
        jp m,inf_strict     ; S=1 et pas d'overflow, donc A < B
        jp sup_ou_egal      ; S=0 et pas d'overflow, donc A ≥ B
    ov_lt:                  ; Cas de l'overflow
        jp p,inf_strict     ; S=0 et overflow, donc A < B
        jp sup_ou_egal      ; S=1 et overflow, donc A ≥ B
    inf_strict:
        ; cas 1 : A < B (signé)
        jp next
    sup_ou_egal:
        ; cas 2 : A ≥ B (signé)
    next:
    

    Comparaisons signées simplifiées (8 bits)

    Lorsque A et B sont dans un intervalle tel qu'aucun overflow ne se produit lors de A−B (tous deux dans [−64, +63] ou toute opération garantie sans débordement) le code est simplifié en retirant les vérifications d'overflow.

    A > B ou A ≤ B (signé, pas d'overflow)

        cp b
        jr z,inf_ou_egal    ; A = B
        jp p,sup_strict     ; S=0, donc A > B
        jp inf_ou_egal      ; S=1, donc A ≤ B
    sup_strict:
        ; cas 1 : A > B (signé)
        jp next
    inf_ou_egal:
        ; cas 2 : A ≤ B (signé)
    next:
    

    A < B ou A ≥ B (signé, pas d'overflow)

        cp b
        jp m,inf_strict     ; S=1, donc A < B
        jp sup_ou_egal      ; S=0, donc A ≥ B
    inf_strict:
        ; cas 1 : A < B (signé)
        jp next
    sup_ou_egal:
        ; cas 2 : A ≥ B (signé)
    next:
    

    Les drapeaux

    L'état des drapeaux après cp b.

    Flag Signification
    Z 1 si A = B (A - B = 0)
    C 1 si A < B (non signé) (emprunt lors de A−B)
    S Bit 7 de A−B (signe du résultat 8 bits)
    P/V 1 si overflow signé (voir conditions ci-dessous)

    À propos de l'overflow signé : il se produit lorsque A et B ont des signes opposés et que le résultat de A−B a un signe différent de celui de A.

    Entre autre, il n'y a pas d'overflow possible si A et B sont de même signe.

    À propos de S et V :

    S V Interprétation
    0 0 A − B ≥ 0, pas de débordement, donc A ≥ B
    1 0 A − B < 0, pas de débordement, donc A < B
    0 1 Résultat positif mais a débordé, donc A < B
    1 1 Résultat négatif mais a débordé, donc A > B

    Instructions et drapeaux

    La syntax des sauts ne fait pas toujours référence directe au nom des drapeaux, ce qui est confusant.

    Instruction Description
    jr z/nz Saut relatif sur Z
    jr c/nc Saut relatif sur C
    jp m/p Saut absolu sur S
    jp pe/po Saut absolu sur P/V

Page 1 / 28 (suivant) »

Tous les tags

3d (15), 6502 (14), 6809 (1), 8bits (1), Affichage (24), AgonLight (2), Altaïr (1), Amstrad CPC (1), Apple (1), Aquarius (2), ASM (33), Atari (1), Atari 800 (1), Atari ST (2), Automatisation (4), BASIC (31), BASIC-80 (4), C (3), Calculs (1), Canon X07 (1), CDC (1), Clion (1), cmake (1), Commodore (1), Commodore PET (1), Compression (5), CP/M (1), CPU (1), Debug (5), Dithering (2), Divers (1), EF9345 (1), Émulation (7), Famicom (10), Forth (15), Game Jam (1), Hector (3), Histoire (1), Hooks (4), Huffman (2), i8008 (1), Image (17), Jeu (17), Jeu Vidéo (4), Livre (1), Logo (2), LZ (2), LZ77 (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), Outil (1), Outils (3), Pascaline (1), Peertube (1), PHC-25 (2), Photo (2), Programmation (20), Python (1), RLE (2), ROM (15), RPUfOS (7), Salon (1), SC-3000 (1), Schéma (5), Synthèse (15), Tortue (1), Triceraprog (1), VG5000 (62), VIC-20 (1), vidéo (4), Z80 (23), z88dk (1), ZX0 (1)

Les derniers articles

Forth sur 6502, épisode 12
Compression de données, la vidéo
TextooM, un jeu pour CP/M
Micreversi, un micro-jeu pour Canon X07
Comparaisons 8 bits en Z80
z80dezasm, désassembler et commenter le Z80
Adressage 6502, un récapitulatif
Forth sur 6502, épisode 11
Deux livres pour le développement sur Famicom
Forth sur 6502, épisode 10

Atom Feed

Réseaux