mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-22 16:41:30 +00:00
gps fiddling
This commit is contained in:
1 parent
788e4dd48b
commit
f83ce0bed4
21 files changed
+1988
-224
No files matched your search
@@ -0,0 +1,113 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# ZephCore crypto benchmark — is our crypto worth accelerating?
|
||||
#
|
||||
# Times every crypto primitive the mesh uses (SHA-256, AES-128-ECB,
|
||||
# HMAC-SHA256, Ed25519, X25519), then divides the per-packet total by LoRa
|
||||
# airtime. See HANDOVER_crypto_hw_accel.md — this tool is the "measure first"
|
||||
# step that gates every option in that document.
|
||||
#
|
||||
# It compiles src/Utils.cpp, src/Identity.cpp and src/Packet.cpp straight from
|
||||
# the main tree, so what is measured is the shipping implementation, not a
|
||||
# copy of it.
|
||||
#
|
||||
# Build:
|
||||
# west build -b rak4631 zephcore/tools/crypto_bench --pristine
|
||||
# west build -b xiao_esp32s3/esp32s3/procpu zephcore/tools/crypto_bench --pristine
|
||||
#
|
||||
# Iteration counts:
|
||||
# west build ... zephcore/tools/crypto_bench -- -DCRYPTO_BENCH_ITERS=1000
|
||||
# west build ... zephcore/tools/crypto_bench -- -DCRYPTO_BENCH_ITERS_SLOW=100
|
||||
#
|
||||
# Group-channel worst case (default 40 = MAX_CHANNELS):
|
||||
# west build ... zephcore/tools/crypto_bench -- -DCRYPTO_BENCH_CHANNELS=8
|
||||
#
|
||||
# Flash: nRF52 — drag build/zephyr/zephyr.uf2 onto the UF2 drive
|
||||
# ESP32 — west flash --esp-device COMx
|
||||
#
|
||||
# The tool never reads or writes stored identity, and never brings up the
|
||||
# radio, flash or BLE. Reflash normal firmware afterwards.
|
||||
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
# Reuse the main project's custom board definitions
|
||||
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
|
||||
|
||||
# Strip the board qualifier (e.g. "/esp32s3/procpu") to get the base name
|
||||
string(REPLACE "/" ";" BOARD_PARTS ${BOARD})
|
||||
list(GET BOARD_PARTS 0 BOARD_BASE)
|
||||
|
||||
# Tool-local board overlay ONLY — deliberately not the node's board.overlay,
|
||||
# which declares /fstab, radio, sensor and partition nodes this tool does not
|
||||
# enable (CONFIG_FLASH=n, SPI=n, I2C=n) and would fail on an undefined
|
||||
# lfs_partition label. Same arrangement as tools/rng_selftest.
|
||||
set(_BENCH_OVERLAY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay")
|
||||
if(EXISTS "${_BENCH_OVERLAY}")
|
||||
set(EXTRA_DTC_OVERLAY_FILE "${_BENCH_OVERLAY}" CACHE STRING "" FORCE)
|
||||
message(STATUS "crypto_bench overlay: ${_BENCH_OVERLAY}")
|
||||
else()
|
||||
message(STATUS "crypto_bench: using upstream board console (no overlay)")
|
||||
endif()
|
||||
|
||||
# Per-board conf (UF2 output, code partition) — nRF52 needs it, ESP32 must not
|
||||
# have it. See boards/rak4631/board.conf.
|
||||
set(_BENCH_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf")
|
||||
if(EXISTS "${_BENCH_CONF}")
|
||||
set(EXTRA_CONF_FILE "${_BENCH_CONF}" CACHE STRING "" FORCE)
|
||||
message(STATUS "crypto_bench conf: ${_BENCH_CONF}")
|
||||
endif()
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(zephcore_crypto_bench)
|
||||
|
||||
# The point of this tool is to time the SHIPPING code. These three files are
|
||||
# compiled from the main tree, at the same optimisation level the node uses,
|
||||
# so the numbers transfer.
|
||||
#
|
||||
# ZephyrRNG.cpp is deliberately NOT linked: nothing here needs entropy (all
|
||||
# test vectors are fixed, on purpose — see src/main.cpp), and pulling it in
|
||||
# would drag CONFIG_ENTROPY_GENERATOR and hwinfo into a config that otherwise
|
||||
# has no reason for them. The one thing it does that this tool needs —
|
||||
# psa_crypto_init() — main.cpp calls itself.
|
||||
target_sources(app PRIVATE
|
||||
src/main.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Utils.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Identity.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Packet.cpp
|
||||
)
|
||||
|
||||
# Ed25519 / X25519 backend — the real one Identity.cpp uses.
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../lib/monocypher monocypher)
|
||||
target_link_libraries(app PRIVATE monocypher)
|
||||
|
||||
target_include_directories(app PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../lib/monocypher
|
||||
)
|
||||
|
||||
# Iteration counts. The headline figure is the MINIMUM over N, so N mainly
|
||||
# buys confidence that no run escaped interference; 200 is plenty for the
|
||||
# symmetric primitives.
|
||||
if(NOT DEFINED CRYPTO_BENCH_ITERS)
|
||||
set(CRYPTO_BENCH_ITERS 200)
|
||||
endif()
|
||||
|
||||
# Ed25519 is ~1000x slower than SHA-256. 20 iterations keeps the total runtime
|
||||
# in seconds; the variance on an operation that long is negligible anyway.
|
||||
if(NOT DEFINED CRYPTO_BENCH_ITERS_SLOW)
|
||||
set(CRYPTO_BENCH_ITERS_SLOW 20)
|
||||
endif()
|
||||
|
||||
# Worst-case group-channel loop (Mesh.cpp:398 tries every configured channel).
|
||||
# Default matches MAX_CHANNELS.
|
||||
if(NOT DEFINED CRYPTO_BENCH_CHANNELS)
|
||||
set(CRYPTO_BENCH_CHANNELS 40)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(app PRIVATE
|
||||
CRYPTO_BENCH_ITERS=${CRYPTO_BENCH_ITERS}
|
||||
CRYPTO_BENCH_ITERS_SLOW=${CRYPTO_BENCH_ITERS_SLOW}
|
||||
CRYPTO_BENCH_CHANNELS=${CRYPTO_BENCH_CHANNELS}
|
||||
)
|
||||
message(STATUS "crypto_bench: N=${CRYPTO_BENCH_ITERS} slow=${CRYPTO_BENCH_ITERS_SLOW} channels=${CRYPTO_BENCH_CHANNELS}")
|
||||
@@ -0,0 +1,16 @@
|
||||
# RAK4631 — crypto benchmark board config
|
||||
#
|
||||
# UF2 output lives here rather than in prj.conf: it is nRF52-with-bootloader
|
||||
# specific, and on ESP32 uf2conv fails outright (no UF2 family for that SoC).
|
||||
#
|
||||
# CONFIG_FLASH is enabled ONLY so the devicetree flash partitions resolve —
|
||||
# uf2conv derives its base address from CONFIG_FLASH_LOAD_OFFSET, which
|
||||
# USE_DT_CODE_PARTITION reads from the `zephyr,code-partition` chosen node.
|
||||
# With flash off the offset comes out empty and uf2conv aborts with
|
||||
# "argument -b/--base: expected one argument".
|
||||
#
|
||||
# This does NOT let the tool touch stored data: CONFIG_FILE_SYSTEM stays off,
|
||||
# nothing mounts /lfs, and the tool never reads or writes an identity.
|
||||
CONFIG_FLASH=y
|
||||
CONFIG_USE_DT_CODE_PARTITION=y
|
||||
CONFIG_BUILD_OUTPUT_UF2=y
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* RAK4631 — crypto benchmark console overlay
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Minimal ON PURPOSE. The node's own board.overlay is NOT reused here: it
|
||||
* declares an /fstab entry, radio, sensors and partitions, none of which this
|
||||
* tool enables (CONFIG_FLASH=n, CONFIG_SPI=n ...) — pulling it in fails the
|
||||
* build on an undefined lfs_partition label. Same reasoning as the LR1110
|
||||
* updater's per-board overlays.
|
||||
*
|
||||
* RAK4631 connects USB straight to the nRF52840, so the console has to ride
|
||||
* USB CDC ACM; uart0's physical pins are not reachable on a plain WisBlock
|
||||
* base board.
|
||||
*/
|
||||
|
||||
/* Same flash layout as the node build.
|
||||
*
|
||||
* NOT just for UF2 packaging: RAK4631 boots through the Adafruit nRF52
|
||||
* bootloader, so the application must be LINKED at the bootloader's app
|
||||
* offset. Without a code partition the image links at 0 and would not run even
|
||||
* if uf2conv managed to package it (it does not — the missing
|
||||
* CONFIG_FLASH_LOAD_OFFSET is what produces "argument -b/--base: expected one
|
||||
* argument"). SoftDevice v6 layout, matching boards/nrf52840/rak4631.
|
||||
*
|
||||
* The delete-nodes are required by that dtsi's own contract (see its header):
|
||||
* it redefines partitions the upstream board DTS already labels, and without
|
||||
* removing those first the build fails with a duplicate 'storage_partition'
|
||||
* label. */
|
||||
/delete-node/ &boot_partition;
|
||||
/delete-node/ &slot0_partition;
|
||||
/delete-node/ &slot1_partition;
|
||||
/delete-node/ &storage_partition;
|
||||
|
||||
#include "../../../../boards/common/nrf52_partitions_sdv6.dtsi"
|
||||
|
||||
/ {
|
||||
chosen {
|
||||
zephyr,code-partition = &code_partition;
|
||||
zephyr,console = &cdc_acm_uart;
|
||||
zephyr,shell-uart = &cdc_acm_uart;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
&zephyr_udc0 {
|
||||
cdc_acm_uart: cdc_acm_uart {
|
||||
compatible = "zephyr,cdc-acm-uart";
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
# ZephCore crypto benchmark — minimal config.
|
||||
#
|
||||
# Mirrors the crypto block of boards/common/zephcore_common.conf EXACTLY, so
|
||||
# what is timed is the same PSA backend selection the node builds. Any
|
||||
# divergence here (a different PSA_WANT set, a different mbedTLS config) would
|
||||
# change which implementation mbedTLS picks and silently invalidate the whole
|
||||
# measurement.
|
||||
|
||||
# ========== Crypto (PSA) — copied verbatim from zephcore_common.conf ==========
|
||||
CONFIG_MBEDTLS=y
|
||||
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
|
||||
CONFIG_PSA_WANT_ALG_SHA_256=y
|
||||
CONFIG_PSA_WANT_ALG_ECB_NO_PADDING=y
|
||||
CONFIG_PSA_WANT_KEY_TYPE_AES=y
|
||||
CONFIG_PSA_WANT_ALG_HMAC=y
|
||||
CONFIG_PSA_WANT_KEY_TYPE_HMAC=y
|
||||
|
||||
# Portable CPU cycle counter — the measurement instrument. Same symbol the
|
||||
# node enables, so enabling it here does not perturb anything relative to the
|
||||
# node's own timing.
|
||||
CONFIG_TIMING_FUNCTIONS=y
|
||||
|
||||
# sys_reboot() — Utils::cryptoPanicReboot() calls it. Kept live rather than
|
||||
# stubbed: if a benchmarked primitive ever panics, the tool should reboot the
|
||||
# way the node would, not quietly print a number that means nothing.
|
||||
CONFIG_REBOOT=y
|
||||
|
||||
# C++ — Utils, Identity and Packet are C++
|
||||
CONFIG_CPP=y
|
||||
CONFIG_STD_CPP17=y
|
||||
CONFIG_REQUIRES_FULL_LIBC=y
|
||||
|
||||
# Console only, no log subsystem. All output is printk, which is synchronous —
|
||||
# important here, because an asynchronous log backend would do work between
|
||||
# timed regions and inflate the mean.
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_CONSOLE=y
|
||||
CONFIG_UART_CONSOLE=y
|
||||
CONFIG_PRINTK=y
|
||||
CONFIG_LOG=n
|
||||
|
||||
# No floating-point printf: every figure this tool prints is integer math on
|
||||
# purpose (see print_result / airtime_us). Leaving FP support off keeps the
|
||||
# image small and rules out an FP-emulation call inside a timed region.
|
||||
CONFIG_CBPRINTF_FP_SUPPORT=n
|
||||
|
||||
# CONFIG_ASSERT stays off: on ESP32 the Espressif blob trips false kswap.h
|
||||
# assertions, and this tool has no reason to differ from the node.
|
||||
CONFIG_ASSERT=n
|
||||
|
||||
# Main stack: Ed25519 verify through Monocypher plus the static test vectors
|
||||
# and the ~200-byte scratch buffers in Utils::encrypt. 4096 is generous; the
|
||||
# node runs 8192.
|
||||
CONFIG_MAIN_STACK_SIZE=4096
|
||||
|
||||
# NOTE: no CONFIG_BUILD_OUTPUT_UF2 here — it is nRF52-with-bootloader
|
||||
# specific and fails outright on ESP32. It lives in boards/rak4631/board.conf.
|
||||
|
||||
# USB CDC ACM console — nRF52840 boards (RAK4631) route console over USB
|
||||
# because the UART pins are not reachable on a plain base board. Harmless on
|
||||
# ESP32, which uses its own console and ignores these.
|
||||
CONFIG_USB_DEVICE_STACK_NEXT=y
|
||||
CONFIG_UART_LINE_CTRL=y
|
||||
CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=y
|
||||
CONFIG_CDC_ACM_SERIAL_ENABLE_AT_BOOT=y
|
||||
|
||||
# Explicitly off — this tool must not touch the radio, storage or BLE. In
|
||||
# particular it must never mount /lfs: it does not read or write any stored
|
||||
# identity, so a device under test keeps whatever it already had.
|
||||
#
|
||||
# Turning BT off matters for a second reason on nRF: CONFIG_CRYPTO_NRF_ECB
|
||||
# depends on !HAS_BT_CTLR. If anyone later extends this tool to compare a
|
||||
# hardware AES backend, that comparison is only valid against a build with the
|
||||
# same BT setting the target role actually uses — see
|
||||
# HANDOVER_crypto_hw_accel.md, "Traps and constraints".
|
||||
CONFIG_BT=n
|
||||
CONFIG_FLASH=n
|
||||
CONFIG_FILE_SYSTEM=n
|
||||
CONFIG_SPI=n
|
||||
CONFIG_I2C=n
|
||||
CONFIG_SENSOR=n
|
||||
CONFIG_GNSS=n
|
||||
@@ -0,0 +1,524 @@
|
||||
/*
|
||||
* ZephCore crypto benchmark
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Answers one question: does ZephCore's crypto cost enough to be worth
|
||||
* accelerating? See HANDOVER_crypto_hw_accel.md — every option in that
|
||||
* document (a second Zephyr-crypto-API backend in Utils.cpp, nrfxlib PSA
|
||||
* drivers in west.yml, or the free software wins) is gated on numbers that
|
||||
* nobody has ever taken.
|
||||
*
|
||||
* WHY A SEPARATE IMAGE rather than a CLI command on a live node:
|
||||
* the measurement has to be free of mesh-loop, BLE and LoRa-RX interference,
|
||||
* and it runs each primitive hundreds of times back to back — which would
|
||||
* starve exactly those subsystems. Same reasoning as tools/rng_selftest.
|
||||
*
|
||||
* WHAT IT MEASURES, and why the answer is a RATIO not a number:
|
||||
* the absolute cost of SHA-256 is uninteresting on its own. What decides the
|
||||
* question is crypto time as a fraction of packet AIRTIME — at SF11/250 kHz a
|
||||
* packet occupies the channel for hundreds of milliseconds, so a per-packet
|
||||
* crypto bill in the tens of microseconds is noise, and the whole hardware
|
||||
* exploration closes with a documented "no". The final table does that
|
||||
* division for you, per LoRa preset, using the SAME airtime formula the node
|
||||
* uses (LoRaRadioBase::getAirtimeMillis) so the comparison is apples to
|
||||
* apples.
|
||||
*
|
||||
* It measures the SHIPPING code: src/Utils.cpp, src/Identity.cpp and
|
||||
* src/Packet.cpp are compiled straight from the main tree, not copied. What
|
||||
* is timed here is byte-for-byte what the node runs.
|
||||
*
|
||||
* The tool never reads or writes stored identity, and never touches the
|
||||
* radio, flash or BLE. A device under test keeps whatever key it had.
|
||||
*/
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/sys/printk.h>
|
||||
#include <zephyr/timing/timing.h>
|
||||
#include <psa/crypto.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/Identity.h>
|
||||
#include <mesh/Packet.h>
|
||||
|
||||
using namespace mesh;
|
||||
|
||||
#ifndef CRYPTO_BENCH_ITERS
|
||||
#define CRYPTO_BENCH_ITERS 200
|
||||
#endif
|
||||
|
||||
/* Ed25519 is ~3 orders of magnitude slower than the symmetric primitives.
|
||||
* At the default iteration count a full sweep would take minutes and tell us
|
||||
* nothing extra — the variance on an operation that long is negligible. */
|
||||
#ifndef CRYPTO_BENCH_ITERS_SLOW
|
||||
#define CRYPTO_BENCH_ITERS_SLOW 20
|
||||
#endif
|
||||
|
||||
/* Payload sizes. 184 is MAX_PACKET_PAYLOAD; calculatePacketHash hashes
|
||||
* 1 + payload_len, so the hash input tops out at 185. 60 is a typical text
|
||||
* message, 100 a typical advert. */
|
||||
#define PAYLOAD_TYPICAL 60
|
||||
#define PAYLOAD_ADVERT 100
|
||||
#define PAYLOAD_MAX MAX_PACKET_PAYLOAD
|
||||
|
||||
/* Worst case for the group-channel loop at Mesh.cpp:398. */
|
||||
#ifndef CRYPTO_BENCH_CHANNELS
|
||||
#define CRYPTO_BENCH_CHANNELS 40
|
||||
#endif
|
||||
|
||||
/* Anti-elision. Without a visible consumer the optimiser is entitled to
|
||||
* delete calls whose output is unused, and at -Os it does.
|
||||
*
|
||||
* It reads only the first and last byte, NOT the whole buffer: the sink runs
|
||||
* INSIDE the timed region, and summing 176 bytes would add real work to every
|
||||
* AES measurement. Two bytes is enough — the compiler cannot prove the rest
|
||||
* of the buffer was unnecessary to produce them. */
|
||||
static volatile uint32_t g_sink;
|
||||
|
||||
static inline void sink(const uint8_t *p, size_t n)
|
||||
{
|
||||
g_sink += (uint32_t)p[0] + (uint32_t)p[n - 1];
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- timing */
|
||||
|
||||
/* Zephyr's portable cycle counter (DWT on Cortex-M, CCOUNT on Xtensa) — the
|
||||
* same mechanism ZephyrRNG's two-clock beat uses. See
|
||||
* memory/zephyrrng-entropy.md for the platform traps.
|
||||
*
|
||||
* MIN, not mean, is the headline figure: every perturbation (interrupt,
|
||||
* cache miss, flash wait state) can only ADD time, so the minimum over N runs
|
||||
* is the closest estimate of the true cost. Mean is printed alongside because
|
||||
* a large min/mean gap is itself information — it says the operation is being
|
||||
* interfered with, which matters on a node where it shares a CPU with the
|
||||
* radio ISR. */
|
||||
struct BenchResult {
|
||||
const char *name;
|
||||
uint64_t min_ns;
|
||||
uint64_t mean_ns;
|
||||
int iters;
|
||||
};
|
||||
|
||||
static uint64_t cycles_to_ns(uint64_t cycles)
|
||||
{
|
||||
return timing_cycles_to_ns(cycles);
|
||||
}
|
||||
|
||||
/* Parameter names are underscore-prefixed on purpose: the preprocessor
|
||||
* substitutes macro arguments after `.` too, so a parameter called `iters`
|
||||
* would rewrite the `(dst).iters` member access into `(dst).200`. */
|
||||
#define BENCH(dst, _label, _n, stmt) \
|
||||
do { \
|
||||
uint64_t _min = UINT64_MAX, _sum = 0; \
|
||||
for (int _i = 0; _i < (_n); _i++) { \
|
||||
timing_t _t0 = timing_counter_get(); \
|
||||
{ stmt; } \
|
||||
timing_t _t1 = timing_counter_get(); \
|
||||
uint64_t _c = timing_cycles_get(&_t0, &_t1); \
|
||||
if (_c < _min) _min = _c; \
|
||||
_sum += _c; \
|
||||
} \
|
||||
(dst).name = (_label); \
|
||||
(dst).min_ns = cycles_to_ns(_min); \
|
||||
(dst).mean_ns = cycles_to_ns(_sum / (uint64_t)(_n)); \
|
||||
(dst).iters = (_n); \
|
||||
} while (0)
|
||||
|
||||
/* us with 3 decimals, integer math — no CBPRINTF_FP_SUPPORT needed. */
|
||||
static void print_result(const BenchResult *r)
|
||||
{
|
||||
uint64_t mn = r->min_ns, mu = r->mean_ns;
|
||||
printk(" %-34s %6llu.%03llu %8llu.%03llu %4d\n",
|
||||
r->name, mn / 1000u, mn % 1000u, mu / 1000u, mu % 1000u, r->iters);
|
||||
}
|
||||
|
||||
static void print_header(const char *section)
|
||||
{
|
||||
printk("\n%s\n", section);
|
||||
printk(" %-34s %10s %12s %4s\n", "operation", "min us", "mean us", "N");
|
||||
printk(" ---------------------------------- ---------- ------------ ----\n");
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- airtime */
|
||||
|
||||
/* Integer port of LoRaRadioBase::getAirtimeMillis(), in microseconds.
|
||||
* Deliberately mirrors that function line for line — including the
|
||||
* preamble-length rule and the LDRO threshold tied to symbol time (not to
|
||||
* SF), because a divergence here would silently distort the only comparison
|
||||
* this tool exists to make. */
|
||||
static uint32_t preamble_syms(uint8_t sf)
|
||||
{
|
||||
return (sf <= 8) ? 32u : 16u; /* PR #1954 parity */
|
||||
}
|
||||
|
||||
static uint64_t airtime_us(uint8_t sf, uint32_t bw_hz, uint8_t cr, uint32_t len)
|
||||
{
|
||||
/* Tsym in ns, to keep the LDRO comparison exact at narrow BW. */
|
||||
uint64_t tsym_ns = ((uint64_t)(1u << sf) * 1000000000ull) / bw_hz;
|
||||
|
||||
/* 4.25 preamble symbols -> multiply by 425, divide by 100. */
|
||||
uint64_t t_pre_ns = ((uint64_t)preamble_syms(sf) * 100ull + 425ull) * tsym_ns / 100ull;
|
||||
|
||||
int de = (tsym_ns > 16380000ull) ? 1 : 0; /* > 16.38 ms */
|
||||
|
||||
int64_t num = 8ll * (int64_t)len - 4ll * sf + 28 + 16;
|
||||
int64_t den = 4ll * ((int64_t)sf - 2ll * de);
|
||||
if (den < 1) den = 4;
|
||||
int64_t ceil_div = (num + den - 1) / den;
|
||||
int64_t n_pay = 8 + (ceil_div * (int64_t)cr > 0 ? ceil_div * (int64_t)cr : 0);
|
||||
|
||||
return (t_pre_ns + (uint64_t)n_pay * tsym_ns) / 1000ull;
|
||||
}
|
||||
|
||||
struct Preset {
|
||||
const char *name;
|
||||
uint8_t sf;
|
||||
uint32_t bw_hz;
|
||||
uint8_t cr; /* 5..8, used as (cr-4+4) == cr in the formula */
|
||||
};
|
||||
|
||||
static const Preset presets[] = {
|
||||
{ "SF7 BW62.5k CR4/5", 7, 62500, 5 }, /* EmpireMesh, since 2026-07-25 */
|
||||
{ "SF8 BW62.5k CR4/8", 8, 62500, 8 }, /* EmpireMesh, before that */
|
||||
{ "SF10 BW125k CR4/5", 10, 125000, 5 },
|
||||
{ "SF11 BW250k CR4/5", 11, 250000, 5 }, /* MeshCore default */
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ main */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
k_msleep(2000); /* USB CDC enumerates after boot on some boards */
|
||||
|
||||
timing_init();
|
||||
timing_start();
|
||||
|
||||
/* Utils.cpp assumes PSA is initialised. On a node that happens inside
|
||||
* ZephyrRNG (adapters/rng/ZephyrRNG.cpp:285), which this tool
|
||||
* deliberately does not link — so do it here, and fail loudly rather
|
||||
* than silently benchmarking a pile of PSA_ERROR returns. */
|
||||
psa_status_t ps = psa_crypto_init();
|
||||
if (ps != PSA_SUCCESS) {
|
||||
printk("FATAL: psa_crypto_init failed: %d\n", (int)ps);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printk("\n");
|
||||
printk("========================================================\n");
|
||||
printk(" ZephCore crypto benchmark\n");
|
||||
printk(" board: %s N=%d (slow ops N=%d)\n",
|
||||
CONFIG_BOARD, CRYPTO_BENCH_ITERS, CRYPTO_BENCH_ITERS_SLOW);
|
||||
printk("========================================================\n");
|
||||
|
||||
/* Test vectors. Fixed, not random: this is a timing measurement and a
|
||||
* deterministic input makes runs comparable across boards and builds.
|
||||
* Ed25519/X25519 timing is data-dependent at the margins, which the
|
||||
* min-over-N already absorbs. */
|
||||
static uint8_t buf[1 + MAX_PACKET_PAYLOAD + CIPHER_BLOCK_SIZE];
|
||||
static uint8_t out[MAX_PACKET_PAYLOAD + CIPHER_BLOCK_SIZE * 2];
|
||||
static uint8_t key[PUB_KEY_SIZE];
|
||||
static uint8_t hash[MAX_HASH_SIZE];
|
||||
static uint8_t sig[SIGNATURE_SIZE];
|
||||
static uint8_t secret[CIPHER_KEY_SIZE * 2];
|
||||
|
||||
for (size_t i = 0; i < sizeof(buf); i++) buf[i] = (uint8_t)(i * 7 + 3);
|
||||
for (size_t i = 0; i < sizeof(key); i++) key[i] = (uint8_t)(i * 11 + 5);
|
||||
|
||||
uint8_t seed[SEED_SIZE];
|
||||
for (size_t i = 0; i < sizeof(seed); i++) seed[i] = (uint8_t)(i * 13 + 1);
|
||||
LocalIdentity self;
|
||||
self.fromSeed(seed);
|
||||
|
||||
uint8_t seed2[SEED_SIZE];
|
||||
for (size_t i = 0; i < sizeof(seed2); i++) seed2[i] = (uint8_t)(i * 17 + 9);
|
||||
LocalIdentity peer;
|
||||
peer.fromSeed(seed2);
|
||||
|
||||
self.sign(sig, buf, PAYLOAD_ADVERT);
|
||||
if (!self.verify(sig, buf, PAYLOAD_ADVERT)) {
|
||||
printk("FATAL: sign/verify self-check failed — results would be "
|
||||
"meaningless\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
BenchResult r;
|
||||
|
||||
/* ---- instrument baseline ---------------------------------------- */
|
||||
print_header("Instrument baseline — subtract this from everything below");
|
||||
|
||||
/* The cost of the two timing_counter_get() calls and the sink, with no
|
||||
* crypto between them. Every figure in this run carries it. If it is
|
||||
* not small compared to the fastest operation measured, the fastest
|
||||
* rows are measuring the ruler, not the thing. */
|
||||
BENCH(r, "empty (timer pair + sink)", CRYPTO_BENCH_ITERS,
|
||||
sink(buf, 16));
|
||||
print_result(&r);
|
||||
uint64_t overhead = r.min_ns;
|
||||
|
||||
/* ---- primitives: SHA-256 ---------------------------------------- */
|
||||
print_header("SHA-256 (PSA one-shot) — the dedup hash");
|
||||
|
||||
BENCH(r, "sha256, 16 B", CRYPTO_BENCH_ITERS,
|
||||
Utils::sha256(hash, MAX_HASH_SIZE, buf, 16); sink(hash, MAX_HASH_SIZE));
|
||||
print_result(&r);
|
||||
uint64_t sha_16 = r.min_ns;
|
||||
|
||||
BENCH(r, "sha256, 61 B (typical msg pkt)", CRYPTO_BENCH_ITERS,
|
||||
Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + PAYLOAD_TYPICAL); sink(hash, MAX_HASH_SIZE));
|
||||
print_result(&r);
|
||||
uint64_t sha_typical = r.min_ns;
|
||||
|
||||
BENCH(r, "sha256, 101 B (typical advert)", CRYPTO_BENCH_ITERS,
|
||||
Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + PAYLOAD_ADVERT); sink(hash, MAX_HASH_SIZE));
|
||||
print_result(&r);
|
||||
|
||||
BENCH(r, "sha256, 185 B (max payload)", CRYPTO_BENCH_ITERS,
|
||||
Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + PAYLOAD_MAX); sink(hash, MAX_HASH_SIZE));
|
||||
print_result(&r);
|
||||
uint64_t sha_max = r.min_ns;
|
||||
|
||||
BENCH(r, "sha256, 2-frag (32+32 B)", CRYPTO_BENCH_ITERS,
|
||||
Utils::sha256(hash, MAX_HASH_SIZE, buf, 32, buf + 32, 32); sink(hash, MAX_HASH_SIZE));
|
||||
print_result(&r);
|
||||
|
||||
/* ---- primitives: AES ------------------------------------------- */
|
||||
print_header("AES-128-ECB (PSA, key imported+destroyed per call)");
|
||||
|
||||
BENCH(r, "encrypt, 16 B (1 block)", CRYPTO_BENCH_ITERS,
|
||||
int n = Utils::encrypt(key, out, buf, 16); sink(out, n > 0 ? n : 1));
|
||||
print_result(&r);
|
||||
uint64_t aes_1blk = r.min_ns;
|
||||
|
||||
BENCH(r, "encrypt, 60 B (4 blocks)", CRYPTO_BENCH_ITERS,
|
||||
int n = Utils::encrypt(key, out, buf, PAYLOAD_TYPICAL); sink(out, n > 0 ? n : 1));
|
||||
print_result(&r);
|
||||
|
||||
BENCH(r, "encrypt, 176 B (11 blocks)", CRYPTO_BENCH_ITERS,
|
||||
int n = Utils::encrypt(key, out, buf, 176); sink(out, n > 0 ? n : 1));
|
||||
print_result(&r);
|
||||
uint64_t aes_max = r.min_ns;
|
||||
|
||||
/* decrypt needs valid ciphertext-shaped input; ECB accepts anything
|
||||
* block-aligned, and the timing does not depend on the plaintext. */
|
||||
BENCH(r, "decrypt, 176 B (11 blocks)", CRYPTO_BENCH_ITERS,
|
||||
int n = Utils::decrypt(key, out, buf, 176); sink(out, n > 0 ? n : 1));
|
||||
print_result(&r);
|
||||
|
||||
/* ---- the per-call key import ------------------------------------ */
|
||||
print_header("PSA key handling — the per-call import/destroy overhead");
|
||||
|
||||
/* Isolates what src/Utils.cpp:86,101 and :149,159 spend on key
|
||||
* lifecycle ALONE, with no cipher or MAC work attached. This is the
|
||||
* number that decides whether caching a key handle per channel secret
|
||||
* is worth changing Utils.cpp for: compare it against "AES encrypt,
|
||||
* 16 B" above. If import+destroy dominates a one-block encrypt, the
|
||||
* group-channel loop is paying for key management, not crypto. */
|
||||
BENCH(r, "psa_import_key + psa_destroy_key", CRYPTO_BENCH_ITERS, {
|
||||
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
|
||||
psa_set_key_bits(&attr, CIPHER_KEY_SIZE * 8);
|
||||
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT);
|
||||
psa_set_key_algorithm(&attr, PSA_ALG_ECB_NO_PADDING);
|
||||
psa_key_id_t kid;
|
||||
if (psa_import_key(&attr, key, CIPHER_KEY_SIZE, &kid) == PSA_SUCCESS) {
|
||||
g_sink += (uint32_t)kid;
|
||||
psa_destroy_key(kid);
|
||||
}
|
||||
});
|
||||
print_result(&r);
|
||||
uint64_t key_import = r.min_ns;
|
||||
|
||||
/* ---- HMAC, by subtraction --------------------------------------- */
|
||||
print_header("HMAC-SHA256 (via encryptThenMAC / MACThenDecrypt)");
|
||||
|
||||
/* compute_hmac_truncated() is static in Utils.cpp, so it cannot be
|
||||
* called directly and is NOT worth un-staticing for a benchmark. It is
|
||||
* derived instead: encryptThenMAC = encrypt + HMAC, and the failing
|
||||
* MACThenDecrypt path is HMAC + a constant-time compare and nothing
|
||||
* else (it returns before decrypt on MAC mismatch) — so the mismatch
|
||||
* case below is very nearly a direct HMAC measurement. */
|
||||
BENCH(r, "encryptThenMAC, 60 B (TX path)", CRYPTO_BENCH_ITERS,
|
||||
int n = Utils::encryptThenMAC(key, out, buf, PAYLOAD_TYPICAL); sink(out, n > 0 ? n : 1));
|
||||
print_result(&r);
|
||||
uint64_t etm_typical = r.min_ns;
|
||||
|
||||
BENCH(r, "encryptThenMAC, 176 B (TX path)", CRYPTO_BENCH_ITERS,
|
||||
int n = Utils::encryptThenMAC(key, out, buf, 176); sink(out, n > 0 ? n : 1));
|
||||
print_result(&r);
|
||||
|
||||
/* A real ciphertext, so the success path actually reaches decrypt. */
|
||||
static uint8_t ct[MAX_PACKET_PAYLOAD + CIPHER_BLOCK_SIZE * 2];
|
||||
int ct_len = Utils::encryptThenMAC(key, ct, buf, PAYLOAD_TYPICAL);
|
||||
if (ct_len <= 0) {
|
||||
printk("FATAL: encryptThenMAC produced nothing\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
BENCH(r, "MACThenDecrypt, MATCH (RX ours)", CRYPTO_BENCH_ITERS,
|
||||
int n = Utils::MACThenDecrypt(key, out, ct, ct_len); sink(out, n > 0 ? n : 1));
|
||||
print_result(&r);
|
||||
uint64_t mtd_match = r.min_ns;
|
||||
|
||||
/* Wrong key -> MAC mismatch -> returns before AES. This is the cost of
|
||||
* ONE non-matching channel in the Mesh.cpp:398 loop. */
|
||||
static uint8_t wrong_key[PUB_KEY_SIZE];
|
||||
for (size_t i = 0; i < sizeof(wrong_key); i++) wrong_key[i] = (uint8_t)(i * 3 + 200);
|
||||
|
||||
BENCH(r, "MACThenDecrypt, MISS (wrong chan)", CRYPTO_BENCH_ITERS,
|
||||
int n = Utils::MACThenDecrypt(wrong_key, out, ct, ct_len); g_sink += (uint32_t)n);
|
||||
print_result(&r);
|
||||
uint64_t mtd_miss = r.min_ns;
|
||||
|
||||
/* ---- asymmetric -------------------------------------------------- */
|
||||
print_header("Ed25519 / X25519 (Monocypher, software only)");
|
||||
|
||||
BENCH(r, "Ed25519 verify (advert RX)", CRYPTO_BENCH_ITERS_SLOW,
|
||||
bool ok = self.verify(sig, buf, PAYLOAD_ADVERT); g_sink += ok ? 1 : 0);
|
||||
print_result(&r);
|
||||
uint64_t ed_verify = r.min_ns;
|
||||
|
||||
BENCH(r, "Ed25519 sign (advert TX)", CRYPTO_BENCH_ITERS_SLOW,
|
||||
self.sign(sig, buf, PAYLOAD_ADVERT); sink(sig, SIGNATURE_SIZE));
|
||||
print_result(&r);
|
||||
|
||||
BENCH(r, "X25519 shared secret (contact)", CRYPTO_BENCH_ITERS_SLOW,
|
||||
self.calcSharedSecret(secret, peer); sink(secret, sizeof(secret)));
|
||||
print_result(&r);
|
||||
|
||||
/* ---- composites: real call paths -------------------------------- */
|
||||
print_header("Per-packet totals — the real RX/TX call paths");
|
||||
|
||||
/* Dedup as implemented today. SimpleMeshTables::wasSeen() and
|
||||
* markSeen() each call packet->calculatePacketHash() independently,
|
||||
* and since the 2026-07-02 hasSeen split every caller invokes markSeen
|
||||
* right after wasSeen returns false — so a NEW packet hashes twice.
|
||||
* The linear memcmp scan those functions also do is not crypto and is
|
||||
* not timed here; it is a 160-entry 8-byte compare, nowhere near the
|
||||
* hash. Packet.cpp is compiled from the main tree, so the buffer
|
||||
* assembly (the memcpy into a stack buffer) is included, as it is on a
|
||||
* node. */
|
||||
/* Not `static`: a function-local static with a non-trivial constructor
|
||||
* emits __cxa_guard_acquire/release, which Zephyr's minimal C++ runtime
|
||||
* does not provide. ~260 bytes on the stack instead. */
|
||||
Packet pkt;
|
||||
pkt.header = (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT) | ROUTE_TYPE_FLOOD;
|
||||
pkt.payload_len = PAYLOAD_TYPICAL;
|
||||
memcpy(pkt.payload, buf, PAYLOAD_TYPICAL);
|
||||
|
||||
BENCH(r, "dedup, 1x calculatePacketHash", CRYPTO_BENCH_ITERS,
|
||||
pkt.calculatePacketHash(hash); sink(hash, MAX_HASH_SIZE));
|
||||
print_result(&r);
|
||||
uint64_t dedup_1 = r.min_ns;
|
||||
|
||||
BENCH(r, "dedup, 2x (wasSeen + markSeen)", CRYPTO_BENCH_ITERS,
|
||||
pkt.calculatePacketHash(hash); sink(hash, MAX_HASH_SIZE);
|
||||
pkt.calculatePacketHash(hash); sink(hash, MAX_HASH_SIZE));
|
||||
print_result(&r);
|
||||
uint64_t dedup_2 = r.min_ns;
|
||||
|
||||
/* Group packet that matches no configured channel: the loop runs every
|
||||
* channel and every one costs a full HMAC. */
|
||||
BENCH(r, "group RX, " STRINGIFY(CRYPTO_BENCH_CHANNELS) " channel misses",
|
||||
CRYPTO_BENCH_ITERS / 4,
|
||||
for (int c = 0; c < CRYPTO_BENCH_CHANNELS; c++) {
|
||||
int n = Utils::MACThenDecrypt(wrong_key, out, ct, ct_len);
|
||||
g_sink += (uint32_t)n;
|
||||
});
|
||||
print_result(&r);
|
||||
uint64_t group_miss_all = r.min_ns;
|
||||
|
||||
/* ---- the verdict table ------------------------------------------ */
|
||||
printk("\n");
|
||||
printk("========================================================\n");
|
||||
printk(" PER-PACKET CRYPTO BUDGET vs AIRTIME\n");
|
||||
printk("--------------------------------------------------------\n");
|
||||
|
||||
struct Scenario {
|
||||
const char *name;
|
||||
uint64_t ns;
|
||||
uint32_t pkt_bytes;
|
||||
};
|
||||
|
||||
/* Packet byte counts are the on-air length the airtime formula wants:
|
||||
* 2 header bytes + path + payload. Path is assumed 3 hops x 1-byte
|
||||
* hash, a common flood. */
|
||||
const uint32_t hdr_path = 2 + 3;
|
||||
|
||||
const Scenario scen[] = {
|
||||
{ "flood not for us (dedup only)",
|
||||
dedup_2, hdr_path + PAYLOAD_TYPICAL },
|
||||
{ "direct msg for us (dedup+MAC+AES)",
|
||||
dedup_2 + mtd_match, hdr_path + PAYLOAD_TYPICAL },
|
||||
{ "group msg, no channel matches",
|
||||
dedup_2 + group_miss_all, hdr_path + PAYLOAD_TYPICAL },
|
||||
{ "advert (dedup + Ed25519 verify)",
|
||||
dedup_2 + ed_verify, hdr_path + PAYLOAD_ADVERT },
|
||||
{ "our TX (encryptThenMAC + dedup)",
|
||||
dedup_1 + etm_typical, hdr_path + PAYLOAD_TYPICAL },
|
||||
};
|
||||
|
||||
for (size_t p = 0; p < ARRAY_SIZE(presets); p++) {
|
||||
printk("\n %s\n", presets[p].name);
|
||||
printk(" %-36s %9s %9s %8s\n",
|
||||
"scenario", "crypto us", "air us", "share");
|
||||
printk(" ------------------------------------ --------- --------- --------\n");
|
||||
for (size_t s = 0; s < ARRAY_SIZE(scen); s++) {
|
||||
uint64_t air = airtime_us(presets[p].sf, presets[p].bw_hz,
|
||||
presets[p].cr, scen[s].pkt_bytes);
|
||||
uint64_t cry_us = scen[s].ns / 1000u;
|
||||
/* share in hundredths of a percent (basis points) */
|
||||
uint64_t bp = air ? (scen[s].ns * 10000ull) / (air * 1000ull) : 0;
|
||||
printk(" %-36s %9llu %9llu %3llu.%02llu%%\n",
|
||||
scen[s].name, cry_us, air, bp / 100u, bp % 100u);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- what the software wins would buy --------------------------- */
|
||||
printk("\n");
|
||||
printk("--------------------------------------------------------\n");
|
||||
printk(" HEADROOM OF THE FREE SOFTWARE WINS (no hardware needed)\n");
|
||||
printk("--------------------------------------------------------\n");
|
||||
printk(" single-hash dedup saves : %llu us/packet\n",
|
||||
(dedup_2 - dedup_1) / 1000u);
|
||||
printk(" cached key handle would save up to: %llu us/packet\n",
|
||||
((uint64_t)CRYPTO_BENCH_CHANNELS * key_import) / 1000u);
|
||||
printk(" (%d channels x %llu.%03llu us import/destroy)\n",
|
||||
CRYPTO_BENCH_CHANNELS, key_import / 1000u, key_import % 1000u);
|
||||
printk(" key import as share of a 1-block AES: %llu%%\n",
|
||||
aes_1blk ? (key_import * 100u) / aes_1blk : 0);
|
||||
|
||||
printk("\n");
|
||||
printk("--------------------------------------------------------\n");
|
||||
printk(" HOW TO READ THIS\n");
|
||||
printk("--------------------------------------------------------\n");
|
||||
printk(" The 'share' column is the whole question. Crypto competes\n");
|
||||
printk(" with airtime, not with itself. If every scenario sits well\n");
|
||||
printk(" under 1%%, hardware acceleration cannot buy throughput the\n");
|
||||
printk(" radio would ever notice, and the honest answer to\n");
|
||||
printk(" HANDOVER_crypto_hw_accel.md is Option C (do nothing) --\n");
|
||||
printk(" regardless of how large the microsecond figures look.\n");
|
||||
printk(" Only a scenario in the double-digit percent range, or one\n");
|
||||
printk(" whose absolute cost approaches the inter-packet gap,\n");
|
||||
printk(" justifies going further.\n");
|
||||
printk(" Power is NOT measured here. A CPU-bound millisecond also\n");
|
||||
printk(" costs battery; if the share numbers are borderline, measure\n");
|
||||
printk(" current draw before deciding.\n");
|
||||
|
||||
printk("\n");
|
||||
printk("========================================================\n");
|
||||
printk(" reference: sha16=%llu.%03llu us sha185=%llu.%03llu us\n",
|
||||
sha_16 / 1000u, sha_16 % 1000u, sha_max / 1000u, sha_max % 1000u);
|
||||
printk(" reference: aes1blk=%llu.%03llu us aes176=%llu.%03llu us\n",
|
||||
aes_1blk / 1000u, aes_1blk % 1000u, aes_max / 1000u, aes_max % 1000u);
|
||||
printk(" reference: hmac_miss=%llu.%03llu us ed_verify=%llu.%03llu us\n",
|
||||
mtd_miss / 1000u, mtd_miss % 1000u, ed_verify / 1000u, ed_verify % 1000u);
|
||||
printk(" instrument overhead=%llu ns (already included in every row)\n",
|
||||
overhead);
|
||||
printk(" (sha_typical=%llu ns, sink=%u — ignore, anti-elision)\n",
|
||||
sha_typical, (unsigned)g_sink);
|
||||
printk("========================================================\n");
|
||||
printk("\n[crypto_bench] done — halting. Reflash normal firmware.\n");
|
||||
|
||||
timing_stop();
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user