Inzieremo ora a parlare di un progetto che ho portato avanti in questo periodo; come potete aver intuito dal titolo, si tratta di un emulatore del processore pdp8, naturalmente scritto in python (anche questa volta, il pitone mi è stato di grande aiuto). Il programma simula il calcolatore didattico che sto affrontando nei miei studi, quindi sarà molto semplificato sotto alcuni aspetti (per esempio non simulo l’interfaccia della tastiera e del monitor come hardware, ma solo il loro comportamento). Anche se non è un processore recente, presenta delle meccaniche funzionali che non sono cambiate negli anni e che quindi possono tornare utili per comprendere sia il funzionamento dei calcolatori, sia l’utilizzo di un linguaggio assembly.
Il programma (sotto GPL 3) ha superato tutti i test che ho ritenuto basilari per il suo rilascio ed è quindi reperibile a questo sito:
http://code.google.com/p/py-pdp8-tk/
Con voi voglio discutere riguardo la sua realizzazione, i problemi che ho incontrato e come sono stati risolti. Anche se il codice non è perfetto, l’emulatore è stato pensato per essere eseguito su qualsiasi piattaforma che abbia un interprete python. Per fare questo ho scelto come libreria grafica Tkinter, perfettamente integrata con il pitone e che, anche se non eccelsa in stile, è semplice, veloce e stabile (forse proprio per questo è stata scelta per essere integrata in python, al posto di WxPython). Non ho trovato molte guide su Tcl/Tk nella nostra lingua che trattano tutti gli argomenti che ho dovuto affrontare per la realizzazione di questo programma, quindi spero che questa serie di articoli sarà molto utile anche a quelle persone che si avvicinano a python per la prima volta e vogliono realizzare una piccola applicazione.
Inizierò quindi ad elencare le scelte più importanti che sono state prese per la realizzazione del programma, successivamente entrerò nel dettaglio del codice, illustrando sia il core (l’emulatore vero e proprio) che la parte che riguarda l’interfaccia grafica.
Concetti cardine
- La prima grande scelta che ho dovuto affrontare, che poi si è rivelata quella più importante, è stata come memorizzare il codice da eseguire. Inizialmente avevo improntato una sorta di interpretazione del codice assembly, che si è rivelata un vero disastro. Non che non funzionase, ma la situazione stava diventando troppo complicata e prolissa, sicuramente c’era una via più facile e veloce (è sempre così quando le cose si fanno complicate). Così decisi di effettuare la conversione da assembly a codice macchina (per pdp8 si intende) memorizzando quest’ultimo con delle stringhe (la conversione è quella che farebbe un normale compilatore per quel processore). In particolar modo, questa soluzione porta molti vantaggi, se si pensa che le stringhe possono essere gestite come array e che conversioni da stringhe a numeri decimali, esadecimali o binari e viceversa (gli unici che mi interessano per questo progetto) sono semplici (se non banali) da implementare.
- L’altra importante scelta è stata quella di integrare l’editor del codice assembly all’interno del programma stesso, così da poter modificare il codice seduta stante e caricare subito le modifiche, senza riaprire il file modificato o quant’altro (naturalmente si può anche salvare). Inizialmente la finestra era unica, poi è stata divisa: da una parte l’editor del codice assembly e dall’altra l’emulatore del pdp8. Tra i vari esperimenti che ho effettuato prima di arrivare a questa versione, avevo provato anche la strada di dividere tutte le finestre, una per ogni componente dell’emulatore. Ho abbandonato questa opzione un pò troppo confusionaria, sopratutto a livello di codice, per una più malleabile ed accessibile anche per futuri ritocchi.
- Anche se non è stata una scelta, ma un obbligo, ho dovuto spendere la maggior parte del mio tempo per costruire la funzione che carica il codice in memoria (simulata con un dizionario, poi vedremo nel dettaglio). La parte più difficile consiste infatti nel ripulire il codice assembly dalle cose che non servono, come spazi, ritorni di carrello, virgole, commenti ecc…
- Per ora, non ho limitato il numero di parole che è possibili contenere nella ram (come dizionario, l’unica limitazione è nel numero di bit del MAR) e non faccio controlli di semantica sul codice scritto. Quest’ultima caratteristica si può implementare facilmente ora che il caricamento in memoria avviene in modo corretto (nel senso che l’impostazione è giusta), magari in futuro aggiungerò questa funzionalità.
Il calcolatore
Prima di entrare più in dettaglio nel progetto, illustrerò brevemente il calcolatore:
Il pdp8 preso in considerazione presenta un controllo cablato, costituito dalle variabili di controllo (F ed R) e dal tempo di clock del sistema (delay). L’unità di controllo scandisce il tempo di esecuzione, ma il tutto è legato alla variabile start (S), che avvia o arresta la macchina.
La memoria principale (RAM) è formata da 4096 parole (WORD = 16 bit) e quindi è indirizzabile da un MAR (Memory Address Register) di 12 bit. L’MBR (Memory Buffer Register) è di 16 bit, poi abbiamo un PC (Program Counter) di 12 bit, un registro OPR da 3 bit ed un registro I da 1 bit che contengono rispettivamente l’operation code ed il tipo di indirizzamento (0 = diretto, 1 = indiretto). Infine, abbiamo il registro AC che rappresenta il nostro accumulatore da 16 bit ed il registro E da 1 bit che lo “affianca”.
L’ALU (Arithmetic Logic Unit) comunica con l’MBR e con AC per eseguire le operazioni logiche disponibili. Le istruzioni disponibili si dividono in tre tipologie e sono elencate di seguito:
Memory Reference Istructions:
- AND = And logico tra AC e la cella indirizzata
- ADD = Somma tra AC e la cella indirizzata
- LDA = Carica in AC il contenuto della cella indirizzata
- STA = Salva nella cella indirizzata il contenuto di AC
- BUN = Salto incondizionato alla cella indirizzata
- BSA = Salvataggio del PC nella cella indirizzata e salto alla cella successiva a quella indirizzata
- ISZ = Incrementa di 1 il contenuto della cella indirizzata e se uguale a 0, salta l’istruzione successiva
Register Reference Istructions:
- CLA = Azzera AC
- CLE = Accera E
- CMA = Complementa logicamente il contenuto di AC
- CME = Complementa logicamente il contenuto di E
- CIR = Sposta verso destra i bit in E-AC
- CIL = Sposta verso sinistra i bit in E-AC
- INC = Incrementa di 1 il contenuto di AC
- SPA = Salta l’istruzione successiva se AC > 0
- SZA = Salta l’istruzione successiva se AC = 0
- SNA = Salta l’istruzione successiva se AC < 0
- SZE = Salta l’istruzione successiva se E = 0
- HLT = Arresta il sistema
I/O Istructions:
- INP = Carica in AC il codice ASCII di un carattere in input
- OUT = Output di un carattere il cui codice ASCII è in AC
- ION = Abilita l’Interrupt
- IOF = Disabilita l’Interrupt
Conclusioni
Con questa panoramica abbiamo gettato le basi per commentare il codice che ho scritto. Con i prossimi articoli spiegherò per filo e per segno quello che mi è passato per la testa mentre progettavo questo programma.