Hi B&R Community,
I am investigating the minimum achievable communication time for UDP communication from a PLC, with a target of 1 ms round-trip latency. For context, I am working on a real-time control application where low-latency communication is critical.
In my experience, UDP communication at 10 ms or 100 ms typically requires four cycle times for a send/receive operation, which is stable at these intervals. To benchmark my setup, I first tested communication with a Python script on a PC (acting as a UDP client) and achieved an average round-trip time of 1.2 ms over 1,000 requests.
My goal is to replicate this performance on a PLC. Given that AsUDP functions are asynchronous and execute during idle time, I conducted several tests to evaluate stability and latency. To calculate the communication time, the PLC sends a life counter, which the server returns. This allows me to count the number of cycles between sending and receiving.
Below are my findings for each configuration:
Case 1
| Idle time | |
|---|---|
| Idle task class | Cyclic#1 |
| Task class idle time | 2000 µs |
| Cyclic #1 | |
|---|---|
| CPU Core | Core #1 |
| Duration | 10000 µs |
| Tolerance | 0 µs |
Results (20s trace):
| Metric | Number of cycles | Time (ms) |
|---|---|---|
| Average cycles (Send/Recv) | 3.608 | 36.08 |
| Max cycles (Send/Recv) | 6 | 60 |
On average, the communication time is below my target of four cycle times.
Case 2
| Idle time | |
|---|---|
| Idle task class | Cyclic#1 |
| Task class idle time | 2000 µs |
| Cyclic #1 | |
|---|---|
| CPU Core | Core #1 |
| Duration | 3000 µs |
| Tolerance | 0 µs |
Results (20s trace):
| Metric | Number of cycles | Time (ms) |
|---|---|---|
| Average cycles (Send/Recv) | 4.096 | 12.29 |
| Max cycles (Send/Recv) | 10 | 30 |
On average, the communication time exceeds my target of four cycle times.
Case 3
| Idle time | |
|---|---|
| Idle task class | Cyclic#1 |
| Task class idle time | 1000 µs |
| Cyclic #1 | |
|---|---|
| CPU Core | Core #1 |
| Duration | 2000 µs |
| Tolerance | 0 µs |
Results (20s trace):
| Metric | Number of cycles | Time (ms) |
|---|---|---|
| Average cycles (Send/Recv) | 4.202 | 8.40 |
| Max cycles (Send/Recv) | 16 | 32 |
On average, the communication time exceeds my target of four cycle times.
Case 4
| Idle time | |
|---|---|
| Idle task class | Cyclic#1 |
| Task class idle time | 200 µs |
| Cyclic #1 | |
|---|---|
| CPU Core | Core #1 |
| Duration | 400 µs |
| Tolerance | 0 µs |
Results (20s trace):
| Metric | Number of cycles | Time (ms) |
|---|---|---|
| Average cycles (Send/Recv) | 7.012 | 2.80 |
| Max cycles (Send/Recv) | 67 | 26.8 |
At this configuration, the communication becomes highly unstable.
PLC Program
CASE State OF
0:
IF EnableCom THEN
Open.enable := TRUE;
Open.port := PlcPort;
Open.pIfAddr := 0;
State := 10;
END_IF
10: // Open
IF Open.status = ERR_OK THEN
Open.enable := FALSE;
Ident := Open.ident;
Send.datalen := SIZEOF(outgoingdata);
Send.pData := ADR(outgoingdata);
Send.ident := Ident;
Send.pHost := ADR(UnrealIPAdr);
Send.port := UnrealPort;
Send.enable := TRUE;
State := 20;
ELSIF Open.status <> ERR_FUB_BUSY THEN
State := 255;
ErrorID := Open.status;
END_IF
20: // Send
outgoingdata.LifeCnt := outgoingdata.LifeCnt + 1;
IF NOT EnableCom THEN
State := 220;
ELSIF Send.status = ERR_OK THEN
Send.enable := FALSE;
Receive.ident := Ident;
Receive.datamax := SIZEOF(ingoingdata);
Receive.pData := ADR(ingoingdata);
Receive.pIpAddr := ADR(UnrealIPAdr);
Receive.enable := TRUE;
State := 30;
ELSIF Send.status <> ERR_FUB_BUSY THEN
State := 255;
ErrorID := Send.status;
END_IF;
30: // Receive
outgoingdata.LifeCnt := outgoingdata.LifeCnt + 1;
IF NOT EnableCom THEN
State := 220;
ELSIF Receive.status = ERR_OK THEN
UnrealPort := Receive.port;
Receive.enable := FALSE;
Send.datalen := SIZEOF(outgoingdata);
Send.pData := ADR(outgoingdata);
Send.ident := Ident;
Send.pHost := ADR(UnrealIPAdr);
Send.port := UnrealPort;
Send.enable := TRUE;
outgoingdata.ReflLifeCnt := ingoingdata.LifeCnt;
LifeDiff := outgoingdata.LifeCnt - ingoingdata.ReflLifeCnt;
State := 20;
ELSIF Receive.status = udpERR_NO_DATA THEN
Receive.enable := FALSE;
ELSIF Receive.status = ERR_FUB_ENABLE_FALSE THEN
Receive.enable := TRUE;
ELSIF Receive.status <> ERR_FUB_BUSY THEN
State := 220;
ErrorID := Receive.status;
END_IF
40: // Close
IF Close.status = ERR_OK THEN
State := 255;
ELSIF Close.status <> ERR_FUB_BUSY THEN
State := 255;
ErrorID := Close.status;
END_IF;
220: // Error during Send/Recv --> Close connection
Close.ident := Ident;
Close.enable := TRUE;
State := 40;
255:
Open.enable := FALSE;
Close.enable := FALSE;
Send.enable := FALSE;
Receive.enable := FALSE;
ErrorID := 0;
Ident := 0;
State := 0;
END_CASE
Open();
Close();
Send();
Receive();
From profiler informations this program takes max 40us (Max Net Time) to be executed.
Questions
- Is it possible to use the IDLE task to achieve fast and stable UDP communication? Or is there a better approach (e.g., higher-priority task, hardware timers)?
- Does anyone have more information about how the IDLE task schedules asynchronous functions? Is there a way to minimize jitter?
- Are there any recommendations for optimizing the PLC configuration or program logic to reduce latency and improve stability?
Hardware and Software
- AS: 6.5.0.306
- PLC: X20CP1686X AR 6.5.1
- Note: Only one program is running in Cyclic#1 on the PLC.
Server UDP
The UDP server is a third-party and I don’t have control over it, I can’t change the protocol or any configuration.
Apologies for the lengthy post! I hope the details are clear. Any advice or insights would be greatly appreciated.
Regards,
Florent





