Skip to content

[beken-72xx] Fix digitalRead() returning a stale level inside GPIO ISR (pulse_counter/hlw8012 read 0)#394

Open
blafois wants to merge 1 commit into
libretiny-eu:masterfrom
blafois:fix/bk72xx-isr-digitalread
Open

[beken-72xx] Fix digitalRead() returning a stale level inside GPIO ISR (pulse_counter/hlw8012 read 0)#394
blafois wants to merge 1 commit into
libretiny-eu:masterfrom
blafois:fix/bk72xx-isr-digitalread

Conversation

@blafois

@blafois blafois commented Jul 15, 2026

Copy link
Copy Markdown

Problem

On BK7238 (and likely other BK7231x), calling digitalRead() inside a GPIO interrupt handler can return a stale level. For a fast pulse train the edge interrupt reaches the ISR after the pin has already returned to its idle level (interrupt latency exceeds the pulse high-time), so the register read no longer reflects the edge that fired.

This breaks any consumer that determines the edge direction by reading the pin inside the ISR. The most common case is ESPHome's pulse_counter (and everything built on it — hlw8012, cse7766, …), whose ISR does:

PulseCounterCountMode mode = arg->isr_pin.digital_read() ? arg->rising_edge_mode : arg->falling_edge_mode;

With the default rising_edge_mode = INCREMENT, falling_edge_mode = DISABLE, and digital_read() always returning LOW inside the ISR, every edge is treated as falling and discarded → the counter never increments. On a BK7238 smart plug with a BL0937 energy meter, power/voltage/current all read 0.

Root cause (verified on hardware)

I instrumented this extensively on a BK7238 (LSC/Action plug, BL0937) using a always-present ~4 kHz signal on a GPIO:

  • Polling digitalRead() in a loop: works (reads the toggling level).
  • Manual GPIO INT registers + poll INTSTA: the edge latches correctly.
  • attachInterrupt() RISING / FALLING / CHANGE: the ISR fires correctly (~2 k/s, ~2 k/s, ~4 k/s).
  • But digitalRead() inside the ISR returned LOW on 72124/72124 calls — never the post-edge level.

So the interrupt path is fine; only the in-ISR pin read is unreliable. There is no separate fast GPIO input-data register on this SoC (unlike ESP8266's GPI, whose ISRInternalGPIOPin::digital_read() reads the live pad register directly), so reading “faster” isn’t possible here.

Fix

The ISR already knows which edge fired: data->irqMode holds the armed edge (before it is flipped for CHANGE emulation), which implies the pin's logical level. Cache that level in file-scope state and return it from digitalRead() while the pin's handler is executing. No behavior change outside an ISR.

GPIO interrupts share a single IRQ vector with non-reentrant dispatch (gpio_isr), so the two file-scope variables cannot race across nested handlers.

Before / after (BK7238 + BL0937, same pin/signal)

before after
ESPHome pulse_counter (default mode) 0 pulses/min 120 000 pulses/min
ESPHome hlw8012 power 0 W ~3065 W (live, uncalibrated)
hlw8012 current / voltage 0 A / 0 V ~9.9 A / ~373 V (live)

Notes

On BK7238 (and BK7231x) a GPIO edge interrupt can reach the ISR after the
pin has already returned to its idle level for a fast pulse train (IRQ
latency exceeds the pulse high-time). digitalRead() inside the interrupt
handler therefore returns a stale level.

Consumers that infer the edge direction by reading the pin in the ISR --
e.g. ESPHome's pulse_counter and hlw8012/BL0937 energy metering, which do
`mode = digital_read() ? rising_edge_mode : falling_edge_mode` -- then
misclassify every edge and count nothing. On a BK7238 plug with a BL0937,
power/voltage/current all read 0.

The ISR already knows which edge fired: data->irqMode holds the armed edge
(before it is flipped for CHANGE emulation), which implies the pin's
logical level. Cache that level and return it from digitalRead() while the
pin's handler is running.

Tested on BK7238 (LSC/Action smart plug, BL0937): ESPHome pulse_counter
went from 0 -> 120k pulses/min and hlw8012 from 0 W -> live power/current/
voltage. Single shared GPIO IRQ vector with non-reentrant dispatch, so the
two file-scope state variables cannot race across nested handlers.

Refs: libretiny-eu#393
@blafois

blafois commented Jul 21, 2026

Copy link
Copy Markdown
Author

@kuba2k2 ?

@kuba2k2

kuba2k2 commented Jul 21, 2026

Copy link
Copy Markdown
Member

This is an interesting workaround. However, since we haven't had issues on BK7231x using BL0937, I'd prefer if your changes were scoped to BK7238 specifically, and perhaps could be adapted to other chips later on - we don't want to break something that's working.

A simple #if LT_BK7238 should do the trick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants