1# Copyright 2020 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Tools to analyze Cortex-M CPU state context captured during an exception."""
15
16
17from pw_cpu_exception_cortex_m import cortex_m_constants
18from pw_cpu_exception_cortex_m_protos import cpu_state_pb2
19import pw_symbolizer
20
21# These registers are symbolized when dumped.
22_SYMBOLIZED_REGISTERS = (
23    'pc',
24    'lr',
25    'bfar',
26    'mmfar',
27    'msp',
28    'psp',
29    'r0',
30    'r1',
31    'r2',
32    'r3',
33    'r4',
34    'r5',
35    'r6',
36    'r7',
37    'r8',
38    'r9',
39    'r10',
40    'r11',
41    'r12',
42)
43
44
45class CortexMExceptionAnalyzer:
46    """This class provides helper functions to dump a ArmV7mCpuState proto."""
47
48    def __init__(
49        self, cpu_state, symbolizer: pw_symbolizer.Symbolizer | None = None
50    ):
51        self._cpu_state = cpu_state
52        self._symbolizer = symbolizer
53        self._active_cfsr_fields: (
54            tuple[cortex_m_constants.BitField, ...] | None
55        ) = None
56
57    def active_cfsr_fields(self) -> tuple[cortex_m_constants.BitField, ...]:
58        """Returns a list of BitFields for each active CFSR flag."""
59
60        if self._active_cfsr_fields is not None:
61            return self._active_cfsr_fields
62
63        temp_field_list = []
64        if self._cpu_state.HasField('cfsr'):
65            for bit_field in cortex_m_constants.PW_CORTEX_M_CFSR_BIT_FIELDS:
66                if self._cpu_state.cfsr & bit_field.bit_mask:
67                    temp_field_list.append(bit_field)
68        self._active_cfsr_fields = tuple(temp_field_list)
69        return self._active_cfsr_fields
70
71    def is_fault_active(self) -> bool:
72        """Returns true if the current CPU state indicates a fault is active."""
73        if self._cpu_state.HasField('cfsr') and self._cpu_state.cfsr != 0:
74            return True
75        if self._cpu_state.HasField('icsr'):
76            exception_number = (
77                self._cpu_state.icsr
78                & cortex_m_constants.PW_CORTEX_M_ICSR_VECTACTIVE_MASK
79            )
80            if (
81                cortex_m_constants.PW_CORTEX_M_HARD_FAULT_ISR_NUM
82                <= exception_number
83                <= cortex_m_constants.PW_CORTEX_M_USAGE_FAULT_ISR_NUM
84            ):
85                return True
86        return False
87
88    def is_nested_fault(self) -> bool:
89        """Returns true if the current CPU state indicates a nested fault."""
90        if not self.is_fault_active():
91            return False
92        if (
93            self._cpu_state.HasField('hfsr')
94            and self._cpu_state.hfsr
95            & cortex_m_constants.PW_CORTEX_M_HFSR_FORCED_MASK
96        ):
97            return True
98        return False
99
100    def exception_cause(self, show_active_cfsr_fields=True) -> str:
101        """Analyzes CPU state to tries and classify the exception.
102
103        Examples:
104            show_active_cfsr_fields=False
105              unknown exception
106              memory management fault at 0x00000000
107              usage fault, imprecise bus fault
108
109            show_active_cfsr_fields=True
110              usage fault [DIVBYZERO]
111              memory management fault at 0x00000000 [DACCVIOL] [MMARVALID]
112        """
113        cause = ''
114
115        # The CFSR can accumulate multiple exceptions.
116        def split_major_cause(cause):
117            return cause if not cause else cause + ', '
118
119        if self._cpu_state.HasField('cfsr') and self.is_fault_active():
120            if (
121                self._cpu_state.cfsr
122                & cortex_m_constants.PW_CORTEX_M_CFSR_USAGE_FAULT_MASK
123            ):
124                cause += 'usage fault'
125
126            if (
127                self._cpu_state.cfsr
128                & cortex_m_constants.PW_CORTEX_M_CFSR_MEM_FAULT_MASK
129            ):
130                cause = split_major_cause(cause)
131                cause += 'memory management fault'
132                if (
133                    self._cpu_state.cfsr
134                    & cortex_m_constants.PW_CORTEX_M_CFSR_MMARVALID_MASK
135                ):
136                    addr = (
137                        '???'
138                        if not self._cpu_state.HasField('mmfar')
139                        else f'0x{self._cpu_state.mmfar:08x}'
140                    )
141                    cause += f' at {addr}'
142
143            if (
144                self._cpu_state.cfsr
145                & cortex_m_constants.PW_CORTEX_M_CFSR_BUS_FAULT_MASK
146            ):
147                cause = split_major_cause(cause)
148                if (
149                    self._cpu_state.cfsr
150                    & cortex_m_constants.PW_CORTEX_M_CFSR_IMPRECISERR_MASK
151                ):
152                    cause += 'imprecise '
153                cause += 'bus fault'
154                if (
155                    self._cpu_state.cfsr
156                    & cortex_m_constants.PW_CORTEX_M_CFSR_BFARVALID_MASK
157                ):
158                    addr = (
159                        '???'
160                        if not self._cpu_state.HasField('bfar')
161                        else f'0x{self._cpu_state.bfar:08x}'
162                    )
163                    cause += f' at {addr}'
164            if show_active_cfsr_fields:
165                for field in self.active_cfsr_fields():
166                    cause += f' [{field.name}]'
167
168        return cause if cause else 'unknown exception'
169
170    def dump_registers(self) -> str:
171        """Dumps all captured CPU registers as a multi-line string."""
172        registers = []
173        for field in self._cpu_state.DESCRIPTOR.fields:
174            if self._cpu_state.HasField(field.name):
175                register_value = getattr(self._cpu_state, field.name)
176                register_str = f'{field.name:<10} 0x{register_value:08x}'
177                if (
178                    self._symbolizer is not None
179                    and field.name in _SYMBOLIZED_REGISTERS
180                ):
181                    symbol = self._symbolizer.symbolize(register_value)
182                    if symbol.name:
183                        register_str += f' {symbol}'
184                registers.append(register_str)
185        return '\n'.join(registers)
186
187    def dump_active_active_cfsr_fields(self) -> str:
188        """Dumps CFSR flags with their descriptions as a multi-line string."""
189        fields = []
190        for field in self.active_cfsr_fields():
191            fields.append(f'{field.name:<11} {field.description}')
192            if isinstance(field.long_description, tuple):
193                long_desc = '    {}'.format(
194                    '\n    '.join(field.long_description)
195                )
196                fields.append(long_desc)
197        return '\n'.join(fields)
198
199    def __str__(self):
200        dump = [f'Exception caused by a {self.exception_cause(False)}.', '']
201        if self.active_cfsr_fields():
202            dump.extend(
203                (
204                    'Active Crash Fault Status Register (CFSR) fields:',
205                    self.dump_active_active_cfsr_fields(),
206                    '',
207                )
208            )
209        else:
210            dump.extend(
211                (
212                    'No active Crash Fault Status Register (CFSR) fields.',
213                    '',
214                )
215            )
216        dump.extend(
217            (
218                'All registers:',
219                self.dump_registers(),
220            )
221        )
222        return '\n'.join(dump)
223
224
225def process_snapshot(
226    serialized_snapshot: bytes,
227    symbolizer: pw_symbolizer.Symbolizer | None = None,
228) -> str:
229    """Returns the stringified result of a SnapshotCpuStateOverlay message run
230    though a CortexMExceptionAnalyzer.
231    """
232    snapshot = cpu_state_pb2.SnapshotCpuStateOverlay()
233    snapshot.ParseFromString(serialized_snapshot)
234
235    if snapshot.HasField('armv7m_cpu_state'):
236        state_analyzer = CortexMExceptionAnalyzer(
237            snapshot.armv7m_cpu_state, symbolizer
238        )
239        return f'{state_analyzer}\n'
240
241    return ''
242