Skip to main content

cu_ist8310/
lib.rs

1//! Copper source driver for the iSentek IST8310 digital magnetometer.
2//!
3//! This source expects an address-scoped I2C resource so the sensor address is
4//! defined by the board resource bundle (hardware mapping), not by component config.
5
6#![cfg_attr(not(feature = "std"), no_std)]
7
8extern crate alloc;
9
10use alloc::{format, string::String};
11use core::fmt::Debug;
12
13pub use cu_sensor_payloads::MagnetometerPayload;
14use cu29::prelude::*;
15use cu29::units::si::magnetic_flux_density::microtesla;
16
17// Registers
18const REG_WHOAMI: u8 = 0x00;
19const REG_STAT1: u8 = 0x02;
20const REG_DATA_X_L: u8 = 0x03;
21const REG_CNTRL1: u8 = 0x0A;
22const REG_AVGCNTL: u8 = 0x41;
23const REG_PDCNTL: u8 = 0x42;
24
25// IDs / bitfields
26const IST8310_CHIP_ID: u8 = 0x10;
27const STAT1_DRDY: u8 = 0x01;
28
29// Configuration
30const CNTRL1_SINGLE_MEASURE: u8 = 0x01;
31const AVGCNTL_16X: u8 = 0x24;
32const PDCNTL_PULSE_DURATION_NORMAL: u8 = 0xC0;
33
34// 0.3 uT / LSB = 3 mG / LSB
35const UT_PER_LSB: f32 = 0.3;
36
37const DETECT_RETRY_LIMIT: usize = 5_000;
38const I2C_TRANSFER_RETRY_LIMIT: usize = 8;
39const OUTPUT_RATE_HZ: u64 = 100;
40const OUTPUT_PERIOD_NS: u64 = 1_000_000_000 / OUTPUT_RATE_HZ;
41const MEASURE_MIN_DELAY_NS: u64 = 6_000_000;
42const MEASURE_TIMEOUT_NS: u64 = 20_000_000;
43const DRIVER_LOG_PERIOD_NS: u64 = 1_000_000_000;
44
45/// Address-scoped bus interface for IST8310 register I/O.
46///
47/// Implement this trait in your board bundle resource type so the driver does
48/// not carry or configure an I2C address.
49pub trait Ist8310Bus: Send + Sync + 'static {
50    type Error: Debug + Send + 'static;
51
52    fn write(&mut self, write: &[u8]) -> Result<(), Self::Error>;
53    fn write_read(&mut self, write: &[u8], read: &mut [u8]) -> Result<(), Self::Error>;
54}
55
56struct Ist8310Driver<BUS>
57where
58    BUS: Ist8310Bus,
59{
60    bus: BUS,
61}
62
63impl<BUS> Ist8310Driver<BUS>
64where
65    BUS: Ist8310Bus,
66{
67    fn new(bus: BUS) -> CuResult<Self> {
68        let mut driver = Self { bus };
69        driver.detect_chip_id()?;
70        driver.configure()?;
71        Ok(driver)
72    }
73
74    fn detect_chip_id(&mut self) -> CuResult<()> {
75        let mut last_id: Option<u8> = None;
76        let mut read_buf = [0u8; 1];
77
78        for _ in 0..DETECT_RETRY_LIMIT {
79            if self.read_reg_buf(REG_WHOAMI, &mut read_buf).is_ok() {
80                let id = read_buf[0];
81                last_id = Some(id);
82                if id == IST8310_CHIP_ID {
83                    debug!("ist8310: detected id=0x{:02X}", id);
84                    return Ok(());
85                }
86            }
87            backoff_spin();
88        }
89
90        let last_id_str = match last_id {
91            Some(id) => format!("0x{id:02X}"),
92            None => String::from("none"),
93        };
94        Err(CuError::from(format!(
95            "ist8310 detect failed: expected=0x{IST8310_CHIP_ID:02X} last_id={last_id_str}"
96        )))
97    }
98
99    fn configure(&mut self) -> CuResult<()> {
100        // Datasheet recommends setting pulse duration first in standby mode.
101        self.write_reg_value(REG_AVGCNTL, AVGCNTL_16X)
102            .map_err(|err| map_debug_error("ist8310 configure AVGCNTL", err))?;
103        self.write_reg_value(REG_PDCNTL, PDCNTL_PULSE_DURATION_NORMAL)
104            .map_err(|err| map_debug_error("ist8310 configure PDCNTL", err))?;
105        debug!("ist8310: configured AVG16 and pulse duration normal");
106        Ok(())
107    }
108
109    fn trigger_measurement(&mut self) -> CuResult<()> {
110        self.write_reg_value(REG_CNTRL1, CNTRL1_SINGLE_MEASURE)
111            .map_err(|err| map_debug_error("ist8310 trigger single measure", err))
112    }
113
114    fn read_measure_if_ready(&mut self) -> CuResult<Option<MagnetometerPayload>> {
115        let stat1 = self.read_reg(REG_STAT1)?;
116        if (stat1 & STAT1_DRDY) == 0 {
117            return Ok(None);
118        }
119
120        let mut buf = [0u8; 6];
121        self.read_reg_buf(REG_DATA_X_L, &mut buf)?;
122
123        let x_raw = i16::from_le_bytes([buf[0], buf[1]]);
124        let y_raw = i16::from_le_bytes([buf[2], buf[3]]);
125        let z_raw = i16::from_le_bytes([buf[4], buf[5]]);
126
127        // IST8310 board/alignment conventions in BF/INAV invert Y to right-hand system.
128        let mag_x = (x_raw as f32) * UT_PER_LSB;
129        let mag_y = -(y_raw as f32) * UT_PER_LSB;
130        let mag_z = (z_raw as f32) * UT_PER_LSB;
131
132        Ok(Some(MagnetometerPayload::from_raw([mag_x, mag_y, mag_z])))
133    }
134
135    fn read_reg(&mut self, reg: u8) -> CuResult<u8> {
136        let mut byte = [0u8; 1];
137        self.read_reg_buf(reg, &mut byte)?;
138        Ok(byte[0])
139    }
140
141    fn read_reg_buf(&mut self, reg: u8, read: &mut [u8]) -> CuResult<()> {
142        for _ in 0..I2C_TRANSFER_RETRY_LIMIT {
143            if self.bus.write_read(&[reg], read).is_ok() {
144                return Ok(());
145            }
146            backoff_spin();
147        }
148        Err(CuError::from(format!(
149            "ist8310 i2c write_read failed: reg=0x{reg:02X} len={} retries={I2C_TRANSFER_RETRY_LIMIT}",
150            read.len()
151        )))
152    }
153
154    fn write_reg_value(&mut self, reg: u8, value: u8) -> CuResult<()> {
155        for _ in 0..I2C_TRANSFER_RETRY_LIMIT {
156            if self.bus.write(&[reg, value]).is_ok() {
157                return Ok(());
158            }
159            backoff_spin();
160        }
161        Err(CuError::from(format!(
162            "ist8310 i2c write failed: reg=0x{reg:02X} value=0x{value:02X} retries={I2C_TRANSFER_RETRY_LIMIT}"
163        )))
164    }
165}
166
167fn backoff_spin() {
168    spin_wait(128);
169}
170
171fn spin_wait(iterations: usize) {
172    for _ in 0..iterations {
173        core::hint::spin_loop();
174    }
175}
176
177fn map_debug_error<E: Debug>(context: &str, err: E) -> CuError {
178    CuError::from(format!("{context}: {err:?}"))
179}
180
181resources!(for <BUS>
182where
183    BUS: Ist8310Bus,
184{
185    i2c => Owned<BUS>,
186});
187
188/// Copper source task for IST8310.
189#[derive(Reflect)]
190#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
191pub struct Ist8310Source<BUS>
192where
193    BUS: Ist8310Bus,
194{
195    #[reflect(ignore)]
196    driver: Ist8310Driver<BUS>,
197    measure_state: MeasureState,
198    last_output_ns: Option<u64>,
199    last_log_ns: Option<u64>,
200}
201
202#[derive(Clone, Copy, Reflect)]
203enum MeasureState {
204    Idle,
205    Measuring { started_ns: u64 },
206}
207
208impl<BUS> TypePath for Ist8310Source<BUS>
209where
210    BUS: Ist8310Bus,
211{
212    fn type_path() -> &'static str {
213        "cu_ist8310::Ist8310Source"
214    }
215
216    fn short_type_path() -> &'static str {
217        "Ist8310Source"
218    }
219
220    fn type_ident() -> Option<&'static str> {
221        Some("Ist8310Source")
222    }
223
224    fn crate_name() -> Option<&'static str> {
225        Some("cu_ist8310")
226    }
227
228    fn module_path() -> Option<&'static str> {
229        Some("")
230    }
231}
232
233impl<BUS> Freezable for Ist8310Source<BUS> where BUS: Ist8310Bus {}
234
235impl<BUS> CuSrcTask for Ist8310Source<BUS>
236where
237    BUS: Ist8310Bus,
238{
239    type Resources<'r> = Resources<BUS>;
240    type Output<'m> = output_msg!(MagnetometerPayload);
241
242    fn new(_config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
243    where
244        Self: Sized,
245    {
246        let driver = Ist8310Driver::new(resources.i2c.0)?;
247        Ok(Self {
248            driver,
249            measure_state: MeasureState::Idle,
250            last_output_ns: None,
251            last_log_ns: None,
252        })
253    }
254
255    fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
256        debug!(ctx, "ist8310: source started");
257        Ok(())
258    }
259
260    fn process<'o>(&mut self, ctx: &CuContext, output: &mut Self::Output<'o>) -> CuResult<()> {
261        let tov = ctx.now();
262        let now_ns = tov.as_nanos();
263
264        let payload = match self.measure_state {
265            MeasureState::Idle => {
266                if let Some(last_output_ns) = self.last_output_ns
267                    && now_ns.saturating_sub(last_output_ns) < OUTPUT_PERIOD_NS
268                {
269                    output.clear_payload();
270                    output.tov = Tov::None;
271                    return Ok(());
272                }
273
274                self.driver.trigger_measurement()?;
275                self.measure_state = MeasureState::Measuring { started_ns: now_ns };
276                output.clear_payload();
277                output.tov = Tov::None;
278                return Ok(());
279            }
280            MeasureState::Measuring { started_ns } => {
281                let elapsed_ns = now_ns.saturating_sub(started_ns);
282                if elapsed_ns < MEASURE_MIN_DELAY_NS {
283                    output.clear_payload();
284                    output.tov = Tov::None;
285                    return Ok(());
286                }
287
288                if elapsed_ns > MEASURE_TIMEOUT_NS {
289                    self.measure_state = MeasureState::Idle;
290                    output.clear_payload();
291                    output.tov = Tov::None;
292                    return Ok(());
293                }
294
295                match self.driver.read_measure_if_ready()? {
296                    Some(payload) => {
297                        self.measure_state = MeasureState::Idle;
298                        payload
299                    }
300                    None => {
301                        output.clear_payload();
302                        output.tov = Tov::None;
303                        return Ok(());
304                    }
305                }
306            }
307        };
308        self.last_output_ns = Some(now_ns);
309
310        if self
311            .last_log_ns
312            .is_none_or(|last_log_ns| now_ns.saturating_sub(last_log_ns) >= DRIVER_LOG_PERIOD_NS)
313        {
314            self.last_log_ns = Some(now_ns);
315            info!(
316                ctx,
317                "ist8310: mag_x_ut={} mag_y_ut={} mag_z_ut={} rate_hz={}",
318                payload.mag_x.get::<microtesla>(),
319                payload.mag_y.get::<microtesla>(),
320                payload.mag_z.get::<microtesla>(),
321                OUTPUT_RATE_HZ
322            );
323        }
324
325        output.tov = Tov::Time(tov);
326        output.set_payload(payload);
327        Ok(())
328    }
329}