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. |
Prerequisites
- 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.
- SSH access to the gateway as user
root. The default password on the example image isroot; change it on first boot. - 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.
- Node-RED ≥ 0.20.x installed via
npm install -g node-red --unsafe-permon the device, plus the two nodes used by the failing flow:node-red-contrib-modbusandnode-red-contrib-modbus-serial. - A USB-to-RS-485 or USB-to-RS-232 converter with FTDI chipset (e.g. ICUSB234858I or equivalent) for bus sniffing.
- 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
-
node-red-logshows a "connected" or "active" status on the modbus node within 2 s of deploy. - A debug node attached to the Modbus output prints a payload at the configured poll interval.
-
stty -F /dev/ttyS2 -astill shows the configured baud and parity after the flow has run for 30 minutes. - A second host on the bus (sniffer) observes valid Modbus RTU frames with correct CRC-16 (Modbus polynomial 0xA001).
- 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 |
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.