port_expander/dev/
pca9702.rs

1//! Support for the `PCA9702` "8-Bit Input-Only Expander with SPI"
2//!
3//! Datasheet: https://www.nxp.com/docs/en/data-sheet/PCA9701_PCA9702.pdf
4//!
5//! The PCA9702 offers eight input pins, with an interrupt output that can be asserted when
6//! one or more of the inputs change state (enabled via the `INT_EN` pin). The device reads
7//! its inputs on each falling edge of `CS` and then presents them on `SDOUT` bit-by-bit as
8//! the clock (SCLK) rises.
9//!
10//! Because the PCA9702 is strictly input-only, there is no way to drive output values or
11//! configure directions. Consequently, calling methods that attempt to write or read back
12//! “set” states (e.g., `set()`, `is_set()`) will return an error.
13
14use crate::{PortDriver, SpiBus};
15use embedded_hal::spi::Operation;
16
17/// An 8-bit input-only expander with SPI, based on the PCA9702.
18pub struct Pca9702<M>(M);
19
20impl<SPI> Pca9702<core::cell::RefCell<Driver<Pca9702Bus<SPI>>>>
21where
22    SPI: crate::SpiBus,
23{
24    pub fn new(bus: SPI) -> Self {
25        Self::with_mutex(Pca9702Bus(bus))
26    }
27}
28
29impl<B, M> Pca9702<M>
30where
31    B: Pca9702BusTrait,
32    M: crate::PortMutex<Port = Driver<B>>,
33{
34    /// Create a PCA9702 driver with a user-supplied mutex type.
35    pub fn with_mutex(bus: B) -> Self {
36        Self(crate::PortMutex::create(Driver::new(bus)))
37    }
38
39    /// Split this device into its 8 input pins.
40    ///
41    /// All pins are always configured as inputs on PCA9702 hardware.
42    pub fn split(&mut self) -> Parts<'_, B, M> {
43        Parts {
44            in0: crate::Pin::new(0, &self.0),
45            in1: crate::Pin::new(1, &self.0),
46            in2: crate::Pin::new(2, &self.0),
47            in3: crate::Pin::new(3, &self.0),
48            in4: crate::Pin::new(4, &self.0),
49            in5: crate::Pin::new(5, &self.0),
50            in6: crate::Pin::new(6, &self.0),
51            in7: crate::Pin::new(7, &self.0),
52        }
53    }
54}
55
56/// Container for all 8 input pins on the PCA9702.
57pub struct Parts<'a, B, M = core::cell::RefCell<Driver<B>>>
58where
59    B: Pca9702BusTrait,
60    M: crate::PortMutex<Port = Driver<B>>,
61{
62    pub in0: crate::Pin<'a, crate::mode::Input, M>,
63    pub in1: crate::Pin<'a, crate::mode::Input, M>,
64    pub in2: crate::Pin<'a, crate::mode::Input, M>,
65    pub in3: crate::Pin<'a, crate::mode::Input, M>,
66    pub in4: crate::Pin<'a, crate::mode::Input, M>,
67    pub in5: crate::Pin<'a, crate::mode::Input, M>,
68    pub in6: crate::Pin<'a, crate::mode::Input, M>,
69    pub in7: crate::Pin<'a, crate::mode::Input, M>,
70}
71
72/// Internal driver struct for PCA9702.
73pub struct Driver<B> {
74    bus: B,
75}
76
77impl<B> Driver<B> {
78    fn new(bus: B) -> Self {
79        Self { bus }
80    }
81}
82
83/// Trait for the underlying PCA9702 SPI bus. Simpler than e.g. MCP23S17
84/// because PCA9702 is read-only and has no register-based protocol.
85pub trait Pca9702BusTrait {
86    type BusError;
87
88    /// Reads 8 bits from the device (which represent the state of inputs [in7..in0])
89    fn read_inputs(&mut self) -> Result<u8, Self::BusError>;
90}
91
92impl<B: Pca9702BusTrait> PortDriver for Driver<B> {
93    /// Our `Error` is a custom enum wrapping both bus errors and an unsupported-ops error.
94    type Error = B::BusError;
95
96    /// PCA9702 is input-only, return an error here.
97    fn set(&mut self, _mask_high: u32, _mask_low: u32) -> Result<(), Self::Error> {
98        panic!("PCA9702 is input-only, cannot set output states");
99    }
100
101    /// PCA9702 is input-only, return an error here.
102    fn is_set(&mut self, _mask_high: u32, _mask_low: u32) -> Result<u32, Self::Error> {
103        panic!("PCA9702 is input-only, cannot read back output states");
104    }
105
106    /// Read the actual input bits from the PCA9702 device
107    fn get(&mut self, mask_high: u32, mask_low: u32) -> Result<u32, Self::Error> {
108        let val = self.bus.read_inputs()? as u32;
109        Ok((val & mask_high) | (!val & mask_low))
110    }
111}
112
113/// Bus wrapper type for PCA9702, implementing `Pca9702BusTrait`.
114pub struct Pca9702Bus<SPI>(pub SPI);
115
116impl<SPI> Pca9702BusTrait for Pca9702Bus<SPI>
117where
118    SPI: SpiBus,
119{
120    type BusError = SPI::BusError;
121
122    fn read_inputs(&mut self) -> Result<u8, Self::BusError> {
123        // PCA9702 wants a total of 8 SCLK rising edges to shift out the input data
124        // from SDOUT: The first rising edge latches the inputs, the next 8 edges
125        // shift them out.
126        let mut buffer = [0u8];
127        let mut ops = [Operation::TransferInPlace(&mut buffer)];
128        self.0.transaction(&mut ops)?;
129
130        // buffer[0] now holds bits [in7..in0]
131        Ok(buffer[0])
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use embedded_hal_mock::eh1::spi::{Mock as SpiMock, Transaction as SpiTransaction};
139
140    #[test]
141    fn pca9702_read_inputs() {
142        let expectations = [
143            // 1st read
144            SpiTransaction::transaction_start(),
145            SpiTransaction::transfer_in_place(vec![0], vec![0xA5]),
146            SpiTransaction::transaction_end(),
147            // 2nd read
148            SpiTransaction::transaction_start(),
149            SpiTransaction::transfer_in_place(vec![0], vec![0xA5]),
150            SpiTransaction::transaction_end(),
151            // 3rd read
152            SpiTransaction::transaction_start(),
153            SpiTransaction::transfer_in_place(vec![0], vec![0xA5]),
154            SpiTransaction::transaction_end(),
155        ];
156        let mut spi_mock = SpiMock::new(&expectations);
157        let mut pca = Pca9702::new(spi_mock.clone());
158        let pins = pca.split();
159
160        // For each call, the driver re-reads from SPI, returning 0xA5 each time.
161        // 0xA5 = 0b10100101 => in0=1, in1=0, in2=1, in3=0, in4=0, in5=1, in6=0, in7=1
162        assert_eq!(pins.in0.is_high().unwrap(), true); // LSB => 1
163        assert_eq!(pins.in1.is_high().unwrap(), false);
164        assert_eq!(pins.in2.is_high().unwrap(), true);
165
166        spi_mock.done();
167    }
168
169    #[test]
170    #[should_panic]
171    fn pca9702_output_fails() {
172        let spi_mock = SpiMock::new(&[]);
173        let mut pca = Pca9702::new(spi_mock);
174        let pins = pca.split();
175
176        pins.in0.access_port_driver(|drv| {
177            drv.set(0x01, 0x00).unwrap_err();
178        });
179    }
180}