IEC 61131-3: Arrays mit variabler Länge

Bei der Deklaration von Arrays musste bisher immer eine konstante Größe angegeben werden. Ab der 3rd Edition der IEC 61131-3 können Arrays mit einer variablen Länge deklariert werden. Funktionen lassen sich dadurch deutlich generischer anlegen als bisher.

Zwar können für die Arraygrenzen auch Variablen benutzt werden, diese Variablen müssen aber als Konstanten deklariert werden. Eine Anpassung der Arraygrenzen zur Laufzeit ist somit nicht möglich.

PROGRAM MAIN
VAR
  arrData             : ARRAY[1..ARRAY_UPPER_BOUND] OF INT;
END_VAR
VAR CONSTANT
  ARRAY_UPPER_BOUND   : INT := 10;
END_VAR

Gerade wenn Arrays als Parameter an Funktionen oder Funktionsblöcken übergeben werden, stellen feste Arraygrenzen eine unangenehme Limitierung dar. Ist diese nicht hinnehmbar, musste bisher auf Pointerarithmetik gewechselt werden, mit allen Nachteilen. Hier ein einfaches Beispiel, welches die Summe eines eindimensionalen Arrays von LREAL-Variablen berechnet.

FUNCTION F_CalcSum1DimArrayOldSchool : LREAL
VAR_INPUT
  pData           : POINTER TO LREAL;
  nSize           : UDINT;
END_VAR
VAR
  pDataIndex      : POINTER TO LREAL;
  nUpperIndex     : UDINT;
  nIndex          : UDINT;
END_VAR

F_CalcSum1DimArrayOldSchool := 0;
nUpperIndex := nSize / SIZEOF(pDataIndex^);
IF (nUpperIndex > 0) THEN
  FOR nIndex := 0 TO (nUpperIndex - 1) DO
    pDataIndex := pData + (nIndex * SIZEOF(pDataIndex^));
    F_CalcSum1DimArrayOldSchool := F_CalcSum1DimArrayOldSchool + pDataIndex^;
  END_FOR
END_IF

Die Funktion kann für die Addition beliebiger LREAL-Arrays genutzt werden. Sie ist unabhängig von der Anzahl der Elemente und von der oberen und unteren Arraygrenze.

PROGRAM MAIN
VAR
  array01    : ARRAY[2..8] OF LREAL := [16.1, 34.1, 4.1, 43.1, 35.1, 2.1, 65.1];
  lrSum01    : LREAL;

  array02    : ARRAY[-1..2] OF LREAL := [16.1, 34.1, 9.1, 13.1];
  lrSum02    : LREAL;

  array03    : ARRAY[-3..-1] OF LREAL := [16.1, 34.1, 8.1];
  lrSum03    : LREAL;
END_VAR
lrSum01 := F_CalcSum1DimArrayOldSchool(ADR(array01), SIZEOF(array01));
lrSum02 := F_CalcSum1DimArrayOldSchool(ADR(array02), SIZEOF(array02));
lrSum03 := F_CalcSum1DimArrayOldSchool(ADR(array03), SIZEOF(array03));

Beispiel 1 (TwinCAT 3.1.4020) auf GitHub

Allerdings hat diese Lösung einige Nachteile. Zum einen eben die Tatsache das Pointerarithmetik benutzt werden muss. Der Quellcode der Funktion wird schon bei relativ einfachen Aufgaben recht komplex. Zum anderen muss an die Funktion auch eine Größen- bzw. Längenangabe übergeben werden. Bei dem Aufruf muss also sichergestellt werden, das der Pointer auf das Array und die Längenangabe übereinstimmen.

Seit der 3rd Edition der IEC 61131-3 können Arrays auch mit variabler Arraygrenze definiert werden. Statt der Arraygrenze, wird ein ’*’ angegeben:

arrData   : ARRAY[*] OF LREAL;

Wird die Funktion aufgerufen, so muss das übergebene Array konstante Arraygrenzen besitzen. In der Funktion kann über die Funktionen LOWER_BOUND und UPPER_BOUND die jeweilige obere- und untere Arraygrenze abgefragt werden.

Derzeit können Arrays mit variabler Länge nur an VAR_IN_OUT Variablen von Funktionen, Funktionsblöcken und Methoden übergeben werden (bleibt zu hoffen, dass in Zukunft auch VAR_INPUT und VAR_OUTPUT Variablen unterstützt werden).

Hier das angepasste Beispiele:

FUNCTION F_CalcSum1DimArray : LREAL
VAR_IN_OUT
  arrData    : ARRAY[*] OF LREAL;
END_VAR
VAR
  nIndex     : DINT;
END_VAR
F_CalcSum1DimArray := 0;
FOR nIndex := LOWER_BOUND(arrData, 1) TO UPPER_BOUND(arrData, 1) DO
  F_CalcSum1DimArray := F_CalcSum1DimArray + arrData[nIndex];
END_FOR

Die Funktion erwartet als Eingangsparameter nur noch ein Array von LREAL-Werten. Die Anzahl der Array-Elemente ist variabel. Mit LOWER_BOUND und UPPER_BOUND kann eine Iteration über das gesamte Array durchgeführt werden. Der Quellcode ist deutlich lesbarer als im ersten Beispiel.

PROGRAM MAIN
VAR
  array01    : ARRAY[2..8] OF LREAL := [16.1, 34.1, 4.1, 43.1, 35.1, 2.1, 65.1];
  lrSum01    : LREAL;

  array02    : ARRAY[-1..2] OF LREAL := [16.1, 34.1, 9.1, 13.1];
  lrSum02    : LREAL;

  array03    : ARRAY[-3..-1] OF LREAL := [16.1, 34.1, 8.1];
  lrSum03    : LREAL;
END_VAR
lrSum01 := F_CalcSum1DimArray(array01);
lrSum02 := F_CalcSum1DimArray(array02);
lrSum03 := F_CalcSum1DimArray(array03);

Beispiel 2 (TwinCAT 3.1.4020) auf GitHub

Auch werden mehrdimensionale Arrays unterstützt. Bei der Deklaration müssen alle Dimensionen als variabel angelegt werden:

arrData    : ARRAY[*, *, *] OF LREAL;

Der zweite Parameter von UPPER_BOUND und LOWER_BOUND gibt die Dimension an, von der die jeweilige Arraygrenze ermittelt werden soll.

FUNCTION F_CalcSum3DimArray : LREAL
VAR_IN_OUT
   arrData      : ARRAY[*, *, *] OF LREAL;
END_VAR
VAR
   nIndex1, nIndex2, nIndex3  : DINT;
END_VAR
F_CalcSum3DimArray := 0;
FOR nIndex1 := LOWER_BOUND(arrData, 1) TO UPPER_BOUND(arrData, 1) DO
  FOR nIndex2 := LOWER_BOUND(arrData, 2) TO UPPER_BOUND(arrData, 2) DO
    FOR nIndex3 := LOWER_BOUND(arrData, 3) TO UPPER_BOUND(arrData, 3) DO
      F_CalcSum3DimArray := F_CalcSum3DimArray + arrData[nIndex1, nIndex2, nIndex3];
    END_FOR
  END_FOR
END_FOR

Bei dem Aufruf kann ein beliebiges dreidimensionales Array von LREAL-Werten an die Funktion übergeben werden.

PROGRAM MAIN
VAR
  array01    : ARRAY[1..2, 3..4, 5..6] OF LREAL := [16.1, 34.1, 4.1, 43.1, 35.1, 2.1, 65.1, 16.1];
  lrSum01    : LREAL;
END_VAR
lrSum01 := F_CalcSum3DimArray(array01);

Beispiel 3 (TwinCAT 3.1.4020) auf GitHub

Somit lassen sich auch komplexere Aufgaben flexibel umsetzen ohne das auf Pointerarithmetik zurückgegriffen werden muss.

Dieses soll zum Schluss an einem Funktionsblock gezeigt werden, der zwei Matrizen miteinander multipliziert. Die Größen der Matrizen sind variabel:

METHOD PUBLIC Multiplication : BOOL
VAR_IN_OUT
  arrayA     : ARRAY[*, *] OF DINT;
  arrayB     : ARRAY[*, *] OF DINT;
  arrayX     : ARRAY[*, *] OF DINT;
END_VAR
VAR
  nIndex1, nIndex2, nIndex3, nIndex4   : DINT;
END_VAR;
FOR nIndex1 := LOWER_BOUND(arrayA, 1) TO UPPER_BOUND(arrayA, 1) DO
  FOR nIndex2 := LOWER_BOUND(arrayB, 2) TO UPPER_BOUND(arrayB, 2) DO
    nIndex4 := 0;
    FOR nIndex3 := LOWER_BOUND(arrayA, 2) TO UPPER_BOUND(arrayA, 2) DO
      nIndex4 := nIndex4 + arrayA[nIndex1, nIndex3] * arrayB[nIndex3, nIndex2];
    END_FOR;
    arrayX[nIndex1, nIndex2] := nIndex4;
  END_FOR;
END_FOR;

Die Methode kann mit unterschiedlich großen Arrays aufgerufen werden.

PROGRAM MAIN
VAR
  fbMatrix     : FB_Matrix;
  arrayA1      : ARRAY[1..2, 1..2] OF DINT := [1, 2, 3, 4];
  arrayB1      : ARRAY[1..2, 1..2] OF DINT := [5, 6, 7, 8];
  arrayX1      : ARRAY[1..2, 1..2] OF DINT;

  arrayA2      : ARRAY[1..3, 1..3] OF DINT := [1, 2, 3, 4, 5, 6, 7, 8, 9];
  arrayB2      : ARRAY[1..3, 1..3] OF DINT := [5, 6, 7, 8, 10, 11, 12, 13, 14];
  arrayX2      : ARRAY[1..3, 1..3] OF DINT;
END_VAR
fbMatrix.Multiplication(arrayA1, arrayB1, arrayX1);
fbMatrix.Multiplication(arrayA2, arrayB2, arrayX2);

Beispiel 4 (TwinCAT 3.1.4020) auf GitHub

Author: Stefan Henneken

I’m Stefan Henneken, a software developer based in Germany. This blog is just a collection of various articles I want to share, mostly related to Software Development.

11 thoughts on “IEC 61131-3: Arrays mit variabler Länge”

    1. Nein. In der Norm IEC 61131-3 Ed 3.0 wird diese Möglichkeit ab Kapitel 6.5.3 beschrieben.
      Es ist durchaus möglich, das nicht alle Punkte der Norm zu 100 % von einem Hersteller umgesetzt werden.
      Gruß
      Stefan henneken

  1. Hallo Stefan.
    Danke für diesen tollen Blog.
    Mit dem __New Operator lassen sich auch zur Laufzeit Arrays variabler Länge erstellen. Gibt es auch eine Möglichkeit ein solches Array mit der TcAds-Bibliothek zu lesen?
    Norman

    1. Hallo Norman,
      ja, mit einer aktuellen TC3 Version ist dieses möglich. Pointer, die sich in der SPS befinden, lassen sich per ADS dereferenzieren. Man kommt also an die Werte, auf die der Pointer zeigt. Hier ein kleines Beispiel:
      Das SPS-Programm:

      {attribute ‘enable_dynamic_creation’}
      TYPE ST_Test :
      STRUCT
      fA : LREAL;
      nB : INT;
      bC : BOOL;
      END_STRUCT
      END_TYPE

      PROGRAM MAIN
      VAR
      pData : POINTER TO ST_Test;
      END_VAR
      IF (pData = 0) THEN
      pData := __NEW(ST_Test);
      pData^.fA := 3.1415;
      pData^.nB := 1234;
      pData^.bC := TRUE;
      END_IF

      Zum Testen habe ich ein C# Programm genutzt.

      TcAdsClient tcClient = new TcAdsClient();
      tcClient.Connect(“192.168.0.100.1.1”, 851);

      int hVar = tcClient.CreateVariableHandle(“MAIN.pData^”);

      AdsStream dataStream = new AdsStream(16);
      BinaryReader binRead = new BinaryReader(dataStream);

      tcClient.Read(hVar, dataStream);

      dataStream.Position = 0;
      Debug.WriteLine(binRead.ReadDouble().ToString());
      Debug.WriteLine(binRead.ReadInt16().ToString());
      Debug.WriteLine(binRead.ReadBoolean().ToString());

      tcClient.DeleteVariableHandle(hVar);

      Wichtig ist hierbei, das der Zugriff auf die Werte, auf die der Pointer zeigt, nur per Symbolname m;glich ist.
      Als Symbolname muss unbedingt das ^ mit angegeben werden, ansonsten erhält man die Adresse zurück, auf die der Pointer zeigt.
      Bei Arrays können nur einzelne Elemente gelesen werden. Also als Symbolname kann nicht nicht das ganze Array angegeben werden, sondern nur ein Element aus dem Array: “MAIN.pData[5]^”

      Ich hoffe dir damit weitergeholfen zu haben.
      Stefan

    1. Hallo Timo,
      das ist leider nicht möglich. Die Paramater, die man per FB_init() an den FB übergeben möchte, müssen im VAR_INPUT-Bereich stehen. Arrays mit variabler Länge müssen per VAR_IN_OUT übergeben werden. Das schließt sich leider gegenseitig aus.
      Gruß
      Stefan

  2. Hallo Herr Henneken,

    sehen/ kennen Sie eine Möglichkeit ein variables Array als Typ während der Laufzeit mit dem __NEW Befehl oder ähnlichem zu erzeugen?

    var:
    pArrayBytes : POINTER TO ARRAY[*] OF INT;
    end_var

    pArrayBytes := __NEW(INT, 25);

    –> Übergabe an Funktion als variables Array, welches jetzt die Länge 25 hat

    Ich danke
    Hannes

    1. Hallo Hannes,
      bei deinem Ansatz wird der Compiler die Zeile anmerken wo der Pointer deklariert wird: pArrayBytes : POINTER TO ARRAY[*] OF INT.
      ‘ARRAY[*]’ erlaubt der Compiler nur im VAR_IN_OUT Bereich. ‘POINTER TO INT’ und ‘ARRAY[*] OF INT’ sind für den Compiler zwei unterschiedliche Datentypen. Deshalb kann mit dem POINTER OF INT keine Funktion aufgerufen werden die als Datentyp ein ARRAY[*] OF INT erwartet.
      Hier würde ich ebenfalls den klassischen Weg nehmen und auch bei Funktion einen Pointer als Datentyp für den Parameter nehmen.
      Aber für deine Idee, ARRAY[*] zu benutzen und diese Arrays dynamisch zu erzeugen, gibt es sicher ausreichend Anwendungsfälle.
      Stefan

  3. Ich habe festgestellt, dass LOWER_BOUND(arrData, 1) TO UPPER_BOUND(arrData, 1)
    nur bei Arrays deren Array Grenzen mit Konstanten definiert wurden funktionieren.
    Die Übergabe per [*] funktioniert nicht bei einem

    VAR
    arrData : ARRAY[1..10] OF INT;
    END_VAR

    nicht, sondern nur wie oben von Dir erklärt :

    PROGRAM MAIN
    VAR
    arrData : ARRAY[1..ARRAY_UPPER_BOUND] OF INT;
    END_VAR
    VAR CONSTANT
    ARRAY_UPPER_BOUND : INT := 10;
    END_VAR

    Liebe Grüße Darius

Leave a comment