mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-30 01:33:34 +00:00
Add guarded nRF SPI startup diagnostic for private 16 MHz trials
This commit is contained in:
1 parent
cb03417509
commit
c83d43e03c
5 files changed
+254
-11
No files matched your search
@@ -0,0 +1,12 @@
|
||||
#include "NrfRadioSpiStartupDiagHal.h"
|
||||
|
||||
#if MESH_HIL_SPI_STARTUP_DIAG
|
||||
namespace mesh { namespace spistartup {
|
||||
extern "C" {
|
||||
volatile Capture mesh_hil_spi_startup = {
|
||||
0x4d435353, 1, sizeof(Capture), Idle,
|
||||
0, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX,
|
||||
0, 0, 0, UINT32_MAX, 0};
|
||||
}
|
||||
} }
|
||||
#endif
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef MESH_HIL_SPI_STARTUP_DIAG
|
||||
#define MESH_HIL_SPI_STARTUP_DIAG 0
|
||||
#endif
|
||||
#if MESH_HIL_SPI_STARTUP_DIAG != 0 && MESH_HIL_SPI_STARTUP_DIAG != 1
|
||||
#error "SPI startup diagnostic flag must be 0 or 1"
|
||||
#endif
|
||||
|
||||
#if MESH_HIL_SPI_STARTUP_DIAG
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
#error "SPI startup diagnostic requires nRF52"
|
||||
#endif
|
||||
|
||||
#include "Nrf52BufferedRadioHal.h"
|
||||
#include <helpers/nrf52/SoftDeviceState.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace spistartup {
|
||||
|
||||
enum Phase : uint32_t { Idle = 0, Query = 1, Transfer = 2, Completed = 3, Refused = 4 };
|
||||
|
||||
// Fixed 14-word ABI for private SWD reads, including before USB is usable.
|
||||
// Each word is independently observable; this is not an atomic snapshot.
|
||||
struct Capture {
|
||||
uint32_t magic, version, size_bytes, phase;
|
||||
uint32_t queries, first_query_result, first_query_enabled;
|
||||
uint32_t last_query_result, last_query_enabled;
|
||||
uint32_t begun, completed, refused, last_opcode, last_length;
|
||||
};
|
||||
static_assert(sizeof(Capture) == 56 && offsetof(Capture, begun) == 36
|
||||
&& offsetof(Capture, last_length) == 52, "SPI startup capture ABI changed");
|
||||
extern "C" {
|
||||
extern volatile Capture mesh_hil_spi_startup;
|
||||
}
|
||||
|
||||
} } // namespace mesh::spistartup
|
||||
|
||||
// Diagnostic only: the global nrfx anomaly-198 workaround cannot coexist
|
||||
// with SoftDevice. Refuse the actual frame unless its disabled state was
|
||||
// positively established by the existing LTO-safe supervisor query.
|
||||
class NrfRadioSpiStartupDiagHal : public Nrf52BufferedRadioHal {
|
||||
public:
|
||||
explicit NrfRadioSpiStartupDiagHal(SPIClass& bus, uint32_t hz)
|
||||
: Nrf52BufferedRadioHal(bus, hz) {}
|
||||
|
||||
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
||||
if (!len) return;
|
||||
auto& capture = mesh::spistartup::mesh_hil_spi_startup;
|
||||
capture.last_opcode = out ? out[0] : UINT32_MAX;
|
||||
capture.last_length = static_cast<uint32_t>(len);
|
||||
capture.phase = mesh::spistartup::Query;
|
||||
uint8_t enabled = 0;
|
||||
const uint32_t result = mesh_nrf52::softdeviceIsEnabled(enabled);
|
||||
if (!capture.queries) {
|
||||
capture.first_query_result = result;
|
||||
capture.first_query_enabled = enabled;
|
||||
}
|
||||
++capture.queries;
|
||||
capture.last_query_result = result;
|
||||
capture.last_query_enabled = enabled;
|
||||
if (result != 0 || enabled != 0) {
|
||||
if (in) memset(in, 0x2a, len); // SX126x command-failed status.
|
||||
++capture.refused;
|
||||
capture.phase = mesh::spistartup::Refused;
|
||||
return;
|
||||
}
|
||||
++capture.begun;
|
||||
capture.phase = mesh::spistartup::Transfer;
|
||||
Nrf52BufferedRadioHal::spiTransfer(out, len, in);
|
||||
++capture.completed;
|
||||
capture.phase = mesh::spistartup::Completed;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Execute the actual default-off SPI startup capture and refusal gate."""
|
||||
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]
|
||||
HELPER = ROOT / "src/helpers/radiolib/NrfRadioSpiStartupDiagHal.cpp"
|
||||
|
||||
MOCK_SDM = r'''
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
extern uint32_t test_sd_result, test_sd_queries;
|
||||
extern uint8_t test_sd_enabled;
|
||||
uint32_t sd_softdevice_is_enabled(uint8_t* state);
|
||||
'''
|
||||
|
||||
HARNESS = r'''
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <helpers/radiolib/NrfRadioSpiStartupDiagHal.h>
|
||||
uint32_t test_sd_result=0,test_sd_queries=0;
|
||||
uint8_t test_sd_enabled=0;
|
||||
using namespace mesh::spistartup;
|
||||
uint32_t sd_softdevice_is_enabled(uint8_t* state) {
|
||||
assert(mesh_hil_spi_startup.phase==Query);
|
||||
++test_sd_queries;*state=test_sd_enabled;return test_sd_result;
|
||||
}
|
||||
void before_test_spi_transfer() {
|
||||
const auto& capture=mesh_hil_spi_startup;
|
||||
assert(capture.phase==Transfer&&capture.begun==capture.completed+1);
|
||||
assert(capture.queries==test_sd_queries);
|
||||
assert(capture.last_query_result==0&&capture.last_query_enabled==0);
|
||||
}
|
||||
int main() {
|
||||
SPIClass bus;NrfRadioSpiStartupDiagHal concrete(bus,16000000);
|
||||
ArduinoHal& hal=concrete;
|
||||
auto& capture=mesh_hil_spi_startup;
|
||||
assert(capture.magic==0x4d435353&&capture.version==1&&capture.size_bytes==56);
|
||||
assert(capture.phase==Idle&&capture.queries==0&&capture.begun==0&&capture.completed==0&&capture.refused==0);
|
||||
assert(capture.first_query_result==UINT32_MAX&&capture.first_query_enabled==UINT32_MAX);
|
||||
assert(concrete.spiSettings.hz==16000000&&concrete.spiSettings.order==MSBFIRST&&concrete.spiSettings.mode==SPI_MODE0);
|
||||
Capture initial;memcpy(&initial,(const void*)&capture,sizeof(initial));
|
||||
// A zero-size call must not touch capture, query the supervisor, or use SPI.
|
||||
hal.spiTransfer(nullptr,0,nullptr);
|
||||
assert(memcmp(&initial,(const void*)&capture,sizeof(initial))==0&&test_sd_queries==0&&bus.bursts==0);
|
||||
std::array<uint8_t,4> out={0x83,0x11,0x22,0x33},in{};
|
||||
const auto original=out;
|
||||
// Positively disabled permits exactly one buffered full-duplex frame.
|
||||
hal.spiTransfer(out.data(),out.size(),in.data());
|
||||
assert(capture.phase==Completed&&capture.queries==1&&capture.begun==1&&capture.completed==1&&capture.refused==0);
|
||||
assert(capture.first_query_result==0&&capture.first_query_enabled==0);
|
||||
assert(capture.last_opcode==0x83&&capture.last_length==4&&bus.bursts==1&&bus.bytes==0);
|
||||
assert(out==original);
|
||||
for(unsigned i=0;i<in.size();++i)assert(in[i]==uint8_t(out[i]^0xa5));
|
||||
// Enabled SoftDevice must refuse the nrfx-workaround frame, report command
|
||||
// failure throughout the response, and keep the original TX data intact.
|
||||
test_sd_enabled=1;in.fill(0x77);out[0]=0xc0;
|
||||
hal.spiTransfer(out.data(),2,in.data());
|
||||
assert(capture.phase==Refused&&capture.queries==2&&capture.begun==1&&capture.completed==1&&capture.refused==1);
|
||||
assert(capture.last_query_result==0&&capture.last_query_enabled==1);
|
||||
assert(capture.last_opcode==0xc0&&capture.last_length==2&&bus.bursts==1);
|
||||
assert(in[0]==0x2a&&in[1]==0x2a&&in[2]==0x77&&in[3]==0x77&&out[0]==0xc0);
|
||||
// A failed query is unsafe even when the returned enabled byte is zero.
|
||||
test_sd_enabled=0;test_sd_result=7;in.fill(0x55);
|
||||
hal.spiTransfer(out.data(),out.size(),in.data());
|
||||
assert(capture.phase==Refused&&capture.queries==3&&capture.begun==1&&capture.completed==1&&capture.refused==2);
|
||||
assert(capture.last_query_result==7&&capture.last_query_enabled==0&&bus.bursts==1);
|
||||
for(auto byte:in)assert(byte==0x2a);
|
||||
// No-RX frames are still refused safely; no null pointer is dereferenced.
|
||||
hal.spiTransfer(out.data(),out.size(),nullptr);
|
||||
assert(capture.refused==3&&capture.queries==4&&bus.bursts==1);
|
||||
// A later positively-disabled query allows transfer and preserves the
|
||||
// original first-query evidence while updating the last-query evidence.
|
||||
test_sd_result=0;out[0]=0x82;
|
||||
hal.spiTransfer(out.data(),out.size(),in.data());
|
||||
assert(capture.phase==Completed&&capture.queries==5&&capture.begun==2&&capture.completed==2&&capture.refused==3);
|
||||
assert(capture.first_query_result==0&&capture.first_query_enabled==0);
|
||||
assert(capture.last_query_result==0&&capture.last_query_enabled==0);
|
||||
assert(capture.last_opcode==0x82&&capture.last_length==4&&bus.bursts==2&&bus.bytes==0);
|
||||
Capture final;memcpy(&final,(const void*)&capture,sizeof(final));
|
||||
hal.spiTransfer(nullptr,0,nullptr);
|
||||
assert(memcmp(&final,(const void*)&capture,sizeof(final))==0&&test_sd_queries==5);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class NrfRadioSpiStartupDiagTests(unittest.TestCase):
|
||||
def test_actual_helper_queries_before_frames_and_refuses_unsafe_workaround(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nrf-spi-startup-") as directory:
|
||||
folder=Path(directory);(folder/"hal/Arduino").mkdir(parents=True)
|
||||
mock=MOCK_HAL.replace("constexpr unsigned MSBFIRST", "void before_test_spi_transfer();\nconstexpr unsigned MSBFIRST")
|
||||
mock=mock.replace(" ++bursts;", " before_test_spi_transfer();\n ++bursts;")
|
||||
(folder/"hal/Arduino/ArduinoHal.h").write_text(mock,encoding="ascii")
|
||||
(folder/"nrf_sdm.h").write_text(MOCK_SDM,encoding="ascii")
|
||||
source=folder/"test.cpp";source.write_text(HARNESS,encoding="ascii")
|
||||
binary=folder/"test"
|
||||
command=[os.environ.get("CXX","c++"),"-std=c++17","-Wall","-Wextra","-Werror",
|
||||
"-fsanitize=address,undefined","-fno-sanitize-recover=all","-fno-pie","-no-pie",
|
||||
"-DNRF52_PLATFORM=1","-DMESH_HIL_SPI_STARTUP_DIAG=1",
|
||||
"-I",str(folder),"-I",str(ROOT/"src"),str(source),str(HELPER),"-o",str(binary)]
|
||||
built=subprocess.run(command,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.stdout+ran.stderr)
|
||||
|
||||
def test_default_off_translation_unit_has_no_code_ram_or_sdk_dependencies(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nrf-spi-startup-off-") as directory:
|
||||
output=Path(directory)/"diag.o"
|
||||
for flags in ([],["-DMESH_HIL_SPI_STARTUP_DIAG=0"]):
|
||||
built=subprocess.run([os.environ.get("CXX","c++"),"-std=c++17","-Wall","-Wextra","-Werror",
|
||||
*flags,"-I",str(ROOT/"src"),"-c",str(HELPER),"-o",str(output)],capture_output=True,text=True)
|
||||
self.assertEqual(built.returncode,0,built.stderr)
|
||||
symbols=subprocess.run(["nm","--defined-only",str(output)],capture_output=True,text=True)
|
||||
self.assertEqual(symbols.returncode,0,symbols.stderr)
|
||||
self.assertEqual(symbols.stdout.strip(),"")
|
||||
|
||||
|
||||
if __name__=="__main__":unittest.main(verbosity=2)
|
||||
@@ -53,6 +53,7 @@ HARNESS = r'''
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
#include <helpers/radiolib/NrfRadioSpiStartupDiagHal.h>
|
||||
@CLOCK_POLICY@
|
||||
SPIClass SPI, SPI1;
|
||||
constexpr unsigned P_LORA_NSS=1, P_LORA_DIO_1=2, P_LORA_RESET=3, P_LORA_BUSY=4;
|
||||
@@ -97,15 +98,18 @@ int main() {
|
||||
'''
|
||||
|
||||
|
||||
def construction_from_target(target):
|
||||
start=target.index("#if MESH_HIL_SPI_STARTUP_DIAG",target.index("RAK3401Board board;"))
|
||||
end=target.index("RADIO_CLASS radio = new Module(",start)
|
||||
return target[start:target.index(";",end)+1]
|
||||
|
||||
|
||||
class Rak3401BufferedRadioHalTests(unittest.TestCase):
|
||||
def test_actual_target_uses_one_transfer_per_command_on_same_spi1_at_8mhz(self):
|
||||
target = (ROOT / "variants/rak3401/target.cpp").read_text(encoding="ascii")
|
||||
clock_policy = target[target.index("#ifndef MC_RAK3401_SPI_HZ"):
|
||||
target.index("\nnamespace {")]
|
||||
construction = "\n".join(re.findall(
|
||||
r"^(?:static Nrf52BufferedRadioHal radioHal\([^;]+;|"
|
||||
r"RADIO_CLASS radio = new Module\([^;]+;)$", target, re.MULTILINE))
|
||||
self.assertEqual(len(construction.splitlines()), 2)
|
||||
construction = construction_from_target(target)
|
||||
helper = (ROOT / "src/helpers/radiolib/Nrf52BufferedRadioHal.h").read_text(
|
||||
encoding="ascii")
|
||||
# Removing the real override reinstates ArduinoHal's per-byte path.
|
||||
@@ -128,7 +132,8 @@ class Rak3401BufferedRadioHalTests(unittest.TestCase):
|
||||
(base / "helpers/radiolib/Nrf52BufferedRadioHal.h").write_text(
|
||||
implementation, encoding="ascii")
|
||||
built = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
"-Werror", "-I", str(base), str(cpp), "-o", str(executable)],
|
||||
"-Werror", "-I", str(base), "-I", str(ROOT / "src"),
|
||||
"-I",str(ROOT / "src/helpers/radiolib"),str(cpp), "-o", str(executable)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
ran = subprocess.run([str(executable)], capture_output=True,
|
||||
@@ -143,14 +148,15 @@ class Rak3401BufferedRadioHalTests(unittest.TestCase):
|
||||
target = (ROOT / "variants/rak3401/target.cpp").read_text(encoding="ascii")
|
||||
policy = target[target.index("#ifndef MC_RAK3401_SPI_HZ"):
|
||||
target.index("\nnamespace {")]
|
||||
construction = "\n".join(re.findall(
|
||||
r"^(?:static Nrf52BufferedRadioHal radioHal\([^;]+;|"
|
||||
r"RADIO_CLASS radio = new Module\([^;]+;)$", target, re.MULTILINE))
|
||||
construction = construction_from_target(target)
|
||||
fixture = HARNESS.replace("@CLOCK_POLICY@", policy).replace("@CONSTRUCTION@", construction)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
folder = Path(directory)
|
||||
(folder / "hal/Arduino").mkdir(parents=True)
|
||||
(folder / "hal/Arduino/ArduinoHal.h").write_text(MOCK_HAL, encoding="ascii")
|
||||
(folder / "nrf_sdm.h").write_text(
|
||||
"#pragma once\n#include <cstdint>\ninline uint32_t sd_softdevice_is_enabled(uint8_t* enabled)"
|
||||
"{*enabled=0;return 0;}\n",encoding="ascii")
|
||||
cpp, binary = folder / "test.cpp", folder / "test.exe"
|
||||
cpp.write_text(fixture, encoding="ascii")
|
||||
cases = [([], True)] + [
|
||||
@@ -166,11 +172,22 @@ class Rak3401BufferedRadioHalTests(unittest.TestCase):
|
||||
"NRFX_SPIM3_NRF52840_ANOMALY_198_WORKAROUND_ENABLED=0"], False),
|
||||
(["MC_RAK3401_SPI_HZ=12000000"], False),
|
||||
(["MC_RAK3401_SPI_HZ=32000000"], False)]
|
||||
diagnostic=["MC_RAK3401_SPI_HZ=16000000","MESH_HIL_SPI_STARTUP_DIAG=1",
|
||||
"MESH_HIL_PROFILE_TIMING=1","SPI_32MHZ_INTERFACE=1",
|
||||
"NRFX_SPIM3_NRF52840_ANOMALY_198_WORKAROUND_ENABLED=1","NRF52_PLATFORM=1"]
|
||||
cases.append((diagnostic,True))
|
||||
for required in diagnostic[1:-1]:
|
||||
cases.append(([flag for flag in diagnostic if flag!=required],False))
|
||||
cases.append(([flag if flag!="MESH_HIL_PROFILE_TIMING=1" else "MESH_HIL_PROFILE_TIMING=0"
|
||||
for flag in diagnostic],False))
|
||||
cases.append(([flag if flag!="MC_RAK3401_SPI_HZ=16000000" else "MC_RAK3401_SPI_HZ=32000000"
|
||||
for flag in diagnostic],False))
|
||||
for flags, valid in cases:
|
||||
with self.subTest(flags=flags):
|
||||
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
|
||||
"-Wall", "-Wextra", "-Werror", *["-D" + flag for flag in flags],
|
||||
"-I", str(folder), "-I", str(ROOT / "src"), str(cpp),
|
||||
str(ROOT/"src/helpers/radiolib/NrfRadioSpiStartupDiagHal.cpp"),
|
||||
"-o", str(binary)], capture_output=True, text=True)
|
||||
if valid:
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "OnboardRadioSleep.h"
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
#include <helpers/radiolib/NrfRadioSpiStartupDiagHal.h>
|
||||
#include <nrf_gpio.h>
|
||||
#ifdef USE_CC310_HW_CRYPTO
|
||||
#include <helpers/NRF52Crypto.h>
|
||||
@@ -12,7 +13,13 @@
|
||||
#define MC_RAK3401_SPI_HZ 8000000
|
||||
#endif
|
||||
#if MC_RAK3401_SPI_HZ > 8000000
|
||||
#error "RAK3401 SPI above 8 MHz needs an unimplemented SoftDevice-compatible SPIM3 mitigation"
|
||||
#if MC_RAK3401_SPI_HZ != 16000000 || !MESH_HIL_SPI_STARTUP_DIAG \
|
||||
|| !defined(MESH_HIL_PROFILE_TIMING) || MESH_HIL_PROFILE_TIMING != 1 \
|
||||
|| !defined(SPI_32MHZ_INTERFACE) || SPI_32MHZ_INTERFACE != 1 \
|
||||
|| !defined(NRFX_SPIM3_NRF52840_ANOMALY_198_WORKAROUND_ENABLED) \
|
||||
|| NRFX_SPIM3_NRF52840_ANOMALY_198_WORKAROUND_ENABLED != 1
|
||||
#error "RAK3401 SPI above 8 MHz requires the explicit 16 MHz startup diagnostic and nrfx workaround"
|
||||
#endif
|
||||
#elif MC_RAK3401_SPI_HZ != 2000000 && MC_RAK3401_SPI_HZ != 4000000 \
|
||||
&& MC_RAK3401_SPI_HZ != 8000000
|
||||
#error "RAK3401 radio SPI clock must be 2, 4 or 8 MHz"
|
||||
@@ -136,10 +143,15 @@ RAK3401Board board;
|
||||
// controller instance for startup and watchdog recovery; resetting the radio
|
||||
// must not replace or tear down the SPI object used by its Module.
|
||||
// Batch each RadioLib command into one SPIM transfer rather than starting a
|
||||
// transfer per byte. Keep 8 MHz by default on SPI1/SPIM2. Higher speeds need
|
||||
// a separate SPIM3/SoftDevice mitigation before they can be offered safely.
|
||||
// transfer per byte. Keep 8 MHz by default on SPI1/SPIM2. The explicit HIL-only
|
||||
// 16 MHz startup diagnostic refuses frames unless SoftDevice is disabled;
|
||||
// this is not a production SPIM3/SoftDevice mitigation.
|
||||
// Changing the speed never replaces this bus or its recovery/handoff path.
|
||||
#if MESH_HIL_SPI_STARTUP_DIAG
|
||||
static NrfRadioSpiStartupDiagHal radioHal(SPI1, MC_RAK3401_SPI_HZ);
|
||||
#else
|
||||
static Nrf52BufferedRadioHal radioHal(SPI1, MC_RAK3401_SPI_HZ);
|
||||
#endif
|
||||
RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
Reference in new issue
Block a user