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(); + } + } +}