#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::{format, string::String};
use core::fmt::Debug;
pub use cu_sensor_payloads::MagnetometerPayload;
use cu29::prelude::*;
use cu29::units::si::magnetic_flux_density::microtesla;
const REG_WHOAMI: u8 = 0x00;
const REG_STAT1: u8 = 0x02;
const REG_DATA_X_L: u8 = 0x03;
const REG_CNTRL1: u8 = 0x0A;
const REG_AVGCNTL: u8 = 0x41;
const REG_PDCNTL: u8 = 0x42;
const IST8310_CHIP_ID: u8 = 0x10;
const STAT1_DRDY: u8 = 0x01;
const CNTRL1_SINGLE_MEASURE: u8 = 0x01;
const AVGCNTL_16X: u8 = 0x24;
const PDCNTL_PULSE_DURATION_NORMAL: u8 = 0xC0;
const UT_PER_LSB: f32 = 0.3;
const DETECT_RETRY_LIMIT: usize = 5_000;
const I2C_TRANSFER_RETRY_LIMIT: usize = 8;
const OUTPUT_RATE_HZ: u64 = 100;
const OUTPUT_PERIOD_NS: u64 = 1_000_000_000 / OUTPUT_RATE_HZ;
const MEASURE_MIN_DELAY_NS: u64 = 6_000_000;
const MEASURE_TIMEOUT_NS: u64 = 20_000_000;
const DRIVER_LOG_PERIOD_NS: u64 = 1_000_000_000;
pub trait Ist8310Bus: Send + Sync + 'static {
type Error: Debug + Send + 'static;
fn write(&mut self, write: &[u8]) -> Result<(), Self::Error>;
fn write_read(&mut self, write: &[u8], read: &mut [u8]) -> Result<(), Self::Error>;
}
struct Ist8310Driver<BUS>
where
BUS: Ist8310Bus,
{
bus: BUS,
}
impl<BUS> Ist8310Driver<BUS>
where
BUS: Ist8310Bus,
{
fn new(bus: BUS) -> CuResult<Self> {
let mut driver = Self { bus };
driver.detect_chip_id()?;
driver.configure()?;
Ok(driver)
}
fn detect_chip_id(&mut self) -> CuResult<()> {
let mut last_id: Option<u8> = None;
let mut read_buf = [0u8; 1];
for _ in 0..DETECT_RETRY_LIMIT {
if self.read_reg_buf(REG_WHOAMI, &mut read_buf).is_ok() {
let id = read_buf[0];
last_id = Some(id);
if id == IST8310_CHIP_ID {
debug!("ist8310: detected id=0x{:02X}", id);
return Ok(());
}
}
backoff_spin();
}
let last_id_str = match last_id {
Some(id) => format!("0x{id:02X}"),
None => String::from("none"),
};
Err(CuError::from(format!(
"ist8310 detect failed: expected=0x{IST8310_CHIP_ID:02X} last_id={last_id_str}"
)))
}
fn configure(&mut self) -> CuResult<()> {
self.write_reg_value(REG_AVGCNTL, AVGCNTL_16X)
.map_err(|err| map_debug_error("ist8310 configure AVGCNTL", err))?;
self.write_reg_value(REG_PDCNTL, PDCNTL_PULSE_DURATION_NORMAL)
.map_err(|err| map_debug_error("ist8310 configure PDCNTL", err))?;
debug!("ist8310: configured AVG16 and pulse duration normal");
Ok(())
}
fn trigger_measurement(&mut self) -> CuResult<()> {
self.write_reg_value(REG_CNTRL1, CNTRL1_SINGLE_MEASURE)
.map_err(|err| map_debug_error("ist8310 trigger single measure", err))
}
fn read_measure_if_ready(&mut self) -> CuResult<Option<MagnetometerPayload>> {
let stat1 = self.read_reg(REG_STAT1)?;
if (stat1 & STAT1_DRDY) == 0 {
return Ok(None);
}
let mut buf = [0u8; 6];
self.read_reg_buf(REG_DATA_X_L, &mut buf)?;
let x_raw = i16::from_le_bytes([buf[0], buf[1]]);
let y_raw = i16::from_le_bytes([buf[2], buf[3]]);
let z_raw = i16::from_le_bytes([buf[4], buf[5]]);
let mag_x = (x_raw as f32) * UT_PER_LSB;
let mag_y = -(y_raw as f32) * UT_PER_LSB;
let mag_z = (z_raw as f32) * UT_PER_LSB;
Ok(Some(MagnetometerPayload::from_raw([mag_x, mag_y, mag_z])))
}
fn read_reg(&mut self, reg: u8) -> CuResult<u8> {
let mut byte = [0u8; 1];
self.read_reg_buf(reg, &mut byte)?;
Ok(byte[0])
}
fn read_reg_buf(&mut self, reg: u8, read: &mut [u8]) -> CuResult<()> {
for _ in 0..I2C_TRANSFER_RETRY_LIMIT {
if self.bus.write_read(&[reg], read).is_ok() {
return Ok(());
}
backoff_spin();
}
Err(CuError::from(format!(
"ist8310 i2c write_read failed: reg=0x{reg:02X} len={} retries={I2C_TRANSFER_RETRY_LIMIT}",
read.len()
)))
}
fn write_reg_value(&mut self, reg: u8, value: u8) -> CuResult<()> {
for _ in 0..I2C_TRANSFER_RETRY_LIMIT {
if self.bus.write(&[reg, value]).is_ok() {
return Ok(());
}
backoff_spin();
}
Err(CuError::from(format!(
"ist8310 i2c write failed: reg=0x{reg:02X} value=0x{value:02X} retries={I2C_TRANSFER_RETRY_LIMIT}"
)))
}
}
fn backoff_spin() {
spin_wait(128);
}
fn spin_wait(iterations: usize) {
for _ in 0..iterations {
core::hint::spin_loop();
}
}
fn map_debug_error<E: Debug>(context: &str, err: E) -> CuError {
CuError::from(format!("{context}: {err:?}"))
}
resources!(for <BUS>
where
BUS: Ist8310Bus,
{
i2c => Owned<BUS>,
});
#[derive(Reflect)]
#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
pub struct Ist8310Source<BUS>
where
BUS: Ist8310Bus,
{
#[reflect(ignore)]
driver: Ist8310Driver<BUS>,
measure_state: MeasureState,
last_output_ns: Option<u64>,
last_log_ns: Option<u64>,
}
#[derive(Clone, Copy, Reflect)]
enum MeasureState {
Idle,
Measuring { started_ns: u64 },
}
impl<BUS> TypePath for Ist8310Source<BUS>
where
BUS: Ist8310Bus,
{
fn type_path() -> &'static str {
"cu_ist8310::Ist8310Source"
}
fn short_type_path() -> &'static str {
"Ist8310Source"
}
fn type_ident() -> Option<&'static str> {
Some("Ist8310Source")
}
fn crate_name() -> Option<&'static str> {
Some("cu_ist8310")
}
fn module_path() -> Option<&'static str> {
Some("")
}
}
impl<BUS> Freezable for Ist8310Source<BUS> where BUS: Ist8310Bus {}
impl<BUS> CuSrcTask for Ist8310Source<BUS>
where
BUS: Ist8310Bus,
{
type Resources<'r> = Resources<BUS>;
type Output<'m> = output_msg!(MagnetometerPayload);
fn new(_config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
where
Self: Sized,
{
let driver = Ist8310Driver::new(resources.i2c.0)?;
Ok(Self {
driver,
measure_state: MeasureState::Idle,
last_output_ns: None,
last_log_ns: None,
})
}
fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
debug!(ctx, "ist8310: source started");
Ok(())
}
fn process<'o>(&mut self, ctx: &CuContext, output: &mut Self::Output<'o>) -> CuResult<()> {
let tov = ctx.now();
let now_ns = tov.as_nanos();
let payload = match self.measure_state {
MeasureState::Idle => {
if let Some(last_output_ns) = self.last_output_ns
&& now_ns.saturating_sub(last_output_ns) < OUTPUT_PERIOD_NS
{
output.clear_payload();
output.tov = Tov::None;
return Ok(());
}
self.driver.trigger_measurement()?;
self.measure_state = MeasureState::Measuring { started_ns: now_ns };
output.clear_payload();
output.tov = Tov::None;
return Ok(());
}
MeasureState::Measuring { started_ns } => {
let elapsed_ns = now_ns.saturating_sub(started_ns);
if elapsed_ns < MEASURE_MIN_DELAY_NS {
output.clear_payload();
output.tov = Tov::None;
return Ok(());
}
if elapsed_ns > MEASURE_TIMEOUT_NS {
self.measure_state = MeasureState::Idle;
output.clear_payload();
output.tov = Tov::None;
return Ok(());
}
match self.driver.read_measure_if_ready()? {
Some(payload) => {
self.measure_state = MeasureState::Idle;
payload
}
None => {
output.clear_payload();
output.tov = Tov::None;
return Ok(());
}
}
}
};
self.last_output_ns = Some(now_ns);
if self
.last_log_ns
.is_none_or(|last_log_ns| now_ns.saturating_sub(last_log_ns) >= DRIVER_LOG_PERIOD_NS)
{
self.last_log_ns = Some(now_ns);
info!(
ctx,
"ist8310: mag_x_ut={} mag_y_ut={} mag_z_ut={} rate_hz={}",
payload.mag_x.get::<microtesla>(),
payload.mag_y.get::<microtesla>(),
payload.mag_z.get::<microtesla>(),
OUTPUT_RATE_HZ
);
}
output.tov = Tov::Time(tov);
output.set_payload(payload);
Ok(())
}
}