immagine copertina del post


Dimenticate il DHT11 e le sue letture approssimative. Se volete davvero domare la termodinamica di un sistema, serve un loop di controllo serio, non un giocattolo.

In questo progetto ho portato l’ESP32 oltre il semplice monitoraggio passivo. Partendo dalle saldature a stagno su convertitori di tensione e ventole PWM — perché i breadboard sono per i prototipi che non funzionano — ho implementato un algoritmo PID (Proporzionale-Integrale-Derivativo) puro, interfacciato a un sensore DS18B20 via protocollo OneWire.

Il risultato? Un sistema di raffreddamento attivo che non si limita a “scattare” in modo isterico quando supera una soglia, ma accompagna la temperatura verso il setpoint con una precisione chirurgica di 0.06°C. Niente interfacce pesanti, niente bloatware: solo codice C++ RAW, log via terminale e automazione industriale nel palmo di una mano.

Perché il controllo ON/OFF ha fallito (e il PID ha vinto)

Chiunque abbia provato a raffreddare un piccolo ambiente (come una camera di fermentazione o un rack server DIY) conosce il problema dell’overshoot. Con un termostato classico (ON/OFF):

  1. La temperatura sale.
  2. La ventola parte al 100%.
  3. Il sensore si raffredda in ritardo, ma l’aria fredda continua a spingere.
  4. La temperatura crolla ben sotto il target.

Questo crea un’onda sinusoidale di temperatura che stressa i componenti. Il PID, invece, modula la potenza della ventola (PWM) calcolando non solo l’errore attuale, ma anche la sua storia (Integrale) e la sua velocità di cambiamento (Derivativo). È la differenza tra inchiodare in auto e frenare dolcemente al semaforo.

L’Hardware: Scelte Critiche

Per ottenere risultati stabili, ho dovuto eliminare due colli di bottiglia:

  1. Il Sensore: Il DHT11 ha una risoluzione ridicola (spesso ±1°C) e un protocollo lento. Il DS18B20 lavora su bus OneWire digitale e offre risoluzioni decimale reali (es. 17.19°C, 17.25°C).
  2. Le Connessioni: I jumper ballano. Per un termine Derivativo ($K_d$) stabile, il segnale deve essere pulito. Saldare i collegamenti è stato l’unico modo per eliminare il rumore elettrico che il PID avrebbe interpretato come picchi di calore.

Pinout utilizzato su ESP32

  • GPIO 27: Bus OneWire (DS18B20) con resistenza di pull-up da 4.7kΩ.
  • GPIO 33: Output PWM per il controllo MOSFET/Ventola.
Schema collegamento ESP32 e Sensore DS18B20

Figura 1: Schema dei collegamenti hardware per il controllo PID

ventola DC estratta da una workstation

Figura 2: Hardware di recupero: ventola DC estratta da una workstation dismessa.

prototipo della ventola con controllo PID

Figura 3: Immagine reale del prototipo della ventola con controllo PID.

Il Codice: Implementazione RAW

Ecco l’implementazione completa. Ho evitato librerie PID pronte per avere il controllo totale sulla formula e sul “Windup” dell’integrale. Notate l’uso di Serial.printf per generare un log CSV-ready direttamente da hardware.



#include <Arduino.h>
#include <OneWire.h>
#include <DallasTemperature.h>

// --- HARDWARE ---
#define ONE_WIRE_BUS 27
#define FAN_PIN 33

// --- PID ---
double setpoint = 17.5;
double Kp = 130.0; 
double Ki = 12.0;   
double Kd = 30.0;  

double input, lastInput, cumError;
bool fanWasOff = true;

// --- PWM ---
#define PWM_CHAN 0
#define PWM_FREQ 5000
#define PWM_RES 8

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

void setup() {
    Serial.begin(115200);
    pinMode(ONE_WIRE_BUS, INPUT_PULLUP);
    sensors.begin();
    
    ledcSetup(PWM_CHAN, PWM_FREQ, PWM_RES);
    ledcAttachPin(FAN_PIN, PWM_CHAN);
    ledcWrite(PWM_CHAN, 0);

    delay(1000);
    Serial.print("--- START PID LOG ---\r\n");
}

void loop() {
    sensors.requestTemperatures(); 
    float tempRead = sensors.getTempCByIndex(0);

    if (tempRead == DEVICE_DISCONNECTED_C) {
        ledcWrite(PWM_CHAN, 0);
        return;
    }

    input = tempRead;
    double output = 0;
    double error = input - setpoint;
    const char* fanStatus = "FAN=Off";

    if (error <= 0) {
        output = 0;
        cumError = 0;
        fanWasOff = true;
        fanStatus = "FAN=Off";
    } else {
        cumError += error;
        if (cumError > 30) cumError = 30;
        double rateError = input - lastInput;
        output = (Kp * error) + (Ki * cumError) - (Kd * rateError);

        if (fanWasOff && output > 10) {
            ledcWrite(PWM_CHAN, 255); 
            delay(150);
            fanWasOff = false;
        }

        if (output < 160) output = 160;
        if (output > 255) output = 255;
        fanStatus = "FAN=On";
    }

    ledcWrite(PWM_CHAN, (int)output);
    lastInput = input;

    // Log ultra-descrittivo per il terminale
    Serial.printf("Timestamp: %lu, Temp: %.2f, Err: %.2f, PWM: %.0f, %s\r\n", 
                  millis(), input, error, output, fanStatus);

    delay(800); 
}

Il LOG del programma

Non c’è automazione senza dati. Ho collegato l’ESP32 al mio Raspberry Pi e ho catturato l’output seriale con picocom per verificare la risposta al gradino (avvicinando una fonte di calore). Ecco uno stralcio del log reale catturato durante lo stress test:

To exit picocom, use CNTL-A followed by CNTL-X.
picocom v3.1

port is        : /dev/ttyUSB0
flowcontrol    : none
baudrate is    : 115200
parity is      : none
databits are   : 8
stopbits are   : 1
escape is      : C-a
local echo is  : no
noinit is      : no
noreset is     : no
hangup is      : no
nolock is      : no
send_cmd is    : sz -vv
receive_cmd is : rz -vv -E
imap is        : 
omap is        : 
emap is        : crcrlf,delbs,
logfile is     : none
initstring     : none
exit_after is  : not set
exit is        : no

Type [C-a] [C-h] to see available commands
Terminal ready
Timestamp: 33467, Temp: 16.75, Err: -0.75, PWM: 0, FAN=Off
Timestamp: 34297, Temp: 16.75, Err: -0.75, PWM: 0, FAN=Off
Timestamp: 35127, Temp: 16.75, Err: -0.75, PWM: 0, FAN=Off
Timestamp: 35957, Temp: 16.75, Err: -0.75, PWM: 0, FAN=Off
Timestamp: 36787, Temp: 16.75, Err: -0.75, PWM: 0, FAN=Off
Timestamp: 37617, Temp: 16.75, Err: -0.75, PWM: 0, FAN=Off
Timestamp: 38447, Temp: 16.81, Err: -0.69, PWM: 0, FAN=Off
Timestamp: 39277, Temp: 16.94, Err: -0.56, PWM: 0, FAN=Off
Timestamp: 40107, Temp: 17.06, Err: -0.44, PWM: 0, FAN=Off
Timestamp: 40937, Temp: 17.19, Err: -0.31, PWM: 0, FAN=Off
Timestamp: 41767, Temp: 17.44, Err: -0.06, PWM: 0, FAN=Off
Timestamp: 42747, Temp: 17.69, Err: 0.19, PWM: 160, FAN=On
Timestamp: 43577, Temp: 18.00, Err: 0.50, PWM: 160, FAN=On
Timestamp: 44407, Temp: 18.38, Err: 0.88, PWM: 160, FAN=On
Timestamp: 45237, Temp: 18.62, Err: 1.12, PWM: 171, FAN=On
Timestamp: 46067, Temp: 18.94, Err: 1.44, PWM: 227, FAN=On
Timestamp: 46897, Temp: 19.19, Err: 1.69, PWM: 255, FAN=On
Timestamp: 47727, Temp: 19.44, Err: 1.94, PWM: 255, FAN=On
Timestamp: 48557, Temp: 19.62, Err: 2.12, PWM: 255, FAN=On
Timestamp: 54367, Temp: 17.44, Err: -0.06, PWM: 0, FAN=Off
Timestamp: 55197, Temp: 17.19, Err: -0.31, PWM: 0, FAN=Off
Timestamp: 56027, Temp: 16.94, Err: -0.56, PWM: 0, FAN=Off
Timestamp: 56857, Temp: 16.81, Err: -0.69, PWM: 0, FAN=Off
Timestamp: 57687, Temp: 16.75, Err: -0.75, PWM: 0, FAN=Off

Terminating...
Thanks for using picocom

Analisi del Comportamento PID e Logica di Controllo

Il log non mente ma per essere utile ppuntualizzo qualche dato interessante

  1. Stabilità in Idle e Identificazione Setpoint Fino al timestamp 41767, il sistema rimane in stato di riposo con una temperatura media di 16.75°C. Considerando che l’errore diventa positivo solo al superamento dei 17.5°C, si deduce un setpoint di riferimento impostato a circa 17.5°C - come viene confermato dalla riga di programma: double setpoint = 17.5;. In questa fase il PWM è a 0, indicando che la componente integrale (se presente) non ha accumulato errore pregresso.
  2. Soglia di Attivazione e Risposta Istantanea Al timestamp 42747, la temperatura tocca i 17.69°C ($Err: 0.19$). Il controller reagisce immediatamente applicando un PWM di 160, che rappresenta il floor (valore minimo di attivazione) per vincere l’inerzia meccanica della ventola e garantire l’avvio del flusso d’aria.
  3. Azione Proporzionale e Saturazione Il log evidenzia una chiara progressione della componente proporzionale ($K_p$):Con $Err: 1.12$, il PWM sale a 171.Con $Err: 1.44$, il PWM accelera a 227.Al raggiungimento di un errore di 1.69, il sistema entra in saturazione (PWM 255), erogando la massima potenza per contrastare il trend termico crescente.
  4. Isteresi e Fase di Spegnimento (Raffreddamento) Nella fase di rientro (dal timestamp 54367), si nota il comportamento dell’isteresi:La ventola rimane attiva durante tutta la discesa termica finché l’errore non torna negativo.Lo spegnimento (FAN=Off) avviene esattamente al timestamp 54367, quando la temperatura scende a 17.44°C ($Err: -0.06$).

Questo conferma che il sistema non utilizza un’isteresi temporale, ma si basa puramente sul segno dell’errore rispetto al setpoint per il cutoff del segnale PWM.

I dati di Sintesi Operativa del controller "artigianale"

Fase OperativaTemperatura (°C)Errore ($Err$)PWMStato Ventola
Idle / Riposo16.75-0.750Off
Soglia Attivazione17.69+0.19160On
Saturazione (Max)19.19+1.69255On
Spegnimento Isteresi17.44-0.060Off

Analisi Logica del Controller

  • Configurazione Setpoint: Il setpoint di riferimento è identificato a 17.5°C. Il sistema mantiene lo stato di idle finché $T \le 17.5$°C.
  • Risposta Proporzionale ($K_p$): Superata la soglia di attivazione, il controller applica un incremento dinamico. Il passaggio da PWM 171 ($Err +1.12$) a PWM 227 ($Err +1.44$) evidenzia un guadagno proporzionale attivo prima della saturazione a 255.
  • Vincolo Meccanico (Floor PWM): È impostato un valore minimo di 160. Questa soglia evita lo stallo del motore, garantendo che la ventola operi sempre all'interno del proprio regime di rotazione lineare.
  • Gestione Isteresi: Il sistema non effettua uno spegnimento immediato al ritorno sul setpoint, ma attende che l'errore diventi negativo ($Err < 0$). La ventola viene disattivata a 17.44°C, prevenendo cicli di accensione/spegnimento troppo frequenti (chattering) in prossimità del valore critico.

Il controller PID: Teoria, Matematica e Implementazione Reale

Siamo arrivati al nodo cruciale. Finora abbiamo parlato di saldature e di collegamenti, ma la vera magia di questo progetto è invisibile: risiede nella matematica. Molti maker si fermano al controllo "Bang-Bang" (acceso/spento), trattando l'ESP32 come se fosse un semplice interruttore bimetallico degli anni '50. Ma quando implementiamo un PID (Proporzionale-Integrale-Derivativo), stiamo facendo compiere al microcontrollore un salto quantico: da semplice esecutore a sistema predittivo.

1. Oltre l'ON/OFF: La Fisica del Controllo

Immaginate di guidare un'auto e di dovervi fermare a un semaforo rosso. Un sistema ON/OFF (come un termostato classico) accelererebbe al massimo fino alla linea di arresto, per poi inchiodare istantaneamente. Risultato? I passeggeri (o nel nostro caso, i componenti elettronici) subiscono uno stress inutile e l'auto probabilmente si fermerebbe oltre la linea (Overshoot).

Il PID, invece, guida come un pilota esperto: inizia a frenare dolcemente molto prima, dosando la pressione sul pedale in base alla distanza rimanente e alla velocità attuale. In termini termodinamici, questo significa che il sistema non aspetta di superare il target per spegnere la ventola, ma ne riduce la potenza man mano che l'errore diminuisce.

2. La Matematica spiegata (Senza mal di testa)

L'algoritmo PID non è altro che una somma pesata di tre termini che agiscono su tre dimensioni temporali diverse. Vediamoli nel dettaglio, perché capire questo significa capire l'automazione industriale:

  • Il Termine Proporzionale ($K_p$ - Il Presente):
    È la forza bruta. Guarda l'errore attuale ($SetPoint - Temperatura Attuale$). Se l'errore è grande, l'uscita è grande. È come una molla: più la tiri, più forza oppone.
    Nel codice: Kp * error
  • Il Termine Integrale ($K_i$ - Il Passato):
    Questa è la "memoria" del sistema. Se il termine Proporzionale non è sufficiente a raggiungere il target (errore a regime o Steady-State Error), l'Integrale accumula questo errore nel tempo. "Ricorda" che non siamo ancora arrivati e aggiunge potenza extra finché il gap non si chiude.
    Nel codice: Ki * cumError
  • Il Termine Derivativo ($K_d$ - Il Futuro):
    Il più affascinante. Analizza la velocità con cui la temperatura sta cambiando. Se stiamo avvicinandoci al target troppo velocemente, il Derivativo "frena" preventivamente l'uscita per evitare di superare il bersaglio. È l'ammortizzatore del sistema.
    Nel codice: Kd * rateError

3. Dall'IoT all'Aeronautica: Perché è uno standard?

Perché insistere tanto su un algoritmo del 1922 in un blog del 2026? Perché il PID è l'algoritmo che "regge il mondo". Lo stesso codice C++ che gira su questo ESP32 da 5 euro è concettualmente identico a quello che mantiene in volo un quadricottero (correggendo la velocità dei motori migliaia di volte al secondo per contrastare il vento) o che stabilizza l'assetto di un aereo di linea.

In ambito IoT (Internet of Things), l'uso del PID è ciò che distingue un gadget amatoriale da un prodotto professionale. Un sistema di riscaldamento smart che usa il PID consuma meno energia (evitando picchi continui) e prolunga la vita dei relè e delle batterie, modulando l'intervento invece di stressare l'hardware con cicli continui di accensione e spegnimento.

4. Un codice sorgente brevissimo:

Spesso si pensa che per fare cose complesse servano librerie enormi. Il mio approccio è opposto: Less is More. Guardate come l'intera logica di controllo si risolva in pochissime righe di C++ puro. Questo snippet è portabile, leggero ed estremamente veloce:


// 1. Calcolo dell'Errore Attuale (Il Presente)
double error = input - setpoint;

// 2. Calcolo dell'Accumulo (Il Passato)
// Nota: aggiungiamo un limite (clamp) a 30 per evitare il "Windup" eccessivo
cumError += error;
if (cumError > 30) cumError = 30;

// 3. Calcolo della Velocità di variazione (Il Futuro)
double rateError = input - lastInput;

// 4. LA SOMMA PESATA (L'Output finale)
output = (Kp * error) + (Ki * cumError) - (Kd * rateError);

Notate la gestione del "Integral Windup" (if (cumError > 30)...). Senza questa riga, se lasciaste il sistema acceso senza ventola collegata, l'errore accumulato crescerebbe all'infinito. Quando ricolleghereste la ventola, questa rimarrebbe al 100% per ore prima di scaricare l'errore accumulato. Piccoli dettagli che fanno la differenza tra un codice che compila e uno che funziona nel mondo reale.

5. Sperimenta: La Guida al Tuning

Il valore di questo progetto non è copiarlo, ma adattarlo. La vostra ventola, il vostro dissipatore e il vostro ambiente hanno un'inerzia termica diversa dalla mia. Ecco come potete modificare i parametri all'inizio dello sketch per adattarli alle vostre esigenze:

  • Il sistema è lento a reagire? Aumentate Kp. Se lo alzate troppo, il sistema inizierà a oscillare violentemente.
  • La temperatura si stabilizza ma rimane sotto il target (es. 17.2°C invece di 17.5°C)? Aumentate Ki. Questo darà la spinta finale per chiudere l'errore.
  • Il sistema raggiunge il target ma lo supera spesso (Overshoot)? Aumentate Kd. Questo aumenterà l'effetto "frenante" all'avvicinarsi della meta.

Fonti e Riferimenti Tecnici