Skip to main content

cu_bmi088/
lib.rs

1//! Copper source driver for the BMI088 6-axis IMU (accelerometer + gyroscope).
2//!
3//! The BMI088 is a high-performance inertial measurement unit with a 16-bit accelerometer
4//! and a 16-bit gyroscope. It is commonly used in flight controllers and robotics applications.
5//!
6//! This driver uses `embedded-hal` 0.2 traits for SPI communication and GPIO chip-select control,
7//! making it portable across different embedded platforms.
8//!
9//! # Hardware Notes
10//!
11//! The BMI088 has separate chip-select lines for the accelerometer and gyroscope:
12//! - Accelerometer: Requires a dummy byte after register address reads
13//! - Gyroscope: Standard SPI read protocol
14//!
15//! # Example Configuration (copperconfig.ron)
16//!
17//! ```ron
18//! (
19//!     id: "imu",
20//!     type: "cu_bmi088::Bmi088Source<MySpi, MyAccCs, MyGyrCs, MyDelay>",
21//!     resources: {
22//!         "spi": "hal.bmi088_spi",
23//!         "acc_cs": "hal.bmi088_acc_cs",
24//!         "gyr_cs": "hal.bmi088_gyr_cs",
25//!         "delay": "hal.bmi088_delay",
26//!     },
27//! )
28//! ```
29
30#![cfg_attr(not(feature = "std"), no_std)]
31#![allow(dead_code)]
32
33use core::fmt::Debug;
34
35pub use cu_sensor_payloads::ImuPayload;
36use cu29::prelude::*;
37use embedded_hal::blocking::delay::DelayMs;
38use embedded_hal::blocking::spi::Transfer;
39use embedded_hal::digital::v2::OutputPin;
40
41// Chip IDs
42const BMI088_ACC_CHIP_ID: u8 = 0x1E;
43const BMI088_GYR_CHIP_ID: u8 = 0x0F;
44
45// Accelerometer registers
46const BMI088_ACC_REG_CHIP_ID: u8 = 0x00;
47const BMI088_ACC_REG_ACCEL_X_LSB: u8 = 0x12;
48const BMI088_ACC_REG_TEMP_MSB: u8 = 0x22;
49const BMI088_ACC_REG_TEMP_LSB: u8 = 0x23;
50const BMI088_ACC_REG_ACC_RANGE: u8 = 0x41;
51const BMI088_ACC_REG_PWR_CONF: u8 = 0x7C;
52const BMI088_ACC_REG_PWR_CTRL: u8 = 0x7D;
53const BMI088_ACC_REG_SOFT_RESET: u8 = 0x7E;
54
55// Gyroscope registers
56const BMI088_GYR_REG_CHIP_ID: u8 = 0x00;
57const BMI088_GYR_REG_X_LSB: u8 = 0x02;
58const BMI088_GYR_REG_RANGE: u8 = 0x0F;
59const BMI088_GYR_REG_BANDWIDTH: u8 = 0x10;
60const BMI088_GYR_REG_POWER_MODE: u8 = 0x11;
61const BMI088_GYR_REG_SOFT_RESET: u8 = 0x14;
62
63// Configuration values
64const BMI088_SOFT_RESET_CMD: u8 = 0xB6;
65const BMI088_ACC_PWR_CONF_ACTIVE: u8 = 0x00;
66const BMI088_ACC_PWR_CTRL_ON: u8 = 0x04;
67const BMI088_GYR_RANGE_2000: u8 = 0x00;
68const BMI088_GYR_BANDWIDTH: u8 = 0x07; // ODR 2000Hz, Filter BW 230Hz
69const BMI088_GYR_PWR_NORMAL: u8 = 0x00;
70
71// Conversion factor for gyroscope at 2000 dps range
72const GYRO_RAD_PER_LSB: f32 = (2000.0 / 32_768.0) * (core::f32::consts::PI / 180.0);
73
74// Resource definitions for the BMI088 source task.
75// The BMI088 requires:
76// - `spi`: SPI bus (shared between acc and gyro)
77// - `acc_cs`: Accelerometer chip-select GPIO
78// - `gyr_cs`: Gyroscope chip-select GPIO
79// - `delay`: Delay provider for initialization timing
80resources!(for <SPI, ACC, GYR, D>
81where
82    SPI: Transfer<u8> + Send + Sync + 'static,
83    SPI::Error: Debug + Send + 'static,
84    ACC: OutputPin + Send + Sync + 'static,
85    ACC::Error: Debug + Send + 'static,
86    GYR: OutputPin + Send + Sync + 'static,
87    GYR::Error: Debug + Send + 'static,
88    D: DelayMs<u32> + Send + Sync + 'static,
89{
90    spi => Owned<SPI>,
91    acc_cs => Owned<ACC>,
92    gyr_cs => Owned<GYR>,
93    delay => Owned<D>,
94});
95
96/// Copper source task for the BMI088 IMU.
97///
98/// This task reads accelerometer and gyroscope data from the BMI088 and outputs
99/// an [`ImuPayload`] with measurements in SI units (m/s² for acceleration,
100/// rad/s for angular velocity, °C for temperature).
101///
102/// # Type Parameters
103///
104/// - `SPI`: SPI bus type implementing `embedded_hal::blocking::spi::Transfer<u8>`
105/// - `ACC`: Accelerometer chip-select GPIO implementing `OutputPin`
106/// - `GYR`: Gyroscope chip-select GPIO implementing `OutputPin`
107/// - `D`: Delay provider implementing `DelayMs<u32>`
108#[derive(Reflect)]
109#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
110pub struct Bmi088Source<SPI, ACC, GYR, D> {
111    #[reflect(ignore)]
112    driver: Bmi088Driver<SPI, ACC, GYR, D>,
113}
114
115impl<SPI, ACC, GYR, D> TypePath for Bmi088Source<SPI, ACC, GYR, D>
116where
117    SPI: 'static,
118    ACC: 'static,
119    GYR: 'static,
120    D: 'static,
121{
122    fn type_path() -> &'static str {
123        "cu_bmi088::Bmi088Source"
124    }
125
126    fn short_type_path() -> &'static str {
127        "Bmi088Source"
128    }
129
130    fn type_ident() -> Option<&'static str> {
131        Some("Bmi088Source")
132    }
133
134    fn crate_name() -> Option<&'static str> {
135        Some("cu_bmi088")
136    }
137
138    fn module_path() -> Option<&'static str> {
139        Some("")
140    }
141}
142
143impl<SPI, ACC, GYR, D> Freezable for Bmi088Source<SPI, ACC, GYR, D>
144where
145    SPI: Transfer<u8> + Send + Sync + 'static,
146    SPI::Error: Debug + Send + 'static,
147    ACC: OutputPin + Send + Sync + 'static,
148    ACC::Error: Debug + Send + 'static,
149    GYR: OutputPin + Send + Sync + 'static,
150    GYR::Error: Debug + Send + 'static,
151    D: DelayMs<u32> + Send + Sync + 'static,
152{
153}
154
155impl<SPI, ACC, GYR, D> CuSrcTask for Bmi088Source<SPI, ACC, GYR, D>
156where
157    SPI: Transfer<u8> + Send + Sync + 'static,
158    SPI::Error: Debug + Send + 'static,
159    ACC: OutputPin + Send + Sync + 'static,
160    ACC::Error: Debug + Send + 'static,
161    GYR: OutputPin + Send + Sync + 'static,
162    GYR::Error: Debug + Send + 'static,
163    D: DelayMs<u32> + Send + Sync + 'static,
164{
165    type Resources<'r> = Resources<SPI, ACC, GYR, D>;
166    type Output<'m> = output_msg!(ImuPayload);
167
168    fn new(_config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
169    where
170        Self: Sized,
171    {
172        let driver = Bmi088Driver::new(
173            resources.spi.0,
174            resources.acc_cs.0,
175            resources.gyr_cs.0,
176            resources.delay.0,
177        )?;
178        Ok(Self { driver })
179    }
180
181    fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
182        Ok(())
183    }
184
185    fn process<'o>(&mut self, clock: &RobotClock, output: &mut Self::Output<'o>) -> CuResult<()> {
186        let tov = clock.now();
187        let payload = self.driver.read_measure()?;
188        output.tov = Tov::Time(tov);
189        output.set_payload(payload);
190        Ok(())
191    }
192}
193
194/// Low-level driver for the BMI088 IMU.
195struct Bmi088Driver<SPI, ACC, GYR, D> {
196    spi: SPI,
197    acc_cs: ACC,
198    gyr_cs: GYR,
199    #[allow(dead_code)]
200    delay: D,
201    acc_mps2_per_lsb: f32,
202}
203
204impl<SPI, ACC, GYR, D> Bmi088Driver<SPI, ACC, GYR, D>
205where
206    SPI: Transfer<u8>,
207    SPI::Error: Debug,
208    ACC: OutputPin,
209    ACC::Error: Debug,
210    GYR: OutputPin,
211    GYR::Error: Debug,
212    D: DelayMs<u32>,
213{
214    /// Initialize the BMI088 driver.
215    ///
216    /// This performs:
217    /// 1. Chip ID verification for both accelerometer and gyroscope
218    /// 2. Soft reset of both sensors
219    /// 3. Power-on and configuration of accelerometer
220    /// 4. Configuration of gyroscope (2000 dps range, 2000 Hz ODR)
221    fn new(mut spi: SPI, mut acc_cs: ACC, mut gyr_cs: GYR, mut delay: D) -> CuResult<Self> {
222        // Ensure CS lines are high (deselected)
223        acc_cs
224            .set_high()
225            .map_err(|err| map_error("bmi088 acc cs high", err))?;
226        gyr_cs
227            .set_high()
228            .map_err(|err| map_error("bmi088 gyr cs high", err))?;
229
230        // Verify chip IDs
231        let acc_id = spi_read_reg_2(&mut spi, &mut acc_cs, BMI088_ACC_REG_CHIP_ID)
232            .map_err(|err| map_error("bmi088 acc chip id", err))?;
233        let gyr_id = spi_read_reg_1(&mut spi, &mut gyr_cs, BMI088_GYR_REG_CHIP_ID)
234            .map_err(|err| map_error("bmi088 gyr chip id", err))?;
235
236        if acc_id != BMI088_ACC_CHIP_ID {
237            return Err(CuError::from("bmi088 accel id mismatch"));
238        }
239        if gyr_id != BMI088_GYR_CHIP_ID {
240            return Err(CuError::from("bmi088 gyro id mismatch"));
241        }
242
243        // Reset accelerometer
244        spi_write_reg(
245            &mut spi,
246            &mut acc_cs,
247            BMI088_ACC_REG_SOFT_RESET,
248            BMI088_SOFT_RESET_CMD,
249        )
250        .map_err(|err| map_error("bmi088 acc reset", err))?;
251        delay.delay_ms(10);
252
253        // Power on accelerometer
254        spi_write_reg(
255            &mut spi,
256            &mut acc_cs,
257            BMI088_ACC_REG_PWR_CONF,
258            BMI088_ACC_PWR_CONF_ACTIVE,
259        )
260        .map_err(|err| map_error("bmi088 acc pwr conf", err))?;
261        delay.delay_ms(1);
262        spi_write_reg(
263            &mut spi,
264            &mut acc_cs,
265            BMI088_ACC_REG_PWR_CTRL,
266            BMI088_ACC_PWR_CTRL_ON,
267        )
268        .map_err(|err| map_error("bmi088 acc pwr ctrl", err))?;
269        delay.delay_ms(50);
270
271        // Reset and configure gyroscope
272        spi_write_reg(
273            &mut spi,
274            &mut gyr_cs,
275            BMI088_GYR_REG_SOFT_RESET,
276            BMI088_SOFT_RESET_CMD,
277        )
278        .map_err(|err| map_error("bmi088 gyro reset", err))?;
279        delay.delay_ms(100);
280        spi_write_reg(
281            &mut spi,
282            &mut gyr_cs,
283            BMI088_GYR_REG_RANGE,
284            BMI088_GYR_RANGE_2000,
285        )
286        .map_err(|err| map_error("bmi088 gyro range", err))?;
287        spi_write_reg(
288            &mut spi,
289            &mut gyr_cs,
290            BMI088_GYR_REG_BANDWIDTH,
291            BMI088_GYR_BANDWIDTH,
292        )
293        .map_err(|err| map_error("bmi088 gyro bandwidth", err))?;
294        spi_write_reg(
295            &mut spi,
296            &mut gyr_cs,
297            BMI088_GYR_REG_POWER_MODE,
298            BMI088_GYR_PWR_NORMAL,
299        )
300        .map_err(|err| map_error("bmi088 gyro pwr mode", err))?;
301
302        // Read accelerometer range for scaling
303        let acc_range_reg = spi_read_reg_2(&mut spi, &mut acc_cs, BMI088_ACC_REG_ACC_RANGE)
304            .map_err(|err| map_error("bmi088 acc range", err))?;
305        let acc_range_g = accel_range_g_from_reg(acc_range_reg);
306        let acc_mps2_per_lsb = acc_range_g * 9.806_65 / 32_768.0;
307        debug!(
308            "bmi088 accel range reg={} -> ±{}g",
309            acc_range_reg, acc_range_g
310        );
311
312        Ok(Self {
313            spi,
314            acc_cs,
315            gyr_cs,
316            delay,
317            acc_mps2_per_lsb,
318        })
319    }
320
321    /// Read accelerometer, gyroscope, and temperature data.
322    ///
323    /// Returns an [`ImuPayload`] with:
324    /// - Acceleration in m/s² (NED frame)
325    /// - Angular velocity in rad/s (NED frame)
326    /// - Temperature in °C
327    fn read_measure(&mut self) -> CuResult<ImuPayload> {
328        // Read accelerometer (6 bytes: X, Y, Z as 16-bit little-endian)
329        let mut acc_buf = [0_u8; 6];
330        spi_read_burst_2(
331            &mut self.spi,
332            &mut self.acc_cs,
333            BMI088_ACC_REG_ACCEL_X_LSB,
334            &mut acc_buf,
335        )
336        .map_err(|err| map_error("bmi088 acc burst", err))?;
337        let ax = bytes_to_i16(acc_buf[0], acc_buf[1]);
338        let ay = bytes_to_i16(acc_buf[2], acc_buf[3]);
339        let az = bytes_to_i16(acc_buf[4], acc_buf[5]);
340
341        // Read gyroscope (6 bytes: X, Y, Z as 16-bit little-endian)
342        let mut gyr_buf = [0_u8; 6];
343        spi_read_burst_1(
344            &mut self.spi,
345            &mut self.gyr_cs,
346            BMI088_GYR_REG_X_LSB,
347            &mut gyr_buf,
348        )
349        .map_err(|err| map_error("bmi088 gyro burst", err))?;
350        let gx = bytes_to_i16(gyr_buf[0], gyr_buf[1]);
351        let gy = bytes_to_i16(gyr_buf[2], gyr_buf[3]);
352        let gz = bytes_to_i16(gyr_buf[4], gyr_buf[5]);
353
354        // Read temperature (11-bit signed, split across 2 registers)
355        let temp_msb = spi_read_reg_2(&mut self.spi, &mut self.acc_cs, BMI088_ACC_REG_TEMP_MSB)
356            .map_err(|err| map_error("bmi088 temp msb", err))?;
357        let temp_lsb = spi_read_reg_2(&mut self.spi, &mut self.acc_cs, BMI088_ACC_REG_TEMP_LSB)
358            .map_err(|err| map_error("bmi088 temp lsb", err))?;
359        let temp_raw = (temp_msb as i16) * 8 + (temp_lsb as i16) / 32;
360        let temp_c = (temp_raw as f32) * 0.125 + 23.0;
361
362        // Remap BMI088 axes into NED body frame: +X forward, +Y right, +Z down.
363        // This mapping depends on how the sensor is mounted on your board.
364        let accel_mps2 = [
365            accel_raw_to_mps2(ay, self.acc_mps2_per_lsb),
366            accel_raw_to_mps2(ax, self.acc_mps2_per_lsb),
367            accel_raw_to_mps2(az, self.acc_mps2_per_lsb),
368        ];
369        let gyro_rad = [
370            -gyro_raw_to_rad(gy),
371            -gyro_raw_to_rad(gx),
372            -gyro_raw_to_rad(gz),
373        ];
374
375        Ok(ImuPayload::from_raw(accel_mps2, gyro_rad, temp_c))
376    }
377}
378
379// SPI helper types and functions
380
381#[derive(Debug)]
382enum SpiCsError<SpiErr, CsErr> {
383    Spi(SpiErr),
384    Cs(CsErr),
385}
386
387fn map_error<E: Debug>(context: &'static str, _err: E) -> CuError {
388    CuError::from(context)
389}
390
391fn spi_transfer<SPI, CS>(
392    spi: &mut SPI,
393    cs: &mut CS,
394    buf: &mut [u8],
395) -> Result<(), SpiCsError<SPI::Error, CS::Error>>
396where
397    SPI: Transfer<u8>,
398    CS: OutputPin,
399{
400    cs.set_low().map_err(SpiCsError::Cs)?;
401    let transfer_res = spi.transfer(buf).map_err(SpiCsError::Spi);
402    let cs_res = cs.set_high().map_err(SpiCsError::Cs);
403    if let Err(err) = transfer_res {
404        let _ = cs_res;
405        return Err(err);
406    }
407    cs_res?;
408    Ok(())
409}
410
411fn spi_write_reg<SPI, CS>(
412    spi: &mut SPI,
413    cs: &mut CS,
414    reg: u8,
415    value: u8,
416) -> Result<(), SpiCsError<SPI::Error, CS::Error>>
417where
418    SPI: Transfer<u8>,
419    CS: OutputPin,
420{
421    let mut buf = [reg & 0x7f, value];
422    spi_transfer(spi, cs, &mut buf)
423}
424
425/// Read a single register using standard SPI protocol (for gyroscope).
426fn spi_read_reg_1<SPI, CS>(
427    spi: &mut SPI,
428    cs: &mut CS,
429    reg: u8,
430) -> Result<u8, SpiCsError<SPI::Error, CS::Error>>
431where
432    SPI: Transfer<u8>,
433    CS: OutputPin,
434{
435    let mut buf = [reg | 0x80, 0x00];
436    spi_transfer(spi, cs, &mut buf)?;
437    Ok(buf[1])
438}
439
440/// Read a single register with dummy byte (for accelerometer).
441fn spi_read_reg_2<SPI, CS>(
442    spi: &mut SPI,
443    cs: &mut CS,
444    reg: u8,
445) -> Result<u8, SpiCsError<SPI::Error, CS::Error>>
446where
447    SPI: Transfer<u8>,
448    CS: OutputPin,
449{
450    let mut buf = [reg | 0x80, 0x00, 0x00];
451    spi_transfer(spi, cs, &mut buf)?;
452    Ok(buf[2])
453}
454
455/// Burst read 6 bytes using standard SPI protocol (for gyroscope).
456fn spi_read_burst_1<SPI, CS>(
457    spi: &mut SPI,
458    cs: &mut CS,
459    reg: u8,
460    out: &mut [u8; 6],
461) -> Result<(), SpiCsError<SPI::Error, CS::Error>>
462where
463    SPI: Transfer<u8>,
464    CS: OutputPin,
465{
466    let mut buf = [0_u8; 7];
467    buf[0] = reg | 0x80;
468    spi_transfer(spi, cs, &mut buf)?;
469    out.copy_from_slice(&buf[1..7]);
470    Ok(())
471}
472
473/// Burst read 6 bytes with dummy byte (for accelerometer).
474fn spi_read_burst_2<SPI, CS>(
475    spi: &mut SPI,
476    cs: &mut CS,
477    reg: u8,
478    out: &mut [u8; 6],
479) -> Result<(), SpiCsError<SPI::Error, CS::Error>>
480where
481    SPI: Transfer<u8>,
482    CS: OutputPin,
483{
484    let mut buf = [0_u8; 8];
485    buf[0] = reg | 0x80;
486    spi_transfer(spi, cs, &mut buf)?;
487    out.copy_from_slice(&buf[2..8]);
488    Ok(())
489}
490
491// Conversion helpers
492
493fn bytes_to_i16(lsb: u8, msb: u8) -> i16 {
494    i16::from_le_bytes([lsb, msb])
495}
496
497fn accel_raw_to_mps2(raw: i16, acc_mps2_per_lsb: f32) -> f32 {
498    raw as f32 * acc_mps2_per_lsb
499}
500
501fn gyro_raw_to_rad(raw: i16) -> f32 {
502    raw as f32 * GYRO_RAD_PER_LSB
503}
504
505fn accel_range_g_from_reg(range_reg: u8) -> f32 {
506    match range_reg & 0x03 {
507        0x00 => 3.0,
508        0x01 => 6.0,
509        0x02 => 12.0,
510        _ => 24.0,
511    }
512}