Triceraprog
La programmation depuis le Crétacé

  • Récréation 3D, Z80 du VG5000µ ()

    Deux ans déjà que j'avais créé quelques modèles 3D... Le temps passe vite. Et l'envie m'a repris.

    Voici donc une petite recréation du Z80 présent dans le VG5000µ. Fait depuis des images et je ne suis donc pas complètement certains des mesures. J'irai vérifier la prochaine fois que j'en démonte un, si j'y pense.

    Z80 présent dans le VG5000µ

    Update: nouvelle version, corrigée avec des dimensions DIP plus correctes (mais le boitier du SGS est plat... ça fait donc un mélange)

    Z80 présent dans le VG5000µ


  • Bonne Année 2019 ! ()

    Bonjour à tous. Cette année, j'espère pouvoir faire aboutir un projet autour du VG5000µ que j'ai commencé il y a longtemps et que j'avance petit pas par petit pas.

    En attendant, je vous souhaite :

    10 B$="AAHGGGGGCB00A@GGGGCBCB00998898=<10"
    20 LB=LEN(B$)
    30 FOR I=1 TO LB STEP 2
    40 A=ASC(MID$(B$,I,1))+ASC(MID$(B$,I+1,1))-64
    50 PRINT CHR$(A);
    60 NEXT I
    

    À essayer sur votre ancienne machine sous BASIC préférée. Ça devrait être portable sur à peu près toutes les machines avec interpréteur BASIC et des capactités par trop limitées sur les chaînes de caractères (adieu ZX81...). Sur certains, il faudra ajouter LET aux lignes 10, 20 et 40 pour les assignations de variable.


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

    CommandesSupportOpérateursFonctions
    128:END178:TAB(185:+195:SGN
    129:FOR179:TO186:-196:INT
    130:NEXT180:FN187:*197:ABS
    131:DATA181:SPC(188:/198:USR
    132:INPUT182:THEN189:^199:FRE
    133:DIM183:NOT190:AND200:LPOS
    134:READ184:STEP191:OR201:POS
    135:LET192:>202:SQR
    136:GOTO193:=203:RND
    137:RUN194:<204:LOG
    138:IF205:EXP
    139:RESTORE206:COS
    140:GOSUB207:SIN
    141:RETURN208:TAN
    142:REM209:ATN
    143:STOP210:PEEK
    144:ON211:LEN
    145:LPRINT212:STR$
    146:DEF213:VAL
    147:POKE214:ASC
    148:PRINT215:STICKX
    149:CONT216:STICKY
    150:LIST217:ACTION
    151:LLIST218:KEY
    152:CLEAR219:LPEN
    153:RENUM220:CHR$
    154:AUTO221:LEFT$
    155:LOAD222:RIGHT$
    156:SAVE223: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 9 sept. 2018)


    La platine K7/Son

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


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


Page 1 / 11 (suivant) »