La memoria dati sui dsPIC : X, Y, DMA e PSV

I dsPIC sono MCU molto potenti, la loro potenza di calcolo è data soprattutto dal core DSP (Digital Signal Processing), ma come dice una certa pubblicità: la potenza è nulla senza controllo. Tutta la potenza di calcolo, difatti, sarebbe vana senza un’accurato sistema di controllo e gestione della memoria. Ed è della memoria dati (ovvero: la RAM) dei dsPIC che mi occuperò in questo articolo.

Alcune considerazioni che farò non valgono per i PIC24F e alcune non valgono nemmeno per i PIC24H, che, non avendo il core DSP, hanno una diversa gestione della memoria.

La memoria RAM sui dsPIC è suddivisa in più sezioni. Per capire bene diamo uno sguardo alla mappa della memoria del dsPIC33FJ128GP802, preso ad esempio:

Le prime 0x07FE locazioni di memoria (2Kb) sono occupate dagli SFR (Special Function Register: i registri di lavoro, i buffer di ricetrasmissione ecc). Segue quindi la memoria RAM a “nostra” disposizione suddivisa in 3 sezioni principali: X, Y e DMA più una quarta zona non implementata.

Memoria X e Y

Le zone di memoria X e Y, in realtà, vengono trattate dalle normali istruzioni CPU come un unico spazio continuo, per cui la suddivisione, relativamente ai normali set di istruzioni, non esiste. Le istruzioni che invece fanno uso del core DSP  (chiamate istruzioni MAC – MAC instructions) trattano questi due spazi separatamente. Il vantaggio è che queste due zone di RAM possono essere accessibili dal DSP attraverso due bus separati, il che si traduce in una riduzione dei tempi di calcolo dato che due dati su cui operare vengono prelevati in contemporanea.

Volendo fare uso dei due spazi separati (solo qualora stessimo utilizzando le funzioni del DSP) le variabili devono avere l’attributo space (che serve ad allocare le variabili in una determinata area di memoria) con l’indicazione della zona ram in cui salvarle, esempio:

int varx __attribute__ ((space(xmemory))); // alloca varx nella RAM X
int vary __attribute__ ((space(ymemory))); // allora vary nella RAM Y

Queste due istruzioni sono supportate solo dai dsPIC. I PIC24F e i PIC24H non avendo il core DSP non hanno la RAM sezionata in X e Y e quindi non supportano questi due attributi.

Il Direct Memory Access

Della zona Y fa parte una sotto-zona di memoria indicata come DMA che viene utilizzata dal controller DMA (Direct Memory Access). Grazie a tale funzionalità possiamo fare in modo che alcune periferiche siano in grado di accedere alla memoria RAM DMA in maniera autonoma ovvero senza il normale intervento della CPU. Questo si traduce nuovamente in una drastica riduzione dei tempi di esecuzione e del carico di lavoro della CPU.

Parafrasando il datasheet: il DMA rappresenta un meccanismo efficiente di copia dei dati dagli SFR delle periferiche (es.: registro di ricezione UART) alle variabili in RAM. Il controller DMA, in pratica, può copiare in automatico interi blocchi di dati sollevandoci dall’incombenza di andare a scrivere o leggere gli SFR delle periferiche interessate via software. Tale funzione viene assolta in automatico, una volta impostata ed abilitata, non appena si verifica un evento (determinato da un interrupt o da un timer).

Esempio: la UART riceve un dato, viene settato il flag di ricezione e il controller DMA copia il dato contenuto nel registro di ricezione nella variabile che abbiamo specificato.

Le variabili interessate a questo trasferimento (cioè quelle in cui copiare i dati) devono ovviamente risiedere nella zona ram dedicata al DMA. Questo viene fatto utilizzando come sempre l’attributo space:

unsigned int Buffer[8] __attribute__((space(dma)));

Questa istruzione è supportata solo dai dsPIC e dai PIC24H. I PIC24F non hanno il controller DMA.

Ovviamente se non viene sfruttato il DMA, tale zona RAM è usata normalmente come parte della zona Y.

Qualche cenno sul DMA

Non starò qui a spiegare nel dettaglio come si configura e utilizza il DMA. Per ora mi sono limitato a leggere qualcosa ma non ho ancora approfondito, per cui questa, come già detto, è solo una raccolta di appunti e per quanto sforzo possa aver fatto per cercare di capire bene tutto ed essere il più accurato possibile, nulla vieta che possano essere presenti degli errori.

Il DMA ha 8 canali indipendenti, il che significa che il controller DMA può gestire il trasferimento diretto per 8 registri. Le periferiche che possono fare uso del DMA sono: ECAN, DCI (Data Converter Interface), ADC, SPI, UART, IC (Input Capture), OC (Output Compare).

Il DMA può eseguire il trasferimento comandato da un interrupt o comandato da un timer, questa selezione viene effettuata attraverso il registro DMAxREQ.

Avendo a disposizione 8 canali DMA, ogni canale ha i suoi registri di controllo, per tale motivo è presente una x nei nomi generici di tali registri che va sostituita con un numero da 0 a 7 a seconda del canale.

Il trasferimento dati è unidirezionale per cui se vogliamo scrivere e anche leggere un registro, bisogna assegnargli due canali.

Abbiamo detto che il DMA trasferisce i dati dall’SFR di una periferica verso una variabile ram o viceversa. L’SFR viene specificato nel registro DMAxPAD (DMA Channel X Peripheral Address Register).

La variabile in cui/da cui trasferire i dati si imposta con i registri DMAxSTA e DMAxSTB nei quali va caricato, rispettivamente, l’offset del registro primario e secondario in cui/dal quale trasferire i dati. Sono specificabili un registro primario e secondario perchè c’è la possibilità di processare un dato mentre se ne legge un altro ma nulla vieta di impostare la stessa variabile per entrambi i registri.

Dal momento che le periferiche DMA hanno bisogno di un offset di memoria RAM DAM da cui partire (in cui trasferire o da cui trasferire i dati da/verso la periferica scelta), ci viene in aiuto una funzione builtin:

// alloco il mio array in una zona RAM dedicata al DMA:
unsigned int mio_bufferA[8] __attribute__((space(dma)));
unsigned int mio_bufferB[8] __attribute__((space(dma)));
 
// i registri DMAxSTA e DMAxSTB impostano rispettivamente il registro primario e secondario
// da usare per il trasferimento DMA. Devono puntare alla zona di memoria RAM nella
// quale abbiamo allocato le nostre variabili ram da usare col DMA.
 
DMA0STA=__builtin_dmaoffset(&mio_bufferA); // questa builtin recupera l'offset di partenza del mio buffer
DMA0STB=__builtin_dmaoffset(&mio_bufferB);

Le cose, in realtà sono molto più semplici utilizzando apposite librerie che purtroppo non sono facili da trovare… (anzi se qualcuno mi spiega per quale motivo sono state posizionate così… mi /ci  fa un piacere!). Ci sono numerose librerie che permettono di impostare e utilizzare le numerose periferiche a disposizione. Queste librerie si trovano nel percorso:

C:\Program Files\Microchip\MPLAB C30\src\peripheral_30F_24H_33F

Bisogna estrarre il file

peripheral_30F_24H_33F.zip

(io, su windows7, non riesco ad estrarlo nello stesso posto in cui risiede e devo copiarlo ed estrarlo in un’altra cartella)

Si ottengono 3 cartelle. In src/pmc ci sono le librerie per ogni periferica. In include ci sono i vari file header da utilizzare per le librerie

I canali DMA, infine, hanno un proprio flag di interrupt che permette di capire quando il trasferimento è stato completato.

Il Program Space Visibility

Si prosegue quindi con un’ampia zona di RAM non implementata della quale l’ultima parte prende il nome di PSV (Program Space Visibility) che fornisce un’ulteriore interessante caratteristica ai dsPIC: la possibilità di accedere ad una zona ROM attraverso il bus dati. Questa funzione è utilizzata da tutti i pic a 16 bit (quindi anche sui PIC24H e PIC24F), cerchiamo di capire di cosa si tratta.

Sappiamo che quando andiamo a definire delle costanti, queste vengono salvate nella ROM, ovvero nella memoria programma all’atto della programmazione, e li rimangono fino a quando il dispositivo non viene cancellato (allo stesso modo del programma insomma), difatti possono essere solo lette ma non scritte (sono appunto costanti!).

Sappiamo, inoltre, che i dsPIC, come tutti i PIC, fanno uso di un’architettura Harvard: abbiamo, cioè, due bus separati, uno per la memoria programma e uno per la memoria dati (ram). Quando la CPU esegue le istruzioni, disponendo di due bus separati, può estrarre contemporaneamente le informazioni da entrambe le memorie, questa caratteristica rende tali processori sicuramente più veloci ed efficienti rispetto a quelli utilizzanti l’architettura Von Neuman. Nel caso in cui bisogna estrarre costanti, che sono salvate cioè nella memoria programma, il processo viene rallentato in quanto bisogna attendere la liberazione del bus della memoria programma, impegnato ad estrarre le istruzioni.

Quest’ultima è la condizione normale che si ha sui pic ad 8 bit. Sui tutti i pic a 16 bit abbiamo invece a disposizione quest’area di RAM  PSV che serve a mappare la zona di ROM in cui vengono salvate le costanti. In pratica è possibile accedere alle costanti sfruttando il bus dati senza impegnare il bus della memoria programma. Il PSV funge quindi da “ponte” tra bus dati e  memoria programma.

Anche questa caratteristica, quindi, ci velocizza di molto le operazioni. Il PSV può mappare zone di ROM da 32K, per cui questa “finestra” può essere spostata nella ROM utilizzando il registro PSVPAG. Osserviamo lo schema di funzionamento:

Per capire il meccanismo con il quale opera il PSV non dobbiamo perdere d’occhio il fatto che la memoria programma è composta da word di 24bit mentre la memoria dati è composta da word di 16 bit.

Guardiamo di nuovo lo schema: il funzionamento del PSV è attivo solo se il bit 2 (Program Space Visibility in Data Space Enable bit) del registro CORCON (Core Control Register) è settato. EA è l’Effective Address: un indirizzo di memoria RAM.

Quando il bit 15 di EA è posto a 1 vuol dire che ci troviamo nell’area PSV della RAM (cioè dall’indirizzo 0x8000 in poi) e quindi, dal momento che CORCON<2> è 1 vuol dire che vogliamo sfruttare la funzionalità di remapping del PSV: gli ultimi 15 bit dell’EA, uniti agli 8 bit di PSVPAG formano una word di 23bit che riporta appunto in una word di memoria ROM…

Dallo schema si vede chiaramente che lo spazio ROM da 0x0100000 a 0x018000 è da 32K e lo spazio RAM PSV da 0x8000 a 0xFFFF è sempre da 32K

E’ evidente, inoltre, che il PSV può mappare unicamente word di memoria ROM da 16bit pur essendo la memoria programma composta da word di 24. In pratica utilizzando il PSV, gli 8 bit alti delle word presenti in memoria programma non saranno lette.

Oltre al PSV, per accedere in maniera rapida alla memoria programma, esistono delle istruzioni apposite (TBLRDTable Read) che permettono di accedere a tutti e 24 i bit di memoria programma, ma si tratta di una funzionalità non intuitiva come il PSV.

In ogni caso non dovremo preoccuparci di tutto questo perchè il compilatore esegue queste operazioni in automatico in maniera da avere sempre la massima efficienza nell’accesso alle variabili. E’ interessante comunque notare una cosa:

Quando nel nostro programma definiamo una variabile e la inizializziamo, ovvero scriviamo una cosa del genere:

unsigned char variabile=20;

in realtà stiamo utilizzando due variabili char e non una come sembra: una viene allocata nella RAM per gestire appunto la variabile, mentre il valore 20, utilizzato per inizializzare la variabile, deve pur essere memorizzato da qualche parte! L’unico posto è appunto la memoria programma. Quando andiamo a compilare il programma, il compilatore memorizza il valore di inizializzazione nella ROM, dopodichè aggiunge silenziosamente uno spezzone di codice (il famoso segmento C0) che permette di recuperare questo valore e inserirlo nello spazio RAM allocato per la variabile. Il compilatore utilizza le istruzioni TBLRD per inizializzare le variabili e il PSV per gestire le costanti.

Gli attributi psv, auto_psv e no_auto_psv

Abbiamo già incontrato la parola chiave __attribute__ in un articolo precedente sui dsPIC, quindi non torno a spiegarlo (ricordate solo che gli attributi vanno racchiusi tra doppie parentesi tonde). Dichiarando una costante con l’attributo auto_psv, per esempio:

const __attribute__((auto_psv)) char variabile&nbsp;= 20;

si fa in modo che tale variabile venga allocata  in uno spazio di ROM gestito automaticamente dal compilatore per il PSV. La variabile così dichiarata sarà richiamabile normalmente senza fare nessun “movimento”: è sempre una variabile in ROM ma viene trattata, via hardware, come se fosse una variabile in RAM per quanto abbiamo detto prima.

L’attributo psv, invece, alloca la variabile in uno spazio di ROM, sempre mappabile attraverso il PSV ,ma non gestito in automatico dal compilatore. Questa funzione potrebbe tornare utile per applicazioni avanzate. Questo vuol dire che PSVPAG, che abbiamo detto serve a puntare la corretta zona di ROM da mappare, va gestito manualmente per trovare la posizione corretta della variabile. Spulciando dagli esempi microchip vediamo che c’è una funzione builtin che ci viene in aiuto:

// dichiaro un array di int da allocare in ROM,
// in uno spazio non gestito automaticamente
// dal compilatore:
const int __attribute__ ((space(psv))) gamma_factor[3] = {13, 23, 7}; 
 
// mi dichiaro una variabile che serve per salvarmi
// il valore attuale di PSVPAG
unsigned psv_shadow;
 
// con la builtin apposita mi trovo il valore giusto a cui impostare
// PSVPAG per puntare alla mia costante salvata in uno spazio ROM
// non gestibile in automatico attraverso il PSV:
PSVPAG = __builtin_psvpage(gamma_factor);
 
// adesso posso utilizzare il mio array di costanti normalmente
// e farci quindi tutte le operazioni di cui ho bisogno
 
// .... codice che utilizza gamma_factor[]
 
// alla fine devo riportare PSVPAG al valore che aveva prima
// della mia modifica, altrimenti le variabili dichiarate come
// auto_psv non saranno più accessibili
 
PSVPAG = psv_shadow;

E’ possibile trovare alcuni esempi in

C:\Program Files\Microchip\MPLAB C30\examples

L’attributo auto_psv può anche essere utilizzato in una routine di interrupt:

void __attribute((interrupt,auto_psv)) _T1Interrupt(void)

Rimando a questo articolo per i dettagli sulle funzioni di interrupt sui pic a 16bit

In questo modo la routine di interrupt è in grado di accedere alle costanti attraverso il PSV (quelle dichiarate come auto_psv). Per gli interrupt questa è la condizione di default anche se non specifichiamo l’attributo auto_psv, in questo caso è possibile, però, che venga restituito un warning che serve appunto a segnalarci che non abbiamo eseguito una scelta e che verrà usata l’impostazione di default.

Per gli interrupt è anche disponibile l’attributo no_auto_psv:

void __attribute((interrupt,no_auto_psv)) _T1Interrupt(void)

la routine di interrupt dichiarata in questo modo non sarà in grado di accedere alle costanti attraverso il PSV. Per contro la latenza della funzione di interrupt così dichiarata sarà minore rispetto a quella dichiarata con auto_psv per il semplice motivo che mettendo no_auto_psv non sarà salvato il registro PSVPAG prima di entrare nell’ISR e quindi saranno eseguite delle istruzioni in meno.

Se questo articolo ti è piaciuto, condividilo su un social:
Se l'articolo ti è piaciuto o ti è stato utile, potresti dedicare un minuto a leggere questa pagina, dove ho elencato alcune cose che potrebbero farmi contento? Grazie :)