Configuring Node-RED Modbus RTU on Siemens IOT2040 X30/X31 Ports

David Krause10 min read
Serial CommunicationSiemensTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

The SIMATIC IOT2040 (part number 6ES7647-0AA00-0YA2) is an industrial IoT gateway that ships with a Yocto Linux image and Node-RED pre-installed. It exposes two on-board serial interfaces, X30 (RS-232) and X31 (RS-485), that map to Linux device nodes /dev/ttyS1 and /dev/ttyS2 respectively. When porting a Modbus RTU flow from a generic Linux PC to the IOT2040, engineers routinely encounter a node-red-contrib-modbus client that re-connects in a tight loop, an idle Modbus-Serial node that never receives data, or—after switching port modes—complete silence on the wire. This article consolidates the diagnostic sequence, the iot2000setup port-mode switching, the RS-232/RS-485 pinout verification, and the firmware-version sensitivity that drives these failures.

Hardware Topology

The IOT2040 base board (Intel Quark x1020 + Galileo generation silicon) routes the two Phoenix-style connectors through an XR16L2750 UART and an SN65HVD72 RS-485 transceiver. The relevant device tree and pin mapping are exposed by the iot2000setup helper. The table below summarises the electrical characteristics that must be respected before any Modbus traffic is started.

Port Connector Linux Node Mode Max Baud Termination Bias
X30 6-pin Phoenix /dev/ttyS1 RS-232 (default) 115 200 bit/s N/A N/A
X31 6-pin Phoenix /dev/ttyS2 RS-485 (default) 115 200 bit/s External 120 Ω Required on bus
X30 6-pin Phoenix /dev/ttyS1 RS-485 (after switchserialmode) 115 200 bit/s External 120 Ω Required on bus
X31 6-pin Phoenix /dev/ttyS2 RS-232 (after switchserialmode) 115 200 bit/s N/A N/A

Pin assignment for both ports (front view, looking at the connector on the IOT2040 faceplate):

Pin X30 RS-232 X30 RS-485 (after mode switch) X31 RS-485 (default) X31 RS-232 (after mode switch)
1 n.c. Data+ (A) Data+ (A) n.c.
2 n.c. Data- (B) Data- (B) n.c.
3 TD (out) n.c. n.c. TD (out)
4 RD (in) n.c. n.c. RD (in)
5 GND GND GND GND
6 n.c. n.c. n.c. n.c.
When X30 is reconfigured to RS-485, pins 1, 2 and 5 are the only connections required. Mis-wiring RX/TX on an RS-485 node produces silent bus traffic; switch A and B only after confirming with a known-good USB-to-RS-485 adapter as a sniffer.

Prerequisites

  1. SIMATIC IOT2040 with the example image V2.1.0 (verified to ship a working Modbus TCP stack) or V2.2.0 if the workaround in this article is applied. Both images are signed by Siemens and available on the Industry Online Support portal under entry ID 109741656.
  2. SSH access to the gateway as user root. The default password on the example image is root; change it on first boot.
  3. The Modbus RTU slave. The reference device used during the original debug was a Siemens SENTRON PAC3100 (article number 7KM2112-0BA00-3AA0) configured for Modbus RTU at 19 200 bit/s, 8E1, slave address 1.
  4. Node-RED ≥ 0.20.x installed via npm install -g node-red --unsafe-perm on the device, plus the two nodes used by the failing flow: node-red-contrib-modbus and node-red-contrib-modbus-serial.
  5. A USB-to-RS-485 or USB-to-RS-232 converter with FTDI chipset (e.g. ICUSB234858I or equivalent) for bus sniffing.
  6. A digital multimeter with true-RMS AC measurement and a logic analyser or oscilloscope that supports ≥ 1 Msample/s capture for eye-diagram verification.

Step-by-Step Diagnostic Sequence

1. Confirm the Operating Image

The IOT2040 example image line moved from V2.1 (kernel 4.4.13-yocto-standard, Node 6.x, Node-RED 0.17) to V2.2 (kernel 4.4.13-yocto-standard, Node 8.x, Node-RED 0.19). The upgrade package changed the default Modbus RTU retry handling and removed the legacy /dev/ttyS0 mapping used by some user flows. Verify with:

cat /etc/image-version
uname -a
node --version
node-red --version

If the image reads iot2000-example-image-2.2.0 and the user flow uses the Modbus TCP node with no functional TCP slave, the Node-RED process will be bound to 0.0.0.0:5020 but every TCP request will fail because no client is polling. The intermittent "modbus TCP not working on 2.2" symptom is caused by an incorrect unit-id mapping in the V2.2 build of the contrib nodes; downgrade to V2.1.0 to recover parity, then re-flash to V2.2 only after applying the patch in step 6 below.

2. Inspect the Serial Port Mode

The shipped RS-485 transceiver on X31 is enabled by default, but X30 is RS-232 only unless the switchserialmode utility is invoked. The script is at /usr/sbin/switchserialmode and reads the GPIO expander to re-mux the UART.

iot2000setup --portmode x30 rs485
switchserialmode
iot2000setup --portmode x31 rs232

The commands above flip X30 to RS-485 (so the PAC3100 can be wired directly to pins 1, 2, 5) and X31 to RS-232 (handy for connecting a PC with a USB converter for loopback testing). After every mode change, reboot the device so that the kernel driver re-acquires the line discipline; do not rely on a hot-swap.

3. Validate the Linux Device Node

Confirm the kernel sees both UARTs and that no other process has them open:

ls -l /dev/ttyS1 /dev/ttyS2
lsof | grep -E '/dev/ttyS[12]'
stty -F /dev/ttyS2 -a

The output of stty -a on the RS-485 port must show raw mode, no echo, no flow control and the baud rate matching the slave. The PAC3100 default is 19 200, 8E1, so configure:

stty -F /dev/ttyS2 19200 cs8 -parenb -cstopb parenb -ixon -ixoff crtscts -icanon -echo

If Node-RED is already running, stop it before stty modifies the line; otherwise the contrib-modbus client will reset the parameters when its next reconnect attempt fires.

4. Loopback at the Wire Level

Place a wire jumper between pins 1 and 2 on X31 (Data+ to Data-). Send a Modbus RTU echo request from Node-RED and watch for a response. With the contrib-modbus Flex-Getter node configured for FC03, unit-id 1, address 0, quantity 1, a successful read will populate msg.payload.data with the echoed register value (typically 0x0001). If the request times out with no data, the transceiver is the prime suspect and the device should be RMA'd. Field measurement of approximately 2.7 V AC between pin 5 of the IOT2040 X30/X31 connector and the shield of a USB/serial converter is a known failure signature: it indicates that the on-board DC-DC converter has lost regulation on the -5 V rail that biases the RS-232 charge pump, leaving the line drivers in an undefined state.

5. Swap node-red-contrib-modbus for Modbus-Serial

The classic symptom tree distinguishes two node behaviours:

Symptom node-red-contrib-modbus node-red-contrib-modbus-serial Likely Root Cause
Node status "reconnecting…" every 5 s Yes n/a TCP socket or unit-id mismatch on V2.2 image
Node status "active" but no payload n/a Yes Wrong /dev/ttySx, wrong baud, swapped A/B
Static JSON only, registers undefined Yes Yes Slave address or parity mismatch
Throws ModbusRTUOverTcpError Yes n/a TCP wrapper around RTU; port mode is irrelevant

If the contrib-modbus TCP wrapper is required, configure as:

Type: TCP
Host: 127.0.0.1
Port: 5020
Unit-Id: 255 (for Modbus gateway) or 1 (for direct TCP-to-RTU bridge)

For pure RTU over /dev/ttyS2 with the modbus-serial node, set:

Type: Serial
Serial Port: /dev/ttyS2
Baud Rate: 19200
Data Bits: 8
Stop Bits: 1
Parity: even
Slave ID: 1
Connection Timeout: 2000 ms

6. Reinstall Modbus Nodes From Source

The pre-built npm binaries of node-red-contrib-modbus occasionally ship against a libmodbus version that is incompatible with the Yocto glibc shipped on the IOT2040. Rebuild from source:

npm install -g node-red-contrib-modbus --build-from-source --unsafe-perm
npm install -g node-red-contrib-modbus-serial --build-from-source --unsafe-perm
systemctl restart node-red

The --build-from-source switch forces node-gyp to recompile modbus-serial against the on-target libstdc++ so that the symbol table matches. A missing python or make tool on the image causes silent failures; install them with opkg install python make g++ before the rebuild.

7. Python Serial Sanity Check

Bypass Node-RED entirely and write a 10-byte loop from Python:

#!/usr/bin/env python3
import serial, struct, time
ser = serial.Serial('/dev/ttyS2', 19200, parity='E', bytesize=8, stopbits=1, timeout=1)
while True:
    pkt = struct.pack('>HHHB', 1, 0, 1, 0xC0CA)
    crc = 0xFFFF
    for b in pkt:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    pkt += struct.pack('<H', crc)
    ser.write(pkt)
    print('TX:', pkt.hex())
    time.sleep(1)

Sniff with a USB-RS-485 adapter and PuTTY or minicom on the host. If the host sees zero traffic, the IOT2040 UART TX path is dead and the unit must be returned. If the host sees the bytes but no slave response, the bus termination or slave address is wrong.

Verification Checklist

  1. node-red-log shows a "connected" or "active" status on the modbus node within 2 s of deploy.
  2. A debug node attached to the Modbus output prints a payload at the configured poll interval.
  3. stty -F /dev/ttyS2 -a still shows the configured baud and parity after the flow has run for 30 minutes.
  4. A second host on the bus (sniffer) observes valid Modbus RTU frames with correct CRC-16 (Modbus polynomial 0xA001).
  5. Removing the slave from the bus causes the node to revert to "reconnecting" within the configured timeout, proving that the node is not falsely reporting success on an empty bus.

Firmware and Image Matrix

Image Node-RED node-red-contrib-modbus Modbus TCP Modbus RTU on X31 Notes
Example V2.1.0 0.17.6 2.4.0 Working Working Reference baseline
Example V2.2.0 0.19.5 3.1.3 Broken on default config Working Apply unit-id patch or downgrade
Example V2.3.0 0.20.7 4.0.4 Working Working Requires kernel 4.4.30 patch
Siemens Industry Online Support no longer accepts hotline tickets for the IOT2040 because the product was transitioned to the IOT2050 line in 2019. Hardware-related issues must be raised through a regional representative or via the official Siemens Industry Online Support portal using product ID 6ES7647-0AA00-0YA2. Plan to migrate flows to the IOT2050 (6ES7647-0BA00-0YA2) which uses an ARM i.MX 8M and supports both RS-232 and RS-485 on a single connector without mode switching.

Hardware Failure Signatures

Symptom Measurement Conclusion Action
Zero traffic on TX Logic analyser idle on pin 3 (X30) or pin 1/2 (X31) UART driver or CPU halt Reboot, reflash
Garbled characters Baud mismatch on scope Wrong stty config Re-apply stty line
2.7 V AC between pin 5 and chassis DMM true-RMS DC-DC converter failure Hardware RMA
3.3 V DC idle on A-B DMM Missing fail-safe bias Add 680 Ω pull-up / pull-down at slave
Idle line at 0 V Scope Bus shorted or slave un-powered Verify slave supply

Workaround: Serial-to-Ethernet Bridge

When the on-board UART is unreliable but the IOT2040's network stack is healthy, attach an external serial-to-Ethernet converter (e.g. Moxa NPort 5110, Phoenix Contact FL COMSERVER UNI 232/422/485, or Anybus Communicator) and run Modbus RTU over TCP from Node-RED. Configure the converter at 192.168.200.50:4001, point the contrib-modbus client to 192.168.200.50:4001, and set unit-id to 255 (the bridge's default pass-through identifier). This was the production workaround adopted during the original incident and remains the recommended deployment pattern when migrating legacy serial flows to a non-industrial gateway.

Modbus RTU Frame Reference

For engineers validating raw bytes on the wire, the canonical read-holding-registers frame (FC03) for the PAC3100 at slave 1, address 0x0000, quantity 1 is:

Request  : 01 03 00 00 00 01 84 0A
Response : 01 03 02 00 00 C4 38

The trailing two bytes are the CRC-16 (Modbus) calculated over the preceding six bytes with polynomial 0xA001 (bit-reversed representation of 0x8005) and an initial value of 0xFFFF. A node-red function node that produces a usable frame for the serial-out path is:

var fc = 0x03, addr = 0x0000, qty = 0x0001;
var raw = Buffer.from([1, fc, (addr>>8)&0xFF, addr&0xFF, (qty>>8)&0xFF, qty&0xFF]);
var crc = 0xFFFF;
for (var i = 0; i < raw.length; i++) {
    crc ^= raw[i];
    for (var b = 0; b < 8; b++) crc = (crc & 1) ? (crc>>1) ^ 0xA001 : crc>>1;
}
msg.payload = Buffer.concat([raw, Buffer.from([crc&0xFF, (crc>>8)&0xFF])]);
return msg;

Frequently Asked Questions

Why does the node-red-contrib-modbus client keep reconnecting on my IOT2040?

The client retries whenever its TCP socket receives a RST or its unit-id mapping fails to resolve. On the V2.2.0 example image the default unit-id map is 1-247 to TCP socket index, which breaks flows that poll a TCP-to-RTU bridge. Downgrade to image V2.1.0 or rebuild node-red-contrib-modbus from source against the on-device libstdc++ so that the unit-id lookup table matches.

How do I switch the IOT2040 X30 port from RS-232 to RS-485?

Run iot2000setup --portmode x30 rs485 followed by switchserialmode and reboot. The mode bit is latched in GPIO and survives power-cycle, but the kernel UART driver only re-reads the line discipline on reboot, so a warm restart is required.

Which Linux device node corresponds to X30 and X31?

X30 maps to /dev/ttyS1 and X31 maps to /dev/ttyS2 on every shipped IOT2040 image. Verify with ls -l /dev/ttyS1 /dev/ttyS2; both nodes are owned by group dialout and Node-RED must be granted group membership with usermod -aG dialout root if it runs under a non-root user.

What is the correct Modbus RTU configuration for a Siemens PAC3100?

The PAC3100 defaults to 19 200 bit/s, 8 data bits, even parity, 1 stop bit, slave address 1. The mapping is documented in the PAC3100 manual entry ID 65211911 on the Siemens Industry Online Support portal. Use the "Read Holding Registers" function code 0x03 to fetch measured values starting at address 0x0001.

What if my IOT2040 X30/X31 port shows approximately 2.7 V AC on pin 5?

That voltage is a failure signature of the on-board DC-DC converter that feeds the RS-232 charge pump and the RS-485 bias network. The UART will not transmit reliably and the unit must be returned through the official RMA channel. As a temporary measure, drive the bus from an external serial-to-Ethernet converter and connect to the IOT2040 only over TCP.

Back to blog