blob: a5dfcd5a2f84fb5b4a52fddcc28aaac70a8d9581 [file] [log] [blame]
// Copyright 2024 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 <cstdint>
#include "pw_cpu_exception/entry.h"
#include "pw_cpu_exception/handler.h"
#include "pw_cpu_exception/support.h"
#include "pw_cpu_exception_cortex_m/exception_entry_test_util.h"
#include "pw_cpu_exception_cortex_m_private/cortex_m_constants.h"
#include "pw_unit_test/framework.h"
namespace pw::cpu_exception::cortex_m {
namespace {
using pw::cpu_exception::RawFaultingCpuState;
// CMSIS/Cortex-M/ARMv6 related constants.
// These values are from the ARMv6-M Architecture Reference Manual DDI 0419
// https://developer.arm.com/documentation/ddi0419/latest
// Memory mapped registers. (ARMv6-M Section B3.2.2, Table B3-4)
volatile uint32_t& cortex_m_ccr =
*reinterpret_cast<volatile uint32_t*>(0xE000ED14u);
// Counter that is incremented if the test's exception handler correctly handles
// a triggered exception.
size_t exceptions_handled = 0;
// Allow up to kMaxFaultDepth faults before determining the device is
// unrecoverable.
constexpr size_t kMaxFaultDepth = 1;
// The instruction size in bytes on the ARMv6-M architecture.
constexpr size_t kMaxInstructionSize = 4;
// The manually captured PC won't be the exact same as the faulting PC. This is
// the maximum tolerated distance between the two to allow the test to pass.
constexpr int32_t kMaxPcDistance = 6;
// Variable to prevent more than kMaxFaultDepth nested crashes.
size_t current_fault_depth = 0;
// Faulting pw_cpu_exception_State is copied here so values can be validated
// after exiting exception handler.
pw_cpu_exception_State captured_states[kMaxFaultDepth] = {};
pw_cpu_exception_State& captured_state = captured_states[0];
// Flag used to check if the contents of span matches the captured state.
bool span_matches = false;
// Forward declaration of the exception handler.
void TestingExceptionHandler(pw_cpu_exception_State*);
// Populate the device's registers with testable values, then trigger exception.
void BeginBaseFaultTest() {
// Make sure divide by zero causes a fault.
uint32_t magic = kMagicPattern;
asm volatile(
" mov r0, %[magic] \n"
" movs r1, #0 \n"
" mov r2, pc \n"
" mov r3, lr \n"
// This instruction reads a bad address.
" ldr r1, [r0] \n"
" nop \n"
" nop \n"
// clang-format off
: /*output=*/
: /*input=*/[magic]"r"(magic)
: /*clobbers=*/"r0", "r1", "r2", "r3"
// clang-format on
);
// Check that the stack align bit was not set.
EXPECT_EQ(captured_state.base.psr & kPsrExtraStackAlignBit, 0u);
}
// Populate the device's registers with testable values, then trigger exception.
// This version causes stack to not be 4-byte aligned initially, testing
// the fault handlers correction for psp.
void BeginBaseFaultUnalignedStackTest() {
uint32_t magic = kMagicPattern;
asm volatile(
// Push one register to cause $sp to be no longer 8-byte aligned,
// assuming it started 8-byte aligned as expected.
" push {r0} \n"
" mov r0, %[magic] \n"
" movs r1, #0 \n"
" mov r2, pc \n"
" mov r3, lr \n"
// This instruction reads a bad address. Our fault handler should
// ultimately advance the pc to the pop instruction.
" ldr r1, [r0] \n"
" nop \n"
" nop \n"
" pop {r0} \n"
// clang-format off
: /*output=*/
: /*input=*/[magic]"r"(magic)
: /*clobbers=*/"r0", "r1", "r2", "r3"
// clang-format on
);
// Check that the stack align bit was set.
EXPECT_EQ(captured_state.base.psr & kPsrExtraStackAlignBit,
kPsrExtraStackAlignBit);
}
// Populate some of the extended set of captured registers, then trigger
// exception.
void BeginExtendedFaultTest() {
uint32_t magic = kMagicPattern;
volatile uint32_t local_msp = 0;
volatile uint32_t local_psp = 0;
asm volatile(
" mov r4, %[magic] \n"
" movs r5, #0 \n"
" mov r11, %[magic] \n"
" mrs %[local_msp], msp \n"
" mrs %[local_psp], psp \n"
// This instruction reads a bad address.
" ldr r1, [r4] \n"
" nop \n"
" nop \n"
// clang-format off
: /*output=*/[local_msp]"=r"(local_msp), [local_psp]"=r"(local_psp)
: /*input=*/[magic]"r"(magic)
: /*clobbers=*/"r4", "r5", "r11", "memory"
// clang-format on
);
// Check that the stack align bit was not set.
EXPECT_EQ(captured_state.base.psr & kPsrExtraStackAlignBit, 0u);
// Check that the captured stack pointers matched the ones in the context of
// the fault.
EXPECT_EQ(static_cast<uint32_t>(captured_state.extended.msp), local_msp);
EXPECT_EQ(static_cast<uint32_t>(captured_state.extended.psp), local_psp);
}
// Populate some of the extended set of captured registers, then trigger
// exception.
// This version causes stack to not be 4-byte aligned initially, testing
// the fault handlers correction for psp.
void BeginExtendedFaultUnalignedStackTest() {
uint32_t magic = kMagicPattern;
volatile uint32_t local_msp = 0;
volatile uint32_t local_psp = 0;
asm volatile(
// Push one register to cause $sp to be no longer 8-byte aligned,
// assuming it started 8-byte aligned as expected.
" push {r0} \n"
" mov r4, %[magic] \n"
" movs r5, #0 \n"
" mov r11, %[magic] \n"
" mrs %[local_msp], msp \n"
" mrs %[local_psp], psp \n"
// This instruction reads a bad address. Our fault handler should
// ultimately advance the pc to the pop instruction.
" ldr r1, [r4] \n"
" nop \n"
" nop \n"
" pop {r0} \n"
// clang-format off
: /*output=*/[local_msp]"=r"(local_msp), [local_psp]"=r"(local_psp)
: /*input=*/[magic]"r"(magic)
: /*clobbers=*/"r0", "r4", "r5", "r11", "memory"
// clang-format on
);
// Check that the stack align bit was set.
EXPECT_EQ(captured_state.base.psr & kPsrExtraStackAlignBit,
kPsrExtraStackAlignBit);
// Check that the captured stack pointers matched the ones in the context of
// the fault.
EXPECT_EQ(static_cast<uint32_t>(captured_state.extended.msp), local_msp);
EXPECT_EQ(static_cast<uint32_t>(captured_state.extended.psp), local_psp);
}
void Setup() {
pw_cpu_exception_SetHandler(TestingExceptionHandler);
InstallVectorTableEntries(
reinterpret_cast<uint32_t*>(pw_cpu_exception_Entry));
exceptions_handled = 0;
current_fault_depth = 0;
captured_state = {};
}
TEST(FaultEntry, BasicFault) {
Setup();
BeginBaseFaultTest();
ASSERT_EQ(exceptions_handled, 1u);
// captured_state values must be cast since they're in a packed struct.
EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r0), kMagicPattern);
EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r1), 0u);
// PC is manually saved in r2 before the exception occurs (where PC is also
// stored). Ensure these numbers are within a reasonable distance.
int32_t captured_pc_distance =
captured_state.base.pc - captured_state.base.r2;
EXPECT_LT(captured_pc_distance, kMaxPcDistance);
EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r3),
static_cast<uint32_t>(captured_state.base.lr));
}
TEST(FaultEntry, BasicUnalignedStackFault) {
Setup();
BeginBaseFaultUnalignedStackTest();
ASSERT_EQ(exceptions_handled, 1u);
// captured_state values must be cast since they're in a packed struct.
EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r0), kMagicPattern);
EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r1), 0u);
// PC is manually saved in r2 before the exception occurs (where PC is also
// stored). Ensure these numbers are within a reasonable distance.
int32_t captured_pc_distance =
captured_state.base.pc - captured_state.base.r2;
EXPECT_LT(captured_pc_distance, kMaxPcDistance);
EXPECT_EQ(static_cast<uint32_t>(captured_state.base.r3),
static_cast<uint32_t>(captured_state.base.lr));
}
TEST(FaultEntry, ExtendedFault) {
Setup();
BeginExtendedFaultTest();
ASSERT_EQ(exceptions_handled, 1u);
ASSERT_TRUE(span_matches);
const ExtraRegisters& extended_registers = captured_state.extended;
// captured_state values must be cast since they're in a packed struct.
EXPECT_EQ(static_cast<uint32_t>(extended_registers.r4), kMagicPattern);
EXPECT_EQ(static_cast<uint32_t>(extended_registers.r5), 0u);
EXPECT_EQ(static_cast<uint32_t>(extended_registers.r11), kMagicPattern);
// Check expected values for this crash.
EXPECT_EQ((extended_registers.icsr & 0x1FFu), kHardFaultIsrNum);
}
TEST(FaultEntry, ExtendedUnalignedStackFault) {
Setup();
BeginExtendedFaultUnalignedStackTest();
ASSERT_EQ(exceptions_handled, 1u);
ASSERT_TRUE(span_matches);
const ExtraRegisters& extended_registers = captured_state.extended;
// captured_state values must be cast since they're in a packed struct.
EXPECT_EQ(static_cast<uint32_t>(extended_registers.r4), kMagicPattern);
EXPECT_EQ(static_cast<uint32_t>(extended_registers.r5), 0u);
EXPECT_EQ(static_cast<uint32_t>(extended_registers.r11), kMagicPattern);
// Check expected values for this crash.
EXPECT_EQ((extended_registers.icsr & 0x1FFu), kHardFaultIsrNum);
}
void TestingExceptionHandler(pw_cpu_exception_State* state) {
if (++current_fault_depth > kMaxFaultDepth) {
volatile bool loop = true;
while (loop) {
// Hit unexpected nested crash, prevent further nesting.
}
}
// After a fault has been handled, the exception handler will
// return to the faulting instruction. For testing, move the
// PC to the next instruction to allow the tests to continue
// without entering an exception loop.
// v6m supports variable instruction size, so always skip the
// max instruction size, knowing there is enough nops after the
// faulting test instruction to ensure it's safe to skip kMaxInstructionSize
// even if it's a 2 byte instruction.
state->base.pc += kMaxInstructionSize;
// Disable traps. Must be disabled before EXPECT, as memcpy() can do unaligned
// operations.
cortex_m_ccr &= ~kUnalignedTrapEnableMask;
// Copy captured state to check later.
std::memcpy(&captured_states[exceptions_handled],
state,
sizeof(pw_cpu_exception_State));
// Ensure span compares to be the same.
span<const uint8_t> state_span = RawFaultingCpuState(*state);
EXPECT_EQ(state_span.size(), sizeof(pw_cpu_exception_State));
if (std::memcmp(state, state_span.data(), state_span.size()) == 0) {
span_matches = true;
} else {
span_matches = false;
}
exceptions_handled++;
EXPECT_EQ(state->extended.shcsr, cortex_m_shcsr);
}
} // namespace
} // namespace pw::cpu_exception::cortex_m