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
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
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