Programmierung eines proportionalen Reglers - Bewässerungsanlage

Guten Abend,

das hier ist mittlerweile mein zweiter Beitrag und ich hoffe, dass ich mein Thema bzw. mein Problem verständlich rüberbringen kann.

Mein Projekt ist eine automatische Bewässerungsanlage, die je nach gemessener Bodenfeuchtigkeit bewässern soll. Aktuell läuft das als "Zweipunkt Regler" nach folgendem Code:


if (soil < 300) // je kleiner, desto trockener
{
  digitalWrite(ledgreen, HIGH);
  digitalWrite(4,1); // Relais an
  delay(2000); // Pumpe läuft 2 Sekunden
  digitalWrite(4, 0); // Relais aus
  digitalWrite(ledgreen, LOW);
}

else if (soil > 500) // zu nass, kein Wasser nachgeben
{
  digitalWrite (ledgreen, LOW);
}
  delay(5000);
}

Zusätzlich geht noch eine grüne LED an, wenn die Pumpe läuft. Das ist mir allerdings zu stumpf und zu ungenau. Deshalb möchte ich einen proportionalen Regler programmieren, der für eine bestimmte Über-/Unterschreitung des Sollwertes (bspw. 50% Feuchtigkeit) um eine proportionale Zeit bewässert. Das hört sich abstrakt an, deshalb versuch ich es mal als Beispiel zu beschreiben:

Sollwert der Feuchtigkeit: 50%
Istwert: 40% ---> Abweichung um 10% (0,1 in Dezimalschreibweise)
Diese Regelabweichung soll jetzt mit einer Konstante multipliziert werden, also mit einer Standardzeit von bspw. 5 sek ---> 5s * 0,1 = 0,5 sek ---> Die Pflanze soll nun also 0,5 sek bewässert werden bzw. die Pumpe soll 0,5 sek laufen.

Mein Plan lautet jetzt:

Ich rechne zunächst mal den Analogwert vom Feuchtigkeitssensor in Prozent und in eine Dezimalzahl um...Die Prozentumrechnung ist erstmal nur für das Datenlogging auf einer SD-Karte...Das Wichtige ist hier also nur die Dezimalzahl, die später mit der Konstante multipliziert wird. Die Umrechnung läuft nach folgendem Code:

void Read_Soilvalue ()
{
  soil = analogRead(A1);
  per = ( (100 / 1023.0) * soil); 
  Serial.print("Bodenfeuchtigkeit: ");
  Serial.println(per);
}
  varout = (per * 0.01);

"per" meint hier die Bodenfeuchtigkeit in Prozent also im Format 10, 20, 25...Deshalb ist per auch als "int" definiert, denn da brauche ich keine Kommastelle. "varout" soll nun die Dezimalzahl werden, weshalb "per" mit 0,01 multipliziert wird. "varout" ist als "float" definiert und kann somit Kommastellen haben (sehe ich das richtig?).

Meine Frage ist jetzt? Wie programmiere ich jetzt einen Regler, der mir die vorhin erklärte Formel berechnet?
Die Formel ist: y = K * e
y (Dauer der Pumpe); K ( Konstante Ausgangszeit bspw. 2 sek); e (errechnete Regeldifferenz in Dezimal)
Wichtig ist vor allem, dass es um die Abweichung vom SOLLWERT (50%) geht. Ich muss es also irgendwie berechnen können, dass 40% einer Abweichung von 10% bzw. 0,1 entspricht...Bisher berechne ich ja nur die Feuchtigkeit in Prozent und in Dezimal, was mir aber nichts bringt...Kann man das mit einer if-Schleife realisieren?

Übrigens: Die fertig berechnete Dauer würde ich dann als eigene Variable definieren und diese in eine delay-Funktion packen, sodass die Pumpe um den errechneten Betrag läuft.

Ihr könnte gerne Fragen stellen falls ihr meine Erklärungen nicht ganz nachvollziehen könnt. Ich schau den ganzen Tag auf meine Notifications! Ich bin für jede Hilfe dankbar!

Für alle die es interessiert kommt jetzt mein vollständiger Quellcode:

#include <SPI.h>
#include <SD.h>
#include "DHT.h"          // Bibliothek für den Sensor /Temperatur /Luftfeuchtigkeit   
#include <NewPing.h>  

#define DHTPIN 7          // Hier die Pin Nummer eintragen wo der Sensor angeschlossen ist
#define DHTTYPE DHT11     // Hier wird definiert was für ein Sensor ausgelesen wird (Typ: DHT11); andere wären bspw. DHT22

File dataFile;

DHT dht(DHTPIN, DHTTYPE); // Das ist ein zwingender Abschnitt, der aus den Protokollen der Bibliothek (DHT.h) hervorgeht..Ich weiß nicht wofür das ist

float h;
float t;

const int chipSelect = 10;

int soil = A1;            // Variablendefinition für den Analogen PIN A1 am Arduino -> Pin A1 ist also für "Soil" = den Bodenfeuchtigkeitssensor
int per;                  // Variablendefinition für die Prozentzahl nach der Berechnung
float varout;             // Variablendefinition für die Regeldifferenz in Prozent (Dezimal) für die Berechnung von p
int Relais = 4;           // Relais bekommt den Digitalen Pin 4
int ledblue = 9;          // Wasserstand zu niedrig
int ledgreen = 8;         // Pumpe läuft



void setup() 
{
Serial.begin(9600);       // Serielle Schnittstelle wird gestartete mit 9600 Baud
while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
 pinMode(Relais, OUTPUT);      // Pin 4 wird als Output gekennzeichnet: Signal AUS dem Arduino IN das Relais
  Serial.println("DHT11 Test");   // Serielle Schnittstelle sagt, dass der DHT11 Sensor jetzt initialisiert wird
  dht.begin();                   // Der DHT11 wird gestartet
  Initialize_SDcard();
}

void loop() 
{  
  Read_TempHum();
  Read_Soilvalue();
  Write_SDcard();

  varout = (per * 0.01);

if (soil < 300) // je kleiner, desto trockener
{
  digitalWrite(ledgreen, HIGH);
  digitalWrite(4,1); // Relais an
  delay(2000); // Pumpe läuft 2 Sekunden
  digitalWrite(4, 0); // Relais aus
  digitalWrite(ledgreen, LOW);
}

else if (soil > 500) // zu nass, kein Wasser nachgeben
{
  digitalWrite (ledgreen, LOW);
}
  delay(5000);
}

void Read_TempHum ()
{
  h = dht.readHumidity();    // Lesen der Luftfeuchtigkeit und speichern in die Variable h
  t = dht.readTemperature(); // Lesen der Temperatur in °C und speichern in die Variable t
  
    /* Jetzt kommt ein Teil, der überprüft, ob die ausgegebenen Werte für Luftfeuchtigkeit und Temperatur auch wirklich Zahlen sind.
   *  Falls nicht, wird ein Fehler an den seriellen Monitor ausgegeben.  isnan = is not a number*/
  
if (isnan(h) || isnan(t)) 
{       
    Serial.println("Fehler beim auslesen des Sensors!");
} 

else 
{

 // Nun senden wir die gemessenen Werte an den PC (seriellen Montor)
  Serial.print("Luftfeuchtigkeit: ");
  Serial.print(h);                  // Ausgeben der Luftfeuchtigkeit
  Serial.print("%\t");              // Tabulator
  Serial.print("Temperatur: ");
  Serial.print(t);                  // Ausgeben der Temperatur
  Serial.print("°");                // Schreiben des ° Zeichen
  Serial.println("C");
}
}

void Read_Soilvalue ()
{
  soil = analogRead(A1);
  per = ( (100 / 1023.0) * soil); 
  Serial.print("Bodenfeuchtigkeit: ");
  Serial.println(per);
}

void Initialize_SDcard ()
{
  if (!SD.begin(chipSelect))
  {
    Serial.println("Card failed, or not present");
    // don't do anything more:
    while (1);
   }
   Serial.println("card initialized.");
   // open the file. note that only one file can be open at a time,
   // so you have to close this one before opening another.
   dataFile = SD.open("Soildata.csv", FILE_WRITE);
   // if the file is available, write to it:
   
    dataFile.println("Temperature:,Humidity:,Bodenfeuchtigkeit:"); //Write the first row of the excel file
    dataFile.close();
   
}

void Write_SDcard ()
{
   // open the file. note that only one file can be open at a time,
   // so you have to close this one before opening another.
   dataFile = SD.open("Soildata.csv", FILE_WRITE);
   // if the file is available, write to it:
   if (dataFile)
   {
    dataFile.print(t); //Store date on SD card
    dataFile.print(","); //Move to next column using a ","
    dataFile.print(h); //Store date on SD card
    dataFile.print(","); //Move to next column using a ","
    dataFile.print(per);
    dataFile.println(); //End of Row move to next row
    dataFile.close(); //Close the file
    Serial.println ("SD card writing successful");
   }
  else
  Serial.println("SD card writing failed");
}

Kommt jetzt vielleicht komisch, wenn ich auf meinen eigenen Beitrag antworte, aber ich mache das zur Übersichtlichkeit ^^.
Wäre dieser Code eine mögliche Lösung für mein Problem?

#define Data A1
int soil;
int per;
float varout;
int ledgreen = 8;
int water;

void setup() 
{
 Serial.begin(9600);
}

void loop()
{
  soil = analogRead(Data);
  per = ( (100 / 1023.0) * soil);    // GEMESSENE Bodenfeuchtigkeit in % bzw. ohne Nachkommastelle, denn "per" ist als int definiert
  
  Serial.print("Bodenfeuchte: ");
  Serial.print(per);
  Serial.println(" %");
  Serial.println(soil);


  if (per < 50) 
  {
  digitalWrite(ledgreen, HIGH);
  varout = (50 - per);
  varout = (varout * 0.01); 
  water = (varout * 5000);    
  digitalWrite(4,1); // Relais an
  delay(water); // Pumpe läuft um bestimmte Zeit in Millisekunden
  digitalWrite(4, 0); // Relais aus
  digitalWrite(ledgreen, LOW);
  }
  else if (per > 50) // zu nass, kein Wasser nachgeben
{
  digitalWrite (ledgreen, LOW);
}
  delay(5000);
}

Also zuerst wird A1 ausgelesen (hier als "soil" definiert) und dann in Prozent umgerechnet (hier als "per"). Diese Umrechnung erfolgt mit dieser Formel:

  per = ( (100 / 1023.0) * soil);    // GEMESSENE Bodenfeuchtigkeit in % bzw. ohne Nachkommastelle, denn "per" ist als int definiert

Dann soll jetzt die Pumpe angehen, wenn die Bodenfeuchtigkeit UNTER 50% liegt...Für die Abweichung vom Sollwert wird folgende Rechnung getätig:

  varout = (50 - per);
  varout = (varout * 0.01); 
  water = (varout * 5000);    

Die Variable "water" ist letztlich die Zeit, die die Pumpe wässern soll.

Meine Fragen sind jetzt:

  1. Darf man innerhalb einer Funktion mehrmals eine Variable verändern? Ich hab ja in der Rechnung zwei mal mit "varout" gerechnet. Zuerst um die Differenz zu ermitteln, also bspw. 50%-20% = 30%. Diese Abweichung soll dann in eine Prozentzahl in Dezimalschreibweise umgerechnet werden, weshalb sie mit 0,01 multipliziert wird: 30% * 0,01 = 0,3. Diese wird im Anschluss mit der Konstanten multipliziert (hier 5 sek oder 5000 Millisek).

  2. Soll ich die Variablendefinitionen alle so lassen? Also bezüglich der Variationen von "int"; "float" etc. Ich kenn mich da nicht so aus und weiß nur, dass "int" ganze Zahlen rechnet, während "float" auch Nachkommastellen unterstützt...Es gibt ja aber auch noch sowas wie "long"...Ich kenne mich damit leider nicht aus.

Danke!

Wie willst Du eine signifikante Feuchtigkeitsabweichung vom Sollwert erreichen wenn Du jede 5 Sekunden die Feuchtigkeit mißt.
Da das System Blumentopf sehr träge ist: Zugeführtes Wasser führt erst nach etlichen Minuten zu einer Feuchtigkeitsänderung der Erde, ist ein Real-Time System wie Dein Sketch (eine Differenz zwischen soll und Istwert führt alle 2 Sekunden sofort zur Bewässerung) ein Überschwemmungsproduzent.

Du mußt in größeren Zeitabständen messen und auch eine Sperrzeit zwischen 2 Bewässerungen.

Grüße Uwe

1 Like

Hallo Uwe,

vielen Dank erstmal für die Antwort! Du hast natürlich recht, das hätte ich noch dazuschreiben müssen. Das ist mein Testprogramm, damit ich sehen kann, ob das überhaupt funktioniert. Dennoch liegst du natürlich richtig. Ich habe derzeit eine Abtastperiode von ungefähr 2h geplant...Eine maximale Bewässerungszeit bzw. eine Sperrzeit wäre auch eine gute Idee! Danke!

Hast du vielleicht eine Meinung zu meinem Lösungsvorschlag? Sind meine Variablendefinitionen so korrekt?

Vielen Dank erstmal!

Grüße Julius

Mal eine Variante nur mit Ganzzahlen. Bei Mikrocontrollern ist es besser float und/oder double Rechnungen zu vermeiden. Das ist kein "Muss" aber ein Vorschlag.

Die Magic Numbers im Code habe ich durch globale Konstanten ersetzt. Werte die nicht negativ sein können, wurden in unsigned Variablen umgewandelt.

// Verwendete Pins
const byte PIN_DATA {A1};
const byte PIN_LEDGREEN {13};
const byte PIN_RELAIS {4};

// Rechen und Steuerkonstanten
const byte P_THRESHOLD {50};
const unsigned int P_FACTOR {9775};
const unsigned long P_DIVIDER {100000};
const int W_DELAY {5000};

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  unsigned int soil = analogRead(PIN_DATA);
  byte per = ((unsigned long)P_FACTOR * soil)/P_DIVIDER;    // GEMESSENE Bodenfeuchtigkeit in % bzw. ohne Nachkommastelle.

  Serial.print("Bodenfeuchte: ");
  Serial.print(per);
  Serial.println(" %");
  Serial.println(soil);

  if (per <  P_THRESHOLD)
  {
    digitalWrite(PIN_LEDGREEN, HIGH);
    digitalWrite(PIN_RELAIS, HIGH); // Relais an
    delay(calcPeriod(per)); // Pumpe läuft um bestimmte Zeit in Millisekunden
    digitalWrite(PIN_RELAIS, LOW); // Relais aus
    digitalWrite(PIN_LEDGREEN, LOW);
  }
  else  // zu nass, kein Wasser nachgeben
  {
    digitalWrite (PIN_LEDGREEN, LOW);
  }
  delay(5000);
}

unsigned int calcPeriod (byte percent) {
  return ((P_THRESHOLD - percent) * W_DELAY / 100 );
}
1 Like

Vielen Dank für deine Hilfe, auch wenn ich leider nicht so viel davon verstehe, was du da genau gemacht hast. Dafür reicht wohl leider mein Wissen nicht aus, ich bin noch sehr neu im Arduino Bereich. Dennoch vielen Dank für deine Mühe! Ich versuche mich da mal durchzuarbeiten. Du schreibt, dass das ein Vorschlag und kein "Muss" ist...Kann meine Variante denn zu Problemen führen, wenn ich das so ca. 1 Woche laufen lasse, mit einer Messung alle 2h?

Grüße Julius

Microcontroller rechnen schneller mit Integer-Zahlen. IdR. raucht es auch weniger Speicherplatz wenn auf Fließkommaoperationen verzichtet wird.

Aber bei deinem kleinen Programm wird deine Variante sicher nicht zu "Problemen" führen. Wenn so ein Programm aber mit der Zeit wächst und immer mehr dazu kommt, könnte es sich jedoch auszahlen, wenn man gleich die ressourcensparsamere Variante (Speicherplatz / Rechenzeit) verwendet.

1 Like

Super, vielen Dank für deine Antwort! Das hilft mir sehr!

Grüße Julius

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.