T&t - PIC 

 

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  7hFh

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.

 


 

 

Copyright © afg. Tutti i diritti riservati.
Aggiornato il 30/07/14.