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 (SDA(A4)/SCL(A5)) 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 (PC5), mentre SDA è una funzione alternativa del bit4 della stessa porta C (PC4). 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 (Interrupt 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