ina229/
lib.rs

1//! This is a platform agnostic Rust driver for the [`INA229`], an SPI output
2//! current/voltage/power monitor with alerts, using the [`embedded-hal`] traits.
3//!
4//! [`INA229`]: https://www.ti.com/product/INA229
5//! [`embedded-hal`]: https://github.com/rust-embedded/embedded-hal
6//!
7//! This driver allows you to:
8//! - Callibrate the device. See [`callibrate()`].
9//! - Read the shunt voltage. See [`shunt_voltage_nanovolts()`].
10//! - Read the bus voltage. See [`bus_voltage_microvolts()`].
11//! - Read the current. See [`current_amps()`].
12//! - Read the power. See [`power_watts()`].
13//!
14//! [`callibrate()`]: struct.INA229.html#method.callibrate
15//! [`shunt_voltage_nanovolts()`]: struct.INA229.html#method.shunt_voltage_nanovolts
16//! [`bus_voltage_microvolts()`]: struct.INA229.html#method.bus_voltage_microvolts
17//! [`current_amps()`]: struct.INA229.html#method.current_amps
18//! [`power_watts()`]: struct.INA229.html#method.power_watts
19//!
20//! ## The device
21//!
22//! The INA229-Q1 is an ultra-precise digital power monitor with a 20-bit delta-sigma ADC
23//! specifically designed for current-sensing applications. The device can measure a full-scale
24//! differential input of ±163.84 mV or ±40.96 mV across a resistive shunt sense element with
25//! common-mode voltage support from –0.3 V to +85 V.
26//!
27//! Datasheet:
28//! - [INA229](https://www.ti.com/lit/gpn/ina229)
29
30#![warn(unsafe_code, missing_docs)]
31#![no_std]
32
33use core::result::Result;
34
35use bitflags::bitflags;
36use byteorder::{BigEndian, ByteOrder};
37use embedded_hal::{
38    blocking::spi::{Transfer, Write},
39    digital::v2::OutputPin,
40    spi::{Mode, MODE_1},
41};
42
43bitflags! {
44    /// Configuration register contents.
45    #[repr(C)]
46    pub struct Configuration: u16 {
47        /// Reset Bit. Setting this bit to '1' generates a system reset that is the same as power-on reset.
48        /// Resets all registers to default values.
49        /// 0h = Normal Operation
50        /// 1h = System Reset sets registers to default values
51        /// This bit self-clears.
52        /// Default: 0.
53        const RST       = 0b1000_0000_0000_0000;
54
55        /// Resets the contents of accumulation registers ENERGY and CHARGE to 0.
56        /// 0h = Normal Operation
57        /// 1h = Clears registers to default values for ENERGY and CHARGE registers
58        /// Default: 0.
59        const RSTACC    = 0b0100_0000_0000_0000;
60
61        /// Sets the Delay for initial ADC conversion in steps of 2 ms.
62        /// 0h = 0 s
63        /// 1h = 2 ms
64        /// FFh = 510 ms
65        /// Default: 0.
66        const CONVDLY   = 0b0011_1111_1100_0000;
67
68        /// Enables temperature compensation of an external shunt
69        /// 0h = Shunt Temperature Compensation Disabled
70        /// 1h = Shunt Temperature Compensation Enabled
71        /// Default: 0.
72        const TEMPCOMP  = 0b0000_0000_0010_0000;
73
74        /// Shunt full scale range selection across IN+ and IN–.
75        /// 0h = ±163.84 mV
76        /// 1h = ± 40.96 mV
77        /// Default: 0.
78        const ADCRANGE  = 0b0000_0000_0001_0000;
79
80        /// Reserved. Always reads 0.
81        const RESERVED  = 0b0000_0000_0000_1111;
82    }
83}
84
85/// The SPI mode for the INA229.
86pub const MODE: Mode = MODE_1;
87
88#[repr(u8)]
89enum Register {
90    Configuration = 0x00,
91    ShuntCalibration = 0x02,
92    ShuntVoltage = 0x04,
93    BusVoltage = 0x05,
94    DieTemperature = 0x06,
95    Current = 0x07,
96    Power = 0x08,
97    ManufacturerID = 0x3E,
98    DeviceID = 0x3F,
99}
100
101enum Command {
102    Read,
103    Write,
104}
105
106/// Error type for INA229 commands.
107#[derive(Debug)]
108pub enum Error<SPIError, CSError> {
109    /// The INA229 is not configured.
110    NotConfigured,
111
112    /// An error occured during an SPI transaction.
113    SPIError(SPIError),
114
115    /// An error occured toggling the chip select.
116    ChipSelectError(CSError),
117}
118
119// Conversion constants
120const BUS_VOLTAGE_UV_PER_LSB: f64 = 195.3125;
121const SHUNT_VOLTAGE_NV_PER_LSB_MODE_0: f64 = 312.5;
122const SHUNT_VOLTAGE_NV_PER_LSB_MODE_1: f64 = 78.125;
123const TEMPERATURE_MC_PER_LSB: f64 = 7.8125;
124const POWER_SCALING_FACTOR: f64 = 3.2;
125
126// Calibration constants
127const DENOMINATOR: f64 = (1 << 19) as f64; // From Datasheet, 2^19
128const INTERNAL_SCALING: f64 = 13107200000.0; // From Datasheet, 13107.2 * 10^6
129
130#[inline(always)]
131fn calculate_calibration_value(
132    configuration: Configuration,
133    shunt_resistance: f64,
134    current_expected_max: f64,
135) -> (f64, u16) {
136    let scale = if configuration.contains(Configuration::ADCRANGE) {
137        4.0
138    } else {
139        1.0
140    };
141    let current_lsb = calculate_current_lsb(current_expected_max);
142    let shunt_cal = INTERNAL_SCALING * current_lsb * shunt_resistance * scale;
143    (current_lsb, shunt_cal as u16)
144}
145
146#[inline(always)]
147fn calculate_current_lsb(current_expected_max: f64) -> f64 {
148    current_expected_max / DENOMINATOR
149}
150
151/// INA229 voltage/current/power monitor
152pub struct INA229<SPI, NCS> {
153    spi: SPI,
154    ncs: NCS,
155    config: Option<Configuration>,
156    current_lsb: Option<f64>,
157}
158
159impl<SPI, NCS, SPIError, CSError> INA229<SPI, NCS>
160where
161    SPI: Transfer<u8, Error = SPIError> + Write<u8, Error = SPIError>,
162    NCS: OutputPin<Error = CSError>,
163{
164    /// Create a new instance of an INA229 device.
165    pub fn new(spi: SPI, ncs: NCS) -> Self {
166        INA229 {
167            spi,
168            ncs,
169            config: None,
170            current_lsb: None,
171        }
172    }
173
174    /// Destroy the INA229 instance and return the SPI.
175    pub fn release(self) -> (SPI, NCS) {
176        (self.spi, self.ncs)
177    }
178
179    fn read_register_u16(&mut self, register: Register) -> Result<u16, Error<SPIError, CSError>> {
180        let mut buffer = [get_frame(register, Command::Read), 0x00, 0x00];
181        self.ncs.set_low().map_err(Error::ChipSelectError)?;
182        self.spi.transfer(&mut buffer).map_err(Error::SPIError)?;
183        self.ncs.set_high().map_err(Error::ChipSelectError)?;
184        let value = BigEndian::read_u16(&buffer[1..3]);
185        Ok(value)
186    }
187
188    fn write_register_u16(
189        &mut self,
190        register: Register,
191        value: u16,
192    ) -> Result<(), Error<SPIError, CSError>> {
193        let mut buffer = [get_frame(register, Command::Write), 0x00, 0x00];
194        BigEndian::write_u16_into(&[value], &mut buffer[1..3]);
195        self.ncs.set_low().map_err(Error::ChipSelectError)?;
196        self.spi.write(&buffer).map_err(Error::SPIError)?;
197        self.ncs.set_high().map_err(Error::ChipSelectError)?;
198        Ok(())
199    }
200
201    fn read_register_i16(&mut self, register: Register) -> Result<i16, Error<SPIError, CSError>> {
202        let mut buffer = [get_frame(register, Command::Read), 0x00, 0x00];
203        self.ncs.set_low().map_err(Error::ChipSelectError)?;
204        self.spi.transfer(&mut buffer).map_err(Error::SPIError)?;
205        self.ncs.set_high().map_err(Error::ChipSelectError)?;
206        let value = BigEndian::read_i16(&buffer[1..3]);
207        Ok(value)
208    }
209
210    fn read_register_u24(&mut self, register: Register) -> Result<u32, Error<SPIError, CSError>> {
211        let mut buffer = [get_frame(register, Command::Read), 0x00, 0x00, 0x00];
212        self.ncs.set_low().map_err(Error::ChipSelectError)?;
213        self.spi.transfer(&mut buffer).map_err(Error::SPIError)?;
214        self.ncs.set_high().map_err(Error::ChipSelectError)?;
215        let value = BigEndian::read_u24(&buffer[1..4]);
216        Ok(value)
217    }
218
219    fn read_register_i24(&mut self, register: Register) -> Result<i32, Error<SPIError, CSError>> {
220        let mut buffer = [get_frame(register, Command::Read), 0x00, 0x00, 0x00];
221        self.ncs.set_low().map_err(Error::ChipSelectError)?;
222        self.spi.transfer(&mut buffer).map_err(Error::SPIError)?;
223        self.ncs.set_high().map_err(Error::ChipSelectError)?;
224        let value = BigEndian::read_i24(&buffer[1..4]);
225        Ok(value)
226    }
227
228    /// Sets the CONFIG register with the value provided.
229    pub fn set_configuration(
230        &mut self,
231        configuration: Configuration,
232    ) -> Result<(), Error<SPIError, CSError>> {
233        self.write_register_u16(Register::Configuration, configuration.bits())?;
234        self.config = Some(configuration);
235        Ok(())
236    }
237
238    /// Get the configuration.
239    pub fn configuration(&mut self) -> Result<Configuration, Error<SPIError, CSError>> {
240        self.read_register_u16(Register::Configuration)
241            .map(Configuration::from_bits_truncate)
242            .map(|config| {
243                self.config = Some(config);
244                config
245            })
246    }
247
248    /// Gets the value from the shunt calibration register.
249    pub fn shunt_calibration(&mut self) -> Result<u16, Error<SPIError, CSError>> {
250        self.read_register_u16(Register::ShuntCalibration)
251    }
252
253    /// Sets the shunt calibration register to the value provided.
254    pub fn set_shunt_calibration(&mut self, value: u16) -> Result<(), Error<SPIError, CSError>> {
255        self.write_register_u16(Register::ShuntCalibration, value)
256    }
257
258    /// Calculate the shunt calibration value and write to the shunt calibration register.
259    pub fn calibrate(
260        &mut self,
261        shunt_resistance: f64,
262        current_expected_max: f64,
263    ) -> Result<(), Error<SPIError, CSError>> {
264        if let Some(config) = self.config {
265            let (current_lsb, value) =
266                calculate_calibration_value(config, shunt_resistance, current_expected_max);
267            self.set_shunt_calibration(value)?;
268            self.current_lsb = Some(current_lsb);
269            Ok(())
270        } else {
271            Err(Error::NotConfigured)
272        }
273    }
274
275    /// Calculate the shunt calibration value and write to the shunt calibration register.
276    pub fn configure_and_calibrate(
277        &mut self,
278        configuration: Configuration,
279        shunt_resistance: f64,
280        current_expected_max: f64,
281    ) -> Result<(), Error<SPIError, CSError>> {
282        self.set_configuration(configuration)
283            .and_then(|_| self.calibrate(shunt_resistance, current_expected_max))
284    }
285
286    /// Get the raw bus voltage reading.
287    pub fn bus_voltage_raw(&mut self) -> Result<i32, Error<SPIError, CSError>> {
288        self.read_register_i24(Register::BusVoltage).map(|x| x >> 4) // 20bit value.
289    }
290
291    /// Get the bus voltage reading in microvolts.
292    pub fn bus_voltage_microvolts(&mut self) -> Result<f64, Error<SPIError, CSError>> {
293        self.bus_voltage_raw()
294            .map(|x| (x as f64) * BUS_VOLTAGE_UV_PER_LSB)
295    }
296
297    /// Get the raw shunt voltage reading.
298    pub fn shunt_voltage_raw(&mut self) -> Result<i32, Error<SPIError, CSError>> {
299        self.read_register_i24(Register::ShuntVoltage)
300            .map(|x| x >> 4)
301    }
302
303    /// Get the shunt voltage reading in nanovolts.
304    pub fn shunt_voltage_nanovolts(&mut self) -> Result<f64, Error<SPIError, CSError>> {
305        if let Some(config) = self.config {
306            self.shunt_voltage_raw().map(|value| {
307                if config.contains(Configuration::ADCRANGE) {
308                    (value as f64) * SHUNT_VOLTAGE_NV_PER_LSB_MODE_1
309                } else {
310                    (value as f64) * SHUNT_VOLTAGE_NV_PER_LSB_MODE_0
311                }
312            })
313        } else {
314            Err(Error::NotConfigured)
315        }
316    }
317
318    /// Get the raw die temperature value.
319    pub fn temperature_raw(&mut self) -> Result<i16, Error<SPIError, CSError>> {
320        self.read_register_i16(Register::DieTemperature)
321    }
322
323    /// Get the die temperature in millidegrees Celsius.
324    pub fn temperature_millidegrees_celsius(&mut self) -> Result<f64, Error<SPIError, CSError>> {
325        self.temperature_raw()
326            .map(|x| (x as f64) * TEMPERATURE_MC_PER_LSB)
327    }
328
329    /// Get the raw value from the current register.
330    pub fn current_raw(&mut self) -> Result<i32, Error<SPIError, CSError>> {
331        self.read_register_i24(Register::Current).map(|x| x >> 4) // 20bit value.
332    }
333
334    /// Get the current reading in Amps.
335    pub fn current_amps(&mut self) -> Result<f64, Error<SPIError, CSError>> {
336        if let Some(current_lsb) = self.current_lsb {
337            self.current_raw().map(|x| (x as f64) * current_lsb)
338        } else {
339            Err(Error::NotConfigured)
340        }
341    }
342
343    /// Get the raw value from the power register
344    pub fn power_raw(&mut self) -> Result<u32, Error<SPIError, CSError>> {
345        self.read_register_u24(Register::Power)
346    }
347
348    /// Get the power reading in Watts.
349    pub fn power_watts(&mut self) -> Result<f64, Error<SPIError, CSError>> {
350        if let Some(current_lsb) = self.current_lsb {
351            self.power_raw()
352                .map(|x| (x as f64) * current_lsb * POWER_SCALING_FACTOR)
353        } else {
354            Err(Error::NotConfigured)
355        }
356    }
357
358    /// Get the unique manufacturer identification number.
359    pub fn manufacturer_id(&mut self) -> Result<u16, Error<SPIError, CSError>> {
360        self.read_register_u16(Register::ManufacturerID)
361    }
362
363    /// Get the unique die identification number.
364    pub fn device_id(&mut self) -> Result<u16, Error<SPIError, CSError>> {
365        self.read_register_u16(Register::DeviceID)
366    }
367}
368
369fn get_frame(register: Register, command: Command) -> u8 {
370    let frame = (register as u8) << 2u8;
371    match command {
372        Command::Write => frame & !0b00000001,
373        Command::Read => frame | 0b00000001,
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::{
380        calculate_calibration_value, calculate_current_lsb, get_frame, Command, Configuration,
381        Register,
382    };
383    use approx::assert_relative_eq;
384
385    #[test]
386    fn get_frame_manufacturer_read() {
387        let result = get_frame(Register::ManufacturerID, Command::Read);
388        assert_eq!(result, 0b1111_1001);
389    }
390
391    #[test]
392    fn get_frame_device_read() {
393        let result = get_frame(Register::DeviceID, Command::Read);
394        assert_eq!(result, 0b1111_1101);
395    }
396
397    #[test]
398    fn calculate_current_lsb_works() {
399        let lsb = calculate_current_lsb(10.0); // 10 Amps
400        assert_relative_eq!(lsb, 0.0000190735, max_relative = 0.00001);
401    }
402
403    #[test]
404    fn calculate_calibration_value_works() {
405        let (_, value) =
406            calculate_calibration_value(Configuration::from_bits_truncate(0), 0.0162, 10.0);
407        assert_eq!(value, 4050);
408    }
409}