L'horloge maître
Tout Phosphoric tient dans une promesse : un appel à emu_cycle(), un cycle de toute la machine. Voici le contrat, l’ordre des puces dans le cycle, et le défaut qui a appris à le tenir.
Dans cette page
Avant : trois horloges
Avant la version 2.0, le temps de la machine vivait à trois endroits. Le processeur avançait sa
propre horloge ; un rappel défini dans main.c faisait avancer les périphériques ; et la boucle
principale calculait elle-même la position du balayage pour savoir quand dessiner une ligne.
Résultat : seule la boucle principale savait cadencer la machine. Le débogueur, les tests ou le
rejeu ne pouvaient pas demander « avance d’un cycle », seulement « exécute une instruction ».
Le contrat
bool emu_cycle(emulator_t* emu); /* un cycle de TOUTE la machine */
int emu_step (emulator_t* emu); /* une instruction, par emu_cycle */
emu_cycle() (src/emu_clock.c) renvoie true quand le cycle qu’il vient d’exécuter terminait une
instruction. N’importe quel appelant peut désormais cadencer la machine : la boucle principale, le
débogueur pas à pas, les tests, le rejeu d’un film.
L’ordre dans le cycle
| Ordre | Qui | Quoi |
|---|---|---|
| 1 | le 6502 | l’unique accès au bus du cycle : lecture, écriture, ou accès factice du NMOS |
| 2 | les périphériques φ2 | VIA, contrôleurs de disquette, ACIA, DTL 2000, MIDI, cassette : avancés d’un cycle |
| 3 | l’ULA | lecture d’une cellule de 6 pixels (la colonne de ce cycle), puis avance du balayage |
Pourquoi le processeur passe avant l’ULA
L’ULA et le 6502 se partagent la mémoire en deux moitiés de la même microseconde, sans jamais se la disputer : il n’y a pas de cycle volé à modéliser. L’ordre de ces deux moitiés a été mesuré à l’oscilloscope sur une vraie machine et publié dans un guide non officiel de l’ULA : au front montant de l’horloge, le 6502 fait son accès ; ensuite commence la lecture de l’ULA.
Conséquence observable : une écriture du processeur au cycle c est vue par la cellule c. Le test
test_cpu_write_visible_in_the_same_cell échoue si l’on inverse l’ordre.
Pas de paquets
Avec le cœur micro-séquencé, chaque cycle est un accès au bus. Le rappel d’horloge reçoit donc
toujours cycles = 1, et les périphériques avancent un cycle à la fois, jamais par lots. Le test
test_peripherals_get_one_cycle_at_a_time échouerait si un paquet réapparaissait.
L’ordre des périphériques entre eux est lui aussi écrit, dans une table à part (io_bus_tick_order[],
décision 0003) : le VIA d’abord, qui pilote les minuteries et
l’interruption, puis la cassette, puis les cartes. Cet ordre a des effets observables ; on ne le
change pas sans le prouver.
Jamais d’appel à vide
Le contrat a une réciproque : chaque appel coûte exactement un cycle au processeur. Une micro-opération qui rendrait la main sans accès au bus ferait avancer l’ULA d’un cycle que ni le processeur ni le VIA n’auraient vécu.
C’est arrivé, en 2.0.0-alpha.8. Un branchement non pris prenait sa décision un cycle trop tard, dans une micro-opération sans accès au bus. Le compteur du processeur restait juste, l’oracle 65x02 était vert à 100 %. Mais l’horloge maître était appelée une fois de trop : environ 410 cycles d’avance du balayage par trame sur la ROM BASIC, une trame de dérive par seconde entre l’image et le reste de la machine.
Ce que porte l’émulateur
Les compteurs du balayage étaient des variables locales de la boucle principale. Les porter dans
emulator_t est ce qui permet à n’importe qui de cadencer la machine :
| Champ | Rôle |
|---|---|
raster_cycle | le cycle courant dans la trame (0 à 19 967) |
raster_rendered | les lignes visibles déjà dessinées (0 à 224) |
raster_ng_line | la ligne déjà traitée par l’ULA-NG (0 à 311) |
raster_next_line | le cycle du prochain changement de ligne |
emu_raster_pos() donne la position du faisceau : ligne PAL (0 à 311) et cycle dans la ligne (0 à
63).
Reprendre en pleine trame
Parce que le balayage est dans l’état de l’émulateur, une sauvegarde d’état (.ost) peut être
prise au milieu d’une trame. Au chargement, Phosphoric recale le balayage, redessine depuis la RAM
les lignes déjà balayées, et rejoue les cellules déjà lues de la ligne en cours pour retrouver l’état
des attributs. Le test make test-savestate-determinism vérifie que la reprise donne les mêmes
arrêts raster, le même VIA et la même RAM qu’une exécution sans interruption.
Plus fin que le cycle
Un seul endroit du projet descend sous le cycle : le bus d’extension de la cartouche LOCI, où se joue une course entre la carte et le front de l’horloge PHI2. Les puces de la carte mère n’en ont pas besoin, puisqu’elles gagnent toujours cette course. Voir Le LOCI.
La boucle d’une trame
Autour de l’horloge, la boucle principale (emulator_run()) déroule chaque trame en étapes nommées,
dans un ordre fixe et observable : les instructions de la trame (avec débogueur, trace et profileur),
puis le LOCI, le son sans affichage, les commandes externes, le chargement rapide, la frappe
automatique, l’affichage et les événements, les captures, l’enregistrement, et enfin la cadence de
50 trames par seconde. Une capture prise à un cycle donné voit donc l’écran après la frappe
automatique de la même trame, jamais avant.