RPi Pico WAV Player UI Control Mechanisms: Push Buttons and Headphone Remote Implementation

The RPi Pico WAV Player uses three physical push buttons (Plus, Minus) and a headphone remote control (Center, Plus, Minus) sampled via ADC, processed through a 20 Hz debouncing timer, and dispatched as logical events through a single-element queue.

The elehobica/rpi_pico_wav_player repository implements a dual-input user interface combining GPIO-sampled mechanical switches and an analog headphone remote. Understanding these RPi Pico WAV Player UI control mechanisms requires examining the hardware abstraction layer in src/ui_control.cpp and the event dispatch system that translates physical inputs into logical actions.

Hardware Interface and Pin Mapping

Push Button GPIO Configuration

The board provides two dedicated mechanical push buttons mapped to specific GPIO pins with internal pull-up resistors. In src/ui_control.cpp, the Plus button connects to GPIO 22 (PIN_SW_PLUS), while the Minus button connects to GPIO 20 (PIN_SW_MINUS). Both pins configure as inputs with pull-ups, making them active-low when pressed【2†L22-L34】.

Headphone Remote ADC Sampling

The headphone remote control utilizes a voltage-divider circuit connected to GPIO 26 (PIN_HP_BUTTON), which maps to ADC0 (ADC_PIN_HP_BUTTON). The remote's inline control buttons create distinct voltage levels across the microphone bias resistor. The firmware samples this analog input and converts raw ADC readings to voltage using calibration coefficients, then maps specific voltage windows to logical button states: Center, D, Plus, and Minus【2†L65-L86】.

Software Architecture and Event Processing

Button State Enumerations

The header file src/ui_control.h defines three critical enumerations that abstract the hardware. The button_status_t enum represents raw hardware states: Open, Center, D, Plus, and Minus. The button_unit_t enum distinguishes between PushButtons (physical GPIO switches) and HpButtons (headphone remote). Finally, button_action_t encodes logical user gestures including single clicks, double clicks, triple clicks, long presses (CenterLong), and extended long presses (CenterLongLong)【1†L11-L27】.

Debouncing and Timing Mechanism

The UI layer executes a repeating hardware timer at 20 Hz (TIMER_UI_BUTTON_HZ), triggering every 50 milliseconds via update_button_action(). This routine maintains a 30-sample history buffer (button_prv[NUM_BTN_HISTORY]) to filter contact bounce. The system defines long-press thresholds at 10 counts (500 ms) for LONG_PUSH_COUNT and 30 counts (1.5 seconds) for LONG_LONG_PUSH_COUNT, comparing these against a running repeat counter (button_repeat_count) to distinguish short clicks from sustained presses【2†L27-L30】【2†L102-L104】.

Event Queue Implementation

When update_button_action() recognizes a valid gesture, it invokes trigger_event() to package the button_action_t and button_unit_t into an element_t structure. This element pushes onto a single-element queue (btn_evt_queue with QueueLength = 1) implemented as a FreeRTOS queue. The queue intentionally holds only one event to prevent backlog; the UI mode logic processes events synchronously each tick and discards overflow. The public API ui_get_btn_evt(button_action_t& btn_act, button_unit_t& btn_unit) retrieves and clears this event, returning true when an action is available【2†L14-L23】【2†L36-L46】.

Practical Implementation Examples

Detecting a single Center-press to toggle playback:

// Inside a UIMode::update() implementation
button_action_t act;
button_unit_t  unit;
if (ui_get_btn_evt(act, unit)) {
    if (act == button_action_t::CenterSingle) {
        // Toggle play/pause
        player.toggle();
    }
}

Handling headphone remote volume control with source verification:

button_action_t act;
button_unit_t  unit;
if (ui_get_btn_evt(act, unit) && unit == button_unit_t::HpButtons) {
    switch (act) {
        case button_action_t::PlusSingle:   audio.set_volume(+1); break;
        case button_action_t::MinusSingle:  audio.set_volume(-1); break;
        case button_action_t::PlusLong:     audio.set_volume(+5); break;
        case button_action_t::MinusLong:    audio.set_volume(-5); break;
        default: break;
    }
}

Configuring the Center button for sleep wake-up:

// Before entering sleep mode
ui_set_center_switch_for_wakeup(true);
pm_enter_sleep();

This reconfigures the remote pin as a plain GPIO input to allow the Center button to trigger a wake interrupt.

Summary

  • The RPi Pico WAV Player employs a dual-input UI system combining GPIO push buttons and an ADC-sampled headphone remote, both managed in src/ui_control.cpp.
  • Hardware mapping assigns Plus/Minus push buttons to GPIO 22 and 20, while the headphone remote uses GPIO 26 (ADC0) with voltage-divider detection.
  • Event processing runs at 20 Hz with 50 ms debouncing, utilizing a 30-sample history buffer and long-press thresholds (500 ms and 1.5 seconds).
  • Logical actions abstract raw hardware into semantic gestures (single, double, triple clicks; long presses) sourced from either PushButtons or HpButtons units.
  • API consumption occurs through ui_get_btn_evt(), retrieving events from a single-element queue populated by the timer-driven update_button_action() routine.

Frequently Asked Questions

What GPIO pins are used for the push buttons and headphone remote in the RPi Pico WAV Player?

The Plus push button connects to GPIO 22 (PIN_SW_PLUS), and the Minus push button connects to GPIO 20 (PIN_SW_MINUS). The headphone remote uses GPIO 26 (PIN_HP_BUTTON), which maps to ADC0 for analog voltage sampling. All push button pins configure as inputs with internal pull-up resistors, making them active-low when pressed.

How does the headphone remote control detection work via ADC?

The headphone remote implements a voltage-divider circuit across the microphone bias resistor. When remote buttons are pressed, they create distinct voltage levels on GPIO 26. The firmware samples ADC0, converts the raw reading to a voltage using calibration coefficients, and maps specific voltage windows to logical states: Center, D, Plus, and Minus. This occurs in adc0_get_hp_button() within src/ui_control.cpp.

What is the polling rate and debouncing strategy for button inputs?

The UI layer executes a repeating hardware timer at 20 Hz (TIMER_UI_BUTTON_HZ), polling inputs every 50 milliseconds via update_button_action(). The system maintains a 30-sample history buffer (button_prv[NUM_BTN_HISTORY]) to filter contact bounce. Long-press detection uses threshold counters: 10 counts (500 ms) for standard long presses and 30 counts (1.5 seconds) for extended long presses.

How can the Center button wake the device from sleep mode?

Before entering low-power sleep, call ui_set_center_switch_for_wakeup(true) to reconfigure the headphone remote pin (GPIO 26) as a plain GPIO input rather than an ADC channel. This allows the Center button press to trigger a hardware interrupt and wake the RP2040 from sleep. After waking, the system restores normal ADC operation for remote button detection. This functionality is implemented in ui_set_center_switch_for_wakeup() within src/ui_control.cpp.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →