Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# thumbv6m-none-eabi -> Cortex-M0, microbit/nRF51 (flash @ 0x0, 16K RAM); a real
# M0 machine, so the canary catches both RAM overflow and
# out-of-ISA (ARMv7E-M) contamination that faults on silicon.
# riscv32imc-unknown-none-elf -> QEMU virt DRAM @ 0x80000000 (-bios none)

[target.thumbv7em-none-eabi] # soft-float
runner = "qemu-system-arm -machine netduinoplus2 -nographic -semihosting-config enable=on,target=native -kernel"
Expand All @@ -32,8 +33,17 @@ rustflags = [
"-C", "link-arg=--nmagic",
]

[target.riscv32imc-unknown-none-elf]
runner = "qemu-system-riscv32 -machine virt -nographic -semihosting-config enable=on,target=native -bios none -kernel"
rustflags = [
"-C", "link-arg=-Tmemory.x", # must come before link x for riscv-rt
Comment thread
kmolan marked this conversation as resolved.
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=--nmagic",
]

[alias]
fit = "run -p multicalc-demos --example curve_fit"
smoke-eabi = "run -p embedded-smoke --release --target thumbv7em-none-eabi"
smoke-eabihf = "run -p embedded-smoke --release --target thumbv7em-none-eabihf"
smoke-m0 = "run -p embedded-smoke --release --no-default-features --target thumbv6m-none-eabi"
smoke-riscv = "run -p embedded-smoke --release --target riscv32imc-unknown-none-elf"
13 changes: 9 additions & 4 deletions .github/workflows/matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Target matrix
# Reusable: called by CI on pull requests and by Release on a version bump.
#
# `full` selects the target set. false (default) = the per-PR representative set
# (x86_64 host + all three bare-metal ABIs, thumbv6m as a reduced canary); true =
# (x86_64 host + all four bare-metal ABIs, thumbv6m as a reduced canary); true =
# the complete matrix, adding the aarch64 host. The `setup` job turns that flag into
# the JSON matrices the build jobs consume, so representative and full share one set
# of steps and differ only in the host list.
Expand Down Expand Up @@ -33,9 +33,9 @@ jobs:
shell: bash
run: |
# thumbv6m runs the reduced canary (--no-default-features) on every PR; the
# thumbv7em ABIs run the full set. The bare set is the same for representative
# and full runs — full only adds the aarch64 host.
bare='[{"target":"thumbv7em-none-eabi","args":""},{"target":"thumbv7em-none-eabihf","args":""},{"target":"thumbv6m-none-eabi","args":"--no-default-features"}]'
# thumbv7em ABIs and riscv32 run the full set. The bare set is the same for
Comment thread
kmolan marked this conversation as resolved.
# representative and full runs — full only adds the aarch64 host.
bare='[{"target":"thumbv7em-none-eabi","args":""},{"target":"thumbv7em-none-eabihf","args":""},{"target":"thumbv6m-none-eabi","args":"--no-default-features"},{"target":"riscv32imc-unknown-none-elf","args":""}]'
if [ "${{ inputs.full }}" = "true" ]; then
hosts='[{"target":"x86_64-unknown-linux-gnu","runner":"ubuntu-latest"},{"target":"aarch64-unknown-linux-gnu","runner":"ubuntu-24.04-arm"}]'
else
Expand Down Expand Up @@ -92,6 +92,11 @@ jobs:
with:
packages: qemu-system-arm
version: 1.0
- name: Install QEMU (RISC-V)
run: |
sudo apt-get update
sudo apt-get install -y qemu-system-misc

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not do apt install. Rather stick with cached deps similar to how the arm is installed above.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reason is to avoid the slow apt update and apt install on every CI

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. My first push put qemu-system-misc in cache-apt-pkgs-action the same way as ARM, but the riscv bare-metal job failed with:

error: could not execute process `qemu-system-riscv32 -machine virt -nographic -semihosting-config enable=on,target=native -bios none -kernel target/riscv32imc-unknown-none-elf/release/embedded-smoke` (never executed)

Caused by:
  No such file or directory (os error 2)

Bumping the cache version didn’t help either. I switched RISC-V to apt-get install as it was the only thing I could think of that worked -- I am open to other suggestions though.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked more into it, and yeah you're right. RISC-V is flaky so apt install is the right move here.

command -v qemu-system-riscv32
- name: Run smoke tests + budget checks under QEMU
run: |
for bin in embedded-smoke; do
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
polynomial compositions. @rtmongold (#129)
- Opt-in Kahan compensated summation for iterative integration and approximation metrics
via `.with_kahan_summation()`; pairwise remains the default. @rtmongold (#134)
- **RISC-V embedded smoke.** `embedded-smoke` runs on `riscv32imc-unknown-none-elf`
under QEMU `virt`, gated with its own flash/stack budgets. @rtmongold (#140)

### Fixed

Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
is CI-enforced, so run it before pushing.
4. `cargo fmt`.

That's it. **You do not need QEMU or ARM toolchains** for most contributions — CI runs
the five-target embedded matrix on your PR for you. If you want the bare-metal targets
That's it. **You do not need QEMU or bare-metal toolchains** for most contributions — CI runs
the multi-target embedded matrix on your PR for you. If you want the bare-metal targets
locally anyway, the setup lives in [ci/README.md](ci/README.md) (optional).

## Workflow
Expand All @@ -19,7 +19,7 @@ locally anyway, the setup lives in [ci/README.md](ci/README.md) (optional).
- Every behavior-facing change adds a line under `## [Unreleased]` in
[CHANGELOG.md](CHANGELOG.md) (grouped `Added`/`Changed`/`Fixed`/`Removed`), in the
same PR.
- CI must be green: host tests (x86_64 + aarch64), clippy, the three bare-metal QEMU
- CI must be green: host tests (x86_64 + aarch64), clippy, the four bare-metal QEMU
smoke runs, and the flash/stack budget gates. If a budget gate trips, see the raise
protocol in [ci/README.md](ci/README.md) — never raise `tolerance_pct`.

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**Scientific computing for single- and multi-variable calculus in pure, safe Rust. Numerical
derivatives, integrals, curve fitting and linear algebra; built and tested on five
derivatives, integrals, curve fitting and linear algebra; built and tested on six
hardware targets. Exercise the same code from a 64-bit server CPU down to a bare-metal microcontroller.**

<p align="center">
Expand All @@ -22,9 +22,9 @@ core — every number measured live.*
## Highlights

- **Pure, safe Rust**: `#![forbid(unsafe_code)]`, no C dependencies.
- **Tested against multiple platforms**: Every commit is built and tested across **five targets**: the
- **Tested against multiple platforms**: Every commit is built and tested across **six targets**: the
`x86_64` and `aarch64` Linux hosts and three ARM Cortex-M bare-metal ABIs (`thumbv7em`
soft-float, `thumbv7em` hardware-FPU, and `thumbv6m`). `no_std`, no heap, and no panics rules apply to every platform build, and the transcendental functions come from
soft-float, `thumbv7em` hardware-FPU, and `thumbv6m`), and `riscv32imc-unknown-none-elf`. `no_std`, no heap, and no panics rules apply to every platform build, and the transcendental functions come from
[`libm`](https://crates.io/crates/libm).
- **Fast, and measured**: a derivative in **~1 ns**, a full Levenberg-Marquardt curve fit in
**microseconds**, and solvers that land on the answer to the **last few bits** (objectives near
Expand Down Expand Up @@ -114,7 +114,7 @@ below.
The published library crate lives in [`crates/multicalc`](crates/multicalc); the repository
root is a Cargo workspace. Runnable demos live in the dev-only [`demos/`](demos) crate (basics and
live Rerun showcases), and [`tools/embedded-smoke`](tools/embedded-smoke) runs `multicalc` on the
three bare-metal Cortex-M targets under QEMU every PR.
four bare-metal targets (three Cortex-M targets + `riscv32imc`) under QEMU every PR.

## Contributing

Expand Down
28 changes: 15 additions & 13 deletions ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,19 @@ another's headroom; keep them tight and per-row.

## Smoke-test harness

The on-target smoke binary is a hand-rolled `cortex-m-rt` `#[entry]` runner. It runs every check
in sequence in a single QEMU invocation, prints machine-readable result lines over semihosting
(`hprintln!`, i.e. `SYS_WRITE0`), and exits QEMU with `debug::exit`. The budget gate and the
cross-ABI guard parse those lines straight from QEMU stdout.
The on-target smoke binary is a hand-rolled `#[entry]` runner (`cortex-m-rt` on ARM,
`riscv-rt` on RISC-V). It runs every check in sequence in a single QEMU invocation, prints
machine-readable result lines over semihosting (`hprintln!`, i.e. `SYS_WRITE0`), and exits
QEMU with `debug::exit`. The budget gate and the cross-ABI guard parse those lines straight
from QEMU stdout.

`embedded-test`/`defmt-test` are not used. Their harness is driven by the `probe-rs` host runner:
it enumerates test cases through a probe-rs-specific semihosting operation and resets the target to
run each `#[test]` in its own invocation. Under the plain `qemu-system-arm -kernel` runner these
smoke tests use, nothing drives that protocol, so the harness aborts before any test runs. The
per-test reset model is also incompatible with the single-invocation stack high-watermark
measurement the budget gate reads. `defmt` output is avoided for the same parsing reason: its RTT
transport needs a host-side decoder, while the plain-text semihosting lines need none.
run each `#[test]` in its own invocation. Under the plain `qemu-system-arm` /
`qemu-system-riscv32 -kernel` runners these smoke tests use, nothing drives that protocol, so
the harness aborts before any test runs. The per-test reset model is also incompatible with the single-invocation stack high-watermark measurement the budget gate reads. `defmt` output is avoided
for the same parsing reason: its RTT transport needs a host-side decoder, while the plain-text
semihosting lines need none.

## Smoke fixtures (shared with the oracle)

Expand All @@ -52,10 +53,11 @@ a stale checked-in copy fails the build. Change a fixture and rerun the generato
## Test-set tiering

The `embedded-smoke` `full-smoke` feature (on by default) selects the check set. Both `thumbv7em`
ABIs build with default features and run the full set; `thumbv6m` builds with `--no-default-features`
and runs only the canary — the portable (no-atomics) path, one golden, and the no-panic negative
path — which keeps the M0 image small while still smoke-running every PR. The target matrix threads
`--no-default-features` into the `thumbv6m` run (see `.github/workflows/matrix.yml`).
ABIs and `riscv32imc-unknown-none-elf` build with default features and run the full set;
`thumbv6m` builds with `--no-default-features` and runs only the canary — the portable
(no-atomics) path, one golden, and the no-panic negative path — which keeps the M0 image small
while still smoke-running every PR. The target matrix threads `--no-default-features` into the
`thumbv6m` run (see `.github/workflows/matrix.yml`).

## Cross-ABI divergence guard

Expand Down
6 changes: 5 additions & 1 deletion ci/budgets.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# Sections are [<binary>.<target>], where <binary> is the cargo package name of a
# smoke crate (embedded-smoke today; per-domain smoke binaries are added at v0.9+,
# each its own section) and <target> is one of the three bare-metal triples below.
# each its own section) and <target> is one of the four bare-metal triples below.
# Splitting budgets per binary keeps the gate fair: a heavy new domain grows its own
# binary's budget and never spends another domain's headroom.
#
Expand Down Expand Up @@ -37,3 +37,7 @@ stack_bytes_f64 = 1548
[embedded-smoke.thumbv6m-none-eabi]
text_bytes = 43424
stack_bytes_f64 = 296

[embedded-smoke.riscv32imc-unknown-none-elf]
text_bytes = 78914
stack_bytes_f64 = 1220
5 changes: 3 additions & 2 deletions ci/check_budgets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# `cargo size`), e.g. embedded-smoke. Must match the "<binary>."
# prefix of a [<binary>.<target>] section in ci/budgets.toml.
# <target-triple> thumbv7em-none-eabi | thumbv7em-none-eabihf | thumbv6m-none-eabi
# | riscv32imc-unknown-none-elf
# <smoke-output-file> captured stdout of the run; contains a STACK_HWM_BYTES[...]=<n>
# line. Produce it with:
# cargo run -p <binary> --release --target <triple> | tee <file>
Expand All @@ -20,7 +21,7 @@
set -euo pipefail

budgets="ci/budgets.toml"
valid_targets="thumbv7em-none-eabi thumbv7em-none-eabihf thumbv6m-none-eabi"
valid_targets="thumbv7em-none-eabi thumbv7em-none-eabihf thumbv6m-none-eabi riscv32imc-unknown-none-elf"

die() {
echo ">>> ERROR: $1" >&2
Expand Down Expand Up @@ -120,7 +121,7 @@ if ! size_out=$(cargo size --release $size_features -p "$binary" --target "$targ
"Add the target: rustup target add $target" \
"Build it at least once: cargo build -p $binary --release --target $target"
fi
text_now=$(printf '%s\n' "$size_out" | awk '/^\.text/ { print $2 }')
text_now=$(printf '%s\n' "$size_out" | awk '$1 == ".text" { print $2; exit }')
printf '%s' "$text_now" | grep -qE '^[0-9]+$' || { echo "$size_out" >&2; die \
"could not parse a numeric .text size for $binary/$target (got '$text_now')." \
"Look for a '.text' row in the 'cargo size ... -- -A' output above."; }
Expand Down
12 changes: 6 additions & 6 deletions crates/multicalc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ Jacobians and Hessians, vector-field operators, and Taylor approximation in a `n

## Why use it

- **Tested across five hardware targets**: Every commit is built and tested on the `x86_64` and
`aarch64` Linux hosts and on three ARM Cortex-M bare-metal ABIs (`thumbv7em` soft-float,
`thumbv7em` hardware-FPU, and `thumbv6m`). The Cortex-M builds run the real math under QEMU on
each ABI and check the answers against known values, so the same code is exercised from a 64-bit
server CPU down to a microcontroller with no operating system. `no_std`, no-heap, and
no-panic rules hold on every one.
- **Tested across six hardware targets**: Every commit is built and tested on the `x86_64` and
`aarch64` Linux hosts and on four bare-metal ABIs (three ARM Cortex-M: `thumbv7em` soft-float,
`thumbv7em` hardware-FPU, and `thumbv6m`; plus `riscv32imc-unknown-none-elf`). The bare-metal builds
run the real math under QEMU on each ABI and check the answers against known values, so the same
code is exercised from a 64-bit server CPU down to a microcontroller with no operating system.
`no_std`, no-heap, and no-panic rules hold on every one.
- **Accurate to the last few bits**: differentiation, Jacobians, Hessians, and Newton steps use
forward-mode automatic differentiation, so derivatives are exact rather than finite-difference
estimates. The least-squares and root-finding solvers drive objectives down near `1e-30` and land
Expand Down
11 changes: 9 additions & 2 deletions tools/embedded-smoke/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@ edition = "2024"
rust-version = "1.85"
publish = false

# thumbv7em runs the full set (default). thumbv6m runs the reduced canary via
# --no-default-features, keeping the M0 image small.
# thumbv7em / riscv32 runs the full set (default). thumbv6m runs the reduced
# canary via --no-default-features, keeping the M0 image small.
[features]
default = ["full-smoke"]
full-smoke = []

[dependencies]
multicalc = { path = "../../crates/multicalc", default-features = false }
multicalc-testkit = { path = "../testkit" }

[target.'cfg(target_arch = "arm")'.dependencies]
cortex-m-rt = "0.7"
cortex-m-semihosting = "0.5"
# With the "exit" feature a panic prints its message and exits QEMU with a failure code.
panic-semihosting = { version = "0.6", features = ["exit"] }

[target.'cfg(any(target_arch = "riscv32", target_arch = "riscv64"))'.dependencies]
riscv-rt = "0.13"
riscv-semihosting = "0.2"
riscv = { version = "0.12", features = ["critical-section-single-hart"] }

# This dev-only crate needs one small unsafe block to measure stack use, so it does
# not inherit the core crate's "no unsafe" rule. It sets its own lints instead of
# using `[lints] workspace = true`.
Expand Down
Loading
Loading