Skip to content

runtime/esp32c3, esp32c6, esp32h2: fix timer alarm handler registration - #6052

Merged
deadprogram merged 2 commits into
tinygo-org:devfrom
gitslav:fix/esp32c3-timer-interrupt
Oct 27, 2026
Merged

deadprogram merged 2 commits into
tinygo-org:devfrom
gitslav:fix/esp32c3-timer-interrupt

Conversation

@gitslav

@gitslav gitslav commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

This registers the esp32c3 TIMG0 alarm handler the way esp32 and esp32s3 already do:

timerAlarmInterrupt = interrupt.New(timerAlarmCPUInterrupt, timerAlarmHandler)
timerAlarmInterrupt.Enable()
and
timerAlarmInterrupt = interrupt.New(timerAlarmCPUInterrupt, timerAlarmHandler)
timerAlarmInterrupt.Enable()

Currently esp32c3 passes a closure and discards the handle. The compiler removes interrupt handles that are never enabled, so the handler is gone and the alarm interrupt is never cleared.

The esp32c3 Enable() no longer uses defer, like esp32s3. The runtime calls it during startup before any goroutine exists, and the defer setup panics there on the nil current task.

You can see the missing handler in any esp32c3 build. Currently handleInterrupt never writes TIMG0.INT_CLR_TIMERS. With this PR it does. On hardware this is mostly silent because sleeps still end through the ticks() check. Espressif's esp-emulator keeps raising the pending interrupt, so there the espradio WiFi example on esp32c3 runs its WiFi ISR about 1.7 million times and never connects. With this PR it connects.

The handler was passed as a closure and the handle was discarded, so the
compiler removed it and the alarm interrupt was never cleared. Register
a named handler and call Enable, as esp32 and esp32s3 do. Enable no
longer uses defer, because the runtime calls it during startup before
any goroutine exists.
@deadprogram

Copy link
Copy Markdown
Member

Thanks for tracking this down @gitslav. I tested this PR on a XIAO ESP32-C3 and here are some notes edited from an automated review:

  1. esp32c6 and esp32h2 have the same problem in runtime_esp32xx_plic.go. The handler there is also a discarded closure, and a xiao-esp32c6 build has no write to TIMG0.INT_CLR_TIMERS. The esp32c6 Enable() also uses defer. Perhaps fix those here too, or in a follow up?

  2. Now that Enable() runs, the manual block after it in initTimerInterrupt mostly repeats it (clear pending, edge type, enable). The only difference is priority 10 instead of 5. Perhaps trim that block down to just the priority write, like esp32s3 which only calls Enable()?

What do you think?

@deadprogram deadprogram added this to the 0.44 milestone Oct 27, 2026
@deadprogram

Copy link
Copy Markdown
Member

Same fix as for esp32c3. The handler was passed as a closure and the
handle was discarded, so the alarm interrupt was never cleared. Register
a named handler and call Enable, and Enable no longer uses defer.

On all three chips, keep only the priority write after Enable, since
Enable already clears, sets edge type and enables the line.
@gitslav

gitslav commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

@deadprogram thanks for testing. Both are done in the second commit. Also the binary size is updated.

esp32c6 and esp32h2 now register a named handler and call Enable, their Enable no longer uses defer. initTimerInterrupt on all three chips is now Enable plus the priority write. I kept priority 10 so the timer stays above peripherals.

I have no C6 or H2 hardware, so a run on real boards would be welcome for a quick sanity check.

@gitslav gitslav changed the title runtime/esp32c3: register the timer alarm handler as esp32s3 does runtime/esp32c3, esp32c6, esp32h2: fix timer alarm handler registration Oct 27, 2026

@deadprogram deadprogram left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gitslav thanks for the update. I tested the second commit on a XIAO ESP32-C6 and a XIAO ESP32-C3.

On both, the timer handler now ends up in the binary with the write to TIMG0.INT_CLR_TIMERS. 100ms sleeps measure about 100.05ms, same as dev. Three goroutines sleeping 30, 70 and 250ms plus a 1s ticker all stay on time. USB serial still works both ways, which also goes through the changed Enable(). The espradio scan example still works on the C3.

I could not test WiFi on the C6, and I have no H2.

This seems good to me. Thank you!

@deadprogram

Copy link
Copy Markdown
Member

Now merging thanks @gitslav

@deadprogram
deadprogram merged commit 06778d8 into tinygo-org:dev Oct 27, 2026
31 checks passed
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