Struttura del calcolatore
Il processore sEP8 descritto in Verilog, memoria e spazio di I/O, interfacce parallele e seriali, conversione A/D e D/A
Questo modulo mette insieme tutto: un calcolatore completo, con processore, memoria e interfacce, descritto in Verilog con gli strumenti dei moduli precedenti. Il punto concettuale è uno: il processore è una rete sequenziale sincronizzata come le altre, con registri operativi, un registro di stato e un μ-programma. È una rete logica capace di eseguire software. Il processore d'esempio è il sEP8 (8-bit simple Educational Processor): elabora byte, rappresenta gli interi in complemento a due, indirizza 16 Mbyte di memoria.
Il calcolatore visto dal programmatore
I blocchi
- Il processore preleva ciclicamente un'istruzione dalla memoria (fetch) e la esegue, finché non trova HLT. Le istruzioni operative si susseguono in memoria; quelle di controllo (salti, CALL, RET) cambiano il flusso.
- La memoria principale contiene istruzioni e dati. Per partire in modo consistente, al reset il processore inizializza IP a un indirizzo preciso, e lì ci deve essere già del codice: quella parte di memoria è una EPROM con il programma di bootstrap.
- Il sottosistema di I/O scambia informazioni con il mondo. Distinguiamo i dispositivi, che codificano davvero l'informazione, e le interfacce, che li presentano al processore in modo standard, attraverso pochi registri.
- Il bus collega tutto: indirizzi, dati, comandi, clock.
Registri e spazi di indirizzamento del sEP8
La memoria è uno spazio lineare di 224 byte; lo spazio di I/O ha 216 = 64K porte, ma solo poche corrispondono a registri reali di interfaccia (qualche decina di interfacce, ognuna con pochi registri). I registri del processore sono di tre tipi: accumulatori AL e AH (8 bit), il registro dei flag F (significativi CF, ZF, SF, OF nei bit 0–3) e tre puntatori a 24 bit: IP, SP e DP, che contiene l'indirizzo degli operandi con indirizzamento indiretto.
Le istruzioni
L'Assembler del sEP8 assomiglia a quello x86 visto nel corso: OPCODE sorgente, destinatario. Le modalità di indirizzamento delle istruzioni operative sono:
| modalità | esempio | dove sta l'operando |
|---|---|---|
| di registro | MOV AL, AH, INC DP | nei registri |
| immediato | MOV $0x10, AL | nell'istruzione stessa |
| di memoria diretto | ADD 0x1010, AL | in memoria, all'indirizzo scritto nell'istruzione |
| di memoria indiretto | MOV (DP), AL | in memoria, all'indirizzo contenuto in DP |
| di I/O | IN 0x1010, AL, OUT AL, 0x9F10 | in una porta, con offset scritto nell'istruzione |
Un operando in memoria può essere sorgente o destinatario, mai tutti e due. Le istruzioni di controllo sono JMP, Jcon e CALL con un indirizzo, e RET; CALL salva in pila IP (3 byte, l'indirizzo dell'istruzione successiva), RET lo ripristina.
Il linguaggio macchina: i formati
Per un umano conta il tipo di operazione: MOV AH,AL, MOV $0x10,AL e MOV (DP),AL sono tutte copie in AL. Per il processore conta dove sono gli operandi: nel primo caso li ha già, nel secondo deve leggere un altro byte dell'istruzione, nel terzo deve leggere la memoria all'indirizzo in DP. L'esecuzione è identica (metti qualcosa in AL); la fase di fetch, in cui ci si procura gli operandi, no. Per questo le istruzioni si raggruppano per formato:
| formato | byte | OPCODE | SOURCE | DEST_ADDR |
|---|---|---|---|---|
| F0 | 1 | readB @ IP | – | – |
| F1 | ? | readB @ IP | – | – |
| F2 | 1 | readB @ IP | readB @ DP | – |
| F3 | 1 | readB @ IP | – | DP |
| F4 | 2 | readB @ IP | readB @ IP | – |
| F5 | 4 | readB @ IP | readM @ IP, poi readB | – |
| F6 | 4 | readB @ IP | – | readM @ IP |
| F7 | 4 | readB @ IP | – | readM @ IP |
Alcune codifiche: HLT = 00000000, NOP = 00000001, MOV $op,AL = 10000000 seguito dall'operando, MOV (DP),AL = 01000000, ADD indirizzo,AL = 10100010 seguito da tre byte di indirizzo, JMP indirizzo = 11100000, CALL indirizzo = 11110011. Il formato F1 raccoglie i casi che non stanno negli altri (IN, OUT, MOV con DP o SP): il fetch legge solo l'opcode e gli operandi li preleva l'esecuzione, istruzione per istruzione.
Bus, memoria e spazio di I/O
I fili del bus
- 24 fili di indirizzo, uscite del processore. Per lo spazio di I/O se ne usano solo 16.
- 8 fili di dati, pilotati a turno dal processore e dagli altri: tutti li collegano con porte tri-state («forchetta»), e la descrizione deve garantire che non vadano mai in conduzione due sorgenti insieme.
- Fili di controllo attivi bassi:
/mr,/mwper la memoria,/ior,/iowper l'I/O. - Il clock, e il /reset che arriva a tutti nello stesso istante.
La descrizione in Verilog del calcolatore è solo un insieme di collegamenti:
Lo spazio di memoria e le maschere
I tre moduli (RAM, memoria video, EPROM) si montano «in serie»: ognuno risponde a un intervallo di indirizzi, e la sua maschera genera il select dagli otto bit più alti. Con le variabili in algebra di Boole:
- /sEPROM = NAND(a23, a22, …, a16): attivo quando sono tutti a 1;
- /sVIDEO = NAND(a23, a22, a21, a20, a19, a18, a17, a16): attivo con 00001010;
- /sRAM = NAND(/sEPROM, /sVIDEO): la RAM risponde quando nessuno degli altri due è selezionato.
Lo spazio di I/O
Dal lato del bus un'interfaccia è una piccola RAM di poche locazioni, le porte: select, /ior, /iow, qualche filo di indirizzo per distinguere le porte, i dati. Le differenze: alcune porte si possono solo leggere o solo scrivere (e allora manca uno dei due comandi), e un'interfaccia con una sola porta non ha fili di indirizzo.
| /s | /ior | /iow | azione |
|---|---|---|---|
| 1 | – | – | nessuna |
| 0 | 1 | 1 | nessuna |
| 0 | 0 | 1 | ciclo di lettura |
| 0 | 1 | 0 | ciclo di scrittura |
| 0 | 0 | 0 | non definita |
Esempio: un'interfaccia con due porte agli offset 'H03C8 e 'H03C9 riceve il filo a0, e la sua maschera controlla che a15 … a1 valgano 0000 0011 1100 100. Un'altra con le porte a 'H0060 e 'H0064 (non contigue) riceve a2, l'unico bit in cui differiscono, e la maschera controlla tutti gli altri. Domanda d'esame classica: chi decide l'offset di una porta? Chi progetta la maschera.
Il processore come rete sequenziale
I registri interni e il reset
Oltre ai registri visibili al programmatore, il processore contiene:
Il registro di stato e il Multiway Jump Register, per i salti a molte vie e le sottoliste di lettura e scrittura. Con 86 stati bastano 7 bit.
Riempiti dal fetch: il codice operativo, l'operando sorgente se sta in memoria, l'indirizzo del destinatario se sta in memoria.
A23_A0, D7_D0, MR_, MW_, IOR_, IOW_, e DIR che abilita la tri-state verso i dati.
Appoggi per le letture e scritture di più byte.
Al reset: IP all'inizio della EPROM, F a zero, tutti i comandi del bus inattivi, DIR a 0 (dati in alta impedenza), STAR al primo stato del fetch. Gli altri registri possono contenere valori casuali: verranno scritti prima di essere usati. Il comportamento è un ciclo fetch → esecuzione, da cui si esce solo con HLT o con un codice operativo non valido: in entrambi i casi il processore resta fermo fino al prossimo reset.
Accanto al μ-programma ci sono alcune reti combinatorie, descritte con function: valid_fetch (il byte è un opcode noto?), first_execution_state (il primo stato dell'esecuzione di quell'opcode), jmp_condition (si salta, dati opcode e flag?), e la ALU con alu_result e alu_flag.
I cicli di bus
In lettura DIR non si porta a 1 subito dopo: le tri-state della RAM vanno in alta impedenza un po' dopo la risalita di /mr, e si rischierebbe un conflitto elettrico. In scrittura DIR non si abbassa in mem_w2, né si cambia l'indirizzo: la RAM memorizza sul fronte di salita di /mw, e lì dati e indirizzi devono essere stabili. D'ora in poi si assume una memoria abbastanza veloce da non richiedere stati di wait.
Leggere e scrivere più byte: μ-sottoprogrammi
Il processore legge 1 byte (opcode, operandi), 2 (offset di I/O) o 3 (indirizzi). Invece di riscrivere ogni volta i cicli di bus, si usano sottoliste con MJR come indirizzo di ritorno: NUMLOC conta i byte, APP0…APP3 li contengono in ordine di lettura (il byte meno significativo per primo).
La scrittura è duale (writeB … writeL, con D7_D0 caricato da APP0…APP3 e un impulso di /mw per byte). Per scrivere un dato a 16 bit: APP1 <= dato[15:8]; APP0 <= dato[7:0]; A23_A0 <= indirizzo; MJR <= Sx_piu_1; STAR <= writeW;.
La fase di fetch
All'uscita dal fetch: OPCODE contiene il codice operativo; se c'è un sorgente in memoria, il suo valore è in SOURCE, comunque fosse indirizzato (indiretto, immediato, diretto); se c'è un destinatario in memoria, il suo indirizzo è in DEST_ADDR; IP punta già all'istruzione successiva. Per questo l'esecuzione di MOV (DP),AL, MOV $op,AL e MOV indirizzo,AL è un unico stato.
Ecco, clock per clock, il μ-programma che esegue ADD 0x000100, AL (codificata 'HA2 'H00 'H01 'H00 a partire da 'HFF0002), dopo un MOV $0x05, AL, con la cella 'H000100 che contiene 'H2A. La traccia è prodotta simulando proprio gli stati scritti sopra; in giallo i registri appena cambiati.
| clock | STAR | A23_A0 | /MR | d7_d0 | IP | MJR | OPCODE | SOURCE | AL | μ-istruzioni eseguite al fronte successivo |
|---|---|---|---|---|---|---|---|---|---|---|
| prelievo del codice operativo | ||||||||||
| 15 | fetch0 | 'HFF0002 | 1 | Z | 'HFF0002 | ldAL | 'H80 | 'H05 | 'H05 | A23_A0<=IP; IP<=IP+1; MJR<=fetch1; STAR<=readB |
| 16 | readB | 'HFF0002 | 1 | Z | 'HFF0003 | fetch1 | 'H80 | 'H05 | 'H05 | MR_<=0; NUMLOC<=1 |
| 17 | read0 | 'HFF0002 | 0 | 'HA2 | 'HFF0003 | fetch1 | 'H80 | 'H05 | 'H05 | APP0<=d7_d0; A23_A0<=A23_A0+1; NUMLOC<=NUMLOC-1 |
| 18 | read4 | 'HFF0003 | 0 | 'H00 | 'HFF0003 | fetch1 | 'H80 | 'H05 | 'H05 | MR_<=1; STAR<=MJR |
| 19 | fetch1 | 'HFF0003 | 1 | Z | 'HFF0003 | fetch1 | 'H80 | 'H05 | 'H05 | OPCODE<=APP0 |
| 20 | fetch2 | 'HFF0003 | 1 | Z | 'HFF0003 | fetch1 | 'HA2 | 'H05 | 'H05 | MJR<=(formato F5); STAR<=fetch3 (codice valido) |
| 21 | fetch3 | 'HFF0003 | 1 | Z | 'HFF0003 | fetchF5_0 | 'HA2 | 'H05 | 'H05 | STAR<=MJR |
| formato F5: indirizzo (3 byte) e operando | ||||||||||
| 22 | fetchF5_0 | 'HFF0003 | 1 | Z | 'HFF0003 | fetchF5_0 | 'HA2 | 'H05 | 'H05 | A23_A0<=IP; IP<=IP+3; MJR<=fetchF5_1; STAR<=readM |
| 23 | readM | 'HFF0003 | 1 | Z | 'HFF0006 | fetchF5_1 | 'HA2 | 'H05 | 'H05 | MR_<=0; NUMLOC<=3 |
| 24 | read0 | 'HFF0003 | 0 | 'H00 | 'HFF0006 | fetchF5_1 | 'HA2 | 'H05 | 'H05 | APP0<=d7_d0; A23_A0<=A23_A0+1; NUMLOC<=NUMLOC-1 |
| 25 | read1 | 'HFF0004 | 0 | 'H01 | 'HFF0006 | fetchF5_1 | 'HA2 | 'H05 | 'H05 | APP1<=d7_d0; A23_A0<=A23_A0+1; NUMLOC<=NUMLOC-1 |
| 26 | read2 | 'HFF0005 | 0 | 'H00 | 'HFF0006 | fetchF5_1 | 'HA2 | 'H05 | 'H05 | APP2<=d7_d0; A23_A0<=A23_A0+1; NUMLOC<=NUMLOC-1 |
| 27 | read4 | 'HFF0006 | 0 | 'H00 | 'HFF0006 | fetchF5_1 | 'HA2 | 'H05 | 'H05 | MR_<=1; STAR<=MJR |
| 28 | fetchF5_1 | 'HFF0006 | 1 | Z | 'HFF0006 | fetchF5_1 | 'HA2 | 'H05 | 'H05 | A23_A0<={APP2,APP1,APP0}; MJR<=fetchF5_2; STAR<=readB |
| 29 | readB | 'H000100 | 1 | Z | 'HFF0006 | fetchF5_2 | 'HA2 | 'H05 | 'H05 | MR_<=0; NUMLOC<=1 |
| 30 | read0 | 'H000100 | 0 | 'H2A | 'HFF0006 | fetchF5_2 | 'HA2 | 'H05 | 'H05 | APP0<=d7_d0; A23_A0<=A23_A0+1; NUMLOC<=NUMLOC-1 |
| 31 | read4 | 'H000101 | 0 | – | 'HFF0006 | fetchF5_2 | 'HA2 | 'H05 | 'H05 | MR_<=1; STAR<=MJR |
| 32 | fetchF5_2 | 'H000101 | 1 | Z | 'HFF0006 | fetchF5_2 | 'HA2 | 'H05 | 'H05 | SOURCE<=APP0 |
| fine del fetch: salto all'esecuzione | ||||||||||
| 33 | fetchEnd | 'H000101 | 1 | Z | 'HFF0006 | fetchF5_2 | 'HA2 | 'H2A | 'H05 | MJR<=first_execution_state(OPCODE) |
| 34 | fetchEnd1 | 'H000101 | 1 | Z | 'HFF0006 | aluAL | 'HA2 | 'H2A | 'H05 | STAR<=MJR |
| esecuzione | ||||||||||
| 35 | aluAL | 'H000101 | 1 | Z | 'HFF0006 | aluAL | 'HA2 | 'H2A | 'H05 | AL<=alu_result(OPCODE,SOURCE,AL); F<=…; STAR<=fetch0 |
| 36 | fetch0 | 'H000101 | 1 | Z | 'HFF0006 | aluAL | 'HA2 | 'H2A | 'H2F | A23_A0<=IP; IP<=IP+1; MJR<=fetch1; STAR<=readB |
Ventuno clock per un'istruzione: quasi tutti spesi a parlare con la memoria. Alla fine AL vale 'H2F = 5 + 42.
La fase di esecuzione
L'esecuzione è semplice: parte della complessità l'ha già assorbita il fetch, e le istruzioni aritmetiche si riducono a un assegnamento che usa la ALU. Ogni esecuzione termina con STAR <= fetch0, tranne HLT.
CALL è una push di IP (già incrementato dal fetch, quindi è l'indirizzo di ritorno) più un salto; RET è una pop di 3 byte in IP. Si noti che in tutto il processore i μ-salti sono al più a due vie, tranne i due salti guidati da MJR: chi chiedesse di sintetizzare la parte operativa di IP o la rete di condizionamento troverebbe solo cose già viste. Dopo due mesi di corso, si sa progettare hardware che esegue programmi arbitrariamente complessi.
Le interfacce
Registri di dati e registri di stato
Un'interfaccia con un registro di ingresso RBR e uno di uscita TBR si usa con IN offset_RBR, AL e OUT AL, offset_TBR. Ma così il processore non può sapere se tra due IN il dispositivo ha prodotto un dato nuovo, né se tra due OUT ha consumato il precedente. Servono registri di stato con un flag: FI (buffer di ingresso pieno) e FO (buffer di uscita vuoto), gestiti dall'interfaccia.
- FI vale 0 al reset; va a 1 quando il dispositivo scrive un dato in RBR; torna a 0 quando il processore legge RBR.
- FO vale 1 al reset; va a 0 quando il processore scrive in TBR; torna a 1 quando il dispositivo ha prelevato il dato.
Leggere un dato nuovo
Scrivere quando si può
È l'accesso a controllo di programma: il processore cicla in attesa attiva, ed è inefficiente. Le alternative (interruzioni, DMA) si vedono nel corso di Calcolatori Elettronici.
Interfacce parallele
L'interfaccia di ingresso più semplice ha una sola porta di sola lettura: select, /ior, 8 fili di dati, nessun indirizzo. Quando /s e /ior sono entrambi a 0 (inizio di un ciclo di lettura) RBR campiona i fili dal dispositivo e le tri-state vanno in conduzione. Ecco perché in I/O il select non deve «ballare» con /ior a 0: un'altra interfaccia memorizzerebbe un dato nel suo RBR. Quella di uscita è duale e memorizza sul fronte di discesa di /iow.
Ingresso
Uscita
Con l'handshake verso il dispositivo, dentro l'interfaccia c'è una piccola RSS che fa da consumatore (in ingresso) o da produttore (in uscita) e gestisce il flag. Il suo clock va ritardato di qualche nanosecondo rispetto a quello del processore, per non campionare eB proprio mentre cambia.
L'interfaccia seriale start/stop
Un'interfaccia seriale riceve byte dal processore e trasmette sequenze di bit su un filo (più la massa), e viceversa. Il problema è sincronizzare trasmettitore e ricevitore senza un clock comune e senza fili di handshake. La soluzione dello standard EIA-RS232C:
- i due lati concordano prima il tempo di bit T (il suo inverso è il bitrate) e il numero di bit di una trama (da 5 a 8);
- a riposo la linea è a 1 (marking); una trama inizia con un bit di start a 0 e finisce con almeno un bit di stop a 1. Con trame di un byte, al massimo 8 bit utili su 10: la velocità netta è l'80% di quella della linea;
- il ricevitore campiona ogni bit a metà: 3/2 T dopo il fronte dello start, poi ogni T. I clock dei due lati non sono identici: con un errore ΔT, dopo n bit l'errore accumulato non deve superare mezzo bit, quindi ΔT/T ≤ 1/(2n): il 5% con n = 10. Per questo le trame non possono essere lunghe.
Le tensioni dello standard non sono quelle delle reti logiche: l'1 è tra −3 e −25 V, lo 0 tra +3 e +25 V. Dentro l'interfaccia c'è una sotto-interfaccia parallela con handshake, collegata a due RSS: il trasmettitore e il ricevitore. Il trasmettitore ha un clock pari al tempo di bit: preleva un byte con l'handshake, prepara in BUFFER la trama intera (start, dati, stop) e ne trasmette un bit per clock, traslando a destra. COUNT parte da 10 e il test è a 1: dieci giri.
Il ricevitore ha differenze importanti. Non può fermare il trasmettitore dall'altra parte del filo, quindi verso la sotto-interfaccia non gestisce un handshake completo: abbassa /dav quando il byte è pronto e lo rialza dopo il bit di stop, che il dato sia stato letto o no (altrimenti lo sovrascriverà la trama successiva: overrun). E deve campionare a metà bit, quindi il suo clock è più veloce: nella dispensa 16 volte il bitrate, per stimare bene l'istante dello start. Con WAIT inizializzato a k e testato a 1 si fanno k giri, più il clock dello stato che lo inizializza: 23 per i 24 clock di 3/2 T, 15 per i 16 clock di T.
Per poterlo seguire in un diagramma leggibile, qui sotto lo stesso ricevitore ha un clock di soli 4 colpi per bit (WAIT a 5 e 3 invece che 23 e 15). La linea porta la trama di 'H2D; puoi cambiarla cliccando su rxd.
Conversione digitale/analogica e analogica/digitale
Una tensione v su una scala di FSR volt (Full-Scale Range) si associa a un numero x su N bit, con costante K = FSR / 2N: idealmente v = K · x.
- unipolare: v ∈ [0, FSR], x ∈ [0, 2N − 1]; bipolare: v ∈ [−FSR/2, +FSR/2], x ∈ [−2N−1, 2N−1 − 1], rappresentato in traslazione (binario bipolare): X = x + 2N−1. Per passare dalla traslazione al complemento a 2 basta complementare il bit più significativo;
- errori: di non linearità (componenti non ideali, sia D/A sia A/D), che deve restare sotto K/2, e di quantizzazione (solo A/D), al più K/2. Esempio: N = 8, FSR = 10,24 V, K = 40 mV; x = +127 dà 5,08 V ± 20 mV.
Il convertitore D/A è una rete di resistenze (scala R-2R) con interruttori comandati dai bit e un amplificatore operazionale che tiene a zero il nodo di somma: la corrente è i = (K/R) · X, e la tensione V = K · X − Vpol, unipolare con Vpol = 0, bipolare con Vpol = FSR/2. È in pratica combinatorio, quindi velocissimo, ma ha transizioni spurie (da 01111111 a 10000000 cambiano tutti i bit): a valle si mette un filtro passa-basso. Due resistenze tarabili correggono gli errori di offset e di guadagno. L'interfaccia è un'interfaccia di uscita senza handshake: il processore scrive in TBR quando vuole.
Il convertitore A/D ad approssimazioni successive contiene una RSS, il SAR (Successive Approximation Register), un convertitore D/A e un comparatore. Fa una ricerca per bisezione sul byte da produrre, un bit per clock dal più significativo; dialoga con l'esterno con un handshake soc/eoc, e un latch analogico tiene ferma la tensione durante la conversione.
L'interfaccia di conversione ha un registro di stato e controllo (SOC scrivibile nel bit 1, EOC leggibile nel bit 0) e RBR con il risultato. Il software di gestione, da non confondere con il μ-programma del convertitore:
Siccome il convertitore inizia la conversione molto in fretta, nel software si può saltare l'attesa di EOC = 0. Non così in un esercizio di descrizione: una rete che comanda un convertitore deve sempre rispettare l'handshake, cioè scrivere qualcosa come S2: begin SOC <= eoc; STAR <= (eoc==1) ? S2 : S3; end, e mai abbassare SOC alla cieca.
Buone norme ed errori tipici
- Nello spazio di I/O si usano
/iore/iow, non/mre/mw, e gli indirizzi sono a 16 bit ({'H00, offset}). - In lettura di I/O l'indirizzo è stabile un clock prima che /ior scenda, e non cambia finché /ior non risale: una lettura può modificare lo stato dell'interfaccia.
- In scrittura di I/O i dati sono sul bus prima del fronte di discesa di /iow; in memoria devono restare fermi attorno al fronte di salita di /mw.
- DIR vale 1 solo durante le scritture, e torna a 0 solo dopo che il comando di scrittura è risalito.
- Sul bus non c'è un filo di select: i select li producono le maschere, e l'offset di una porta lo decide chi le progetta.
- Le interfacce sono RSS: hanno il loro /reset e, se campionano segnali del processore, un clock leggermente ritardato.
- In un esercizio con un convertitore o un'altra rete asincrona, gli handshake si rispettano sempre per intero.
- Nei ricevitori seriali conta l'istante di campionamento: la regola k − j + 1 e il clock perso nello stato che inizializza il contatore.