Pointer and Memory Management
Variable:
When we declare a variable for our program, then the variable is stored in the PLC’s memory. The operating system stores the variable in the memory location. We can’t store the variable by ourselves but we can know where the variable has been stored. We declare a INT type variable and initialized it to 15. Lets assume, 15 is located at address 0x0004. The concept is explained in the following figure. Lets assume our PLC memory has memory address started from 0x0000 (just as an example). In real life the address can be for 64 bit PLC 8 bytes long. Download the sample.
nIntVar1 : INT := 15; //OS saves nIntVar1 somewhere (in this case at 0x0004)
//nIntVar1 : INT; // it reserves the memory address but 15 has not stored yet
Figure 01: nIntVar1 variable is initialized to 15 and stored at address 0x0004
Pointer :
The pointer is itself a variable. So when we declare a pointer variable that is stored in the memory area. The following declaration declares a pointer variable whose type is INT
pInt : POINTER TO INT; //pInt just like a variable, it stores variable address
This is shown in the following figure. The pointer has been declared but has not initialized that means it does not point anywhere yet. So for example, pInt variable is stored at address 0x0006. We know that the pointer variable is located at address 0x0006 but it does not point anywhere yet (Fig 2a).
Figure 02: pInt pointer variable stored at address 0x0006
The pointer can store the addresses of variables, function blocks, and programs, while an application program is running. A pointer points to one of the named objects or a variable with any data type.
We can find the address of the variable by the following statement and set the address to pInt variable
pInt := ADR(nIntVar1); //pInt is the address where it was saved
After the above assignment, pInt pointer variable points to 0x0004 not to the value 15. The concept is shown in the figure 02 2b.
Dereferencing a pointer means obtaining the value of the address to which the pointer points. You can dereference a pointer by appending the content operator to the pointer identifier, for example, see pInt^ in the example below.
pInt^ will be the value of a variable where it points to (in this case nIntVar1 with value 15).
We can change the value of nIntVar1 indirectly by the following way.
pInt^ := 345; //now nIntVar1 will contain 345
Let put the example as a summary. We can show pInt2 and valViaPointer in the memory address, but we have not shown. If you copy paste the code to Visual Studio then you can see the result.
VAR
nIntVar1 : INT := 15; //Declare an integer type variable
pInt : POINTER TO INT; //Declare a pointer type variable of INT
pInt2 : POINTER TO INT; //Declare a pointer type variable of INT
valViaPointer : INT ; //Declare an integer type variable of INT for holding data
END_VAR
//Body, lower part of the program
pInt := ADR(nIntVar1); //Now address of nIntVar1 is assigned to pInt
valViaPointer := pInt^; //Assign the content by dereferencing pInt (note the ^ operator)
pInt2 := pInt; //set the pointer variable pInt to pInt2
pInt2^:= 345; //Assign new value 345 to pInt2 and note that nIntVar1 is having value 345
Figure 03: running the code in Visual Studio and updating original variable indirectly via pointer
YouTube Video on Pointer
Pointer Arithmetic
In our previous example, we have shown the memory addresses are 0, 1, 2, etc. But in practice, those address depends on what kinds of data size we are storing. Strictly speaking, those are virtual addresses but we don’t need to care for them. We have declared an array of INT types with 3 elements. We declare 3 pointer variables which will hold the address of those 3 items.
plainInt : ARRAY[0..2] OF INT;
plainIntAdd0 : POINTER TO INT;
plainIntAdd1 : POINTER TO INT;
plainIntAdd2 : POINTER TO INT;
//Init array with some values
plainInt[0]:= 2; // see next figure 04
plainInt[1]:= 5;
plainInt[2]:= 7;
Now let us assign the address to those pointers. If the value of plainIntAdd0 0x0002 then the values of plainIntAdd1 and plainIntAdd2 will be 0x0004 and 0x0006, and the offset is 2. This 2 is coming from the size of the data type which can be determined by sizeof operator.
plainIntAdd0:= ADR(plainInt[0]); //plainIntAdd0 holds the address of 1st element
plainIntAdd1:= ADR(plainInt[1]);
plainIntAdd2:= ADR(plainInt[2]); //plainIntAdd2 holds the address of 3rd element
Value of plainIntAdd2 will be plainIntAdd0 + 2 * sizeof (INT);
plainIntAdd2:= ADR(plainInt[0])+2*SIZEOF (INT);//equivalent plainIntAdd2:=ADR(plainInt[2]);
testvar := plainIntAdd2^; //testvar will contains 3rd value of array
plainIntPointer:= plainIntAdd0 + 0 * SIZEOF (INT); //First one
//equivalent plainIntPointer:= ADR(plainInt) + 0 * SIZEOF (INT); //First one
plainIntPointer:= plainIntAdd0 + 1 * SIZEOF (INT); //Second one
plainIntPointer:= plainIntAdd0 + 2 * SIZEOF (INT); //Third one and so on
plainInt[0] is located at 0x0002 and plainInt[1] is located at 0x0004. The difference is 2 and that is SIZEOF INT . We can know the next address by adding the size of data, in our case INT which has a size of 2 bytes. If we have a data size LREAL then it the offset will be 8 (SIZEOF (LREAL) == 8)
Figure 04: INT type data size has 2 bytes, next address = current address + size of data type
Multi Dimensional Array
In our previous example, we have shown a one-dimensional array of INT data. Now we shall show 2 dimensional data and its type is REAL
page : ARRAY[0..MaxRow]OF ARRAY[0..MaxColumn]OF REAL;
dataReal : REAL; // can holds any element of the array VAR CONSTANT MaxRow : INT := 3; MaxColumn : INT := 5; END_VAR
This is called an array of arrays. The first index (MaxRow) is the number of rows and the second one number of columns. The size of REAL is 4 and LREAL is 8. If we declare INT type i and j, we can initialize all elements to 0.0 in the following way. Since the initialization is not done in each cycle, we can guard it by a BOOL variable.
IF NOT initDone THEN FOR i := 0 TO MaxRow DO FOR j := 0 TO MaxColumn DO page[i][j]:= 0.0; END_FOR END_FOR
initDone:= TRUE; // will not execute in each cycle but the first one END_IF
If the values of pages are initialized with individual values then we can assign a unique value to each cell.
page[rowIndex][columnIndex] := some real data;
page[0][0]:= 0; //assign 0 to first row, first column page[3][0]:= 30; //page[row_value][column_value] row first then column page[0][1]:= 1; page[3][1]:= 31;//assign 31 to 4th row, second column
We can assign each row to a pointer variable and manipulate those as needed. In the following snippet, we can manipulate each row one by one with a single pointer or different pointer variables.
pageFirstRow : POINTER TO ARRAY[0..MaxColumn] OF REAL; pageSecondRow : POINTER TO ARRAY[0..MaxColumn] OF REAL; pageThirdRow : POINTER TO ARRAY[0..MaxColumn] OF REAL;
Figure 05: Multidimensional array initialization with specific value for the table name page (in our excercise)
The following page shows the memory addresses of the above-mentioned variables. We can pass the variable for example pageFirstRow to other functions and make changes which will be reflected to the caller. It reduces the number of assignments and is a bit faster as we directly manipulate memory location.
Figure 06: Actual 8 bytes address (64 bit CPU)is shown from real PLC
YouTube Video on pointer arithmetic and array
Page Fault and Pointer
Page fault issues in PLC are related to pointer manipulation. If we misuse the pointer then OS kills the process and we see a note on the PLC desktop as shown in the following image. We shall show here an example and how to fix it. We can make similar problems while using a pointer. It can be hard to debug this type of issue sometimes.
Figure 06: Page fault reasons
If we see the above problems in our application, we follow the following steps
=> Close the dialog on the PLC Desktop
=> Change the TwinCAT icon to config mode
=> Fix the bug until we are able to run the PLC in run mode
Now we describe our application which leads to this situation:
We want to read motor speed by passing a variable to a function block. The speed will be measured in the function block and will be used in the caller program. Here are the sample function blocks.
//Header of the function block
FUNCTION_BLOCK MOTORBLOCK
VAR_INPUT
inputPointer :POINTER TO REAL;
END_VAR
VAR_OUTPUT
END_VAR
VAR
initDone : BOOL:= FALSE;
realMotorSpeed : REAL;
END_VAR
//Body of the function block
IF NOT initDone THEN
inputPointer := ADR(realMotorSpeed);
realMotorSpeed := 123.04; // default value
initDone:= TRUE;
END_IF
inputPointer^:=realMotorSpeed; //new value is set after reading from the HW (not shown)
In the caller program, we have the following code. We see from the function block header file, it will take a pointer to type REAL and it will set the measured speed to the pointer. Now we try to access the speed in the following way. When we start the program we get a page fault, what causes this? InputPointer is not initialized! We must initialize the pointer before using it and we need to check that the pointer is not NULL.
VAR
motorSpeed : POINTER TO REAL;
fnMOTORBLOCK : MOTORBLOCK;
END_VAR
//Call the function block by passing the pointer, compile and links fine
fnMOTORBLOCK(inputPointer := motorSpeed);
Solution:
//We declare a variable and its address is passed to the input of the function block
Option 1:
motorSpeed : REAL;
//motorSpeed : POINTER TO REAL; // uncomment this and comment previous line to produce page fault
//We pass the address of motorSpeed not the NULL pointer
fnMOTORBLOCK(inputPointer := ADR(motorSpeed));
//fnMOTORBLOCK(inputPointer := motorSpeed); // uncomment this and comment previous line to produce page fault
//We check if the pointer is NULL before using it.
IF inputPointer <> NULL THEN
inputPointer^:=realMotorSpeed; //new value is set after reading from the HW (not shown)
END_IF
Option 2:
Declare an output pointer in the function block, set the pointer with local variable and return it to caller
VAR_OUTPUT
outputPointer :POINTER TO REAL;
END_VAR
IF NOT initDone THEN
outputPointer := ADR(realMotorSpeed);
realMotorSpeed := 123.04; // default value
initDone:= TRUE;
END_IF
motorSpeedOut:= fnMOTORBLOCK.outputPointer; // dereference motorSpeedOut to get the motor speed
We can download the sample from the above links and try to reproduce it.
YouTube Video on page fault and how to avoid it
We have another concept that is REFERENCES. A reference points implicitly to another object. The reference is implicitly deferenced during the access and therefore requires no special content operator ^ like a pointer.
//declare variables
refInt : REFERENCE TO INT;
nA : INT := 200;
//Set the references, after that, if you change one variable then the other will have the same value
refInt REF= nA; // refInt points now to nA
Thank you for reading this far. If it is useful why not share it?
Memcpy
Library contains a number of functions which provide direct access to memory areas in the PLC runtime system of the PLC Controller. Default editor includes the library by default.
The fact that these functions allow direct access to the physical memory of the PLC. Misuse of such functions can result in a system crash, or in access to forbidden memory areas. So we need to be careful when we are using these function.
MEMCMP Compares the values of variables in two memory areas
MEMCPY Copies the values of variables from one memory area to another
MEMSET Sets the variables in a memory area to a particular value
Memcpy example
//Declaration
PROGRAM MAIN
VAR
org : DINT :=12;
dest : DINT :=13;
another : DINT := 14;
myTimer : TON;
startTimer : BOOL := FALSE;
END_VAR
//Body
myTimer(IN:= startTimer, PT:=T#1S);
MEMCPY(ADR(dest), ADR(org),4);
MEMCPY(ADR(another), ADR(dest),4);
IF myTimer.Q = TRUE THEN
myTimer(IN:= FALSE);
org:= org +1;
END_IF
Download the sample from the link given above.
Next, let’s try to understand persistent data https://www.hemelix.com/plc/structured-text-function-block/
Ask questions related to Hemelix sample code and design at Google group https://groups.google.com/g/hemelix