ESP32-S3: hard fault at soft reload while I2SOut is playing
Maintainer antworten meist innerhalb von 1 Tag
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Anfängerfreundlichkeit
- 45/100
- Issue-Typ
- Bug
- Klarheit
- Größtenteils klar
- Aktivitätsstatus
- Aktiv
- Bereich
- audio-video-rtc, embedded-iot
Rechercherichtung
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.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
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.
- Vorherrschende Sprache
- C
- Sterne
- 4.6k
- Forks
- 1.4k
- Ø Merge
- 1 T. 1 Std.
- Gemergte PRs (30 T.)
- 208
Entwicklungsumgebung
Dieses Projekt bietet weder Dev-Container noch Dockerfile noch Beitragsleitfaden – die Einrichtung liegt bei Ihnen. Beginnen Sie mit der README; die allgemeinen Schritte stehen in unserem Leitfaden für den ersten Beitrag.
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus adafruit/circuitpython
-
SPIDevice selects an active-high device as soon as it is createdEvtl. vergeben @mikeysklar hat das heute übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 73/100
adafruit/circuitpython#11869 ·
Maintainer antworten meist innerhalb von 1 Tag
-
documentation rp2
Schwierigkeit 1/5 1-3 Stunden Anfängerfreundlichkeit 67/100
adafruit/circuitpython#11779 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
documentation enhancement
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 67/100
adafruit/circuitpython#11689 ·
Maintainer antworten meist innerhalb von 1 Tag
-
enhancement network
Schwierigkeit 4/5 3-5 Tage Anfängerfreundlichkeit 48/100
adafruit/circuitpython#11845 ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug
Schwierigkeit 5/5 Über eine Woche Anfängerfreundlichkeit 25/100
adafruit/circuitpython#11834 ·
Maintainer antworten meist innerhalb von 1 Tag
Alle Issues in adafruit/circuitpython
Ähnliche Issues
-
bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 85/100
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 78/100
gnif/LookingGlass#1352 ·
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 85/100
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 62/100
bazel-contrib/rules_go#4784 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag