Skip to content

Feature/dmg volume potentiometer#283

Open
TadeuBotelho wants to merge 4 commits intoducalex:masterfrom
TadeuBotelho:feature/dmg-volume-potentiometer
Open

Feature/dmg volume potentiometer#283
TadeuBotelho wants to merge 4 commits intoducalex:masterfrom
TadeuBotelho:feature/dmg-volume-potentiometer

Conversation

@TadeuBotelho
Copy link
Copy Markdown

A suggestion for the Retro Go firmware:

potentiometer-dmg-01

Integrated Software

  • Retro-Go 1.46 firmware fully operational
  • Digital D-PAD configured on GPIOs
  • Volume control via potentiometer (GPIO 7 - ADC1_CH6)
  • Periodic reading with hysteresis (5%) and safe initialization verification

Technical Characteristics

  • Analog conversion: 0V → 0% volume, 3.3V → 100% volume
  • Resolution: 12-bit ADC (0-4095 raw values)
  • Update every 100ms
  • 4-sample averaging for stability
  • No reboots or panics during boot (with protection condition)

Volume Control via Potentiometer - Technical Documentation

Overview

This document describes the implementation of analog volume control through a potentiometer connected to the ESP32-S3, based on the existing Retro-Go battery reading architecture.

Hardware

Physical Connections

  • GPIO 7 (ADC1_CH6): Potentiometer wiper (center pin)
  • GND: Potentiometer GND pin
  • VCC (3.3V): Potentiometer VCC pin

Characteristics

  • Input voltage: 0V to 3.3V
  • ADC resolution: 12-bit (0-4095 raw values)
  • Mapping: 0V = 0% volume, 3.3V = 100% volume

Implementation

1. Configuration File (config.h)

The file components/retro-go/targets/esp32-s3-devkit/config.h contains the following definitions:

c
// Volume Control Potentiometer (GPIO 7 / ADC1_CH6)
#define RG_POTENTIOMETER_ENABLED 1
#define RG_POTENTIOMETER_ADC_UNIT ADC_UNIT_1
#define RG_POTENTIOMETER_ADC_CHANNEL ADC_CHANNEL_6
#define RG_POTENTIOMETER_ADC_ATTEN ADC_ATTEN_DB_11
// Converts raw ADC value (0-4095) to volume percentage (0-100%)
#define RG_POTENTIOMETER_CALC_VOLUME(raw) ((int32_t)((raw) * 100 / 4095))
// Hysteresis threshold to prevent volume changes from small ADC fluctuations
#define RG_POTENTIOMETER_UPDATE_THRESHOLD 5
// Update interval in microseconds (update every 100ms)
#define RG_POTENTIOMETER_UPDATE_INTERVAL (100 * 1000)

2. Reading Functions (rg_input.c)

rg_input_read_potentiometer_raw(int *out)

Function that reads the raw ADC value from the potentiometer. Similar to the battery reading function:

c
bool rg_input_read_potentiometer_raw(int *out)
{
int raw_value = 0;

#ifdef ESP_PLATFORM
// Read the potentiometer multiple times and average for stability
for (int i = 0; i < 4; ++i)
{
int value = adc_get_raw(RG_POTENTIOMETER_ADC_UNIT, RG_POTENTIOMETER_ADC_CHANNEL);
if (value < 0)
return false;
raw_value += value;
}
raw_value /= 4;
#else
return false;
#endif

if (out)
    *out = RG_POTENTIOMETER_CALC_VOLUME(raw_value);
return true;

}

Characteristics:

  • Reads the ADC 4 times and averages the result to increase stability
  • Returns the value converted to volume percentage (0-100%)
  • Follows the same pattern as battery reading

3. Input Task (input_task)

The input task was modified to include:

c
static void input_task(void *arg)
{
// ... other variables ...
#ifdef RG_POTENTIOMETER_ENABLED
int64_t next_potentiometer_update = 0;
int last_potentiometer_volume = -1000; // Initialize impossibly low
#endif

while (input_task_running)
{
    // ... gamepad and battery reading ...

#ifdef RG_POTENTIOMETER_ENABLED
if (rg_system_timer() >= next_potentiometer_update)
{
int potentiometer_volume = 0;
if (rg_input_read_potentiometer_raw(&potentiometer_volume))
{
// Hysteresis: only update if change >= RG_POTENTIOMETER_UPDATE_THRESHOLD
if (abs(potentiometer_volume - last_potentiometer_volume) >= RG_POTENTIOMETER_UPDATE_THRESHOLD)
{
int volume = RG_MAX(0, RG_MIN(100, potentiometer_volume));
// Critical check: only call if audio has been initialized
if (rg_audio_get_driver() != NULL)
{
rg_audio_set_volume(volume);
last_potentiometer_volume = volume;
RG_LOGD("Potentiometer adjusted volume to %d%%\n", volume);
}
}
}
next_potentiometer_update = rg_system_timer() + RG_POTENTIOMETER_UPDATE_INTERVAL;
}
#endif

    rg_task_delay(10);
}

}

Important Characteristics:

  1. Hysteresis: Volume is only updated if the change is >= 5%, preventing jitter caused by ADC noise
  2. Initialization Check: Verifies rg_audio_get_driver() != NULL before calling rg_audio_set_volume() to avoid panic during boot
  3. Update Period: Updates every 100ms, balancing responsiveness and CPU usage
  4. Compile-Time Condition: All code is wrapped with #ifdef RG_POTENTIOMETER_ENABLED allowing easy disabling

4. ADC Initialization

In the function rg_input_init(), the potentiometer ADC is configured:

c
#ifdef RG_POTENTIOMETER_ENABLED
RG_LOGI("Initializing ADC potentiometer driver...");
if (RG_POTENTIOMETER_ADC_UNIT == ADC_UNIT_1)
{
adc1_config_width(ADC_WIDTH_MAX - 1);
adc1_config_channel_atten(RG_POTENTIOMETER_ADC_CHANNEL, RG_POTENTIOMETER_ADC_ATTEN);
}
else if (RG_POTENTIOMETER_ADC_UNIT == ADC_UNIT_2)
{
adc2_config_channel_atten(RG_POTENTIOMETER_ADC_CHANNEL, RG_POTENTIOMETER_ADC_ATTEN);
}
else
{
RG_LOGE("Only ADC1 and ADC2 are supported for potentiometer driver!");
}
#endif

Functional Flow

┌─────────────────────────────────────────────────────────────┐
│ Potentiometer (0-3.3V) ──→ GPIO 7 (ADC1_CH6) │
└────────────────────────────────┬──────────────────────────────┘


┌──────────────────────────┐
│ ADC Reads Value (0-4095) │
└────────────┬─────────────┘

▼ (Average 4 samples)
┌──────────────────────────┐
│ Calc Volume % (0-100) │
└────────────┬─────────────┘

┌─────────┴─────────┐
│ Every 100ms │
└────────┬──────────┘

┌────────▼────────┐
│ Check Hysteresis│
│ Δ >= 5% ? │
└────────┬────────┘

┌────────────┴────────────┐
│ Audio Initialized? │
│ (driver != NULL) │
└────────────┬────────────┘


┌──────────────────────────┐
│ rg_audio_set_volume() │
│ Update UI (if applicable)│
└──────────────────────────┘

Quick Disable

If you need to disable the potentiometer control quickly, modify in config.h:

c
#define RG_POTENTIOMETER_ENABLED 0 // Change to 0

Recompile and reflash. All related code will be removed by the preprocessor.

Testing and Validation

Log Verification

On startup, you will see:

[info] rg_input_init: Initializing ADC potentiometer driver...

When moving the potentiometer:

[debug] Potentiometer adjusted volume to 35%
[debug] Potentiometer adjusted volume to 42%

Troubleshooting

Problem Possible Cause Solution
Volume does not change GPIO 7 not properly connected Check physical connection
Volume oscillates heavily ADC noise Increase RG_POTENTIOMETER_UPDATE_THRESHOLD
Boot panic Audio not initialized in time Check initialization order (already fixed)
ADC always reads 0 Potentiometer not powered Check VCC/GND connection

Comparison with Previous Solution

This solution follows the pattern established in Retro-Go for battery reading:

Aspect Battery Potentiometer
ADC Frame ADC1_CH3 ADC1_CH6
Update Frequency 2 seconds 100 ms
Averaging 4 samples 4 samples
Hysteresis % level % volume
Conversion mV → % battery RAW → % volume

Next Improvements (Optional)

  1. Calibration: Allow user to calibrate potentiometer min/max
  2. Callback Function: Add callback to notify volume changes
  3. Persistence: Save last potentiometer position in settings
  4. Smoothing: Implement smooth ramp between volumes to avoid clicks
  5. Visual Indicator: Display potentiometer value (0-100%) in debug menu
  6. On-Screen Shortcuts: Show volume percentage next to battery icon

References

Author: Tadeu Botelho
E-mail: tadeubotelho@hotmail.com
GitHub: https://github.com/TadeuBotelho
Date: February 11, 2026
Version: 1.0
Tested Target: esp32-s3-devkit

@Shadowyingjian
Copy link
Copy Markdown

Shadowyingjian commented Feb 12, 2026 via email

@DynaMight1124
Copy link
Copy Markdown
Contributor

DynaMight1124 commented Feb 12, 2026

Hi

Very exciting, its been something I added to my recent device without even knowing if it would ever work! My device is a ESP32-P4 and IDF have stopped it from using what they now call legacy drivers (although older devices can still use them) so I couldnt initially use your code due to errors compiling however AI was able to adjust your code to work with the new ADC driver. I then added some ifdefs so it uses the new driver for P4 devices and the legacy driver for S3 and below, same as the original code. Chances are the new driver will work on older devices anyway!

Its all working fine, volume control works fine, there is some repeatability issues, not 100% sure if thats HW with poor quality pots or software not reading is exactly right. Whenever I've checked the voltage on the ESP pin its been pretty stable and accurate so who knows. I did have to adjust RG_POTENTIOMETER_CALC_VOLUME as the stock setting only allows 0-20% volume, so amended to 1600 to allow the full range. Maybe this is related as its only using 10bit not 11bit. Edit: corrected with updated code, which now allows full range of 4095 and keeps the battery working. Currently seems to resolve the repeatability issue, hasnt failed yet so fingers crossed. Updated AI butchery code below.

So I'll paste my amended code below, but just note my code is based on the latest dev, not master which brings me to the next point with this pull request, but theres likely to be a few issues if the dev is happy to accept. 1) ideally you want to be pushing to the dev build, not master since theres been a lot of changes since master and likely cant be merged and 2) theres other changes to esp32-s3-devkit/config.h (I assume its to match your device) but you will want to remove that, its probably an idea to leave the additional volume related defines as they need to be seen somewhere.

So enough rambling, heres the code. I'm not sure if AI has changed other stuff too, sometimes its hard to stop it needlessly re-writing stuff!

I have tested it on my ESP32-P4 device and also built for a ESP32-S3 without any volume defines and also with them (my other ESP32 devices dont have any volume pots so cant actually test it works but it builds without error)

EDIT:

Adjusted the below code to allow better adjustability on my device, it does require different defines in config.h but feels like its easier for the end user to understand & adjust to suit their setup. Example config.h:

#define RG_POTENTIOMETER_ADC {ADC_UNIT_2, ADC_CHANNEL_4, ADC_ATTEN_DB_11, 1775, 4095} // Last two numbers are RAW_MIN & RAW_MAX
// You can use serial log to confirm the highest and lowest, also min & max can be swapped to invert the action.
#define RG_POTENTIOMETER_UPDATE_THRESHOLD 5   // Percentage threshold to prevent volume changes from small ADC fluctuations
#define RG_POTENTIOMETER_UPDATE_INTERVAL  (100 * 1000)   // Update interval in microseconds (update every 100ms)

Updated code:

#include "rg_system.h"
#include "rg_input.h"
#include "rg_audio.h"

#include <stdlib.h>
#include <string.h>
#include <math.h>

#if CONFIG_IDF_TARGET_ESP32P4
#define USE_ADC_DRIVER_NG
#endif

#ifdef ESP_PLATFORM
#include <driver/gpio.h>
#ifdef USE_ADC_DRIVER_NG
#include <esp_adc/adc_oneshot.h>
#include <esp_adc/adc_cali.h>
#include <esp_adc/adc_cali_scheme.h>
static adc_oneshot_unit_handle_t adc_handles[4];
static adc_cali_handle_t adc_cali_handles[4];
#else
#include <driver/adc.h>
#include <esp_adc_cal.h>
static esp_adc_cal_characteristics_t adc_chars;
#endif
// This is a lazy way to silence deprecation notices on esp-idf 5.2+ stating
// that ADC_ATTEN_DB_12 should be used instead (which is the same value)
#ifndef ADC_ATTEN_DB_11
#define ADC_ATTEN_DB_11 3 /* ADC_ATTEN_DB_12 */
#endif
#else
#include <SDL2/SDL.h>
#endif

#ifdef RG_POTENTIOMETER_ADC
typedef struct { int unit, channel, atten, min, max; } rg_pot_cfg_t;
static const rg_pot_cfg_t pot_cfg = RG_POTENTIOMETER_ADC;
#define RG_POTENTIOMETER_ENABLED 1
#define RG_POTENTIOMETER_ADC_UNIT pot_cfg.unit
#define RG_POTENTIOMETER_ADC_CHANNEL pot_cfg.channel
#define RG_POTENTIOMETER_ADC_ATTEN pot_cfg.atten
#define RG_POTENTIOMETER_RAW_MIN pot_cfg.min
#define RG_POTENTIOMETER_RAW_MAX pot_cfg.max
#endif

#ifdef RG_GAMEPAD_ADC_MAP
static rg_keymap_adc_t keymap_adc[] = RG_GAMEPAD_ADC_MAP;
#endif
#ifdef RG_GAMEPAD_GPIO_MAP
static rg_keymap_gpio_t keymap_gpio[] = RG_GAMEPAD_GPIO_MAP;
#endif
#ifdef RG_GAMEPAD_I2C_MAP
static rg_keymap_i2c_t keymap_i2c[] = RG_GAMEPAD_I2C_MAP;
#endif
#ifdef RG_GAMEPAD_KBD_MAP
static rg_keymap_kbd_t keymap_kbd[] = RG_GAMEPAD_KBD_MAP;
#endif
#ifdef RG_GAMEPAD_SERIAL_MAP
static rg_keymap_serial_t keymap_serial[] = RG_GAMEPAD_SERIAL_MAP;
#endif
#ifdef RG_GAMEPAD_VIRT_MAP
static rg_keymap_virt_t keymap_virt[] = RG_GAMEPAD_VIRT_MAP;
#endif
static bool input_task_running = false;
static uint32_t gamepad_state = -1; // _Atomic
static uint32_t gamepad_mapped = 0;
static rg_battery_t battery_state = {0};

#define UPDATE_GLOBAL_MAP(keymap)                 \
    for (size_t i = 0; i < RG_COUNT(keymap); ++i) \
        gamepad_mapped |= keymap[i].key;          \

#ifdef ESP_PLATFORM
static inline bool _adc_setup_channel(adc_unit_t unit, adc_channel_t channel, adc_atten_t atten, bool calibrate)
{
    RG_ASSERT(unit == ADC_UNIT_1 || unit == ADC_UNIT_2, "Invalid ADC unit");
    esp_err_t err = ESP_FAIL;

#ifdef USE_ADC_DRIVER_NG
    // --- New Driver  ---
    if (!adc_handles[unit])
{
        adc_oneshot_unit_init_cfg_t config = {
            .unit_id = unit,
            .clk_src = 0, 
            .ulp_mode = ADC_ULP_MODE_DISABLE,
        };
        err = adc_oneshot_new_unit(&config, &adc_handles[unit]);
        
        if (err != ESP_OK) {
        }
    }


    // The Battery needs 12-bit (Default).
    // The Potentiometer needs 11-bit.
    adc_bitwidth_t target_width = ADC_BITWIDTH_DEFAULT; 
    
    if (channel == RG_POTENTIOMETER_ADC_CHANNEL) {
        target_width = ADC_BITWIDTH_11; 
    }

    const adc_oneshot_chan_cfg_t config = {
        .atten = atten,
        .bitwidth = target_width
    };
    
    err = adc_oneshot_config_channel(adc_handles[unit], channel, &config);
#else
    // --- Legacy Driver ---
    if (unit == ADC_UNIT_1)
    {
        adc1_config_width(ADC_WIDTH_MAX - 1);
        err = adc1_config_channel_atten(channel, atten);
    }
    else if (unit == ADC_UNIT_2)
    {
        err = adc2_config_channel_atten(channel, atten);
    }
#endif

    if (err != ESP_OK)
    {
        RG_LOGE("Failed to configure ADC_UNIT_%d channel:%d atten:%d error:0x%02X",
                (int)unit, (int)channel, (int)atten, (int)err);
        return false;
    }

    if (calibrate)
    {
#ifdef USE_ADC_DRIVER_NG
    #if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
        const adc_cali_curve_fitting_config_t config = {
            .unit_id = unit,
            .atten = atten,
            .bitwidth = target_width, 
        };
        err = adc_cali_create_scheme_curve_fitting(&config, &adc_cali_handles[unit]);
    #elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
        const adc_cali_line_fitting_config_t config = {
            .unit_id = unit,
            .atten = atten,
            .bitwidth = target_width, 
            #if CONFIG_IDF_TARGET_ESP32
            .default_vref = 1100,
            #endif
        };
        err = adc_cali_create_scheme_line_fitting(&config, &adc_cali_handles[unit]);
    #else
        err = ESP_ERR_NOT_SUPPORTED;
    #endif
#else
        err = esp_adc_cal_characterize(unit, atten, ADC_WIDTH_MAX - 1, 1100, &adc_chars);
#endif
        if (err != ESP_OK)
        {
            // Calibration often fails on P4 for bitwidths != 12, but raw reading will still work
            RG_LOGW("Calibration warning ADC_UNIT_%d atten:%d error:0x%02X",
                    (int)unit, (int)atten, (int)err);
        }
    }
    return true;
}

static inline int _adc_get_raw(adc_unit_t unit, adc_channel_t channel)
{
    RG_ASSERT(unit == ADC_UNIT_1 || unit == ADC_UNIT_2, "Invalid ADC unit");
    int adc_raw_value = -1;
#ifdef USE_ADC_DRIVER_NG
    if (adc_oneshot_read(adc_handles[unit], channel, &adc_raw_value) != ESP_OK)
        RG_LOGE("ADC reading failed, this can happen while wifi is active.");
#else
    if (unit == ADC_UNIT_1)
        adc_raw_value = adc1_get_raw(channel);
    else if (adc2_get_raw(channel, ADC_WIDTH_MAX - 1, &adc_raw_value) != ESP_OK)
        RG_LOGE("ADC2 reading failed, this can happen while wifi is active.");
#endif
    return adc_raw_value;
}

static inline int _adc_get_voltage(adc_unit_t unit, adc_channel_t channel)
{
    int raw_value = _adc_get_raw(unit, channel);
    if (raw_value < 0)
        return -1;
#ifdef USE_ADC_DRIVER_NG
    int voltage;
    if (adc_cali_raw_to_voltage(adc_cali_handles[unit], raw_value, &voltage) != ESP_OK)
        return -1;
    return voltage;
#else
    return esp_adc_cal_raw_to_voltage(raw_value, &adc_chars);
#endif
}
#endif

bool rg_input_read_battery_raw(rg_battery_t *out)
{
    uint32_t raw_value = 0;
    bool present = true;
    bool charging = false;

#if RG_BATTERY_DRIVER == 1 /* ADC */
    for (int i = 0; i < 4; ++i)
    {
        int value = _adc_get_voltage(RG_BATTERY_ADC_UNIT, RG_BATTERY_ADC_CHANNEL);
        if (value < 0)
            return false;
        raw_value += value;
    }
    raw_value /= 4;
#elif RG_BATTERY_DRIVER == 2 /* I2C */
    uint8_t data[5];
    if (!rg_i2c_read(0x20, -1, &data, 5))
        return false;
    raw_value = data[4];
    charging = data[4] == 255;
#else
    return false;
#endif

    if (!out)
        return true;

    *out = (rg_battery_t){
        .level = RG_MAX(0.f, RG_MIN(100.f, RG_BATTERY_CALC_PERCENT(raw_value))),
        .volts = RG_BATTERY_CALC_VOLTAGE(raw_value),
        .present = present,
        .charging = charging,
    };
    return true;
}

#ifdef RG_POTENTIOMETER_ENABLED

#ifndef RG_POTENTIOMETER_RAW_MIN
#define RG_POTENTIOMETER_RAW_MIN 0
#endif

#ifndef RG_POTENTIOMETER_RAW_MAX
#define RG_POTENTIOMETER_RAW_MAX 4095
#endif

#ifndef RG_POTENTIOMETER_CALC_VOLUME
#define RG_POTENTIOMETER_CALC_VOLUME(raw) \
    ((((int32_t)(raw)) - RG_POTENTIOMETER_RAW_MIN) * 100 / (RG_POTENTIOMETER_RAW_MAX - RG_POTENTIOMETER_RAW_MIN))
#endif

bool rg_input_read_potentiometer_raw(int *out)
{
    int raw_value = 0;

#ifdef ESP_PLATFORM
    // Read the potentiometer multiple times and average for stability
    for (int i = 0; i < 4; ++i)
    {
#ifdef USE_ADC_DRIVER_NG
        // New Driver
        int value = _adc_get_raw(RG_POTENTIOMETER_ADC_UNIT, RG_POTENTIOMETER_ADC_CHANNEL);
#else
        // Legacy Driver
        int value = _adc_get_raw(RG_POTENTIOMETER_ADC_UNIT, RG_POTENTIOMETER_ADC_CHANNEL);
#endif
        if (value < 0)
            return false;
        raw_value += value;
    }
    raw_value /= 4;
#else
    return false;
#endif

    if (out)
        *out = raw_value;
    return true;
}
#endif

bool rg_input_read_gamepad_raw(uint32_t *out)
{
    uint32_t state = 0;

#if defined(RG_GAMEPAD_ADC_MAP)
    static int old_adc_values[RG_COUNT(keymap_adc)];
    for (size_t i = 0; i < RG_COUNT(keymap_adc); ++i)
    {
        const rg_keymap_adc_t *mapping = &keymap_adc[i];
        int value = _adc_get_raw(mapping->unit, mapping->channel);
        if (value >= mapping->min && value <= mapping->max)
        {
            if (abs(old_adc_values[i] - value) < RG_GAMEPAD_ADC_FILTER_WINDOW)
                state |= mapping->key;
            old_adc_values[i] = value;
        }
    }
#endif

#if defined(RG_GAMEPAD_GPIO_MAP)
    for (size_t i = 0; i < RG_COUNT(keymap_gpio); ++i)
    {
        const rg_keymap_gpio_t *mapping = &keymap_gpio[i];
        if (gpio_get_level(mapping->num) == mapping->level)
            state |= mapping->key;
    }
#endif

#if defined(RG_GAMEPAD_I2C_MAP)
    uint32_t buttons = 0;
#if defined(RG_I2C_GPIO_DRIVER)
    int data0 = rg_i2c_gpio_read_port(0), data1 = rg_i2c_gpio_read_port(1);
    if (data0 > -1)
    {
        buttons = (data1 << 8) | (data0);
#elif defined(RG_TARGET_T_DECK_PLUS)
    uint8_t data[5];
    if (rg_i2c_read(T_DECK_KBD_ADDRESS, -1, &data, 5))
    {
        buttons = ((data[0] << 25) | (data[1] << 18) | (data[2] << 11) | ((data[3] & 0xF8) << 4) | (data[4]));
#else
    uint8_t data[5];
    if (rg_i2c_read(RG_I2C_GPIO_ADDR, -1, &data, 5))
    {
        buttons = (data[2] << 8) | (data[1]);
#endif
        for (size_t i = 0; i < RG_COUNT(keymap_i2c); ++i)
        {
            const rg_keymap_i2c_t *mapping = &keymap_i2c[i];
            if (((buttons >> mapping->num) & 1) == mapping->level)
                state |= mapping->key;
        }
    }
#endif

#if defined(RG_GAMEPAD_KBD_MAP)
#ifdef RG_TARGET_SDL2
    int numkeys = 0;
    const uint8_t *keys = SDL_GetKeyboardState(&numkeys);
    for (size_t i = 0; i < RG_COUNT(keymap_kbd); ++i)
    {
        const rg_keymap_kbd_t *mapping = &keymap_kbd[i];
        if (mapping->src < 0 || mapping->src >= numkeys)
            continue;
        if (keys[mapping->src])
            state |= mapping->key;
    }
#endif
#endif

#if defined(RG_GAMEPAD_SERIAL_MAP)
    gpio_set_level(RG_GPIO_GAMEPAD_LATCH, 0);
    rg_usleep(5);
    gpio_set_level(RG_GPIO_GAMEPAD_LATCH, 1);
    rg_usleep(1);
    uint32_t buttons = 0;
    for (int i = 0; i < 16; i++)
    {
        buttons |= gpio_get_level(RG_GPIO_GAMEPAD_DATA) << (15 - i);
        gpio_set_level(RG_GPIO_GAMEPAD_CLOCK, 0);
        rg_usleep(1);
        gpio_set_level(RG_GPIO_GAMEPAD_CLOCK, 1);
        rg_usleep(1);
    }
    for (size_t i = 0; i < RG_COUNT(keymap_serial); ++i)
    {
        const rg_keymap_serial_t *mapping = &keymap_serial[i];
        if (((buttons >> mapping->num) & 1) == mapping->level)
            state |= mapping->key;
    }
#endif

#if defined(RG_GAMEPAD_VIRT_MAP)
    for (size_t i = 0; i < RG_COUNT(keymap_virt); ++i)
    {
        if (state == keymap_virt[i].src)
            state = keymap_virt[i].key;
    }
#endif

    if (out)
        *out = state;
    return true;
}

static void input_task(void *arg)
{
    uint8_t debounce[RG_KEY_COUNT];
    uint32_t local_gamepad_state = 0;
    uint32_t state;
    int64_t next_battery_update = 0;

#ifdef RG_POTENTIOMETER_ENABLED
    int64_t next_potentiometer_update = 0;
    int last_potentiometer_volume = -1000;
#endif

    memset(debounce, 0xFF, sizeof(debounce));
    input_task_running = true;

    while (input_task_running)
    {
        if (rg_input_read_gamepad_raw(&state))
        {
            for (int i = 0; i < RG_KEY_COUNT; ++i)
            {
                uint32_t val = ((debounce[i] << 1) | ((state >> i) & 1));
                debounce[i] = val & 0xFF;

                if ((val & ((1 << RG_GAMEPAD_DEBOUNCE_PRESS) - 1)) == ((1 << RG_GAMEPAD_DEBOUNCE_PRESS) - 1))
                {
                    local_gamepad_state |= (1 << i);
                }
                else if ((val & ((1 << RG_GAMEPAD_DEBOUNCE_RELEASE) - 1)) == 0)
                {
                    local_gamepad_state &= ~(1 << i);
                }
            }
            gamepad_state = local_gamepad_state;
        }

        if (rg_system_timer() >= next_battery_update)
        {
            rg_battery_t temp = {0};
            if (rg_input_read_battery_raw(&temp))
            {
                if (fabsf(battery_state.level - temp.level) < RG_BATTERY_UPDATE_THRESHOLD)
                    temp.level = battery_state.level;
                if (fabsf(battery_state.volts - temp.volts) < RG_BATTERY_UPDATE_THRESHOLD_VOLT)
                    temp.volts = battery_state.volts;
            }
            battery_state = temp;
            next_battery_update = rg_system_timer() + 2 * 1000000;
        }

#ifdef RG_POTENTIOMETER_ENABLED
        if (rg_system_timer() >= next_potentiometer_update)
        {
            int raw_value = 0;
            if (rg_input_read_potentiometer_raw(&raw_value))
            {
                int potentiometer_volume = RG_POTENTIOMETER_CALC_VOLUME(raw_value);
                if (abs(potentiometer_volume - last_potentiometer_volume) >= RG_POTENTIOMETER_UPDATE_THRESHOLD)
                {
                    int volume = RG_MAX(0, RG_MIN(100, potentiometer_volume));
                    if (rg_audio_get_driver() != NULL)
                    {
                        rg_audio_set_volume(volume);
                        last_potentiometer_volume = volume;
                        RG_LOGD("Potentiometer adjusted volume to %d%% (raw: %d)\n", volume, raw_value);
                    }
                }
            }
            next_potentiometer_update = rg_system_timer() + RG_POTENTIOMETER_UPDATE_INTERVAL;
        }
#endif
        rg_task_delay(10);
    }

    input_task_running = false;
    gamepad_state = -1;
}

void rg_input_init(void)
{
    RG_ASSERT(!input_task_running, "Input already initialized!");

#if defined(RG_GAMEPAD_ADC_MAP)
    RG_LOGI("Initializing ADC gamepad driver...");
    for (size_t i = 0; i < RG_COUNT(keymap_adc); ++i)
    {
        const rg_keymap_adc_t *mapping = &keymap_adc[i];
        _adc_setup_channel(mapping->unit, mapping->channel, mapping->atten, false);
    }
    UPDATE_GLOBAL_MAP(keymap_adc);
#endif

#if defined(RG_GAMEPAD_GPIO_MAP)
    RG_LOGI("Initializing GPIO gamepad driver...");
    for (size_t i = 0; i < RG_COUNT(keymap_gpio); ++i)
    {
        const rg_keymap_gpio_t *mapping = &keymap_gpio[i];
        gpio_set_direction(mapping->num, GPIO_MODE_INPUT);
        if (mapping->pullup && mapping->pulldown)
            gpio_set_pull_mode(mapping->num, GPIO_PULLUP_PULLDOWN);
        else if (mapping->pullup || mapping->pulldown)
            gpio_set_pull_mode(mapping->num, mapping->pullup ? GPIO_PULLUP_ONLY : GPIO_PULLDOWN_ONLY);
        else
            gpio_set_pull_mode(mapping->num, GPIO_FLOATING);
    }
    UPDATE_GLOBAL_MAP(keymap_gpio);
#endif

#if defined(RG_GAMEPAD_I2C_MAP)
    RG_LOGI("Initializing I2C gamepad driver...");
    rg_i2c_init();
#if defined(RG_I2C_GPIO_DRIVER)
    for (size_t i = 0; i < RG_COUNT(keymap_i2c); ++i)
    {
        const rg_keymap_i2c_t *mapping = &keymap_i2c[i];
        if (mapping->pullup)
            rg_i2c_gpio_set_direction(mapping->num, RG_GPIO_INPUT_PULLUP);
        else
            rg_i2c_gpio_set_direction(mapping->num, RG_GPIO_INPUT);
    }
#elif defined(RG_TARGET_T_DECK_PLUS)
    rg_i2c_write_byte(T_DECK_KBD_ADDRESS, -1, T_DECK_KBD_MODE_RAW_CMD);
#endif
    UPDATE_GLOBAL_MAP(keymap_i2c);
#endif

#if defined(RG_GAMEPAD_KBD_MAP)
    RG_LOGI("Initializing KBD gamepad driver...");
    UPDATE_GLOBAL_MAP(keymap_kbd);
#endif

#if defined(RG_GAMEPAD_SERIAL_MAP)
    RG_LOGI("Initializing SERIAL gamepad driver...");
    gpio_set_direction(RG_GPIO_GAMEPAD_CLOCK, GPIO_MODE_OUTPUT);
    gpio_set_direction(RG_GPIO_GAMEPAD_LATCH, GPIO_MODE_OUTPUT);
    gpio_set_direction(RG_GPIO_GAMEPAD_DATA, GPIO_MODE_INPUT);
    gpio_set_level(RG_GPIO_GAMEPAD_LATCH, 0);
    gpio_set_level(RG_GPIO_GAMEPAD_CLOCK, 1);
    UPDATE_GLOBAL_MAP(keymap_serial);
#endif

#if RG_BATTERY_DRIVER == 1 /* ADC */
    RG_LOGI("Initializing ADC battery driver...");
    _adc_setup_channel(RG_BATTERY_ADC_UNIT, RG_BATTERY_ADC_CHANNEL, ADC_ATTEN_DB_11, true);
#endif

#ifdef RG_POTENTIOMETER_ENABLED
    RG_LOGI("Initializing ADC potentiometer driver...");

#ifdef USE_ADC_DRIVER_NG
    // --- New Driver ---
    if (!_adc_setup_channel(RG_POTENTIOMETER_ADC_UNIT, RG_POTENTIOMETER_ADC_CHANNEL, RG_POTENTIOMETER_ADC_ATTEN, false))
    {
        RG_LOGE("Failed to init Potentiometer ADC (NG Driver)");
    }

#else
    // --- Legacy Driver ---
    if (RG_POTENTIOMETER_ADC_UNIT == ADC_UNIT_1)
    {
        adc1_config_width(ADC_WIDTH_MAX - 1);
        adc1_config_channel_atten(RG_POTENTIOMETER_ADC_CHANNEL, RG_POTENTIOMETER_ADC_ATTEN);
    }
    else if (RG_POTENTIOMETER_ADC_UNIT == ADC_UNIT_2)
    {
        adc2_config_channel_atten(RG_POTENTIOMETER_ADC_CHANNEL, RG_POTENTIOMETER_ADC_ATTEN);
    }
    else
    {
        RG_LOGE("Only ADC1 and ADC2 are supported for potentiometer driver!");
    }
#endif

#endif

    // The first read returns bogus data in some drivers, waste it.
    rg_input_read_gamepad_raw(NULL);

    // Start background polling
    rg_task_create("rg_input", &input_task, NULL, 3 * 1024, 1, RG_TASK_PRIORITY_6, 1);
    while (gamepad_state == -1)
        rg_task_yield();
    RG_LOGI("Input ready. state=" PRINTF_BINARY_16 "\n", PRINTF_BINVAL_16(gamepad_state));
}

void rg_input_deinit(void)
{
    input_task_running = false;
    RG_LOGI("Input terminated.\n");
}

bool rg_input_key_is_present(rg_key_t mask)
{
    return (gamepad_mapped & mask) == mask;
}

uint32_t rg_input_read_gamepad(void)
{
#ifdef RG_TARGET_SDL2
    SDL_PumpEvents();
#endif
    return gamepad_state;
}

bool rg_input_key_is_pressed(rg_key_t mask)
{
    return (bool)(rg_input_read_gamepad() & mask);
}

bool rg_input_wait_for_key(rg_key_t mask, bool pressed, int timeout_ms)
{
    int64_t expiration = timeout_ms < 0 ? INT64_MAX : (rg_system_timer() + timeout_ms * 1000);
    while (rg_input_key_is_pressed(mask) != pressed)
    {
        if (rg_system_timer() > expiration)
            return false;
        rg_task_delay(10);
    }
    return true;
}

rg_battery_t rg_input_read_battery(void)
{
    return battery_state;
}

const char *rg_input_get_key_name(rg_key_t key)
{
    switch (key)
    {
    case RG_KEY_UP: return "Up";
    case RG_KEY_RIGHT: return "Right";
    case RG_KEY_DOWN: return "Down";
    case RG_KEY_LEFT: return "Left";
    case RG_KEY_SELECT: return "Select";
    case RG_KEY_START: return "Start";
    case RG_KEY_MENU: return "Menu";
    case RG_KEY_OPTION: return "Option";
    case RG_KEY_A: return "A";
    case RG_KEY_B: return "B";
    case RG_KEY_X: return "X";
    case RG_KEY_Y: return "Y";
    case RG_KEY_L: return "Left Shoulder";
    case RG_KEY_R: return "Right Shoulder";
    case RG_KEY_NONE: return "None";
    default: return "Unknown";
    }
}

@TadeuBotelho
Copy link
Copy Markdown
Author

TadeuBotelho commented Feb 13, 2026

Hi,

I’m glad to hear the code was adapted for the ESP32-P4 and that it worked! Here on my ESP32-S3 it’s working perfectly :-)

Your suggestion about submitting PRs to the development branch instead of master makes total sense. I agree it will make merging easier — thanks for the tip.

I only read your message after I had already submitted another contribution to the master branch. From now on, my next contributions will go to the development branch as you suggested.

I also adapted the volume control code (potentiometer-based) so that a second potentiometer can be used to control the LCD backlight brightness. I tested it extensively and it follows the same logic as the volume control. I’m even using a second potentiometer identical to the original DMG-01 volume potentiometer.

After reviewing your ESP32-P4 adaptation, I can see the structure remains essentially the same, and it looks well optimized. Great job on the port!

I don’t have an ESP32-P4 board in stock yet, but I ordered one today. It should arrive in about two weeks — I’m aiming to run 16-bit emulators (Mega Drive / Genesis and Super NES).

At this point, I’ve finished tuning the Retro-Go firmware for my needs: using the original Game Boy DMG-01 shell in a way that stays as close to the original as possible (pure nerd nostalgia).

My current challenge is fitting the prototype inside the original shell, and I think I’ll need to design a custom PCB for this.

To avoid cluttering config.h, it may be better for me to document my hardware setup and propose a dedicated target, e.g. "Retro-Go-DMG-01". I had to modify config.h due to the new features (volume and LCD brightness potentiometers), and because my D-PAD wiring also consumes a few additional GPIOs.

Thanks again!

@DynaMight1124
Copy link
Copy Markdown
Contributor

Hi

Yes saw the brightness wheel too, sounds like it could be a handy addition as well.

As for your "Retro-Go-DMG-01" target, that probably wouldnt be something the developer would accept as an official target unless its something that others can make (with a guide) or buy. One off builds are generally not accepted, but that doesnt mean you obviously cant keep it on your own fork. The only targets that are not that are the devkit ones, which are there to serve as a base for people to build from.

The GB build sounds cool, I have been tempted to build one too!

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants