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

Aberta
#11,822 2 comentários 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

Ninguém assumiu esta issue ainda.

Avaliação

Dificuldade
4/5
Tempo estimado
3-5 dias
Facilidade para iniciantes
45/100
Tipo de issue
Bug
Clareza
Razoavelmente clara
Status de atividade
Ativa
Stack de tecnologia
c, python

Direção de pesquisa

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.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

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.

Linguagem predominante
C
Estrelas
4.6k
Forks
1.4k
Merge médio
1d 1h
PRs com merge (30d)
208

Preparar o ambiente

Este projeto não oferece contêiner de desenvolvimento, Dockerfile nem guia de contribuição, então a configuração fica por sua conta: comece pelo README e veja nosso guia da primeira contribuição para os passos gerais.

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de adafruit/circuitpython

Todas as issues de adafruit/circuitpython

Issues semelhantes

Mais issues de C

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.