Skip to main content

pamoja_sensors/ina219/
driver.rs

1//! The INA219 driven over I2C: configure, calibrate, trigger, and read all four results.
2
3use embedded_hal::delay::DelayNs;
4use embedded_hal::i2c::I2c;
5use pamoja_core::Sensor;
6
7use super::{
8    calibration, conversion_ready, minimum_current_lsb_microamps, register, Configuration, Mode,
9    Reading,
10};
11use crate::driver::{read_word, write_word};
12use crate::error::{DriverError, SensorError};
13
14/// How many times the Bus Voltage register is polled, 1 ms apart, after the
15/// conversion time has elapsed before the conversion is called overdue.
16pub const STATUS_POLLS: u8 = 20;
17
18/// An INA219 on an I2C bus, measuring shunt and bus voltage, current, and power on
19/// demand.
20///
21/// [`init`](Ina219::init) resets the part, writes the configuration, programs the
22/// calibration register for the shunt and the current resolution, and reads the
23/// calibration back, which is the identity check a part without an id register
24/// offers. [`measure`](Ina219::measure) writes the triggered shunt-and-bus mode,
25/// which starts one conversion of each, waits the conversion time for the ADC
26/// settings, polls the conversion-ready flag in the Bus Voltage register, then reads
27/// the shunt, current, and power registers, power last, because reading it clears
28/// the flag. As a [`Sensor`] its reading is the [`Reading`]; one quantity is
29/// selected with [`Sensor::map`].
30///
31/// # Examples
32///
33/// A solar charger's output, read through an INA219 that is not plugged in yet: 12 V on the
34/// bus and 1 A through a 100 mΩ shunt, with the part calibrated for up to 3.2 A.
35///
36/// ```
37/// use pamoja_core::Sensor;
38/// use pamoja_hal::script::{block_on, DelayLog};
39/// use pamoja_sensors::ina219::{sim, Ina219, BASE_ADDRESS};
40///
41/// let part = sim::reporting(BASE_ADDRESS, 100, 3_200_000, 12_000, 1_000_000);
42/// let mut charger = Ina219::new(part, BASE_ADDRESS, DelayLog::new()).with_shunt(100, 3_200_000);
43/// let reading = block_on(charger.read())?;
44/// assert_eq!(reading.bus_millivolts(), 12_000);
45/// assert!((reading.current_microamps() - 1_000_000).abs() < 100);
46/// # Ok::<(), pamoja_core::Error>(())
47/// ```
48#[derive(Debug)]
49pub struct Ina219<I2C, D> {
50    bus: I2C,
51    delay: D,
52    address: u8,
53    configuration: Configuration,
54    shunt_milliohms: u32,
55    current_lsb_microamps: u32,
56    initialized: bool,
57}
58
59impl<I2C, D> Ina219<I2C, D> {
60    /// Wraps an I2C bus and the part's address.
61    ///
62    /// # Arguments
63    ///
64    /// * `bus` - the I2C bus the part is on.
65    /// * `address` - [`BASE_ADDRESS`](super::BASE_ADDRESS) plus what the A1 and A0
66    ///   pins add.
67    /// * `delay` - the timer that paces the conversions.
68    ///
69    /// # Returns
70    ///
71    /// The driver, with the reset configuration and a 100 mΩ shunt sized for 3.2 A,
72    /// the common breakout.
73    pub fn new(bus: I2C, address: u8, delay: D) -> Self {
74        Ina219 {
75            bus,
76            delay,
77            address,
78            configuration: Configuration::default(),
79            shunt_milliohms: 100,
80            current_lsb_microamps: minimum_current_lsb_microamps(3_200_000),
81            initialized: false,
82        }
83    }
84
85    /// Sets the shunt and the largest current expected through it.
86    ///
87    /// The current resolution is the smallest the datasheet allows for that maximum,
88    /// which gives the Current register its finest scale; the calibration register
89    /// follows from the two by the datasheet's equation.
90    ///
91    /// # Arguments
92    ///
93    /// * `milliohms` - the shunt resistance.
94    /// * `max_microamps` - the largest current the shunt will carry.
95    ///
96    /// # Returns
97    ///
98    /// The driver.
99    pub fn with_shunt(mut self, milliohms: u32, max_microamps: u32) -> Self {
100        self.shunt_milliohms = milliohms;
101        self.current_lsb_microamps = minimum_current_lsb_microamps(max_microamps);
102        self.initialized = false;
103        self
104    }
105
106    /// Sets the current resolution outright, for a round figure such as 100 µA.
107    ///
108    /// # Arguments
109    ///
110    /// * `microamps` - microamps per count of the Current register.
111    ///
112    /// # Returns
113    ///
114    /// The driver.
115    pub fn with_current_lsb(mut self, microamps: u32) -> Self {
116        self.current_lsb_microamps = microamps;
117        self.initialized = false;
118        self
119    }
120
121    /// Sets the range, gain, and ADC settings; the mode is chosen per conversion.
122    ///
123    /// # Arguments
124    ///
125    /// * `configuration` - the settings to write.
126    ///
127    /// # Returns
128    ///
129    /// The driver.
130    pub fn with_configuration(mut self, configuration: Configuration) -> Self {
131        self.configuration = configuration;
132        self.initialized = false;
133        self
134    }
135
136    /// Returns the current resolution in microamps per count.
137    pub fn current_lsb_microamps(&self) -> u32 {
138        self.current_lsb_microamps
139    }
140
141    /// Returns the calibration word the driver programs.
142    pub fn calibration(&self) -> u16 {
143        calibration(self.current_lsb_microamps, self.shunt_milliohms)
144    }
145
146    /// Gives back the bus and the delay.
147    ///
148    /// # Returns
149    ///
150    /// The bus and delay the driver was built from.
151    pub fn release(self) -> (I2C, D) {
152        (self.bus, self.delay)
153    }
154
155    fn triggered(&self) -> Configuration {
156        Configuration {
157            reset: false,
158            mode: Mode::ShuntAndBusTriggered,
159            ..self.configuration
160        }
161    }
162}
163
164impl<I2C: I2c, D: DelayNs> Ina219<I2C, D> {
165    /// Resets the part, writes the configuration and calibration, and reads the
166    /// calibration back.
167    ///
168    /// # Errors
169    ///
170    /// Returns [`DriverError::Bus`] if the bus fails, and [`DriverError::Sensor`]
171    /// with [`SensorError::Identity`] if the calibration register does not hold what
172    /// was written.
173    pub fn init(&mut self) -> Result<(), DriverError<I2C::Error>> {
174        let reset = Configuration {
175            reset: true,
176            ..Configuration::default()
177        };
178        self.write(register::CONFIGURATION, reset.bits())?;
179        let settings = Configuration {
180            reset: false,
181            ..self.configuration
182        };
183        self.write(register::CONFIGURATION, settings.bits())?;
184        let calibration = self.calibration();
185        self.write(register::CALIBRATION, calibration)?;
186        if self.read(register::CALIBRATION)? != calibration {
187            return Err(SensorError::Identity.into());
188        }
189        self.initialized = true;
190        Ok(())
191    }
192
193    /// Triggers one shunt and bus conversion and returns every result.
194    ///
195    /// Initializes the part first if [`init`](Ina219::init) has not run.
196    ///
197    /// # Returns
198    ///
199    /// The four registers with the current resolution they scale by.
200    ///
201    /// # Errors
202    ///
203    /// Returns [`DriverError::Bus`] if the bus fails, [`DriverError::Timeout`] if
204    /// the conversion-ready flag never sets, or any error of [`init`](Ina219::init).
205    pub fn measure(&mut self) -> Result<Reading, DriverError<I2C::Error>> {
206        if !self.initialized {
207            self.init()?;
208        }
209        let triggered = self.triggered();
210        self.write(register::CONFIGURATION, triggered.bits())?;
211        self.delay.delay_us(triggered.conversion_micros());
212
213        let mut bus = None;
214        for _ in 0..STATUS_POLLS {
215            let word = self.read(register::BUS_VOLTAGE)?;
216            if conversion_ready(word) {
217                bus = Some(word);
218                break;
219            }
220            self.delay.delay_ms(1);
221        }
222        let bus = bus.ok_or(DriverError::Timeout)?;
223
224        let shunt = self.read(register::SHUNT_VOLTAGE)? as i16;
225        let current = self.read(register::CURRENT)? as i16;
226        let power = self.read(register::POWER)?;
227        Ok(Reading {
228            shunt,
229            bus,
230            current,
231            power,
232            current_lsb_microamps: self.current_lsb_microamps,
233        })
234    }
235
236    fn read(&mut self, register: u8) -> Result<u16, DriverError<I2C::Error>> {
237        read_word(&mut self.bus, self.address, register).map_err(DriverError::Bus)
238    }
239
240    fn write(&mut self, register: u8, value: u16) -> Result<(), DriverError<I2C::Error>> {
241        write_word(&mut self.bus, self.address, register, value).map_err(DriverError::Bus)
242    }
243}
244
245impl<I2C, D> Sensor for Ina219<I2C, D>
246where
247    I2C: I2c + Send,
248    I2C::Error: core::fmt::Debug,
249    D: DelayNs + Send,
250{
251    type Reading = Reading;
252
253    async fn read(&mut self) -> pamoja_core::Result<Reading> {
254        self.measure().map_err(pamoja_core::Error::from)
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::ina219::{
262        bus_register, current_register, power_register, shunt_register, Adc, BASE_ADDRESS,
263        CONFIG_RESET,
264    };
265    use alloc::vec;
266    use alloc::vec::Vec;
267    use embedded_hal::i2c::{ErrorKind, NoAcknowledgeSource};
268    use pamoja_hal::script::{block_on, DelayLog, I2cScript, I2cStep};
269
270    const LSB: u32 = 98;
271
272    fn word(address: u8, register: u8, value: u16) -> I2cStep {
273        let [high, low] = value.to_be_bytes();
274        I2cStep::write(address, [register, high, low])
275    }
276
277    fn reply(address: u8, register: u8, value: u16) -> I2cStep {
278        I2cStep::write_read(address, [register], value.to_be_bytes())
279    }
280
281    fn init_steps(address: u8, settings: u16, calibration: u16) -> Vec<I2cStep> {
282        vec![
283            word(address, register::CONFIGURATION, CONFIG_RESET | 0x8000),
284            word(address, register::CONFIGURATION, settings),
285            word(address, register::CALIBRATION, calibration),
286            reply(address, register::CALIBRATION, calibration),
287        ]
288    }
289
290    fn measure_steps(address: u8, triggered: u16) -> Vec<I2cStep> {
291        vec![
292            word(address, register::CONFIGURATION, triggered),
293            reply(address, register::BUS_VOLTAGE, bus_register(12_000) | 0x02),
294            reply(
295                address,
296                register::SHUNT_VOLTAGE,
297                shunt_register(100_000) as u16,
298            ),
299            reply(
300                address,
301                register::CURRENT,
302                current_register(1_000_000, LSB) as u16,
303            ),
304            reply(address, register::POWER, power_register(12_000_000, LSB)),
305        ]
306    }
307
308    #[test]
309    fn init_resets_configures_calibrates_and_reads_the_calibration_back() {
310        let calibration = calibration(LSB, 100);
311        assert_eq!(calibration, 4179, "the datasheet design example");
312        let mut steps = init_steps(BASE_ADDRESS, CONFIG_RESET, calibration);
313        steps.extend(measure_steps(BASE_ADDRESS, CONFIG_RESET & !0x0004));
314        let mut monitor = Ina219::new(I2cScript::new(steps), BASE_ADDRESS, DelayLog::new())
315            .with_shunt(100, 3_200_000);
316        assert_eq!(monitor.current_lsb_microamps(), LSB);
317        monitor.init().unwrap();
318        let reading = monitor.measure().unwrap();
319        assert_eq!(reading.bus_millivolts(), 12_000);
320        assert_eq!(reading.shunt_microvolts(), 100_000);
321        assert_eq!(
322            reading.current_microamps(),
323            current_register(1_000_000, LSB) as i32 * LSB as i32
324        );
325        assert!(!reading.math_overflow());
326        let (bus, delay) = monitor.release();
327        assert!(bus.done());
328        assert_eq!(
329            delay.waits_ns(),
330            [1_064_000],
331            "12-bit shunt and bus conversions"
332        );
333    }
334
335    #[test]
336    fn the_adc_settings_set_the_wait_and_reach_the_register() {
337        let configuration = Configuration {
338            bus_adc: Adc::Samples16,
339            shunt_adc: Adc::Samples16,
340            ..Configuration::default()
341        };
342        let settings = configuration.bits();
343        let triggered = (settings & !0x0007) | 0x0003;
344        let mut steps = init_steps(0x41, settings, calibration(100, 100));
345        steps.extend(measure_steps(0x41, triggered));
346        let mut monitor = Ina219::new(I2cScript::new(steps), 0x41, DelayLog::new())
347            .with_shunt(100, 3_200_000)
348            .with_current_lsb(100)
349            .with_configuration(configuration);
350        monitor.measure().unwrap();
351        let (bus, delay) = monitor.release();
352        assert!(bus.done());
353        assert_eq!(delay.waits_ns(), [17_020_000]);
354    }
355
356    #[test]
357    fn a_calibration_that_does_not_read_back_is_an_identity_failure() {
358        let calibration = calibration(LSB, 100);
359        let steps = [
360            word(BASE_ADDRESS, register::CONFIGURATION, CONFIG_RESET | 0x8000),
361            word(BASE_ADDRESS, register::CONFIGURATION, CONFIG_RESET),
362            word(BASE_ADDRESS, register::CALIBRATION, calibration),
363            reply(BASE_ADDRESS, register::CALIBRATION, 0x0000),
364        ];
365        let mut monitor = Ina219::new(I2cScript::new(steps), BASE_ADDRESS, DelayLog::new());
366        assert_eq!(
367            monitor.init(),
368            Err(DriverError::Sensor(SensorError::Identity))
369        );
370    }
371
372    #[test]
373    fn a_conversion_that_never_finishes_is_a_timeout() {
374        let calibration = calibration(LSB, 100);
375        let mut steps = init_steps(BASE_ADDRESS, CONFIG_RESET, calibration);
376        steps.push(word(
377            BASE_ADDRESS,
378            register::CONFIGURATION,
379            CONFIG_RESET & !0x0004,
380        ));
381        for _ in 0..STATUS_POLLS {
382            steps.push(reply(
383                BASE_ADDRESS,
384                register::BUS_VOLTAGE,
385                bus_register(12_000) & !0x0002,
386            ));
387        }
388        let mut monitor = Ina219::new(I2cScript::new(steps), BASE_ADDRESS, DelayLog::new());
389        assert_eq!(monitor.measure(), Err(DriverError::Timeout));
390        assert!(monitor.release().0.done());
391    }
392
393    #[test]
394    fn a_bus_fault_surfaces_as_a_bus_error() {
395        let kind = ErrorKind::NoAcknowledge(NoAcknowledgeSource::Address);
396        let mut monitor = Ina219::new(
397            I2cScript::new([I2cStep::fault(BASE_ADDRESS, kind)]),
398            BASE_ADDRESS,
399            DelayLog::new(),
400        );
401        assert!(matches!(monitor.init(), Err(DriverError::Bus(_))));
402    }
403
404    #[test]
405    fn as_a_sensor_it_reads_and_maps_to_one_quantity() {
406        let mut steps = init_steps(BASE_ADDRESS, CONFIG_RESET, calibration(LSB, 100));
407        steps.extend(measure_steps(BASE_ADDRESS, CONFIG_RESET & !0x0004));
408        let monitor = Ina219::new(I2cScript::new(steps), BASE_ADDRESS, DelayLog::new());
409        let mut watts = monitor.map(|reading| reading.power_microwatts() as f32 / 1e6);
410        let reading = block_on(watts.read()).unwrap();
411        assert!((reading - 12.0).abs() < 0.01);
412    }
413
414    #[test]
415    fn calibration_and_one_reading_make_the_transfers_the_datasheet_gives() {
416        use crate::ina219::{bus_register, current_register, power_register, shunt_register};
417
418        const PART: u8 = crate::ina219::BASE_ADDRESS;
419        const LSB: u32 = 98;
420        let bus = I2cScript::new([
421            I2cStep::write(PART, [0x00, 0xB9, 0x9F]),
422            I2cStep::write(PART, [0x00, 0x39, 0x9F]),
423            I2cStep::write(PART, [0x05, 0x10, 0x53]),
424            I2cStep::write_read(PART, [0x05], [0x10, 0x53]),
425            I2cStep::write(PART, [0x00, 0x39, 0x9B]),
426            I2cStep::write_read(PART, [0x02], (bus_register(12_000) | 0x02).to_be_bytes()),
427            I2cStep::write_read(PART, [0x01], shunt_register(100_000).to_be_bytes()),
428            I2cStep::write_read(PART, [0x04], current_register(1_000_000, LSB).to_be_bytes()),
429            I2cStep::write_read(PART, [0x03], power_register(12_000_000, LSB).to_be_bytes()),
430        ]);
431
432        let mut monitor = Ina219::new(bus, PART, DelayLog::new()).with_shunt(100, 3_200_000);
433        let reading = block_on(Sensor::read(&mut monitor)).expect("the scripted part answers");
434        assert_eq!(reading.bus_millivolts(), 12_000);
435        assert_eq!(reading.shunt_microvolts(), 100_000);
436        assert!((reading.current_microamps() - 1_000_000).abs() < LSB as i32);
437    }
438}