pw_persistent_ram: Adds initial Persistent object
Adds the pw_persistent_ram module with initial documentation on
persistent RAM usage and life cycle management and the initial
Persistent<t> container with built in integrity checking.
Change-Id: I7f539228ba5fcc76c6d80a72110f2365724ebf23
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/38021
Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Reviewed-by: David Rogers <davidrogers@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/BUILD.gn b/BUILD.gn
index b650baf..c16c6ba 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -214,6 +214,7 @@
"$dir_pw_hdlc",
"$dir_pw_i2c",
"$dir_pw_metric",
+ "$dir_pw_persistent_ram",
"$dir_pw_polyfill",
"$dir_pw_preprocessor",
"$dir_pw_protobuf",
@@ -267,6 +268,7 @@
"$dir_pw_malloc_freelist:tests",
"$dir_pw_metric:tests",
"$dir_pw_multisink:tests",
+ "$dir_pw_persistent_ram:tests",
"$dir_pw_polyfill:tests",
"$dir_pw_preprocessor:tests",
"$dir_pw_protobuf:tests",
diff --git a/docs/BUILD.gn b/docs/BUILD.gn
index e904252..aa3c397 100644
--- a/docs/BUILD.gn
+++ b/docs/BUILD.gn
@@ -87,6 +87,7 @@
"$dir_pw_module:docs",
"$dir_pw_multisink:docs",
"$dir_pw_package:docs",
+ "$dir_pw_persistent_ram:docs",
"$dir_pw_polyfill:docs",
"$dir_pw_preprocessor:docs",
"$dir_pw_presubmit:docs",
diff --git a/modules.gni b/modules.gni
index 655f3fc..7da2072 100644
--- a/modules.gni
+++ b/modules.gni
@@ -63,6 +63,7 @@
dir_pw_multisink = get_path_info("pw_multisink", "abspath")
dir_pw_fuzzer = get_path_info("pw_fuzzer", "abspath")
dir_pw_package = get_path_info("pw_package", "abspath")
+ dir_pw_persistent_ram = get_path_info("pw_persistent_ram", "abspath")
dir_pw_polyfill = get_path_info("pw_polyfill", "abspath")
dir_pw_preprocessor = get_path_info("pw_preprocessor", "abspath")
dir_pw_presubmit = get_path_info("pw_presubmit", "abspath")
diff --git a/pw_persistent_ram/BUILD b/pw_persistent_ram/BUILD
new file mode 100644
index 0000000..3d35525
--- /dev/null
+++ b/pw_persistent_ram/BUILD
@@ -0,0 +1,42 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"]) # Apache License 2.0
+
+pw_cc_library(
+ name = "pw_persistent_ram",
+ hdrs = [
+ "public/pw_persistent_ram/persistent.h",
+ ],
+ includes = ["public"],
+)
+
+pw_cc_test(
+ name = "persistent_test",
+ srcs = [
+ "persistent_test.cc",
+ ],
+ deps = [
+ ":pw_persistent_ram",
+ "//pw_unit_test",
+ ],
+)
diff --git a/pw_persistent_ram/BUILD.gn b/pw_persistent_ram/BUILD.gn
new file mode 100644
index 0000000..6c7bdcf
--- /dev/null
+++ b/pw_persistent_ram/BUILD.gn
@@ -0,0 +1,48 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("pw_persistent_ram") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_persistent_ram/persistent.h" ]
+ public_deps = [
+ dir_pw_assert,
+ dir_pw_checksum,
+ ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":persistent_test" ]
+}
+
+pw_test("persistent_test") {
+ deps = [ ":pw_persistent_ram" ]
+ sources = [ "persistent_test.cc" ]
+}
+
+# TODO(ewout): add size reports in a follow up CL.
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_persistent_ram/docs.rst b/pw_persistent_ram/docs.rst
new file mode 100644
index 0000000..e0d94c6
--- /dev/null
+++ b/pw_persistent_ram/docs.rst
@@ -0,0 +1,167 @@
+.. _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
+-------
+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
+ }
+
+Compatibility
+-------------
+* C++17
+
+Dependencies
+------------
+* ``pw_checksum``
diff --git a/pw_persistent_ram/persistent_test.cc b/pw_persistent_ram/persistent_test.cc
new file mode 100644
index 0000000..6f45489
--- /dev/null
+++ b/pw_persistent_ram/persistent_test.cc
@@ -0,0 +1,85 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_persistent_ram/persistent.h"
+
+#include <type_traits>
+
+#include "gtest/gtest.h"
+
+namespace pw::persistent_ram {
+namespace {
+
+class PersistentTest : public ::testing::Test {
+ protected:
+ PersistentTest() { ZeroPersistentMemory(); }
+
+ // Emulate invalidation of persistent section(s).
+ void ZeroPersistentMemory() { memset(&buffer_, 0, sizeof(buffer_)); }
+
+ // Allocate a chunk of aligned storage that can be independently controlled.
+ std::aligned_storage_t<sizeof(Persistent<uint32_t>),
+ alignof(Persistent<uint32_t>)>
+ buffer_;
+};
+
+TEST_F(PersistentTest, DefaultConstructionAndDestruction) {
+ { // Emulate a boot where the persistent sections were invalidated.
+ // Although the fixture always does this, we do this an extra time to be
+ // 100% confident that an integrity check cannot be accidentally selected
+ // which results in reporting there is valid data when zero'd.
+ ZeroPersistentMemory();
+ auto& persistent = *(new (&buffer_) Persistent<uint32_t>());
+ EXPECT_FALSE(persistent.has_value());
+
+ persistent = 42;
+ ASSERT_TRUE(persistent.has_value());
+ EXPECT_EQ(42u, persistent.value());
+
+ persistent.~Persistent(); // Emulate shutdown / global destructors.
+ }
+
+ { // Emulate a boot where persistent memory was kept as is.
+ auto& persistent = *(new (&buffer_) Persistent<uint32_t>());
+ ASSERT_TRUE(persistent.has_value());
+ EXPECT_EQ(42u, persistent.value());
+ }
+}
+
+TEST_F(PersistentTest, Reset) {
+ { // Emulate a boot where the persistent sections were invalidated.
+ auto& persistent = *(new (&buffer_) Persistent<uint32_t>());
+ persistent = 42u;
+ EXPECT_TRUE(persistent.has_value());
+ persistent.reset();
+
+ persistent.~Persistent(); // Emulate shutdown / global destructors.
+ }
+
+ { // Emulate a boot where persistent memory was kept as is.
+ auto& persistent = *(new (&buffer_) Persistent<uint32_t>());
+ EXPECT_FALSE(persistent.has_value());
+ }
+}
+
+TEST_F(PersistentTest, Emplace) {
+ auto& persistent = *(new (&buffer_) Persistent<uint32_t>());
+ EXPECT_FALSE(persistent.has_value());
+
+ persistent.emplace(42u);
+ ASSERT_TRUE(persistent.has_value());
+ EXPECT_EQ(42u, persistent.value());
+}
+
+} // namespace
+} // namespace pw::persistent_ram
diff --git a/pw_persistent_ram/public/pw_persistent_ram/persistent.h b/pw_persistent_ram/public/pw_persistent_ram/persistent.h
new file mode 100644
index 0000000..06f356b
--- /dev/null
+++ b/pw_persistent_ram/public/pw_persistent_ram/persistent.h
@@ -0,0 +1,124 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstdint>
+#include <cstring>
+#include <span>
+#include <type_traits>
+#include <utility>
+
+#include "pw_assert/light.h"
+#include "pw_checksum/crc16_ccitt.h"
+
+namespace pw::persistent_ram {
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wuninitialized"
+#elif defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wuninitialized"
+#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
+#endif
+
+// A simple container for holding a value T with CRC16 integrity checking.
+//
+// A Persistent is simply a value T plus integrity checking for use in a
+// persistent RAM section which is not initialized on boot.
+//
+// WARNING: Unlike a DoubleBufferedPersistent, a Persistent will be lost if a
+// write/set operation is interrupted or otherwise not completed.
+//
+// TODO(pwbug/348): Consider a different integrity check implementation which
+// does not use a 512B lookup table.
+template <typename T>
+class Persistent {
+ public:
+ // Constructor which does nothing, meaning it never sets the value.
+ constexpr Persistent() {}
+
+ Persistent(const Persistent&) = delete; // Copy constructor is disabled.
+ Persistent(Persistent&&) = delete; // Move constructor is disabled.
+ ~Persistent() {} // The destructor does nothing.
+
+ // Construct the value in-place.
+ template <class... Args>
+ const T& emplace(Args&&... args) {
+ new (const_cast<T*>(&contents_)) T(std::forward<Args>(args)...);
+ crc_ = Crc(contents_);
+ return const_cast<T&>(contents_);
+ }
+
+ // Assignment operator.
+ template <typename U = T>
+ Persistent& operator=(U&& value) {
+ contents_ = std::move(value);
+ crc_ = Crc(contents_);
+ return *this;
+ }
+
+ // Destroys any contained value.
+ void reset() {
+ // The trivial destructor is skipped as it's trivial.
+ std::memset(const_cast<T*>(&contents_), 0, sizeof(contents_));
+ crc_ = 0;
+ }
+
+ // Returns true if a value is held by the Persistent.
+ bool has_value() const {
+ return crc_ == Crc(contents_); // There's a value if its CRC matches.
+ }
+
+ // Access the value.
+ //
+ // Precondition: has_value() must be true.
+ const T& value() const {
+ PW_ASSERT(has_value());
+ return const_cast<T&>(contents_);
+ }
+
+ private:
+ static_assert(std::is_trivially_copy_constructible<T>::value,
+ "If a Persistent persists across reboots, it is effectively "
+ "loaded through a trivial copy constructor.");
+ static_assert(std::is_trivially_destructible<T>::value,
+ "A Persistent's destructor does not invoke the value's "
+ "destructor, ergo only trivially destructible types are "
+ "supported.");
+
+ static uint16_t Crc(volatile const T& value) {
+ return checksum::Crc16Ccitt::Calculate(
+ std::as_bytes(std::span(const_cast<const T*>(&value), 1)));
+ }
+
+ // Use unions to denote that these members are never initialized by design and
+ // on purpose. Volatile is used to ensure that the compiler cannot optimize
+ // out operations where it seems like there is no further usage of a
+ // Persistent as this may be on the next boot.
+ union {
+ volatile T contents_;
+ };
+ union {
+ volatile uint16_t crc_;
+ };
+};
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#elif defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+
+} // namespace pw::persistent_ram