PID-Regler auf dem ESP32: Lüfterdrehzahl sauber regeln

Ein PID-Regler setzt die Lüfterdrehzahl stetig statt per Schwellwert. Wir zeigen Grundgleichung, Rechenbeispiel und ESP32-Code mit PWM und ihre Grenzen.

Titelkarte mit „PID-Regler auf dem ESP32“ in heller Schrift auf dunklem Grund, Säule Maker und Elektronik, Logo ts.

Ein Lüfter, der bei jeder Temperaturschwelle hart ein- und ausgeschaltet wird, ist laut und nervt. Ein PID-Regler setzt die Drehzahl stattdessen stetig so, dass die Temperatur nahe am Sollwert bleibt. Am Ende kennen wir die Grundgleichung des Reglers, haben einen 4-Pin-PWM-Lüfter am ESP32 angesteuert und wissen, woran eine Regelung auf einem trägen Sensor scheitern kann.

Warum Schalten nicht reicht

Ein Thermostat mit Schwellwert hat zwei Nachteile. Die Temperatur schwankt zwischen den Schwellen, und der Lüfter taktet ständig an und aus. In einem Community-Beitrag zu einem ESPHome-Lüfterregler beschreibt der Autor genau dieses Problem: Das Takten war bei Filmabenden ein Störfaktor. Die Lösung ist, die Leistung kontinuierlich zu stellen, statt sie nur zu schalten.

Lüfter ansteuern mit PWM

Bei Computerlüftern erfolgt die Drehzahlsteuerung über ein Pulsweitenmodulationssignal (PWM). Das Signal hat eine feste Frequenz $f$ und ein variables Tastverhältnis (Duty Cycle) $D$. Für die Periodendauer $T$ und die Einschaltdauer $t_\mathrm{ein}$ gilt:

$$D = \frac{t_\mathrm{ein}}{T} = t_\mathrm{ein} \cdot f$$

Für Lüfter nennt die Quelle eine typische Frequenz von etwa 25 kHz [DroneBot Workshop]. Daraus folgt eine Periodendauer von $T = 1/(25\,\mathrm{kHz}) = 40\,\mathrm{\mu s}$. Bei $D = 0{,}22$ ist der Puls also $t_\mathrm{ein} = 8{,}8\,\mathrm{\mu s}$ lang.

Die Lüfterelektronik wertet das Tastverhältnis aus. Ein Multimeter zeigt bei solchen Frequenzen den Mittelwert an. Dieser ergibt sich aus dem Duty Cycle und der High-Spannung $U_\mathrm{H}$:

$$U_\mathrm{mittel} = D \cdot U_\mathrm{H}$$

Bei $U_\mathrm{H} = 3{,}3\,\mathrm{V}$ (ESP32-Logikpegel) und $D = 0{,}22$ erwartet man also $U_\mathrm{mittel} = 0{,}73\,\mathrm{V}$. Das ist ein guter Test, bevor man den Lüfter anschließt. Ob das Multimeter bei 25 kHz noch zuverlässig mittelt, sollte man prüfen.

Wichtig ist die Lüfterwahl. Nach der Quelle braucht man einen 4-Pin-Lüfter mit PWM-Eingang. Dreipolige Lüfter sind nur ein- und ausschaltbar und liefern lediglich ein Drehzahlsignal [GitHub, patrickcollins12/esphome-fan-controller]. Ältere Schaltungen mit Darlington-Transistoren funktionieren dagegen nicht mit Lüftern, die einen Drehzahlausgang haben [thomaspfeifer.net]. Außerdem berichtet dieselbe Quelle, dass manche PWM-Lüfter zu summen beginnen. Dort half ein parallel geschalteter Kondensator, dessen Wert man nach Bedarf erhöhen kann [thomaspfeifer.net].

Der PID-Regler

Der Regler bildet aus dem Regelfehler $e(t)$ eine Stellgröße $u(t)$. In kontinuierlicher Form lautet die Gleichung:

$$u(t) = K_p\, e(t) + K_i \int_0^t e(\tau)\,\mathrm{d}\tau + K_d\, \frac{\mathrm{d}e(t)}{\mathrm{d}t}$$

Der Proportionalanteil reagiert auf den aktuellen Fehler. Der Integralanteil summiert vergangene Fehler und beseitigt einen bleibenden Restfehler. Der Differenzialanteil reagiert auf die Änderungsrate und dämpft Überschwinger [SaludPCB]. Die Gains haben Einheiten: $K_p$ in $\mathrm{\%/K}$, $K_i$ in $\mathrm{\%/(K\,s)}$ und $K_d$ in $\mathrm{\%\,s/K}$.

Beim Kühlen muss man das Vorzeichen des Fehlers beachten. Die Quelle definiert $e = w - x$ (Sollwert minus Messwert), was für Heizungen passt. Ein Lüfter soll aber mehr leisten, wenn es zu warm ist. Deshalb setzen wir hier $e = T_\mathrm{ist} - T_\mathrm{soll}$. Im zeitdiskreten Code mit Schrittweite $\Delta t$ ergibt sich:

$$u_k = K_p\, e_k + K_i \sum_{j=1}^{k} e_j\,\Delta t + K_d\,\frac{e_k - e_{k-1}}{\Delta t}$$

Rechenbeispiel

Der Sollwert liegt bei $T_\mathrm{soll} = 30\,\mathrm{°C}$, gemessen werden $T_\mathrm{ist} = 33\,\mathrm{°C}$. Der Fehler beträgt also $e = 3\,\mathrm{K}$. Wir wählen $K_p = 5\,\mathrm{\%/K}$, $K_i = 0{,}05\,\mathrm{\%/(K\,s)}$ und $K_d = 0$.

Der Proportionalanteil ist $5\,\mathrm{\%/K} \cdot 3\,\mathrm{K} = 15\,\%$. Bleibt der Fehler eine Minute lang bei $3\,\mathrm{K}$, liefert der Integralanteil $0{,}05\,\mathrm{\%/(K\,s)} \cdot 3\,\mathrm{K} \cdot 60\,\mathrm{s} = 9\,\%$. Die Stellgröße ist damit $u = 24\,\%$. Das entspricht einem Mittelwert von $0{,}24 \cdot 3{,}3\,\mathrm{V} \approx 0{,}79\,\mathrm{V}$ am PWM-Pin. Das Beispiel vereinfacht, weil sich der Fehler in der Realität laufend ändert, zeigt aber das Prinzip.

Praxis: Aufbau und Code

Materialliste (sinngemäß nach der Quelle):

  • ESP32-Board (z. B. NodeMCU-kompatibel)
  • 12-V-PWM-Lüfter mit 4 Pins
  • Temperatursensor DHT11 (3-Pin-Modul)
  • 12-V-Netzteil (1 A bis 2 A je nach Lüfterstrom)
  • Abwärtswandler LM2596 für 3,3 V zur ESP32-Versorgung
  • Jumperkabel, Multimeter

Die Masse von Netzteil, Lüfter und ESP32 muss gemeinsam sein. Ohne gemeinsame Masse hat das PWM-Signal keinen definierten Bezug. Den Abwärtswandler stellt man vor dem Anschluss am Multimeter auf 3,3 V ein.

Der folgende Code nutzt die LEDC-Schnittstelle von ESP-IDF und den Regler. Der Sensortreiber ist nur angedeutet, weil das DHT11-Timingprotokoll den Rahmen sprengen würde.

#include <stdint.h>
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define FAN_GPIO      25
#define PWM_FREQ_HZ   25000
#define DUTY_BITS     LEDC_TIMER_10_BIT   // 1024 Stufen
#define DUTY_MAX      1023
#define SETPOINT_C    30.0f
#define DT_S          1.0f

typedef struct {
    float kp, ki, kd;
    float integral;
    float prev_error;
    float out_min, out_max;
} pid_ctrl_t;

// Fehler e = T_ist - T_soll: positiv bedeutet zu warm, also mehr Kühlung
static float pid_step(pid_ctrl_t *c, float setpoint, float measured, float dt)
{
    float e = measured - setpoint;
    float d = (e - c->prev_error) / dt;
    float i_new = c->integral + e * dt;
    float out = c->kp * e + c->ki * i_new + c->kd * d;

    if (out > c->out_max) {
        out = c->out_max;               // Anti-Windup: bei Sättigung nicht weiter integrieren
    } else if (out < c->out_min) {
        out = c->out_min;
    } else {
        c->integral = i_new;
    }
    c->prev_error = e;
    return out;
}

static void fan_pwm_init(void)
{
    ledc_timer_config_t timer = {
        .speed_mode      = LEDC_LOW_SPEED_MODE,
        .timer_num       = LEDC_TIMER_0,
        .duty_resolution = DUTY_BITS,
        .freq_hz         = PWM_FREQ_HZ,
        .clk_cfg         = LEDC_AUTO_CLK,
    };
    ledc_timer_config(&timer);

    ledc_channel_config_t ch = {
        .gpio_num   = FAN_GPIO,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel    = LEDC_CHANNEL_0,
        .timer_sel  = LEDC_TIMER_0,
        .duty       = 0,
        .hpoint     = 0,
    };
    ledc_channel_config(&ch);
}

// Platzhalter: hier den DHT11-Treiber einbinden (Messung nur im Abstand von mindestens 1 s)
static float read_temperature_c(void)
{
    return 30.0f;
}

void app_main(void)
{
    fan_pwm_init();
    pid_ctrl_t pid = { .kp = 5.0f, .ki = 0.05f, .kd = 0.0f,
                       .out_min = 0.0f, .out_max = 100.0f };

    while (1) {
        float t_ist = read_temperature_c();
        float speed = pid_step(&pid, SETPOINT_C, t_ist, DT_S);   // 0..100 %
        uint32_t duty = (uint32_t)(speed / 100.0f * DUTY_MAX + 0.5f);

        ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
        ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);

        vTaskDelay(pdMS_TO_TICKS(DT_S * 1000));
    }
}

Inbetriebnahme in dieser Reihenfolge:

  1. Zuerst nur $K_p$ einstellen, $K_i$ und $K_d$ auf null lassen. Der Regler reagiert dann direkt, bleibt aber mit Restfehler.
  2. Anschließend $K_i$ langsam erhöhen, bis der Restfehler verschwindet, ohne dass die Drehzahl pendelt.
  3. $K_d$ bleibt bei einem Temperatursensor mit Quantisierung und Trägheit meist null, weil er Messrauschen verstärkt.

Gängige Einstellregeln wie die nach Ziegler und Nichols setzen eine Sprungantwort voraus. Ob sie für einen trägen Kühlkreis taugen, muss man prüfen.

Stolpersteine und Grenzen

Der Sensor ist das schwächste Glied. Der DHT11 misst grob und langsam. Sein Datenblatt nennt eine Auflösung und Mindestabstände zwischen Messungen, die man vor dem Einsatz nachschlagen sollte [PRÜFEN: DHT11-Auflösung und Mindestabstand laut Datenblatt]. Ein Sprung von 0,5 K im Messwert wirkt auf den Regler wie ein Sprung im Fehler. Den Sensor deshalb nicht direkt im Luftstrom des Lüfters platzieren.

Integrator-Windup. Läuft der Lüfter auf 100 %, während die Temperatur noch hoch ist, wächst der Integralanteil weiter. Nach dem Abkühlen überschwingt der Regler dann deutlich. Die Anti-Windup-Logik im Code verhindert das nur grob. Die Ausgangsbegrenzung im Regler ist der Schutz, den man zuerst testet.

Die Mindestdrehzahl ist nicht null. Die Quelle erwartet, dass 4-Pin-Lüfter bei 0 % stehen bleiben. Einige Nutzer hätten davon abweichende Erfahrungen berichtet, der Beleg im Recherchematerial ist jedoch abgeschnitten [PRÜFEN: Verhalten von PWM-Lüftern bei 0 % und bei niedrigen Duty Cycles]. Man sollte daher die Anlaufdrehzahl jedes Lüfters messen und die Untergrenze out_min entsprechend setzen.

Ein Community-Bericht zeigt ein Problem im Betrieb. Ein Nutzer berichtet, dass die Kühlung nach einer Abkühlung unter den Sollwert nicht wieder einsetzte, bis der ESP neu gestartet wurde. Die Ursache wird im Thread nicht geklärt [Home Assistant Community]. Dieser Fall ist ein Hinweis, die Wiedereinschalt-Logik bei Dauertests gezielt zu prüfen.

Das Modell ist vereinfacht. Der Zusammenhang zwischen Drehzahl und Kühlleistung ist nicht linear, und die Strecke hat Totzeiten. Der Regler mit festen Gains gilt nur in dem Betriebsbereich, in dem man ihn eingestellt hat.

Für den Unterricht

Jahrgangsstufe: 11 oder 12 am bayerischen Gymnasium, sinnvoll nach der Einführung von Differenzialgleichungen und Integralen. Zeitbedarf: etwa 90 Minuten, etwa eine Doppelstunde. Material: Tabellenkalkulation, optional ein Mikrocontroller mit Temperatursensor. Für die Messwerterfassung bietet phyphox ein Experiment zur Lufttemperatur mit ESP32 [phyphox].

Einsatzidee: Die Klasse modelliert die Abkühlung eines Gehäuses in einer Tabelle. Dazu setzt man $T_{k+1} = T_k - a\,(T_k - T_\mathrm{U}) + b\,u_k$ an, mit Umgebungstemperatur $T_\mathrm{U}$ und Lüfterleistung $u_k$ in Prozent. Anschließend implementiert man die PID-Gleichung in einer weiteren Spalte und variiert $K_p$ und $K_i$. Die Lernenden erkennen so Überschwingen und bleibenden Fehler, ohne dass Hardware nötig ist. Die Modellparameter $a$ und $b$ sind frei gewählt und keine Messwerte.

Fazit

Ein PID-Regler macht aus einem Lüfter mit Schwellwert einen Regler, der die Drehzahl stetig nachführt. Die Grundgleichung lässt sich am Rechenbeispiel nachvollziehen, die Umsetzung auf dem ESP32 ist mit LEDC und einer kurzen Schleife machbar. Ob der Regler im Gehäuse wirklich ruhig läuft, entscheiden Sensorwahl, Mindestdrehzahl und Einstellung der Gains.

Weiterlesen