pw_persistent_ram

The pw_persistent_ram module contains utilities and containers for using persistent RAM. By persistent RAM we are referring to memory which is not initialized across reboots by the hardware nor bootloader(s). This memory may decay or bit rot between reboots including brownouts, ergo integrity checking is highly recommended.

Note

This is something that not all architectures and applications built on them support and requires hardware in the loop testing to verify it works as intended.

Warning

Do not treat the current containers provided in this module as stable storage primitives. We are still evaluating lighterweight checksums from a code size point of view. In other words, future updates to this module may result in a loss of persistent data across software updates.

Persistent RAM Placement

Persistent RAM is typically provided through specially carved out linker script sections and/or memory ranges which are located in such a way that any bootloaders and the application boot code do not clobber it.

  1. If persistent linker sections are provided, we recommend using our section placement macro. For example imagine the persistent section name is called .noinit, then you could instantiate an object as such:

    #include "pw_persistent_ram/persistent.h"
    #include "pw_preprocessor/compiler.h"
    
    using pw::persistent_ram::Persistent;
    
    PW_KEEP_IN_SECTION(".noinit") Persistent<bool> persistent_bool;
    
  2. If persistent memory ranges are provided, we recommend using a struct to wrap the different persisted objects. This then could be checked to fit in the provided memory range size, for example by asserting against variables provided through a linker script.

    #include "pw_assert/check.h"
    #include "pw_persistent_ram/persistent.h"
    
    // Provided for example through a linker script.
    extern "C" uint8_t __noinit_begin;
    extern "C" uint8_t __noinit_end;
    
    struct PersistentData {
      Persistent<bool> persistent_bool;
    };
    PersistentData& persistent_data =
      *reinterpret_cast<NoinitData*>(&__noinit_begin);
    
    void CheckPersistentDataSize() {
      PW_DCHECK_UINT_LE(sizeof(PersistentData),
                        __noinit_end - __noinit_begin,
                        "PersistentData overflowed the noinit memory range");
    }
    

Persistent RAM Lifecycle Management

In order for persistent RAM containers to be as useful as possible, any invalidation of persistent RAM and the containers therein should be executed before the global static C++ constructors, but after the BSS and data sections are initialized in RAM.

The preferred way to clear Persistent RAM is to simply zero entire persistent RAM sections and/or memory regions. Pigweed’s persistent containers have picked integrity checks which work with zeroed memory, meaning they do not hold a value after zeroing. Alternatively containers can be individually cleared.

The boot sequence itself is tightly coupled to the number of persistent sections and/or memory regions which exist in the final image, ergo this is something which Pigweed cannot provide to the user directly. However, we do recommend following some guidelines:

  1. Do not instantiate regular types/objects in persistent RAM, ensure integrity checking is always used! This is a major risk with this technique and can lead to unexpected memory corruption.

  2. Always instantiate persistent containers outside of the objects which depend on them and use dependency injection. This permits unit testing and avoids placement accidents of persistents and/or their users.

  3. Always erase persistent RAM data after software updates unless the persistent storage containers are explicitly stored at fixed address and with a fixed layout. This prevents use of swapped objects or their members where the same integrity checks are used.

  4. Consider zeroing persistent RAM to recover from crashes which may be induced by persistent RAM usage, for example by checking the reboot/crash reason.

  5. Consider zeroing persistent RAM on cold boots to always start from a consistent state if persistence is only desired across warm reboots. This can create determinism from cold boots when using for example DRAM.

  6. Consider an explicit persistent clear request which can be set before a warm reboot as a signal to zero all persistent RAM on the next boot to emulate persistent memory loss in a threadsafe manner.

pw::persistent_ram::Persistent<T>

The Persistent is a simple container for holding its templated value T with CRC16 integrity checking. Note that a Persistent will be lost if a write/set operation is interrupted or otherwise not completed, as it is not double buffered.

The default constructor does nothing, meaning it will result in either invalid state initially or a valid persisted value from a previous session.

The destructor does nothing, ergo it is okay if it is not executed during shutdown.

Example: Storing an integer

A common use case of persistent data is to track boot counts, or effectively how often the device has rebooted. This can be useful for monitoring how many times the device rebooted and/or crashed. This can be easily accomplished using the Persistent container.

#include "pw_persistent_ram/persistent.h"
#include "pw_preprocessor/compiler.h"

using pw::persistent_ram::Persistent;

class BootCount {
 public:
  explicit BootCount(Persistent<uint16_t>& persistent_boot_count)
      : persistent_(persistent_boot_count) {
    if (!persistent_.has_value()) {
      persistent_ = 0;
    } else {
      persistent_ = persistent_.value() + 1;
    }
    boot_count_ = persistent_.value();
  }

  uint16_t GetBootCount() { return boot_count_; }

 private:
  Persistent<uint16_t>& persistent_;
  uint16_t boot_count_;
};

PW_KEEP_IN_SECTION(".noinit") Persistent<uint16_t> persistent_boot_count;
BootCount boot_count(persistent_boot_count);

int main() {
  const uint16_t boot_count = boot_count.GetBootCount();
  // ... rest of main
}

Example: Storing larger objects

Larger objects may be inefficient to copy back and forth due to the need for a working copy. To work around this, you can get a Mutator handle that provides direct access to the underlying object. As long as the Mutator is in scope, it is invalid to access the underlying Persistent, but you’ll be able to directly modify the object in place. Once the Mutator goes out of scope, the Persistent object’s checksum is updated to reflect the changes.

#include "pw_persistent_ram/persistent.h"
#include "pw_preprocessor/compiler.h"

using pw::persistent_ram::Persistent;

contexpr size_t kMaxReasonLength = 256;

struct LastCrashInfo {
  uint32_t uptime_ms;
  uint32_t boot_id;
  char reason[kMaxReasonLength];
}

PW_KEEP_IN_SECTION(".noinit") Persistent<LastBootInfo> persistent_crash_info;

void HandleCrash(const char* fmt, va_list args) {
  // Once this scope ends, we know the persistent object has been updated
  // to reflect changes.
  {
    auto& mutable_crash_info =
        persistent_crash_info.mutator(GetterAction::kReset);
    vsnprintf(mutable_crash_info->reason,
              sizeof(mutable_crash_info->reason),
              fmt,
              args);
    mutable_crash_info->uptime_ms = system::GetUptimeMs();
    mutable_crash_info->boot_id = system::GetBootId();
  }
  // ...
}

int main() {
  if (persistent_crash_info.has_value()) {
    LogLastCrashInfo(persistent_crash_info.value());
    // Clear crash info once it has been dumped.
    persistent_crash_info.Invalidate();
  }

  // ... rest of main
}

pw::persistent_ram::PersistentBuffer

The PersistentBuffer is a persistent storage container for variable-length serialized data. Rather than allowing direct access to the underlying buffer for random-access mutations, the PersistentBuffer is mutable through a PersistentBufferWriter that implements the pw::stream::Writer interface. This removes the potential for logical errors due to RAII or open()/close() semantics as both the PersistentBuffer and PersistentBufferWriter can be used validly as long as their access is serialized.

Example

An example use case is emitting crash handler logs to a buffer for them to be available after a the device reboots. Once the device reboots, the logs would be emitted by the logging system. While this isn’t always practical for plaintext logs, tokenized logs are small enough for this to be useful.

#include "pw_persistent_ram/persistent_buffer.h"
#include "pw_preprocessor/compiler.h"

using pw::persistent_ram::PersistentBuffer;
using pw::persistent_ram::PersistentBuffer::PersistentBufferWriter;

PW_KEEP_IN_SECTION(".noinit") PersistentBuffer<2048> crash_logs;
void CheckForCrashLogs() {
  if (crash_logs.has_value()) {
    // A function that dumps sequentially serialized logs using pw_log.
    DumpRawLogs(crash_logs.written_data());
    crash_logs.clear();
  }
}

void HandleCrash(CrashInfo* crash_info) {
  PersistentBufferWriter crash_log_writer = crash_logs.GetWriter();
  // Sets the pw::stream::Writer that pw_log should dump logs to.
  crash_log_writer.clear();
  SetLogSink(crash_log_writer);
  // Handle crash, calling PW_LOG to log useful info.
}

int main() {
  void CheckForCrashLogs();
  // ... rest of main
}

Size Report

The following size report showcases the overhead for using Persistent. Note that this is templating the Persistent only on a uint32_t, ergo the cost without pw_checksum’s CRC16 is the approximate cost per type.

Label

Segment

Before

Delta

After

Persistent including pw_checksum’s CRC16

FLASH
20,616
+640
21,256

Persistent without pw_checksum’s CRC16

FLASH
21,184
+72
21,256

Compatibility

  • C++17

Dependencies

  • pw_checksum