Add gated XIAO nRF52 buffered SPIM2 qualification candidate
This commit is contained in:
1 parent
e4907edd5f
commit
1b9fcf2af5
6 files changed
+243
-1
No files matched your search
@@ -825,6 +825,7 @@ jobs:
|
||||
python3 -B test/test_nrf_radio_spi_startup_diag.py -v
|
||||
python3 -B test/test_nrf_radio_spi_read_probe.py -v
|
||||
python3 -B test/test_sx126x_nrf_rp_constructors.py -v
|
||||
python3 -B test/test_xiao_nrf52_buffered_spi.py -v
|
||||
python3 -B test/test_esp32_buffered_radio_targets.py -v
|
||||
python3 -B test/test_xiao_battery_safety.py -v
|
||||
python3 -B test/test_peripheral_mode_prefs.py -v
|
||||
|
||||
@@ -124,7 +124,12 @@ void check() {{
|
||||
self.assertEqual(len(sources),32)
|
||||
for source in sources:
|
||||
text=source.read_text()
|
||||
self.assertNotIn("BufferedRadioHal",text,str(source))
|
||||
if source.parent.name == "xiao_nrf52":
|
||||
# Its separate constructor test executes the default-off
|
||||
# candidate gate and actual legacy/opt-in branches.
|
||||
self.assertIn("#if MC_XIAO_NRF52_HIL_FAST_SPI",text)
|
||||
else:
|
||||
self.assertNotIn("BufferedRadioHal",text,str(source))
|
||||
constructions=re.findall(r"^RADIO_CLASS radio = new Module\(P_LORA_NSS[^;]+;",text,re.MULTILINE)
|
||||
fallback=source.parent.name in {"heltec_t096","heltec_t1","heltec_t114"}
|
||||
self.assertEqual(len(constructions),2 if fallback else 1,str(source))
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Execute XIAO's default and HIL-only SPIM2 radio constructor/recovery paths."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
from test_sx126x_buffered_radio_hal import MOCK
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TARGET = ROOT / "variants/xiao_nrf52/target.cpp"
|
||||
CONFIG = ROOT / "variants/xiao_nrf52/XiaoRadioSpiConfig.h"
|
||||
SDK = Path.home() / ".platformio/packages/framework-arduinoadafruitnrf52/libraries/SPI/SPI.cpp"
|
||||
BASE_FLAGS = ["-DNRF52_PLATFORM", "-DNRF52840_XXAA", "-DXIAO_NRF52"]
|
||||
FAST_FLAGS = BASE_FLAGS + ["-DMC_XIAO_NRF52_HIL_FAST_SPI=1", "-DSPI_32MHZ_INTERFACE=1"]
|
||||
|
||||
# Exact global bus selection from the pinned core. When the package exists,
|
||||
# compare this fixture against its real source before executing either branch.
|
||||
SDK_MAPPING = """#ifndef SPI_32MHZ_INTERFACE
|
||||
#define SPI_32MHZ_INTERFACE 0
|
||||
#endif
|
||||
|
||||
#if SPI_32MHZ_INTERFACE == 0
|
||||
#define _SPI_DEV NRF_SPIM3 // 32 Mhz
|
||||
#define _SPI1_DEV NRF_SPIM2
|
||||
|
||||
#elif SPI_32MHZ_INTERFACE == 1
|
||||
#define _SPI_DEV NRF_SPIM2
|
||||
#define _SPI1_DEV NRF_SPIM3 // 32 Mhz
|
||||
|
||||
#else
|
||||
#error "not supported yet"
|
||||
#endif
|
||||
|
||||
#if SPI_INTERFACES_COUNT >= 1
|
||||
// use SPIM3 for highspeed 32Mhz
|
||||
SPIClass SPI(_SPI_DEV, PIN_SPI_MISO, PIN_SPI_SCK, PIN_SPI_MOSI);
|
||||
#endif
|
||||
|
||||
#if SPI_INTERFACES_COUNT >= 2
|
||||
SPIClass SPI1(_SPI1_DEV, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI);
|
||||
#endif
|
||||
"""
|
||||
HAL_MOCK = MOCK.replace("struct SPIClass {", """
|
||||
struct MockSpim { unsigned number; };
|
||||
inline MockSpim spim2{2},spim3{3};
|
||||
inline MockSpim* NRF_SPIM2=&spim2;
|
||||
inline MockSpim* NRF_SPIM3=&spim3;
|
||||
struct SPIClass {
|
||||
MockSpim* device;
|
||||
unsigned miso,sck,mosi;
|
||||
SPIClass(MockSpim* port,unsigned input,unsigned clock,unsigned output)
|
||||
: device(port),miso(input),sck(clock),mosi(output) {}
|
||||
void setPins(unsigned input,unsigned clock,unsigned output) {
|
||||
miso=input;sck=clock;mosi=output;
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
class XiaoNrf52BufferedSpiTests(unittest.TestCase):
|
||||
def test_actual_default_and_candidate_preserve_global_bus_and_reinitialization(self):
|
||||
text = TARGET.read_text()
|
||||
start = text.index("#if MC_XIAO_NRF52_HIL_FAST_SPI\n")
|
||||
constructor = text[start:text.index("#endif", start) + len("#endif")]
|
||||
initialization = method(text, "bool radio_init()")
|
||||
if SDK.exists():
|
||||
actual = SDK.read_text()
|
||||
self.assertIn(SDK_MAPPING, actual)
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <hal/Arduino/ArduinoHal.h>
|
||||
#include "XiaoRadioSpiConfig.h"
|
||||
#define SPI_INTERFACES_COUNT 2
|
||||
#define PIN_SPI_MISO 9
|
||||
#define PIN_SPI_MOSI 10
|
||||
#define PIN_SPI_SCK 8
|
||||
#define PIN_SPI1_MISO 25
|
||||
#define PIN_SPI1_MOSI 26
|
||||
#define PIN_SPI1_SCK 29
|
||||
#define P_LORA_MISO PIN_SPI_MISO
|
||||
#define P_LORA_MOSI PIN_SPI_MOSI
|
||||
#define P_LORA_SCLK PIN_SPI_SCK
|
||||
constexpr unsigned P_LORA_NSS=4,P_LORA_DIO_1=1,P_LORA_RESET=2,P_LORA_BUSY=3;
|
||||
@SDK_MAPPING@
|
||||
struct Module {
|
||||
ArduinoHal* hal;
|
||||
bool owns;
|
||||
Module(ArduinoHal* adapter,unsigned cs,unsigned irq,unsigned reset,unsigned busy)
|
||||
: hal(adapter),owns(false) { assert(cs==4 && irq==1 && reset==2 && busy==3); }
|
||||
Module(unsigned cs,unsigned irq,unsigned reset,unsigned busy,SPIClass& bus)
|
||||
: Module(new ArduinoHal(bus,SPISettings(2000000,MSBFIRST,SPI_MODE0)),cs,irq,reset,busy) {
|
||||
owns=true;
|
||||
}
|
||||
~Module() { if(owns) delete hal; }
|
||||
};
|
||||
struct Radio {
|
||||
Module* module;
|
||||
bool next=true;
|
||||
unsigned starts=0;
|
||||
Radio(Module* value):module(value) {}
|
||||
~Radio() { delete module; }
|
||||
bool std_init(SPIClass* bus) {
|
||||
assert(bus==&SPI && module->hal->spi==bus);
|
||||
bus->setPins(P_LORA_MISO,P_LORA_SCLK,P_LORA_MOSI);bus->begin();++starts;
|
||||
return next;
|
||||
}
|
||||
};
|
||||
#define RADIO_CLASS Radio
|
||||
@CONSTRUCTOR@
|
||||
static bool target_radio_available=false;
|
||||
unsigned Wire=0;
|
||||
struct Rtc { unsigned begins=0; void begin(unsigned) { ++begins; } } rtc_clock;
|
||||
@INITIALIZATION@
|
||||
int main() {
|
||||
assert(radio.module->hal->spi==&SPI);
|
||||
assert(SPI.device->number==(MC_XIAO_NRF52_HIL_FAST_SPI?2:3));
|
||||
assert(SPI1.device->number==(MC_XIAO_NRF52_HIL_FAST_SPI?3:2));
|
||||
assert(radio.module->hal->spiSettings.hz==(MC_XIAO_NRF52_HIL_FAST_SPI?8000000:2000000));
|
||||
assert(!radio.module->hal->initInterface && SPI.begins==0 && SPI1.begins==0);
|
||||
radio.module->hal->init();radio.module->hal->term();
|
||||
assert(SPI.begins==0 && SPI.ends==0); // The board still owns bus lifetime.
|
||||
assert(radio_init() && target_radio_available && radio.starts==1 && SPI.begins==1);
|
||||
assert(SPI.miso==9 && SPI.sck==8 && SPI.mosi==10);
|
||||
uint8_t tx[]={0x86,0x12,0x34,0x56},rx[4]{};
|
||||
radio.module->hal->spiTransfer(tx,4,rx);
|
||||
assert(SPI.bursts==unsigned(MC_XIAO_NRF52_HIL_FAST_SPI));
|
||||
assert(SPI.bytes==unsigned(MC_XIAO_NRF52_HIL_FAST_SPI?0:4));
|
||||
assert(rx[0]==uint8_t(tx[0]^0xa5) && rx[3]==uint8_t(tx[3]^0xa5));
|
||||
const unsigned bursts=SPI.bursts,bytes=SPI.bytes;
|
||||
radio.module->hal->spiTransfer(nullptr,0,nullptr);
|
||||
assert(SPI.bursts==bursts && SPI.bytes==bytes);
|
||||
// Existing shutdown/recovery can restart the same global object. Neither
|
||||
// the HAL nor a failed radio initialization changes its bus or ownership.
|
||||
SPI.end();SPI.setPins(P_LORA_MISO,P_LORA_SCLK,P_LORA_MOSI);SPI.begin();
|
||||
radio.next=false;assert(!radio_init() && !target_radio_available);
|
||||
radio.next=true;assert(radio_init() && target_radio_available);
|
||||
assert(radio.module->hal->spi==&SPI && radio.starts==3 && SPI.begins==4);
|
||||
assert(SPI1.begins==0 && SPI1.ends==0 && SPI1.bursts==0 && SPI1.bytes==0);
|
||||
#ifdef DISABLE_I2C_RTC_SCAN
|
||||
assert(rtc_clock.begins==0);
|
||||
#else
|
||||
assert(rtc_clock.begins==3);
|
||||
#endif
|
||||
}
|
||||
'''.replace("@SDK_MAPPING@", SDK_MAPPING).replace("@CONSTRUCTOR@", constructor).replace(
|
||||
"@INITIALIZATION@", initialization)
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-xiao-spi-", dir="/var/tmp") as directory:
|
||||
directory = Path(directory)
|
||||
(directory / "hal/Arduino").mkdir(parents=True)
|
||||
(directory / "hal/Arduino/ArduinoHal.h").write_text(HAL_MOCK, encoding="ascii")
|
||||
cpp, binary = directory / "test.cpp", directory / "test.exe"
|
||||
cpp.write_text(fixture, encoding="ascii")
|
||||
for fast, solar in ((False, False), (True, False), (False, True), (True, True)):
|
||||
with self.subTest(fast=fast, solar=solar):
|
||||
flags = FAST_FLAGS if fast else BASE_FLAGS
|
||||
if solar:
|
||||
flags = flags + ["-DDISABLE_I2C_RTC_SCAN"]
|
||||
built = subprocess.run([
|
||||
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
*flags, "-I", str(directory), "-I", str(ROOT / "src"),
|
||||
"-I", str(CONFIG.parent), str(cpp), "-o", str(binary),
|
||||
], capture_output=True, text=True, env={**os.environ, "TMPDIR": str(directory)})
|
||||
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_candidate_fails_closed_without_exact_board_bus_or_safe_peripheral_policy(self):
|
||||
cases = [
|
||||
(["-DMC_XIAO_NRF52_HIL_FAST_SPI=2"], "must be 0 or 1"),
|
||||
(["-DMC_XIAO_NRF52_HIL_FAST_SPI=1"], "requires the nRF52840 XIAO target"),
|
||||
(BASE_FLAGS + ["-DMC_XIAO_NRF52_HIL_FAST_SPI=1"], "mapped to SPIM2"),
|
||||
(BASE_FLAGS + ["-DMC_XIAO_NRF52_HIL_FAST_SPI=1", "-DSPI_32MHZ_INTERFACE=0"], "mapped to SPIM2"),
|
||||
(FAST_FLAGS + ["-DNRFX_SPIM3_NRF52840_ANOMALY_198_WORKAROUND_ENABLED=1"], "SoftDevice-conflicting"),
|
||||
(FAST_FLAGS + ["-DOTA_SD_STORE=1"], "additional SD or Ethernet"),
|
||||
(FAST_FLAGS + ["-DETHERNET_ENABLED=1"], "additional SD or Ethernet"),
|
||||
]
|
||||
for flags, diagnostic in cases:
|
||||
with self.subTest(flags=flags):
|
||||
result = subprocess.run([os.environ.get("CXX", "g++"), "-E", "-x", "c++",
|
||||
*flags, "-I", str(ROOT / "src"), str(CONFIG)],
|
||||
capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(diagnostic, result.stderr)
|
||||
|
||||
def test_default_candidate_has_no_code_or_sdk_dependency_and_recipes_do_not_enable_it(self):
|
||||
result = subprocess.run([os.environ.get("CXX", "g++"), "-E", "-P", "-x", "c++", str(CONFIG)],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(result.stdout.strip())
|
||||
config = CONFIG.with_name("platformio.ini").read_text()
|
||||
active = "\n".join(line for line in config.splitlines() if not line.lstrip().startswith(";"))
|
||||
self.assertNotIn("MC_XIAO_NRF52_HIL_FAST_SPI", active)
|
||||
self.assertNotIn("SPI_32MHZ_INTERFACE", active)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
// Private physical-qualification candidate only. The normal recipes keep
|
||||
// their existing SPI transport until BLE, radio and storage checks pass.
|
||||
#ifndef MC_XIAO_NRF52_HIL_FAST_SPI
|
||||
#define MC_XIAO_NRF52_HIL_FAST_SPI 0
|
||||
#endif
|
||||
#if MC_XIAO_NRF52_HIL_FAST_SPI != 0 && MC_XIAO_NRF52_HIL_FAST_SPI != 1
|
||||
#error "XIAO nRF52 HIL SPI candidate must be 0 or 1"
|
||||
#endif
|
||||
#if MC_XIAO_NRF52_HIL_FAST_SPI
|
||||
#if !defined(NRF52_PLATFORM) || !defined(NRF52840_XXAA) || !defined(XIAO_NRF52)
|
||||
#error "XIAO HIL SPI candidate requires the nRF52840 XIAO target"
|
||||
#endif
|
||||
#if !defined(SPI_32MHZ_INTERFACE) || SPI_32MHZ_INTERFACE != 1
|
||||
#error "XIAO HIL SPI candidate requires global SPI mapped to SPIM2"
|
||||
#endif
|
||||
#if defined(NRFX_SPIM3_NRF52840_ANOMALY_198_WORKAROUND_ENABLED) && NRFX_SPIM3_NRF52840_ANOMALY_198_WORKAROUND_ENABLED
|
||||
#error "XIAO HIL SPI candidate must not enable the SPIM3 SoftDevice-conflicting workaround"
|
||||
#endif
|
||||
#if defined(OTA_SD_STORE) || defined(OTA_RAK15002_SD_CACHE) || defined(ETHERNET_ENABLED)
|
||||
#error "XIAO HIL SPI candidate has not qualified an additional SD or Ethernet bus owner"
|
||||
#endif
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
#endif
|
||||
@@ -1,4 +1,6 @@
|
||||
[Xiao_nrf52]
|
||||
; Private qualification only: pass MC_XIAO_NRF52_HIL_FAST_SPI=1 together with
|
||||
; SPI_32MHZ_INTERFACE=1 to test buffered 8 MHz on SPIM2. Neither is a default.
|
||||
extends = rak4631_hw
|
||||
board = seeed-xiao-afruitnrf52-nrf52840
|
||||
board_build.ldscript = boards/nrf52840_s140_v7.ld
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include "XiaoRadioSpiConfig.h"
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#ifdef USE_CC310_HW_CRYPTO
|
||||
#include <helpers/NRF52Crypto.h>
|
||||
@@ -14,7 +15,14 @@ static bool target_radio_available = false;
|
||||
|
||||
XiaoNrf52Board board;
|
||||
|
||||
#if MC_XIAO_NRF52_HIL_FAST_SPI
|
||||
// The explicit SDK mapping keeps global SPI, including std_init/recovery and
|
||||
// base shutdown, on SPIM2. Dedicated NRF_QSPI storage never starts SPI1.
|
||||
static Nrf52BufferedRadioHal radioHal(SPI, 8000000);
|
||||
RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
#else
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
#endif
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
|
||||
Reference in new issue
Block a user