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