Skip to content

support i2c gpio expanders #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions examples/Example-05_I2C_Expander/Example-05_I2C_Expander.ino
Original file line number Diff line number Diff line change
@@ -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 <Arduino.h>
#include <KeyDetector.h>
#include <SparkFun_I2C_Expander_Arduino_Library.h>

// 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);
}
}
}
73 changes: 73 additions & 0 deletions examples/Example-05_I2C_Expander/README.md
Original file line number Diff line number Diff line change
@@ -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
59 changes: 39 additions & 20 deletions src/KeyDetector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,35 +52,54 @@ 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 {

// 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 {
Expand Down
Loading