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

+2
View File
@@ -320,6 +320,8 @@ jobs:
python3 -B test/test_power_saving_diagnostics.py -v
python3 -B test/test_power_saving_role_diagnostics.py -v
python3 -B test/test_companion_power_diagnostics.py -v
python3 -B test/test_nrf52_sleep_cycle.py -v
python3 -B test/test_nrf52_sleep_events.py -v
python3 -B test/test_esp32_scanner_power_yield.py -v
python3 -B test/test_esp32_gpio_hold.py -v
+151
View File
@@ -0,0 +1,151 @@
# nRF52 idle-sleep regression
## Cause
The MCU could repeatedly enter the board's sleep function without actually
sleeping. Three operations could leave the Cortex-M event register set:
1. The pinned Adafruit core yields after every Arduino `loop()`. Its FreeRTOS
yield issues `SEV`.
2. The old board sleep function queried SoftDevice state immediately before
waiting. In the measured SoftDevice-off case this SVC recreated the event.
Moving only the scheduler-event clear was therefore insufficient.
3. Adafruit CDC `available()` yields when its receive FIFO is empty. A USB
client that never asserted DTR but established an RX session could therefore
recreate an event on every idle console poll.
With SoftDevice enabled there is an additional, separate application-interrupt
history. The core's PendSV yield updates it. Clearing the ARM event register
does not acknowledge that history, so the next `sd_app_evt_wait()` still
returns immediately. This follows from the public SDK contract and the pinned
S140 6.1.1/7.3.0 interrupt-forwarder/wait code, not just the ARM event model.
The failed SoftDevice-on captures below had another blocker as well.
There was also a disabled but pending CryptoCell IRQ42 after BLE startup.
SoftDevice's documented disabled-pending-IRQ rule prevents sleep in this
state, regardless of the event-register/history fixes. The pinned CC310
library polls rather than enabling that interrupt; the pinned BSP already
clears its pending bit in its own idle wait. The application-owned wait had
missed that cleanup.
This is distinct from the existing sleep-policy guards. A large eligible time
or a high number of wait calls does not establish CPU sleep residency.
## Fix and ordering
All four nRF52 role loops now begin with `prepareSleepCycle()`. It queries the
actual SDK SoftDevice state and USB power status. When SoftDevice is active,
preparation forces a real application PendSV with `taskYIELD()`, checks that it
was serviced in an unmasked application thread, and acknowledges its SDK
history with one early `sd_app_evt_wait()`. A bare `SEV` cannot guarantee that
this SDK call returns: the SDK can internally wait again without an application
interrupt. The real PendSV is required. This ordering depends on the pinned
core: the other SDK wait caller is the lower-priority idle task, which cannot
consume that acknowledgement while the sketch task is ready.
Preparation rejects exception context, masked interrupts, a suspended/stopped
scheduler, a still-pending PendSV, or an invalidated cycle instead of trying
that early SDK acknowledgement without proof.
Preparation then clears the stale ARM event with `SEV; WFE`, before servicing
work or checking sleep predicates. A current
loop's USB-power snapshot also avoids another state-query SVC during UI work.
On chips with CryptoCell, preparation also clears that specific pending IRQ
only when its interrupt is disabled and the SDK is not in a critical region.
It never sweeps other pending bits or clears an enabled CryptoCell owner's
interrupt. Clearing NVIC pending does not discard the polling crypto result.
The snapshot is valid for one loop and at most one sleep attempt. Both
application-task `Bluefruit.begin()` call sites invalidate it before startup,
including partial or failed startup. Invalid, missing or failed preparation
declines sleep. The existing SDK/direct-register USB access policy is retained.
SoftDevice startup remains owned by the application task; the cache is not
permission to add an asynchronous SDK enable/disable path racing a wait.
Primary USB RX polling now uses the native CDC0 FIFO query, with the existing
session checks both before and after that query. It does not change DTR policy,
CDC1 logging ownership, or the facade's blocking-read/write protections.
The late wait remains a single `sd_app_evt_wait()` when SoftDevice is enabled,
or the existing `WFE; SEV; WFE` sequence when disabled. There is no late
`SEV; WFE; WFE` double-wait: that could discard the only radio IRQ arriving
after the caller checked for work. No blanket IRQ clearing or SEVONPEND change
was added.
## Controlled hardware evidence
The test device was a RAK3401/nRF52840 with an external RAK13302 SX1262 on
Mercerwood. Tests used 908.5 MHz, BW250, SF9, CR5, power saving on, manual
RXPS 30000/10000 us, AGC 0, TX power 2, no forwarding/adverts and USB watchdog
off. A single private benchmark binary allowed changes without reflashing
between the decisive controls. Benchmark controls are not production features.
| Control | Capture | Time-weighted sleeping observations |
| --- | --- | --- |
| Original behavior | 30 s | 0% |
| Early clear only | 30 s | 0% |
| Non-yielding USB poll only | 30 s | 0% |
| Early clear plus USB poll, still with late state SVC | 30 s | 0% |
| No late SVC, non-yielding USB poll, without early clear | 30 s | 0% |
| Early clear, no late SVC, non-yielding USB poll | 90 s | 76.7% |
| Never-DTR client held open, old empty USB poll | 30 s | 0% |
| Same client held open, non-yielding USB poll | 30 s | 82.8% |
The first production candidate restored sleeping observations with SoftDevice
off (6522/8331 samples, about 78% over 90 s), but not with SoftDevice on
(0/8344 samples with a never-DTR client over 90 s; 0/2776 with the client closed
over 30 s). This negative result exposed the separate SDK history and is why
the final preparation also acknowledges a real PendSV before servicing work.
The second candidate added that acknowledgement but still did not sleep with
SoftDevice on. A breakpoint then confirmed the SDK branch, the actual S140
6.1.1 vector, and disabled IRQ42 pending (`ISER1=0x81`, `ISPR1=0x400`). Clearing
only that bit, with the same second binary and no configuration change,
restored 1911/2775 sleeping observations over 30 s. That control justified the
specific disabled-CryptoCell cleanup, rather than a speculative IRQ change.
The final, normal Full repeater artifact was then tested without benchmark
controls or a DTR bypass. Its application readback matched the packaged image
byte-for-byte (SHA-256
`8f6a1654f95d0179dad358d1fc68887eaacc41f4c60281a23099482e1f44af94`).
| Final production test | Capture | Sleeping samples | Weighted estimate |
| --- | --- | --- | --- |
| SoftDevice off, USB terminal closed | 90 s | 6526/8310 | 78.3% |
| SoftDevice on after OTA start/stop, never-DTR client held open | 90 s | 6077/8341 | 73.0% |
| SoftDevice on, client closed | 30 s | 1943/2776 | 70.0% |
The client received valid CLI replies both before and after the capture.
The final frozen SDK-on diagnostics had 281272 completed late waits, only
12 short returns, and zero vetoes/errors. Their accounting window was longer
than the capture; those counters are not its sleep-residency measurement.
All final samples had the same valid debugger/clock state described below.
The never-DTR client successfully issued `get powersaving` and held the same
file descriptor open across both captures. Early clearing and removal of the
late state SVC were held constant in that pair, isolating the USB poll change.
A separate unsafe late-double-wait
control also slept, but was rejected because of the lost-work race above.
Samples read DHCSR S_SLEEP at jittered 7-13 ms intervals. All causal captures
had C_DEBUGEN set, HFCLKSTAT 0x10001 and DCDC baseline 1, without sampled halt,
step, mask, lockup or reset flags. All timestamps were monotonic with no gap
over 20 ms. Percentages weight each sampled state until the next timestamp;
they are point-observation estimates, not exact sleep-residency accounting.
Zero sleeping observations does not exclude extremely short sleeps. Different
calibration phases can contribute to variation between otherwise fixed captures.
## Limits
There was no current meter. These results establish the CPU-sleep regression,
not a measured mA reduction or a complete explanation of the T096/Wio L1
screenshots. SWD and USB alter the power/clock environment. Radio RXPS, FEM/LNA,
GPS, screen, regulator and USB clock loads require matched electrical tests.
In particular the T096 FEM/LNA default differs from EasySkyMesh; this RAK
cannot measure that board-specific load. No RF delivery or long battery-life
claim follows from these captures.
See [power-saving diagnostics](power_saving_diagnostics.md) for the permanent
on-demand CLI counters. Counters remain off by default. Wait-call counters
cover the late eligible wait, not the guaranteed-nonblocking early SDK
acknowledgement. Preparation failures are reported through the board error or
veto path instead.
+4
View File
@@ -62,3 +62,7 @@ wrap (about 49 days).
See the [CLI reference](cli_commands.md#diagnose-what-prevents-power-saving)
for all page names and commands.
The [nRF52 idle-sleep investigation](nrf52_sleep_event_validation.md) describes
an eligible-but-not-sleeping regression, its fix, controlled hardware evidence
and the limits of CPU-state sampling without a current meter.
+3
View File
@@ -4086,6 +4086,9 @@ static bool usbLoggingRecoverySafe(void*) {
}
void loop() {
#if defined(NRF52_PLATFORM)
board.prepareSleepCycle();
#endif
#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
if (pico_wifi_active && WiFi.status() != WL_CONNECTED
&& static_cast<uint32_t>(millis() - last_wifi_reconnect_attempt)
+3
View File
@@ -579,6 +579,9 @@ static bool usbLoggingRecoverySafe(void*) {
}
void loop() {
#if defined(NRF52_PLATFORM)
board.prepareSleepCycle();
#endif
mesh::serviceUsbLoggingPort();
mesh::wireless::control().service(millis());
#if defined(NRF52_PLATFORM)
+3
View File
@@ -298,6 +298,9 @@ static bool usbLoggingRecoverySafe(void*) {
}
void loop() {
#if defined(NRF52_PLATFORM)
board.prepareSleepCycle();
#endif
mesh::serviceUsbLoggingPort();
mesh::wireless::control().service(millis());
#if defined(NRF52_PLATFORM)
+3
View File
@@ -275,6 +275,9 @@ static bool usbLoggingRecoverySafe(void*) {
}
void loop() {
#if defined(NRF52_PLATFORM)
board.prepareSleepCycle();
#endif
mesh::serviceUsbLoggingPort();
#if defined(NRF52_PLATFORM)
board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0);
+92 -8
View File
@@ -4,6 +4,7 @@
#include "NRF52VoltagePolicy.h"
#include "Nrf52BootloaderVersion.h"
#include "nrf52/SoftDeviceState.h"
#include "nrf52/SleepCycleState.h"
#include "nrf52/ShutdownLeds.h"
#include <target.h>
#include "UserGpioPinPolicy.h"
@@ -13,6 +14,7 @@
#include "ble_gap.h"
#include "ble_hci.h"
#include <nrf.h>
#include <nrf_nvic.h>
#include <nrf_soc.h>
#ifdef USE_TINYUSB
#include <Adafruit_TinyUSB.h>
@@ -464,19 +466,88 @@ void NRF52BoardDCDC::begin() {
}
bool NRF52Board::isExternalPowered() {
// Check if SoftDevice is enabled before using its API
mesh_nrf52::SleepCycleSnapshot cycle;
if (mesh_nrf52::sleepCycleState().snapshot(cycle))
return (cycle.usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
// Setup and callers outside a prepared loop retain the SDK access policy.
// Never reuse a cached value after a SoftDevice transition invalidated it.
uint8_t sd_enabled = 0;
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
if (sd_enabled) {
uint32_t usb_status;
sd_power_usbregstatus_get(&usb_status);
volatile uint32_t status = 0;
const uint32_t error = sd_power_usbregstatus_get((uint32_t*)&status);
__asm volatile ("" ::: "memory");
if (error != NRF_SUCCESS) return false;
const uint32_t usb_status = status;
return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
} else {
return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
}
}
void NRF52Board::prepareSleepCycle() {
auto& state = mesh_nrf52::sleepCycleState();
const uint32_t token = state.begin();
const auto application_context = []() {
return __get_IPSR() == 0 && __get_PRIMASK() == 0
&& __get_BASEPRI() == 0 && __get_FAULTMASK() == 0
// SDK critical regions can mask application IRQs in the NVIC even
// while the CPU mask registers are clear. This public field is
// volatile: read it afresh before queries and after the forced yield.
&& nrf_nvic_state.__cr_flag == 0
&& xTaskGetSchedulerState() == taskSCHEDULER_RUNNING;
};
if (!application_context()) {
state.publish(token, false, 0, NRF_ERROR_INVALID_STATE);
return;
}
#ifdef NRF_CRYPTOCELL
// The pinned Adafruit CC310 library polls (no-interrupts archive), but can
// leave its disabled NVIC IRQ pending. The SDK treats that as application
// work forever. Match the core's waitForEvent cleanup only at loop entry;
// never clear an enabled IRQ belonging to an interrupt-driven owner.
if (NVIC_GetEnableIRQ(CRYPTOCELL_IRQn) == 0)
NVIC_ClearPendingIRQ(CRYPTOCELL_IRQn);
#endif
uint8_t sd_enabled = 0;
uint32_t error = mesh_nrf52::softdeviceIsEnabled(sd_enabled);
uint32_t usb_status = 0;
if (error == NRF_SUCCESS) {
if (sd_enabled) {
volatile uint32_t status = 0;
error = sd_power_usbregstatus_get((uint32_t*)&status);
__asm volatile ("" ::: "memory");
usb_status = status;
} else {
usb_status = NRF_POWER->USBREGSTATUS;
}
}
if (error == NRF_SUCCESS && sd_enabled && state.current(token)) {
// Raw SEV/WFE does not acknowledge the SDK's separate application-IRQ
// bookkeeping. First force a real application PendSV while the loop task
// remains ready (so the RTOS idle task cannot consume that acknowledgement).
// The public SDK contract then makes this one early wait non-blocking.
// The pinned S140 6.1.1/7.3.0 forwarding/wait code confirms its private IRQ
// sequence cannot wrap back to the last acknowledged value. Do not access
// those SDK-private counters or rely on SEV alone to release an SDK wait.
taskYIELD();
if (!application_context() || (SCB->ICSR & SCB_ICSR_PENDSVSET_Msk)) {
error = NRF_ERROR_INVALID_STATE;
} else if (state.current(token)) {
error = sd_app_evt_wait();
}
}
state.publish(token, sd_enabled != 0, usb_status, error);
// FreeRTOS yields and the state-query SVC can both leave a stale event.
// Clear it ONLY at loop entry, before servicing/checking application work.
// A radio interrupt after this point must remain visible to the late wait.
__SEV();
__WFE();
}
bool NRF52Board::isUsbDataConnected() {
#if defined(USE_TINYUSB)
#if defined(CFG_TUD_CDC) && CFG_TUD_CDC
@@ -498,12 +569,25 @@ bool NRF52Board::isUsbHostConnected() {
}
void NRF52Board::sleep(uint32_t secs) {
(void)secs;
const bool diagnostics = mesh::powerdiag::enabled();
auto& state = mesh_nrf52::sleepCycleState();
mesh_nrf52::SleepCycleSnapshot cycle;
const bool prepared = state.take(cycle);
if (isRadioTestActive()) {
if (diagnostics) mesh::powerdiag::boardVeto(mesh::powerdiag::Reason::Board, millis(), 2);
delay(1);
return;
}
if (!prepared || !state.current(cycle.generation)) {
if (diagnostics) {
const uint32_t error = state.lastError();
if (error != NRF_SUCCESS) mesh::powerdiag::boardError(static_cast<int32_t>(error), millis());
else mesh::powerdiag::boardVeto(mesh::powerdiag::Reason::Board, millis(), 3);
}
delay(1);
return;
}
// Clear FPU interrupt flags to avoid insomnia
// see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html
#if (__FPU_USED == 1)
@@ -514,10 +598,7 @@ void NRF52Board::sleep(uint32_t secs) {
// On nRF52, we use event-driven sleep instead of timed sleep
// The 'secs' parameter is ignored - we wake on any interrupt
uint8_t sd_enabled = 0;
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
if (cycle.softdevice_enabled) {
// A single call is required here. If an interrupt arrived since the last
// wait, SoftDevice returns immediately so the main loop can service the
// flag or BLE queue before sleeping again.
@@ -708,6 +789,9 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[], bool force_ap) {
Bluefruit.configPrphConn(BLE_GATT_ATT_MTU_DEFAULT,
BLE_GAP_EVENT_LENGTH_MIN, 2, 1);
// Even a failed begin can leave SoftDevice enabled partway through SDK
// startup. Require a fresh authoritative query before the next sleep.
mesh_nrf52::sleepCycleState().invalidate();
if (!Bluefruit.begin(1, 0)) {
return false;
}
+3
View File
@@ -73,6 +73,9 @@ public:
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
virtual bool startOTAUpdate(const char *id, char reply[], bool force_ap = false) override;
virtual bool stopOTAUpdate(char reply[]) override;
// Must precede all work servicing/checks in each application loop. The
// authoritative SDK queries and stale-event clear are never done at sleep.
void prepareSleepCycle();
virtual void sleep(uint32_t secs) override;
bool isUserGpioAvailable(uint8_t pin) const override;
bool isExternalPowered() override;
+21 -1
View File
@@ -736,7 +736,27 @@ static size_t writeTinyUsbCdcOnce(void* context, const uint8_t* data,
return tud_cdc_n_write(instance, data, attempt);
}
static SingleAttemptNonBlockingStream nonblocking_primary_usb_companion_port(
#if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)
// Adafruit's empty CDC available() calls yield(), which sets the Cortex-M
// event register through FreeRTOS. Polling an idle console must not recreate
// that scheduler event after the loop has prepared its sleep decision.
// Only CDC0 is the primary protocol/console; CDC1 keeps its logging owner.
class Nrf52PrimaryUsbStream : public SingleAttemptNonBlockingStream {
public:
using SingleAttemptNonBlockingStream::SingleAttemptNonBlockingStream;
int available() override {
if (!canAccessPrimaryUsbSession(nullptr)) return 0;
const int count = static_cast<int>(tud_cdc_n_available(0));
// A USB owner callback may have closed the epoch during the FIFO query.
return canAccessPrimaryUsbSession(nullptr) ? count : 0;
}
};
#else
using Nrf52PrimaryUsbStream = SingleAttemptNonBlockingStream;
#endif
static Nrf52PrimaryUsbStream nonblocking_primary_usb_companion_port(
Serial, writeTinyUsbCdcOnce, reinterpret_cast<void*>(uintptr_t{0})
#if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)
, canAccessPrimaryUsbSession
+4
View File
@@ -2,6 +2,7 @@
#include "../BluetoothMac.h"
#include "../CompanionFrameQueue.h"
#include "SoftDeviceState.h"
#include "SleepCycleState.h"
#include "BleDebugTrace.h"
#include <stdio.h>
#include <string.h>
@@ -712,6 +713,9 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
// task observation incorrectly reports that the BLE/SOC workers never
// started. Bluefruit.begin() is the reliable startup result available to
// this application.
// A partial/failed begin must not leave the current loop's cached
// SoftDevice state eligible for bare-metal sleep.
mesh_nrf52::sleepCycleState().invalidate();
if (!Bluefruit.begin()) {
uint8_t softdevice_enabled = 0;
const uint32_t softdevice_status =
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include <atomic>
#include <stdint.h>
namespace mesh_nrf52 {
struct SleepCycleSnapshot {
uint32_t generation = 0;
uint32_t usb_status = 0;
bool softdevice_enabled = false;
};
// The application task publishes once at loop entry, before checking work.
// A SoftDevice startup attempt can invalidate from another task without
// racing the publication. No SDK state is inferred from NVIC/vector state.
class SleepCycleState {
std::atomic<uint32_t> generation_{0};
std::atomic<uint32_t> prepared_generation_{0};
std::atomic<uint32_t> usb_status_{0};
std::atomic<uint32_t> error_{0};
std::atomic<bool> softdevice_enabled_{false};
std::atomic<bool> prepared_{false};
public:
uint32_t begin() {
const uint32_t token = generation_.fetch_add(1, std::memory_order_acq_rel) + 1;
prepared_.store(false, std::memory_order_release);
return token;
}
bool publish(uint32_t token, bool softdevice_enabled, uint32_t usb_status,
uint32_t error = 0) {
if (!current(token)) return false;
usb_status_.store(usb_status, std::memory_order_relaxed);
softdevice_enabled_.store(softdevice_enabled, std::memory_order_relaxed);
error_.store(error, std::memory_order_relaxed);
prepared_generation_.store(token, std::memory_order_relaxed);
prepared_.store(error == 0, std::memory_order_release);
return current(token);
}
void invalidate() {
generation_.fetch_add(1, std::memory_order_acq_rel);
}
bool current(uint32_t token) const {
return generation_.load(std::memory_order_acquire) == token;
}
bool snapshot(SleepCycleSnapshot& out) const {
const uint32_t token = generation_.load(std::memory_order_acquire);
if (!prepared_.load(std::memory_order_acquire)) return false;
SleepCycleSnapshot candidate;
candidate.generation = prepared_generation_.load(std::memory_order_relaxed);
candidate.usb_status = usb_status_.load(std::memory_order_relaxed);
candidate.softdevice_enabled = softdevice_enabled_.load(std::memory_order_relaxed);
if (candidate.generation != token || !current(token)
|| !prepared_.load(std::memory_order_acquire)) return false;
out = candidate;
return true;
}
bool take(SleepCycleSnapshot& out) {
if (!snapshot(out)) return false;
prepared_.store(false, std::memory_order_release);
return current(out.generation);
}
uint32_t lastError() const {
const uint32_t token = generation_.load(std::memory_order_acquire);
const uint32_t prepared_token = prepared_generation_.load(std::memory_order_relaxed);
const uint32_t error = error_.load(std::memory_order_relaxed);
return prepared_token == token && current(token) ? error : 0;
}
};
static_assert(sizeof(SleepCycleState) <= 24, "Sleep-cycle cache exceeded its RAM budget");
inline SleepCycleState& sleepCycleState() {
static SleepCycleState value;
return value;
}
} // namespace mesh_nrf52
+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',