Skip to content

Buzzer Module (sound system)

The Buzzer module provides audible feedback for the ESP3D-X pendant firmware. Since a14 it is the sound module (main/modules/buzzer/, global esp3d_sound): the rest of the firmware only names what happened (ESP3DSoundEvent), never how it sounds. The module resolves each event to a note list (compiled defaults, ui_resources blob, or a user /esp3dsound.ini), sequences it with a single esp_timer, and drives the hardware through a backend interface. Today’s only backend is a PWM tone on the LEDC buzzer driver; an audio decoder backend can be added later without touching callers.

Authoritative design reference: sound_system.md (API rules, engine, priorities, limits, ini format, blob layout). Design rationale and remaining work: sound_customization_design.md.

Built only when BUZZER_SERVICE is ON (ESP3D_BUZZER_FEATURE); today only pibot_pendant_v1_0 enables it.



hardware/common/drivers/buzzer/

main/modules/buzzer/

Callers

UI macros (esp3d_ui.h)

ESP3D_SELECTION_BEEP …

[ESP250] playTone()

[ESP910] / [ESP401] begin()

settings_screen

enable()

ESP3DSound (esp3d_sound)

events · priorities · limits

mute stack · enabled setting

Definitions

compiled defaults (esp3d_sound_defaults.h)

or ui_resources blob 'sounds'

esp_timer one-shot

note sequencer (esp_timer task, Core 0)

ESP3DSoundBackend

tone backend (esp3d_sound_backend_tone.cpp)

buzzer.c

buzzer_tone_start() / buzzer_tone_stop()

LEDC PWM

board_init()

buzzer_configure(&buzzer_cfg)


Location: hardware/common/drivers/buzzer/ (buzzer.c, buzzer.h, buzzer_config.h)

The driver only starts and stops a PWM tone. Sequencing and timing belong to the sound module. The former blocking buzzer_bip(), the malloc + task based buzzer_play() and buzzer_tone.h were removed.

FunctionDescription
esp_err_t buzzer_configure(const buzzer_config_t *config)Configures the LEDC timer and channel for the buzzer GPIO
esp_err_t buzzer_tone_start(uint16_t freq_hz)Starts a continuous tone; non-blocking, no log on success, safe from an esp_timer callback. 0 silences the buzzer
esp_err_t buzzer_tone_stop(void)Silences the buzzer; non-blocking, no log on success

buzzer_config_t:

FieldMeaning
output_invertInvert the GPIO (active-low buzzer)
gpio_numBuzzer GPIO
timer_idx / channel_idxLEDC timer / channel
freq_hzDefault PWM frequency
resolution_bitsPWM resolution
duty_percentTone duty cycle 1–50 % = volume (50 = loudest, lower = quieter and less supply current); 0 (field omitted) = 50 %, the previous behavior

Location: main/modules/buzzer/

FileRole
esp3d_sound.hPublic API: ESP3DSoundEvent, ESP3DSound, esp3d_sound
esp3d_sound.cppEngine: definitions, limits, priorities, esp_timer sequencer, mute stack, /esp3dsound.ini update
esp3d_sound_defaults.hGenerated (gitignored) from resources/sounds/sounds_default.ini: compiled-in defaults + blob ID
esp3d_sound_backend.hBackend interface (internal)
esp3d_sound_backend_tone.cppTone backend (LEDC buzzer driver)

selection, forward, backward, active_item, back, response_no, error, lock, wakeup, success, startup, unlock (ESP3DSoundEvent, in storage order — new events are always appended).

MethodDescription
begin() / end()Start (idempotent, also reloads the enabled state from settings) / stop
reloadDefinitions()Point each event to its slot in the mapped ui_resources blob, or keep the compiled default
enabled(fromSettings) / enable(on, toSettings)Global on / off (esp3d_buzzer_on setting)
play(event)Non-blocking, any task, best effort: dropped when disabled or when a higher-priority sound is playing
playTone(freq_hz, duration_ms)Raw tone for [ESP250]: 100–5000 Hz, 1–3000 ms; ESP_ERR_INVALID_STATE when disabled or busy, ESP_ERR_INVALID_ARG when out of range
stop()Stop the current sound
muteAndSave() / restoreMute() / resetMute()Counted mute stack (tool change, probe…); does not touch the setting

One static one-shot esp_timer sequences the notes: no task, no queue, no heap allocation at play time. play() stores the event as pending under a spinlock and fires the timer; the timer callback (esp_timer task, Core 0) starts / stops notes through the backend outside the spinlock, with a 10 ms gap between notes. The hardware is only touched from the callback, so there is no concurrent LEDC access. The callback shares the esp_timer task with the LVGL tick timer: no LVGL call, no blocking call, no log on the normal path.

Priority — a new sound preempts the current one if its priority is ≥ the current one, otherwise it is dropped: 0 = forward / backward; 1 = selection, back, active_item, response_no; 2 = error, lock, unlock, wakeup, success, startup, raw tone. An error is never cut by an encoder click, and fast encoder turns restart the click instead of piling up.

Limits (applied at play time) — max notes / total duration per event class, from 1 note / 40 ms (forward, backward) to 16 notes / 3000 ms (startup); frequencies outside 100–5000 Hz play as rests. Full table: sound_system.md “Limits”.


Single source of the default sounds: resources/sounds/sounds_default.ini. generate_resources.py (with tools/build_scripts/sound_defs.py) derives from it both the sounds blob of the ui_resources partition and the compiled-in fallback header.

  • Early boot: esp3d_sound.begin() uses the compiled defaults (partition not mapped yet).
  • UIManager::initialize() calls reloadDefinitions() right after esp3d_resources_init(): events then read the mapped blob (no copy); an absent, invalid or older blob (fewer events) keeps the compiled defaults.

Customization paths (same ini format, partial files allowed):

PathHow
SD card /esp3dsound.iniApplied at boot by the update service (ESP3DUpdateType::sounds): merged into the current definitions, blob rewritten, file renamed .ok / .bad, reboot. A syntax error rejects the whole file
Board overlayboards/<board>/resources/sounds/esp3dsound.ini, merged at build time
RebuildEdit sounds_default.ini and rebuild ui_resources
SD kit (ui_resources_kit)An esp3dsound.ini in the overrides folder is baked into the rebuilt ui_resources.bin

Format (one section per event, notes = freq:duration_ms, …, note names C3..B8 with optional #, 0 = rest, off mutes the event):

[error]
notes = 100:200
[forward]
notes = off

User guide: docs/user documentation/ui_resources_customization.md; example: docs/ui_resources/esp3dsound.ini.example.


main/display/esp3d_ui.h maps the UI macros to events (empty when the feature is off): ESP3D_SELECTION_BEEP → selection, ESP3D_MOVEMENT_FORWARD_BEEP / _BACKWARD_BEEP → forward / backward, ESP3D_ACTIVE_ITEM_BEEP, ESP3D_BACK_BEEP, ESP3D_RESPONSE_NO_BEEP, ESP3D_ERROR_BEEP, ESP3D_LOCK_BEEP, ESP3D_UNLOCK_BEEP, ESP3D_WAKEUP_SOUND, ESP3D_SUCCESS_SOUND, ESP3D_STARTING_SOUND; ESP3D_SAVE_SOUND_STATE_AND_DISABLE / ESP3D_RESTORE_SOUND_STATE → muteAndSave() / restoreMute(). See ui_core.md §3.7.


File: boards/pibot_pendant_v1_0/components/bsp/buzzer_def.h

buzzer_config_t buzzer_cfg = {
.output_invert = !BUZZER_ACTIVE_HIGH_FLAG, // Invert if not active-high
.gpio_num = BUZZER_PIN,
.timer_idx = BUZZER_PWM_TIMER_IDX,
.channel_idx = BUZZER_PWM_CHANNEL_IDX,
.freq_hz = BUZZER_PWM_FREQ_HZ,
.resolution_bits = BUZZER_PWM_RESOLUTION_BITS,
.duty_percent = BUZZER_PWM_DUTY_PERCENT,
};

board_init() passes it to buzzer_configure() when ESP3D_BUZZER_FEATURE is set. PiBot: BUZZER_PWM_DUTY_PERCENT = 15 (lowered because SC8002B current peaks on fast repeated key beeps are suspected of causing BROWNOUT resets, 2026-11-01, not yet confirmed on hardware). The volume is fixed at build time: there is no user volume setting.

The output_invert field handles both active-high and active-low buzzers without changing driver logic: !BUZZER_ACTIVE_HIGH_FLAG inverts the GPIO signal for active-low hardware, keeping the drive waveform correct in either polarity.

Boards without a physical buzzer either leave ESP3D_BUZZER_FEATURE OFF in their CMakeLists, or define BUZZER_PIN as GPIO_NUM_NC. In the latter case the driver’s GPIO_IS_VALID_OUTPUT_GPIO check returns ESP_ERR_INVALID_ARG at configure time.


The factory test application and the custom bootloader use entirely separate, minimalist buzzer implementations. These have no dependency on the main firmware buzzer stack.

Location: boards/*/Factory/main/buzzer.c and buzzer.h

Implemented as a direct GPIO-toggle loop with esp_rom_delay_us. No LEDC, no FreeRTOS tasks.

No

Yes

buzzer_init

gpio_config OUTPUT

gpio_set_level 0

buzzer_beep_short

GPIO_IS_VALID_GPIO?

return

Toggle GPIO at BEEP_FREQ_HZ

for BEEP_DURATION_MS cycles

Boards with no buzzer hardware define BUZZER_PIN as GPIO_NUM_NC; the guard GPIO_IS_VALID_GPIO(BUZZER_PIN) makes both functions safe no-ops on those boards.

Location: boards/pibot_pendant_v1_0/Factory/custom_bootloader/hooks.c

Before the RTOS scheduler starts, the bootloader hooks use gpio_ll_set_level() directly, bypassing the driver layer entirely:

FunctionTone produced
beep_short()2700 Hz, 100 ms
beep_confirm()2700 Hz 150 ms → 100 ms silence → 3200 Hz 150 ms

These functions provide auditory confirmation of button-triggered factory-reset events during the bootloader phase, where no heap, no RTOS, and no LEDC peripheral setup is available.

Bootloader

gpio_ll_set_level

No RTOS, no heap

Factory App

GPIO toggle + esp_rom_delay_us

No LEDC, no tasks

Main Firmware

LEDC PWM + esp_timer sequencer

sound module

The Factory apps keep their own buzzer.c copies, independent of the main firmware driver.


# CMakeLists.txt
OPTION(BUZZER_SERVICE "Buzzer service" OFF)

cmake/features.cmake turns it into -DESP3D_BUZZER_FEATURE=1. Without it, the sound module is not built, the UI macros are empty, board_init() skips buzzer_configure() and no LEDC timer or channel is allocated.


ModuleRelationship
bsp_board_initializationboard_init() calls buzzer_configure() with the board-specific buzzer_cfg
bsp_buzzerBSP view of the driver
ui_coreUI sound macros, reloadDefinitions() call in UIManager::initialize()
esp3d_commands[ESP250] raw tone, [ESP910] enable / disable
update_service/esp3dsound.ini SD update (ESP3DUpdateType::sounds)
tools_resource_generationsounds blob generation
factory_appStandalone buzzer implementations of the factory apps and the custom bootloader