Resolving IOT2040 Digital Input GPIO Not Triggering in Node-RED

David Krause11 min read
I/O ModulesSiemensTroubleshooting
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

Problem Overview

A SIMATIC IOT2040 gateway runs Node-RED, and the engineer wires a 3.3 V signal into one of the Arduino-shield digital inputs. The node-red-contrib-iot2000 digital input node is installed, the debug tab reports payload: 1, and yet no downstream function ever fires. The wire is plugged, unplugged, and re-plugged; the value never leaves 1. The engineer expects rising-edge behaviour (idle low, transition high = event), the way a Raspberry Pi GPIO behaves, and concludes that the input is dead.

This is not a hardware fault and not a dead node. The IOT2040 digital input stage powers up with its internal pull-up resistor enabled, so the resting level is logic HIGH (1) at boot. The polling node only emits a message when the read value changes, and a floating or +3.3 V wire does not change that value. Triggering a function therefore requires a HIGH-to-LOW transition, or a low pulse long enough for the configured update interval to sample it.

Symptom in one line: Digital input debug shows msg.payload = 1 at all times; no change event is emitted regardless of wire state, so the function node is never reached.

SIMATIC IOT2040 Hardware Architecture (Arduino Shield Side)

The IOT2040 (6ES7647-0AA00-0YA2) is an Intel Quark x1000 based IoT gateway with an Arduino-Uno-R3 compatible shield interface. The shield side exposes the pins used by node-red-contrib-iot2000 and the operating voltage of those pins is dictated by the IOREF jumper on the underside of the board. The relevant block is summarised below.

Block Part / Reference Value
CPU Intel Quark x1000 (x86, 400 MHz) Single core, 32-bit
RAM / Flash 1 GB DDR3 / 8 MB onboard + SD Yocto / Example Image
Arduino-shield digital I/O D0–D13 (ATmega32u4-compatible mapping on the shield connector) 14 lines, 3.3 V or 5 V
Arduino-shield analog inputs A0–A5 10-bit ADC
Shield logic level IOREF jumper (JP1) on PCB underside 3.3 V or 5 V
Digital input idle state Internal pull-up enabled by firmware at boot Logic HIGH = 1
Default update interval node-red-contrib-iot2000 polling timer 1000 ms (user-configurable)

The IOREF jumper is the first thing to verify. If the jumper is set to 5 V and the field wiring only produces 3.3 V, the line may be below the Vil threshold of the IOT2040 shield input and interpreted as indeterminate. With the jumper set to 3.3 V the Vil/Vih thresholds become compatible with 3.3 V CMOS logic and 3.3 V signals from sensors, buttons, or another MCU read cleanly.

Root Cause Analysis

Three independent causes can produce the symptom. Diagnose them in this order:

  1. IOREF jumper is on 5 V. The shield input stage expects 5 V CMOS levels. A 3.3 V drive lands in the indeterminate band; some firmware reads it as a stable 1, others as a stable 0, and the input never transitions cleanly.
  2. Engineer is looking for a rising edge, the firmware only emits a change event. The iot2000-in-digital node polls the input every N ms and emits a message only when current != previous. If you start at 1 and stay at 1, no message is ever sent. A floating input on the IOT2040 also reads as 1 because the internal pull-up is enabled at boot, so "unplugged" is not equivalent to "low".
  3. Pull-up never disabled and contact-to-GND wiring not used. To get a reliable logic 0, the input must be actively driven to GND. Disconnecting the wire simply leaves the pull-up to charge the line back to 3.3 V, which is exactly what the user is observing.
Key insight: The IOT2040 is not a Raspberry Pi. There is no falling/rising-edge selection on the digital input node. The node is a level-polling block. If you need edge semantics, implement them in a function node, a switch node, or a trigger node downstream of the digital input.

Step 1: Verify the IOREF Jumper

Power down the IOT2040 and remove all wiring from the Arduino-shield connector. Flip the unit over and locate the IOREF jumper (labelled JP1 in the hardware manual; the shield connector is on the top side).

  1. Identify the three pads marked 3V3 – 5V – GND.
  2. Place the jumper across the 3V3 centre pad and the corresponding 3.3 V pad. The shield digital I/O will now use 3.3 V logic levels.
  3. Reassemble the unit and power it back on. The shield side is now Vih ≥ 2.0 V, Vil ≤ 0.8 V, which matches 3.3 V CMOS drivers.
The jumper position is read once at boot. Power cycling is mandatory after moving it; simply restarting Node-RED will not reconfigure the shield logic level.

Step 2: Wire the Digital Input Correctly

The internal pull-up on the IOT2040 shield input is enabled by firmware, so the resting level is 3.3 V. To create a HIGH-to-LOW transition you must actively short the input to GND. The recommended wiring is shown below.

 +3.3 V (Arduino shield "IOREF=3V3" pin)
      |
      [ 10 kOhm external pull-up ]  <-- only required if you disable the internal pull-up
      |
      +---- IOT2040 shield D7  ----+   (chosen for clarity; D2..D12 behave identically)
      |
      [ momentary N.O. push button ]
      |
      GND (Arduino shield GND pin)

Operation:

  • Button released: D7 is pulled to 3.3 V through the internal pull-up → payload = 1.
  • Button pressed: D7 is shorted to GND → payload = 0.
  • Node emits a message only on the transition 1→0 (or 0→1, if your button release is what you want to detect).

If you wire a sensor with an open-collector output, wire the output transistor between the shield digital input and GND, and leave the internal pull-up enabled. Most proximity, limit-switch, and opto-isolated sensor outputs are wired exactly this way.

Step 3: Install and Configure node-red-contrib-iot2000

The Node-RED palette on the IOT2040 example image ships without the IOT2000 nodes by default. Install the official package from the Siemens-Support GitHub release so the polling interval can be set explicitly.

 # on the IOT2040 shell, via SSH or serial console
 cd ~/.node-red
 npm install node-red-contrib-iot2000
 node-red-restart

After restart, drag an iot2000 in (digital) node from the palette onto the flow. The relevant configuration parameters are:

Parameter Recommended Notes
Pin D7 (or D2–D12) Avoid D0/D1 if you also use the shield UART; avoid D13 if you need the onboard LED.
Direction Input Set explicitly even though the node only supports input.
Update interval (ms) 100 Lower = lower latency, higher CPU; below 20 ms the Quark is noticeably loaded.
Initial value 1 Matches the pull-up default. Pre-seeds the change detector.
Versions of node-red-contrib-iot2000 published before mid-2018 require a manual inject pulse on a separate trigger node to refresh the value. Always pull the latest release from the Siemens-Support GitHub; older revisions silently drop the 1→0 transition if the interval is set too long.

Step 4: Build the Trigger Flow

The minimum working flow is digital input → switch → function → debug. The switch node filters the transition you care about, and the function node holds your application logic.

 [ iot2000-in D7 ] --> [ switch: payload === 0 ] --> [ function: onPress ] --> [ debug ]
            |
            +-----> [ debug: "raw level" ]   (temporarily, for verification)

Switch node configuration:

  • Property: msg.payload
  • Type: ==
  • Value: 0

This routes only the HIGH-to-LOW transition into the function. The complementary transition (release, 0→1) can be routed to a second function if you need release-edge semantics.

Function Node Code

The Node-RED function node receives a JavaScript object called msg. A minimal press handler looks like this:

// Detect a single button press on the IOT2040 shield digital input
// msg.payload is 0 when the input is shorted to GND, 1 when released.

let pressCount = context.get('pressCount') || 0;
let lastEdge  = context.get('lastEdge')  || Date.now();

if (msg.payload === 0) {
    const now = Date.now();
    const debounceMs = 50;          // match to the polling interval + 10% margin
    if (now - lastEdge > debounceMs) {
        pressCount += 1;
        context.set('pressCount', pressCount);
        context.set('lastEdge', now);

        // Application payload: do something useful here
        msg.payload = { event: 'press', count: pressCount, t: now };
        msg.topic   = 'iot2040/d7';
        return msg;
    } else {
        return null;                 // debounced, swallow
    }
}
return null;                         // ignore releases

The 50 ms debounce matches the 100 ms polling interval and prevents the function from firing twice on a single press if the contact bounces across the sample point. If you set the polling interval to 20 ms, drop the debounce to 25 ms.

Step 5: Verify the Flow

  1. Deploy the flow. The right-hand debug tab should show payload: 1 on the "raw level" branch within one update interval.
  2. Press the push button (or short the input to GND with a jumper lead). The debug tab should show two messages almost simultaneously: payload: 0 on the raw branch and payload: { event: 'press', count: 1, t: ... } on the function branch.
  3. Release the button. The raw branch emits payload: 1; the function branch is silent because of the early return null for releases.
  4. Press the button a second time. count should increment to 2.

If step 2 produces no payload: 0 message, the input is not actually transitioning. Re-check the IOREF jumper and the wiring, and verify the shield pin number matches the configuration in the node (D7 on the connector, D7 in the node config).

SVG: Trigger State Machine

RELEASEDpayload = 1PRESSEDpayload = 0FUNCTION FIREScount += 1press (1→0)release (0→1)edge detected

Edge Cases and Field Pitfalls

Symptom Likely cause Fix
Debug always shows 1, even with wire shorted to GND IOREF jumper on 5 V, signal below Vil threshold Move jumper to 3V3 position and power-cycle
Debug shows 0 the moment the wire is unplugged Input is floating, internal pull-up not loaded yet Re-check Node-RED startup order; the firmware enables pull-ups at boot, before Node-RED starts
Function fires multiple times on a single press Polling interval shorter than contact bounce Increase debounce in the function node to 1.5× the polling interval
Function never fires, but debug shows 0 then 1 Switch node misconfigured (e.g. !== instead of ==) Use switch with type ==, value 0, or implement the comparison in the function
No messages on the debug tab at all Old node-red-contrib-iot2000 revision that requires an external trigger pulse Update the node package from the Siemens-Support GitHub release
Latency is > 1 s Update interval left at default 1000 ms Set Update interval to 100 ms for human-perceptible buttons, 20 ms for fast sensors
Function fires once after deploy, then never again Initial value not pre-seeded to 1; first transition is treated as initial state Set Initial value = 1 in the node config

Alternatives if the Shield Input Is Unavailable

If the IOT2040 Arduino-shield connector is already populated with a stackable shield and you cannot move the IOREF jumper, or if the wire run is too long for the shield input to read reliably, use one of the two on-board RS-232/422/485 ports with an external dry-contact-to-serial converter (e.g. a Moxa ioLogik-style module) and parse the bytes in a function node. Alternatively, install a USB-to-IO bridge on the USB 2.0 port and use the node-red-contrib-modbus or node-red-contrib-opcua nodes; the IOT2040 supports both server and client roles for Modbus TCP and OPC UA out of the box.

Safety and Wiring Notes

  • The Arduino-shield connector on the IOT2040 is not isolated from the gateway's 24 V supply. A field wiring fault that injects mains voltage onto a shield pin will destroy the Quark SoC. Use opto-isolated sensor interfaces when wiring to plant-side contacts.
  • Do not source more than 20 mA from a shield 3.3 V pin. The on-board LDO is shared with the Quark core and will brown-out the CPU if overloaded.
  • Always power-cycle the IOT2040 after changing the IOREF jumper; the firmware samples the level once at boot.
  • Use shielded cable for runs longer than 3 m in electrically noisy cabinets. The shield input is a high-impedance CMOS line and couples capacitively to nearby relay coils and VFD cables.

Document References

FAQ

Why does the IOT2040 digital input read 1 even with nothing connected?

The shield digital inputs power up with their internal pull-up resistors enabled, so the resting level is 3.3 V (logic 1). To get a reliable 0 you must short the input to GND, not simply disconnect the wire.

How do I select 3.3 V or 5 V logic on the IOT2040 shield?

Move the IOREF jumper (JP1) on the underside of the PCB to the 3V3 or 5V position, then power-cycle the unit. The firmware reads the level once at boot; restarting Node-RED alone is not sufficient.

What polling interval should I use for the iot2000-in digital node?

100 ms is a good default for human-operated push buttons. Drop to 20 ms for fast sensor signals, but expect higher CPU load on the Quark x1000. Always debounce the input in the downstream function node with a margin of 1.5× the polling interval.

Can the iot2000-in node detect a rising edge instead of a falling edge?

No. The node only reports the current level and emits a message on change. Implement rising-edge detection with a function node that compares the incoming msg.payload against the previous value stored in context.

My function never fires even though the debug tab shows the level changing. What is wrong?

Most often the switch or function node is filtering on the wrong value. Confirm the switch property is msg.payload with type == and value 0, and that the function returns msg (not null) on the edge you care about. Verify the wiring topology matches the state machine in this article.

Back to blog