No more EEPROM with Giga R1, how to use flash

I was previously using an UNO and my code for a weight scale was using the EEPROM to store the calibration values so that they would save and remain during power cycles. Now I'm switching over to a Giga R1 and it doesn't seem to have an EEPROM like the previous UNO did.

Any ideas on how I can implement the EEPROM functionality in the Giga R1?

Hi @rangerman24. The GIGA R1 WiFi board's core is based on the Mbed OS operating system. Mbed OS provides a "KVStore" API which allows you to store data in the non-volatile flash memory.

Some examples of how to use the Mbed OS "KVStore" API in Arduino sketches are provided here:

and here:

Even though those discussions were about other boards, they are equally applicable to the GIGA R1 WiFi since the KVStore API is available when using any of these Arduino boards that have an Mbed OS-based core.

1 Like

Thank you for the quick reply! I'm not super familiar with this KVStore. I'm not having much luck with using it yet, but will continue to try and learn it.

Below is the code I was using where the EEPROM was still part of it for the UNO. Do you have any suggestions on how to modify this code to remove the EEPROM functions and add the KVStore functions?

/*
  Use the Qwiic Scale to read load cells and scales
  By: Nathan Seidle @ SparkFun Electronics
  Date: March 3rd, 2019
  License: This code is public domain but you buy me a beer if you use this
  and we meet someday (Beerware license).

  This example shows how to setup a scale complete with zero offset (tare),
  and linear calibration.

  If you know the calibration and offset values you can send them directly to
  the library. This is useful if you want to maintain values between power cycles
  in EEPROM or Non-volatile memory (NVM). If you don't know these values then
  you can go through a series of steps to calculate the offset and calibration value.

  Background: The IC merely outputs the raw data from a load cell. For example, the
  output may be 25776 and change to 43122 when a cup of tea is set on the scale.
  These values are unitless - they are not grams or ounces. Instead, it is a
  linear relationship that must be calculated. Remeber y = mx + b?
  If 25776 is the 'zero' or tare state, and 43122 when I put 15.2oz of tea on the
  scale, then what is a reading of 57683 in oz?

  (43122 - 25776) = 17346/15.2 = 1141.2 per oz
  (57683 - 25776) = 31907/1141.2 = 27.96oz is on the scale

  SparkFun labored with love to create this code. Feel like supporting open
  source? Buy a board from SparkFun!
  https://www.sparkfun.com/products/15242

  Hardware Connections:
  Plug a Qwiic cable into the Qwiic Scale and a RedBoard Qwiic
  If you don't have a platform with a Qwiic connection use the SparkFun Qwiic Breadboard Jumper (https://www.sparkfun.com/products/14425)
  Open the serial monitor at 115200 baud to see the output
*/

#include <Wire.h>
#include <EEPROM.h> //Needed to record user settings

#include "SparkFun_Qwiic_Scale_NAU7802_Arduino_Library.h" // Click here to get the library: http://librarymanager/All#SparkFun_NAU8702

NAU7802 myScale; //Create instance of the NAU7802 class

//EEPROM locations to store 4-byte variables
#define EEPROM_SIZE 100 //Allocate 100 bytes of EEPROM
#define LOCATION_CALIBRATION_FACTOR 0 //Float, requires 4 bytes of EEPROM
#define LOCATION_ZERO_OFFSET 10 //Must be more than 4 away from previous spot. int32_t, requires 4 bytes of EEPROM

bool settingsDetected = false; //Used to prompt user to calibrate their scale

//Create an array to take average of weights. This helps smooth out jitter.
#define AVG_SIZE 4
float avgWeights[AVG_SIZE];
byte avgWeightSpot = 0;

void setup()
{
  EEPROM.begin(EEPROM_SIZE); //Some platforms need this. Comment this line if needed

  Serial.begin(115200);
  Serial.println("Qwiic Scale Example");

  Wire.begin();
  Wire.setClock(400000); //Qwiic Scale is capable of running at 400kHz if desired

  if (myScale.begin() == false)
  {
    Serial.println("Scale not detected. Please check wiring. Freezing...");
    while (1);
  }
  Serial.println("Scale detected!");

  readSystemSettings(); //Load zeroOffset and calibrationFactor from EEPROM

  myScale.setSampleRate(NAU7802_SPS_320); //Increase to max sample rate
  myScale.calibrateAFE(); //Re-cal analog front end when we change gain, sample rate, or channel 

  Serial.print("Zero offset: ");
  Serial.println(myScale.getZeroOffset());
  Serial.print("Calibration factor: ");
  Serial.println(myScale.getCalibrationFactor());

  Serial.println("\r\nPress 't' to Tare or Zero the scale.");
}

void loop()
{
  if (myScale.available() == true)
  {
    int32_t currentReading = myScale.getReading();
    float currentWeight = myScale.getWeight();

    Serial.print("Reading: ");
    Serial.print(currentReading);
    Serial.print("\tWeight: ");
    Serial.print(currentWeight, 2); //Print 2 decimal places

    avgWeights[avgWeightSpot++] = currentWeight;
    if(avgWeightSpot == AVG_SIZE) avgWeightSpot = 0;

    float avgWeight = 0;
    for (int x = 0 ; x < AVG_SIZE ; x++)
      avgWeight += avgWeights[x];
    avgWeight /= AVG_SIZE;

    Serial.print("\tAvgWeight: ");
    Serial.print(avgWeight, 2); //Print 2 decimal places

    if(settingsDetected == false)
    {
      Serial.print("\tScale not calibrated. Press 'c'.");
    }

    Serial.println();
  }

  if (Serial.available())
  {
    byte incoming = Serial.read();

    if (incoming == 't') //Tare the scale
      myScale.calculateZeroOffset();
    else if (incoming == 'c') //Calibrate
    {
      calibrateScale();
    }
  }
}

//Gives user the ability to set a known weight on the scale and calculate a calibration factor
void calibrateScale(void)
{
  Serial.println();
  Serial.println();
  Serial.println(F("Scale calibration"));

  Serial.println(F("Setup scale with no weight on it. Press a key when ready."));
  while (Serial.available()) Serial.read(); //Clear anything in RX buffer
  while (Serial.available() == 0) delay(10); //Wait for user to press key

  myScale.calculateZeroOffset(64); //Zero or Tare the scale. Average over 64 readings.
  Serial.print(F("New zero offset: "));
  Serial.println(myScale.getZeroOffset());

  Serial.println(F("Place known weight on scale. Press a key when weight is in place and stable."));
  while (Serial.available()) Serial.read(); //Clear anything in RX buffer
  while (Serial.available() == 0) delay(10); //Wait for user to press key

  Serial.print(F("Please enter the weight, without units, currently sitting on the scale (for example '4.25'): "));
  while (Serial.available()) Serial.read(); //Clear anything in RX buffer
  while (Serial.available() == 0) delay(10); //Wait for user to press key

  //Read user input
  float weightOnScale = Serial.parseFloat();
  Serial.println();

  myScale.calculateCalibrationFactor(weightOnScale, 64); //Tell the library how much weight is currently on it
  Serial.print(F("New cal factor: "));
  Serial.println(myScale.getCalibrationFactor(), 2);

  Serial.print(F("New Scale Reading: "));
  Serial.println(myScale.getWeight(), 2);

  recordSystemSettings(); //Commit these values to EEPROM

  settingsDetected = true;
}

//Record the current system settings to EEPROM
void recordSystemSettings(void)
{
  //Get various values from the library and commit them to NVM
  EEPROM.put(LOCATION_CALIBRATION_FACTOR, myScale.getCalibrationFactor());
  EEPROM.put(LOCATION_ZERO_OFFSET, myScale.getZeroOffset());

  EEPROM.commit(); //Some platforms need this. Comment this line if needed
}

//Reads the current system settings from EEPROM
//If anything looks weird, reset setting to default value
void readSystemSettings(void)
{
  float settingCalibrationFactor; //Value used to convert the load cell reading to lbs or kg
  int32_t settingZeroOffset; //Zero value that is found when scale is tared

  //Look up the calibration factor
  EEPROM.get(LOCATION_CALIBRATION_FACTOR, settingCalibrationFactor);
  if (settingCalibrationFactor == 0xFFFFFFFF)
  {
    settingCalibrationFactor = 1.0; //Default to 1.0
    EEPROM.put(LOCATION_CALIBRATION_FACTOR, settingCalibrationFactor);
  }

  //Look up the zero tare point
  EEPROM.get(LOCATION_ZERO_OFFSET, settingZeroOffset);
  if (settingZeroOffset == 0xFFFFFFFF)
  {
    settingZeroOffset = 0; //Default to 0 - i.e. no offset
    EEPROM.put(LOCATION_ZERO_OFFSET, settingZeroOffset);
  }

  //Pass these values to the library
  myScale.setCalibrationFactor(settingCalibrationFactor);
  myScale.setZeroOffset(settingZeroOffset);

  settingsDetected = true; //Assume for the moment that there are good cal values
  if (settingCalibrationFactor == 1.0 || settingZeroOffset == 0)
    settingsDetected = false; //Defaults detected. Prompt user to cal scale.
}

It looks like you are storing a float and a int32_t

#define LOCATION_CALIBRATION_FACTOR 0 //Float, requires 4 bytes of EEPROM
#define LOCATION_ZERO_OFFSET 10 //Must be more than 4 away from previous spot. int32_t, requires 4 bytes of EEPROM

Follow the example in the referenced code to save struct.

I suggest that you get started by writing a simple test code, to run on the Giga, to store and retrieve your scale data in a struct using KVStore.

When you have that working, then patching it into the full code will be very simple.

I have a similar need and want to clarify a couple of points before I jump head first into this approach.

  1. Where is the persistent storage utilized by KVStore located?
  2. Is it affected (erased) by any of these: downloading a new sketch to internal FLASH, by downloading an OTA file to a FATFS file on QSPI FLASH, by repartitioning the QSPI flash?
  3. I need to store a 2x10 array of strings, ie SSID and Password for up to 10 WiFi networks. Is there a recommended way to serialize all these potentially variable length strings? I'm considering creating a class to hold the 10 sets of credentials and creating my own serialized format. But this seems like reinventing a very common wheel on the GIGA with WiFi.
  4. What's the KVP Update process? Do I just write new (differently sized) data to the existing key? Or do I need to delete/erase the existing KVP in some way before writing a new one? There are lots of examples of Set and Get but I haven't seen one for Update yet.
    Ideas, suggestions, pointers?

Hi Joe.
I'm using TDBStore which is part of the KVStore class family (same interface). I prefer it as you allocate a raw partition of the external QSPI dedicated to it. I'll answer your Qs as they relate to TDBStore as I didn't even try KVStore.

On the external QSPI flash. It supports up to 4 partitions so I have created:

QSPIFBlockDevice root;
mbed::MBRBlockDevice wifi_data(&root, WIFI_PRTN); // 1MB
mbed::MBRBlockDevice  ota_data(&root,  OTA_PRTN); // 5MB
mbed::MBRBlockDevice user_data(&root, USER_PRTN); // 4MB
mbed::MBRBlockDevice  tdb_data(&root,  TDB_PRTN); // 4MB

The first three are FAT32 while TDB is a raw partition.

No, No, possibly Yes, depending on what you do.

It's just a key/value store and the value can be anything. I use it to store individual strings like SSID and PASS

result = config.get(tdb_WiFiSID, &wifiSid, PRF_WIFI_SID_LEN);
result = config.get(tdb_WiFiPWD, &wifiPwd, PRF_WIFI_PWD_LEN);

but also complex structures:

result = config.get(tdb_profile, reinterpret_cast<uint8_t*>(&activeSet), sizeof(activeSet));

It's just gets and sets. When setting, a new record is written and the old marked for garbage collection. This is part of how it achieves wear-levelling.
There are functions to interate over keys and incremental adds if you want to get fancy:
https://os.mbed.com/docs/mbed-os/v6.16/mbed-os-api-doxy/classmbed_1_1_t_d_b_store.html
There's details of the internal design here:
https://iot.sites.arm.com/open-iot-sdk/libraries/storage/md_kvstore_docs_TDBStore_TDBStore_design.html

p.s. With TDB you don't need any of the /kv/ business in the key names, just a char array:

const char tdb_WiFiSID[]  = "WiFiSID";

Steve Thanks so much for the comments and all your great insights. I really like your approach and I may try to do something similar.

How did you partition the QSPIF to get that fourth partition? The tools I've seen only had two modes for 2 or 3 partitions.

Is config a class you wrote that wraps the tab get and set functions?

Are you dealing with fixed length strings such that things like PRF_WIFI_SID_LEN are all constants or are you using Info calls to learn the size of the key's value for the get call?

I used this sketch to create partitions 2, 3 and 4, leaving partition 1 (WiFi) untouched:

#include "QSPIFBlockDevice.h"
#include "MBRBlockDevice.h"
#include "FATFileSystem.h"

QSPIFBlockDevice root(QSPI_SO0, QSPI_SO1, QSPI_SO2, QSPI_SO3,  QSPI_SCK, QSPI_CS, QSPIF_POLARITY_MODE_1, 40000000);
mbed::MBRBlockDevice ota_data(&root, 2);
mbed::MBRBlockDevice user_data(&root, 3);
mbed::MBRBlockDevice tdb_data(&root, 4);
mbed::FATFileSystem ota_data_fs("fs");
mbed::FATFileSystem user_data_fs("user");

void setup() {
  int err;
  Serial.begin(115200);
  while (!Serial);

  mbed::MBRBlockDevice::partition(&root, 2, 0x0B,  1 * 1024 * 1024,  6 * 1024 * 1024);
  mbed::MBRBlockDevice::partition(&root, 3, 0x0B,  6 * 1024 * 1024, 10 * 1024 * 1024);
  mbed::MBRBlockDevice::partition(&root, 4, 0x0B, 10 * 1024 * 1024, 14 * 1024 * 1024);

  err = ota_data_fs.reformat(&ota_data);
  if (err) {
    Serial.println("Error formatting ota partition");
    return;
  }
  err = user_data_fs.reformat(&user_data);
  if (err) {
    Serial.println("Error formatting user partition");
    return;
  }

  Serial.println("It's now safe to reboot or disconnect your board.");
}

void loop() {

}

No, it's just the object created from the class. This is basically all you need to do once the tdb partition exists:

#include <QSPIFBlockDevice.h>
#include <MBRBlockDevice.h>
#include <TDBStore.h>

QSPIFBlockDevice root;
mbed::MBRBlockDevice tdb_data(&root,  4);
mbed::TDBStore config(&tdb_data);

const char tdb_WiFiSID[] = "WiFiSID";
char wifiSid[20];

void setup() {
  config.init();
  config.get(tdb_WiFiSID, &wifiSid, sizeof(wifiSid));
}

void loop() {
  // put your main code here, to run repeatedly:

}

I use all fixed lengths. It's a bit wasteful but there's more space in the partition than I could ever need. I just use the null terminator in char [] to determine actual length.

One reason I went for tdb over kv was I couldn't (like yourself) work out where /kv/ was created. Each platform has its own implementation with the global api and the examples I'd seen suggested it was using the internal flash (on some boards anyway). I'd seen some posts of those using kv that had lost or corrupted data. I feared sketches were overwriting the /kv/. I may dig into the /kv/ implementation on the GIGA when I get the urge but tdb is ideal for me with this board.

p.s. If you add this to your sketch you can see the partitions on your PC when USB connected.

#include <PluggableUSBMSD.h>
USBMSD MassStorage(&root);

I can confirm that, with the exception of the EDGE_CONTROL board, default storage configuration values are used for all boards using the ArduinoCore-mbed. The defaults place the /kv/ store in the internal flash at the end of your sketch.

The mbed doc below details the config options and their defaults. It confirms, as I feared:

"For this configuration, please define the section of the internal storage that will be used for data, by defining these parameters in your app.config file: internal_base_address and internal_size. If not defined, the storage will start in the first sector immediately after the end of the application. This can reduce the ability to update the application with a bigger one."

https://os.mbed.com/docs/mbed-os/v6.16/apis/data-options-and-config.html

2 Likes

Steve Sorry it took me a bit to get back to you. I've been focused on building up the latest hardware boards I got back from the fab house. I really really appreciate all of the valuable info you've sent my way. I had done some very basic tests to see if uploading a new sketch destroyed the KVstore and it did not. But I didn't try making the new upload significantly larger than the initial one that established the KV store.

It's interesting that a default KVstore has a single initial key-value named TDBS. And as such I wonder if it introduces any dependency of TDBS to the location or persistence of the KVstore.

The default storage option for kv is TDB_INTERNAL. Others include fat/littlefs filesystems, SD card, securestore. My deployment uses TDBStore directly (TDB_EXTERNAL on QSPI) which is possible with kv_global but would require config changes and a recomp of the mbed lib.