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:

7 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