Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions src/devices/Pca95x4/Pca95x4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@
using System;
using System.Device.Gpio;
using System.Device.I2c;
using System.Threading;

namespace Iot.Device.Pca95x4
{
/// <summary>
/// A general purpose parallel I/O expansion for I2C applications.
/// </summary>
public class Pca95x4 : IDisposable
/// <remarks>
/// This binding derives from <see cref="GpioDriver"/> so the expander pins can be accessed
/// through a standard <see cref="GpioController"/> (for example <c>new GpioController(pca95x4)</c>),
/// in addition to the lower level register access methods exposed directly on this type.
/// </remarks>
public class Pca95x4 : GpioDriver
{
/// <summary>
/// The number of GPIO pins provided by the PCA95x4.
/// </summary>
private const int PinCountConst = 8;

private readonly int? _interrupt;
private I2cDevice _i2cDevice;
private GpioController? _controller = null;
Expand Down Expand Up @@ -164,7 +175,7 @@ public void InvertInputRegisterPolarity(bool invert)
public void InvertInputRegisterBitPolarity(int bitNumber, bool invert) => WriteBit(Register.PolarityInversion, bitNumber, invert);

/// <inheritdoc/>
public void Dispose()
protected override void Dispose(bool disposing)
{
_i2cDevice?.Dispose();
_i2cDevice = null!;
Expand All @@ -173,6 +184,60 @@ public void Dispose()
_controller?.Dispose();
_controller = null;
}

base.Dispose(disposing);
}

/// <inheritdoc/>
protected override int PinCount => PinCountConst;

/// <inheritdoc/>
protected override void OpenPin(int pinNumber) => ValidateBitNumber(pinNumber);

/// <inheritdoc/>
protected override void ClosePin(int pinNumber)
{
// No hardware action required, the pin state is kept in the device registers.
}

/// <inheritdoc/>
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);
}

/// <inheritdoc/>
protected override PinMode GetPinMode(int pinNumber) =>
ReadBit(Register.Configuration, pinNumber) ? PinMode.Input : PinMode.Output;

/// <inheritdoc/>
protected override bool IsPinModeSupported(int pinNumber, PinMode mode) =>
mode == PinMode.Input || mode == PinMode.Output;

/// <inheritdoc/>
protected override PinValue Read(int pinNumber) =>
ReadBit(Register.InputPort, pinNumber) ? PinValue.High : PinValue.Low;

/// <inheritdoc/>
protected override void Write(int pinNumber, PinValue value) =>
WriteBit(Register.OutputPort, pinNumber, value == PinValue.High);

/// <inheritdoc/>
protected override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, CancellationToken cancellationToken) =>
throw new NotImplementedException("Event-based GPIO operations are not supported by the PCA95x4 driver.");

/// <inheritdoc/>
protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback) =>
throw new NotImplementedException("Event-based GPIO operations are not supported by the PCA95x4 driver.");

/// <inheritdoc/>
protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) =>
throw new NotImplementedException("Event-based GPIO operations are not supported by the PCA95x4 driver.");
}
}
22 changes: 19 additions & 3 deletions src/devices/Pca95x4/Pca95x4.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand Down
19 changes: 19 additions & 0 deletions src/devices/Pca95x4/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions src/devices/Pca95x4/samples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Pca95x4 pca95x4 = GetPca95x4Device();
////CycleOutputBits(pca95x4);
////ReadInputPort(pca95x4);
////UseAsGpioController(pca95x4);
CheckInputRegisterPolarityInversion(pca95x4);

Pca95x4 GetPca95x4Device()
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions src/devices/Pca95x4/tests/Pca95x4.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultTestTfms)</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Pca95x4.csproj" />
</ItemGroup>
</Project>
174 changes: 174 additions & 0 deletions src/devices/Pca95x4/tests/Pca95x4Test.cs
Original file line number Diff line number Diff line change
@@ -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));
}

/// <summary>
/// Simple in-memory mock that mimics the four PCA95x4 registers.
/// </summary>
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<byte> 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<byte> buffer)
{
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = _registers[_address];
}
}

public override void WriteRead(ReadOnlySpan<byte> writeBuffer, Span<byte> readBuffer) =>
throw new NotImplementedException();
}
}
}
Loading