Runs smoke tests across the platforms multicalc supports: x86_64 and aarch64 Linux
hosts, plus four bare-metal ABIs (Cortex-M4 soft-float, Cortex-M4 hard-float,
Cortex-M0, and riscv32imc). Host tests cover the first two. Nothing but real
on-target execution can cover the bare-metal set, and that is this crate. Each result
must agree with a known answer to a tolerance — fixture abs/rel bands on-target, 1e-9
relative between ABIs — not bit-for-bit, since soft-float paths round intermediates
differently. The two Cortex-M4 ABIs and riscv32imc run the full check set; the
Cortex-M0 runs a smaller canary (it builds with --no-default-features). A divergence
beyond tolerance on any leg fails the build.
It hosts a no_std / no_main smoke test that runs multicalc on bare-metal targets
under QEMU. It is a dev-only crate (publish = false, not in default-members) and
is never built for a host target. cortex-m-rt links for thumb* triples; riscv-rt
links for riscv32*.
rustup target add thumbv7em-none-eabi thumbv7em-none-eabihf thumbv6m-none-eabi riscv32imc-unknown-none-elf
sudo apt-get install -y qemu-system-arm qemu-system-misc # provides qemu-system-arm
cargo install cargo-binutils # provides cargo size, for the gate
cargo run -p embedded-smoke --release --target thumbv7em-none-eabi
cargo run -p embedded-smoke --release --target thumbv7em-none-eabihf
cargo run -p embedded-smoke --release --target thumbv6m-none-eabi --no-default-features
cargo run -p embedded-smoke --release --target riscv32imc-unknown-none-elfAliases: cargo smoke-eabi, cargo smoke-eabihf, cargo smoke-m0, cargo smoke-riscv.
| Target | Codegen | QEMU machine | RAM |
|---|---|---|---|
thumbv7em-none-eabi |
Cortex-M4, soft-float | netduinoplus2 |
64K |
thumbv7em-none-eabihf |
Cortex-M4, hard-float | netduinoplus2 |
64K |
thumbv6m-none-eabi |
Cortex-M0, CAS-free | microbit |
16K |
riscv32imc-unknown-none-elf |
RV32IMC, soft-float | virt (-bios none) |
256K |
The thumbv7em ABIs run on netduinoplus2 (Cortex-M4, FPU, 64K RAM, flash at
0x08000000). thumbv6m runs on microbit (Cortex-M0, nRF51, 16K RAM, flash
at 0x0) — a real M0 core, so the run now asserts both RAM-size and ISA
fidelity: an oversized image or an out-of-ISA (ARMv7E-M) instruction faults just
as it would on silicon. riscv32imc runs on QEMU virt with -bios none
(DRAM at 0x80000000). build.rs picks each target's memory map.
The runners and rustflags (-Tlink.x, --nmagic) live in
.cargo/config.toml; the per-target memory map is supplied by build.rs.
These smoke tests cannot live next to the code they check. This is a full
program for a bare chip, not a normal test. It brings its own start-up code, its
own crash handler, and a custom link step, and it needs chip-only crates (like
cortex-m-rt) that only build for the chip targets. Pulling those into
multicalc would break it on a normal computer and drag chip-only dependencies
and build settings into a library that ships to crates.io. Keeping the two apart lets multicalc
stay small, safe, and portable, while this crate stays dev-only and never ships.
src/checks.rs holds the on-target cases against the public API. Each asserts a
known answer to a tolerance, so a wrong result panics instead of passing, and
every check stays no_std. Golden checks assert against values taken from the
host QA fixtures (src/fixtures.rs, generated from tools/qa); identity
checks assert a mathematical identity that needs no fixture.
The set is tiered by the full-smoke feature (on by default): the canary runs on
every target including the Cortex-M0; the full set adds the heavier checks on the
thumbv7em ABIs and riscv32imc. thumbv6m builds with --no-default-features.
| Test | Targets | Details |
|---|---|---|
portable_path |
all (incl. thumbv6m) |
Identity: multicalc vector dot product [1,2,3,4]·[4,3,2,1] = 20 — a no-atomics library call exercised on M0. |
svd_golden |
all (incl. thumbv6m) |
Golden: singular values of a 3×3 fixture matrix vs the svd_3x3 QA golden. Also emits SMOKE_VAL_svd_s* for the cross-ABI guard. |
error_path_returns_err |
all (incl. thumbv6m) |
Negative path: a singular matrix's lu() and an indefinite matrix's cholesky() return a typed Err, not a panic. |
pid_step |
all (incl. thumbv6m) |
Identity: a PID controller's first output is the exact kp·e + ki·e·dt, and the plant it drives climbs to within 1% of the setpoint. Emits SMOKE_VAL_pid. |
lm_fit |
full set | Golden: Levenberg-Marquardt Rosenbrock least-squares minimizer vs the rosenbrock QA golden. |
autodiff_derivative |
full set | Identity: forward-mode autodiff of x³ at x = 2, expects 12. |
autodiff_derivative_f32 |
full set | Identity in f32: the same x³ derivative at single precision, where soft-float and the hardware FPU are most likely to differ. Emits SMOKE_VAL_ad_f32. |
ode_identity |
full set | Identity: RK4 harmonic oscillator round trip and RK45 y' = -y to e^{-1}. |
lie_group_identity |
full set | Identity: SO(3)/SE(3) exp/log round trips and one known rotation. |
quadrature_identity |
full set | Identity: Gauss-Legendre order 4 of 2x on [0,2], expects 4. Emits SMOKE_VAL_quad. |
jacobian_identity |
full set | Identity: Jacobian of [x·y·z, x²+y²] at (1,2,3) = [[6,3,2],[2,4,0]]. Emits SMOKE_VAL_jac00. |
vector_field_identity |
full set | Identity: curl of [y,-x,2z] = [0,0,-2] and divergence = 2. Emits SMOKE_VAL_div3d. |
root_finding_golden |
full set | Golden: Newton on Wien's equation vs the wien_newton QA golden. Emits SMOKE_VAL_wien_root. |
kalman_filter_golden |
full set | Golden: eight predict/update steps of a constant-velocity Kalman filter vs the kalman_filter_constant_velocity_one_dimensional QA golden. Emits SMOKE_VAL_kalman. |
extended_kalman_filter_golden |
full set | Golden: eight steps of an extended Kalman filter tracking a turning vehicle from position fixes, vs the extended_kalman_filter_coordinated_turn_fusion QA golden. The only check driving 5×5 autodiff Jacobians on target. Emits SMOKE_VAL_extended_kalman. |
kalman_filter_identity_f32 |
full set | Identity in f32: two unit-measurement steps of a constant-velocity filter land on the exact [5/3, 2/3]. Emits SMOKE_VAL_kalman_f32. |
pure_pursuit_identity |
full set | Identity: pure pursuit returns zero curvature for a point dead ahead and the exact 2·lateral/L² for one off to the side. Emits SMOKE_VAL_pure_pursuit. |
follow_the_gap_identity |
full set | Identity: a 31-beam clear scan drives straight ahead at cruise speed; a wall all round stops the robot and reports it blocked. Emits SMOKE_VAL_follow_the_gap. |
The binary runs under QEMU semihosting:
- Clean finish →
debug::exit(EXIT_SUCCESS)→ QEMU exits 0. - Panic (failed
assert!/expect) → on ARM,panic-semihosting(with theexitfeature) prints and exits QEMU non-zero; on RISC-V, the crate's#[panic_handler]does the same viariscv-semihosting.
Note:
cargo run … | teemasks the exit code unlesspipefailis set. CI runs steps underbash -eo pipefail, so a panic still fails the job.
main.rs measures peak stack by painting free stack below the entry frame with
a sentinel and scanning for the deepest overwritten byte after the checks
return. It emits one line, STACK_HWM_BYTES=<n>, read by the size/stack gate.
PAINT: sentinel byte (0xAA) written across the watched window.WINDOW: bytes of free stack to paint and scan (4 KiB). Must exceed the deepest check and fit the target RAM. If a printed value equalsWINDOW, the window saturated. Raise it and confirm headroom.GUARD: bytes just below the current SP left untouched (the live frame), so painting never corrupts running code.
This is a fixed-window scan below the SP, so it needs no linker symbol and is
identical across machines. If a target cannot yield a stable number, drop its
STACK_HWM_BYTES line and leave that ABI .text-gated only.
ci/budgets.toml holds per-target .text and stack budgets with a shared
tolerance_pct. ci/check_budgets.sh reads .text via cargo size -- -A,
parses STACK_HWM_BYTES from the run output, and fails if either exceeds
budget × (1 + tolerance_pct/100):
cargo run -p embedded-smoke --release --target thumbv6m-none-eabi | tee smoke.txt
bash ci/check_budgets.sh embedded-smoke thumbv6m-none-eabi smoke.txtIntentional growth must bump ci/budgets.toml in the same PR so it is visible
in review.
This crate does not use [lints] workspace = true. It needs one small
unsafe block for the stack scan, so it sets unsafe_code = "allow" locally
and does not inherit the library's panic-free / no-unsafe lints.