pw_persistent_ram: Add PersistentBuffer
Adds a PersistentBuffer object that acts as a persistent form of a
MemoryWriterBuffer.
Change-Id: I61db1a77476ceececd088fd60bc10bfaf1b99e0e
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/39360
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Armando Montanez <amontanez@google.com>
diff --git a/pw_persistent_ram/BUILD b/pw_persistent_ram/BUILD
index 3d35525..1cde0cd 100644
--- a/pw_persistent_ram/BUILD
+++ b/pw_persistent_ram/BUILD
@@ -26,8 +26,16 @@
name = "pw_persistent_ram",
hdrs = [
"public/pw_persistent_ram/persistent.h",
+ "public/pw_persistent_ram/persistent_buffer.h",
],
includes = ["public"],
+ srcs = ["persistent_buffer.cc"],
+ deps = [
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_checksum",
+ "//pw_stream",
+ ],
)
pw_cc_test(
@@ -40,3 +48,15 @@
"//pw_unit_test",
],
)
+
+pw_cc_test(
+ name = "persistent_buffer_test",
+ srcs = [
+ "persistent_buffer_test.cc",
+ ],
+ deps = [
+ ":pw_persistent_ram",
+ "//pw_unit_test",
+ "//pw_random",
+ ],
+)
diff --git a/pw_persistent_ram/BUILD.gn b/pw_persistent_ram/BUILD.gn
index b58ccd5..729656f 100644
--- a/pw_persistent_ram/BUILD.gn
+++ b/pw_persistent_ram/BUILD.gn
@@ -26,15 +26,24 @@
pw_source_set("pw_persistent_ram") {
public_configs = [ ":public_include_path" ]
- public = [ "public/pw_persistent_ram/persistent.h" ]
+ public = [
+ "public/pw_persistent_ram/persistent.h",
+ "public/pw_persistent_ram/persistent_buffer.h",
+ ]
+ sources = [ "persistent_buffer.cc" ]
public_deps = [
dir_pw_assert,
+ dir_pw_bytes,
dir_pw_checksum,
+ dir_pw_stream,
]
}
pw_test_group("tests") {
- tests = [ ":persistent_test" ]
+ tests = [
+ ":persistent_test",
+ ":persistent_buffer_test",
+ ]
}
pw_test("persistent_test") {
@@ -42,6 +51,14 @@
sources = [ "persistent_test.cc" ]
}
+pw_test("persistent_buffer_test") {
+ deps = [
+ ":pw_persistent_ram",
+ dir_pw_random,
+ ]
+ sources = [ "persistent_buffer_test.cc" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
report_deps = [ ":persistent_size" ]
diff --git a/pw_persistent_ram/docs.rst b/pw_persistent_ram/docs.rst
index 75e8175..5286c86 100644
--- a/pw_persistent_ram/docs.rst
+++ b/pw_persistent_ram/docs.rst
@@ -209,6 +209,54 @@
// ... 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
diff --git a/pw_persistent_ram/persistent_buffer.cc b/pw_persistent_ram/persistent_buffer.cc
new file mode 100644
index 0000000..9cee8fc
--- /dev/null
+++ b/pw_persistent_ram/persistent_buffer.cc
@@ -0,0 +1,43 @@
+// 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_buffer.h"
+
+#include "pw_bytes/span.h"
+#include "pw_checksum/crc16_ccitt.h"
+#include "pw_status/status.h"
+
+namespace pw::persistent_ram {
+
+Status PersistentBufferWriter::DoWrite(ConstByteSpan data) {
+ if (ConservativeWriteLimit() == 0) {
+ return Status::OutOfRange();
+ }
+ if (ConservativeWriteLimit() < data.size_bytes()) {
+ return Status::ResourceExhausted();
+ }
+ if (data.empty()) {
+ return OkStatus();
+ }
+
+ std::memcpy(buffer_.data() + size_, data.data(), data.size_bytes());
+
+ // Only checksum newly written data.
+ checksum_ = checksum::Crc16Ccitt::Calculate(
+ ByteSpan(buffer_.data() + size_, data.size_bytes()), checksum_);
+ size_ += data.size_bytes();
+
+ return OkStatus();
+}
+
+} // namespace pw::persistent_ram
diff --git a/pw_persistent_ram/persistent_buffer_test.cc b/pw_persistent_ram/persistent_buffer_test.cc
new file mode 100644
index 0000000..cf72fc2
--- /dev/null
+++ b/pw_persistent_ram/persistent_buffer_test.cc
@@ -0,0 +1,158 @@
+// 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_buffer.h"
+
+#include <cstddef>
+#include <span>
+#include <type_traits>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_random/xor_shift.h"
+
+namespace pw::persistent_ram {
+namespace {
+
+class PersistentTest : public ::testing::Test {
+ protected:
+ static constexpr size_t kBufferSize = 256;
+ PersistentTest() { ZeroPersistentMemory(); }
+
+ // Emulate invalidation of persistent section(s).
+ void ZeroPersistentMemory() { memset(buffer_, 0, sizeof(buffer_)); }
+ void RandomFillMemory() {
+ random::XorShiftStarRng64 rng(0x9ad75);
+ StatusWithSize sws = rng.Get(buffer_);
+ ASSERT_TRUE(sws.ok());
+ ASSERT_EQ(sws.size(), sizeof(buffer_));
+ }
+
+ PersistentBuffer<kBufferSize>& GetPersistentBuffer() {
+ return *(new (buffer_) PersistentBuffer<kBufferSize>());
+ }
+
+ // Allocate a chunk of aligned storage that can be independently controlled.
+ alignas(PersistentBuffer<kBufferSize>)
+ std::byte buffer_[sizeof(PersistentBuffer<kBufferSize>)];
+};
+
+TEST_F(PersistentTest, DefaultConstructionAndDestruction) {
+ constexpr uint32_t kExpectedNumber = 0x6C2C6582;
+ {
+ // 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 = GetPersistentBuffer();
+ auto writer = persistent.GetWriter();
+ EXPECT_EQ(persistent.size(), 0u);
+
+ writer.Write(std::as_bytes(std::span(&kExpectedNumber, 1)));
+ ASSERT_TRUE(persistent.has_value());
+
+ persistent.~PersistentBuffer(); // Emulate shutdown / global destructors.
+ }
+
+ { // Emulate a boot where persistent memory was kept as is.
+ auto& persistent = GetPersistentBuffer();
+ ASSERT_TRUE(persistent.has_value());
+ EXPECT_EQ(persistent.size(), sizeof(kExpectedNumber));
+
+ uint32_t temp = 0;
+ memcpy(&temp, persistent.data(), sizeof(temp));
+ EXPECT_EQ(temp, kExpectedNumber);
+ }
+}
+
+TEST_F(PersistentTest, LongData) {
+ constexpr std::string_view kTestString(
+ "A nice string should remain valid even if written incrementally!");
+ constexpr size_t kWriteSize = 5;
+
+ { // Initialize the buffer.
+ RandomFillMemory();
+ auto& persistent = GetPersistentBuffer();
+ ASSERT_FALSE(persistent.has_value());
+
+ auto writer = persistent.GetWriter();
+ for (size_t i = 0; i < kTestString.length(); i += kWriteSize) {
+ writer.Write(kTestString.data() + i,
+ std::min(kWriteSize, kTestString.length() - i));
+ }
+ // Need to manually write a null terminator since std::string_view doesn't
+ // include one in the string length.
+ writer.Write(std::byte(0));
+
+ persistent.~PersistentBuffer(); // Emulate shutdown / global destructors.
+ }
+
+ { // Ensure data is valid.
+ auto& persistent = GetPersistentBuffer();
+ ASSERT_TRUE(persistent.has_value());
+ ASSERT_STREQ(kTestString.data(),
+ reinterpret_cast<const char*>(persistent.data()));
+ }
+}
+
+TEST_F(PersistentTest, ZeroDataIsNoValue) {
+ ZeroPersistentMemory();
+ auto& persistent = GetPersistentBuffer();
+ EXPECT_FALSE(persistent.has_value());
+}
+
+TEST_F(PersistentTest, RandomDataIsInvalid) {
+ RandomFillMemory();
+ auto& persistent = GetPersistentBuffer();
+ ASSERT_FALSE(persistent.has_value());
+}
+
+TEST_F(PersistentTest, AppendingData) {
+ constexpr std::string_view kTestString("Test string one!");
+ constexpr uint32_t kTestNumber = 42;
+
+ { // Initialize the buffer.
+ RandomFillMemory();
+ auto& persistent = GetPersistentBuffer();
+ auto writer = persistent.GetWriter();
+ EXPECT_EQ(persistent.size(), 0u);
+
+ // Write an integer.
+ writer.Write(std::as_bytes(std::span(&kTestNumber, 1)));
+ ASSERT_TRUE(persistent.has_value());
+
+ persistent.~PersistentBuffer(); // Emulate shutdown / global destructors.
+ }
+
+ { // Get a pointer to the buffer and validate the contents.
+ auto& persistent = GetPersistentBuffer();
+ ASSERT_TRUE(persistent.has_value());
+ EXPECT_EQ(persistent.size(), sizeof(kTestNumber));
+
+ // Write more data.
+ auto writer = persistent.GetWriter();
+ EXPECT_EQ(persistent.size(), sizeof(kTestNumber));
+ writer.Write(std::as_bytes(std::span<const char>(kTestString)));
+
+ persistent.~PersistentBuffer(); // Emulate shutdown / global destructors.
+ }
+ { // Ensure data was appended.
+ auto& persistent = GetPersistentBuffer();
+ ASSERT_TRUE(persistent.has_value());
+ EXPECT_EQ(persistent.size(), sizeof(kTestNumber) + kTestString.length());
+ }
+}
+
+} // namespace
+} // namespace pw::persistent_ram
diff --git a/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h b/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h
new file mode 100644
index 0000000..033d244
--- /dev/null
+++ b/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h
@@ -0,0 +1,150 @@
+// 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_bytes/span.h"
+#include "pw_checksum/crc16_ccitt.h"
+#include "pw_status/status.h"
+#include "pw_stream/stream.h"
+
+namespace pw::persistent_ram {
+
+// A PersistentBufferWriter implements the pw::stream::Writer interface and
+// provides handles to mutate and access the underlying data of a
+// PersistentBuffer. This object should NOT be stored in persistent RAM.
+//
+// Only one writer should be open at a given time.
+class PersistentBufferWriter : public stream::Writer {
+ public:
+ PersistentBufferWriter() = delete;
+
+ size_t ConservativeWriteLimit() const override {
+ return buffer_.size_bytes() - size_;
+ }
+
+ private:
+ template <size_t>
+ friend class PersistentBuffer;
+
+ PersistentBufferWriter(ByteSpan buffer,
+ volatile size_t& size,
+ volatile uint16_t& checksum)
+ : buffer_(buffer), size_(size), checksum_(checksum) {}
+
+ // Implementation for writing data to this stream.
+ Status DoWrite(ConstByteSpan data) override;
+
+ ByteSpan buffer_;
+ volatile size_t& size_;
+ volatile uint16_t& checksum_;
+};
+
+#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
+
+// When a PersistentBuffer is statically allocated in persistent memory, its
+// state will persist across soft resets in accordance with the expected
+// behavior of the underlying RAM. This object is completely safe to use before
+// static constructors are called as its constructor is effectively a no-op.
+//
+// While the stored data can be read by PersistentBuffer's public functions,
+// each public function must validate the integrity of the stored data. It's
+// typically more performant to get a handle to a PersistentBufferWriter
+// instead, as data is validated on creation of the PersistentBufferWriter,
+// which allows access to the underlying data without needing to validate the
+// data's integrity with each call to PersistentBufferWriter functions.
+template <size_t kMaxSizeBytes>
+class PersistentBuffer {
+ public:
+ // The default constructor intentionally does not initialize anything. This
+ // allows a persistent buffer statically allocated in persistent RAM to be
+ // highly available.
+ //
+ // Explicitly declaring an empty constructor rather than using the default
+ // constructor prevents the object from being zero-initialized when the object
+ // is value initialized. If this was left as a default constructor,
+ // PersistentBuffer objects declared as value-initialized would be
+ // zero-initialized.
+ //
+ // // Value initialization:
+ // PersistentBuffer<256> persistent_buffer();
+ //
+ // // Default initialization:
+ // PersistentBuffer<256> persistent_buffer;
+ PersistentBuffer() {}
+ // Disable copy and move constructors.
+ PersistentBuffer(const PersistentBuffer&) = delete;
+ PersistentBuffer(PersistentBuffer&&) = delete;
+ // Explicit no-op destructor.
+ ~PersistentBuffer() {}
+
+ PersistentBufferWriter GetWriter() {
+ if (!has_value()) {
+ clear();
+ }
+ return PersistentBufferWriter(
+ ByteSpan(const_cast<std::byte*>(buffer_), kMaxSizeBytes),
+ size_,
+ checksum_);
+ }
+
+ size_t size() const {
+ if (has_value()) {
+ return size_;
+ }
+ return 0;
+ }
+
+ const std::byte* data() const { return const_cast<std::byte*>(buffer_); }
+
+ void clear() {
+ size_ = 0;
+ checksum_ = checksum::Crc16Ccitt::kInitialValue;
+ }
+
+ bool has_value() const {
+ if (size_ > kMaxSizeBytes || size_ == 0) {
+ return false;
+ }
+
+ // Check checksum. This is more costly.
+ return checksum_ == checksum::Crc16Ccitt::Calculate(ConstByteSpan(
+ const_cast<std::byte*>(buffer_), size_));
+ }
+
+ private:
+ // None of these members are initialized by the constructor by design.
+ volatile uint16_t checksum_;
+ volatile size_t size_;
+ volatile std::byte buffer_[kMaxSizeBytes];
+};
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#elif defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+} // namespace pw::persistent_ram