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 + + + + + + + + + + +