L'unico modo per capire come funziona qualcosa, è costruirne una da zero
Oppure emularla via software. Ma cominciamo dalle basi.
Cos'è una CPU?
La CPU (Central Processing Unit, Unità di Elaborazione Centrale) è il nocciolo di un sistema di elaborazione in grado di eseguire programmi. Da una cinquantina di anni le CPU sono integrate in un unico pezzo di silicio e quindi un unico microchip. Oggi le CPU possiamo trovarle a loro volta integrate dentro sistemi più complessi, come i microcontrollori (usati nei sistemi embedded) o i SoC (System on Chip).
Dati e Indirizzi
L'architettura della CPU definisce il suo cosiddetto "parallelismo", ovvero la lunghezza in bit dei numeri che possono essere elaborati con una singola istruzione, matematica o logica. Avremo quindi CPU con architetture a 8, 16, 32, 64 e 128 bit. Una CPU a 32 bit avrà un'istruzione in grado per esempio di sommare tra loro due numeri da 32 bit ciascuno, contenuti in due registri interni sempre da 32 bit. E di memorizzare il risultato della somma in un altro registro, sempre da 32 bit. Più l'eventuale bit di riporto. Il termine "WORD" è associato a questa lunghezza. Un altra caratteristica importante delle CPU è il loro "spazio di indirizzamento". Ovvero quante celle di memoria possono essere gestite, lette e scritte singolarmente. In genere la singola cella di memoria occupa 1 byte (8 bit). Ma possiamo avere anche celle più "larghe". Larghe ad esempio come una "word". Lo spazio di indirizzamento si esprime in bit. Con 10 bit posso indirizzare 1K, ovvero 1024 celle (2^10). Con 16 bit indirizzo 64K celle (2^16). Con 20 bit indirizzo 1Mega (2^20, ovvero 1024x1024 celle). Con 32bit arrivo fino a 4Giga (2^32 celle). Per semplificare la gestione della memoria installata, soprattutto con spazi di indirizzamento di 20 bit e oltre, si può trovare integrata nella CPU una MMU (Memory Management Unit, Unità per la Gestione della Memoria). Inoltre possiamo avere uno o più livelli di Cache per velocizzare l'accesso alle zone di memoria indirizzate più di frequente (Cache di primo, secondo e perfino di Terzo livello, magari dedicate per codice e/o dati).
La struttura interna
La CPU che tipo di operazioni compie? E' in grado di caricare le istruzioni da eseguire (dette "codice macchina") da una memoria esterna, di eseguirle, e di scrivere i risultati di questa esecuzione sulla stessa memoria esterna, oppure su un'altra dedicata ai soli dati. Della decodifica delle istruzioni e della loro esecuzione vera e propria si occupa la cosiddetta Control Unit. La CU si appoggia a una serie di registri interni alla CPU, organizzati e interconnessi attraverso un Data Path comandato dalla CU, per poter eseguire le operazioni matematiche e logiche di base e memorizzare i risultati negli stessi registri.
Tutti i problemi che possiamo risolvere utilizzando i computer, e quindi le CPU al loro interno, perfino i più complessi come elaborare un'immagine realistica di un buco nero (come abbiamo visto nel film "Interstellar"), o mandare una sonda su Marte, possono essere scomposti in problemi più semplici. Si procede finchè questa scomposizione non si riconduce a singole operazioni matematiche (somme, moltiplicazioni) e logiche (AND, OR, NOT) di base. La CPU può eseguire queste operazioni semplici a milioni (o miliardi) al secondo, aiutandoci quindi a risolvere i problemi complessi.
Le CPU sono anche in grado di acquisire dati dalle periferiche e di pilotarle, queste periferiche. Ciò avviene attraverso delle porte di I/O (Input/Output). Queste porte possono essere accedute dalla CPU in maniera dedicata, attraverso istruzioni per l'I/O. Oppure possono le porte possono essere Memory Mapped (MM, "mappate in memoria"). Significa che alcune porzioni di indirizzi di memoria non corrispondono in realtà a celle che possono essere lette e riscritte, ma a queste porte di I/O che permettono la comunicazione dati da e verso le periferiche.
La classificazione delle CPU
Le CPU si dividono in due grandi famiglie: RISC e CISC.
Consigliati da LinkedIn
CPU di tipo RISC
Le CPU della prima famiglia (RISC, Reduced Instruction Set Computer, Computer con Set di Istruzioni Ridotto) sono in grado di eseguire un numero piccolo di istruzioni diverse. Tipicamente una decina, o comunque poche decine. E queste istruzioni hanno tutte la stessa dimensione in bit, in genere coincidente con una word. La loro lunghezza fissa e la loro semplicità, ne consente spesso l'esecuzione in un singolo colpo di clock. Le CPU RISC per fare questo hanno quindi una CU più semplice. Di contro, hanno un gran numero di registri interni, diverse decine, ognuno di larghezza pari al parallelismo, quindi un DP più complesso. L'I/O avviene in genere attraverso porte MM. Tutto questo comporta il bisogno di scrivere ed eseguire più istruzioni, anche per implementare operazioni relativamente semplici.
CPU di tipo CISC
Le CPU della seconda famiglia (CISC, Complex Instruction Set Computer, Computer con Set di Istruzioni Complesso) sono invece in grado di eseguire un gran numero di istruzioni diverse. Un centinaio, o anche diverse centinaia. Le istruzioni poi non hanno una dimensione fissa. Possono essere lunghe da un singolo byte fino a qualche decina di byte, dipende dall'istruzione. Hanno quindi una CU più complessa, perchè questa deve essere in grado di decodificare ed eseguire un numero elevato di istruzioni, di lunghezza variabile. E le istruzioni possono aver bisogno di diversi colpi di clock, prima di arrivare a produrre un risultato. Dall'altro lato, le CPU CISC hanno un DP più semplice, in genere solo una decina di registri, per fare le loro operazioni. Nei CISC possiamo trovare anche delle istruzioni dedicate all'I/O, oltre a istruzioni singole che possono portare a termine operazioni anche molto complesse. Per fare un esempio pratico, nell'architettura Intel x86 abbiamo la singola istruzione macchina MOVSW. Questa istruzione, quando viene eseguita, copia una word dall'indirizzo di memoria indicato nel registro ESI (Source Index) passando per un registro temporaneo interno alla CPU, all'indirizzo indicato nel registro EDI (Destination Index). Alla fine incrementa di uno questi due registri indice, e decrementa di uno il registro contatore ECX. Tutto in una singola istruzione macchina.
Cosa vogliamo fare?
L'obbiettivo è che per capire meglio tutto questo, si potrebbe scrivere da zero usando il linguaggio C (ma va bene qualsiasi linguaggio di alto livello) un "emulatore Software" di una CPU, in grado di simularne il funzionamento, ovvero il caricamento delle istruzioni dalla memoria, la loro esecuzione utilizzando i registri interni, e la scrittura del risultato su una memoria esterna. Alla fine realizzeremo uno strumento didattico, che consenta a uno studente che si approccia verso questi argomenti, una comprensione più approfondita, attraverso la progettazione e la costruzione da zero dell'oggetto di studio.
Emuleremo una CPU relativamente semplice, che chiameremo KPU, per mantenere il software compatto e facile da scrivere e da capire. Puntiamo a realizzare una CPU RISC a 16 bit. Gestiremo quindi delle WORD, e avremo istruzioni e registri interni, da 16 bit cadauno. Come spazio di indirizzamento, anche qui andremo sui 16 bit. Avremo una memoria, che conterrà al suo interno sia codice che dati, grande 64Kword da 16 bit cadauna. Una porzione di questo spazio di indirizzamento lo dedicheremo al I/O. Ovvero, avremo un'intervallo di indirizzi di memoria che non sarà memoria "vera", ma ci servirà per "parlare" con semplici periferiche, emulate via software anche queste. Oppure connesse a periferiche reali come display (console) e tastiera.
Cominciamo a mettere giù due righe (di codice)
Scriviamo un po' di codice e gettiamo le basi del nostro emulatore. Prima di tutto i tipi di dato di cui avremo bisogno per modellare la nostra KPU. Tutti gli oggetti della nostra KPU faranno riferimento a questi tipi predefiniti di base, in modo da facilitare eventuali espansioni o versioni della KPU con caratteristiche diverse, senza dover riscrivere tutto. Utilizziamo come tipi di partenza, gli interi a lunghezza fissa definiti in stdint.h
/* types */
typedef uint16_t kword_t; /* internal word base type */
typedef uint32_t kdword_t; /* internal double word base type */
typedef uint8_t kbyte_t; /* UNUSED - 8 bits BYTE type */
typedef uint16_t kaddr_t; /* memory address base type */
typedef kword_t kinstr_t; /* KPU instruction base type */
Poi un po' di macro costanti, autocalcolate partendo dai tipi, per evitare "magic numbers" nel codice.
#define KPUWORDSIZE (sizeof(kword_t)) /* KPU word size, in bytes */
#define KPUBITS (KPUWORDSIZE<<3) /* KPU word size, in bits */
#define KPUMAXWORD ((1u<<KPUBITS)-1) /* word MAX value */
#define KPUPTRSIZE (sizeof(kaddr_t))
/* KPU Memory pointer size in bytes */
#define KPUADDRBITS (KPUPTRSIZE<<3) /* address bus size, in bits */
#define KPUMEMSIZE (1u<<KPUADDRBITS) /* memory size, in words */
#define KPUMEMLASTWORD (KPUMEMSIZE-1) /* last word's memory address */
La nostra KPU, come le altre CPU "vere", avrà al suo interno una batteria di 8 registri, denominati da r0 a r7. Avrà anche un Instruction Register (IR) che conterrà di volta in volta l'istruzione appena caricata dalla memoria, pronta per l'esecuzione. E infine un Program Counter (PC), che conterrà l'indirizzo in memoria della prossima istruzione da caricare ed eseguire. Tutto questo comporrà il Data Path (DP) della nostra KPU. Nel Data Path delle CPU reali c'è anche la cosiddetta ALU (Arithmetic Logic Unit, Unità Logico Aritmetica). Ovvero tutta quella logica che realizza le operazioni matematiche (somme, sottrazioni, moltiplicazioni...) e logiche (NOT, AND, OR, XOR...) di base, usando i registri come operandi e destinazione per i risultati.
Possiamo modellare il DP usando una struttura C (struct) relativamente semplice. Istanziamo in memoria, come variabili globali, la nostra kpu0 con queste caratteristiche, insieme alla memoria da 64Kword che sarà indirizzata ed usata dalla nostra KPU.
typedef struct {
/* user registers */
kword_t R[8]; /* Registers r0..r7 */
/* internal instruction register */
kinstr_t IR;
/* Internal Program Counter */
kaddr_t PC;
} kpu_t;
/* globals */
/* kpu(s) */
kpu_t kpu0;
/* installed memory */
kword_t m[KPUMEMSIZE];
Nell'articolo seguente vedremo le prime operazioni fondamentali su questi oggetti, come l'inizializzazione al Reset. Definiremo poi le operazioni base sulla memoria, lettura e scrittura di singole word. Il ciclo di Fetch&Execute delle istruzioni macchina e, alla fine, cominceremo a progettare quelle che saranno le istruzioni della nostra KPU, magari ispirandoci a qualcosa di reale o esistente.
.
1 annoAvevo iniziato un progettino simile per il momento accantonato per questioni di tempo. Penso che leggerò avidamente tutti gli articoli!
Software Manager presso MASMEC SpA
1 annoOttimo articolo Massimo 👍 Attento che se guardi nell'abisso poi l'abisso guarda in te 😀