ESP32-S3: hard fault at soft reload while I2SOut is playing

Abierto
#11,822 2 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
45/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
c, python

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

audio bug crash esp32-s3 needs retest
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, then gc_deinit(), then port_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's on_sent handler, i2s_event_interrupt(), calls background_callback_add(&self->callback, i2s_callback_fun, self_in). self is inside the I2SOut object, on the Python heap.
  • The channel is only disabled by the I2SOut finaliser, which runs inside gc_deinit(). reset_port() in ports/espressif/supervisor/port.c has 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.

Lenguaje dominante
C
Estrellas
4.6k
Forks
1.4k
Merge medio
1 d 1 h
PR fusionados (30 d)
208

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de adafruit/circuitpython

Todos los issues de adafruit/circuitpython

Issues similares

Más issues de C

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.