ds4432/
lib.rs

1//! DS4432 driver.
2//!
3//! The DS4432 contains two I2C programmable current
4//! DACs that are each capable of sinking and sourcing
5//! current up to 200µA. Each DAC output has 127 sink
6//! and 127 source settings that are programmed using the
7//! I2C interface. The current DAC outputs power up in a
8//! high-impedance state.
9//!
10//! - [DS4432 product page](https://www.digikey.com/en/products/detail/analog-devices-inc-maxim-integrated/DS4432U-T-R/2062898)
11//! - [DS4432 datasheet](https://www.analog.com/media/en/technical-documentation/data-sheets/DS4432.pdf)
12
13#![no_std]
14#![macro_use]
15pub(crate) mod fmt;
16
17mod error;
18pub use error::{Error, Result};
19
20#[cfg(not(any(feature = "sync", feature = "async")))]
21compile_error!("You should probably choose at least one of `sync` and `async` features.");
22
23#[cfg(feature = "sync")]
24use embedded_hal::i2c::ErrorType;
25#[cfg(feature = "sync")]
26use embedded_hal::i2c::I2c;
27#[cfg(feature = "async")]
28use embedded_hal_async::i2c::ErrorType as AsyncErrorType;
29#[cfg(feature = "async")]
30use embedded_hal_async::i2c::I2c as AsyncI2c;
31
32/// The DS4432's I2C addresses.
33#[cfg(any(feature = "async", feature = "sync"))]
34const SLAVE_ADDRESS: u8 = 0b1001000; // This is I2C address 0x48
35
36#[cfg(not(feature = "not-recommended-rfs"))]
37const RECOMMENDED_RFS_MIN: u32 = 40_000;
38#[cfg(not(feature = "not-recommended-rfs"))]
39const RECOMMENDED_RFS_MAX: u32 = 160_000;
40
41const IOUT_UA_MIN: f32 = 50.0;
42const IOUT_UA_MAX: f32 = 200.0;
43
44/// An output controllable by the DS4432. This device has two.
45#[derive(Debug, Clone, Copy)]
46#[cfg_attr(feature = "defmt", derive(defmt::Format))]
47#[repr(u8)]
48pub enum Output {
49    Zero = 0xF8,
50    One = 0xF9,
51}
52
53impl From<Output> for u8 {
54    fn from(value: Output) -> Self {
55        value as u8
56    }
57}
58
59/// The status of an output.
60#[derive(Debug, Clone, Copy, PartialEq)]
61#[cfg_attr(feature = "defmt", derive(defmt::Format))]
62pub enum Status {
63    /// The output is completely disabled
64    Disable,
65    /// The output sink at the given code
66    Sink(u8),
67    /// The output sink at the given current value
68    SinkMicroAmp(f32),
69    /// The output source at the given code
70    Source(u8),
71    /// The output source at the given current value
72    SourceMicroAmp(f32),
73}
74
75impl Status {
76    /// Return the raw DAC code for a given Status
77    /// MicroAmp variants return None because Rfs is unknown to make the conversion.
78    ///
79    /// # Example
80    /// ```
81    /// use ds4432::Status;
82    ///
83    /// assert_eq!(Status::Sink(42).code(), Some(0x2A));
84    /// assert_eq!(Status::Source(42).code(), Some(0x2A));
85    /// assert_eq!(Status::Disable.code(), Some(0x00));
86    /// assert_eq!(Status::Sink(0).code(), Some(0x00));
87    /// assert_eq!(Status::Source(0).code(), Some(0x00));
88    /// assert_eq!(Status::SinkMicroAmp(42.0).code(), None);
89    /// assert_eq!(Status::SourceMicroAmp(42.0).code(), None);
90    /// ```
91    pub fn code(&self) -> Option<u8> {
92        match self {
93            Self::Sink(c) | Self::Source(c) => Some(*c),
94            Self::Disable => Some(0),
95            _ => None,
96        }
97    }
98
99    /// Convert a raw DAC code into its Current value in microamps according to the Rfs value.
100    /// MicroAmp variants return None because conversion is pointless.
101    ///
102    /// # Example
103    /// ```
104    /// use ds4432::Status;
105    ///
106    /// // example from datasheet
107    /// assert_eq!(Status::Source(42).current_ua(80_000), Some(32.71406));
108    /// assert_eq!(Status::Sink(42).current_ua(80_000), Some(32.71406));
109    /// assert_eq!(Status::Disable.current_ua(1000), Some(0.0));
110    /// assert_eq!(Status::SourceMicroAmp(42.0).current_ua(80_000), None);
111    /// assert_eq!(Status::SinkMicroAmp(42.0).current_ua(80_000), None);
112    /// ```
113    pub fn current_ua(&self, rfs_ohm: u32) -> Option<f32> {
114        self.code()
115            .map(|code| ((62_312.5 * code as f64) / (rfs_ohm as f64)) as f32)
116    }
117}
118
119impl From<u8> for Status {
120    fn from(value: u8) -> Self {
121        let sourcing = value & 0x80 == 0x80;
122        let code = value & 0x7F;
123
124        match (sourcing, code) {
125            (true, 0) => Self::Disable,
126            (false, 0) => Self::Disable,
127            (true, c) => Self::Source(c),
128            (false, c) => Self::Sink(c),
129        }
130    }
131}
132
133/// A DS4432 Digital To Analog (DAC) converter on the I2C bus `I`.
134#[maybe_async_cfg2::maybe(
135    sync(feature = "sync", self = "DS4432"),
136    async(feature = "async", keep_self)
137)]
138pub struct AsyncDS4432<I> {
139    i2c: I,
140    rfs0_ohm: Option<u32>,
141    rfs1_ohm: Option<u32>,
142}
143
144#[maybe_async_cfg2::maybe(
145    sync(
146        feature = "sync",
147        self = "DS4432",
148        idents(AsyncI2c(sync = "I2c"), AsyncErrorType(sync = "ErrorType"))
149    ),
150    async(feature = "async", keep_self)
151)]
152impl<I: AsyncI2c + AsyncErrorType> AsyncDS4432<I> {
153    /// Create a new DS4432 using the given I2C implementation.
154    ///
155    /// Using this constructor doesn't allow the driver to know the Rfs values so only raw DAC code
156    /// are supported in the Status.
157    pub fn new(i2c: I) -> Self {
158        trace!("new");
159        Self::with_rfs(i2c, None, None).unwrap()
160    }
161
162    /// Create a new DS4432 using the given I2C implementation and the optinal Rfs values.
163    ///
164    /// If a Rfs value is given for an Output:
165    /// - reading status will automatically convert to microamps value instead of giving the raw DAC code.
166    /// - writing status will allow automatic convertion from microamps value to raw DAC code.
167    ///
168    /// Note: if you want to only deal with raw DAC code, use `new` instead and use Status::current_ua() to
169    /// do manual convertion into microamps.
170    pub fn with_rfs(
171        i2c: I,
172        rfs0_ohm: Option<u32>,
173        rfs1_ohm: Option<u32>,
174    ) -> Result<Self, I::Error> {
175        for rfs in [rfs0_ohm, rfs1_ohm].into_iter().flatten() {
176            #[cfg(feature = "not-recommended-rfs")]
177            if rfs == 0 {
178                return Err(Error::InvalidRfs);
179            }
180            #[cfg(not(feature = "not-recommended-rfs"))]
181            if !(RECOMMENDED_RFS_MIN..=RECOMMENDED_RFS_MAX).contains(&rfs) {
182                return Err(Error::InvalidRfs);
183            }
184        }
185        Ok(Self {
186            i2c,
187            rfs0_ohm,
188            rfs1_ohm,
189        })
190    }
191
192    /// Set the current sink/source status and code of an output
193    pub async fn set_status(&mut self, output: Output, status: Status) -> Result<(), I::Error> {
194        trace!("set_status");
195
196        let reg = output.into();
197        let value = match status {
198            Status::Disable | Status::Sink(0) | Status::Source(0) => 0,
199            Status::Sink(code) => {
200                if code > 127 {
201                    return Err(Error::InvalidCode(code));
202                } else {
203                    code
204                }
205            }
206            Status::Source(code) => {
207                if code > 127 {
208                    return Err(Error::InvalidCode(code));
209                } else {
210                    // ensures MSB is 1
211                    code | 0x80
212                }
213            }
214            Status::SinkMicroAmp(current) => {
215                if !(IOUT_UA_MIN..=IOUT_UA_MAX).contains(&current) {
216                    return Err(Error::InvalidIout);
217                }
218                let rfs = match output {
219                    Output::Zero => self.rfs0_ohm.ok_or(Error::UnknownRfs)?,
220                    Output::One => self.rfs1_ohm.ok_or(Error::UnknownRfs)?,
221                };
222                ((current * (rfs as f32)) / 62_312.5) as u8
223            }
224            Status::SourceMicroAmp(current) => {
225                if !(IOUT_UA_MIN..=IOUT_UA_MAX).contains(&current) {
226                    return Err(Error::InvalidIout);
227                }
228                let rfs = match output {
229                    Output::Zero => self.rfs0_ohm.ok_or(Error::UnknownRfs)?,
230                    Output::One => self.rfs1_ohm.ok_or(Error::UnknownRfs)?,
231                };
232                // ensures MSB is 1
233                ((current * (rfs as f32)) / 62_312.5) as u8 | 0x80
234            }
235        };
236
237        debug!("W @0x{:x}={:x}", reg, value);
238
239        self.i2c
240            .write(SLAVE_ADDRESS, &[reg, value])
241            .await
242            .map_err(Error::I2c)
243    }
244
245    /// Get the current sink/source status and code of an output
246    pub async fn status(&mut self, output: Output) -> Result<Status, I::Error> {
247        trace!("status");
248
249        let mut buf = [0x00];
250        let reg = output.into();
251
252        self.i2c
253            .write_read(SLAVE_ADDRESS, &[reg], &mut buf)
254            .await
255            .map_err(Error::I2c)?;
256
257        debug!("R @0x{:x}={:x}", reg, buf[0]);
258
259        let mut status = buf[0].into();
260        match output {
261            Output::Zero => {
262                if let Some(rfs) = self.rfs0_ohm {
263                    status = match status {
264                        Status::Sink(code) => {
265                            Status::SinkMicroAmp(Status::Sink(code).current_ua(rfs).unwrap())
266                        }
267                        Status::Source(code) => {
268                            Status::SourceMicroAmp(Status::Source(code).current_ua(rfs).unwrap())
269                        }
270                        _ => status,
271                    }
272                }
273            }
274            Output::One => {
275                if let Some(rfs) = self.rfs1_ohm {
276                    status = match status {
277                        Status::Sink(code) => {
278                            Status::SinkMicroAmp(Status::Sink(code).current_ua(rfs).unwrap())
279                        }
280                        Status::Source(code) => {
281                            Status::SourceMicroAmp(Status::Source(code).current_ua(rfs).unwrap())
282                        }
283                        _ => status,
284                    }
285                }
286            }
287        }
288        Ok(status)
289    }
290
291    /// Return the underlying I2C device
292    pub fn release(self) -> I {
293        self.i2c
294    }
295
296    /// Destroys this driver and releases the I2C bus `I`.
297    pub fn destroy(self) -> Self {
298        self
299    }
300}
301
302#[cfg(test)]
303mod test {
304    // extern crate alloc;
305    extern crate std;
306
307    use super::*;
308    use embedded_hal_mock::eh1::i2c;
309    use std::vec;
310
311    #[test]
312    fn u8_to_status_conversion() {
313        assert_eq!(Status::from(0x2A), Status::Sink(42));
314        assert_eq!(Status::from(0xAA), Status::Source(42));
315        assert_eq!(Status::from(0x00), Status::Disable);
316        assert_eq!(Status::from(0x80), Status::Disable);
317    }
318
319    #[test]
320    fn can_get_output_0_status() {
321        let expectations = [i2c::Transaction::write_read(
322            SLAVE_ADDRESS,
323            vec![Output::Zero as u8],
324            vec![0xAA],
325        )];
326        let mock = i2c::Mock::new(&expectations);
327        let mut ds4432 = DS4432::new(mock);
328
329        let status = ds4432.status(Output::Zero).unwrap();
330        assert!(matches!(status, Status::Source(42)));
331
332        let mut mock = ds4432.release();
333        mock.done();
334    }
335
336    #[test]
337    fn can_set_output_1_status() {
338        let expectations = [i2c::Transaction::write(
339            SLAVE_ADDRESS,
340            vec![Output::One as u8, 0x2A],
341        )];
342        let mock = i2c::Mock::new(&expectations);
343        let mut ds4432 = DS4432::new(mock);
344
345        // just making sure it doesn't error
346        ds4432.set_status(Output::One, Status::Sink(42)).unwrap();
347
348        let mut mock = ds4432.release();
349        mock.done();
350    }
351
352    #[test]
353    fn can_get_output_0_status_current() {
354        let expectations = [i2c::Transaction::write_read(
355            SLAVE_ADDRESS,
356            vec![Output::Zero as u8],
357            vec![0xAA],
358        )];
359        let mock = i2c::Mock::new(&expectations);
360        let mut ds4432 = DS4432::with_rfs(mock, Some(80_000), None).unwrap();
361
362        let status = ds4432.status(Output::Zero).unwrap();
363        assert!(matches!(status, Status::SourceMicroAmp(32.71406)));
364
365        let mut mock = ds4432.release();
366        mock.done();
367    }
368
369    #[test]
370    fn can_set_output_1_status_current() {
371        let expectations = [i2c::Transaction::write(
372            SLAVE_ADDRESS,
373            vec![Output::One as u8, 0x70],
374        )];
375        let mock = i2c::Mock::new(&expectations);
376        let mut ds4432 = DS4432::with_rfs(mock, None, Some(80_000)).unwrap();
377
378        // just making sure it doesn't error
379        ds4432
380            .set_status(Output::One, Status::SinkMicroAmp(88.0))
381            .unwrap();
382
383        let mut mock = ds4432.release();
384        mock.done();
385    }
386}