TwinCAT3 - Wrong values for timestamp when reading from ADS datastream with Matlab

1k Views Asked by At

I am trying to read an ADS datastream from a TwinCAT3 Project.

The function I wrote should read the datastream whenever the CycleCount (coming from the SPS) changes its value - so CycleCount is the trigger for the callback function and is checked for a change every millisecond.

The datastream to be read consists of a structure containing the two values "nCycleCount" (DWORD-4Bytes) and "TStamp" (ULINT-8Bytes). Therefore the whole stream is containing 12 bytes of data.

One cycle in TwinCAT is configured as 0.5ms, so the variable CycleCount should change 2 times per second (if the PLC-tasks cycle time is one cycle-tick). As my program is checking every millisecond if the variable CycleCount changed, the callback function should be called every millisecond and write the timestamp to a Buffer ("myBuffer"). But I noticed that for a runtime of 2 seconds I only receive 1000 values (instead of 2000 expected) and I can't find the reason why?

The PLC task in TwinCAT3 seems to show the correct values, but when reading them with MatLab the timestamp values are incorrect and not every millisecond as stated before:

enter image description here

These are some outputs from Matlab where the CycleCounter is written to column 1 and timestamp is written to column 2:

enter image description here

I use the following Codes in TwinCAT to define the structure and Main-Program:

Structure:

   TYPE ST_CC :
   STRUCT
    nCycleCount       : DWORD;              //4Bytes
    TStamp            : ULINT;              //8Bytes
                                            //Stream with 12Bytes total     
   END_STRUCT
   END_TYPE

MAIN_CC (for PlcTask):

   PROGRAM MAIN_CC
   VAR
     CC_struct : ST_CC;
   END_VAR;

   CC_struct.nCycleCount := _TaskInfo[1].CycleCount;    
   CC_struct.TStamp :=  IO_Mapping.ulint_i_TimeStamp; 

Matlab Code to read stream on Notification:

    function ReadTwinCAT()

    %% Import Ads.dll
    AdsAssembly = NET.addAssembly('D:\TwinCat3\AdsApi\.NET\v4.0.30319\TwinCAT.Ads.dll');
    import TwinCAT.Ads.*;

    %% Create TcAdsClient instance
    tcClient = TcAdsClient;

    %% Connect to ADS port 851 on the local machine
    tcClient.Connect(851);

    %% ADS Device Notifications variables

    % ADS stream
    dataStream = AdsStream(12); %12Bytes necessary 

    % reader
    binRead = AdsBinaryReader(dataStream);

    % Variable to trigger notification
    CCount = 'MAIN_CC.CC_struct.nCycleCount';

    %% Create unique variable handles for structure
    try
        st_handle = tcClient.CreateVariableHandle('MAIN_CC.CC_struct');
    catch err
        tcClient.Dispose();
        msgbox(err.message,'Fehler beim Erstellen des Variablenhandles','error');
        error(err.message);
    end

    %% Create buffer for values
         myBuffer = {};
         MAXBUFFLEN = 1000;

    %% Register ADS Device
    try   
        % Register callback function
        tcClient.addlistener('AdsNotification',@OnNotification);

        % Register notifications 
    %   %AddDeviceNotification( variableName As String,
    %                           dataStream As AdsStream,
    %                           offset As Integer,
    %                           length As Integer (in Byte),
    %                           transMode As AdsTransMode,
    %                           cycleTime As Integer,
    %                           maxDelay As Integer,
    %                           userData As Object)

        % Notification handle
        hConnect = tcClient.AddDeviceNotification(CCount,dataStream,0,4,AdsTransMode.OnChange,1,0,CCount);

        % Listen to ADS notifications for x seconds
        pause(2);
    catch err
        msgbox(err.message,'Error reading array via ADS','error');
        disp(['Error registering ADS notifications: ' err.message]);
    end


    %% Delete ADS notifications
    for idx=1:length(hConnect)
        tcClient.DeleteDeviceNotification(hConnect(idx));
    end

    %% Dispose ADS client
    tcClient.Dispose();


    %% MatlabAdsSample_Notification: OnNotification
    function OnNotification(sender, e)

        e.DataStream.Position = e.Offset; %Startposition = 0                

        %% load variables from workspace
        hConnect = evalin('caller','hConnect');
        binRead = evalin('caller','binRead');

        %% assign to ADS variable and convert to string
        if( e.NotificationHandle == hConnect )

            %% Read timestamp and encodervalues & append to Buffer

            tcClient.Read(st_handle, dataStream);   %Read structure from stream       

            %nCycleCount
            nCycleCount = binRead.ReadInt32;
            [bufflen, ~] = size(myBuffer);          %Get current buffer length
            myBuffer{bufflen+1,1} = nCycleCount;

            %Read & Append Timestamp to Buffer
            tstamp = binRead.ReadInt64;             %Read tstamp from dataStream and shift reading position by 8bytes (int64)        
            myBuffer{bufflen+1,2} = tstamp;   

            if bufflen < MAXBUFFLEN-1
                return;
            else
                assignin('base','myBuffer', myBuffer);
                disp("buffer assigned in workspace")
                myBuffer = {};                                      %empty Buffer
            end                     

        else
            %do nothing
        end

    end

Hope you can help me with my problems - thanks in advance!

2

There are 2 best solutions below

4
On BEST ANSWER

I found a solution which seems to work as a 12hour test with 43million datasets was successful.

The way I do it now is appending my structure (containing the values to read) to an array of structs with a size of 10.000. As soon as the array is full, my notification variable triggers the callback function to read the whole array (1.000 * 40 bytes).

But this only seems to work with arrays of a big size. When using smaller arrays with a size of 100 or 1.000 I noticed that there is a higher chance of faulty values caused by incorrect reading.

Structure:

TYPE ST_ENC :
  STRUCT    
    TStamp            : ULINT;              //8Bytes
    EncRAx1           : DINT;               //4Bytes
    EncRAx2           : DINT;               //4Bytes    
    EncRAx3           : DINT;               //4Bytes
    EncRAx4           : DINT;               //4Bytes
    EncRAx5           : DINT;               //4Bytes
    EncRAx6           : DINT;               //4Bytes
    EncEAx1           : DINT;               //4Bytes
    EncEAx2           : DINT;               //4Bytes
  END_STRUCT
END_TYPE

MAIN:

PROGRAM MAIN_Array
VAR
   encVal : ST_ENC; //Structure of encoder values and timestamp
   arr2write : ARRAY [0..9999] OF ST_ENC; //array of structure to write to
   arr2read  : ARRAY [0..9999] OF ST_ENC; //array of structure to read from
   ARR_SIZE : INT := 9999;
   counter : INT := 0; //Counter for arraysize
END_VAR;

// --Timestamp & Encoderwerte
encVal.TStamp   :=  IO_Mapping.ulint_i_TimeStamp; 
encVal.EncRAx1  :=  IO_Mapping.dint_i_EncoderValue_RAx1; 
encVal.EncRAx2  :=  IO_Mapping.dint_i_EncoderValue_RAx2;
encVal.EncRAx3  :=  IO_Mapping.dint_i_EncoderValue_RAx3;
encVal.EncRAx4  :=  IO_Mapping.dint_i_EncoderValue_RAx4;
encVal.EncRAx5  :=  IO_Mapping.dint_i_EncoderValue_RAx5;
encVal.EncRAx6  :=  IO_Mapping.dint_i_EncoderValue_RAx6;
encVal.EncEAx1  :=  IO_Mapping.dint_i_EncoderValue_EAx1;
encVal.EncEAx2  :=  IO_Mapping.dint_i_EncoderValue_EAx2;

//Append to array 
IF counter < ARR_SIZE
THEN
    arr2write[counter] := encVal;
    counter := counter + 1;
ELSE
    arr2write[ARR_SIZE] := encVal; //Write last Bufferentry - otherwise 1 cycle of struct missing   
    arr2read := arr2write;  
    counter := 0;
END_IF

MATLAB

function ReadTwinCAT() 

   %% Import Ads.dll
   AdsAssembly = NET.addAssembly('D:\TwinCat3\AdsApi\.NET\v4.0.30319\TwinCAT.Ads.dll');
   import TwinCAT.Ads.*;

   %% Initialize POOL
   pool = gcp();
   disp("Worker pool for parallel computing initalized");

   %% Create TcAdsClient instance
   tcClient = TcAdsClient;

   %% Connect to ADS port 851 on the local machine
   tcClient.Connect(851);

   %% ADS Device Notifications variables
   % ADS stream
   ARR_SIZE = 10000; %Groesse des auszulesenden Arrays of Struct
   STREAM_SIZE = 40; %in Byte 

   dataStream = AdsStream(ARR_SIZE * STREAM_SIZE); %40Bytes per entry

   % Binary reader
   binRead = AdsBinaryReader(dataStream);

   % Variable to trigger notification
   arr2read = 'MAIN_Array.arr2read[0].TStamp'; %Notification handle = first TStamp entry

   %% Create unique variable handles for encoder-array
   try
       arr_handle = tcClient.CreateVariableHandle('MAIN_Array.arr2read');
   catch err
       tcClient.Dispose();
       msgbox(err.message,'Fehler beim Erstellen des Variablenhandles','error');
       error(err.message);
   end

   %% Create buffer for values
   myBuffer = {}; %Creates empty buffer
   buffcount = 0; %Nur fuer Workspace-Ausgabe

   %% Register ADS Device
   try   
       % Register callback function
       tcClient.addlistener('AdsNotification',@OnNotification);

       % Notification handle
       hConnect = tcClient.AddDeviceNotification(arr2read,dataStream,0,8,AdsTransMode.OnChange,1,0,arr2read);

       % Listen to ADS notifications for x seconds
       pause(15);

   catch err
       msgbox(err.message,'Error reading array via ADS','error');
       disp(['Error registering ADS notifications: ' err.message]);
   end

   %% Delete ADS notifications
   tcClient.DeleteDeviceNotification(hConnect);

   %% Dispose ADS client
   tcClient.Dispose();

   %% MatlabAdsSample_Notification: OnNotification
   function OnNotification(sender, e)

       e.DataStream.Position = e.Offset; 

       %% load variables from workspace
       hConnect = evalin('caller','hConnect');
       binRead = evalin('caller','binRead');

       %% assign to ADS variable and convert to string
       if( e.NotificationHandle == hConnect )

          %% Read timestamp and encodervalues & append to Buffer

           tcClient.Read(arr_handle, dataStream);   %Read structure from stream

           for idx=1:ARR_SIZE               

               %Read & Append Timestamp to Buffer
               [bufflen, ~] = size(myBuffer);           %Get current buffer length
               tstamp = binRead.ReadUInt64;             %Read tstamp from dataStream and shift reading position by 8bytes (int64)        
               myBuffer{bufflen+1,1} = tstamp; 

               %Read & Append Encodervalues to Buffer
               for n=1:8
                   encval = binRead.ReadInt32;             %Read tstamp from dataStream and shift reading position by 8bytes (int64)        
                   myBuffer{bufflen+1,n+1} = encval; 
               end

           end

           Assign arraybuffer
           buffname = 'myBuffer';
           buffcount = buffcount+1;
           buffcount_str = num2str(buffcount);
           assignin('base',strcat(buffname, buffcount_str), myBuffer);
           myBuffer = {}; %empty Buffer for next array
           disp("buffer assigned")         

       else
           %do nothing
       end
   end
end
23
On

As far as I can see your program is behaving correctly.

1)

Because the notifications are asynchronous, they may arrive after your wait time is over.At that time though you already disposed the notification.

To test if this theory is correct, add a timer in your Twincat progam.

Declaration:

fbTimer : TON;

Implementation:

fbTimer(IN:=TRUE,PT:=T#2s);
IF NOT fbTimer.Q
THEN
 cc_struct.nCycleCount := _TaskInfo[1].CycleCount;
END_IF

Make sure that your matlab program is already started before you start the plc and raise your pause time in Matlab to 120s.

If you get 2000 values then you know the problem derives from the asynchronous nature of the communication.

2)

The conversion error derives from the ReadInt64 method which:

Reads an 8-byte signed integer from the current stream and advances the current position of the stream by eight bytes.

https://learn.microsoft.com/en-us/dotnet/api/system.io.binaryreader.readint64?redirectedfrom=MSDN&view=netframework-4.8#System_IO_BinaryReader_ReadInt64

You should use ReadUInt64 instead.


To see if I could reproduce your same behaviour, I created a small c# test program. The test program behaved normally and I was able to receive the correct amount of notifications.

Here the ST code :

Declaration:

PROGRAM MAIN
VAR
    fbTimer: TON;
    nCycleCount : DWORD;
END_VAR

Implementation:

fbTimer(IN:=TRUE,PT:=T#2S);
IF NOT fbTimer.Q
THEN
 nCycleCount := _TaskInfo[1].CycleCount;
END_IF

Here the C# code :

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TwinCAT.Ads;

namespace AdsNotificationTest
{
    class Program
    {
        static TcAdsClient tcClient;
        static int hConnect;
        static AdsStream dataStream;
        static BinaryReader binReader;
        static uint uVal, huValHandle;
        static int counter = 0;

        static void Main(string[] args)
        {
            tcClient = new TcAdsClient();
            dataStream = new AdsStream(31);

            binReader = new BinaryReader(dataStream, System.Text.Encoding.ASCII);
            tcClient.Connect(851);
            try
            {
                hConnect = tcClient.AddDeviceNotification("MAIN.nCycleCount", dataStream, 0, 4, AdsTransMode.OnChange, 1, 0, huValHandle);
                tcClient.AdsNotification += new AdsNotificationEventHandler(OnNotification);
            }
            catch (Exception err)
            {
                Console.WriteLine("Exception.");
            }

            Console.ReadKey();

            tcClient.DeleteDeviceNotification(hConnect);
            tcClient.Dispose();

        }

        private static void OnNotification(object sender, AdsNotificationEventArgs e)
        {

            if (e.NotificationHandle == hConnect)
            {
                counter += 1;
                uVal = binReader.ReadUInt32();
                Console.WriteLine(counter + ": " + uVal);
            }


        }
    }
}