Corso di microprogrammazione: il primo microcontrollore

codice

Entriamo sempre più nel vivo del nostro corso, quest'oggi, analizzando nel dettaglio l'architettura del nostro primo microcontrollore. Che c'è dentro? Come funziona? Come si usa? A queste e ad altre domande iniziamo a rispondere da oggi in poi. Siete pronti?

Oggi continuiamo a rendere il nostro corso ancora più pratico, sempre più manuale e meno teorico mettendo finalmente il naso dentro l'architettura di un microcontrollore il cui scopo è prevalentemente didattico.
Se dovessimo cercare un modo per descriverlo sicuramente quello migliore sarebbe "una strana macchina sequenziale" e per capire meglio questa definizione, analizziamo il seguente diagramma a blocchi:

il diagramma risulta di per sé esplicativo, dal momento che all'interno di ciascun blocco costitutivo è presente la sua descrizione. Le interconnessioni che vengono dimostrate servono per evidenziare tutti i segnali fondamentali che, col tempo, anche nel corso di puntate successive a questa, analizzeremo e studieremo nel dettaglio.
Supponiamo di avere a disposizione una serie di componenti che possiamo descrivere così:

  • un circuito di clock a frequenza nota (diciamo f);
  • un sequencer, capace di generare (a partire dal clock d'ingresso) otto uscite della cui tempo rilevazione viene mostrato nella figura che segue
  • un contatore modulo 256 (quindi ad 8 bit, e con valori da 0 a 255), con funzione di reset, precaricabile, incrementabile (Program Counter);
  • una memoria EPROM da 256 parole a 13 bit (Program Memory);
  • una logica di controllo (control logic), alla quale arrivano in ingresso le linee D8-D12 dalla EPROM, le uscite del sequencer, i flag dell'ALU;
  • un registro ad 8 bit (Numeric N), il cui scopo è mantenere l’informazione numerica del OpCode;
  • un registro ad 8 bit (Accumulator A), che viene utilizzato come registro comune per diverse operazioni;
  • un registro ad 8 bit (Temporary T), utilizzato principalmente per la memorizzazione temporanea di uno dei due termini di una operazione sulla ALU;
  • un registro ad 8 bit (Result R), il quale immagazzina il risultato della ALU;
  • un registro ad 2 bit (Flag F), che memorizza i flag della ALU;
  • un registro ad 8 bit (PCLatch L), che serve per memorizzare il passo di programma attualmente in esecuzione;
  • una SRAM da 256 parole ad 8 bit, utilizzato come area di immagazzinamento temporaneo;
  • un contatore modulo 256 di tipo Up/Down con funzione di reset;
  • una SRAM da 256 parole ad 8 bit, utilizzato come area di stack;
  • un dispositivo ALU (acronimo di Aritmetic Logic Unit).

Quest'ultimo, come sappiamo, rappresenta il vero e proprio cuore pulsante dell'intera struttura, dal momento che si tratta del dispositivo deputato ad effettuare le operazioni sia aritmetiche sia logiche. Pertanto, è sua caratteristica quella di poter eseguire le seguenti operazioni:

  • Somma (Add);
  • Sottrazione (Sub);
  • Comparazione (Cmp);
  • Not (Not);
  • And (And);
  • Or (Or);
  • Exclusive Or (XOr);
  • Shift a sinistra (Shl);
  • Shift a destra (Shr).

Vengono resi disponibili i seguenti dati:

  • risultato;
  • flag di Zero (pari ad uno se il risultato è nullo);
  • flag di Carry (pari ad uno se il risultato genera il riporto).

La EPROM gestisce parole con una profondità di 13 bit; di questi, 5 sono utilizzati per definire il tipo di operazione (Opcode) mentre i restanti 8 possono essere utilizzati per indicare un indirizzo di memoria oppure un passo di programma. Da questo discende che possiamo facilmente schematizzare la codifica di una generica istruzione così come segue:

ISTRUZIONE

    12  8        7      1

OPCODE

DATO / INDIRIZZO

Quando il sistema inizia a lavorare, tutti i registri non contengono alcun tipo di istruzione o valore (reset); questo fa sì che il PC punti alla locazione zero della Program Memory. Pertanto il dato presente nell'allocazione della memoria Program Memory a cui punta il PC viene decodificato dalla control logic, in base alla decodifica che viene effettuata, vengono attivati i segnali di sistema. Una volta eseguita l'istruzione, viene calcolato l'indirizzo della nuova istruzione, quella che segue. Questo processo diventa ciclico e vale per ogni istruzione.
Dalla tabella del sequencer si evince chiaramente che, dato un clock di frequenza assegnata (f), la frequenza di esecuzione sarà pari ad f/4; pertanto, se il clock è di 4 MHz, il programma verrà certamente è seguito ad 1 MHz. Naturalmente questo vuol dire che il tempo di esecuzione della singola istruzione (di ogni singolo istruzione) è pari ad 1 us.

Una breve notazione sui nomi dei segnali utilizzati nel diagramma; vengono impiegate tra le lettere, la prima indica il tipo d'operazione (L = caricamento o scrittura, R = lettura, I = incremento unitario, D = decremento unitario), la seconda il nome del dispositivo così come indicato nella precedente lista e la terza il tipo di dispositivo (che può essere un R = registro, C = contatore, P = pointer).

La struttura del sistema permette diverso tipo di operazioni; in particolare è possibile eseguire operazioni di trasferimento dei dati sia in uscita sia in ingresso rispetto ad una memoria di riferimento e, più in generale, rispetto al mondo esterno. Possono essere eseguite quelle operazioni logico aritmetiche di cui abbiamo parlato prima ma anche esecuzione condizionale del flusso di un programma. È inoltre possibile organizzare l'esecuzione di un software tramite l'utilizzo di subroutines.

Instruction Set

Come, naturalmente, tutti sappiamo, non sarebbe possibile eseguire alcun tipo di operazione se non ci fosse un elenco di istruzioni, un set di comandi, che scandisce da un lato i tempi dall'altro la logica sequenza delle operazioni da eseguire. Ecco perché, nella prossima tabella, che indicheremo le operazioni che il sistema deve effettuare per ciascun istruzione possibile.

MNEMONIC

OPCODE

13 Bit Opcode

DESCRIZIONE

FLAG

LOAD A,#N

00

0 0000 nnnn nnnn

#N A ; PC + 1 PC

LOAD A,@M

01

0 0001 mmmm mmmm

@M A ; PC + 1 PC

LOAD @M,A

02

0 0010 mmmm mmmm

A @M ; PC + 1 PC

ADD A,#N

03

0 0011 nnnn nnnn

A + #N A ; PC + 1 PC

Z,C

SUB A,#N

04

0 0100 nnnn nnnn

A - #N A ; PC + 1 PC

Z,C

CMP A,#N

05

0 0101 nnnn nnnn

? A = #N : Z = 1, Z = 0; PC + 1 PC

Z

NOT A

06

0 0110 XXXX XXXX

NOT A A ; PC + 1 PC

Z

AND A,#N

07

0 0111 nnnn nnnn

A AND #N A ; PC + 1 PC

Z

OR A,#N

08

0 1000 nnnn nnnn

A OR #N A ; PC + 1 PC

C

XOR A,#N

09

0 1001 nnnn nnnn

A XOR #N A ; PC + 1 PC

Z

SHR A,#N

10

0 1010 nnnn nnnn

A >> #N A ; PC + 1 PC

C

SHL A,#N

11

0 1011 nnnn nnnn

A << #N A ; PC + 1 PC

C

ADD A,#M

12

0 1100 mmmm mmmm

A + #M A ; PC + 1 PC

Z,C

SUB A,#M

13

0 1101 mmmm mmmm

A - #M A ; PC + 1 PC

Z,C

CMP A,#M

14

0 1110 mmmm mmmm

? A = #M : Z=1 ; PC + 1 PC

Z

AND A,#M

15

0 1111 mmmm mmmm

A AND #M A ; PC + 1 PC

Z

OR A,#M

16

1 0000 mmmm mmmm

A OR #M A ; PC + 1 PC

C

XOR A,#M

17

1 0001 mmmm mmmm

A XOR #M A ; PC + 1 PC

Z

SHR A,#M

18

1 0010 mmmm mmmm

A >> #M A ; PC + 1 PC

C

SHL A,#M

19

1 0011 mmmm mmmm

A << #M A ; PC + 1 PC

C

JMP #N

20

1 0100 nnnn nnnn

#N PC

JMP Z,#N

21

1 0101 nnnn nnnn

? Z = 1 : #N PC, PC + 1 PC

JMP NZ,#N

22

1 0110 nnnn nnnn

? Z = 0 : #N PC, PC + 1 PC

JMP C,#N

23

1 0111 nnnn nnnn

? C = 1 : #N PC, PC + 1 PC

JMP NC,#N

24

1 1000 nnnn nnnn

? C = 0 : #N PC, PC + 1 PC

CALL #N

25

1 1001 nnnn nnnn

PC @SP ; SP + 1 SP; #N PC

RET

26

1 1010 XXXX XXXX

SP - 1 SP ; @SP PC

PUSH A

27

1 1011 XXXX XXXX

A @SP ; SP + 1 SP

POP A

28

1 1100 XXXX XXXX

SP - 1 SP ; @SP A

INP A

29

1 1101 XXXX XXXX

I A ; PC + 1 PC

OUT A

30

1 1110 XXXX XXXX

A O ; PC + 1 PC
NOP

31

1 1111 XXXX XXXX PC + 1 PC

vale la pena di sottolineare che è stata utilizzata la seguente notazione:

  • ZZZ X,Y indica una operazione in cui X è la destinazione, Y la sorgente;
  • #N indica un numero ad 8 bit, in notazione binaria nnnn nnnn;
  • @M indica il contenuto della locazione di memoria di indirizzo M in rappresentazione binaria mmmm mmmm;
  • X -> Y indica il trasferimento da X a Y;
  • << e >>  indicano, rispettivamente, lo scorrimento a sinistra e a destra;
  • ; indica un'azione indipendente dalla precedente;
  • ? C : V, F indica che se la condizione C è vera, viene eseguita l’operazione V, altrimenti la F.

La differenziazione delle istruzioni, quindi, viene effettuata sulla base dell'OpCode. Questi rappresentano i 5 bit più significativi e sarà la Control Logic a doverne decodificare il valore per poter attivare i dispositivi necessari proprio nella giusta sequenza per ottenere lo scopo.

Esaminiamo adesso l'istruzione LOAD A,@#N

naturalmente lo scopo di questo comando è quello di trasferire il dato numerico N all'interno della registro accumulatore. Quello che notiamo prima di tutto, dalla tabella delle istruzioni, è che il numero N è parte del codice dell'istruzione, esattamente corrispondente ai bit 0-7. Per renderli disponibili bisogna abilitare le uscite del Numeric Register, utilizzando il segnale RNR. Fatto questo, il dato sarà disponibile sul bus interno del sistema e pertanto potrà essere acquisito dall'accumulatore. Quando il bus diventa libero viene anche incrementato il valore del Program Counter, attivando IPC, che si è reso disponibile attivando RPC, e quindi memorizzato utilizzando LPL.
A questo punto il sistema è pronto per decodificare l'istruzione successiva.

In prima analisi, si può pensare ad un sistema formato sostanzialmente da due blocchi dei quali uno decodifica l'OpCode e l'altro sintetizza il singolo segnale del sistema.

Nell'esempio viene riportata la sintesi del segnale LAR, attivato in fasi differenti del sequencer a seconda dell'operazione che venga effettuata. Nello schema la porta Or dovrebbe avere, oltre ai due ingressi delle istituzioni di caricamento, tanti interessi quante sono le operazioni disponibili sull'ALU (indicate in sintesi con una generica decodifica OPX).

 

Di fatto, grazie alla flessibilità della logica programmabile, possiamo ridurre nello stesso blocco la decodifica del comando insieme con la sintesi del segnale.
Negli schemi,Op0 indica il bit 8 dell'istruzione e così Op1 il 9, Op2 il 10 fino ad Op4 che è il 12.

Questo approccio dimostra un vantaggio: una esecuzione più veloce a scapito, però, di un maggior numero di porte utilizzate.

Lo Stack

Lo stack è una forma di "organizzazione" dei dati che è basata sul metodo LIFO (Last In - First Out). Per spiegare questo metodo un esempio tipico che viene utilizzato: la pila dei piatti da lavare. Il primo viene posto sul tavolo, quindi il secondo sul primo e così via. Quando è necessario utilizzare un piatto, il primo che verrà certamente usato è l'ultimo che è stato messo in cima.
Lo stack è composto da due elementi:

  • l'area di stack, utilizzata per la memorizzazione dei dati;
  • lo Stack Pointer che, come suggerisce il nome, è un puntatore che indica la cima della pila, ovvero l'ultimo elemento che è entrato in memoria.

Le operazioni possibili sono due: inserimento (PUSH), ovvero la scrittura con il dato della locazione attualmente puntata dal puntatore, che successivamente viene incrementato, e la rimozione (POP), che naturalmente implica il decremento del puntatore.
Riportiamone un esempio giusto per chiarezza:

Ma soprattutto, riportiamo i diagrammi per l'esecuzione delle due istituzioni:

Alcuni esempi

Ancora, nonostante tutto, quello che abbiamo detto potrebbe sembrare di carattere un po' teorico. Per questo motivo, vediamo che cosa succede utilizzando un set di istruzioni, già illustrate, scrivendo un paio di programmi di prova.
Supponiamo di voler:

  1. scrivere 5 nella locazione di memoria 00;
  2. scrivere 1 nella locazione di memoria 01;
  3. sommare la locazione 00 alla locazione 01 e salvare il risultato nella locazione 02;
  4. se il contenuto della locazione 02 è 10, azzerare la locazione 02;
  5. ripetere il tutto dal passo due.

Per fare questo scriveremo:

;    Scrivere 5 nella locazione di memoria 00.
00    0005    LOAD    A,#5
01    0205    LOAD    @00,A
;    Scrivere 1 nella locazione di memoria 01.
02    0001    LOAD    A,01
03    0201    LOAD    @01,A
;    Sommare la locazione 00 alla locazione 01,
;    salvare il risultato nella locazione 02.
04    0100    LOAD    A,@00
05    0C01    ADD    A,@01
06    0202    LOAD    @02,A
;    Se il contenuto della locazione 02 è 10, azzerare la locazione 02.
07    050A    CMP    A,#0A
08    1602    JMP    NZ,02
09    0000    LOAD    A,#0
0A    0202    LOAD    @02,A
;    Ripetere dal passo 2.
0B    1402    JMP    02

Nel codice appena riportato possiamo facilmente notare alcune cose: la prima, e più evidente, è che il ";" indica i commenti al programma. Si vede poi che la prima colonna, a partire da sinistra, indica il numero di istruzioni del programma. La seconda colonna, sempre da sinistra, esprime l'OpCode dell'istruzione mentre le ultime due colonne portano il “source code”, o “assembly code”, ovvero il testo che indica l’operazione richiesta.
Quello che abbiamo visto è il list di un programma assemblato, ovvero già processato dal programma (assembler), il cui ruolo è quello di tradurre il file di testo scritto dal programmatore nel file binario che deve essere utilizzato per programmare la EPROM del microcontrollore.

Vediamo, allora, adesso come può essere fatto un source file:

CELLA_0    .EQU    @00
CELLA_1    .EQU    @01
CELLA_2    .EQU    @02

    .ORG     0
LOAD    A,#5
LOAD    CELLA_0,A
Loop    :
LOAD    A,#01
LOAD    CELLA_1,A
LOAD    A CELLA_0
ADD    A, CELLA_1
LOAD    CELLA_2,A
CMP    A,#0A
JMP    NZ,Loop
LOAD    A,#0
LOAD    CELLA_2,A
JMP    Loop

Il file contiene solo codice assembly e, come si vede, sono state utilizzate le direttive ".Equ" e ".ORG". La prima delle due definisce una macro e pertanto, ovunque l’assembler trovi la definizione CELLA_1, essa sarà rimpiazzata con @01. La seconda, invece, definisce la locazione di memoria del codice.
Viene utilizzata una label "Loop", che serve ad indicare dove seguire l'istruzione di "jump".

Dal momento che adesso sappiamo muoverci molto meglio e abbiamo capito diverse cose, proviamo a scrivere un nuovo codice. Questa volta, vogliamo:

  1. leggere dal registro di ingresso;
  2. complementare il dato letto;
  3. scrivere il registro in uscita con il dato letto una volta che è stato complementato;
  4. ripetere il tutto dal passo 1.

Come lo fareste? Che idee avete?

Ed ecco altre due possibilità: generate un'onda quadra in uscita sul bit 0 e poi, tanto per complicare (ma non di molto) le cose, provate a generare due onde quadre distinte complementari in uscita sul bit 0,1.

Saluti

Siamo ai saluti. Anche questa volta la nostra puntata giunge al termine. Come promesso, però, nel corso del tempo le domande aumentano, il corso si fa più specifico e soprattutto più specializzato. La prossima volta ci occuperemo di un sistema di calcolo Embedded, analizzandone l'architettura, la struttura di base e così via dicendo. Però prima, perché non provate a rispondere alle domande di cui sopra?

 

Quello che hai appena letto è un Articolo Premium reso disponibile affinché potessi valutare la qualità dei nostri contenuti!

 

Gli Articoli Tecnici Premium sono infatti riservati agli abbonati e vengono raccolti mensilmente nella nostra rivista digitale EOS-Book in PDF, ePub e mobi.
volantino eos-book1
Vorresti accedere a tutti gli altri Articoli Premium e fare il download degli EOS-Book? Allora valuta la possibilità di sottoscrivere un abbonamento a partire da € 2,95!
Scopri di più

6 Comments

  1. willygroup willygroup 30 luglio 2013
  2. Piero Boccadoro Piero Boccadoro 30 luglio 2013
  3. IvanScordato Ivan Scordato 30 luglio 2013
  4. delfino_curioso delfino_curioso 31 luglio 2013
  5. Piero Boccadoro Piero Boccadoro 1 agosto 2013
  6. Giorgio B. Giorgio B. 5 agosto 2013

Leave a Reply

Raspberry Pi 3 GRATIS! (Win10 compatibile)

Fai un abbonamento Platinum (EOS-Book + Firmware), ricevi in OMAGGIO la RASPBERRY 3, inviaci il tuo progetto e OTTIENI IL RIMBORSO