Record SPI9 hardware checks and fix idle sleep test fixture

This commit is contained in:
mikecarper committed 2026-10-23 13:18:04 -07:00
1 parent d6853f4521
commit 837847f891
5 files changed
+319 -2

No files matched your search

+1
View File
@@ -11,6 +11,7 @@ Completed validation reports, experiments, and test guides are collected here. O
## Radio and RF
- [Saved SPI option 9: native, build and V3/V4 persistence checks, 2026-10-23](saved_spi9_2026-10-23.md)
- [Heltec V3/V4 8 MHz versus 8.889 MHz radio switching measurements, 2026-10-23](esp32_spi_clock_retune_2026-10-23.md)
- [Five-board eight-peer WiFi UDP bridge hardware qualification, 2026-10-22](udp_bridge_hardware_2026-10-22.md)
- [Eight-peer WiFi UDP bridge: V3/V4/R8 Full build and host validation, 2026-10-22](udp_bridge_2026-10-22.md)
@@ -0,0 +1,81 @@
<!-- meshcore-hosted-doc-link:start -->
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/saved_spi9_2026-10-23/">View this page on MeshCore Docs</a>.</p>
<!-- meshcore-hosted-doc-link:end -->
# Saved SPI option 9, 23 October 2026
Status: **Passed native, build and V3/V4 setting checks.** Firmware source:
`d6853f4521b6676298260c1c49095369a106000e`.
```text
get radio.fast.caps
set radio.fast.spi 9
get radio.fast.spi
```
The integer label `9` selects 8,888,888 Hz, approximately 8.889 MHz. It does
not select 9,000,000 Hz. The setting is saved without floating-point storage
or a file-format change. `0` restores the board's original default.
## Availability and defaults
The option is advertised by tunable radio HALs on the supported ESP32,
ESP32-S3 and ESP32-C3 boards when the permitted clock is at least 8,888,888 Hz.
The pinned ESP32-C6 core cannot select this divider and does not advertise it.
Current nRF52, RP2040 and STM32 radio buses do not offer it either. Unsupported
selections are rejected before changing or saving the policy.
Seven older ESP32 targets now use a tunable bytewise HAL: Heltec V2, GEPRC
Linkflow 900, T3S3 SX1276, T-Beam SX1276, T-LoRa V2.1, Meshnology W12 and
Waveshare ESP32-S3 LR1121. They retain their 2 MHz default, existing pins,
SPI controller, bus ownership and byte transfers. Existing buffered targets
retain their defaults too; enabling this option does not enable 16 MHz on
targets that previously lacked it.
The existing capability record remains two bytes and the saved policy remains
24 bytes at format version 1. Native tests cover strict integer parsing,
actual SPISettings mapping, unavailable choices, clock limits, saved and early
boot restore, transaction failure handling and output bounds. The constructor
test executes all eleven paths in the seven older targets. A further CLI case
covers clock-only radios with no other acceleration features.
## Builds and physical checks
Canonical builds passed for V3 Full observer MQTT Repeater, V4 Full observer
MQTT Repeater, Heltec V2 BLE Companion and XIAO ESP32-C3 USB Companion. Their
normal capacity and capability checks passed without feature reductions.
The first C3 attempt was stopped during a slow dependency download; the final
build used the identical cached, pinned RadioLib commit.
Both physical repeaters were updated through application-only USB writes.
Complete application readback matched the candidate images, partition tables
and OTA selection metadata stayed unchanged, and readable saved settings were
preserved.
| Physical board | Verified body | Saved 9 after one normal reboot | RX profile switch count | Switch errors |
| --- | --- | --- | ---: | ---: |
| Heltec V3 | `B05EC4F8E75A2A24` | Passed, 8888888 Hz | 8 to 332 | 0 |
| Heltec V4.3 OLED | `C4E28F80E5FBDF2E` | Passed, 8888888 Hz | 15 to 329 | 0 |
The short runtime check used primary 917 MHz and secondary 918 MHz, both
BW 62.5 kHz, SF7, CR5, preamble 32, with the secondary receive-only. No test
packets were deliberately transmitted. Core flags stayed zero and watchdog
recovery counters did not increase within either measured startup epoch.
Both boards retain the normal new firmware with the option available. Cleanup
restored their original selection `0` at 8 MHz, all seven fast features on,
radio2 off, temporary profiles cleared and their saved preferences unchanged.
The deliberate reboot resets volatile counters; it is recorded separately
from unexpected recoveries.
Production getters report the requested/resolved SPISettings clock. They are
not hardware-divider measurements. The earlier
[physical V3/V4 comparison](esp32_spi_clock_retune_2026-10-23.md) verified the
80 MHz / 9 divider; this change adds a saved selector for the same requested
clock. The timing campaign was not repeated. These checks qualify saving,
reboot persistence and retune liveness, not RF packet delivery, BLE, CPU sleep
or all firmware roles on physical hardware.
The [sanitized evidence](saved_spi9_2026-10-23.safe.json) includes build and
hardware results and hashes of the retained private receipts. Raw device
identities and CLI logs are excluded.
@@ -0,0 +1,192 @@
{
"schema": 1,
"passed": true,
"source_commit": "d6853f4521b6676298260c1c49095369a106000e",
"selection": 9,
"requested_spi_hz": 8888888,
"defaults_changed": false,
"capability_storage_bytes": 2,
"saved_record_bytes": 24,
"saved_record_version": 1,
"availability": {
"supported_current_mcus": [
"esp32",
"esp32s3",
"esp32c3"
],
"unsupported_current_mcus": [
"esp32c6",
"nrf52",
"rp2040",
"stm32"
],
"note": "Advertised only on supporting tunable HALs; current C6 core cannot select this divider."
},
"scope": "Saving SPI9, normal reboot persistence and RX retune liveness; no RF delivery, BLE or CPU sleep qualification.",
"clock_evidence": "Production getters report requested/resolved SPISettings. Physical divider was measured in the earlier V3/V4 HIL campaign, not repeated here.",
"native_checks": {
"focused_repository_methods_passed": 30,
"legacy_constructor_paths_passed": 11,
"additional_clock_only_cli_case_passed": true,
"private_installer_methods_passed": 25,
"private_controller_methods_passed": 12
},
"builds": {
"Heltec_v3_repeater_observer_mqtt": {
"passed": true,
"platform": "ESP32_PLATFORM",
"mcu": "esp32s3",
"build_profile": "full",
"application_bytes": 2302216,
"application_sha256": "9c7ad5ae3704fded2ab75ef42da3b1101c09cd6a2db2ca66fb35b2b5c163c755",
"capacity_passed": true,
"available_internal_bytes": 208768,
"required_heap_bytes": 207024,
"largest_internal_region_bytes": 176000,
"required_contiguous_bytes": 19168,
"reductions": [],
"elf_sha256": "fe75b5759e308738e6912ca9a6b0c62cfa4c156b3b271b3c37da5622066570f8",
"memory_receipt_sha256": "9988e39bd1b5053c448bd6ee88a78e7461f0b2a78ac1eabb56bfb1c3a64838c2",
"capabilities_receipt_sha256": "52c025efe6fc67e5086fa4becd39fe64500d2d35e99c184e2ab86b9a6ecca78f",
"build_summary_sha256": "4c26c4e0e504081da322b5fcb8d6d1cd57082b3401639c37888015575989c8fc"
},
"heltec_v4_repeater_observer_mqtt": {
"passed": true,
"platform": "ESP32_PLATFORM",
"mcu": "esp32s3",
"build_profile": "full",
"application_bytes": 2343800,
"application_sha256": "daf6fd466aa39c32d4a155e9bbdfdee3bb6c5e316e7bcc84f2dfab2a580a9beb",
"capacity_passed": true,
"available_internal_bytes": 201880,
"required_heap_bytes": 195760,
"largest_internal_region_bytes": 169112,
"required_contiguous_bytes": 19168,
"reductions": [],
"elf_sha256": "e1943b021faf410302582ebd665fb1354d279589f56df70696ba2735f3422f98",
"memory_receipt_sha256": "9754cf3f6eebca5724f71be6e0924ef253d498ee8992baf048a8a7a3de2897fe",
"capabilities_receipt_sha256": "da714d8f23735f5b395f527cfc83561fd45a6d15b6b5ac7a595b4bce84d0cb65",
"build_summary_sha256": "f3fee6f98707378063c2f36703066f7657ee171acd63493e7e4d6b6930899574"
},
"Heltec_v2_companion_radio_ble": {
"passed": true,
"platform": "ESP32_PLATFORM",
"mcu": "esp32",
"build_profile": "auto",
"application_bytes": 1821016,
"application_sha256": "378df81ff7b0e60b46a5078927c737aa80166d5ede7a7776866cb5f1c7939575",
"capacity_passed": true,
"available_internal_bytes": 181272,
"required_heap_bytes": 141104,
"largest_internal_region_bytes": 113840,
"required_contiguous_bytes": 18464,
"reductions": [],
"elf_sha256": "281229fe997ce4b854af5ccd5a2d4fbb7bcc2eb28d2f26afbcf7345ca54711ab",
"memory_receipt_sha256": "e59953d945260ed23deea74effd1ef3c9c9d186784a41f7bf4a79d3d96ec67f6",
"capabilities_receipt_sha256": "a2cb45d0e410261bc414d6cf1fb0dc83eb6a33d8e3427a5f9d4f37840616ea59",
"build_summary_sha256": "03edad5a409fd997478dfca6684a0ec60b68518e913bb316ee00e30fa23bac92"
},
"Xiao_C3_companion_radio_usb": {
"passed": true,
"platform": "ESP32_PLATFORM",
"mcu": "esp32c3",
"build_profile": "auto",
"application_bytes": 1494728,
"application_sha256": "15f29a99e9915d9217fc71991f67a56c9693c7f6710f316ccdd23bdc37cb0901",
"capacity_passed": true,
"available_internal_bytes": 119792,
"required_heap_bytes": 72736,
"largest_internal_region_bytes": 119792,
"required_contiguous_bytes": 18464,
"reductions": [],
"elf_sha256": "9d4598abd647292c4c4d84f85917f35396cd038932e3c525c8f3fbfe9ac9cebe",
"memory_receipt_sha256": "72b704a15c5cb778bcdde1b4406565b9a111e372b71e3e2ab50d0b3007115fa9",
"capabilities_receipt_sha256": "a77d314543cbd72052eac4a337c101ec42d17c541137de13f6b05811ce62066d",
"build_summary_sha256": "70320556ee234398363cced43fd939f3afd28e34db507f3f77da87fa37e208ca"
}
},
"hardware": {
"v3": {
"passed": true,
"source_commit": "d6853f4521b6676298260c1c49095369a106000e",
"application_body": "B05EC4F8E75A2A24",
"application_sha256": "9c7ad5ae3704fded2ab75ef42da3b1101c09cd6a2db2ca66fb35b2b5c163c755",
"app_only_install_and_full_readback_passed": true,
"partition_table_and_ota_selection_unchanged": true,
"readable_settings_preserved": true,
"spi9_save_and_getters_passed": true,
"capability_clocks": [
0,
2,
4,
8,
9
],
"requested_spi_hz": 8888888,
"fast_features": {
"warm": "on",
"fs": "on",
"packet": "on",
"iq": "on",
"modem": "on",
"batch": "on",
"buffer": "on"
},
"single_application_reboot_persistence_passed": true,
"pre_reboot_uptime_secs": 38,
"post_reboot_uptime_secs": 5,
"runtime_switches_before": 8,
"runtime_switches_after": 332,
"runtime_switch_errors": 0,
"new_core_flags": 0,
"new_watchdog_recoveries": 0,
"cleanup_passed": true,
"restored_spi_selection": 0,
"restored_resolved_spi_hz": 8000000,
"temporary_profiles_cleared": true,
"install_receipt_sha256": "4f92d878d603496515a79f69632cf7356a16fe8b9dc254a8f57b7b53cc7142d2",
"focus_receipt_sha256": "fdb2fd99ea266eec62ea12571a382520318b1a3a4618d666bc94eac73fbbd80c"
},
"v4": {
"passed": true,
"source_commit": "d6853f4521b6676298260c1c49095369a106000e",
"application_body": "C4E28F80E5FBDF2E",
"application_sha256": "daf6fd466aa39c32d4a155e9bbdfdee3bb6c5e316e7bcc84f2dfab2a580a9beb",
"app_only_install_and_full_readback_passed": true,
"partition_table_and_ota_selection_unchanged": true,
"readable_settings_preserved": true,
"spi9_save_and_getters_passed": true,
"capability_clocks": [
0,
2,
4,
8,
9
],
"requested_spi_hz": 8888888,
"fast_features": {
"warm": "on",
"fs": "on",
"packet": "on",
"iq": "on",
"modem": "on",
"batch": "on",
"buffer": "on"
},
"single_application_reboot_persistence_passed": true,
"pre_reboot_uptime_secs": 48,
"post_reboot_uptime_secs": 7,
"runtime_switches_before": 15,
"runtime_switches_after": 329,
"runtime_switch_errors": 0,
"new_core_flags": 0,
"new_watchdog_recoveries": 0,
"cleanup_passed": true,
"restored_spi_selection": 0,
"restored_resolved_spi_hz": 8000000,
"temporary_profiles_cleared": true,
"install_receipt_sha256": "718c11c61ec972c2988a2e1e28d16bc83e6184c21da23028a62ee159a45732aa",
"focus_receipt_sha256": "1baad7dd0242a95c2a42ed958a871355ea3e647ee22cf83359fa6dc5d35896ce"
}
}
}
+43
View File
@@ -232,6 +232,48 @@ static void roundedClockRequiresCapabilityAndBootAdmission() {
puts("Fast tuning: rounded SPI 9 uses exact divider Hz, persists unchanged and rejects unsupported clocks");
}
static void clockOnlyLegacyHalKeepsFeaturesUnavailable() {
TuningFixture f;
f.radio.caps.flags = f.radio.tuning.flags = 0;
f.radio.caps.spi_clocks = 2 | 4 | 8 | mesh::RadioSwitchCapabilities::Spi9;
f.radio.default_spi_hz = 2000000;
assert(!strcmp(f.cmd("get radio.fast.caps"), "> features:; spi=0,2,4,8,9"));
assert(!strcmp(f.cmd("get radio.fast"),
"> spi=0 (2000000Hz) warm=na fs=na packet=na iq=na modem=na batch=na buffer=na"));
f.cmd("set radio.fast.spi 9");
assert(!f.radio.tuning.flags && f.radio.tuning.spi_mhz == 9);
assert(f.radio.switchSpiHz() == 8888888);
const auto saved = f.fs.files;
TuningRadio reboot;
reboot.caps = f.radio.caps;
reboot.tuning.flags = 0;
reboot.default_spi_hz = 2000000;
mesh::RadioProfileCLI::configureBootTuning(&f.fs, reboot);
assert(!reboot.tuning.flags && reboot.tuning.spi_mhz == 9);
assert(reboot.switchSpiHz() == 8888888 && reboot.boot_configurations == 1);
mesh::RadioProfileCLI restored;
restored.begin(&f.fs, &reboot, &f.clock);
assert(reboot.tuning == f.radio.tuning && f.fs.files == saved);
const auto original = f.radio.tuning;
const auto attempts = f.radio.attempts;
const auto persists = f.radio.persists;
for (const char* feature : {"warm", "fs", "packet", "iq", "modem", "batch", "buffer"}) {
char command[48];
snprintf(command, sizeof(command), "set radio.fast.%s on", feature);
f.cmd(command, false);
assert(f.radio.tuning == original && f.fs.files == saved);
assert(f.radio.attempts == attempts && f.radio.persists == persists);
}
f.cmd("set radio.fast off");
assert(!f.radio.tuning.flags && f.radio.tuning.spi_mhz == 2);
assert(f.radio.switchSpiHz() == 2000000);
f.cmd("set radio.fast on");
assert(!f.radio.tuning.flags && !f.radio.tuning.spi_mhz);
assert(f.radio.switchSpiHz() == 2000000);
puts("Fast tuning: clock-only legacy HAL saves SPI 9 without exposing cached feature controls");
}
static void localOnlyAndBusyTransactions() {
TuningFixture f;
f.cmd("set radio.fast.fs off");
@@ -447,6 +489,7 @@ int main() {
controlsAndPersistence();
strictInputsAndUnsupportedFeatures();
roundedClockRequiresCapabilityAndBootAdmission();
clockOnlyLegacyHalKeepsFeaturesUnavailable();
localOnlyAndBusyTransactions();
storageFailureKeepsCommittedPolicy();
bootRecoveryAndDeferredAdmission();
+2 -2
View File
@@ -84,9 +84,9 @@ struct Mesh {
RadioLibWrapper& radio;
bool getDualRadioIdleDelay(uint32_t& out){return radio.getProfileIdleDelay(out);}
};
struct Board {unsigned calls=0;uint32_t deadline=0;void sleepUntilMicroseconds(uint32_t at){++calls;deadline=at;}};
struct ProfileIdleBoard {unsigned calls=0;uint32_t deadline=0;void sleepUntilMicroseconds(uint32_t at){++calls;deadline=at;}};
int main(){
RadioLibWrapper w;ready(w);Mesh m{w};Board b;
RadioLibWrapper w;ready(w);Mesh m{w};ProfileIdleBoard b;
auto start=w._esp32_profile_visit_started_us;auto visit=w._esp32_profile_idle_visit_us;
elapsed_us=start+visit-14000;assert(idle(w,10));
assert(change("set powersaving.test guard_ms 9"));assert(idle(w,5));