Fix nRF52 idle sleep event and CryptoCell handling

This commit is contained in:
mikecarper committed 2026-10-17 02:22:26 -07:00
1 parent 756d887bad
commit 629557fcfe
22 files changed
+1767 -20

No files matched your search

+17 -3
View File
@@ -2,6 +2,7 @@
#include <helpers/PowerSavingDiagnostics.h>
#include <cstdarg>
static unsigned usb_services=0,command_services=0,transport_services=0,delays=0;
static bool usb_watchdog_armed=false,wireless_pending=false;
struct Stream {
int available() const { return 0; }
char read() const { return 0; }
@@ -18,17 +19,18 @@ namespace mesh {
void serviceUsbLoggingPort() { ++usb_services; }
void serviceUsbTerminalPort() { ++usb_services; }
bool isUsbLoggingEnabled() { return false; }
bool isUsbLoggingWatchdogArmed() { return false; }
bool isUsbLoggingWatchdogArmed() { return usb_watchdog_armed; }
bool serviceUsbLoggingWatchdog(bool(*)()) { ++usb_services; return false; }
Stream& usbConsolePort() { return console; }
void noteUsbLoggingStatsCommand(const char*) { ++usb_services; }
namespace wireless {
struct Control { void service(uint32_t) { ++transport_services; } bool pending() { return false; } } controller;
struct Control { void service(uint32_t) { ++transport_services; } bool pending() { return wireless_pending; } } controller;
Control& control() { return controller; }
}
}
struct Board {
unsigned calls=0,watchdogs=0,sleeps=0,fans=0,reboots=0;
unsigned calls=0,watchdogs=0,sleeps=0,fans=0,reboots=0,preparations=0;
void prepareSleepCycle() { ++preparations; }
void loop() { ++calls; }
void feedWatchdog(bool=true) { ++watchdogs; }
void sleep(uint32_t) { ++sleeps; }
@@ -102,6 +104,7 @@ int main() {
loop();
assert(sensors.calls==3 && ui_task.calls==6 && board.sleeps==3 && !delays);
assert(the_mesh.calls==6 && rtc_clock.calls==6 && board.watchdogs==6);
assert(board.preparations==6); // Prepare even when Morse vetoes actual sleep.
#if !ROLE_SENSOR
assert(board.fans==6);
#endif
@@ -113,5 +116,16 @@ int main() {
assert(transport_services==6 && board.calls==6);
#else
assert(board.calls==6);
#endif
const auto sleeps_before_hold=board.sleeps;
usb_watchdog_armed=true;
loop();
assert(board.sleeps==sleeps_before_hold);
usb_watchdog_armed=false;
#if !ROLE_SENSOR
wireless_pending=true;
loop();
assert(board.sleeps==sleeps_before_hold);
wireless_pending=false;
#endif
}
+307
View File
@@ -0,0 +1,307 @@
// Exercise the real portable cache and extracted board methods. Hardware
// primitives implement the documented event contract, not Nordic SDK internals.
#include <cassert>
#include <cstdint>
#include <cstring>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/nrf52/SleepCycleState.h>
void invalidateSleepCycleFromOtherTranslationUnit();
static bool event_latched = true, application_work = false;
static bool softdevice_enabled = false, interrupt_during_query = false;
static bool invalidate_during_query = false;
static uint32_t state_error = 0, usb_error = 0, wait_error = 0, clock_us = 10000;
static unsigned state_queries = 0, usb_queries = 0, sdk_waits = 0;
static unsigned raw_wfes = 0, real_sleeps = 0, short_returns = 0;
static uint8_t app_irq_current = 1, app_irq_acknowledged = 0;
static bool cc_enabled = false, cc_pending = false, other_irq_pending = false;
static bool sdk_critical_after_yield = false;
static unsigned cc_clears = 0;
constexpr uint32_t NRF_SUCCESS = 0;
constexpr uint32_t NRF_ERROR_INVALID_STATE = 8;
constexpr uint32_t POWER_USBREGSTATUS_VBUSDETECT_Msk = 1;
constexpr uint32_t SCB_ICSR_PENDSVSET_Msk = 1u << 28;
constexpr int taskSCHEDULER_RUNNING = 1;
#define NRF_CRYPTOCELL 1
constexpr int CRYPTOCELL_IRQn = 42;
struct SdkNvicState { volatile uint32_t __cr_flag = 0; } nrf_nvic_state;
static uint32_t NVIC_GetEnableIRQ(int irq) {
assert(irq == CRYPTOCELL_IRQn);
return cc_enabled;
}
static void NVIC_ClearPendingIRQ(int irq) {
assert(irq == CRYPTOCELL_IRQn);
++cc_clears;
cc_pending = false;
}
struct SystemRegisters { uint32_t ICSR = 0; } system_registers;
static SystemRegisters* const SCB = &system_registers;
static uint32_t __get_IPSR() { return 0; }
static uint32_t __get_PRIMASK() { return 0; }
static uint32_t __get_BASEPRI() { return 0; }
static uint32_t __get_FAULTMASK() { return 0; }
static int xTaskGetSchedulerState() { return taskSCHEDULER_RUNNING; }
struct PowerRegisters { uint32_t USBREGSTATUS = 0; } power_registers;
static PowerRegisters* const NRF_POWER = &power_registers;
static uint32_t micros() { return clock_us; }
static uint32_t millis() { return clock_us / 1000; }
static void delay(unsigned amount) { clock_us += amount * 1000; }
static void recordApplicationInterrupt() {
const uint8_t next = static_cast<uint8_t>(app_irq_current + 1);
if (next != app_irq_acknowledged) app_irq_current = next;
event_latched = true;
}
void taskYIELD() {
recordApplicationInterrupt();
if (sdk_critical_after_yield) nrf_nvic_state.__cr_flag = 1;
}
static void applicationIrq() { application_work = true; recordApplicationInterrupt(); }
namespace mesh_nrf52 {
static uint32_t softdeviceIsEnabled(uint8_t& enabled) {
++state_queries;
event_latched = true; // The state-query SVC is itself an event source.
if (interrupt_during_query) applicationIrq();
if (invalidate_during_query) invalidateSleepCycleFromOtherTranslationUnit();
enabled = softdevice_enabled;
return state_error;
}
}
static uint32_t sd_power_usbregstatus_get(uint32_t* status) {
++usb_queries;
event_latched = true;
*status = power_registers.USBREGSTATUS;
return usb_error;
}
static void waitOnce() {
if (event_latched) {
++short_returns;
event_latched = false;
} else {
assert(!application_work); // A second late wait would lose this work.
++real_sleeps;
clock_us += 547;
recordApplicationInterrupt(); // The next periodic IRQ wakes the CPU.
}
}
static void __SEV() { event_latched = true; }
static void __WFE() { ++raw_wfes; waitOnce(); }
static uint32_t sd_app_evt_wait() {
assert(softdevice_enabled);
++sdk_waits;
const bool app_irq_pending = app_irq_current != app_irq_acknowledged;
// Conservative API abstraction, not the exact ROM counter-copy timing:
// retaining a waking IRQ in private history avoids hiding application work.
// The ROM copies after its first WFE triplet/comparison, not at this point.
app_irq_acknowledged = app_irq_current;
// Public SVC entry consumes its own ARM event, then the SDK's separate
// application-IRQ latch decides immediate return versus a real sleep.
event_latched = false;
if (!app_irq_pending && !cc_pending && !other_irq_pending) {
assert(!application_work);
++real_sleeps;
clock_us += 547;
recordApplicationInterrupt();
} else {
++short_returns;
}
__SEV();
event_latched = false;
return wait_error;
}
struct NRF52Board {
bool radio_test = false;
bool isRadioTestActive() const { return radio_test; }
void prepareSleepCycle();
bool isExternalPowered();
void sleep(uint32_t secs);
};
@BOARD_METHODS@
static void cacheContract() {
using mesh_nrf52::SleepCycleSnapshot;
mesh_nrf52::SleepCycleState state;
SleepCycleSnapshot value = {};
assert(!state.snapshot(value) && !state.take(value));
const uint32_t first = state.begin();
assert(state.publish(first, false, 1));
assert(state.current(first) && state.snapshot(value));
assert(value.generation == first && !value.softdevice_enabled && value.usb_status == 1);
assert(state.take(value) && !state.take(value));
const uint32_t stale = state.begin();
state.invalidate();
assert(!state.current(stale) && !state.publish(stale, true, 2));
assert(!state.snapshot(value) && !state.take(value));
const uint32_t failed = state.begin();
assert(state.publish(failed, false, 0, 23));
assert(state.lastError() == 23 && !state.take(value));
const uint32_t recovery = state.begin();
assert(state.publish(recovery, true, 2));
assert(state.lastError() == 0 && state.snapshot(value));
// A late completion from an obsolete cycle cannot overwrite new state.
assert(!state.publish(stale, false, 99, 44));
assert(state.snapshot(value) && value.generation == recovery);
assert(value.softdevice_enabled && value.usb_status == 2);
}
static void boardFlow(bool enabled) {
NRF52Board board;
softdevice_enabled = enabled;
// No historic app IRQ may be assumed when preparing the first SDK cycle.
// A real forced PendSV, not SEV alone, must guarantee early SDK return.
app_irq_acknowledged = app_irq_current;
power_registers.USBREGSTATUS = 1;
board.prepareSleepCycle();
assert(state_queries == 1 && !event_latched && real_sleeps == 0);
assert(board.isExternalPowered());
const unsigned queries_before_sleep = state_queries + usb_queries;
board.sleep(0);
assert(state_queries + usb_queries == queries_before_sleep);
assert(real_sleeps == 1 && sdk_waits == (enabled ? 2u : 0u));
const unsigned waits = raw_wfes + sdk_waits;
board.sleep(0);
assert(raw_wfes + sdk_waits == waits); // Prepared attempt is single-use.
}
static void lateIrq(bool enabled) {
NRF52Board board;
softdevice_enabled = enabled;
board.prepareSleepCycle();
assert(!event_latched);
// The main loop has already serviced inputs and accepted its idle predicate.
applicationIrq();
board.sleep(0);
assert(application_work && real_sleeps == 0);
board.prepareSleepCycle();
assert(application_work); // Preparation consumes only the event, not work.
application_work = false; // The next actual service pass processes it.
board.sleep(0);
assert(real_sleeps == 1);
}
static void transition() {
NRF52Board board;
board.prepareSleepCycle();
invalidateSleepCycleFromOtherTranslationUnit();
softdevice_enabled = true; // Also models a partial Bluefruit.begin failure.
const unsigned waits = raw_wfes + sdk_waits;
board.sleep(0);
assert(raw_wfes + sdk_waits == waits && real_sleeps == 0);
board.prepareSleepCycle();
board.sleep(0);
assert(sdk_waits == 2 && real_sleeps == 1);
}
static void failedQuery() {
NRF52Board board;
state_error = 17;
board.prepareSleepCycle();
const unsigned waits = raw_wfes + sdk_waits;
board.sleep(0);
assert(raw_wfes + sdk_waits == waits && real_sleeps == 0);
state_error = 0;
board.prepareSleepCycle();
board.sleep(0);
assert(real_sleeps == 1);
}
static void invalidationDuringQuery() {
NRF52Board board;
invalidate_during_query = true;
board.prepareSleepCycle();
const unsigned waits = raw_wfes + sdk_waits;
board.sleep(0);
assert(raw_wfes + sdk_waits == waits && real_sleeps == 0);
}
static void irqDuringPreparation() {
NRF52Board board;
interrupt_during_query = true;
board.prepareSleepCycle();
assert(application_work && real_sleeps == 0);
application_work = false; // Work is serviced after preparation, never before.
board.sleep(0);
assert(real_sleeps == 1);
}
static void usbSnapshot() {
NRF52Board board;
power_registers.USBREGSTATUS = 1;
board.prepareSleepCycle();
assert(board.isExternalPowered());
power_registers.USBREGSTATUS = 0;
const unsigned queries = state_queries + usb_queries;
assert(board.isExternalPowered()); // Per-cycle value, not another late SVC.
assert(state_queries + usb_queries == queries);
board.prepareSleepCycle();
assert(!board.isExternalPowered());
}
static void ccRecovery(bool late) {
NRF52Board board;
softdevice_enabled = true;
cc_pending = !late;
board.prepareSleepCycle();
assert(!cc_pending && cc_clears == 1);
if (late) {
cc_pending = true;
event_latched = true;
board.sleep(0);
assert(cc_pending && real_sleeps == 0); // No unsafe late pending clear.
board.prepareSleepCycle();
assert(!cc_pending && cc_clears == 2);
}
board.sleep(0);
assert(real_sleeps == 1);
}
static void preserveOwner(bool other_irq) {
NRF52Board board;
softdevice_enabled = true;
cc_enabled = !other_irq;
cc_pending = !other_irq;
other_irq_pending = other_irq;
board.prepareSleepCycle();
assert(other_irq ? other_irq_pending : cc_pending);
if (!other_irq) assert(cc_clears == 0);
board.sleep(0);
assert(real_sleeps == 0);
assert(other_irq ? other_irq_pending : cc_pending);
}
static void sdkCriticalRegion(bool after_yield) {
NRF52Board board;
softdevice_enabled = true;
cc_pending = true;
sdk_critical_after_yield = after_yield;
nrf_nvic_state.__cr_flag = after_yield ? 0 : 1;
board.prepareSleepCycle();
assert(sdk_waits == 0 && real_sleeps == 0);
assert(mesh_nrf52::sleepCycleState().lastError() == NRF_ERROR_INVALID_STATE);
if (!after_yield) assert(state_queries == 0 && cc_clears == 0 && cc_pending);
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!std::strcmp(argv[1], "cache")) cacheContract();
else if (!std::strcmp(argv[1], "raw")) boardFlow(false);
else if (!std::strcmp(argv[1], "sd")) boardFlow(true);
else if (!std::strcmp(argv[1], "raw_irq")) lateIrq(false);
else if (!std::strcmp(argv[1], "sd_irq")) lateIrq(true);
else if (!std::strcmp(argv[1], "transition")) transition();
else if (!std::strcmp(argv[1], "query_error")) failedQuery();
else if (!std::strcmp(argv[1], "during_query")) invalidationDuringQuery();
else if (!std::strcmp(argv[1], "prepare_irq")) irqDuringPreparation();
else if (!std::strcmp(argv[1], "usb")) usbSnapshot();
else if (!std::strcmp(argv[1], "cc_recover")) ccRecovery(false);
else if (!std::strcmp(argv[1], "cc_late")) ccRecovery(true);
else if (!std::strcmp(argv[1], "cc_owner")) preserveOwner(false);
else if (!std::strcmp(argv[1], "other_owner")) preserveOwner(true);
else if (!std::strcmp(argv[1], "sdk_critical")) sdkCriticalRegion(false);
else if (!std::strcmp(argv[1], "sdk_critical_after_yield")) sdkCriticalRegion(true);
else assert(false);
}
+5
View File
@@ -0,0 +1,5 @@
#include <helpers/nrf52/SleepCycleState.h>
void invalidateSleepCycleFromOtherTranslationUnit() {
mesh_nrf52::sleepCycleState().invalidate();
}
+410
View File
@@ -0,0 +1,410 @@
// Host-only causal model, not an emulator of the opaque Nordic SoftDevice.
//
// ARM Cortex-M4: SEV sets the event register; WFE consumes an already-set
// event without sleeping. With SEVONPEND an IRQ can leave the event latched
// after its handler has run. The installed FreeRTOS portYIELD also sends SEV.
// Nordic sd_app_evt_wait's documented contract preserves an application IRQ
// that happened before its single wait so the main loop can service it.
//
// This model proves ordering properties and known counterexamples only.
// It cannot prove actual firmware call placement, SDK filtering, ISR details,
// elapsed sleep time, or current consumption; those need integration/hardware.
#include <cassert>
#include <cstdint>
#include <cstring>
#include <initializer_list>
enum class Backend { Raw, SoftDevice };
enum class Point {
None,
BeforePrepare,
BeforeForcedYield,
AfterForcedYield,
AfterEntrySdkWait,
BetweenEarlySevAndWfe,
AfterPrepare,
BeforeService,
AfterService,
BeforePredicate,
AfterPredicate,
BeforeWait,
DuringWait,
AfterFirstWait,
BetweenCleanupSevAndWfe
};
struct Cpu {
bool event = true; // Prior loop-task yield already sent SEV.
bool work_pending = false;
bool slept_with_work = false;
unsigned services = 0;
unsigned sleeps = 0;
unsigned immediate_returns = 0;
unsigned waits = 0;
unsigned sdk_calls = 0;
unsigned yields = 0;
unsigned polls = 0;
unsigned predicate_vetoes = 0;
Point injection = Point::None;
bool injected = false;
uint8_t app_irq_current = 1, app_irq_acknowledged = 0;
bool cc_pending = false, cc_enabled = false, other_irq_pending = false;
unsigned cc_clears = 0;
void sev() { event = true; }
void recordApplicationInterrupt() {
const uint8_t next = static_cast<uint8_t>(app_irq_current + 1);
// Model the ROM's wrap-safe private latch: never let unacknowledged
// application interrupts wrap back to the previous wait's count.
if (next != app_irq_acknowledged) app_irq_current = next;
event = true;
}
void schedulerYield() {
++yields;
recordApplicationInterrupt(); // Real application PendSV, not just SEV.
}
void pendCryptoCell() {
if (!cc_pending) event = true; // New pending transition with SEVONPEND.
cc_pending = true; // A disabled IRQ does not run an ISR/update SDK counter.
}
void applicationIrq() {
work_pending = true; // ISR-published work survives event consumption.
recordApplicationInterrupt();
}
void inject(Point point) {
if (!injected && injection == point) {
applicationIrq();
injected = true;
}
}
// Exactly one WFE decision. A simulated next periodic IRQ ends real sleep,
// without generating application work unless DuringWait was requested.
void waitOnce() {
++waits;
if (event) {
event = false;
++immediate_returns;
return;
}
++sleeps;
if (work_pending) slept_with_work = true;
inject(Point::DuringWait);
// WFE has now been woken by an IRQ. Cleanup below consumes its event.
recordApplicationInterrupt();
}
// A guaranteed nonblocking consume, performed only before all work polls.
void prepare(Backend backend, bool acknowledge_sd = true, bool clean_cc = true) {
inject(Point::BeforePrepare);
if (clean_cc && !cc_enabled) {
cc_pending = false;
++cc_clears;
}
if (backend == Backend::SoftDevice && acknowledge_sd) {
inject(Point::BeforeForcedYield);
schedulerYield();
inject(Point::AfterForcedYield);
const unsigned before = sleeps;
singleWait(backend, true);
assert(sleeps == before); // The real PendSV guarantees a pending app IRQ.
inject(Point::AfterEntrySdkWait);
}
sev();
inject(Point::BetweenEarlySevAndWfe);
assert(event);
event = false; // Early WFE; deliberately no second WFE here.
inject(Point::AfterPrepare);
}
void service() {
inject(Point::BeforeService);
if (work_pending) {
++services;
work_pending = false;
}
inject(Point::AfterService);
}
void pollRx(bool yielding_empty_poll) {
++polls;
if (yielding_empty_poll) {
schedulerYield(); // Adafruit_USBD_CDC::available -> yield -> portYIELD.
}
}
void singleWait(Backend backend, bool entry_ack = false) {
if (backend == Backend::SoftDevice) {
++sdk_calls;
++waits;
const bool app_irq_pending = app_irq_current != app_irq_acknowledged;
// Conservative public-contract abstraction, not exact ROM timing: ack
// the pre-call history before simulating the wait. This intentionally
// retains a new IRQ from during the wait for the next call. The ROM
// copies its counter after an initial WFE triplet/comparison instead.
app_irq_acknowledged = app_irq_current;
// Public SDK entry itself uses SVC and consumes that ARM event first.
// Its private application-IRQ counter then decides whether to return
// or loop into a real wait. Raw ER consumption does not acknowledge it.
sev();
event = false;
if (!app_irq_pending && !cc_pending && !other_irq_pending) {
++sleeps;
if (work_pending) slept_with_work = true;
inject(Point::DuringWait);
recordApplicationInterrupt(); // Periodic application IRQ wakes SDK.
} else {
++immediate_returns;
}
} else {
waitOnce();
}
if (!entry_ack) inject(Point::AfterFirstWait);
sev();
if (!entry_ack) inject(Point::BetweenCleanupSevAndWfe);
assert(event);
event = false; // Cleanup WFE cannot sleep because its own SEV is set.
}
void iteration(Backend backend, bool entry_clear, bool yielding_empty_poll,
bool predicate_sees_work = true, bool unsafe_late_clear = false,
bool acknowledge_sd = true, bool clean_cc = true) {
if (entry_clear) prepare(backend, acknowledge_sd, clean_cc);
service();
pollRx(yielding_empty_poll);
inject(Point::BeforePredicate);
const bool eligible = !predicate_sees_work || !work_pending;
inject(Point::AfterPredicate);
if (eligible) {
inject(Point::BeforeWait);
if (unsafe_late_clear) {
// Raw SEV/WFE/WFE or two consecutive SD waits: the first consumes
// the late application's only event, the second can then sleep.
if (backend == Backend::SoftDevice) {
singleWait(backend);
} else {
sev();
event = false;
}
}
singleWait(backend);
} else {
++predicate_vetoes;
}
// Actual pinned Arduino main.cpp yields after every sketch loop().
schedulerYield();
}
};
static void scheduler() {
for (Backend backend : {Backend::Raw, Backend::SoftDevice}) {
Cpu cpu;
for (unsigned n = 0; n < 100; ++n) cpu.iteration(backend, false, false);
assert(cpu.sleeps == 0 && cpu.immediate_returns == 100);
assert(cpu.waits == 100 && cpu.yields == 100);
assert(!cpu.slept_with_work);
assert(cpu.sdk_calls == (backend == Backend::SoftDevice ? 100u : 0u));
}
}
static void entryIdle() {
for (Backend backend : {Backend::Raw, Backend::SoftDevice}) {
Cpu cpu;
for (unsigned n = 0; n < 100; ++n) cpu.iteration(backend, true, false);
assert(cpu.sleeps == 100);
assert(cpu.immediate_returns == (backend == Backend::SoftDevice ? 100u : 0u));
assert(cpu.polls == 100 && cpu.yields == (backend == Backend::SoftDevice ? 200u : 100u));
assert(!cpu.slept_with_work && cpu.services == 0);
}
}
static void pollYield() {
for (Backend backend : {Backend::Raw, Backend::SoftDevice}) {
Cpu cpu;
for (unsigned n = 0; n < 100; ++n) cpu.iteration(backend, true, true);
assert(cpu.sleeps == 0);
assert(cpu.immediate_returns == (backend == Backend::SoftDevice ? 200u : 100u));
assert(cpu.polls == 100 && cpu.yields == (backend == Backend::SoftDevice ? 300u : 200u));
assert(!cpu.slept_with_work);
}
}
static const Point irq_points[] = {
Point::BeforePrepare, Point::BeforeForcedYield, Point::AfterForcedYield,
Point::AfterEntrySdkWait, Point::BetweenEarlySevAndWfe, Point::AfterPrepare,
Point::BeforeService, Point::AfterService, Point::BeforePredicate,
Point::AfterPredicate, Point::BeforeWait, Point::DuringWait,
Point::AfterFirstWait, Point::BetweenCleanupSevAndWfe
};
static void injections(bool predicate_sees_work) {
for (Backend backend : {Backend::Raw, Backend::SoftDevice}) {
for (Point point : irq_points) {
if (backend == Backend::Raw && (point == Point::BeforeForcedYield
|| point == Point::AfterForcedYield || point == Point::AfterEntrySdkWait)) continue;
Cpu cpu;
cpu.injection = point;
cpu.iteration(backend, true, false, predicate_sees_work);
assert(cpu.injected);
assert(!cpu.slept_with_work);
if (point == Point::AfterPredicate || point == Point::BeforeWait) {
assert(cpu.sleeps == 0);
assert(cpu.immediate_returns == (backend == Backend::SoftDevice ? 2u : 1u));
assert(cpu.work_pending && cpu.services == 0);
}
if (!predicate_sees_work &&
(point == Point::AfterService || point == Point::BeforePredicate)) {
assert(cpu.sleeps == 0);
assert(cpu.immediate_returns == (backend == Backend::SoftDevice ? 2u : 1u));
}
// Work arriving during cleanup must not be followed by another wait.
// Entry clear next time is safe because service follows it.
cpu.iteration(backend, true, false, predicate_sees_work);
assert(cpu.services == 1 && !cpu.work_pending);
assert(!cpu.slept_with_work);
assert(cpu.sdk_calls <= 4);
}
}
}
static void lateDouble() {
for (Backend backend : {Backend::Raw, Backend::SoftDevice}) {
for (Point point : {Point::AfterPredicate, Point::BeforeWait}) {
Cpu cpu;
cpu.injection = point;
cpu.iteration(backend, true, false, true, true);
assert(cpu.injected && cpu.services == 0 && cpu.work_pending);
assert(cpu.sleeps == 1 && cpu.slept_with_work);
if (backend == Backend::SoftDevice) {
assert(cpu.sdk_calls == 3 && cpu.waits == 3);
assert(cpu.immediate_returns == 2);
}
}
}
}
static void cleanup() {
for (Point point : {Point::AfterFirstWait, Point::BetweenCleanupSevAndWfe}) {
Cpu cpu;
cpu.injection = point;
cpu.iteration(Backend::Raw, true, false);
assert(cpu.waits == 1 && cpu.sleeps == 1);
assert(cpu.injected && cpu.work_pending && !cpu.slept_with_work);
cpu.iteration(Backend::Raw, true, false);
assert(cpu.services == 1 && !cpu.work_pending && !cpu.slept_with_work);
}
}
static void privateCounter() {
Cpu cpu;
for (unsigned n = 0; n < 100; ++n)
cpu.iteration(Backend::SoftDevice, true, false, true, false, false);
assert(cpu.sleeps == 0 && cpu.sdk_calls == 100);
assert(cpu.immediate_returns == 100); // Raw early clear cannot ack SDK state.
}
static void sevOnlyEntry() {
Cpu cpu;
cpu.app_irq_acknowledged = cpu.app_irq_current;
cpu.work_pending = true; // Preexisting service work, not a new IRQ.
cpu.sev();
cpu.singleWait(Backend::SoftDevice);
assert(cpu.sleeps == 1 && cpu.slept_with_work);
}
static void privateCounterWrap() {
Cpu cpu;
cpu.app_irq_current = cpu.app_irq_acknowledged = 19;
for (unsigned n = 0; n < 2048; ++n) {
cpu.recordApplicationInterrupt();
assert(cpu.app_irq_current != cpu.app_irq_acknowledged);
}
const unsigned before = cpu.sleeps;
cpu.singleWait(Backend::SoftDevice);
assert(cpu.sleeps == before && cpu.app_irq_current == cpu.app_irq_acknowledged);
}
static void waitIrqHistory() {
Cpu cpu;
cpu.app_irq_acknowledged = cpu.app_irq_current;
cpu.injection = Point::DuringWait;
cpu.singleWait(Backend::SoftDevice);
assert(cpu.sleeps == 1 && cpu.work_pending && !cpu.slept_with_work);
assert(cpu.app_irq_current != cpu.app_irq_acknowledged);
const unsigned sleeps = cpu.sleeps;
// The next acknowledgement cannot sleep or erase the ISR-published work.
cpu.singleWait(Backend::SoftDevice);
assert(cpu.sleeps == sleeps && cpu.work_pending);
assert(cpu.app_irq_current == cpu.app_irq_acknowledged);
cpu.service();
assert(!cpu.work_pending && cpu.services == 1);
}
static void persistentDisabledIrq() {
Cpu cpu;
cpu.pendCryptoCell();
for (unsigned n = 0; n < 100; ++n)
cpu.iteration(Backend::SoftDevice, true, false, true, false, true, false);
assert(cpu.cc_pending && cpu.sleeps == 0);
// ER consumption and private SDK acknowledgements did not clear NVIC state.
cpu.iteration(Backend::SoftDevice, true, false);
assert(!cpu.cc_pending && cpu.sleeps == 1 && cpu.cc_clears == 1);
}
static void lateDisabledIrq() {
Cpu cpu;
cpu.prepare(Backend::SoftDevice);
cpu.service();
cpu.pendCryptoCell(); // New pending IRQ after all service/predicate checks.
cpu.singleWait(Backend::SoftDevice);
assert(cpu.cc_pending && cpu.sleeps == 0);
cpu.iteration(Backend::SoftDevice, true, false);
assert(!cpu.cc_pending && cpu.sleeps == 1);
}
static void preserveIrqOwner() {
Cpu cpu;
cpu.cc_enabled = true;
cpu.pendCryptoCell();
cpu.prepare(Backend::SoftDevice);
assert(cpu.cc_pending && cpu.cc_clears == 0);
cpu.singleWait(Backend::SoftDevice);
assert(cpu.sleeps == 0 && cpu.cc_pending);
// Only the enabled owner's ISR consumes its pending bit and publishes work.
cpu.cc_pending = false;
cpu.applicationIrq();
cpu.iteration(Backend::SoftDevice, true, false);
assert(cpu.services == 1 && !cpu.work_pending && cpu.sleeps == 1);
assert(cpu.cc_clears == 0);
Cpu other;
other.other_irq_pending = true;
for (unsigned n = 0; n < 3; ++n)
other.iteration(Backend::SoftDevice, true, false);
assert(other.other_irq_pending && other.sleeps == 0);
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!std::strcmp(argv[1], "scheduler")) scheduler();
else if (!std::strcmp(argv[1], "entry_idle")) entryIdle();
else if (!std::strcmp(argv[1], "poll_yield")) pollYield();
else if (!std::strcmp(argv[1], "injections")) injections(true);
else if (!std::strcmp(argv[1], "unobserved")) injections(false);
else if (!std::strcmp(argv[1], "late_double")) lateDouble();
else if (!std::strcmp(argv[1], "cleanup")) cleanup();
else if (!std::strcmp(argv[1], "private_counter")) privateCounter();
else if (!std::strcmp(argv[1], "sev_only")) sevOnlyEntry();
else if (!std::strcmp(argv[1], "counter_wrap")) privateCounterWrap();
else if (!std::strcmp(argv[1], "wait_irq_history")) waitIrqHistory();
else if (!std::strcmp(argv[1], "disabled_irq")) persistentDisabledIrq();
else if (!std::strcmp(argv[1], "late_disabled_irq")) lateDisabledIrq();
else if (!std::strcmp(argv[1], "irq_owner")) preserveIrqOwner();
else assert(false);
}
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""Execute the one-cycle SDK snapshot and its actual nRF52 board consumers."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
class Nrf52SleepCycleTests(unittest.TestCase):
def execute(self, code):
with tempfile.TemporaryDirectory(prefix="meshcore-sleep-cycle-") as directory:
source = Path(directory) / "test.cpp"
binary = Path(directory) / "test"
source.write_text(code, encoding="ascii")
result = subprocess.run([
os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
"-pthread", "-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie", "-I", str(ROOT / "src"),
str(source), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_cache_one_shot_error_invalidation_and_concurrent_publication(self):
self.execute(r'''
#include <cassert>
#include <thread>
#include <helpers/nrf52/SleepCycleState.h>
int main() {
using namespace mesh_nrf52;
SleepCycleState state;
SleepCycleSnapshot out;
assert(!state.snapshot(out) && !state.take(out) && state.lastError() == 0);
auto token = state.begin();
assert(!state.snapshot(out));
assert(state.publish(token, false, 3));
assert(state.snapshot(out) && !out.softdevice_enabled && out.usb_status == 3);
assert(state.take(out) && !state.take(out) && !state.snapshot(out));
token = state.begin();
state.invalidate(); // Startup began during an authoritative SDK query.
assert(!state.publish(token, false, 1) && !state.snapshot(out));
token = state.begin();
assert(state.publish(token, true, 1, 17));
assert(!state.take(out) && state.lastError() == 17);
state.invalidate();
assert(state.lastError() == 0); // Do not attribute the old error to a new transition.
token = state.begin();
assert(state.publish(token, true, 0) && state.snapshot(out));
const auto previous = out;
token = state.begin();
assert(!state.current(previous.generation) && !state.snapshot(out));
assert(state.publish(token, false, 1) && state.snapshot(out));
assert(!out.softdevice_enabled && out.usb_status == 1);
std::atomic<bool> done{false};
std::thread writer([&]() {
for (unsigned i = 0; i < 100000; ++i) {
const auto next = state.begin();
state.publish(next, (next & 1) != 0, next ^ 0x75201346U);
}
done.store(true, std::memory_order_release);
});
// Seed a consistent record before accepting the publication stress cases.
state.invalidate();
while (!done.load(std::memory_order_acquire)) {
if (state.snapshot(out)) {
assert(out.usb_status == (out.generation ^ 0x75201346U));
assert(out.softdevice_enabled == ((out.generation & 1) != 0));
}
}
writer.join();
}
''')
def test_actual_prepare_power_cache_sleep_and_partial_startup_fail_closed(self):
source = (ROOT / "src/helpers/NRF52Board.cpp").read_text(encoding="ascii")
methods = "\n".join(extract_braced(source, signature) for signature in (
"void NRF52Board::prepareSleepCycle()",
"bool NRF52Board::isExternalPowered()",
"void NRF52Board::sleep(uint32_t secs)",
))
code = r'''
#include <cassert>
#include <initializer_list>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/nrf52/SleepCycleState.h>
constexpr uint32_t NRF_SUCCESS = 0, NRF_ERROR_INVALID_STATE = 8;
constexpr uint32_t POWER_USBREGSTATUS_VBUSDETECT_Msk = 1;
constexpr uint32_t SCB_ICSR_PENDSVSET_Msk = 1UL << 28;
constexpr int taskSCHEDULER_RUNNING = 2;
#define NRF_CRYPTOCELL 1
constexpr int CRYPTOCELL_IRQn = 42;
struct NvicState { volatile uint32_t __cr_flag = 0; } nrf_nvic_state;
static bool cc_enabled = false, cc_pending = false;
static unsigned cc_clears = 0;
static uint32_t NVIC_GetEnableIRQ(int irq) {
assert(irq == CRYPTOCELL_IRQn); return cc_enabled;
}
static void NVIC_ClearPendingIRQ(int irq) {
assert(irq == CRYPTOCELL_IRQn); cc_pending = false; ++cc_clears;
}
struct Scb { uint32_t ICSR = 0; } scb;
static Scb* SCB = &scb;
struct Power { uint32_t USBREGSTATUS = 0; } power;
static Power* NRF_POWER = &power;
static bool actual_sd = false, invalidate_during_query = false, event = false;
static uint32_t actual_usb = 0, sd_error = 0, usb_error = 0;
static unsigned state_queries = 0, usb_queries = 0, sd_waits = 0, bare_waits = 0, delays = 0;
static unsigned acknowledgements = 0, forced_yields = 0;
static uint32_t ipsr = 0, primask = 0, basepri = 0, faultmask = 0;
static int scheduler_state = taskSCHEDULER_RUNNING;
static bool yielded_app_irq = false, defer_pendsv = false, invalidate_during_yield = false;
static bool critical_during_yield = false;
static uint32_t acknowledgement_error = 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 void taskYIELD() {
++forced_yields; event = true;
if (defer_pendsv) scb.ICSR |= SCB_ICSR_PENDSVSET_Msk;
else yielded_app_irq = true;
if (invalidate_during_yield) mesh_nrf52::sleepCycleState().invalidate();
if (critical_during_yield) nrf_nvic_state.__cr_flag = 1;
}
static uint32_t now_us = 1000;
static uint32_t millis() { return now_us / 1000; }
static uint32_t micros() { return now_us; }
static void delay(unsigned ms) { delays += ms; }
namespace mesh_nrf52 {
uint32_t softdeviceIsEnabled(uint8_t& out) {
++state_queries; out = actual_sd; event = true; // Model the SVC-caused event.
if (invalidate_during_query) sleepCycleState().invalidate();
return sd_error;
}}
static uint32_t sd_power_usbregstatus_get(uint32_t* out) {
++usb_queries; *out = actual_usb; event = true; return usb_error;
}
static uint32_t sd_app_evt_wait() {
if (yielded_app_irq) {
yielded_app_irq = false; ++acknowledgements;
return acknowledgement_error;
}
++sd_waits; now_us += 300; return 0;
}
static void __SEV() { event = true; }
static void __WFE() {
if (event) event = false;
else { ++bare_waits; now_us += 500; }
}
struct NRF52Board {
bool radio_test = false;
bool isRadioTestActive() const { return radio_test; }
void prepareSleepCycle();
bool isExternalPowered();
void sleep(uint32_t secs);
};
@METHODS@
int main() {
using namespace mesh::powerdiag;
NRF52Board board;
board.sleep(0); assert(state_queries == 0 && sd_waits == 0 && bare_waits == 0);
power.USBREGSTATUS = 1;
board.prepareSleepCycle();
assert(state_queries == 1 && usb_queries == 0 && !event);
assert(forced_yields == 0 && acknowledgements == 0);
for (int i = 0; i < 20; ++i) assert(board.isExternalPowered());
assert(state_queries == 1 && !event); // UI power polling must not create late SVC events.
power.USBREGSTATUS = 0;
assert(board.isExternalPowered()); // Stable only within this prepared cycle.
board.prepareSleepCycle();
assert(!board.isExternalPowered() && state_queries == 2);
board.sleep(0);
assert(bare_waits == 1 && sd_waits == 0 && state_queries == 2);
board.sleep(0); assert(bare_waits == 1 && state_queries == 2); // One-shot, not a global SD cache.
// Startup can partially enable SD before failing. Invalidation is BEFORE
// the attempt, not conditional on a successful Bluefruit return value.
board.prepareSleepCycle();
mesh_nrf52::sleepCycleState().invalidate(); actual_sd = true; actual_usb = 1;
assert(board.isExternalPowered()); // Stale SD-off cycle cannot bypass the SDK.
assert(usb_queries == 1);
const auto before = state_queries;
board.sleep(0); assert(state_queries == before && sd_waits == 0);
power.USBREGSTATUS = 0;
board.prepareSleepCycle();
assert(!event && board.isExternalPowered() && usb_queries == 2);
assert(forced_yields == 1 && acknowledgements == 1);
board.sleep(0); assert(sd_waits == 1 && bare_waits == 1);
setEnabled(true, millis()); reset(millis());
sd_error = 17;
board.prepareSleepCycle(); board.sleep(0);
auto value = snapshot(millis());
assert(value.errors == 1 && value.detail == 17 && value.calls == 0);
assert(sd_waits == 1 && bare_waits == 1);
sd_error = 0; usb_error = 23; reset(millis());
board.prepareSleepCycle(); board.sleep(0);
value = snapshot(millis());
assert(value.errors == 1 && value.detail == 23 && value.calls == 0);
usb_error = 0; invalidate_during_query = true; reset(millis());
board.prepareSleepCycle(); board.sleep(0);
value = snapshot(millis());
assert(value.board_vetoes == 1 && value.detail == 3 && value.calls == 0);
invalidate_during_query = false;
for (uint32_t* mask : {&ipsr, &primask, &basepri, &faultmask}) {
*mask = 1; reset(millis());
const auto before_query = state_queries, before_ack = acknowledgements;
board.prepareSleepCycle(); board.sleep(0);
value = snapshot(millis());
assert(value.errors == 1 && value.detail == NRF_ERROR_INVALID_STATE);
assert(state_queries == before_query && acknowledgements == before_ack);
*mask = 0;
}
scheduler_state = 0; reset(millis());
board.prepareSleepCycle(); board.sleep(0);
assert(snapshot(millis()).detail == NRF_ERROR_INVALID_STATE);
scheduler_state = taskSCHEDULER_RUNNING;
defer_pendsv = true; reset(millis());
const auto before_ack = acknowledgements;
board.prepareSleepCycle(); board.sleep(0);
assert(acknowledgements == before_ack && snapshot(millis()).detail == NRF_ERROR_INVALID_STATE);
defer_pendsv = false; scb.ICSR = 0;
acknowledgement_error = 31; reset(millis());
board.prepareSleepCycle(); board.sleep(0);
assert(snapshot(millis()).errors == 1 && snapshot(millis()).detail == 31);
acknowledgement_error = 0;
invalidate_during_yield = true; reset(millis());
const auto before_invalid_ack = acknowledgements;
board.prepareSleepCycle(); board.sleep(0);
assert(acknowledgements == before_invalid_ack && snapshot(millis()).detail == 3);
invalidate_during_yield = false; yielded_app_irq = false;
board.prepareSleepCycle(); board.radio_test = true; board.sleep(0);
board.radio_test = false; board.sleep(0);
assert(sd_waits == 1); // A veto consumed the prior preparation too.
// Only the disabled polling CryptoCell IRQ may be cleaned, and only at
// entry. An enabled owner and unrelated NVIC sources are never touched.
cc_pending = true;
const auto before_cc_clear = cc_clears;
board.prepareSleepCycle();
assert(!cc_pending && cc_clears == before_cc_clear + 1);
cc_enabled = true; cc_pending = true;
board.prepareSleepCycle();
assert(cc_pending && cc_clears == before_cc_clear + 1);
cc_enabled = false;
// A public SDK critical region can virtually mask an enabled owner even
// with all CPU mask registers clear. Refuse preparation before cleanup.
nrf_nvic_state.__cr_flag = 1;
const auto before_critical_query = state_queries;
board.prepareSleepCycle();
assert(state_queries == before_critical_query && cc_pending);
assert(cc_clears == before_cc_clear + 1);
assert(mesh_nrf52::sleepCycleState().lastError() == NRF_ERROR_INVALID_STATE);
nrf_nvic_state.__cr_flag = 0;
// Recheck the volatile SDK critical flag after taskYIELD, not just at entry.
critical_during_yield = true;
const auto before_critical_ack = acknowledgements;
board.prepareSleepCycle();
assert(acknowledgements == before_critical_ack);
assert(mesh_nrf52::sleepCycleState().lastError() == NRF_ERROR_INVALID_STATE);
}
'''.replace("@METHODS@", methods)
self.execute(code)
def test_every_application_bluefruit_attempt_invalidates_before_call(self):
for filename, call in (
("src/helpers/NRF52Board.cpp", "if (!Bluefruit.begin(1, 0))"),
("src/helpers/nrf52/SerialBLEInterface.cpp", "if (!Bluefruit.begin())"),
):
with self.subTest(filename=filename):
source = (ROOT / filename).read_text(encoding="ascii")
at = source.index(call)
self.assertTrue(source[:at].rstrip().endswith(
"mesh_nrf52::sleepCycleState().invalidate();"))
board = (ROOT / "src/helpers/NRF52Board.cpp").read_text(encoding="ascii")
late = extract_braced(board, "void NRF52Board::sleep(uint32_t secs)")
self.assertNotIn("softdeviceIsEnabled", late)
self.assertNotIn("sd_power_usbregstatus_get", late)
self.assertNotIn("NVIC->", late)
if __name__ == "__main__":
unittest.main()
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""Execute sleep preparation/cache code with modeled nRF52 event semantics.
The pinned BSP's loop-task yield and empty CDC available() poll both send SEV
through portYIELD. The fixture models the documented event-register/single-SD-
wait contract so fixes cannot trade stale-event insomnia for lost application
work. Production cache and extracted board methods run against these hardware
primitives. Actual residency and SoftDevice behavior still need hardware tests.
"""
from pathlib import Path
import os
import shlex
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
class Nrf52SleepEventTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-nrf52-event-test-")
cls.addClassCleanup(cls.temp.cleanup)
cls.binary = Path(cls.temp.name) / "sleep-events"
result = subprocess.run(
shlex.split(os.environ.get("CXX", "g++"))
+ ["-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic",
str(ROOT / "test/fixtures/nrf52_sleep_events.cpp"),
"-o", str(cls.binary)],
capture_output=True, text=True,
)
if result.returncode:
raise RuntimeError(result.stdout + result.stderr)
board = (ROOT / "src/helpers/NRF52Board.cpp").read_text(encoding="ascii")
methods = [body(board, signature) for signature in (
"void NRF52Board::prepareSleepCycle()",
"bool NRF52Board::isExternalPowered()",
"void NRF52Board::sleep(uint32_t secs)",
)]
fixture = (ROOT / "test/fixtures/nrf52_sleep_cycle.cpp").read_text(encoding="ascii")
cls.cycle_source = fixture.replace("@BOARD_METHODS@", "\n".join(methods))
cls.cycle_binary = cls.compile_cycle("cycle", cls.cycle_source)
# Negative control executes the real code with only the old late state
# SVC restored. It must reproduce insomnia with non-yielding RX polls.
late_query = ("uint8_t ignored_sd_state = 0;\n"
" mesh_nrf52::softdeviceIsEnabled(ignored_sd_state);\n ")
late_methods = methods.copy()
late_methods[-1] = late_methods[-1].replace(
"if (cycle.softdevice_enabled)", late_query + "if (cycle.softdevice_enabled)", 1)
cls.late_query_binary = cls.compile_cycle(
"late-query", fixture.replace("@BOARD_METHODS@", "\n".join(late_methods)))
no_ack = cls.cycle_source.replace("error = sd_app_evt_wait();", "error = NRF_SUCCESS;", 1)
cls.no_ack_binary = cls.compile_cycle("no-sdk-ack", no_ack)
no_yield = cls.cycle_source.replace(" taskYIELD();", " // Negative control: no real PendSV.", 1)
cls.no_yield_binary = cls.compile_cycle("no-forced-pendsv", no_yield)
@classmethod
def compile_cycle(cls, name, source):
cpp = Path(cls.temp.name) / (name + ".cpp")
binary = Path(cls.temp.name) / name
cpp.write_text(source, 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", "-I", str(ROOT / "src"), str(cpp),
str(ROOT / "test/fixtures/nrf52_sleep_cycle_other.cpp"),
"-o", str(binary)],
capture_output=True, text=True,
)
if result.returncode:
raise RuntimeError(result.stdout + result.stderr)
return binary
def run_case(self, name):
result = subprocess.run([str(self.binary), name], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def run_cycle_case(self, name):
result = subprocess.run([str(self.cycle_binary), name], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_prior_scheduler_yield_prevents_every_single_late_wait(self):
self.run_case("scheduler")
def test_entry_clear_and_non_yielding_rx_allow_idle_sleep(self):
self.run_case("entry_idle")
def test_entry_clear_alone_cannot_fix_empty_cdc_poll_yield(self):
self.run_case("poll_yield")
def test_irqs_at_every_entry_work_predicate_and_wait_boundary_are_safe(self):
self.run_case("injections")
def test_event_latch_protects_work_not_visible_to_the_sleep_predicate(self):
self.run_case("unobserved")
def test_late_double_wait_can_sleep_with_unserviced_application_work(self):
self.run_case("late_double")
def test_raw_cleanup_after_first_wait_does_not_wait_again(self):
self.run_case("cleanup")
def test_raw_entry_clear_cannot_acknowledge_private_sdk_irq_state(self):
self.run_case("private_counter")
def test_sev_alone_cannot_guarantee_nonblocking_early_sdk_wait(self):
self.run_case("sev_only")
def test_private_irq_latch_cannot_wrap_back_to_acknowledged_state(self):
self.run_case("counter_wrap")
def test_sdk_entry_ack_preserves_irq_history_from_during_its_wait(self):
self.run_case("wait_irq_history")
def test_persistent_disabled_irq_survives_event_clear_and_sdk_ack(self):
self.run_case("disabled_irq")
def test_late_disabled_crypto_irq_returns_then_next_entry_recovers(self):
self.run_case("late_disabled_irq")
def test_enabled_crypto_owner_and_other_pending_irqs_are_preserved(self):
self.run_case("irq_owner")
def test_all_sleeping_roles_prepare_before_any_work_or_policy_check(self):
entry = "\nvoid loop() {\n#if defined(NRF52_PLATFORM)\n board.prepareSleepCycle();\n#endif\n"
for role in ("simple_repeater", "simple_room_server", "simple_sensor",
"companion_radio"):
with self.subTest(role=role):
source = (ROOT / "examples" / role / "main.cpp").read_text(encoding="ascii")
actual = body(source, "\nvoid loop() {")
self.assertTrue(actual.startswith(entry))
self.assertEqual(actual.count("board.prepareSleepCycle();"), 1)
def test_actual_cache_single_attempt_stale_publication_errors_and_recovery(self):
self.run_cycle_case("cache")
def test_actual_board_raw_wait_does_not_issue_a_late_state_query(self):
self.run_cycle_case("raw")
def test_actual_board_softdevice_wait_does_not_issue_a_late_state_query(self):
self.run_cycle_case("sd")
def test_actual_raw_wait_keeps_an_irq_after_the_predicate_visible(self):
self.run_cycle_case("raw_irq")
def test_actual_softdevice_wait_keeps_an_irq_after_the_predicate_visible(self):
self.run_cycle_case("sd_irq")
def test_actual_cross_translation_unit_start_or_partial_start_invalidates_sleep(self):
self.run_cycle_case("transition")
def test_actual_query_errors_remain_awake_and_next_valid_cycle_recovers(self):
self.run_cycle_case("query_error")
def test_actual_invalidation_during_query_cannot_publish_stale_state(self):
self.run_cycle_case("during_query")
def test_actual_irq_during_preparation_preserves_work_for_following_service(self):
self.run_cycle_case("prepare_irq")
def test_actual_usb_power_snapshot_avoids_ui_svcs_and_refreshes_each_loop(self):
self.run_cycle_case("usb")
def test_actual_disabled_crypto_pending_cleanup_allows_sdk_sleep(self):
self.run_cycle_case("cc_recover")
def test_actual_late_crypto_pending_bit_is_not_cleared_until_next_entry(self):
self.run_cycle_case("cc_late")
def test_actual_enabled_crypto_owner_and_other_irqs_are_not_cleared(self):
self.run_cycle_case("cc_owner")
self.run_cycle_case("other_owner")
def test_actual_sdk_critical_region_prevents_queries_or_early_ack(self):
self.run_cycle_case("sdk_critical")
self.run_cycle_case("sdk_critical_after_yield")
def test_restoring_old_late_svc_is_a_detected_insomnia_negative_control(self):
result = subprocess.run(
[str(self.late_query_binary), "raw"], capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("Assertion", result.stderr)
def test_missing_early_sdk_ack_or_forced_real_irq_are_detected_negative_controls(self):
for binary in (self.no_ack_binary, self.no_yield_binary):
with self.subTest(binary=binary.name):
result = subprocess.run([str(binary), "sd"], capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("Assertion", result.stderr)
if __name__ == "__main__":
unittest.main()
+134 -6
View File
@@ -1,8 +1,12 @@
#!/usr/bin/env python3
"""Static integration guards for nRF52 nonblocking USB diagnostics."""
"""Integration guards and native tests for nRF52 nonblocking USB diagnostics."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
@@ -133,11 +137,7 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
helper_start = source.index("static size_t writeTinyUsbCdcOnce(")
helper_end = source.index(
"static SingleAttemptNonBlockingStream", helper_start
)
helper = source[helper_start:helper_end]
helper = extract_braced(source, "static size_t writeTinyUsbCdcOnce(")
self.assertEqual(helper.count("tud_cdc_n_write(instance, data, attempt)"), 1)
self.assertNotIn("Adafruit_USBD_CDC::write", helper)
self.assertIn("tud_cdc_n_connected(instance)", helper)
@@ -148,6 +148,134 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
"usb_serial_interface.begin(mesh::usbCompanionPort(),", main
)
def test_primary_empty_rx_query_never_yields_and_preserves_session_gates(self):
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text(encoding="ascii")
facade = extract_braced(source, "class Nrf52PrimaryUsbStream") + ";"
access = extract_braced(source, "static bool canAccessPrimaryUsbSession(")
self.assertIn("static Nrf52PrimaryUsbStream nonblocking_primary_usb_companion_port(", source)
self.assertIn("using Nrf52PrimaryUsbStream = SingleAttemptNonBlockingStream;", source)
fixture = r'''
#include <atomic>
#include <cassert>
#include <functional>
#include <helpers/NonBlockingWriteStream.h>
static bool mounted = true;
static unsigned queried[2] = {}, delegate_queries = 0, delegate_yields = 0;
static unsigned rx[2] = {};
static std::function<void()> during_query, during_read;
static bool tud_mounted() { return mounted; }
static uint32_t tud_cdc_n_available(uint8_t instance) {
assert(instance < 2);
++queried[instance];
const uint32_t count = rx[instance];
if (during_query) {
auto callback = std::move(during_query);
during_query = nullptr;
callback();
}
return count;
}
struct Delegate : Stream {
int available() override {
++delegate_queries;
if (rx[0] == 0) ++delegate_yields;
return static_cast<int>(rx[0]);
}
int read() override {
const bool had_byte = rx[0] != 0;
if (had_byte) --rx[0];
if (during_read) {
auto callback = std::move(during_read);
during_read = nullptr;
callback();
}
return had_byte ? 42 : -1;
}
int peek() override { return rx[0] ? 42 : -1; }
};
namespace mesh {
static std::atomic<uint32_t> primary_usb_reset_generation{0};
static std::atomic<uint32_t> primary_usb_allowed_generation{0};
static std::atomic<bool> primary_usb_terminal_discard_pending{false};
static std::atomic<bool> nrf52_usb_reenumeration_pending{false};
static std::atomic<bool> primary_usb_line_state_dtr{true};
static std::atomic<uint32_t> primary_usb_rx_generation{UINT32_MAX};
static uint32_t primaryUsbSessionGeneration() { return primary_usb_reset_generation.load(); }
@ACCESS@
@FACADE@
}
int main() {
Delegate delegate;
mesh::Nrf52PrimaryUsbStream port(delegate, nullptr, nullptr,
mesh::canAccessPrimaryUsbSession);
static_assert(sizeof(port) == sizeof(mesh::SingleAttemptNonBlockingStream),
"Primary RX polling must not add per-stream RAM");
for (unsigned i = 0; i < 1000; ++i) assert(port.available() == 0);
assert(delegate_queries == 0 && delegate_yields == 0);
assert(queried[0] == 1000 && queried[1] == 0);
rx[1] = 17;
assert(port.available() == 0 && queried[1] == 0); // Never borrow CDC1 input.
rx[0] = 3;
assert(port.available() == 3 && port.peek() == 42);
assert(port.read() == 42 && rx[0] == 2); // Keep the inherited read/peek route.
during_query = [] { ++mesh::primary_usb_reset_generation; };
assert(port.available() == 0); // Closed during the count query.
const unsigned closed_queries = queried[0];
assert(port.available() == 0 && queried[0] == closed_queries);
mesh::primary_usb_allowed_generation = mesh::primaryUsbSessionGeneration();
mesh::primary_usb_line_state_dtr = false;
mesh::primary_usb_rx_generation = UINT32_MAX;
rx[0] = 0;
assert(port.available() == 0);
assert(mesh::primary_usb_rx_generation == UINT32_MAX); // Empty RX is not proof.
rx[0] = 2;
assert(port.available() == 2); // MeshCLI may send with DTR low.
assert(mesh::primary_usb_rx_generation == mesh::primaryUsbSessionGeneration());
assert(port.read() == 42 && port.available() == 1);
during_read = [] { ++mesh::primary_usb_reset_generation; };
assert(port.read() == -1); // Inherited read drops an old-epoch byte.
assert(port.available() == 0);
mesh::primary_usb_allowed_generation = mesh::primaryUsbSessionGeneration();
mesh::primary_usb_line_state_dtr = true;
for (auto* gate : {&mesh::primary_usb_terminal_discard_pending,
&mesh::nrf52_usb_reenumeration_pending}) {
*gate = true;
const unsigned before = queried[0];
assert(port.available() == 0 && queried[0] == before);
*gate = false;
}
mounted = false;
assert(port.available() == 0);
assert(delegate_queries == 0 && delegate_yields == 0 && queried[1] == 0);
}
'''.replace("@ACCESS@", access).replace("@FACADE@", facade)
controls = {
"production": fixture,
"delegated-empty-query": fixture.replace(
"static_cast<int>(tud_cdc_n_available(0))", "SingleAttemptNonBlockingStream::available()"),
"missing-post-query-gate": fixture.replace(
"return canAccessPrimaryUsbSession(nullptr) ? count : 0;", "return count;"),
"wrong-cdc-index": fixture.replace(
"static_cast<int>(tud_cdc_n_available(0))", "static_cast<int>(tud_cdc_n_available(1))"),
}
compiler = shutil.which(os.environ.get("CXX", "g++"))
self.assertIsNotNone(compiler, "a host C++17 compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-nrf52-empty-rx-") as directory:
for label, code in controls.items():
with self.subTest(control=label):
cpp, binary = Path(directory) / f"{label}.cpp", Path(directory) / label
cpp.write_text(code, encoding="ascii")
built = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-Wno-sign-compare",
*([] if os.name == "nt" else ["-fsanitize=address,undefined",
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]),
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=30)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(ran.returncode == 0, label == "production", ran.stdout + ran.stderr)
def test_dedicated_logging_is_drained_only_from_tinyusb_task(self):
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
stream = (ROOT / "src/helpers/NonBlockingWriteStream.h").read_text()
@@ -215,6 +215,7 @@ int main() {
#include <cassert>
#include <cstring>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/nrf52/SleepCycleState.h>
static uint32_t now_us = 1000, wait_duration = 547, sdk_error = 0;
static bool softdevice = true;
static char sequence[32] = {}; static unsigned steps = 0, yields = 0;
@@ -234,28 +235,38 @@ struct NRF52Board {
void sleep(uint32_t secs);
};
@METHOD@
static void prepare() {
auto& state = mesh_nrf52::sleepCycleState();
const uint32_t token = state.begin();
assert(state.publish(token, softdevice, 0));
}
int main() {
using namespace mesh::powerdiag;
NRF52Board board;
prepare();
board.sleep(0);
assert(steps == 1 && sequence[0] == 'A');
assert(snapshot(millis()).calls == 0);
setEnabled(true, millis()); reset(millis());
prepare();
board.sleep(0);
auto value = snapshot(millis());
assert(value.calls == 1 && value.returns == 1 && value.last_call_us == 547);
assert(value.errors == 0 && value.board_vetoes == 0);
board.test = true;
prepare();
board.sleep(0);
value = snapshot(millis());
assert(steps == 2 && yields == 1);
assert(value.calls == 1 && value.board_vetoes == 1 && value.detail == 2);
assert(value.current & mask(Reason::Board));
board.test = false; sdk_error = 17;
prepare();
board.sleep(0);
value = snapshot(millis());
assert(value.errors == 1 && value.detail == 17 && value.calls == 2 && value.returns == 2);
softdevice = false; steps = 0; memset(sequence, 0, sizeof(sequence));
prepare();
board.sleep(0);
assert(!strcmp(sequence, "WSW")); // Do not erase a radio GPIO edge with SEV/WFE/WFE.
value = snapshot(millis());
+8 -1
View File
@@ -284,7 +284,14 @@ class UsbLoggingWatchdogRoleTests(unittest.TestCase):
self.assertIn('mesh::serviceUsbLoggingPort();', loop)
self.assertIn('if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) '
'board.reboot();', loop)
self.assertIn('!mesh::isUsbLoggingWatchdogArmed()', source)
if role == 'companion_radio':
guard = method(loop, 'mesh::powerdiag::Mask sleep_reason = [&]()')
self.assertIn('if (mesh::isUsbLoggingWatchdogArmed()) '
'return mask(Reason::UsbRecovery);', guard)
self.assertIn('const bool can_sleep = sleep_reason == 0;', loop)
self.assertLess(loop.index(guard), loop.index('if (can_sleep)'))
else:
self.assertIn('!mesh::isUsbLoggingWatchdogArmed()', loop)
@staticmethod
def _harness(declaration, definition, callback, role, commands, companion):
+11 -1
View File
@@ -301,7 +301,17 @@ int main(){
source = (ROOT/'examples'/role/'main.cpp').read_text()
self.assertIn(f'control().begin({backend})', source)
self.assertIn('control().service(millis())', source[source.index('\nvoid loop()'):])
self.assertIn('&& !mesh::wireless::control().pending()', source[source.index('\nvoid loop()'):])
loop = method(source, '\nvoid loop()')
if role == 'companion_radio':
guard = method(loop, 'mesh::powerdiag::Mask sleep_reason = [&]()')
self.assertIn('if (mesh::wireless::control().pending()) '
'return mask(Reason::Wireless);', guard)
self.assertIn('const bool can_sleep = sleep_reason == 0;', loop)
self.assertLess(loop.index(guard), loop.index('if (can_sleep)'))
else:
self.assertIn('&& sleep_guard(!mesh::wireless::control().pending(), '
'mesh::powerdiag::Reason::Wireless)', loop)
self.assertIn('return allowed;', method(loop, 'const auto sleep_guard = []'))
gates = {
'src/helpers/bridges/MQTTBridge.cpp': 'WiFi',
'src/helpers/bridges/ESPNowBridge.cpp': 'EspNow',