Abstract
La lettura di un sensore analogico non può essere ridotta
all’esecuzione di una singola istruzione
analogRead(). In un sistema destinato a funzionare
continuativamente occorre gestire l’alimentazione del sensore,
attendere la stabilizzazione del segnale, acquisire più campioni,
riconoscere i valori anomali, applicare un filtro e controllare
l’invio del risultato verso un sistema remoto.
La procedura descritta utilizza un MKR WiFi 1010 per leggere un sensore analogico di umidità del terreno. Il sensore viene alimentato soltanto durante la misura. Il convertitore analogico-digitale viene configurato a 12 bit e il risultato finale viene ricavato da dieci letture valide, ordinate e filtrate mediante eliminazione dei quattro valori estremi.
Il valore RAW filtrato viene convertito in un indice compreso
tra 0 e 10. Soltanto dopo il completamento della misura viene
attivata la connessione Wi-Fi, utilizzata per trasmettere RAW
e indice tramite HTTPS. Il programma è organizzato come
macchina a stati e non utilizza attese bloccanti mediante
delay().
Parole chiave: MKR WiFi 1010, ingresso analogico, ADC, sensore di umidità, acquisizione dati, filtro digitale, media troncata, HTTPS, macchina a stati, millis.
Obiettivo della procedura
Il sistema deve eseguire automaticamente una sequenza completa di misura e trasmissione:
- accendere il sensore;
- attendere la stabilizzazione elettrica;
- acquisire più letture analogiche;
- rifiutare i campioni esterni all’intervallo ammesso;
- ordinare i campioni validi;
- eliminare i valori estremi;
- calcolare il valore RAW filtrato;
- convertire il risultato in un indice da 0 a 10;
- spegnere il sensore;
- attivare il Wi-Fi;
- inviare i dati tramite HTTPS;
- disconnettere il Wi-Fi;
- attendere un’ora prima del ciclo successivo.
La misura e la comunicazione sono quindi due fasi separate. Il sensore non rimane alimentato durante l’attesa tra due acquisizioni e il Wi-Fi non rimane connesso permanentemente.
Collegamenti
Il sensore utilizza tre collegamenti:
- alimentazione del sensore collegata al pin digitale 6;
- massa del sensore collegata a GND;
- uscita analogica del sensore collegata all’ingresso A6.
const uint8_t PIN_SENSOR_PWR = 6;
const uint8_t PIN_SENSOR = A6;
Il pin 6 viene configurato come uscita digitale e utilizzato per controllare l’alimentazione del sensore.
pinMode(PIN_SENSOR_PWR, OUTPUT);
digitalWrite(PIN_SENSOR_PWR, LOW);
All’avvio della scheda il sensore viene mantenuto spento. L’accensione avviene soltanto all’inizio del ciclo di misura:
digitalWrite(PIN_SENSOR_PWR, HIGH);
Al termine dell’acquisizione viene spento:
digitalWrite(PIN_SENSOR_PWR, LOW);
Prima di alimentare direttamente un sensore mediante un pin digitale è necessario verificarne l’assorbimento. Se la corrente richiesta non è compatibile con il pin, l’alimentazione deve essere comandata mediante un transistor o un MOSFET esterno.
Configurazione del convertitore analogico-digitale
Nel setup() la risoluzione del convertitore
analogico-digitale viene impostata a 12 bit:
analogReadResolution(12);
Con questa configurazione il valore teorico restituito dalla conversione è compreso tra 0 e 4095.
La lettura viene eseguita mediante:
int raw = analogRead(PIN_SENSOR);
Il numero ottenuto è un valore RAW proporzionale alla tensione presente sul pin A6. Non rappresenta direttamente una percentuale di umidità e non costituisce una misura assoluta del contenuto d’acqua nel terreno.
Il valore dipende dalle caratteristiche del sensore, dall’alimentazione, dal terreno, dalla profondità di inserimento, dalla salinità, dalla temperatura e dal contatto fisico tra sonda e materiale misurato.
Configurazione dei tempi
Il programma utilizza temporizzazioni basate su
millis().
const unsigned long INTERVALLO_CICLO_MS =
60UL * 60UL * 1000UL;
const unsigned long STABILIZZAZIONE_MS =
3000UL;
const unsigned long INTERVALLO_LETTURE_MS =
300UL;
const unsigned long WIFI_TIMEOUT_MS =
90000UL;
const unsigned long HTTP_TIMEOUT_MS =
15000UL;
I parametri corrispondono a:
- un’ora tra due cicli completi;
- tre secondi di stabilizzazione del sensore;
- 300 millisecondi tra due letture analogiche;
- 90 secondi di timeout per la connessione Wi-Fi;
- 15 secondi di timeout per la risposta HTTP.
Il suffisso UL forza l’esecuzione dei calcoli
mediante valori di tipo unsigned long, lo stesso
tipo restituito dalla funzione millis().
Assenza di attese bloccanti
Il programma non utilizza delay(). Ogni attesa
viene controllata confrontando il valore corrente di
millis() con il momento in cui è iniziata la fase.
if (millis() - inizioStato < STABILIZZAZIONE_MS) {
return;
}
Durante il periodo di attesa il programma continua quindi a eseguire il ciclo principale e può controllare lo stato corrente senza rimanere bloccato dentro una funzione.
La forma:
millis() - inizioStato
è inoltre compatibile con il ritorno a zero periodico del
contatore di millis(), purché le variabili
coinvolte siano di tipo unsigned long.
Macchina a stati
Il programma è suddiviso in sei stati operativi:
enum StatoSistema {
STATO_AVVIO_CICLO,
STATO_STABILIZZAZIONE,
STATO_LETTURE,
STATO_ATTESA_WIFI,
STATO_ATTESA_HTTP,
STATO_ATTESA_ORA
};
StatoSistema stato = STATO_AVVIO_CICLO;
Il ciclo principale richiama soltanto la funzione associata allo stato corrente:
void loop() {
switch (stato) {
case STATO_AVVIO_CICLO:
avviaCiclo();
break;
case STATO_STABILIZZAZIONE:
gestisciStabilizzazione();
break;
case STATO_LETTURE:
gestisciLetture();
break;
case STATO_ATTESA_WIFI:
gestisciWiFi();
break;
case STATO_ATTESA_HTTP:
gestisciHTTP();
break;
case STATO_ATTESA_ORA:
if (millis() - inizioAttesaOra >=
INTERVALLO_CICLO_MS) {
stato = STATO_AVVIO_CICLO;
}
break;
}
}
Questa struttura separa l’avvio della misura, la stabilizzazione, l’acquisizione, la connessione, la risposta del server e l’attesa tra due cicli.
Avvio del ciclo di acquisizione
All’inizio di ogni ciclo vengono incrementato il contatore generale e azzerati i contatori delle letture:
void avviaCiclo() {
cicli++;
numeroLettureValide = 0;
numeroTentativi = 0;
digitalWrite(PIN_SENSOR_PWR, HIGH);
inizioStato = millis();
stato = STATO_STABILIZZAZIONE;
}
Da questo momento il sensore è alimentato, ma la lettura analogica non viene ancora eseguita.
Stabilizzazione del sensore
Dopo l’accensione vengono attesi tre secondi. Questo intervallo permette all’alimentazione e all’uscita analogica del sensore di raggiungere una condizione sufficientemente stabile.
void gestisciStabilizzazione() {
if (millis() - inizioStato < STABILIZZAZIONE_MS) {
return;
}
ultimaLettura = millis() - INTERVALLO_LETTURE_MS;
stato = STATO_LETTURE;
}
L’assegnazione:
ultimaLettura = millis() - INTERVALLO_LETTURE_MS;
consente di effettuare il primo campionamento immediatamente dopo il passaggio allo stato delle letture, senza aggiungere un’ulteriore attesa di 300 millisecondi.
Numero di letture e intervallo di validità
Il sistema deve ottenere dieci letture valide:
const uint8_t NUM_LETTURE_VALIDE = 10;
Per impedire che l’acquisizione prosegua indefinitamente viene imposto un massimo di cinquanta tentativi:
const uint8_t MAX_TENTATIVI_LETTURA = 50;
Un campione è considerato valido soltanto se compreso nell’intervallo da 1000 a 2900:
const int RAW_VALIDO_MIN = 1000;
const int RAW_VALIDO_MAX = 2900;
La verifica viene eseguita prima di inserire il campione nel vettore:
if (raw >= RAW_VALIDO_MIN &&
raw <= RAW_VALIDO_MAX) {
letture[numeroLettureValide] = raw;
numeroLettureValide++;
}
Le letture inferiori a 1000 o superiori a 2900 vengono scartate e non partecipano al filtro.
Queste soglie appartengono alla specifica installazione. Non sono valori universali e devono essere verificate nuovamente quando cambiano sensore, alimentazione, terreno, cablaggio o condizioni operative.
Acquisizione dei dieci campioni validi
Le letture vengono conservate in un vettore:
int letture[NUM_LETTURE_VALIDE];
La funzione di acquisizione controlla innanzitutto che siano trascorsi almeno 300 millisecondi dalla lettura precedente.
if (millis() - ultimaLettura <
INTERVALLO_LETTURE_MS) {
return;
}
ultimaLettura = millis();
int raw = analogRead(PIN_SENSOR);
numeroTentativi++;
Se il valore è valido viene memorizzato. In caso contrario viene conteggiato soltanto il tentativo.
Quando sono disponibili dieci letture valide il sensore viene spento e il programma passa al calcolo:
if (numeroLettureValide >=
NUM_LETTURE_VALIDE) {
digitalWrite(PIN_SENSOR_PWR, LOW);
calcolaRisultato();
avviaWiFi();
return;
}
Se vengono raggiunti cinquanta tentativi senza ottenere dieci letture valide, il sensore viene spento e il ciclo termina senza trasmettere una misura incompleta:
if (numeroTentativi >=
MAX_TENTATIVI_LETTURA) {
digitalWrite(PIN_SENSOR_PWR, LOW);
terminaCiclo();
}
Ordinamento delle letture
Prima del filtraggio i dieci campioni vengono ordinati in modo crescente:
void ordinaLetture() {
for (uint8_t i = 0;
i < NUM_LETTURE_VALIDE - 1;
i++) {
for (uint8_t j = i + 1;
j < NUM_LETTURE_VALIDE;
j++) {
if (letture[j] < letture[i]) {
int temporaneo = letture[i];
letture[i] = letture[j];
letture[j] = temporaneo;
}
}
}
}
Dopo l’ordinamento:
letture[0]contiene il valore più basso;letture[1]contiene il secondo valore più basso;letture[8]contiene il secondo valore più alto;letture[9]contiene il valore più alto.
Il numero ridotto di elementi rende adeguato un algoritmo semplice e leggibile.
Filtro mediante media troncata
Calcolare la media di tutti i campioni renderebbe il risultato sensibile a letture isolate molto più alte o più basse rispetto al gruppo principale.
Il filtro elimina i due valori più bassi e i due valori più alti. La media viene calcolata esclusivamente sui sei valori centrali, memorizzati nelle posizioni da 2 a 7.
long somma = 0;
for (uint8_t i = 2; i <= 7; i++) {
somma += letture[i];
}
rawMedio = somma / 6;
La variabile somma è dichiarata come
long. In questa applicazione anche un
int sarebbe sufficiente, ma il tipo più ampio
evita problemi se in seguito vengono aumentati il numero
dei campioni o la risoluzione utilizzata.
Il metodo è una media troncata: riduce l’effetto dei disturbi impulsivi senza richiedere librerie esterne o calcoli complessi.
Non può però correggere errori sistematici, deriva lenta, cattiva calibrazione o spostamenti reali del segnale.
Conversione del valore RAW nella scala 0–10
Il valore RAW filtrato viene convertito in un indice relativo. I riferimenti utilizzati sono:
const int RAW_SECCO = 2500;
const int RAW_BAGNATO = 1150;
La corrispondenza impostata è:
- RAW 2500 uguale a indice 0;
- RAW 1150 uguale a indice 10.
In questa installazione il valore RAW diminuisce quando aumenta l’umidità.
float calcolaIndice(int raw) {
float valore =
(float)(RAW_SECCO - raw) * 10.0 /
(float)(RAW_SECCO - RAW_BAGNATO);
if (valore < 0.0) {
valore = 0.0;
}
if (valore > 10.0) {
valore = 10.0;
}
return valore;
}
Il limite inferiore impedisce la produzione di indici negativi. Il limite superiore impedisce di superare il valore 10.
Questa scala è un indice operativo basato su due riferimenti sperimentali. Non deve essere presentata come percentuale volumetrica di acqua nel terreno.
Separazione tra acquisizione e connessione Wi-Fi
Il Wi-Fi viene attivato soltanto dopo il completamento della misura e del filtro.
La sequenza reale è:
- accensione del sensore;
- stabilizzazione;
- acquisizione;
- spegnimento del sensore;
- calcolo del risultato;
- attivazione del Wi-Fi;
- invio HTTPS;
- disconnessione;
- attesa del ciclo successivo.
La connessione viene avviata mediante:
void avviaWiFi() {
client.stop();
WiFi.disconnect();
inizioStato = millis();
WiFi.begin(ssid, password);
stato = STATO_ATTESA_WIFI;
}
Le credenziali non devono essere pubblicate nel file HTML, nel repository o negli esempi distribuiti.
const char* ssid = "NOME_RETE";
const char* password = "PASSWORD_RETE";
Non è presente un controllo Wi-Fi ogni minuto. La connessione viene controllata esclusivamente durante la fase necessaria alla trasmissione.
Verifica della connessione
La connessione viene considerata pronta soltanto quando risultano vere due condizioni:
- lo stato Wi-Fi è connesso;
- la scheda dispone di un indirizzo IP non nullo.
bool ipValido(IPAddress ip) {
return (
ip[0] != 0 ||
ip[1] != 0 ||
ip[2] != 0 ||
ip[3] != 0
);
}
bool wifiPronto() {
return (
WiFi.status() == WL_CONNECTED &&
ipValido(WiFi.localIP())
);
}
Se la connessione non viene completata entro 90 secondi, il tentativo viene considerato fallito.
if (millis() - inizioStato >=
WIFI_TIMEOUT_MS) {
WiFi.disconnect();
wifiFail++;
terminaCiclo();
}
Il riavvio viene eseguito soltanto dopo dieci errori Wi-Fi consecutivi:
const uint8_t MAX_WIFI_FAIL = 10;
Preparazione dei dati per l’invio
I dati trasmessi sono:
raw, contenente il valore filtrato;idx, contenente l’indice da 0 a 10.
Il corpo della richiesta viene costruito nel formato
application/x-www-form-urlencoded:
String postData = "raw=" + String(rawMedio);
postData += "&idx=" + String(indiceUmidita, 2);
Un contenuto trasmesso può assumere questa forma:
raw=1784&idx=5.30
Il dominio e il percorso del ricevitore devono essere configurati nel programma senza inserire credenziali o altri dati riservati nell’articolo.
const char* serverHost = "server.example";
const char* serverPath = "/ricezione/logger.php";
const uint16_t serverPort = 443;
Invio tramite HTTPS
La connessione cifrata utilizza un client SSL:
WiFiSSLClient client;
Dopo la connessione al server viene inviata una richiesta POST:
client.print("POST ");
client.print(serverPath);
client.println(" HTTP/1.1");
client.print("Host: ");
client.println(serverHost);
client.println(
"User-Agent: MKR-WiFi-1010"
);
client.println(
"Content-Type: application/x-www-form-urlencoded"
);
client.print("Content-Length: ");
client.println(postData.length());
client.println("Connection: close");
client.println();
client.print(postData);
L’intestazione Content-Length deve corrispondere
esattamente al numero di caratteri contenuti in
postData.
L’intestazione Connection: close indica che
la connessione deve essere chiusa al termine della risposta.
Controllo della risposta HTTP
Il programma legge la prima riga restituita dal server. Una risposta corretta può essere:
HTTP/1.1 200 OK
Il codice numerico viene estratto dalla prima riga:
int estraiCodiceHTTP(const String& riga) {
if (!riga.startsWith("HTTP/")) {
return -1;
}
int primoSpazio = riga.indexOf(' ');
if (primoSpazio < 0) {
return -1;
}
if (riga.length() < primoSpazio + 4) {
return -1;
}
return riga.substring(
primoSpazio + 1,
primoSpazio + 4
).toInt();
}
La trasmissione viene considerata riuscita quando il codice appartiene all’intervallo da 200 a 299:
if (codice >= 200 && codice < 300) {
httpFail = 0;
terminaCiclo();
}
Il controllo non è quindi limitato al solo codice 200.
Errori HTTP e riavvio
Il programma gestisce:
- connessione HTTPS non riuscita;
- risposta HTTP non valida;
- connessione chiusa senza risposta;
- mancata risposta entro 15 secondi.
Dopo ogni errore il contatore viene incrementato:
if (httpFail < 255) {
httpFail++;
}
Il riavvio avviene dopo cinque errori HTTP consecutivi:
const uint8_t MAX_HTTP_FAIL = 5;
if (httpFail >= MAX_HTTP_FAIL) {
riavvia("[REBOOT] troppi errori HTTP");
}
Una trasmissione corretta azzera il contatore:
httpFail = 0;
Intervallo tra due misure
Al termine del ciclo viene memorizzato il momento di inizio dell’attesa:
inizioAttesaOra = millis();
stato = STATO_ATTESA_ORA;
Il ciclo successivo parte quando è trascorsa un’ora:
if (millis() - inizioAttesaOra >=
INTERVALLO_CICLO_MS) {
stato = STATO_AVVIO_CICLO;
}
Durante l’attesa il sensore rimane spento, il client è chiuso e il Wi-Fi è disconnesso.
Riavvio periodico della scheda
Il programma esegue un riavvio dopo dodici cicli:
const uint8_t REBOOT_CICLI = 12;
Con un intervallo di un’ora, il riavvio avviene approssimativamente ogni dodici ore.
if (cicli >= REBOOT_CICLI) {
riavvia("[REBOOT] completati 12 cicli");
}
Prima del riavvio vengono spenti il sensore, il client e la connessione Wi-Fi:
void riavvia(const char* motivo) {
digitalWrite(PIN_SENSOR_PWR, LOW);
client.stop();
WiFi.disconnect();
Serial.println();
Serial.println(motivo);
NVIC_SystemReset();
}
Non è presente un watchdog. Il riavvio è comandato direttamente dalla logica applicativa.
Sequenza procedurale completa
- configurare il pin 6 come uscita;
- mantenere inizialmente spento il sensore;
- configurare l’ADC a 12 bit;
- accendere il sensore all’inizio del ciclo;
- attendere tre secondi;
- leggere A6 ogni 300 millisecondi;
- accettare soltanto valori compresi tra 1000 e 2900;
- continuare fino a ottenere dieci letture valide;
- interrompere dopo un massimo di cinquanta tentativi;
- spegnere il sensore;
- ordinare le dieci letture;
- eliminare le due più basse;
- eliminare le due più alte;
- calcolare la media delle sei centrali;
- convertire il RAW nella scala da 0 a 10;
- attivare il Wi-Fi;
- verificare connessione e indirizzo IP;
- aprire la connessione HTTPS;
- inviare RAW e indice mediante POST;
- leggere il codice restituito dal server;
- chiudere client e Wi-Fi;
- attendere un’ora;
- ripetere il ciclo;
- riavviare la scheda dopo dodici cicli.
Controlli prima della messa in servizio
Prima dell’utilizzo continuativo devono essere verificati i collegamenti, l’assorbimento del sensore e l’effettivo intervallo dei valori RAW.
È necessario controllare dalla porta seriale:
- il numero dei tentativi necessari per ogni misura;
- la distribuzione delle dieci letture valide;
- i quattro valori eliminati dal filtro;
- il RAW medio ottenuto;
- l’indice calcolato;
- la qualità della connessione Wi-Fi;
- il codice HTTP restituito dal server;
- la frequenza degli errori e dei riavvii.
I valori RAW_VALIDO_MIN,
RAW_VALIDO_MAX, RAW_SECCO e
RAW_BAGNATO devono essere determinati sulla
specifica installazione.
Limiti della misura
Il filtro riduce l’influenza di campioni isolati, ma non corregge automaticamente:
- deriva lenta del sensore;
- degrado della sonda;
- variazioni dovute alla temperatura;
- variazioni dovute alla salinità;
- spostamento fisico del sensore;
- disomogeneità del terreno;
- errori sistematici di calibrazione;
- interruzioni del cablaggio;
- variazioni reali molto rapide.
L’indice da 0 a 10 deve essere interpretato come indicatore relativo dell’installazione e non come misura assoluta trasferibile automaticamente ad altri sensori o terreni.
Conclusioni
Una lettura analogica affidabile richiede una procedura controllata. Il sistema descritto alimenta il sensore soltanto durante la misura, configura il convertitore a 12 bit, acquisisce dieci campioni validi e limita il numero massimo di tentativi.
Le letture vengono ordinate. I due valori più bassi e i due più alti vengono eliminati, mentre il risultato è calcolato sui sei valori centrali. Il valore RAW filtrato viene quindi convertito in un indice relativo compreso tra 0 e 10.
La connessione Wi-Fi viene attivata soltanto dopo il
completamento della misura. RAW e indice vengono trasmessi
tramite HTTPS, controllando il codice restituito dal server.
Timeout, errori consecutivi e riavvio periodico sono gestiti
senza utilizzare delay() e senza mantenere
permanentemente attivi sensore e rete.
Fonti principali
MKR WiFi 1010, documentazione tecnica della scheda.
Microchip Technology, documentazione tecnica del microcontrollore SAMD21.
u-blox, documentazione tecnica del modulo radio NINA-W102.
WiFiNINA, documentazione della libreria di comunicazione.
RFC 9110, HTTP Semantics.
Codice operativo del sistema di acquisizione descritto nell’articolo.