Files
MeshCore/test/fixtures/nrf52_sleep_events.cpp
T

411 lines
14 KiB
C++

// 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);
}