Je reprends le fil de ce post pour expliciter les différentes étapes de la création de mon émulateur.
A partir du moment où les outils de développement sont en place, que la programmation sous Windows commence à être maitrisée vient la première question : par où commencer pour créer un émulateur ? vaste question...
Les différentes documentations que j'ai lues indiquent que développer un émulateur est conceptuellement assez simple : c'est une simple boucle dans laquelle on va lire une zone mémoire prédéfinie afin d'y récupérer une valeur, l'interpréter [ce sera le rôle du processeur émulée], permettant une mise à jour des variables internes du processeur, lire une autre zone mémoire et ... ainsi de suite, tout simplement !
Et voilà, c'est fini !
Bon, en pratique c'est bien évidemment un peu plus complexe que cela et le premier aspect sur lequel j'ai travaillé est la question du ... TIMING.
Car faire une boucle est facile, faire une boucle qui exécute des instructions avec un timing très précis l'est un peu moins.
Tout ordinateur fonctionne au rythme d'un signal périodique très précis fourni par d'un cristal de quartz. La première chose à faire est d'émuler ce battement cardiaque et c'est d'autant plus primordial sur un AMSTRAD CPC dans lequel plusieurs des composants sont cadencés et synchronisés via ce signal périodique.
Le premier obstacle que j'ai rencontré a donc été d'ordre technique : comment à partir de la fréquence de mon PC (qui tourne en Ghz) créer une boucle qui me permette d'émuler le signal d'horloge de 16 Mhz correspondant à la fréquence d'origine du Gate Array équipant le CPC ?
[MAITRISER LES HORLOGES]
Au début, j'ai pensé utiliser les fonctions intégrées du C++ pour cet usage (bibliothèque <chrono> notamment) et je me suis vite aperçu qu'elles n'étaient pas assez rapide. Une boucle de 16 Mhz nécessite un chronomètre qui puisse donner un top tous les 62,5 nanosecondes. Au final, la solution que j'ai trouvé est de faire directement appel à une instruction hardware du microprocesseur, à savoir RDTSC, qui s'incrémente en temps réel à chaque nouveau cycle processeur.
L'utilisation de cette instruction va me permettre d'en déduire la fréquence réelle du microprocesseur qui sera ainsi divisée par 16 Mhz afin d'en déduire le nombre de cycles d'attente qu'il faudra faire tourner dans une boucle vide au sein de ma boucle principal pour cadencer l'émulateur à la fréquence souhaitée.
Au début je mesurais constamment ce rapport car je pensais que la fréquence du CPU variait constamment dans le temps. Et bien non, quand l'émulateur démarre il semblerait bien que la fréquence du CPU soit fixée une fois pour toute et ne varie plus tant que celui-ci est actif. Tant mieux !
La division par 4 et 16 de ce signal va ainsi me permettre de fixer les fréquences pour l'émulation du Z80 (4Mhz) et pour les autres chipsets fonctionnant à 1 Mhz (CRTC / PSG notamment).
Maintenant, dernière question : comment être certain que la fréquence de l'émulateur soit bien celle voulue ?
Il suffit de mettre une variable qui s'incrémente à la fin de chaque cycle d'attente, de faire tourner en parallèle un timer qui va mesurer précisément le nombre de cycles écoulés chaque seconde afin d'en déduire la fréquence d'émulation et enfin de l'afficher dans la fenêtre Windows. Cela me permet de vérifier s'il n'y a pas un problème de timing lors de l'exécution de l'émulateur...
Au final, je suis surpris de voir que la fréquence d'émulation est extrêmement précise et fluctue très peu dans le temps. C'est très utile pour vérifier que certaines routines ne consomment pas trop de ressources CPU entre chaque tour de boucle. J'en reparlerai plus tard.
Voilà pour cette première étape importante. A suivre
PS : La méthode que je donne ici correspond la solution empirique que j'ai trouvé pour répondre à mon problème de Timing. Il y a certainement d’autres techniques pour arriver à cette même fin. Je serai ravi d'échanger dessus avec d'autres concepteurs d'émulateurs.