262 lines
10 KiB
Python
262 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Execute the real two-phase nRF52 profile-idle preparation and tick guard."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import shlex
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from cpp_source import body
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class Nrf52ProfileIdleTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-profile-idle-")
|
|
cls.addClassCleanup(cls.temp.cleanup)
|
|
source = (ROOT / "src/helpers/NRF52Board.cpp").read_text(encoding="ascii")
|
|
methods = "\n".join(body(source, signature) for signature in (
|
|
"void NRF52Board::prepareSleepCycle()",
|
|
"bool NRF52Board::canIdleRadioProfile()",
|
|
"void NRF52Board::prepareProfileIdleCycle()",
|
|
"void NRF52Board::sleep(uint32_t secs)",
|
|
))
|
|
fixture = r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include <helpers/nrf52/SleepCycleState.h>
|
|
#ifndef TEST_TICK_RATE
|
|
#define TEST_TICK_RATE 1024
|
|
#endif
|
|
#define configTICK_RATE_HZ TEST_TICK_RATE
|
|
#define __FPU_USED 1
|
|
#define NRF_CRYPTOCELL 1
|
|
constexpr uint32_t NRF_SUCCESS = 0, NRF_ERROR_INVALID_STATE = 8;
|
|
constexpr uint32_t SCB_ICSR_PENDSVSET_Msk = 1UL << 28;
|
|
constexpr uint32_t RTC_INTENSET_TICK_Msk = 1;
|
|
constexpr int RTC1_IRQn = 17, FPU_IRQn = 38, CRYPTOCELL_IRQn = 42;
|
|
constexpr int taskSCHEDULER_RUNNING = 1;
|
|
struct SdkNvicState { volatile uint32_t __cr_flag = 0; } nrf_nvic_state;
|
|
struct Scb { uint32_t ICSR = 0; } scb;
|
|
static Scb* const SCB = &scb;
|
|
struct Rtc { uint32_t PRESCALER = 31, INTENSET = RTC_INTENSET_TICK_Msk; } rtc;
|
|
static Rtc* const NRF_RTC1 = &rtc;
|
|
struct Power { uint32_t USBREGSTATUS = 0; } power;
|
|
static Power* const NRF_POWER = &power;
|
|
static bool irq_enabled[64] = {}, irq_pending[64] = {};
|
|
static unsigned clears[64] = {};
|
|
static uint32_t ipsr = 0, primask = 0, basepri = 0, faultmask = 0;
|
|
static uint32_t fpscr = 0, clock_us = 10000;
|
|
static int scheduler_state = taskSCHEDULER_RUNNING;
|
|
static bool event = false, softdevice = false, work_pending = false;
|
|
static bool inject_during_prepare = false;
|
|
static unsigned app_irq = 1, acknowledged_irq = 0;
|
|
static unsigned state_queries = 0, sdk_calls = 0, sleeps = 0, delays = 0;
|
|
static uint32_t __get_IPSR() { return ipsr; }
|
|
static uint32_t __get_PRIMASK() { return primask; }
|
|
static uint32_t __get_BASEPRI() { return basepri; }
|
|
static uint32_t __get_FAULTMASK() { return faultmask; }
|
|
static int xTaskGetSchedulerState() { return scheduler_state; }
|
|
static uint32_t NVIC_GetEnableIRQ(int irq) { return irq_enabled[irq]; }
|
|
static void NVIC_ClearPendingIRQ(int irq) { irq_pending[irq] = false; ++clears[irq]; }
|
|
static uint32_t __get_FPSCR() { return fpscr; }
|
|
static void __set_FPSCR(uint32_t value) { fpscr = value; }
|
|
static uint32_t millis() { return clock_us / 1000; }
|
|
static uint32_t micros() { return clock_us; }
|
|
static void delay(uint32_t ms) { delays += ms; }
|
|
static void applicationIrq() { ++app_irq; event = true; work_pending = true; }
|
|
static void taskYIELD() { ++app_irq; event = true; }
|
|
namespace mesh_nrf52 {
|
|
static uint32_t softdeviceIsEnabled(uint8_t& enabled) {
|
|
++state_queries; enabled = softdevice; event = true; return NRF_SUCCESS;
|
|
}
|
|
}
|
|
static uint32_t sd_power_usbregstatus_get(uint32_t* value) {
|
|
*value = power.USBREGSTATUS; event = true; return NRF_SUCCESS;
|
|
}
|
|
static uint32_t sd_app_evt_wait() {
|
|
++sdk_calls;
|
|
const bool history = app_irq != acknowledged_irq;
|
|
acknowledged_irq = app_irq;
|
|
event = false;
|
|
if (!history && !irq_pending[FPU_IRQn] && !irq_pending[CRYPTOCELL_IRQn]) {
|
|
assert(!work_pending); ++sleeps; clock_us += 500;
|
|
++app_irq;
|
|
}
|
|
return NRF_SUCCESS;
|
|
}
|
|
static void __SEV() { event = true; }
|
|
static void __WFE() {
|
|
if (inject_during_prepare) { inject_during_prepare = false; applicationIrq(); }
|
|
if (event) event = false;
|
|
else { assert(!work_pending); ++sleeps; clock_us += 500; }
|
|
}
|
|
struct NRF52Board {
|
|
bool radio_test = false;
|
|
bool isRadioTestActive() const { return radio_test; }
|
|
void prepareSleepCycle();
|
|
static bool canIdleRadioProfile();
|
|
void prepareProfileIdleCycle();
|
|
void sleep(uint32_t secs);
|
|
};
|
|
@METHODS@
|
|
static void floatingWork() {
|
|
fpscr = 0x60000010UL; irq_pending[FPU_IRQn] = true;
|
|
event = true; // SEVONPEND retains the event even after NVIC pending clears.
|
|
}
|
|
static void ready() {
|
|
irq_enabled[RTC1_IRQn] = true;
|
|
assert(NRF52Board::canIdleRadioProfile() == (TEST_TICK_RATE == 1024));
|
|
}
|
|
static void normal(bool sdk) {
|
|
ready(); softdevice = sdk;
|
|
NRF52Board board;
|
|
board.prepareSleepCycle();
|
|
const auto initial_queries = state_queries;
|
|
floatingWork();
|
|
board.prepareProfileIdleCycle();
|
|
assert(state_queries == initial_queries + 1);
|
|
assert(!event && !irq_pending[FPU_IRQn] && (fpscr & 0x9f) == 0);
|
|
assert((fpscr & ~0x9fUL) == 0x60000000UL);
|
|
assert(clears[FPU_IRQn] == 1 && !work_pending);
|
|
const auto waits_before = sdk_calls;
|
|
assert(NRF52Board::canIdleRadioProfile()); // Final pure recheck creates no event/SVC.
|
|
assert(!event && state_queries == initial_queries + 1 && sdk_calls == waits_before);
|
|
board.sleep(0);
|
|
assert(sleeps == 1 && delays == 0);
|
|
}
|
|
static void legacy() {
|
|
ready(); NRF52Board board;
|
|
board.prepareSleepCycle(); floatingWork();
|
|
board.sleep(0);
|
|
// The late FPU cleanup cannot remove an already-latched ARM event. This
|
|
// negative control reproduces the measured tuple-dependent raw-WFE insomnia.
|
|
assert(sleeps == 0 && !irq_pending[FPU_IRQn]);
|
|
}
|
|
static void vetoes() {
|
|
ready();
|
|
if (TEST_TICK_RATE != 1024) return;
|
|
for (uint32_t* value : {&ipsr, &primask, &basepri, &faultmask}) {
|
|
*value = 1; assert(!NRF52Board::canIdleRadioProfile()); *value = 0;
|
|
}
|
|
scheduler_state = 0; assert(!NRF52Board::canIdleRadioProfile());
|
|
scheduler_state = taskSCHEDULER_RUNNING;
|
|
nrf_nvic_state.__cr_flag = 1; assert(!NRF52Board::canIdleRadioProfile());
|
|
nrf_nvic_state.__cr_flag = 0;
|
|
scb.ICSR = SCB_ICSR_PENDSVSET_Msk; assert(!NRF52Board::canIdleRadioProfile());
|
|
scb.ICSR = 0;
|
|
rtc.PRESCALER = 0; assert(!NRF52Board::canIdleRadioProfile()); rtc.PRESCALER = 31;
|
|
rtc.INTENSET = 0; assert(!NRF52Board::canIdleRadioProfile());
|
|
rtc.INTENSET = RTC_INTENSET_TICK_Msk;
|
|
irq_enabled[RTC1_IRQn] = false; assert(!NRF52Board::canIdleRadioProfile());
|
|
irq_enabled[RTC1_IRQn] = true;
|
|
assert(NRF52Board::canIdleRadioProfile() && state_queries == 0 && sdk_calls == 0);
|
|
}
|
|
static void owners() {
|
|
ready(); NRF52Board board;
|
|
floatingWork(); irq_enabled[FPU_IRQn] = true;
|
|
irq_pending[7] = true;
|
|
board.prepareProfileIdleCycle();
|
|
assert(clears[FPU_IRQn] == 0 && irq_pending[FPU_IRQn]);
|
|
assert(fpscr == 0x60000010UL && irq_pending[7] && clears[7] == 0);
|
|
irq_enabled[FPU_IRQn] = false;
|
|
nrf_nvic_state.__cr_flag = 1;
|
|
const auto before = state_queries;
|
|
board.prepareProfileIdleCycle();
|
|
assert(state_queries == before && clears[FPU_IRQn] == 0 && irq_pending[FPU_IRQn]);
|
|
assert(mesh_nrf52::sleepCycleState().lastError() == NRF_ERROR_INVALID_STATE);
|
|
}
|
|
static void arrivals(bool sdk, bool during_prepare) {
|
|
ready(); softdevice = sdk; NRF52Board board;
|
|
floatingWork(); inject_during_prepare = during_prepare;
|
|
board.prepareProfileIdleCycle();
|
|
if (during_prepare) {
|
|
// Preparation may consume the event, but the ENTIRE following role
|
|
// predicate still observes the ISR-owned work and must decline sleep.
|
|
assert(work_pending && sleeps == 0);
|
|
return;
|
|
}
|
|
assert(!work_pending && NRF52Board::canIdleRadioProfile());
|
|
applicationIrq(); // Arrives after the role predicate's final decision.
|
|
board.sleep(0);
|
|
assert(work_pending && sleeps == 0 && delays == 0);
|
|
}
|
|
static void lateFloatingWork() {
|
|
ready(); NRF52Board board;
|
|
board.prepareProfileIdleCycle();
|
|
floatingWork(); // A missed float call must degrade idle, never erase its event.
|
|
board.sleep(0);
|
|
assert(sleeps == 0 && delays == 0);
|
|
}
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 2);
|
|
if (!std::strcmp(argv[1], "raw")) normal(false);
|
|
else if (!std::strcmp(argv[1], "sdk")) normal(true);
|
|
else if (!std::strcmp(argv[1], "legacy")) legacy();
|
|
else if (!std::strcmp(argv[1], "vetoes")) vetoes();
|
|
else if (!std::strcmp(argv[1], "owners")) owners();
|
|
else if (!std::strcmp(argv[1], "late-raw")) arrivals(false, false);
|
|
else if (!std::strcmp(argv[1], "late-sdk")) arrivals(true, false);
|
|
else if (!std::strcmp(argv[1], "prepare-raw")) arrivals(false, true);
|
|
else if (!std::strcmp(argv[1], "prepare-sdk")) arrivals(true, true);
|
|
else if (!std::strcmp(argv[1], "late-fpu")) lateFloatingWork();
|
|
else assert(false);
|
|
}
|
|
'''.replace("@METHODS@", methods)
|
|
cls.binaries = {}
|
|
for tick_rate in (1024, 1000):
|
|
cpp = Path(cls.temp.name) / ("idle-%d.cpp" % tick_rate)
|
|
binary = cpp.with_suffix("")
|
|
cpp.write_text(fixture, encoding="ascii")
|
|
result = subprocess.run(
|
|
shlex.split(os.environ.get("CXX", "g++"))
|
|
+ ["-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic",
|
|
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
|
"-fno-pie", "-no-pie", "-DTEST_TICK_RATE=%d" % tick_rate,
|
|
"-I", str(ROOT / "src"), str(cpp), "-o", str(binary)],
|
|
capture_output=True, text=True, timeout=60)
|
|
if result.returncode:
|
|
raise RuntimeError(result.stdout + result.stderr)
|
|
cls.binaries[tick_rate] = binary
|
|
|
|
def run_case(self, case, tick_rate=1024):
|
|
result = subprocess.run([str(self.binaries[tick_rate]), case],
|
|
capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_fpu_event_refresh_restores_raw_and_softdevice_single_wait(self):
|
|
self.run_case("raw")
|
|
self.run_case("sdk")
|
|
|
|
def test_late_existing_fpu_cleanup_reproduces_raw_insomnia(self):
|
|
self.run_case("legacy")
|
|
|
|
def test_tick_context_and_pending_scheduler_guards_are_pure(self):
|
|
self.run_case("vetoes")
|
|
self.run_case("vetoes", tick_rate=1000)
|
|
|
|
def test_enabled_fpu_owner_unrelated_irq_and_sdk_critical_are_preserved(self):
|
|
self.run_case("owners")
|
|
|
|
def test_work_during_preparation_survives_for_complete_role_predicate(self):
|
|
self.run_case("prepare-raw")
|
|
self.run_case("prepare-sdk")
|
|
|
|
def test_irq_after_predicate_prevents_sleep_without_late_event_clear(self):
|
|
self.run_case("late-raw")
|
|
self.run_case("late-sdk")
|
|
|
|
def test_unexpected_float_after_preparation_remains_immediate_not_double_wait(self):
|
|
self.run_case("late-fpu")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|