Skip to content

Commit 151d2a9

Browse files
committed
fix(irq): restore GPIO interrupts broken by WiFi blob
Three sources of GPIO interrupt loss after WiFi init: 1. ROM ets_isr_mask bypass: WiFi blob clears INTENABLE bits via ROM calls that bypass the OS adapter. Fix: snapshot INTENABLE at the start of schedOnce(), OR it back in espradio_wifi_unmask() so no TinyGo-owned bits (GPIO=bit10 on S3, bit6 on C3) are lost. 2. Interrupt matrix corruption: espradio_set_intr (S3) called intr_matrix_set for every blob-requested source, risking overwrite of the GPIO→CPU-int routing. Fix: make set_intr a no-op for routing (same as C3); prewire in espradio_prewire_wifi_interrupts() only. Restore GPIO source routing in espradio_wifi_unmask() each cycle. 3. Missed edge + stuck PS.INTLEVEL: TinyGo GC's tinygo_scanCurrentStack uses rsil/3 to flush Xtensa register windows; a goroutine yield during the recursive spill leaves PS.INTLEVEL=3 permanently, blocking all level-1 interrupts. Additionally, if a button edge arrived while INTENABLE[10]=0, the Xtensa edge latch missed it (GPIO_STATUS set but INTERRUPT[10]=0). Fix: espradio_wifi_unmask() lowers PS.INTLEVEL to 0 and toggles the GPIO interrupt matrix routing (disconnect/reconnect with memw readback fence) to synthesize a new rising edge so any missed GPIO events are replayed. Fixes #40. Signed-off-by: deadprogram <ron@hybridgroup.com>
1 parent 895d24f commit 151d2a9

4 files changed

Lines changed: 145 additions & 6 deletions

File tree

esp32c3/isr.c

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
* TinyGo registers its handler on this interrupt via interrupt.New(). */
2222
#define ESPRADIO_WIFI_CPU_INT 1u
2323

24+
/* TinyGo routes ETS_GPIO_INTR_SOURCE (16) to this CPU interrupt (cpuInterruptFromPin
25+
* in machine_esp32c3.go). Restored after every schedOnce() so that blob ROM
26+
* calls (e.g. direct intr_matrix_set) cannot permanently steal the GPIO source. */
27+
#define ESPRADIO_GPIO_CPU_INT 6u
28+
2429
/* Pre-wire WiFi peripheral interrupt sources to the WiFi CPU interrupt.
2530
* Must be called before esp_wifi_init so routing is in place before the
2631
* blob enables the peripheral-side interrupts.
@@ -90,6 +95,20 @@ void espradio_wifi_int_raise_priority(void) {
9095
__asm__ volatile ("fence" ::: "memory");
9196
}
9297

98+
/* INTC enable snapshot taken at the start of schedOnce(), before any blob
99+
* code runs. espradio_wifi_unmask() ORs this back so that bits cleared by
100+
* blob OS-adapter ints_off or ROM calls during processing are restored — e.g.
101+
* bit 6 which TinyGo uses for GPIO on ESP32-C3. */
102+
static volatile uint32_t s_intenable_snapshot;
103+
104+
void espradio_snapshot_intenable(void) {
105+
s_intenable_snapshot = ESPRADIO_INTC_ENABLE_REG;
106+
}
107+
108+
/* No-op on RISC-V: PS.INTLEVEL does not exist. */
109+
void espradio_lower_intlevel(void) {
110+
}
111+
93112
/* Called at the end of espradio_call_wifi_isr(). In level-triggered
94113
* mode, mask CPU int 1 via the enable register to prevent re-entry
95114
* if the hardware line is still asserted after the blob ISR ran.
@@ -101,6 +120,24 @@ void espradio_wifi_isr_post_mask(void) {
101120
}
102121

103122
void espradio_wifi_unmask(void) {
104-
ESPRADIO_INTC_ENABLE_REG |= (1u << ESPRADIO_WIFI_CPU_INT);
123+
/* Restore any TinyGo-owned INTC enable bits that blob code may have
124+
* cleared, then ensure the WiFi CPU interrupt is enabled. */
125+
ESPRADIO_INTC_ENABLE_REG |= s_intenable_snapshot | (1u << ESPRADIO_WIFI_CPU_INT);
126+
127+
/* Re-route GPIO source → TinyGo's CPU interrupt in case blob ROM code
128+
* (direct intr_matrix_set calls inside the binary) corrupted it during
129+
* schedOnce() processing. */
130+
intr_matrix_set(0, ETS_GPIO_INTR_SOURCE, ESPRADIO_GPIO_CPU_INT);
131+
132+
/* Re-fire GPIO CPU interrupt if it was registered and its INTC enable bit
133+
* was cleared during schedOnce (blob ets_isr_mask or ints_off). On
134+
* RISC-V, GPIO is level-triggered so toggling the ENABLE bit causes the
135+
* controller to re-sample the level and assert the interrupt if the GPIO
136+
* source is still pending. */
137+
if (s_intenable_snapshot & (1u << ESPRADIO_GPIO_CPU_INT)) {
138+
ESPRADIO_INTC_ENABLE_REG &= ~(1u << ESPRADIO_GPIO_CPU_INT);
139+
__asm__ volatile ("fence" ::: "memory");
140+
ESPRADIO_INTC_ENABLE_REG |= (1u << ESPRADIO_GPIO_CPU_INT);
141+
}
105142
}
106143

esp32s3/isr.c

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@
1616
* WiFi MAC which holds its interrupt line high until acknowledged). */
1717
#define ESPRADIO_WIFI_CPU_INT 12u
1818

19+
/* TinyGo routes ETS_GPIO_INTR_SOURCE (16) to this CPU interrupt (cpuInterruptFromPin
20+
* in machine_esp32s3.go). Restored after every schedOnce() so that blob ROM
21+
* calls (e.g. direct intr_matrix_set) cannot permanently steal the GPIO source. */
22+
#define ESPRADIO_GPIO_CPU_INT 10u
23+
24+
/* Direct volatile pointer to INTERRUPT_CORE0.GPIO_INTERRUPT_PRO_MAP (offset
25+
* 0x40). Writing cpu_int routes ETS_GPIO_INTR_SOURCE to that CPU interrupt.
26+
* Writing 0 disconnects the GPIO source from all CPU interrupts.
27+
* Base 0x600C2000, source 16 → offset 16*4 = 0x40 */
28+
#define ESPRADIO_GPIO_MAP_REG (*(volatile uint32_t *)(0x600C2040u))
29+
1930
/* Pre-wire WiFi peripheral interrupt sources to the WiFi CPU interrupt.
2031
* Must be called before esp_wifi_init so routing is in place before the
2132
* blob enables the peripheral-side interrupts. */
@@ -29,11 +40,20 @@ void espradio_prewire_wifi_interrupts(void) {
2940

3041
extern void espradio_mark_wifi_isr_slot(int32_t n);
3142

32-
/* Route the blob's requested peripheral source to our fixed WiFi CPU interrupt
33-
* and record the blob's requested intr_num as a WiFi ISR slot so that
34-
* espradio_call_wifi_isr() only calls the relevant handlers. */
43+
/* No-op: the blob calls set_intr to route peripheral sources to CPU interrupts,
44+
* but on Xtensa (ESP32-S3) the routing is already configured by
45+
* espradio_prewire_wifi_interrupts(). Letting the blob re-route arbitrary
46+
* sources via intr_matrix_set at arbitrary times is dangerous: if the blob
47+
* passes a source that TinyGo owns (e.g. ETS_GPIO_INTR_SOURCE = 16) we would
48+
* overwrite the GPIO → CPU-int-10 mapping that TinyGo set up, silently routing
49+
* GPIO events into wifiISRHandler and breaking all pin-change interrupts.
50+
* The Rust esp-wifi and the ESP32-C3 path in espradio both use the same no-op
51+
* strategy. Record the blob's requested intr_num as a WiFi ISR slot so that
52+
* espradio_call_wifi_isr() still calls the correct blob handler. */
3553
void espradio_set_intr(int32_t cpu_no, uint32_t intr_source, uint32_t intr_num, int32_t intr_prio) {
36-
intr_matrix_set(0, intr_source, ESPRADIO_WIFI_CPU_INT);
54+
(void)cpu_no;
55+
(void)intr_source;
56+
(void)intr_prio;
3757
espradio_mark_wifi_isr_slot((int32_t)intr_num);
3858
}
3959

@@ -87,6 +107,38 @@ void espradio_wifi_int_raise_priority(void) {
87107
/* nothing to do — Xtensa interrupt priorities are fixed */
88108
}
89109

110+
/* INTENABLE snapshot taken at the start of schedOnce(), before any blob code
111+
* runs. espradio_wifi_unmask() ORs this back into INTENABLE so that bits
112+
* cleared by blob ROM calls (e.g. ets_isr_mask) during processing are
113+
* restored — in particular bit 10 (GPIO) and bit 9 (timer alarm) which the
114+
* WiFi blob may clear when it uses those CPU interrupt numbers internally. */
115+
static volatile uint32_t s_intenable_snapshot;
116+
117+
void espradio_snapshot_intenable(void) {
118+
uint32_t val;
119+
__asm__ volatile ("rsr %0, intenable" : "=r"(val));
120+
s_intenable_snapshot = val;
121+
}
122+
123+
/* Lower PS.INTLEVEL to 0, allowing level-1 (GPIO, WiFi) interrupts to fire.
124+
*
125+
* The TinyGo GC's tinygo_scanCurrentStack does "rsil a4, 3" to flush the
126+
* Xtensa register windows. If a cooperative goroutine yield occurs during
127+
* the recursive window-spill loop, the goroutine is suspended and later
128+
* resumed with PS.INTLEVEL=3 still active — permanently blocking all
129+
* level-1 interrupts (GPIO at CPU int 10, WiFi at CPU int 12) for that
130+
* goroutine until it voluntarily lowers PS.INTLEVEL again.
131+
*
132+
* We call this at the end of espradio_wifi_unmask() (and thus at the end
133+
* of every schedOnce() cycle) to ensure that after blob processing, the
134+
* schedTicker goroutine runs with PS.INTLEVEL=0. */
135+
void espradio_lower_intlevel(void) {
136+
uint32_t ps;
137+
__asm__ volatile ("rsr %0, ps" : "=r"(ps));
138+
ps &= ~0x0Fu; /* clear INTLEVEL bits [3:0] */
139+
__asm__ volatile ("wsr %0, ps; rsync" :: "r"(ps));
140+
}
141+
90142
/* On Xtensa, level-triggered interrupts auto-clear when the peripheral
91143
* de-asserts. We still mask/unmask to prevent re-entry while the
92144
* bottom-half runs. */
@@ -95,5 +147,48 @@ void espradio_wifi_isr_post_mask(void) {
95147
}
96148

97149
void espradio_wifi_unmask(void) {
98-
espradio_ints_on(1u << ESPRADIO_WIFI_CPU_INT);
150+
/* Restore any TinyGo-owned INTENABLE bits that blob code may have cleared
151+
* (e.g. via ROM ets_isr_mask), then ensure the WiFi CPU interrupt is on. */
152+
uint32_t val;
153+
__asm__ volatile ("rsr %0, intenable" : "=r"(val));
154+
val |= s_intenable_snapshot | (1u << ESPRADIO_WIFI_CPU_INT);
155+
__asm__ volatile ("wsr %0, intenable; rsync" :: "r"(val));
156+
157+
/* Re-route GPIO source → TinyGo's CPU interrupt in case blob ROM code
158+
* (direct intr_matrix_set calls inside the binary) corrupted it during
159+
* schedOnce() processing. intr_matrix_set(cpu_no, source, cpu_int). */
160+
intr_matrix_set(0, ETS_GPIO_INTR_SOURCE, ESPRADIO_GPIO_CPU_INT);
161+
162+
/* Force a new rising edge at CPU int 10 by briefly disconnecting the GPIO
163+
* source then reconnecting it.
164+
*
165+
* WHY: CPU interrupt 10 is edge-triggered. On Xtensa LX7 the edge latch
166+
* only captures when INTENABLE[n]=1 at the moment the edge arrives. If
167+
* ets_isr_mask cleared INTENABLE[10] while a button edge was in-flight,
168+
* the edge was missed: GPIO_STATUS is set (peripheral saw the edge) but
169+
* INTERRUPT[10] is 0 (CPU latch did not capture it).
170+
*
171+
* FIX: disconnect GPIO source (matrix output → LOW, edge 1→0 at int 10
172+
* input), read back the register to flush the write pipeline across the
173+
* APB bus, then reconnect (matrix output → HIGH, edge 0→1 captured by
174+
* the latch because INTENABLE[10]=1 now). The readback + memw ensures
175+
* the LOW has propagated before we write HIGH — without it, back-to-back
176+
* writes coalesce in the write buffer and no real LOW pulse is generated.
177+
*
178+
* A spurious trigger when GPIO_STATUS=0 is harmless: the handler reads 0
179+
* and calls no callbacks. */
180+
if (s_intenable_snapshot & (1u << ESPRADIO_GPIO_CPU_INT)) {
181+
ESPRADIO_GPIO_MAP_REG = 0u; /* disconnect → int 10 input LOW */
182+
(void)ESPRADIO_GPIO_MAP_REG; /* read back: flush write pipeline */
183+
__asm__ volatile ("memw" ::: "memory"); /* Xtensa memory-wait fence */
184+
ESPRADIO_GPIO_MAP_REG = ESPRADIO_GPIO_CPU_INT; /* reconnect → rising edge latched */
185+
__asm__ volatile ("memw" ::: "memory");
186+
}
187+
188+
/* Ensure PS.INTLEVEL=0 so pending level-1 interrupts (GPIO, WiFi) can
189+
* actually be taken by the CPU. The TinyGo GC's tinygo_scanCurrentStack
190+
* uses "rsil 3" and a goroutine yield during the window-spill loop can
191+
* leave the schedTicker goroutine permanently at INTLEVEL=3, silencing
192+
* all level-1 interrupts until explicitly lowered here. */
193+
espradio_lower_intlevel();
99194
}

espradio.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ void espradio_prewire_wifi_interrupts(void);
2626
void espradio_wifi_int_to_level(void);
2727
void espradio_wifi_int_raise_priority(void);
2828
void espradio_wifi_unmask(void);
29+
void espradio_snapshot_intenable(void);
30+
void espradio_lower_intlevel(void);
2931
void espradio_ints_on(uint32_t mask);
3032
void espradio_ints_off(uint32_t mask);
3133
int32_t espradio_queue_send(void *queue, void *item, uint32_t block_time_tick);

radio.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ func startSchedTicker() {
116116
var wifiInitDone uint32
117117

118118
func schedOnce() {
119+
// Snapshot INTENABLE before any blob code runs so that wifi_unmask can
120+
// restore TinyGo-owned bits (e.g. GPIO at bit 10 on ESP32-S3) that the
121+
// blob may clear via ROM calls (ets_isr_mask) bypassing the OS adapter.
122+
C.espradio_snapshot_intenable()
123+
119124
// Mask WiFi CPU interrupt before the ISR softcall. On Xtensa (ESP32-S3)
120125
// the WiFi interrupt is level-triggered at level 1. If the MAC asserts
121126
// its interrupt while we're already iterating the ISR handlers below,

0 commit comments

Comments
 (0)