diff --git a/src/devices/Mcp23xxx/Mcp23x1x.cs b/src/devices/Mcp23xxx/Mcp23x1x.cs index 89f24f8f46..a5727b43a3 100644 --- a/src/devices/Mcp23xxx/Mcp23x1x.cs +++ b/src/devices/Mcp23xxx/Mcp23x1x.cs @@ -54,5 +54,27 @@ protected Mcp23x1x(BusAdapter device, int reset, int interruptA, int interruptB, /// Reads the interrupt pin for the given port if configured. /// public PinValue ReadInterrupt(Port port) => InternalReadInterrupt(port); + + /// + /// Reads all bits of port A in a single operation. + /// + /// In the low byte: A bit field of the value of the first 8 GPIO ports + /// (Bit 0: GPIO 0, Bit 1: GPIO 1 etc.). Only the bits of input ports are defined. + public int ReadPortA() + { + int value = ReadByte(Register.GPIO, Port.PortA); + return value; + } + + /// + /// Reads all bits of port B in a single operation. + /// + /// In the low byte: A bit field of the value of the second 8 GPIO ports + /// (Bit 0: GPIO 8, Bit 1: GPIO 9 etc.). Only the bits of input ports are defined. + public int ReadPortB() + { + int value = ReadByte(Register.GPIO, Port.PortB); + return value; + } } } diff --git a/src/devices/Mcp23xxx/Mcp23xxx.cs b/src/devices/Mcp23xxx/Mcp23xxx.cs index 37a5f73f13..4a1e437b4e 100644 --- a/src/devices/Mcp23xxx/Mcp23xxx.cs +++ b/src/devices/Mcp23xxx/Mcp23xxx.cs @@ -2,8 +2,10 @@ // 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.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -18,6 +20,7 @@ public abstract partial class Mcp23xxx : GpioDriver private readonly int _interruptA; private readonly int _interruptB; private readonly Dictionary _pinValues = new Dictionary(); + private readonly ConcurrentDictionary _eventHandlers = new ConcurrentDictionary(); private BankStyle _bankStyle; private GpioController? _controller; private bool _shouldDispose; @@ -33,6 +36,11 @@ public abstract partial class Mcp23xxx : GpioDriver private bool _cacheValid; private bool _disabled; + private object _interruptHandlerLock = new object(); + + private byte[] _interruptPins; + private byte[] _interruptLastInputValues; + /// /// A general purpose parallel I/O expansion for I2C or SPI applications. /// @@ -60,6 +68,9 @@ protected Mcp23xxx(BusAdapter bus, int reset = -1, int interruptA = -1, int inte _interruptA = interruptA; _interruptB = interruptB; + _interruptPins = new byte[2]; + _interruptLastInputValues = new byte[2]; + // Only need master controller if there are external pins provided. if (_reset != -1 || _interruptA != -1 || _interruptB != -1) { @@ -177,7 +188,7 @@ protected void InternalWriteByte(Register register, byte value, Port port) /// Read a byte from the given register. /// /// - /// Writes to the A port registers on 16 bit devices. + /// Reads from the A port registers on 16 bit devices. /// public byte ReadByte(Register register) => InternalReadByte(register, Port.PortA); @@ -289,7 +300,7 @@ public void Enable() /// Reads interrupt value /// /// Port to read interrupt on - /// Value of intterupt pin + /// Value of interrupt pin protected PinValue InternalReadInterrupt(Port port) { int pinNumber = port switch @@ -315,6 +326,10 @@ protected PinValue InternalReadInterrupt(Port port) /// public PinValue ReadInterrupt() => InternalReadInterrupt(Port.PortA); + 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. /// @@ -322,30 +337,44 @@ protected PinValue InternalReadInterrupt(Port port) /// The mode to be set. protected override void SetPinMode(int pinNumber, PinMode mode) { - if (mode != PinMode.Input && mode != PinMode.Output) + lock (_interruptHandlerLock) { - throw new ArgumentException("The Mcp controller supports Input and Output modes only."); - } + 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."); + } - ValidatePin(pinNumber); + ValidatePin(pinNumber); - byte SetBit(byte data, int bitNumber) => (byte)(data | (1 << bitNumber)); + Port port = GetPortForPinNumber(pinNumber); + if (port == Port.PortB) + { + pinNumber -= 8; + } - byte ClearBit(byte data, int bitNumber) => (byte)(data & ~(1 << bitNumber)); + byte value; + if (mode == PinMode.Output) + { + value = ClearBit(InternalReadByte(Register.IODIR, port), pinNumber); + } + else + { + value = SetBit(InternalReadByte(Register.IODIR, port), pinNumber); + } - if (pinNumber < 8) - { - byte value = mode == PinMode.Output - ? ClearBit(InternalReadByte(Register.IODIR, Port.PortA), pinNumber) - : SetBit(InternalReadByte(Register.IODIR, Port.PortA), pinNumber); - InternalWriteByte(Register.IODIR, value, Port.PortA); - } - else - { - byte value = mode == PinMode.Output - ? ClearBit(InternalReadByte(Register.IODIR, Port.PortB), pinNumber - 8) - : SetBit(InternalReadByte(Register.IODIR, Port.PortB), pinNumber - 8); - InternalWriteByte(Register.IODIR, value, Port.PortB); + InternalWriteByte(Register.IODIR, value, port); + + byte value2; + if (mode == PinMode.InputPullUp) + { + value2 = SetBit(InternalReadByte(Register.GPPU, port), pinNumber); + } + else + { + value2 = ClearBit(InternalReadByte(Register.GPPU, port), pinNumber); + } + + InternalWriteByte(Register.GPPU, value2, port); } } @@ -356,14 +385,17 @@ protected override void SetPinMode(int pinNumber, PinMode mode) /// High or low pin value. protected override PinValue Read(int pinNumber) { - ValidatePin(pinNumber); - Span pinValuePairs = stackalloc PinValuePair[] + lock (_interruptHandlerLock) { - new PinValuePair(pinNumber, default) - }; - Read(pinValuePairs); - _pinValues[pinNumber] = pinValuePairs[0].PinValue; - return _pinValues[pinNumber]; + ValidatePin(pinNumber); + Span pinValuePairs = stackalloc PinValuePair[] + { + new PinValuePair(pinNumber, default) + }; + Read(pinValuePairs); + _pinValues[pinNumber] = pinValuePairs[0].PinValue; + return _pinValues[pinNumber]; + } } /// @@ -374,34 +406,37 @@ protected override PinValue Read(int pinNumber) /// protected void Read(Span pinValuePairs) { - (uint pins, _) = new PinVector32(pinValuePairs); - if ((pins >> PinCount) > 0) + lock (_interruptHandlerLock) { - ThrowBadPin(nameof(pinValuePairs)); - } + (uint pins, _) = new PinVector32(pinValuePairs); + if ((pins >> PinCount) > 0) + { + ThrowBadPin(nameof(pinValuePairs)); + } - ushort result; - if (pins < 0xFF + 1) - { - // Only need to get the first 8 pins (PortA) - result = InternalReadByte(Register.GPIO, Port.PortA); - } - else if ((pins & 0xFF) == 0) - { - // Only need to get the second 8 pins (PortB) - result = (ushort)(InternalReadByte(Register.GPIO, Port.PortB) << 8); - } - else - { - // Need to get both - result = InternalReadUInt16(Register.GPIO); - } + ushort result; + if (pins < 0xFF + 1) + { + // Only need to get the first 8 pins (PortA) + result = InternalReadByte(Register.GPIO, Port.PortA); + } + else if ((pins & 0xFF) == 0) + { + // Only need to get the second 8 pins (PortB) + result = (ushort)(InternalReadByte(Register.GPIO, Port.PortB) << 8); + } + else + { + // Need to get both + result = InternalReadUInt16(Register.GPIO); + } - for (int i = 0; i < pinValuePairs.Length; i++) - { - int pin = pinValuePairs[i].PinNumber; - pinValuePairs[i] = new PinValuePair(pin, result & (1 << pin)); - _pinValues[pin] = pinValuePairs[i].PinValue; + for (int i = 0; i < pinValuePairs.Length; i++) + { + int pin = pinValuePairs[i].PinNumber; + pinValuePairs[i] = new PinValuePair(pin, result & (1 << pin)); + _pinValues[pin] = pinValuePairs[i].PinValue; + } } } @@ -412,13 +447,16 @@ protected void Read(Span pinValuePairs) /// The value to be written. protected override void Write(int pinNumber, PinValue value) { - ValidatePin(pinNumber); - Span pinValuePairs = stackalloc PinValuePair[] + lock (_interruptHandlerLock) { - new PinValuePair(pinNumber, value) - }; - Write(pinValuePairs); - _pinValues[pinNumber] = value; + ValidatePin(pinNumber); + Span pinValuePairs = stackalloc PinValuePair[] + { + new PinValuePair(pinNumber, value) + }; + Write(pinValuePairs); + _pinValues[pinNumber] = value; + } } /// @@ -426,44 +464,47 @@ protected override void Write(int pinNumber, PinValue value) /// protected void Write(ReadOnlySpan pinValuePairs) { - (uint mask, uint newBits) = new PinVector32(pinValuePairs); - if ((mask >> PinCount) > 0) + lock (_interruptHandlerLock) { - ThrowBadPin(nameof(pinValuePairs)); - } + (uint mask, uint newBits) = new PinVector32(pinValuePairs); + if ((mask >> PinCount) > 0) + { + ThrowBadPin(nameof(pinValuePairs)); + } - if (!_cacheValid) - { - UpdateCache(); - } + if (!_cacheValid) + { + UpdateCache(); + } - ushort cachedValue = _gpioCache; - ushort newValue = SetBits(cachedValue, (ushort)newBits, (ushort)mask); - if (cachedValue == newValue) - { - return; - } + ushort cachedValue = _gpioCache; + ushort newValue = SetBits(cachedValue, (ushort)newBits, (ushort)mask); + if (cachedValue == newValue) + { + return; + } - if (mask < 0xFF + 1) - { - // Only need to change the first 8 pins (PortA) - InternalWriteByte(Register.GPIO, (byte)newValue, Port.PortA); - } - else if ((mask & 0xFF) == 0) - { - // Only need to change the second 8 pins (PortB) - InternalWriteByte(Register.GPIO, (byte)(newValue >> 8), Port.PortB); - } - else - { - // Need to change both - InternalWriteUInt16(Register.GPIO, newValue); - } + if (mask < 0xFF + 1) + { + // Only need to change the first 8 pins (PortA) + InternalWriteByte(Register.GPIO, (byte)newValue, Port.PortA); + } + else if ((mask & 0xFF) == 0) + { + // Only need to change the second 8 pins (PortB) + InternalWriteByte(Register.GPIO, (byte)(newValue >> 8), Port.PortB); + } + else + { + // Need to change both + InternalWriteUInt16(Register.GPIO, newValue); + } - _gpioCache = newValue; - foreach (PinValuePair pinValuePair in pinValuePairs) - { - _pinValues[pinValuePair.PinNumber] = pinValuePair.PinValue; + _gpioCache = newValue; + foreach (PinValuePair pinValuePair in pinValuePairs) + { + _pinValues[pinValuePair.PinNumber] = pinValuePair.PinValue; + } } } @@ -564,23 +605,304 @@ protected override PinMode GetPinMode(int pinNumber) } } - /// + /// + /// Enables interrupts for a specified pin. On 16-Pin devices, Pins 0-7 trigger the INTA pin and Pins 8-15 + /// trigger the INTB pin. The interrupt signals are configured as active-low. + /// + /// The pin number for which an interrupt shall be triggered + /// Event(s) that should trigger the interrupt on the given pin + /// EventTypes is not valid (must have at least one event type selected) + /// After calling this method, call once to make sure the interrupt flag for the given port is cleared + public void EnableInterruptOnChange(int pinNumber, PinEventTypes eventTypes) + { + ValidatePin(pinNumber); + byte oldValue, newValue; + lock (_interruptHandlerLock) + { + if (eventTypes == PinEventTypes.None) + { + throw new ArgumentException("No event type specified"); + } + + Port port = Port.PortA; + if (pinNumber >= 8) + { + pinNumber -= 8; + port = Port.PortB; + } + + // Set the corresponding bit in the GPINTEN (Interrupt-on-Change) register + oldValue = InternalReadByte(Register.GPINTEN, port); + newValue = SetBit(oldValue, pinNumber); + InternalWriteByte(Register.GPINTEN, newValue, port); + oldValue = InternalReadByte(Register.INTCON, port); + // If the interrupt shall happen on either edge, we clear the INTCON (Interrupt-on-Change-Control) register, + // which will trigger an interrupt on every change. Otherwise, set the INTCON register bit and set the + // DefVal register. + if (eventTypes == (PinEventTypes.Falling | PinEventTypes.Rising)) + { + newValue = ClearBit(oldValue, pinNumber); + } + else + { + newValue = SetBit(oldValue, pinNumber); + } + + InternalWriteByte(Register.INTCON, newValue, port); + + oldValue = InternalReadByte(Register.DEFVAL, port); + // If we clear the bit, the interrupt occurs on a rising edge, if we set it, it occurs on a falling edge. + // If INTCON is clear, the value is ignored. + if (eventTypes == PinEventTypes.Rising) + { + newValue = ClearBit(oldValue, pinNumber); + } + else + { + newValue = SetBit(oldValue, pinNumber); + } + + InternalWriteByte(Register.DEFVAL, newValue, port); + + // Finally make sure that IOCON.ODR is low and IOCON.INTPOL is low, too (interrupt is low-active, the default) + // For this register, it doesn't matter which port we use, it exists only once. + oldValue = InternalReadByte(Register.IOCON, Port.PortA); + newValue = ClearBit(oldValue, 1); + newValue = ClearBit(newValue, 2); + InternalWriteByte(Register.IOCON, newValue, Port.PortA); + + _interruptPins[(int)port] = SetBit(_interruptPins[(int)port], pinNumber); + _interruptLastInputValues[(int)port] = InternalReadByte(Register.GPIO, port); + } + } + + private static Port GetPortForPinNumber(int pinNumber) + { + Port port = Port.PortA; + if (pinNumber >= 8) + { + port = Port.PortB; + } + + return port; + } + + /// + /// Disables triggering interrupts on a certain pin + /// + /// The pin number + public void DisableInterruptOnChange(int pinNumber) + { + ValidatePin(pinNumber); + byte oldValue, newValue; + lock (_interruptHandlerLock) + { + if (pinNumber < 8) + { + // Set the corresponding bit in the GPINTEN (Interrupt-on-Change) register + oldValue = InternalReadByte(Register.GPINTEN, Port.PortA); + newValue = ClearBit(oldValue, pinNumber); + InternalWriteByte(Register.GPINTEN, newValue, Port.PortA); + _interruptPins[0] = ClearBit(_interruptPins[0], pinNumber); + } + else + { + oldValue = InternalReadByte(Register.GPINTEN, Port.PortB); + newValue = ClearBit(oldValue, pinNumber - 8); + InternalWriteByte(Register.GPINTEN, newValue, Port.PortB); + _interruptPins[1] = ClearBit(_interruptPins[1], pinNumber - 8); + } + } + } + + private void InterruptHandler(object sender, PinValueChangedEventArgs e) + { + Port port; + int interruptPending; + int newValues; + + lock (_interruptHandlerLock) + { + port = e.PinNumber == _interruptA ? Port.PortA : Port.PortB; + + // It seems that this register has at most 1 bit set - the one that triggered the interrupt. + // If another pin which has interrupt handling enabled changes until we clear the interrupt flag, that + // interrupt is lost. + int pinThatCausedInterrupt = InternalReadByte(Register.INTF, port); + newValues = InternalReadByte(Register.GPIO, port); + + interruptPending = (newValues ^ _interruptLastInputValues[(int)port]) & _interruptPins[(int)port]; // Which values changed? + interruptPending |= pinThatCausedInterrupt; // this one certainly did (even if the value is now the same) + _interruptLastInputValues[(int)port] = (byte)newValues; + } + + int offset = 0; + if (port == Port.PortB) + { + offset = 8; + } + + int mask = 1; + int pin = 0; + + while (mask < 0x10) + { + if ((interruptPending & mask) != 0) + { + CallHandlerOnPin(pin + offset, newValues & mask); + } + + mask = mask << 1; + pin++; + } + } + + /// + /// 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, int valueFlag) + { + if (_eventHandlers.TryGetValue(pin, out var handler)) + { + handler.Invoke(this, new PinValueChangedEventArgs(valueFlag != 0 ? PinEventTypes.Rising : PinEventTypes.Falling, 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 eventTypes, - PinChangeEventHandler callback) => throw new NotImplementedException(); + 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"); + } + + EnableInterruptOnChange(pinNumber, eventTypes); + Port port = GetPortForPinNumber(pinNumber); + if (port == Port.PortA) + { + if (_interruptA < 0) + { + throw new InvalidOperationException("No GPIO pin defined for interrupt line A. Please specify an interrupt line in the constructor."); + } + + if (!_eventHandlers.Any(x => x.Key <= 7)) + { + _controller.RegisterCallbackForPinValueChangedEvent(_interruptA, PinEventTypes.Falling, InterruptHandler); + } + + _eventHandlers[pinNumber] = callback; + InternalReadByte(Register.GPIO, Port.PortA); // Clear the interrupt flags + } + else + { + if (_interruptB < 0) + { + throw new InvalidOperationException("No GPIO pin defined for interrupt line B. Please specify an interrupt line in the constructor."); + } + + if (!_eventHandlers.Any(x => x.Key >= 8)) + { + _controller.RegisterCallbackForPinValueChangedEvent(_interruptB, PinEventTypes.Falling, InterruptHandler); + } + + _eventHandlers[pinNumber] = callback; + InternalReadByte(Register.GPIO, Port.PortB); // Clear the interrupt flags + } + } /// - protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) => - throw new NotImplementedException(); + 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 _)) + { + Port port = GetPortForPinNumber(pinNumber); + if (port == Port.PortA) + { + if (!_eventHandlers.Any(x => x.Key <= 7)) + { + _controller.UnregisterCallbackForPinValueChangedEvent(_interruptA, InterruptHandler); + } + } + else + { + if (!_eventHandlers.Any(x => x.Key >= 8)) + { + _controller.UnregisterCallbackForPinValueChangedEvent(_interruptB, 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) => throw new NotImplementedException(); + 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.Input || mode == PinMode.Output || mode == PinMode.InputPullUp); } } diff --git a/src/devices/Mcp23xxx/README.md b/src/devices/Mcp23xxx/README.md index d08569f2df..7a550b1950 100644 --- a/src/devices/Mcp23xxx/README.md +++ b/src/devices/Mcp23xxx/README.md @@ -94,12 +94,35 @@ mcp23S17.Enable(); mcp23S17.Disable(); ``` -**TODO**: Interrupt pins can only be read for now. Events are coming in a future PR. +### Interrupt support + +The `Mcp23xxx` has one (8-bit variants) or two (16-bit variants) interrupt pins. These allow external +signalisation on interrupt change. 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 -var mcp23S17 = new Mcp23S17(spiDevice, 0x20, 10, 25, 17); -PinValue interruptA = mcp23S17.ReadInterruptA(); -PinValue interruptB = mcp23S17.ReadInterruptB(); +// Gpio controller from parent device (eg. Raspberry Pi) +_gpioController = new GpioController(PinNumberingScheme.Logical); +_i2c = I2cDevice.Create(new I2cConnectionSettings(1, 0x21)); +// The "InterruptA" line of the Mcp23017 is connected to GPIO input 11 of the Raspi +_device = new Mcp23017(_i2c, -1, 11, -1, _gpioController, false); +GpioController theDeviceController = new GpioController(PinNumberingScheme.Logical, _device); +theDeviceController.OpenPin(1, PinMode.Input); +theDeviceController.RegisterCallbackForPinValueChangedEvent(1, PinEventTypes.Rising, Callback); +``` + +Alternatively, you can also manually control the event handling: + +```csharp +_gpioController = new GpioController(); +_device = I2cDevice.Create(new I2cConnectionSettings(1, 0x21)); +// Interrupt pin B is connected to GPIO pin 22 +_mcp23017 = new Mcp23017(_device, -1, -1, 22, gpioController, false); +_mcp23017.EnableInterruptOnChange(8, PinEventTypes.Rising | PinEventTypes.Falling); // Enable interrupt for pin 8 +_gpioController.RegisterCallbackForPinValueChangedEvent(22, PinEventTypes.Falling, Interrupt); +// Read the interrupt register, to make sure we get any further interrupts +_mcp23017.ReadByte(Register.GPIO, Port.PortB); ``` ## Binding Notes diff --git a/src/devices/Mcp23xxx/tests/EventHandlingTests.cs b/src/devices/Mcp23xxx/tests/EventHandlingTests.cs new file mode 100644 index 0000000000..37392a3fd2 --- /dev/null +++ b/src/devices/Mcp23xxx/tests/EventHandlingTests.cs @@ -0,0 +1,136 @@ +// 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.Gpio; +using System.Device.I2c; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Iot.Device.Mcp23xxx; +using Iot.Device.Mcp23xxx.Tests; +using Xunit; +using Moq; + +namespace Iot.Device.Mcp23xxx.Tests +{ + public sealed class EventHandlingTests : Mcp23xxxTest, IDisposable + { + private I2cDeviceMock _mockI2c; + private GpioDriverMock _driverMock; + private Iot.Device.Mcp23xxx.Mcp23xxx _device; + private GpioController _gpioController; + private int _callbackNo; + + public EventHandlingTests() + { + _callbackNo = 0; + _mockI2c = new I2cDeviceMock(2, null); + _driverMock = new GpioDriverMock(); + _gpioController = new GpioController(PinNumberingScheme.Logical, _driverMock); + _device = new Mcp23017(_mockI2c, -1, 11, 22, _gpioController, false); + } + + [Fact] + public void EnableDisableEvents() + { + _device.EnableInterruptOnChange(0, PinEventTypes.Falling | PinEventTypes.Rising); + _device.DisableInterruptOnChange(0); + } + + [Fact] + public void AddEventHandlerPortA() + { + GpioController theDeviceController = new GpioController(PinNumberingScheme.Logical, _device); + theDeviceController.OpenPin(1, PinMode.Input); + theDeviceController.RegisterCallbackForPinValueChangedEvent(1, PinEventTypes.Rising, Callback); + + _mockI2c.DeviceMock.Registers[14] = 2; // Port A INTF register (pin 1 triggered the event) + _mockI2c.DeviceMock.Registers[0x12] = 2; // Port A GPIO register (pin 1 is high now) + // This should simulate the interrupt being triggered on the master controller, not the mcp! + _driverMock.FireEvent(new PinValueChangedEventArgs(PinEventTypes.Falling, 11)); + Assert.True(_callbackNo == 1); + // Nothing registered for an event on interrupt B, so this shouldn't do anything + _driverMock.FireEvent(new PinValueChangedEventArgs(PinEventTypes.Falling, 22)); + Assert.True(_callbackNo == 1); + } + + [Fact] + public void AddEventHandlerPortB() + { + GpioController theDeviceController = new GpioController(PinNumberingScheme.Logical, _device); + theDeviceController.OpenPin(10, PinMode.Input); + theDeviceController.RegisterCallbackForPinValueChangedEvent(10, PinEventTypes.Rising, Callback); + + _mockI2c.DeviceMock.Registers[0x0F] = 4; // Port B INTF register (pin 1 triggered the event) + _mockI2c.DeviceMock.Registers[0x13] = 4; // Port B GPIO register (pin 1 is high now) + // This should simulate the interrupt being triggered on the master controller, not the mcp! + _driverMock.FireEvent(new PinValueChangedEventArgs(PinEventTypes.Falling, 22)); + Assert.True(_callbackNo == 1); + // Nothing registered for an event on interrupt B, so this shouldn't do anything + _driverMock.FireEvent(new PinValueChangedEventArgs(PinEventTypes.Falling, 11)); + Assert.True(_callbackNo == 1); + } + + [Fact] + public void AddMultipleEventHandlers() + { + _callbackNo = 0; + GpioController theDeviceController = new GpioController(PinNumberingScheme.Logical, _device); + theDeviceController.OpenPin(0, PinMode.Input); + theDeviceController.RegisterCallbackForPinValueChangedEvent(0, PinEventTypes.Rising | PinEventTypes.Falling, Callback2); + + theDeviceController.OpenPin(1, PinMode.Input); + theDeviceController.RegisterCallbackForPinValueChangedEvent(1, PinEventTypes.Rising | PinEventTypes.Falling, Callback2); + + _mockI2c.DeviceMock.Registers[0x0E] = 1; // Port A INTF register (pin 0 triggered the event) + _mockI2c.DeviceMock.Registers[0x12] = 3; // Port A GPIO register (pin 0 and 1 are high) + // This should simulate the interrupt being triggered on the master controller, not the mcp! + _driverMock.FireEvent(new PinValueChangedEventArgs(PinEventTypes.Rising, 11)); + Assert.Equal(3, _callbackNo); + } + + [Fact] + public void AddRemoveEventHandler() + { + GpioController theDeviceController = new GpioController(PinNumberingScheme.Logical, _device); + theDeviceController.OpenPin(1, PinMode.Input); + theDeviceController.RegisterCallbackForPinValueChangedEvent(1, PinEventTypes.Rising, Callback); + theDeviceController.UnregisterCallbackForPinValueChangedEvent(1, Callback); + + // Now trigger an event, shouldn't do anything + _mockI2c.DeviceMock.Registers[14] = 2; // Port A INTF register (pin 1 triggered the event) + _mockI2c.DeviceMock.Registers[0x12] = 2; // Port A GPIO register (pin 1 is high now) + // This should simulate the interrupt being triggered on the master controller, not the mcp! + _driverMock.FireEvent(new PinValueChangedEventArgs(PinEventTypes.Falling, 11)); + Assert.True(_callbackNo == 0); + theDeviceController.ClosePin(1); + } + + private void Callback(object sender, PinValueChangedEventArgs e) + { + Assert.Equal(PinEventTypes.Rising, e.ChangeType); + Assert.True(e.PinNumber == 1 || e.PinNumber == 10); + _callbackNo++; + } + + private void Callback2(object sender, PinValueChangedEventArgs e) + { + if (e.PinNumber == 0) + { + _callbackNo |= 1; + } + else if (e.PinNumber == 1) + { + _callbackNo |= 2; + } + } + + public void Dispose() + { + _gpioController.Dispose(); + _device.Dispose(); + } + } +} diff --git a/src/devices/Mcp23xxx/tests/GpioReadTests.cs b/src/devices/Mcp23xxx/tests/GpioReadTests.cs index 76dca62d6b..6c8f912c90 100644 --- a/src/devices/Mcp23xxx/tests/GpioReadTests.cs +++ b/src/devices/Mcp23xxx/tests/GpioReadTests.cs @@ -41,5 +41,40 @@ public void Read_GoodPin(TestDevice testDevice) Assert.Equal(PinValue.Low, testDevice.Controller.Read(pin)); } } + + [Theory] + [MemberData(nameof(TestDevices))] + public void Read_GoodPinPullUp(TestDevice testDevice) + { + Mcp23xxx device = testDevice.Device; + for (int pin = 0; pin < testDevice.Controller.PinCount; pin++) + { + bool first = pin < 8; + int register = testDevice.Controller.PinCount == 16 + ? (first ? 0x12 : 0x13) + : 0x09; + + testDevice.Controller.OpenPin(pin, PinMode.InputPullUp); + + // Flip the bit on (set the backing buffer directly to simulate incoming data) + testDevice.ChipMock.Registers[register] = (byte)(1 << (first ? pin : pin - 8)); + Assert.Equal(PinValue.High, testDevice.Controller.Read(pin)); + + // Clear the register + testDevice.ChipMock.Registers[register] = 0x00; + Assert.Equal(PinValue.Low, testDevice.Controller.Read(pin)); + } + } + + [Theory] + [MemberData(nameof(TestDevices))] + public void Read_InvalidMode(TestDevice testDevice) + { + Mcp23xxx device = testDevice.Device; + for (int pin = 0; pin < testDevice.Controller.PinCount; pin++) + { + Assert.Throws(() => testDevice.Controller.OpenPin(pin, PinMode.InputPullDown)); + } + } } } diff --git a/src/devices/Mcp23xxx/tests/Mcp23xxxTest.cs b/src/devices/Mcp23xxx/tests/Mcp23xxxTest.cs index e8f364981b..79cf335a80 100644 --- a/src/devices/Mcp23xxx/tests/Mcp23xxxTest.cs +++ b/src/devices/Mcp23xxx/tests/Mcp23xxxTest.cs @@ -185,6 +185,7 @@ public class GpioDriverMock : GpioDriver { private Dictionary _pinValues = new Dictionary(); private ConcurrentDictionary _pinModes = new ConcurrentDictionary(); + private PinChangeEventHandler? _callback; protected override int PinCount => 10; @@ -247,9 +248,20 @@ protected override void OpenPin(int pinNumber) protected override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, CancellationToken cancellationToken) => throw new NotImplementedException(); - protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback) => throw new NotImplementedException(); + protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback) + { + _callback = callback; // Keep it simple for this test class + } - protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) => throw new NotImplementedException(); + protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) + { + _callback = null; + } + + public void FireEvent(PinValueChangedEventArgs e) + { + _callback?.Invoke(this, e); + } } } }