ESP32-S3: hard fault at soft reload while I2SOut is playing
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 45/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Ambito
- audio-video-rtc, embedded-iot
Direzione di ricerca
Start with main.c's stop_mp() and the ESP32 audiobusio implementation in ports/espressif/common-hal/audiobusio/__init__.c, especially i2s_event_interrupt() and I2SOut finalization. Check how callbacks are unlinked while I2S interrupts can still fire during teardown. Reproduce with the provided reload script, then verify a fix by repeating reloads with a playing stream and confirming the hard fault no longer occurs.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
CircuitPython version and board name
Adafruit CircuitPython 10.2.1 on 2026-05-13; Seeed Xiao ESP32-S3 Sense with ESP32S3
Code/REPL
# Wi-Fi comes from settings.toml. A MAX98357A was on the I2S pins in these tests; I2S is output only,
# so none should be needed. The program reloads itself until the board lands in safe mode.
import array
import math
import random
import time
import audiobusio
import audiomixer
import board
import supervisor
import synthio
SAMPLE_RATE = 88_200
USE_MDNS = True
USE_SOCKET = True
FIX = False # True: stop the stream before the VM is torn down, and the fault goes away
sine = array.array("h", [int(math.sin(2 * math.pi * i / 64) * 32767) for i in range(64)])
audio = audiobusio.I2SOut(bit_clock=board.D9, word_select=board.D8, data=board.D10)
mixer = audiomixer.Mixer(voice_count=1, channel_count=1, bits_per_sample=16, samples_signed=True,
sample_rate=SAMPLE_RATE, buffer_size=2048)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE, waveform=sine)
audio.play(mixer)
mixer.voice[0].play(synth)
if FIX:
import atexit
atexit.register(audio.stop)
if USE_MDNS:
import mdns
import wifi
server = mdns.Server(wifi.radio)
server.hostname = "repro"
server.advertise_service(service_type="_http", protocol="_tcp", port=80)
if USE_SOCKET:
import socketpool
import wifi
pool = socketpool.SocketPool(wifi.radio)
listener = pool.socket(pool.AF_INET, pool.SOCK_STREAM)
listener.setsockopt(pool.SOL_SOCKET, pool.SO_REUSEADDR, 1)
listener.bind(("0.0.0.0", 80))
listener.listen(2)
print("repro: start")
time.sleep(0.3 + random.random() * 0.3)
supervisor.reload()
Behavior
After some number of reloads the board drops off USB and comes back in safe mode:
Running in safe mode! Not running saved code.
You are in safe mode because:
CircuitPython core code crashed hard. Whoops!
Hard fault: memory access or instruction error.
supervisor.runtime.safe_mode_reason is HARD_FAULT and microcontroller.cpu.reset_reason is SOFTWARE.
How soon varies a lot. With the program above, one run faulted four times in a row within two starts of each reset; the next run faulted twice in 114 starts. With FIX = True it ran 151 starts without a fault.
Description
I met this in a small web server that keeps an I2SOut stream open (a mixer with a synthio voice, playing silence between notes) next to an mDNS server, a listening socket and the camera. Saving a file to CIRCUITPY, or Ctrl-C then Ctrl-D, sometimes ended in safe mode instead of a reload.
Measured on one board, reloading by Ctrl-C and Ctrl-D over the serial console, stream at 22,050 Hz:
| Program | Hard faults / reloads |
|---|---|
| The server as written | 10 / 95 |
| The same server without any audio objects | 0 / 60 |
The same server with atexit.register(audio.stop) |
0 / 120 |
With self-reloading test programs at 88.2 kHz the fault also appeared with fewer parts, at lower rates: once in 151 starts with only the audio objects and a list of 20,000 small bytearrays (no Wi-Fi objects), once in 70 with audio plus the camera, once in 122 with audio plus a socket. Across every run with the stream stopped at exit: 0 faults in 446 reloads.
So the fault needs a playing I2SOut at the moment the VM is torn down, and stopping the stream first avoids it. atexit.register(audio.stop) is a workable workaround.
Additional information
I have no backtrace: the UART TX pin is wired to a motor driver on my board. What follows is my reading of the 10.2.1 source, which fits the measurements but is not confirmed.
main.c,stop_mp():background_callback_reset()runs first, thengc_deinit(), thenport_free(_heap). The reset's own comment says heap-allocated callbacks must be unlinked because the heap is about to disappear.ports/espressif/common-hal/audiobusio/__init__.c: the channel'son_senthandler,i2s_event_interrupt(), callsbackground_callback_add(&self->callback, i2s_callback_fun, self_in).selfis inside the I2SOut object, on the Python heap.- The channel is only disabled by the I2SOut finaliser, which runs inside
gc_deinit().reset_port()inports/espressif/supervisor/port.chas no I2S reset.
If the interrupt fires after background_callback_reset() and before the finaliser, a callback node that lives on the heap is back on the list when the heap is freed. The interrupt comes once per DMA buffer (1023 frames: every 46 ms at 22,050 Hz, every 12 ms at 88,200 Hz).
main has the same order in stop_mp() today. I tested 10.2.1 only.
I can run a patched build on this board. If a backtrace would help, tell me the simplest way to capture one.
The bisect and the source reading were done with the help of an AI assistant (Claude). The measurements are from my own board and I can rerun any of them.
- Lingua principale
- C
- Stelle
- 4.6k
- Fork
- 1.4k
- Merge medio
- 1g 1h
- PR unite (30g)
- 208
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di adafruit/circuitpython
-
SPIDevice selects an active-high device as soon as it is createdForse già presa @mikeysklar l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 73/100
adafruit/circuitpython#11869 ·
I maintainer di solito rispondono entro 1 giorno
-
documentation rp2
Difficoltà 1/5 1-3 ore Idoneità per principianti 67/100
adafruit/circuitpython#11779 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
documentation enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
adafruit/circuitpython#11689 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement network
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
adafruit/circuitpython#11845 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 5/5 Più di una settimana Idoneità per principianti 25/100
adafruit/circuitpython#11834 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di adafruit/circuitpython
Issue simili
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 78/100
gnif/LookingGlass#1352 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
bazel-contrib/rules_go#4784 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Bug: settings::init() silently continues when neutralino.config.json is missingForse già presa @mishradwaterlaw l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
neutralinojs/neutralinojs#1882 ·
I maintainer di solito rispondono entro 1 giorno