Sensore di umidità con ESP32-C6: SHT40 e umidità del terreno
Misurare l’umidità è una delle prime cose che si chiedono a un nodo sensore: in una serra, in cantina, in un armadio rack o in una stazione meteo. In questo tutorial colleghi all’ESP32-C6 due sensori molto diversi tra loro: il Sensirion SHT40, digitale e preciso, per temperatura e umidità relativa dell’aria, e un sensore capacitivo di umidità del terreno, analogico ed economico, letto con l’ADC. Usi il nuovo driver I2C master e il driver ADC oneshot con calibrazione di ESP-IDF 5.4, e calcoli anche il punto di rugiada.
Cosa serve
| Componente | Esempio | Note |
|---|---|---|
| Scheda ESP32-C6 | ESP32-C6-DevKitC-1 | Qualsiasi board C6 va bene, adatta i pin |
| Sensore T/RH digitale | Breakout SHT40 (Adafruit, Seeed o generico) | Alimentazione 3,3 V, interfaccia I2C, indirizzo 0x44 per la versione SHT40-AD1B |
| Sensore umidità terreno | Modulo “Capacitive Soil Moisture Sensor v1.2” o simile | Uscita analogica; alimentalo a 3,3 V |
| Resistenze di pull-up | 2 × 4,7 kΩ | Solo se il breakout non le ha già |
| Varie | Breadboard, cavetti, bicchiere d’acqua | L’acqua serve per la calibrazione del sensore terreno |
Come funziona
Umidità relativa e punto di rugiada
L’aria può contenere una quantità massima di vapore acqueo che cresce con la temperatura. L’umidità relativa (RH) è il rapporto, in percentuale, tra la pressione parziale del vapore presente e quella di saturazione alla stessa temperatura. Per questo RH da sola dice poco: 60 % a 10 °C e 60 % a 30 °C sono quantità d’acqua molto diverse.
Il punto di rugiada è la temperatura alla quale, raffreddando l’aria senza cambiarne il contenuto di vapore, si arriva alla saturazione e compare la condensa. È il numero che conta davvero per capire se su una parete fredda o dentro un contenitore si formerà acqua. Lo calcoli con la formula di Magnus, usando le costanti che Sensirion indica per l’acqua liquida (β = 17,62, λ = 243,12 °C):
γ = ln(RH/100) + β·T / (λ + T) Td = λ·γ / (β − γ)
È un’approssimazione, ma nel campo di temperature tipico di casa e giardino l’errore è ben inferiore all’incertezza del sensore.
SHT40
Dentro l’SHT40 c’è un sensore capacitivo di umidità (un dielettrico polimerico che assorbe vapore e cambia capacità) e un sensore di temperatura, con ADC e compensazione già integrati. Tu invii un comando da un byte, aspetti la conversione e leggi sei byte: due per la temperatura, due per l’umidità, ognuno seguito da un CRC-8. Le formule di conversione del datasheet sono:
T [°C] = −45 + 175 · S_T / 65535 RH [%] = −6 + 125 · S_RH / 65535
Il valore di RH può uscire leggermente da 0-100 %: il datasheet stesso consiglia di limitarlo.
Sensore capacitivo del terreno
Il modulo capacitivo ha due “armature” ricavate sul PCB e coperte dal solder mask: non c’è metallo esposto e quindi niente corrosione, a differenza dei vecchi sensori resistivi a forchetta. Un oscillatore (spesso un 555) pilota il condensatore formato dalle piste e dal terreno; più il terreno è bagnato, più sale la costante dielettrica e la capacità, e più scende la tensione in uscita. Il risultato non è una misura assoluta di contenuto d’acqua: devi calibrare il sensore agli estremi (aria e acqua) e ragionare in percentuale relativa.
Collegamenti
| Pin sensore | GPIO ESP32-C6 | Note |
|---|---|---|
| SHT40 VIN / VCC | 3V3 | Molti breakout accettano anche 5 V, ma a 3,3 V i livelli I2C sono già corretti |
| SHT40 GND | GND | |
| SHT40 SDA | GPIO6 | Pull-up 4,7 kΩ verso 3,3 V (spesso già sul breakout) |
| SHT40 SCL | GPIO7 | Pull-up 4,7 kΩ verso 3,3 V |
| Sensore terreno VCC | 3V3 | Non usare 5 V: l’uscita potrebbe superare il fondo scala dell’ADC |
| Sensore terreno GND | GND | |
| Sensore terreno AOUT | GPIO2 (ADC1_CH2) | Sull’ESP32-C6 l’ADC1 è su GPIO0-6 |
Il driver abilita anche i pull-up interni dei GPIO, ma sono deboli (decine di kΩ): vanno bene per un test a 100 kHz su cavetti corti, non per un bus lungo. Se il tuo breakout ha già i pull-up non aggiungerne altri in parallelo senza motivo. Il pin GPIO2 è libero sulla DevKitC-1 e non è un pin di strapping, a differenza di GPIO8, 9 e 15.
La periferica dell’ESP32-C6
Per l’SHT40 usi il driver driver/i2c_master.h, introdotto in ESP-IDF 5.2 e ormai quello di riferimento: crei un bus con i2c_new_master_bus(), ci agganci un device con il suo indirizzo e la sua velocità, e poi chiami i2c_master_transmit() e i2c_master_receive(). Niente più command link da costruire a mano come nel vecchio driver/i2c.h, che in ESP-IDF 5.4 è deprecato. La funzione i2c_master_probe() ti dice subito se all’indirizzo 0x44 risponde qualcuno.
Per il sensore del terreno usi esp_adc/adc_oneshot.h: una conversione a richiesta, perfetta per un segnale che cambia in minuti. L’attenuazione ADC_ATTEN_DB_12 porta il fondo scala a circa 3,3 V. Il valore grezzo dell’ADC non è lineare né identico tra chip diversi, per cui crei uno schema di calibrazione con adc_cali_create_scheme_curve_fitting() (sull’ESP32-C6 è supportato il curve fitting, che usa i coefficienti scritti in eFuse in fabbrica) e converti in millivolt con adc_cali_raw_to_voltage().
Il codice
Crea un progetto ESP-IDF con main/main.c e in main/CMakeLists.txt dichiara le dipendenze PRIV_REQUIRES esp_driver_i2c esp_adc. Questo è il main.c completo:
/*
* Sensore di umidita' con ESP32-C6 (ESP-IDF 5.4)
* - SHT40 su I2C (nuovo driver i2c_master): temperatura, umidita' relativa, punto di rugiada
* - Sensore capacitivo di umidita' del terreno su ADC1 (adc_oneshot + calibrazione)
* www.webappportal.com
*/
#include <stdio.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_log.h"
static const char *TAG = "umidita";
/* ---------- Pin e parametri ---------- */
#define I2C_SDA_GPIO GPIO_NUM_6
#define I2C_SCL_GPIO GPIO_NUM_7
#define SHT40_ADDR 0x44 /* SHT40-AD1B: 0x44 (altre varianti: vedi datasheet) */
#define SHT40_CMD_MEAS_HIGH 0xFD /* misura T+RH, alta ripetibilita', senza heater */
#define SHT40_CMD_SOFTRESET 0x94
#define SOIL_ADC_CHANNEL ADC_CHANNEL_2 /* GPIO2 = ADC1_CH2 sull'ESP32-C6 */
#define SOIL_SAMPLES 16
/* Valori da misurare sul TUO sensore (vedi articolo): mV in aria e in acqua */
#define SOIL_MV_DRY 2600
#define SOIL_MV_WET 1150
static i2c_master_dev_handle_t sht40;
static adc_oneshot_unit_handle_t adc1;
static adc_cali_handle_t adc1_cali;
static bool adc_calibrated;
/* CRC-8 del datasheet Sensirion: polinomio 0x31, init 0xFF */
static uint8_t sht_crc8(const uint8_t *data, int len)
{
uint8_t crc = 0xFF;
for (int i = 0; i < len; i++) {
crc ^= data[i];
for (int b = 0; b < 8; b++) {
crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1);
}
}
return crc;
}
static esp_err_t sht40_read(float *temp_c, float *rh)
{
uint8_t cmd = SHT40_CMD_MEAS_HIGH;
uint8_t rx[6];
esp_err_t err = i2c_master_transmit(sht40, &cmd, 1, 100);
if (err != ESP_OK) {
return err;
}
/* Alta ripetibilita': max ~8,3 ms secondo il datasheet, aspettiamo di piu' */
vTaskDelay(pdMS_TO_TICKS(20));
err = i2c_master_receive(sht40, rx, sizeof(rx), 100);
if (err != ESP_OK) {
return err;
}
if (sht_crc8(&rx[0], 2) != rx[2] || sht_crc8(&rx[3], 2) != rx[5]) {
return ESP_ERR_INVALID_CRC;
}
uint16_t t_raw = (uint16_t)((rx[0] << 8) | rx[1]);
uint16_t h_raw = (uint16_t)((rx[3] << 8) | rx[4]);
*temp_c = -45.0f + 175.0f * (float)t_raw / 65535.0f;
float h = -6.0f + 125.0f * (float)h_raw / 65535.0f;
/* Il datasheet consiglia di limitare RH all'intervallo 0..100 % */
if (h < 0.0f) {
h = 0.0f;
}
if (h > 100.0f) {
h = 100.0f;
}
*rh = h;
return ESP_OK;
}
/* Punto di rugiada con la formula di Magnus (costanti Sensirion per acqua liquida) */
static float dew_point_c(float t, float rh)
{
const float beta = 17.62f;
const float lambda = 243.12f;
if (rh < 0.1f) {
rh = 0.1f; /* evita log(0) */
}
float gamma = logf(rh / 100.0f) + beta * t / (lambda + t);
return lambda * gamma / (beta - gamma);
}
static void i2c_init(void)
{
i2c_master_bus_config_t bus_cfg = {
.i2c_port = -1, /* scelta automatica della porta */
.sda_io_num = I2C_SDA_GPIO,
.scl_io_num = I2C_SCL_GPIO,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true, /* utile solo come riserva: usa pull-up esterni */
};
i2c_master_bus_handle_t bus;
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus));
ESP_ERROR_CHECK(i2c_master_probe(bus, SHT40_ADDR, 100));
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = SHT40_ADDR,
.scl_speed_hz = 100000,
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus, &dev_cfg, &sht40));
uint8_t cmd = SHT40_CMD_SOFTRESET;
ESP_ERROR_CHECK(i2c_master_transmit(sht40, &cmd, 1, 100));
vTaskDelay(pdMS_TO_TICKS(10));
}
static void adc_init(void)
{
adc_oneshot_unit_init_cfg_t unit_cfg = {
.unit_id = ADC_UNIT_1,
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&unit_cfg, &adc1));
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_12, /* fondo scala di circa 3,3 V */
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1, SOIL_ADC_CHANNEL, &chan_cfg));
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
adc_cali_curve_fitting_config_t cali_cfg = {
.unit_id = ADC_UNIT_1,
.chan = SOIL_ADC_CHANNEL,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
adc_calibrated = (adc_cali_create_scheme_curve_fitting(&cali_cfg, &adc1_cali) == ESP_OK);
#endif
if (!adc_calibrated) {
ESP_LOGW(TAG, "Calibrazione ADC non disponibile: uso i valori grezzi");
}
}
static int soil_read_mv(void)
{
int sum = 0;
for (int i = 0; i < SOIL_SAMPLES; i++) {
int raw = 0;
ESP_ERROR_CHECK(adc_oneshot_read(adc1, SOIL_ADC_CHANNEL, &raw));
sum += raw;
vTaskDelay(pdMS_TO_TICKS(2));
}
int raw_avg = sum / SOIL_SAMPLES;
int mv = raw_avg;
if (adc_calibrated) {
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali, raw_avg, &mv));
}
return mv;
}
/* Sensore capacitivo: tensione ALTA = terreno secco, BASSA = terreno bagnato */
static float soil_percent(int mv)
{
float pct = 100.0f * (float)(SOIL_MV_DRY - mv) / (float)(SOIL_MV_DRY - SOIL_MV_WET);
if (pct < 0.0f) {
pct = 0.0f;
}
if (pct > 100.0f) {
pct = 100.0f;
}
return pct;
}
void app_main(void)
{
i2c_init();
adc_init();
ESP_LOGI(TAG, "SHT40 e sensore terreno pronti");
while (1) {
float t, rh;
esp_err_t err = sht40_read(&t, &rh);
if (err == ESP_OK) {
ESP_LOGI(TAG, "Aria: T=%.2f C RH=%.1f %% Rugiada=%.1f C",
t, rh, dew_point_c(t, rh));
} else {
ESP_LOGE(TAG, "Errore lettura SHT40: %s", esp_err_to_name(err));
}
int mv = soil_read_mv();
ESP_LOGI(TAG, "Terreno: %d mV umidita' relativa=%.0f %%", mv, soil_percent(mv));
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
I punti chiave:
sht40_read(): invia0xFD(misura ad alta ripetibilità, heater spento), aspetta 20 ms (il datasheet indica un massimo di circa 8,3 ms, ma con il tick FreeRTOS di default a 10 ms conviene stare larghi) e legge 6 byte. Se un CRC non torna la funzione restituisceESP_ERR_INVALID_CRC: su un bus disturbato è meglio scartare una lettura che loggare un valore sbagliato.sht_crc8(): CRC-8 con polinomio 0x31 e valore iniziale 0xFF, come da datasheet.dew_point_c(): la formula di Magnus vista sopra; il limite inferiore su RH evita il logaritmo di zero.adc_init(): se la calibrazione non è disponibile (eFuse non programmati) il codice continua con i valori grezzi e lo segnala con un warning.soil_read_mv(): media di 16 letture per ridurre il rumore del 555 e dell’ADC.SOIL_MV_DRYeSOIL_MV_WET: i valori nel codice sono solo segnaposto. Misura il tuo sensore: annota i mV con il sensore asciutto in aria, poi con la parte sensibile immersa in acqua fino alla linea di riferimento stampata sul PCB (mai oltre: l’elettronica non è impermeabile), e sostituiscili. La funzionesoil_percent()fa una semplice interpolazione lineare tra i due estremi.
Prova e risultati attesi
Compila e carica con idf.py -p COMx flash monitor. Con il sensore del terreno in aria dovresti vedere qualcosa del genere (i valori dipendono dall’ambiente e dal tuo esemplare):
I (312) umidita: SHT40 e sensore terreno pronti I (342) umidita: Aria: T=23.41 C RH=48.7 % Rugiada=12.0 C I (382) umidita: Terreno: 2594 mV umidita' relativa=0 % I (2392) umidita: Aria: T=23.43 C RH=48.6 % Rugiada=12.0 C I (2432) umidita: Terreno: 1480 mV umidita' relativa=77 %
Per verificare l’SHT40 basta alitarci sopra: l’umidità sale di decine di punti in un secondo e torna giù in poche decine di secondi. Per il sensore del terreno, infilalo in un vaso appena annaffiato e poi in uno asciutto.
Problemi comuni
- Il programma si ferma subito con
ESP_ERR_NOT_FOUNDsui2c_master_probe: SDA e SCL invertiti, sensore non alimentato, oppure hai una variante SHT40 con indirizzo diverso da 0x44 (esistono versioni a 0x45 e 0x46: controlla la sigla completa sul datasheet). - Errori CRC saltuari: cavi lunghi o pull-up troppo deboli. Aggiungi pull-up esterni da 4,7 kΩ o scendi di frequenza.
- Temperatura sempre qualche grado più alta del reale: il sensore è troppo vicino all’ESP32-C6 o al regolatore, che scaldano. Allontanalo con qualche centimetro di cavo.
- RH bloccata al 100 % dopo una notte in ambiente saturo: gli SHT4x hanno un heater integrato proprio per recuperare dopo lunghe esposizioni a condensa; va usato con parsimonia e seguendo il datasheet (i comandi sono diversi da quello di misura normale).
- Il sensore del terreno legge circa 3300 mV fissi: lo stai alimentando a 5 V oppure il pin AOUT è scollegato. Alcuni cloni economici hanno anche un regolatore mancante o un timer 555 sbagliato: se l’uscita non cambia tra aria e acqua, cambia modulo.
- Letture che derivano nei giorni: i bordi del PCB tagliati non sono protetti e assorbono acqua. Una mano di smalto trasparente o di resina sui bordi e sull’elettronica ne allunga molto la vita.
Va oltre
- Porta i valori di temperatura, umidità e punto di rugiada nella dashboard della stazione meteo, oppure inviali via MQTT a Home Assistant come nei progetti della sezione domotica.
- Per un nodo a batteria, alimenta il sensore del terreno da un GPIO o da un MOSFET solo durante la misura e metti l’ESP32-C6 in deep sleep tra una lettura e l’altra.
- Salva i valori di calibrazione del terreno in NVS invece di tenerli nel codice, con un comando seriale per acquisirli sul campo.
- Altri tutorial sulla famiglia ESP32 li trovi nella sezione ESP32.
