Tutorial sulla struttura interna di una CPU

Stampa
( 2 Votes ) 
Valutazione attuale:  / 2
ScarsoOttimo 
Categoria: Informatica
Data pubblicazione
Scritto da Equelna Visite: 2078

Tutorial sulla struttura interna di una CPU

 Breve tutorial su come funziona una CPU  

Facendo seguito al tutorial presentato da TheAlu, al quale va dato merito per il progetto e l'articolo presentato, devo far presente che la circuitazione (ALU  4 bit) da sola è solo un bel poco di chip ben interconnessi. 
Per funzionare  una ALU  ha bisogno  di altri  blocchi funzionali che instradino correttamente il flusso dei dati in ingresso ed in uscita.

Un esempio di minimo di blocchi può essere il seguente:

 - Registro accumulatore (RA)
 - Registro per il secondo operando (RB)
 - Registro intermedio di memorizzazione risultato (RF)
 - Registro di stato delle operazioni (ST)
 - Registro program-counter per l'indirizzamento della memoria programma (PC)
 - Registro puntatore allo stack (SP)
 - Registro puntatore all'area dati (IX)
 - Logica di comando per il flusso (Sequencer)

 

Schema logico

Ipotesi
 Costruire una CPU (Central Process Unit) che abbia le seguenti caratteristiche :
 - 8 bit dati
 - 16 bit indirizzamento
 - 4 registri accumulatori e/o uso generico, più ne sono meglio è, in quanto per
   ogni accesso alla memoria esterna il tempo di esecuzione, legato alle velocità
   di accesso, è maggiore
 - 2 registri puntatori area dati con indirizzamento diretto, indiretto e indicizzato
 - Stack-pointer per la gestione delle chiamate a sotto programmi
 - 1 registro di stato che memorizzi i bit-flag della ALU (Carry, Zero, A>B, A
 - Massima velocità di clock 40MHz - 60MHz
 
Per raggiungere le prestazioni richieste avremo bisogno di tutta componentistica di tipo CMOS F la quale garantisce tempi di risposta e ritardo nell'ordine di  5 - 13 nS, contro i 15 - 35nS della tecnologia TTLS.

Qualcuno  potrebbe  obiettare che con tempi di ritardo così  piccoli si potrebbe alzare  ulteriormente  la   frequenza   di  clock   fino   a  ca.   80MHz,  ma è certamente buona cosa mantenere sempre un  margine di tolleranza ampio  in  modo da non dover poi avere brutte sorprese.

Avremo disogno di:
 - ALU 8 bit (duplichiamo ALU di TheAlu)
 - Registro (RA) : accumulatore    (1x74F373)
 - Registro (RB) : generico       (1x74F373)
 - Registro (RC) : generico      (1x74F373)
 - Registro (RD) : generico      (1x74F373)
 - Registro (PC) : program counter   (4x74F191)
 - Registro (SP) : stack pointer    (4x74F191)
 - Registro (IX) : indirizzamento dati (4x74F191)
 - Registro (IY) : indirizzamento dati (4x74F191)
 - Registro (ST) : stato ALU      (1x74F373)
 - Logica di comando e controllo (Sequencer) porte logiche, DFF e quanto altro possa servire
 Inoltre altri registri di uso interno non direttamente indirizzabili da codice
 - Registro (AA) : operando A della ALU (1x74F373)
 - Registro (AB) : operando B della ALU (1x74F373)
 - Registro (RF) : risultato operazione ALU (1x74F373)
 - Registro (OP) : contiene il byte OpCode letto in fase di fetch (1x74F373)
 - Registro (O2) : contiene il byte 2 dell'istruzione (1x74F373)
 I nove  registri formati  dalle 74F373  postrebbero essere  sistituiti con una  coppia  di 74F89 (74F289)  una  RAM 16x4  bit  e ne  rimarrebbero altri 7 a disposizione.

La logica di controllo 

Un microcontroller o CPU (AMD, Intel,  etc) il flusso dei dati ed i segnali di controllo generati sono regolati da una precisa sequenza  operativa   che   serve  a  smistare  correttamente le informazioni. In effetti all'interno di  una CPU è presente un  altro "computer" che  gestisce  tutti  i  flussi ed  esegue  le  instruzioni  relative ai  codici operativi (OpCode) della CPU.

La più elementare delle sequenze operative che il sequencer deve eseguire ha  inseguenti passi:

 - 1) accesso alla memoria esterna
 - 2) lettura di un byte (fetch)
 - 3) rilascio del bus dati e indirizzi
 - 4) decodifica dello OpCode
 - 5) esecuzione del comando
 - 6)  incremento PC
 - 7) ritorno al passo 1
 
Analizziamo ogni singolo passo del sequencer
 Passo 1 - Accesso alla memoria esterna
 
     Prima  di  accedere  all'esterno  una CPU  deve  verificare  che  i due  bus   (Indirizzi e Dati)  siano disponibili, ovvero  non sono  utilizzati da altro   dispositivo esterno es.: DMA (Direct Memory Access) o dispositivo di I/O.   Verificata la disponibilità genera il segnale di accesso ai Bus (NBREQ).    Se i Bus sono occupati rimane in  attesa fintanto che questi sono disponibili.
  
 Passo 2 - Lettura di un byte (fetch)
  
     La CPU genera il segnale di lettura dalla memoria esterna (NRDS) e memorizza  il dato letto in un registro di transito (OP).
  
 Passo 3 - Rilascio bus dati e indirizzi
  
     La CPU pone i segnali BREQ e NRDS nello stato di rilascio, generalmente alto.

 Passo 4 - Decodifica dello OpCode
  
     La CPU analizza il dato memorizzato nel registro di transito. 

     Lo  OpCode per sua natura è un  "codice parlante",  e chi abitualmente è avvezzo alla gestione dati in genere sa che questo metodo è il più  efficace per il riconoscimento di  un  prodotto, conto e quanto altro sia stato codificato e gestito a mezzo programma, ovvero  nel codice stesso  sono definiti tutti i soggetti ed oggetti interessati all'operazione.

   Prima  di  proseguire  nella  decodifica dello  OpCode  va  definita  la sua   struttura cioè il significato di ogni singolo bit che lo compone.
   Per semplicità dividiamo i codici  operativi (OpCode) in 5 blocchi  così che le operazioni possano essere facilmente distinguibili.
  
   1 - Operazioni logico-aritmetiche
   2 - Operazioni di accesso alla memoria esterna (lettura,scrittura)
   3 - Operazioni di chiamata a subroutine e di accesso allo SP
   4 - Operazioni di salto condizionato ed incondizionato
   5 - Operazioni varie (tutte quelle che non rientrano nelle 4 categorie precedenti)
         Es. Set carry, Clear carry, Enable interrupt, etc.
   
  Poniamo per esempio la seguente codifica alla nostra suddivisione

Gruppo Bit opcode 

7  6  5  4  3  2  1  0

Base OpCode
1 0  0  1  X  X  X  X  X  h2X
2 0  1  X  X  X  X  X  X  h4X
3 1  0  0  X  X  X  X  X h8X
4 1  0  1  X  X  X  X  X  h9X
5 1  1  X  X  X  X  X  X  hCX
  
E' evidente come è semplice distinguere le operazioni che la CPU  deve eseguire e che non Ã¨ possibile costruire un codice macchina ambiguo. Gli OpCode da  h20  a h3F  sono  quelli che  interessano  la ALU,  da  h40 a h7F operazioni di lettura  scrittura della memoria,  da h80 a  h9F le chiamate a subroutine e gestione dello SP, da hA0  a hBF operazioni di salto, da hC0  a  hFF tutte quelle della  categoria   5.

Come avrete notato ho quasi saturato la capacità del byte OpCode.
  
  Adesso dovrebbe essere definito il significato intrinseco dei bit  rimanenti  per ogni gruppo.
  
  Per semplicità analizziamo il gruppo 1
  
   Bit opcode  7  6   5   4   3   2   1   0
   1 -     0  0  1  X  X  X  X  X
             
    Bit 5 = 0 : Non segue altro byte
    Bit 5 = 1 : Lettura byte successivo
    Bit 4 = 0 : Operazione tra registri
    Bit 4 = 1 : Operazione tra registri e memoria o memoria e registri o valore
    Bit 3 = 1 : Operazione tra registri e memoria con accesso indicizzato
    Bit 2 a 0 : identificano l'operazione che deve essere eseguita.
    
    Va premesso che in tutti gli esempi di codici operativi la sintassi è:
    Codice mnemonico destinazione, sorgente
    
    Es.: ADD RA, RB - somma al registro RA il valore del registro RB e  poni
       il risultato in RA
      
       Il  nostro  OpCode sarà  :  0 0 1 0 0 0 0 0 = h20
       (posto che l'operazione di somma abbia come codifica 0)
      
       A  questo punto è necessario definire ed analizzare il  significato dei bit del secondo byte che deve essere letto dalla CPU.
 
      Bit da 7 a 4 destinazione
      Bit da 3 a 0 sorgente
      
      Bit 7 = 0 : registro
      Bit 7 = 1 : memoria
      Bit 6 a 4 : se bit 7=0 quale registro es. RA 000
      Bit 3 = 0 : registro
      Bit 3 = 1 : memoria
      Bit 2 a 0 : se bit 3=0 quale registro es. RB 001

 

      Definiamo la codifica dei registri destinazione
      0 - RA
      1 - RB
      2 - RC
      3 - RD
      Nel caso di operazioni tra registri 
      4 - IX LSB
      5 - IX MSB
      6 - IY LSB
      7 - IY MSB
      
      Nel caso di operazioni tra registri e memoria
      4 - IX : dove IX è l'indirizzo del dato
      5 - IY : dove IY è l'indirizzo del dato
      6 - Dato ovvero il byte che segue è una costante
      7 - SP 
      
Per chiarire la funzionalità faccio degli esempi:      



Operazioni del gruppo 1 Base OpCode Secondo byte
ADD RB, RA         Bit  001XXXXX 0000 1000  h20  0001 0000  h10
ADD RA, (IX) 0011 0000 h30 0000 1100 h0C
ADD RD, #XX  0011 1110 h30 0011 0110 h36 + dato
ADD RB, IX[LSB] 0010 0000 h20 0001 0100 h14
ADD RC,(@IX) 0011 1000 h38 0010 0100 h24

     
      Il simbolo @ prima di IX indica che dopo l'operazione il puntatore  viene incrementato dopo l'operazione di lettura/scrittura
      
      Formalismo valido anche per le operazioni di accesso alla memoria.
      
      Dopo questo chiarimento potremo allo riscrivere il ns OpCode per 



Codice mnemonico Codifica 1 byte Codifica 2 byte
MOV RA, RB 0011 0000 0000 1100
h30 h0C
Va da se che le altre possibilià vanno sviluppate.
Adesso possiamo analizzare lo OpCode letto dal sequencer (ADD RA,RB)

Il sequencer per primo identifica  il gruppo di appertenenza del  codice  macchina da eseguire,  un semplice decodificatore  a 3 bit,  nel ns caso uscita  0, poi  se il  bit  4  è 0  prende  i  bit da  0  a  3,  imposta l'operazione  della  ALU   e salta  al  passo  5.  Se  il bit   4  è   1 incrementa il   PC di 1 legge   il secondo   operando   dell'istruzione, previa  la  verifica  per  l'accesso  ai  bus,  lo  pone  nel   registro transitorio  secondo  operando  (O2)  per  continuare  nella  decodifica  dell'istruzione. 


Ora  analizza  il  valore del  bit  4  per sapere  quale  è  la sorgente (operando A della ALU) del dato su cui operare, se è 0 pone il contenuto del registro sorgente, come da tabella, in ingresso all'operando A (AA), nel  ns.  caso  ADD  RA,RB  è RB,  pone il contenuto del  registro destinazione (RA) all'ingresso  dell'operando B  (AB),  e  va al  passo 5.  Se il secondo operando è  il   valore puntato da uno dei registri puntatori  (es.  ADD   RA,(IX))  fa accesso alla memoria esterna ponendo sul bus indirizzi il  valore del  registro   puntatore, previa verifica degli accessi ai  bus, legge il valore e quindi lo pone all'ingresso  dell'operando  A,   il  contenuto di RA all'ingresso dell'operando B e  va al passo 5.    

Passo 5 - Esecuzione del comando
    
    Il  sequencer memorizza il risultato della ALU nel registro di  transito  (RF) e lo stato nel registro  di stato (ST), se l'operazione prevede  un registro  di  destinazione  copia il contenuto di RF nel registro destinazione, nel  caso ADD  RA,RB  in RA.

Se  la destinazione  è  una locazione di  memoria (es.  ADD (IX),RA)  il valore  di RF viene copiato nella locazione di memoria puntata dal registro (IX).
 
Passo 6 - Incremento PC
  
    Il  sequencer a  questo punto   incrementa di  1 il  valore  del program counter prima di iniziare un nuovo ciclo operativo.
  
  La sequenza  descritta dei  passi che  il sequencer  deve eseguire  è sempre  valida per le istruzioni dei gruppi 1  e 2 in quanto in ambedue sono  sempre  previste una destinazione ed una sorgente su cui operare.

 3 - Operazioni di salto.
  
  Per i gruppi 3 e 4 dove il bit 5 dello OpCode è già di per sè significativo, cioè è il discriminante tra i due gruppi, bisogna che il significato dei bit  rimanenti (4 a 0) sia ridefinito.



 


   Bit opcode  7  6   5   4   3   2   1   0
    2 -  1 0 0 X 1 X X X
             
    Bit 4 = 0 :salto incondizionato
    Bit 3 = 0  :va letto il byte successivo che contiene il valore del displacement
                     (DSP) che sommato al valore del PC formerà l'indirizzo di arrivo
    Bit 3 = 1  :vanno letti i due byte successivi che sono l'indirizzo di arrivo
    
    Come si può  notare grazie al  solo bit 3  possiamo gestire sia  i salti
    brevi che quelli lunghi.
    
    Se bit 4 = 0 : i bit da 2 a 0 non hanno alcun significato devono  essere posti a zero a meno che  a qualcuno non venga  voglia di
        utilizzarli per operazioni di salto relative ad un registro puntatore del tipo:
    
     JMP (IX) :  salta all'indirizzo contenuto  nella (bit 3=0)  nelle  (bit 3=1) locazioni di memoria puntate da IX.
     
    Quindi in questo caso avremo:
    
    Bit 2 = 0 : salto riferito a PC
    Bit 2 = 1 : salto riferito a registro puntatore
    
    
    Bit 4 = 1 :salto condizionato
    Bit 3 = 0  : "     "     "    breve
    Bit 3 = 1  : "     "   "      lungo
    Bit 2 a 0  :identificano quale dei bit del registro di stato (ST) deve essere 1 per soddisfare la condizione per il salto

 

     Es:
     Bit 7 : Zero (il risultato dell'operazione è 0)
     Bit 6 : Not Zero
     Bit 5 : A = B (operazione di confronto)
     Bit 4 : A > B
     Bit 3 : A < B
     Bit 2 : Carry
     Bit 1 : Not Carry
     Bit 0 : Positivo
     
    Già con questa  semplice codifica è  possibile definire le  istruzioni di salto condizionato più comuni ed utilizzate:
   
      JZ  - Salta se è zero
      JNZ - Salta se non è zero
      JEQ - Salta se A = B
      JG - Salta se A > B
      JNG - Salta se A < B
      JC - Salta se Carry = 1
      JNC - Salta se Carry = 0
      JP - Salta se positivo
           
    con  poche altre  porte  logiche e molta  pazienza  si  possono anche definire altre condizioni di salto.


 

    Per definizione  i salti  brevi sono  sempre nell'ambito  dei ±  128 byte  rispetto al valore corrente del  PC. Ottenere questo è molto  semplice in  quanto se il bit 7 del displacement (DSP) è zero allora avremo un salto  in avanti se è 1 indietro.
    Vi  starete domandando come  è possibile ciò.  Per  ottenere  l'indirizzo  di salto il valore  del  PC LSB  deve essere  sommato algebricamente  al valore del  displacement(DSP), questo implica che il   micro  codice  del sequencer  deve  essere in  grado  di  accedere  al registro puntatore PC  per poter effettuate, tramite la ALU, questa operazione. 
    
Chiariamo  : in  AA val  il valore  LSB di  PC in  AB il valore DSP, se questo Ã¨ positivo (bit  7=0), mentre (bit 7=1) in AB val il valore negato di DSP. 
Fatto questo è sufficiente attivare il comando di somma ed il gioco è fatto. Con l'utilizzo  di una  semplice porta   AND o NAND 2,3 ingressi in  unione ad un multiplexer un possiamo istruire il sequencer sul valore che ha  il  bit  di  stato  interessato nell'istruzione  di  salto  e di  conseguenza  attivare  la  lettura  del/i byte  successivi  o  andare direttamente  ad  incrementare  solo PC  di 1  salto breve ,2 salto  lungo (condizione  non soddisfatta).

    Quindi se per esempio nel corso del codice

Riga pgm Descrizione dell'operazione Valore PC
h1020  JNZ $h40   salto corto dovremo aggiungere a h10 il valore h40 = h60 e impostarlo in PC LSB h1060
h1200  JNZ $h80  dovremo aggiungere a h1000 il valore negato di h80 (h7F) che ha come risultato h01 ed impostarlo in PC LSB h1001

In questa operazione di somma il carry in ingresso della ALU deve essere  zero (no carry) ed  il valore che esso assume dopo la somma  deve essere scartato.  

Utilizzo e funzionamento dei registri puntatori.
  
   I registri IX e IY sono utilizzati nello svolgimento del pgm per leggere e scrivere in aree di memoria RAM  e/o di IO senza dover specificare  ogni volta l'indizizzo in cui essi  sono allocati. Ho ipotizzato l'utilizzo di U/D Counter presettabili in quanto sempre nell'ipotesi di  progetto ho previsto l'indirizzamento indicizzato e quindi sarebbe sicuramente più agevole da implementare con questo tipo di componenti che con l'utilizzo di D-Latch e poi passi esecutivi del micro codice che interesserebbero la ALU.

Utilizzo e Funzionamento del registro stack pointer.
   
   Questo registro puntatore è utilizzato nelle operazioni di chiamata  a subroutine e per memorizzare eventuali parametri da passare  alle stesse.
   Una chiamata   a suburoutine   (call $XXXX  e relativa  RET)  consente  al normale flusso di programma  di eseguire un gruppo di istruzioni  che eseguono una specifica sequenza operativa attivata dallo evolversi degli eventi e dei dati del programma base per poi  tornare al normale flusso  elaborativo.

    Il  registro, una volta  inizializzato con un  valore appropriato, ha un funzionamento di tipo FIFO  (First Input  - First Output).   

   Quando viene invocata una istruzione CALL la sequenza delle operazioni è:
    
    - salva il valore del PC nella locazione di memoria puntata da SP
    - decrementa SP

    - salta all'indirizzo specificato
    
    Nel caso della ns CPU avremo:
    
     - salva MSB di PC nella locazione di memoria puntata da SP
     - decrementa SP
     - salva LSB di PC nella locazione di memoria puntata da SP
     - decrementa SP

     - imposta PC con il valore indicato nei byte successivi prima LSB e poi MSB

   Quando viene invocata una istruzione RET la sequenza delle operazioni è:
   
     - incrementa SP
     - copia il valore puntato da SP in PC
   
    Nel caso della ns CPU avremo:
    
     - incrementa SP     
     - copia il valore puntato da SP in LSB di PC
     - incrementa SP     
     - copia il valore puntato da SP in MSB di PC

    Per  esempio se  abbiamo inizializzato  il ns  SP con  hAFFF quando  incontriamo una istruzione CALL avremo:
    
      hAFFF = MSB di PC
      hAFFE = LSB di PC
      SP = hAFFD

   Il passaggio dei valori da un pgm principale ad una subroutine si effettua tramite il comando PUSH (spingi su) che salva il  contenuto di un registro nella  locazione di memoria puntata da SP,  gli  eventuali valori devono essere passati prima della CALL inquanto  l'istruzione RET valorizza il  PC  con il contenuto  della locazione di  memoria puntata da SP.

L'operazione inversa è POP  registro (tira giù) ovvero recupera il valore puntato da SP nel registro indicato.

     Nel ns caso :
      
      Stato inizale di SP hAFF0
     flusso Pgm principale

     .....
            PUSH RA   SP = hAFEF ;Param. 1 operazione
            PUSH RB   SP = hAFEE ;Param. 2 opearazione
            CALL hXXXX  SP = hAFEC
      ....
      
      Subroutine
      
       Recupera i valori da SP  fa la somma e restituisce il  risultato in RB puntato da SP
       

Istruzione Valore SP Commento
PUSH RA hAFEB salva RA nello stack
PUSH RB  hAFEA salva RB nello stack
Mov RA,(SP-6)
Mov RB,(SP-5)
ADD RA,RB somma
Mov (SP-5),RA  salva il risultato in param. 2 
POP RB  hAFEB ripristino valore iniziale RB
POP RA

RET

hAFEC
hAFEE
ripristino valore iniziale RB

ritorno al pgm principale


       
       Come si può notare abbiamo perso 2 locazioni di  memoria, ora se questa operazione viene ripetuta  diciamo solo 20 volte  alla fine avremo  perso,  rettifico reso inutilizzabili,  40  locazioni di memoria nello stack, per cui la instruzione RET diventa RET 2 dove il valore 2 comunica al gestore dello SP che dopo aver  recuperato il valore di PC ed incrementato il proprio deve sommare 2 al se stesso.
       
       Il codice finale della subroutine diventa 
       
       .....
       POP RA   SP = hAFEB
       RET 2       SP = hAFF0  

Quì ci fermiamo per ora.

Credo di aver chiarito, lo spero, i concetti relativi al funzionamento di una CPU in maniera semplice e concisa, so per esperienza che 1 esempio vale più di 100 parole, ma senza queste lo stesso non ha alcun significato, anzi diventa del tutto incomprensibile. 

Se i miei impegni mi lasceranno un poco di tempo spero in un prossimo futuro di presentarvi uno schema di sequencer che sarà quasi sicuramente diviso in più articoli vista la complessità dell'argomento e anche perchè credo non sia possibile presentare una circuitazione di questo tipo senza doverla commentare approfonditamente.

Saluti a tutti e buone saldature
 
PS. a TheAlu10000  - Non sarebbe il caso di proporre un concorso, uno stage o quanto altro alla comunità.

Joomla 1.7 Templates designed by College Jacke