pw_cpu_exception_cortex_m: Add CFSR decode tool

Adds a very simple tool to break down a CFSR value into the various
fault flags.

Change-Id: I3df3e938c6b62b3d57ba28ce3b7713bdf5eae6bd
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/40667
Reviewed-by: David Rogers <davidrogers@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Pigweed-Auto-Submit: Armando Montanez <amontanez@google.com>
diff --git a/pw_cpu_exception_cortex_m/docs.rst b/pw_cpu_exception_cortex_m/docs.rst
index fd23ec6..3b7c9be 100644
--- a/pw_cpu_exception_cortex_m/docs.rst
+++ b/pw_cpu_exception_cortex_m/docs.rst
@@ -113,3 +113,33 @@
    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.
+
+Exception Analysis
+==================
+This module provides Python tooling to analyze CPU state captured by a Cortex-M
+core during an exception. This can be particularly useful as part of a larger
+crash report analyzer.
+
+CFSR decoder
+------------
+The ARMv7-M and ARMv8-M architectures have a Configurable Fault Status Register
+(CFSR) that explains what illegal behavior caused a fault. Even with no
+additional context, it can provide quite a bit of insight into what caused the
+CPU to fault. This module provides a simple command-line tool to decode raw CFSR
+contents (e.g. 0x00010000) as human-readable information (e.g. "Encountered
+invalid instruction"). An example of this tool in use is provided below:
+
+  .. code-block::
+
+    $ python -m pw_cpu_exception_cortex_m.cfsr_decoder 0x00010100
+    20210412 15:11:14 INF Exception caused by a usage fault, bus fault.
+
+    Active Crash Fault Status Register (CFSR) fields:
+    IBUSERR     Bus fault on instruction fetch.
+    UNDEFINSTR  Encountered invalid instruction.
+
+    All registers:
+    cfsr       0x00010100
+
+.. note::
+  The CFSR is not supported on ARMv6-M CPUs (Cortex M0, M0+, M1).
diff --git a/pw_cpu_exception_cortex_m/py/BUILD.gn b/pw_cpu_exception_cortex_m/py/BUILD.gn
index df5dc96..efa0b85 100644
--- a/pw_cpu_exception_cortex_m/py/BUILD.gn
+++ b/pw_cpu_exception_cortex_m/py/BUILD.gn
@@ -20,6 +20,7 @@
   setup = [ "setup.py" ]
   sources = [
     "pw_cpu_exception_cortex_m/__init__.py",
+    "pw_cpu_exception_cortex_m/cfsr_decoder.py",
     "pw_cpu_exception_cortex_m/cortex_m_constants.py",
     "pw_cpu_exception_cortex_m/exception_analyzer.py",
   ]
@@ -27,6 +28,7 @@
   python_deps = [
     "$dir_pw_cli/py",
     "$dir_pw_protobuf_compiler/py",
+    "..:cpu_state_protos.python",
   ]
   pylintrc = "$dir_pigweed/.pylintrc"
 }
diff --git a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cfsr_decoder.py b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cfsr_decoder.py
new file mode 100644
index 0000000..380fb1f
--- /dev/null
+++ b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cfsr_decoder.py
@@ -0,0 +1,61 @@
+# 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.
+"""A simple tool to decode a CFSR register from the command line
+
+Example usage:
+
+  $ python -m pw_cpu_exception_cortex_m.cfsr_decoder 0x00010100
+
+  20210412 15:09:01 INF Exception caused by a usage fault, bus fault.
+
+  Active Crash Fault Status Register (CFSR) fields:
+  IBUSERR     Bus fault on instruction fetch.
+  UNDEFINSTR  Encountered invalid instruction.
+
+  All registers:
+  cfsr       0x00010100
+"""
+
+import argparse
+import logging
+import sys
+import pw_cli.log
+
+from pw_cpu_exception_cortex_m_protos import cpu_state_pb2
+from pw_cpu_exception_cortex_m import exception_analyzer
+
+_LOG = logging.getLogger('decode_cfsr')
+
+
+def _parse_args() -> argparse.Namespace:
+    """Parses arguments for this script, splitting out the command to run."""
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('cfsr',
+                        type=lambda val: int(val, 0),
+                        help='The Cortex-M CFSR to decode')
+    return parser.parse_args()
+
+
+def dump_cfsr(cfsr: int) -> int:
+    cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
+    cpu_state_proto.cfsr = cfsr
+    cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
+        cpu_state_proto)
+    _LOG.info(cpu_state_info)
+    return 0
+
+
+if __name__ == '__main__':
+    pw_cli.log.install(level=logging.INFO)
+    sys.exit(dump_cfsr(**vars(_parse_args())))