Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions esp32c3/isr.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
* TinyGo registers its handler on this interrupt via interrupt.New(). */
#define ESPRADIO_WIFI_CPU_INT 1u

/* TinyGo routes ETS_GPIO_INTR_SOURCE (16) to this CPU interrupt (cpuInterruptFromPin
* in machine_esp32c3.go). Restored after every schedOnce() so that blob ROM
* calls (e.g. direct intr_matrix_set) cannot permanently steal the GPIO source. */
#define ESPRADIO_GPIO_CPU_INT 6u

/* Pre-wire WiFi peripheral interrupt sources to the WiFi CPU interrupt.
* Must be called before esp_wifi_init so routing is in place before the
* blob enables the peripheral-side interrupts.
Expand All @@ -31,16 +36,19 @@ void espradio_prewire_wifi_interrupts(void) {
intr_matrix_set(0, ETS_WIFI_PWR_INTR_SOURCE, ESPRADIO_WIFI_CPU_INT);
}

extern void espradio_mark_wifi_isr_slot(int32_t n);

/* No-op: the blob calls set_intr to route peripheral sources to CPU
* interrupts, but on RISC-V (ESP32-C3) the routing is already configured
* by espradio_prewire_wifi_interrupts(). Letting the blob call
* intr_matrix_set at arbitrary times interferes with TinyGo's interrupt
* controller state. The Rust esp-wifi does the same (no-op set_intr). */
* controller state. The Rust esp-wifi does the same (no-op set_intr).
* Record the blob's requested intr_num as a WiFi ISR slot. */
void espradio_set_intr(int32_t cpu_no, uint32_t intr_source, uint32_t intr_num, int32_t intr_prio) {
(void)cpu_no;
(void)intr_source;
(void)intr_num;
(void)intr_prio;
espradio_mark_wifi_isr_slot((int32_t)intr_num);
}

/* No-op: the Rust esp-wifi also no-ops clear_intr. */
Expand Down Expand Up @@ -87,6 +95,20 @@ void espradio_wifi_int_raise_priority(void) {
__asm__ volatile ("fence" ::: "memory");
}

/* INTC enable snapshot taken at the start of schedOnce(), before any blob
* code runs. espradio_wifi_unmask() ORs this back so that bits cleared by
* blob OS-adapter ints_off or ROM calls during processing are restored — e.g.
* bit 6 which TinyGo uses for GPIO on ESP32-C3. */
static volatile uint32_t s_intenable_snapshot;

void espradio_snapshot_intenable(void) {
s_intenable_snapshot = ESPRADIO_INTC_ENABLE_REG;
}

/* No-op on RISC-V: PS.INTLEVEL does not exist. */
void espradio_lower_intlevel(void) {
}

/* Called at the end of espradio_call_wifi_isr(). In level-triggered
* mode, mask CPU int 1 via the enable register to prevent re-entry
* if the hardware line is still asserted after the blob ISR ran.
Expand All @@ -98,6 +120,24 @@ void espradio_wifi_isr_post_mask(void) {
}

void espradio_wifi_unmask(void) {
ESPRADIO_INTC_ENABLE_REG |= (1u << ESPRADIO_WIFI_CPU_INT);
/* Restore any TinyGo-owned INTC enable bits that blob code may have
* cleared, then ensure the WiFi CPU interrupt is enabled. */
ESPRADIO_INTC_ENABLE_REG |= s_intenable_snapshot | (1u << ESPRADIO_WIFI_CPU_INT);

/* Re-route GPIO source → TinyGo's CPU interrupt in case blob ROM code
* (direct intr_matrix_set calls inside the binary) corrupted it during
* schedOnce() processing. */
intr_matrix_set(0, ETS_GPIO_INTR_SOURCE, ESPRADIO_GPIO_CPU_INT);

/* Re-fire GPIO CPU interrupt if it was registered and its INTC enable bit
* was cleared during schedOnce (blob ets_isr_mask or ints_off). On
* RISC-V, GPIO is level-triggered so toggling the ENABLE bit causes the
* controller to re-sample the level and assert the interrupt if the GPIO
* source is still pending. */
if (s_intenable_snapshot & (1u << ESPRADIO_GPIO_CPU_INT)) {
ESPRADIO_INTC_ENABLE_REG &= ~(1u << ESPRADIO_GPIO_CPU_INT);
__asm__ volatile ("fence" ::: "memory");
ESPRADIO_INTC_ENABLE_REG |= (1u << ESPRADIO_GPIO_CPU_INT);
}
}

133 changes: 121 additions & 12 deletions esp32s3/isr.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@
* WiFi MAC which holds its interrupt line high until acknowledged). */
#define ESPRADIO_WIFI_CPU_INT 12u

/* TinyGo routes ETS_GPIO_INTR_SOURCE (16) to this CPU interrupt (cpuInterruptFromPin
* in machine_esp32s3.go). Restored after every schedOnce() so that blob ROM
* calls (e.g. direct intr_matrix_set) cannot permanently steal the GPIO source. */
#define ESPRADIO_GPIO_CPU_INT 10u

/* Direct volatile pointer to INTERRUPT_CORE0.GPIO_INTERRUPT_PRO_MAP (offset
* 0x40). Writing cpu_int routes ETS_GPIO_INTR_SOURCE to that CPU interrupt.
* Writing 0 disconnects the GPIO source from all CPU interrupts.
* Base 0x600C2000, source 16 → offset 16*4 = 0x40 */
#define ESPRADIO_GPIO_MAP_REG (*(volatile uint32_t *)(0x600C2040u))

/* Pre-wire WiFi peripheral interrupt sources to the WiFi CPU interrupt.
* Must be called before esp_wifi_init so routing is in place before the
* blob enables the peripheral-side interrupts. */
Expand All @@ -27,11 +38,23 @@ void espradio_prewire_wifi_interrupts(void) {
intr_matrix_set(0, ETS_WIFI_BB_INTR_SOURCE, ESPRADIO_WIFI_CPU_INT); /* src 3 */
}

/* No-op: the blob calls set_intr to route peripheral sources to CPU
* interrupts, but routing is already configured by
* espradio_prewire_wifi_interrupts(). */
extern void espradio_mark_wifi_isr_slot(int32_t n);

/* No-op: the blob calls set_intr to route peripheral sources to CPU interrupts,
* but on Xtensa (ESP32-S3) the routing is already configured by
* espradio_prewire_wifi_interrupts(). Letting the blob re-route arbitrary
* sources via intr_matrix_set at arbitrary times is dangerous: if the blob
* passes a source that TinyGo owns (e.g. ETS_GPIO_INTR_SOURCE = 16) we would
* overwrite the GPIO → CPU-int-10 mapping that TinyGo set up, silently routing
* GPIO events into wifiISRHandler and breaking all pin-change interrupts.
* The Rust esp-wifi and the ESP32-C3 path in espradio both use the same no-op
* strategy. Record the blob's requested intr_num as a WiFi ISR slot so that
* espradio_call_wifi_isr() still calls the correct blob handler. */
void espradio_set_intr(int32_t cpu_no, uint32_t intr_source, uint32_t intr_num, int32_t intr_prio) {
intr_matrix_set(0, intr_source, ESPRADIO_WIFI_CPU_INT);
(void)cpu_no;
(void)intr_source;
(void)intr_prio;
espradio_mark_wifi_isr_slot((int32_t)intr_num);
}

/* No-op: same as set_intr. */
Expand All @@ -40,22 +63,33 @@ void espradio_clear_intr(uint32_t intr_source, uint32_t intr_num) {
(void)intr_num;
}

/* Enable CPU interrupts using Xtensa INTENABLE special register.
* The blob calls ints_on with its own mask (1<<0 for WiFi MAC).
* We translate: if the blob's mask includes a WiFi-related bit,
* also set our actual WiFi CPU interrupt bit. */
/* Enable/disable CPU interrupts using Xtensa INTENABLE special register.
*
* IMPORTANT: We deliberately ignore the blob's mask and only operate on
* ESPRADIO_WIFI_CPU_INT (bit 12). The blob passes its own original CPU
* interrupt number (e.g. 0 or 1), which may coincide with CPU interrupts
* that TinyGo has allocated for other purposes (bit 10 = GPIO, bit 9 =
* timer alarm). If we forwarded the blob's mask, espradio_ints_off would
* clear those bits from INTENABLE, permanently disabling user interrupts
* such as GPIO PinFalling callbacks (issue #40).
*
* The blob calls these for its own critical-section protection. Since all
* blob ISR handlers run in goroutine context (never from real ISR context),
* the blob's critical sections are already serialised by TinyGo's
* cooperative scheduler; ignoring the mask is safe. */
void espradio_ints_on(uint32_t mask) {
uint32_t actual_mask = mask | (1u << ESPRADIO_WIFI_CPU_INT);
(void)mask;
uint32_t val;
__asm__ volatile ("rsr %0, intenable" : "=r"(val));
val |= actual_mask;
val |= (1u << ESPRADIO_WIFI_CPU_INT);
__asm__ volatile ("wsr %0, intenable; rsync" :: "r"(val));
}

void espradio_ints_off(uint32_t mask) {
(void)mask;
uint32_t val;
__asm__ volatile ("rsr %0, intenable" : "=r"(val));
val &= ~mask;
val &= ~(1u << ESPRADIO_WIFI_CPU_INT);
__asm__ volatile ("wsr %0, intenable; rsync" :: "r"(val));
}

Expand All @@ -73,6 +107,38 @@ void espradio_wifi_int_raise_priority(void) {
/* nothing to do — Xtensa interrupt priorities are fixed */
}

/* INTENABLE snapshot taken at the start of schedOnce(), before any blob code
* runs. espradio_wifi_unmask() ORs this back into INTENABLE so that bits
* cleared by blob ROM calls (e.g. ets_isr_mask) during processing are
* restored — in particular bit 10 (GPIO) and bit 9 (timer alarm) which the
* WiFi blob may clear when it uses those CPU interrupt numbers internally. */
static volatile uint32_t s_intenable_snapshot;

void espradio_snapshot_intenable(void) {
uint32_t val;
__asm__ volatile ("rsr %0, intenable" : "=r"(val));
s_intenable_snapshot = val;
}

/* Lower PS.INTLEVEL to 0, allowing level-1 (GPIO, WiFi) interrupts to fire.
*
* The TinyGo GC's tinygo_scanCurrentStack does "rsil a4, 3" to flush the
* Xtensa register windows. If a cooperative goroutine yield occurs during
* the recursive window-spill loop, the goroutine is suspended and later
* resumed with PS.INTLEVEL=3 still active — permanently blocking all
* level-1 interrupts (GPIO at CPU int 10, WiFi at CPU int 12) for that
* goroutine until it voluntarily lowers PS.INTLEVEL again.
*
* We call this at the end of espradio_wifi_unmask() (and thus at the end
* of every schedOnce() cycle) to ensure that after blob processing, the
* schedTicker goroutine runs with PS.INTLEVEL=0. */
void espradio_lower_intlevel(void) {
uint32_t ps;
__asm__ volatile ("rsr %0, ps" : "=r"(ps));
ps &= ~0x0Fu; /* clear INTLEVEL bits [3:0] */
__asm__ volatile ("wsr %0, ps; rsync" :: "r"(ps));
}

/* On Xtensa, level-triggered interrupts auto-clear when the peripheral
* de-asserts. We still mask/unmask to prevent re-entry while the
* bottom-half runs. */
Expand All @@ -81,5 +147,48 @@ void espradio_wifi_isr_post_mask(void) {
}

void espradio_wifi_unmask(void) {
espradio_ints_on(1u << ESPRADIO_WIFI_CPU_INT);
/* Restore any TinyGo-owned INTENABLE bits that blob code may have cleared
* (e.g. via ROM ets_isr_mask), then ensure the WiFi CPU interrupt is on. */
uint32_t val;
__asm__ volatile ("rsr %0, intenable" : "=r"(val));
val |= s_intenable_snapshot | (1u << ESPRADIO_WIFI_CPU_INT);
__asm__ volatile ("wsr %0, intenable; rsync" :: "r"(val));

/* Re-route GPIO source → TinyGo's CPU interrupt in case blob ROM code
* (direct intr_matrix_set calls inside the binary) corrupted it during
* schedOnce() processing. intr_matrix_set(cpu_no, source, cpu_int). */
intr_matrix_set(0, ETS_GPIO_INTR_SOURCE, ESPRADIO_GPIO_CPU_INT);

/* Force a new rising edge at CPU int 10 by briefly disconnecting the GPIO
* source then reconnecting it.
*
* WHY: CPU interrupt 10 is edge-triggered. On Xtensa LX7 the edge latch
* only captures when INTENABLE[n]=1 at the moment the edge arrives. If
* ets_isr_mask cleared INTENABLE[10] while a button edge was in-flight,
* the edge was missed: GPIO_STATUS is set (peripheral saw the edge) but
* INTERRUPT[10] is 0 (CPU latch did not capture it).
*
* FIX: disconnect GPIO source (matrix output → LOW, edge 1→0 at int 10
* input), read back the register to flush the write pipeline across the
* APB bus, then reconnect (matrix output → HIGH, edge 0→1 captured by
* the latch because INTENABLE[10]=1 now). The readback + memw ensures
* the LOW has propagated before we write HIGH — without it, back-to-back
* writes coalesce in the write buffer and no real LOW pulse is generated.
*
* A spurious trigger when GPIO_STATUS=0 is harmless: the handler reads 0
* and calls no callbacks. */
if (s_intenable_snapshot & (1u << ESPRADIO_GPIO_CPU_INT)) {
ESPRADIO_GPIO_MAP_REG = 0u; /* disconnect → int 10 input LOW */
(void)ESPRADIO_GPIO_MAP_REG; /* read back: flush write pipeline */
__asm__ volatile ("memw" ::: "memory"); /* Xtensa memory-wait fence */
ESPRADIO_GPIO_MAP_REG = ESPRADIO_GPIO_CPU_INT; /* reconnect → rising edge latched */
__asm__ volatile ("memw" ::: "memory");
}

/* Ensure PS.INTLEVEL=0 so pending level-1 interrupts (GPIO, WiFi) can
* actually be taken by the CPU. The TinyGo GC's tinygo_scanCurrentStack
* uses "rsil 3" and a goroutine yield during the window-spill loop can
* leave the schedTicker goroutine permanently at INTLEVEL=3, silencing
* all level-1 interrupts until explicitly lowered here. */
espradio_lower_intlevel();
}
3 changes: 3 additions & 0 deletions espradio.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ void espradio_ensure_osi_ptr(void);
void espradio_coex_adapter_init(void);
void espradio_call_saved_isr(int32_t n);
void espradio_call_wifi_isr(void);
void espradio_mark_wifi_isr_slot(int32_t n);
uint32_t espradio_get_wifi_isr_count(void);
void espradio_prewire_wifi_interrupts(void);
void espradio_wifi_int_to_level(void);
void espradio_wifi_int_raise_priority(void);
void espradio_wifi_unmask(void);
void espradio_snapshot_intenable(void);
void espradio_lower_intlevel(void);
void espradio_ints_on(uint32_t mask);
void espradio_ints_off(uint32_t mask);
int32_t espradio_queue_send(void *queue, void *item, uint32_t block_time_tick);
Expand Down
19 changes: 17 additions & 2 deletions isr.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ void espradio_user_exception(uint32_t cause, uint32_t epc, uint32_t excvaddr, ui
static void (*s_isr_fn[32])(void *);
static void *s_isr_arg[32];

/* Bitmask of ISR slots registered via espradio_set_intr (WiFi sources only). */
static uint32_t s_wifi_isr_slots;

void espradio_mark_wifi_isr_slot(int32_t n) {
if (n >= 0 && n < 32) {
s_wifi_isr_slots |= (1u << n);
}
}

void espradio_set_isr(int32_t n, void *f, void *arg) {
if (n >= 0 && n < 32) {
s_isr_fn[n] = (void (*)(void *))f;
Expand All @@ -98,8 +107,14 @@ void espradio_call_wifi_isr(void) {
s_wifi_isr_count++;
s_in_isr = 1;
ESPRADIO_MEMORY_BARRIER();
// CALL ALL ISRs from 0 to 31 just in case, to see if they are set!
for (int i = 0; i < 32; i++) {
/* Only call ISR slots that were registered via espradio_set_intr for a
* WiFi peripheral source. Calling all 32 slots risks invoking blob
* handlers at slot numbers that coincide with TinyGo's GPIO or timer
* CPU interrupts, which can corrupt INTENABLE. */
uint32_t slots = s_wifi_isr_slots;
while (slots) {
int i = __builtin_ctz(slots);
slots &= slots - 1;
if (s_isr_fn[i]) {
s_isr_fn[i](s_isr_arg[i]);
}
Expand Down
5 changes: 5 additions & 0 deletions radio.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ func startSchedTicker() {
var wifiInitDone uint32

func schedOnce() {
// Snapshot INTENABLE before any blob code runs so that wifi_unmask can
// restore TinyGo-owned bits (e.g. GPIO at bit 10 on ESP32-S3) that the
// blob may clear via ROM calls (ets_isr_mask) bypassing the OS adapter.
C.espradio_snapshot_intenable()

// Mask WiFi CPU interrupt before the ISR softcall. On Xtensa (ESP32-S3)
// the WiFi interrupt is level-triggered at level 1. If the MAC asserts
// its interrupt while we're already iterating the ISR handlers below,
Expand Down
Loading