How to Interface with and Control the RPi Pico WAV Player Using the 3 Push Buttons

The RPi Pico WAV Player exposes two physical button groups (GPIO switches and an ADC-sensed headphone remote) through the ui_control.h API, where ui_get_btn_evt() delivers debounced, high-level events like single clicks, double clicks, and long presses to your application code.

The elehobica/rpi_pico_wav_player firmware abstracts the three physical buttons into a robust event system that handles hardware debouncing, repeat detection, and power management. Whether you are building a custom UI or integrating the player into a larger embedded project, understanding the button interface allows you to capture user input reliably without managing raw GPIO or ADC reads directly.

Hardware Architecture of the Three-Button Interface

The firmware treats the three physical buttons as two distinct input groups: direct GPIO push-buttons and a voltage-divided headphone remote.

Push-Button Group (GPIO 22 and GPIO 20)

Two momentary switches on the PCB connect to GPIO 22 (Minus) and GPIO 20 (Plus). These lines are configured as digital inputs with internal pull-ups. In src/ui_control.cpp, the constants PIN_SW_PLUS and PIN_SW_MINUS define these mappings, and the function get_sw_status() reads both pins to return a button_status_t of Plus, Minus, or Open.

Headphone Remote Group (ADC0 on GPIO 26)

The third button channel is the 3-wire headset jack, which uses a resistor ladder to encode four possible states (Center, D, Plus, Minus) into a single analog voltage on GPIO 26 (ADC0). The constant PIN_HP_BUTTON references this pin. The adc0_get_hp_button() function samples the ADC, converts the raw value to millivolts, and maps specific voltage ranges to the logical states Center, D, Plus, Minus, or Open.

Low-Level Input Handling in ui_control.cpp

The low-level driver in src/ui_control.cpp merges both input groups into a unified state machine.

Pin Definitions and Initialization

At startup, ui_init() configures the GPIO pull-ups and initializes the ADC channel. The pin constants are defined near the top of the file:

// From src/ui_control.cpp
#define PIN_SW_PLUS   20
#define PIN_SW_MINUS  22
#define PIN_HP_BUTTON 26  // ADC0

Digital GPIO Reading

The get_sw_status() function (lines 52-62) performs a simple digital read on the two push-button pins. Because the pins are pulled high internally, a pressed button reads low, which the function translates into the corresponding button_status_t enumeration value.

ADC Voltage Ladder Decoding

For the headphone remote, adc0_get_hp_button() (lines 65-86) samples ADC0 and compares the millivolt reading against threshold constants to determine which resistor path is active. This allows a single wire to represent up to four distinct button signals through voltage division.

Event Generation and Debouncing Logic

Rather than exposing raw pin states, the firmware runs a 20 Hz background timer that processes history buffers and emits high-level actions.

20 Hz Polling Timer

A repeating timer initialized in ui_init() calls update_button_action() at 20 Hz. This frequency provides responsive user feedback while maintaining low CPU overhead.

State Machine and Action Detection

Inside update_button_action(), the firmware:

  1. Reads both input groups (get_sw_status() and adc0_get_hp_button()).
  2. Maintains a history buffer (button_prv[]) to implement software debouncing.
  3. Tracks press duration and repeat counts to distinguish single click, double/triple click, short press, long press, and very long press.
  4. Assigns a button_unit_t (PushButtons or HpButtons) to identify the physical source.

The Event Queue

Detected actions are packaged as button_action_t values and pushed into a small FIFO queue named btn_evt_queue. This decouples the interrupt-driven timer from the main application loop, preventing event loss during heavy audio decoding tasks.

Consuming Button Events in Your Application

The public API in src/ui_control.h provides a simple polling interface for your main loop or UI state machine.

The Public API

Two functions expose the queue to higher layers:

bool ui_get_btn_evt(button_action_t& btn_act, button_unit_t& btn_unit);
void ui_clear_btn_evt();

ui_get_btn_evt() dequeues the next event, populating the reference arguments with the action type and originating unit. It returns true if an event was available, false otherwise.

Polling Example

The following pattern, adapted from src/UIMode.cpp, shows how to react to navigation commands:

#include "ui_control.h"

while (true) {
    button_action_t act;
    button_unit_t   unit;
    
    if (ui_get_btn_evt(act, unit)) {
        switch (act) {
        case button_action_t::PlusSingle:
            // Trigger next track or increase volume
            break;
        case button_action_t::MinusSingle:
            // Trigger previous track or decrease volume
            break;
        case button_action_t::CenterLong:
            // Toggle play/pause
            break;
        case button_action_t::PlusDouble:
            // Fast-forward or next album
            break;
        default:
            break;
        }
    }
    
    // Audio decoding and UI rendering logic here
}

Differentiating Button Sources

If your application requires different behavior for the PCB buttons versus the headphone remote, inspect the button_unit_t value:

if (ui_get_btn_evt(act, unit)) {
    if (unit == button_unit_t::HpButtons) {
        // Handle headphone remote specifically
        if (act == button_action_t::CenterSingle) {
            // Accept call or play/pause
        }
    } else {
        // Handle PCB push-buttons
    }
}

Advanced: Wake-on-Button and Power Management

The firmware supports waking the RP2040 from deep sleep using the headphone remote’s center button. Before entering low-power mode, call ui_set_center_switch_for_wakeup(true), which reconfigures the HP pin from ADC mode to a plain GPIO input capable of generating wake-up interrupts. Upon resume, ui_init() automatically restores the ADC configuration. This functionality is integrated with the power management utilities in src/power_manage.cpp.

Summary

  • The three physical buttons map to two GPIO pins (22 and 20) for the PCB switches and one ADC pin (26) for the headphone remote resistor ladder.
  • The src/ui_control.cpp driver handles raw reads, 20 Hz debouncing, and history tracking, emitting high-level button_action_t events.
  • Use ui_get_btn_evt() in your main loop to poll for single clicks, double clicks, long presses, and very long presses without managing hardware details.
  • Inspect button_unit_t to differentiate between PushButtons and HpButtons when implementing context-sensitive controls.
  • Call ui_set_center_switch_for_wakeup() to enable deep-sleep wake-on-button functionality for battery-powered applications.

Frequently Asked Questions

How do I read the button states without missing events during audio playback?

The firmware uses a FIFO queue (btn_evt_queue) fed by a 20 Hz background timer in src/ui_control.cpp. By calling ui_get_btn_evt() each frame in your main loop—as demonstrated in src/UIMode.cpp—you dequeue events asynchronously from the audio decoder, ensuring no input is lost during CPU-intensive operations.

Can I distinguish between the PCB buttons and the headphone remote buttons?

Yes. The ui_get_btn_evt() function returns a button_unit_t parameter that is set to either PushButtons (for GPIO 20/22) or HpButtons (for the ADC-sensed remote). Check this value after polling to apply different logic for in-line remote controls versus onboard switches.

What types of button presses can the firmware detect?

According to the state machine in src/ui_control.cpp, the firmware recognizes single click, double click, triple click, short press, long press, and very long press. These map to enumerations like PlusSingle, CenterLong, or MinusDouble, allowing complex UI navigation with only three physical inputs.

How do I use the center button to wake the device from sleep?

Before entering deep sleep, call ui_set_center_switch_for_wakeup(true) from ui_control.h. This function reconfigures the headphone remote pin (GPIO 26) as a GPIO wake source. When the center button is pressed, it triggers a hardware interrupt to resume the RP2040, after which ui_init() restores normal ADC operation for button sensing.

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 →