Fix nRF52 idle sleep event and CryptoCell handling
This commit is contained in:
1 parent
756d887bad
commit
629557fcfe
22 files changed
+1767
-20
No files matched your search
Vendored
+17
-3
@@ -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
|
||||
}
|
||||
Vendored
+307
@@ -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
@@ -0,0 +1,5 @@
|
||||
#include <helpers/nrf52/SleepCycleState.h>
|
||||
|
||||
void invalidateSleepCycleFromOtherTranslationUnit() {
|
||||
mesh_nrf52::sleepCycleState().invalidate();
|
||||
}
|
||||
Vendored
+410
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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());
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user