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>`
108pub struct Bmi088Source<SPI, ACC, GYR, D> {
109    driver: Bmi088Driver<SPI, ACC, GYR, D>,
110}
111
112impl<SPI, ACC, GYR, D> Freezable for Bmi088Source<SPI, ACC, GYR, D>
113where
114    SPI: Transfer<u8> + Send + Sync + 'static,
115    SPI::Error: Debug + Send + 'static,
116    ACC: OutputPin + Send + Sync + 'static,
117    ACC::Error: Debug + Send + 'static,
118    GYR: OutputPin + Send + Sync + 'static,
119    GYR::Error: Debug + Send + 'static,
120    D: DelayMs<u32> + Send + Sync + 'static,
121{
122}
123
124impl<SPI, ACC, GYR, D> CuSrcTask for Bmi088Source<SPI, ACC, GYR, D>
125where
126    SPI: Transfer<u8> + Send + Sync + 'static,
127    SPI::Error: Debug + Send + 'static,
128    ACC: OutputPin + Send + Sync + 'static,
129    ACC::Error: Debug + Send + 'static,
130    GYR: OutputPin + Send + Sync + 'static,
131    GYR::Error: Debug + Send + 'static,
132    D: DelayMs<u32> + Send + Sync + 'static,
133{
134    type Resources<'r> = Resources<SPI, ACC, GYR, D>;
135    type Output<'m> = output_msg!(ImuPayload);
136
137    fn new(_config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
138    where
139        Self: Sized,
140    {
141        let driver = Bmi088Driver::new(
142            resources.spi.0,
143            resources.acc_cs.0,
144            resources.gyr_cs.0,
145            resources.delay.0,
146        )?;
147        Ok(Self { driver })
148    }
149
150    fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
151        Ok(())
152    }
153
154    fn process<'o>(&mut self, clock: &RobotClock, output: &mut Self::Output<'o>) -> CuResult<()> {
155        let tov = clock.now();
156        let payload = self.driver.read_measure()?;
157        output.tov = Tov::Time(tov);
158        output.set_payload(payload);
159        Ok(())
160    }
161}
162
163/// Low-level driver for the BMI088 IMU.
164struct Bmi088Driver<SPI, ACC, GYR, D> {
165    spi: SPI,
166    acc_cs: ACC,
167    gyr_cs: GYR,
168    #[allow(dead_code)]
169    delay: D,
170    acc_mps2_per_lsb: f32,
171}
172
173impl<SPI, ACC, GYR, D> Bmi088Driver<SPI, ACC, GYR, D>
174where
175    SPI: Transfer<u8>,
176    SPI::Error: Debug,
177    ACC: OutputPin,
178    ACC::Error: Debug,
179    GYR: OutputPin,
180    GYR::Error: Debug,
181    D: DelayMs<u32>,
182{
183    /// Initialize the BMI088 driver.
184    ///
185    /// This performs:
186    /// 1. Chip ID verification for both accelerometer and gyroscope
187    /// 2. Soft reset of both sensors
188    /// 3. Power-on and configuration of accelerometer
189    /// 4. Configuration of gyroscope (2000 dps range, 2000 Hz ODR)
190    fn new(mut spi: SPI, mut acc_cs: ACC, mut gyr_cs: GYR, mut delay: D) -> CuResult<Self> {
191        // Ensure CS lines are high (deselected)
192        acc_cs
193            .set_high()
194            .map_err(|err| map_error("bmi088 acc cs high", err))?;
195        gyr_cs
196            .set_high()
197            .map_err(|err| map_error("bmi088 gyr cs high", err))?;
198
199        // Verify chip IDs
200        let acc_id = spi_read_reg_2(&mut spi, &mut acc_cs, BMI088_ACC_REG_CHIP_ID)
201            .map_err(|err| map_error("bmi088 acc chip id", err))?;
202        let gyr_id = spi_read_reg_1(&mut spi, &mut gyr_cs, BMI088_GYR_REG_CHIP_ID)
203            .map_err(|err| map_error("bmi088 gyr chip id", err))?;
204
205        if acc_id != BMI088_ACC_CHIP_ID {
206            return Err(CuError::from("bmi088 accel id mismatch"));
207        }
208        if gyr_id != BMI088_GYR_CHIP_ID {
209            return Err(CuError::from("bmi088 gyro id mismatch"));
210        }
211
212        // Reset accelerometer
213        spi_write_reg(
214            &mut spi,
215            &mut acc_cs,
216            BMI088_ACC_REG_SOFT_RESET,
217            BMI088_SOFT_RESET_CMD,
218        )
219        .map_err(|err| map_error("bmi088 acc reset", err))?;
220        delay.delay_ms(10);
221
222        // Power on accelerometer
223        spi_write_reg(
224            &mut spi,
225            &mut acc_cs,
226            BMI088_ACC_REG_PWR_CONF,
227            BMI088_ACC_PWR_CONF_ACTIVE,
228        )
229        .map_err(|err| map_error("bmi088 acc pwr conf", err))?;
230        delay.delay_ms(1);
231        spi_write_reg(
232            &mut spi,
233            &mut acc_cs,
234            BMI088_ACC_REG_PWR_CTRL,
235            BMI088_ACC_PWR_CTRL_ON,
236        )
237        .map_err(|err| map_error("bmi088 acc pwr ctrl", err))?;
238        delay.delay_ms(50);
239
240        // Reset and configure gyroscope
241        spi_write_reg(
242            &mut spi,
243            &mut gyr_cs,
244            BMI088_GYR_REG_SOFT_RESET,
245            BMI088_SOFT_RESET_CMD,
246        )
247        .map_err(|err| map_error("bmi088 gyro reset", err))?;
248        delay.delay_ms(100);
249        spi_write_reg(
250            &mut spi,
251            &mut gyr_cs,
252            BMI088_GYR_REG_RANGE,
253            BMI088_GYR_RANGE_2000,
254        )
255        .map_err(|err| map_error("bmi088 gyro range", err))?;
256        spi_write_reg(
257            &mut spi,
258            &mut gyr_cs,
259            BMI088_GYR_REG_BANDWIDTH,
260            BMI088_GYR_BANDWIDTH,
261        )
262        .map_err(|err| map_error("bmi088 gyro bandwidth", err))?;
263        spi_write_reg(
264            &mut spi,
265            &mut gyr_cs,
266            BMI088_GYR_REG_POWER_MODE,
267            BMI088_GYR_PWR_NORMAL,
268        )
269        .map_err(|err| map_error("bmi088 gyro pwr mode", err))?;
270
271        // Read accelerometer range for scaling
272        let acc_range_reg = spi_read_reg_2(&mut spi, &mut acc_cs, BMI088_ACC_REG_ACC_RANGE)
273            .map_err(|err| map_error("bmi088 acc range", err))?;
274        let acc_range_g = accel_range_g_from_reg(acc_range_reg);
275        let acc_mps2_per_lsb = acc_range_g * 9.806_65 / 32_768.0;
276        debug!(
277            "bmi088 accel range reg={} -> ±{}g",
278            acc_range_reg, acc_range_g
279        );
280
281        Ok(Self {
282            spi,
283            acc_cs,
284            gyr_cs,
285            delay,
286            acc_mps2_per_lsb,
287        })
288    }
289
290    /// Read accelerometer, gyroscope, and temperature data.
291    ///
292    /// Returns an [`ImuPayload`] with:
293    /// - Acceleration in m/s² (NED frame)
294    /// - Angular velocity in rad/s (NED frame)
295    /// - Temperature in °C
296    fn read_measure(&mut self) -> CuResult<ImuPayload> {
297        // Read accelerometer (6 bytes: X, Y, Z as 16-bit little-endian)
298        let mut acc_buf = [0_u8; 6];
299        spi_read_burst_2(
300            &mut self.spi,
301            &mut self.acc_cs,
302            BMI088_ACC_REG_ACCEL_X_LSB,
303            &mut acc_buf,
304        )
305        .map_err(|err| map_error("bmi088 acc burst", err))?;
306        let ax = bytes_to_i16(acc_buf[0], acc_buf[1]);
307        let ay = bytes_to_i16(acc_buf[2], acc_buf[3]);
308        let az = bytes_to_i16(acc_buf[4], acc_buf[5]);
309
310        // Read gyroscope (6 bytes: X, Y, Z as 16-bit little-endian)
311        let mut gyr_buf = [0_u8; 6];
312        spi_read_burst_1(
313            &mut self.spi,
314            &mut self.gyr_cs,
315            BMI088_GYR_REG_X_LSB,
316            &mut gyr_buf,
317        )
318        .map_err(|err| map_error("bmi088 gyro burst", err))?;
319        let gx = bytes_to_i16(gyr_buf[0], gyr_buf[1]);
320        let gy = bytes_to_i16(gyr_buf[2], gyr_buf[3]);
321        let gz = bytes_to_i16(gyr_buf[4], gyr_buf[5]);
322
323        // Read temperature (11-bit signed, split across 2 registers)
324        let temp_msb = spi_read_reg_2(&mut self.spi, &mut self.acc_cs, BMI088_ACC_REG_TEMP_MSB)
325            .map_err(|err| map_error("bmi088 temp msb", err))?;
326        let temp_lsb = spi_read_reg_2(&mut self.spi, &mut self.acc_cs, BMI088_ACC_REG_TEMP_LSB)
327            .map_err(|err| map_error("bmi088 temp lsb", err))?;
328        let temp_raw = (temp_msb as i16) * 8 + (temp_lsb as i16) / 32;
329        let temp_c = (temp_raw as f32) * 0.125 + 23.0;
330
331        // Remap BMI088 axes into NED body frame: +X forward, +Y right, +Z down.
332        // This mapping depends on how the sensor is mounted on your board.
333        let accel_mps2 = [
334            accel_raw_to_mps2(ay, self.acc_mps2_per_lsb),
335            accel_raw_to_mps2(ax, self.acc_mps2_per_lsb),
336            accel_raw_to_mps2(az, self.acc_mps2_per_lsb),
337        ];
338        let gyro_rad = [
339            -gyro_raw_to_rad(gy),
340            -gyro_raw_to_rad(gx),
341            -gyro_raw_to_rad(gz),
342        ];
343
344        Ok(ImuPayload::from_raw(accel_mps2, gyro_rad, temp_c))
345    }
346}
347
348// SPI helper types and functions
349
350#[derive(Debug)]
351enum SpiCsError<SpiErr, CsErr> {
352    Spi(SpiErr),
353    Cs(CsErr),
354}
355
356fn map_error<E: Debug>(context: &'static str, _err: E) -> CuError {
357    CuError::from(context)
358}
359
360fn spi_transfer<SPI, CS>(
361    spi: &mut SPI,
362    cs: &mut CS,
363    buf: &mut [u8],
364) -> Result<(), SpiCsError<SPI::Error, CS::Error>>
365where
366    SPI: Transfer<u8>,
367    CS: OutputPin,
368{
369    cs.set_low().map_err(SpiCsError::Cs)?;
370    let transfer_res = spi.transfer(buf).map_err(SpiCsError::Spi);
371    let cs_res = cs.set_high().map_err(SpiCsError::Cs);
372    if let Err(err) = transfer_res {
373        let _ = cs_res;
374        return Err(err);
375    }
376    cs_res?;
377    Ok(())
378}
379
380fn spi_write_reg<SPI, CS>(
381    spi: &mut SPI,
382    cs: &mut CS,
383    reg: u8,
384    value: u8,
385) -> Result<(), SpiCsError<SPI::Error, CS::Error>>
386where
387    SPI: Transfer<u8>,
388    CS: OutputPin,
389{
390    let mut buf = [reg & 0x7f, value];
391    spi_transfer(spi, cs, &mut buf)
392}
393
394/// Read a single register using standard SPI protocol (for gyroscope).
395fn spi_read_reg_1<SPI, CS>(
396    spi: &mut SPI,
397    cs: &mut CS,
398    reg: u8,
399) -> Result<u8, SpiCsError<SPI::Error, CS::Error>>
400where
401    SPI: Transfer<u8>,
402    CS: OutputPin,
403{
404    let mut buf = [reg | 0x80, 0x00];
405    spi_transfer(spi, cs, &mut buf)?;
406    Ok(buf[1])
407}
408
409/// Read a single register with dummy byte (for accelerometer).
410fn spi_read_reg_2<SPI, CS>(
411    spi: &mut SPI,
412    cs: &mut CS,
413    reg: u8,
414) -> Result<u8, SpiCsError<SPI::Error, CS::Error>>
415where
416    SPI: Transfer<u8>,
417    CS: OutputPin,
418{
419    let mut buf = [reg | 0x80, 0x00, 0x00];
420    spi_transfer(spi, cs, &mut buf)?;
421    Ok(buf[2])
422}
423
424/// Burst read 6 bytes using standard SPI protocol (for gyroscope).
425fn spi_read_burst_1<SPI, CS>(
426    spi: &mut SPI,
427    cs: &mut CS,
428    reg: u8,
429    out: &mut [u8; 6],
430) -> Result<(), SpiCsError<SPI::Error, CS::Error>>
431where
432    SPI: Transfer<u8>,
433    CS: OutputPin,
434{
435    let mut buf = [0_u8; 7];
436    buf[0] = reg | 0x80;
437    spi_transfer(spi, cs, &mut buf)?;
438    out.copy_from_slice(&buf[1..7]);
439    Ok(())
440}
441
442/// Burst read 6 bytes with dummy byte (for accelerometer).
443fn spi_read_burst_2<SPI, CS>(
444    spi: &mut SPI,
445    cs: &mut CS,
446    reg: u8,
447    out: &mut [u8; 6],
448) -> Result<(), SpiCsError<SPI::Error, CS::Error>>
449where
450    SPI: Transfer<u8>,
451    CS: OutputPin,
452{
453    let mut buf = [0_u8; 8];
454    buf[0] = reg | 0x80;
455    spi_transfer(spi, cs, &mut buf)?;
456    out.copy_from_slice(&buf[2..8]);
457    Ok(())
458}
459
460// Conversion helpers
461
462fn bytes_to_i16(lsb: u8, msb: u8) -> i16 {
463    i16::from_le_bytes([lsb, msb])
464}
465
466fn accel_raw_to_mps2(raw: i16, acc_mps2_per_lsb: f32) -> f32 {
467    raw as f32 * acc_mps2_per_lsb
468}
469
470fn gyro_raw_to_rad(raw: i16) -> f32 {
471    raw as f32 * GYRO_RAD_PER_LSB
472}
473
474fn accel_range_g_from_reg(range_reg: u8) -> f32 {
475    match range_reg & 0x03 {
476        0x00 => 3.0,
477        0x01 => 6.0,
478        0x02 => 12.0,
479        _ => 24.0,
480    }
481}