Resolving Modbus Read Failures Over Time on Siemens IoT2000
Field deployments of the Siemens IoT2000 (SIMATIC IOT2050, IOT2040) reading Delta, Mitsubishi, or generic slave PLCs over RS232 and Modbus RTU frequently exhibit a class of fault where reads work cleanly for 18 to 36 hours and then silently drop one register at a time. A four-register poll reads four, then three, then two, then one, until the link appears to be hung. Polling rate, cable distance, and node timing look correct at install, so the regression is almost always attributed to a software bug when it is actually a stack of physical, task-cycle, and timeout issues that compound under continuous duty.
This reference walks through the protocol fundamentals, the five root causes most likely to surface after a 24-hour burn-in, the diagnostic workflow, and the corrected Node-RED / CODESYS configuration that keeps an IoT2000 master polling four D-registers indefinitely.
1. Problem Statement and Observable Symptoms
The original failure is reproducible:
- Hardware: SIMATIC IoT2000 (IOT2050 or IOT2040) running Node-RED with
node-red-contrib-modbusv5.x, or CODESYS 3.5 SP16 with theCAA Modbus Masterlibrary. - Link: Cross DB9 cable, 4 ft (1.2 m), direct to PLC COM port. Pins 7 (RTS) and 8 (CTS) jumpered for self-shake. No 120 Ω termination (RS232 does not require it, but the wiring note matters for diagnostic clarity).
- Slave: PLC exposing holding registers at D1030, D1040, D1044, and D1054. Modbus address mapping depends on the vendor but is functionally 4x1029, 4x1039, 4x1043, and 4x1053 in the standard 1-based holding-register addressing.
- Schedule: Four
Modbus Readnodes, scheduled 1 s apart, each requesting one register (FC 03, Quantity = 1). Outer poll loop runs every 60 s. - Symptom: Reads are stable for 18 to 36 hours, then the first
Modbus Readstarts returningILLEGAL_FUNCTIONor simply times out. After another few hours a second node drops. The other nodes continue to respond normally, suggesting a per-instance state problem rather than a link-level fault.
The stepped degradation (4 → 3 → 2 → 1) is a signature of a per-node resource problem, not a shared physical-layer failure. If the entire link died at once, the cause would be a cable, termination, ESD, or ground problem. Per-instance degradation points at the Modbus client stack on the IoT2000.
2. Modbus RTU Protocol Constraints That Drive the Failure
Modbus RTU is sensitive to two inter-frame timing rules defined in the Modbus over Serial Line specification V1.02:
| Parameter | Definition | Value at 9600 bps | Value at 19200 bps |
|---|---|---|---|
| t1.5 (inter-character) | 1.5 character times = 16.5 bit times | 1.72 ms | 0.86 ms |
| t3.5 (inter-frame silence) | 3.5 character times = 38.5 bit times | 4.48 ms | 2.24 ms |
| t1 (response timeout) | Application defined, typically 1 s for RS232, 3-5 s for RS485 | 1000 ms | 1000 ms |
The slave treats any gap larger than t1.5 between received bytes as a frame error and discards the PDU. The master treats any gap larger than t3.5 between two ADUs as the end of a frame. The Modbus Application Protocol V1.1b3 explicitly states the inter-frame silence must be observed even when requests are queued back-to-back. On Linux, the UART driver and the Modbus library must cooperate to insert these gaps; otherwise the slave can collapse two ADUs into one frame and discard both, producing silent timeouts that look identical to the original symptom.
For FC 03 with a single register, the request ADU is 8 bytes (1 + 1 + 2 + 2 + 2) and the response ADU is 7 bytes (1 + 1 + 2 + 1 + 2). At 9600 8N1 the round-trip on the wire is approximately:
- Request: 8 × 10 / 9600 = 8.33 ms
- Response: 7 × 10 / 9600 = 7.29 ms
- t3.5 silence after response: 4.48 ms
- Slave turnaround: 1-10 ms (PLC dependent)
Total cycle for a single read is roughly 25-35 ms at 9600 bps. Four back-to-back reads spaced 1 s apart therefore occupy 100-140 ms of radio time inside a 4 s window — well within budget — but the budget collapses if the user-space Modbus client retries internally or holds the file descriptor open across multiple requests.
3. Root Cause Analysis: Five Compounding Mechanisms
3.1 Serial Port Handle Exhaustion (Highest Probability)
Node-RED's node-red-contrib-modbus keeps a per-node ModbusRTU client. Each client opens the underlying TTY, sets termios, and creates a TCP socket to the internal modbus-serial server on 127.0.0.1. If the closeOnTimeout parameter is left at default, a client that hits an inter-frame error closes the socket but leaves the termios-locked /dev/ttyS2 handle in the FLUSHING state. After ~1500-2000 cycles the IoT2000's process table fills with half-closed modbus-serial worker processes that still hold the UART. Subsequent open() calls return EBUSY, which the node surfaces as a silent {} payload — exactly the symptom observed.
Reference: node-red-contrib-modbus official README documents the closeOnTimeout, delayOnAck, and retryOnTimeout knobs. The default closeOnTimeout: true combined with retryOnTimeout: true produces the leak.
3.2 Task Cycle Time Smaller Than t3.5
If the four Modbus Read nodes run inside a Node-RED flow whose exec scheduler calls them within the same 10-50 ms tick, the second request hits the slave before the first t3.5 gap has elapsed. The slave discards both. The client marks the request as RECONNECTING and queues a backoff. Over thousands of cycles the queue grows, latency compounds, and a node that looked healthy on day one drops off at hour 22.
Verify by inspecting modbus-serial logs at DEBUG level. Look for repeating Timed out followed by State: INIT → RECONNECTING.
3.3 Watchdog Reset on the IoT2000
The IoT2000 firmware exposes a hardware watchdog at /dev/watchdog that pings every 60 s. If the user-space Modbus client holds the UART for more than that interval (which can happen on cold-start when the PLC is in STOP mode and the client retries 5 times × 5 s timeout), the watchdog fires and reboots the SIMATIC. After reboot the four Modbus nodes re-init serially, and the race for the TTY handle begins again — masking as a fault that "recovers" when you restart the flow.
Reference: SIMATIC IOT2050 operating instructions, section on watchdog configuration, available on the Siemens Industry Online Support product page for 6ES7647-0BA00-0YA2 / 6ES7647-0BA00-1YA2.
3.4 RS232 Signal Levels and Cable-Induced Errors
RS232 is single-ended, full-duplex, and tolerates cable lengths up to 15 m at 9600 bps, but the cable capacitance budget is consumed by every meter of conductor. A 4 ft cross cable is fine. What is not fine is:
- Pins 7 (RTS) and 8 (CTS) jumpered: This forces permanent request-to-send, defeating hardware flow control. Some Modbus slaves use RTS to gate transmission; if RTS is held asserted, the slave may transmit continuously and corrupt the master's receiver.
- No ground reference between chassis: If the IoT2000 and the PLC are powered from different branch circuits, the GND pin (DB9 pin 5) may float ±5 V relative to the slave's GND. The receiver interprets any differential less than ±3 V as indeterminate. Field experience shows this becomes intermittent at temperature cycles inside enclosures.
- DB9 shield not bonded: Without a bonded shield, the cable picks up VFD-induced common-mode noise that biases the receiver threshold.
3.5 Slave PLC Dropping the Link
The slave may itself be a victim of cumulative state. Delta DVP, Mitsubishi FX5U, and Keyence KV-8000 all implement a Modbus RTU slave with a finite transaction counter and a watchdog that resets the UART after N idle seconds. If the IoT2000's poll rate is 60 s and the slave's internal idle reset is also 60 s, the timer fires before the next request, the slave closes its UART, and the next request is lost. The slave will recover on the next poll, but the master's expectation of four successful reads per cycle is permanently broken until both timers are realigned.
4. Diagnostic Procedure
- Capture the UART traffic. Connect a USB-to-serial tap (FTDI FT2232H or compatible) in parallel with the existing DB9. Run
tio -b 9600 -d 8 -s 1 -p none -f /dev/ttyUSB0 | tee /tmp/capture.log. Look for dropped bytes, FCS errors, and inter-frame gaps shorter than 4 ms at 9600 bps. - Log the Modbus client state. Add a
debugnode to eachModbus Readoutput and capturemsg.payload,msg.error, and the innerModbusResponseobject. Run for 48 hours. The first error's timestamp is the regression point. - Inspect the process table. Periodically run
ps aux | grep -E 'modbus|node'into a cron-log file. Compare the number of node processes at hour 1 and hour 30. If the count grew, you have a handle leak. - Disable the IoT2000 hardware watchdog during testing:
systemctl stop watchdogor remove theWATCHDOG=1environment variable if running a custom systemd unit. If the regression disappears, the watchdog is implicated. - Swap the IoT2000 with a Windows machine running Modbus Poll or QModMaster over the same cable. If the symptom disappears on Windows but persists on Linux, the bug is in the user-space Modbus stack, not the physical layer.
- Use an oscilloscope on TXD, RXD, and GND. Look for signal rise times > 1 ms (indicating capacitive loading), ringing > ±5 V (indicating missing termination on the line driver), or asymmetric mark/space voltages (indicating ground offset).
5. Hardware Verification
Verify the DB9 wiring against the standard EIA-574 pinout (the IoT2000 COM ports implement DTE behavior):
| DB9 Pin | Signal | Direction (DTE) | IoT2000 COM2 (ttyS2) |
|---|---|---|---|
| 2 | RXD | Input | Receive from slave TXD |
| 3 | TXD | Output | Transmit to slave RXD |
| 5 | GND | — | Common signal ground |
| 7 | RTS | Output | Do NOT loop back to CTS on a peer DCE |
| 8 | CTS | Input | Do NOT loop to RTS |
Verify RS232 levels with a multimeter in DC mode between DB9 pin 5 (GND) and pins 2 and 3. The idle state of TXD should be between −3 V and −15 V. The idle state of RXD should also be in that range. Voltages between −3 V and +3 V on either line indicate a ground-offset problem; replace the cable or bond the chassis grounds with a separate 12 AWG conductor.
For runs longer than 2 m or noisy environments (VFDs, soft starters, contactors), migrate to RS485. The IoT2000 COM2 supports RS485 half-duplex on the same DB9 connector when configured via the device tree overlay; reference the SIMATIC IOT2050 manual, section "Serial interface configuration". RS485 requires a 120 Ω termination resistor between the A and B conductors at both ends of the bus; do not place termination on stubs.
6. Software Fix: Node-RED Configuration
The following settings resolve the regression in node-red-contrib-modbus 5.x:
Modbus Read node configuration (per node):
Name: D1030
Topic: plc-d1030
Unit-Id: 1
FC: 3 (FC 3 - Read Holding Registers)
Address: 1029 // 0-based; equivalent to D1030 in 1-based PLC convention
Quantity: 1
Poll Rate: 1000 // ms, used only when no trigger input is wired
Timeout (ms): 1500
Delay on Ack: 50 // ms, ensures t3.5 gap is honored between ADUs
Retry on Timeout: false
Reconnect on Timeout: true
Reconnect Timeout: 5000 // ms
Key changes:
-
Retry on Timeout: falseprevents the inner 3-retry loop from holding the socket open while it cycles. -
Reconnect on Timeout: truewith a 5 s backoff gives the slave time to reset its UART without piling up handles. -
Delay on Ack: 50 msinjects the inter-frame silence that the Linux UART driver does not naturally produce when back-to-backwrite()calls land in the same termios buffer. - The outer trigger is an
injectnode firing every 60 s ontopic "poll-cycle", connected to all fourModbus Readnodes through adelaychain: 0 s, 1 s, 2 s, 3 s. This guarantees sequential polling and eliminates the same-tick collision risk.
Add a watchdog "kicker" flow that emits a reset message on msg.topic = "modbus-master-reset" if any of the four Modbus Read nodes has not produced a successful payload in 5 minutes. Wire that reset into the Modbus Connector node's close input. This purges accumulated state every 5 minutes of abnormal behavior, which is much less aggressive than waiting 20 hours for a full regression.
7. Software Fix: CODESYS 3.5 SP16+ Modbus Master
If the deployment runs CODESYS Control for IOT2000 instead of Node-RED, the equivalent fix lives in the PLC task configuration and the CAA Modbus library parameters:
// CODESYS task configuration (Task Configuration -> Task1)
Task1:
Priority: 15
Cycle time: 20 ms // t3.5 at 9600 = 4.48 ms; 20 ms guarantees inter-frame
Watchdog: 100 ms
Event-driven: FALSE
// CAA_ModbusMaster parameter block
iSlaveAddr := 1;
iFuncCode := MODBUS_FC_READ_HOLDING_REGISTERS; // 16#03
iDataAddr := 1029;
iDataCount := 1;
iTimeout := 1500; // ms
xBlockUntilDone := FALSE;
xExecute := TRUE;
// Re-arm after each successful read:
IF xBusy = FALSE AND xError = FALSE THEN
xExecute := FALSE; // mandatory rising-edge reset
tDelay(IN := TRUE, PT := #50ms); // 50 ms inter-frame gap
IF tDelay.Q THEN
tDelay(IN := FALSE);
iDataAddr := iDataAddr; // cycle to next register in external sequencer
xExecute := TRUE;
END_IF;
END_IF;
The CODESYS task cycle of 20 ms is deliberately larger than the 4.48 ms t3.5 at 9600 bps. If a faster task is needed (sub-10 ms), switch to RS485 with hardware-managed direction control, or insert a TON delay between back-to-back requests.
Reference: CODESYS Online Help, "CAA Modbus Master" library documentation, available from CODESYS Store under the CAA bundle. The library's ModbusChannel FB exposes iTimeout and iPollCycle; both must be aligned with the slave's idle-reset timer.
8. RS485 Migration Path (Recommended for Permanent Installations)
RS232 over a 4 ft cross cable is acceptable for bench commissioning but is not appropriate for permanent industrial deployment. The IoT2000 COM2 can be reconfigured for RS485 half-duplex via the device tree overlay documented in the SIMATIC IOT2050 manual. RS485 differential signaling provides:
- ±7 V common-mode rejection (vs ±2 V for RS232 single-ended).
- Cable runs up to 1200 m at 9600 bps.
- Multi-drop support for up to 32 slaves on a single twisted pair.
- Survivability in VFD, motor-start, and welding environments.
Reference the TIA-485-A standard from TIA for the electrical characteristics and the Modbus over Serial Line V1.02 for the cable, termination, and stub-length requirements. Place a 120 Ω ±1 % resistor at each physical end of the bus. Do not place termination on stubs or Y branches. Use a shielded twisted pair with the shield bonded to chassis ground at one end only (typically the master).
9. Verification Procedure
After applying the fixes, run a 72-hour burn-in with the following observability stack:
- Capture every
Modbus Readpayload into a CSV with columns: timestamp, register address, raw value, response time, error code, node state. Use the Node-REDcsvnode or afunctionnode appending to a rotating file. - Sample
/proc/$(pidof node-red)/fd | wc -levery 60 minutes. The count must be flat over 72 hours (typically 30-45 file descriptors steady-state). - Run
strace -p $(pidof node-red) -e read,write,openat -o /tmp/trace.logfor 5 minutes every 6 hours. Verify/dev/ttyS2open count does not grow. - Plot
response_timeover time. A healthy link showsP99 ≤ 50 mswith a flat tail. A regressing link shows a long right tail after hour 18-30 — that's the precursor to the four-to-three dropout. - Verify the IoT2000 uptime with
uptime. The watchdog should not have fired; the load average should remain under 0.5 on the dual-core ARM.
Acceptance criteria: zero dropped reads over 72 hours, P99 response time under 50 ms, zero RECONNECTING events in the modbus-serial debug log, and process file-descriptor count constant.
10. Fault Code Mapping
| Modbus Exception Code | Name | Common Cause | Resolution |
|---|---|---|---|
| 0x01 | ILLEGAL_FUNCTION | Slave does not support FC 03 at that address | Check slave function map; reduce FC scope |
| 0x02 | ILLEGAL_DATA_ADDRESS | Address 1029 is outside the slave's data range | Verify D-register existence; check 0/1-based offset |
| 0x03 | ILLEGAL_DATA_VALUE | Quantity out of bounds or PLC in STOP | Confirm Quantity = 1; verify PLC in RUN |
| 0x04 | SLAVE_DEVICE_FAILURE | PLC is in error state or scan time exceeded | Check PLC error LEDs; reset scan watchdog |
| 0x05 | ACKNOWLEDGE | Long execution; slave delays response | Increase client timeout to 5 s |
| 0x06 | SLAVE_DEVICE_BUSY | Slave processing previous request | Increase inter-frame delay to 100 ms |
| 0x0A / 0x0B | GATEWAY_PATH / TARGET | Bridged gateway problem | Verify Ethernet-to-serial bridge configuration |
Codes 0x06 and 0x0B are the most common co-conspirators in the 20-30 hour regression because they trigger the master's reconnect logic, which compounds handle exhaustion under load.
11. Troubleshooting Matrix
| Symptom | Likely Cause | Fix |
|---|---|---|
| Reads work, then 1 of 4 drops after 20 h | Handle exhaustion + retry loop | Disable inner retry; enable reconnect backoff |
| All reads drop after exactly 60 s | IoT2000 watchdog reboot | Disable watchdog during polling; ensure main loop yields |
| Reads drop randomly across all 4 nodes | CRC errors from ground offset or noise | Bond chassis ground; migrate to RS485 |
| Reads drop after a few hours in hot enclosure | Thermal drift on UART driver | Replace cable; verify TXD idle voltage is < -7 V |
| Reads work at 9600 but fail at 19200 | Slave cannot sustain t1.5 at higher baud | Drop baud to 9600; verify slave supports 19200 RTU |
| Reads work on Windows, fail on IoT2000 | Linux UART termios mismatch | Set -parenb -cstopb cs8 -ixon -ixoff crtscts on ttyS2 |
| Reads fail after PLC power cycle | Slave UART not re-armed | Add 5 s reconnect delay after first error |
12. Field-Proven Checklist
- Confirm ttyS2 mapping:
stty -F /dev/ttyS2 9600 cs8 -cstopb -parenb -crtscts rawbefore starting Node-RED. - Set
delayOnAckor equivalent inter-frame guard to at least 50 ms. - Disable inner retry; rely on outer reconnect timer.
- Schedule the four reads sequentially with a 1 s gap, not concurrently.
- Bind
reconnectTimeoutto 5 s to give the slave UART time to reset. - Bond chassis ground if devices are on different branch circuits.
- Remove pins 7-8 jumper; allow CTS to float on the master.
- For new installations, specify RS485 with proper termination.
- Plan a 72-hour burn-in with CSV logging before commissioning.
- Monitor file-descriptor count and IoT2000 uptime during the burn-in.
Why do my Modbus reads work for 20-30 hours and then drop one at a time?
This is almost always a serial-port handle leak combined with an aggressive inner-retry loop. Each timeout closes and reopens the inner TCP socket to modbus-serial but leaves the underlying /dev/ttyS2 file descriptor in a half-closed state. After ~1500 cycles the process table fills, new opens fail with EBUSY, and the affected node silently returns an empty payload. Disable inner retry, set delayOnAck ≥ 50 ms, and enable a 5 s reconnectTimeout.
Does a 4 ft RS232 cable really need a 120 Ω termination?
No. RS232 is single-ended and does not use differential termination. The 120 Ω resistor is required only on RS485 buses between the A and B conductors at the physical ends of the trunk. If you see termination advice for an RS232 Modbus link, it is a misapplied RS485 recommendation and adding the resistor on RS232 will load the line drivers incorrectly.
Should pins 7 (RTS) and 8 (CTS) be jumpered on the IoT2000 RS232 port?
Only when the connected device is a true DCE that never asserts CTS. Most Modbus RTU slaves (Delta DVP, Mitsubishi FX, Keyence KV) do not implement hardware flow control and leave CTS floating. Jumpering 7-8 on the master holds RTS asserted, which on some PLC firmware inhibits slave transmission. Leave pins 7 and 8 unconnected for Modbus RTU use.
What is the right task cycle time in CODESYS for Modbus RTU at 9600 bps?
Set the task cycle to at least 20 ms. The Modbus over Serial Line specification requires a 3.5-character inter-frame silence of 4.48 ms at 9600 bps, and the task cycle must be larger than this so back-to-back requests are separated correctly. Pair the task cycle with a 50 ms inter-request delay inside the FB to absorb slave turnaround.
How do I know if the IoT2000 watchdog is rebooting the controller?
Run uptime periodically and log it. If the value resets to a small number of minutes during the regression window, the watchdog has fired. The IoT2000 watchdog pings every 60 s; if your Modbus client holds the UART for more than that interval (for example, due to a 5-retry × 5 s timeout chain on a stuck slave), the kernel fires /dev/watchdog and reboots. Disable the watchdog during commissioning with systemctl stop watchdog to confirm.