zurück zum Artikel

Die Heimautomatisierung mit speicherprogrammierbarer Steuerung (SPS)

Wolfgang Klimt

Die Programmierung speicherprogrammierbarer Steuerungen führte in der Softwareentwicklung bislang ein Nischendasein. Die entsprechenden Steuerungen wurden hauptsächlich im industriellen Umfeld verwendet und waren, zusammen mit den notwendigen I/O-Modulen, relativ teuer. Nun rücken Automatisierungsprodukte gerade im Smart-Home-Bereich stärker in den technischen Fokus.

Die Heimautomatisierung mit speicherprogrammierbarer Steuerung (SPS)

Die Programmierung speicherprogrammierbarer Steuerungen (SPS) führte in der Softwareentwicklung bislang ein Nischendasein. Die entsprechenden Steuerungen wurden hauptsächlich im industriellen Umfeld verwendet und waren, zusammen mit den notwendigen I/O-Modulen, relativ teuer. Mit dem Aufkommen des Internet der Dinge rücken Automatisierungsprodukte gerade im Smart-Home-Bereich stärker in den technischen Fokus.

Der Beitrag erläutert den Einsatz und die Programmierung einer SPS-Lösung für die Steuerung der Beleuchtung, Fußbodenheizung, Jalousien und einen Teil eines Aquariums in einem intelligenten Einfamilienhaus. Die Anwendung hat der Autor dieses Beitrags im Rahmen seines Hausbaus eingeführt und programmiert.

Im gewerblichen Gebäudesektor werden speicherprogrammierte Steuerungen aufgrund ihrer Vielseitigkeit und Robustheit seit vielen Jahren gerne eingesetzt. Die Nutzung ist allerdings mit hohen Kosten verbunden, was sie für den privaten Einsatz nachteilig machte. Einige Hersteller von SPS-Programmiersystemen haben nun den Raspberry Pi als Plattform für sich entdeckt und bieten eine entsprechende Unterstützung. Das macht die SPS-Programmierung auch für den privaten Bereich attraktiv. Zwar genügt die Raspberry-Plattform nicht den harten Echtzeitanforderungen, die im Bereich der Industrieanlagensteuerung existieren. Aber ein Jitter im Bereich von 50 bis 400 Mikrosekunden, wie ihn zum Beispiel 3S-Software für die eigene Codesys-Raspberry-Implementierung angibt, ist für viele Anwendungen im Smart-Home-Bereich ausreichend.

Da IO-Bausteine für die Raspberry-Bus-Systeme I2C und SPI im Elektronik-Fachhandel für wenig Geld erhältlich sind, prädestinieren sie den Pi für zahlreiche kleinere Automatisierungsaufgaben. Das Codesys-Programmiersystem ist nach der Registrierung kostenlos verfügbar und umfasst einen Simulator, der die Ausführung der Programme auch ohne Vorhandensein eines Zielsystems erlaubt. Die hier vorgestellten Programme und Funktionen sind unter Verwendung dieses Simulators entstanden.

Die von 3S-Software angebotene Raspberry-Laufzeitumgebung funktioniert im kostenlosen Demo-Modus zwei Stunden lang. Für die unbeschränkte Laufzeit ist eine Lizenz fällig, die an das Gerät gebunden ist und derzeit 35 Euro kostet. Im Download-Paket befinden sich sowohl eine um die Codesys-Komponenten erweiterte Raspbian-Umgebung für das Aufsetzen eines Neugeräts als auch ein Debian-Paket, das sich auf einem bereits konfigurierten Raspberry Pi einfach installieren lässt. Letzteres war im vorliegenden Fall allerdings mit einem kleinen Problem verbunden: Das installierte Programm /usr/bin/codesyscontrol.bin ist dynamisch gelinkt und erwartet eine Shared Lib unter [/lib/ld-linux-armhf.so.3. Diese existierte in der entsprechenden Raspbian-Distribution nicht, sodass sich das Programm nicht starten ließ. Ein

cd /lib; ln -s arm-linux-gnueabi/ld-2.13.so ld-linux-armhf.so.3

als Benutzer "root" löste das Problem.

Für die SPS-Programmierung existiert seit vielen Jahren ein Standard: die IEC-Norm 61131. Sie definiert insgesamt fünf Programmiersprachen, drei davon grafisch, zwei textbasiert. Im vorliegenden Anwendungsbeispiel hat sich der Autor auf die textbasierte Sprache ST (strukturierter Text, structured text) und die grafische Sprache FB (Funktionsblock, function block) konzentriert.

In der SPS-Programmierung unterscheidet man zwischen den Konstrukten PROGRAM, FUNCTION und FUNCTION_BLOCK. Letztere beiden dienen zur Definition wiederverwertbarer Bausteine. FUNCTION ist zustandsfrei und entspricht dem klassischen Funktionsbegriff aus der herkömmlichen Programmierung, während FUNCTION_BLOCK Variablen umfassen darf, deren Inhalt über mehrere Aufrufe hinweg erhalten bleibt und der daher über einen inneren Zustand verfügt. Ein PROGRAM fasst Funktionen und Funktionsblöcke zu einer Einheit zusammen, die in der SPS ausgeführt wird und damit ihr Verhalten festlegt.

Für Entwickler, die mit Hochsprachen wie Pascal, C oder objektorientierten Sprachen vertraut sind, bedeutet die SPS-Programmierung eine gewisse Umstellung. Das eigene Programm wird in ein Laufzeitsystem der SPS eingebettet und von diesem zyklisch aufgerufen. Datenwerte im Speicher repräsentieren dabei Ein- und Ausgänge der an den Controller angeschlossenen I/O-Bausteine. Stellt beispielsweise ein Baustein digitale Eingänge zur Verfügung, wird der Zustand eines jeden Eingangs als ein bestimmtes Bit im Speicher der SPS dargestellt. Ein Baustein mit digitalen Ausgängen wird entsprechend durch das Setzen der korrespondierenden Bits auf "0" oder "1" angesteuert. Diese Bits lassen sich auch lesen und damit ebenfalls in der Programmlogik verwenden.

Ein schreibender Zugriff auf Eingänge durch das Programmiersystem wird typischerweise unterbunden. Zu beachten ist dabei: Die Übertragung der Daten vom Baustein in den Speicher der SPS erfolgt zu Beginn eines jeden Zyklus durch das Laufzeitsystem. Das Setzen der Ergebniswerte in den IO-Bausteinen findet am Ende eines jeden Zyklus statt. Erzeugt also ein Programm innerhalb eines Zyklus unterschiedliche Werte für einen Ausgang, wird nur der letzte davon tatsächlich an den Baustein übertragen.

Entwickler stellen sich am besten um das eigene Programm herum eine Endlosschleife der folgenden Art vor:

while true
{
sleep x ms
read IO values from bus
program execution
write output values to bus
}

Die Laufzeit des PROGRAM-Blocks sollte dabei möglichst kurz sein, da ja erst nach seiner Abarbeitung tatsächlich das Ergebnis an den angeschlossenen Bausteinen sichtbar wird. Aufgaben, die mehrere Schritte oder das Warten auf ein externes Ereignis oder einen bestimmten Zeitpunkt enthalten, sind in einzelne Schritte zu zerlegen. Dies muss so erfolgen, dass in einem Zyklus immer nur der Schritt ausgeführt wird, der gerade tatsächlich zur Ausführung ansteht. Das erfordert gelegentlich ein etwas aufwendiges internes Zustandsmanagement, insbesondere wenn komplexe Kommunikationsprotokolle zu befolgen sind.

Eine SPS erlaubt typischerweise die Konfiguration mehrerer Tasks mit unterschiedlichen Aufrufintervallen und Prioritäten. Im Beispiel ist ein Task dafür zuständig, die Lichtschalter im Haus abzufragen und entsprechend die Lampen zu schalten. Die Logik hierzu erfordert keine komplexen Rechnungen. Die Ausführungszeit eines entsprechenden Programms liegt daher im Milliksekundenbereich – also für den Menschen, der den Lichtschalter betätigt, nicht wahrnehmbar. Der Task für die Lichtsteuerung läuft im Zyklus von 20 Millisekunden, die durchschnittliche Ausführungszeit liegt bei zwei Millisekunden.

Mit der Steuerung der Jalousien gingen Berechnungen des Sonnenstandes einher. Sie basieren auf trigonometrischen Funktionen, die auf dem Controller ohne FPU einige Millisekunden in Anspruch nehmen. Für den aktuellen Anwendungsfall ist eine minütliche Berechnung ausreichend. Aus diesem Grund wurde die Rechnung in einen entsprechenden Task mit niedriger Priorität ausgelagert. Gleiches gilt für die Steuerung der Fußbodenheizung: Auch hier reicht ein Task, der nur einmal pro Minute ausgeführt wird. Kürzere Zyklen wären unnötig, da allein das Öffnen und Schließen der im Haus verbauten thermischen Ventile schon zwei Minuten dauert.

Im Falle eines SPS-Laufzeitsystems mit preemptivem Multitasking können Tasks höherer Priorität solche mit niedrigerer auch unterbrechen. Deren Ausführung wird solange angehalten, bis der höher priorisierte Task abgearbeitet ist, und erst dann wieder fortgesetzt. Solche Fälle können dazu führen, dass sich die Werte der IO-Variablen während der Abarbeitung eines Zyklus plötzlich ändern, weil ein höher priorisierter Task das entsprechende Speicherabbild aktualisiert hat. Dies ist bei der Aufteilung der Steuerungsaufgaben auf die einzelnen Tasks zu berücksichtigen.

Als Beispiel für ein SPS-Programm dient nun die Lichtsteuerung mit einem Stromstoßschalter mit eingebauter Zeituhr. Per Tastendruck soll die angeschlossene Lampe an- oder ausgeschaltet werden, gleichzeitig soll sie nach einer konfigurierbaren Zeit von selbst ausgehen. Die verbleibende Brenndauer wird zusätzlich mit ausgegeben und lässt sich beispielsweise in einer grafischen Oberfläche anzeigen. Als Komfortfunktion soll zudem die Möglichkeit vorgesehen werden, das Licht über einen Zentralschalter aus oder (als Alarmlicht) einschalten zu können, unabhängig vom vorherigen Schaltzustand. Als Funktionsblock soll die Lichtsteuerung also folgendermaßen aussehen:

Die Lichtsteuerung als Baustein in FB-Syntax (Abb. 1)

Die Lichtsteuerung als Baustein in FB-Syntax (Abb. 1)

Der Baustein selbst wird in der Pascal-ähnlichen Sprache ST implementiert. Zunächst die Deklarationen der Ein- und Ausgabeparameter:

FUNCTION_BLOCK lightcontroller
VAR_INPUT
switch: BOOL; (* Schaltereingang *)
all_on: BOOL; (* Eingang für Alarmschalter *)
all_off: BOOL; (* Eingang für "Alles aus" *)
max_seconds: DINT; (* maximale Brenndauer der Lampe in Sekunden *)
END_VAR
VAR_OUTPUT
light: BOOL; (* Ausgang für die Lampe *)
remaining: DINT; (* Restliche Leuchtdauer in Sekunden *)
END_VAR

Um die abgelaufene Zeit zu ermitteln, ist ein Sekundenzähler notwendig. Die Zeitroutinen des IEC-Standards sind in dieser Hinsicht recht minimalistisch. Insbesondere existieren keine Standardfunktionen für die Ermittlung von Datum und Uhrzeit, da der Standard nicht davon ausgeht, dass alle Steuerungen tatsächlich über eine Echtzeituhr verfügen. Steuerungen mit einer ebensolchen bieten den Zugriff auf diese typischerweise über eine entsprechende Bibliotheksfunktion an. Für das Beispiel sei beim Standard geblieben. Er definiert eine Funktion TIME(), welche die Zeit in Millisekunden seit Systemstart liefert. Da der Wert intern als 32-Bit-Wert codiert wird, kommt es alle 49 Tage zu einem "Überlauf", sodass sich der Wert nicht ohne weiteres einsetzen lässt. Als Hilfsfunktion dient hierbei ein Sekundenzähler, der nach einem Initialisierungssignal als Eingang die abgelaufenen Sekunden als Ausgang liefert und auch den Überlauf von TIME() intern abfängt:

Sekundenzähler in FB-Syntax (Abb. 2)

Sekundenzähler in FB-Syntax (Abb. 2)

Der Sekundenzähler ist wiederum als Funktionsblock in ST implementiert:

FUNCTION_BLOCK runseconds
VAR_INPUT
init: BOOL; (* startet den Zaehler *)
END_VAR
VAR_OUTPUT
seconds: UDINT;
END_VAR
VAR
runtime: UDINT;
lasttime: UDINT;
currtime: UDINT;
maxudint: UDINT := 4294967295; (* Höchster 32-Bit-Wert *)
END_VAR


(* Implementierung *)
IF init
THEN
runtime := 0;
lasttime := TIME_TO_UDINT(myTIME());
seconds := 0;
ELSE
currtime := TIME_TO_UDINT(myTIME());
IF currtime >= lasttime
THEN
(* Standardfall *)
runtime := runtime + (currtime - lasttime);
ELSE
(* Überlauf *)
runtime := runtime + (maxudint - lasttime) + currtime;
END_IF
lasttime := currtime;
seconds := runtime / 1000;
END_IF

Der Quelltext von runseconds verwendet anstelle der Funktion TIME die Funktion myTime(). Diese multipliziert das Ergebnis von TIME() mit einem konfigurierbaren Multiplikator, um den Test der Überlaufbehandlung zu vereinfachen. Für den Test von runseconds wurde im Beispiel als Multiplikator 1024 eingesetzt. Der Vollständigkeit halber hier der Quelltext von mytime:

FUNCTION mytime : TIME
VAR_INPUT
END_VAR
VAR
(* Für Debugging hier einen Multiplikator eintragen.
1024 erzeugt ca 20x pro Tag einen Überlauf *)
mult : UINT := 1;
END_VAR


(* Die Implementierung passt in eine Zeile *)
mytime := TIME() * mult;

Im Vergleich zum Funktionsblock fällt auf, dass eine Funktion keine Ausgabevariable definiert. Der Rückgabewert der Funktion wird durch Zuweisung an eine implizit definierte Variable mit dem Namen der Funktion (mytime) gesetzt. Der Code liefert außerdem ein Beispiel dafür, wie Variablen in ST initialisiert werden.

In der vorliegenden Implementierung unterstützt runseconds Laufzeiten von bis zu 49 Tagen, was für diesen und die meisten anderen Anwendungsfälle ausreichend sein sollte. Für längere Laufzeiten müsste die Variable runtime vom ULINT 64 Bit sein, wodurch sich allerdings auf den meisten Steuerungen das Laufzeitverhalten mangels nativer 64-Bit-Unterstützung etwas verschlechtern dürfte.

Da eine Instanz von runseconds intern in dem Lightcontroller benötigt wird, wird eine entsprechende Variable angelegt:

VAR
runtime: runseconds;
END_VAR

Nun zur Implementierung. Der einfachste Fall ist all_off:

IF all_off
THEN
remaining := 0;
light := FALSE;
RETURN;
END_IF

RETURN bricht die Verarbeitung der Funktion an dieser Stelle ab. Implizit erhält damit der all_off-Eingang die höchste Priorität, die Belegung der beiden anderen Schaltereingänge bleibt davon unberücksichtigt.

Der zweite Fall – Alarmlicht:

IF all_on
THEN
runtime(init:= TRUE);
remaining := max_seconds;
light := TRUE;
RETURN;
END_IF

Hiermit lässt sich der Laufzeitzähler initialisieren. Das Licht bleibt also so lange an, bis es entweder ausgeschaltet wird oder die maximale Laufzeit erreicht.

Schließlich der Hauptfall: Das Schalten über den Lichtschalter:

IF switch
THEN
IF light
THEN
(* Licht ist an, wir schalten aus *)
light := FALSE;
remaining := 0;
RETURN;
ELSE
(* Licht ist aus, wir schalten an *)
light := TRUE;
remaining := max_seconds;
runtime(init:= TRUE);
RETURN;
END_IF
END_IF

Sobald das Licht aus ist, wird auch der Sekundenzähler nicht mehr aufgerufen. Daher fehlt im ersten Zweig der entsprechende Aufruf. Die meiste Zeit wird allerdings kein Schalter gedrückt sein. Hier noch der Codeblock für diesen Fall:

(* Kein Schalter gedrückt *)
IF light
THEN
(* Prüfen, ob maximale Brenndauer erreicht *)
runtime(init:=FALSE);
remaining := max_seconds - UDINT_TO_DINT(runtime.seconds);
IF remaining <= 0
THEN
light := FALSE;
remaining := 0;
END_IF
END_IF

Die Konvertierung von runtime.seconds könnte man im Prinzip auch weglassen, Codesys erzeugt in dem Fall allerdings eine Warnung. Da es theoretisch sein könnte, dass die Routine seltener als einmal pro Sekunde aufgerufen wird, rechnet man beim Wert remaining mit einem vorzeichenbehafteten Datentyp und reagiert auch auf Werte unter null, also eine Überschreitung der vorgegebenen Laufzeit. Im Hauptprogramm der SPS könnte ein Lightcontroller-Baustein dann folgendermaßen verwendet werden:

Der Controllerbaustein in FB (Abb. 3)

Der Controllerbaustein in FB (Abb. 3)

In diesem Fall sind die Eingänge für all_on und all_off mit Konstanten belegt, das Licht schaltet sich nach spätestens vier Stunden selbst aus.

Eine Erweiterung des Anwendungsfalls zeigt die Flexibilität, die man durch den Einsatz einer SPS im Haus gewinnt: Will man beispielsweise das Licht in einem Flur von jedem angrenzenden Zimmer aus schalten können, lässt sich das elegant in der Software durch Hinzunahme einer Oder-Verknüpfung (OR) lösen. Der OR-Baustein gehört zu den Standardfunktionen in IEC61131 und kann beliebig viele Eingangswerte erhalten.

Fünf Lichtschalter schalten die gleiche Lampe (Abb. 4).

Fünf Lichtschalter schalten die gleiche Lampe (Abb. 4).

Eine SPS-Programmierung ist auch für die Heimautomatisierung gut geeignet. Sie bietet die notwendige Flexibilität, auch nachträglich bestimmte Anforderungen umzusetzen. Das ist bei einer klassischen Hauselektrik meist mit wesentlich höherem Aufwand verbunden. Hat sich ein Entwickler erst einmal in die Funktionsweise von SPS eingefunden, bietet das System praktische Steuerungsmöglichkeiten.

Und es gibt noch eine gute Nachricht: Auch in der SPS-Programmierung gibt es Open Source. Seit 2006 programmiert eine kleine Entwicklergemeinde, die Open Source Community for Automation Technology, die OSCAT [1]-Bibliothek. Aufgrund des Umfangs von inzwischen mehreren hundert Funktionen und Funktionsbausteinen ist die Bibliothek seit einiger Zeit dreigeteilt in Basis-, Netzwerk- und Gebäudetechnikfunktionen. Alle Bausteine basieren ausschließlich auf IEC-61131-Standardfunktionen und liegen komplett im Quelltext vor. Das macht Anpassungen an die eigenen Bedürfnisse einfach umsetzbar. Die Community betreibt ein Forum, in dem Fragen zur Anwendung der Bausteine auf den unterschiedlichen SPS-Plattformen sowie Vorschläge zur Erweiterung der Bibliothek diskutiert werden.

Die Idee für die Implementierung der Funktion myTime() in diesem Artikel basiert auf einer Idee aus der OSCAT-Bibliothek. Dort heißt die vergleichbare Funktion T_PLC_MS(). Der Quelltext der OSCAT-Bibliothek bietet viele Beispiele für die Lösung gängiger und ausgefallener Automatisierungsprobleme und damit eine fruchtbare Informationsquelle für jeden, der sich ernsthaft mit SPS-Programmierung beschäftigt.

Wolfgang Klimt
ist Diplom-Informatiker und Leiter des Bereichs Delivery der ConSol* Consulting & Solutions Software GmbH in München. Seit Anfang der 1990er-Jahre entwickelt er Software, administriert Unix-Rechner und tunt Datenbanken. Seit 2007 lebt er mit seiner Familie, einer Katze und ein paar Fischen in einem "intelligenten" Haus, programmiert an der Steuerung und sammelt dabei haufenweise Messdaten.
(ane [2])


URL dieses Artikels:
https://www.heise.de/-2680300

Links in diesem Artikel:
[1] http://www.oscat.de/
[2] mailto:ane@heise.de