Add bounded USB IRQ isolation experiment for nRF radio retunes
This commit is contained in:
1 parent
c83d43e03c
commit
bb9431973a
10 files changed
+627
-12
No files matched your search
@@ -818,6 +818,8 @@ jobs:
|
||||
python3 -B test/test_nrf_radio_yield_budget.py -v
|
||||
python3 -B test/test_nrf_radio_priority_lease.py -v
|
||||
python3 -B test/test_nrf_radio_priority_integration.py -v
|
||||
python3 -B test/test_nrf_radio_usb_irq_guard.py -v
|
||||
python3 -B test/test_nrf_radio_spi_startup_diag.py -v
|
||||
python3 -B test/test_sx126x_nrf_rp_constructors.py -v
|
||||
python3 -B test/test_esp32_buffered_radio_targets.py -v
|
||||
python3 -B test/test_xiao_battery_safety.py -v
|
||||
|
||||
@@ -120,7 +120,8 @@ class CustomSX1262 : public SX1262 {
|
||||
void beginProfileSwitch(bool continuousRx) { _profileSwitch.begin(continuousRx, standbyXOSC); }
|
||||
void endProfileSwitch(bool success) { _profileSwitch.end(success); }
|
||||
bool profileSwitchFailed() const { return _profileSwitch.failed(); }
|
||||
#if defined(MC_NRF_RADIO_PROFILE_PRIORITY) && MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
#if (defined(MC_NRF_RADIO_PROFILE_PRIORITY) && MC_NRF_RADIO_PROFILE_PRIORITY) \
|
||||
|| (defined(MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD) && MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD)
|
||||
bool hilOwnsWarmProfileRetune() const {
|
||||
return _profileSwitch.open && _profileSwitch.active
|
||||
&& _profileSwitch.rxValid && !_coldStandby;
|
||||
|
||||
@@ -24,7 +24,8 @@ protected:
|
||||
void endProfileRetune(bool success) override {
|
||||
static_cast<CustomSX1262*>(_radio)->endProfileSwitch(success);
|
||||
}
|
||||
#if defined(MC_NRF_RADIO_PROFILE_PRIORITY) && MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
#if (defined(MC_NRF_RADIO_PROFILE_PRIORITY) && MC_NRF_RADIO_PROFILE_PRIORITY) \
|
||||
|| (defined(MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD) && MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD)
|
||||
bool ownsWarmProfileRetune() const override {
|
||||
return static_cast<const CustomSX1262*>(_radio)->hilOwnsWarmProfileRetune();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
#include "NrfRadioPriorityLease.h"
|
||||
#endif
|
||||
#ifndef MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
#define MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD 0
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
#include "NrfRadioUsbIrqGuard.h"
|
||||
#endif
|
||||
|
||||
#ifndef MC_NRF_RADIO_BUSY_SPIN_POLLS
|
||||
#define MC_NRF_RADIO_BUSY_SPIN_POLLS 0
|
||||
@@ -27,11 +33,16 @@ public:
|
||||
explicit Nrf52BufferedRadioHal(SPIClass& bus, uint32_t hz = 16000000)
|
||||
: ArduinoHal(bus, SPISettings(hz, MSBFIRST, SPI_MODE0)) {}
|
||||
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY || MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
void spiBeginTransaction() override {
|
||||
// Check before RadioLib lowers CS, so restoring task priority does not
|
||||
// suspend this task with a newly selected, unclocked radio command.
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
mesh::radiousb::beforeFrame();
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
mesh::radiopriority::beforeFrame();
|
||||
#endif
|
||||
ArduinoHal::spiBeginTransaction();
|
||||
}
|
||||
#endif
|
||||
@@ -44,13 +55,21 @@ public:
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MC_NRF_RADIO_BUSY_SPIN_POLLS > 0 || MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
#if MC_NRF_RADIO_BUSY_SPIN_POLLS > 0 || MC_NRF_RADIO_PROFILE_PRIORITY || MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
// Diagnostic only, off by default. RadioLib calls yield() during SPI BUSY
|
||||
// polls and other waits. Spend at most N non-yielding polls after a real
|
||||
// frame, then delegate every call until another frame resets the budget.
|
||||
// An unused budget can reach a later non-BUSY wait; this is not BUSY-only
|
||||
// detection. No GPIO reads, status checks, timeouts or interrupts change.
|
||||
void yield() override {
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
if (mesh::radiousb::beforeYield()) {
|
||||
// Normal Arduino yield flushes TinyUSB CDC. While its IRQ is deferred,
|
||||
// retain task scheduling without entering that USB flush/mutex path.
|
||||
taskYIELD();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
const auto decision = mesh::radiopriority::beforeYield();
|
||||
if (decision == mesh::radiopriority::Suppress) return;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "NrfRadioUsbIrqGuard.h"
|
||||
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
namespace mesh { namespace radiousb {
|
||||
extern "C" {
|
||||
volatile Capture mesh_hil_radio_usb_irq = {
|
||||
0x4d435255, 1, sizeof(Capture), DeadlineTicks,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
}
|
||||
Work work;
|
||||
} }
|
||||
|
||||
extern "C" void RTC2_IRQHandler(void) {
|
||||
mesh::radiousb::interrupt();
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,249 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
#define MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD 0
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD != 0 && MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD != 1
|
||||
#error "Radio USB IRQ experiment must be 0 or 1"
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
#if !defined(NRF52_PLATFORM) || !defined(MESH_HIL_PROFILE_TIMING) || !MESH_HIL_PROFILE_TIMING
|
||||
#error "Radio USB IRQ experiment requires nRF52 HIL profile timing"
|
||||
#endif
|
||||
#if defined(MC_NRF_RADIO_PROFILE_PRIORITY) && MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
#error "Measure the USB IRQ and task-priority experiments separately"
|
||||
#endif
|
||||
#include <Arduino.h>
|
||||
#include <atomic>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <nrf_nvic.h>
|
||||
#include <nrf_rtc.h>
|
||||
#include "../nrf52/SoftDeviceState.h"
|
||||
#if defined(NRFX_RTC2_ENABLED) && NRFX_RTC2_ENABLED
|
||||
#error "Radio USB IRQ experiment cannot replace an nrfx RTC2 owner"
|
||||
#endif
|
||||
|
||||
namespace mesh { namespace radiousb {
|
||||
|
||||
static constexpr uint32_t DeadlineTicks = 32; // about 0.977 ms at 32768 Hz
|
||||
static constexpr uint32_t RtcMask = 0x00ffffff;
|
||||
static constexpr uint32_t TimerPriority = 2; // SDK application IRQ; no RTOS API
|
||||
|
||||
// Private independent single-word observations, not an atomic snapshot.
|
||||
struct Capture {
|
||||
uint32_t magic, version, size_bytes, deadline_ticks;
|
||||
uint32_t active, entries, releases, expired;
|
||||
uint32_t skipped_not_owned, skipped_isr, skipped_nested, skipped_task;
|
||||
uint32_t skipped_interrupts, skipped_usb, skipped_timer, skipped_priority, skipped_sdk;
|
||||
uint32_t last_sdk_result, last_sdk_enabled, last_nvic_result;
|
||||
uint32_t restore_errors, task_yields, frame_checks, usb_reenabled;
|
||||
};
|
||||
struct Work {
|
||||
TaskHandle_t owner = nullptr;
|
||||
uint32_t generation = 0, started_ticks = 0, cc0 = 0;
|
||||
uint32_t rtc_priority = 0, usb_priority = 0, inten = 0, evten = 0;
|
||||
std::atomic<uint32_t> active{0};
|
||||
std::atomic<uint32_t> usb_masked{0};
|
||||
bool softdevice_enabled = false;
|
||||
};
|
||||
extern "C" {
|
||||
extern volatile Capture mesh_hil_radio_usb_irq;
|
||||
}
|
||||
extern Work work;
|
||||
|
||||
inline bool legalAppPriority(uint32_t priority) {
|
||||
return priority < 8 && priority != 0 && priority != 1 && priority != 4;
|
||||
}
|
||||
|
||||
// The SDK app-NVIC helpers are inline, not SVCs, including in RTC2's ISR.
|
||||
// SD-off may restore the unused timer's original priority zero exactly.
|
||||
inline uint32_t setPriority(IRQn_Type irq, uint32_t priority) {
|
||||
if (work.softdevice_enabled) return sd_nvic_SetPriority(irq, priority);
|
||||
NVIC_SetPriority(irq, priority);
|
||||
return NRF_SUCCESS;
|
||||
}
|
||||
inline uint32_t enableIrq(IRQn_Type irq) {
|
||||
if (work.softdevice_enabled) return sd_nvic_EnableIRQ(irq);
|
||||
NVIC_EnableIRQ(irq);
|
||||
return NRF_SUCCESS;
|
||||
}
|
||||
inline uint32_t disableIrq(IRQn_Type irq) {
|
||||
if (work.softdevice_enabled) return sd_nvic_DisableIRQ(irq);
|
||||
NVIC_DisableIRQ(irq);
|
||||
return NRF_SUCCESS;
|
||||
}
|
||||
inline uint32_t clearTimerPending() {
|
||||
if (work.softdevice_enabled) return sd_nvic_ClearPendingIRQ(RTC2_IRQn);
|
||||
NVIC_ClearPendingIRQ(RTC2_IRQn);
|
||||
return NRF_SUCCESS;
|
||||
}
|
||||
inline void restored(uint32_t result) {
|
||||
mesh_hil_radio_usb_irq.last_nvic_result = result;
|
||||
if (result != NRF_SUCCESS) ++mesh_hil_radio_usb_irq.restore_errors;
|
||||
}
|
||||
|
||||
inline void release(bool expired = false) {
|
||||
if (!work.active.load(std::memory_order_acquire)) return;
|
||||
// Mask only our timer IRQ. Its ISR can finish first if it interrupted this
|
||||
// call before the disable; recheck before touching the saved state.
|
||||
restored(disableIrq(RTC2_IRQn));
|
||||
if (!work.active.load(std::memory_order_acquire)) return;
|
||||
if (work.usb_masked.load(std::memory_order_acquire) && NVIC_GetEnableIRQ(USBD_IRQn))
|
||||
++mesh_hil_radio_usb_irq.usb_reenabled;
|
||||
work.usb_masked.store(0, std::memory_order_release);
|
||||
work.active.store(0, std::memory_order_release);
|
||||
mesh_hil_radio_usb_irq.active = 0;
|
||||
++mesh_hil_radio_usb_irq.releases;
|
||||
if (expired) ++mesh_hil_radio_usb_irq.expired;
|
||||
nrf_rtc_int_disable(NRF_RTC2, RTC_INTENSET_COMPARE0_Msk);
|
||||
nrf_rtc_event_clear(NRF_RTC2, NRF_RTC_EVENT_COMPARE_0);
|
||||
restored(clearTimerPending());
|
||||
nrf_rtc_cc_set(NRF_RTC2, 0, work.cc0);
|
||||
// Admission requires all RTC2 interrupt/PPI event enables to be zero;
|
||||
// only COMPARE0's interrupt was changed. Counter/prescaler never change.
|
||||
restored(setPriority(RTC2_IRQn, work.rtc_priority));
|
||||
restored(setPriority(USBD_IRQn, work.usb_priority));
|
||||
// Keep every pending USB event. Re-enabling lets the normal driver drain
|
||||
// it; no USBD peripheral registers, endpoint state or buffers are changed.
|
||||
restored(enableIrq(USBD_IRQn));
|
||||
work.owner = nullptr;
|
||||
}
|
||||
|
||||
inline bool isOwner() {
|
||||
return work.active.load(std::memory_order_acquire) && !isInISR()
|
||||
&& work.owner == xTaskGetCurrentTaskHandle();
|
||||
}
|
||||
inline bool checkDeadline() {
|
||||
if (!isOwner()) return false;
|
||||
// A USB reconfiguration/power event may re-enable its IRQ independently.
|
||||
// End the experiment rather than silently count that hop as isolated.
|
||||
if (work.usb_masked.load(std::memory_order_acquire) && NVIC_GetEnableIRQ(USBD_IRQn)) {
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
if (((NRF_RTC2->COUNTER - work.started_ticks) & RtcMask) < DeadlineTicks)
|
||||
return true;
|
||||
release(true); // secondary check if an IRQ was briefly delayed
|
||||
return false;
|
||||
}
|
||||
inline void beforeFrame() {
|
||||
if (!isOwner()) return;
|
||||
++mesh_hil_radio_usb_irq.frame_checks;
|
||||
(void)checkDeadline();
|
||||
}
|
||||
inline bool beforeYield() {
|
||||
if (!checkDeadline()) return false;
|
||||
++mesh_hil_radio_usb_irq.task_yields;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint32_t enter(bool owned_warm_rx) {
|
||||
auto& capture = mesh_hil_radio_usb_irq;
|
||||
if (!owned_warm_rx) { ++capture.skipped_not_owned; return 0; }
|
||||
if (isInISR()) { ++capture.skipped_isr; return 0; }
|
||||
if (work.active.load(std::memory_order_acquire)) { ++capture.skipped_nested; return 0; }
|
||||
const TaskHandle_t owner = xTaskGetCurrentTaskHandle();
|
||||
const char* name = pcTaskGetName(owner);
|
||||
if (!name || strcmp(name, "loop") != 0 || uxTaskPriorityGet(owner) != TASK_PRIO_LOW) {
|
||||
++capture.skipped_task;
|
||||
return 0;
|
||||
}
|
||||
if (__get_PRIMASK() || __get_BASEPRI() || __get_FAULTMASK() || nrf_nvic_state.__cr_flag) {
|
||||
++capture.skipped_interrupts;
|
||||
return 0;
|
||||
}
|
||||
uint8_t enabled = 0;
|
||||
capture.last_sdk_result = mesh_nrf52::softdeviceIsEnabled(enabled);
|
||||
capture.last_sdk_enabled = enabled;
|
||||
if (capture.last_sdk_result != NRF_SUCCESS) { ++capture.skipped_sdk; return 0; }
|
||||
if (!NVIC_GetEnableIRQ(USBD_IRQn)) { ++capture.skipped_usb; return 0; }
|
||||
if (NVIC_GetEnableIRQ(RTC2_IRQn) || NVIC_GetPendingIRQ(RTC2_IRQn)
|
||||
|| NRF_RTC2->INTENSET || NRF_RTC2->EVTEN || NRF_RTC2->PRESCALER) {
|
||||
++capture.skipped_timer;
|
||||
return 0;
|
||||
}
|
||||
for (unsigned channel = 0; channel < 4; ++channel) {
|
||||
if (NRF_RTC2->EVENTS_COMPARE[channel]) { ++capture.skipped_timer; return 0; }
|
||||
}
|
||||
work.rtc_priority = NVIC_GetPriority(RTC2_IRQn);
|
||||
work.usb_priority = NVIC_GetPriority(USBD_IRQn);
|
||||
work.softdevice_enabled = enabled != 0;
|
||||
if (enabled && (!legalAppPriority(work.rtc_priority) || !legalAppPriority(work.usb_priority))) {
|
||||
++capture.skipped_priority;
|
||||
return 0;
|
||||
}
|
||||
work.owner = owner;
|
||||
work.usb_masked.store(0, std::memory_order_release);
|
||||
work.cc0 = nrf_rtc_cc_get(NRF_RTC2, 0);
|
||||
work.inten = NRF_RTC2->INTENSET;
|
||||
work.evten = NRF_RTC2->EVTEN;
|
||||
work.started_ticks = NRF_RTC2->COUNTER & RtcMask;
|
||||
if (++work.generation == 0) ++work.generation;
|
||||
const uint32_t generation = work.generation;
|
||||
capture.last_nvic_result = setPriority(RTC2_IRQn, TimerPriority);
|
||||
if (capture.last_nvic_result != NRF_SUCCESS) {
|
||||
++capture.skipped_sdk;
|
||||
work.owner = nullptr;
|
||||
return 0;
|
||||
}
|
||||
nrf_rtc_cc_set(NRF_RTC2, 0, (work.started_ticks + DeadlineTicks) & RtcMask);
|
||||
nrf_rtc_event_clear(NRF_RTC2, NRF_RTC_EVENT_COMPARE_0);
|
||||
work.active.store(1, std::memory_order_release);
|
||||
capture.active = 1;
|
||||
++capture.entries;
|
||||
nrf_rtc_int_enable(NRF_RTC2, RTC_INTENSET_COMPARE0_Msk);
|
||||
capture.last_nvic_result = enableIrq(RTC2_IRQn);
|
||||
if (capture.last_nvic_result != NRF_SUCCESS) {
|
||||
++capture.skipped_sdk;
|
||||
release();
|
||||
return 0;
|
||||
}
|
||||
if (!checkDeadline()) return 0;
|
||||
capture.last_nvic_result = disableIrq(USBD_IRQn);
|
||||
if (capture.last_nvic_result != NRF_SUCCESS) {
|
||||
++capture.skipped_sdk;
|
||||
release();
|
||||
return 0;
|
||||
}
|
||||
work.usb_masked.store(1, std::memory_order_release);
|
||||
// An interrupt can expire between arming the timer and masking USB. Never
|
||||
// return with USB disabled after the expiry ISR has already restored it.
|
||||
if (!work.active.load(std::memory_order_acquire)) {
|
||||
work.usb_masked.store(0, std::memory_order_release);
|
||||
restored(enableIrq(USBD_IRQn));
|
||||
return 0;
|
||||
}
|
||||
if (!checkDeadline()) return 0;
|
||||
return generation;
|
||||
}
|
||||
|
||||
inline void interrupt() {
|
||||
// Only our admitted COMPARE0 event is acknowledged. Existing timer owners
|
||||
// are rejected before admission; we never enable another RTC2 source.
|
||||
if (work.active.load(std::memory_order_acquire)
|
||||
&& (NRF_RTC2->INTENSET & RTC_INTENSET_COMPARE0_Msk)
|
||||
&& nrf_rtc_event_check(NRF_RTC2, NRF_RTC_EVENT_COMPARE_0)) release(true);
|
||||
}
|
||||
|
||||
// Diagnostic causal isolation only. The timer restores USB even if an nrfx
|
||||
// blocking transfer never returns. Higher-priority IRQs can delay that ISR;
|
||||
// this is not an absolute hard real-time bound, nor a production policy.
|
||||
class Guard {
|
||||
uint32_t _generation;
|
||||
public:
|
||||
explicit Guard(bool owned_warm_rx) : _generation(enter(owned_warm_rx)) {}
|
||||
Guard(const Guard&) = delete;
|
||||
Guard& operator=(const Guard&) = delete;
|
||||
~Guard() { release(); }
|
||||
void release() {
|
||||
if (!_generation) return;
|
||||
const uint32_t generation = _generation;
|
||||
_generation = 0;
|
||||
if (work.generation == generation) radiousb::release();
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(sizeof(Capture) == 96, "Unexpected USB IRQ diagnostic ABI");
|
||||
} } // namespace mesh::radiousb
|
||||
#endif
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "RadioProfileChip.h"
|
||||
#include "ProfileSwitchTiming.h"
|
||||
#include "NrfRadioPriorityLease.h"
|
||||
#include "NrfRadioUsbIrqGuard.h"
|
||||
#if MESH_HIL_PROFILE_TIMING
|
||||
namespace mesh { namespace profiletiming {
|
||||
extern "C" {
|
||||
@@ -647,12 +648,19 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
mesh::radiopriority::Lease profile_priority(dual_profile && _active_profile != profile
|
||||
&& ownsWarmProfileRetune());
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
mesh::radiousb::Guard profile_usb_irq(dual_profile && _active_profile != profile
|
||||
&& ownsWarmProfileRetune());
|
||||
#endif
|
||||
const uint8_t resume = beginReconfigure();
|
||||
if (resume > 1) {
|
||||
endProfileRetune(resume == 2); // Only a BUSY guard deferred before changing hardware.
|
||||
#if MESH_HIL_PROFILE_TIMING
|
||||
const uint32_t finished_ticks = mesh::profiletiming::readClock();
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
profile_usb_irq.release();
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
profile_priority.release();
|
||||
#endif
|
||||
@@ -706,6 +714,9 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
|
||||
// Finish before rollback: restoration is a different operation and must
|
||||
// not overwrite the failed hop's partial stage evidence.
|
||||
const uint32_t finished_ticks = mesh::profiletiming::readClock();
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
profile_usb_irq.release();
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
profile_priority.release();
|
||||
#endif
|
||||
@@ -759,6 +770,9 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
|
||||
#if MESH_HIL_PROFILE_TIMING
|
||||
mesh::profiletiming::mark(mesh::profiletiming::Tail, mesh::profiletiming::readClock());
|
||||
const uint32_t finished_ticks = mesh::profiletiming::readClock();
|
||||
#if MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD
|
||||
profile_usb_irq.release();
|
||||
#endif
|
||||
#if MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
profile_priority.release();
|
||||
#endif
|
||||
|
||||
@@ -387,7 +387,8 @@ protected:
|
||||
virtual void setProfileStandbyWarm(bool) {}
|
||||
virtual void beginProfileRetune(bool) {}
|
||||
virtual void endProfileRetune(bool) {}
|
||||
#if defined(MC_NRF_RADIO_PROFILE_PRIORITY) && MC_NRF_RADIO_PROFILE_PRIORITY
|
||||
#if (defined(MC_NRF_RADIO_PROFILE_PRIORITY) && MC_NRF_RADIO_PROFILE_PRIORITY) \
|
||||
|| (defined(MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD) && MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD)
|
||||
virtual bool ownsWarmProfileRetune() const { return false; }
|
||||
#endif
|
||||
// 0 = reconfigure from idle, 1 = resume RX afterwards, 2 = currently busy.
|
||||
|
||||
@@ -14,6 +14,12 @@ class NrfRadioPriorityIntegrationTests(unittest.TestCase):
|
||||
compile_run=timing.ProfileSwitchTimingTest.compile_run
|
||||
|
||||
def test_real_transaction_includes_entry_cost_and_releases_before_stats_or_rollback(self):
|
||||
self.exercise_real_transaction('MC_NRF_RADIO_PROFILE_PRIORITY', 'radiopriority', 'Lease')
|
||||
|
||||
def test_usb_irq_guard_uses_same_measured_endpoint_and_failure_release_contract(self):
|
||||
self.exercise_real_transaction('MC_NRF_RADIO_PROFILE_USB_IRQ_GUARD', 'radiousb', 'Guard')
|
||||
|
||||
def exercise_real_transaction(self, flag, namespace, guard_type):
|
||||
source=(ROOT/"src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
|
||||
methods="\n".join(method(source,signature) for signature in (
|
||||
"uint8_t RadioLibWrapper::beginReconfigure(",
|
||||
@@ -38,11 +44,11 @@ static uint32_t sequence_at_start=0;
|
||||
lease=r'''
|
||||
#include <helpers/radiolib/ProfileSwitchTiming.h>
|
||||
@GLOBALS@
|
||||
namespace mesh { namespace radiopriority {
|
||||
class Lease {
|
||||
namespace mesh { namespace @NAMESPACE@ {
|
||||
class @GUARD_TYPE@ {
|
||||
bool admitted=false;
|
||||
public:
|
||||
explicit Lease(bool owned) {
|
||||
explicit @GUARD_TYPE@(bool owned) {
|
||||
assert(owned==expected_owned);
|
||||
if(!owned) return;
|
||||
assert(!priority_active);
|
||||
@@ -57,14 +63,14 @@ class Lease {
|
||||
admitted=priority_active=false;++releases;
|
||||
elapsed_us+=1000; // Model immediate USB scheduling during restoration.
|
||||
}
|
||||
~Lease() { release(); }
|
||||
~@GUARD_TYPE@() { release(); }
|
||||
};
|
||||
} }
|
||||
'''.replace("@GLOBALS@",globals_source)
|
||||
'''.replace("@GLOBALS@",globals_source).replace('@NAMESPACE@',namespace).replace('@GUARD_TYPE@',guard_type)
|
||||
fixture=fixture.replace("@METHODS@",lease+methods)
|
||||
fixture=r'''#define MESH_HIL_PROFILE_TIMING 1
|
||||
#define MC_NRF_RADIO_PROFILE_PRIORITY 1
|
||||
'''+fixture+r'''
|
||||
#define @FLAG@ 1
|
||||
'''.replace('@FLAG@',flag)+fixture+r'''
|
||||
int main() {
|
||||
using Result=mesh::RadioParamApplyResult;
|
||||
for(unsigned scenario=0;scenario<8;++scenario) {
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
"""Execute the HIL-only USB IRQ guard, including its independent deadline ISR."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_rak3401_buffered_radio_hal import MOCK_HAL
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / 'src/helpers/radiolib/NrfRadioUsbIrqGuard.cpp'
|
||||
|
||||
ARDUINO = r'''
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
struct TaskMock { unsigned priority; const char* name; };
|
||||
using TaskHandle_t=TaskMock*;
|
||||
enum { TASK_PRIO_LOW=1 };
|
||||
enum IRQn_Type { GPIOTE_IRQn=6,RTC2_IRQn=36,USBD_IRQn=39 };
|
||||
struct RtcMock {
|
||||
uint32_t COUNTER=0,PRESCALER=0,INTENSET=0,EVTEN=0;
|
||||
uint32_t EVENTS_COMPARE[4]{},CC[4]{};
|
||||
};
|
||||
extern RtcMock rtc;
|
||||
#define NRF_RTC2 (&rtc)
|
||||
struct NvicState { unsigned __cr_flag=0; };
|
||||
extern NvicState nrf_nvic_state;
|
||||
extern TaskMock loopTask,otherTask;
|
||||
extern TaskHandle_t currentTask;
|
||||
extern unsigned primask,basepri,faultmask;
|
||||
extern bool inIsr,irqEnabled[64],irqPending[64],fireBeforeUsbMask;
|
||||
extern unsigned irqPriority[64],nvicChanges,sdkCalls,sdkFailures,taskYields;
|
||||
extern int failOperation;
|
||||
extern bool failTransfer;
|
||||
extern "C" void RTC2_IRQHandler();
|
||||
void expire();
|
||||
inline bool isInISR() { return inIsr; }
|
||||
inline TaskHandle_t xTaskGetCurrentTaskHandle() { return currentTask; }
|
||||
inline const char* pcTaskGetName(TaskHandle_t task) { return task->name; }
|
||||
inline unsigned uxTaskPriorityGet(TaskHandle_t task) { return task->priority; }
|
||||
inline unsigned __get_PRIMASK() { return primask; }
|
||||
inline unsigned __get_BASEPRI() { return basepri; }
|
||||
inline unsigned __get_FAULTMASK() { return faultmask; }
|
||||
inline void taskYIELD() { ++taskYields; }
|
||||
inline bool NVIC_GetEnableIRQ(IRQn_Type irq) { return irqEnabled[irq]; }
|
||||
inline bool NVIC_GetPendingIRQ(IRQn_Type irq) { return irqPending[irq]; }
|
||||
inline unsigned NVIC_GetPriority(IRQn_Type irq) { return irqPriority[irq]; }
|
||||
inline void permitted(IRQn_Type irq) {
|
||||
assert(irq==USBD_IRQn || irq==RTC2_IRQn);++nvicChanges;
|
||||
}
|
||||
inline void NVIC_SetPriority(IRQn_Type irq,unsigned priority) {
|
||||
permitted(irq);irqPriority[irq]=priority;
|
||||
}
|
||||
inline void NVIC_EnableIRQ(IRQn_Type irq) { permitted(irq);irqEnabled[irq]=true; }
|
||||
inline void NVIC_DisableIRQ(IRQn_Type irq) {
|
||||
permitted(irq);
|
||||
if(irq==USBD_IRQn && fireBeforeUsbMask) { fireBeforeUsbMask=false;expire(); }
|
||||
irqEnabled[irq]=false;
|
||||
}
|
||||
inline void NVIC_ClearPendingIRQ(IRQn_Type irq) { permitted(irq);irqPending[irq]=false; }
|
||||
'''
|
||||
|
||||
SDM = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
constexpr unsigned NRF_SUCCESS=0;
|
||||
extern unsigned sdkQueryResult,sdkQueryEnabled,sdkQueries;
|
||||
inline unsigned sd_softdevice_is_enabled(uint8_t* enabled) {
|
||||
++sdkQueries;*enabled=sdkQueryEnabled;return sdkQueryResult;
|
||||
}
|
||||
'''
|
||||
|
||||
NVIC = r'''
|
||||
#pragma once
|
||||
#include <nrf_sdm.h>
|
||||
inline bool sdkFail(int operation) {
|
||||
++sdkCalls;
|
||||
if(operation==failOperation) { failOperation=0;++sdkFailures;return true; }
|
||||
return false;
|
||||
}
|
||||
inline bool legal(unsigned priority) { return priority<8 && priority!=0 && priority!=1 && priority!=4; }
|
||||
inline unsigned sd_nvic_SetPriority(IRQn_Type irq,unsigned priority) {
|
||||
if(sdkFail(1)) return 91;
|
||||
assert(legal(priority));NVIC_SetPriority(irq,priority);return 0;
|
||||
}
|
||||
inline unsigned sd_nvic_EnableIRQ(IRQn_Type irq) {
|
||||
if(sdkFail(2)) return 92;
|
||||
assert(legal(irqPriority[irq]));NVIC_EnableIRQ(irq);return 0;
|
||||
}
|
||||
inline unsigned sd_nvic_DisableIRQ(IRQn_Type irq) {
|
||||
if(sdkFail(3)) return 93;
|
||||
NVIC_DisableIRQ(irq);return 0;
|
||||
}
|
||||
inline unsigned sd_nvic_ClearPendingIRQ(IRQn_Type irq) {
|
||||
if(sdkFail(4)) return 94;
|
||||
NVIC_ClearPendingIRQ(irq);return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
RTC = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
constexpr unsigned RTC_INTENSET_COMPARE0_Msk=1u<<16;
|
||||
enum nrf_rtc_event_t { NRF_RTC_EVENT_COMPARE_0=0 };
|
||||
inline void nrf_rtc_int_enable(RtcMock* p,unsigned mask) { p->INTENSET|=mask; }
|
||||
inline void nrf_rtc_int_disable(RtcMock* p,unsigned mask) { p->INTENSET&=~mask; }
|
||||
inline void nrf_rtc_event_clear(RtcMock* p,nrf_rtc_event_t) { p->EVENTS_COMPARE[0]=0; }
|
||||
inline bool nrf_rtc_event_check(RtcMock* p,nrf_rtc_event_t) { return p->EVENTS_COMPARE[0]!=0; }
|
||||
inline void nrf_rtc_cc_set(RtcMock* p,unsigned ch,unsigned value) { p->CC[ch]=value; }
|
||||
inline unsigned nrf_rtc_cc_get(RtcMock* p,unsigned ch) { return p->CC[ch]; }
|
||||
'''
|
||||
|
||||
MOCK = MOCK_HAL.replace('#include <cassert>', '#include <Arduino.h>\n#include <cassert>')
|
||||
MOCK = MOCK.replace(' ++bursts;', '''
|
||||
if(failTransfer) { expire();assert(irqEnabled[USBD_IRQn]); }
|
||||
++bursts;''')
|
||||
MOCK = MOCK.replace('virtual ~ArduinoHal() = default;', r'''
|
||||
unsigned yields=0,transactions=0;
|
||||
bool transactionUsb=false;
|
||||
virtual ~ArduinoHal() = default;
|
||||
virtual void yield() { assert(irqEnabled[USBD_IRQn]);++yields; }
|
||||
virtual void spiBeginTransaction() { ++transactions;transactionUsb=irqEnabled[USBD_IRQn]; }
|
||||
''')
|
||||
|
||||
FIXTURE = r'''
|
||||
#include <initializer_list>
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
RtcMock rtc;
|
||||
TaskMock loopTask{1,"loop"},otherTask{3,"usbd"};
|
||||
TaskHandle_t currentTask=&loopTask;
|
||||
NvicState nrf_nvic_state;
|
||||
unsigned primask=0,basepri=0,faultmask=0;
|
||||
bool inIsr=false,irqEnabled[64]{},irqPending[64]{},fireBeforeUsbMask=false;
|
||||
unsigned irqPriority[64]{},nvicChanges=0,sdkCalls=0,sdkFailures=0,taskYields=0;
|
||||
unsigned sdkQueryResult=0,sdkQueryEnabled=0,sdkQueries=0;
|
||||
int failOperation=0;
|
||||
bool failTransfer=false;
|
||||
void expire() {
|
||||
rtc.COUNTER=rtc.CC[0];rtc.EVENTS_COMPARE[0]=1;irqPending[RTC2_IRQn]=true;
|
||||
assert(irqEnabled[RTC2_IRQn]);
|
||||
const bool was=inIsr;inIsr=true;RTC2_IRQHandler();inIsr=was;
|
||||
}
|
||||
void resetHardware(bool sd=false) {
|
||||
using namespace mesh::radiousb;
|
||||
assert(!work.active.load());
|
||||
rtc=RtcMock{};rtc.COUNTER=100;rtc.CC[0]=9876;
|
||||
primask=basepri=faultmask=0;inIsr=false;nrf_nvic_state.__cr_flag=0;
|
||||
currentTask=&loopTask;loopTask.priority=1;
|
||||
sdkQueryResult=0;sdkQueryEnabled=sd;sdkQueries=0;failOperation=0;
|
||||
for(unsigned i=0;i<64;++i) { irqEnabled[i]=false;irqPending[i]=false;irqPriority[i]=0; }
|
||||
irqEnabled[USBD_IRQn]=true;irqPriority[USBD_IRQn]=6;
|
||||
irqEnabled[GPIOTE_IRQn]=true;irqPriority[GPIOTE_IRQn]=6;
|
||||
if(sd) irqPriority[RTC2_IRQn]=7;
|
||||
nvicChanges=sdkCalls=sdkFailures=taskYields=0;
|
||||
}
|
||||
void restoredHardware(bool sd=false) {
|
||||
using namespace mesh::radiousb;
|
||||
assert(!work.active.load() && mesh_hil_radio_usb_irq.active==0);
|
||||
assert(irqEnabled[USBD_IRQn] && !irqEnabled[RTC2_IRQn]);
|
||||
assert(irqPriority[RTC2_IRQn]==unsigned(sd?7:0) && irqPriority[USBD_IRQn]==6);
|
||||
assert(!irqPending[RTC2_IRQn] && rtc.INTENSET==0 && rtc.EVTEN==0 && rtc.CC[0]==9876);
|
||||
assert(rtc.PRESCALER==0 && rtc.EVENTS_COMPARE[0]==0);
|
||||
assert(irqEnabled[GPIOTE_IRQn] && irqPriority[GPIOTE_IRQn]==6);
|
||||
assert(primask==0 && basepri==0 && faultmask==0);
|
||||
}
|
||||
int earlyFailure() { mesh::radiousb::Guard guard(true);assert(!irqEnabled[USBD_IRQn]);return -1; }
|
||||
int main() {
|
||||
using namespace mesh::radiousb;
|
||||
auto& capture=mesh_hil_radio_usb_irq;
|
||||
assert(capture.magic==0x4d435255 && capture.version==1 && capture.size_bytes==96);
|
||||
SPIClass bus;Nrf52BufferedRadioHal concrete(bus,8000000);ArduinoHal& hal=concrete;
|
||||
// All admission exclusions make no NVIC writes and preserve other owners.
|
||||
for(unsigned gate=0;gate<18;++gate) {
|
||||
resetHardware();bool owned=true;
|
||||
switch(gate) {
|
||||
case 0: owned=false;break;
|
||||
case 1: inIsr=true;break;
|
||||
case 2: currentTask=&otherTask;break;
|
||||
case 3: loopTask.priority=3;break;
|
||||
case 4: primask=1;break;
|
||||
case 5: basepri=1;break;
|
||||
case 6: faultmask=1;break;
|
||||
case 7: nrf_nvic_state.__cr_flag=1;break;
|
||||
case 8: irqEnabled[USBD_IRQn]=false;break;
|
||||
case 9: irqEnabled[RTC2_IRQn]=true;break;
|
||||
case 10: irqPending[RTC2_IRQn]=true;break;
|
||||
case 11: rtc.INTENSET=1;break;
|
||||
case 12: rtc.EVTEN=1u<<17;break;
|
||||
case 13: rtc.EVENTS_COMPARE[1]=1;break;
|
||||
case 14: rtc.PRESCALER=31;break;
|
||||
case 15: sdkQueryResult=123;break;
|
||||
case 16: sdkQueryEnabled=1;irqPriority[RTC2_IRQn]=0;break;
|
||||
case 17: sdkQueryEnabled=1;irqPriority[USBD_IRQn]=4;break;
|
||||
}
|
||||
{ Guard guard(owned);assert(!work.active.load()); }
|
||||
assert(nvicChanges==0);
|
||||
}
|
||||
resetHardware();
|
||||
irqPending[USBD_IRQn]=true;
|
||||
{ Guard outer(true);
|
||||
assert(work.active.load() && !irqEnabled[USBD_IRQn] && irqEnabled[RTC2_IRQn]);
|
||||
assert(irqPending[USBD_IRQn] && irqPriority[RTC2_IRQn]==2 && rtc.CC[0]==132);
|
||||
{ Guard inner(true); }assert(work.active.load() && !irqEnabled[USBD_IRQn]);
|
||||
const unsigned before=hal.yields;
|
||||
hal.yield();assert(hal.yields==before && taskYields==1);
|
||||
hal.spiBeginTransaction();assert(!hal.transactionUsb);
|
||||
outer.release();outer.release();
|
||||
assert(irqPending[USBD_IRQn]);
|
||||
}
|
||||
restoredHardware();assert(sdkCalls==0 && sdkQueries==1);
|
||||
// The timer ISR restores USB without another HAL poll/frame returning.
|
||||
resetHardware();
|
||||
{ Guard guard(true);expire();restoredHardware(); }
|
||||
restoredHardware();
|
||||
// Real buffered transfer can trigger the simulated deadline independently.
|
||||
resetHardware();failTransfer=true;
|
||||
{ Guard guard(true);uint8_t tx[3]={0x86,1,2},rx[3]{};hal.spiTransfer(tx,3,rx);
|
||||
assert(rx[0]==uint8_t(tx[0]^0xa5) && bus.bursts==1);
|
||||
}
|
||||
failTransfer=false;restoredHardware();
|
||||
// Cooperative checks also restore before CS or normal TinyUSB flush.
|
||||
for(bool frame:{false,true}) {
|
||||
resetHardware();rtc.COUNTER=0xfffff0;
|
||||
{ Guard guard(true);assert(rtc.CC[0]==0x10);rtc.COUNTER=0x10;
|
||||
if(frame) { hal.spiBeginTransaction();assert(hal.transactionUsb); }
|
||||
else { const unsigned before=hal.yields;hal.yield();assert(hal.yields==before+1); }
|
||||
}
|
||||
restoredHardware();
|
||||
}
|
||||
// Expiry racing immediately before USB masking cannot leave USB disabled.
|
||||
resetHardware();fireBeforeUsbMask=true;
|
||||
{ Guard guard(true);restoredHardware(); }restoredHardware();
|
||||
// An unrelated/spurious timer IRQ does not clear another source/event.
|
||||
resetHardware();rtc.EVENTS_COMPARE[2]=1;RTC2_IRQHandler();
|
||||
assert(rtc.EVENTS_COMPARE[2]==1 && nvicChanges==0);
|
||||
resetHardware();assert(earlyFailure()==-1);restoredHardware();
|
||||
// A separate USB owner cannot silently invalidate the isolation experiment.
|
||||
resetHardware();
|
||||
{ Guard guard(true);const unsigned before=capture.usb_reenabled;
|
||||
irqEnabled[USBD_IRQn]=true;hal.yield();
|
||||
assert(capture.usb_reenabled==before+1);restoredHardware();
|
||||
}
|
||||
// SD-on path uses checked inline app-NVIC helpers, including deadline ISR.
|
||||
resetHardware(true);
|
||||
{ Guard guard(true);assert(capture.last_sdk_enabled==1 && capture.last_sdk_result==0);
|
||||
expire();restoredHardware(true); }
|
||||
assert(sdkCalls>0 && sdkFailures==0 && sdkQueries==1);
|
||||
// Fail each admission API before USB can remain masked; restore exact state.
|
||||
for(int operation:{1,2,3}) {
|
||||
resetHardware(true);failOperation=operation;
|
||||
{ Guard guard(true); }restoredHardware(true);
|
||||
assert(sdkFailures==1);
|
||||
}
|
||||
resetHardware();
|
||||
const unsigned before=hal.yields;
|
||||
for(unsigned i=0;i<1000;++i) hal.yield();
|
||||
assert(hal.yields==before+1000 && taskYields==0 && nvicChanges==0);
|
||||
assert(capture.entries==capture.releases && capture.active==0 && capture.expired>=5);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class NrfRadioUsbIrqGuardTests(unittest.TestCase):
|
||||
def test_real_guard_hal_and_deadline_isr_preserve_irq_ownership(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
folder=Path(directory)
|
||||
(folder/'hal/Arduino').mkdir(parents=True)
|
||||
for name,content in (('Arduino.h',ARDUINO),('nrf_sdm.h',SDM),
|
||||
('nrf_nvic.h',NVIC),('nrf_rtc.h',RTC),
|
||||
('hal/Arduino/ArduinoHal.h',MOCK),('test.cpp',FIXTURE)):
|
||||
(folder/name).write_text(content,encoding='ascii')
|
||||
binary=folder/'test.exe'
|
||||
built=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall',
|
||||
'-Wextra','-Werror','-DNRF52_PLATFORM','-DMESH_HIL_PROFILE_TIMING=1',
|
||||
'-DMC_NRF_RADIO_PROFILE_USB_IRQ_GUARD=1','-I',str(folder),'-I',str(ROOT/'src'),
|
||||
str(folder/'test.cpp'),str(SOURCE),'-o',str(binary)],capture_output=True,text=True,
|
||||
env={**os.environ,'TMPDIR':str(folder)})
|
||||
self.assertEqual(built.returncode,0,built.stderr)
|
||||
ran=subprocess.run([str(binary)],capture_output=True,text=True)
|
||||
self.assertEqual(ran.returncode,0,ran.stderr)
|
||||
|
||||
def test_default_off_has_no_code_types_ram_or_sdk_dependencies(self):
|
||||
for source in ('#include <helpers/radiolib/NrfRadioUsbIrqGuard.h>\n',SOURCE.read_text()):
|
||||
result=subprocess.run([os.environ.get('CXX','g++'),'-E','-P','-x','c++',
|
||||
'-I',str(ROOT/'src'),'-I',str(SOURCE.parent),'-'],input=source,
|
||||
capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
self.assertFalse(result.stdout.strip())
|
||||
|
||||
def test_experiment_requires_hil_nrf_and_separate_priority_measurement(self):
|
||||
for flags,diagnostic in (([], 'requires nRF52 HIL profile timing'),
|
||||
(['-DNRF52_PLATFORM'],'requires nRF52 HIL profile timing'),
|
||||
(['-DMESH_HIL_PROFILE_TIMING=1'],'requires nRF52 HIL profile timing'),
|
||||
(['-DNRF52_PLATFORM','-DMESH_HIL_PROFILE_TIMING=1',
|
||||
'-DMC_NRF_RADIO_PROFILE_PRIORITY=1'],'experiments separately')):
|
||||
result=subprocess.run([os.environ.get('CXX','g++'),'-E','-x','c++',
|
||||
'-DMC_NRF_RADIO_PROFILE_USB_IRQ_GUARD=1',*flags,'-I',str(ROOT/'src'),'-'],
|
||||
input='#include <helpers/radiolib/NrfRadioUsbIrqGuard.h>\n',
|
||||
capture_output=True,text=True)
|
||||
self.assertNotEqual(result.returncode,0)
|
||||
self.assertIn(diagnostic,result.stderr)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user