BNO055 driver fork

Maintained fork of the Rust driver for the Bosch BNO055 IMU. Removed unsafe blocks, fixed an axis-remap bug hidden behind a clippy allow, cut sensor-read latency 27-36%, added 206 tests.

Apr 2026 — Apr 2026 · Maintainer
  • Rust
  • no_std
  • embedded-hal 1.0
  • I2C
  • Pi 5

Full sensor loop

−36%

9.83 ms → 6.28 ms · 102 Hz → 159 Hz on Pi 5

Per-read latency

−27%

page-tracking eliminates redundant I2C writes

Tests added

0 → 206

integration tests covering all sensor paths

What it is

A no_std Rust driver for the Bosch BNO055 9-axis IMU. Forked from eupn/bno055 and reworked for memory safety, correctness, and per-read latency. Used inside the PID autonomous motor-control pipeline on a Raspberry Pi 5.

What I found and fixed

Bug: AxisRemap::y() returned the X axis. The getter was self.x instead of self.y, hidden under #[allow(clippy::misnamed_getters)]. Any user calling .y() got the wrong axis silently. Fixed and the lint allow removed so it can’t regress.

Two unsafe blocks in calibration serialisation. from_buf and as_bytes used raw pointer casts to reinterpret a BNO055Calibration struct as bytes. Rewrote both to use field-by-field construction and an owned [u8; 22] return value. No more unsafe anywhere in the crate.

Redundant I2C writes on every sensor read. The driver re-set the register page on each call, even when already on the correct page. Added page field tracking with set_page() skipping the write when the requested page is already active. −27% on all reads (eliminated ~0.5 ms per call).

No bulk read API. Reading all six sensors meant 6+ separate I2C transactions. Added all_sensor_data() reading registers 0x08-0x34 (45 bytes) in one transaction; returns an AllSensorData struct with Option fields based on operation-mode availability. −36% over the per-sensor baseline.

Unit error type on AxisRemapBuilder::build(). Replaced Result<AxisRemap, ()> with Result<AxisRemap, InvalidAxisRemap> so callers can distinguish failure modes.

Three unnecessary dependencies. Replaced byteorder with i16::from_le_bytes() from core. Replaced num-derive/num-traits FromPrimitive with manual match arms. Three fewer transitive deps, including two proc-macros.

Zero tests. Added 206 integration tests covering the sensor reads, calibration profile read/write, axis-remap builder, mode transitions, and error paths.

996-line monolithic lib.rs. Split into 9 focused modules (sensors, axis, mode, calibration, acc_config, status, regs, std, plus a slimmer lib.rs for the public surface). No breaking changes to the public API; internal helpers moved from pub to pub(crate).

Measurements

Pi 5, I2C-1 at 100 kHz, sensor in NDOF fusion mode, stationary, cargo bench --bench sensor_reads:

Read typeBeforeAfterΔ
temperature1.02 ms0.60 ms−41%
accel_data (6 bytes)1.69 ms1.26 ms−25%
gyro_data (6 bytes)1.67 ms1.24 ms−26%
mag_data (6 bytes)1.71 ms1.26 ms−26%
euler_angles1.70 ms1.24 ms−27%
quaternion (8 bytes)1.97 ms1.53 ms−22%
all 6 sensors (individual)9.83 ms7.16 ms−27%
all_sensor_data bulk(no API)6.28 ms−36%

Full sensor loop: 9.83 ms → 6.28 ms (102 Hz → 159 Hz). A loop that barely fit a 10 ms window now has 3.7 ms of headroom for other work on the same tick.

What I take from this

The page-tracking and bulk-read wins came from reading the datasheet carefully and noticing that the upstream driver was structurally suboptimal — not from clever tricks. The bug fix came from running clippy without the allow annotation upstream had silenced.

The shape that travels is: when a library has been used productively for years and looks fine, the obvious-on-second-read mistakes are usually still there. Someone has to actually look.

Repo: github.com/Niek-Kamer/BNO055.