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
3041extern 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. */
3553void 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
97149void 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}
0 commit comments