From f90a929227aeb6e38e2b7b22a5e7e948b8f8d402 Mon Sep 17 00:00:00 2001 From: Ian Foster Date: Mon, 18 May 2026 17:18:10 -0700 Subject: [PATCH] feat(expander): snapshot INTF / INTCAP at begin() for deep-sleep wake source detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IOExpander::begin() does a 22-byte sequential bulk read of the MCP23017's registers starting at 0x00. That sequence passes through INTFA (0x0E) and INTFB (0x0F) BEFORE INTCAPA (0x10) and INTCAPB (0x11) — and on the MCP, reading INTCAP is what clears the INTF latch as a side effect. So by the time begin() returns, the latch is cleared at the chip; subsequent calls to getInt() return 0 even when a wake-triggering interrupt was actually pending before begin() ran. v11.0.1 added getIntState() which reads INTCAP fresh — but INTCAP can be overwritten by any new interrupt event (capacitive noise, application activity arming new interrupts, etc.) between begin() and the call. For applications that need to reliably determine a deep-sleep wake source, the only correct moment to capture INTF/INTCAP is during begin() itself, before any subsequent transactions can disturb either register. Snapshot the captured INTF and INTCAP into dedicated member variables inside begin(), and expose them via: uint16_t getInterruptFlagsAtBegin(); // INTFA in low byte, INTFB in high byte uint16_t getInterruptCaptureAtBegin(); // INTCAPA in low, INTCAPB in high Callers pair the two to reconstruct rising vs falling edges on each interrupt-enabled pin even after begin() has cleared the live latch: rising = INTF & INTCAP // pins HIGH at trigger = press falling = INTF & ~INTCAP // pins LOW at trigger = release Backward-compatible: no existing API or behavior changes; new public methods and two new private members only. --- src/system/mcpExpander/mcpExpander.cpp | 56 ++++++++++++++++++++++++++ src/system/mcpExpander/mcpExpander.h | 8 ++++ 2 files changed, 64 insertions(+) diff --git a/src/system/mcpExpander/mcpExpander.cpp b/src/system/mcpExpander/mcpExpander.cpp index c0245d35..ed94e9d9 100644 --- a/src/system/mcpExpander/mcpExpander.cpp +++ b/src/system/mcpExpander/mcpExpander.cpp @@ -36,6 +36,20 @@ bool IOExpander::begin(uint8_t _addr) readMCPRegisters(); + // Snapshot the INTF and INTCAP values that we just read into the cache. + // The bulk read in readMCPRegisters() returns registers 0x00..0x15 + // sequentially, so INTFA (0x0E) and INTFB (0x0F) are read BEFORE INTCAPA + // (0x10) and INTCAPB (0x11). On the MCP23017, reading INTCAP is what + // clears the INTF latch as a side effect. So the cache positions for + // INTF (14, 15) and INTCAP (16, 17) currently hold the pre-clear values + // captured at the moment of the most recent interrupt — but those cache + // positions can be overwritten by subsequent reads (e.g. getInt() or + // getIntState()). Save the pre-clear values into dedicated members so + // application code can determine which expander pin caused a wake from + // deep sleep after begin() has cleared the live latch. + _interruptFlagsAtBegin = ((uint16_t)_ioExpanderRegs[15] << 8) | _ioExpanderRegs[14]; + _interruptCaptureAtBegin = ((uint16_t)_ioExpanderRegs[17] << 8) | _ioExpanderRegs[16]; + #ifdef IO_INT_ADDR if (_addr == IO_INT_ADDR) { @@ -244,6 +258,48 @@ uint16_t IOExpander::getIntState() return getINTStateInternal(); } +/** + * @brief getInterruptFlagsAtBegin returns the value of the MCP23017's + * INTFA/INTFB latch as it was at the moment begin() was called. + * + * begin()'s bulk register read passes through INTCAP, which + * clears the INTF latch as a side effect. After begin() returns, + * calling getInt() reads the live INTF register, which is now 0 + * even if a wake-triggering interrupt was pending. This getter + * returns the value that was captured BEFORE that clear happened + * — useful for determining which pin triggered a wake from deep + * sleep on hardware where the MCP is kept powered during sleep + * (e.g. Inkplate 6 / 10). + * + * @return INTFA in low byte, INTFB in high byte + */ +uint16_t IOExpander::getInterruptFlagsAtBegin() +{ + return _interruptFlagsAtBegin; +} + +/** + * @brief getInterruptCaptureAtBegin returns the value of the MCP23017's + * INTCAPA/INTCAPB register as it was at the moment begin() was + * called. + * + * Pairing this with getInterruptFlagsAtBegin() lets callers + * distinguish rising vs falling edges on each interrupt-enabled + * pin even after begin() has cleared the latch. + * + * The library's getIntState() also returns INTCAP but does so + * via a fresh I2C read; that fresh value can be overwritten by + * any subsequent interrupt event (e.g. capacitive noise during + * application startup), whereas this snapshot is fixed at + * begin() time. + * + * @return INTCAPA in low byte, INTCAPB in high byte + */ +uint16_t IOExpander::getInterruptCaptureAtBegin() +{ + return _interruptCaptureAtBegin; +} + /** * @brief setPorts sets states on every IO Expander pin at once. * diff --git a/src/system/mcpExpander/mcpExpander.h b/src/system/mcpExpander/mcpExpander.h index 4d8f6eb4..0dc02b18 100644 --- a/src/system/mcpExpander/mcpExpander.h +++ b/src/system/mcpExpander/mcpExpander.h @@ -94,6 +94,8 @@ class IOExpander void removeIntPin(uint8_t _pin); uint16_t getInt(); uint16_t getIntState(); + uint16_t getInterruptFlagsAtBegin(); + uint16_t getInterruptCaptureAtBegin(); uint16_t getPorts(); void blockPinUsage(uint8_t _pin); void unblockPinUsage(uint8_t _pin); @@ -101,6 +103,12 @@ class IOExpander private: uint16_t _blockedPinsForUser = 0; + // INTF and INTCAP register values captured during begin()'s bulk read, + // BEFORE the same read clears the INTF latch as a side effect of passing + // through INTCAP. Lets callers determine which pin caused a wake from + // deep sleep after begin() has run. + uint16_t _interruptFlagsAtBegin = 0; + uint16_t _interruptCaptureAtBegin = 0; const uint8_t regAddresses[22] = { MCP23017_IODIRA, MCP23017_IODIRB, MCP23017_IPOLA, MCP23017_IPOLB, MCP23017_GPINTENA, MCP23017_GPINTENB,