From 08b1c8826c688d412c05850fbc4ae3aba025cafb Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 27 Oct 2026 11:41:32 -0700 Subject: [PATCH] Save RAK repeater flash while retaining fast field arithmetic Use compact scalar crypto, reviewed GNSS version parsing and shared CLI reply copies for RAK3401/RAK4631 repeaters. Preserve keys, protocols, commands and configured capacities. Both Reduced images save 11,580 bytes. SWD measurements on the RAK3401 found scalar-only signing still near 50.9 ms and key exchange near 38 ms. Keep compact field arithmetic off: its extra 4.3 KiB saving adds about a quarter second and delays radio servicing. Validate Full and Reduced production OTA packages on both boards, focused crypto/GPS/CLI tests, and linked ARM crypto outputs. Include the RAM benchmark harness and scoped results. No live RF packet-loss qualification is claimed. --- docs/firmware_picker.md | 5 + .../rak_compact_crypto_2026-10-27.md | 115 +++ .../rak_compact_crypto_2026-10-27.safe.json | 923 ++++++++++++++++++ scripts/nrf52_flash_trim.py | 42 +- scripts/nrf52_gnss_version_trim.py | 15 +- test/test_nrf52_flash_trim.py | 42 + test/test_nrf52_gnss_version_trim.py | 23 +- tools/hil/nrf52_crypto_benchmark.c | 92 ++ tools/hil/nrf52_crypto_benchmark.ld | 9 + 9 files changed, 1256 insertions(+), 10 deletions(-) create mode 100644 docs/test-results/rak_compact_crypto_2026-10-27.md create mode 100644 docs/test-results/rak_compact_crypto_2026-10-27.safe.json create mode 100644 tools/hil/nrf52_crypto_benchmark.c create mode 100644 tools/hil/nrf52_crypto_benchmark.ld diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 97e783693..96ed6efc1 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -584,6 +584,11 @@ reduction does not disable generic I2C or unrelated board-integrated peripherals These reduced RAK profiles keep their own readable OTA target name and omit the all-board name catalog. Updates for other targets display numeric IDs; OTA compatibility checks and transfers are unchanged. +RAK3401/RAK4631 Repeater builds also use compact scalar arithmetic, shared CLI +reply copies, and a smaller GPS version parser. Keys, packet formats, commands, +responses and GPS support are preserved. Fast field arithmetic remains enabled +to avoid the long crypto pause; see the +[measured size and timing comparison](test-results/rak_compact_crypto_2026-10-27.md). Reduced RAK3401 and RAK4631 targets retain INA219/INA226/INA260/INA3221 as voltage/current entries in the optional sensor table. They are not the only I2C users: the SSD1306 OLED, supported autodiscovered RTCs, and RAK12500 GPS remain diff --git a/docs/test-results/rak_compact_crypto_2026-10-27.md b/docs/test-results/rak_compact_crypto_2026-10-27.md new file mode 100644 index 000000000..e9099b5ea --- /dev/null +++ b/docs/test-results/rak_compact_crypto_2026-10-27.md @@ -0,0 +1,115 @@ +# RAK repeater flash and crypto timing, 27 October 2026 + +Selected: **compact scalar arithmetic with the existing fast field arithmetic**. +The GPS parser and CLI reply-copy changes are also enabled. No commands, GPS +providers, keys, packet formats or configured capacities are removed. + +Both Reduced RAK repeater builds save **11,580 bytes (11.31 KiB)**. The much +slower compact-field experiment saves only another 4,384 bytes (4.28 KiB) and +is **not enabled by this change**. + +## Physical RAK3401 measurements + +The Pi's GPIO SWD debugger ran each implementation on the same RAK3401. Values +below are medians from 18 measurements per operation, using two public test +seeds from RFC 8032 and one discarded warmup per seed/operation. + +| Operation | Before, ms | Selected scalar-only, ms | Rejected scalar + field, ms | +| --- | ---: | ---: | ---: | +| Generate keypair | 47.513 | 47.145 | 297.863 | +| Sign empty message | 48.738 | 48.753 | 299.472 | +| Sign 96-byte message | 50.887 | 50.902 | 301.621 | +| Sign 256-byte message | 52.991 | 53.006 | 303.725 | +| Calculate shared secret | 38.147 | 37.951 | 274.195 | + +The selected implementation adds **0.015 ms** to signing in this measurement. +The unchanged field routines also show small timing differences between +layouts; these measurements do not promise a 15 us difference in every build. +The rejected field implementation adds about 251 ms to signing and 236 ms to +key exchange. + +Code and constants ran from **SRAM**, at 64 MHz, with interrupts masked. The +Cortex-M4 DWT cycle counter measured each operation. The 20-cycle measurement +overhead is included in every result. These are isolated algorithm timings, +not flash-resident firmware, Bluetooth, LoRa airtime or end-to-end CLI timings. + +Every measured public key, signature and shared secret matched independent +host cryptography. Signature checks covered empty, 96-byte and 256-byte +messages for both seeds. Shared secrets were checked through an independent +Edwards-to-Montgomery conversion and X25519 implementation. + +The existing watchdog survives software reset. The harness feeds its enabled +channels between operations, outside the timed region. Initial attempts that +failed reset/watchdog setup gates produced no accepted measurements. The final +three runs completed and reset into the existing application. Application +flash, bootloader and stored data were not updated; the Pi's gateway and host +CLI services were restored and reported active. + +## When this work runs + +- Signing happens when creating the repeater's own adverts and signed fleet or + host-service messages. Advert frequency follows the saved advert intervals. +- Fresh shared-secret calculations happen for login attempts and anonymous + region, owner and clock requests addressed to the node, including outgoing + region queries. Boot also reconstructs secrets for stored ACL entries. +- Established ACL sessions copy cached secrets. Ordinary relaying does not + sign every forwarded packet. Receive-side signature verification retains + its existing CC310 implementation and existing software fallback. + +Already queued software packets can wait during crypto, but the radio ISR only +records an event. The main loop reads the radio FIFO and services radio2 +switching later. Extending a synchronous operation by a quarter second can +therefore lose new RF packets; software queues do not make that lossless. +The selected policy avoids that added pause. **Live RF packet-loss testing was +not performed for this optimization.** + +## Firmware size and build checks + +Sizes are ELF `.text + .data`, not the larger UF2 container size. Baselines +already include the earlier reduced-build OTA target-name catalog saving. + +| Build | Before, bytes | Selected, bytes | Saving, bytes | +| --- | ---: | ---: | ---: | +| RAK3401 Reduced repeater | 575,644 | 564,064 | 11,580 | +| RAK4631 Reduced repeater | 626,884 | 615,304 | 11,580 | + +Full and Reduced production packaging passed for both RAKs with the Cascade +profile and required LoRa OTA. Checks included capability markers, conservative +RAM budgets, compiled Bluetooth DFU handoff and application-only UF2 address +ranges. This is build validation, not physical OTA qualification. Full RAK4631 +retains a tight conservative RAM budget: 83,180 bytes available against an +83,136-byte estimate. The existing 8 KiB loop stack is retained. + +Focused scalar/field arithmetic, RFC/random crypto parity, sanitizer, GNSS +parser, CLI argument and fleet schedule tests passed. Checks of both selected +linked ARM firmware images also reproduced the expected public keys, +signatures and shared secrets. Their emulated signing frame grows from 3,312 +to 3,512 bytes, excluding caller and interrupt frames; keypair and key-exchange +frames are unchanged. This fits within the retained loop stack but is not a +measurement of whole-application stack usage. + +## Reproduction and evidence + +The [RAM harness](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/hil/nrf52_crypto_benchmark.c) and +[linker script](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/hil/nrf52_crypto_benchmark.ld) are for debugger use +on an nRF52840 only. They are not production application images. Build with +GCC 14.2.1, `-Os -flto -mcpu=cortex-m4 -mthumb -mfloat-abi=hard +-mfpu=fpv4-sp-d16 -ffunction-sections -fdata-sections -fno-strict-aliasing`, +`ED25519_COMPACT_BASE=1`, `ED25519_COMPACT_SHA512=1` and `ED25519_NO_SEED=1`. +Link `fe.c`, `ge.c`, `keypair.c`, `sc.c`, `sha512.c`, `sign.c` and +`key_exchange.c` from `lib/ed25519`, with `-nostartfiles`, the supplied linker +script, `--gc-sections`, libc and libgcc. + +Use scalar/field flags `0/0` for baseline, `1/0` for selected, and `1/1` for the +rejected experiment. `BENCH_COMPACT` is only a report label (0, 2 and 1, +respectively). The recorded scalar-only run used scalar flag 2, which selects +the same nonzero preprocessor branch as production value 1. + +After verifying the physical chip identity, reset/halt, load the ELF into RAM, +resume at `bench_start`, and read `report` only after its completion marker is +`0xC0DEF17E`. The watchdog must pause while halted. Reset back into the normal +application afterward. No flash programming is needed for this benchmark. + +[Machine-readable results](rak_compact_crypto_2026-10-27.safe.json) include raw +cycle counts, image and harness hashes, build checks, and the rejected candidate +for comparison. diff --git a/docs/test-results/rak_compact_crypto_2026-10-27.safe.json b/docs/test-results/rak_compact_crypto_2026-10-27.safe.json new file mode 100644 index 000000000..8b00d82e6 --- /dev/null +++ b/docs/test-results/rak_compact_crypto_2026-10-27.safe.json @@ -0,0 +1,923 @@ +{ + "schema_version": 1, + "date": "2026-10-27", + "base_commit": "308fec1162e77134d15206b819dd59e898ed910b", + "selected_policy": "Compact scalar arithmetic, ordinary fast field arithmetic, reviewed GPS parser, shared CLI reply-copy compiler policy. RAK3401/RAK4631 repeater roles only.", + "scope": "RAK3401 physical RAM-resident algorithm timing and production Full/Reduced build checks for both RAKs. This is not flash-resident application timing or live RF/OTA qualification.", + "selected_flash_images": { + "3401": { + "full": { + "flash_text_bytes": 622456, + "flash_data_bytes": 880, + "flash_bytes": 623336, + "bss_bytes": 234568, + "elf_sha256": "1e7764ba9d5b081fe1dde7cff5d6a67a067d38ab03549d2889eadace09706dd7", + "available_internal_bytes": 80212, + "conservative_required_heap_bytes": 75920, + "capabilities_manifest_sha256": "5d1dcea9e49241c94caf77632ae5e277794571ce8b079817027a24c8f228a5bf" + }, + "reduced": { + "flash_text_bytes": 563184, + "flash_data_bytes": 880, + "flash_bytes": 564064, + "bss_bytes": 234568, + "elf_sha256": "4f7ea9df20e9fbbcab97fc1f98d67cbaaea9fd59d79111b4c01dcf83b4338330", + "available_internal_bytes": 82720, + "conservative_required_heap_bytes": 73872, + "capabilities_manifest_sha256": "9e9d02a55b1ace5be35a092ce51cb90509523183cfccfce6597c8e7d4a1eb4da" + }, + "reduced_before": { + "flash_text_bytes": 574764, + "flash_data_bytes": 880, + "flash_bytes": 575644, + "bss_bytes": 234568, + "elf_sha256": "cc735f36b3976527b7a295fed78a43627c41e565b81ce54418ff1ecd22e712f2" + }, + "reduced_saved_bytes": 11580 + }, + "4631": { + "full": { + "flash_text_bytes": 682064, + "flash_data_bytes": 884, + "flash_bytes": 682948, + "bss_bytes": 234564, + "elf_sha256": "a4b4fda84ca6cadb733de703e692f608691a5dcd3cf31e474ad0c3898105eef4", + "available_internal_bytes": 83180, + "conservative_required_heap_bytes": 83136, + "capabilities_manifest_sha256": "c1b05b741f6a822334d33083d51737435b2f7e72eea3633b24eda07fc43a6971" + }, + "reduced": { + "flash_text_bytes": 614420, + "flash_data_bytes": 884, + "flash_bytes": 615304, + "bss_bytes": 234564, + "elf_sha256": "23fae794233d6df9d2c44cf490dba5ee1b4454f49d614a8291fd92e10e04eab1", + "available_internal_bytes": 85696, + "conservative_required_heap_bytes": 81088, + "capabilities_manifest_sha256": "4aa11fff367ddd1f7c70b103994dd50cad984158658eb361a5b411c2e8fcefde" + }, + "reduced_before": { + "flash_text_bytes": 626000, + "flash_data_bytes": 884, + "flash_bytes": 626884, + "bss_bytes": 234564, + "elf_sha256": "560e9863598a122fe39bc85fa150d0ec4c96482cd00e74779d99c74fe1407bb9" + }, + "reduced_saved_bytes": 11580 + } + }, + "hardware_timing": { + "baseline": { + "independent_outputs_checked": 10, + "elf_sha256": "05e2292b9cc1ea3273dc21c366d0244a8c8176a7d817f65308fc70db51b14d90", + "report_sha256": "e14a1135ddb7cc8f3800a6d75320b3aced6390ed09900b9a85179b832757f8e4", + "cpu_hz": 64000000, + "cpuid": "0x410fc241", + "clock_stat": "0x10001", + "watchdog": { + "running": 1, + "config": 1, + "timeout_s": 60.0 + }, + "measurement_overhead_cycles": [ + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20 + ], + "measurements": { + "keypair": { + "cycles": [ + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814, + 3040814 + ], + "min_ms": 47.51271875, + "median_ms": 47.51271875, + "max_ms": 47.51271875 + }, + "sign_empty": { + "cycles": [ + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219, + 3119219 + ], + "min_ms": 48.737796875, + "median_ms": 48.737796875, + "max_ms": 48.737796875 + }, + "sign96": { + "cycles": [ + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753, + 3256753 + ], + "min_ms": 50.886765625, + "median_ms": 50.886765625, + "max_ms": 50.886765625 + }, + "sign256": { + "cycles": [ + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411, + 3391411 + ], + "min_ms": 52.990796875, + "median_ms": 52.990796875, + "max_ms": 52.990796875 + }, + "shared_secret": { + "cycles": [ + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426, + 2441426 + ], + "min_ms": 38.14728125, + "median_ms": 38.14728125, + "max_ms": 38.14728125 + } + } + }, + "compact": { + "independent_outputs_checked": 10, + "elf_sha256": "f637e1d5fda112ca1a8b19aa1cee6589d70db30de894ec28529c7a3dce8d16d3", + "report_sha256": "fa1ba95afd7a7d7d1b1047e82894ed0644608ddafc3ce9403949b62842721bd6", + "cpu_hz": 64000000, + "cpuid": "0x410fc241", + "clock_stat": "0x10001", + "watchdog": { + "running": 1, + "config": 1, + "timeout_s": 60.0 + }, + "measurement_overhead_cycles": [ + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20 + ], + "measurements": { + "keypair": { + "cycles": [ + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260, + 19063260 + ], + "min_ms": 297.8634375, + "median_ms": 297.8634375, + "max_ms": 297.8634375 + }, + "sign_empty": { + "cycles": [ + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191, + 19166191 + ], + "min_ms": 299.471734375, + "median_ms": 299.471734375, + "max_ms": 299.471734375 + }, + "sign96": { + "cycles": [ + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725, + 19303725 + ], + "min_ms": 301.620703125, + "median_ms": 301.620703125, + "max_ms": 301.620703125 + }, + "sign256": { + "cycles": [ + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383, + 19438383 + ], + "min_ms": 303.724734375, + "median_ms": 303.724734375, + "max_ms": 303.724734375 + }, + "shared_secret": { + "cycles": [ + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456, + 17548456 + ], + "min_ms": 274.194625, + "median_ms": 274.194625, + "max_ms": 274.194625 + } + } + }, + "scalar_only": { + "independent_outputs_checked": 10, + "elf_sha256": "b4da043bc9e91fb4ddacce31e766d363e01dcfb861bff796c76d35391a1450d8", + "report_sha256": "ce660aa04cacfa7ef671c943611ac2ce7b56e53fe5e91a2012e5d03a33dbdde4", + "cpu_hz": 64000000, + "cpuid": "0x410fc241", + "clock_stat": "0x10001", + "watchdog": { + "running": 1, + "config": 1, + "timeout_s": 60.0 + }, + "measurement_overhead_cycles": [ + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20, + 20 + ], + "measurements": { + "keypair": { + "cycles": [ + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258, + 3017258 + ], + "min_ms": 47.14465625, + "median_ms": 47.14465625, + "max_ms": 47.14465625 + }, + "sign_empty": { + "cycles": [ + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189, + 3120189 + ], + "min_ms": 48.752953125, + "median_ms": 48.752953125, + "max_ms": 48.752953125 + }, + "sign96": { + "cycles": [ + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723, + 3257723 + ], + "min_ms": 50.901921875, + "median_ms": 50.901921875, + "max_ms": 50.901921875 + }, + "sign256": { + "cycles": [ + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381, + 3392381 + ], + "min_ms": 53.005953125, + "median_ms": 53.005953125, + "max_ms": 53.005953125 + }, + "shared_secret": { + "cycles": [ + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881, + 2428881 + ], + "min_ms": 37.951265625, + "median_ms": 37.951265625, + "max_ms": 37.951265625 + } + } + }, + "comparison": { + "keypair": { + "added_ms": 250.35071874999997, + "ratio": 6.269130568328086 + }, + "sign_empty": { + "added_ms": 250.73393749999997, + "ratio": 6.144548042314438 + }, + "sign96": { + "added_ms": 250.73393749999997, + "ratio": 5.927291692062615 + }, + "sign256": { + "added_ms": 250.73393750000002, + "ratio": 5.731650631551293 + }, + "shared_secret": { + "added_ms": 236.04734374999998, + "ratio": 7.187789431258617 + } + }, + "scalar_only_comparison": { + "keypair": { + "added_ms": -0.3680625000000006 + }, + "sign_empty": { + "added_ms": 0.015156249999996874 + }, + "sign96": { + "added_ms": 0.015156249999996874 + }, + "sign256": { + "added_ms": 0.015156249999996874 + }, + "shared_secret": { + "added_ms": -0.19601562500000114 + } + } + }, + "hardware_method": { + "clock_hz": 64000000, + "code_and_constants": "SRAM", + "interrupts": "masked", + "per_operation_samples": 18, + "vectors": 2, + "warmups_per_vector_and_operation": 1, + "watchdog": "fed before each operation, outside the timed region; 60 second configured timeout", + "counter": "Cortex-M4 DWT CYCCNT", + "overhead": "20 cycles, included in the displayed timings", + "normal_application_flash_changed": false, + "bootloader_changed": false, + "existing_application_and_gateway_services_restored": true + }, + "benchmark_sources": { + "tools/hil/nrf52_crypto_benchmark.c": "64f4d2f4d189e9d4b01a64b33f1f2d461a86bc7c276f4a514459325c7a022a23", + "tools/hil/nrf52_crypto_benchmark.ld": "c12f3aaabad0a81c788c5eb869d09e5ecd5de5b2982af5c034daf2140b688f17" + }, + "compiler": "arm-none-eabi-gcc 14.2.1", + "benchmark_flags": [ + "-std=c99", + "-Os", + "-flto", + "-mcpu=cortex-m4", + "-mthumb", + "-mfloat-abi=hard", + "-mfpu=fpv4-sp-d16", + "-ffunction-sections", + "-fdata-sections", + "-fno-strict-aliasing", + "-DED25519_NO_SEED=1", + "-DED25519_COMPACT_BASE=1", + "-DED25519_COMPACT_SHA512=1" + ], + "benchmark_modes": { + "baseline": { + "BENCH_COMPACT": 0, + "ED25519_COMPACT_SCALAR": 0, + "ED25519_COMPACT_FIELD": 0 + }, + "compact": { + "BENCH_COMPACT": 1, + "ED25519_COMPACT_SCALAR": 1, + "ED25519_COMPACT_FIELD": 1 + }, + "scalar_only": { + "BENCH_COMPACT": 2, + "ED25519_COMPACT_SCALAR": 2, + "ED25519_COMPACT_FIELD": 0, + "note": "Scalar flag 2 is true in the same preprocessor gate as the selected production value 1." + } + }, + "rejected_field_candidate": { + "reason": "Adds about 251 ms to signing and 236 ms to shared-secret calculation; synchronous radio servicing can be delayed.", + "production_flash_images": { + "3401": { + "reduced_before": { + "flash_text_bytes": 574764, + "flash_data_bytes": 880, + "flash_bytes": 575644, + "bss_bytes": 234568, + "elf_sha256": "cc735f36b3976527b7a295fed78a43627c41e565b81ce54418ff1ecd22e712f2" + }, + "reduced_after": { + "flash_text_bytes": 558800, + "flash_data_bytes": 880, + "flash_bytes": 559680, + "bss_bytes": 234568, + "elf_sha256": "4306af7914d4f2a21296d21537a6959e9d69bddb6c6f5ac24b007ce1bfaea34a" + }, + "full_after": { + "flash_text_bytes": 618056, + "flash_data_bytes": 880, + "flash_bytes": 618936, + "bss_bytes": 234568, + "elf_sha256": "320cf1ef5a436c77781f69618fee4824cf52518d6756f216fdebc8a2e3b46b71" + }, + "reduced_saved_bytes": 15964 + }, + "4631": { + "reduced_before": { + "flash_text_bytes": 626000, + "flash_data_bytes": 884, + "flash_bytes": 626884, + "bss_bytes": 234564, + "elf_sha256": "560e9863598a122fe39bc85fa150d0ec4c96482cd00e74779d99c74fe1407bb9" + }, + "reduced_after": { + "flash_text_bytes": 610036, + "flash_data_bytes": 884, + "flash_bytes": 610920, + "bss_bytes": 234564, + "elf_sha256": "09355d3e839821ad3147be87199dd3cd608b3a1978cbd19234661f304d0a7e84" + }, + "full_after": { + "flash_text_bytes": 677680, + "flash_data_bytes": 884, + "flash_bytes": 678564, + "bss_bytes": 234564, + "elf_sha256": "5f75a4b1b699279d0746aa8c041015665948c6bc3ba02a1d1c1e146c23b7aa10" + }, + "reduced_saved_bytes": 15964 + } + }, + "additional_reduced_savings_bytes": 4384, + "prior_4631_incremental_savings_bytes": { + "compact_scalar_and_field": 14008, + "gps_version_parser": 1940, + "cli_reply_copy_compiler_policy": 16, + "total": 15964 + } + }, + "production_validation": { + "3401_full": { + "capability_markers": 14, + "passed": true + }, + "3401_reduced": { + "capability_markers": 12, + "passed": true + }, + "4631_full": { + "capability_markers": 16, + "passed": true + }, + "4631_reduced": { + "capability_markers": 14, + "passed": true + }, + "checks": [ + "LoRa OTA required by production packaging", + "Conservative RAM budget", + "Compiled Bluetooth DFU handoff", + "Application-only UF2 address range" + ] + }, + "linked_arm_checks": { + "3401": { + "passed": true, + "elf_sha256": "4f7ea9df20e9fbbcab97fc1f98d67cbaaea9fd59d79111b4c01dcf83b4338330", + "instruction_meter": "native", + "measurements": [ + { + "operation": "sign", + "vector": 1, + "message_bytes": 0, + "stack_bytes": 3512, + "instructions": 1678474 + }, + { + "operation": "sign", + "vector": 1, + "message_bytes": 96, + "stack_bytes": 3512, + "instructions": 1706221 + }, + { + "operation": "sign", + "vector": 1, + "message_bytes": 256, + "stack_bytes": 3512, + "instructions": 1733968 + }, + { + "operation": "keypair", + "vector": 1, + "stack_bytes": 2808, + "instructions": 1645602 + }, + { + "operation": "shared_secret", + "vector": 1, + "stack_bytes": 768, + "instructions": 1290321 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 1, + "stack_bytes": 3512, + "instructions": 1678524 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 96, + "stack_bytes": 3512, + "instructions": 1706221 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 256, + "stack_bytes": 3512, + "instructions": 1733968 + }, + { + "operation": "keypair", + "vector": 2, + "stack_bytes": 2808, + "instructions": 1645602 + }, + { + "operation": "shared_secret", + "vector": 2, + "stack_bytes": 768, + "instructions": 1289673 + } + ], + "scope": "Actual linked ARM crypto frames/instruction counts only; excludes caller/ISR stacks and physical timing." + }, + "4631": { + "passed": true, + "elf_sha256": "23fae794233d6df9d2c44cf490dba5ee1b4454f49d614a8291fd92e10e04eab1", + "instruction_meter": "native", + "measurements": [ + { + "operation": "sign", + "vector": 1, + "message_bytes": 0, + "stack_bytes": 3512, + "instructions": 1678474 + }, + { + "operation": "sign", + "vector": 1, + "message_bytes": 96, + "stack_bytes": 3512, + "instructions": 1706221 + }, + { + "operation": "sign", + "vector": 1, + "message_bytes": 256, + "stack_bytes": 3512, + "instructions": 1733968 + }, + { + "operation": "keypair", + "vector": 1, + "stack_bytes": 2808, + "instructions": 1645602 + }, + { + "operation": "shared_secret", + "vector": 1, + "stack_bytes": 768, + "instructions": 1290321 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 1, + "stack_bytes": 3512, + "instructions": 1678524 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 96, + "stack_bytes": 3512, + "instructions": 1706221 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 256, + "stack_bytes": 3512, + "instructions": 1733968 + }, + { + "operation": "keypair", + "vector": 2, + "stack_bytes": 2808, + "instructions": 1645602 + }, + { + "operation": "shared_secret", + "vector": 2, + "stack_bytes": 768, + "instructions": 1289673 + } + ], + "scope": "Actual linked ARM crypto frames/instruction counts only; excludes caller/ISR stacks and physical timing." + } + }, + "linked_arm_baseline_4631": { + "passed": true, + "elf_sha256": "2beff2f916231421287e0a774b6289df365336bfaa0d325a1521985b9001a18b", + "instruction_meter": "native", + "measurements": [ + { + "operation": "sign", + "vector": 1, + "message_bytes": 0, + "stack_bytes": 3312, + "instructions": 1666244 + }, + { + "operation": "sign", + "vector": 1, + "message_bytes": 96, + "stack_bytes": 3312, + "instructions": 1693991 + }, + { + "operation": "sign", + "vector": 1, + "message_bytes": 256, + "stack_bytes": 3312, + "instructions": 1721738 + }, + { + "operation": "keypair", + "vector": 1, + "stack_bytes": 2808, + "instructions": 1645602 + }, + { + "operation": "shared_secret", + "vector": 1, + "stack_bytes": 768, + "instructions": 1290329 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 1, + "stack_bytes": 3312, + "instructions": 1666294 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 96, + "stack_bytes": 3312, + "instructions": 1693991 + }, + { + "operation": "sign", + "vector": 2, + "message_bytes": 256, + "stack_bytes": 3312, + "instructions": 1721738 + }, + { + "operation": "keypair", + "vector": 2, + "stack_bytes": 2808, + "instructions": 1645602 + }, + { + "operation": "shared_secret", + "vector": 2, + "stack_bytes": 768, + "instructions": 1289681 + } + ], + "scope": "Actual linked ARM crypto frames/instruction counts only; excludes caller/ISR stacks and physical timing." + } +} diff --git a/scripts/nrf52_flash_trim.py b/scripts/nrf52_flash_trim.py index 90a48c3f4..9efa478bd 100644 --- a/scripts/nrf52_flash_trim.py +++ b/scripts/nrf52_flash_trim.py @@ -5,6 +5,9 @@ and flags: build.sh can synthesize profiles over another PlatformIO recipe. Unknown roles fail closed. No shared framework or downloaded library is edited. CDC, USB host, sensor/GPS support, BLE P-256, and stored Ed25519 keys remain. Infrastructure uses RAM-built compact Ed25519 and display-font trims. +RAK3401/RAK4631 repeaters also share CLI reply copies and use compact scalar +arithmetic and the reviewed GNSS version parser without changing protocols. +Fast field arithmetic is retained to avoid long radio-service stalls. Companion uses a 960-byte Ed25519 flash table to keep signing stack bounded, and retains all UI fonts and the Reader. The nRF52 base keeps an 8 KiB loop stack for crypto and the existing OTA path. @@ -70,6 +73,11 @@ COMPANION_TRIM_DEFINES["ED25519_SMALL_BASE"] = 1 COMPANION_TRIM_DEFINES["ED25519_COMPACT_SHA512"] = 1 COMPANION_TRIM_DEFINES["ED25519_COMPACT_SCALAR"] = 1 +RAK_REPEATER_TRIM_DEFINES = { + "ED25519_COMPACT_SCALAR": 1, + "MESH_CLI_SHARE_REPLIES": 1, +} + def flag_defines(flags=(), cppdefines=()): """Read effective defines without importing PlatformIO/SCons for tests.""" @@ -115,6 +123,15 @@ def source_selected(src_filter, source): return selected +def is_rak_repeater(mcu, src_filter, values): + """Qualify hardware and the effective role, including release overlays.""" + return (str(mcu).lower() == "nrf52840" + and any(_enabled(values.get(board)) for board in ("RAK_3401", "RAK_4631")) + and source_selected(src_filter, ROLE_SOURCES[0]) + and not any(source_selected(src_filter, source) + for source in ROLE_SOURCES[1:] + COMPANION_SOURCES)) + + def trim_policy(platform, mcu, env_name, src_filter, flags=(), cppdefines=(), ota_name_mode="front-coded"): """Return defaults for a qualified non-Companion role, otherwise nothing. @@ -141,6 +158,8 @@ def trim_policy(platform, mcu, env_name, src_filter, flags=(), cppdefines=(), if ota_name_mode != "front-coded": raise ValueError("unsupported nRF52 OTA name trim policy: " + ota_name_mode) definitions = dict(TRIM_DEFINES, OTA_TARGET_NAME_FRONT_CODED=1) + if is_rak_repeater(mcu, src_filter, values): + definitions.update(RAK_REPEATER_TRIM_DEFINES) if _enabled(values.get("MESH_OTA_LOCAL_TARGET_NAME_ONLY")): # Release overlays can compile a different PIOENV from their deployed # OTA identity. Keep the logical name/ID in that case; direct builds @@ -240,6 +259,20 @@ def compile_packed_help(build_env, node): return build_env.Object(node, **flags) +def compile_shared_cli_replies(build_env, node): + """Keep constant reply copies out of line in the qualified CLI object only.""" + source = Path(node.srcnode().get_abspath()).resolve() + expected = Path(build_env.subst("$PROJECT_DIR/src/helpers/CommonCLI.cpp")).resolve() + if source != expected: + return node + # GCC otherwise expands many constant strcpy replies into repeated stores. + # Use the same libc function and strings, with no parser or output changes. + flags = list(build_env.get("CCFLAGS", ())) + if "-fno-builtin-strcpy" not in flags: + flags.append("-fno-builtin-strcpy") + return build_env.Object(node, CCFLAGS=flags) + + def install(build_env): platform = build_env.PioPlatform() mcu = build_env.BoardConfig().get("build.mcu", "") @@ -265,6 +298,12 @@ def install(build_env): hook = importlib.util.module_from_spec(hook_spec) hook_spec.loader.exec_module(hook) hook.install(build_env) + values = flag_defines(flags, cppdefines) + rak_repeater = infrastructure and is_rak_repeater( + mcu, build_env.get("SRC_FILTER", ()), values) + if rak_repeater and _enabled(values.get("MESH_CLI_SHARE_REPLIES", + definitions.get("MESH_CLI_SHARE_REPLIES"))): + build_env.AddBuildMiddleware(compile_shared_cli_replies, "*CommonCLI.cpp") if infrastructure: names = ("local OTA target name only" if definitions.get("OTA_TARGET_NAME_TABLE") == 0 or flag_defines(flags, cppdefines).get("OTA_TARGET_NAME_TABLE") == "0" @@ -272,13 +311,14 @@ def install(build_env): print("nRF52 infrastructure flash trim: compact crypto, unused USB device classes/GPS cache/vendor splash, " + names) else: print("nRF52 Companion flash trim: small crypto table, unused USB device classes/GPS cache/vendor splash; fonts and Reader retained") + if (not infrastructure or rak_repeater) and _enabled( + values.get("MESH_NRF52_GNSS_VERSION_TRIM", "1")): hook_path = build_env.subst("$PROJECT_DIR/scripts/nrf52_gnss_version_trim.py") hook_spec = importlib.util.spec_from_file_location("mesh_nrf52_gnss_trim", hook_path) hook = importlib.util.module_from_spec(hook_spec) hook_spec.loader.exec_module(hook) hook.install(build_env) framework = platform.get_package_dir("framework-arduinoadafruitnrf52") - values = flag_defines(flags, cppdefines) if "MESH_NRF52_NO_PROGRAM_EXIT" not in values and supported_program_lifetime(framework): build_env.AppendUnique(CPPDEFINES=[("MESH_NRF52_NO_PROGRAM_EXIT", 1)]) print("nRF52 reviewed firmware lifetime: omit unreachable process-exit registrations") diff --git a/scripts/nrf52_gnss_version_trim.py b/scripts/nrf52_gnss_version_trim.py index eb3397d29..aa1e20519 100644 --- a/scripts/nrf52_gnss_version_trim.py +++ b/scripts/nrf52_gnss_version_trim.py @@ -1,7 +1,8 @@ """Keep the reviewed GNSS version parser without linking the scanf engine. -Only the qualified nRF52 Companion trim installs this middleware. Compile a -private copy of the reviewed SparkFun GNSS sources; never change downloaded dependencies. +Qualified nRF52 Companion and RAK3401/RAK4631 Repeater trims install this +middleware. Compile a private copy of the reviewed SparkFun GNSS sources; +never change downloaded dependencies. Unknown sources or headers retain their ordinary implementation. """ @@ -11,6 +12,9 @@ from pathlib import Path SOURCE_NAME = "SparkFun_u-blox_GNSS_Arduino_Library.cpp" SOURCE_SHA256 = "8ed1d313177df864b6821b3cb3e4fbcc59d850d09e2460269d571f70cd2b9f1c" +# 2.2.28 has the same version parser and headers as 2.2.29. Its only source +# differences are two file-buffer boundary checks, which this trim preserves. +SOURCE_2_2_28_SHA256 = "4c6c40fe01b15b486791d9ceb045093a394bf157489c2b22d96d7afb92789c49" HEADERS = { "SparkFun_u-blox_GNSS_Arduino_Library.h": "5da7a95ddd4b24e26ca474d017a6a5347137a31aee58fb7b450a4ce85bbcc07a", "u-blox_config_keys.h": "86cae35ec200871f748be21d6d59583c48db82621f4c5504e257e8358a241499", @@ -38,7 +42,8 @@ def digest(source): def patched_source(source): source = source.replace("\r\n", "\n") - if (digest(source) != SOURCE_SHA256 or source.count(ORIGINAL_CONVERSION) != 1 + if (digest(source) not in (SOURCE_SHA256, SOURCE_2_2_28_SHA256) + or source.count(ORIGINAL_CONVERSION) != 1 or source.count(INCLUDE) != 1): raise RuntimeError("changed reviewed SparkFun GNSS source") return source.replace(INCLUDE, "#include \n" + INCLUDE, 1).replace( @@ -63,7 +68,7 @@ def replace_gnss_source(build_env, node): raise RuntimeError("changed reviewed SparkFun GNSS header: " + name) headers[name] = header except (OSError, UnicodeError, RuntimeError) as error: - print("nRF52 Companion GNSS trim: ordinary source retained (" + str(error) + ")") + print("nRF52 GNSS trim: ordinary source retained (" + str(error) + ")") return node destination = Path(build_env.subst("$BUILD_DIR")) / "trimmed-nrf52-gnss" / source.name destination.parent.mkdir(parents=True, exist_ok=True) @@ -71,7 +76,7 @@ def replace_gnss_source(build_env, node): for name, header in headers.items(): write_if_changed(destination.parent / name, header) write_if_changed(destination, patched) - print("nRF52 Companion GNSS trim: reviewed version conversion uses strtol") + print("nRF52 GNSS trim: reviewed version conversion uses strtol") return build_env.File(str(destination)) diff --git a/test/test_nrf52_flash_trim.py b/test/test_nrf52_flash_trim.py index d1cd69251..9b4788840 100644 --- a/test/test_nrf52_flash_trim.py +++ b/test/test_nrf52_flash_trim.py @@ -67,6 +67,29 @@ class Nrf52FlashTrimTest(unittest.TestCase): self.assertNotIn("SSD1306_NO_SPLASH", defines) self.assertEqual(TRIM.flag_defines("-DMESH_NRF52_FLASH_TRIM=1 -UMESH_NRF52_FLASH_TRIM")["MESH_NRF52_FLASH_TRIM"], None) + def test_rak_repeater_trims_follow_hardware_and_effective_role(self): + for board in ("RAK_3401", "RAK_4631"): + for suffix in ("repeater", "repeater_lora_ota_no_external_sensors", + "repeater_unified_lora_ota", "repeater_bridge_rs232_serial1"): + with self.subTest(board=board, suffix=suffix): + defines = self.policy(name=board + "_" + suffix, flags="-D" + board) + for name in ("ED25519_COMPACT_SCALAR", "MESH_CLI_SHARE_REPLIES"): + self.assertEqual(defines[name], 1) + self.assertNotIn("ED25519_COMPACT_FIELD", defines) + for role in ("simple_room_server", "simple_sensor", "kiss_modem", "companion_radio"): + defines = self.policy(role=role, flags="-D" + board) + for name in TRIM.RAK_REPEATER_TRIM_DEFINES: + self.assertNotIn(name, defines) + for options in (dict(flags="-DRAK_3401=0"), dict(mcu="nrf52832", flags="-DRAK_3401"), + dict(src_filter="+<../examples/simple_repeater> +<../examples/simple_sensor>", + flags="-DRAK_4631")): + for name in TRIM.RAK_REPEATER_TRIM_DEFINES: + self.assertNotIn(name, self.policy(**options)) + defines = self.policy(flags="-DRAK_3401 -DED25519_COMPACT_SCALAR=0 " + "-UED25519_COMPACT_FIELD -DMESH_CLI_SHARE_REPLIES=0") + for name in TRIM.RAK_REPEATER_TRIM_DEFINES: + self.assertNotIn(name, defines) + def test_local_names_honor_overlay_identity_and_explicit_defines(self): flags = ('-DMESH_OTA_LOCAL_TARGET_NAME_ONLY=1 ' '-DMOTA_TARGET_ID=0x29a0da19 ' @@ -82,6 +105,25 @@ class Nrf52FlashTrimTest(unittest.TestCase): for flags in ("", "-DMESH_OTA_LOCAL_TARGET_NAME_ONLY=0"): self.assertNotIn("OTA_TARGET_NAME_TABLE", self.policy(flags=flags)) + def test_cli_reply_copy_flag_is_attached_only_to_exact_cli_object(self): + class Node: + def __init__(self, path): self.path = path + def srcnode(self): return self + def get_abspath(self): return str(self.path) + + class Env(dict): + def subst(self, value): return value.replace("$PROJECT_DIR", str(ROOT)) + def Object(self, node, **flags): return node, flags + + env = Env(CCFLAGS=["-Os", "-flto"]) + cli = Node(ROOT / "src/helpers/CommonCLI.cpp") + result, flags = TRIM.compile_shared_cli_replies(env, cli) + self.assertIs(result, cli) + self.assertEqual(flags["CCFLAGS"], ["-Os", "-flto", "-fno-builtin-strcpy"]) + self.assertEqual(env["CCFLAGS"], ["-Os", "-flto"]) + other = Node(ROOT / "test/fixtures/CommonCLI.cpp") + self.assertIs(TRIM.compile_shared_cli_replies(env, other), other) + def test_local_names_do_not_change_companion_or_unqualified_roles(self): for options in (dict(name="RAK_4631_companion_radio_full"), dict(role="custom"), dict(platform="espressif32", mcu="esp32")): diff --git a/test/test_nrf52_gnss_version_trim.py b/test/test_nrf52_gnss_version_trim.py index c633b24d4..a3e8ce1af 100644 --- a/test/test_nrf52_gnss_version_trim.py +++ b/test/test_nrf52_gnss_version_trim.py @@ -50,6 +50,13 @@ class BuildEnv: class GnssVersionTrimTests(unittest.TestCase): + def test_both_reviewed_revisions_use_the_same_bounded_transform(self): + for hash_name in ("SOURCE_SHA256", "SOURCE_2_2_28_SHA256"): + with self.subTest(revision=hash_name), patch.object(TRIM, hash_name, TRIM.digest(SOURCE)): + patched = TRIM.patched_source(SOURCE) + self.assertNotIn(TRIM.ORIGINAL_CONVERSION, patched) + self.assertIn(TRIM.VERSION_CONVERSION, patched) + def test_private_copy_checks_source_and_all_headers_and_preserves_downloads(self): # Small synthetic dependencies test the transform mechanics; production # fingerprints remain fixed to the complete reviewed library files. @@ -90,7 +97,7 @@ class GnssVersionTrimTests(unittest.TestCase): self.assertFalse(other_env.path.exists()) target.write_bytes(before) - def test_only_qualified_companion_installs_middleware(self): + def test_only_qualified_roles_install_middleware_and_opt_out_is_preserved(self): spec = importlib.util.spec_from_file_location("flash_trim", ROOT / "scripts/nrf52_flash_trim.py") policy = importlib.util.module_from_spec(spec) spec.loader.exec_module(policy) @@ -107,14 +114,22 @@ class GnssVersionTrimTests(unittest.TestCase): def subst(self, value): return value.replace("$PROJECT_DIR", str(ROOT)) def AddBuildMiddleware(self, callback, pattern): self.setdefault("middleware", []).append(pattern) - for role, flags, expected in (("companion_radio", "", True), - ("companion_radio", "-DMESH_NRF52_COMPANION_TRIM=0", False), - ("simple_repeater", "", False), ("custom", "", False)): + for role, flags, expected, shared_replies in (("companion_radio", "", True, False), + ("companion_radio", "-DMESH_NRF52_COMPANION_TRIM=0", False, False), + ("simple_repeater", "", False, False), ("custom", "", False, False), + ("simple_repeater", "-DRAK_3401", True, True), + ("simple_repeater", "-DRAK_4631", True, True), + ("simple_room_server", "-DRAK_4631", False, False), + ("simple_sensor", "-DRAK_3401", False, False), + ("simple_repeater", "-DRAK_4631 -DMESH_NRF52_GNSS_VERSION_TRIM=0", False, True), + ("simple_repeater", "-DRAK_4631 -DMESH_CLI_SHARE_REPLIES=0", True, False), + ("simple_repeater", "-DRAK_3401 -DMESH_NRF52_FLASH_TRIM=0", False, False)): with self.subTest(role=role, flags=flags): env = Env(BUILD_FLAGS=flags, CPPDEFINES=[], PIOENV="custom", SRC_FILTER="+<../examples/" + role + ">") policy.install(env) self.assertEqual("*" + TRIM.SOURCE_NAME in env.get("middleware", []), expected) + self.assertEqual("*CommonCLI.cpp" in env.get("middleware", []), shared_replies) def test_native_gnss_conversion_parity_and_bounded_cli_tokens(self): compiler = shutil.which("g++") or shutil.which("clang++") diff --git a/tools/hil/nrf52_crypto_benchmark.c b/tools/hil/nrf52_crypto_benchmark.c new file mode 100644 index 000000000..f64b9530b --- /dev/null +++ b/tools/hil/nrf52_crypto_benchmark.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include "ed_25519.h" + +#ifndef BENCH_COMPACT +#define BENCH_COMPACT 0 +#endif +#define SAMPLES 9 +#define REG32(a) (*(volatile uint32_t *)(a)) +struct Case { + uint8_t public_key[32], shared_secret[32], signatures[3][64]; + uint32_t cycles[5][SAMPLES]; +}; +struct Report { + uint32_t magic, schema, compact, cpu_hz, samples, complete, cpuid, clock_stat; + uint32_t overhead[SAMPLES]; + uint32_t wdt_run, wdt_config, wdt_crv; + struct Case cases[2]; +}; +__attribute__((used)) volatile struct Report report; +static const uint8_t seeds[2][32] = { + {0x9d,0x61,0xb1,0x9d,0xef,0xfd,0x5a,0x60,0xba,0x84,0x4a,0xf4,0x92,0xec,0x2c,0xc4,0x44,0x49,0xc5,0x69,0x7b,0x32,0x69,0x19,0x70,0x3b,0xac,0x03,0x1c,0xae,0x7f,0x60}, + {0x4c,0xcd,0x08,0x9b,0x28,0xff,0x96,0xda,0x9d,0xb6,0xc3,0x46,0xec,0x11,0x4e,0x0f,0x5b,0x8a,0x31,0x9f,0x35,0xab,0xa6,0x24,0xda,0x8c,0xf6,0xed,0x4f,0xb8,0xa6,0xfb} +}; +static uint8_t public_keys[2][32], private_keys[2][64], message[256]; +static void feed_watchdog(void) { + if (REG32(0x40010400)) { + uint32_t enabled = REG32(0x40010508); + for (unsigned n = 0; n < 8; ++n) + if (enabled & (1U << n)) REG32(0x40010600 + n*4) = 0x6E524635; + } +} +static inline void barrier(void) { __asm volatile("dsb\n isb" ::: "memory"); } +static inline uint32_t ticks(void) { barrier(); return REG32(0xE0001004); } +__attribute__((noinline,used,externally_visible)) void benchmark(void) { + /* Reset has disabled the SoftDevice. Mask all interrupts, including USB. */ + __asm volatile("cpsid i" ::: "memory"); + REG32(0xE000E010) = 0; + REG32(0xE000ED88) |= 0xF << 20; + feed_watchdog(); + REG32(0x40000000) = 1; /* HFCLKSTART */ + while ((REG32(0x4000040C) & 0x10001) != 0x10001) { feed_watchdog(); } + REG32(0xE000EDFC) |= 1 << 24; /* TRCENA */ + REG32(0xE0001004) = 0; + REG32(0xE0001000) |= 1; + barrier(); + memset((void *)&report, 0, sizeof(report)); + report.magic = 0x52414B43; + report.schema = 1; + report.compact = BENCH_COMPACT; + report.cpu_hz = 64000000; + report.samples = SAMPLES; + report.cpuid = REG32(0xE000ED00); + report.clock_stat = REG32(0x4000040C); + report.wdt_run = REG32(0x40010400); + report.wdt_config = REG32(0x4001050C); + report.wdt_crv = REG32(0x40010504); + for (unsigned i = 0; i < sizeof(message); ++i) message[i] = (i * 73 + 19) & 255; + for (unsigned i = 0; i < SAMPLES; ++i) { + uint32_t start = ticks(); + uint32_t end = ticks(); + report.overhead[i] = end - start; + } + for (unsigned v = 0; v < 2; ++v) ed25519_create_keypair(public_keys[v], private_keys[v], seeds[v]); + for (unsigned v = 0; v < 2; ++v) { + for (unsigned operation = 0; operation < 5; ++operation) { + for (unsigned n = 0; n < SAMPLES + 1; ++n) { + feed_watchdog(); + uint32_t start = ticks(); + if (operation == 0) { + ed25519_create_keypair(public_keys[v], private_keys[v], seeds[v]); + } else if (operation == 4) { + ed25519_key_exchange((uint8_t *)report.cases[v].shared_secret, public_keys[1-v], private_keys[v]); + } else { + unsigned length = operation == 1 ? 0 : (operation == 2 ? 96 : 256); + ed25519_sign((uint8_t *)report.cases[v].signatures[operation-1], message, length, public_keys[v], private_keys[v]); + } + uint32_t end = ticks(); + if (n) report.cases[v].cycles[operation][n-1] = end-start; + } + } + memcpy((void *)report.cases[v].public_key, public_keys[v], 32); + } + report.complete = 0xC0DEF17E; + barrier(); + __asm volatile("bkpt #0"); + for (;;) {} +} +__attribute__((naked,used,section(".entry"))) void bench_start(void) { + __asm volatile("cpsid i\n ldr sp, =0x20040000\n movs r0, #0\n msr control, r0\n isb\n bl benchmark\n bkpt #0\n b ."); +} diff --git a/tools/hil/nrf52_crypto_benchmark.ld b/tools/hil/nrf52_crypto_benchmark.ld new file mode 100644 index 000000000..ccc85c4a4 --- /dev/null +++ b/tools/hil/nrf52_crypto_benchmark.ld @@ -0,0 +1,9 @@ +ENTRY(bench_start) +MEMORY { RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K } +SECTIONS { + .text : { KEEP(*(.entry)) *(.text*) *(.rodata*) } > RAM + .ARM.exidx : { *(.ARM.exidx*) } > RAM + .data : { *(.data*) } > RAM + .bss (NOLOAD) : { *(.bss*) *(COMMON) } > RAM + /DISCARD/ : { *(.comment*) *(.note*) } +}