From a7ec573230de31f8035890a6fef24240b21bca49 Mon Sep 17 00:00:00 2001 From: ericlewisplease Date: Wed, 5 Mar 2025 09:58:37 -0500 Subject: [PATCH 1/2] support i2c gpio expanders --- src/KeyDetector.cpp | 59 ++++++++++++++++++++++++++++++--------------- src/KeyDetector.h | 19 ++++++++++++++- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/KeyDetector.cpp b/src/KeyDetector.cpp index 7ea5847..15325ce 100644 --- a/src/KeyDetector.cpp +++ b/src/KeyDetector.cpp @@ -52,24 +52,27 @@ void KeyDetector::detect() { if (_keys[i].level > -1) { // Detect multiplexed keys (analog signal) - val = analogRead(_keys[i].pin); - if (_debounceDelay > 0 && (val > _keys[i].level-_analogThreshold && val < _keys[i].level+_analogThreshold)) { - delay(_debounceDelay); + // Note: Analog reading is not supported for I2C expanders, only direct pins + if (_keys[i].ioType == KEY_IO_TYPE_DIRECT) { val = analogRead(_keys[i].pin); - } - if (val > _keys[i].level-_analogThreshold && val < _keys[i].level+_analogThreshold) { - // Support for simultaneous detection of analog readings may lead to unstable triggers when detection ranges - // of analog signal are close or overlapping, hence disabling for now. As a result, analog readings are - // detected as a primary key press (stored in 'current') if no other primary key presses were detected, - // and can not be detected as a secondary key press (stored in 'secondary'). - // When digital Key objects present in the same Keys array as analog Key objects, they will be detected either - // as primary (when placed before analog Key objects in constructor) or secondary (when placed after analog Key - // objects in constructor). - if (!firstKey) { - firstKey = _keys[i].code; - }/* else { - secondKey = _keys[i].code; - } */ + if (_debounceDelay > 0 && (val > _keys[i].level-_analogThreshold && val < _keys[i].level+_analogThreshold)) { + delay(_debounceDelay); + val = analogRead(_keys[i].pin); + } + if (val > _keys[i].level-_analogThreshold && val < _keys[i].level+_analogThreshold) { + // Support for simultaneous detection of analog readings may lead to unstable triggers when detection ranges + // of analog signal are close or overlapping, hence disabling for now. As a result, analog readings are + // detected as a primary key press (stored in 'current') if no other primary key presses were detected, + // and can not be detected as a secondary key press (stored in 'secondary'). + // When digital Key objects present in the same Keys array as analog Key objects, they will be detected either + // as primary (when placed before analog Key objects in constructor) or secondary (when placed after analog Key + // objects in constructor). + if (!firstKey) { + firstKey = _keys[i].code; + }/* else { + secondKey = _keys[i].code; + } */ + } } } else { @@ -77,10 +80,26 @@ void KeyDetector::detect() { // Detect single keys (digital signal), // works with buttons (e.g. momentary switches) wired either with pulldown resistor (so the HIGH means that button is pressed), // or with pullup resistor (so the LOW means that button is pressed) - if (_debounceDelay > 0 && digitalRead(_keys[i].pin) == (_pullup ? LOW : HIGH)) { - delay(_debounceDelay); + bool pinState; + + if (_keys[i].ioType == KEY_IO_TYPE_DIRECT) { + // Read from direct Arduino pin + if (_debounceDelay > 0 && digitalRead(_keys[i].pin) == (_pullup ? LOW : HIGH)) { + delay(_debounceDelay); + } + pinState = digitalRead(_keys[i].pin) == (_pullup ? LOW : HIGH); + } else if (_keys[i].ioType == KEY_IO_TYPE_I2C_EXPANDER && _keys[i].expander != nullptr) { + // Read from I2C expander pin + if (_debounceDelay > 0 && _keys[i].expander->digitalRead(_keys[i].pin) == (_pullup ? LOW : HIGH)) { + delay(_debounceDelay); + } + pinState = _keys[i].expander->digitalRead(_keys[i].pin) == (_pullup ? LOW : HIGH); + } else { + // Unsupported IO type or null expander + pinState = false; } - if (digitalRead(_keys[i].pin) == (_pullup ? LOW : HIGH)) { + + if (pinState) { if (!firstKey) { firstKey = _keys[i].code; } else { diff --git a/src/KeyDetector.h b/src/KeyDetector.h index 1fa4940..a496327 100644 --- a/src/KeyDetector.h +++ b/src/KeyDetector.h @@ -34,14 +34,31 @@ #include #define KEY_NONE 0 +#define KEY_IO_TYPE_DIRECT 0 +#define KEY_IO_TYPE_I2C_EXPANDER 1 + +// Forward declaration for I2C expander interface +class I2CExpander; // Declaration of Key element type struct Key { - Key(byte c, int p, int l = -1) : code(c), pin(p), level(l) {} + Key(byte c, int p, int l = -1, byte t = KEY_IO_TYPE_DIRECT, I2CExpander* e = nullptr) + : code(c), pin(p), level(l), ioType(t), expander(e) {} byte code; // Identifier of the key int pin; // Pin the key is attached to int level; // Level of the analog signal at which press event of the multiplexed key is triggered // (if not explicitly set - default value of -1 is assigned and key considered to be digital) + byte ioType; // Type of IO connection: direct (KEY_IO_TYPE_DIRECT) or via I2C expander (KEY_IO_TYPE_I2C_EXPANDER) + I2CExpander* expander; // Pointer to the I2C expander object if ioType is KEY_IO_TYPE_I2C_EXPANDER +}; + +// Abstract class for I2C expander interface +class I2CExpander { + public: + virtual bool begin() = 0; + virtual void pinMode(uint8_t pin, uint8_t mode) = 0; + virtual void digitalWrite(uint8_t pin, uint8_t val) = 0; + virtual int digitalRead(uint8_t pin) = 0; }; // Declaration of KeyDetector class From 9f21b6458a9562458213608a052f58d05af705bb Mon Sep 17 00:00:00 2001 From: ericlewisplease Date: Wed, 5 Mar 2025 10:30:02 -0500 Subject: [PATCH 2/2] add example / readme --- .../Example-05_I2C_Expander.ino | 222 ++++++++++++++++++ examples/Example-05_I2C_Expander/README.md | 73 ++++++ 2 files changed, 295 insertions(+) create mode 100644 examples/Example-05_I2C_Expander/Example-05_I2C_Expander.ino create mode 100644 examples/Example-05_I2C_Expander/README.md diff --git a/examples/Example-05_I2C_Expander/Example-05_I2C_Expander.ino b/examples/Example-05_I2C_Expander/Example-05_I2C_Expander.ino new file mode 100644 index 0000000..ebe2e0e --- /dev/null +++ b/examples/Example-05_I2C_Expander/Example-05_I2C_Expander.ino @@ -0,0 +1,222 @@ +/* + I2C Expander signal readings using KeyDetector library with SparkFun Qwiic Directional Pad. + + Demonstrates how to use KeyDetector to trigger action based on digital signal readings from + a SparkFun Qwiic Directional Pad which uses a PCA9554 8-bit I2C GPIO expander. + + The SparkFun Qwiic Directional Pad has 5 buttons: + - Up (pin 4) + - Down (pin 5) + - Left (pin 6) + - Right (pin 7) + - Center/Select (pin 3) + + Connection: + - Connect the Qwiic Directional Pad to your Arduino/ESP32/etc using a Qwiic cable + or connect directly to the I2C pins (SDA/SCL) + + Additional info about KeyDetector library available on GitHub: + https://github.com/Spirik/KeyDetector + + This example code is in the public domain. +*/ + +#include +#include +#include + +// Define signal identifiers for five buttons +#define KEY_UP 1 +#define KEY_DOWN 2 +#define KEY_LEFT 3 +#define KEY_RIGHT 4 +#define KEY_SELECT 5 + +// D-Pad pins on the PCA9554 +const byte pinSelect = 3; +const byte pinUp = 4; +const byte pinDown = 5; +const byte pinLeft = 6; +const byte pinRight = 7; + +const int keyPressDelay = 500; // Delay after key press event triggered and before continuous press is detected, ms +const int keyPressRepeatDelay = 250; // Delay between "remains pressed" message is printed, ms + +long keyPressTime = 0; // Variable to hold time of the key press event +long now; // Variable to hold current time taken with millis() function at the beginning of loop() + +// Implementation of the I2CExpander interface for PCA9554 +class PCA9554Expander : public I2CExpander { + private: + SFE_PCA95XX io; + + public: + PCA9554Expander() : io() {} + + bool begin() override { + return io.begin(); + } + + void pinMode(uint8_t pin, uint8_t mode) override { + io.pinMode(pin, mode); + } + + void digitalWrite(uint8_t pin, uint8_t val) override { + io.digitalWrite(pin, val); + } + + int digitalRead(uint8_t pin) override { + return io.isConnected() ? io.digitalRead(pin) == LOW : LOW; + } +}; + +// Create PCA9554 expander instance +PCA9554Expander dpadExpander; + +// Create array of Key objects that will link defined key identifiers with pins on the expander +// Note: We set ioType to KEY_IO_TYPE_I2C_EXPANDER and pass our expander object +// Also set pullup to true since the D-Pad buttons are active LOW +Key keys[] = { + {KEY_UP, pinUp, -1, KEY_IO_TYPE_I2C_EXPANDER, &dpadExpander}, + {KEY_DOWN, pinDown, -1, KEY_IO_TYPE_I2C_EXPANDER, &dpadExpander}, + {KEY_LEFT, pinLeft, -1, KEY_IO_TYPE_I2C_EXPANDER, &dpadExpander}, + {KEY_RIGHT, pinRight, -1, KEY_IO_TYPE_I2C_EXPANDER, &dpadExpander}, + {KEY_SELECT, pinSelect, -1, KEY_IO_TYPE_I2C_EXPANDER, &dpadExpander} +}; + +// Create KeyDetector object with 10ms debounce and pullup set to true +KeyDetector dpad(keys, sizeof(keys)/sizeof(Key), 10, 16, true); + +void setup() { + // Serial communications setup + Serial.begin(115200); + while (!Serial) { + ; // Wait for serial port to connect (needed for native USB port only) + } + + Serial.println("SparkFun Qwiic Directional Pad Example with KeyDetector"); + + // Initialize the I2C expander + if (!dpadExpander.begin()) { + Serial.println("Failed to initialize PCA9554 expander! Check connections."); + while (1); // Halt if we can't communicate with the expander + } + + dpadExpander.pinMode(pinUp, INPUT); + dpadExpander.pinMode(pinDown, INPUT); + dpadExpander.pinMode(pinLeft, INPUT); + dpadExpander.pinMode(pinRight, INPUT); + dpadExpander.pinMode(pinSelect, INPUT); + + Serial.println("PCA9554 expander initialized successfully."); + Serial.println("Press any button on the D-Pad..."); +} + +void loop() { + // Get current time to use later on + now = millis(); + + // Check the current state of input signal + dpad.detect(); + + // When button press is detected ("triggered"), print corresponding message + // and save current time as a time of the key press event + switch (dpad.trigger) { + case KEY_UP: + Serial.println("UP pressed!"); + keyPressTime = now; + break; + case KEY_DOWN: + Serial.println("DOWN pressed!"); + keyPressTime = now; + break; + case KEY_LEFT: + Serial.println("LEFT pressed!"); + keyPressTime = now; + break; + case KEY_RIGHT: + Serial.println("RIGHT pressed!"); + keyPressTime = now; + break; + case KEY_SELECT: + Serial.println("SELECT pressed!"); + keyPressTime = now; + break; + } + + // When button release is detected, print message + switch (dpad.triggerRelease) { + case KEY_UP: + Serial.println("UP released."); + break; + case KEY_DOWN: + Serial.println("DOWN released."); + break; + case KEY_LEFT: + Serial.println("LEFT released."); + break; + case KEY_RIGHT: + Serial.println("RIGHT released."); + break; + case KEY_SELECT: + Serial.println("SELECT released."); + break; + } + + // After keyPressDelay passed since keyPressTime, handle continuous press + if (now > keyPressTime + keyPressDelay) { + // Determine currently pressed button and print message with repeat delay + switch (dpad.current) { + case KEY_UP: + Serial.println("UP held down..."); + delay(keyPressRepeatDelay); + break; + case KEY_DOWN: + Serial.println("DOWN held down..."); + delay(keyPressRepeatDelay); + break; + case KEY_LEFT: + Serial.println("LEFT held down..."); + delay(keyPressRepeatDelay); + break; + case KEY_RIGHT: + Serial.println("RIGHT held down..."); + delay(keyPressRepeatDelay); + break; + case KEY_SELECT: + Serial.println("SELECT held down..."); + delay(keyPressRepeatDelay); + break; + } + } + + // Also demonstrate detection of simultaneous key presses (if secondary key detected) + if (dpad.secondary != KEY_NONE) { + String primaryKey, secondaryKey; + + // Determine name of primary key + switch (dpad.current) { + case KEY_UP: primaryKey = "UP"; break; + case KEY_DOWN: primaryKey = "DOWN"; break; + case KEY_LEFT: primaryKey = "LEFT"; break; + case KEY_RIGHT: primaryKey = "RIGHT"; break; + case KEY_SELECT: primaryKey = "SELECT"; break; + } + + // Determine name of secondary key + switch (dpad.secondary) { + case KEY_UP: secondaryKey = "UP"; break; + case KEY_DOWN: secondaryKey = "DOWN"; break; + case KEY_LEFT: secondaryKey = "LEFT"; break; + case KEY_RIGHT: secondaryKey = "RIGHT"; break; + case KEY_SELECT: secondaryKey = "SELECT"; break; + } + + if (dpad.previousSecondary != dpad.secondary) { + Serial.print("Multiple buttons pressed: "); + Serial.print(primaryKey); + Serial.print(" + "); + Serial.println(secondaryKey); + } + } +} \ No newline at end of file diff --git a/examples/Example-05_I2C_Expander/README.md b/examples/Example-05_I2C_Expander/README.md new file mode 100644 index 0000000..199496e --- /dev/null +++ b/examples/Example-05_I2C_Expander/README.md @@ -0,0 +1,73 @@ +# Example-05: I2C Expander with SparkFun Qwiic Directional Pad + +This example demonstrates how to use the KeyDetector library with the SparkFun Qwiic Directional Pad, which uses a PCA9554 8-bit I2C GPIO expander chip. + +## Hardware Requirements + +- Arduino board (or any other compatible board like ESP32, ESP8266, etc.) +- [SparkFun Qwiic Directional Pad](https://www.sparkfun.com/products/15316) +- Qwiic cable or I2C connection wires + +## Wiring + +The SparkFun Qwiic Directional Pad can be connected to your Arduino/board in one of two ways: + +### Option 1: Using Qwiic Connector (Recommended) +If your board has a Qwiic connector or you have a Qwiic shield, simply connect the D-Pad to your board using a Qwiic cable. + +### Option 2: Manual I2C Connection +If your board doesn't have a Qwiic connector, connect the D-Pad to your board using the following connections: + +- D-Pad GND → Arduino GND +- D-Pad 3.3V → Arduino 3.3V +- D-Pad SDA → Arduino SDA (A4 on most Arduinos) +- D-Pad SCL → Arduino SCL (A5 on most Arduinos) + +## Button Mapping + +The SparkFun Qwiic Directional Pad has 5 buttons mapped to the following pins on the PCA9554 chip: + +- Up: Pin 4 +- Down: Pin 5 +- Left: Pin 6 +- Right: Pin 7 +- Center/Select: Pin 3 + +## Active Low Logic + +The buttons on the D-Pad are active LOW, meaning they are pulled up by default and pressing them connects the pin to ground. This example takes care of this by setting the `pullup` parameter to `true` in the KeyDetector constructor. + +## Features Demonstrated + +This example demonstrates: + +1. Implementation of the `I2CExpander` interface for the PCA9554 chip +2. Reading button states from the I2C expander +3. Detecting button press, release, and held states +4. Handling simultaneous button presses +5. Sending button events to the Serial monitor + +## Serial Output + +Open the Serial Monitor at 115200 baud to see the button press, release, and held events. The example will print: +- When a button is pressed +- When a button is released +- When a button is held down +- When multiple buttons are pressed simultaneously + +## Troubleshooting + +If the example doesn't work: + +1. Check your I2C connections +2. Verify that the I2C address is correct (0x27 by default) +3. Make sure the D-Pad is properly powered (3.3V) +4. Check if your Arduino can communicate with the D-Pad using an I2C scanner sketch + +## Further Customization + +You can modify the example to: +- Change the debounce delay (currently 10ms) +- Adjust the key press and repeat delays +- Add custom actions for different button combinations +- Integrate with other I2C devices using the same Wire interface \ No newline at end of file