blob: 81d7a31aaab42a5608dbb070328913224649d6cd [file] [log] [blame]
.. _module-pw_snapshot-setup:
==============================
Setting up a Snapshot Pipeline
==============================
-------------------
Crash Handler Setup
-------------------
The Snapshot proto was designed first and foremost as a crash reporting format.
This section covers how to set up a crash handler to capture Snapshots.
.. image:: images/generic_crash_flow.svg
:width: 600
:alt: Generic crash handler flow
A typical crash handler has two entry points:
1. A software entry path through developer-written ASSERT() or CHECK() calls
that indicate a device should go down for a crash if a condition is not met.
2. A hardware-triggered exception handler path that is initiated when a CPU
encounters a fault signal (invalid memory access, bad instruction, etc.).
Before deferring to a common crash handler, these entry paths should disable
interrupts to force the system into a single-threaded execution mode. This
prevents other threads from operating on potentially bad data or clobbering
system state that could be useful for debugging.
The first step in a crash handler should always be a check for nested crashes to
prevent infinitely recursive crashes. Once it's deemed it's safe to continue,
the crash handler can re-initialize logging, initialize storage for crash report
capture, and then build a snapshot to later be retrieved from the device. Once
the crash report collection process is complete, some post-crash callbacks can
be run on a best-effort basis to clean up the system before rebooting. For
devices with debug port access, it's helpful to optionally hold the device in
an infinite loop rather than resetting to allow developers to access the device
via a hardware debugger.
Assert Handler Setup
====================
:ref:`pw_assert <module-pw_assert>` is Pigweed's entry point for software
crashes. Route any existing assert functions through pw_assert to centralize the
software crash path. Youll need to create a :ref:`pw_assert backend
<module-pw_assert-backend_api>` or a custom :ref:`pw_assert_basic handler
<module-pw_assert_basic-custom_handler>` to pass collected information to a more
sophisticated crash handler. One way to do this is to collect the data into a
statically allocated struct that is passed to a common crash handler. Its
important to immediately disable interrupts to prevent the system from doing
other things while in an impacted state.
.. code-block:: cpp
// This can be be directly accessed by a crash handler
static CrashData crash_data;
extern "C" void pw_assert_basic_HandleFailure(const char* file_name,
int line_number,
const char* format,
...) {
// Always disable interrupts first! How this is done depends
// on your platform.
__disable_irq();
va_list args;
va_start(args, format);
crash_data.file_name = file_name;
crash_data.line_number = line_number;
crash_data.reason_fmt = format;
crash_data.reason_args = &args;
crash_data.cpu_state = nullptr;
HandleCrash(crash_data);
PW_UNREACHABLE;
}
Exception Handler Setup
=======================
:ref:`pw_cpu_exception <module-pw_cpu_exception>` is Pigweed's recommended entry
point for CPU-triggered faults (divide by zero, invalid memory access, etc.).
You will need to provide a definition for pw_cpu_exception_DefaultHandler() that
passes the exception state produced by pw_cpu_exception to your common crash
handler.
.. code-block:: cpp
static CrashData crash_data;
// This helper turns a format string to a va_list that can be used by the
// common crash handling path.
void HandleExceptionWithString(pw_cpu_exception_State& state,
const char* fmt,
...) {
va_list args;
va_start(args, fmt);
crash_data.cpu_state = state;
crash_data.file_name = nullptr;
crash_data.reason_fmt = fmt;
crash_data.reason_args = &args;
HandleCrash(crash_data);
PW_UNREACHABLE;
}
extern "C" void pw_cpu_exception_DefaultHandler(
pw_cpu_exception_State* state) {
// Always disable interrupts first! How this is done depends
// on your platform.
__disable_irq();
crash_data.state = cpu_state;
// The CFSR is an extremely useful register for understanding ARMv7-M and
// ARMv8-M CPU faults. Other architectures should put something else here.
HandleExceptionWithString(crash_data,
"Exception encountered, cfsr=0x%",
cpu_state->extended.cfsr);
}
Common Crash Handler Setup
==========================
To minimize duplication of crash handling logic, it's good practice to route the
pw_assert and pw_cpu_exception handlers to a common crash handling codepath.
Ensure you can pass both pw_cpu_exception's CPU state and pw_assert's assert
information to the shared handler.
.. code-block:: cpp
struct CrashData {
pw_cpu_exception_State *cpu_state;
const char *reason_fmt;
const va_list *reason_args;
const char *file_name;
int line_number;
};
// This function assumes interrupts are properly disabled BEFORE it is called.
[[noreturn]] void HandleCrash(CrashData& crash_info) {
// Handle crash
}
In the crash handler your project can re-initialize a minimal subset of the
system needed to safely capture a snapshot before rebooting the device. The
remainder of this section focuses on ways you can improve the reliability and
usability of your project's crash handler.
Check for Nested Crashes
------------------------
It’s important to include crash handler checks that prevent infinite recursive
nesting of crashes. Maintain a static variable that checks the crash nesting
depth. After one or two nested crashes, abort crash handling entirely and reset
the device or sit in an infinite loop to wait for a hardware debugger to attach.
It’s simpler to put this logic at the beginning of the shared crash handler, but
if your assert/exception handlers are complex it might be safer to inject the
checks earlier in both codepaths.
.. code-block:: cpp
[[noreturn]] void HandleCrash(CrashData &crash_info) {
static size_t crash_depth = 0;
if (crash_depth > kMaxCrashDepth) {
Abort(/*run_callbacks=*/false);
}
crash_depth++;
...
}
Re-initialize Logging (Optional)
--------------------------------
Logging can be helpful for debugging your crash handler, but depending on your
device/system design may be challenging to safely support at crash time. To
re-initialize logging, you’ll need to re-construct C++ objects and re-initialize
any systems/hardware in the logging codepath. You may even need an entirely
separate logging pipeline that is single-threaded and interrupt-safe. Depending
on your system’s design, this may be difficult to set up.
Reinitialize Dependencies
-------------------------
It's good practice to design a crash handler that can run before C++ static
constructors have run. This means any initialization (whether manual or through
constructors) that your crash handler depends on should be manually invoked at
crash time. If an initialization step might not be safe, evaluate if it's
possible to omit the dependency.
System Cleanup
--------------
After collecting a snapshot, some parts of your system may benefit from some
cleanup before explicitly resetting a device. This might include flushing
buffers or safely shutting down attached hardware. The order of shutdown should
be deterministic, keeping in mind that any of these steps may have the potential
of causing a nested crash that skips the remainder of the handlers and forces
the device to immediately reset.
----------------------
Snapshot Storage Setup
----------------------
Use a storage class with a ``pw::stream::Writer`` interface to simplify
capturing a pw_snapshot proto. This can be a :ref:`pw::BlobStore
<module-pw_blob_store>`, an in-memory buffer that is flushed to flash, or a
:ref:`pw::PersistentBuffer <module-pw_persistent_ram-persistent_buffer>` that
lives in persistent memory. It's good practice to use lazy initialization for
storage objects used by your Snapshot capture codepath.
.. code-block:: cpp
// Persistent RAM objects are highly available. They don't rely on
// their constructor being run, and require no initialization.
PW_PLACE_IN_SECTION(".noinit")
pw::persistent_ram::PersistentBuffer<2048> persistent_snapshot;
void CaptureSnapshot(CrashInfo& crash_info) {
...
persistent_snapshot.clear();
PersistentBufferWriter& writer = persistent_snapshot.GetWriter();
...
}
----------------------
Snapshot Capture Setup
----------------------
.. note::
These instructions do not yet use the ``pw::protobuf::StreamEncoder``.
Capturing a snapshot is as simple as encoding any other proto message. Some
modules provide helper functions that will populate parts of a Snapshot, which
eases the burden of custom work that must be set up uniquely for each project.
Capture Reason
==============
A snapshot's "reason" should be considered the single most important field in a
captured snapshot. If a snapshot capture was triggered by a crash, this should
be the assert string. Other entry paths should describe here why the snapshot
was captured ("Host communication buffer full!", "Exception encountered at
0x00000004", etc.).
.. code-block:: cpp
Status CaptureSnapshot(CrashData& crash_info) {
// Temporary buffer for encoding "reason" to.
static std::byte temp_buffer[500];
// Temporary buffer to encode serialized proto to before dumping to the
// final ``pw::stream::Writer``.
static std::byte proto_encode_buffer[512];
...
pw::protobuf::NestedEncoder<kMaxDepth> proto_encoder(proto_encode_buffer);
pw::snapshot::Snapshot::Encoder snapshot_encoder(&proto_encoder);
size_t length = snprintf(temp_buffer,
sizeof(temp_buffer,
crash_info.reason_fmt),
*crash_info.reason_args);
snapshot_encoder.WriteReason(temp_buffer, length));
// Final encode and write.
Result<ConstByteSpan> encoded_proto = proto_encoder.Encode();
PW_TRY(encoded_proto.status());
PW_TRY(writer.Write(encoded_proto.value()));
...
}
Capture CPU State
=================
When using pw_cpu_exception, exceptions will automatically collect CPU state
that can be directly dumped into a snapshot. As it's not always easy to describe
a CPU exception in a single "reason" string, this captures the information
needed to more verbosely automatically generate a descriptive reason at analysis
time once the snapshot is retrieved from the device.
.. code-block:: cpp
Status CaptureSnapshot(CrashData& crash_info) {
...
proto_encoder.clear();
// Write CPU state.
if (crash_info.cpu_state) {
PW_TRY(DumpCpuStateProto(snapshot_encoder.GetArmv7mCpuStateEncoder(),
*crash_info.cpu_state));
// Final encode and write.
Result<ConstByteSpan> encoded_proto = proto_encoder.Encode();
PW_TRY(encoded_proto.status());
PW_TRY(writer.Write(encoded_proto.value()));
}
}
-----------------------
Snapshot Transfer Setup
-----------------------
Pigweeds pw_rpc system is well suited for retrieving a snapshot from a device.
Pigweed does not yet provide a generalized transfer service for moving files
to/from a device. When this feature is added to Pigweed, this section will be
updated to include guidance for connecting a storage system to a transfer
service.
----------------------
Snapshot Tooling Setup
----------------------
When using the upstream ``Snapshot`` proto, you can directly use
``pw_snapshot.process`` to process snapshots into human-readable dumps. If
you've opted to extend Pigweed's snapshot proto, you'll likely want to extend
the processing tooling to handle custom project data as well. This can be done
by creating a light wrapper around
``pw_snapshot.processor.process_snapshots()``.
.. code-block:: python
def _process_hw_failures(serialized_snapshot: bytes) -> str:
"""Custom handler that checks wheel state."""
wheel_state = wheel_state_pb2.WheelStateSnapshot()
output = []
wheel_state.ParseFromString(serialized_snapshot)
if len(wheel_state.wheels) != 2:
output.append(f'Expected 2 wheels, found {len(wheel_state.wheels)}')
if len(wheel_state.wheels) < 2:
output.append('Wheels fell off!')
# And more...
return '\n'.join(output)
def process_my_snapshots(serialized_snapshot: bytes) -> str:
"""Runs the snapshot processor with a custom callback."""
return pw_snapshot.processor.process_snapshots(
serialized_snapshot, user_processing_callback=_process_hw_failures)