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.
This commit is contained in:
mikecarper committed 2026-10-27 11:41:32 -07:00
1 parent 308fec1162
commit 08b1c8826c
9 files changed
+1256 -10

No files matched your search

+5
View File
@@ -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
@@ -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.
@@ -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."
}
}
+41 -1
View File
@@ -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")
+10 -5
View File
@@ -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 <stdlib.h>\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))
+42
View File
@@ -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")):
+19 -4
View File
@@ -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++")
+92
View File
@@ -0,0 +1,92 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#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 .");
}
+9
View File
@@ -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*) }
}