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 standardsvoidsetpoint(uint16_tx,uint8_ty)// Définition de la fonction// En entrée …
À 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
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 …
Dans le dernier article, j'avais cherché une version plus rapide pour effectuer une division par 3, toujours dans l'optique d'afficher un point à l'écran en transcrivant la routine écrite en BASIC vers de l'assembleur Z80.
Le résultat était mitigé : une routine en moyenne plus rapide et plus stable, mais sur le domaine de définition considéré (le nombre de lignes graphiques du VG5000µ), pas entièrement gagnante.
Dans cet article, je vais donc étudier une autre manière de faire, un peu différente. Alors que les deux premières implémentations permettait de diviser par n'importe quel nombre de 1 à 255, cette nouvelle version est spécialisée dans la division par 3.
Est-ce que cette spécialisation permettra de gagner en performance et/ou en taille ?
L'idée
Le Z80 n'a pas d'instruction pour diviser de manière générale, cela a déjà été mentionné dans les articles précédents. Par contre, il est tout à fait possible de …
Dans le dernier article, j'avais écrit une première manière d'effectuer une division, en spécialisant la routine pour une division par 3 puisque c'est ce qui m'intéresse pour afficher un point à l'écran du VG5000µ.
Pour référence, cette routine nécessitait 15 octets en mémoire et son exécution pouvait prendre jusqu'à 695 cycles, ce qui est plutôt grand.
Micro optimisation
La première optimisation est une micro optimisation. On appelle micro optimisation une amélioration de la routine par un détail de fonctionnement, plutôt que par une réflexion sur l'ensemble de la méthode.
Ici, l'idée est de remplacer le couple push bc / pop bc en début et fin de la routine par un couple exx / exx. L'instruction exx du Z80 effectue un renommage des registresBC, DE et HL. Ces registres (ainsi que d'autres, mais qui ne sont pas touchés par cette instruction) existent en deux exemplaires dans le Z80. Un exemplaire de chaque …
À présent que j'ai un garde fou pour vérifier que je ne fais pas d'erreur d'inattention, me voilà près à diviser des nombres. Pour rappel j'ai besoin de diviser des nombres afin de faire les calculs permettant d'affiche le bon pixel à l'écran.
Pour second rappel, le Z80, au cœur du VG5000µ (et de beaucoup d'autres ordinateurs de l'époque) n'a pas d'instruction de division.
La division
Lorsque je divise de manière entièrea par b, je veux trouver le nombre c tel quel $c * b = a$. Comme la division ne tombe pas toujours juste, j'ai aussi un rester tel que $c * b + r = a$.
Autrement dit, combien de fois dois-je additionner b pour obtenir a (au reste près). Une manière de trouver le résultat est de soustraireb à a autant de fois que l'on peut sans passer sous 0.
Par exemple, $\frac{21}{7}$ se trouve comme ceci …
Après avoir mis en place une vérification (légère) de l'intégrité de la pile, je passe à la vérification de la validité de l'appel d'une fonction.
Le fonctionnement du test est assez simple : je prends une suite de nombres, j'appelle une fonction avec en paramètre chacun de ces nombres, je vérifie que le résultat est conforme à ce que j'attendais.
Par exemple, si je veux tester une fonction diviser par 2 (division entière), je peux utiliser la suite de nombre 0, 10, 32, 255 et comparer les résultats respectifs avec 0, 5, 16, 127 (255 étant impair, le résultat de la division entière est 127, avec un reste égal à 1).
Encore plus simple qu'une division par 2, il y a la fonction identité : celle qui renvoie le paramètre sans le toucher. Tester cette fonction permet de se concentrer sur le développement du test.
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.
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 …
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 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.
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 …
Précédemment, j'ai commencé à parler d'une chaîne d'assemblage pour développer un programme depuis un ordinateur actuel vers une machine ancienne, le VG5000µ qui est l'ordinateur qui nous suit depuis le début de ces articles.
Un des avantages d'utiliser une machine de développement différente de la machine cible est de ne pas souffrir des erreurs, plantages et reboot provoquées par des erreurs. Imaginez, développer sur la machine que l'on programme elle-même, à une époque où le système d'exploitation ne protège rien du tout : une erreur, un reboot et si vous n'avez pas sauvé, il faut recommencer. Et si vous avez sauvé, les temps de chargements et de sauvegardes sont assez long, surtout sur une machine qui n'a pour mémoire externe qu'un magnétophone à cassettes !
Bref, développer sur une machine annexe et envoyer sur la machine cible, c'est plus simple, cela permet de profiter d'outils modernes mais... ça reste lent dans la …
Puisque la fin de l'année approche et que les fêtes dites de fin d'années ainsi que le nouvel an sont d'actualités, j'ai ressorti mon convertisseur d'image vers le VG5000µ publié ici et décrit ici pour une image « haute résolution » sur la machine.
L'astuce pour que la compression passe bien a été de positionner le maximum de motifs identiques sur une grille. Pour les dates et l'adresse du site, j'ai ajouté dans le programme BASIC à la main l'affichage en texte.
On y voit que pour écrire en double hauteur, il faut doubler l'affichage sur la ligne suivante. Et pour écrire en largeur, il faut doubler les lettres à afficher. Le tout en précisant 3 en second paramètre de l'instruction TX.
Le dithering qu'effectue la conversion malheureusement n'a …
Depuis le début de la création de ce site, la majorité des articles a été consacré à du développement sur VG5000µ. Les programmes sont testés sur du matériel réel, même si je les mets au points sur un ordinateur moderne. Les captures d'écrans sont aussi faites sur un vrai VG5000µ, en sortie sur un moniteur récent grâce à un adaptateur Peritel vers HDMI.
Le VG5000µ a cette particularité d'exister sous différentes marques : Philipps, Schneider et Radiola. Plus de détails peuvent se trouver sur des sites plus détaillés dans l'histoire de cette machine, comme je l'indiquais dans un billet en début d'année.
En attendant la suite des articles sur les outils pour programmer cette machine en assembleur, voici une petite photo de famille.
La dernière fois, j'avais évoqué le logiciel d'assemblage, ou assembleur comme étant le programme permettant de passer d'un programme sous sa forme source, c'est-à-dire écrite sous forme lisible par un humain, dans une forme binaire, qui sera plus exactement nommée forme objet. C'est cette forme qui pourra être exécutée par la machine.
Pour programmer, quel que soit la forme que revêt cette programmation, on a besoin d'outils. Les outils primaires peuvent être une table de référence, des manuels, un stylo, des feuilles et beaucoup de patience. Rapidement, le besoin d'outils pour effectuer les tâches automatiques et répétitives qui forment la transformation d'un programme source en programme objet se fait sentir. Et quoi de mieux pour traier des tâches automatiques et répétitives de transformation qu'un... ordinateur.
Le problème de la poule et de l'œuf n'est pas le sujet ici. Le principal est qu'à l'heure de l'écriture de cet article, il y …
Après cette pause estivale, reprenons là où l'on était restés. Dans l'article précédent, je parlais du langage machine, une suite de signaux provoquant l'activité d'un processeur selon des directives précises.
Un inconvénient du langage machine, c'est qu'il est peu pratique à manipuler. Par exemple, sur un Z80, charger l'accumulateur (une mémoire spécifique interne au processeur) avec la valeur 1, je dois écrire : 00111110 00000001 en binaire, ou encore 3E 01 en système hexadécimal.
Écrire de cette manière n'est pas simple, prend beaucoup de temps avec un grand risque d'erreurs. Relire est encore pire. Programmez une machine de cette manière et vous ressentirez a priori rapidement le besoin de manipuler des éléments plus faciles à comprendre pour un Humain.
Et c'est ainsi que du langage machine on passe au langage d'assemblage, ou, par abus de langage, à l'assembleur.
L'assembleur
La première chose à savoir est que l'assembleur est un programme. Mais …
Lors des articlesprécédents sur l'affichage, un résultat était net : c'est lent ! Extrêmement lent. Les magazines ou de livres consacrés à la programmation des machines personnelles des années 1980 affirmaient tous ceci : si vous voulez quelque chose de rapide, passez à l'assembleur.
Que signifie utiliser l'assembleur, et en quoi c'est différent du BASIC ? Pourquoi est-ce que c'est plus rapide ? Était-ce vraiment la seule solution ? C'est ce que nous allons voir dans cet article et les suivants.
J'ai tenté plusieurs approches pour arriver au premier programme en assembleur dans une série d'articles. Et j'en suis arrivé à la conclusion qu'il n'y a pas moyen de passer outre quelques explications rapides des constituants de l'ordinateur et de leurs fonctionnements.
On va tout de même garder une vue large et schématique pour la plupart des composants. Pour le microprocesseur, un Z80 sur le VG5000µ, il faudra descendre un peu vers le fonctionnement …
Il y a deux mois, je publiais ma première récréation en 3D, une évocation d'un VG5000µ. J'avais modélisé la machine un peu au jugé et au final, il y avait pas mal d'erreurs dans les dimensions. Un peu trop à mon goût. J'ai donc refait l'exercice, cette fois avec un VG5000µ et une règle à côté de moi.
Et voici le nouveau résultat, bien plus satisfaisant.
Après la description de l'utilisation de l'affichage haute-définition du VG5000µ, voici le fichier au format K7 et format WAV qui vous permettra de lancer le programme sur votre propre VG5000µ ou un émulateur.
Au passage, le programme contient la version avec la maison sans l'inversion vidéo qui avait lieu dans l'article précédent.
Avec ce mode d'affichage, on obtient une résolution de 80 pixels horizontalement par 75 pixels verticalement. Ce qui donne un total de 6000 pixels. Cependant, le manuel d'utilisation de la machine indique une définition d'image de 80 000 points. Plus de 13 fois plus. Existe-t-il une façon d'afficher des pixels plus petits et d'atteindre une haute (toute proportion gardée) définition ?
Petit rappel : je ne m'occupe pour le moment que des capacités du VG5000µ offertes par le BASIC, et se mettant à la place d'une utilisation à l'époque, avec juste le manuel de base.
Plus fin
Pour atteindre une définition plus fine depuis le BASIC, il faut …
Maintenant que nous avons un algorithme pour tracer un cercle ainsi que le moyen d'afficher un gros pixel à l'écran, nous voilà prêts pour traduire tout cela en BASIC, comme cela a été fait auparavant avec le segment de droite.
Pour rappel, voici l'implémentation en BASIC d'affichage d'un point à l'écran.
100REM AFFICHE UN POINT DANS L'ESPACE SEMI-GRAPHIQUE110REM X CONTIENT L'ABSCISSE ENTRE 0 et 79120REM Y CONTIENT L'ABSCISSE ENTRE 0 et 74130ZX=INT(X/2):ZY=INT(Y/3)140RX=X-ZX*2:RY=Y-ZY*3150CH=2^(RY*2+RX)160DI=&"4000"+ZY*80+ZX*2170AT=PEEK(DI+1)200OL=64:IF(ATAND128)=128THENOL=PEEK(DI)220IF(OLAND128)=128THENOL=64230POKEDI,OLORCH240POKEDI+1,224250RETURN …
Avec de passer à l'implémentation en BASIC du cercle décrit dans un article précédent, passons un peu par une phase de simplification. Cet article va contenir quelques lignes de mathématiques.
L'outil mathématique le plus complexe utilisé est celui des factorisations et développements de carrés. Les identités remarquables seront d'un grand secours. Niveau de fin de collège je crois.
Pour rappel, voici l'algorithme de tracé de cercle de Bresenham :
initialiser cx et cy avec les coordonnées du centre du cercle, r avec son rayon
initialiser (x, y) à (0, r), c'est à dire le point au sommet du cercle.
tant que x ≤ y
tracer le pixel(x + cx, y + cy) et ses sept symétries.
calculer $m = (x+1)^2 + (y-0.5)^2 - r^2$
si m ≥ 0 alors $x \leftarrow x + 1$ et $y \leftarrow y - 1$
sinon $x \leftarrow x + 1$
fin
On pourrait l'implémenter tel quel. Mais je veux …
Si vous avez fait un peu de trigonométrie à l'école, vous devez savoir que, pour un angle α allant de 0 à 2π, tracer des points aux coordonnées (cos(α), sin(α)). Ces coordonnées sont multipliées par le rayon et déplacées au centre du cercle. Et c'est comme cela qu'il peut sembler de prime abord intéressant de tracer un cercle.
Cela ressemble à quelque chose comme ça.
initialiser cx et cy avec les coordonnées du centre du cercle, r avec son rayon
initialiser a à 0
tant que $a < 2\pi$
$x = cx + r.cos(a)$
$y = cy + r.sin(a)$
tracer un point en (x, y)
augmenter a
fin
Super simple.
Mais trop simple.
Dans l'algorithme ci-dessus, la ligne qui indique augmenter a n'indique pas de combien il faudrait augmenter a.
Après avoir décrit un algorithme de tracé de segment de droite de manière générique, voyons un peu comment traduire ça en BASIC sur le VG5000µ. Pour ce premier article, il s'agira d'une implémentation simpliste, qui servira de base.
Pour rappel, voici l'algorithme générique :
En entrée, on a deux points de coordonnées (x, y) et (x', y')
Si x' est plus petit que x, échanger les valeurs de x et x' ainsi que de y et y'
Choix de l'octant en fonction de |y' - y| et de |x' - x|
Si on fait un balayage des x, alors
Calculer la pente $a = \frac{(y' - y)}{(x' - x)}$
Calculer $b = \frac{x'y - xy'}{x'-x}$
Pour tous les x'' de x à x', calculer $y'' = ax'' + b$.
Tracer un pixel en (x'', y'').
Si on fait un balayage des y, alors
Si y' est plus petit que y, échanger les valeurs de x …