You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
276 lines
10 KiB
276 lines
10 KiB
8 months ago
|
.. _module-pw_persistent_ram:
|
||
|
|
||
|
=================
|
||
|
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:
|
||
|
|
||
|
.. code-block:: cpp
|
||
|
|
||
|
#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.
|
||
|
|
||
|
.. code-block:: cpp
|
||
|
|
||
|
#include "pw_assert/assert.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 persistents containers have picked
|
||
|
integrity checks which work with zerod 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.
|
||
|
|
||
|
.. 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_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.
|
||
|
|
||
|
.. code-block:: cpp
|
||
|
|
||
|
#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.reset();
|
||
|
}
|
||
|
|
||
|
// ... 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.
|
||
|
|
||
|
.. code-block:: cpp
|
||
|
|
||
|
#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.
|
||
|
|
||
|
.. include:: persistent_size
|
||
|
|
||
|
Compatibility
|
||
|
-------------
|
||
|
* C++17
|
||
|
|
||
|
Dependencies
|
||
|
------------
|
||
|
* ``pw_checksum``
|