How to Update or Customize Configuration Parameters Stored in Flash Memory in rpi_pico_wav_player

The rpi_pico_wav_player stores all user settings—volume, UI mode, button layout, and playback position—in the Raspberry Pi Pico’s on-board flash using the pico_flash_param library, which provides atomic read/write operations through the ConfigParam singleton.

The elehobica/rpi_pico_wav_player project persists runtime settings across power cycles by mapping each configuration item to a typed FlashParamNs::Parameter<T> member. When the device boots, ConfigParam::initialize() loads values from flash; when the UI exits, ConfigParam::finalize() commits dirty parameters back to memory in a single atomic write.

How Flash-Backed Configuration Works

The architecture separates generic flash management from application-specific settings. The pico_flash_param submodule (external library) provides the base FlashParam class that handles flash-sector management, serialization, and checksums, while the ConfigParam class defines the specific parameters for the WAV player.

Component Role Source Location
pico_flash_param Generic FlashParam base class, ID handling, and flash-sector operations External submodule: FlashParam.h
ConfigParam Application-specific singleton inheriting from FlashParamNs::FlashParam; declares parameter IDs (CFG_ID_…) and typed Parameter<T> members src/ConfigParam.h
UIMode Bridges UI lifecycle to flash operations; loadFromFlash() triggers initialize() and storeToFlash() triggers finalize() src/UIMode.cpp (lines 33-38, 1100-1102)
ConfigMenu High-level UI that reads/writes values via cfgParam.getValue<> and cfgParam.setValue<> src/ConfigMenu.cpp (lines 44-48, 143-148)

Reading and Updating Existing Parameters

All parameters are public members of the ConfigParam singleton. To modify a value programmatically, obtain the singleton instance, call set() on the specific parameter member, and optionally trigger an immediate save.

Basic Read/Write Pattern

#include "ConfigParam.h"

// Obtain the singleton instance
ConfigParam &cfg = ConfigParam::instance();

// Read current volume (uint8_t)
uint8_t currentVol = cfg.P_CFG_VOLUME.get();

// Update volume to 80%
cfg.P_CFG_VOLUME.set(80);

// Persist immediately (optional—otherwise saves on next power-off)
cfg.finalize();

The UI automatically persists changes when the player powers off. In src/UIMode.cpp, the storeToFlash() method calls ConfigParam::finalize() to write all dirty parameters atomically. If you modify parameters outside the normal UI flow—such as from a serial command interface—you must invoke finalize() to ensure the changes survive reboot.

Runtime Updates via the Configuration Menu

The ConfigMenu class provides the UI entry points for parameter adjustment. When a user navigates the configuration menu:

  1. ConfigMenu::enter() increments the menu level.
  2. On selection, cfgParam.setValue<uint32_t>(curItem->paramID, idx) writes the new index to the corresponding parameter.
  3. If the menu item defines a hook function (e.g., to reconfigure LCD brightness), it executes immediately.
  4. The change remains in RAM until finalize() persists it to flash.

The underlying implementation in src/ConfigMenu.cpp uses type-safe templates:

// Reading current selection
uint32_t current = cfgParam.getValue<uint32_t>(item.paramID);

// Writing new selection
cfgParam.setValue<uint32_t>(curItem->paramID, selectedIndex);

Adding New Configuration Parameters

To extend the firmware with custom settings, you must modify src/ConfigParam.h to declare a new ID and parameter member. The pico_flash_param library handles serialization automatically once declared.

Step 1: Reserve a Parameter ID

Append a new entry to the ParamId_t enum in src/ConfigParam.h:

enum ParamId_t {
    // ... existing IDs ...
    CFG_ID_ENABLE_WIFI,   // automatically assigned next sequential value
    CFG_ID_MAX
};

Step 2: Declare the Parameter

Add a FlashParamNs::Parameter<T> member to the ConfigParam class with a default value and size (size is required for strings, optional for primitives):

class ConfigParam : public FlashParamNs::FlashParam {
public:
    // ... existing parameters ...
    
    // New boolean flag (stored as uint8_t)
    FlashParamNs::Parameter<uint8_t> P_CFG_ENABLE_WIFI {
        CFG_ID_ENABLE_WIFI,
        "CFG_ENABLE_WIFI",
        0,      // default: disabled
        1       // size in bytes
    };
    
    // New uint16_t setting with default 123
    FlashParamNs::Parameter<uint16_t> P_CFG_TIMEOUT_SEC {
        CFG_ID_TIMEOUT_SEC,
        "CFG_TIMEOUT_SEC",
        123,    // default value
        2       // size in bytes
    };
};

Step 3: Use the New Parameter

Access the new parameter exactly like built-in members:

ConfigParam &cfg = ConfigParam::instance();

// Read
bool wifiEnabled = cfg.P_CFG_ENABLE_WIFI.get() != 0;

// Write
cfg.P_CFG_ENABLE_WIFI.set(1);
cfg.finalize();  // Persist to flash

Step 4: Optional UI Integration

To expose the parameter in the configuration menu, add an entry to ConfigMenu::menuMap in src/ConfigMenu.h. The menu system automatically binds to the parameter ID and handles getValue<>/setValue<> calls.

Persisting Changes and Initialization Lifecycle

Understanding when data moves between RAM and flash is critical for reliable operation.

Phase Method Call Location Effect
Boot cfgParam.initialize() UIMode::loadFromFlash() (src/UIMode.cpp lines 33-38) Reads flash sector into RAM; populates parameters with persisted values or defaults if flash is empty/corrupt.
Runtime cfgParam.<member>.set(value) User code Updates RAM copy only; marks parameter as "dirty".
Immediate Save cfgParam.finalize() User code or UIMode::storeToFlash() Atomically writes all dirty parameters to flash sector with checksum.
Power-off cfgParam.finalize() UIMode::storeToFlash() (src/UIMode.cpp lines 1100-1102) Ensures clean shutdown with latest settings stored.

The finalize() operation is atomic: it writes the entire parameter block to a reserved flash sector, ensuring that power loss during write does not corrupt the configuration. Before UIMode::finalize() writes settings, it saves the current runtime state—including playback position, volume, and navigation stack—back into the ConfigParam members (src/UIMode.cpp lines 1089-1102).

Summary

  • Architecture: The pico_flash_param library provides the generic flash storage engine, while ConfigParam defines the application-specific settings as typed Parameter<T> members.
  • Access Pattern: Use ConfigParam::instance() to get the singleton, then call get() and set() on specific members like P_CFG_VOLUME.
  • Persistence: Call finalize() to commit changes immediately, or rely on UIMode::storeToFlash() during normal power-off sequences.
  • Extension: Add new parameters by extending the ParamId_t enum and declaring new FlashParamNs::Parameter<T> members in src/ConfigParam.h.
  • Safety: All flash writes are atomic and checksum-protected, preventing corruption during unexpected power loss.

Frequently Asked Questions

How do I reset configuration parameters to factory defaults?

The ConfigParam::initialize() method automatically falls back to default values if the flash sector is erased or the checksum is invalid. To force a reset programmatically, call the inherited clear() method from the FlashParam base class (provided by pico_flash_param) before initialize(), or manually overwrite parameters with their default values and call finalize().

Can I store strings or arrays in flash configuration?

Yes. The FlashParamNs::Parameter<T> template supports arbitrary types, but you must specify the size in bytes when declaring the parameter. For example, a 32-character string would use FlashParamNs::Parameter<char[32]> or FlashParamNs::Parameter<uint8_t> with the size parameter set to 32. Ensure the total size of all parameters does not exceed the allocated flash sector.

Why are my configuration changes lost after reboot?

Changes are only lost if finalize() was not called before power loss. The UI mode only calls finalize() during clean shutdown via storeToFlash(). If you modify parameters in custom code (e.g., interrupt handlers or serial commands), you must explicitly call ConfigParam::instance().finalize() to persist the changes immediately.

Where is the flash sector physically located?

The pico_flash_param library manages the flash address automatically, reserving space at the end of the program flash or in a dedicated region defined by the linker script. You do not specify the raw flash address; the library handles sector erase, wear leveling, and alignment constraints internally.

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 →