Saving values to Flash memory using PersistentStorage class on Daisy Pod

Here is an example of using the PersistentStorage class to save parameter values to flash using the daisy Pod. Storing the values in non volatile memory allows them to be accessed between power cycles. This allows for storing presets or settings of knobs used to control multiple parameters

I enjoy making posts like this because not only do I think examples like these are helpful to the less experienced in the community (and also few and far between), but it also helps me to better learn these concepts myself.

PersistentStorage is a class in LibDaisy that stores a user defined struct in the seeds external flash memory. It also keeps a local copy stored in the RAM. In this example, two parameters of a reverb effect are stored, DryWet mix level and the feedback. These parameters are set using the pods two potentiometers. Button 2 is used to save the current settings. Button 1 is used to switch between the saved preset or knob control of the parameters. LED1 is set red when the reverb parameters are read from the knobs and green when the saved settings are being used. Here is the code:

#include "daisy_pod.h"
#include "daisysp.h"

using namespace daisy;
using namespace daisysp;

DaisyPod hw;

//Setting Struct containing parameters we want to save to flash
struct Settings {
	float p1; // DryWet
	float p2; // Feedback

	//Overloading the != operator
	//This is necessary as this operator is used in the PersistentStorage source code
	bool operator!=(const Settings& a) const {
        return !(a.p1==p1 && a.p2==p2);
    }
};

//Persistent Storage Declaration. Using type Settings and passed the devices qspi handle
PersistentStorage<Settings> SavedSettings(hw.seed.qspi);

//Reverb and Parameters
ReverbSc Verb;
float DryWet;
Parameter Feedback;

bool use_preset = false;
bool trigger_save = false;


void Load() {

	//Reference to local copy of settings stored in flash
	Settings &LocalSettings = SavedSettings.GetSettings();
	
	DryWet = LocalSettings.p1;
	Verb.SetFeedback(LocalSettings.p2);

	use_preset = true;
}

void Save() {

	//Reference to local copy of settings stored in flash
	Settings &LocalSettings = SavedSettings.GetSettings();

	LocalSettings.p1 = hw.knob1.Process();
	LocalSettings.p2 = Feedback.Value();

	trigger_save = true;
}

void ProcessControls() {

	hw.ProcessAllControls();

	//Switches
	if(hw.button1.RisingEdge()){
		if(use_preset)
			use_preset = false;
		else 
			Load();
	}

	if(hw.button2.RisingEdge()){
		Save();
	}

	//Knobs
	if(!use_preset){
		Verb.SetFeedback(Feedback.Process());
		DryWet = hw.knob1.Process();
	}

	//LEDs
	if(use_preset)
		hw.led1.Set(0, 1, 0); // green
	else 
		hw.led1.Set(1, 0, 0); // red

	if(trigger_save)
		hw.led2.Set(0, 1, 0); // green
	else 
		hw.led2.Set(0, 0, 0); // red

	hw.UpdateLeds();

}


void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size)
{
	float inl, inr, outl, outr;
	ProcessControls();

	for (size_t i = 0; i < size; i++)
	{
		inl = in[0][i];
		inr = in[1][i];

		Verb.Process(inl, inr, &outl, &outr);
    	out[0][i] = DryWet * outl + (1 - DryWet) * inl;
    	out[1][i] = DryWet * outr + (1 - DryWet) * inr;
	}
}

int main(void)
{
	hw.Init();
	hw.SetAudioBlockSize(4); // number of samples handled per callback
	hw.SetAudioSampleRate(SaiHandle::Config::SampleRate::SAI_48KHZ);

	Verb.Init(hw.AudioSampleRate());
	Feedback.Init(hw.knob2, 0.0f, 1.0f, daisy::Parameter::LINEAR);
	
	//Initilize the PersistentStorage Object with default values.
	//Defaults will be the first values stored in flash when the device is first turned on. They can also be restored at a later date using the RestoreDefaults method
	Settings DefaultSettings = {0.0f, 0.0f};
	SavedSettings.Init(DefaultSettings);

	hw.StartAdc();
	hw.StartAudio(AudioCallback);
	while(1) {
		if(trigger_save) {
			
			SavedSettings.Save(); // Writing locally stored settings to the external flash
			trigger_save = false;
		}
		System::Delay(100);
	}
}

First, the settings struct type is defined. This type is passed to the class template PesistentStorage. The settings struct should contain all the values we want to save in flash. It is necessary to overload the != operator for the struct as this is used in the PersistentStorage source code. Its used to check that the locally stored settings struct is not equal to the struct stored in flash before the latter is overwritten. This can be defined as you see fit in order to determine which parameter changes are necessary before a saved preset can be overwritten.

//Setting Struct containing parameters we want to save to flash
struct Settings {
	float p1; // DryWet
	float p2; // Feedback

	//Overloading the != operator
	//This is necessary as this operator is used in the PersistentStorage source code
	bool operator!=(const Settings& a) const {
        return !(a.p1==p1 && a.p2==p2);
    }
};

The PersistentStorage object is then defined using the settings struct type and also the devices QSPI handle. QSPI is the mechanism the processor uses to communicate with the external flash memory. In the main function, our PersistentStorage object, SavedSettings, is initialized. The initialization method is passed the default values of the parameters. These will be the first values stored in flash when the device is first booted after programming. They can also always be restored later using the RestoreDefaults() method.

The locally stored settings can then be accessed using the GetSettings() method which returns a reference to the PersistentClasses internal settings struct. These can then be read and written to as seen in the Load() and Save() functions.

Finally, any edits made to the locally stored settings can be saved to flash using the Save)() method. This method can not be called from the audio callback as it takes quite a while and uses Delay which cant be used in the audio thread. Therefore, a bool is used as a trigger to initiate a save in the while loop of the main function as shown

	while(1) {
		if(trigger_save) {
			
			SavedSettings.Save(); // Writing locally stored settings to the external flash
			trigger_save = false;
		}
		System::Delay(100);
	}

I found it was necessary to add the Delay after the if block. Iā€™m not fully sure why this was the case. I can only assume that evaluating the condition too often causes problems. I havenā€™t experimented with the bounds of this condition but I found the 100ms delay to work fine.

I hope some one finds this helpful! Let me know if I got anything particularly egregious wrong :grin:

8 Likes

Awesome tutorial, thanks for sharing. I was completely oblivious thatā€™s even possible.

A bit off-topic, but I am curious if there is a way to implement simple multi-threading and async/await in C++. If possible, all calls that take long time, such as Save/Load, can be called as await Save().

Thereā€™s nothing like that in libDaisy, but itā€™s not really a big problem. The time-critical stuff (audio) is driven by interrupts, and thereā€™s not much besides storage that takes much time.

Microcontroller programming is a different discipline than programming on top of an OS.

Thank you so much for writing this tutorial and sharing it with the community :slight_smile:

1 Like

@mrahc626 Great work!
Iā€™m having a problem though. After saving my data, if I change the structure of the saved data during development, for example by renaming a property, the PersistentStorage fails to do the Init.
This is because in init it tries to reload the data and the cast fails.

Have you had this problem?

Andrea

So correct me if Iā€™m wrong but you are changing the settings struct during development and then after re-flashing your device, the PersistentStorage Init() fails cause it attempts to read what remains from before and cant cast?

This seems strange to me. I would assume when you flash a new program that uses PersistentStorage that space in flash is simply reserved for it and any knowledge of what was there before is removed. Iā€™ve definitely been able to change the contents of the settings struct before without issue during development. Could you provide code demonstrating what exactly is causing the issue?

Could be the problem because Iā€™m using the address_offset_ = 0x40000?
Iā€™m seeing now that in your tutorial you are not setting the address_offset.

That definitely could be it! I never experimented with this

1 Like

That is definitely whatā€™s happening. The data stored in persistent storage persists across multiple flashes. Take a look at the RestoreDefaults(); function on the PersistantStorage class. What I like to do is store a storage ā€œversionNumberā€ as the very first field of my settings. If I change my settings structure I also change the ā€œversionNumberā€ and my code checks the versionNumber on first load of the settings and if itā€™s different that what the code is expecting, I automatically call RestoreDefaults to reset the storage. In the future this also makes it possible to read the versionNumber and parse and upgrade the data to a newer format if you want to provide a way to migrate data across versionNumbers.

2 Likes

Thanks for this tutorial, exactly what I needed now that I want to store my sequencer values.

One thing I stumble upon is ProcessAllControls which I donĀ“t have on the daisy seed.

Is it ok if I leave that part out, or how could it be replaced?

(hw.seed.qspi);

I had to change to hw.qspi, because ā€œclass daisy seed has no member seed.ā€

would be cool to adapt the tutorial to daisy seed users,

also I would only want to save arrays of values which I have created with an encoder,
so I would have problems adapting an example that only uses adc inputs for controlsā€¦

thanks!

I used a struct to store all the sequencer values easier @programchild:

struct MidiData {
    uint16_t midiNotes[8];  // Array to hold the MIDI notes
    uint16_t gateValues[8]; // Array to hold the gate values
};

// not sure Iā€™m doing this right, you have to overload the not equals operator, in the two param it looked quite simple, to expand to a multidimensional array was a little more complex.

struct Settings {
    // int banks = 8;
    // int layers = 2;
    // int sequences = 4;
    MidiData midiData[8][2][4]; // MIDI data

    // // Overloading the != operator
    bool operator!=(const Settings& a) const {
        // Compare each member for inequality
        for (size_t bank = 0; bank < 8; ++bank) {
            for (size_t layer = 0; layer < 2; ++layer) {
                for (size_t sequence = 0; sequence < 4; ++sequence) {
                    for (size_t i = 0; i < 8; ++i) {
                        if (midiData[bank][layer][sequence].midiNotes[i] != a.midiData[bank][layer][sequence].midiNotes[i] ||
                            midiData[bank][layer][sequence].gateValues[i] != a.midiData[bank][layer][sequence].gateValues[i]) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
};

then save and load look like:


void Load() {
    // Reference to local copy of settings stored in flash
    Settings &LocalSettings = SavedSettings.GetSettings();

    // Load MIDI data
    for (uint16_t bank = 0; bank < 8; ++bank) {
        for (uint16_t layer = 0; layer < 2; ++layer) {
            for (uint16_t seq = 0; seq < 4; ++seq) {

                for (uint16_t notes = 0; notes < 8; notes++)
                {
                    midiData[bank][layer][seq].midiNotes[notes] = LocalSettings.midiData[bank][layer][seq].midiNotes[notes];
                    midiData[bank][layer][seq].gateValues[notes] = LocalSettings.midiData[bank][layer][seq].gateValues[notes];
                }
                
            }
        }
    }
}

void Save() {
    // Reference to local copy of settings stored in flash
    Settings &LocalSettings = SavedSettings.GetSettings();

    // Save MIDI data
    for (uint16_t bank = 0; bank < 8; ++bank) {
        for (uint16_t layer = 0; layer < 2; ++layer) {
            for (uint16_t sequence = 0; sequence < 4; ++sequence) 
            {
                for (uint16_t notes = 0; notes < 8; notes++)
                {
                    LocalSettings.midiData[bank][layer][sequence].midiNotes[notes] = midiData[bank][layer][sequence].midiNotes[notes];
                    LocalSettings.midiData[bank][layer][sequence].gateValues[notes] = midiData[bank][layer][sequence].gateValues[notes];
                }
            }
        }
    }

    trigger_save = true;
}

do this before you main loop as described in the post above:

Settings DefaultSettings;
memcpy(DefaultSettings.midiData, midiData, sizeof(midiData));
SavedSettings.Init(DefaultSettings);
    

map load and save onto a button or something, and have a test.

SavedSettings.Save();
Save(); // Writing locally stored settings to the external flash

your part about process all controls is just a function in the pod/patch etc that goes through all the analog and digital pins and reads the values.

Small gripe:

Settings &LocalSettings

if prefer:

Settings &m_Settings

(member settings, or maybe this: localSettings, reading capitalised variables confuses me, as usually Iā€™d denote objects with capitals. Also apparently snake case is easier to read than camel case: https://www.cs.kent.edu/~jmaletic/papers/ICPC2010-CamelCaseUnderScoreClouds.pdf )

Thank you! I got it working with a single preset just fine.

Have you tried this with multiple presets already?

I am trying to do so, but while it recalls presets fine within one power cycle, presets will come up empty after I rebooted the daisy.

btw it comes up with my default values if I do this:

Settings &LocalSettings = presets[preset_index];

and it comes up with all zeros if I do this:

Settings &LocalSettings = SavedSettings.GetSettings();

So maybe I do something wrong with the structs?

//Setting Struct containing parameters we want to save to flash
    struct Settings {
    int p1[8]; // Sequence Pitch
    int p2[8]; // Beat
    int p3[8]; // Cutoff
    int p4[8]; // Decay
    int p5; //lfoFenv
    int p6; //lfoFamt
    int p7; //lfoAenv
    int p8; //lfoAamt
    int p9; //osc2pv
    int p10; //scale

	//Overloading the != operator
    //This is necessary as this operator is used in the PersistentStorage source code
    bool operator!=(const Settings& a) const {
    for (int i = 0; i < 8; ++i) {
        if (p1[i] != a.p1[i] || p2[i] != a.p2[i] || p3[i] != a.p3[i] || p4[i] != a.p4[i]
        || p5 != a.p5 || p6 != a.p6 || p7 != a.p7 || p8 != a.p8 || p9 != a.p9 || p10 != a.p10) 
        
        {
            return true;
        }
    }
    
}

};

declaring 10 presets

Settings presets[10];
int current_preset = 0;// Track the current preset index

loading/saving

void LoadPreset(int preset_index) {
    // Ensure SavedSettings.Init() has been called before Load()
    if (preset_index < 0 || preset_index >= 10) return; // Invalid preset index

    const Settings &LocalSettings = SavedSettings.GetSettings();
    for (int i = 0; i < 8; i++) {
        seqP[i] = LocalSettings.p1[i];
        beat[i] = LocalSettings.p2[i];
        seqD[i] = LocalSettings.p3[i];
        seDc[i] = LocalSettings.p4[i];
    }
    lfoFenv = LocalSettings.p5;
    lfoFamt = LocalSettings.p6;
    lfoAenv = LocalSettings.p7;
    lfoAamt = LocalSettings.p8;
    osc2pv = LocalSettings.p9;
    scale = LocalSettings.p10;
}
 
void SavePreset(int preset_index) {
    if (preset_index < 0 || preset_index >= 10) return; // Invalid preset index
    Settings &LocalSettings = SavedSettings.GetSettings();
    for (int i = 0; i < 8; i++) {
        LocalSettings.p1[i] = seqP[i];
        LocalSettings.p2[i] = beat[i];
        LocalSettings.p3[i] = seqD[i];
        LocalSettings.p4[i] = seDc[i];
    }
    LocalSettings.p5 = lfoFenv;
    LocalSettings.p6 = lfoFamt;
    LocalSettings.p7 = lfoAenv;
    LocalSettings.p8 = lfoAamt;
    LocalSettings.p9 = osc2pv;
    LocalSettings.p10 = scale;
    
    trigger_save = true;
}

defaults

   // Initialize PersistentStorage with default settings
    Settings DefaultSettings = {{12, 24, 48, 24, 12, 0, 0, 0}, {0, 1, 0, 1, 0, 1, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0},0,0,0,0,0,0};
    for(int i = 0; i < 10; i++) {
        presets[i] = DefaultSettings;
    }

   

    // Load settings on boot
    LoadPreset(current_preset);

and ontop of while(1);

if(trigger_save) {
            SavedSettings.Save(); // Writing locally stored settings to the external flash
            trigger_save = false;
            }
            ProcessControls();

I really am out of ideas what to try next :wink:

Looks to me like you are setting up PersistentStorage to handle a single preset, but you really want it to handle an array of presets.

Iā€™d create a struct which contains an array of presets.

Hi,
after I got this working fine in another project,
in my current project trying to save a preset will freeze the daisy.

I am doing it in the same way as before, but now with 2 dimensional arrays.
I am hypnotizing the code, but I cannot see the mistake.

Could it have to do with the 64kb reserved in qspi when I boot from SRAM?
The debugger takes me to the SRAM adress when I hit save.

struct:

struct Settings {
float p[5][6];
int modAssign[3][6];
int ModSource[3][8];

	//Overloading the != operator
    //This is necessary as this operator is used in the PersistentStorage source code
    bool operator!=(const Settings& a) const {
        for (int i = 0; i < 5; i++) 
        {
            for (int y = 0; y < 6; y++) {
            if (p[i][y] != a.p[i][y]) {
                return true;
            }
            }
        }
                for (int i = 0; i < 3; i++) 
        {
            for (int y = 0; y < 6; y++) {
            if (modAssign[i][y] != a.modAssign[i][y]) {
                return true;
            }
            }
        }
                for (int i = 0; i < 3; i++) 
        {
            for (int y = 0; y < 8; y++) {
            if (ModSource[i][y] != a.ModSource[i][y]) {
                return true;
            }
            }
        }
        return false;
    }
};

void Load / Save

PersistentStorage<Settings>storage(hw.qspi);

bool trigger_save = false;

void Load() {
    // Ensure storage.Init() has been called before Load()
    storage.Save();
    Settings &LocalSettings = storage.GetSettings();
    //current_preset = LocalSettings.current_preset;
    for (int i = 0; i < 5; i++) 
    {
        for (int y = 0; y < 6; y++) 
    {
       p[i][y] = LocalSettings.p[i][y];
        
    }
    }
    for (int i = 0; i < 3; i++) 
    {
        for (int y = 0; y < 6; y++) 
    {
        modAssign[i][y] = LocalSettings.modAssign[i][y];
    }
    }
        for (int i = 0; i < 3; i++) 
    {
        for (int y = 0; y < 8; y++) 
    {
        ModSource[i][y] = LocalSettings.ModSource[i][y];
    }
    }
}


void Save() {
   Settings &LocalSettings = storage.GetSettings();
//    LocalSettings.current_preset = current_preset;
    for (int i = 0; i < 5; i++) {
        for (int y = 0; y < 6; y++) 
    {
        LocalSettings.p[i][0] = p[i][0];       
    }
    }
        for (int i = 0; i < 3; i++) 
    {
        for (int y = 0; y < 6; y++) 
    {
        LocalSettings.modAssign[i][y] = modAssign[i][y];
    }
    }
        for (int i = 0; i < 3; i++) 
    {
        for (int y = 0; y < 8; y++) 
    {
        LocalSettings.ModSource[i][y] = ModSource[i][y];
    }
    }
    trigger_save = true;
}

Load / Save Buttons:

        if (encB[1]) {
            Load();
                //Settings &LocalSettings = storage.GetSettings();
                //LocalSettings.current_preset = current_preset;
                storage.Save();
                display.Fill(false);
                display.SetCursor(38, 24);
                display.WriteString("Loaded", Font_11x18, true);
                display.Update();
        }
            
            else if(encB[2]){
                Save();
                display.Fill(false);
                display.SetCursor(38, 24);
                display.WriteString("Saved", Font_11x18, true);
                display.Update();
        }

Init:

     // Load settings on boot
    Settings DefaultSettings;
    //memset(&DefaultSettings, 0, sizeof(Settings));
    DefaultSettings = {0};
    storage.Init(DefaultSettings);
    Load();

and execute save:

    while(1) {
        if(trigger_save) {
                storage.Save(); // Writing locally stored settings to the external flash
                trigger_save = false;
                System::Delay(10);
            }

I moved the initialisation right up after the hardware init and
gave it a 64kb address offset.

It saved once (and recalls upon startup) but then crashes again on every save attempt.

int main(void)
{
    hw.Configure();
    hw.Init(true);
    hw.SetAudioBlockSize(32);
    hw.SetAudioSampleRate(SaiHandle::Config::SampleRate::SAI_48KHZ);
    float sample_rate = hw.AudioSampleRate();

    
         // Load settings on boot
    Settings DefaultSettings;
    memset(&DefaultSettings, 0, sizeof(Settings));
    //DefaultSettings = {0};
    storage.Init(DefaultSettings, 0x10000);
    Load();

hi @programchild, how did you get that to work? Iā€™m using the seed and when I use hw.qspi I get an error: ā€˜class DaisyHardwareā€™ has no member named ā€˜qspiā€™. Iā€™ve tried many different things, but didnā€™t get it to work. What are you doing that Iā€™m missing? Thanks

Could it be because Iā€™m using Arduino IDE?