Dichiarazione della RAM in
Asssembly
|
Come effettuare al dichiarazione della RAM in
Assembly
Quando il programma richiede l' uso di RAM, occorre definirla prima di
poterla utilizzare.
"Definire" vuol dire assegnare una label alle locazioni che si
utilizzeranno.
EQU
Un modo molto semplice è
quello di dichiarare un "blocco" di RAM da usare una equivalenza: sappiamo che la RAM disponibile in
quel dato chip parte da uno specifico indirizzo; quindi,
con la direttiva EQU (equate,
=) assegniamo le label a successivi indirizzi di memoria. Ad esempio, per un
12F519, l' area RAM inizia all' indirizzo 07h; volendo riservare tre
locazioni, possiamo agire così:
####################################################################
;====================================================================
;=
MEMORIA RAM
=
;====================================================================
; general purpose RAM 12F519
d1 EQU 0x07
; contatori per ritardo
d2 EQU 0x08
d3 EQU 0x09 |
In questo modo dichiariamo semplicemente che le tre label sono equivalenti per il
compilatore ai tre valori numerici:
d1 = 0x07
d2 = 0x08
d3 = 0x09
Attenzione: valori numerici, non indirizzi. L' uso che poi faremo di queste
label ne definirà lo scopo. Quindi è possibile:
movlw
d3 ;
W = 09h
movwf d2
; copia W in RAM all' indirizzo 08h
bsf
PORTB,d1 ; bit 7 di PORTB = 1 |
Questo modo è molto semplice, ma non è particolarmente consigliabile a meno avere la necessità
di conoscere o fissare con immediata certezza la corrispondenza tra label e
indirizzo.
Infatti, osservando le caratteristiche di diversi componenti la famiglia
Baseline, troviamo che l' indirizzo di partenza dell' area dati è variabile
a seconda del modello di chip.
Occorre quindi, per ognuno, conoscere dove è allocata la RAM. Ad esempio, per
un 16F628, in cui la memoria dati utilizzabile inizia a 20h, dovrò modificare
così:
####################################################################
;====================================================================
;=
MEMORIA RAM
=
;====================================================================
; general purpose RAM 16F628
d1 EQU 0x20
; contatori per ritardo
d2 EQU 0x21
d3 EQU 0x22 |
Osserviamo che occorre modificare TUTTE le linee di definizione delle label;
e cambiando processore, si dovrà probabilmente riscrivere il tutto. L' equivalenza diretta con valori assoluti fa si che il codice risultate
sia minimamente portabile.
CBLOCK - Codice assoluto
Esiste una soluzione: l' uso della direttiva CBLOCK.
La sintassi di CBLOCK
è semplice:
CBLOCK indirizzo
<label>[:<incremento>][,<label>][,<label>]
<;commento eventuale>
ENDC
L' oggetto di CBLOCK è
l' indirizzo di memoria da cui partire, in questo caso 07h, dove inizia la RAM
dati.
CBLOCK 0x07
; inizio area RAM |
Le label potranno essere assegnate a righe singole oppure sulla stessa
linea, separate da virgole.
Esaurite le dichiarazioni, occorre chiudere il blocco, con la direttiva ENDC.
Se dimentichiamo questo particolare, il compilatore genererà un errore (e
probabilmente molti errori successivi) dato che non è in grado di definire
dove il blocco viene terminato. Questo errore impedisce la conclusione
corretta della compilazione.
Ad esempio:
CBLOCK 0x07
; inizio area RAM 12F519
d1, d2, d3 ;
3 contatori per ritardo
memA:3
; buffer 3 bytes
sGPIO
; byte shadow
ENDC |
Come nel caso precedente, occorre conoscere dove allocare il blocco della RAM
dati, per cui, per 16F628:
CBLOCK 0x20
; inizio area RAM 16F628 |
Rispetto alla modalità precedente, dove, cambiando indirizzo di inizio RAM
occorre riscrivere ogni singolo equate, qui basterà cambiare unicamente l'
indirizzo di inizio blocco.
All' interno del blocco, dichiariamo le label delle locazioni di RAM da utilizzare; l' algoritmo
scelto ne richiede tre, denominate d1, d2 e d3. Potremo benissimo
utilizzare altre label di nostro gradimento; quello che importa è comprendere
il meccanismo: la direttiva indica al compilatore che esiste questa relazione
tra label e indirizzo nella mappa della RAM dati:
Label |
Indirizzo
in RAM |
|
Label |
Indirizzo
in RAM |
d1 |
07h |
|
memA |
0Ah |
d2 |
08h |
|
|
0Bh, 0Ch |
d3 |
09h |
|
sGPIO |
0Dh |
Osserviamo che se alla label non viene assegnato un incremento, essa riserva una sola locazione.
Però, se occorre, la label può essere assegnata ad un gruppo di locazioni.
Ad esempio:
CBLOCK 0x07
memA:3
ENDC
assegna alla label memA tre locazioni successive a partire
dalla posizione in cui si trova nella lista. Da notare che in questo caso l'
equivalenza simbolica è:
Label |
Indirizzo
in RAM |
memA |
0Ah |
memA+1 |
0Bh |
memA+2 |
0Ch |
ovvero delle tre locazioni solamente la prima ha un vero e proprio
"nome", mentre le successive, se devono essere identificate, usano
la label della prima e l' indicazione +n a seconda della loro
posizione.
Non c'è una regola particolare per usare uno o l' altro modo; anche
assegnando label ad ogni locazione non si creano problemi al compilatore, che,
comunque, lavora a velocità dipendenti dalle capacità del personal computer; l' aumento degli elementi da trattare non penalizza sensibilmente il
tempo di compilazione.
Per contro, l' uso di una sola label per un gruppo di locazioni potrà essere
vantaggioso quando il gruppo costituisce un unicum rispetto ad una certa
funzione, ad esempio un buffer circolare, dove non ha alcuna importanza dare
un nome proprio a ciascuno degli elementi che lo compongono.
La direttiva CBLOCK:
- non va posta in prima colonna del
testo.
- deve essere chiusa da ENDC
- può comprendere più righe, composte dalle label e da una indicazione
di quante locazioni occupano. Se questo non è indicato, il valore è 1.
Da notare che le label definite nel blocco non iniziano in prima colonna,
anche si si tratta di una definizione, dato che essa dipende dalla direttiva. Esistono, comunque,
altri modi per assegnare la RAM, che vedremo in seguito.
La sintassi di CBLOCK
è analoga a quella di ORG.
La differenza di uso è sostanziale:
- CBLOCK fa
riferimento all' area RAM dati, dove determina un blocco di
definizioni e richiede ENDC alla fine
del blocco
- ORG fa riferimento
alla memoria programma e determina a quale indirizzo si posiziona l'
istruzione successivamente indicata. Non richiede, quindi, una
"chiusura".
Se la sua implementazione è semplice e abbastanza comprensibile a prima
vista, essa nasconde sempre la necessità di conoscere l'indirizzo di partenza
dell' area dati, che è variabile a seconda delle risorse del chip.
La cosa ci costringe, agendo nei modi "manuali" fino ad ora
visti, a conoscere questi dati e a trovarci nella situazione di scrivere un
sorgente che può essere limitato ad un solo tipo di microcontroller, mentre
potrebbe essere più generalizzato. Esiste un modo per ovviare a
questo?
UDATA - Codice rilocabile
Se andiamo a visionare, ad esempio, il 12F519_g.lkr (che si
trova C:\Programmi\Microchip\MPASM Suite\LKR\) troviamo elencate alcune
equivalenze che riguardano al memoria:
DATABANK NAME=sfr0
START=0x0 END=0x6
DATABANK NAME=sfr1 START=0x20 END=0x26
DATABANK NAME=gpr0 START=0x10 END=0x1F
SHAREBANK NAME=gprnobnk START=0x7 END=0xF
In questi riconosciamo la mappa della memoria RAM del chip, ovvero:
- SFR nel banco 0 tra 0x00 e 0x06, replicati come alias nel banco 1 tra
0x20 e 0x26
- la RAM general purpose tra 0x10 e 0x1F
- l' area di RAM condivisa tra i banchi da 0x07 a 0x0F e replicata in
alias tra 0x27 e 0x2F
Per l' Assembler queste aree di memoria RAM sono individuate da due
direttive:
Nome |
Acronimo
di |
Area |
UDATA |
User
DATA RAM |
da 10h a 1Fh
e da 20h a 2Fh |
UDATA_SHR |
User
DATA RAM SHaRed |
da 7h a
Fh |
Queste informazioni sono personalizzate per ogni chip: ad esempio, 12F683_g.lkr
elenca le risorse relative a questo PIC e che sono significativamente
differenti da quelle di 12F519:
DATABANK NAME=sfr0
START=0x0 END=0x1F
DATABANK NAME=sfr1 START=0x80 END=0x9F
DATABANK NAME=gpr0 START=0x20 END=0x6F
SHAREBANK NAME=gprnobnk START=0x70 END=0x7F
Questo significa che il compilatore ha a disposizione le
informazioni necessarie per operare autonomamente. Posso allora fargli
definire in modo automatico l' allocazione della RAM.
La sintassi è semplice:
[<label>]
UDATA [<indirizzo>]
[<;commento eventuale>]
<label> res
<quantità> [
<;commento eventuale>]
La <label> iniziale è opzionale:
si utilizza se esistono più sezioni con la direttiva.
Se è specificato un indirizzo, l'area di RAM definita sarà iniziata con
quello; altrimenti inizierà nella prima locazione disponibile.
Le locazioni sono specificate con la relativa label e la direttiva res
a cui va applicato il numero di locazioni impegnate. Così,
ad esempio:
####################################################################
;===================================================================
;=
MEMORIA RAM
=
;===================================================================
; general purpose RAM per 12F519 a partire da 07h
UDATA
d1 res 1
d2 res 1
d3 res 1 |
UDATA
stabilisce l' indirizzo a cui far corrispondere le label successive e
la direttiva res
(reserve - riserva) attribuisce la quantità indicata alle label.
Otteniamo così lo stesso effetto dei metodi precedenti, ma senza la necessità
di conoscere il punto di start della memoria RAM dati: esso verrà derivato
dalle informazione che l' aver definito un chip su cui lavorare fornisce
automaticamente all' Assembler.
Da notare che la direttiva non deve iniziare in prima colonna, mentre le
label, venendo dichiarate per la prima volta, devono iniziare in prima
colonna.
Rispetto al CBLOCK
o agli EQU , UDATA
è un ulteriore passo di allontanamento dai valori assoluti delle
risorse del chip. Infatti, se usassi la direttiva con un altro PIC, essa
adatterebbe automaticamente l' indirizzo di partenza del blocco di RAM
dati in base alle informazioni fornite dal file .lkr. Così si avrebbe, ad
esempio:
UDATA
d1 res 1
d2 res 1
d3 res 2 |
|
PIC12F519 |
PIC16F84 |
PIC12F683 |
d1 |
07h |
0Ch |
20h |
d2 |
08h |
0Dh |
21h |
d3 |
09h |
0Eh |
22h |
d3+1 |
0Ah |
0Fh |
23h |
Questa via è assai pratica, anche perchè, utilizzata con una
programmazione modulare, consente di dichiarare locazioni di RAM dati non solo
nel sorgente, ma anche nei moduli che vengono inclusi.
Per contro, non permette di conoscere dalla lettura del sorgente la
corrispondenza tra label e indirizzo (come ad esempio nel caso degli EQU).
Negli esempi che si incontrano sul WEB, non è molto comune l' uso di UDATA,
in quanto si tratta quasi sempre di esempio di programmazione dedicata
specificamente alla realizzazione di un certo dispositivo e che non sono
previsti per essere generalizzati, anche se molto spesso il passaggio da un
microcontroller ad un altro, nell' ambito della stessa famiglia, è una
operazione molto semplice.
Sicuramente, una scrittura mirata ad ottenere un determinato risultato e
nient' altro, potrà essere molto ottimizzata in quanto a numero di
istruzioni, ma, per contro, non darà origine a nulla di più che quella certa
applicazione e molto probabilmente richiederà più tempo per la sua stesura
che se si fosse partiti da una concezione modulare.
Modularità vuol dire comporre il programma utilizzando quanto possibile
automatismi del linguaggio e riutilizzando codici generalizzati. Questo metodo
avvicina molto l' Assembly ai linguaggi superiori e consente una maggiore
rapidità di scrittura e minori problemi nel debug. Ovviamente questo
incide in modo essenziale su programmi di una certa dimensione, mentre su
sorgenti di poche righe può essere poco rilevante. Però, anche nel piccolo,
il mettere in opera questi meccanismi è una via per facilitarsi il lavoro di
programmazione.
Va notato che la direttiva UDATA
può essere utilizzata anche all' interno di un codice assoluto.
UDATA_SHR
Nella creazione di un codice rilocabile, si possono utilizzare altre
direttive in relazione all' assegnazione della RAM:
- UDATA_SHR
- UDATA_OVR
- IDATA
UDATA_SHR
ha lo scopo di indirizzare quell' area di RAM dati che si trova in comune su
tutti i banchi (shared). Quindi, ad esempio per 16F628, l' area tra 70h e 7Fh
è accessibile nei 4 banchi. Per utilizzarla:
; shared
purpose RAM per 16F628 70h-7Fh
UDATA_SHR
saveW res 1
; 70h
saveS res 1
; 71h
saveR res 3
; 72-73-74h |
UDATA_OVR ha lo scopo di indirizzare un' area RAM, come , ma
per labe che sono dichiarate allo steso indirizzo in punti diversi del
sorgente o in moduli diversi. Un tipico uso è quello di buffer temporanei.
Queste direttive collegano locazione RAM e label, ma non variano il
contenuto della memoria. Nel caso in cui sia richiesta un a inizializzazione,
si ricorrerà a IDATA.
Questa direttiva riserva memoria e nello stesso tempo la carica con i valori
che vengono indicati. La forma è quella tipica delle tabelle, con DB,
DW e DATA:
IDATA
Bytes DB
1,2,3,4
; singoli bytes
Words DW
0x1234, H'5678' ; word= 2
bytes, litte
endian
String DATA "Hello",0
; text string |
Che, in memoria, originano:
IDATA
0000 01 02 03 04 Bytes DB
1,2,3,4
; singoli bytes
0004 34 12 78 56 Words DW
0x1234, H'5678' ; word litte endian
0008 48 65 6C 6C String DATA
"Hello",0 ;
text string
000C 6F 00 |
Banchi
Nella creazione di un codice rilocabili il problema essenziale deriva dal
fatto che non è possibile sapere a priori dove il compilatore allocherà le
risorse di RAM (e anche quelle di memoria programma).
In un programma semplice, si potrà dedurre questo dato dal fatto che ad una
direttiva generica UDATA viene
dato come indirizzo di partenza quello di inizio delle risorse RAM disponibili; se un
solo modulo concorre alla compilazione, il suo indirizzo di partenza, se non
specificato, corrisponderà all' inizio della RAM dati e sarà possibile definire
rapidamente se si sta superando o meno lo spazio del banco 0. In programmi di
una certa complessità, dove concorrono più moduli, questo potrà essere impossibile:
ad esempio, in una compilazione costituita da 4, 5 o più moduli, ognuno dei
quali impegna una certa quantità di RAM, non posso saper a priori dove si
troverà la label d3, richiamata dal modulo
x, se non consultando il
file .map che risulta dalla compilazione.
Fino a che le locazioni di RAM sono contenute nel solo banco 0 (che attivo di
default al POR), il problema di commutare i banchi non sussiste, ma se la richiesta
di RAM supera l' ampiezza del banco, e questo è facile nei
piccoli PIC con poca memoria dati, occorre disporre di un comando per passare
agli altri banchi.
Questo è possibile attraverso istruzioni bsf/bcf che agiscano su bit che
commutano i banchi.
Il problema è che si tratta di un modo "non rilocabile", nel senso
che le varie famiglie di PIC dispongono di differenti registri per la
commutazione dei banchi:
- FSR per i Baseline
- Status per i Midrange
- BSR per gli Enhanced
Inoltre, a seconda del numero di banchi, occorrono da 1 a 4 bit per la
commutazione. Diventa ancora una volta necessario conoscere le caratteristiche
del PIC ed una azione "non rilocabile" sarebbe relativa solamente
quel determinato tipo di chip.
Esiste, però un aiuto da parte del compilatore, attraverso la direttiva banksel.
Questa commuta il banco in relazione all' oggetto indicato. La sua sintassi
è
banksel <label>
[<;commento eventuale>]
La label deve essere stata definita
prima e non può essere costituita da una
operazione; può, però, avere anche un valore numerico. Ad esempio:
banksel 0
; commuta sul banco 0
banksel TRISA
; commuta sul banco in cui sta TRISA |
I vantaggi sono molteplici; la direttiva sceglie in modo appropriato i bit
su cui agire:
- in base alla famiglia del microcontroller. Quindi, per un Basseline
agirà su FSR, per un Mid sullo Status, per un Enhanced con le
istruzioni MOVLB o MOVLR
- in base alla posizione della label, per cui non sarà necessario
conoscere dove si trova un determinato registro, dato che il compilatore
provvederà automaticamente ad indentificarne la posizione
Quindi, un acceso ad una locazione di RAM sarà del tipo:
banksel d3
; commuta sul banco in cui sta d3
movlw
0xAA ;
W = AAh
movlw
d3 ;
d3 = AAh
banksel d2
; commuta sul banco in cui sta d2
movlw
0x55 ;
W = 55h
movlw
d2 ;
d3 = 55h |
Si può certo obbiettare che l' uso intensivo di banksel
penalizza il codice ottenuto, sia per quanto riguarda l'
impegno della memoria programma, sia per quello che riguarda il tempo di
esecuzione. Però si tratta di un prezzo che va pagato per avere in cambio la
possibilità di realizzare codici modulari di alta complessità. Nella
pratica, è ben difficile che i pochi bytes occupati dalle istruzioni di
commutazione dei banchi influenzino in maniera negativa il funzionamento del
programma. Dove occorra una estrema ottimizzazione delle risorse o della
velocità di esecuzione si ricorrerà ad altre soluzioni, come ad esempio l'
allocazione della RAM necessaria alle funzioni critiche in uno spazio
determinato. Per codici semplici, resta sempre la possibilità di intervenire
a monte della prima compilazione eliminando le commutazioni di banco non
necessarie.
Va ricordato che l' area RAM condivisa, accessibile con UDATA_SHR,
non richiede commutazione dei banchi.
|