Skip to content

Latest commit

 

History

History
143 lines (113 loc) · 9.09 KB

File metadata and controls

143 lines (113 loc) · 9.09 KB

embedded-smoke

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*.

Running

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-elf

Aliases: cargo smoke-eabi, cargo smoke-eabihf, cargo smoke-m0, cargo smoke-riscv.

Targets and QEMU machine

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.

Why a separate crate, not tests inside multicalc

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.

Checks

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 at x = 2, expects 12.
autodiff_derivative_f32 full set Identity in f32: the same 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.

Pass/fail contract

The binary runs under QEMU semihosting:

  • Clean finish → debug::exit(EXIT_SUCCESS) → QEMU exits 0.
  • Panic (failed assert!/expect) → on ARM, panic-semihosting (with the exit feature) prints and exits QEMU non-zero; on RISC-V, the crate's #[panic_handler] does the same via riscv-semihosting.

Note: cargo run … | tee masks the exit code unless pipefail is set. CI runs steps under bash -eo pipefail, so a panic still fails the job.

Stack high-water mark

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 equals WINDOW, 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.

Size and stack gate

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.txt

Intentional growth must bump ci/budgets.toml in the same PR so it is visible in review.

Lints

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.