CPUSim: il simulatore di CPU che ho costruito per Zanichelli
Oggi mi è tornato in mente CPUSim, il simulatore di CPU nel browser creato per Zanichelli nel 2020 per mostrare agli studenti come gira il codice macchina.
Oggi mi è tornato in mente un mio vecchio progetto, CPUSim, e non ho resistito alla tentazione di riaprirlo. Nell’estate del 2020 l’ho costruito per Zanichelli, la casa editrice scolastica: un piccolo simulatore che mostra agli studenti come una CPU esegue il codice macchina. Scrivi qualche riga in un linguaggio assembly giocattolo, premi play e guardi l’istruzione viaggiare dalla memoria ai registri, mentre ogni pezzo dello schema si accende di rosso. Lavorarci è stato divertentissimo, e rileggere il codice sei anni dopo mi ha fatto sorridere più di una volta.
In breve
- CPUSim è uno strumento didattico, descritto nel repo come “a CPU simulator used in schools to teach machine code”.
- Anima il ciclo fetch–decode–execute su uno schema con registri, ALU, bus e 1.000 celle di RAM.
- Il set di istruzioni ha una ventina di mnemonici, tre modalità di indirizzamento e uno stack.
- L’ho costruito tra maggio e luglio 2020 con React, TypeScript, Redux Toolkit, l’editor Monaco e una grammatica Ohm, e l’ho impacchettato per Windows con Electron.
Cosa mostra il simulatore
Su un lato dello schermo c’è un editor per il programma, più due editor più piccoli per i dati e per lo stack. Sull’altro c’è un disegno SVG della CPU: i registri operandi R0 e R1, l’accumulatore A, il registro indice IX, lo stack pointer SP, il program counter PC, il registro istruzioni IR, un decoder, una ALU, i registri MAR e MDR, e i bus indirizzi e dati che collegano tutto alla RAM.
La memoria è un unico spazio di indirizzi diviso in tre sezioni: le celle da 0 a 99 contengono il codice, quelle da 100 a 499 i dati e quelle da 500 a 999 lo stack. Ogni registro è cliccabile, così lo studente può impostarne il valore a mano prima di far partire il programma. C’è uno slider per la velocità, pausa e ripresa, un pulsante per eseguire un’istruzione alla volta, e puoi salvare un programma su file e riaprirlo in seguito.
Le specifiche da cui sono partito sono un PDF, ancora presente nel repo. Descrivono l’architettura, elencano tutte le istruzioni e per ognuna dicono esattamente quali componenti si devono accendere, e in che ordine. Un dettaglio che adoro: una nota a piè di pagina precisa che MAR e MDR sono “presenti solo nella grafica”, perché la RAM in realtà è solo un array.
Il set di istruzioni
Il linguaggio è abbastanza piccolo da imparare in una lezione:
SET R0 #5carica una costante in un registro.ADD,SUB,MULeDIVcalcolano sempreR0 op R1e mettono il risultato inA.MOV R0copiaAin un registro, eINC IX/DEC IXfanno avanzare o arretrare il registro indice.LOD R0 120eSTO 120leggono e scrivono in memoria.LOD R0 @3somma IX all’indirizzo, e$rende l’indirizzo relativo a SP.JMP,JMZ,JMLeJMGsaltano sempre, oppure solo quandoAè zero, negativo o positivo.PSH,POP,CALeRETlavorano con lo stack, eHLTferma la macchina.
La sintassi sta in una grammatica scritta con Ohm, che analizza ogni riga mentre scrivi e segnala gli errori nell’editor. Si legge quasi come le specifiche:
Set = "SET" SetRegister "#"Integer
Lod = "LOD" LodBody
Fetch, decode, execute
Il programma predefinito nell’editor è lungo cinque righe:
SET R0 #1
SET R1 #2
ADD
STO 100
HLT
Prendi l’ADD, che si trova all’indirizzo 2 (l’editor numera le righe da 0, proprio come la memoria). Nella fase di fetch PC vale 2. Si accendono PC, il bus indirizzi e MAR, poi la cella di memoria 2, poi MDR, il bus dati e IR. PC si accende ancora una volta mentre viene incrementato a 3, pronto per il giro successivo. Nella fase di decode si accendono IR e il decoder. Nella fase di execute si accendono R0 e R1, poi la ALU, poi A, che adesso contiene 3. L’istruzione successiva, STO 100, manda quel 3 sul bus dati fino alla cella 100, la prima della sezione dati, e HLT chiude il programma.
Il codice che fa tutto questo è piacevolmente letterale. Ogni istruzione è un thunk Redux asincrono che accende e spegne le luci con una pausa in mezzo. Questa è la fase di fetch, leggermente accorciata:
const instruction = cpu.codeMemory[cpu.pc]
const animationInterval = computeAnimationInterval(cpu.executionSpeed)
dispatch(setLightsFetchStart(true)) // PC, address bus, MAR
await sleep(animationInterval)
dispatch(setLightsFetchStart(false))
dispatch(lightRamAddress({ address: cpu.pc, light: true }))
await sleep(animationInterval)
dispatch(setLightsFetchEnd(true)) // MDR, data bus, IR
await sleep(animationInterval)
dispatch(setLightsFetchEnd(false))
dispatch(lightPc(true))
dispatch(incrementPc())
Se guardi bene, il simulatore bara un po’. L’istruzione viene letta dall’array già alla prima riga, prima che si accenda una sola luce. Tutto il resto è teatro per gli studenti. Per uno strumento didattico è il compromesso giusto: i cambi di stato sono semplici reducer, e lo sforzo va tutto nel rendere ogni passo visibile e abbastanza lento da poterlo seguire.
Com’è fatto
La storia completa è in git: 114 commit tra il 22 maggio e il 28 luglio 2020, fino alla versione 1.0.0. È un progetto Create React App in TypeScript, con Redux Toolkit per lo stato della CPU, Tailwind per il layout e l’editor Monaco (quello dentro VS Code) con evidenziazione della sintassi e autocompletamento per le istruzioni. Lo schema della CPU è un grande SVG inline i cui colori e valori arrivano dritti dallo stato Redux, e buona parte della storia è fatta di commit come “Align address bus label” e “Draw lines between ALU and registers”. Negli ultimi commit ho aggiunto Electron e la build di un installer per Windows, così da poterlo usare anche come app desktop.
Perché ci ripenso ancora
Oggi passo le mie giornate su sistemi real-time e agenti AI, molto lontano da R0 e R1. Eppure dover rendere visibile ogni passo del ciclo fetch–decode–execute è stato un ottimo esercizio, e l’abitudine di chiedermi cosa succede davvero sotto è rimasta utile. Quando una query Postgres è lenta, spesso la risposta sta in come le righe vengono lette dal disco. Quando un modello è lento da servire, spesso sta in come si muove la memoria sulla GPU. Le astrazioni sono fantastiche, fino al momento in cui hai bisogno di guardarci attraverso.
Se ti incuriosisce quello su cui ho lavorato da allora, trovi di più su di me qui. E se insegni informatica e vuoi giocare con CPUSim, il codice è su GitHub.