Fix nRF52 idle sleep event and CryptoCell handling

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

No files matched your search

+92 -8
View File
@@ -4,6 +4,7 @@
#include "NRF52VoltagePolicy.h"
#include "Nrf52BootloaderVersion.h"
#include "nrf52/SoftDeviceState.h"
#include "nrf52/SleepCycleState.h"
#include "nrf52/ShutdownLeds.h"
#include <target.h>
#include "UserGpioPinPolicy.h"
@@ -13,6 +14,7 @@
#include "ble_gap.h"
#include "ble_hci.h"
#include <nrf.h>
#include <nrf_nvic.h>
#include <nrf_soc.h>
#ifdef USE_TINYUSB
#include <Adafruit_TinyUSB.h>
@@ -464,19 +466,88 @@ void NRF52BoardDCDC::begin() {
}
bool NRF52Board::isExternalPowered() {
// Check if SoftDevice is enabled before using its API
mesh_nrf52::SleepCycleSnapshot cycle;
if (mesh_nrf52::sleepCycleState().snapshot(cycle))
return (cycle.usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
// Setup and callers outside a prepared loop retain the SDK access policy.
// Never reuse a cached value after a SoftDevice transition invalidated it.
uint8_t sd_enabled = 0;
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
if (sd_enabled) {
uint32_t usb_status;
sd_power_usbregstatus_get(&usb_status);
volatile uint32_t status = 0;
const uint32_t error = sd_power_usbregstatus_get((uint32_t*)&status);
__asm volatile ("" ::: "memory");
if (error != NRF_SUCCESS) return false;
const uint32_t usb_status = status;
return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
} else {
return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
}
}
void NRF52Board::prepareSleepCycle() {
auto& state = mesh_nrf52::sleepCycleState();
const uint32_t token = state.begin();
const auto application_context = []() {
return __get_IPSR() == 0 && __get_PRIMASK() == 0
&& __get_BASEPRI() == 0 && __get_FAULTMASK() == 0
// SDK critical regions can mask application IRQs in the NVIC even
// while the CPU mask registers are clear. This public field is
// volatile: read it afresh before queries and after the forced yield.
&& nrf_nvic_state.__cr_flag == 0
&& xTaskGetSchedulerState() == taskSCHEDULER_RUNNING;
};
if (!application_context()) {
state.publish(token, false, 0, NRF_ERROR_INVALID_STATE);
return;
}
#ifdef NRF_CRYPTOCELL
// The pinned Adafruit CC310 library polls (no-interrupts archive), but can
// leave its disabled NVIC IRQ pending. The SDK treats that as application
// work forever. Match the core's waitForEvent cleanup only at loop entry;
// never clear an enabled IRQ belonging to an interrupt-driven owner.
if (NVIC_GetEnableIRQ(CRYPTOCELL_IRQn) == 0)
NVIC_ClearPendingIRQ(CRYPTOCELL_IRQn);
#endif
uint8_t sd_enabled = 0;
uint32_t error = mesh_nrf52::softdeviceIsEnabled(sd_enabled);
uint32_t usb_status = 0;
if (error == NRF_SUCCESS) {
if (sd_enabled) {
volatile uint32_t status = 0;
error = sd_power_usbregstatus_get((uint32_t*)&status);
__asm volatile ("" ::: "memory");
usb_status = status;
} else {
usb_status = NRF_POWER->USBREGSTATUS;
}
}
if (error == NRF_SUCCESS && sd_enabled && state.current(token)) {
// Raw SEV/WFE does not acknowledge the SDK's separate application-IRQ
// bookkeeping. First force a real application PendSV while the loop task
// remains ready (so the RTOS idle task cannot consume that acknowledgement).
// The public SDK contract then makes this one early wait non-blocking.
// The pinned S140 6.1.1/7.3.0 forwarding/wait code confirms its private IRQ
// sequence cannot wrap back to the last acknowledged value. Do not access
// those SDK-private counters or rely on SEV alone to release an SDK wait.
taskYIELD();
if (!application_context() || (SCB->ICSR & SCB_ICSR_PENDSVSET_Msk)) {
error = NRF_ERROR_INVALID_STATE;
} else if (state.current(token)) {
error = sd_app_evt_wait();
}
}
state.publish(token, sd_enabled != 0, usb_status, error);
// FreeRTOS yields and the state-query SVC can both leave a stale event.
// Clear it ONLY at loop entry, before servicing/checking application work.
// A radio interrupt after this point must remain visible to the late wait.
__SEV();
__WFE();
}
bool NRF52Board::isUsbDataConnected() {
#if defined(USE_TINYUSB)
#if defined(CFG_TUD_CDC) && CFG_TUD_CDC
@@ -498,12 +569,25 @@ bool NRF52Board::isUsbHostConnected() {
}
void NRF52Board::sleep(uint32_t secs) {
(void)secs;
const bool diagnostics = mesh::powerdiag::enabled();
auto& state = mesh_nrf52::sleepCycleState();
mesh_nrf52::SleepCycleSnapshot cycle;
const bool prepared = state.take(cycle);
if (isRadioTestActive()) {
if (diagnostics) mesh::powerdiag::boardVeto(mesh::powerdiag::Reason::Board, millis(), 2);
delay(1);
return;
}
if (!prepared || !state.current(cycle.generation)) {
if (diagnostics) {
const uint32_t error = state.lastError();
if (error != NRF_SUCCESS) mesh::powerdiag::boardError(static_cast<int32_t>(error), millis());
else mesh::powerdiag::boardVeto(mesh::powerdiag::Reason::Board, millis(), 3);
}
delay(1);
return;
}
// Clear FPU interrupt flags to avoid insomnia
// see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html
#if (__FPU_USED == 1)
@@ -514,10 +598,7 @@ void NRF52Board::sleep(uint32_t secs) {
// On nRF52, we use event-driven sleep instead of timed sleep
// The 'secs' parameter is ignored - we wake on any interrupt
uint8_t sd_enabled = 0;
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
if (cycle.softdevice_enabled) {
// A single call is required here. If an interrupt arrived since the last
// wait, SoftDevice returns immediately so the main loop can service the
// flag or BLE queue before sleeping again.
@@ -708,6 +789,9 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[], bool force_ap) {
Bluefruit.configPrphConn(BLE_GATT_ATT_MTU_DEFAULT,
BLE_GAP_EVENT_LENGTH_MIN, 2, 1);
// Even a failed begin can leave SoftDevice enabled partway through SDK
// startup. Require a fresh authoritative query before the next sleep.
mesh_nrf52::sleepCycleState().invalidate();
if (!Bluefruit.begin(1, 0)) {
return false;
}
+3
View File
@@ -73,6 +73,9 @@ public:
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
virtual bool startOTAUpdate(const char *id, char reply[], bool force_ap = false) override;
virtual bool stopOTAUpdate(char reply[]) override;
// Must precede all work servicing/checks in each application loop. The
// authoritative SDK queries and stale-event clear are never done at sleep.
void prepareSleepCycle();
virtual void sleep(uint32_t secs) override;
bool isUserGpioAvailable(uint8_t pin) const override;
bool isExternalPowered() override;
+21 -1
View File
@@ -736,7 +736,27 @@ static size_t writeTinyUsbCdcOnce(void* context, const uint8_t* data,
return tud_cdc_n_write(instance, data, attempt);
}
static SingleAttemptNonBlockingStream nonblocking_primary_usb_companion_port(
#if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)
// Adafruit's empty CDC available() calls yield(), which sets the Cortex-M
// event register through FreeRTOS. Polling an idle console must not recreate
// that scheduler event after the loop has prepared its sleep decision.
// Only CDC0 is the primary protocol/console; CDC1 keeps its logging owner.
class Nrf52PrimaryUsbStream : public SingleAttemptNonBlockingStream {
public:
using SingleAttemptNonBlockingStream::SingleAttemptNonBlockingStream;
int available() override {
if (!canAccessPrimaryUsbSession(nullptr)) return 0;
const int count = static_cast<int>(tud_cdc_n_available(0));
// A USB owner callback may have closed the epoch during the FIFO query.
return canAccessPrimaryUsbSession(nullptr) ? count : 0;
}
};
#else
using Nrf52PrimaryUsbStream = SingleAttemptNonBlockingStream;
#endif
static Nrf52PrimaryUsbStream nonblocking_primary_usb_companion_port(
Serial, writeTinyUsbCdcOnce, reinterpret_cast<void*>(uintptr_t{0})
#if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)
, canAccessPrimaryUsbSession
+4
View File
@@ -2,6 +2,7 @@
#include "../BluetoothMac.h"
#include "../CompanionFrameQueue.h"
#include "SoftDeviceState.h"
#include "SleepCycleState.h"
#include "BleDebugTrace.h"
#include <stdio.h>
#include <string.h>
@@ -712,6 +713,9 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
// task observation incorrectly reports that the BLE/SOC workers never
// started. Bluefruit.begin() is the reliable startup result available to
// this application.
// A partial/failed begin must not leave the current loop's cached
// SoftDevice state eligible for bare-metal sleep.
mesh_nrf52::sleepCycleState().invalidate();
if (!Bluefruit.begin()) {
uint8_t softdevice_enabled = 0;
const uint32_t softdevice_status =
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include <atomic>
#include <stdint.h>
namespace mesh_nrf52 {
struct SleepCycleSnapshot {
uint32_t generation = 0;
uint32_t usb_status = 0;
bool softdevice_enabled = false;
};
// The application task publishes once at loop entry, before checking work.
// A SoftDevice startup attempt can invalidate from another task without
// racing the publication. No SDK state is inferred from NVIC/vector state.
class SleepCycleState {
std::atomic<uint32_t> generation_{0};
std::atomic<uint32_t> prepared_generation_{0};
std::atomic<uint32_t> usb_status_{0};
std::atomic<uint32_t> error_{0};
std::atomic<bool> softdevice_enabled_{false};
std::atomic<bool> prepared_{false};
public:
uint32_t begin() {
const uint32_t token = generation_.fetch_add(1, std::memory_order_acq_rel) + 1;
prepared_.store(false, std::memory_order_release);
return token;
}
bool publish(uint32_t token, bool softdevice_enabled, uint32_t usb_status,
uint32_t error = 0) {
if (!current(token)) return false;
usb_status_.store(usb_status, std::memory_order_relaxed);
softdevice_enabled_.store(softdevice_enabled, std::memory_order_relaxed);
error_.store(error, std::memory_order_relaxed);
prepared_generation_.store(token, std::memory_order_relaxed);
prepared_.store(error == 0, std::memory_order_release);
return current(token);
}
void invalidate() {
generation_.fetch_add(1, std::memory_order_acq_rel);
}
bool current(uint32_t token) const {
return generation_.load(std::memory_order_acquire) == token;
}
bool snapshot(SleepCycleSnapshot& out) const {
const uint32_t token = generation_.load(std::memory_order_acquire);
if (!prepared_.load(std::memory_order_acquire)) return false;
SleepCycleSnapshot candidate;
candidate.generation = prepared_generation_.load(std::memory_order_relaxed);
candidate.usb_status = usb_status_.load(std::memory_order_relaxed);
candidate.softdevice_enabled = softdevice_enabled_.load(std::memory_order_relaxed);
if (candidate.generation != token || !current(token)
|| !prepared_.load(std::memory_order_acquire)) return false;
out = candidate;
return true;
}
bool take(SleepCycleSnapshot& out) {
if (!snapshot(out)) return false;
prepared_.store(false, std::memory_order_release);
return current(out.generation);
}
uint32_t lastError() const {
const uint32_t token = generation_.load(std::memory_order_acquire);
const uint32_t prepared_token = prepared_generation_.load(std::memory_order_relaxed);
const uint32_t error = error_.load(std::memory_order_relaxed);
return prepared_token == token && current(token) ? error : 0;
}
};
static_assert(sizeof(SleepCycleState) <= 24, "Sleep-cycle cache exceeded its RAM budget");
inline SleepCycleState& sleepCycleState() {
static SleepCycleState value;
return value;
}
} // namespace mesh_nrf52