Makers Blog Archive

Datalogging in IEC 61131-3: The basic principle

Sven Lemmens 05 December 2019 min. read
484 views 0 comments

PLCnext Technology offers different possibilities to capture data.

Some examples:

  • PLCnext Engineer library: The function blocks for Data Logging are used to collect data of any data types and store into csv or binary files. In combination with the PLCnextBaseLibrary it is possible to copy the data to an FTP server.
  • Datalogger: A service component of the PLCnext Technology firmware. This component transfers real-time data from the GDS to a database for recording and storage purposes. When starting and stopping the PLCnext Technology firmware, the DataLogger is started and stopped automatically. XML configuration files are used for configuration of the DataLogger. The DataLogger can record data from any IN or OUT ports and variables.
  • App available in the PLCnext Store: DataLogger powered by FourZero

If you want to build something specific for your application, this blog is what you need.

We are going to build up a PLCnext Engineer project which stores data on the flash memory.

What are the different steps for this task?

  • Log data in retain memory and add a timestamp
  • Convert this data to a byte array
  • Create a new file, write our data and close the file again

If you are interested in the complete PLCnext Engineer project, just let me know. (slemmens@phoenixcontact.be)

First, we are going to create a custom datatype for our memory structure.

strDataSampleTimestamp: // Timestamp for each dataset
        STRUCT   
            iDay:       INT;
            iMonth:     INT;
            iYear:      INT;
            iHour:      INT;
            iMinute:    INT;
            iSecond:    INT;
        END_STRUCT;

strDataSample: // Dataset including timestamp
        STRUCT
            strTimestamp:   strDataSampleTimestamp;
            iValue_1:       INT;
            xValue_2:       BOOL;
            rValue_3:       REAL;
            sValue_4:       STRING;
        END_STRUCT;

arr_1_20_strDataSample:     ARRAY[1..20] OF strDataSample; // In total we have a buffer for 20 datasets

strDataMemory: // The complete structure including pointers for read and write actions
        STRUCT
            iWritePointer:      INT;
            iReadPointer:       INT;
            iDataCount:         INT;
            arrData:            arr_1_20_strDataSample;
        END_STRUCT;

Second step is to build up some PLC code:

  • a function block which is able to write data on the flash memory
  • a method for the above function block to insert data
  • a program which calls the above method
// *** Function block code: ***

CASE iLogDataSetToFile OF

0:  IF strDataBuffer.iDataCount > 0 THEN /* Data available */
        iLogDataSetToFile:= 1;
    END_IF;

1:  /* Convert all datapoints to a string */

    IF strDataBuffer.iReadPointer = 0 THEN /* This memory location does not exist */
        strDataBuffer.iReadPointer:= 1;
    END_IF;

    iTempReadPointer:= strDataBuffer.iReadPointer;

    sTempInput_1:= TO_STRING(strDataBuffer.arrData[iTempReadPointer].iValue_1,'{0:d}');
    IF strDataBuffer.arrData[iTempReadPointer].xValue_2 THEN
        sTempInput_2:= 'TRUE';
    ELSE
        sTempInput_2:= 'FALSE';
    END_IF;
    sTempInput_3:= TO_STRING(strDataBuffer.arrData[iTempReadPointer].rValue_3,'{0:f2}');
    sTempInput_4:= strDataBuffer.arrData[iTempReadPointer].sValue_4;

    iLogDataSetToFile:= 2;

2:  /* Build up the timestamp */

    sTimeStampValue:= TO_STRING(strDataBuffer.arrData[iTempReadPointer].strTimestamp.iDay,'{0:d2}');
    sTempText:= sTimeStampValue;
    sTempText:= CONCAT(sTempText,'/');
    sTimeStampValue:= TO_STRING(strDataBuffer.arrData[iTempReadPointer].strTimestamp.iMonth,'{0:d2}');
    sTempText:= CONCAT(sTempText,sTimeStampValue);
    sTempText:= CONCAT(sTempText,'/');
    sTimeStampValue:= TO_STRING(strDataBuffer.arrData[iTempReadPointer].strTimestamp.iYear,'{0:d}');
    sTempText:= CONCAT(sTempText,sTimeStampValue);
    sTempText:= CONCAT(sTempText,' ');  
    sTimeStampValue:= TO_STRING(strDataBuffer.arrData[iTempReadPointer].strTimestamp.iHour,'{0:d2}');
    sTempText:= CONCAT(sTempText,sTimeStampValue);
    sTempText:= CONCAT(sTempText,':');
    sTimeStampValue:= TO_STRING(strDataBuffer.arrData[iTempReadPointer].strTimestamp.iMinute,'{0:d2}');
    sTempText:= CONCAT(sTempText,sTimeStampValue);
    sTempText:= CONCAT(sTempText,':');
    sTimeStampValue:= TO_STRING(strDataBuffer.arrData[iTempReadPointer].strTimestamp.iSecond,'{0:d2}');
    sTempText:= CONCAT(sTempText,sTimeStampValue);
    sTempText:= CONCAT(sTempText,' ');     

    iLogDataSetToFile:= 3;

3:  /* Build up the complete text */

    sTempText:= CONCAT(sTempText,sTempInput_1);
    sTempText:= CONCAT(sTempText,';');
    sTempText:= CONCAT(sTempText,sTempInput_2);
    sTempText:= CONCAT(sTempText,';');   
    sTempText:= CONCAT(sTempText,sTempInput_3);
    sTempText:= CONCAT(sTempText,';');      
    sTempText:= CONCAT(sTempText,sTempInput_4);

    iLengthTempText:= LEN(sTempText);

    iLogDataSetToFile:= 4;

4:  /* Convert to a byte array */

    xConvert:= TRUE;
    diLengthTempText:= TO_DINT(iLengthTempText);
    iLogDataSetToFile:= 5;

5:  IF xDoneConvert THEN
        xConvert:= FALSE;
        iTempPointer:= iLengthTempText + 1;
        arrBuffer_FW[iTempPointer]:= BYTE#16#0D; /* Carriage return */
        iTempPointer:= iTempPointer + 1;
        arrBuffer_FW[iTempPointer]:= BYTE#16#0A; /* Line feed */
        udiLength_FW:= TO_UDINT(iTempPointer);
        iLogDataSetToFile:= 6;
    END_IF;

6:  /* Open a file */

    sName_FO:= 'ExampleMakersBlog.txt';
    xExecute_FO:= TRUE;
    iLogDataSetToFile:= 7;

7:  IF xDone_FO THEN
        uiHandleMemory:= uiHandle_FO;
        xExecute_FO:= FALSE;
        iLogDataSetToFile:= 8;
    END_IF;

8:  /* Move the current file pointer to a new position, in our case the end of the file */

    xExecute_FS:= TRUE;
    uiHandle_FS:= uiHandleMemory;
    diPosition_FS:= DINT#0;
    uiMode_FS:= UINT#2;
    iLogDataSetToFile:= 9;

9:  IF xDone_FS THEN
        xExecute_FS:= FALSE;
        iLogDataSetToFile:= 100;
    END_IF;

10: /* Write data in the file */

    xExecute_FW:= TRUE;
    uiHandle_FW:= uiHandleMemory;
    iLogDataSetToFile:= 11;

11: IF xDone_FW THEN
        xExecute_FW:= FALSE;
        iLogDataSetToFile:= 12;
    END_IF;    

12: /* Close the file */

    xExecute_FC:= TRUE;
    uiHandle_FC:= uiHandleMemory;
    iLogDataSetToFile:= 13;

13: IF xDone_FC THEN
        xExecute_FC:= FALSE;

        strDataBuffer.iReadPointer:= strDataBuffer.iReadPointer + 1;
        IF strDataBuffer.iReadPointer > 20 THEN
            strDataBuffer.iReadPointer:= 1;
        END_IF;

        strDataBuffer.iDataCount:= strDataBuffer.iDataCount - 1;

        iLogDataSetToFile:= 0;
    END_IF;      

END_CASE;

/* Converts the string variable to a byte array */

ConvertString_To_Buf(Req := xConvert,
Buf_Format := FALSE,
Buf_Offs := DINT#0,
Buf_Cnt := diLengthTempText,
Done => xDoneConvert,
Error => xErrorConvert,
Status => iStatusConvert,
SRC := sTempText,
BUFFER := arrBuffer_FW);    

/* Opens an existing file in the parameterization memory with a specific name or creates a new file. */

FileOpenLogging(Execute := xExecute_FO,
Name := sName_FO,
Done => xDone_FO,
Handle => uiHandle_FO,
Error => xError_FO,
ErrorID => uiErrorID_FO);

/* Moves the current file pointer to a new position. */

FileSeekLogging(Execute := xExecute_FS,
Handle := uiHandle_FS,
Position := diPosition_FS,
Mode := uiMode_FS,
Done => xDone_FS,
Error => xError_FS,
ErrorID => uiErrorID_FS);

/* Writes data to a file that was opened previously. */

FileWriteLogging(Execute := xExecute_FW,
Handle := uiHandle_FW,
Done => xDone_FW,
LengthWritten => udiLengthWritten_FW,
Buffer := arrBuffer_FW,
Length := udiLength_FW,
Error => xError_FW,
ErrorID => uiErrorID_FW);

/* Closes a file in the parameterization memory that has been opened using the FILE_OPEN function block. */

FileCloseLogging(Execute := xExecute_FC,
Handle := uiHandle_FC,
Done => xDone_FC,
Error => xError_FC,
ErrorID => uiErrorID_FC);
// *** Method code: ***

IF strDataBuffer.iWritePointer = 0 THEN /* This memory location does not exist */
    strDataBuffer.iWritePointer:= 1;
END_IF;

IF strDataBuffer.iDataCount < 20 THEN /* Free space in buffer */

    iTempWritePointer:= strDataBuffer.iWritePointer; /* Select current write position */

    /* Data */

    strDataBuffer.arrData[iTempWritePointer].iValue_1:= iInput_1;
    strDataBuffer.arrData[iTempWritePointer].xValue_2:= xInput_2;
    strDataBuffer.arrData[iTempWritePointer].rValue_3:= rInput_3;   
    strDataBuffer.arrData[iTempWritePointer].sValue_4:= sInput_4;

    /* Timestamp */

    strDataBuffer.arrData[iTempWritePointer].strTimestamp.iDay:= RTC.DAY;
    strDataBuffer.arrData[iTempWritePointer].strTimestamp.iMonth:= RTC.MONTH;
    strDataBuffer.arrData[iTempWritePointer].strTimestamp.iYear:= TO_INT(RTC.YEAR);
    strDataBuffer.arrData[iTempWritePointer].strTimestamp.iHour:= RTC.HOURS;
    strDataBuffer.arrData[iTempWritePointer].strTimestamp.iMinute:= RTC.MINUTES;
    strDataBuffer.arrData[iTempWritePointer].strTimestamp.iSecond:= RTC.SECONDS;

    strDataBuffer.iWritePointer:= strDataBuffer.iWritePointer + 1; /* Select next position */
    IF strDataBuffer.iWritePointer > 20 THEN
        strDataBuffer.iWritePointer:= 1;
    END_IF;

    strDataBuffer.iDataCount:= strDataBuffer.iDataCount + 1; /* 1 extra dataset available in the buffer */

    iNumberOfSamples:= iNumberOfSamples + 1; /* Increment number of samples */

END_IF;
// *** Program code: ***

/* FB call */
RetainDataBuffer(iNumberOfSamples => iMyLoggingSamples);

/* Log 1 dataset in retain memory */

IF xLogSample THEN
    xLogSample:= FALSE;

    RetainDataBuffer.InsertDataSample(55,FALSE,2.3,'This is text information - Log 1 sample');

END_IF;

/* Log 4 datasets in retain memory */

IF xLogFourSample THEN
    xLogFourSample:= FALSE;

    RetainDataBuffer.InsertDataSample(3,FALSE,2.3,'This is text information - Log 1/4 sample');
    RetainDataBuffer.InsertDataSample(17,TRUE,8.9,'This is text information - Log 2/4 sample');
    RetainDataBuffer.InsertDataSample(888,FALSE,5.233,'This is text information - Log 3/4 sample');
    RetainDataBuffer.InsertDataSample(482,TRUE,777.85,'This is text information - Log 4/4 sample');

END_IF;

Note:

The Makers Blog shows applications and user stories of community members that are not tested or reviewed by Phoenix Contact. Use them at your own risk.

Discussion

Please login/register to comment

Login/Register

Leave a Reply

Newsletter
Never miss a new article
Sign up for the newsletter
Never miss news about PLCnext Technology
Get interesting content via newsletter four times a year
Receive exclusive information before all other users