blob: 10ed8cb15336ef3f2c347bc6ae47f4333d828b06 [file]
.. _module-pw_persistent_ram:
=================
pw_persistent_ram
=================
.. pigweed-module::
:name: pw_persistent_ram
This module provides utilities and containers for storing data (usually crash
dumps in the form of snapshots) into RAM that persists across reboots. For many
common MCUs, RAM is retained during a soft reboot, but this is not guaranteed.
The containers therefore include integrity checks to detect corruption.
.. note::
Not all hardware supports this; see the :ref:`Hardware Requirements
<module-pw_persistent_ram-hw_requirements>` section.
------------------------
Persistent RAM Lifecycle
------------------------
Understanding the lifecycle of persistent RAM is crucial for its effective use.
Key considerations include when to initialize, clear, and access the data
stored in persistent RAM. The following diagram illustrates a common pattern
for managing persistent RAM, particularly in scenarios involving crash data
capture and retrieval:
.. raw:: html
<style>
#lifecycle > svg { height: 1000px; }
</style>
.. mermaid::
:name: lifecycle
graph TD
DB[Device Boot]
DB --> PreMainInit[Pre-Main<br>Initialization];
PreMainInit --> AppStart[Application Starts];
%% After application starts, check for crash data from a previous boot
AppStart --> CheckCrashData{Check Persistent RAM<br>for Crash Data};
%% Path 1: No crash data found / Leads to normal operation
CheckCrashData -- No Data --> NormalOp[Normal Device<br>Operation];
%% Normal Operation can lead to a clean reboot or a crash
NormalOp -- Clean Reboot --> DB;
NormalOp -- Crash Event --> CrashHdlr[Crash Handler<br>Executes];
%% Crash Handling Path (occurs during NormalOp, leads to reboot)
CrashHdlr --> CaptureToPersistentRAM[Capture Crash Data<br>to Persistent RAM];
CaptureToPersistentRAM -- Crash Reboot --> DB;
%% Path 2: Crash data found after a reboot (from CheckCrashData)
CheckCrashData -- Data Found --> ProcessAndClearCrashData[Log, Transmit, and<br>Clear Crash Data];
ProcessAndClearCrashData --> NormalOp;
The key aspect of this flow is that the crash exception handler stores crash
data into persistent RAM. Then, after rebooting into a known-good state, the
crash data is read from persistent RAM to be stored or transmitted.
The lifecycle flow above highlights several stages:
- **Device Boot & Pre-Main Initialization**: Standard boot procedures,
including setting up BSS/data sections and running C++ static constructors.
Special care must be taken (e.g., via the linker script) to avoid clearing
persistent RAM at this stage.
- **Application Start**: The main application begins execution.
- **Crash Data Check**: Upon starting, the application checks persistent RAM
for any crash data logged from a previous, unexpected reboot. If crash data
is found and its checksum indicates it is not corrupt, product-specific
logic to transmit or store the data is executed at this point. Finally,
the persistent RAM is cleared.
- **Normal Operation**: If no crash data is found, or after crash data has
been processed, the device proceeds with its normal operations. Persistent
RAM can be used by the application as needed during this phase.
- **Crash Handling**: If a crash occurs during normal operation, a dedicated
crash handler captures relevant system state and writes it to persistent
RAM before the system reboots.
The management of persistent RAM is tightly coupled to the device's boot
sequence and application logic.
-----------------------------------
Guidelines for Persistent RAM Usage
-----------------------------------
While Pigweed cannot provide a one-size-fits-all solution for persistent RAM
management due to its hardware-dependent nature, we recommend following these
guidelines:
1. **Only use persistent containers in the persistent RAM region** -
Arbitrary objects in the persistent RAM section are unsafe since their
integrity is not guarded by checksums. All types in persistent memory
regions should be wrapped in persistent RAM containers to detect data loss
and prevent operations on corrupt data.
2. **Do not use persistent containers as members in other objects** - Due to
the placement restrictions for persistent RAM containers, it is not
generally possible to make these containers members of other objects.
Instead, create them separately and use dependency injection to connect
other objects like crash handling infrastructure. This also facilitates unit
testing.
3. **Erase persistent RAM in your update flow** - If persistent RAM
addresses shift between software versions, there will be a skew between
the addresses on the current boot (with the old code) and those on the
post-update boot (with the new code). The shifted addresses can cause
arbitrary memory from the old boot to become the contents of the new
persistent RAM. This results in a checksum mismatch and will be reported as
corrupted persistent memory by your crash storage/transmit code, even though
the underlying cause is innocuous. This is avoidable by fixing the size and
address of your persistent RAM containers between software updates.
Alternatively, zeroing out the persistent memory can reduce the chance of
spurious corruption.
4. **Zero persistent RAM on cold boot** - To ensure deterministic cold boots,
clear persistent RAM when booting from a power-off state. This requires
detecting a cold boot, which not all systems support.
5. **Provide a mechanism to manually clear persistent RAM** - Add a reliable
hook or request flag that can be set (e.g., before a reboot) to zero all
persistent RAM on the next boot. This is useful for emulating persistent
memory loss in a thread-safe manner for testing, and provides a recovery
path if persistent RAM data causes boot loops or other unexpected behavior.
6. **Watch out for boot loops** - In rare cases, bugs in handling the
persistent RAM transmit or storage phase can lead to boot loops.
.. _module-pw_persistent_ram-hw_requirements:
---------------------
Hardware Requirements
---------------------
The use of persistent RAM is dependent on specific hardware and system
configurations. It requires memory regions that retain their state across
reboots without being cleared by bootloaders or hardware initialization
routines. Many common MCUs operate this way; for example, many common STM32 and
NXP MCUs retain their memory when rebooting.
------------------------
Persistent RAM Placement
------------------------
Persistent RAM is typically provided through specially carved-out linker script
sections and/or memory ranges located such that bootloaders and application
boot code do not clobber them.
1. If persistent linker sections are provided, use the ``PW_PLACE_IN_SECTION()``
macro to assign variables to that memory region. For example, if the
persistent memory section name is ``.noinit``, you could instantiate an
object as follows:
.. code-block:: cpp
#include "pw_persistent_ram/persistent.h"
#include "pw_preprocessor/compiler.h"
using pw::persistent_ram::Persistent;
PW_PLACE_IN_SECTION(".noinit") Persistent<bool> persistent_bool;
2. If persistent memory ranges are provided, you can use a struct to wrap
the different persisted objects. This makes it possible to ensure that the
data fits in the provided memory range. This must be done via a runtime check
against variables provided through the linker script since the addresses
of linker script symbols aren't available at compile time.
.. code-block:: cpp
#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<PersistentData*>(&__noinit_begin);
void CheckPersistentDataSize() {
PW_DCHECK_UINT_LE(sizeof(PersistentData),
__noinit_end - __noinit_begin,
"PersistentData overflowed the noinit memory range");
}
---------------------------------
pw::persistent_ram::Persistent<T>
---------------------------------
The ``Persistent<T>`` class is a simple container for holding its templated
value ``T`` with CRC16 integrity checking. Note that a ``Persistent<T>``
object's contents 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; therefore, it is okay if it is not executed during
shutdown.
Example: Storing an integer
===========================
A common use case for persistent data is to track boot counts, effectively
measuring how often the device has rebooted. This can be useful for monitoring
how many times the device has rebooted and/or crashed. This can be easily
accomplished using the ``Persistent<T>`` container.
.. code-block:: cpp
#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_PLACE_IN_SECTION(".noinit") Persistent<uint16_t> persistent_boot_count;
int main() {
BootCount boot_count(persistent_boot_count);
// Example usage: 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<T>`` object
directly, but you'll be able to modify the object in place. Once the
``Mutator`` goes out of scope, the ``Persistent<T>`` object's checksum is
updated to reflect the changes.
.. code-block:: cpp
#include "pw_persistent_ram/persistent.h"
#include "pw_preprocessor/compiler.h"
using pw::persistent_ram::Persistent;
constexpr size_t kMaxReasonLength = 256;
struct LastCrashInfo {
uint32_t uptime_ms;
uint32_t boot_id;
char reason[kMaxReasonLength];
};
PW_PLACE_IN_SECTION(".noinit") Persistent<LastCrashInfo> 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
}
.. _module-pw_persistent_ram-persistent_buffer:
------------------------------------
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.
An example use case is emitting crash handler logs to a buffer so they are
available after 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.
Example: Logging to a persistent buffer
=======================================
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.
.. code-block:: cpp
#include "pw_bytes/span.h" // For pw::ConstByteSpan
#include "pw_persistent_ram/persistent_buffer.h"
#include "pw_preprocessor/compiler.h"
using pw::persistent_ram::PersistentBuffer;
using pw::persistent_ram::PersistentBufferWriter;
PW_PLACE_IN_SECTION(".noinit") PersistentBuffer<2048> crash_logs;
void CheckForCrashLogs() {
if (crash_logs.has_value()) {
// A function that dumps sequentially serialized logs using pw_log.
// Assumes DumpRawLogs can take data() and size() or a ConstByteSpan.
DumpRawLogs(pw::ConstByteSpan(crash_logs.data(), crash_logs.size()));
crash_logs.clear();
}
}
void HandleCrash(CrashInfo* crash_info) {
// Clear previous logs before getting a new writer.
crash_logs.clear();
PersistentBufferWriter crash_log_writer = crash_logs.GetWriter();
// Sets the pw::stream::Writer that pw_log should dump logs to.
SetLogSink(crash_log_writer);
// Handle crash, calling PW_LOG to log useful info.
}
int main() {
CheckForCrashLogs();
// ... rest of main
}
Size Report
-----------
The following size report showcases the overhead for using ``Persistent<T>``.
Note that this templates ``Persistent<T>`` only on a ``uint32_t``; therefore,
the cost without ``pw_checksum``'s CRC16 is the approximate cost per type.
.. include:: persistent_size
Compatibility
-------------
* C++17
Dependencies
------------
* ``pw_checksum``