diff --git a/src/devices/Tca955x/README.md b/src/devices/Tca955x/README.md
new file mode 100644
index 0000000000..60c6d13d7b
--- /dev/null
+++ b/src/devices/Tca955x/README.md
@@ -0,0 +1,31 @@
+# Tca955x - I/O Expander device family
+
+## Summary
+
+The TCA955X device family provides 8/16-bit, general purpose I/O expansion for I2C. The devices can be configured with polariy invertion and interrupts.
+
+## Device Family
+
+The family contains the TCA9554 (8-bit) and the TCA9555 (16-bit) device. Both devices are compatible with 400kHz Bus speed.
+
+- **TCA9554**: [datasheet](https://www.ti.com/lit/ds/symlink/tca9554.pdf)
+- **TCA9555**: [datasheet](https://www.ti.com/lit/ds/symlink/tca9555.pdf)
+
+## Interrupt support
+
+The `Tca955x` has one interrupt pin. The corresponding pins need to be connected to a master GPIO controller for this feature to work. You can use a GPIO controller around the MCP device to handle everything for you:
+
+```csharp
+// Gpio controller from parent device (eg. Raspberry Pi)
+_gpioController = new GpioController();
+_i2c = I2cDevice.Create(new I2cConnectionSettings(1, Tca955x.DefaultI2cAdress));
+// The "Interrupt" line of the TCA9554 is connected to GPIO input 11 of the Raspi
+_device = new Tca9554(_i2c, 11, _gpioController, false);
+GpioController theDeviceController = new GpioController(_device);
+theDeviceController.OpenPin(1, PinMode.Input);
+theDeviceController.RegisterCallbackForPinValueChangedEvent(1, PinEventTypes.Rising, Callback);
+```
+
+## Binding Notes
+
+The bindings includes an `Tca955x` abstract class and derived for 8-bit `Tca9554` and 16-bit `Tca9555`.
diff --git a/src/devices/Tca955x/Register.cs b/src/devices/Tca955x/Register.cs
new file mode 100644
index 0000000000..bfea767ee7
--- /dev/null
+++ b/src/devices/Tca955x/Register.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Iot.Device.Tca955x
+{
+ ///
+ /// Register for the base class and the 8-Bit device
+ ///
+ public enum Register
+ {
+ ///
+ /// Register Adress for the Inputs P0 - P7
+ /// Only Read Allowed on this Register
+ ///
+ InputPort = 0x00,
+
+ ///
+ /// Register Adress for the Outputs P0 - P7
+ ///
+ OutputPort = 0x01,
+
+ ///
+ /// Register Adress for the Polarity Inversion P0 - P7
+ ///
+ PolarityInversionPort = 0x02,
+
+ ///
+ /// Register Adress for the Configuration P0 - P7
+ ///
+ ConfigurationPort = 0x03,
+ }
+}
diff --git a/src/devices/Tca955x/Tca9554.cs b/src/devices/Tca955x/Tca9554.cs
new file mode 100644
index 0000000000..7bb7b749d4
--- /dev/null
+++ b/src/devices/Tca955x/Tca9554.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Device.Gpio;
+using System.Device.I2c;
+
+namespace Iot.Device.Tca955x
+{
+ ///
+ /// Class for the Tca9554 8-Bit I/O Exander
+ ///
+ public class Tca9554 : Tca955x
+ {
+ ///
+ /// Constructor for the Tca9554 I2C I/O Expander.
+ ///
+ /// The I2C device used for communication
+ /// The input pin number that is connected to the interrupt.
+ /// The controller for the reset and interrupt pins. If not specified, the default controller will be used.
+ /// True to dispose the Gpio Controller.
+ public Tca9554(I2cDevice device, int interrupt = -1, GpioController? gpioController = null, bool shouldDispose = true)
+ : base(device, interrupt, gpioController, shouldDispose)
+ {
+ }
+
+ ///
+ protected override int PinCount => 8;
+
+ ///
+ protected override byte GetByteRegister(int pinNumber, ushort value)
+ {
+ return (byte)(value & 0xFF);
+ }
+
+ ///
+ protected override int GetBitNumber(int pinNumber)
+ {
+ return pinNumber;
+ }
+
+ ///
+ protected override byte GetRegisterIndex(int pinNumber, Register registerType)
+ {
+ // No conversion for 8-Bit devices
+ return (byte)registerType;
+ }
+
+ ///
+ /// Write a byte to the given Register.
+ ///
+ /// The given Register.
+ /// The value to write.
+ public void WriteByte(Register register, byte value) => InternalWriteByte((byte)register, value);
+
+ ///
+ /// Read a byte from the given Register.
+ ///
+ /// The given Register.
+ /// The readed byte from the Register.
+ public byte ReadByte(Register register) => InternalReadByte((byte)register);
+ }
+}
diff --git a/src/devices/Tca955x/Tca9555.cs b/src/devices/Tca955x/Tca9555.cs
new file mode 100644
index 0000000000..28b5ad3499
--- /dev/null
+++ b/src/devices/Tca955x/Tca9555.cs
@@ -0,0 +1,98 @@
+// 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;
+
+namespace Iot.Device.Tca955x
+{
+ ///
+ /// Class for the Tca9555 16-Bit I/O Exander
+ ///
+ public class Tca9555 : Tca955x
+ {
+ ///
+ /// Constructor for the Tca9555 I2C I/O Expander.
+ ///
+ /// The I2C Device the device is connected to. Expect an I2C Adress between 0x20 and 0x27
+ /// The input pin number that is connected to the interrupt.
+ /// The controller for the reset and interrupt pins. If not specified, the default controller will be used.
+ /// True to dispose the Gpio Controller.
+ public Tca9555(I2cDevice device, int interrupt = -1, GpioController? gpioController = null, bool shouldDispose = true)
+ : base(device, interrupt, gpioController, shouldDispose)
+ {
+ }
+
+ ///
+ protected override int PinCount => 16;
+
+ ///
+ protected override byte GetByteRegister(int pinNumber, ushort value)
+ {
+ if (pinNumber >= 0 &&
+ pinNumber <= 7)
+ {
+ return (byte)(value & 0xFF);
+ }
+ else if (pinNumber >= 8 &&
+ pinNumber <= 15)
+ {
+ return (byte)((value >> 8) & 0xFF);
+ }
+
+ return 0;
+ }
+
+ ///
+ protected override int GetBitNumber(int pinNumber)
+ {
+ if (pinNumber >= 0 &&
+ pinNumber <= 7)
+ {
+ return pinNumber;
+ }
+ else if (pinNumber >= 8 &&
+ pinNumber <= 15)
+ {
+ return pinNumber - 8;
+ }
+
+ return 0;
+ }
+
+ ///
+ protected override byte GetRegisterIndex(int pinNumber, Register registerType)
+ {
+ byte register = (byte)registerType;
+ register += (byte)registerType;
+
+ if (pinNumber >= 0 &&
+ pinNumber <= 7)
+ {
+ return register;
+ }
+ else if (pinNumber >= 8 &&
+ pinNumber <= 15)
+ {
+ return ++register;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(pinNumber));
+ }
+
+ ///
+ /// Write a byte to the given Register.
+ ///
+ /// The given Register.
+ /// The value to write.
+ public void WriteByte(Tca9555Register register, byte value) => InternalWriteByte((byte)register, value);
+
+ ///
+ /// Read a byte from the given Register.
+ ///
+ /// The given Register.
+ /// The readed byte from the Register.
+ public byte ReadByte(Tca9555Register register) => InternalReadByte((byte)register);
+ }
+}
diff --git a/src/devices/Tca955x/Tca9555Register.cs b/src/devices/Tca955x/Tca9555Register.cs
new file mode 100644
index 0000000000..98f66edb74
--- /dev/null
+++ b/src/devices/Tca955x/Tca9555Register.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Iot.Device.Tca955x
+{
+ ///
+ /// Register for the 16 Bit Device
+ ///
+ public enum Tca9555Register
+ {
+ ///
+ /// Register Adress for the Inputs P00 - P07
+ /// Only Read Allowed on this Register
+ ///
+ InputPort0 = 0x00,
+
+ ///
+ /// Register Adress for the Inputs P10 - P17
+ /// Only Read Allowed on this Register
+ ///
+ InputPort1 = 0x01,
+
+ ///
+ /// Register Adress for the Outputs P00 - P07
+ ///
+ OutputPort0 = 0x02,
+
+ ///
+ /// Register Adress for the Outputs P10 - P17
+ ///
+ OutputPort1 = 0x03,
+
+ ///
+ /// Register Adress for the Polarity Inversion P00 - P07
+ ///
+ PolarityInversionPort0 = 0x04,
+
+ ///
+ /// Register Adress for the Polarity Inversion P10 - P17
+ ///
+ PolarityInversionPort1 = 0x05,
+
+ ///
+ /// Register Adress for the Configuration P00 - P07
+ ///
+ ConfigurationPort0 = 0x06,
+
+ ///
+ /// Register Adress for the Configuration P10 - P17
+ ///
+ ConfigurationPort1 = 0x06,
+ }
+}
diff --git a/src/devices/Tca955x/Tca955x.cs b/src/devices/Tca955x/Tca955x.cs
new file mode 100644
index 0000000000..4334f0cf79
--- /dev/null
+++ b/src/devices/Tca955x/Tca955x.cs
@@ -0,0 +1,517 @@
+// 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.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Device.Gpio;
+using System.Device.I2c;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace Iot.Device.Tca955x
+{
+ ///
+ /// Base class for the Tca55x I2C I/O Expander
+ ///
+ public abstract class Tca955x : GpioDriver
+ {
+ private readonly int _interrupt;
+ private readonly Dictionary _pinValues = new Dictionary();
+ private readonly ConcurrentDictionary _eventHandlers = new ConcurrentDictionary();
+ private readonly Dictionary _interruptPins = new Dictionary();
+ private readonly Dictionary _interruptLastInputValues = new Dictionary();
+
+ private GpioController? _controller;
+
+ private bool _shouldDispose;
+
+ private I2cDevice _busDevice;
+
+ private object _interruptHandlerLock = new object();
+
+ private ushort _gpioOutputCache;
+
+ ///
+ /// Default Adress of the Tca955X Family.
+ ///
+ public const byte DefaultI2cAdress = 0x20;
+
+ ///
+ /// Constructor for the Tca9555 I2C I/O Expander.
+ ///
+ /// The I2C device used for communication.
+ /// The input pin number that is connected to the interrupt.
+ /// The controller for the reset and interrupt pins. If not specified, the default controller will be used.
+ /// True to dispose the Gpio Controller.
+ protected Tca955x(I2cDevice device, int interrupt = -1, GpioController? gpioController = null, bool shouldDispose = true)
+ {
+ _busDevice = device;
+ _interrupt = interrupt;
+
+ if (_busDevice.ConnectionSettings.DeviceAddress < DefaultI2cAdress ||
+ _busDevice.ConnectionSettings.DeviceAddress > DefaultI2cAdress + 7)
+ {
+ new ArgumentOutOfRangeException(nameof(device), "Adress should be in Range 0x20 to 0x27");
+ }
+
+ if (_interrupt != -1)
+ {
+ _shouldDispose = shouldDispose || gpioController is null;
+ _controller = gpioController ?? new GpioController();
+ if (!_controller.IsPinOpen(_interrupt))
+ {
+ _controller.OpenPin(_interrupt);
+ }
+
+ if (_controller.GetPinMode(_interrupt) != PinMode.Input)
+ {
+ _controller.SetPinMode(interrupt, PinMode.Input);
+ }
+ }
+ }
+
+ ///
+ /// Reads a number of bytes from registers.
+ ///
+ /// The register to read from.
+ /// The buffer to read bytes into.
+ protected void InternalRead(byte register, Span buffer)
+ {
+ // First send write then read.
+ _busDevice.WriteRead(stackalloc byte[1] { register }, buffer);
+ }
+
+ ///
+ /// Writes a number of bytes to registers.
+ ///
+ /// The register address to write to.
+ /// The data to write to the registers.
+ protected void InternalWrite(byte register, byte data)
+ {
+ _busDevice.Write(stackalloc byte[2] { register, data });
+ }
+
+ ///
+ /// Reads byte from the device register
+ ///
+ /// Register to read the value from
+ /// Byte read from the device register
+ protected byte InternalReadByte(byte register)
+ {
+ Span buffer = stackalloc byte[1];
+ InternalRead(register, buffer);
+ return buffer[0];
+ }
+
+ ///
+ /// Write byte to device register
+ ///
+ /// Register to write the value to
+ /// Value to be written to the
+ protected void InternalWriteByte(byte register, byte value)
+ {
+ InternalWrite(register, value);
+ }
+
+ ///
+ /// Read a byte from the given register.
+ ///
+ public byte ReadByte(byte register) => InternalReadByte(register);
+
+ ///
+ /// Write a byte to the given register.
+ ///
+ public void WriteByte(byte register, byte value) => InternalWriteByte(register, value);
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (_shouldDispose)
+ {
+ _controller?.Dispose();
+ _controller = null;
+ }
+
+ _pinValues.Clear();
+ _busDevice?.Dispose();
+ _busDevice = null!;
+ base.Dispose(disposing);
+ }
+
+ ///
+ /// Returns the value of the interrupt pin if configured
+ ///
+ /// Value of interrupt pin
+ protected PinValue ReadInterrupt()
+ {
+ if (_interrupt == -1 || _controller is null)
+ {
+ throw new ArgumentException("No interrupt pin configured.", nameof(_interrupt));
+ }
+
+ return _controller.Read(_interrupt);
+ }
+
+ private byte SetBit(byte data, int bitNumber) => (byte)(data | (1 << bitNumber));
+
+ private byte ClearBit(byte data, int bitNumber) => (byte)(data & ~(1 << bitNumber));
+
+ ///
+ /// Sets a mode to a pin.
+ ///
+ /// The pin number.
+ /// The mode to be set.
+ protected override void SetPinMode(int pinNumber, PinMode mode)
+ {
+ lock (_interruptHandlerLock)
+ {
+ if (mode != PinMode.Input && mode != PinMode.Output && mode != PinMode.InputPullUp)
+ {
+ throw new ArgumentException("The Mcp controller supports the following pin modes: Input, Output and InputPullUp.");
+ }
+
+ byte polarityInversionRegister = GetRegisterIndex(pinNumber, Register.PolarityInversionPort);
+ byte configurationRegister = GetRegisterIndex(pinNumber, Register.ConfigurationPort);
+ ValidatePin(pinNumber);
+
+ byte value;
+ if (mode == PinMode.Output)
+ {
+ value = ClearBit(InternalReadByte(configurationRegister), GetBitNumber(pinNumber));
+ }
+ else
+ {
+ value = SetBit(InternalReadByte(configurationRegister), GetBitNumber(pinNumber));
+ }
+
+ InternalWriteByte(configurationRegister, value);
+
+ byte value2;
+ if (mode == PinMode.InputPullUp)
+ {
+ value2 = SetBit(InternalReadByte(polarityInversionRegister), GetBitNumber(pinNumber));
+ }
+ else
+ {
+ value2 = ClearBit(InternalReadByte(polarityInversionRegister), GetBitNumber(pinNumber));
+ }
+
+ InternalWriteByte(polarityInversionRegister, value2);
+ }
+ }
+
+ ///
+ /// Converts the pin number to the Register byte.
+ ///
+ /// The pin number.
+ /// The register byte.
+ /// The register byte.
+ protected abstract byte GetRegisterIndex(int pinNumber, Register registerType);
+
+ ///
+ /// Converts the pin number to the Bit number.
+ ///
+ /// The pin number.
+ /// The bit position.
+ protected abstract int GetBitNumber(int pinNumber);
+
+ ///
+ /// Mask the right byte from an input based on the pinnumber.
+ ///
+ /// The pin number.
+ /// The reding value of the inputs
+ /// The masked byte of the given value.
+ protected abstract byte GetByteRegister(int pinNumber, ushort value);
+
+ ///
+ /// Reads the value of a pin.
+ ///
+ /// The pin number.
+ /// High or low pin value.
+ protected override PinValue Read(int pinNumber)
+ {
+ lock (_interruptHandlerLock)
+ {
+ ValidatePin(pinNumber);
+ Span pinValuePairs = stackalloc PinValuePair[]
+ {
+ new PinValuePair(pinNumber, default)
+ };
+ Read(pinValuePairs);
+ return _pinValues[pinNumber];
+ }
+ }
+
+ ///
+ protected override void Toggle(int pinNumber) => Write(pinNumber, !_pinValues[pinNumber]);
+
+ ///
+ /// Reads the value of a set of pins
+ ///
+ protected void Read(Span pinValuePairs)
+ {
+ lock (_interruptHandlerLock)
+ {
+ (uint pins, _) = new PinVector32(pinValuePairs);
+ if ((pins >> PinCount) > 0)
+ {
+ ThrowBadPin(nameof(pinValuePairs));
+ }
+
+ for (int i = 0; i < pinValuePairs.Length; i++)
+ {
+ int pin = pinValuePairs[i].PinNumber;
+ byte register = GetRegisterIndex(pin, Register.InputPort);
+ byte result = InternalReadByte(register);
+ pinValuePairs[i] = new PinValuePair(pin, result & (1 << GetBitNumber(pin)));
+ _pinValues[pin] = pinValuePairs[i].PinValue;
+ }
+ }
+ }
+
+ ///
+ /// Writes a value to a pin.
+ ///
+ /// The pin number.
+ /// The value to be written.
+ protected override void Write(int pinNumber, PinValue value)
+ {
+ lock (_interruptHandlerLock)
+ {
+ ValidatePin(pinNumber);
+ Span pinValuePairs = stackalloc PinValuePair[]
+ {
+ new PinValuePair(pinNumber, value)
+ };
+ Write(pinValuePairs);
+ _pinValues[pinNumber] = value;
+ }
+ }
+
+ ///
+ /// Writes values to a set of pins
+ ///
+ protected void Write(ReadOnlySpan pinValuePairs)
+ {
+ lock (_interruptHandlerLock)
+ {
+ (uint mask, uint newBits) = new PinVector32(pinValuePairs);
+ if ((mask >> PinCount) > 0)
+ {
+ ThrowBadPin(nameof(pinValuePairs));
+ }
+
+ ushort cachedValue = _gpioOutputCache;
+ ushort newValue = SetBits(cachedValue, (ushort)newBits, (ushort)mask);
+ if (cachedValue == newValue)
+ {
+ return;
+ }
+
+ _gpioOutputCache = newValue;
+ foreach (PinValuePair pinValuePair in pinValuePairs)
+ {
+ byte register = GetRegisterIndex(pinValuePair.PinNumber, Register.OutputPort);
+ byte value = GetByteRegister(pinValuePair.PinNumber, newValue);
+ InternalWriteByte(register, value);
+ _pinValues[pinValuePair.PinNumber] = pinValuePair.PinValue;
+ }
+ }
+ }
+
+ private ushort SetBits(ushort current, ushort bits, ushort mask)
+ {
+ current &= (ushort)~mask;
+ current |= bits;
+ return current;
+ }
+
+ private void ValidatePin(int pinNumber)
+ {
+ if (pinNumber >= PinCount || pinNumber < 0)
+ {
+ ThrowBadPin(nameof(pinNumber));
+ }
+ }
+
+ private void ThrowBadPin(string argument)
+ {
+ throw new ArgumentOutOfRangeException(argument, $"Only pins {0} through {PinCount - 1} are valid.");
+ }
+
+ ///
+ protected override void OpenPin(int pinNumber)
+ {
+ // No-op
+ if (!_pinValues.ContainsKey(pinNumber))
+ {
+ _pinValues.Add(pinNumber, PinValue.Low);
+ }
+ }
+
+ ///
+ protected override void ClosePin(int pinNumber)
+ {
+ // No-op
+ _pinValues.Remove(pinNumber);
+ }
+
+ ///
+ protected override PinMode GetPinMode(int pinNumber)
+ {
+ ValidatePin(pinNumber);
+
+ // IsBitSet returns true if bitNumber is flipped on in data.
+ bool IsBitSet(byte data, int bitNumber) => (data & (1 << bitNumber)) != 0;
+
+ byte register = GetRegisterIndex(pinNumber, Register.ConfigurationPort);
+ return IsBitSet(InternalReadByte(register), GetBitNumber(pinNumber))
+ ? PinMode.Input
+ : PinMode.Output;
+ }
+
+ private void InterruptHandler(object sender, PinValueChangedEventArgs e)
+ {
+ lock (_interruptHandlerLock)
+ {
+ if (_interruptPins.Count > 0)
+ {
+ foreach (var interruptPin in _interruptPins)
+ {
+ PinValue newValue = Read(interruptPin.Key);
+ PinValue lastValue = _interruptLastInputValues[interruptPin.Key];
+
+ if ((interruptPin.Value.HasFlag(PinEventTypes.Rising) &&
+ lastValue == PinValue.Low &&
+ newValue == PinValue.High) ||
+ (interruptPin.Value.HasFlag(PinEventTypes.Falling) &&
+ lastValue == PinValue.High &&
+ newValue == PinValue.Low))
+ {
+ CallHandlerOnPin(interruptPin.Key, interruptPin.Value);
+ }
+
+ _interruptLastInputValues[interruptPin.Key] = newValue;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Calls the event handler for the given pin, if any.
+ ///
+ /// Pin to call the event handler on (if any exists)
+ /// Non-zero if the value is currently high (therefore assuming the pin value was rising), otherwise zero
+ private void CallHandlerOnPin(int pin, PinEventTypes pinEvent)
+ {
+ if (_eventHandlers.TryGetValue(pin, out var handler))
+ {
+ handler.Invoke(this, new PinValueChangedEventArgs(pinEvent, pin));
+ }
+ }
+
+ ///
+ /// Calls an event handler if the given pin changes.
+ ///
+ /// Pin number of the MCP23xxx
+ /// Whether the handler should trigger on rising, falling or both edges
+ /// The method to call when an interrupt is triggered
+ /// There's no GPIO controller for the master interrupt configured, or no interrupt lines are configured for the
+ /// required port.
+ /// Only one event handler can be registered per pin. Calling this again with a different handler for the same pin replaces the handler
+ protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventType, PinChangeEventHandler callback)
+ {
+ if (_controller == null)
+ {
+ throw new InvalidOperationException("No GPIO controller available. Specify a GPIO controller and the relevant interrupt line numbers in the constructor");
+ }
+
+ if (_interrupt == -1)
+ {
+ throw new InvalidOperationException("No interrupt pin configured");
+ }
+
+ _interruptPins.Add(pinNumber, eventType);
+ _interruptLastInputValues.Add(pinNumber, Read(pinNumber));
+ _controller.RegisterCallbackForPinValueChangedEvent(_interrupt, PinEventTypes.Falling, InterruptHandler);
+
+ _eventHandlers[pinNumber] = callback;
+ }
+
+ ///
+ protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback)
+ {
+ if (_controller == null)
+ {
+ // If we had any callbacks registered, this would have thrown up earlier.
+ throw new InvalidOperationException("No valid GPIO controller defined. And no callbacks registered either.");
+ }
+
+ if (_eventHandlers.TryRemove(pinNumber, out _))
+ {
+ _interruptPins.Remove(pinNumber);
+ _interruptLastInputValues.Remove(pinNumber);
+ _controller.UnregisterCallbackForPinValueChangedEvent(_interrupt, InterruptHandler);
+ }
+ }
+
+ ///
+ protected override int ConvertPinNumberToLogicalNumberingScheme(int pinNumber) => pinNumber;
+
+ ///
+ /// Waits for an event to occur on the given pin.
+ ///
+ /// The pin on which to wait
+ /// The event to wait for (rising, falling or either)
+ /// A timeout token
+ /// The wait result
+ /// This method should only be used on pins that are not otherwise used in event handling, as it clears any
+ /// existing event handlers for the same pin.
+ protected override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes,
+ CancellationToken cancellationToken)
+ {
+ ManualResetEventSlim slim = new ManualResetEventSlim();
+ slim.Reset();
+ PinEventTypes eventTypes1 = PinEventTypes.None;
+ void InternalHandler(object sender, PinValueChangedEventArgs pinValueChangedEventArgs)
+ {
+ if (pinValueChangedEventArgs.PinNumber != pinNumber)
+ {
+ return;
+ }
+
+ if ((pinValueChangedEventArgs.ChangeType & eventTypes) != 0)
+ {
+ slim.Set();
+ }
+
+ eventTypes1 = pinValueChangedEventArgs.ChangeType;
+ }
+
+ AddCallbackForPinValueChangedEvent(pinNumber, eventTypes, InternalHandler);
+ slim.Wait(cancellationToken);
+ RemoveCallbackForPinValueChangedEvent(pinNumber, InternalHandler);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return new WaitForEventResult()
+ {
+ EventTypes = PinEventTypes.None,
+ TimedOut = true
+ };
+ }
+
+ return new WaitForEventResult()
+ {
+ EventTypes = eventTypes1,
+ TimedOut = false
+ };
+ }
+
+ ///
+ protected override bool IsPinModeSupported(int pinNumber, PinMode mode) =>
+ (mode == PinMode.Input || mode == PinMode.Output || mode == PinMode.InputPullUp);
+
+ }
+}
diff --git a/src/devices/Tca955x/Tca955x.csproj b/src/devices/Tca955x/Tca955x.csproj
new file mode 100644
index 0000000000..c50eaf549e
--- /dev/null
+++ b/src/devices/Tca955x/Tca955x.csproj
@@ -0,0 +1,16 @@
+
+
+
+ $(DefaultBindingTfms)
+
+ false
+ 9
+
+
+
+
+
+
+
+
+
diff --git a/src/devices/Tca955x/Tca955x.sln b/src/devices/Tca955x/Tca955x.sln
new file mode 100644
index 0000000000..fd3136b2ac
--- /dev/null
+++ b/src/devices/Tca955x/Tca955x.sln
@@ -0,0 +1,73 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35506.116
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tca955x", "Tca955x.csproj", "{B82C190A-642B-465B-BD3F-DB56FFF22253}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{6A4DE7B1-03F3-4EE0-BF73-A0BAEF88BA2B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tca955x.Samples", "samples\Tca955x.Samples.csproj", "{3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AC41B656-7638-401A-87BB-72D4937BF034}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tca955x.Tests", "tests\Tca955x.Tests.csproj", "{F3BCAFCA-A6B8-4530-B435-36FA238D947A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x64.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Debug|x86.Build.0 = Debug|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x64.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x64.Build.0 = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x86.ActiveCfg = Release|Any CPU
+ {B82C190A-642B-465B-BD3F-DB56FFF22253}.Release|x86.Build.0 = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x64.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Debug|x86.Build.0 = Debug|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x64.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x64.Build.0 = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x86.ActiveCfg = Release|Any CPU
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF}.Release|x86.Build.0 = Release|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Debug|x64.Build.0 = Debug|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Debug|x86.Build.0 = Debug|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x64.ActiveCfg = Release|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x64.Build.0 = Release|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x86.ActiveCfg = Release|Any CPU
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {3CFA13D6-1D29-4C87-B0C1-01A6901A50EF} = {6A4DE7B1-03F3-4EE0-BF73-A0BAEF88BA2B}
+ {F3BCAFCA-A6B8-4530-B435-36FA238D947A} = {AC41B656-7638-401A-87BB-72D4937BF034}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1E19B02E-99EC-4004-960C-57C63C36082D}
+ EndGlobalSection
+EndGlobal
diff --git a/src/devices/Tca955x/category.txt b/src/devices/Tca955x/category.txt
new file mode 100644
index 0000000000..4e639f60b2
--- /dev/null
+++ b/src/devices/Tca955x/category.txt
@@ -0,0 +1 @@
+io-expander
diff --git a/src/devices/Tca955x/samples/README.md b/src/devices/Tca955x/samples/README.md
new file mode 100644
index 0000000000..b5b388a4c9
--- /dev/null
+++ b/src/devices/Tca955x/samples/README.md
@@ -0,0 +1,34 @@
+# Usage of the Tca9554 or Tca9555
+
+## Example 1
+
+Use the Tca9554 or Tca9555 class directly and write to the specific register.
+First, write to the configuration register to define either input or output (a high bit stands for an input).
+Read inputs with the input register or write outputs with the output register.
+
+```csharp
+I2cConnectionSettings i2cConnectionSettings = new(1, 0x20);
+I2cDevice i2cDevice = I2cDevice.Create(i2cConnectionSettings);
+var tca9554 = new Tca9554(i2cDevice);
+tca9554.WriteByte(Register.ConfigurationPort, 0x0F);
+byte readInputs = tca9554.ReadByte(Register.InputPort);
+Console.WriteLine($"Current input state: {readInputs.ToString("X2")}");
+tca9554.WriteByte(Register.OutputPort, 0xF0);
+```
+
+## Example 2
+
+Use the GPIO Controller to open and close pins.
+With the read and write methods, the current state of the pin can be read or written.
+Also, interrupts with a callback can be used.
+
+```csharp
+// Gpio controller from parent device (eg. Raspberry Pi)
+_gpioController = new GpioController();
+_i2c = I2cDevice.Create(new I2cConnectionSettings(1, 0x20));
+// The "Interrupt" line of the TCA9554 is connected to GPIO input 11 of the Raspi
+_device = new Tca9554(_i2c, 11, _gpioController, false);
+GpioController theDeviceController = new GpioController(_device);
+theDeviceController.OpenPin(1, PinMode.Input);
+theDeviceController.RegisterCallbackForPinValueChangedEvent(1, PinEventTypes.Rising, Callback);
+```
diff --git a/src/devices/Tca955x/samples/Tca955x.Sample.cs b/src/devices/Tca955x/samples/Tca955x.Sample.cs
new file mode 100644
index 0000000000..b6c979849b
--- /dev/null
+++ b/src/devices/Tca955x/samples/Tca955x.Sample.cs
@@ -0,0 +1,59 @@
+// 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 System.Device.Spi;
+using System.Threading;
+using Iot.Device.Tca955x;
+
+I2cConnectionSettings i2cConnectionSettings = new(1, 0x20);
+I2cDevice i2cDevice = I2cDevice.Create(i2cConnectionSettings);
+
+var tca9554 = new Tca9554(i2cDevice);
+
+// Use the TCA955X either directly and Read and Write the Register or use an GPIO Controller.
+// Example 1: Use the TXA9554 directly
+
+// Set the first 4 bits to Input the others as Output
+tca9554.WriteByte(Register.ConfigurationPort, 0x0F);
+
+// Reads the full Output Register
+byte readInputs = tca9554.ReadByte(Register.InputPort);
+Console.WriteLine($"Current input state: {readInputs.ToString("X2")}");
+
+// Writes to the full Output Register
+// Set the output to high
+tca9554.WriteByte(Register.OutputPort, 0xF0);
+
+// Example 2: Use the TCA9555 with the GPIO Controller
+// Create an GPIO Controller where the Interrupt Pin connected is.
+GpioController controller = new GpioController();
+
+I2cConnectionSettings i2cConnectionSettings_tca9555 = new(1, Tca955x.DefaultI2cAdress);
+I2cDevice i2cDevice_tca9555 = I2cDevice.Create(i2cConnectionSettings_tca9555);
+var tca9555 = new Tca9555(i2cDevice_tca9555, 4);
+// Create an GPIO Controller which represent the TCA9554
+GpioController tca9554Controller = new GpioController(tca9554);
+tca9554Controller.OpenPin(0, PinMode.Input);
+tca9554Controller.OpenPin(1, PinMode.Output);
+
+Console.WriteLine("Write Pin 1 to High");
+tca9554Controller.Write(1, PinValue.High);
+Thread.Sleep(1000);
+Console.WriteLine("Write Pin 1 to Low");
+tca9554Controller.Write(1, PinValue.Low);
+
+Console.WriteLine($"Current input state on Pin 0: {tca9554Controller.Read(0)}");
+Console.WriteLine("Wait for Pin Event Rising");
+Console.ReadKey();
+
+// Enable Interrupt on pin 0
+controller.RegisterCallbackForPinValueChangedEvent(0, PinEventTypes.Rising, Interrupt);
+
+void Interrupt(object sender, PinValueChangedEventArgs pinValueChangedEventArgs)
+{
+ Console.WriteLine($"Interrupt on pin: {pinValueChangedEventArgs.PinNumber} with changetype: {pinValueChangedEventArgs.ChangeType}");
+}
+
diff --git a/src/devices/Tca955x/samples/Tca955x.Samples.csproj b/src/devices/Tca955x/samples/Tca955x.Samples.csproj
new file mode 100644
index 0000000000..47f10fb780
--- /dev/null
+++ b/src/devices/Tca955x/samples/Tca955x.Samples.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ $(DefaultSampleTfms)
+
+
+
+
+
+
+
diff --git a/src/devices/Tca955x/tests/MockableI2cDevice.cs b/src/devices/Tca955x/tests/MockableI2cDevice.cs
new file mode 100644
index 0000000000..3f15863024
--- /dev/null
+++ b/src/devices/Tca955x/tests/MockableI2cDevice.cs
@@ -0,0 +1,45 @@
+// 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.Collections.Generic;
+using System.Device.I2c;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Iot.Device.Tca955x.Tests
+{
+ ///
+ /// This class allows mocking of the Read/Write functions of I2cDevice taking Spans
+ ///
+ public abstract class MockableI2cDevice : I2cDevice
+ {
+ ///
+ /// These are mockable, operations taking Span<T> are not
+ ///
+ ///
+ public abstract void Read(byte[] data);
+ public sealed override void Read(Span buffer)
+ {
+ byte[] b = new byte[buffer.Length];
+ Read(b);
+ b.CopyTo(buffer);
+ }
+
+ public abstract void Write(byte[] data);
+
+ public sealed override void Write(ReadOnlySpan buffer)
+ {
+ byte[] data = new byte[buffer.Length];
+ buffer.CopyTo(data);
+ Write(data);
+ }
+
+ public sealed override void WriteRead(ReadOnlySpan writeBuffer, Span readBuffer)
+ {
+ Write(writeBuffer);
+ Read(readBuffer);
+ }
+ }
+}
diff --git a/src/devices/Tca955x/tests/Tca9554Tests.cs b/src/devices/Tca955x/tests/Tca9554Tests.cs
new file mode 100644
index 0000000000..50706cdc3b
--- /dev/null
+++ b/src/devices/Tca955x/tests/Tca9554Tests.cs
@@ -0,0 +1,72 @@
+// 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.Gpio.Tests;
+using System.Device.I2c;
+using Moq;
+using Xunit;
+
+namespace Iot.Device.Tca955x.Tests
+{
+ public class Tca9554Tests : IDisposable
+ {
+ private readonly Mock _device;
+ private readonly GpioController _controller;
+ private readonly Mock _driver;
+
+ public Tca9554Tests()
+ {
+ _device = new Mock(MockBehavior.Loose);
+ _device.CallBase = true;
+ _driver = new Mock();
+ _controller = new GpioController(_driver.Object);
+ _device.Setup(x => x.ConnectionSettings).Returns(new I2cConnectionSettings(0, Tca9554.DefaultI2cAdress));
+ }
+
+ public void Dispose()
+ {
+ _driver.VerifyAll();
+ _device.VerifyAll();
+ }
+
+ [Fact]
+ public void CreateWithInterrupt()
+ {
+ var testee = new Tca9554(_device.Object, 10, _controller);
+ Assert.NotNull(testee);
+ }
+
+ [Fact]
+ public void CreateWithoutInterrupt()
+ {
+ var testee = new Tca9554(_device.Object, -1);
+ Assert.NotNull(testee);
+ }
+
+ [Fact]
+ public void TestRead()
+ {
+ _device.Setup(x => x.Write(new byte[1]
+ {
+ 0
+ }));
+ _device.Setup(x => x.Read(It.IsAny())).Callback((byte[] b) =>
+ {
+ b[0] = 1;
+ });
+
+ var testee = new Tca9554(_device.Object, -1);
+ var tcaController = new GpioController(testee);
+ Assert.Equal(8, tcaController.PinCount);
+ GpioPin pin0 = tcaController.OpenPin(0);
+ Assert.NotNull(pin0);
+ Assert.True(tcaController.IsPinOpen(0));
+ var value = pin0.Read();
+ Assert.Equal(PinValue.High, value);
+ pin0.Dispose();
+ Assert.False(tcaController.IsPinOpen(0));
+ }
+ }
+}
diff --git a/src/devices/Tca955x/tests/Tca955x.Tests.csproj b/src/devices/Tca955x/tests/Tca955x.Tests.csproj
new file mode 100644
index 0000000000..5f1017feee
--- /dev/null
+++ b/src/devices/Tca955x/tests/Tca955x.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+
+ $(DefaultSampleTfms)
+ 10
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+