Add bounded nRF radio yield experiment and correlated worst-hop timings
This commit is contained in:
1 parent
2629c15804
commit
47e9dc4b14
5 files changed
+159
-5
No files matched your search
@@ -815,6 +815,7 @@ jobs:
|
||||
python3 -B test/test_rak3401_buffered_radio_hal.py -v
|
||||
python3 -B test/test_sx126x_buffered_radio_hal.py -v
|
||||
python3 -B test/test_profile_switch_timing.py -v
|
||||
python3 -B test/test_nrf_radio_yield_budget.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
|
||||
|
||||
@@ -2,15 +2,44 @@
|
||||
|
||||
#include <hal/Arduino/ArduinoHal.h>
|
||||
|
||||
#ifndef MC_NRF_RADIO_BUSY_SPIN_POLLS
|
||||
#define MC_NRF_RADIO_BUSY_SPIN_POLLS 0
|
||||
#endif
|
||||
#if MC_NRF_RADIO_BUSY_SPIN_POLLS < 0 || MC_NRF_RADIO_BUSY_SPIN_POLLS > 16
|
||||
#error "nRF radio yield experiment requires a bounded 0..16 poll budget"
|
||||
#endif
|
||||
|
||||
// nRF52840 SPIClass::transfer(uint8_t) starts one SPIM transaction per byte.
|
||||
// RadioLib sends whole command buffers; keep its CS, BUSY, and status handling
|
||||
// but transfer each buffer in one SPIM transaction. LR1110 supports 16 MHz SPI.
|
||||
class Nrf52BufferedRadioHal : public ArduinoHal {
|
||||
#if MC_NRF_RADIO_BUSY_SPIN_POLLS > 0
|
||||
uint8_t _busy_spin_polls_remaining = 0;
|
||||
#endif
|
||||
public:
|
||||
explicit Nrf52BufferedRadioHal(SPIClass& bus, uint32_t hz = 16000000)
|
||||
: ArduinoHal(bus, SPISettings(hz, MSBFIRST, SPI_MODE0)) {}
|
||||
|
||||
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
||||
if (len) spi->transfer(out, in, len);
|
||||
if (!len) return;
|
||||
spi->transfer(out, in, len);
|
||||
#if MC_NRF_RADIO_BUSY_SPIN_POLLS > 0
|
||||
_busy_spin_polls_remaining = MC_NRF_RADIO_BUSY_SPIN_POLLS;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MC_NRF_RADIO_BUSY_SPIN_POLLS > 0
|
||||
// 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 (_busy_spin_polls_remaining) {
|
||||
--_busy_spin_polls_remaining;
|
||||
return;
|
||||
}
|
||||
ArduinoHal::yield();
|
||||
}
|
||||
#endif
|
||||
};
|
||||
@@ -18,7 +18,7 @@ enum Stage : uint32_t {
|
||||
};
|
||||
enum Result : uint32_t { Success = 0, Busy = 1, Failure = 2 };
|
||||
static constexpr uint32_t Magic = 0x4d435054; // MCPT
|
||||
static constexpr uint32_t Version = 1;
|
||||
static constexpr uint32_t Version = 2;
|
||||
static constexpr uint32_t HistogramCount = 16;
|
||||
static constexpr uint32_t HistogramWidthUs = 64;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
@@ -37,6 +37,7 @@ struct StageStats {
|
||||
struct DirectionStats {
|
||||
uint32_t successes, busy, failures, last_result, last_mask;
|
||||
uint32_t last_us[StageCount];
|
||||
uint32_t worst_us[StageCount]; // stages from the same worst successful hop
|
||||
StageStats stage[StageCount];
|
||||
};
|
||||
struct Capture {
|
||||
@@ -58,7 +59,13 @@ extern volatile Capture mesh_hil_profile_timing;
|
||||
extern Work mesh_hil_profile_timing_work;
|
||||
}
|
||||
|
||||
inline void barrier() { __asm__ __volatile__("" ::: "memory"); }
|
||||
inline void barrier() {
|
||||
#if defined(__arm__) || defined(__thumb__)
|
||||
__asm__ __volatile__("dmb" ::: "memory");
|
||||
#else
|
||||
__asm__ __volatile__("" ::: "memory");
|
||||
#endif
|
||||
}
|
||||
|
||||
// Call once during wrapper initialization, outside the measured switch.
|
||||
// The nRF core enables trace and CYCCNT only; it does not enable debug/halt.
|
||||
@@ -113,6 +120,8 @@ inline void finish(Result result, uint32_t now_ticks) {
|
||||
++capture.sequence;
|
||||
barrier();
|
||||
auto& direction = capture.direction[work.direction];
|
||||
const bool worst = result == Success && (direction.stage[Whole].count == 0
|
||||
|| work.duration_us[Whole] > direction.stage[Whole].max_us);
|
||||
direction.last_result = result;
|
||||
direction.last_mask = work.mask;
|
||||
if (result == Success) ++direction.successes;
|
||||
@@ -121,6 +130,7 @@ inline void finish(Result result, uint32_t now_ticks) {
|
||||
for (uint32_t i = 0; i < StageCount; ++i) {
|
||||
const uint32_t duration = work.duration_us[i];
|
||||
direction.last_us[i] = duration;
|
||||
if (worst) direction.worst_us[i] = duration;
|
||||
if (result != Success || !(work.mask & (uint32_t(1) << i))) continue;
|
||||
auto& stats = direction.stage[i];
|
||||
const uint32_t previous_low = stats.sum_low_us;
|
||||
@@ -137,7 +147,7 @@ inline void finish(Result result, uint32_t now_ticks) {
|
||||
++capture.sequence;
|
||||
}
|
||||
|
||||
static_assert(sizeof(Capture) == 40 + 2 * (20 + StageCount * (4 + 88)),
|
||||
static_assert(sizeof(Capture) == 40 + 2 * (20 + StageCount * (8 + 88)),
|
||||
"Unexpected profile timing ABI");
|
||||
|
||||
} // namespace profiletiming
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Execute finite per-frame radio yield budgets and default-off behavior."""
|
||||
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]
|
||||
|
||||
MOCK = MOCK_HAL.replace('virtual ~ArduinoHal() = default;', '''
|
||||
unsigned yields = 0;
|
||||
virtual ~ArduinoHal() = default;
|
||||
virtual void yield() { ++yields; }
|
||||
''')
|
||||
|
||||
FIXTURE = r'''
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
int main() {
|
||||
SPIClass bus;
|
||||
Nrf52BufferedRadioHal concrete(bus,8000000);
|
||||
ArduinoHal& radio=concrete;
|
||||
constexpr unsigned budget=MC_NRF_RADIO_BUSY_SPIN_POLLS;
|
||||
#if MC_NRF_RADIO_BUSY_SPIN_POLLS == 0
|
||||
static_assert(sizeof(Nrf52BufferedRadioHal)==sizeof(ArduinoHal),"default RAM growth");
|
||||
#endif
|
||||
// The first pre-transfer wait delegates immediately, even in opt-in builds.
|
||||
for(unsigned i=0;i<1000;++i) radio.yield();
|
||||
assert(radio.yields==1000 && bus.bursts==0);
|
||||
radio.spiTransfer(nullptr,0,nullptr);
|
||||
radio.yield();
|
||||
assert(radio.yields==1001 && bus.bursts==0);
|
||||
|
||||
uint8_t tx[]={0x86,0x12,0x34},rx[3]={};
|
||||
for(unsigned frame=0;frame<1000;++frame) {
|
||||
const unsigned before=radio.yields;
|
||||
radio.spiTransfer(tx,sizeof(tx),rx);
|
||||
assert(rx[0]==uint8_t(tx[0]^0xa5) && rx[2]==uint8_t(tx[2]^0xa5));
|
||||
for(unsigned i=0;i<budget;++i) radio.yield();
|
||||
assert(radio.yields==before);
|
||||
radio.yield(); assert(radio.yields==before+1);
|
||||
// A zero-byte call must not replenish an exhausted budget.
|
||||
radio.spiTransfer(nullptr,0,nullptr);
|
||||
radio.yield(); assert(radio.yields==before+2);
|
||||
}
|
||||
assert(bus.bursts==1000 && bus.bytes==0);
|
||||
|
||||
// Neither a zero-size transfer midway through a budget nor later ordinary
|
||||
// waits can turn the finite budget into perpetual spinning.
|
||||
unsigned before=radio.yields;
|
||||
radio.spiTransfer(tx,sizeof(tx),rx);
|
||||
for(unsigned i=0;i<budget/2;++i) radio.yield();
|
||||
radio.spiTransfer(nullptr,0,nullptr);
|
||||
for(unsigned i=budget/2;i<budget;++i) radio.yield();
|
||||
assert(radio.yields==before);
|
||||
for(unsigned i=0;i<100000;++i) radio.yield();
|
||||
assert(radio.yields==before+100000);
|
||||
before=radio.yields;
|
||||
radio.spiTransfer(tx,sizeof(tx),rx);
|
||||
for(unsigned i=0;i<100000;++i) radio.yield();
|
||||
assert(radio.yields==before+100000-budget);
|
||||
assert(bus.bursts==1002 && bus.bytes==0);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class NrfRadioYieldBudgetTest(unittest.TestCase):
|
||||
def test_zero_four_eight_sixteen_preserve_transfer_and_bound_skipped_yields(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
folder=Path(directory)
|
||||
(folder/'hal/Arduino').mkdir(parents=True)
|
||||
(folder/'hal/Arduino/ArduinoHal.h').write_text(MOCK,encoding='ascii')
|
||||
cpp,binary=folder/'test.cpp',folder/'test.exe'
|
||||
cpp.write_text(FIXTURE,encoding='ascii')
|
||||
for budget in (0,4,8,16):
|
||||
with self.subTest(budget=budget):
|
||||
built=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17',
|
||||
'-Wall','-Wextra','-Werror',f'-DMC_NRF_RADIO_BUSY_SPIN_POLLS={budget}',
|
||||
'-I',str(folder),'-I',str(ROOT/'src'),str(cpp),'-o',str(binary)],
|
||||
capture_output=True,text=True)
|
||||
self.assertEqual(built.returncode,0,built.stderr)
|
||||
ran=subprocess.run([str(binary)],capture_output=True,text=True)
|
||||
self.assertEqual(ran.returncode,0,ran.stderr)
|
||||
for budget in (-1,17):
|
||||
with self.subTest(invalid_budget=budget):
|
||||
built=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17',
|
||||
f'-DMC_NRF_RADIO_BUSY_SPIN_POLLS={budget}','-I',str(folder),
|
||||
'-I',str(ROOT/'src'),str(cpp),'-o',str(binary)],
|
||||
capture_output=True,text=True)
|
||||
self.assertNotEqual(built.returncode,0)
|
||||
self.assertIn('bounded 0..16 poll budget',built.stderr)
|
||||
|
||||
def test_default_preprocessing_omits_budget_and_override(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
folder=Path(directory)
|
||||
(folder/'hal/Arduino').mkdir(parents=True)
|
||||
(folder/'hal/Arduino/ArduinoHal.h').write_text(MOCK,encoding='ascii')
|
||||
result=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-E','-P',
|
||||
'-x','c++','-I',str(folder),'-I',str(ROOT/'src'),'-'],
|
||||
input='#include <helpers/radiolib/Nrf52BufferedRadioHal.h>\n',
|
||||
capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
production=result.stdout.split('class Nrf52BufferedRadioHal',1)[1]
|
||||
self.assertNotIn('_busy_spin_polls_remaining',production)
|
||||
self.assertNotIn('void yield()',production)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main()
|
||||
@@ -48,7 +48,7 @@ void dwt_enable() { clockEnabled=true; ++enableCalls; }
|
||||
int main() {
|
||||
using namespace mesh::profiletiming;
|
||||
auto& capture=mesh_hil_profile_timing;
|
||||
assert(capture.magic==Magic && capture.size_bytes==1736);
|
||||
assert(capture.magic==Magic && capture.version==2 && capture.size_bytes==1808);
|
||||
assert(capture.clock_hz==64000000 && capture.sequence==0);
|
||||
initializeClock();
|
||||
assert(enableCalls==1 && capture.clock_state==2);
|
||||
@@ -68,6 +68,7 @@ int main() {
|
||||
const auto& forward=capture.direction[0];
|
||||
assert(forward.successes==1 && forward.busy==0 && forward.failures==0);
|
||||
assert(forward.last_mask==511 && forward.last_us[Whole]==600);
|
||||
assert(forward.worst_us[Whole]==600 && forward.worst_us[Frequency]==50);
|
||||
assert(forward.stage[Whole].sum_low_us==600 && forward.stage[Whole].sum_high_us==0);
|
||||
assert(forward.stage[Whole].min_us==600 && forward.stage[Whole].max_us==600);
|
||||
assert(forward.stage[Whole].histogram[9]==1);
|
||||
@@ -94,10 +95,13 @@ int main() {
|
||||
begin(0,1,0); mark(Frequency,20*64); finish(Success,20*64);
|
||||
assert(sum.sum_low_us==9 && sum.sum_high_us==4);
|
||||
assert(sum.min_us==20 && sum.max_us==50 && sum.count==2);
|
||||
assert(forward.worst_us[Whole]==600 && forward.worst_us[Frequency]==50);
|
||||
begin(1,0,0); mark(Frequency,960*64); finish(Success,960*64);
|
||||
assert(capture.direction[1].stage[Frequency].histogram[15]==1);
|
||||
begin(1,0,0); mark(Frequency,10000*64); finish(Success,10000*64);
|
||||
assert(capture.direction[1].stage[Frequency].histogram[15]==2);
|
||||
assert(capture.direction[1].worst_us[Whole]==10000);
|
||||
assert(capture.direction[1].worst_us[Frequency]==10000);
|
||||
}
|
||||
'''.replace("@GLOBALS@", self.production_globals()))
|
||||
|
||||
|
||||
Reference in new issue
Block a user