Lezione 1 (10.03.2021)
Lo scopo del corso è quello di progettare un sistema basato su un processore: dobbiamo evidenziare le varie parti del sistema e dobbiamo imparare a costruire il sistema componendo le diverse parti. Queste parti sono:
- Il processore
- Il bus
- La memoria
- Dispositivi di I/O
Quando andiamo a disegnare il sistema fisico, avremo la macchina fisica “F”, il software di base, e poi via via possiamo pensare che la nostra macchina utilizza delle applicazioni che utilizzano o il software di base (sistema operativo) o altri sistemi. Per poter realizzare questi sistemi, vediamo tutte le informazioni che ci servono. Dobbiamo conoscere bene il processore, perché altrimenti non possiamo utilizzarlo al meglio. Con riferimento al processore, abbiamo capito qual è il modello di funzionamento e poi abbiamo visto il modello di programmazione del processore.
Il processore è una macchina sequenziale, e in quanto macchina sequenziale dobbiamo capire come funziona, la sua architettura interna e come si può programmare. I processori che abbiamo visto sono di due tipi diversi: a stack e a registri generali (Motorola 68000). Qualunque processore si può strutturare in 2 parti: una parte operativa ed una parte di controllo responsabile di interpretare le istruzioni e eseguirle. Quindi nel progetto di processore, definisco cosa deve fare e di conseguenza qual è il modello e poi la sua architettura interna.
Per capire l'interesse di quello che vogliamo fare: se vogliamo costruire un sistema che deve controllare un impianto (ruota, binario), normalmente non penso di usare un computer commerciale perché probabilmente non avrei componenti necessarie al mio progetto, ed è ovvio che costruisco un sistema ad hoc. Il sistema industriale permette di costruire due tipi di sistemi:
- Quello in cui io ex novo costruisco il mio computer, quindi monto processore, memoria, I/O su una scheda (ovviamente processore e memoria normalmente li acquisisco, non li faccio io. Per I/O o uso degli standard oppure li costruisco io utilizzando FPGA).
- Quello in cui utilizzo un oggetto più grande, cioè un oggetto che è stato già costruito con processore, memoria e I/O, ed ha solo questi 3 elementi. Le chiamiamo schede (Arduino, MAPI), esse pur avendo questi 3 componenti, finisce per avere più dispositivi di I/O. Il numero di connettori è limitato, quindi non tutti i dispositivi di I/O arriveranno in uscita; se arrivano tutti non c'è problema, se non arrivano tutti verrà usato un dispositivo di cross-bar (uno switch, che viene fatto con MUX/DEMUX) che in base all'applicazione utilizza certi I/O piuttosto che altri.
Nel caso di costruzione ex novo, noi con ASIM monteremo processore, memoria e I/O. ASIM utilizza il processore 68000, che è un modo per programmare un sistema e farlo funzionare. Nel secondo caso, il processore è un pipelined, e lo possiamo programmare in C (dei sistemi, non quello degli algoritmi).
I processori abbiamo detto che si dividono in diverse tipologie:
- A stack, che fanno operazioni assumendo implicitamente che gli operandi siano in una pila
- A registri generali, che hanno più registri che contengono dati, che contengono indirizzi, per poter fare delle operazioni (Motorola 68000); oppure contengono entrambi (MIPS).
Le istruzioni sono composte da codici operativi + modi di indirizzamento (prendere operandi dal sistema). Se io dovessi dividere i processori in 2 categorie in base alle istruzioni:
- CISC, l'insieme delle istruzioni e dei modi di indirizzamento è complesso
- RISC, l'insieme delle istruzioni e dei modi di indirizzamento è ridotto, allora riduco i modi di indirizzamento e quindi posso costruire dei processori veloci.
Allora, i modi di indirizzamento sono 4:
- Registri, gli operandi sono nei registri
- Immediato, l'operando è messo in un'istruzione
- Diretto, metto l'indirizzo dell'operando
- Indiretto, raggiungo l'operando mediante un puntatore
In assembler 68000
- MOVE.W D0,D1 = metti il valore di D0 in D1, dove d0 e d1 sono registri interni al processore e quindi l’operazione non riguarda la memoria
- MOVE.W #4,D4 = il cancelletto individua dal punto di vista sintattico che voglio mettere proprio 4 in d1, quindi è immediato
- MOVE.W VAR,D1 oppure MOVE.W 4,D1 = è diretto, perché VAR è un indirizzo di memoria, oppure 4 senza cancelletto è un indirizzo di memoria
- MOVE.W (A0),D1 = voglio andare in A0 che contiene un indirizzo, prendere il valore di quell’indirizzo e voglio metterlo in D1, e quindi si parla di modo di indirizzamento indiretto
In C
- Possiamo definire delle variabili di tipo register, non sempre può accadere e quindi l’assembler è più efficiente.
- e 3) le posso fare un po’ miste. Posso fare A=7 (immediato diretto) e A=B (diretto-indiretto).
- Ragionevolmente posso accedere a qualunque locazione di memoria con i puntatori.
Allora se RISC deve tenere meno roba e meno modi di indirizzamento, qual è tra questi modi di indirizzamento che posso non avere? Quello diretto, perché accedo in memoria. Se voglio accedere in memoria, dall’indiretto posso fare il diretto, al contrario dal diretto non posso fare l’indiretto. Se avessi solo il diretto non posso considerare che un puntatore si incrementa ogni volta. Questo modo fa pagare pegno quando dobbiamo caricare i dati ma ci consente di fare calcoli veloci.
Domanda
Come fai rapidamente un’equivalenza tra Motorola 68000 e un Intel? Come ti fai ad orientare? Le istruzioni di qualunque processore le posso classificare: trasferimento dati, logico/aritmetiche, operazioni di salto. Con queste operazioni, posso fare tutto quello esistente. Per fare un programma semplice devo allora avere l’equivalente di MOVE (trasferimento dati), ADD e CMP (logico/aritmetiche), JMP (salto incondizionato) BCC (salto condizionato).
Lezione 2 (12.03.2021)
Nel 68000 abbiamo registri di tipo dato (D0..D7) e registri che contengono indirizzi (A0..A7). Abbiamo presentato alcune istruzioni, abbiamo presentato alcuni registri. Questi registri che abbiamo presentato sono 32 bit, quelli del memory address è a 32 bit, il ms è a 16 bit, quindi il processore è a 16 bit: se gli operandi se sono di tipo registro, riesco ad impacchettare le istruzioni in 16 bit, altrimenti devo fare più accessi in memoria (5 nell’esempio).
Un altro concetto importante è l’ortogonalità: con “move” io ho tutti i modi possibili di indirizzamento, il che mi consente di sfruttare al meglio le potenzialità del mio sistema.
Cosa sono le direttive di assemblaggio e perché servono?
Se sto progettando un sistema, vorrei una possibilità che mi consenta di allocare i dati dove voglio, e quindi poter fare un disegno della memoria; questo disegno, non sono istruzioni, ma sono direttive per istanziare il programma come voglio. Se non conosco l’hardware non posso fare questa cosa, perché posso sì allocare in memoria, ma non posso allocare nei registri se non li conosco.
OSS: in C non ho una direttiva per poter dire al sistema di mettere il programma in una certa locazione: dichiaro i registri come costanti e poi uso queste costanti per accedere alle locazioni di memoria. “ORG” non è un’istruzione, è una direttiva che mi consente di partire da un indirizzo di riferimento. Con “DB.S” non inizializzo, sto dando solo una quantità di spazio; per inizializzare devo utilizzare “DB.C”.
L’algoritmo è indipendente dal linguaggio; infatti se noi prendiamo un costrutto di tipo for, ci consente di fare un costrutto iterativo noto il numero di iterazioni, se prendiamo il while è più importante del for perché ci consente di fare un numero di iterazioni non determinato e in realtà dal while posso costruirmi un for mentre non vale il viceversa. Un insieme funzionalmente completo, non è nemmeno il while, ma è il costrutto “if go to, go to”.
Al termine di questa catena, abbiamo il codice assembler: Con CLR D0 resetto D0. Poi che faccio? Utilizzo l’operazione di comparazione, CMP.W D1,D0 (supposto che l’operando sia in un registro, se non fosse stato così avrei dovuto scrivere #numero). A questo punto, posso usare un codice di salto, BGE (se è uguale o maggiore) facciamo una determinata cosa. Finita questa cosa, utilizzo un codice di addizione ADD.W #1,D0. Dopo di che, faccio un salto ad L1 BRA L1 (salto incondizionato. JMP cambia il valore del program counter; BRA invece non può saltare dove vuole, può saltare un po’ di locazioni prima o un po’ di locazioni dopo).
Il 68000 è un processore di tipo CISC, quindi se scrivo MOVE.W VAR1,VAR2 ha un problema, perché poiché l’istruzione è a lunghezza variabile, può prendere l’istruzione in più colpi. Allora abbiamo che l’istruzione complessiva è così: Per poter prendere l’istruzione deve prendere 1, 1+1, 1+1.
Esempio di utilizzo dei processori
Se prendo un processore a 64 bit, le istruzioni sono a lunghezza fissa, per fare questa codifica userei 64+16 bit. Dal punto di vista del compilatore è peggio, però viene fatto perché alcune istruzioni sono raramente frequenti. Le istruzioni più frequenti le rendo più veloci, quelle meno frequenti più lente. Chi ha ragione CISC o RISC? Dipende da quello che devo fare: se ho un ridotto set di istruzioni che uso sempre il RISC non mi serve; altrimenti il viceversa.
Il processore a 32 bit è vantaggioso se io scrivo MOVE.L VAR1,VAR2, devo fare 5 accessi; quest’operazione mi consente di fare VAR1->PC, PC->VAR2. Se voglio fare questa operazione su una LONG (.L), per prendere un dato ci mettiamo 2 (per VAR1) +2 (per VAR2); invece su 32 bit ci mettiamo 1+1. Se prendo l’istruzione MOVE.L (A0),(A1), per fare il prelievo quanti accessi devo fare? (40). Facciamo un esempio. Utilizzando i puntatori, voglio fare l’operazione che in C si farebbe VAR1[0]= VAR2[0], VAR1[1]=VAR2[1] con i puntatori, e lo voglio fare in assembler.
Modo di indirizzamento diretto
- ASSEMBLER: Mi sposto di “2” perché .W occupa 2 spazi. Se fosse stato un byte sarebbe stato 1, se fosse stato una long word sarebbe stato 4. Il #VAR2 significa prendi VAR2 come indirizzo e sommaci 2.
Modo di indirizzamento indiretto
- C
- ASSEMBLER: Se dovessi operare su tutti gli elementi del vettore, sarebbe preferibile il secondo modo, perché per il primo modo ogni volta dovrei scrivere un’istruzione, mentre per il secondo modo posso scrivere un ciclo per copia e incremento.
Il modo di indirizzamento indiretto serve a fare iterazioni. Supponiamo che volevo copiare quello che c’era nella locazione di memoria 9000 e lo volevo mettere in 1000, perché da 1000 ho la memoria video (è una rappresentazione del problema di sopra). Supponiamo che dobbiamo copiare 4 cose. L’algoritmo va fatto 4 volte.
Devo usare delle cautele? Si. In C questa cosa non posso farlo, perché quando vado a fare P=&VAR1[0] mette P dove vuole il compilatore, quindi devo scrivere P=9000 e Q=1000. In questo modo ho tenuto conto dell’hardware. Quindi, se devo fare una cosa virtuale posso usare l’algoritmo di prima; se devo fare una cosa reale devo fare queste considerazioni.
Invece in assembler, questo lo posso fare in 2 modi: in questo modo ho detto che i due puntatori iniziano dove dico io. Questo sistema non è tanto pulito perché tra 10 anni non ricordo chi è 9000 e chi è 1000. Un metodo più pulito è questo: In questo modo se un giorno dovesse cambiare qualcosa, non devo cambiare tutto il codice, ma vado a cambiare i valori in ORG. La stessa cosa la posso fare in C, andando a dichiarare delle costanti prima di dichiarare P e Q. In C senza i puntatori potrei caricare il valore dove voglio? No, perché senza i puntatori non posso dire quelle variabili dove sono, ma sceglie il compilatore.
Concetto di chiamata a sottoprogramma
Vogliamo adesso comprendere meglio il concetto di chiamata a sottoprogramma. La chiamata a sottoprogramma serve a decomporre sistemi più complessi in problemi più semplici per poi utilizzare un mai che li riunisce (divide et impera). Questo è vero, e risulta essere ancora più vero quando pensiamo ad un sistema di architetture, perché posso pensare di far interagire un programma C con un programma di più basso livello. A questo punto, l’interazione viene gestita sostanzialmente dal fatto che lo scambio di informazioni tra programma chiamante e chiamato deve avvenire secondo una logica predefinita (che noi non vediamo perché utilizziamo librerie); se noi dovessimo scrivere invece tutto noi, avremmo un programma di collegamento tra basso ed alto livello.
Lo stack risulta essere la modalità con cui la modalità di scambio di parametri tra un programma a basso e uno ad alto livello; lo stack altro non è che memoria, allora in una certa area di memoria devo mettere dei parametri in un certo modo. Allora, sappiamo che esiste una differenza tra un salto, un salto condizionato e una chiamata a sottoprogramma. Nel caso di chiamata a sottoprogramma (JSR un sotto programma che spiazza rispetto a qualcosa lontano, BSR un sotto programma che spiazza rispetto a qualcosa vicino) non solo i parametri vengono messi nello stack ma c’è anche la necessità di mettere nello stack l’indirizzo di ritorno che viene messo automaticamente dall’esecuzione del codice operativo.
Per fare questo, supponiamo che in un certo sistema ci siano A e B che devono essere concatenati in C. OSS: tutte le strutture dati vengono passate per indirizzo, mentre le variabili possono essere copiate per valore o per indirizzo. L’errore che fanno tutti è quello di non disegnare lo stack, cosa che è importante in quanto rappresenta la convenzione tra programma chiamato e programma chiamante. Devo andare a capire st STACK quanto è grande =
- Segniamo l’indirizzo di ritorno in ROSSO, che ci deve stare per forza.
- Segniamo gli altri dati: A è un indirizzo, quindi 4 bit; dim di A è un valore quindi mi servono 2 locazioni; B ancora 4; dim di B ancora 2; C ancora 4; dim di C, poiché è un parametro di uscita, gli mettiamo 4 perché possiamo scambiarlo anche per indirizzo.
Proviamo a fare una dichiarazione per ricordare i codici: OSS: il “-7” verrà fatto in complemento alla base OSS: se dichiaro N come costante, la dichiaro “EQU”, quindi come direttiva di assemblaggio OSS: per C posso fare N*2 perché N è 4, oppure posso scrivere 8 OSS: per dimC metto dimensione 1 perché è una sola variabile. Adesso se voglio caricare questi valori come il disegno in alto: A questo punto posso saltare alla mia subroutine e scrivere JSR 1. Ora faccio ADDA #20,A7 (se vado a contare le dimensioni dello stack disegnato ottengo che A7 è arrivato alla posizione 20, per riportarlo sotto, aggiungo l’istruzione ADDA #20,A7) A questo punto cosa devo fare? Devo caricare 4 registri e 2 valori dato. Una cosa la metto in A0, una in D0, una in A1, una in D1, una in A2 e una in A3. “Mettere” significa fare codice di MOVE. Devo mettere dei valori, come lo faccio? Prendi quello che è puntato da A7 sempre, però devi giustamente spiazzare rispetto ad A7 qualcoosa.
Se A7 è il primo, per prendere l’indirizzo di A cosa devi fare? Ci devi mettere un 18(A7) (modo di indirizzamento base+indice); per prendere D0 deve prendere 16, A1 deve mettere 14, D1 con 12, A2 con 8 e A3 con 4. Poiché a noi i numeri nel programma non ci piacciono, lo scriviamo in un altro modo. Ci scriviamo che c’è un offeset di A che è ufuale a 18 (OA EQU 18), così che posso scrivere MOVEA.L OA(A7). Il risultato è lo stesso però ho il vantaggio di utilizzare un valore simbolico.
Lezione 3 (15.03.2021)
La lezione della volta scorsa, era tutta centrata sul tema “memoria”, perché tutto quello che abbiamo detto riguardava il prendere e spostati dati nella memoria. Avevamo parlato dei sottoprogrammi: o puntiamo al registro A7 che punta allo stack, o decrementiamo il puntatore A7, o determiniamo degli offset “OA” per poi fare le operazioni.
La prima cosa che dobbiamo notare è che nel 68000 qualunque registro A0,..A6,A7 si possono utilizzare i pre e i post incremento: in realtà, quando facciamo la chiamata a sottoprogramma, l’indirizzo di ritorno è A7. La seconda cosa è che le strutture a stack ci consentono di gestire le variabili dinamiche. La movimentazione della memoria è uno degli elementi base dei sistemi operativi: supponiamo che io voglia caricare un programma dal disco in memoria. Per farlo, quali sono le operazioni che devo fare? Il disco è un’unità esterna e quindi per poterci interagire dovrò usare un’interfaccia di I/O; quando prelevo i dati dall’unità esterna, poi li devo copiare all’interno della memoria, quindi quello che succede nella pratica è che si va a copiare un vettore in memoria.
Se io voglio costruire un programma che si attiva in modo “remote procedure call”: ho due unità, una che manda un messaggio all’altra, ma questo messaggio che cosa è? È un’applicazione. Questa copia in memoria è alla base dei meccanismi di sicurezza: se siamo in grado di operare con la memoria sappiamo fare tutto.
Vediamo ora 4 esempi:
- Operazioni sui bit
- Operazioni su stringhe
- Operazioni su matrici (tabelle)
- Operazioni su code
OSS: Nei casi 2,3 e 4 abbiamo insiemi di byte, mentre nel 1 caso abbiamo bit. Vediamo le operazioni sui bit. Voglio contare i bit alti in una word. Questo mi può servire per fare cosa? Se ho un...
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
-
Appunti di Calcolatori elettronici
-
Appunti Calcolatori Elettronici
-
Appunti Informatica
-
Appunti lezione Calcolatori elettronici