ModbusTCP - Add and read/write to Server (Slave) during runtime

Hello everyone,

I am currently trying to connect a Belimo Energy Valve via Modbus TCP using Codesys 3.5 SP21 Patch 3 and the WagoAppPlcModbus Library version 1.1.5.13. Here is an excerpt from the Modbus interface description of the pump:

No distinction is made between data types (Discrete Inputs, Coils, Input Registers and Holding Registers). As a consequence, all data can be accessed with the two commands for Holding Register

All values in the register are unsigned integer data types. Exceptions are marked with **). Signed integers are represented as two’s complement

And here is an excerpt from the register list:

No. Address Register Access
1 0 Setpoint Relative [%] R / W
14 13 Sensor 1 Temperature [°C] **) R
15 14 Sensor 1 Temperature [°F] **) R
24 23 Delta Temperature [K] R
25 24 Delta Temperature [°F] R

**) → signed Integer

Currently, I am simulating a device (CODESYS Control Win V3 x64) on my computer, and later it will run on a PFC200.

I can ping the pump via Windows Terminal (PC and pump are in the same network), and I can also retrieve the pump values using the Mbpoll program. I have already successfully connected the pump via the device tree (Ethernet - Master - Slave) and read/write values. However, the requirements for the program are to dynamically add slaves at runtime through visualization configuration (e.g., specify slave IP address and port – registers for the device have already been pre-programmed in the background), similar to how the Wago WALM solution works.

My current program implementing this logic looks like this:

PROGRAM ModbusMasterTest
VAR_INPUT
    Jobliste: ARRAY [0..1] OF typMbQuery := [ 
        ( //JOB 0, read inputs
            bUnitId:= 1, 
            bFunctionCode:= 3, 
            uiReadAddress:= 0, 
            uiReadQuantity:= 30,    // read the first 30 registers?
            uiWriteAddress:= 0, 
            uiWriteQuantity:= 0, 
            awWriteData:= [125(0)]
        ),
        ( //JOB 1, write outputs (register access)
            bUnitId:= 1, 
            bFunctionCode:= 16, 
            uiReadAddress:= 0, 
            uiReadQuantity:= 0, 
            uiWriteAddress:= 0, 
            uiWriteQuantity:= 1, 
            awWriteData:= [125(0)]
        )];
END_VAR
VAR
    iJobCounter     : INT     := 0;
    xTrigger        : BOOL    := TRUE;
    FB_F_Trig       : F_Trig;

    Responseliste   : ARRAY [0..1] OF typMbResponse;

    FB_ModbusMasterTCP: FbMbMasterTcp;
    utKeepAlive     : typKeepAlive := (
        xEnable := TRUE,
        tMaxIdleTime := T#5S,    //TIME#5s0ms Maximum time of inactivity
        tInterval := T#2S,       //TIME#2s0ms Interval between two successive KA-Packets
        udiProbes := 5           //5 Number of KA retry before giving up
    );
END_VAR
// Call the communication block
FB_ModbusMasterTCP (xConnect := TRUE, 
                    sHost := '192.168.4.112', 
                    wPort := 502, 
                    utKeepAlive := utKeepAlive,
                    eFrameType := eMbFrameType.Ethernet, 
                    tTimeOut := T#2S, 
                    utQuery := Jobliste[iJobCounter], 
                    xTrigger := xTrigger, 
                    utResponse := Responseliste[iJobCounter]);

// Wait for job completion
FB_F_Trig (clk:=xTrigger);

// Job completed, start next job
IF FB_F_Trig.Q THEN
    xTrigger := TRUE;
    iJobCounter := (iJobCounter+1) MOD 2;
END_IF

And this is my “Main” program:

PROGRAM PLC_PRG
VAR
    Ain1 : WORD;
    Ain2 : WORD;   
    AinArray : ARRAY [0..1] OF typMbResponse;
END_VAR

// Call the ModbusMaster subroutine
ModbusMasterTest();
Ain1 := ModbusMasterTest.Responseliste[0].awData[0];
Ain2 := ModbusMasterTest.Responseliste[0].awData[1];
AinArray := ModbusMasterTest.Responseliste;

This is what it looks like during execution:

Screenshot 2025-12-18 143331

Can someone help me? Because as I understand it, I am connected to the slave (pump) since xError is FALSE and oStatus shows nothing else. However, it still does not display any values. When I expand utResponse and the awData category in FB_ModbusMasterTCP, I only see zeros.

I also tried reading a specific register (e.g., uiReadAddress:= 13, uiReadQuantity:= 1), but had the same issue.

Thank you very much in advance!

At quick glance, the first thing I would verify is that your xTrigger bit is cycling. Does the function block need to see an initial off-on transition and so the intialization to TRUE doesn’t work? I honestly don’t remember.

The bit changes. However, I also tried it directly with the example from the WagoAppPlcModbus library under:

  • 20 Programm Organization Units
    • 20 Simple Modbus Master

There, I only replaced the sHost (IP address), and it still doesn’t work. With this solution, xError even switches between False and True, and the values under oStatus also fluctuate:

  • xError FALSE/TRUE
  • oStatus
    • eSeverity → none | error
    • sDescription → ‘OK’ | ‘Error can not connect’
    • sSeverity → ‘None’ | ‘Error’
    • xError → False | True

In this example, xTrigger also changes significantly faster (probably due to timing). Example code from the library documentation:

VAR
    myTcpMaster     : FbMbMasterTcp :=  (   xConnect    := TRUE,
                                            sHost       := '192.168.4.112',  // IP of the remote server
                                            wPort       := 502,             // port at the remote server

                                            utKeepAlive := (    xEnable      := TRUE, // use keep alive
                                                                tMaxIdleTime := T#5S, //  Maximum time of inactivity
                                                                tInterval    := T#2S, //  Interval between two successive KA-Packets
                                                                udiProbes    := 5     //  Number of KA retry before giving up
                                                            ),

                                            eFrameType  := eMbFrameType.ETHERNET,
                                            tTimeOut    := T#30MS
                                        );
    //--- Identification Object for use of FC43 (in case the slave support it) ------
    myBaseIdentification    :   FbIdentifyBaseObject; // only needed for FC43
    //-------------------------------------------------------------------------------
    utQuery         : typMbQuery := (   bUnitId         := 0,       // Slaveaddress
                                        bFunctionCode   := 16#03,   // read input registers
                                        IIdentifyObject := myBaseIdentification, // only needed for FC43
                                        uiReadAddress   := 16#00,       // Startaddress
                                        uiReadQuantity  := 16#64,      // Quantity of wanted registers // --> 100
                                        uiWriteAddress  := 0,       // not needed for FC4
                                        uiWriteQuantity := 0,       // not needed for FC4
                                        awWriteData     := [124(0)] // not needed for FC4
                                    );

    xTxTrigger      : BOOL;             (* Set this variable once for start a job.
                                           This variable will be automatically reset by the master
                                           if the job is done.
                                        *)

    utResponse      : typMbResponse;    (* After the job is done you can find at this structure
                                           the result.
                                        *)

    tonDelay        : TON := (PT := T#20MS); // This is the silence time between two requests
END_VAR



//--- delay between two requests ----------------------
tonDelay( IN := (NOT tonDelay.Q) AND (NOT xTxTrigger));
xTxTrigger S= tonDelay.Q; // trigger the next request

//--- call cyclic the master --------------
myTcpMaster(    utQuery     := utQuery,
                xTrigger    := xTxTrigger,
                utResponse  := utResponse
              );
//-----------------------------------------

Once during error status:

And one second later, the error status is gone again and I am connected. Under myTcpMaster/utResponse/awData, no values are displayed either.

But the output value xIsOpen is always shown as False. I’m not sure if it shouldn’t be True when connected?

In the documentation, it only says under Output: xIsOpen - Bool, nothing more.

Hi,

I see you are trying to use the WAGO library on a Windows based Runtime.

I do not know if the library, or it’s sub libraries, is that universal to work on any Codesys runtimes.

1 Like

Thank you!

I have now used my PFC200 as a Master, and it worked right away. The response from @AdamReeve was also correct. In my setup, the values were only read once, but once I set xTrigger to True once, the values were always updated.

IF xFirstCall THEN
    xFirstCall := FALSE;
    xTrigger := TRUE;
END_IF

Now I have another question regarding writing values using a typMbQuery struct from the same Wago library. My query looks like this:

( //JOB 1, Write outputs (register access)
    bUnitId:= 1, 
    bFunctionCode:= 6, 
    uiReadAddress:= 0, 
    uiReadQuantity:= 0, 
    uiWriteAddress:= 0, 
    uiWriteQuantity:= 1, 
    awWriteData:= [125(0)]
)

Register 0 corresponds to my Belimo actuator, which accepts values from 0 to 10000 and internally converts them to percentages. If I want to write a value to register 0, do I need to place the value to be written into awWrite[0] (the 0th element of the array)? And if I want to write to register 5, would I write the value into awWrite[5]?

If I want to write values to registers in my program:

  • Register 0 → Write value into awWriteData[0]?
    • Jobliste[1].awWriteData[0] := 5000;
  • Register 5 → Write value into awWriteData[5]?
    • Jobliste[1].awWriteData[5] := 5000;

Or how would this work? Because if it’s done this way, what influence does uiWriteAddress := 0 have at all when I’m specifying the value directly in the array?

And what’s still unclear to me: if I now have multiple slaves and want to connect them to my PFC, do I need to create one FB_ModbusMasterTCP block for each slave and then exchange the sHost (IP addresses)?

IODrvModbusTCP should contain similar functionality in a CODESYS library.

Hello, depending how are you set these value you can write one or several registers.
First case.

uiWriteAddress:= 0,
uiWriteQuantity:= 5,
awWriteData:= [125(0)]

important, library writes 5 register.

Second case.
uiWriteAddress:= 0,
uiWriteQuantity:= 1,
awWriteData:= [0] = value for register 0.

uiWriteAddress:= 4,
uiWriteQuantity:= 1,
awWriteData:= [0] = value for register 5.

Check in the Modbus specification what does mean **bFunctionCode:= 16,
**
About multiply slaves,
each slave has address, in your case address is 0
bUnitId := 0, // Slaveaddress

1 Like