Partiamo subito analizzando il cuore dell’applicazione, cioè la parte che simula il calcolatore pdp8, ma prima devo ringraziare Walter Valentini per aver trasformato in un package python il programma, cosa che non avevo il tempo di fare (questa è la cosa più bella dei progetti open source, cioè che più persone possono contribuire per migliorare l’applicazione).
Il file del quale sto parlando è pdp8.py, il cuore pulsante dell’emulatore. La classe pdp8 contenuta in questo file, rappresenta il nostro calcolatore didattico.
Codice
def __init__ (self,codice=None): """ Inizializza i registri e le variabili di controllo S,F ed R """ self.PC = '000000000000' self.I = '0' self.OPR = '000' self.E = '0' self.AC = '0000000000000000' self.MAR = '000000000000' self.MBR = '0000000000000000' self.S = False self.F = False self.R = False self.Interrupt = True self.START = '0' self.RAM = {} self.LABEL = {} self.nstep = 1 self.microistruzioni = '' self.inout = '' self.nextistr = '' self.previstr = '' self.Opcodes = { 'CLA':'0111100000000000', 'CLE':'0111010000000000', 'CMA':'0111001000000000', 'CME':'0111000100000000', 'CIR':'0111000010000000', 'CIL':'0111000001000000', 'INC':'0111000000100000', 'SPA':'0111000000010000', 'SNA':'0111000000001000', 'SZA':'0111000000000100', 'SZE':'0111000000000010', 'HLT':'0111000000000001', 'INP':'1111100000000000', 'OUT':'1111010000000000', 'SKI':'1111001000000000', 'SKO':'1111000100000000', 'ION':'1111000010000000', 'IOF':'1111000001000000', 'AND':'000', 'ADD':'001', 'LDA':'010', 'STA':'011', 'BUN':'100', 'BSA':'101', 'ISZ':'110' } if codice is not None: self.carica(codice.lstrip())
Analisi
- Osserviamo per prima cosa come viene inizializzata la macchina. Come ho già spiegato nel precedente articolo, tratto le informazioni contenute all’interno del processore come delle stringhe ed è per questo che tutti i registri sono inizializzati a 0 con una stringa di zeri lunga quanti sono i bit del registro.
- Le variabili di controllo S, F ed R, compresa quella di interruzione (Interrupt) sono trattate come variabili booleane.
- La variabile START serve per stabilire gli indirizzi delle istruzioni all’interno della ram. Di default, il programma viene caricato partendo dalla prima cella (0), altrimenti si inizia dal valore indicato dalla pseudo istruzione ORG, che specifica da quale cella di memoria si deve iniziare a caricare il programma. Il valore di ORG è espresso con un numero esadecimale e non decimale, quindi ORG 10 vuol dire dalla cella 10 HEX, in decimale corrisponderà alla cella 16.
- Il dizionario RAM conterrà le istruzioni caricate (in codice macchina) con i rispettivi indirizzi, mentre LABEL contiene le etichette utilizzate nel programma e la cella di memoria a cui si riferiscono (è necessario durante la conversione per tenere traccia delle celle di memoria utilizzate, sostituendole con il loro indirizzo in binario).
- nstep, è un contatore per tenere conto dei passi eseguiti, così se si vuole eseguire solo un certo numero di cicli, lo si può indicare con questa variabile.
- inout rappresenta la stringa di output di sistema, in poche parole il nostro schermo virtuale, che poi verrà stampato tramite l’interfaccia grafica.
- nextistr e previstr servono per tenere traccià dell’istruzione che verrà eseguita successivamente e di quella corrente. Queste due variabili sono necessarie solo per la parte grafica; sono state aggiunte in un secondo momento quando il core era già completo.
- Opcodes è un dizionario che contiene tutti i codici macchina delle istruzioni disponibili.
- Se alla classe viene passato subito il codice da memorizzare, si richiama il metodo carica che effetuerà il parsing della stringa passata (eliminando gli spazi bianchi presenti all’inizio tramite il metodo lstrip() delle stringhe).
Conclusioni
Come potete vedere, la macchina è abbastanza banale. Successivamente analizzerò tutti i vari metodi presenti, a partire da quelli statici che sono di grande importanza per il parsing del codice. Infatti avrete notato che già si cerca di eliminare eventuali impurità quando il codice viene passato utilizzando lstrip(); il prossimo passo sarà quello di dividere il codice per righe e di ripurirlo fino all’osso per non avere equivoci ed è quello che vedremo nella prossima parte.
Manca ancora il resto del codice da vedere, ma al momento mi ha sorpreso la scelta di utilizzare le stringhe per codificare le informazioni dei registri. Come mai non hai usato gli interi?
@ Cesare
Semplicemente perché ho scelto di memorizzare tutti i dati in base due, quindi con delle stringhe di numeri binari.
Volevo rimanere il più attaccato possibile a quello che effettivamente c’è dentro la macchina e credo di esserci riuscito :D .
In effetti in questo modo ti complichi un bel po’ le cose, perchè dovrai implementarti tutte le operazioni della ALU bit per bit… IMHO avrebbe più senso in un “simulatore” che tenga conto del circuito elettronico da simulare, con tanto di diagrammi di transizione dettagliati, ecc… Ma in un emulatore normale di solito si tende a semplificare di più la rappresentazione della macchina emulata, curando più il funzionamento esterno che l’esatta architettura interna.
Per intenderci, il classico PC += 1 non lo potrai fare e dovrai chiamare una funzione che faccia la somma tra due stringhe rappresentanti cifre binarie.
@ Antonio
Infatti è proprio quello che faccio:
temp = bin2dec(PC)
temp += 1
PC = extendpositive(binario(temp),12)
extendpositive estende la cifra binaria positiva al numero di bit indicato aggiungendo degli 0 in testa. Binario è una funziona che ritorna il numero convertito in binario senza 0b e/o il segno in testa (in complemento a 2 su 16 bit quindi).
In compenso, per prelevare l’opcode mi basta questo:
OPR = MBR[1:4]
mentre se erano interi sarebbe stato
OPR = (MBR/4096)%8
Per me le cose sono state più semplici in questo modo, forse perché non mi è mai passato per la testa di lavorare con gli interi, visto che avevo avanti ai miei occhi solo fogli con codici binari :D.
Ora che mi avete messo la pulce nell’orecchio, mi piacerebbe riscrivere il codice utilizzando gli interi, per vedere se veramente le cose si facevano più semplici.
La cosa che posso riscontrare è che, se avessi avuto una spalla con cui scrivere il codice, forse questi problemi sarebbero venuti alla luce e avrei optato per altre soluzioni (ecco perché in un progetto è indispensabile la collaborazione). Per fortuna che il funzionamento esterno è corretto, altrimenti avrei dovuto ricominciare tutto da zero :P.
fossero stati interi per prelevare l’opcode avresti dovuto fare soltanto uno shift binario e un AND con una maschera. Non conosco il python ma ti faccio l’equivalente in C:
int OPR = (MBR & 0x0F00) >> 8;
Tutto ciò occupa veramente poche istruzioni macchina. Ma ovviamente io sto ragionando in termini di ottimizzazione del codice (che presuppone quindi l’uso di un linguaggio compilato in codice macchina). Dal punto di vista funzionale/algoritmico è esattamente la stessa cosa.
@ Antonio
La tua soluzione non è tanto diversa in python:
data la maschera mask = 28672 (“0111 0000 0000 0000” in binario)
OPR = (MBR & mask) >> 12
Mask, OPR e MBR naturalmente sono di tipo intero. & opera sui bit, quindi non capisco come non mi sia venuto in mente. Come hai già detto, dal punto di vista funzionale/algoritmico non cambia nulla, quindi se ho un pò di tempo e pazienza, credo vivamente che riscriverò il core :D (naturalmente solo per fini personali, visto che il programma funziona, sarà solo un esercizio di “stile”, se così possiamo definirlo e quindi continuerò comunque a scrivere articoli su questa versione con le stringhe, così da poter confrontare le mie idee con le vostre).
Una nota positiva è che la parte grafica per ora ne è tagliata fuori, nel senso che anche se utilizzassi gli interi, basterebbe convertire il tutto in stringhe quando è necessario.
Non avevo contato correttamente il numero di bit, correggendo sarebbe int OPR = (MBR & 0x7000) >> 12;
comunque si, il concetto è quello..
PS: ti si è “scricchiata” la formattazione del post!
@ Antonio
Credo che non ci sia il numero di linea e basta, anche perché ha poco senso effettivamente :D
no adesso si vede bene, prima erano comparsi dei tag “span” a manetta…
Visto che alla fine converti le stringhe “binarie” in interi per eseguire le operazioni, per poi riconvertirle in stringhe, è sufficiente eseguire quest’ultima operazione soltanto nel momento in cui si deve visualizzare il risultato (con la GUI, immagino).
Per il resto, come ti ha suggerito anche Antonio, gli interi sono la soluzione sicuramente più efficiente.
Però considerato che lo scopo è didattico, da Pythonista, mi è venuto in mente in questo momento una soluzione diversa:
class Register(object):
Ti lascio immaginare cosa ci si potrebbe fare e… con che comodità. O:-)
Ciao, progetto interessante davvero. Adesso vado a spulciarmi il sorgente intero. Ho provato a clonare il mercurial ma è vuoto, problema mio o è una scelta? comunque mi sono scaricato lo zip, e mi riservo di tornare a rompere le balle più tardi :D
Ne approfitto per complimentarmi per il blog, ho notato diverse volte i post linkati su hwup, ma non avevo colto la vostra passione per python, che condivido. Apprezzo inoltre la qualità dei post e dei commenti. Per il poco che conta, avete un follower in più.
Simo
@ Antonio
class Register(object):
Questa soluzione neanche la immaginavo! Purtroppo sono solo un dilettante in python e queste finezze ancora non le penso… uffi!!!
@ simock85
Felice di averti tra noi! Non ho utilizzato mercurial per il progetto, credo che più avanti aggiungerò i sorgenti, una volta apportate le ultime modifiche ed aver reso l’emulatore un programma “più che decente” (Anche se funzionante, sto aggiungendo altre feature su richista, ma non mi preoccupo molto dell’ottimizzazione, visto che non è una macchina particolarmente complessa. Se ne avrò tempo, lo farò in un secondo momento).
Nell’informatica ritengo siano molto importanti le idee. Fino a quando non avevi scritto questo:
“In compenso, per prelevare l’opcode mi basta questo:
OPR = MBR[1:4]”
l’unica cosa che m’era venuta in mente di dirti riguardava l’uso degli interi.
Dopo questa tua frase, invece, mi si è accesa la lampadina, come si suol dire, e m’è venuto in mente di utilizzare una classe nella quale incapsulare il funzionamento di un registro che si facesse carico di tutte le operazioni in maniera semplice e intuitiva, mettendo a disposizione anche lo “slicing” del registro per recuperare l’opcode come fai tu con le stringhe, appunto.
Senza questa tua idea di base, mirata alla semplicità (caratteristica che apprezzo moltissimo, di gran lunga più dell’efficienza), non ci sarebbe mai stata la mia che è, però, frutto della pratica e dell’esperienza (ok, forse serve anche un po’ di fantasia; o perversione mentale :D). ;)
@ Cesare
Infatti il confronto con persone che hanno un diverso tipo di esperienza porta solo benefici al prodotto finale. Con questi articoli sicuramente stiamo anche affrontando delle tematiche di ingegneria del software e di “problem solving”, quindi mi fa molto piacere ricevere tutti questi suggerimenti. Anche se non potrò subito mettere in pratica la cosa, sarebbe un buon passatempo per quest’estate :D.