sabato 12 marzo 2022

Puntatori e Ram

In C i puntatori sono usati frequentemente, poiché non esiste alternativa più efficiente. In C++, in aggiunta ai puntatori, abbiamo gli alias, spesso usati per passare argomenti alle funzioni.

Vediamo di capire come sono fatti i puntatori. Come si dichiarano e come si usano lo troviamo descritto ampiamente in qualunque libro sul C/C++ e questo articolo non lo spiega per niente.

Chi punta e, a cosa punta

Le variabili risiedono nella RAM, ad esempio, una semplice variabile contatore, di tipo byte come questa:

  byte counter = 255;       

ha un valore intero 255 (0xFF), ma siamo curiosi, dove si trova questa variabile?
Si troverà certo in memoria RAM. Sappiamo, che ogni cella della RAM è numerata in modo sequenziale, da 0÷2047. Quindi chiaramente, ho il nome della variabile e se il linguaggio mi premettesse di ricavare, oltre al valore, anche il numero di cella (indirizzo), potrei almeno farmi una idea. Ma si può concretamente fare di più ricavando il numero della cella, detto indirizzo.

Il C/C++, per ricavare l’indirizzo dal nome di una variabile, ci mette a disposizione l’operatore &, che usiamo così:

  byte *counterPtr = &counter;

L’asterisco tra byte e counter, fa parte della sintassi per dichiarare un puntatore, che non dimentichiamo è esso stesso una variabile. Sappiamo già che una variabile ha: nome, valore e indirizzo. Ciò vale anche per counterPtr.

Vediamo di seguito il valore e indirizzo di ogni cella di memoria a partire dall’indirizzo 256.

   ADDR  00 01 02
   100   02 01 FF 

Questa rappresentazione ci risulta poco comprensibile per i seguenti motivi:

  • Stiamo usando il sistema di numerazione in base 16 al posto del più familiare sistema di numerazione in base 10.
  • L’indirizzo di una variabile è grande 16 bit (due byte), cioè un byte non è sufficiente, poiché il massimo valore decimale che può contenere, vale 255 (0xFF in esadecimale). Visto che ci sono 2048 celle di memoria, ci serve un tipo di dato senza segno grande due byte (uint16_t).
  • L’indirizzo di partenza vale 0x100 (256 decimale) anziché 0, 1, 2 ecc. Questo è legato alla architettura hardware dello specifico microntrollore. Su ATmega328, i primi 255 byte di memoria sono riservati al funzionamento del microcontrollore.

Si comprende qualcosa in più sapendo che, 0x100 (256), 0x101 (257), 0x102 (258). Allora il puntatore counterPtr occupa i primi due byte di valore 02 01 che espressi in esadecimale leggiamo così: 0x0102 o 0x102 (258). In effetti all’indirizzo 0x102 (258), c’è il valore FF (0xFF), che è il valore assegnato alla variabile counter.

Adesso dire che, il valore di un puntatore è un indirizzo di memoria, è più comprensibile. Uno dei motivi per cui ho usato la numerazione esadecimale è che con soli due caratteri, posso esprimere un valore nel range 0÷255. Usando il sistema decimale, la tabella precedente sarebbe come segue:

   ADDR    000 001 002
   256     002 001 255

Risulta sempre poco comprensibile, specie il valore dei primi due byte 002 001. Perché 002 001 equivale a 258 decimale?

La rappresentazione binaria di due byte seguente ci svela il perché:

   Byte alto   Byte basso
   0000 0001   0000 0010

Byte alto vale 256 + byte basso vale 2 = 256 + 2 = 258. A questo punto possiamo dire tranquillamente, che all’indirizzo 256 (0x100), ci sono due byte che puntano all’indirizzo 258 (0x102), il cui valore è 255 (0xFF), senza rischio di risultare incomprensibili.

Si ok ma come si usano?

In effetti quanto letto in precedenza ci fa capire come sono fatti i puntatori, ma non ci dice come, quando usarli e perché sono efficienti. L’argomento è vasto e complesso ed è sempre trattato nei libri sul C/C++, per cui non posso affrontarlo qui. Invece qui posso dire, che i puntatori non sono una invenzione del linguaggio C/C++, ma sono una conseguenza della struttura di qualunque memoria RAM. Il C/C++ ci viene incontro semplificando e rendendo più esplicito ciò che è un puntatore da quello che non lo è, tutto qui.

RAMSTART e RAMEND

Per sapere quale è il primo indirizzo utile di ram e quale è l’ultimo, la libreria avr-libc, definisce delle macro dal nome esplicito: RAMSTART e RAMEND, possiamo tranquillamente stamparle su seriale con:

   Serial.print("\nRAMSTART  = ");
   Serial.print(RAMSTART);
   Serial.print("\nRAMEND    = ");
   Serial.print(RAMEND);

Ciò che otteniamo varia in base al microcontrollore scelto.

ATmega328

RAMSTART = 256 (0x100)
RAMEND = 2303 (0x8FF)

ATmega2560

RAMSTART = 512 (0x200)
RAMEND = 8704 (0x2200)

Se eseguiamo la sottrazione seguente 8704 - 512 = 8192, otteniamo il numero di celle di cui è composta la RAM, nel caso di ATmega2560 8192 / 1024 = 8K, in effetti ci sono 8K di RAM.

Trasformare un numero in puntatore

Il C/C++ ci permette di scegliere in modo arbitrario dove puntare il nostro puntatore. In altre parole, posso andare a leggere (o scrivere), in una cella di memoria, a partire dall’indirizzo di questa.

Conosciamo l’indirizzo 0x100, conosciamo cosa contengono i due byte, possiamo provare a creare il nostro puntatore, in modo che punti a 0x100.

Per fare ciò, possiamo scegliere tra sintassi C o C++, vediamo prima la sintassi C:

   uint16_t *myPtr_c = (uint16_t*)0x100U;
   Serial.println((uint16_t)*myPtr_c, HEX);

La sintassi C è basata sul casting esplicito specificato tra le parentesi tonde (uint16_t*). La prima riga crea il puntatore di nome myPtr_c e vi assegna il numero intero senza segno 0x100, ma trasformato in puntatore, grazie al casting esplicito. La seconda riga stampa il contenuto della memoria puntata, cioè cosa c’è nei primi due byte all’indirizzo 0x100. Ovviamente ci troviamo 0x102. Senza saperlo abbiamo quasi creato un puntatore a puntatore, quasi, poiché c’è una sintassi specifica per creare un puntatore a puntatore.

La sintassi C++ usa una specifica parola chiave e questo sarebbe il modo più corretto con arduino, poichè esso usa il compilatore C++. Facciamo la stessa cosa del codice precedente, ma per distinguerlo diamo al puntatore il nome myPtr_cpp, vediamo il codice C++:

   uint16_t *myPtr_cpp = reinterpret_cast<uint16_t*>(0x100U);
   Serial.println((uint16_t)*myPtr_cpp, HEX);

La parola chiave specifica, è reinterpret_cast, che reinterpreta il valore specificato tra parentesi tonde, al tipo di dato specificato tra le parentesi angolari <uint16_t*>.

Le variabili nello stack

Nel caso delle due variabili counter e counterPtr, il loro indirizzo vale come abbiamo visto 0x100 e 0x102, questo è vero per quelle variabili dichiarate fuori dal corpo di qualsiasi funzione, quindi dichiarate come globali. Per le variabili dichiarate all’interno di una funzione, essendo queste locali, sono sempre memorizzate in RAM, ma in una area detta stack. Lo stack si trova vicino alla fine della RAM, cioè inizia da RAMEND.

Così come abbiamo fatto con counter, proviamo a creare una variabile locale e a ricavarne l’indirizzo con l’operatore &. La variabile la dichiariamo dentro la funzione setup(). Vediamo il codice e ciò che stampa su seriale.

   byte counterLocale = 0xFF;
   Serial.println((uint16_t)&counterLocale, HEX);

La seconda riga stampa 0x8FB, noi però vogliamo visualizzare, anche il valore delle due celle precedenti questo indirizzo. Quindi stampiamo ogni cella nel range 0x8F9÷0x8FF e otteniamo questo output:

   ADDR    00 01 02 03 04 05 06 
   8F9     00 01 FF 00 65 00 8C 

Troviamo la variabile counterLocale all’indirizzo 0x8F9 + 0x02 = 0x8FB e come possiamo leggere il suo valore è 0xFF (255).

Mentre per i due byte iniziali, il loro valore 00 01 ci ricorda qualcosa, ricordate byte alto e basso, bene HB 0000 0001 e LB 0000 0000 e allora in base 16 leggo 0x100, evidentemente si tratta di uno dei puntatori creati in precedenza, potrebbe benissimo essere myPtr_cpp.

Licenza Creative Commons
Quest'opera è distribuita con Licenza Creative Commons Attribuzione 4.0 Internazionale.

Nessun commento:

Posta un commento