Resolving USB Serial Port Issues on SIMATIC IOT2020 and IOT2040

David Krause11 min read
Industrial NetworkingSiemensTroubleshooting
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 IOT2020 and SIMATIC IOT2040 are Intel Quark x86 based IoT gateways designed as the bridge between shop-floor field devices (typically RS-232/RS-485 serial slaves) and higher-level SCADA, MES, or cloud services. Both run the SIMATIC IOT2000 Example Image - a customized Yocto Linux distribution with Node-RED pre-installed - and expose one USB 2.0 host port (IOT2020) or two USB 2.0 host ports (IOT2040), one RS-232 port on the X21 sub-D connector, and one RS-485 port on the X21 terminal block.

Field engineers commonly attach a USB-to-RS-232 or USB-to-RS-485 converter (e.g., FTDI FT232, CH340/CH341, or Moxa UPort 1110) to the gateway to read Modbus RTU slaves, drive a label printer, or connect to a vendor-proprietary instrument. In the example image 2.1.2 and several early 2.1.3 builds, three reproducible problems appear when attempting to do this from Node-RED:

  1. The kernel creates /dev/ttyUSB0, but a non-root user receives Permission denied when opening the device.
  2. Node-RED's node-red-contrib-modbus or its node-red-node-serialport node throws an Illegal Instruction (SIGILL) and crashes the flow runtime.
  3. The node-serialport JavaScript layer cannot find the compiled native binding (cannot find bindings) and silently fails to register the device.

This article consolidates the verified, upstream fixes for each symptom, identifies the example-image versions in which the fixes were merged, and provides a deterministic commissioning procedure you can run on a fresh SD card.

Hardware and Software Prerequisites

Confirm the following baseline before troubleshooting the application layer.

Item Specification
Controller 6ES7647-0AA00-0YA2 (IOT2020) or 6ES7647-0AA00-1YA2 (IOT2040)
CPU Intel Quark x1020 (x86), 400 MHz, SSE2 only (no SSE4, no AVX)
RAM / Flash 512 MB DDR3 / 8 MB user-flash (typical SD-card boot, 8-32 GB Class 10)
USB ports 1× USB 2.0 Type A (IOT2020), 2× USB 2.0 Type A (IOT2040), 500 mA each
Onboard serial X21: 1× RS-232 (DSUB-9) + 1× RS-485 (terminal block), 115 200 bit/s max
Example image SIMATIC IOT2000 SD-Card Example Image V2.1.3 or later (kernel 4.4.47, Node 4.6.x or 6.x)
Node-RED 0.14.6 (shipped) or updated via the procedure below
USB-to-serial adapter FTDI FT232 (recommended), CH340/CH341 (works, see notes), or industrial-grade Moxa UPort 1110/1130
Node.js ABIs node-serialport pre-built binary must match the running Node.js major version (4.x or 6.x); mixing 4.7+ binaries with 4.2.4 runtime triggers SIGILL

Always download the example image from the official Siemens Industry Online Support portal entry "Latest SD Card Example Image for SIMATIC IOT2000". Verify the SHA-256 hash published in the same thread before flashing.

USB-Serial Subsystem Architecture on IOT2000

When a USB-to-serial converter is plugged in, the Linux kernel loads the appropriate driver and creates a /dev/ttyUSBn device node. The SIMATIC IOT2000 example image enables the following modules by default:

  • usbserial - generic USB-serial core
  • ftdi_sio - FTDI FT232/FT4232H/FT230X
  • ch341 - WCH CH340/CH341
  • cp210x - Silicon Labs CP2102/CP2104
  • pl2303 - Prolific PL2303 (legacy revs only)

To confirm the kernel sees the device, run from an SSH or serial-console session:

root@iot2000:~# lsusb
Bus 001 Device 002: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC
root@iot2000:~# dmesg | tail -20
usb 1-1: new full-speed USB device number 2 using uhci_hcd
usbcore: registered new interface driver ftdi_sio
usbserial: USB Serial support registered for FTDI USB Serial Device
ftdi_sio 1-1:1.0: FT232 USB-Serial (UART) converter now attached to ttyUSB0

If lsusb does not list the converter, the most common cause on the IOT2020 is the 500 mA USB current limit. Some industrial-grade converters draw up to 180 mA inrush; combined with a marginal host port, the device enumerates and then disappears. Use a self-powered hub or move the converter to the IOT2040's second port.

Note: Prolific PL2303 chips shipped after 2014 (HXA/HXD/TA/RA/EC/SA chip revs) ship with a vendor-locked VID/PID and do not bind to the kernel's pl2303 module. The kernel logs disconnecting USB device and never creates /dev/ttyUSB0. Replace the adapter with FTDI or CH340 based hardware for production.

Diagnosing the /dev/ttyUSB0 Permission Denied Error

By default, the SIMATIC IOT2000 image places /dev/ttyUSB0 in the root group with mode 0660. The node-red systemd service drops privileges to user root as well, but the same is not true if you start Node-RED manually as user node-red or run an arbitrary node script. Verify the file mode:

root@iot2000:~# ls -l /dev/ttyUSB0
crw-rw---- 1 root root 188, 0 Jan 1 1970 /dev/ttyUSB0

There are three accepted fixes; pick exactly one.

Option A - Add the user to the dialout group (recommended)

On Debian-derived images this is the dialout group; the SIMATIC Yocto image uses root or a custom tty group. Create a dedicated group and add the service user:

root@iot2000:~# groupadd -f serial
root@iot2000:~# usermod -aG serial root
root@iot2000:~# chown root:serial /dev/ttyUSB0
root@iot2000:~# chmod 0660 /dev/ttyUSB0

Because the device node is created by udev on hot-plug, persist the rule in /etc/udev/rules.d/99-serial.rules:

KERNEL=="ttyUSB[0-9]*", MODE="0660", GROUP="serial"
KERNEL=="ttyACM[0-9]*", MODE="0660", GROUP="serial"

Reload and re-trigger:

root@iot2000:~# udevadm control --reload-rules
root@iot2000:~# udevadm trigger

Option B - Change the systemd service user

Edit /lib/systemd/system/node-red.service and set User=root (acceptable on an air-gapped IoT gateway) or create a dedicated nodered user in the serial group. Always use the official systemd unit shipped with the example image as the baseline - hand-rolled init scripts bypass the resource-limit and watchdog protection.

Option C - ACL on the device

root@iot2000:~# setfacl -m u:nodered:rw /dev/ttyUSB0

ACLs do not survive a re-plug event; use Option A for any permanent deployment.

Resolving the node-serialport Illegal Instruction Crash

The Illegal Instruction (SIGILL, signal 4) reported by Node-RED and the standalone serialport npm package on the IOT2020 is a hardware-level x86 fault: the prebuilt native binding (serialport.node) shipped with older node-serialport releases was compiled with -msse4.1 or -mavx on the maintainer's CI, while the Intel Quark x1020 only implements SSE2. When the JIT-compiled binding executes a movntdqa, vinsertf128, or similar SSE4/AVX opcode, the CPU raises #UD and the process is terminated.

To confirm the diagnosis, capture the register dump from the kernel log:

node[3127]: illegal instruction ip=0xb6f0a034 sp=0xbf1ff6a0
Code: 0f 38 2a 06  (movntdqa %xmm0,(%esi))

The opcode 0f 38 2a is MOVNTDQA, an SSE4.1 instruction - this is the smoking gun.

Upstream fix

The issue is tracked as node-serialport issue #747 and was resolved in the upstream repository at commit ff29e7b. The fix replaces the offending Buffer zero-fill path with an SSE2-safe memset call. The first releases that include the fix are [email protected] and [email protected]. Node-RED's node-red-node-serialport wraps the lower library; an updated [email protected] (and later) pulls the fixed transitive dependency.

Step-by-step remediation

  1. Stop Node-RED: systemctl stop node-red.
  2. Remove the broken binding and the stale module:
    rm -rf /usr/lib/node_modules/node-red/node_modules/node-serialport
    rm -rf /home/root/.node-red/node_modules/node-serialport (if user-level install)
  3. Reinstall using the example image's package manager so the binary is rebuilt for the Quark architecture:
    cd /usr/lib/node_modules/node-red
    npm install --unsafe-perm [email protected]
  4. The --unsafe-perm flag is required because the post-install node-gyp rebuild runs as root inside the Yocto image; without it, the build script is silently skipped and the prebuilt binary is fetched, reintroducing the SSE4 opcode.
  5. Restart and verify:
    systemctl start node-red
    node -e "require('serialport').list((e,ports)=>console.log(ports))"
    The output must list /dev/ttyUSB0 with the FTDI vendor/product descriptor.

Building a Modbus RTU Flow That Actually Opens the Port

The node-red-contrib-modbus package is a fork of modbus-serial that exposes a modbus-read, modbus-write, and modbus-server node. When the underlying serialport binding crashes, the Modbus node surfaces a generic Initialization error in the debug sidebar. After the previous section's fix, the Modbus flow should initialize cleanly.

A minimal, verified flow for reading holding register 0x0000 from slave 1 at 9600/8N1:

[{"id":"a1","type":"modbus-read","z":"f1","name":"Read HR0",
"topic":"","showStatusActivities":false,"showErrors":true,
"logIOActivities":false,"logIOActivities":false,
"unitid":"1","dataType":"HoldingRegister","adr":"0",
"quantity":"1","rate":"1000","rateUnit":"ms","delayOnStart":false,
"startDelayTime":"","server":"b1","useIOFile":false,
"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":false,
"x":240,"y":160,"wires":[["c1"]]},
{"id":"b1","type":"modbus-client","z":"f1","name":"USB RTU",
"clienttype":"serialport","bufferCommands":true,
"stateLogEnabled":false,"tcpHost":"127.0.0.1","tcpPort":"502",
"tcpType":"DEFAULT","serialport":"/dev/ttyUSB0",
"serialBaudRate":"9600","serialDatabits":"8","serialStopbits":"1",
"serialParity":"none","serialConnectionDelay":"100",
"commandDelay":"1","clientTimeout":"1000","reconnectTimeout":"2000"}]

Field notes from commissioning:

  • Set serialConnectionDelay to at least 100 ms to give the FTDI chip time to settle after the DTR/RTS toggle the library performs on open.
  • If the slave is RS-485 half-duplex, the adapter itself must handle direction control. Pure USB-to-RS-232 converters will not drive the bus correctly; choose a USB-to-RS-485 adapter with auto-direction hardware (e.g., FTDI FT232R + MAX485).
  • On a noisy shop floor, set clientTimeout to 1000 ms and increase reconnectTimeout to 2000-5000 ms. The default 50 ms timeout produces a flood of EAGAIN errors during electrical disturbances.

Preferring the Onboard RS-232 Port (X21) Instead of USB

The X21 connector on both IOT2020 and IOT2040 is exposed by the 8250_pci driver and surfaces as /dev/ttyS0 (RS-232) and /dev/ttyS1 (RS-485). Using the onboard port avoids the entire USB stack and is the recommended path for any 24×7 deployment:

root@iot2000:~# stty -F /dev/ttyS0 9600 cs8 -cstopb -parenb -ixon
root@iot2000:~# cat /dev/ttyS0 &
root@iot2000:~# echo -e "\x01\x03\x00\x00\x00\x01\x84\x0A" > /dev/ttyS0

Jumper settings on the IOT2040: place the X21 block in RS-232 mode (default) and the X20 termination jumper OFF for point-to-point links. For RS-485, terminate at both ends with 120 Ω and switch the mode jumper to RS-485; the port is then accessible as /dev/ttyS1 with hardware direction control handled by the onboard transceiver.

Note that the SIMATIC IOT2000 example image disables hardware flow control on both X21 ports by default. If your instrument requires RTS/CTS (rare on industrial sensors but common on legacy PLC programming ports), enable it from the application:

root@iot2000:~# stty -F /dev/ttyS0 crtscts

Verification Procedure

Run this four-step verification on a freshly flashed SD card before shipping the unit to site.

  1. USB enumeration - Plug the converter, run lsusb and dmesg | tail. Expect to see the FTDI/CH340 entry and the now attached to ttyUSB0 line. A blank output indicates a hardware/cable issue, not a software one.
  2. Permission test - Run echo test > /dev/ttyUSB0 from a non-root shell. If the prompt returns Permission denied, re-apply Section 4 Option A.
  3. Round-trip loopback - Fit a female-to-female null-modem adapter between X21 RS-232 and the USB converter, run a 9 600 bit/s Modbus poll from a PC, and confirm echo with a logic analyzer. A clean square wave at 9 600 Hz confirms the electrical layer.
  4. Node-RED smoke test - Deploy the minimal flow from the previous section with a known-good Modbus slave (e.g., a Siemens S7-1200 CM1241 set to slave 1, HR0 = 0x1234). Confirm the debug sidebar shows a numeric payload 4660 (decimal of 0x1234) updating once per second.

Troubleshooting Matrix

Symptom Likely Root Cause Fix
lsusb empty, LED on adapter off Under-powered USB port or defective cable Use a self-powered hub; replace cable; try second port on IOT2040
lsusb lists device, /dev/ttyUSB0 missing pl2303 HXD/RA chip rev not bound, or ftdi_sio not in image Switch to FTDI hardware; verify with modprobe ftdi_sio
Permission denied on /dev/ttyUSB0 udev rule missing or wrong group Apply Section 4 Option A udev rule
Node-RED Initialization error in modbus node Stale serialport.node binary with SSE4 opcode Reinstall node-red-node-serialport with --unsafe-perm rebuild
Standalone node -v returns Illegal Instruction after n 6.9.4 Node 6.9.4 was cross-compiled with SSE4.1; Quark is SSE2-only Use the example image's bundled Node 4.6.x, or build Node 6.x from source with -msse2 -mtune=generic
Flow deploy succeeds, but no data; debug shows EAGAIN Timeout too aggressive or wrong parity Increase clientTimeout to 1000 ms; re-verify parity at slave
Works on USB but not on X21 RS-232 Mode jumper not in RS-232 position, or DTE/DCE mismatch Check X21 jumper; use a null-modem adapter if connecting to a DTE device
cannot find bindings in Node-RED startup log Multiple Node.js versions in PATH, module installed under wrong ABI Remove all global modules and reinstall with the running node binary

Field-Commissioning Checklist

  • Document the USB-to-serial vendor and chip rev in the project EPLAN. FTDI FT232R is the recommended baseline because it is the only one that binds cleanly to the in-kernel ftdi_sio on every IOT2000 image revision.
  • Pin the example image version (V2.1.3 or later) and the node-red-node-serialport version (≥ 0.6.5) in the project bill of materials. Avoid npm install -g n for Node upgrades - the resulting binary is the SSE4 one and will crash immediately.
  • Disable the systemd service in the example image called node-red-restapi if not needed; it adds a third-party HTTP endpoint that complicates firewall rules.
  • Set the IOT2000 clock to UTC with NTP (or a battery-backed RTC) before any Modbus poll that uses timestamped logging; the example image's default time is the build timestamp and causes time-jump artifacts in the historian.
  • When deploying in cabinets above 50 °C ambient, use the IOT2040 (extended temperature) rather than the IOT2020. The IOT2020 is rated 0 to 50 °C operating, and USB-serial converters frequently fail first under thermal stress.

FAQs

Why does Node-RED crash with "Illegal Instruction" the moment I deploy a serial-port node on the IOT2020?

The bundled native serialport.node binary is compiled with SSE4.1 opcodes; the Intel Quark x1020 in the IOT2020 only implements SSE2. Reinstall [email protected] or later using npm install --unsafe-perm so the binding is rebuilt locally for the Quark target. The same fix is required for IOT2040 and is upstream in node-serialport issue #747.

What is the correct udev rule to give the node-red user access to /dev/ttyUSB0?

Create /etc/udev/rules.d/99-serial.rules with the line KERNEL=="ttyUSB[0-9]*", MODE="0660", GROUP="serial", create the serial group, add the Node-RED service user, then run udevadm control --reload-rules && udevadm trigger. The device mode will then be crw-rw---- with the correct group on every hot-plug event.

Can I upgrade Node.js from 4.2.4 to 6.x on the example image to silence the contrib-modbus warning?

Do not use npm install -g n - the prebuilt Node 6.x binaries are compiled for SSE4 and crash with Illegal Instruction on the Quark CPU. Either stay on the example image's bundled Node 4.6.x, or cross-compile Node from source with -msse2 -mtune=generic. A patched source is not officially distributed, so the recommended path is to stay on Node 4.6.x and update only the npm modules.

Why are no USB ports visible in Node-RED after I flash image 2.1.3?

Early 2.1.3 builds re-introduced a stale serialport.node. Apply the upstream commit ff29e7b fix by reinstalling node-red-node-serialport with --unsafe-perm. Verify the fix with node -e "require('serialport').list(c=>console.log(c))"; /dev/ttyUSB0 must appear in the printed list.

Should I use the onboard X21 RS-232 or a USB-to-serial adapter for Modbus RTU?

Prefer the onboard X21 port (/dev/ttyS0 for RS-232, /dev/ttyS1 for RS-485) for any permanent installation - it removes the USB stack, hot-plug events, and converter-thermal failures from the equation. Use the USB path only for ad-hoc commissioning or when the slave device is already wired for a USB isolator (common in medical and laboratory instruments).

Back to blog