From c01e34e78c299be90326cd48f07461a32d2cf741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Nord=C3=A9n?= Date: Thu, 20 Nov 2025 20:30:38 +0100 Subject: [PATCH 01/10] Updated device.yaml with more registers --- device.yaml | 236 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 200 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 435 insertions(+), 1 deletion(-) diff --git a/device.yaml b/device.yaml index d0c48c4..b8b29e1 100644 --- a/device.yaml +++ b/device.yaml @@ -4,6 +4,84 @@ config: default_bit_order: LSB0 defmt_feature: defmt-03 +INPUT_PORT0: + type: register + address: 0x00 + size_bits: 8 + access: ReadOnly + fields: + I0_0: + base: bool + start: 0 + description: Input Port 0 Pin 0. Reflects the incoming logic level of the pin. + I0_1: + base: bool + start: 1 + description: Input Port 0 Pin 1. Reflects the incoming logic level of the pin. + I0_2: + base: bool + start: 2 + description: Input Port 0 Pin 2. Reflects the incoming logic level of the pin. + I0_3: + base: bool + start: 3 + description: Input Port 0 Pin 3. Reflects the incoming logic level of the pin. + I0_4: + base: bool + start: 4 + description: Input Port 0 Pin 4. Reflects the incoming logic level of the pin. + I0_5: + base: bool + start: 5 + description: Input Port 0 Pin 5. Reflects the incoming logic level of the pin. + I0_6: + base: bool + start: 6 + description: Input Port 0 Pin 6. Reflects the incoming logic level of the pin. + I0_7: + base: bool + start: 7 + description: Input Port 0 Pin 7. Reflects the incoming logic level of the pin. + +INPUT_PORT1: + type: register + address: 0x01 + size_bits: 8 + access: ReadOnly + fields: + I1_0: + base: bool + start: 0 + description: Input Port 1 Pin 0. Reflects the incoming logic level of the pin. + I1_1: + base: bool + start: 1 + description: Input Port 1 Pin 1. Reflects the incoming logic level of the pin. + I1_2: + base: bool + start: 2 + description: Input Port 1 Pin 2. Reflects the incoming logic level of the pin. + I1_3: + base: bool + start: 3 + description: Input Port 1 Pin 3. Reflects the incoming logic level of the pin. + I1_4: + base: bool + start: 4 + description: Input Port 1 Pin 4. Reflects the incoming logic level of the pin. + I1_5: + base: bool + start: 5 + description: Input Port 1 Pin 5. Reflects the incoming logic level of the pin. + I1_6: + base: bool + start: 6 + description: Input Port 1 Pin 6. Reflects the incoming logic level of the pin. + I1_7: + base: bool + start: 7 + description: Input Port 1 Pin 7. Reflects the incoming logic level of the pin. + OUTPUT_PORT0: type: register address: 0x02 @@ -157,4 +235,160 @@ CONFIG_PORT1: C1_7: base: bool start: 7 - description: Config Port 1 Pin 7. Host clears this bit to set the pin as an output. Bit is set by default and configures the pin as an input. \ No newline at end of file + description: Config Port 1 Pin 7. Host clears this bit to set the pin as an output. Bit is set by default and configures the pin as an input. + +PULL_UP_DOWN_ENABLE_PORT0: + type: register + address: 0x46 + size_bits: 8 + reset_value: 0x00 + fields: + PE0_0: + base: bool + start: 0 + description: Pull-up/Pull-down Enable Port 0 Pin 0. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE0_1: + base: bool + start: 1 + description: Pull-up/Pull-down Enable Port 0 Pin 1. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE0_2: + base: bool + start: 2 + description: Pull-up/Pull-down Enable Port 0 Pin 2. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE0_3: + base: bool + start: 3 + description: Pull-up/Pull-down Enable Port 0 Pin 3. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE0_4: + base: bool + start: 4 + description: Pull-up/Pull-down Enable Port 0 Pin 4. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE0_5: + base: bool + start: 5 + description: Pull-up/Pull-down Enable Port 0 Pin 5. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE0_6: + base: bool + start: 6 + description: Pull-up/Pull-down Enable Port 0 Pin 6. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE0_7: + base: bool + start: 7 + description: Pull-up/Pull-down Enable Port 0 Pin 7. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + +PULL_UP_DOWN_ENABLE_PORT1: + type: register + address: 0x47 + size_bits: 8 + reset_value: 0x00 + fields: + PE1_0: + base: bool + start: 0 + description: Pull-up/Pull-down Enable Port 1 Pin 0. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE1_1: + base: bool + start: 1 + description: Pull-up/Pull-down Enable Port 1 Pin 1. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE1_2: + base: bool + start: 2 + description: Pull-up/Pull-down Enable Port 1 Pin 2. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE1_3: + base: bool + start: 3 + description: Pull-up/Pull-down Enable Port 1 Pin 3. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE1_4: + base: bool + start: 4 + description: Pull-up/Pull-down Enable Port 1 Pin 4. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE1_5: + base: bool + start: 5 + description: Pull-up/Pull-down Enable Port 1 Pin 5. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE1_6: + base: bool + start: 6 + description: Pull-up/Pull-down Enable Port 1 Pin 6. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + PE1_7: + base: bool + start: 7 + description: Pull-up/Pull-down Enable Port 1 Pin 7. Host clears this bit to disable Pull-up/Pull-down resistor for the pin. Set the bit to enable Pull-up/Pull-down resistors for the pin. + +PULL_UP_DOWN_SELECT_PORT0: + type: register + address: 0x48 + size_bits: 8 + reset_value: 0xFF + fields: + PUD0_0: + base: bool + start: 0 + description: Pull-up/Pull-down Selection Port 0 Pin 0. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD0_1: + base: bool + start: 1 + description: Pull-up/Pull-down Selection Port 0 Pin 1. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD0_2: + base: bool + start: 2 + description: Pull-up/Pull-down Selection Port 0 Pin 2. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD0_3: + base: bool + start: 3 + description: Pull-up/Pull-down Selection Port 0 Pin 3. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD0_4: + base: bool + start: 4 + description: Pull-up/Pull-down Selection Port 0 Pin 4. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD0_5: + base: bool + start: 5 + description: Pull-up/Pull-down Selection Port 0 Pin 5. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD0_6: + base: bool + start: 6 + description: Pull-up/Pull-down Selection Port 0 Pin 6. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD0_7: + base: bool + start: 7 + description: Pull-up/Pull-down Selection Port 0 Pin 7. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + +PULL_UP_DOWN_SELECT_PORT1: + type: register + address: 0x49 + size_bits: 8 + reset_value: 0xFF + fields: + PUD1_0: + base: bool + start: 0 + description: Pull-up/Pull-down Selection Port 1 Pin 0. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD1_1: + base: bool + start: 1 + description: Pull-up/Pull-down Selection Port 1 Pin 1. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD1_2: + base: bool + start: 2 + description: Pull-up/Pull-down Selection Port 1 Pin 2. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD1_3: + base: bool + start: 3 + description: Pull-up/Pull-down Selection Port 1 Pin 3. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD1_4: + base: bool + start: 4 + description: Pull-up/Pull-down Selection Port 1 Pin 4. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD1_5: + base: bool + start: 5 + description: Pull-up/Pull-down Selection Port 1 Pin 5. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD1_6: + base: bool + start: 6 + description: Pull-up/Pull-down Selection Port 1 Pin 6. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. + PUD1_7: + base: bool + start: 7 + description: Pull-up/Pull-down Selection Port 1 Pin 7. Host clears this bit enables a 100 kΩ pull-down resistor for the pin. Set the bit to enable a 100 kΩ pull-up resistor. diff --git a/src/lib.rs b/src/lib.rs index 4a19b48..892ee8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,6 +125,86 @@ mod tests { use super::*; + #[tokio::test] + async fn read_input_port_0_async() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b01110111])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let input_port_0 = dev.input_port_0().read_async().await.unwrap(); + assert_eq!(input_port_0.i_0_7(), false); + assert_eq!(input_port_0.i_0_6(), true); + assert_eq!(input_port_0.i_0_5(), true); + assert_eq!(input_port_0.i_0_4(), true); + assert_eq!(input_port_0.i_0_3(), false); + assert_eq!(input_port_0.i_0_2(), true); + assert_eq!(input_port_0.i_0_1(), true); + assert_eq!(input_port_0.i_0_0(), true); + dev.interface.i2cbus.done(); + } + + #[test] + fn read_input_port_0() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b01110111])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let input_port_0 = dev.input_port_0().read().unwrap(); + assert_eq!(input_port_0.i_0_7(), false); + assert_eq!(input_port_0.i_0_6(), true); + assert_eq!(input_port_0.i_0_5(), true); + assert_eq!(input_port_0.i_0_4(), true); + assert_eq!(input_port_0.i_0_3(), false); + assert_eq!(input_port_0.i_0_2(), true); + assert_eq!(input_port_0.i_0_1(), true); + assert_eq!(input_port_0.i_0_0(), true); + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn read_input_port_1_async() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b01010101])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let input_port_1 = dev.input_port_1().read_async().await.unwrap(); + assert_eq!(input_port_1.i_1_7(), false); + assert_eq!(input_port_1.i_1_6(), true); + assert_eq!(input_port_1.i_1_5(), false); + assert_eq!(input_port_1.i_1_4(), true); + assert_eq!(input_port_1.i_1_3(), false); + assert_eq!(input_port_1.i_1_2(), true); + assert_eq!(input_port_1.i_1_1(), false); + assert_eq!(input_port_1.i_1_0(), true); + dev.interface.i2cbus.done(); + } + + #[test] + fn read_input_port_1() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b01010101])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let input_port_1 = dev.input_port_1().read().unwrap(); + assert_eq!(input_port_1.i_1_7(), false); + assert_eq!(input_port_1.i_1_6(), true); + assert_eq!(input_port_1.i_1_5(), false); + assert_eq!(input_port_1.i_1_4(), true); + assert_eq!(input_port_1.i_1_3(), false); + assert_eq!(input_port_1.i_1_2(), true); + assert_eq!(input_port_1.i_1_1(), false); + assert_eq!(input_port_1.i_1_0(), true); + dev.interface.i2cbus.done(); + } + #[tokio::test] async fn read_output_port_0_async() { let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b01000011])]; @@ -473,6 +553,126 @@ mod tests { dev.interface.i2cbus.done(); } + #[tokio::test] + async fn read_pull_up_down_enable_port_0_async() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x46], vec![0b00111010])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let pull_up_down_enable_port_0 = dev.pull_up_down_enable_port_0().read_async().await.unwrap(); + assert_eq!(pull_up_down_enable_port_0.pe_0_7(), false); + assert_eq!(pull_up_down_enable_port_0.pe_0_6(), false); + assert_eq!(pull_up_down_enable_port_0.pe_0_5(), true); + assert_eq!(pull_up_down_enable_port_0.pe_0_4(), true); + assert_eq!(pull_up_down_enable_port_0.pe_0_3(), true); + assert_eq!(pull_up_down_enable_port_0.pe_0_2(), false); + assert_eq!(pull_up_down_enable_port_0.pe_0_1(), true); + assert_eq!(pull_up_down_enable_port_0.pe_0_0(), false); + dev.interface.i2cbus.done(); + } + + #[test] + fn read_pull_up_down_enable_port_1() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x47], vec![0b11101100])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let pull_up_down_enable_port_1 = dev.pull_up_down_enable_port_1().read().unwrap(); + assert_eq!(pull_up_down_enable_port_1.pe_1_7(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_6(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_5(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_4(), false); + assert_eq!(pull_up_down_enable_port_1.pe_1_3(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_2(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_1(), false); + assert_eq!(pull_up_down_enable_port_1.pe_1_0(), false); + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn read_pull_up_down_select_port_0_async() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x48], vec![0b00111010])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let pull_up_down_select_port_0 = dev.pull_up_down_select_port_0().read_async().await.unwrap(); + assert_eq!(pull_up_down_select_port_0.pud_0_7(), false); + assert_eq!(pull_up_down_select_port_0.pud_0_6(), false); + assert_eq!(pull_up_down_select_port_0.pud_0_5(), true); + assert_eq!(pull_up_down_select_port_0.pud_0_4(), true); + assert_eq!(pull_up_down_select_port_0.pud_0_3(), true); + assert_eq!(pull_up_down_select_port_0.pud_0_2(), false); + assert_eq!(pull_up_down_select_port_0.pud_0_1(), true); + assert_eq!(pull_up_down_select_port_0.pud_0_0(), false); + dev.interface.i2cbus.done(); + } + + #[test] + fn read_pull_up_down_select_port_0() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x48], vec![0b00111010])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let pull_up_down_select_port_0 = dev.pull_up_down_select_port_0().read().unwrap(); + assert_eq!(pull_up_down_select_port_0.pud_0_7(), false); + assert_eq!(pull_up_down_select_port_0.pud_0_6(), false); + assert_eq!(pull_up_down_select_port_0.pud_0_5(), true); + assert_eq!(pull_up_down_select_port_0.pud_0_4(), true); + assert_eq!(pull_up_down_select_port_0.pud_0_3(), true); + assert_eq!(pull_up_down_select_port_0.pud_0_2(), false); + assert_eq!(pull_up_down_select_port_0.pud_0_1(), true); + assert_eq!(pull_up_down_select_port_0.pud_0_0(), false); + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn read_pull_up_down_select_port_1_async() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x49], vec![0b01100111])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let pull_up_down_select_port_1 = dev.pull_up_down_select_port_1().read_async().await.unwrap(); + assert_eq!(pull_up_down_select_port_1.pud_1_7(), false); + assert_eq!(pull_up_down_select_port_1.pud_1_6(), true); + assert_eq!(pull_up_down_select_port_1.pud_1_5(), true); + assert_eq!(pull_up_down_select_port_1.pud_1_4(), false); + assert_eq!(pull_up_down_select_port_1.pud_1_3(), false); + assert_eq!(pull_up_down_select_port_1.pud_1_2(), true); + assert_eq!(pull_up_down_select_port_1.pud_1_1(), true); + assert_eq!(pull_up_down_select_port_1.pud_1_0(), true); + dev.interface.i2cbus.done(); + } + + #[test] + fn read_pull_up_down_select_port_1() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x49], vec![0b01100111])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let pull_up_down_select_port_1 = dev.pull_up_down_select_port_1().read().unwrap(); + assert_eq!(pull_up_down_select_port_1.pud_1_7(), false); + assert_eq!(pull_up_down_select_port_1.pud_1_6(), true); + assert_eq!(pull_up_down_select_port_1.pud_1_5(), true); + assert_eq!(pull_up_down_select_port_1.pud_1_4(), false); + assert_eq!(pull_up_down_select_port_1.pud_1_3(), false); + assert_eq!(pull_up_down_select_port_1.pud_1_2(), true); + assert_eq!(pull_up_down_select_port_1.pud_1_1(), true); + assert_eq!(pull_up_down_select_port_1.pud_1_0(), true); + dev.interface.i2cbus.done(); + } + #[tokio::test] async fn write_low_address() { let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x07, 0])]; From 121c98e61390d3aeb4e10c57f3c5cd45b85aaeeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Nord=C3=A9n?= Date: Fri, 21 Nov 2025 11:18:35 +0100 Subject: [PATCH 02/10] added missing unit tests --- src/lib.rs | 228 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 892ee8e..f2ad07b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -573,6 +573,93 @@ mod tests { dev.interface.i2cbus.done(); } + #[test] + fn read_pull_up_down_enable_port_0() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x46], vec![0b00111010])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let pull_up_down_enable_port_0 = dev.pull_up_down_enable_port_0().read().unwrap(); + assert_eq!(pull_up_down_enable_port_0.pe_0_7(), false); + assert_eq!(pull_up_down_enable_port_0.pe_0_6(), false); + assert_eq!(pull_up_down_enable_port_0.pe_0_5(), true); + assert_eq!(pull_up_down_enable_port_0.pe_0_4(), true); + assert_eq!(pull_up_down_enable_port_0.pe_0_3(), true); + assert_eq!(pull_up_down_enable_port_0.pe_0_2(), false); + assert_eq!(pull_up_down_enable_port_0.pe_0_1(), true); + assert_eq!(pull_up_down_enable_port_0.pe_0_0(), false); + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn write_pull_up_down_enable_port_0_async() { + let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x46, 0b00111010])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.pull_up_down_enable_port_0() + .write_async(|c| { + c.set_pe_0_7(false); + c.set_pe_0_6(false); + c.set_pe_0_5(true); + c.set_pe_0_4(true); + c.set_pe_0_3(true); + c.set_pe_0_2(false); + c.set_pe_0_1(true); + c.set_pe_0_0(false); + }) + .await + .unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn write_pull_up_down_enable_port_0() { + let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x46, 0b00111010])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.pull_up_down_enable_port_0() + .write(|c| { + c.set_pe_0_7(false); + c.set_pe_0_6(false); + c.set_pe_0_5(true); + c.set_pe_0_4(true); + c.set_pe_0_3(true); + c.set_pe_0_2(false); + c.set_pe_0_1(true); + c.set_pe_0_0(false); + }) + .unwrap(); + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn read_pull_up_down_enable_port_1_async() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x47], vec![0b11101100])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let pull_up_down_enable_port_1 = dev.pull_up_down_enable_port_1().read_async().await.unwrap(); + assert_eq!(pull_up_down_enable_port_1.pe_1_7(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_6(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_5(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_4(), false); + assert_eq!(pull_up_down_enable_port_1.pe_1_3(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_2(), true); + assert_eq!(pull_up_down_enable_port_1.pe_1_1(), false); + assert_eq!(pull_up_down_enable_port_1.pe_1_0(), false); + dev.interface.i2cbus.done(); + } + #[test] fn read_pull_up_down_enable_port_1() { let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x47], vec![0b11101100])]; @@ -593,6 +680,53 @@ mod tests { dev.interface.i2cbus.done(); } + #[tokio::test] + async fn write_pull_up_down_enable_port_1_async() { + let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x47, 0b01011100])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.pull_up_down_enable_port_1() + .write_async(|c| { + c.set_pe_1_7(false); + c.set_pe_1_6(true); + c.set_pe_1_5(false); + c.set_pe_1_4(true); + c.set_pe_1_3(true); + c.set_pe_1_2(true); + c.set_pe_1_1(false); + c.set_pe_1_0(false); + }) + .await + .unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn write_pull_up_down_enable_port_1() { + let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x47, 0b11101010])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.pull_up_down_enable_port_1() + .write(|c| { + c.set_pe_1_7(true); + c.set_pe_1_6(true); + c.set_pe_1_5(true); + c.set_pe_1_4(false); + c.set_pe_1_3(true); + c.set_pe_1_2(false); + c.set_pe_1_1(true); + c.set_pe_1_0(false); + }) + .unwrap(); + dev.interface.i2cbus.done(); + } + #[tokio::test] async fn read_pull_up_down_select_port_0_async() { let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x48], vec![0b00111010])]; @@ -633,6 +767,53 @@ mod tests { dev.interface.i2cbus.done(); } + #[tokio::test] + async fn write_pull_up_down_select_port_0_async() { + let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x48, 0b01011001])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.pull_up_down_select_port_0() + .write_async(|c| { + c.set_pud_0_7(false); + c.set_pud_0_6(true); + c.set_pud_0_5(false); + c.set_pud_0_4(true); + c.set_pud_0_3(true); + c.set_pud_0_2(false); + c.set_pud_0_1(false); + c.set_pud_0_0(true); + }) + .await + .unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn write_pull_up_down_select_port_0() { + let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x48, 0b11101010])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.pull_up_down_select_port_0() + .write(|c| { + c.set_pud_0_7(true); + c.set_pud_0_6(true); + c.set_pud_0_5(true); + c.set_pud_0_4(false); + c.set_pud_0_3(true); + c.set_pud_0_2(false); + c.set_pud_0_1(true); + c.set_pud_0_0(false); + }) + .unwrap(); + dev.interface.i2cbus.done(); + } + #[tokio::test] async fn read_pull_up_down_select_port_1_async() { let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x49], vec![0b01100111])]; @@ -673,6 +854,53 @@ mod tests { dev.interface.i2cbus.done(); } + #[tokio::test] + async fn write_pull_up_down_select_port_1_async() { + let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x49, 0b00011011])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.pull_up_down_select_port_1() + .write_async(|c| { + c.set_pud_1_7(false); + c.set_pud_1_6(false); + c.set_pud_1_5(false); + c.set_pud_1_4(true); + c.set_pud_1_3(true); + c.set_pud_1_2(false); + c.set_pud_1_1(true); + c.set_pud_1_0(true); + }) + .await + .unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn write_pull_up_down_select_port_1() { + let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x49, 0b00011011])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.pull_up_down_select_port_1() + .write(|c| { + c.set_pud_1_7(false); + c.set_pud_1_6(false); + c.set_pud_1_5(false); + c.set_pud_1_4(true); + c.set_pud_1_3(true); + c.set_pud_1_2(false); + c.set_pud_1_1(true); + c.set_pud_1_0(true); + }) + .unwrap(); + dev.interface.i2cbus.done(); + } + #[tokio::test] async fn write_low_address() { let expectations = vec![Transaction::write(IOEXP_ADDR_LOW, vec![0x07, 0])]; From 83a3c6d82323f9c8917b7ab01a06854c88b6ac63 Mon Sep 17 00:00:00 2001 From: Felipe Balbi Date: Fri, 21 Nov 2025 08:42:17 -0800 Subject: [PATCH 03/10] Bump version Needed to maintain semver --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f187859..4dd6e9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pcal6416a" -version = "0.2.0" +version = "0.3.0" repository = "https://github.com/OpenDevicePartnership/pcal6416a" license = "MIT" description = "Platform-agnostic Rust driver for the NXP PCAL6416A I/O expander." From 9e30ca0b4414ed940d5d7fc379484bd4b1b0cde4 Mon Sep 17 00:00:00 2001 From: Jerry Xie Date: Tue, 25 Nov 2025 09:21:21 -0600 Subject: [PATCH 04/10] RFC provide GPIO interface for pcal6416a pins --- src/lib.rs | 388 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index dd556dd..c151e67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,6 +119,265 @@ impl device_driver::RegisterInterface for Pcal6416a } } +/// Pin number for the PCAL6416A device +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Pin { + /// Pin 0 (Port 0, bit 0) + Pin0, + /// Pin 1 (Port 0, bit 1) + Pin1, + /// Pin 2 (Port 0, bit 2) + Pin2, + /// Pin 3 (Port 0, bit 3) + Pin3, + /// Pin 4 (Port 0, bit 4) + Pin4, + /// Pin 5 (Port 0, bit 5) + Pin5, + /// Pin 6 (Port 0, bit 6) + Pin6, + /// Pin 7 (Port 0, bit 7) + Pin7, + /// Pin 8 (Port 1, bit 0) + Pin8, + /// Pin 9 (Port 1, bit 1) + Pin9, + /// Pin 10 (Port 1, bit 2) + Pin10, + /// Pin 11 (Port 1, bit 3) + Pin11, + /// Pin 12 (Port 1, bit 4) + Pin12, + /// Pin 13 (Port 1, bit 5) + Pin13, + /// Pin 14 (Port 1, bit 6) + Pin14, + /// Pin 15 (Port 1, bit 7) + Pin15, +} + +impl Pin { + /// Get the port number (0 or 1) + #[must_use] + const fn port(self) -> u8 { + match self { + Self::Pin0 | Self::Pin1 | Self::Pin2 | Self::Pin3 | Self::Pin4 | Self::Pin5 | Self::Pin6 | Self::Pin7 => 0, + Self::Pin8 + | Self::Pin9 + | Self::Pin10 + | Self::Pin11 + | Self::Pin12 + | Self::Pin13 + | Self::Pin14 + | Self::Pin15 => 1, + } + } + + /// Get the bit position within the port (0-7) + #[must_use] + const fn bit(self) -> u8 { + match self { + Self::Pin0 | Self::Pin8 => 0, + Self::Pin1 | Self::Pin9 => 1, + Self::Pin2 | Self::Pin10 => 2, + Self::Pin3 | Self::Pin11 => 3, + Self::Pin4 | Self::Pin12 => 4, + Self::Pin5 | Self::Pin13 => 5, + Self::Pin6 | Self::Pin14 => 6, + Self::Pin7 | Self::Pin15 => 7, + } + } + + /// Get the pin number (0-15) + #[must_use] + pub const fn number(&self) -> u8 { + match self { + Self::Pin0 => 0, + Self::Pin1 => 1, + Self::Pin2 => 2, + Self::Pin3 => 3, + Self::Pin4 => 4, + Self::Pin5 => 5, + Self::Pin6 => 6, + Self::Pin7 => 7, + Self::Pin8 => 8, + Self::Pin9 => 9, + Self::Pin10 => 10, + Self::Pin11 => 11, + Self::Pin12 => 12, + Self::Pin13 => 13, + Self::Pin14 => 14, + Self::Pin15 => 15, + } + } +} + +impl Device> { + /// Read the state of an input pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_pin_high(&mut self, pin: Pin) -> Result> { + let port = pin.port(); + let bit = pin.bit(); + + let value = if port == 0 { + let reg = self.input_port_0().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } else { + let reg = self.input_port_1().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + }; + + Ok(value) + } + + /// Read the state of an input pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_pin_low(&mut self, pin: Pin) -> Result> { + Ok(!self.is_pin_high(pin)?) + } + + /// Set an output pin to high state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn set_pin_high(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { + let port = pin.port(); + let bit = pin.bit(); + + if port == 0 { + self.output_port_0().modify(|r| match bit { + 0 => r.set_o_0_0(true), + 1 => r.set_o_0_1(true), + 2 => r.set_o_0_2(true), + 3 => r.set_o_0_3(true), + 4 => r.set_o_0_4(true), + 5 => r.set_o_0_5(true), + 6 => r.set_o_0_6(true), + 7 => r.set_o_0_7(true), + _ => unreachable!(), + }) + } else { + self.output_port_1().modify(|r| match bit { + 0 => r.set_o_1_0(true), + 1 => r.set_o_1_1(true), + 2 => r.set_o_1_2(true), + 3 => r.set_o_1_3(true), + 4 => r.set_o_1_4(true), + 5 => r.set_o_1_5(true), + 6 => r.set_o_1_6(true), + 7 => r.set_o_1_7(true), + _ => unreachable!(), + }) + } + } + + /// Set an output pin to low state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn set_pin_low(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { + let port = pin.port(); + let bit = pin.bit(); + + if port == 0 { + self.output_port_0().modify(|r| match bit { + 0 => r.set_o_0_0(false), + 1 => r.set_o_0_1(false), + 2 => r.set_o_0_2(false), + 3 => r.set_o_0_3(false), + 4 => r.set_o_0_4(false), + 5 => r.set_o_0_5(false), + 6 => r.set_o_0_6(false), + 7 => r.set_o_0_7(false), + _ => unreachable!(), + }) + } else { + self.output_port_1().modify(|r| match bit { + 0 => r.set_o_1_0(false), + 1 => r.set_o_1_1(false), + 2 => r.set_o_1_2(false), + 3 => r.set_o_1_3(false), + 4 => r.set_o_1_4(false), + 5 => r.set_o_1_5(false), + 6 => r.set_o_1_6(false), + 7 => r.set_o_1_7(false), + _ => unreachable!(), + }) + } + } + + /// Toggle an output pin state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn toggle_pin(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { + let port = pin.port(); + let bit = pin.bit(); + + if port == 0 { + self.output_port_0().modify(|r| match bit { + 0 => r.set_o_0_0(!r.o_0_0()), + 1 => r.set_o_0_1(!r.o_0_1()), + 2 => r.set_o_0_2(!r.o_0_2()), + 3 => r.set_o_0_3(!r.o_0_3()), + 4 => r.set_o_0_4(!r.o_0_4()), + 5 => r.set_o_0_5(!r.o_0_5()), + 6 => r.set_o_0_6(!r.o_0_6()), + 7 => r.set_o_0_7(!r.o_0_7()), + _ => unreachable!(), + }) + } else { + self.output_port_1().modify(|r| match bit { + 0 => r.set_o_1_0(!r.o_1_0()), + 1 => r.set_o_1_1(!r.o_1_1()), + 2 => r.set_o_1_2(!r.o_1_2()), + 3 => r.set_o_1_3(!r.o_1_3()), + 4 => r.set_o_1_4(!r.o_1_4()), + 5 => r.set_o_1_5(!r.o_1_5()), + 6 => r.set_o_1_6(!r.o_1_6()), + 7 => r.set_o_1_7(!r.o_1_7()), + _ => unreachable!(), + }) + } + } + + /// Read the current state of an output pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_pin_set_high(&mut self, pin: Pin) -> Result> { + let port = pin.port(); + let bit = pin.bit(); + + let value = if port == 0 { + let reg = self.output_port_0().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } else { + let reg = self.output_port_1().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + }; + + Ok(value) + } + + /// Read the current state of an output pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_pin_set_low(&mut self, pin: Pin) -> Result> { + Ok(!self.is_pin_set_high(pin)?) + } +} + #[cfg(test)] mod tests { use embedded_hal_mock::eh1::i2c::{Mock, Transaction}; @@ -972,4 +1231,133 @@ mod tests { let _ = dev.config_port_1().read_async().await.unwrap(); dev.interface.i2cbus.done(); } + + #[test] + fn input_pin_is_high() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + assert!(dev.is_pin_high(Pin::Pin0).unwrap()); + dev.interface.i2cbus.done(); + } + + #[test] + fn input_pin_is_low() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + assert!(dev.is_pin_low(Pin::Pin0).unwrap()); + dev.interface.i2cbus.done(); + } + + #[test] + fn input_pin_port1() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b1000_0000])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + assert!(dev.is_pin_high(Pin::Pin15).unwrap()); + dev.interface.i2cbus.done(); + } + + #[test] + fn output_pin_set_high() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.set_pin_high(Pin::Pin0).unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn output_pin_set_low() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b1111_1111]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b1111_1110]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.set_pin_low(Pin::Pin0).unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn output_pin_port1() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0111_1111]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b1111_1111]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.set_pin_high(Pin::Pin15).unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn toggle_pin() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.toggle_pin(Pin::Pin0).unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn is_pin_set_high() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + assert!(dev.is_pin_set_high(Pin::Pin0).unwrap()); + dev.interface.i2cbus.done(); + } + + #[test] + fn multiple_pins_at_once() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0011]), + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b1111_1111]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + // Set multiple pins without borrowing conflicts + dev.set_pin_high(Pin::Pin0).unwrap(); + dev.set_pin_high(Pin::Pin1).unwrap(); + assert!(dev.is_pin_high(Pin::Pin7).unwrap()); + dev.interface.i2cbus.done(); + } } From 1d06017d7813a52eb109fe0ab87dd622e15e0d2d Mon Sep 17 00:00:00 2001 From: Jerry Xie Date: Sun, 8 Feb 2026 08:12:44 -0600 Subject: [PATCH 05/10] feat: add split pin API and embedded-hal support Add comprehensive GPIO pin interface with individual pin control: - Implement IoPin type for individual pin operations - Add Device::split() method to create array of 16 IoPin instances - Support both sync and async pin operations (set/read/toggle) - Implement embedded-hal digital traits (InputPin, OutputPin, StatefulOutputPin) - Add full async GPIO methods to Device (set_pin_high_async, etc.) - Implement digital::Error trait for Pcal6416aError - Add comprehensive test coverage for pin operations and embedded-hal trait compliance - Temporarily disable unsafe_code lint to allow UnsafeCell for interior mutability The split() API enables passing individual pins to different functions while maintaining shared access to the underlying I2C device through safe interior mutability patterns. --- Cargo.toml | 2 +- src/lib.rs | 808 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 809 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4dd6e9b..117aa5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ embedded-hal-mock = { version = "0.11.1", features = ["embedded-hal-async"] } tokio = { version = "1.42.0", features = ["rt", "macros"] } [lints.rust] -unsafe_code = "forbid" +#unsafe_code = "forbid" missing_docs = "deny" [lints.clippy] diff --git a/src/lib.rs b/src/lib.rs index c151e67..9fa437f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,13 @@ pub enum Pcal6416aError { /// I2C bus error I2c(E), } + +impl embedded_hal::digital::Error for Pcal6416aError { + fn kind(&self) -> embedded_hal::digital::ErrorKind { + embedded_hal::digital::ErrorKind::Other + } +} + const IOEXP_ADDR_LOW: u8 = 0x20; const IOEXP_ADDR_HIGH: u8 = 0x21; const LARGEST_REG_SIZE_BYTES: usize = 2; @@ -213,6 +220,205 @@ impl Pin { } } +/// Individual pin instance that provides GPIO operations for a single pin +/// +/// This struct is created by calling `split()` on a `Device` instance. +/// It provides methods to read and write the state of a single pin without +/// requiring access to the entire device. +/// +/// Note: This uses `UnsafeCell` for interior mutability. While this uses unsafe +/// internally, the API is designed to be safe when pins are not used concurrently +/// (which is the normal case in embedded contexts). +pub struct IoPin<'a, I2c> { + pin: Pin, + device: &'a core::cell::UnsafeCell>>, +} + +impl<'a, I2c> IoPin<'a, I2c> { + const fn new(pin: Pin, device: &'a core::cell::UnsafeCell>>) -> Self { + Self { pin, device } + } + + /// Get the pin number (0-15) + #[must_use] + pub const fn number(&self) -> u8 { + self.pin.number() + } + + /// Get the Pin enum for this pin + #[must_use] + pub const fn pin(&self) -> Pin { + self.pin + } +} + +impl IoPin<'_, I2c> { + /// Read the state of this input pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_high(&self) -> Result> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).is_pin_high(self.pin) } + } + + /// Read the state of this input pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_low(&self) -> Result> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).is_pin_low(self.pin) } + } + + /// Set this output pin to high state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn set_high(&self) -> Result<(), Pcal6416aError> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).set_pin_high(self.pin) } + } + + /// Set this output pin to low state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn set_low(&self) -> Result<(), Pcal6416aError> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).set_pin_low(self.pin) } + } + + /// Toggle this output pin state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn toggle(&self) -> Result<(), Pcal6416aError> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).toggle_pin(self.pin) } + } + + /// Read the current state of this output pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_set_high(&self) -> Result> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).is_pin_set_high(self.pin) } + } + + /// Read the current state of this output pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_set_low(&self) -> Result> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).is_pin_set_low(self.pin) } + } +} + +impl IoPin<'_, I2c> { + /// Read the state of this input pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_high_async(&self) -> Result> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).is_pin_high_async(self.pin).await } + } + + /// Read the state of this input pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_low_async(&self) -> Result> { + Ok(!self.is_high_async().await?) + } + + /// Set this output pin to high state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn set_high_async(&self) -> Result<(), Pcal6416aError> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).set_pin_high_async(self.pin).await } + } + + /// Set this output pin to low state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn set_low_async(&self) -> Result<(), Pcal6416aError> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).set_pin_low_async(self.pin).await } + } + + /// Toggle this output pin state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn toggle_async(&self) -> Result<(), Pcal6416aError> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).toggle_pin_async(self.pin).await } + } + + /// Read the current state of this output pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_set_high_async(&self) -> Result> { + // SAFETY: This is safe because pin operations are atomic and complete immediately + unsafe { (*self.device.get()).is_pin_set_high_async(self.pin).await } + } + + /// Read the current state of this output pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_set_low_async(&self) -> Result> { + Ok(!self.is_set_high_async().await?) + } +} + +// Implement embedded-hal digital traits for IoPin +impl embedded_hal::digital::ErrorType for IoPin<'_, I2c> { + type Error = Pcal6416aError; +} + +impl embedded_hal::digital::InputPin for IoPin<'_, I2c> { + fn is_high(&mut self) -> Result { + IoPin::is_high(self) + } + + fn is_low(&mut self) -> Result { + IoPin::is_low(self) + } +} + +impl embedded_hal::digital::OutputPin for IoPin<'_, I2c> { + fn set_low(&mut self) -> Result<(), Self::Error> { + IoPin::set_low(self) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + IoPin::set_high(self) + } +} + +impl embedded_hal::digital::StatefulOutputPin for IoPin<'_, I2c> { + fn is_set_high(&mut self) -> Result { + IoPin::is_set_high(self) + } + + fn is_set_low(&mut self) -> Result { + IoPin::is_set_low(self) + } + + fn toggle(&mut self) -> Result<(), Self::Error> { + IoPin::toggle(self) + } +} + impl Device> { /// Read the state of an input pin /// # Errors @@ -378,6 +584,240 @@ impl Device> { } } +impl Device> { + /// Read the state of an input pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_pin_high_async(&mut self, pin: Pin) -> Result> { + let port = pin.port(); + let bit = pin.bit(); + + let value = if port == 0 { + let reg = self.input_port_0().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } else { + let reg = self.input_port_1().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + }; + + Ok(value) + } + + /// Read the state of an input pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_pin_low_async(&mut self, pin: Pin) -> Result> { + Ok(!self.is_pin_high_async(pin).await?) + } + + /// Set an output pin to high state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn set_pin_high_async(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { + let port = pin.port(); + let bit = pin.bit(); + + if port == 0 { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(true), + 1 => r.set_o_0_1(true), + 2 => r.set_o_0_2(true), + 3 => r.set_o_0_3(true), + 4 => r.set_o_0_4(true), + 5 => r.set_o_0_5(true), + 6 => r.set_o_0_6(true), + 7 => r.set_o_0_7(true), + _ => unreachable!(), + }) + .await + } else { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(true), + 1 => r.set_o_1_1(true), + 2 => r.set_o_1_2(true), + 3 => r.set_o_1_3(true), + 4 => r.set_o_1_4(true), + 5 => r.set_o_1_5(true), + 6 => r.set_o_1_6(true), + 7 => r.set_o_1_7(true), + _ => unreachable!(), + }) + .await + } + } + + /// Set an output pin to low state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn set_pin_low_async(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { + let port = pin.port(); + let bit = pin.bit(); + + if port == 0 { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(false), + 1 => r.set_o_0_1(false), + 2 => r.set_o_0_2(false), + 3 => r.set_o_0_3(false), + 4 => r.set_o_0_4(false), + 5 => r.set_o_0_5(false), + 6 => r.set_o_0_6(false), + 7 => r.set_o_0_7(false), + _ => unreachable!(), + }) + .await + } else { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(false), + 1 => r.set_o_1_1(false), + 2 => r.set_o_1_2(false), + 3 => r.set_o_1_3(false), + 4 => r.set_o_1_4(false), + 5 => r.set_o_1_5(false), + 6 => r.set_o_1_6(false), + 7 => r.set_o_1_7(false), + _ => unreachable!(), + }) + .await + } + } + + /// Toggle an output pin state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn toggle_pin_async(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { + let port = pin.port(); + let bit = pin.bit(); + + if port == 0 { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(!r.o_0_0()), + 1 => r.set_o_0_1(!r.o_0_1()), + 2 => r.set_o_0_2(!r.o_0_2()), + 3 => r.set_o_0_3(!r.o_0_3()), + 4 => r.set_o_0_4(!r.o_0_4()), + 5 => r.set_o_0_5(!r.o_0_5()), + 6 => r.set_o_0_6(!r.o_0_6()), + 7 => r.set_o_0_7(!r.o_0_7()), + _ => unreachable!(), + }) + .await + } else { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(!r.o_1_0()), + 1 => r.set_o_1_1(!r.o_1_1()), + 2 => r.set_o_1_2(!r.o_1_2()), + 3 => r.set_o_1_3(!r.o_1_3()), + 4 => r.set_o_1_4(!r.o_1_4()), + 5 => r.set_o_1_5(!r.o_1_5()), + 6 => r.set_o_1_6(!r.o_1_6()), + 7 => r.set_o_1_7(!r.o_1_7()), + _ => unreachable!(), + }) + .await + } + } + + /// Read the current state of an output pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_pin_set_high_async(&mut self, pin: Pin) -> Result> { + let port = pin.port(); + let bit = pin.bit(); + + let value = if port == 0 { + let reg = self.output_port_0().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } else { + let reg = self.output_port_1().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + }; + + Ok(value) + } + + /// Read the current state of an output pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_pin_set_low_async(&mut self, pin: Pin) -> Result> { + Ok(!self.is_pin_set_high_async(pin).await?) + } +} + +impl Device> { + /// Split the driver into an array of individual pin instances + /// + /// This borrows the device mutably and returns an array of 16 `IoPin` instances, + /// one for each GPIO pin. The pins can be passed individually to different functions. + /// + /// # Example + /// ```ignore + /// let mut device = Device::new(Pcal6416aDevice { addr_pin, i2cbus }); + /// let pins = device.split(); + /// + /// // Pass individual pins to different functions + /// use_led(&pins[0]); + /// use_button(&pins[1]); + /// + /// // Or access by index + /// pins[2].set_high()?; + /// pins[3].set_low()?; + /// + /// // Iterate over pins + /// for (i, pin) in pins.iter().enumerate() { + /// println!("Pin {} number: {}", i, pin.number()); + /// } + /// ``` + pub fn split(&mut self) -> [IoPin<'_, I2c>; 16] { + use core::cell::UnsafeCell; + + // SAFETY: We use UnsafeCell to allow interior mutability. + // This is safe because: + // 1. Each pin operation is atomic and completes before another starts + // 2. The array borrows the device mutably, ensuring exclusive access + // 3. All pins share the same device lifetime + // 4. This is a common pattern in embedded HAL drivers for sharing hardware + let device_cell = + unsafe { &*(self as *mut Device> as *const UnsafeCell>>) }; + + [ + IoPin::new(Pin::Pin0, device_cell), + IoPin::new(Pin::Pin1, device_cell), + IoPin::new(Pin::Pin2, device_cell), + IoPin::new(Pin::Pin3, device_cell), + IoPin::new(Pin::Pin4, device_cell), + IoPin::new(Pin::Pin5, device_cell), + IoPin::new(Pin::Pin6, device_cell), + IoPin::new(Pin::Pin7, device_cell), + IoPin::new(Pin::Pin8, device_cell), + IoPin::new(Pin::Pin9, device_cell), + IoPin::new(Pin::Pin10, device_cell), + IoPin::new(Pin::Pin11, device_cell), + IoPin::new(Pin::Pin12, device_cell), + IoPin::new(Pin::Pin13, device_cell), + IoPin::new(Pin::Pin14, device_cell), + IoPin::new(Pin::Pin15, device_cell), + ] + } +} + #[cfg(test)] mod tests { use embedded_hal_mock::eh1::i2c::{Mock, Transaction}; @@ -1360,4 +1800,372 @@ mod tests { assert!(dev.is_pin_high(Pin::Pin7).unwrap()); dev.interface.i2cbus.done(); } + + #[test] + fn split_pins() { + let expectations = vec![ + // Set pin 0 high + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // Set pin 1 high + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0011]), + // Read pin 0 + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0011]), + // Toggle pin 1 + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0011]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + + // Use individual pins independently + pins[0].set_high().unwrap(); + pins[1].set_high().unwrap(); + assert!(pins[0].is_high().unwrap()); + pins[1].toggle().unwrap(); + } + + // Verify mock expectations + dev.interface.i2cbus.done(); + } + + #[test] + fn split_pin_numbers() { + let expectations = vec![]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + + // Verify all 16 pins have correct numbers (0-15) + for i in 0..16 { + assert_eq!(pins[i].number(), i as u8, "Pin at index {} should have number {}", i, i); + } + + // Verify Pin enum values for all pins + assert_eq!(pins[0].pin(), Pin::Pin0); + assert_eq!(pins[1].pin(), Pin::Pin1); + assert_eq!(pins[2].pin(), Pin::Pin2); + assert_eq!(pins[3].pin(), Pin::Pin3); + assert_eq!(pins[4].pin(), Pin::Pin4); + assert_eq!(pins[5].pin(), Pin::Pin5); + assert_eq!(pins[6].pin(), Pin::Pin6); + assert_eq!(pins[7].pin(), Pin::Pin7); + assert_eq!(pins[8].pin(), Pin::Pin8); + assert_eq!(pins[9].pin(), Pin::Pin9); + assert_eq!(pins[10].pin(), Pin::Pin10); + assert_eq!(pins[11].pin(), Pin::Pin11); + assert_eq!(pins[12].pin(), Pin::Pin12); + assert_eq!(pins[13].pin(), Pin::Pin13); + assert_eq!(pins[14].pin(), Pin::Pin14); + assert_eq!(pins[15].pin(), Pin::Pin15); + } + + dev.interface.i2cbus.done(); + } + + #[test] + fn embedded_hal_traits() { + use embedded_hal::digital::{InputPin, OutputPin, StatefulOutputPin}; + + let expectations = vec![ + // OutputPin::set_high + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // OutputPin::set_low + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // InputPin::is_high (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // InputPin::is_low (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // InputPin::is_high (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // InputPin::is_low (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // StatefulOutputPin::is_set_high (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + // StatefulOutputPin::is_set_low (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + // StatefulOutputPin::toggle (low to high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // StatefulOutputPin::is_set_high (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // StatefulOutputPin::is_set_low (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // StatefulOutputPin::toggle (high to low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let mut pins = dev.split(); + + // Test OutputPin trait - set_high + OutputPin::set_high(&mut pins[0]).unwrap(); + + // Test OutputPin trait - set_low + OutputPin::set_low(&mut pins[0]).unwrap(); + + // Test InputPin trait - is_high when pin is high + assert!(InputPin::is_high(&mut pins[0]).unwrap()); + + // Test InputPin trait - is_low when pin is high + assert!(!InputPin::is_low(&mut pins[0]).unwrap()); + + // Test InputPin trait - is_high when pin is low + assert!(!InputPin::is_high(&mut pins[0]).unwrap()); + + // Test InputPin trait - is_low when pin is low + assert!(InputPin::is_low(&mut pins[0]).unwrap()); + + // Test StatefulOutputPin trait - is_set_high when output is low + assert!(!StatefulOutputPin::is_set_high(&mut pins[0]).unwrap()); + + // Test StatefulOutputPin trait - is_set_low when output is low + assert!(StatefulOutputPin::is_set_low(&mut pins[0]).unwrap()); + + // Test StatefulOutputPin trait - toggle from low to high + StatefulOutputPin::toggle(&mut pins[0]).unwrap(); + + // Test StatefulOutputPin trait - is_set_high when output is high + assert!(StatefulOutputPin::is_set_high(&mut pins[0]).unwrap()); + + // Test StatefulOutputPin trait - is_set_low when output is high + assert!(!StatefulOutputPin::is_set_low(&mut pins[0]).unwrap()); + + // Test StatefulOutputPin trait - toggle from high to low + StatefulOutputPin::toggle(&mut pins[0]).unwrap(); + } + + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_set_high() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + pins[0].set_high_async().await.unwrap(); + } + + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_set_low() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + pins[0].set_low_async().await.unwrap(); + } + + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_is_high() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + assert!(pins[0].is_high_async().await.unwrap()); + } + + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_is_low() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + assert!(pins[0].is_low_async().await.unwrap()); + } + + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_toggle() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + pins[0].toggle_async().await.unwrap(); + } + + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_is_set_high() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + assert!(pins[0].is_set_high_async().await.unwrap()); + } + + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_is_set_low() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let pins = dev.split(); + assert!(pins[0].is_set_low_async().await.unwrap()); + } + + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn embedded_hal_async_traits() { + use embedded_hal_async::digital::{InputPin, OutputPin, StatefulOutputPin}; + + let expectations = vec![ + // OutputPin::set_high + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // OutputPin::set_low + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // InputPin::is_high (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // InputPin::is_low (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // InputPin::is_high (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // InputPin::is_low (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // StatefulOutputPin::is_set_high (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + // StatefulOutputPin::is_set_low (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + // StatefulOutputPin::toggle (low to high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // StatefulOutputPin::is_set_high (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // StatefulOutputPin::is_set_low (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // StatefulOutputPin::toggle (high to low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + + { + let mut pins = dev.split(); + + // Test OutputPin trait - set_high + OutputPin::set_high(&mut pins[0]).await.unwrap(); + + // Test OutputPin trait - set_low + OutputPin::set_low(&mut pins[0]).await.unwrap(); + + // Test InputPin trait - is_high when pin is high + assert!(InputPin::is_high(&mut pins[0]).await.unwrap()); + + // Test InputPin trait - is_low when pin is high + assert!(!InputPin::is_low(&mut pins[0]).await.unwrap()); + + // Test InputPin trait - is_high when pin is low + assert!(!InputPin::is_high(&mut pins[0]).await.unwrap()); + + // Test InputPin trait - is_low when pin is low + assert!(InputPin::is_low(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - is_set_high when output is low + assert!(!StatefulOutputPin::is_set_high(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - is_set_low when output is low + assert!(StatefulOutputPin::is_set_low(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - toggle from low to high + StatefulOutputPin::toggle(&mut pins[0]).await.unwrap(); + + // Test StatefulOutputPin trait - is_set_high when output is high + assert!(StatefulOutputPin::is_set_high(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - is_set_low when output is high + assert!(!StatefulOutputPin::is_set_low(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - toggle from high to low + StatefulOutputPin::toggle(&mut pins[0]).await.unwrap(); + } + + dev.interface.i2cbus.done(); + } } From fc46286247d9fd033da5de6feb390e4fba95d3c7 Mon Sep 17 00:00:00 2001 From: Jerry Xie Date: Sun, 8 Feb 2026 08:33:11 -0600 Subject: [PATCH 06/10] test: add port 0 and port 1 coverage to pin tests Improved test coverage for pin operations by testing pins from both ports (port 0 and port 1) in each test case. This ensures the logic is correctly validated across different register addresses since the device has separate registers for each port. Changes: - Updated sync pin tests (is_high, is_low, set_high, set_low, toggle, is_set_high) to test both Pin0 (port 0) and Pin15 (port 1) - Updated async pin tests (is_high_async, is_low_async, set_high_async, set_low_async, toggle_async, is_set_high_async, is_set_low_async) to test both Pin0 and Pin15 - Added inline comments to test expectations for clarity --- src/lib.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9fa437f..59eaaf9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1674,25 +1674,37 @@ mod tests { #[test] fn input_pin_is_high() { - let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001])]; + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b1000_0000]), + ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); assert!(dev.is_pin_high(Pin::Pin0).unwrap()); + assert!(dev.is_pin_high(Pin::Pin15).unwrap()); dev.interface.i2cbus.done(); } #[test] fn input_pin_is_low() { - let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000])]; + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b0000_0000]), + ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); assert!(dev.is_pin_low(Pin::Pin0).unwrap()); + assert!(dev.is_pin_low(Pin::Pin15).unwrap()); dev.interface.i2cbus.done(); } @@ -1711,8 +1723,12 @@ mod tests { #[test] fn output_pin_set_high() { let expectations = vec![ + // Port 0 pin Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b1000_0000]), ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { @@ -1720,14 +1736,19 @@ mod tests { i2cbus, }); dev.set_pin_high(Pin::Pin0).unwrap(); + dev.set_pin_high(Pin::Pin15).unwrap(); dev.interface.i2cbus.done(); } #[test] fn output_pin_set_low() { let expectations = vec![ + // Port 0 pin Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b1111_1111]), Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b1111_1110]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1111_1111]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0111_1111]), ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { @@ -1735,6 +1756,7 @@ mod tests { i2cbus, }); dev.set_pin_low(Pin::Pin0).unwrap(); + dev.set_pin_low(Pin::Pin15).unwrap(); dev.interface.i2cbus.done(); } @@ -1756,8 +1778,12 @@ mod tests { #[test] fn toggle_pin() { let expectations = vec![ + // Port 0 pin Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0000_0000]), ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { @@ -1765,18 +1791,25 @@ mod tests { i2cbus, }); dev.toggle_pin(Pin::Pin0).unwrap(); + dev.toggle_pin(Pin::Pin15).unwrap(); dev.interface.i2cbus.done(); } #[test] fn is_pin_set_high() { - let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001])]; + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); assert!(dev.is_pin_set_high(Pin::Pin0).unwrap()); + assert!(dev.is_pin_set_high(Pin::Pin15).unwrap()); dev.interface.i2cbus.done(); } @@ -1961,8 +1994,12 @@ mod tests { #[tokio::test] async fn async_pin_set_high() { let expectations = vec![ + // Port 0 pin Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b1000_0000]), ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { @@ -1973,6 +2010,7 @@ mod tests { { let pins = dev.split(); pins[0].set_high_async().await.unwrap(); + pins[15].set_high_async().await.unwrap(); } dev.interface.i2cbus.done(); @@ -1981,8 +2019,12 @@ mod tests { #[tokio::test] async fn async_pin_set_low() { let expectations = vec![ + // Port 0 pin Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0000_0000]), ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { @@ -1993,6 +2035,7 @@ mod tests { { let pins = dev.split(); pins[0].set_low_async().await.unwrap(); + pins[15].set_low_async().await.unwrap(); } dev.interface.i2cbus.done(); @@ -2000,7 +2043,12 @@ mod tests { #[tokio::test] async fn async_pin_is_high() { - let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001])]; + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b1000_0000]), + ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, @@ -2010,6 +2058,7 @@ mod tests { { let pins = dev.split(); assert!(pins[0].is_high_async().await.unwrap()); + assert!(pins[15].is_high_async().await.unwrap()); } dev.interface.i2cbus.done(); @@ -2017,7 +2066,12 @@ mod tests { #[tokio::test] async fn async_pin_is_low() { - let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000])]; + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b0000_0000]), + ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, @@ -2027,6 +2081,7 @@ mod tests { { let pins = dev.split(); assert!(pins[0].is_low_async().await.unwrap()); + assert!(pins[15].is_low_async().await.unwrap()); } dev.interface.i2cbus.done(); @@ -2035,8 +2090,12 @@ mod tests { #[tokio::test] async fn async_pin_toggle() { let expectations = vec![ + // Port 0 pin Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0000_0000]), ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { @@ -2047,6 +2106,7 @@ mod tests { { let pins = dev.split(); pins[0].toggle_async().await.unwrap(); + pins[15].toggle_async().await.unwrap(); } dev.interface.i2cbus.done(); @@ -2054,7 +2114,12 @@ mod tests { #[tokio::test] async fn async_pin_is_set_high() { - let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001])]; + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, @@ -2064,6 +2129,7 @@ mod tests { { let pins = dev.split(); assert!(pins[0].is_set_high_async().await.unwrap()); + assert!(pins[15].is_set_high_async().await.unwrap()); } dev.interface.i2cbus.done(); @@ -2071,7 +2137,12 @@ mod tests { #[tokio::test] async fn async_pin_is_set_low() { - let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000])]; + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0000_0000]), + ]; let i2cbus = Mock::new(&expectations); let mut dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, @@ -2081,6 +2152,7 @@ mod tests { { let pins = dev.split(); assert!(pins[0].is_set_low_async().await.unwrap()); + assert!(pins[15].is_set_low_async().await.unwrap()); } dev.interface.i2cbus.done(); From ea0cb389a294b8fc70e82425890224d168360802 Mon Sep 17 00:00:00 2001 From: Jerry Xie Date: Wed, 11 Feb 2026 20:07:22 -0600 Subject: [PATCH 07/10] feat: replace UnsafeCell with embassy-sync Mutex Replace the UnsafeCell-based interior mutability pattern with embassy-sync's Mutex for safe concurrent access to individual GPIO pins. Changes: - Add SharedDevice wrapper with Mutex - Update IoPin to use &Mutex instead of &UnsafeCell - Remove synchronous pin methods, keep only async variants - Implement embedded-hal-async traits instead of blocking traits - Update all tests to use SharedDevice and async patterns - Add embassy-sync dependency (0.7.2) - Re-enable unsafe_code lint (was commented out) - Add embedded-hal git patches for latest async traits This provides proper mutex-based synchronization for safe concurrent pin access in async contexts. --- Cargo.toml | 9 +- src/lib.rs | 416 +++++++++++++++++++++-------------------------------- 2 files changed, 171 insertions(+), 254 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 117aa5d..f857661 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,13 +22,16 @@ device-driver = { version = "1.0.7", default-features = false, features = ["yaml defmt = { version = "1.0", optional = true } embedded-hal = "1.0.0" embedded-hal-async = "1.0.0" +embassy-sync = "0.7.2" [dev-dependencies] embedded-hal-mock = { version = "0.11.1", features = ["embedded-hal-async"] } tokio = { version = "1.42.0", features = ["rt", "macros"] } +embassy-sync = { version = "0.7.2", features = ["std"] } +critical-section = { version = "1.2", features = ["std"] } [lints.rust] -#unsafe_code = "forbid" +unsafe_code = "forbid" missing_docs = "deny" [lints.clippy] @@ -40,3 +43,7 @@ pedantic = "deny" [features] defmt = ["dep:defmt", "device-driver/defmt-03"] + +[patch.crates-io] +embedded-hal = {git = "https://github.com/rust-embedded/embedded-hal.git"} +embedded-hal-async = {git = "https://github.com/rust-embedded/embedded-hal.git"} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 59eaaf9..a4ef4a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,10 @@ device_driver::create_device!( manifest: "device.yaml" ); +pub struct SharedDevice { + pub device: embassy_sync::mutex::Mutex>>, +} + impl device_driver::AsyncRegisterInterface for Pcal6416aDevice { type Error = Pcal6416aError; type AddressType = u8; @@ -222,20 +226,19 @@ impl Pin { /// Individual pin instance that provides GPIO operations for a single pin /// -/// This struct is created by calling `split()` on a `Device` instance. +/// This struct is created by calling `split()` on a `SharedDevice` instance. /// It provides methods to read and write the state of a single pin without -/// requiring access to the entire device. +/// requiring mutable access to the entire device. /// -/// Note: This uses `UnsafeCell` for interior mutability. While this uses unsafe -/// internally, the API is designed to be safe when pins are not used concurrently -/// (which is the normal case in embedded contexts). -pub struct IoPin<'a, I2c> { +/// Note: This uses a shared mutex to provide safe concurrent access to the device. +/// All pin operations acquire the mutex lock before performing I2C operations. +pub struct IoPin<'a, I2c: embedded_hal_async::i2c::I2c, M: embassy_sync::blocking_mutex::raw::RawMutex> { pin: Pin, - device: &'a core::cell::UnsafeCell>>, + device: &'a embassy_sync::mutex::Mutex>>, } -impl<'a, I2c> IoPin<'a, I2c> { - const fn new(pin: Pin, device: &'a core::cell::UnsafeCell>>) -> Self { +impl<'a, I2c: embedded_hal_async::i2c::I2c, M: embassy_sync::blocking_mutex::raw::RawMutex> IoPin<'a, I2c, M> { + const fn new(pin: Pin, device: &'a embassy_sync::mutex::Mutex>>) -> Self { Self { pin, device } } @@ -252,79 +255,14 @@ impl<'a, I2c> IoPin<'a, I2c> { } } -impl IoPin<'_, I2c> { - /// Read the state of this input pin - /// # Errors - /// - /// Will return `Err` if underlying I2C bus operation fails - pub fn is_high(&self) -> Result> { - // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).is_pin_high(self.pin) } - } - - /// Read the state of this input pin - /// # Errors - /// - /// Will return `Err` if underlying I2C bus operation fails - pub fn is_low(&self) -> Result> { - // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).is_pin_low(self.pin) } - } - - /// Set this output pin to high state - /// # Errors - /// - /// Will return `Err` if underlying I2C bus operation fails - pub fn set_high(&self) -> Result<(), Pcal6416aError> { - // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).set_pin_high(self.pin) } - } - - /// Set this output pin to low state - /// # Errors - /// - /// Will return `Err` if underlying I2C bus operation fails - pub fn set_low(&self) -> Result<(), Pcal6416aError> { - // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).set_pin_low(self.pin) } - } - - /// Toggle this output pin state - /// # Errors - /// - /// Will return `Err` if underlying I2C bus operation fails - pub fn toggle(&self) -> Result<(), Pcal6416aError> { - // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).toggle_pin(self.pin) } - } - - /// Read the current state of this output pin - /// # Errors - /// - /// Will return `Err` if underlying I2C bus operation fails - pub fn is_set_high(&self) -> Result> { - // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).is_pin_set_high(self.pin) } - } - - /// Read the current state of this output pin - /// # Errors - /// - /// Will return `Err` if underlying I2C bus operation fails - pub fn is_set_low(&self) -> Result> { - // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).is_pin_set_low(self.pin) } - } -} - -impl IoPin<'_, I2c> { +impl IoPin<'_, I2c, M> { /// Read the state of this input pin (async version) /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails pub async fn is_high_async(&self) -> Result> { // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).is_pin_high_async(self.pin).await } + self.device.lock().await.is_pin_high_async(self.pin).await } /// Read the state of this input pin (async version) @@ -341,7 +279,7 @@ impl IoPin<'_, I2c> { /// Will return `Err` if underlying I2C bus operation fails pub async fn set_high_async(&self) -> Result<(), Pcal6416aError> { // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).set_pin_high_async(self.pin).await } + self.device.lock().await.set_pin_high_async(self.pin).await } /// Set this output pin to low state (async version) @@ -350,7 +288,7 @@ impl IoPin<'_, I2c> { /// Will return `Err` if underlying I2C bus operation fails pub async fn set_low_async(&self) -> Result<(), Pcal6416aError> { // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).set_pin_low_async(self.pin).await } + self.device.lock().await.set_pin_low_async(self.pin).await } /// Toggle this output pin state (async version) @@ -359,7 +297,7 @@ impl IoPin<'_, I2c> { /// Will return `Err` if underlying I2C bus operation fails pub async fn toggle_async(&self) -> Result<(), Pcal6416aError> { // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).toggle_pin_async(self.pin).await } + self.device.lock().await.toggle_pin_async(self.pin).await } /// Read the current state of this output pin (async version) @@ -368,7 +306,7 @@ impl IoPin<'_, I2c> { /// Will return `Err` if underlying I2C bus operation fails pub async fn is_set_high_async(&self) -> Result> { // SAFETY: This is safe because pin operations are atomic and complete immediately - unsafe { (*self.device.get()).is_pin_set_high_async(self.pin).await } + self.device.lock().await.is_pin_set_high_async(self.pin).await } /// Read the current state of this output pin (async version) @@ -381,41 +319,49 @@ impl IoPin<'_, I2c> { } // Implement embedded-hal digital traits for IoPin -impl embedded_hal::digital::ErrorType for IoPin<'_, I2c> { +impl embedded_hal::digital::ErrorType + for IoPin<'_, I2c, M> +{ type Error = Pcal6416aError; } -impl embedded_hal::digital::InputPin for IoPin<'_, I2c> { - fn is_high(&mut self) -> Result { - IoPin::is_high(self) +impl + embedded_hal_async::digital::InputPin for IoPin<'_, I2c, M> +{ + async fn is_high(&mut self) -> Result { + IoPin::is_high_async(self).await } - fn is_low(&mut self) -> Result { - IoPin::is_low(self) + async fn is_low(&mut self) -> Result { + IoPin::is_low_async(self).await } } -impl embedded_hal::digital::OutputPin for IoPin<'_, I2c> { - fn set_low(&mut self) -> Result<(), Self::Error> { - IoPin::set_low(self) +impl + embedded_hal_async::digital::OutputPin for IoPin<'_, I2c, M> +{ + async fn set_low(&mut self) -> Result<(), Self::Error> { + IoPin::set_low_async(self).await } - fn set_high(&mut self) -> Result<(), Self::Error> { - IoPin::set_high(self) + async fn set_high(&mut self) -> Result<(), Self::Error> { + IoPin::set_high_async(self).await } } -impl embedded_hal::digital::StatefulOutputPin for IoPin<'_, I2c> { - fn is_set_high(&mut self) -> Result { - IoPin::is_set_high(self) +impl + embedded_hal_async::digital::StatefulOutputPin for IoPin<'_, I2c, M> +{ + async fn is_set_high(&mut self) -> Result { + IoPin::is_set_high_async(self).await } - fn is_set_low(&mut self) -> Result { - IoPin::is_set_low(self) + async fn is_set_low(&mut self) -> Result { + IoPin::is_set_low_async(self).await } - fn toggle(&mut self) -> Result<(), Self::Error> { - IoPin::toggle(self) + async fn toggle(&mut self) -> Result<(), Self::Error> { + IoPin::toggle_async(self).await } } @@ -761,59 +707,56 @@ impl Device> { } } -impl Device> { +impl SharedDevice { + pub fn new(device: Device>) -> Self { + Self { + device: embassy_sync::mutex::Mutex::new(device), + } + } + /// Split the driver into an array of individual pin instances /// - /// This borrows the device mutably and returns an array of 16 `IoPin` instances, + /// This borrows the shared device mutably and returns an array of 16 `IoPin` instances, /// one for each GPIO pin. The pins can be passed individually to different functions. + /// Each pin uses the shared mutex to safely access the underlying device. /// /// # Example /// ```ignore - /// let mut device = Device::new(Pcal6416aDevice { addr_pin, i2cbus }); - /// let pins = device.split(); + /// let device = Device::new(Pcal6416aDevice { addr_pin, i2cbus }); + /// let mut shared = SharedDevice::new(device); + /// let pins = shared.split(); /// /// // Pass individual pins to different functions - /// use_led(&pins[0]); - /// use_button(&pins[1]); + /// use_led(&pins[0]).await; + /// use_button(&pins[1]).await; /// /// // Or access by index - /// pins[2].set_high()?; - /// pins[3].set_low()?; + /// pins[2].set_high_async().await?; + /// pins[3].set_low_async().await?; /// /// // Iterate over pins /// for (i, pin) in pins.iter().enumerate() { /// println!("Pin {} number: {}", i, pin.number()); /// } /// ``` - pub fn split(&mut self) -> [IoPin<'_, I2c>; 16] { - use core::cell::UnsafeCell; - - // SAFETY: We use UnsafeCell to allow interior mutability. - // This is safe because: - // 1. Each pin operation is atomic and completes before another starts - // 2. The array borrows the device mutably, ensuring exclusive access - // 3. All pins share the same device lifetime - // 4. This is a common pattern in embedded HAL drivers for sharing hardware - let device_cell = - unsafe { &*(self as *mut Device> as *const UnsafeCell>>) }; - + pub fn split(&mut self) -> [IoPin<'_, I2c, M>; 16] { [ - IoPin::new(Pin::Pin0, device_cell), - IoPin::new(Pin::Pin1, device_cell), - IoPin::new(Pin::Pin2, device_cell), - IoPin::new(Pin::Pin3, device_cell), - IoPin::new(Pin::Pin4, device_cell), - IoPin::new(Pin::Pin5, device_cell), - IoPin::new(Pin::Pin6, device_cell), - IoPin::new(Pin::Pin7, device_cell), - IoPin::new(Pin::Pin8, device_cell), - IoPin::new(Pin::Pin9, device_cell), - IoPin::new(Pin::Pin10, device_cell), - IoPin::new(Pin::Pin11, device_cell), - IoPin::new(Pin::Pin12, device_cell), - IoPin::new(Pin::Pin13, device_cell), - IoPin::new(Pin::Pin14, device_cell), - IoPin::new(Pin::Pin15, device_cell), + IoPin::new(Pin::Pin0, &self.device), + IoPin::new(Pin::Pin1, &self.device), + IoPin::new(Pin::Pin2, &self.device), + IoPin::new(Pin::Pin3, &self.device), + IoPin::new(Pin::Pin4, &self.device), + IoPin::new(Pin::Pin5, &self.device), + IoPin::new(Pin::Pin6, &self.device), + IoPin::new(Pin::Pin7, &self.device), + IoPin::new(Pin::Pin8, &self.device), + IoPin::new(Pin::Pin9, &self.device), + IoPin::new(Pin::Pin10, &self.device), + IoPin::new(Pin::Pin11, &self.device), + IoPin::new(Pin::Pin12, &self.device), + IoPin::new(Pin::Pin13, &self.device), + IoPin::new(Pin::Pin14, &self.device), + IoPin::new(Pin::Pin15, &self.device), ] } } @@ -1834,8 +1777,8 @@ mod tests { dev.interface.i2cbus.done(); } - #[test] - fn split_pins() { + #[tokio::test] + async fn split_pins() { let expectations = vec![ // Set pin 0 high Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), @@ -1854,32 +1797,42 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let mut pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); // Use individual pins independently - pins[0].set_high().unwrap(); - pins[1].set_high().unwrap(); - assert!(pins[0].is_high().unwrap()); - pins[1].toggle().unwrap(); + pins[0].set_high_async().await.unwrap(); + pins[1].set_high_async().await.unwrap(); + assert!(pins[0].is_high_async().await.unwrap()); + pins[1].toggle_async().await.unwrap(); } // Verify mock expectations - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } - #[test] - fn split_pin_numbers() { + #[tokio::test] + async fn split_pin_numbers() { let expectations = vec![]; let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { + let dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); // Verify all 16 pins have correct numbers (0-15) for i in 0..16 { @@ -1905,90 +1858,7 @@ mod tests { assert_eq!(pins[15].pin(), Pin::Pin15); } - dev.interface.i2cbus.done(); - } - - #[test] - fn embedded_hal_traits() { - use embedded_hal::digital::{InputPin, OutputPin, StatefulOutputPin}; - - let expectations = vec![ - // OutputPin::set_high - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), - Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), - // OutputPin::set_low - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), - Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), - // InputPin::is_high (when high) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), - // InputPin::is_low (when high) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), - // InputPin::is_high (when low) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), - // InputPin::is_low (when low) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), - // StatefulOutputPin::is_set_high (when low) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), - // StatefulOutputPin::is_set_low (when low) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), - // StatefulOutputPin::toggle (low to high) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), - Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), - // StatefulOutputPin::is_set_high (when high) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), - // StatefulOutputPin::is_set_low (when high) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), - // StatefulOutputPin::toggle (high to low) - Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), - Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), - ]; - let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { - addr_pin: AddrPinState::Low, - i2cbus, - }); - - { - let mut pins = dev.split(); - - // Test OutputPin trait - set_high - OutputPin::set_high(&mut pins[0]).unwrap(); - - // Test OutputPin trait - set_low - OutputPin::set_low(&mut pins[0]).unwrap(); - - // Test InputPin trait - is_high when pin is high - assert!(InputPin::is_high(&mut pins[0]).unwrap()); - - // Test InputPin trait - is_low when pin is high - assert!(!InputPin::is_low(&mut pins[0]).unwrap()); - - // Test InputPin trait - is_high when pin is low - assert!(!InputPin::is_high(&mut pins[0]).unwrap()); - - // Test InputPin trait - is_low when pin is low - assert!(InputPin::is_low(&mut pins[0]).unwrap()); - - // Test StatefulOutputPin trait - is_set_high when output is low - assert!(!StatefulOutputPin::is_set_high(&mut pins[0]).unwrap()); - - // Test StatefulOutputPin trait - is_set_low when output is low - assert!(StatefulOutputPin::is_set_low(&mut pins[0]).unwrap()); - - // Test StatefulOutputPin trait - toggle from low to high - StatefulOutputPin::toggle(&mut pins[0]).unwrap(); - - // Test StatefulOutputPin trait - is_set_high when output is high - assert!(StatefulOutputPin::is_set_high(&mut pins[0]).unwrap()); - - // Test StatefulOutputPin trait - is_set_low when output is high - assert!(!StatefulOutputPin::is_set_low(&mut pins[0]).unwrap()); - - // Test StatefulOutputPin trait - toggle from high to low - StatefulOutputPin::toggle(&mut pins[0]).unwrap(); - } - - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } #[tokio::test] @@ -2002,18 +1872,23 @@ mod tests { Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b1000_0000]), ]; let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { + let dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); pins[0].set_high_async().await.unwrap(); pins[15].set_high_async().await.unwrap(); } - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } #[tokio::test] @@ -2027,18 +1902,23 @@ mod tests { Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0000_0000]), ]; let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { + let dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); pins[0].set_low_async().await.unwrap(); pins[15].set_low_async().await.unwrap(); } - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } #[tokio::test] @@ -2050,18 +1930,23 @@ mod tests { Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b1000_0000]), ]; let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { + let dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); assert!(pins[0].is_high_async().await.unwrap()); assert!(pins[15].is_high_async().await.unwrap()); } - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } #[tokio::test] @@ -2073,18 +1958,23 @@ mod tests { Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b0000_0000]), ]; let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { + let dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); assert!(pins[0].is_low_async().await.unwrap()); assert!(pins[15].is_low_async().await.unwrap()); } - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } #[tokio::test] @@ -2098,18 +1988,23 @@ mod tests { Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0000_0000]), ]; let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { + let dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); pins[0].toggle_async().await.unwrap(); pins[15].toggle_async().await.unwrap(); } - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } #[tokio::test] @@ -2121,18 +2016,23 @@ mod tests { Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), ]; let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { + let dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); assert!(pins[0].is_set_high_async().await.unwrap()); assert!(pins[15].is_set_high_async().await.unwrap()); } - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } #[tokio::test] @@ -2144,18 +2044,23 @@ mod tests { Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0000_0000]), ]; let i2cbus = Mock::new(&expectations); - let mut dev = Device::new(Pcal6416aDevice { + let dev = Device::new(Pcal6416aDevice { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let pins = dev.split(); + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); assert!(pins[0].is_set_low_async().await.unwrap()); assert!(pins[15].is_set_low_async().await.unwrap()); } - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } #[tokio::test] @@ -2197,9 +2102,14 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); + let mut dev = SharedDevice::new(dev); { - let mut pins = dev.split(); + let mut pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); // Test OutputPin trait - set_high OutputPin::set_high(&mut pins[0]).await.unwrap(); @@ -2238,6 +2148,6 @@ mod tests { StatefulOutputPin::toggle(&mut pins[0]).await.unwrap(); } - dev.interface.i2cbus.done(); + dev.device.lock().await.interface.i2cbus.done(); } } From 65baeb69363fdfec5d3b5ce5e17e2a641afb210d Mon Sep 17 00:00:00 2001 From: Jerry Xie Date: Sun, 22 Feb 2026 12:24:53 -0600 Subject: [PATCH 08/10] Refactor Pin enum to use Port+Pin pair for GPIO Split the GPIO pin model into separate Port and Pin enums so that each pin is identified by a (Port, Pin) pair instead of a single Pin0-Pin15 value. - Add Port enum with Port0 and Port1 variants - Reduce Pin enum from 16 variants (Pin0-Pin15) to 8 (Pin0-Pin7) - Add port field to IoPin struct alongside existing pin field - Update all Device methods (sync and async) to accept (port: Port, pin: Pin) parameters - Replace if/else port checks with exhaustive match on Port enum - Update SharedDevice::split() to assign correct Port to each pin - Update all tests to use new (Port, Pin) API --- src/lib.rs | 557 +++++++++++++++++++++++++++-------------------------- 1 file changed, 279 insertions(+), 278 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a4ef4a1..e8eed0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,79 +130,42 @@ impl device_driver::RegisterInterface for Pcal6416a } } -/// Pin number for the PCAL6416A device +/// Port number for the PCAL6416A device +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Port { + /// Port 0 (pins 0-7) + Port0, + /// Port 1 (pins 0-7) + Port1, +} + +/// Pin number within a port (0-7) for the PCAL6416A device #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Pin { - /// Pin 0 (Port 0, bit 0) + /// Pin 0 Pin0, - /// Pin 1 (Port 0, bit 1) + /// Pin 1 Pin1, - /// Pin 2 (Port 0, bit 2) + /// Pin 2 Pin2, - /// Pin 3 (Port 0, bit 3) + /// Pin 3 Pin3, - /// Pin 4 (Port 0, bit 4) + /// Pin 4 Pin4, - /// Pin 5 (Port 0, bit 5) + /// Pin 5 Pin5, - /// Pin 6 (Port 0, bit 6) + /// Pin 6 Pin6, - /// Pin 7 (Port 0, bit 7) + /// Pin 7 Pin7, - /// Pin 8 (Port 1, bit 0) - Pin8, - /// Pin 9 (Port 1, bit 1) - Pin9, - /// Pin 10 (Port 1, bit 2) - Pin10, - /// Pin 11 (Port 1, bit 3) - Pin11, - /// Pin 12 (Port 1, bit 4) - Pin12, - /// Pin 13 (Port 1, bit 5) - Pin13, - /// Pin 14 (Port 1, bit 6) - Pin14, - /// Pin 15 (Port 1, bit 7) - Pin15, } impl Pin { - /// Get the port number (0 or 1) - #[must_use] - const fn port(self) -> u8 { - match self { - Self::Pin0 | Self::Pin1 | Self::Pin2 | Self::Pin3 | Self::Pin4 | Self::Pin5 | Self::Pin6 | Self::Pin7 => 0, - Self::Pin8 - | Self::Pin9 - | Self::Pin10 - | Self::Pin11 - | Self::Pin12 - | Self::Pin13 - | Self::Pin14 - | Self::Pin15 => 1, - } - } - /// Get the bit position within the port (0-7) #[must_use] - const fn bit(self) -> u8 { - match self { - Self::Pin0 | Self::Pin8 => 0, - Self::Pin1 | Self::Pin9 => 1, - Self::Pin2 | Self::Pin10 => 2, - Self::Pin3 | Self::Pin11 => 3, - Self::Pin4 | Self::Pin12 => 4, - Self::Pin5 | Self::Pin13 => 5, - Self::Pin6 | Self::Pin14 => 6, - Self::Pin7 | Self::Pin15 => 7, - } - } - - /// Get the pin number (0-15) - #[must_use] - pub const fn number(&self) -> u8 { + pub const fn bit(self) -> u8 { match self { Self::Pin0 => 0, Self::Pin1 => 1, @@ -212,16 +175,14 @@ impl Pin { Self::Pin5 => 5, Self::Pin6 => 6, Self::Pin7 => 7, - Self::Pin8 => 8, - Self::Pin9 => 9, - Self::Pin10 => 10, - Self::Pin11 => 11, - Self::Pin12 => 12, - Self::Pin13 => 13, - Self::Pin14 => 14, - Self::Pin15 => 15, } } + + /// Get the pin number within the port (0-7) + #[must_use] + pub const fn number(&self) -> u8 { + self.bit() + } } /// Individual pin instance that provides GPIO operations for a single pin @@ -233,16 +194,21 @@ impl Pin { /// Note: This uses a shared mutex to provide safe concurrent access to the device. /// All pin operations acquire the mutex lock before performing I2C operations. pub struct IoPin<'a, I2c: embedded_hal_async::i2c::I2c, M: embassy_sync::blocking_mutex::raw::RawMutex> { + port: Port, pin: Pin, device: &'a embassy_sync::mutex::Mutex>>, } impl<'a, I2c: embedded_hal_async::i2c::I2c, M: embassy_sync::blocking_mutex::raw::RawMutex> IoPin<'a, I2c, M> { - const fn new(pin: Pin, device: &'a embassy_sync::mutex::Mutex>>) -> Self { - Self { pin, device } + const fn new( + port: Port, + pin: Pin, + device: &'a embassy_sync::mutex::Mutex>>, + ) -> Self { + Self { port, pin, device } } - /// Get the pin number (0-15) + /// Get the pin number within the port (0-7) #[must_use] pub const fn number(&self) -> u8 { self.pin.number() @@ -253,6 +219,12 @@ impl<'a, I2c: embedded_hal_async::i2c::I2c, M: embassy_sync::blocking_mutex::raw pub const fn pin(&self) -> Pin { self.pin } + + /// Get the Port enum for this pin + #[must_use] + pub const fn port(&self) -> Port { + self.port + } } impl IoPin<'_, I2c, M> { @@ -262,7 +234,7 @@ impl Result> { // SAFETY: This is safe because pin operations are atomic and complete immediately - self.device.lock().await.is_pin_high_async(self.pin).await + self.device.lock().await.is_pin_high_async(self.port, self.pin).await } /// Read the state of this input pin (async version) @@ -279,7 +251,7 @@ impl Result<(), Pcal6416aError> { // SAFETY: This is safe because pin operations are atomic and complete immediately - self.device.lock().await.set_pin_high_async(self.pin).await + self.device.lock().await.set_pin_high_async(self.port, self.pin).await } /// Set this output pin to low state (async version) @@ -288,7 +260,7 @@ impl Result<(), Pcal6416aError> { // SAFETY: This is safe because pin operations are atomic and complete immediately - self.device.lock().await.set_pin_low_async(self.pin).await + self.device.lock().await.set_pin_low_async(self.port, self.pin).await } /// Toggle this output pin state (async version) @@ -297,7 +269,7 @@ impl Result<(), Pcal6416aError> { // SAFETY: This is safe because pin operations are atomic and complete immediately - self.device.lock().await.toggle_pin_async(self.pin).await + self.device.lock().await.toggle_pin_async(self.port, self.pin).await } /// Read the current state of this output pin (async version) @@ -306,7 +278,11 @@ impl Result> { // SAFETY: This is safe because pin operations are atomic and complete immediately - self.device.lock().await.is_pin_set_high_async(self.pin).await + self.device + .lock() + .await + .is_pin_set_high_async(self.port, self.pin) + .await } /// Read the current state of this output pin (async version) @@ -370,18 +346,20 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub fn is_pin_high(&mut self, pin: Pin) -> Result> { - let port = pin.port(); + pub fn is_pin_high(&mut self, port: Port, pin: Pin) -> Result> { let bit = pin.bit(); - let value = if port == 0 { - let reg = self.input_port_0().read()?; - let reg: [u8; 1] = reg.into(); - reg[0] & (1 << bit) != 0 - } else { - let reg = self.input_port_1().read()?; - let reg: [u8; 1] = reg.into(); - reg[0] & (1 << bit) != 0 + let value = match port { + Port::Port0 => { + let reg = self.input_port_0().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + Port::Port1 => { + let reg = self.input_port_1().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } }; Ok(value) @@ -391,20 +369,19 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub fn is_pin_low(&mut self, pin: Pin) -> Result> { - Ok(!self.is_pin_high(pin)?) + pub fn is_pin_low(&mut self, port: Port, pin: Pin) -> Result> { + Ok(!self.is_pin_high(port, pin)?) } /// Set an output pin to high state /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub fn set_pin_high(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { - let port = pin.port(); + pub fn set_pin_high(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { let bit = pin.bit(); - if port == 0 { - self.output_port_0().modify(|r| match bit { + match port { + Port::Port0 => self.output_port_0().modify(|r| match bit { 0 => r.set_o_0_0(true), 1 => r.set_o_0_1(true), 2 => r.set_o_0_2(true), @@ -414,9 +391,8 @@ impl Device> { 6 => r.set_o_0_6(true), 7 => r.set_o_0_7(true), _ => unreachable!(), - }) - } else { - self.output_port_1().modify(|r| match bit { + }), + Port::Port1 => self.output_port_1().modify(|r| match bit { 0 => r.set_o_1_0(true), 1 => r.set_o_1_1(true), 2 => r.set_o_1_2(true), @@ -426,7 +402,7 @@ impl Device> { 6 => r.set_o_1_6(true), 7 => r.set_o_1_7(true), _ => unreachable!(), - }) + }), } } @@ -434,12 +410,11 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub fn set_pin_low(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { - let port = pin.port(); + pub fn set_pin_low(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { let bit = pin.bit(); - if port == 0 { - self.output_port_0().modify(|r| match bit { + match port { + Port::Port0 => self.output_port_0().modify(|r| match bit { 0 => r.set_o_0_0(false), 1 => r.set_o_0_1(false), 2 => r.set_o_0_2(false), @@ -449,9 +424,8 @@ impl Device> { 6 => r.set_o_0_6(false), 7 => r.set_o_0_7(false), _ => unreachable!(), - }) - } else { - self.output_port_1().modify(|r| match bit { + }), + Port::Port1 => self.output_port_1().modify(|r| match bit { 0 => r.set_o_1_0(false), 1 => r.set_o_1_1(false), 2 => r.set_o_1_2(false), @@ -461,7 +435,7 @@ impl Device> { 6 => r.set_o_1_6(false), 7 => r.set_o_1_7(false), _ => unreachable!(), - }) + }), } } @@ -469,12 +443,11 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub fn toggle_pin(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { - let port = pin.port(); + pub fn toggle_pin(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { let bit = pin.bit(); - if port == 0 { - self.output_port_0().modify(|r| match bit { + match port { + Port::Port0 => self.output_port_0().modify(|r| match bit { 0 => r.set_o_0_0(!r.o_0_0()), 1 => r.set_o_0_1(!r.o_0_1()), 2 => r.set_o_0_2(!r.o_0_2()), @@ -484,9 +457,8 @@ impl Device> { 6 => r.set_o_0_6(!r.o_0_6()), 7 => r.set_o_0_7(!r.o_0_7()), _ => unreachable!(), - }) - } else { - self.output_port_1().modify(|r| match bit { + }), + Port::Port1 => self.output_port_1().modify(|r| match bit { 0 => r.set_o_1_0(!r.o_1_0()), 1 => r.set_o_1_1(!r.o_1_1()), 2 => r.set_o_1_2(!r.o_1_2()), @@ -496,7 +468,7 @@ impl Device> { 6 => r.set_o_1_6(!r.o_1_6()), 7 => r.set_o_1_7(!r.o_1_7()), _ => unreachable!(), - }) + }), } } @@ -504,18 +476,20 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub fn is_pin_set_high(&mut self, pin: Pin) -> Result> { - let port = pin.port(); + pub fn is_pin_set_high(&mut self, port: Port, pin: Pin) -> Result> { let bit = pin.bit(); - let value = if port == 0 { - let reg = self.output_port_0().read()?; - let reg: [u8; 1] = reg.into(); - reg[0] & (1 << bit) != 0 - } else { - let reg = self.output_port_1().read()?; - let reg: [u8; 1] = reg.into(); - reg[0] & (1 << bit) != 0 + let value = match port { + Port::Port0 => { + let reg = self.output_port_0().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + Port::Port1 => { + let reg = self.output_port_1().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } }; Ok(value) @@ -525,8 +499,8 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub fn is_pin_set_low(&mut self, pin: Pin) -> Result> { - Ok(!self.is_pin_set_high(pin)?) + pub fn is_pin_set_low(&mut self, port: Port, pin: Pin) -> Result> { + Ok(!self.is_pin_set_high(port, pin)?) } } @@ -535,18 +509,20 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub async fn is_pin_high_async(&mut self, pin: Pin) -> Result> { - let port = pin.port(); + pub async fn is_pin_high_async(&mut self, port: Port, pin: Pin) -> Result> { let bit = pin.bit(); - let value = if port == 0 { - let reg = self.input_port_0().read_async().await?; - let reg: [u8; 1] = reg.into(); - reg[0] & (1 << bit) != 0 - } else { - let reg = self.input_port_1().read_async().await?; - let reg: [u8; 1] = reg.into(); - reg[0] & (1 << bit) != 0 + let value = match port { + Port::Port0 => { + let reg = self.input_port_0().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + Port::Port1 => { + let reg = self.input_port_1().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } }; Ok(value) @@ -556,46 +532,48 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub async fn is_pin_low_async(&mut self, pin: Pin) -> Result> { - Ok(!self.is_pin_high_async(pin).await?) + pub async fn is_pin_low_async(&mut self, port: Port, pin: Pin) -> Result> { + Ok(!self.is_pin_high_async(port, pin).await?) } /// Set an output pin to high state (async version) /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub async fn set_pin_high_async(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { - let port = pin.port(); + pub async fn set_pin_high_async(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { let bit = pin.bit(); - if port == 0 { - self.output_port_0() - .modify_async(|r| match bit { - 0 => r.set_o_0_0(true), - 1 => r.set_o_0_1(true), - 2 => r.set_o_0_2(true), - 3 => r.set_o_0_3(true), - 4 => r.set_o_0_4(true), - 5 => r.set_o_0_5(true), - 6 => r.set_o_0_6(true), - 7 => r.set_o_0_7(true), - _ => unreachable!(), - }) - .await - } else { - self.output_port_1() - .modify_async(|r| match bit { - 0 => r.set_o_1_0(true), - 1 => r.set_o_1_1(true), - 2 => r.set_o_1_2(true), - 3 => r.set_o_1_3(true), - 4 => r.set_o_1_4(true), - 5 => r.set_o_1_5(true), - 6 => r.set_o_1_6(true), - 7 => r.set_o_1_7(true), - _ => unreachable!(), - }) - .await + match port { + Port::Port0 => { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(true), + 1 => r.set_o_0_1(true), + 2 => r.set_o_0_2(true), + 3 => r.set_o_0_3(true), + 4 => r.set_o_0_4(true), + 5 => r.set_o_0_5(true), + 6 => r.set_o_0_6(true), + 7 => r.set_o_0_7(true), + _ => unreachable!(), + }) + .await + } + Port::Port1 => { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(true), + 1 => r.set_o_1_1(true), + 2 => r.set_o_1_2(true), + 3 => r.set_o_1_3(true), + 4 => r.set_o_1_4(true), + 5 => r.set_o_1_5(true), + 6 => r.set_o_1_6(true), + 7 => r.set_o_1_7(true), + _ => unreachable!(), + }) + .await + } } } @@ -603,38 +581,40 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub async fn set_pin_low_async(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { - let port = pin.port(); + pub async fn set_pin_low_async(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { let bit = pin.bit(); - if port == 0 { - self.output_port_0() - .modify_async(|r| match bit { - 0 => r.set_o_0_0(false), - 1 => r.set_o_0_1(false), - 2 => r.set_o_0_2(false), - 3 => r.set_o_0_3(false), - 4 => r.set_o_0_4(false), - 5 => r.set_o_0_5(false), - 6 => r.set_o_0_6(false), - 7 => r.set_o_0_7(false), - _ => unreachable!(), - }) - .await - } else { - self.output_port_1() - .modify_async(|r| match bit { - 0 => r.set_o_1_0(false), - 1 => r.set_o_1_1(false), - 2 => r.set_o_1_2(false), - 3 => r.set_o_1_3(false), - 4 => r.set_o_1_4(false), - 5 => r.set_o_1_5(false), - 6 => r.set_o_1_6(false), - 7 => r.set_o_1_7(false), - _ => unreachable!(), - }) - .await + match port { + Port::Port0 => { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(false), + 1 => r.set_o_0_1(false), + 2 => r.set_o_0_2(false), + 3 => r.set_o_0_3(false), + 4 => r.set_o_0_4(false), + 5 => r.set_o_0_5(false), + 6 => r.set_o_0_6(false), + 7 => r.set_o_0_7(false), + _ => unreachable!(), + }) + .await + } + Port::Port1 => { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(false), + 1 => r.set_o_1_1(false), + 2 => r.set_o_1_2(false), + 3 => r.set_o_1_3(false), + 4 => r.set_o_1_4(false), + 5 => r.set_o_1_5(false), + 6 => r.set_o_1_6(false), + 7 => r.set_o_1_7(false), + _ => unreachable!(), + }) + .await + } } } @@ -642,38 +622,40 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub async fn toggle_pin_async(&mut self, pin: Pin) -> Result<(), Pcal6416aError> { - let port = pin.port(); + pub async fn toggle_pin_async(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { let bit = pin.bit(); - if port == 0 { - self.output_port_0() - .modify_async(|r| match bit { - 0 => r.set_o_0_0(!r.o_0_0()), - 1 => r.set_o_0_1(!r.o_0_1()), - 2 => r.set_o_0_2(!r.o_0_2()), - 3 => r.set_o_0_3(!r.o_0_3()), - 4 => r.set_o_0_4(!r.o_0_4()), - 5 => r.set_o_0_5(!r.o_0_5()), - 6 => r.set_o_0_6(!r.o_0_6()), - 7 => r.set_o_0_7(!r.o_0_7()), - _ => unreachable!(), - }) - .await - } else { - self.output_port_1() - .modify_async(|r| match bit { - 0 => r.set_o_1_0(!r.o_1_0()), - 1 => r.set_o_1_1(!r.o_1_1()), - 2 => r.set_o_1_2(!r.o_1_2()), - 3 => r.set_o_1_3(!r.o_1_3()), - 4 => r.set_o_1_4(!r.o_1_4()), - 5 => r.set_o_1_5(!r.o_1_5()), - 6 => r.set_o_1_6(!r.o_1_6()), - 7 => r.set_o_1_7(!r.o_1_7()), - _ => unreachable!(), - }) - .await + match port { + Port::Port0 => { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(!r.o_0_0()), + 1 => r.set_o_0_1(!r.o_0_1()), + 2 => r.set_o_0_2(!r.o_0_2()), + 3 => r.set_o_0_3(!r.o_0_3()), + 4 => r.set_o_0_4(!r.o_0_4()), + 5 => r.set_o_0_5(!r.o_0_5()), + 6 => r.set_o_0_6(!r.o_0_6()), + 7 => r.set_o_0_7(!r.o_0_7()), + _ => unreachable!(), + }) + .await + } + Port::Port1 => { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(!r.o_1_0()), + 1 => r.set_o_1_1(!r.o_1_1()), + 2 => r.set_o_1_2(!r.o_1_2()), + 3 => r.set_o_1_3(!r.o_1_3()), + 4 => r.set_o_1_4(!r.o_1_4()), + 5 => r.set_o_1_5(!r.o_1_5()), + 6 => r.set_o_1_6(!r.o_1_6()), + 7 => r.set_o_1_7(!r.o_1_7()), + _ => unreachable!(), + }) + .await + } } } @@ -681,18 +663,20 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub async fn is_pin_set_high_async(&mut self, pin: Pin) -> Result> { - let port = pin.port(); + pub async fn is_pin_set_high_async(&mut self, port: Port, pin: Pin) -> Result> { let bit = pin.bit(); - let value = if port == 0 { - let reg = self.output_port_0().read_async().await?; - let reg: [u8; 1] = reg.into(); - reg[0] & (1 << bit) != 0 - } else { - let reg = self.output_port_1().read_async().await?; - let reg: [u8; 1] = reg.into(); - reg[0] & (1 << bit) != 0 + let value = match port { + Port::Port0 => { + let reg = self.output_port_0().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + Port::Port1 => { + let reg = self.output_port_1().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } }; Ok(value) @@ -702,8 +686,8 @@ impl Device> { /// # Errors /// /// Will return `Err` if underlying I2C bus operation fails - pub async fn is_pin_set_low_async(&mut self, pin: Pin) -> Result> { - Ok(!self.is_pin_set_high_async(pin).await?) + pub async fn is_pin_set_low_async(&mut self, port: Port, pin: Pin) -> Result> { + Ok(!self.is_pin_set_high_async(port, pin).await?) } } @@ -741,22 +725,22 @@ impl [IoPin<'_, I2c, M>; 16] { [ - IoPin::new(Pin::Pin0, &self.device), - IoPin::new(Pin::Pin1, &self.device), - IoPin::new(Pin::Pin2, &self.device), - IoPin::new(Pin::Pin3, &self.device), - IoPin::new(Pin::Pin4, &self.device), - IoPin::new(Pin::Pin5, &self.device), - IoPin::new(Pin::Pin6, &self.device), - IoPin::new(Pin::Pin7, &self.device), - IoPin::new(Pin::Pin8, &self.device), - IoPin::new(Pin::Pin9, &self.device), - IoPin::new(Pin::Pin10, &self.device), - IoPin::new(Pin::Pin11, &self.device), - IoPin::new(Pin::Pin12, &self.device), - IoPin::new(Pin::Pin13, &self.device), - IoPin::new(Pin::Pin14, &self.device), - IoPin::new(Pin::Pin15, &self.device), + IoPin::new(Port::Port0, Pin::Pin0, &self.device), + IoPin::new(Port::Port0, Pin::Pin1, &self.device), + IoPin::new(Port::Port0, Pin::Pin2, &self.device), + IoPin::new(Port::Port0, Pin::Pin3, &self.device), + IoPin::new(Port::Port0, Pin::Pin4, &self.device), + IoPin::new(Port::Port0, Pin::Pin5, &self.device), + IoPin::new(Port::Port0, Pin::Pin6, &self.device), + IoPin::new(Port::Port0, Pin::Pin7, &self.device), + IoPin::new(Port::Port1, Pin::Pin0, &self.device), + IoPin::new(Port::Port1, Pin::Pin1, &self.device), + IoPin::new(Port::Port1, Pin::Pin2, &self.device), + IoPin::new(Port::Port1, Pin::Pin3, &self.device), + IoPin::new(Port::Port1, Pin::Pin4, &self.device), + IoPin::new(Port::Port1, Pin::Pin5, &self.device), + IoPin::new(Port::Port1, Pin::Pin6, &self.device), + IoPin::new(Port::Port1, Pin::Pin7, &self.device), ] } } @@ -1628,8 +1612,8 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); - assert!(dev.is_pin_high(Pin::Pin0).unwrap()); - assert!(dev.is_pin_high(Pin::Pin15).unwrap()); + assert!(dev.is_pin_high(Port::Port0, Pin::Pin0).unwrap()); + assert!(dev.is_pin_high(Port::Port1, Pin::Pin7).unwrap()); dev.interface.i2cbus.done(); } @@ -1646,8 +1630,8 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); - assert!(dev.is_pin_low(Pin::Pin0).unwrap()); - assert!(dev.is_pin_low(Pin::Pin15).unwrap()); + assert!(dev.is_pin_low(Port::Port0, Pin::Pin0).unwrap()); + assert!(dev.is_pin_low(Port::Port1, Pin::Pin7).unwrap()); dev.interface.i2cbus.done(); } @@ -1659,7 +1643,7 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); - assert!(dev.is_pin_high(Pin::Pin15).unwrap()); + assert!(dev.is_pin_high(Port::Port1, Pin::Pin7).unwrap()); dev.interface.i2cbus.done(); } @@ -1678,8 +1662,8 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); - dev.set_pin_high(Pin::Pin0).unwrap(); - dev.set_pin_high(Pin::Pin15).unwrap(); + dev.set_pin_high(Port::Port0, Pin::Pin0).unwrap(); + dev.set_pin_high(Port::Port1, Pin::Pin7).unwrap(); dev.interface.i2cbus.done(); } @@ -1698,8 +1682,8 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); - dev.set_pin_low(Pin::Pin0).unwrap(); - dev.set_pin_low(Pin::Pin15).unwrap(); + dev.set_pin_low(Port::Port0, Pin::Pin0).unwrap(); + dev.set_pin_low(Port::Port1, Pin::Pin7).unwrap(); dev.interface.i2cbus.done(); } @@ -1714,7 +1698,7 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); - dev.set_pin_high(Pin::Pin15).unwrap(); + dev.set_pin_high(Port::Port1, Pin::Pin7).unwrap(); dev.interface.i2cbus.done(); } @@ -1733,8 +1717,8 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); - dev.toggle_pin(Pin::Pin0).unwrap(); - dev.toggle_pin(Pin::Pin15).unwrap(); + dev.toggle_pin(Port::Port0, Pin::Pin0).unwrap(); + dev.toggle_pin(Port::Port1, Pin::Pin7).unwrap(); dev.interface.i2cbus.done(); } @@ -1751,8 +1735,8 @@ mod tests { addr_pin: AddrPinState::Low, i2cbus, }); - assert!(dev.is_pin_set_high(Pin::Pin0).unwrap()); - assert!(dev.is_pin_set_high(Pin::Pin15).unwrap()); + assert!(dev.is_pin_set_high(Port::Port0, Pin::Pin0).unwrap()); + assert!(dev.is_pin_set_high(Port::Port1, Pin::Pin7).unwrap()); dev.interface.i2cbus.done(); } @@ -1771,9 +1755,9 @@ mod tests { i2cbus, }); // Set multiple pins without borrowing conflicts - dev.set_pin_high(Pin::Pin0).unwrap(); - dev.set_pin_high(Pin::Pin1).unwrap(); - assert!(dev.is_pin_high(Pin::Pin7).unwrap()); + dev.set_pin_high(Port::Port0, Pin::Pin0).unwrap(); + dev.set_pin_high(Port::Port0, Pin::Pin1).unwrap(); + assert!(dev.is_pin_high(Port::Port0, Pin::Pin7).unwrap()); dev.interface.i2cbus.done(); } @@ -1834,9 +1818,20 @@ mod tests { embassy_sync::blocking_mutex::raw::NoopRawMutex, >; 16] = dev.split(); - // Verify all 16 pins have correct numbers (0-15) - for i in 0..16 { + // Verify all 16 pins have correct numbers (0-7 per port) + for i in 0..8 { assert_eq!(pins[i].number(), i as u8, "Pin at index {} should have number {}", i, i); + assert_eq!(pins[i].port(), Port::Port0, "Pin at index {} should be on Port 0", i); + } + for i in 8..16 { + assert_eq!( + pins[i].number(), + (i - 8) as u8, + "Pin at index {} should have number {}", + i, + i - 8 + ); + assert_eq!(pins[i].port(), Port::Port1, "Pin at index {} should be on Port 1", i); } // Verify Pin enum values for all pins @@ -1848,14 +1843,20 @@ mod tests { assert_eq!(pins[5].pin(), Pin::Pin5); assert_eq!(pins[6].pin(), Pin::Pin6); assert_eq!(pins[7].pin(), Pin::Pin7); - assert_eq!(pins[8].pin(), Pin::Pin8); - assert_eq!(pins[9].pin(), Pin::Pin9); - assert_eq!(pins[10].pin(), Pin::Pin10); - assert_eq!(pins[11].pin(), Pin::Pin11); - assert_eq!(pins[12].pin(), Pin::Pin12); - assert_eq!(pins[13].pin(), Pin::Pin13); - assert_eq!(pins[14].pin(), Pin::Pin14); - assert_eq!(pins[15].pin(), Pin::Pin15); + assert_eq!(pins[8].pin(), Pin::Pin0); + assert_eq!(pins[9].pin(), Pin::Pin1); + assert_eq!(pins[10].pin(), Pin::Pin2); + assert_eq!(pins[11].pin(), Pin::Pin3); + assert_eq!(pins[12].pin(), Pin::Pin4); + assert_eq!(pins[13].pin(), Pin::Pin5); + assert_eq!(pins[14].pin(), Pin::Pin6); + assert_eq!(pins[15].pin(), Pin::Pin7); + + // Verify Port enum values + assert_eq!(pins[0].port(), Port::Port0); + assert_eq!(pins[7].port(), Port::Port0); + assert_eq!(pins[8].port(), Port::Port1); + assert_eq!(pins[15].port(), Port::Port1); } dev.device.lock().await.interface.i2cbus.done(); From 166100205a106e36dbd99e50d4089aaf2dbc7eee Mon Sep 17 00:00:00 2001 From: Jerry Xie Date: Sun, 22 Feb 2026 12:46:12 -0600 Subject: [PATCH 09/10] chore: remove invalid SAFETY comments `// SAFETY:` is reserved for `unsafe` blocks. These methods use safe async mutex locks. --- src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e8eed0f..b392e86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -233,7 +233,6 @@ impl Result> { - // SAFETY: This is safe because pin operations are atomic and complete immediately self.device.lock().await.is_pin_high_async(self.port, self.pin).await } @@ -250,7 +249,6 @@ impl Result<(), Pcal6416aError> { - // SAFETY: This is safe because pin operations are atomic and complete immediately self.device.lock().await.set_pin_high_async(self.port, self.pin).await } @@ -259,7 +257,6 @@ impl Result<(), Pcal6416aError> { - // SAFETY: This is safe because pin operations are atomic and complete immediately self.device.lock().await.set_pin_low_async(self.port, self.pin).await } @@ -268,7 +265,6 @@ impl Result<(), Pcal6416aError> { - // SAFETY: This is safe because pin operations are atomic and complete immediately self.device.lock().await.toggle_pin_async(self.port, self.pin).await } @@ -277,7 +273,6 @@ impl Result> { - // SAFETY: This is safe because pin operations are atomic and complete immediately self.device .lock() .await From c22c561920679ba64a6d7d7f17985cca086bc43c Mon Sep 17 00:00:00 2001 From: Jerry Xie Date: Sun, 22 Feb 2026 12:57:33 -0600 Subject: [PATCH 10/10] docs: add SharedDevice docs, make field private - Add doc comments to SharedDevice struct and ::new() - Make SharedDevice.device field private --- src/lib.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index b392e86..b903658 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,8 +55,14 @@ device_driver::create_device!( manifest: "device.yaml" ); +/// A shared, mutex-protected wrapper around a [`Device`] for concurrent access. +/// +/// This type wraps a [`Device`] inside an [`embassy_sync::mutex::Mutex`], allowing +/// multiple async tasks to access the same PCAL6416A device concurrently with +/// synchronized I2C register access. Use [`SharedDevice::split`] to obtain +/// individual [`IoPin`] instances that can be passed to different tasks. pub struct SharedDevice { - pub device: embassy_sync::mutex::Mutex>>, + device: embassy_sync::mutex::Mutex>>, } impl device_driver::AsyncRegisterInterface for Pcal6416aDevice { @@ -687,6 +693,10 @@ impl Device> { } impl SharedDevice { + /// Create a new `SharedDevice` from a [`Device`] instance. + /// + /// The device is wrapped in a mutex to enable safe shared access + /// from multiple [`IoPin`] instances. pub fn new(device: Device>) -> Self { Self { device: embassy_sync::mutex::Mutex::new(device),