martedì 21 dicembre 2021

Base.pin.arduino.hardware

 Corrispondenza pinout

In rete si trovano moltissimi arrangiamenti che mostrano le funzioni dei singoli pin attraverso sigle che assumono un senso quando apriamo il datasheet del ATmega328p. Anche io allora mi sono improvvisato grafico con inkscape per realizzare il mio personale arrangiamento dove tutti i pin in verde hanno comportamento digitale, mentre in giallo/dorato sono evidenziati i pin che possono aquisire valori analogici e convertirli in digitale. Ad esempio, PC0÷PC5 sono pin digitali, gli stessi pin ADC0÷ADC5 fanno capo al convertitore ADC presente dentro la MCU. 

Alcuni pin non compaiono poiché sono usati ad esempio per il reset, per il quarzo (o risonatore cermico). SCK/MOSI/MISO e SS fanno capo al device SPI (Serial Peripheral Interface) e sono tutti evidenziati in blu.

Sulla sinistra in alto abbiamo tutti i pin di alimentazione: 2 pin per 5.0V, 2 pin per GND. La tensione ausiliaria da 3.3V (~50mA). Il pin VIN vi permette di prelevare la tensione di alimentazione che proviene dal connettore DC-VIN dove collegherete un alimentatore la cui tensione sia nel range 7÷12V.

Terminiamo con i pin evidenziati in celeste (SCL/SDA) che fanno capo al device 2-wire (i2c compatibile) usati spesso per comunicare con sensori di vario tipo e controllare display LCD. Particolarità di questo device è la possibilità di collegare più dispositivi (sensori ecc) agli stessi due fili. SCL è una funzione alternativa del bit5 di porta C, mentre SDA è una funzione alternativa del bit4 della stessa porta C. Detto in altri termini se uso 2-wire non posso acquisire dati analogici da ADC4 e ADC5, né tanto meno usarli come pin digitali.

Nota sui colori - L'uso di cavetti colorati riduce il rischio di fare danni, per cui consiglio di usare i seguenti cavetti colorati:

Rosso      +5.0V
Nero        GND
Giallo      +3.3V
Arancio   VIN

A seguire c'è il disegno della MCU ATmega328p, dove vedrete molte sigle presenti nel pinout di arduino uno.

Tutte queste sigle per ogni pin, indicano le funzioni principali e quelle alternative, ad esempio, PCINT sono pin che possono sollevare un interrupt (se abilitati) PINCHANGED quando il loro stato cambia. Il numero a seguire PCINT indica la priorità, occhio che PCINT0 ha priorità su PCINT1 e su tutti gli altri fino a 22. In breve la priorità maggiore inizia da zero e la minore è PCINT22.


Stessa considerazione per i due pin PD2, PD3 i quali rispettivamente possono sollevare una IRQ (Iterrupt Request) rispettivamente INT0, INT1. A differenza di PCINT, INT0 e INT1 sono configurabili per sollevare una IRQ, su fronte di salita o su quello di discesa o entrambe. 

Le porte

Le porte sono siglate con prefisso P (Port), una lettere (A÷Z) e un numero che indica il bit (o peso) assunto all'interno della porta. Per una MCU ad 8 bit una porta completa è composta da 8 pin. 

L'unica porta (fisicamente disponibile) completa è la porta D (PD7, PD6, PD5, PD4, PD3, PD2, PD1, PD0). Può stupire che l'elenco inizi da PD7 per finire con PD0, ma secondo la numerazione binaria se accendo il bit PD7, in decimale ho 128, se accendo anche PD0, in decimale ho 129. 

Pertanto si dice, che il bit più a destra (PD0) è il bit meno significativo (LSb), quello più a sinistra è il più significativo (MSb). LSb sta per Least Significant bit, mentre MSb sta per Most Significant bit.

Nella pratica PD0 e PD1 sono impegnati con funzione TX e RX, sia per trasferire il firmware, sia per comunicare con il PC via serial monitor, pertanto considerare PD una porta completa usabile è possibile solo rinunciando alla comunicazione seriale e optando per la scrittura del firmware tramite ICSP. Per tale motivo è bene non collegare nulla ai pin PD0 e PD1, diversamente potrebbe non essere possibile il trasferimento del firmware.

Pin Impegnati

PC6 è usato per potere resettare la MCU. Non si consiglia usarlo per altre funzioni. Il pin deve stare HIGH con pull-up, un fronte di discesa resetta la MCU.

PD0, PD1 sono impegnati per il trasferimento del firmware e per comunicare con il serial monitor. Non si consiglia l'uso per altre funzioni.

PB6, PB7 sono impiegati per collegare un quarzo o risonatore. In standalone scegliendo come sorgente di clock l'oscillatore RC interno questi due pin sono liberi di essere usati. 

Conclusione

Per terminare facciamo la conta dei pin digitali disponibili che sono 18 in totale, 6 (4 se usiamo 2-wire interface) di questi possono acquisire informazioni analogiche. In totale 6 uscite possono generare segnali PWM, sfruttando i 3 timer hardware interni alla MCU.


Licenza Creative Commons
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Condividi allo stesso modo 4.0 Internazionale

martedì 14 dicembre 2021

Base.intro-arduino-uno.hardware

Introduzione Arduino UNO R3

La scheda in questione monta una MCU (Micro Controller Unit) ATmega328P in PDIP package (3), ed è questa che programmiamo tramite Arduino IDE. Il clock di questa MCU è fornito da un risonatore ceramico evidenziato in viola.

Il chip (7) è una MCU ATmega16U2 (e il suo quarzo (8)) già programmata per svolgere la funzione di convertitore USB to Serial, ed è proprio grazie a questo che possiamo con un clic trasferire il firmware (così si chiama il nostro programma una volta compilato) presente sul disco del PC e memorizzarlo all'interno della memoria FLASH della MCU ATmega328p.

Il pulsante (6) ci permette di riavviare la MCU ATmega328p. Dopo la pressione verrà avviato il firmware del bootloader (presente di default) e questo avvierà il firmware utente (cioè quello che abbiamo sviluppato). Di default il firmware utente è già presente nella memoria FLASH, si tratta di un blink del led L (evidenziato in giallo) per testare il corretto funzionamento della MCU.

Tramite il connettore USB (1) alimentiamo la scheda e il led ON (in verde) si illumina.  La connessione ci serve anche per programmare la FLASH della MCU ATmega328p, inoltre il nostro programma può scambiare informazioni con il PC.

Evidenziato in blu sotto il connettore (1) abbiamo un fusibile auto-ripristinante che riduce la possibilità di guasti al PC a seguito di corto circuito accidentale che può verificarsi durante la sperimentazione (o eccessiva corrente assorbita). 

Questo fusibile è presente solo nel circuito di alimentazione tramite connettore USB (1). Quando l'alimentazione viene fornita dal connettore (2) questo fusibile è escluso dal circuito.

Nota: Sarebbe bene non mettere alla prova questo fusibile evitando cortocircuiti o eccessi di corrente poiché non c'è certezza che possa evitare guasti. Ma durante la sperimentazione pratica, anche prestando la dovuta attenzione, tra breadboard, cavetti e componenti elettronici tutto può accadere.

Il connettore (2) ci permette di alimentare la scheda in modo indipendente dalla  connessione USB del connettore (1). Detto in altri termini, entrambe le connessione USB (connettore 1) e alimentazione su connettore (2) sono permesse, quindi in queste condizioni un circuito interviene e preleva l'energia dal connettore (2), anziché dal connettore USB. Il circuito che permette ciò, appare qui di seguito semplificato.

La tensione di alimentazione da fornire a questo connettore deve essere nel range 7Vdc ÷ 9Vdc. Una tensione superiore a 9Vdc potrebbe surriscaldare il regolatore di tensione (9). La corrente minima che l'alimentatore deve potere fornire è di circa 500mA, non c'è limite per la corrente massima.

Il connettore (4) viene impiegato dalle shield impilabili e fornisce loro alimentazione e un canale di comunicazione SPI. Il connettore viene anche usato per connettervi un programmatore esterno o un debugger hardware. Questo connettore viene sfruttato in produzione per scrivere il firmware del bootloader nella memoria FLASH. Il connettore (5) svolge la stessa funzione ma relativa alla MCU ATmega16U2.

Evidenziati in celeste vi sono due led TX e RX che indicano attività (cioè transito di dati) su questi due canali di trasmissione seriale. Tx lampeggia quando ad esempio inviamo dei dati al PC. Rx lampeggia quando è il PC ad inviare dati, ad esempio quando con IDE arduino facciamo click su Upload.

Si termina la descrizione con i connettori (10) e (11) i quali richiedono un approfondita trattazione non presente qui. Il pinout di questi connettori è reperibile in rete, cercando ad esempio "arduino uno pinout".

Questa è una brevissima introduzione alla scheda e pertanto è necessario approfondire certi aspetti come ad esempio:

  1. Tensione di alimentazione su connettore (2). Se non colleghiamo alcun circuito o un solo led alla scheda possiamo anche superare la tensione di 9Vdc consigliata, ad esempio 12Vdc. Diversamente se colleghiamo un  circuito, questo potrebbe assorbire corrente che attraversa il regolatore (9) scaldandolo oltre le specifiche. Con 12Vdc e con un assorbimento di circa 250mA il regolatore deve dissipare in calore la potenza così calcolata: (12 - 5) x 0.25 = 1.75Watt (sono tanti, troppi), con 9Vdc avremo invece (9 - 5) x 0.25 = 1Watt (sono tanti ma va già meglio), con 7Vdc: (7 - 5) x 0.25 = 0.5Watt (ottimo). 
  2. Il programma scritto con Arduino IDE prima di potere essere trasferito nella memoria FLASH deve subire una trasformazione profonda, di ciò si occupa L'IDE richiamando opportuni strumenti software (compilatore, linker, toolchain ecc) che provvedono a trasformare il sorgente comprensibile a noi esseri umani, in un linguaggio binario comprensibile alla CPU contenuta all'interno del ATmega328p. Questo processo complesso di trasformazione avviene in modo trasparente nei confronti dell'utente e ciò può indurre l'utente principiante a pensare che il suo programma in versione sorgente sia presente nella memoria FLASH della MCU, mentre invece nella FLASH c'è del codice binario a noi umani non comprensibile e non esiste nessuno strumento software che esegua il processo inverso, tipo un de-compilatore che trasformi il codice binario in codice sorgente.
  3. Sketch, tradotto in Italiano Schizzo, in questo modo chiamano il programma scritto con Arduino IDE. Prendiamo atto quindi che Sketch e Programma sono sinonimi e indicano un listato (o più di uno) che descrivono un algoritmo attraverso l'uso di parole chiave specifiche per ogni linguaggio. In sostanza il nostro programma sorgente scritto con Arduino IDE destinato ad essere eseguito dalla CPU di una MCU prende il nome generico Sketch.
  4. Firmware è il termine più appropriato ad indicare la traduzione binaria del nostro Sketch. La traduzione da codice sorgente a codice binario ha come risultato la generazione di un file con estensione .hex (formato Intel Hex)

Termina qui l'introduzione dalla quale si possono ricavare parecchie informazioni senza tuttavia scendere nei dettagli. Coloro i quali fossero interessati ad approfondire, consiglio un link a wikipedia dove viene descritta  l'architettura AVR.


Licenza Creative Commons
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Condividi allo stesso modo 4.0 Internazionale

mercoledì 8 dicembre 2021

Advanced.arduino.copybit

Copia di bit per bit

Copiare dati un bit alla volta significa leggere un bit da una variabile (es:myBitDataOrg) salvarlo in una variabile (es:myBit) per poi scrivere myBit nella variabile myBitDataDest. Fare ciò in C/C++ ha poco senso poiché basta usare l'operatore di assegnamento (=) tra le due variabili per avere lo stesso risultato della copia bit per bit. Tuttavia sperimentalmente è utile scrivere l'algoritmo perché ci si può trovare nella condizione di dovere leggere bit per bit con un digitalRead(), questo vuole dire che non sappiamo se digitalRead(pin) restituisce 1 o 0 e lo scopriamo solo dopo che digitalRead(pin) è stata eseguita. 

Un tipico esempio di lettura bit per bit lo possiamo vedere nel metodo HX711::read() della libreria per HX711, qui. Nello specifico la funzione uint8_t shiftInSlow(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder) legge un bit alla volta e lo salva nella variabile value per poi restituirlo al chiamante.

Qui di seguito semplifico la funzione shiftInSlow per vedere come legge un bit per volta:

digitalWrite(clockPin, HIGH);   // alza clockPin
byte bitValue = digitalRead(dataPin); // legge lo stato di dataPin
digitalWrite(clockPin, LOW);  // abbassa clockPin 

Quanto sopra ripetuto in ciclo 24 volte legge dall'HX711 i 24 bit della ultima conversione. Attenzione che questo non è il modo corretto per leggere bit per bit dal chip HX711 e quanto meno durante la lettura dei 24 bit dobbiamo prima spegnere gli interrupt globali. 

Detto questo, noi non abbiamo il chip su banco di lavoro e allora creiamo la funzione hxread() la quale ci restituisce un bit ogni volta che la chiamiamo. La funzione in questione preleva un bit per volta da una variabile di origine di nome appunto myDataOrg di tipo senza segno grande 32 bit (uint32__t).

uint32_t myDataOrg = 157690;
int8_t bitPos = 23;  // nota il tipo con segno da 8 bit

uint8_t hxread() {
    uint8_t bit = (((myDataOrg) >> (bitPos)) & 1);
    bitPos--;
    if (bitPos < 0 )
        bitPos = 23;

    return bit;
}

Provate a richiamarla 24 volte all'interno della funzione setup() e ad ogni iterazione stampate il valore resituito ad esempio così:

void setup() {
    Serial.begin(9600);
    for (int i=0; i<24; i++)
        Serial.print( hxread() );
    Serial.println("");
}

Nel monitor seriale dovreste vedere stampato:


000000100110011111111010

  

La stessa sequenza di seguito, ma separata da ' ogni 4 bit, così sarà più facile contarli.

 

0000'0010'0110'0111'1111'1010

 

Contandoli in effetti sono 24 bit, vedendoli così in binario non siamo capaci di esprimere il valore in decimale, ma siamo avvantaggiati poiché quella in binario è la rappresentazione (in binario appunto) del valore decimale 157690.

 

Questo esempio già ci permette di stampare la rappresentazione binaria di qualunque numero grande 24 bit contenuto nella variabile myDataOrg. Ricordiamoci, il massimo valore che possiamo assegnare alla variabile myDataOrg è 16777215, che in binario equivale ad accendere tutti i 24 bit.

 

Quella binaria per noi è solo una rappresentazione, ma se potessimo guardare all'interno della memoria ram (composta in questo caso da 4 celle, grande ogni una 1 byte) troveremo la stessa sequenza di bit ad eccezione del fatto che 4 celle da 1 byte fanno 32 bit per cui gli 8 bit a sinistra avranno valore 0. 

 

Adesso grazie alla nostra funzione hxread() possiamo provare a copiare ogni bit nella variabile di destinazione myDataDest per poi stamparne il valore. Se il nostro algoritmo è valido la stampa di myDataDest dove essere 157690, diversamente il nostro algoritmo non è corretto. Allora vediamolo questo algoritmo sempre all'interno della funzione setup():

 

uint32_t myDataOrg = 157690;
int8_t bitPos = 23;  // nota il tipo con segno da 8 bit
uint8_t hxread() {
    uint8_t bit = (((myDataOrg) >> (bitPos)) & 1);
    bitPos--;
    if (bitPos < 0 )
        bitPos = 23;
    return bit;
}
void setup() {
    Serial.begin(9600);
    uint32_t myDataDest = 0; // fondamentale che myDataDest deve avere valore 0 
    uint8_t data = 0;
    for (int i = 24; i > 0; i--) {
        data = hxread();
        Serial.print( data );
        myDataDest |= ((uint32_t)data << (i-1));  // occhio al cast (uin32_t)
    }
    Serial.println("");
    Serial.println(myDataDest);  // dovrà stampare 157690
}

void loop() {
    // empty loop 
} 
 

Nel caso in cui la funzione hxread() dovesse restiuire i bit leggendoli da destra verso sinistra cioè partendo dal bit meno significativo LSB, anche noi nel copiarli in myDataDest dobremmo iniziare a scrivere dal bit più a destra e via via salire verso il 23 esimo bit.

 

In merito al commento "// occhio al cast... dobbiamo dire che è necessario con architettura AVR, mentre ad esempio su ESP dovrebbe funzionare anche senza il cast esplicito. 

 

L'inganno sta nel fatto che, se la variabile data è grande solo 8 bit, come è possibile accendere il bit 23? infatti, ma c'è da fare il conto con la promozione del tipo di dato attuata dal compilatore in base alla architettura. Ad esempio sul PC il cast esplicito non serve e quindi ci pensa il compilatore a promuovere il risultato di (data << (i - 1)) ad un tipo di dato sufficientemente capiente, cioè uno di quelli che almeno ha 24 bit che di default è il tipo long. Su architettura AVR la promozione del risultato è al massimo il tipo int.

 

Quanto qui trattato ci mostra che anche senza hardware (in attesa che arrivi) possiamo portarci avanti anziché rimanere con le mani in mano.


Licenza Creative Commons
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Condividi allo stesso modo 4.0 Internazionale

domenica 5 dicembre 2021

Arduino.millis.azioniComposte

Eseguire azioni composte

Una azione composta è appunto composta da singole azioni dove la sequenza di queste è rilevante. Ad esempio 3 pin di arduino configurati come OUTPUT devono essere accesi in sequenza. Una azione composta di questo tipo è da considerare eseguita quando tutte le 3 uscite risultano HIGH. 

digitalWrite(pin1, HIGH);
digitalWrite(pin2, HIGH);
digitalWrite(pin3, HIGH);

Questo esempio appare banale perché non abbiamo specificato delle sequenze temporali. Introduciamo le sequenze temporali usando la funzione delay() che ricordiamo avere un comportamento non desiderato definito da molti "bloccante".

digitalWrite(pin1, HIGH);
delay(1000);
digitalWrite(pin2, HIGH);
delay(2000);
digitalWrite(pin3, HIGH);
Serial.println("compito completato"); 

Da un lato il delay() ci permette di mettere in sequenza temporale 3 singole azioni che una volta compiute noi consideriamo come un "compito completato".

Rispettare queste azioni con delle tempistiche usando millis() al posto di delay() ci sconvolge il ragionamento. Usando millis() ci troviamo nella condizione in cui altro codice viene eseguito nell'intervallo di tempo di 3 secondi e dobbiamo considerare "compito completato" solo quando tutte le 3 uscite pin1, pin2 e pin3 sono nello stato HIGH. Visto che abbiamo 3 digitalWrite() usiamo 3 variabili di stato, es: statoPin1, statoPin2 e statoPin3.

void loop() {

    // qui il codice che modifica il valore delle 3 variabili di stato

digitalWrite(pin1, statoPin1);
digitalWrite(pin2, statoPin2);
digitalWrite(pin3, statoPin3);

}

Al posto di // qui il codice ... dobbiamo inserire il codice che modifica il valore delle 3 variabili di stato in funzione del tempo che scorre. Il codice sotto esegue immediatamente la prima, dopo 1000ms la seconda e dopo 2000ms la terza azione.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const byte pin1 = 2;
const byte pin2 = 3;
const byte pin3 = 4;
// variabili di stato
bool statoPin1 = LOW;
bool statoPin2 = LOW;
bool statoPin3 = LOW;

// saveMillis è usata per temporizzare le azioni
uint32_t saveMillis = 0;
uint32_t intervallo = 0:   // potrebbe essere 1000 al posto di 0

void setup() {

    pinMode(pin1, OUTPUT);
    pinMode(pin2, OUTPUT);
    pinMode(pin3, OUTPUT);
    
    Serial.begin(9600);
}
   
void loop() {

    if (statoPin1 == LOW) {

        statoPin1 = HIGH;
        intervallo = 1000;      // vale 1000ms
        saveMillis = millis();

    }

    if (millis() - saveMillis >= intervallo) {

        if (statoPin2 == LOW) {

            statoPin2 = HIGH;
            intervallo = 2000;  // se prima era 1000ms ora è 2000ms
            saveMillis = millis();

        } else {

            statoPin3 = HIGH;
        }
    }
    
    digitalWrite(pin1, statoPin1);
    digitalWrite(pin2, statoPin2);
    digitalWrite(pin3, statoPin3);
    
    if (statoPin1 && statoPin2 && statoPin3) {
        // questo messaggio apparirà dopo circa 3 secondi dopo il reset.
        Serial.println("compito completato);
    }

}

Notate come da riga 24 a 30 nel corpo della if ci entra una sola volta e mai più. Quando serve questo comportamento abbiamo necessità di una variabile di stato che una volta modificata nel corpo della if impedirà di essere eseguita altre volte (ricordate che la funzione loop() termina e ricomincia all'infinito).


Licenza Creative Commons
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Condividi allo stesso modo 4.0 Internazionale

mercoledì 20 gennaio 2021

Arduino: Array e array di caratteri (C string)

Array o vettore

Array o vettore è una porzione di memoria a cui attribuiamo un nome e una dimensione. L'accesso in lettura e scrittura avvengono tramite operatore sottoscrizione [], al cui interno si specifica l'indice numerico. Vediamo prima la dichiarazione senza inizializzazione e poi l'accesso.

Dichiarazione

byte unoArray[5];

byte è il tipo di dato usato per ogni elemento. Altri tipi predefiniti sono: int, word, unsigned long ecc. unoArray è il nome che abbiamo attribuito alla porzione di memoria. 5 è la dimensione del vettore espressa in numero di elementi di tipo byte in questo caso.

Accesso agli elementi di array

Il primo elemento di unoArray è accessibile a partire dall'indice zero (0), l'ultimo elemento è allora 5 - 1 = 4, o in altri termini n elementi meno 1. Attenzione che questo crea confusione ed è facile cadere in errore accedendo ad un elemento non lecito.

unoArray[0] = 1;
unoArray[1] = 2;
unoArray[2] = 3;
unoArray[3] = 4;
unoArray[4] = 5;

Di seguito invece l'errore di accedere ad un elemento non esistente:

unoArray[5] = 6;

Attenzione che l'errore non viene segnalato dal compilatore, spetta al programmatore fare attenzione a questo errore che ha conseguenze non prevedibili. In ogni caso questo errore compromette l'integrità dei dati presenti in memoria. Un modo sicuro per usare indici validi e di verificare che l'indice non superi il numero di elementi meno 1.

Occupazione di memoria

L'occupazione di memoria lo esprimiamo in byte e nel primo esempio il numero di elementi coincide con l'occupazione di memoria, cioè 5 byte. Se al posto del tipo byte, usiamo il tipo int l'occupazione in memoria ammonta a 2 x 5 = 10 byte, ma sempre di 5 elementi sarà composto il vettore. Segue un esempio con il tipo longche occupa 4 x 5 = 20 byte.

Dichiarazione e inizializzazione

Possiamo assegnare un valore ad ogni elemento durante l'esecuzione del programma (detto run-time) oppure inizializzare il vettore durante la compilazione (detto compile-time). Assegnare un valore a run-time lo abbiamo già visto, di seguito inizializziamo il vettore a compile-time.

byte byteArray[] = { 1, 2, 3, 4, 5 };

Come si vede manca il numero 5 tra le parentesi quadre, ma il compilatore lavora correttamente ricavando la dimensione dall'elenco specificato tra le parentesi graffe. Poiché l'inizializzazione avviene a compile-time, nell'elenco possono comparire solo costanti. Se si specifica la dimensione del vettore questa deve corrispondere al numero di elementi specificati tra le graffe, diversamente ad esempio, nel caso specifico, se minore di 5, il compilatore emette un errore, se maggiore di 5, gli altri elementi sono inizializzati a zero (0).

Array di char (C string)

Ciò che è conosciuto come C string è in effetti un vettore contenente codice ascii in cui l'ultimo elemento è il codice 0, usato come terminatore di elementi utili. In altri termini, poiché non è efficiente salvare un simbolo grafico all'interno di un elemento, si usa un numero nel range 0÷255 che occupa un solo byte e ad ogni numero è associato un simbolo rappresentabile o meno, per approfondire circa la tabella ascii segui il link.

Dichiarazione di C string

La dichiarazione è simile a quanto già visto, sostituendo a byte il tipo predefinito char, esempio:

char str[7];

Dove str è una C string valida poiché se dichiarata globale tutti gli elementi sono automaticamente inizializzati a zero, nonostante occupi 7 byte, il numero di caratteri utili è nullo. In questo caso str viene detto genericamente buffer. Per usare str nel modo appropriato dobbiamo fare attenzione a non modificare il valore dell'ultimo elemento che vale 0 come tutti gli altri, solo così preserviamo la C string, vediamo un esempio:

for (byte i = 0; i < 6; i++) {
str[i] = 65 + i;
}

Se stampiamo str con Serial.println(str); otteniamo il seguente output sul monitor seriale:

ABCDEF

Se stampiamo ogni singolo elemento in str otteniamo:

65, 66, 67, 68, 69, 70, 0

L'ultimo elemento vale 0 e quindi str è una C string valida. Essa è composta da 6 caratteri rappresentabili e l'elemento all'indice 6 è il terminatore. Molte funzioni della libreria standard string.h fanno affidamento sulla presenza del terminatore C string e in mancanza il loro comportamento è imprevedibile, ad esempio la funzione strlen() restituisce la lunghezza di str contando tutti i caratteri presenti tranne lo 0.

Quindi ad esempio Serial.println(strlen(str)); stamperà:

6

Errori insidiosi

Per comprendere quanto la cattiva gestione degli indici sia disastrosa ho voluto scrivere un programma errato che casualmente si comporta correttamente. Nel programma sovrascrivo il terminatore di stringa ma la funzione strlen() restituisce un valore plausibile ma errato.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void setup() {
    Serial.begin(9600);
    char str[7] = "";
    char str1[2] = "";
    for (byte i = 0; i < 7; i++) {
        str[i] = 65 + i;
    }
    Serial.println(str);
    Serial.print("strlen(str) = ");
    Serial.println(strlen(str));
    delay(1000);
}

void loop() { 
    // empty loop
}

La riga 8 stampa:

ABCDEFG

Le righe 9÷10 stampa:

strlen(str) = 7

Il programma lavora apparentemente corretto e non si presentano comportamenti anomali casuali, tuttavia str non è una C string valida in quanto priva di terminatore '\0'. La lunghezza di str vale 7, ma se modifichiamo la riga 4 in questo modo:

char str1[] = "Abbbbb";

Eseguendo il programma adesso la funzione strlen(str) restituisce 13, e guarda caso 7 + 6 = 13. dove 6 è la lunghezza della C string str1, ma la cosa divertente è che riga 8 stampa str + str1, cioè: ABCDEFGAbbbb.

Il motivo di ciò è dovuto al terminatore presente solo in str1 e Serial.print() si ferma di stampare solo quando trova l'unico terminatore di C string . Ora proviamo immaginare che Serial.print() non trovi un terminatore e non si fermi di stampare, questo sarebbe davvero un bel problema che potrebbe portare il programma a comportarsi in modo casuale fino a bloccarsi del tutto.

L'uso delle C string mette in difficoltà anche i programmatori professionisti, figuriamoci come se la possa cavare un principiante. Tuttavia i vettori e le C string sono troppo efficienti per non usarle e cercare un loro sostituto è spesso tempo perso. Un suo possibile sostituto è la classe String() di arduino che tuttavia alla lunga produce effetti disastrosi, perdendo al contempo di efficienza, questo è vero con arduino UNO, meno con la MEGA che dispone di maggiore RAM. Discorso diverso su piattaforma ESP32, dove l'uso moderato e coscienzioso raramente produce disastri.

Uso ricorrente

Con arduino spesso si ricorre ai vettori all'interno del quale specificare quali pin configurare ad esempio come uscita. Ciò è particolarmente conveniente quando molti pin devono essere configurati nella stessa modalità, inoltre ciò risulta pure efficiente se il vettore viene dichiarato all'interno di una funzione anziché essere globale. Il codice banalmente è composto da un ciclo for al cui interno chiamare la funzione pinMode(), anche se banale vediamo come fare ciò all'interno della funzione setup():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void setup() {
    Serial.begin(9600);
    byte pinsAsOutput[] = {4, 5, 7, 8, 10, 11, 13};
    char str1[2] = "";
    for (byte i = 0; i < sizeof(pinAsOutput); i++) {
        pinMode(pinsAsOutput[i], OUTPUT);
    }
    
    delay(1000);
}

void loop() { 
    // empty loop
}

Certamente si evita di scrivere 7 pinMode(... nel setup, bene tanto di guadagnato, tuttavia si può migliorare aggiungendo un commento alla riga 3. Inoltre ciò ci da la possibilità di mostrare l'uso della istruzione sizeof() grazie a ciò non rischiamo di commettere un errore contando il numero di elementi presenti nel vettore, vantaggio non da poco, inoltre possiamo modificare il numero di elementi senza preoccuparci di altro. Sempre con scopo didattico possiamo anche creare una funzione da chiamare alla quale passiamo 3 argomenti, cioè il vettore, la dimensione e la modalità (INPUT, OUTPUT ecc). Vediamola subito a seguire.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void setPins(const byte *arr, size_t size, uint8_t mode) {
    for (byte i = 0; i < size; i>++) {
        pinMode(arr[i], mode);
    }
}

void setup() {
    Serial.begin(9600);
    byte pinsAsOutput[] = { 4, 5, 7, 8, 10, 11, 13 };
    
    setPins(pinsAsOutput, sizeof(pinsAsOutput), OUTPUT);
    delay(1000);
}
void loop() { 
    // empty loop
}

L'operatore sizeof non restituisce il numero di elementi di cui è composto il vettore ma la dimensione in byte occupata, nel caso specifico pinAsOutput occupa 7 byte ed ha 7 elementi e il problema non si pone ma se ogni elemento fosse grande 2 byte, sizeof restituirebbe 14.

L'operatore sizeof

L'operatore sizeof è risolto a tempo di compilazione e questo ci permette di usarlo per creare array la cui dimensione è ricavata da altri array, segue un esempio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const byte message[] = { 0x95, 0x01, 0x00, 0x20
                       , 0x00, 0x00, 0x00, 0x00
                       , 0x00, 0x00, 0x00, 0x00
                       , 0x00, 0x00, 0x00, 0x00
                       , 0x00, 0x33 };

byte dest[sizeof message];

void setup() {
    Serial.begin(115200);
    Serial.println(sizeof message);
    Serial.println(sizeof dest);
}

void loop() {
    // empty loop
}

La dimensione del vettore message viene ricavata in automatico dal compilatore, mentre il vettore dest viene dimensionato ricavando la dimensione del vettore message attraverso l'operatore sizeof.

Alla riga 11 e 12 stampiamo la dimensione espressa in byte degli array message e dest ed entrambe hanno lo stessa dimensione (18 byte).

Quanto mostrato è valido solo nel caso in cui gli elementi del vettore sono di tipo byte, char o uint8_t, grandi 8-bit cioè 1 byte. Nel caso seguente vediamo come ricavare la dimensione in elementi di tipo più grande di 1 byte, ad esempio con un vettore di tipo long. Il programma è sempre lo stesso ma alla riga 1 e 7 al posto di byte ci scriviamo long. Il programma stampa due numeri differenti:

Dimensione in byte di message :     72
Dimensione in byte di dest :    288

Possiamo giustificare questi valori poiché sappiamo che il tipo long è grande 4 byte, per cui 72 / 4 = 18 elementi di tipo long. Il vettore dest è composto da 288 / 4 = 72 elementi di tipo long. L'operatore sizeof usato così sizeof(long) restituisce la dimensione in byte del tipo specificato tra parentesi e nel caso specifico resituisce il valore 4. Allora se volessimo stampare di quanti elementi è composto il vettore dest potremmo benissimo scrivere:

Serial.println(sizeof dest / sizeof(long));

Oppure ancora meglio:

Serial.println(sizeof dest / sizeof dest[0]);

Risulta migliore perché grazie alla sintassi sizeof dest[0] è il compilatore che ricava la dimensione del tipo di dato usato per dichiarare il vettore dest . Mentre nel caso di sizeof(long) è nostra responsabilità specificare il tipo di dato giusto. Quanto mostrato adesso dovrebbe suggerirci qualcosa, poiché siamo pigri e meno scriviamo meglio è per noi, proviamo a creare una macro del preprocessore di nome ITEMOF , così digitiamo qualche carattere in meno, ok vediamola:

#define ITEMOF(a) (sizeof a / sizeof a[0])

Proviamo ad usarla per dichiarare il vettore dest e per stampare il numero di elementi di cui è composto il vettore:

long dest[ITEMOF(message)];

Per stampare usiamo le due righe seguenti all'interno della funzione setup():

Serial.println(ITEMOF(message));
Serial.println(ITEMOF(dest));


Questo articolo ha subito modifiche a seguito di questo post, nel quale sono intervenuti nid69ita, Standardoil e gpb01 che ringranzio per avere argomentato.

Licenza Creative Commons
Quest' opera è distribuita con licenza Creative Commons Attribuzione - Non commerciale - Non opere derivate 3.0 Unported.