Site logo

Triceraprog
La programmation depuis le Crétacé

  • VG5000µ, ajouter des instructions au BASIC ? ()

    Il y a quelques temps, un message sur le forum Config.cfg demandait s'il était simple ou même possible d'ajouter des instructions supplémentaires à la ROM d'un VG5000µ. C'est une question que je me posais aussi, avec dans l'idée d'ajouter des instructions graphiques utilisant l'implémentation des articles précédents].

    J'ai donc continué à étudier la ROM (que je commence à bien connaître maintenant) à la recherche d'une méthode. Et on va voir que ça n'est pas gagné.

    Le parseur

    Lorsque la touche RET du clavier est appuyée, il se passe plusieurs choses. Tout d'abord, un caractère NUL (valeur 0) est placé dans le buffer d'entrée à l'emplacement du dernier caractère qui n'est pas un espace (adresses $3c4a à $3c56). Le RET à la fin de cette fonction ramène hors de la boucle principal du traitement interactif.

    Après un traitement de la protection de programmes qui n'est pas le sujet ici, une fonction cherche si la ligne commence par un numéro de ligne puis débute la transcription de la ligne vers une ligne « tokenisée ». La procédure de tokénisation, en bref, va repérer toutes les instructions, fonctions et opérations connues et les transformer en « tokens » : un octet dont le bit de poids fort est à 1.

    Un token permet de prendre moins de place en mémoire et facilite le travail de l'évaluateur.

    La procédure saute bien entendu les chaînes de caractères en repérant les paires de guillemets.

    Je laisse de côté le traitement de caractères à significations particulières (le ? qui remplace PRINT comme dans la plupart des BASIC par exemple) pour arriver à la recherche principale. Cette routine, qui commence globalement en $23a5, va comparer le contenu des caractères à décoder (en les passant en majuscules si nécessaire) avec une liste de noms et symboles dans une table située en $23a7.

    Premier problème, cette liste est figée. Si aucune correspondance n'est trouvée, la routine ne dispose pas de hook pour passer la main à une éventuelle liste gérée par l'utilisateur. Les caractères sans correspondance sont laissés tels quels dans le buffer. Si plus tard, à l'évaluation, ces caractères n'ont pas de sens (ne désignent pas une variable par exemple), une erreur sera émise.

    On pourrait alors imaginer se servir d'un autre hook, celui qui est appelé à chaque affichage de caractère ($47e2) ou celui qui est appelé lors d'un retour à la ligne (\$47e5), afin d'ajouter un parsing à la main. Mais pour cela, il faudrait choisir des tokens libres pour les nouvelles instructions ; ce qui amène à étudier les tokens.

    Les tokens

    Voici la liste des tokens avec leurs valeurs.

    Commandes Support - Opérateurs Fonctions
    128:END 178:TAB( 185:+ 195:SGN
    129:FOR 179:TO 186:- 196:INT
    130:NEXT 180:FN 187:* 197:ABS
    131:DATA 181:SPC( 188:/ 198:USR
    132:INPUT 182:THEN 189:^ 199:FRE
    133:DIM 183:NOT 190:AND 200:LPOS
    134:READ 184:STEP 191:OR 201:POS
    135:LET 192:> 202:SQR
    136:GOTO 193:= 203:RND
    137:RUN 194:< 204:LOG
    138:IF 205:EXP
    139:RESTORE 206:COS
    140:GOSUB 207:SIN
    141:RETURN 208:TAN
    142:REM 209:ATN
    143:STOP 210:PEEK
    144:ON 211:LEN
    145:LPRINT 212:STR$
    146:DEF 213:VAL
    147:POKE 214:ASC
    148:PRINT 215:STICKX
    149:CONT 216:STICKY
    150:LIST 217:ACTION
    151:LLIST 218:KEY
    152:CLEAR 219:LPEN
    153:RENUM 220:CHR$
    154:AUTO 221:LEFT$
    155:LOAD 222:RIGHT$
    156:SAVE 223:MID$
    157:CLOAD
    158:CSAVE
    159:CALL
    160:INIT
    161:SOUND
    162:PLAY
    163:TX
    164:GR
    165:SCREEN
    166:DISPLAY
    167:STORE
    168:SCROLL
    169:PAGE
    170:DELIM
    171:SETE
    172:ET
    173:EG
    174:CURSOR
    175:DISK
    176:MODEM
    177:NEW

    On peut remarquer que cette liste comporte plusieurs parties. La première, de 128 à 177, contient des instructions, c'est-à-dire des commandes impératives, sans valeur de retour.

    De 178 à 184, il s'agit de commandes de support, qui ne peuvent être trouvées qu'en conjonction de commandes maîtresses. Par exemple THEN avec IF ; TO et STEP avec FOR,...

    De 185 à 194, il s'agit d'opérateurs logiques et arithmétiques. Enfin, à partir de 195 jusqu'à la fin, 223, il s'agit de fonctions, qui retournent des valeurs, et qui apparaîtrons donc, au côté des opérateurs, dans des expressions.

    On commence à sentir que s'il fallait ajouter des mot-clés, en fonction de leur nature, il faudrait les placer dans le bon groupe. Sauf que cette liste est compacte. On peut imaginer ajouter des fonctions après MID$, mais ajouter une instruction ou un opérateur décalerait toute la table, ce qui poserait un problème de compatibilité au moins avec les programmes enregistrés (les programmes BASIC enregistrés le sont sous forme tokenisée).

    L'évaluateur

    C'est donc vers l'évaluateur qu'il faut se tourner et se demander comment sont traitées les lignes en BASIC. Et c'est là que vient se planter le dernier clou dans le cercueil de l'ajout de commandes utilisateurs... du moins sans modifier la ROM.

    Tout d'abord, examinons le décodage des tokens en $250f.

                 sub      a,$80
                 jp       c,inst_let           ; Comme tous les tokens sont supérieurs ou égaux
                                               ; à $80, si le caractère est inférieur, c'est le début du nom d'une variable.
                                               ; C'est un LET implicite.
    
                 cp       a,$32
                 jp       nc,stx_err_prt       ; Les 50 ($32) premiers tokens seulement sont des instructions
                                               ; Si le token est après, alors c'est une Erreur de syntaxe.
    

    Voilà... la ROM contient en dur les bornes des instructions pouvant être décodées.

    Et ce n'est pas fini. Lorsque l'on regarde l'évaluation d'expression en $28d8 :

                 xor      a,a
                 ld       (valtyp),a           ; Type numérique par défaut
                 rst      chget
                 jp       z,missing_op         ; Cas où le caractère est NUL
                 jp       c,str_to_num         ; Cas où le caractère est un chiffre.
                 cp       a,$26
                 jp       z,str_hex_dec        ; Saut si sur le point de parser un nombre en hexa (caractère '&')
                 call     a_to_z_2
                 jr       nc,str_to_var        ; Saut si la valeur est entre A et Z
                 cp       a,$b9                ; Token pour '+'
                 jr       z,parse_value
                 cp       a,$2e                ; Caractère '.'
                 jp       z,str_to_num
                 cp       a,$ba                ; Token pour '-'
                 jr       z,str_to_min
                 cp       a,$22                ; Caractère '"'
                 jp       z,str_to_str
                 cp       a,$b7                ; Token pour 'NOT'
                 jp       z,str_to_not
                 cp       a,$b4                ; Token pour 'FN'
                 jp       z,str_to_fn
                 sub      a,$c3                ; Token pour 'SGN', la première des fonctions
                 jr       nc,str_to_func
    

    Afin de déterminer le type de valeur à décoder, un certain nombre de tokens est là aussi en dur.

    Et donc ?

    Et donc tel quel, la ROM ne fourni pas de mécanisme d’extension pour écrire de nouvelles instructions. D'autre part, même s'il reste une trentaine de tokens libres à la fin de la liste, les constantes dans le parseur et dans l'évaluateur exigent que les tailles des groupes du tableau soient respectés.

    Reste la possibilité de modifier la ROM pour ajouter les nouvelles instructions, ce qui n'est pas très compliqué, mais qui nécessitera de la conversion au chargement pour être compatible avec des programmes en BASIC enregistrés sur K7.


  • VG5000µ, Schémas de principe ()

    Le schéma de principe scanné depuis la documentation de maintenance trouvée sur le site My VG5000 et reconstitué du VG5000µ m'a souvent aidé à comprendre le fonctionnement de cette machine. Et je remercie l'auteur de ce travail !

    Pour contribuer à mon tour à la documentation VG5000µ, j'ai refait les schémas de la platine principale et de la platine k7 au propre, afin d'en augmenter la lisibilité.

    Quelques commentaires :

    • j'ai ajouté en précision les broches non branchées du Z80 et de l'EF9345. Cela montre rapidement les choix hardware du VG5000µ qui ne seront pas contournables sans modification du matériel.

    • j'ai utilisé les nomenclatures des datasheets des composants, ce qui change un peu la nomenclature originale.

    • j'ai gardé la disposition générale du schéma, pour ne pas perdre les habitués, mais il peut y avoir quelques changements dans le détail, lorsque je pensais pouvoir améliorer la lisibilité.

    • j'ai modifié les marquages des portes logiques vers une nomenclature en toute lettre ; par contre, je n'ai aucune idée de la nomenclature officiel pour un Buffer, j'ai mis BUF en attendant.

    • une porte AND, en sortie des deux portes NAND du 7812 n'avait pas de marquage sur le schéma originel, j'ai retrouvé le composant en suivant le schéma d'implantation (ce n'était pas trop compliqué, puisque qu'il n'y a qu'un seul composant qui fourni des portes AND, mais au moins, c'est vérifié)

    • le schéma original comporte la plupart des signaux en anglais, sauf quelques-uns (genre RVB), j'ai gardé RVB sur les sorties mais changé en RGB sur la nomenclature EF9345, comme sur la datasheet.

    • De même il y a un mélange COMMUT.RAPIDE mais SOUND sur la sortie vidéo, j'ai tout unifié en français.

    • Mais du coup, j'ai des signaux en anglais sur la sortie SON/K7...

    Merci à tous les commentateurs du fil de discussion sur System.cfg qui m'ont permis d'améliorer les schémas.

    La platine principale

    Image cliquable pour une version en haute définition. (mise à jour 29 avril 2021)

    Platine principale

    La platine K7/Son

    Image cliquable pour une version en haute définition. (mise à jour 9 sept. 2018)

    Platine K7/Son

    Note

    Le schéma a été mis à jour dans un nouvel article.


  • VG5000µ, SetPoint en C ()

    Après cette implémentation en assembleur Z80 d'une fonction setpoint qui affiche, de manière assez basique, un point à l'écran, je me pose la question d'utiliser un langage de plus haut niveau... mais pas trop.

    J'ai une assez longue expérience du C et la question que je me pose est : qu'est-ce que ça donne de programmer en C pour générer du code sur Z80.

    Programmer en C a quelques avantages a priori : c'est nettement plus concis et lisible que de l'assembleur, j'y suis plus habitué et c'est portable sur de nombreuses plateformes. C'est le cas d'autres langages, mais le choix naturel pour moi puisque écrire du C m'est habituel. En tout cas bien plus habituel que d'écrire directement de l'assembleur Z80.

    Premier essai

    Voici le code C d'un premier essai :

    #include <stdint.h> // Afin d'utiliser les types standards
    
    void setpoint(uint16_t x, uint8_t y) // Définition de la fonction
                                         // En entrée, les coordonnées
                                         // Pas de valeur de retour (void)
    {
        const uint8_t zx = x / 2;       // Les mêmes calculs qu'en BASIC
        const uint8_t rx = x & 2;
    
        const uint8_t zy = y / 3;
        const uint8_t ry = y - (zy * 3);
    
        const uint8_t ch = 1 << (ry * 2 + rx);
    
        const uint16_t address = 0x4000 + zy * 80 + zx * 2;
        const uint8_t at = *((uint8_t*)address + 1);
    
        uint8_t old = 64;
    
        if (at & 0x80 == 0x80) {
            old = *((uint8_t*)address);
        }
    
        if (old & 0x80 == 0x80) {
            old = 64;
        }
    
        uint8_t new = ch | old;
    
        *((uint8_t*)address) = new;
        *((uint8_t*)address+1) = 224;
    }
    
    int main() // Je dois ajouter une fonction `main` pour que le fichier CRT puisse en trouver une.
    {
        return 0;
    }
    

    J'ai globalement laissé les mêmes noms de variable que j'ai utilisé dans la version BASIC, et les mêmes calculs. C'est un portage simple, sans trop se poser de questions.

    Le compilateur SCCZ80

    Afin de transformer le code C en code machine, il me faut un compilateur qui sache sortir du langage machine Z80. J'en connais deux, mais il en existe d'autres. Le premier fait parti de l'environnement complet z88dk et se nomme SCCZ80. L'autre est SDCC, pour Small Device C Compiler. Ce dernier peut générer du code pour de nombreux microprocesseurs. Il peut être aussi utilisé par l'environnement spécialisé z80 qu'est z88dk.

    Je commence avec SCCZ80 : zcc +vg5k -O3 -m c_setpoint_basic.c

    • +vg5k indique que à la suite que ma plateforme cible est un VG5000µ,
    • -O3 indique que je veux optimiser le code au niveau maximum,
    • -m indique que je veux générer un fichier avec des informations sur le résultat,
    • et enfin le nom du fichier en C.

    Pour que la compilation fonctionne, je dois ajouter une fonction main(). En effet, le CRT (C Run Time) va chercher cette fonction main() pour l'appeler au démarrage. Or pour utiliser la fonction -m du compilateur, je dois construire une application complète.

    Je pourrais tenter de compiler seulement le fichier .c en fichier objet (.o), malheureusement, comme on va le voir juste après, SCCZ80 appelle de nombreuses fonctions... Bref, c'est plus simple de faire une application complète.

    Et cela me permet d'aller voir dans le fichier .map la taille de la fonction setpoint telle que compilée pour ce compilateur.

    251 octets.

    La fonction écrite à la main fait 196 octets, fonctions de multiplication et division comprises. Ici, c'est 251 octets sans les appels aux nombreuses fonctions donc le compilateur se sert.

    Le compilateur SDCC

    Avant d'aller plus loin, un petit essai avec SDCC donne 125 octets, auquel il faut ajouter la fonction de division, 42 octets, pour un total de 167 octets. C'est beaucoup mieux. C'est même mieux que le code écrit directement en assembleur, qui ne l'oublions pas utilise une table assez importante pour la division. Le code manuel du setpoint hors appels de fonction reste plus petit.

    Par contre, SDCC fait grand usage des registres IX et IY. Et IX est strictement réservé par la ROM du VG5000µ, il faut l'utiliser avec des grandes précautions. Le fichier CRT du VG5000µ le préserve en sauvant sa valeur, et part du principe que l'on configure l'assembleur pour échanger l'utilisation de IY et IX (pour au final ne pas utiliser IX).

    Pourquoi c'est si gros ?

    Je reviens en premier lieu sur la compilation en SCCZ80. Ce compilateur part sur une stratégie : la compilation d'un programme entier doit donner un exécutable petit. Pour cela, les fonctions les plus courantes sont factorisées dans des routines assembleurs utilisées par le compilateur.

    C'est le cas par exemple pour les fonctions de division et de multiplication. Mais c'est aussi le cas pour des services comme « aller chercher un entier sur la pile », utile dans le modèle C pour le passage de paramètres aux fonctions.

    En regardant le code généré, on peut voir aussi beaucoup de manipulations de registres pour essayer de faire les calculs sur 8 bits que je demande. Ça à l'air un peu trop complexe. Et si on essayait de partir sur une base de calculs en 16 bits ?

    Pour cela, je change les types uint8_t du code source ci-dessus en uint16_t.

    216 octets ! Voici qui est beaucoup mieux.

    Le compilateur SCCZ80 a donc l'air de mieux travailler avec des entiers de 16 bits. Cela est en partie du à l'utilisation de ces routines annexes, qui sont écrites avec un modèle sur 16 bits en tête. En demandant des calculs sur 8 bits, j'oblige le compilateur à sans cesse passer de 8 à 16 bits et inversement.

    Et SDCC ? ... 183 octets. Là, ce n'est pas bon. De son côté, SDCC était plus efficace à faire ses opérations sur du 8 bits.

    SDCC contrairement à SCCZ80, a pour objectif principal la vitesse d'exécution, quitte à avoir du code machine plus gros. Pour cela, certaines stratégies sont employées. Ici, tous les calculs sont faits sur place, avec des manipulations de registres, à l'exception notable de la division.

    Du coup, travailler sur les registres 16 bits est un peu plus compliqué et cela a un impact direct sur la taille du code.

    Est-ce qu'on peut aider ?

    Première idée, puisque SDCC passe beaucoup de temps à récupérer les arguments de la fonctions, c'est de repasser ceux-ci sur 8 bits.

    void setpoint(uint8_t x, uint8_t y)
    

    Avec moins d'instruction d'accès à la pile, on fait gagner 10 octets à SDCC. Pas négligeable. Côté SCCZ80, c'est un gain de 3 octets. C'est toujours ça.

    On peut aussi aider en utilisant une fonction de division par 3 spécialisé, comme en assembleur, sous cette forme :

    static uint8_t div3_table[] = {
        0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,
        6,6,6,7,7,7,8,8,8,9,9,9,10,10,10,11,11,11,
        12,12,12,13,13,13,14,14,14,15,15,15,16,16,16,17,17,17,
        18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,23
    };
    
    uint8_t div3(uint8_t dividend)
    {
        return div3_table[dividend];
    }
    

    La fonction de division générique utilisée pour SCCZ80 fait 37 octets et pour SDCC, 42 octets. La fonction par tableau utilisée ici est plus grosse, mais plus rapide.

    Côté SCCZ80, on descend à 212 octets... Soit 1 octet de gain. Pas top. Côté SDCC, ça remonte même à 177 octets (ou 176 en jouant sur les tailles des types entiers).

    Conventions d'appel

    Lorsque l'on appelle une fonction avec des paramètres, il y a plusieurs moyens de les transmettre. On peut passer par une convention basée sur l'utilisation de registres, ou bien passer par la pile par exemple. Par défaut, les deux compilateurs passent pas la pile. Cependant, on peut indiquer à la fonction que l'on préfère passer par une autre convention lorsqu'il n'y a qu'un paramètre à la fonction.

    Pour cela, on modifie la fonction div3 comme ceci :

    uint8_t div3(uint8_t dividend) __z88dk_fastcall
    {
        return div3_table[dividend];
    }
    

    Côté SDCC, la fonction div3 fond grâce au passage du paramètre et de la valeur de retour par le registre l. Plus besoin de mécanisme pour aller chercher la valeur dans la pile, calcul qui était généré de manière assez complexe par SDCC. La fonction passe de 27 octets à... 10 octets !

    Le code généré est tout de même étrange :

    _div3:
        ld  c, l                ; Sauvegarde de L dans C
        ld  de,_div3_table+0
        ld  l,c                 ; Récupération de C dans L... mais pourquoi ?
        ld  h,0x00
        add hl, de
        ld  l, (hl)
    l_div3_00101:
        ret
    

    Ce petit tour de passe-passe entre les registres C et L est inutile. L n'est pas utilisé entre temps, et C n'est pas utilisé ensuite. Voilà de quoi gagner 2 octets supplémentaire en réécrivant la fonction à la main.

    Côté appelant, setpoint est aussi un peu simplifié et tombe à 174 octets.

    Utilisons SCCZ80 à présent avec la même convention d'appel. La fonction initiale n'était pas très grosse, avec ses 14 octets, elle passe à 14 octets. Mais là encore les compilateur génère des choses étranges :

    ._div3
        push    hl          ; HL est poussé sur la pile
        ld  de,_div3_table
        pop hl              ; HL est récupéré de la pile
        push    hl          ; pour y être immédiatement remis
        ld  h,0
        add hl,de
        ld  l,(hl)
        ld  h,0
        pop bc              ; pour que la valeur soit ignorée...
        ret
    

    Ces push et pop du registre HL sont rigoureusement inutiles. Il n'y a pas de valeur à préserver. Et au final, le contenu de la pile est extrait dans un registre dont on ne s'est pas servi BC mais qui du coup est invalidé. Ce sont donc 4 octets à économiser (et pas mal de cycles d'exécutions) en enlevant ces instructions. Cela nous amène à 10 octets.

    La différence, si on oubli ces instructions inutiles, entre les deux codes générés est le LD H,0 (2 octets) pour s'assurer que la valeur de retour sur 16 bits est valide avec SCCZ80. SDCC ne prend pas cette précaution, à la charge de l'appelant de n'utiliser que la valeur du registre 8 bits L.

    Et l'inlining ?

    Une possibilité de compilation de l'appel d'une fonction est... de ne pas l'appeler, mais plutôt de faire comme si son code était « sur place ». C'est le mot-clé anglais « inline » qui nous permet de spécifier cela.

    Côté positif, si le code de la fonction lui-même est petit par rapport au code nécessaire au passage des paramètres et code de retour, cela peut-être gagnant d'injecter le code sur place. Côté négatif, il est possible que ce code injecté un peu partout fasse augmenter la taille du code final.

    inline uint8_t div3(uint8_t dividend)
    {
        return div3_table[dividend];
    }
    

    Avec SCCZ80, je ne suis pas allé bien loin. L'utilisation de « inline » fait émettre des erreurs au compilateur. Je ne me suis pas penché plus sur le problème.

    Avec SDCC par contre, non seulement le mot clé est pris en compte, mais le code est réduit de pas mal : 161 octets. Et forcément, pas de code généré pour div3. C'est plus petit que ce que j'avais écrit à la main (ce qui peut aussi s'expliquer par mes compétences limitées en assembleur Z80).

    Conclusion

    Le C est utilisable pour générer du code Z80. Les compilateurs utilisent des versions réduites du C actuel, mais néanmoins très corrects. Le bon côté est que le temps d'écriture du code, pour certaines opérations, est réduit, en tout cas pour moi.

    Par contre, il est nécessaire de garder le code généré à l’œil. Celui-ci peut rapidement être gourmand ou générer des choses inutiles. Il faut alors se poser la question d'adapter se code et de le passer en assembleur, en perdant au passage la portabilité.

    Cette portabilité reste limitée, puisqu'il est nécessaire d'aider fortement le compilateur.


  • VG5000µ, SetPoint en ASM, afficher le point ()

    À présent que l'on sait diviser par 3, reprenons l'affichage d'un point à l'écran. Pour rappel.

    En entrée, nous avons : des coordonnées X et Y, comprises entre 0 et 79 pour X et 0 et 74 pour Y.

    En effet de bord, c'est-à-dire en modification de l'état de la machine, nous voulons : le point correspondant à l'écran qui prend la couleur d'encre définie.

    Pour cette version, la procédure ne prendra pas d'information de couleur, je me contenterai d'utiliser la couleur d'encre 0 (noir) sur fond 6 (bleu), qui est la combinaison à l'initialisation de la machine.

    Les étapes, d'après les articles précédents, sont donc :

    • À partir de X et Y, trouver les coordonnées du caractère à modifier à l'écran
    • À partir de X et Y, trouver les coordonnées à l’intérieur du caractère semi-graphique
    • À partir de coordonnées du caractère, calculer l'adresse mémoire écran correspondante
    • Récupérer les valeurs pour la paire d'adresse mémoire
    • Si le caractère présent n'était pas un caractère semi-graphique standard, considérer qu'il était complètement éteint (valeur 0 pour le caractère)
    • Modifier la valeur du caractère récupéré en fonction des coordonnées à l'intérieur du caractère semi-graphique
    • Modifier la mémoire écran avec les nouvelles valeurs

    Comme le code est plutôt long, je vais changer de méthode. Le code entier va suivre, commenté au maximum en ligne.

    Le Code

            ; La procédure se nomme 'setpoint' et sera appelée avec `call setpoint`
            ;
            ; Les coordonnées (x,y) du point à allumer sont mises dans, respectivement
            ; L et H. Autrement dit, HL contient yx.
            ;
            ; C'est le format utilisé par la ROM du VG5000µ pour ses coordonnées de
            ; curseur. Autant le garder.
            ;
    setpoint:
            ; La procédure sauve tous les registres exceptés IX et IY.
            ;
            ; On pourrait aussi considérer que c'est à l'appelant de veiller à
            ; garder ses registres intègres
            ;
            ; Ce n'est pas le plus efficace, mais pour le moment, c'est le plus sûr.
            push    hl
            push    bc
            push    af
            push    de
    
            ; Pour le moment :
            ; - HL contient les coordonnées (y,x)
            ; - Les autres registres sont libres
    
            ld      a,h             ; On travaille sur y
            call    div3            ; Que l'on divise par 3
            ld      d,a             ; Et donc D contient y/3
    
            ld      a,l             ; On travaille sur x
    
            ; La ligne suivante ne fonctionne que si C (le Drapeau de retenue)
            ; est bien à zéro (ce qui est assuré avec le div_3 utilisé)
            ; Sinon, il faudrait utiliser `srl a`, qui est codé sur deux octets plutôt que 1
            rra                     ; On divise A par 2
            ld      e,a             ; Et donc D contient x/2
    
            ; À ce point :
            ; - DE contient les coordonnées (y/3,x/2)
            ; - HL contient toujours les coordonnées (y,x)
            ; - Les autres registres sont libres
    
            ; B sera utilisé temporairement
            ld      a,l             ; On travaille à nouveau sur x
            and     $01             ; On ne garde de A que le bit de poids faible.
            ld      b,a             ; Et donc B contient x modulo 2 (le reste de la division entière par 2)
    
            ld      a,h             ; On travaille à nouveau sur y
            sub     d               ; On soustrait D, qui contient y/3 (partie entière)
            sub     d               ; Une seconde fois
            sub     d               ; Puis une troisième fois.
                                    ; Et donc A contient y modulo 3
    
            ; On remarque ici qu'une fonction qui retournerait en même temps le quotient
            ; ET le reste de la division entière pourrait faire gagner un peu de temps...
    
                                    ; On travaille dessus immédiatement sur (y modulo 3)
                                    ; Dorénavant, j'utiliserai le signe % pour modulo.
    
            add     a               ; A contient à présent (y % 3) * 2
            add     b               ; A contient à présent (y % 3) * 2 + (x % 2)
    
            ; Petit point :
            ; - A contient la puissance de 2 nécessaire à trouver le bon caractère
            ; - DE contient toujours les coordonnées (y/3,x/2)
            ; - BC ne contient plus rien d'intéressant
            ; - HL ne contient plus rien d'intéressant
            ;   À vrai dire, HL n'est plus utile depuis le ld a,h précédent
    
            or      a               ; Équivalent à cp $0 mais plus conci et rapide
                                    ; Le résultat de cette comparaison de A avec 0
                                    ; va être conservé par les drapeaux jusqu'au
                                    ; JR suivant, car les instructions LD n'altèrent
                                    ; par les drapeaux.
    
            ; Il faut à présent calculer la valeur 2 à la puissance A
    
            ld      b,a             ; On charge B avec la valeur A. B va servir de
                                    ; compteur de boucle.
            ld      a,1             ; On initialise le résultat à 1
    
            jr      z,no_power      ; Si A était égal à zéro, on n'a rien besoin
                                    ; de calculer, donc on passe à la suite.
    
            ; La boucle suivante décale vers la gauche le contenu de A de 1 position
            ; Autrement dit, A est multiplié par 2 à chaque tour de boucle.
            ; À la fin de la boucle, A contient donc 2 puissance B.
    power_of_2:
            rla                     ;
            djnz    power_of_2
    
    no_power:
            ; A contient l'index du caractère semi-graphique à aller chercher.
            ; On sauve cette valeur pour plus tard dans la pile.
            push af
    
            ; Petit point :
            ; - HL est libre
            ; - BC est libre
            ; - DE contient les coordonnées (y/3, x/2)
            ; - A est sauvé sur la pile, on pourra donc l'utiliser pour des calculs
    
            ; L'objectif est à présent d'aller calculer l'adresse mémoire du caractère
            ; à changer dans la plage mémoire dédiée en RAM.
    
            ; La fonction de multiplication que j'utilise ici, et qui n'aura pas
            ; son article dédié, utilise HL et DE. DE est transféré dans BC.
            ld      b,d
            ld      c,e
    
            ; Et donc à présent DE est libre
            ; Et BC contient (y/3, x/2)
    
            ; Le premier calcul à faire est (y/3)*80
            ld      h,80            ; H contient 80
            ld      e,b             ; E contient y / 3
    
            call    mult            ; Appel de la multiplication
                                    ; À présent, HL contient (y/3)*80
    
            ; Le second calcul à faire est d'arrondir X à l'entier pair
            ; inférieur le plus proche. Pour cela, (x/2)*2, en utilisant
            ; une division entière donne le ŕésultat.
    
            ld      a,c             ; A contient x/2 (division entière)
            add     a               ; A contient (x/2)*2 (division entière)
    
            ; Petit point :
            ; - HL contient le déplacement mémoire sur le début de la ligne
            ; - A contient le déplacement en colonnes sur la ligne
            ; - BC est libre
            ; - DE est libre
    
            ; Il faut donc additionner HL et A pour avoir l'index mémoire.
            ; Le Z80 ne peut pas faire ça directement. Il faut donc charger
            ; A dans un registre 16 bits. BC par exemple.
            ld      b,0
            ld      c,a
    
            add     hl,bc           ; HL contient à présent (x/2)*2 + (y/3)*80
    
            ; On se ressert de BC pour indiquer la base de l'adresse mémoire vidéo
            ; auquel on ajoute l'index, pour obtenir l'adresse mémoire dans HL.
            ld      bc,$4000
            add     hl,bc
    
            ; Il est temps d'aller chercher les informations déjà présentes
            ; en mémoire. Pour cela, on a besoin des deux adresses HL et HL+1
            ; (voir les articles sur l'agencement de la mémoire vidéo)
            ld      b,h
            ld      c,l
    
            inc     bc              ; BC contient HL + 1
    
            ld      a,(bc)          ; A contient donc la valeur d'attribut du caractère
            bit     7,a             ; Ce qui nous intéresse est sont bit numéro 7
            jr      z,set_base_char ; S'il est à 0, ce n'est pas un caractère semi-graphique
    
                                    ; Sinon, on a besoin de connaître la valeur actuelle de ce
                                    ; caractère
    
            ld      a,(hl)          ; On récupère la valeur du caractère semi-graphique
                                    ; actuellement à l'écran dans A
    
            ; Si le caractère fait partie de la place 64 à 127 (les caractères pleins)
            ; alors on continue plus loin.
            bit     7,a
            jr      z,char_ok
    
    set_base_char:
            ld      a,64            ; Dans le cas où le caractère à l'écran n'était pas
                                    ; semi-grapique plein, on part sur une base du
                                    ; caractère 64, qui est le caractère semi-graphique
                                    ; 'tout éteint'
    char_ok:
            pop     de              ; Recupération dans D de l'index du caractère calculé.
                                    ; Cette valeur vient du 'push af' effectué plus haut.
    
            or      d               ; Une opération bit à bit 'OU' entre l'ancienne valeur
                                    ; et le nouvel index donne le nouveau caractère.
    
            ld      (hl),a          ; Ce caractère est placé à l'écran
    
            ld      a,224           ; Et dans cette implémentation, on fixe les attributs
            ld      (bc),a          ; selon les valeurs de couleurs à l'allumage du VG5000µ
                                    ; Une amélioration sera d'aller chercher dans les variables
                                    ; systèmes quels sont les couleurs courantes.
    
            ; La routine se termine, on restitue la valeur de tous les registres
            ; utilisés pour revenir à l'appelant.
            pop de
            pop af
            pop bc
            pop hl
    
    
            ret
    
    div3:                           ; Entrée: registre A, Sortie: valeur divisée par 3, dans A
            exx                     ;
            ld      hl,div3_table
            ld      b,0
            ld      c,a
            add     hl,bc
            ld      a,(hl)
    
            exx                     ;
            ret
    
    div3_table:
            defb    0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5 ; 18
            defb    6,6,6,7,7,7,8,8,8,9,9,9,10,10,10,11,11,11
            defb    12,12,12,13,13,13,14,14,14,15,15,15,16,16,16,17,17,17
            defb    18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,23
    
    mult:                           ; Entrée, registre H et registre E
                                    ; Sortie, le registre HL comtient le résultat
                                    ; de l'opération H * E
                                    ; Utilise HL, B, DE
            ld      d,0
            ld      l,d
            ld      b,8
    mult_loop:
            add     hl,hl
            jr      nc,mult_skip
            add     hl,de
    mult_skip:
            djnz    mult_loop
            ret
    

    Comment est-ce que ça s'utilise ?

    Cette routine s'utilise donc en mettant dans HL les coordonnées (y, x) du point à afficher. Elle pourrait être améliorée en déterminant si on veut afficher ou éteindre le point, spécifier les couleurs ou les récupérer des variables systèmes. Il y a probablement quelque optimisations qui trainent.

    Toujours est-il que par rapport à la routine est basique, l'exécution sera beaucoup plus rapide. Il serait possible d'être encore plus réactif en s'adressant directement au processeur vidéo. Mais je préférais utiliser le buffer vidéo en RAM pour plus de simplicité. Une implémetnation utilisant le processeur vidéo peut se trouver dans la bibliothèque d'affichage de Z88DK pour le VG5000µ. Z88DK est tout un système, basé sur un compilateur C, pour programmer les machines Z80.

    Pour revenir à l'utilisation de cette routine, voici un exemple qui affiche plusieurs lignes horizontales à l'écran.

            org     $7000
    
            ; Sauvegarde des registres utilisés.
            push    hl
            push    bc
    
            ld      h,$0A           ; La coordonnée y est 10 ($A en hexa)
            ld      b,25            ; On prépare une boucle de 25 itérations
    loop_1:
            ld      c,b             ; Sauvegarde temporaire de la boucle externe
                                    ; afin de préparer une boucle interne
    
            ld      l,$10           ; La coordonnée x est 16 ($10 en hexa)
            ld      b,40            ; On prépare une boucle de 40 itérations
    loop_2:
            call    setpoint        ; On affiche un point
            inc     l               ; On incrémente la coordonnée x de 1
            djnz    loop_2          ; Et on boucle
                                    ; Ce qui affiche une ligne de 40 pixels de large
                                    ; à la coordonnée y courante.
    
            ld      b,c             ; Récupération de l'index de boucle externe
    
            inc     h               ; On incrémente la coordonnée y deux fois
            inc     h               ; on "saute" donc une ligne.
    
            djnz    loop_1          ; Et on recommence ceci 25 fois.
                                    ; On affiche donc 25 lignes les unes sous les autres
                                    ; séparées à chaque fois par une hauteur d'un pixel
    
            ; Restauration des registres utilisés et retour à l'appelant
            pop     bc
            pop     hl
    
            ret
    

    Résultat

    Affichage des résultats de tests dans MAME


  • VG5000µ, SetPoint en ASM, diviser par 3 sans diviser ()

    Les trois derniers articles sur la division on permit de s'attarder sur trois manière de diviser un nombre entier par 3.

    La méthode de cet article, qui sera le dernier avant de revenir à l'affichage d'un point, va diviser grâce à, globalement, une seule addition. Oui ! Une seule addition.

    L'idée

    Au tout début de la série d'articles sur la division, j'ai mis en place un système de tests pour m'assurer que mes bouts d'assembleurs faisaient ce qu'il étaient censés faire. Et pour cela, je comparais une série de divisions avec un tableau de résultats.

    Mais alors, pourquoi ne pas utiliser un tableau de résultats directement ? On stock quelque part le résultat de toutes les divisions par 3 des nombres entiers de 0 à 255, et on va piocher dedans. Facile à implémenter, ultra rapide.

    La contrepartie, évidemment, c'est que cela va prendre un peu de place. Mais voyons ce que cela donne.

    Le code

    Nul besoin d'une longue explication pour cette méthode. L'entier à diviser (le dividende) est dans le registre A, on l'ajoute à l'adresse du tableau des résultats pré-calculés, on récupère la valeur dans A et voilà !

    div3_5:                         ; Entrée: registre A, Sortie: valeur divisée par 3, dans A
            exx                     ; Utilisation des registres secondaires
            ld      hl,div3_table   ; Chargement dans HL de l'adresse de la table des résultats
            ld      b,0             ; Mise à 0 de B
            ld      c,a             ; Placement de la valeur de A dans C
                                    ; On a donc à présent le registre BC qui contient le dividende
            add     hl,bc           ; Addition du dividende et de l'adresse du tableau
            ld      a,(hl)          ; Récupération du résultat
    
            exx                     ; Échange des registres secondaires
            ret                     ; Retour à l'appelant.
    
    div3_table:                     ; La table des résultats
            defb    0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5
            defb    6,6,6,7,7,7,8,8,8,9,9,9,10,10,10,11,11,11
            defb    12,12,12,13,13,13,14,14,14,15,15,15,16,16,16,17,17,17
            defb    18,18,18,19,19,19,20,20,20,21,21,21,22,22,22,23,23,23
            defb    24,24,24,25,25,25,26,26,26,27,27,27,28,28,28,29,29,29
            defb    30,30,30,31,31,31,32,32,32,33,33,33,34,34,34,35,35,35
            defb    36,36,36,37,37,37,38,38,38,39,39,39,40,40,40,41,41,41
            defb    42,42,42,43,43,43,44,44,44,45,45,45,46,46,46,47,47,47
            defb    48,48,48,49,49,49,50,50,50,51,51,51,52,52,52,53,53,53
            defb    54,54,54,55,55,55,56,56,56,57,57,57,58,58,58,59,59,59
            defb    60,60,60,61,61,61,62,62,62,63,63,63,64,64,64,65,65,65
            defb    66,66,66,67,67,67,68,68,68,69,69,69,70,70,70,71,71,71
            defb    72,72,72,73,73,73,74,74,74,75,75,75,76,76,76,77,77,77
            defb    78,78,78,79,79,79,80,80,80,81,81,81,82,82,82,83,83,83
            defb    84,84,84,85,85,85
    

    Les instructions utilisées

    Il n'y a pas vraiment de nouvelles instructions utilisées ici, mais de nouvelles formes :

    • Comme on ne peut pas additionner directement les registres HL et A, on doit passer la valeur de A dans un registre 16 bits. BC est souvent utilisé, mais ça aurait pu être DE.
    • Comme on ne peut pas charger le contenu de A directement dans BC, on le fait en deux étapes. Le registre 16 bits BC est constitué des deux registres 8 bits B et C. On place donc 0 dans B et C prend la valeur de A.
    • LD A,(HL) récupère la valeur pointée par le registre HL, plutôt que la valeur de HL. C'est-à-dire que l'octet à l'adresse mémoire pointée par HL est récupérée, puis chargé dans A.

    C'est mieux du coup ?

    D'un point de vu code de la fonction elle-même, c'est nettement mieux, 11 octets seulement. Par contre suivi d'un tableau de 255 octets. C'est donc à la fois le code le plus concis jusqu'à maintenant, mais aussi la fonction la plus grosse dans sa globalité.

    Il est possible cependant de la réduire dans le cas présent. En effet, comme on ne divise que des numéros de lignes, on pourrait s'arrêter à 75 résultats. Cela donne 86 octets, c'est toujours plus imposant que les autres versions, mais un peu mieux.

    Attention aussi, l'accès au tableau n'est pas protégé. S´il est plus petit que 255, rien n'empêche l'appelant de passer une valeur qui va déborder. Cela donnera des résultats faux.

    D'un point de vu rapidité c'est constant et rapide : 15 cycles. Comme c'est une fonction très courte, il est possible aussi, au besoin, de l'inliner, c'est-à-dire de faire l'opération à l'endroit où elle est nécessaire plutôt que d'appeler une fonction. Cela économise le temps de mise en place et de retour de la fonction (les EXX et RET). On tombe alors à 10 cycles, que l'on peut potentiellement améliorer en utilisant les valeurs de registres au moment de l'appel.

    Difficile de faire plus rapide comme méthode (à quelques astuces potentielles prêt).

    Que choisir ?

    C'est la question. Nous voici avec quatre méthodes pour diviser par trois. Deux sont des méthodes générales de division, deux sont spécialisées dans la division par 3. La première question à se poser est donc de savoir de quoi on a besoin.

    Ensuite, nous avons des fonctions qui ont des compromis en terme de taille et de rapidité.

    C'est ici qu´utiliser une fonction s'avère utile. Pour le moment, le choix n'a pas d'importance. Les quatre fonctions demandent un dividende dans A et retournent le résultat dans A. Il est donc possible de remplacer l'une par l'autre et de continuer le développement de l'affichage du point.

    Il sera temps, ensuite, de choisir quelle méthode sera la plus adéquate.

    Spoiler : probablement aucune de celles-ci en l'état, car nous allons avoir besoin du reste de la division par 3...

    À la prochaine !


« (précédent) Page 13 / 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