pw_cpu_exception_cortex_m: Expose LogExceptionAnalysis()

Exposes an internal part of the support backend to log the
exception analysis as an optional utility API.

The cpu_state.cc file is renamed to support.cc to reflect what
the facade/backend that the file is associated with.

The cpu_state.h is pulled out to a separate target to remove
unnecessary circular dependencies within the module.

In addition the last remaining TODOs for pwbug/296 are removed and
the Bazel build files are cleaned up a little bit.

Also updates the docs to include the configuration options.

Bug: 296
Change-Id: Id2ea6adc5dc4f3f99c2d9cf15ea048886739736a
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/78860
Reviewed-by: Armando Montanez <amontanez@google.com>
Commit-Queue: Ewout van Bekkum <ewout@google.com>
diff --git a/pw_cpu_exception_cortex_m/BUILD.bazel b/pw_cpu_exception_cortex_m/BUILD.bazel
index 73da801..9eea2aa 100644
--- a/pw_cpu_exception_cortex_m/BUILD.bazel
+++ b/pw_cpu_exception_cortex_m/BUILD.bazel
@@ -28,16 +28,37 @@
 )
 
 pw_cc_library(
-    name = "support_armv7m",
-    srcs = ["cpu_state.cc"],
-    hdrs = [
-        "public/pw_cpu_exception_cortex_m/cpu_state.h",
-        "public_overrides/pw_cpu_exception_backend/state.h",
+    name = "cpu_state",
+    hdrs = ["public/pw_cpu_exception_cortex_m/cpu_state.h"],
+    includes = ["public"],
+    deps = [
+        "//pw_preprocessor",
+        "//pw_preprocessor:arch",
     ],
+)
+
+pw_cc_library(
+    name = "util",
+    srcs = ["util.cc"],
+    hdrs = ["public/pw_cpu_exception_cortex_m/util.h"],
     includes = ["public"],
     deps = [
         ":config",
         ":cortex_m_constants",
+        ":cpu_state",
+        "//pw_log",
+        "//pw_preprocessor:arch",
+    ],
+)
+
+pw_cc_library(
+    name = "support",
+    srcs = ["support.cc"],
+    deps = [
+        ":config",
+        ":cortex_m_constants",
+        ":cpu_state",
+        ":util",
         "//pw_log",
         "//pw_preprocessor",
         "//pw_preprocessor:arch",
@@ -46,13 +67,15 @@
 )
 
 pw_cc_library(
-    name = "proto_dump_armv7m",
+    name = "proto_dump",
     srcs = ["proto_dump.cc"],
     hdrs = ["public/pw_cpu_exception_cortex_m/proto_dump.h"],
+    includes = ["public"],
     deps = [
         ":config",
+        ":cpu_state",
         ":cpu_state_protos",
-        ":support_armv7m",
+        ":support",
         "//pw_protobuf",
         "//pw_status",
         "//pw_stream",
@@ -64,17 +87,20 @@
     srcs = ["pw_cpu_exception_cortex_m_protos/cpu_state.proto"],
 )
 
-# TODO(pwbug/296): The *_armv7m libraries work on ARMv8-M, but needs some minor
-# patches for complete correctness. Add *_armv8m targets that use the same files
-# but provide preprocessor defines to enable/disable architecture specific code.
 pw_cc_library(
-    name = "cpu_exception_armv7m",
+    name = "cpu_exception",
     srcs = ["entry.cc"],
+    hdrs = [
+        "public/pw_cpu_exception_cortex_m/cpu_state.h",
+        "public_overrides/pw_cpu_exception_backend/state.h",
+    ],
+    includes = ["public"],
     deps = [
         ":config",
+        ":cpu_state",
         ":cortex_m_constants",
-        ":proto_dump_armv7m",
-        ":support_armv7m",
+        ":proto_dump",
+        ":support",
         # TODO(pwbug/101): Need to add support for facades/backends to Bazel.
         "//pw_cpu_exception",
         "//pw_preprocessor",
@@ -89,11 +115,10 @@
     deps = [
         ":config",
         ":cortex_m_constants",
+        ":cpu_state",
         ":cpu_state_protos",
-        ":proto_dump_armv7m",
-        ":support_armv7m",
-        # TODO(pwbug/101): Need to add support for facades/backends to Bazel.
-        "//pw_cpu_exception",
+        ":proto_dump",
+        ":support",
         "//pw_log",
         "//pw_protobuf",
         "//pw_status",
@@ -115,6 +140,7 @@
         "exception_entry_test.cc",
     ],
     deps = [
-        ":cpu_exception_armv7m",
+        ":cpu_exception",
+        ":cpu_state",
     ],
 )
diff --git a/pw_cpu_exception_cortex_m/BUILD.gn b/pw_cpu_exception_cortex_m/BUILD.gn
index 7ae1640..53d05d5 100644
--- a/pw_cpu_exception_cortex_m/BUILD.gn
+++ b/pw_cpu_exception_cortex_m/BUILD.gn
@@ -51,13 +51,13 @@
   deps = [
     ":config",
     ":cortex_m_constants",
-    ":cpu_exception",
+    ":util",
     "$dir_pw_cpu_exception:support.facade",
     "$dir_pw_preprocessor:arch",
     dir_pw_log,
     dir_pw_string,
   ]
-  sources = [ "cpu_state.cc" ]
+  sources = [ "support.cc" ]
 }
 
 # The following targets are deprecated, use ":support" instead.
@@ -68,6 +68,19 @@
   public_deps = [ ":support" ]
 }
 
+pw_source_set("util") {
+  public_configs = [ ":public_include_path" ]
+  public = [ "public/pw_cpu_exception_cortex_m/util.h" ]
+  public_deps = [ ":cpu_state" ]
+  deps = [
+    ":config",
+    ":cortex_m_constants",
+    "$dir_pw_preprocessor:arch",
+    dir_pw_log,
+  ]
+  sources = [ "util.cc" ]
+}
+
 pw_source_set("proto_dump") {
   public_configs = [ ":public_include_path" ]
   public_deps = [
@@ -96,19 +109,23 @@
   sources = [ "pw_cpu_exception_cortex_m_protos/cpu_state.proto" ]
 }
 
-# TODO(pwbug/296): The *_armv7m libraries work on ARMv8-M, but needs some minor
-# patches for complete correctness. Add *_armv8m targets that use the same files
-# but provide preprocessor defines to enable/disable architecture specific code.
+pw_source_set("cpu_state") {
+  public_configs = [ ":public_include_path" ]
+  public = [ "public/pw_cpu_exception_cortex_m/cpu_state.h" ]
+  public_deps = [
+    "$dir_pw_preprocessor",
+    "$dir_pw_preprocessor:arch",
+  ]
+}
+
 pw_source_set("cpu_exception") {
   public_configs = [
     ":backend_config",
     ":public_include_path",
   ]
-  public = [
-    "public/pw_cpu_exception_cortex_m/cpu_state.h",
-    "public_overrides/pw_cpu_exception_backend/state.h",
-  ]
+  public = [ "public_overrides/pw_cpu_exception_backend/state.h" ]
   public_deps = [
+    ":cpu_state",
     "$dir_pw_preprocessor",
     "$dir_pw_preprocessor:arch",
   ]
@@ -122,14 +139,12 @@
   public_deps = [ ":cpu_exception" ]
 }
 
-# TODO(pwbug/296): The *_armv7m libraries work on ARMv8-M, but needs some minor
-# patches for complete correctness. Add *_armv8m targets that use the same files
-# but provide preprocessor defines to enable/disable architecture specific code.
 pw_source_set("cpu_exception.impl") {
   sources = [ "entry.cc" ]
   deps = [
     ":config",
     ":cortex_m_constants",
+    ":cpu_state",
     "$dir_pw_cpu_exception:entry.facade",
     "$dir_pw_cpu_exception:handler",
     "$dir_pw_preprocessor:arch",
@@ -147,7 +162,7 @@
 pw_source_set("snapshot") {
   public_configs = [ ":public_include_path" ]
   public_deps = [
-    ":cpu_exception",
+    ":cpu_state",
     ":cpu_state_protos.pwpb",
     "$dir_pw_thread:protos.pwpb",
     "$dir_pw_thread:snapshot",
@@ -200,6 +215,7 @@
   deps = [
     ":cortex_m_constants",
     ":cpu_exception",
+    ":cpu_state",
     "$dir_pw_cpu_exception:entry",
     "$dir_pw_cpu_exception:handler",
     "$dir_pw_cpu_exception:support",
diff --git a/pw_cpu_exception_cortex_m/CMakeLists.txt b/pw_cpu_exception_cortex_m/CMakeLists.txt
index 4c41786..36603bd 100644
--- a/pw_cpu_exception_cortex_m/CMakeLists.txt
+++ b/pw_cpu_exception_cortex_m/CMakeLists.txt
@@ -24,12 +24,21 @@
     pw_cpu_exception_cortex_m_private/config.h
 )
 
+pw_add_module_library(pw_cpu_exception_cortex_m.cpu_state
+  PUBLIC_DEPS
+    pw_preprocessor
+    pw_preprocessor.arch
+  HEADERS
+    public/pw_cpu_exception_cortex_m/cpu_state.h
+)
+
 pw_add_module_library(pw_cpu_exception_cortex_m.cpu_exception
   IMPLEMENTS_FACADES
     pw_cpu_exception.entry
   PUBLIC_DEPS
     pw_preprocessor
     pw_preprocessor.arch
+    pw_cpu_exception_cortex_m.cpu_state
   PRIVATE_DEPS
     pw_cpu_exception.handler
     pw_cpu_exception_cortex_m.config
@@ -37,21 +46,35 @@
   SOURCES
     entry.cc
   HEADERS
-    public/pw_cpu_exception_cortex_m/cpu_state.h
     public_overrides/pw_cpu_exception_backend/state.h
 )
 
+pw_add_module_library(pw_cpu_exception_cortex_m.util
+  PUBLIC_DEPS
+    pw_cpu_exception_cortex_m.cpu_state
+  PRIVATE_DEPS
+    pw_cpu_exception_cortex_m.config
+    pw_cpu_exception_cortex_m.constants
+    pw_log
+    pw_preprocessor.arch
+  SOURCES
+    util.cc
+  HEADERS
+    public/pw_cpu_exception_cortex_m/util.h
+)
+
 pw_add_module_library(pw_cpu_exception_cortex_m.support
   IMPLEMENTS_FACADES
     pw_cpu_exception.support
   PRIVATE_DEPS
     pw_cpu_exception_cortex_m.config
     pw_cpu_exception_cortex_m.constants
+    pw_cpu_exception_cortex_m.util
     pw_log
     pw_preprocessor.arch
     pw_string
   SOURCES
-    cpu_state.cc
+    support.cc
 )
 
 pw_proto_library(pw_cpu_exception_cortex_m.cpu_state_protos
@@ -61,7 +84,7 @@
 
 pw_add_module_library(pw_cpu_exception_cortex_m.proto_dump
   PUBLIC_DEPS
-    pw_cpu_exception.entry
+    pw_cpu_exception_cortex_m.cpu_state
     pw_protobuf
     pw_status
     pw_stream
@@ -76,8 +99,8 @@
 
 pw_add_module_library(pw_cpu_exception_cortex_m.snapshot
   PUBLIC_DEPS
+    pw_cpu_exception_cortex_m.cpu_state
     pw_cpu_exception_cortex_m.cpu_state_protos.pwpb
-    pw_cpu_exception_cortex_m.cpu_exception
     pw_protobuf
     pw_status
   PRIVATE_DEPS
diff --git a/pw_cpu_exception_cortex_m/cpu_state.cc b/pw_cpu_exception_cortex_m/cpu_state.cc
deleted file mode 100644
index b41c168..0000000
--- a/pw_cpu_exception_cortex_m/cpu_state.cc
+++ /dev/null
@@ -1,272 +0,0 @@
-// Copyright 2019 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_cpu_exception_cortex_m/cpu_state.h"
-
-#include <cinttypes>
-#include <cstdint>
-#include <span>
-
-#include "pw_cpu_exception/support.h"
-#include "pw_cpu_exception_cortex_m_private/config.h"
-#include "pw_cpu_exception_cortex_m_private/cortex_m_constants.h"
-#include "pw_log/log.h"
-#include "pw_preprocessor/arch.h"
-#include "pw_string/string_builder.h"
-
-namespace pw::cpu_exception {
-namespace cortex_m {
-namespace {
-
-[[maybe_unused]] void AnalyzeCfsr(const uint32_t cfsr) {
-  if (cfsr == 0) {
-    return;
-  }
-
-  PW_LOG_INFO("Active CFSR fields:");
-
-  // Memory managment fault fields.
-  if (cfsr & kCfsrIaccviolMask) {
-    PW_LOG_ERROR("  IACCVIOL: MPU violation on instruction fetch");
-  }
-  if (cfsr & kCfsrDaccviolMask) {
-    PW_LOG_ERROR("  DACCVIOL: MPU violation on memory read/write");
-  }
-  if (cfsr & kCfsrMunstkerrMask) {
-    PW_LOG_ERROR("  MUNSTKERR: 'MPU violation on exception return");
-  }
-  if (cfsr & kCfsrMstkerrMask) {
-    PW_LOG_ERROR("  MSTKERR: MPU violation on exception entry");
-  }
-  if (cfsr & kCfsrMlsperrMask) {
-    PW_LOG_ERROR("  MLSPERR: MPU violation on lazy FPU state preservation");
-  }
-  if (cfsr & kCfsrMmarvalidMask) {
-    PW_LOG_ERROR("  MMARVALID: MMFAR register is valid");
-  }
-
-  // Bus fault fields.
-  if (cfsr & kCfsrIbuserrMask) {
-    PW_LOG_ERROR("  IBUSERR: Bus fault on instruction fetch");
-  }
-  if (cfsr & kCfsrPreciserrMask) {
-    PW_LOG_ERROR("  PRECISERR: Precise bus fault");
-  }
-  if (cfsr & kCfsrImpreciserrMask) {
-    PW_LOG_ERROR("  IMPRECISERR: Imprecise bus fault");
-  }
-  if (cfsr & kCfsrUnstkerrMask) {
-    PW_LOG_ERROR("  UNSTKERR: Derived bus fault on exception context save");
-  }
-  if (cfsr & kCfsrStkerrMask) {
-    PW_LOG_ERROR("  STKERR: Derived bus fault on exception context restore");
-  }
-  if (cfsr & kCfsrLsperrMask) {
-    PW_LOG_ERROR("  LSPERR: Derived bus fault on lazy FPU state preservation");
-  }
-  if (cfsr & kCfsrBfarvalidMask) {
-    PW_LOG_ERROR("  BFARVALID: BFAR register is valid");
-  }
-
-  // Usage fault fields.
-  if (cfsr & kCfsrUndefinstrMask) {
-    PW_LOG_ERROR("  UNDEFINSTR: Encountered invalid instruction");
-  }
-  if (cfsr & kCfsrInvstateMask) {
-    PW_LOG_ERROR(
-        "  INVSTATE: Attempted to execute an instruction with an invalid "
-        "Execution Program Status Register (EPSR) value");
-  }
-  if (cfsr & kCfsrInvpcMask) {
-    PW_LOG_ERROR("  INVPC: Program Counter (PC) is not legal");
-  }
-  if (cfsr & kCfsrNocpMask) {
-    PW_LOG_ERROR("  NOCP: Coprocessor disabled or not present");
-  }
-  if (cfsr & kCfsrUnalignedMask) {
-    PW_LOG_ERROR("  UNALIGNED: Unaligned memory access");
-  }
-  if (cfsr & kCfsrDivbyzeroMask) {
-    PW_LOG_ERROR("  DIVBYZERO: Division by zero");
-  }
-#if _PW_ARCH_ARM_V8M_MAINLINE
-  if (cfsr & kCfsrStkofMask) {
-    PW_LOG_ERROR("  STKOF: Stack overflowed");
-  }
-#endif  // _PW_ARCH_ARM_V8M_MAINLINE
-}
-
-void AnalyzeException(const pw_cpu_exception_State& cpu_state) {
-  // This provides a high-level assessment of the cause of the exception.
-  // These conditionals are ordered by priority to ensure the most critical
-  // issues are highlighted first. These are not mutually exclusive; a bus fault
-  // could occur during the handling of a MPU violation, causing a nested fault.
-  if (cpu_state.extended.hfsr & kHfsrForcedMask) {
-    PW_LOG_CRITICAL("Encountered a nested CPU fault (See active CFSR fields)");
-  }
-#if _PW_ARCH_ARM_V8M_MAINLINE
-  if (cpu_state.extended.cfsr & kCfsrStkofMask) {
-    if (cpu_state.extended.exc_return & kExcReturnStackMask) {
-      PW_LOG_CRITICAL("Encountered stack overflow in thread mode");
-    } else {
-      PW_LOG_CRITICAL("Encountered main (interrupt handler) stack overflow");
-    }
-  }
-#endif  // _PW_ARCH_ARM_V8M_MAINLINE
-  if (cpu_state.extended.cfsr & kCfsrMemFaultMask) {
-    if (cpu_state.extended.cfsr & kCfsrMmarvalidMask) {
-      PW_LOG_CRITICAL(
-          "Encountered Memory Protection Unit (MPU) violation at 0x%08" PRIx32,
-          cpu_state.extended.mmfar);
-    } else {
-      PW_LOG_CRITICAL("Encountered Memory Protection Unit (MPU) violation");
-    }
-  }
-  if (cpu_state.extended.cfsr & kCfsrBusFaultMask) {
-    if (cpu_state.extended.cfsr & kCfsrBfarvalidMask) {
-      PW_LOG_CRITICAL("Encountered bus fault at 0x%08" PRIx32,
-                      cpu_state.extended.bfar);
-    } else {
-      PW_LOG_CRITICAL("Encountered bus fault");
-    }
-  }
-  if (cpu_state.extended.cfsr & kCfsrUsageFaultMask) {
-    PW_LOG_CRITICAL("Encountered usage fault (See active CFSR fields)");
-  }
-  if ((cpu_state.extended.icsr & kIcsrVectactiveMask) == kNmiIsrNum) {
-    PW_LOG_INFO("Encountered non-maskable interrupt (NMI)");
-  }
-#if PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
-  AnalyzeCfsr(cpu_state.extended.cfsr);
-#endif  // PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
-}
-
-}  // namespace
-}  // namespace cortex_m
-
-std::span<const uint8_t> RawFaultingCpuState(
-    const pw_cpu_exception_State& cpu_state) {
-  return std::span(reinterpret_cast<const uint8_t*>(&cpu_state),
-                   sizeof(cpu_state));
-}
-
-// Using this function adds approximately 100 bytes to binary size.
-void ToString(const pw_cpu_exception_State& cpu_state,
-              const std::span<char>& dest) {
-  StringBuilder builder(dest);
-  const cortex_m::ExceptionRegisters& base = cpu_state.base;
-  const cortex_m::ExtraRegisters& extended = cpu_state.extended;
-
-#define _PW_FORMAT_REGISTER(state_section, name) \
-  builder.Format("%s=0x%08" PRIx32 "\n", #name, state_section.name)
-
-  // Other registers.
-  if (base.pc != cortex_m::kUndefinedPcLrOrPsrRegValue) {
-    _PW_FORMAT_REGISTER(base, pc);
-  }
-  if (base.lr != cortex_m::kUndefinedPcLrOrPsrRegValue) {
-    _PW_FORMAT_REGISTER(base, lr);
-  }
-  if (base.psr != cortex_m::kUndefinedPcLrOrPsrRegValue) {
-    _PW_FORMAT_REGISTER(base, psr);
-  }
-  _PW_FORMAT_REGISTER(extended, msp);
-  _PW_FORMAT_REGISTER(extended, psp);
-  _PW_FORMAT_REGISTER(extended, exc_return);
-#if _PW_ARCH_ARM_V8M_MAINLINE
-  _PW_FORMAT_REGISTER(extended, msplim);
-  _PW_FORMAT_REGISTER(extended, psplim);
-#endif  // _PW_ARCH_ARM_V8M_MAINLINE
-  _PW_FORMAT_REGISTER(extended, cfsr);
-  _PW_FORMAT_REGISTER(extended, mmfar);
-  _PW_FORMAT_REGISTER(extended, bfar);
-  _PW_FORMAT_REGISTER(extended, icsr);
-  _PW_FORMAT_REGISTER(extended, hfsr);
-  _PW_FORMAT_REGISTER(extended, shcsr);
-  _PW_FORMAT_REGISTER(extended, control);
-
-  // General purpose registers.
-  _PW_FORMAT_REGISTER(base, r0);
-  _PW_FORMAT_REGISTER(base, r1);
-  _PW_FORMAT_REGISTER(base, r2);
-  _PW_FORMAT_REGISTER(base, r3);
-  _PW_FORMAT_REGISTER(extended, r4);
-  _PW_FORMAT_REGISTER(extended, r5);
-  _PW_FORMAT_REGISTER(extended, r6);
-  _PW_FORMAT_REGISTER(extended, r7);
-  _PW_FORMAT_REGISTER(extended, r8);
-  _PW_FORMAT_REGISTER(extended, r9);
-  _PW_FORMAT_REGISTER(extended, r10);
-  _PW_FORMAT_REGISTER(extended, r11);
-  _PW_FORMAT_REGISTER(base, r12);
-
-#undef _PW_FORMAT_REGISTER
-}
-
-// Using this function adds approximately 100 bytes to binary size.
-void LogCpuState(const pw_cpu_exception_State& cpu_state) {
-  const cortex_m::ExceptionRegisters& base = cpu_state.base;
-  const cortex_m::ExtraRegisters& extended = cpu_state.extended;
-
-  cortex_m::AnalyzeException(cpu_state);
-
-  PW_LOG_INFO("All captured CPU registers:");
-
-#define _PW_LOG_REGISTER(state_section, name) \
-  PW_LOG_INFO("  %-10s 0x%08" PRIx32, #name, state_section.name)
-
-  // Other registers.
-  if (base.pc != cortex_m::kUndefinedPcLrOrPsrRegValue) {
-    _PW_LOG_REGISTER(base, pc);
-  }
-  if (base.lr != cortex_m::kUndefinedPcLrOrPsrRegValue) {
-    _PW_LOG_REGISTER(base, lr);
-  }
-  if (base.psr != cortex_m::kUndefinedPcLrOrPsrRegValue) {
-    _PW_LOG_REGISTER(base, psr);
-  }
-  _PW_LOG_REGISTER(extended, msp);
-  _PW_LOG_REGISTER(extended, psp);
-  _PW_LOG_REGISTER(extended, exc_return);
-#if _PW_ARCH_ARM_V8M_MAINLINE
-  _PW_LOG_REGISTER(extended, msplim);
-  _PW_LOG_REGISTER(extended, psplim);
-#endif  // _PW_ARCH_ARM_V8M_MAINLINE
-  _PW_LOG_REGISTER(extended, cfsr);
-  _PW_LOG_REGISTER(extended, mmfar);
-  _PW_LOG_REGISTER(extended, bfar);
-  _PW_LOG_REGISTER(extended, icsr);
-  _PW_LOG_REGISTER(extended, hfsr);
-  _PW_LOG_REGISTER(extended, shcsr);
-  _PW_LOG_REGISTER(extended, control);
-
-  // General purpose registers.
-  _PW_LOG_REGISTER(base, r0);
-  _PW_LOG_REGISTER(base, r1);
-  _PW_LOG_REGISTER(base, r2);
-  _PW_LOG_REGISTER(base, r3);
-  _PW_LOG_REGISTER(extended, r4);
-  _PW_LOG_REGISTER(extended, r5);
-  _PW_LOG_REGISTER(extended, r6);
-  _PW_LOG_REGISTER(extended, r7);
-  _PW_LOG_REGISTER(extended, r8);
-  _PW_LOG_REGISTER(extended, r9);
-  _PW_LOG_REGISTER(extended, r10);
-  _PW_LOG_REGISTER(extended, r11);
-  _PW_LOG_REGISTER(base, r12);
-
-#undef _PW_LOG_REGISTER
-}
-
-}  // namespace pw::cpu_exception
diff --git a/pw_cpu_exception_cortex_m/docs.rst b/pw_cpu_exception_cortex_m/docs.rst
index e441fc5..b8726b2 100644
--- a/pw_cpu_exception_cortex_m/docs.rst
+++ b/pw_cpu_exception_cortex_m/docs.rst
@@ -236,3 +236,27 @@
   r10        0xbd15c968
   r11        0x759b95ab
   r12        0x00000000
+
+Module Configuration Options
+============================
+The following configurations can be adjusted via compile-time configuration of
+this module, see the
+:ref:`module documentation <module-structure-compile-time-configuration>` for
+more details.
+
+.. c:macro:: PW_CPU_EXCEPTION_CORTEX_M_LOG_LEVEL
+
+  The log level to use for this module. Logs below this level are omitted.
+
+  This defaults to ``PW_LOG_LEVEL_DEBUG``.
+
+.. c:macro:: PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
+
+  Enables extended logging in pw::cpu_exception::LogCpuState() and
+  pw::cpu_exception::cortex_m::LogExceptionAnalysis() that dumps the active
+  CFSR fields with help strings. This is disabled by default since it
+  increases the binary size by >1.5KB when using plain-text logs, or ~460
+  Bytes when using tokenized logging. It's useful to enable this for device
+  bringup until your application has an end-to-end crash reporting solution.
+
+  This is disabled by default.
diff --git a/pw_cpu_exception_cortex_m/public/pw_cpu_exception_cortex_m/util.h b/pw_cpu_exception_cortex_m/public/pw_cpu_exception_cortex_m/util.h
new file mode 100644
index 0000000..a760bb5
--- /dev/null
+++ b/pw_cpu_exception_cortex_m/public/pw_cpu_exception_cortex_m/util.h
@@ -0,0 +1,22 @@
+// Copyright 2022 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 "pw_cpu_exception_cortex_m/cpu_state.h"
+
+namespace pw::cpu_exception::cortex_m {
+
+void LogExceptionAnalysis(const pw_cpu_exception_State& cpu_state);
+
+}  // namespace pw::cpu_exception::cortex_m
diff --git a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
index c34a49b..eb0b7dd 100644
--- a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
+++ b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
@@ -31,8 +31,9 @@
 #define PW_CPU_EXCEPTION_CORTEX_M_LOG_LEVEL PW_LOG_LEVEL_DEBUG
 #endif  // PW_CPU_EXCEPTION_CORTEX_M_LOG_LEVEL
 
-// Enables extended logging in pw::cpu_exception::LogCpuState() that dumps the
-// active CFSR fields with help strings. This is disabled by default since it
+// Enables extended logging in pw::cpu_exception::LogCpuState() and
+// pw::cpu_exception::cortex_m::LogExceptionAnalysis() that dumps the active
+// CFSR fields with help strings. This is disabled by default since it
 // increases the binary size by >1.5KB when using plain-text logs, or ~460
 // Bytes when using tokenized logging. It's useful to enable this for device
 // bringup until your application has an end-to-end crash reporting solution.
diff --git a/pw_cpu_exception_cortex_m/support.cc b/pw_cpu_exception_cortex_m/support.cc
new file mode 100644
index 0000000..719ac5b
--- /dev/null
+++ b/pw_cpu_exception_cortex_m/support.cc
@@ -0,0 +1,145 @@
+// Copyright 2019 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_cpu_exception/support.h"
+
+#include <cinttypes>
+#include <cstdint>
+#include <span>
+
+#include "pw_cpu_exception_cortex_m/cpu_state.h"
+#include "pw_cpu_exception_cortex_m/util.h"
+#include "pw_cpu_exception_cortex_m_private/config.h"
+#include "pw_cpu_exception_cortex_m_private/cortex_m_constants.h"
+#include "pw_log/log.h"
+#include "pw_preprocessor/arch.h"
+#include "pw_string/string_builder.h"
+
+namespace pw::cpu_exception {
+
+std::span<const uint8_t> RawFaultingCpuState(
+    const pw_cpu_exception_State& cpu_state) {
+  return std::span(reinterpret_cast<const uint8_t*>(&cpu_state),
+                   sizeof(cpu_state));
+}
+
+// Using this function adds approximately 100 bytes to binary size.
+void ToString(const pw_cpu_exception_State& cpu_state,
+              const std::span<char>& dest) {
+  StringBuilder builder(dest);
+  const cortex_m::ExceptionRegisters& base = cpu_state.base;
+  const cortex_m::ExtraRegisters& extended = cpu_state.extended;
+
+#define _PW_FORMAT_REGISTER(state_section, name) \
+  builder.Format("%s=0x%08" PRIx32 "\n", #name, state_section.name)
+
+  // Other registers.
+  if (base.pc != cortex_m::kUndefinedPcLrOrPsrRegValue) {
+    _PW_FORMAT_REGISTER(base, pc);
+  }
+  if (base.lr != cortex_m::kUndefinedPcLrOrPsrRegValue) {
+    _PW_FORMAT_REGISTER(base, lr);
+  }
+  if (base.psr != cortex_m::kUndefinedPcLrOrPsrRegValue) {
+    _PW_FORMAT_REGISTER(base, psr);
+  }
+  _PW_FORMAT_REGISTER(extended, msp);
+  _PW_FORMAT_REGISTER(extended, psp);
+  _PW_FORMAT_REGISTER(extended, exc_return);
+#if _PW_ARCH_ARM_V8M_MAINLINE
+  _PW_FORMAT_REGISTER(extended, msplim);
+  _PW_FORMAT_REGISTER(extended, psplim);
+#endif  // _PW_ARCH_ARM_V8M_MAINLINE
+  _PW_FORMAT_REGISTER(extended, cfsr);
+  _PW_FORMAT_REGISTER(extended, mmfar);
+  _PW_FORMAT_REGISTER(extended, bfar);
+  _PW_FORMAT_REGISTER(extended, icsr);
+  _PW_FORMAT_REGISTER(extended, hfsr);
+  _PW_FORMAT_REGISTER(extended, shcsr);
+  _PW_FORMAT_REGISTER(extended, control);
+
+  // General purpose registers.
+  _PW_FORMAT_REGISTER(base, r0);
+  _PW_FORMAT_REGISTER(base, r1);
+  _PW_FORMAT_REGISTER(base, r2);
+  _PW_FORMAT_REGISTER(base, r3);
+  _PW_FORMAT_REGISTER(extended, r4);
+  _PW_FORMAT_REGISTER(extended, r5);
+  _PW_FORMAT_REGISTER(extended, r6);
+  _PW_FORMAT_REGISTER(extended, r7);
+  _PW_FORMAT_REGISTER(extended, r8);
+  _PW_FORMAT_REGISTER(extended, r9);
+  _PW_FORMAT_REGISTER(extended, r10);
+  _PW_FORMAT_REGISTER(extended, r11);
+  _PW_FORMAT_REGISTER(base, r12);
+
+#undef _PW_FORMAT_REGISTER
+}
+
+// Using this function adds approximately 100 bytes to binary size.
+void LogCpuState(const pw_cpu_exception_State& cpu_state) {
+  const cortex_m::ExceptionRegisters& base = cpu_state.base;
+  const cortex_m::ExtraRegisters& extended = cpu_state.extended;
+
+  cortex_m::LogExceptionAnalysis(cpu_state);
+
+  PW_LOG_INFO("All captured CPU registers:");
+
+#define _PW_LOG_REGISTER(state_section, name) \
+  PW_LOG_INFO("  %-10s 0x%08" PRIx32, #name, state_section.name)
+
+  // Other registers.
+  if (base.pc != cortex_m::kUndefinedPcLrOrPsrRegValue) {
+    _PW_LOG_REGISTER(base, pc);
+  }
+  if (base.lr != cortex_m::kUndefinedPcLrOrPsrRegValue) {
+    _PW_LOG_REGISTER(base, lr);
+  }
+  if (base.psr != cortex_m::kUndefinedPcLrOrPsrRegValue) {
+    _PW_LOG_REGISTER(base, psr);
+  }
+  _PW_LOG_REGISTER(extended, msp);
+  _PW_LOG_REGISTER(extended, psp);
+  _PW_LOG_REGISTER(extended, exc_return);
+#if _PW_ARCH_ARM_V8M_MAINLINE
+  _PW_LOG_REGISTER(extended, msplim);
+  _PW_LOG_REGISTER(extended, psplim);
+#endif  // _PW_ARCH_ARM_V8M_MAINLINE
+  _PW_LOG_REGISTER(extended, cfsr);
+  _PW_LOG_REGISTER(extended, mmfar);
+  _PW_LOG_REGISTER(extended, bfar);
+  _PW_LOG_REGISTER(extended, icsr);
+  _PW_LOG_REGISTER(extended, hfsr);
+  _PW_LOG_REGISTER(extended, shcsr);
+  _PW_LOG_REGISTER(extended, control);
+
+  // General purpose registers.
+  _PW_LOG_REGISTER(base, r0);
+  _PW_LOG_REGISTER(base, r1);
+  _PW_LOG_REGISTER(base, r2);
+  _PW_LOG_REGISTER(base, r3);
+  _PW_LOG_REGISTER(extended, r4);
+  _PW_LOG_REGISTER(extended, r5);
+  _PW_LOG_REGISTER(extended, r6);
+  _PW_LOG_REGISTER(extended, r7);
+  _PW_LOG_REGISTER(extended, r8);
+  _PW_LOG_REGISTER(extended, r9);
+  _PW_LOG_REGISTER(extended, r10);
+  _PW_LOG_REGISTER(extended, r11);
+  _PW_LOG_REGISTER(base, r12);
+
+#undef _PW_LOG_REGISTER
+}
+
+}  // namespace pw::cpu_exception
diff --git a/pw_cpu_exception_cortex_m/util.cc b/pw_cpu_exception_cortex_m/util.cc
new file mode 100644
index 0000000..ff817b3
--- /dev/null
+++ b/pw_cpu_exception_cortex_m/util.cc
@@ -0,0 +1,153 @@
+// Copyright 2022 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_cpu_exception_cortex_m/util.h"
+
+#include <cinttypes>
+
+#include "pw_cpu_exception_cortex_m/cpu_state.h"
+#include "pw_cpu_exception_cortex_m_private/config.h"
+#include "pw_cpu_exception_cortex_m_private/cortex_m_constants.h"
+#include "pw_log/log.h"
+#include "pw_preprocessor/arch.h"
+
+namespace pw::cpu_exception::cortex_m {
+namespace {
+
+[[maybe_unused]] void LogCfsrAnalysis(const uint32_t cfsr) {
+  if (cfsr == 0) {
+    return;
+  }
+
+  PW_LOG_INFO("Active CFSR fields:");
+
+  // Memory managment fault fields.
+  if (cfsr & kCfsrIaccviolMask) {
+    PW_LOG_ERROR("  IACCVIOL: MPU violation on instruction fetch");
+  }
+  if (cfsr & kCfsrDaccviolMask) {
+    PW_LOG_ERROR("  DACCVIOL: MPU violation on memory read/write");
+  }
+  if (cfsr & kCfsrMunstkerrMask) {
+    PW_LOG_ERROR("  MUNSTKERR: 'MPU violation on exception return");
+  }
+  if (cfsr & kCfsrMstkerrMask) {
+    PW_LOG_ERROR("  MSTKERR: MPU violation on exception entry");
+  }
+  if (cfsr & kCfsrMlsperrMask) {
+    PW_LOG_ERROR("  MLSPERR: MPU violation on lazy FPU state preservation");
+  }
+  if (cfsr & kCfsrMmarvalidMask) {
+    PW_LOG_ERROR("  MMARVALID: MMFAR register is valid");
+  }
+
+  // Bus fault fields.
+  if (cfsr & kCfsrIbuserrMask) {
+    PW_LOG_ERROR("  IBUSERR: Bus fault on instruction fetch");
+  }
+  if (cfsr & kCfsrPreciserrMask) {
+    PW_LOG_ERROR("  PRECISERR: Precise bus fault");
+  }
+  if (cfsr & kCfsrImpreciserrMask) {
+    PW_LOG_ERROR("  IMPRECISERR: Imprecise bus fault");
+  }
+  if (cfsr & kCfsrUnstkerrMask) {
+    PW_LOG_ERROR("  UNSTKERR: Derived bus fault on exception context save");
+  }
+  if (cfsr & kCfsrStkerrMask) {
+    PW_LOG_ERROR("  STKERR: Derived bus fault on exception context restore");
+  }
+  if (cfsr & kCfsrLsperrMask) {
+    PW_LOG_ERROR("  LSPERR: Derived bus fault on lazy FPU state preservation");
+  }
+  if (cfsr & kCfsrBfarvalidMask) {
+    PW_LOG_ERROR("  BFARVALID: BFAR register is valid");
+  }
+
+  // Usage fault fields.
+  if (cfsr & kCfsrUndefinstrMask) {
+    PW_LOG_ERROR("  UNDEFINSTR: Encountered invalid instruction");
+  }
+  if (cfsr & kCfsrInvstateMask) {
+    PW_LOG_ERROR(
+        "  INVSTATE: Attempted to execute an instruction with an invalid "
+        "Execution Program Status Register (EPSR) value");
+  }
+  if (cfsr & kCfsrInvpcMask) {
+    PW_LOG_ERROR("  INVPC: Program Counter (PC) is not legal");
+  }
+  if (cfsr & kCfsrNocpMask) {
+    PW_LOG_ERROR("  NOCP: Coprocessor disabled or not present");
+  }
+  if (cfsr & kCfsrUnalignedMask) {
+    PW_LOG_ERROR("  UNALIGNED: Unaligned memory access");
+  }
+  if (cfsr & kCfsrDivbyzeroMask) {
+    PW_LOG_ERROR("  DIVBYZERO: Division by zero");
+  }
+#if _PW_ARCH_ARM_V8M_MAINLINE
+  if (cfsr & kCfsrStkofMask) {
+    PW_LOG_ERROR("  STKOF: Stack overflowed");
+  }
+#endif  // _PW_ARCH_ARM_V8M_MAINLINE
+}
+
+}  // namespace
+
+void LogExceptionAnalysis(const pw_cpu_exception_State& cpu_state) {
+  // This provides a high-level assessment of the cause of the exception.
+  // These conditionals are ordered by priority to ensure the most critical
+  // issues are highlighted first. These are not mutually exclusive; a bus fault
+  // could occur during the handling of a MPU violation, causing a nested fault.
+  if (cpu_state.extended.hfsr & kHfsrForcedMask) {
+    PW_LOG_CRITICAL("Encountered a nested CPU fault (See active CFSR fields)");
+  }
+#if _PW_ARCH_ARM_V8M_MAINLINE
+  if (cpu_state.extended.cfsr & kCfsrStkofMask) {
+    if (cpu_state.extended.exc_return & kExcReturnStackMask) {
+      PW_LOG_CRITICAL("Encountered stack overflow in thread mode");
+    } else {
+      PW_LOG_CRITICAL("Encountered main (interrupt handler) stack overflow");
+    }
+  }
+#endif  // _PW_ARCH_ARM_V8M_MAINLINE
+  if (cpu_state.extended.cfsr & kCfsrMemFaultMask) {
+    if (cpu_state.extended.cfsr & kCfsrMmarvalidMask) {
+      PW_LOG_CRITICAL(
+          "Encountered Memory Protection Unit (MPU) violation at 0x%08" PRIx32,
+          cpu_state.extended.mmfar);
+    } else {
+      PW_LOG_CRITICAL("Encountered Memory Protection Unit (MPU) violation");
+    }
+  }
+  if (cpu_state.extended.cfsr & kCfsrBusFaultMask) {
+    if (cpu_state.extended.cfsr & kCfsrBfarvalidMask) {
+      PW_LOG_CRITICAL("Encountered bus fault at 0x%08" PRIx32,
+                      cpu_state.extended.bfar);
+    } else {
+      PW_LOG_CRITICAL("Encountered bus fault");
+    }
+  }
+  if (cpu_state.extended.cfsr & kCfsrUsageFaultMask) {
+    PW_LOG_CRITICAL("Encountered usage fault (See active CFSR fields)");
+  }
+  if ((cpu_state.extended.icsr & kIcsrVectactiveMask) == kNmiIsrNum) {
+    PW_LOG_INFO("Encountered non-maskable interrupt (NMI)");
+  }
+#if PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
+  LogCfsrAnalysis(cpu_state.extended.cfsr);
+#endif  // PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
+}
+
+}  // namespace pw::cpu_exception::cortex_m