Aller au contenu
Phosphoricwiki
Sommaire

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

Un cycle de la machine, dans l'ordreChaque appel d'emu_cycle() fait vivre un cycle d'une microseconde à toute la machine, dans un ordre figé : d'abord l'unique accès du 6502 au bus, puis les périphériques cadencés par φ2 avancent d'un cycle, enfin l'ULA lit la cellule d'écran de ce même cycle. Une écriture du processeur au cycle c est donc vue par la cellule c ; jusqu'en 2.0.1, l'ordre inverse décalait chaque split d'une cellule vers la droite.Un cycle = 1 µs = un appel d'emu_cycle()début du cyclecycle suivant① 6502l'unique accès busdu cycle : lecture,écriture ou accès factice② périphériques φ2VIA, FDC, ACIA, cassette,cartes : avancésd'exactement un cycle③ ULAlit une cellule de 6 pixels(la colonne du cycle),puis avance le balayageUne écriture du 6502 au cycle c est vue par la cellule c, et pas c + 1.L'ordre vient de mesures faites à l'oscilloscope sur une vraie machine, publiées dans un guide de l'ULA.Jamais d'appel à vide : chaque appel coûte exactement un cycle au processeur.
L’ordre est figé : processeur, puis périphériques φ2, puis ULA.
OrdreQuiQuoi
1le 6502l’unique accès au bus du cycle : lecture, écriture, ou accès factice du NMOS
2les périphériques φ2VIA, contrôleurs de disquette, ACIA, DTL 2000, MIDI, cassette : avancés d’un cycle
3l’ULAlecture 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 :

ChampRôle
raster_cyclele cycle courant dans la trame (0 à 19 967)
raster_renderedles lignes visibles déjà dessinées (0 à 224)
raster_ng_linela ligne déjà traitée par l’ULA-NG (0 à 311)
raster_next_linele 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.

Sources dans le dépôt : docs/architecture/master-clock.md, docs/adr/0003-ordre-des-ticks.md, docs/articles/v2-cycle-stepped.md, README.md.