diff --git a/src/devices/Pca95x4/Pca95x4.cs b/src/devices/Pca95x4/Pca95x4.cs
index 7bf9192eba..3f681ff25c 100644
--- a/src/devices/Pca95x4/Pca95x4.cs
+++ b/src/devices/Pca95x4/Pca95x4.cs
@@ -4,14 +4,25 @@
using System;
using System.Device.Gpio;
using System.Device.I2c;
+using System.Threading;
namespace Iot.Device.Pca95x4
{
///
/// A general purpose parallel I/O expansion for I2C applications.
///
- public class Pca95x4 : IDisposable
+ ///
+ /// This binding derives from so the expander pins can be accessed
+ /// through a standard (for example new GpioController(pca95x4)),
+ /// in addition to the lower level register access methods exposed directly on this type.
+ ///
+ public class Pca95x4 : GpioDriver
{
+ ///
+ /// The number of GPIO pins provided by the PCA95x4.
+ ///
+ private const int PinCountConst = 8;
+
private readonly int? _interrupt;
private I2cDevice _i2cDevice;
private GpioController? _controller = null;
@@ -164,7 +175,7 @@ public void InvertInputRegisterPolarity(bool invert)
public void InvertInputRegisterBitPolarity(int bitNumber, bool invert) => WriteBit(Register.PolarityInversion, bitNumber, invert);
///
- public void Dispose()
+ protected override void Dispose(bool disposing)
{
_i2cDevice?.Dispose();
_i2cDevice = null!;
@@ -173,6 +184,60 @@ public void Dispose()
_controller?.Dispose();
_controller = null;
}
+
+ base.Dispose(disposing);
}
+
+ ///
+ protected override int PinCount => PinCountConst;
+
+ ///
+ protected override void OpenPin(int pinNumber) => ValidateBitNumber(pinNumber);
+
+ ///
+ protected override void ClosePin(int pinNumber)
+ {
+ // No hardware action required, the pin state is kept in the device registers.
+ }
+
+ ///
+ protected override void SetPinMode(int pinNumber, PinMode mode)
+ {
+ if (!IsPinModeSupported(pinNumber, mode))
+ {
+ throw new ArgumentException($"The pin mode {mode} is not supported.", nameof(mode));
+ }
+
+ // A set bit in the Configuration register enables the pin as an input, a cleared bit as an output.
+ WriteBit(Register.Configuration, pinNumber, mode == PinMode.Input);
+ }
+
+ ///
+ protected override PinMode GetPinMode(int pinNumber) =>
+ ReadBit(Register.Configuration, pinNumber) ? PinMode.Input : PinMode.Output;
+
+ ///
+ protected override bool IsPinModeSupported(int pinNumber, PinMode mode) =>
+ mode == PinMode.Input || mode == PinMode.Output;
+
+ ///
+ protected override PinValue Read(int pinNumber) =>
+ ReadBit(Register.InputPort, pinNumber) ? PinValue.High : PinValue.Low;
+
+ ///
+ protected override void Write(int pinNumber, PinValue value) =>
+ WriteBit(Register.OutputPort, pinNumber, value == PinValue.High);
+
+ ///
+ protected override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, CancellationToken cancellationToken) =>
+ throw new NotImplementedException("Event-based GPIO operations are not supported by the PCA95x4 driver.");
+
+ ///
+ protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback) =>
+ throw new NotImplementedException("Event-based GPIO operations are not supported by the PCA95x4 driver.");
+
+ ///
+ protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) =>
+ throw new NotImplementedException("Event-based GPIO operations are not supported by the PCA95x4 driver.");
}
}
diff --git a/src/devices/Pca95x4/Pca95x4.sln b/src/devices/Pca95x4/Pca95x4.sln
index 2e322bc23b..0a9fb322e6 100644
--- a/src/devices/Pca95x4/Pca95x4.sln
+++ b/src/devices/Pca95x4/Pca95x4.sln
@@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pca95x4.Samples", "samples\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pca95x4", "Pca95x4.csproj", "{B6233788-8F94-4193-9285-C935748E8125}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pca95x4.Tests", "tests\Pca95x4.Tests.csproj", "{8E3ADC28-07F1-4322-B174-36648E26F375}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,9 +22,6 @@ Global
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3532B861-27D2-4866-84C1-EC19AFB0EEF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3532B861-27D2-4866-84C1-EC19AFB0EEF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
@@ -46,6 +47,21 @@ Global
{B6233788-8F94-4193-9285-C935748E8125}.Release|x64.Build.0 = Release|Any CPU
{B6233788-8F94-4193-9285-C935748E8125}.Release|x86.ActiveCfg = Release|Any CPU
{B6233788-8F94-4193-9285-C935748E8125}.Release|x86.Build.0 = Release|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Debug|x64.Build.0 = Debug|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Debug|x86.Build.0 = Debug|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Release|x64.ActiveCfg = Release|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Release|x64.Build.0 = Release|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Release|x86.ActiveCfg = Release|Any CPU
+ {8E3ADC28-07F1-4322-B174-36648E26F375}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3532B861-27D2-4866-84C1-EC19AFB0EEF1} = {E36E35C8-844F-48E9-AFB8-10A1D40AEC51}
diff --git a/src/devices/Pca95x4/README.md b/src/devices/Pca95x4/README.md
index 3d67b2a485..7f415606aa 100644
--- a/src/devices/Pca95x4/README.md
+++ b/src/devices/Pca95x4/README.md
@@ -69,6 +69,25 @@ data = pca95x4.Read(Register.InputPort);
Console.WriteLine($"Input Register: 0x{data:X2}");
```
+### Use as a GpioController
+
+`Pca95x4` derives from `GpioDriver`, so its pins can also be accessed through a standard `GpioController`. This provides consistent access alongside on-board GPIO pins.
+
+```csharp
+I2cConnectionSettings i2cConnectionSettings = new(1, 0x38);
+I2cDevice i2cDevice = I2cDevice.Create(i2cConnectionSettings);
+Pca95x4 pca95x4 = new Pca95x4(i2cDevice);
+GpioController controller = new GpioController(pca95x4);
+
+// Drive pin 0 as an output.
+controller.OpenPin(0, PinMode.Output);
+controller.Write(0, PinValue.High);
+
+// Read pin 1 as an input.
+controller.OpenPin(1, PinMode.Input);
+PinValue value = controller.Read(1);
+```
+
## Binding Notes
PCA9534/PCA9554 and PCA9534A/PCA9554A are identical except for a few differences.
diff --git a/src/devices/Pca95x4/samples/Program.cs b/src/devices/Pca95x4/samples/Program.cs
index 7bb95ad41f..134ee5f607 100644
--- a/src/devices/Pca95x4/samples/Program.cs
+++ b/src/devices/Pca95x4/samples/Program.cs
@@ -14,6 +14,7 @@
using Pca95x4 pca95x4 = GetPca95x4Device();
////CycleOutputBits(pca95x4);
////ReadInputPort(pca95x4);
+////UseAsGpioController(pca95x4);
CheckInputRegisterPolarityInversion(pca95x4);
Pca95x4 GetPca95x4Device()
@@ -43,6 +44,25 @@ void ReadInputPort(Pca95x4 pca95x4)
Console.WriteLine($"Input Port: 0x{data:X2}");
}
+void UseAsGpioController(Pca95x4 pca95x4)
+{
+ // Pca95x4 derives from GpioDriver, so it can be used through a standard GpioController.
+ using GpioController controller = new(pca95x4);
+
+ controller.OpenPin(0, PinMode.Output);
+ controller.OpenPin(1, PinMode.Input);
+
+ for (int i = 0; i < 4; i++)
+ {
+ controller.Write(0, PinValue.High);
+ Console.WriteLine($"Pin 1: {controller.Read(1)}");
+ Thread.Sleep(500);
+ controller.Write(0, PinValue.Low);
+ Console.WriteLine($"Pin 1: {controller.Read(1)}");
+ Thread.Sleep(500);
+ }
+}
+
void CheckInputRegisterPolarityInversion(Pca95x4 pca95x4)
{
pca95x4.Write(Register.Configuration, 0xFF); // Make all inputs.
diff --git a/src/devices/Pca95x4/tests/Pca95x4.Tests.csproj b/src/devices/Pca95x4/tests/Pca95x4.Tests.csproj
new file mode 100644
index 0000000000..95aa5d2bca
--- /dev/null
+++ b/src/devices/Pca95x4/tests/Pca95x4.Tests.csproj
@@ -0,0 +1,10 @@
+
+
+ $(DefaultTestTfms)
+ false
+ false
+
+
+
+
+
diff --git a/src/devices/Pca95x4/tests/Pca95x4Test.cs b/src/devices/Pca95x4/tests/Pca95x4Test.cs
new file mode 100644
index 0000000000..5b18ef57e3
--- /dev/null
+++ b/src/devices/Pca95x4/tests/Pca95x4Test.cs
@@ -0,0 +1,174 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Device.Gpio;
+using System.Device.I2c;
+using Xunit;
+
+namespace Iot.Device.Pca95x4.Tests
+{
+ public class Pca95x4Test
+ {
+ [Fact]
+ public void PinCountIsEight()
+ {
+ using Pca95x4Chip chip = new Pca95x4Chip();
+ using Pca95x4 device = new Pca95x4(chip);
+ using GpioController controller = new GpioController(device);
+ Assert.Equal(8, controller.PinCount);
+ }
+
+ [Theory]
+ [InlineData(PinMode.Input)]
+ [InlineData(PinMode.Output)]
+ public void SupportedPinModes(PinMode mode)
+ {
+ using Pca95x4Chip chip = new Pca95x4Chip();
+ using Pca95x4 device = new Pca95x4(chip);
+ using GpioController controller = new GpioController(device);
+ Assert.True(controller.IsPinModeSupported(0, mode));
+ }
+
+ [Theory]
+ [InlineData(PinMode.InputPullUp)]
+ [InlineData(PinMode.InputPullDown)]
+ public void UnsupportedPinModes(PinMode mode)
+ {
+ using Pca95x4Chip chip = new Pca95x4Chip();
+ using Pca95x4 device = new Pca95x4(chip);
+ using GpioController controller = new GpioController(device);
+ Assert.False(controller.IsPinModeSupported(0, mode));
+ }
+
+ [Fact]
+ public void SetPinModeUpdatesConfigurationRegister()
+ {
+ using Pca95x4Chip chip = new Pca95x4Chip();
+ using Pca95x4 device = new Pca95x4(chip);
+ using GpioController controller = new GpioController(device);
+
+ controller.OpenPin(2, PinMode.Output);
+ // A cleared configuration bit designates an output.
+ Assert.Equal(0, chip.Registers[(int)Register.Configuration] & (1 << 2));
+ Assert.Equal(PinMode.Output, controller.GetPinMode(2));
+
+ controller.SetPinMode(2, PinMode.Input);
+ // A set configuration bit designates an input.
+ Assert.NotEqual(0, chip.Registers[(int)Register.Configuration] & (1 << 2));
+ Assert.Equal(PinMode.Input, controller.GetPinMode(2));
+ }
+
+ [Fact]
+ public void WriteUpdatesOutputRegister()
+ {
+ using Pca95x4Chip chip = new Pca95x4Chip();
+ using Pca95x4 device = new Pca95x4(chip);
+ using GpioController controller = new GpioController(device);
+
+ controller.OpenPin(5, PinMode.Output);
+ controller.Write(5, PinValue.High);
+ Assert.NotEqual(0, chip.Registers[(int)Register.OutputPort] & (1 << 5));
+
+ controller.Write(5, PinValue.Low);
+ Assert.Equal(0, chip.Registers[(int)Register.OutputPort] & (1 << 5));
+ }
+
+ [Fact]
+ public void ReadReflectsInputRegister()
+ {
+ using Pca95x4Chip chip = new Pca95x4Chip();
+ using Pca95x4 device = new Pca95x4(chip);
+ using GpioController controller = new GpioController(device);
+
+ controller.OpenPin(3, PinMode.Input);
+
+ chip.Registers[(int)Register.InputPort] = 1 << 3;
+ Assert.Equal(PinValue.High, controller.Read(3));
+
+ chip.Registers[(int)Register.InputPort] = 0x00;
+ Assert.Equal(PinValue.Low, controller.Read(3));
+ }
+
+ [Fact]
+ public void TogglingFlipsOutput()
+ {
+ using Pca95x4Chip chip = new Pca95x4Chip();
+ using Pca95x4 device = new Pca95x4(chip);
+ using GpioController controller = new GpioController(device);
+
+ controller.OpenPin(1, PinMode.Output);
+ controller.Write(1, PinValue.Low);
+ controller.Toggle(1);
+ Assert.NotEqual(0, chip.Registers[(int)Register.OutputPort] & (1 << 1));
+ controller.Toggle(1);
+ Assert.Equal(0, chip.Registers[(int)Register.OutputPort] & (1 << 1));
+ }
+
+ [Fact]
+ public void RegisterApiStillWorks()
+ {
+ using Pca95x4Chip chip = new Pca95x4Chip();
+ using Pca95x4 device = new Pca95x4(chip);
+
+ device.Write(Register.OutputPort, 0xAA);
+ Assert.Equal(0xAA, device.Read(Register.OutputPort));
+ Assert.True(device.ReadBit(Register.OutputPort, 1));
+ Assert.False(device.ReadBit(Register.OutputPort, 0));
+ }
+
+ ///
+ /// Simple in-memory mock that mimics the four PCA95x4 registers.
+ ///
+ private sealed class Pca95x4Chip : I2cDevice
+ {
+ private readonly byte[] _registers = new byte[4];
+ private int _address;
+
+ public Pca95x4Chip()
+ {
+ // At reset the I/Os are configured as inputs.
+ _registers[(int)Register.Configuration] = 0xFF;
+ }
+
+ public byte[] Registers => _registers;
+
+ public override I2cConnectionSettings ConnectionSettings => new I2cConnectionSettings(1, 0x38);
+
+ public override void WriteByte(byte value) => _address = value;
+
+ public override byte ReadByte() => _registers[_address];
+
+ public override void Write(ReadOnlySpan buffer)
+ {
+ _address = buffer[0];
+ if (buffer.Length > 1)
+ {
+ _registers[_address] = buffer[1];
+
+ // On real hardware the Input Port reflects the actual pin levels. Pins configured
+ // as outputs (a cleared Configuration bit) are driven by the Output Port, while
+ // pins configured as inputs (a set Configuration bit) keep their external level.
+ if (_address == (int)Register.OutputPort)
+ {
+ byte configuration = _registers[(int)Register.Configuration];
+ byte input = _registers[(int)Register.InputPort];
+ _registers[(int)Register.InputPort] =
+ (byte)((input & configuration) | (buffer[1] & ~configuration));
+ }
+ }
+ }
+
+ public override void Read(Span buffer)
+ {
+ for (int i = 0; i < buffer.Length; i++)
+ {
+ buffer[i] = _registers[_address];
+ }
+ }
+
+ public override void WriteRead(ReadOnlySpan writeBuffer, Span readBuffer) =>
+ throw new NotImplementedException();
+ }
+ }
+}