Skip to main content

stm_32/peripherals/
ist8308.rs

1// ******************************************************************************
2// * File     : platforms/stm_32/src/peripherals/ist8308.rs
3// * Date     : June 28, 2026
4// ******************************************************************************
5// *
6// * Copyright (c) 2023, AeroVironment, Inc.
7// * All rights reserved.
8// *
9// * Redistribution and use in source and binary forms, with or without
10// * modification, are permitted provided that the following conditions are met:
11// *
12// * 1.Redistributions of source code must retain the above copyright notice, this
13// * list of conditions and the following disclaimer.
14// *
15// * 2.Redistributions in binary form must reproduce the above copyright notice,
16// * this list of conditions and the following disclaimer in the documentation
17// * and/or other materials provided with the distribution.
18// *
19// * 3.Neither the name of the copyright holder nor the names of its
20// * contributors may be used to endorse or promote products derived from
21// * this software without specific prior written permission.
22// *
23// * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24// * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25// * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26// * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27// * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28// * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29// * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30// * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31// * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32// * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33// *
34// ******************************************************************************
35
36pub use embassy_stm32::mode::Async;
37pub use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
38pub use embassy_sync::signal::Signal;
39use veloxity_core::{errors, packets};
40
41// I2C Specific
42use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice;
43use embassy_stm32::i2c::I2c;
44use embedded_hal_async::i2c::I2c as _;
45
46// Polled Sensors
47use crate::synch_at;
48use embassy_time::Duration;
49use embassy_time::Timer;
50
51// Other
52
53pub static MAG_SIGNAL: Signal<
54    CriticalSectionRawMutex,
55    Result<packets::MagPacket, errors::SensorError>,
56> = Signal::<CriticalSectionRawMutex, Result<packets::MagPacket, errors::SensorError>>::new();
57
58fn publish_mag(result: Result<packets::MagPacket, errors::SensorError>) {
59    #[cfg(feature = "runtime-diagnostics")]
60    crate::runtime_diagnostics::record_signal_publish(
61        crate::runtime_diagnostics::SensorKind::Mag,
62        MAG_SIGNAL.signaled(),
63        result.is_err(),
64    );
65    MAG_SIGNAL.signal(result);
66}
67
68// ROSflight C compatibility scales for the Pixracer Pro IST8308 driver. The
69// sensor datasheet specifies a nominal 1.5e-7 T/LSB in the configured ±500 µT
70// range, but C uses these axis-specific values. Preserve them here so raw and
71// calibrated measurements, including persisted calibration parameters, remain
72// interchangeable between the C and Rust firmware.
73const ROSFLIGHT_C_SCALE_X_T_PER_LSB: f32 = 1.515e-7;
74const ROSFLIGHT_C_SCALE_YZ_T_PER_LSB: f32 = 1.1515e-7;
75
76pub struct Ist8308Sensor {
77    pub dev: I2cDevice<
78        'static,
79        CriticalSectionRawMutex,
80        I2c<'static, Async, embassy_stm32::i2c::mode::Master>,
81    >,
82}
83
84impl Ist8308Sensor {
85    async fn write_read(
86        &mut self,
87        address: u8,
88        register: &[u8],
89        data: &mut [u8],
90    ) -> Result<(), ()> {
91        match self.dev.write(address, register).await {
92            Err(_e) => return Err(()),
93            Ok(_) => {}
94        }
95
96        Timer::after(Duration::from_micros(0)).await;
97
98        // Read register
99        match self.dev.read(address, data).await {
100            Err(_e) => return Err(()),
101            Ok(_) => {}
102        }
103
104        Ok(())
105    }
106
107    pub async fn run(&mut self) {
108        const ADDRESS: u8 = 0x0C;
109
110        // Check device ID
111
112        const WAI_REG: u8 = 0x00;
113        let mut device_id = [0u8; 1];
114        if self
115            .write_read(ADDRESS, &[WAI_REG], &mut device_id)
116            .await
117            .is_err()
118        {
119            publish_mag(Err(errors::SensorError::GenericSensorError(
120                "IST8308 Mag failed: reading WAI_REG",
121            )));
122            return;
123        }
124        const DEVICE_ID: u8 = 0x08;
125        if device_id[0] != DEVICE_ID {
126            publish_mag(Err(errors::SensorError::GenericSensorError(
127                "IST8308 Mag failed: bad device ID",
128            )));
129            return;
130        }
131
132        // Reset
133
134        const CNTL3_REG: u8 = 0x32;
135        const CNTL3_VAL_SRST: u8 = 1;
136        if self
137            .dev
138            .write(ADDRESS, &[CNTL3_REG, CNTL3_VAL_SRST])
139            .await
140            .is_err()
141        {
142            publish_mag(Err(errors::SensorError::GenericSensorError(
143                "IST8308 Mag failed: writing CNTL3_REG",
144            )));
145            return;
146        }
147        Timer::after(Duration::from_millis(20)).await; // allow 20 ms to reset
148
149        //  Check status
150        let mut cntrl3 = [0u8; 1];
151        if self
152            .write_read(ADDRESS, &[CNTL3_REG], &mut cntrl3)
153            .await
154            .is_err()
155        {
156            publish_mag(Err(errors::SensorError::GenericSensorError(
157                "IST8308 Mag failed: reading CNTL3_REG",
158            )));
159            return;
160        }
161        if (cntrl3[0] & 0x01) != 0 {
162            publish_mag(Err(errors::SensorError::GenericSensorError(
163                "IST8308 Mag failed: bad status CNTL3_REG",
164            )));
165            return;
166        }
167
168        // Configure. The Pixracer Pro does not route IST8308 DRDY to the MCU,
169        // so leave its output disabled and use ROSflight C's timed
170        // command/wait/read sequence.
171
172        const CNTL4_REG: u8 = 0x34;
173        const CNTL4_VAL_DYNAMIC_RANGE_500: u8 = 0;
174
175        if self
176            .dev
177            .write(ADDRESS, &[CNTL4_REG, CNTL4_VAL_DYNAMIC_RANGE_500])
178            .await
179            .is_err()
180        {
181            publish_mag(Err(errors::SensorError::GenericSensorError(
182                "IST8308 Mag failed: writing CNTL4_REG",
183            )));
184            return;
185        }
186
187        const OSRCNTL_REG: u8 = 0x41;
188        const OSRCNTL_VAL_Y_16: u8 = 4 << 3;
189        const OSRCNTL_VAL_XZ_16: u8 = 4;
190
191        if self
192            .dev
193            .write(
194                ADDRESS,
195                &[OSRCNTL_REG, OSRCNTL_VAL_Y_16 | OSRCNTL_VAL_XZ_16],
196            )
197            .await
198            .is_err()
199        {
200            publish_mag(Err(errors::SensorError::GenericSensorError(
201                "IST8308 Mag failed: writing OSRCNTL_REG",
202            )));
203            return;
204        }
205
206        // ROSflight C operates the IST8308 in single-conversion mode. Trigger
207        // once here as C does during initialization; each acquisition period
208        // below explicitly triggers the next conversion.
209        const CNTL2_REG: u8 = 0x31;
210        const CNTL2_VAL_SINGLE_MODE: u8 = 0x01;
211        if self
212            .dev
213            .write(ADDRESS, &[CNTL2_REG, CNTL2_VAL_SINGLE_MODE])
214            .await
215            .is_err()
216        {
217            publish_mag(Err(errors::SensorError::GenericSensorError(
218                "IST8308 Mag failed: writing CNTL2_REG",
219            )));
220            return;
221        }
222
223        const STAT1_REG: u8 = 0x10;
224        const STAT1_VAL_DRDY: u8 = 0x01;
225        let loop_period = Duration::from_hz(100);
226        loop {
227            // Match C's 10 kHz polling-state phases: command at 0.0 ms,
228            // select STAT1 at 8.9 ms, and receive seven bytes at 9.2 ms.
229            let period_start = synch_at(loop_period);
230            Timer::at(period_start).await;
231
232            if self
233                .dev
234                .write(ADDRESS, &[CNTL2_REG, CNTL2_VAL_SINGLE_MODE])
235                .await
236                .is_err()
237            {
238                #[cfg(feature = "runtime-diagnostics")]
239                crate::runtime_diagnostics::record_mag_i2c_error();
240                publish_mag(Err(errors::SensorError::GenericSensorError(
241                    "IST8308 Mag failed: commanding single conversion",
242                )));
243                continue;
244            }
245            #[cfg(feature = "runtime-diagnostics")]
246            crate::runtime_diagnostics::record_mag_conversion_command();
247
248            let timestamp = period_start + Duration::from_micros(8_900);
249            Timer::at(timestamp).await;
250            if self.dev.write(ADDRESS, &[STAT1_REG]).await.is_err() {
251                #[cfg(feature = "runtime-diagnostics")]
252                crate::runtime_diagnostics::record_mag_i2c_error();
253                publish_mag(Err(errors::SensorError::GenericSensorError(
254                    "IST8308 Mag failed: selecting STAT1_REG",
255                )));
256                continue;
257            }
258
259            Timer::at(period_start + Duration::from_micros(9_200)).await;
260
261            // Read Data
262            let mut data = [0u8; 7];
263            if self.dev.read(ADDRESS, &mut data).await.is_err() {
264                #[cfg(feature = "runtime-diagnostics")]
265                crate::runtime_diagnostics::record_mag_i2c_error();
266                publish_mag(Err(errors::SensorError::GenericSensorError(
267                    "IST8308 Mag failed: reading STAT1_REG",
268                )));
269                continue;
270            }
271
272            let status = data[0];
273            let data_ready = status == STAT1_VAL_DRDY;
274            #[cfg(feature = "runtime-diagnostics")]
275            crate::runtime_diagnostics::record_mag_drdy(data_ready);
276            if data_ready {
277                let flux = [
278                    f32::from((((data[2] as u16) << 8) | (data[1] as u16)) as i16)
279                        * ROSFLIGHT_C_SCALE_X_T_PER_LSB,
280                    f32::from((((data[4] as u16) << 8) | (data[3] as u16)) as i16)
281                        * ROSFLIGHT_C_SCALE_YZ_T_PER_LSB,
282                    // Match the ROSflight C Pixracer driver coordinate convention.
283                    -f32::from((((data[6] as u16) << 8) | (data[5] as u16)) as i16)
284                        * ROSFLIGHT_C_SCALE_YZ_T_PER_LSB,
285                ]; // Units of Tesla
286
287                let timestamp_us = timestamp.as_micros();
288
289                let header = packets::RosflightPacketHeader {
290                    timestamp: timestamp_us,
291                    status: status as u16,
292                };
293
294                let mag_packet = packets::MagPacket {
295                    header,
296                    flux,
297                    temperature: 0.0f32,
298                };
299                publish_mag(Ok(mag_packet)); // make data available for other tasks
300                // previous_timestamp_us = timestamp_us;
301            }
302        }
303    }
304}
305
306#[embassy_executor::task]
307pub async fn task(mut ist: Ist8308Sensor) {
308    ist.run().await;
309}