Repository navigation
Conversation
The espressif port had no audiopwmio at all, so boards whose speaker is not wired to a DAC pin had no way to play samples. On the Adafruit FunHouse the piezo is on GPIO42, while the ESP32-S2 DAC is hardwired to GPIO17/GPIO18 (and GPIO18 is the light sensor), so audioio cannot reach it. LEDC has no DMA path for its duty registers, and the S2's I2S has no PDM transmitter (no SOC_I2S_SUPPORTS_PDM_TX), so samples are pushed from a GPTimer alarm ISR running at the sample rate. The ISR drains one of two device buffers; audiopwmout_background(), called from port_background_task(), refills the other from the audiosample protocol. The ISR deliberately touches nothing but its own object's volatile state. It must not queue a background callback: on this port that macro is taskENTER_CRITICAL(), which aborts when called from ISR context. Driving the refill by polling instead also means a buffer the ISR found empty is always picked up again, rather than relying on the ISR to have queued the refill before it stalled. Every gptimer call is checked, so a timer that cannot be configured raises instead of leaving `playing` true forever. alarm_config is static because CircuitPython builds with CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM, under which gptimer_set_alarm_action() rejects a config outside internal RAM. The carrier is APB_CLK / 2**10 = 78.125 kHz, which keeps it out of the audio band while leaving 10 bits of amplitude resolution. The GPTimer runs at 40 MHz (the finest the timer group divider allows), putting the sample-rate error below 0.01% at 8k/11k/22k/44.1k. A single-buffer sample that loops wraps inside the ISR rather than round-tripping through the background task, so looping RawSamples do not click. ledc_ll.h lives in the per-target esp_hal_ledc directory, which was not on the include path, so add it. Enabled for esp32s2 only; the S3 has neither DAC nor audiopwmio and could reuse this later. Costs 2336 bytes of flash and 648 bytes of internal SRAM on adafruit_funhouse, and nothing on boards that leave it off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gptimer_register_event_callbacks() returned ESP_ERR_INVALID_ARG on the FunHouse, so the alarm callback was never installed: the timer ran, no alarm was ever delivered, nothing consumed samples, and play() left `playing` true forever. The driver is built with CONFIG_GPTIMER_ISR_CACHE_SAFE (selected by CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM, which defaults on), so gptimer_register_event_callbacks() requires the callback in IRAM and its user context in internal RAM. The callback was fine; the user context was not. It was `self`, a MicroPython heap object, and port_malloc() takes MALLOC_CAP_SPIRAM first, so on any board with PSRAM the whole object is external. Clearing that check alone would not be enough. A cache-safe ISR can run while the flash cache is disabled, which happens during any flash write, including the user saving code.py, so everything it dereferences has to be in internal RAM. Follow the pattern common-hal/frequencyio already uses: put the ISR-reachable state in an audiopwmio_internal_data_t allocated with MALLOC_CAP_INTERNAL and pass that as the user context. The LEDC channel numbers are copied in too, so the ISR no longer reaches into the pwmio objects. The audio buffers move to heap_caps_malloc(MALLOC_CAP_ INTERNAL) for the same reason, capped so a large sample fails with ESP_ERR_NO_MEM rather than exhausting internal RAM. The refill path still runs in task context and reaches the object as before. Buffers now hold one value per output channel rather than always a stereo pair, which halves the internal RAM a mono output needs. play() asserts esp_ptr_in_iram() for the callback and esp_ptr_internal() for the control block and both buffers, so a regression surfaces there instead of as a fault at a random later moment. Error paths report the underlying esp_err_t through raise_esp_error() instead of a generic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The espressif port has no
audiopwmio, so a board whose speaker is not wired to a DAC pin has no way to play samples at all. On the Adafruit FunHouse the piezo is on GPIO42, while the ESP32-S2 DAC is hardwired to GPIO17/GPIO18 — and GPIO18 is the ambient light sensor — soaudioiocannot reach it.Mechanism
LEDC plus a GPTimer alarm ISR running at the sample rate. The alternatives were checked against the in-tree IDF rather than assumed:
soc/esp32s2/include/soc/soc_caps.hhas noSOC_I2S_SUPPORTS_PDM_TX; PDM TX arrives on the S3.The alarm callback writes the duty registers through
ledc_ll_*, draining one of two buffers while a background callback refills the other from the audiosample protocol — the same shape the port'saudioiouses for its DMA refills.Carrier is
APB_CLK / 2**10= 78.125 kHz with 10 bits of duty, the same resolution choice the RP2040 port makes. The GPTimer runs at 40 MHz, the finest the timer-group divider allows, putting sample-rate error under 0.01% (8000 exact, 22050 -> 22050.7, 44100 -> 44101.4 Hz).ISR state must be in internal RAM
Worth highlighting for review, because it is not obvious and it is why a first attempt failed on hardware: CircuitPython builds with
CONFIG_GPTIMER_ISR_CACHE_SAFE, sogptimer_register_event_callbacks()requires the callback to be in IRAM anduser_datato be in internal RAM. Passingselffails withESP_ERR_INVALID_ARG, becauseport_malloc()takesMALLOC_CAP_SPIRAMfirst and the whole MicroPython heap is external on a PSRAM board.So, following
common-hal/frequencyio, everything the ISR dereferences lives in aMALLOC_CAP_INTERNALcontrol block passed asuser_data— including the sample buffers, which would otherwise come from that same PSRAM heap and fault whenever the cache is disabled during a flash write.play()assertsesp_ptr_in_iram()on the callback andesp_ptr_internal()on the block and both buffers, so a regression surfaces there rather than as a random crash.Verification
Confirmed on an Adafruit FunHouse: a 440 Hz
RawSampletest tone plays, and asynthioMIDI track plays throughPWMAudioOuton the onboard piezo to completion. Tested on that one board only.On a piezo specifically, expect a thin, bass-free sound — it is a resonant capacitive element driven straight off a GPIO with no amplifier. Note also that a 78 kHz carrier draws appreciably more current into that capacitance than the 1-4 kHz tones
pwmionormally produces; raisingPWM_BITSfrom 10 to 11 halves it at a 39 kHz carrier.Size
adafruit_funhouse,CIRCUITPY_AUDIOPWMIO0 vs 1:Scoped to esp32s2; boards leaving it off pay nothing. The S3 has neither DAC nor audiopwmio and could reuse this.
🤖 Generated with Claude Code
-- Edit -- The uf2 with example installable 11-alpha2 + this PR #11663 on top of this PR stack #11662
funhouse_halloween_audiopwmio.zip
From tyeth#39 plus a melody.mid file.
Shows synthio playing a polyphonic midi file on the pwm "speaker" pin via audiopwmio, potentially allowing mixing and control too etc.