From 00646466f1496b684658478185e5b823796cab02 Mon Sep 17 00:00:00 2001 From: myembeddedstuff Date: Thu, 16 Jul 2026 00:09:18 +0200 Subject: [PATCH] feat: add driver for MCP23017 --- src/detect/ScanI2C.cpp | 4 +- src/detect/ScanI2C.h | 1 + src/detect/ScanI2CTwoWire.cpp | 13 +- src/input/MCP23017Keyboard.cpp | 380 +++++++++++++++++++++++++++++++++ src/input/MCP23017Keyboard.h | 55 +++++ src/input/cardKbI2cImpl.cpp | 6 +- src/input/kbI2cBase.cpp | 64 ++++++ src/input/kbI2cBase.h | 2 + src/main.cpp | 4 + 9 files changed, 523 insertions(+), 6 deletions(-) create mode 100644 src/input/MCP23017Keyboard.cpp create mode 100644 src/input/MCP23017Keyboard.h diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index a147194a7ee..6fa4b7666f7 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -31,8 +31,8 @@ ScanI2C::FoundDevice ScanI2C::firstRTC() const ScanI2C::FoundDevice ScanI2C::firstKeyboard() const { - ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB}; - return firstOfOrNONE(6, types); + ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MCP23017, MPR121KB, TCA8418KB}; + return firstOfOrNONE(7, types); } ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 3dcbe3c6a7e..3471abbc7e0 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -70,6 +70,7 @@ class ScanI2C SCD4X, MAX30102, TPS65233, + MCP23017, MPR121KB, CGRADSENS, INA226, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 151e559f15b..3bd164c07eb 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -648,7 +648,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("BMA423", (uint8_t)addr.address); } break; - case TCA9535_ADDR: + case TCA9535_ADDR: // this can also be MCP23017_ADDR (both 0x20 case RAK120352_ADDR: case RAK120353_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1); @@ -656,8 +656,15 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = RAK12035; logFoundDevice("RAK12035", (uint8_t)addr.address); } else { - type = TCA9535; - logFoundDevice("TCA9535", (uint8_t)addr.address); + // TCA9535 only has registers 0x00-0x07; MCP23017 has IOCON at 0x0A + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); + if (registerValue != 0xFF) { + type = MCP23017; + logFoundDevice("MCP23017", (uint8_t)addr.address); + } else { + type = TCA9535; + logFoundDevice("TCA9535", (uint8_t)addr.address); + } } break; diff --git a/src/input/MCP23017Keyboard.cpp b/src/input/MCP23017Keyboard.cpp new file mode 100644 index 00000000000..edd314cb836 --- /dev/null +++ b/src/input/MCP23017Keyboard.cpp @@ -0,0 +1,380 @@ +#include "MCP23017Keyboard.h" + +// Registers +#define _MCP23017_IODIRA 0x00 +#define _MCP23017_IODIRB 0x01 +#define _MCP23017_IPOLA 0x02 +#define _MCP23017_IPOLB 0x03 +#define _MCP23017_GPINTENA 0x04 +#define _MCP23017_GPINTENB 0x05 +#define _MCP23017_DEFVALA 0x06 +#define _MCP23017_DEFVALB 0x07 +#define _MCP23017_INTCONA 0x08 +#define _MCP23017_INTCONB 0x09 +#define _MCP23017_IOCONA 0x0A +#define _MCP23017_IOCONB 0x0B +#define _MCP23017_GPPUA 0x0C +#define _MCP23017_GPPUB 0x0D +#define _MCP23017_INTFA 0x0E +#define _MCP23017_INTFB 0x0F +#define _MCP23017_INTCAPA 0x10 +#define _MCP23017_INTCAPB 0x11 +#define _MCP23017_GPIOA 0x12 +#define _MCP23017_GPIOB 0x13 + +#define _KEY_MASK 0x7FFF +#define _NUM_KEYS 15 + +// Internal codes +#define KB_NONE 0x00 +#define KB_LEFT 0xb4 +#define KB_UP 0xb5 +#define KB_DOWN 0xb6 +#define KB_RIGHT 0xb7 +#define KB_ESC 0x1b +#define KB_BSP 0x08 +#define KB_SELECT 0x0d + +// Default keymap +#ifndef CUSTOM_MCP23017_MAP + +#define MCP23017_KEYMAP_0 0 +#define MCP23017_KEYMAP_1 1 +#define MCP23017_KEYMAP_2 2 +#define MCP23017_KEYMAP_3 3 +#define MCP23017_KEYMAP_4 4 +#define MCP23017_KEYMAP_5 5 +#define MCP23017_KEYMAP_6 6 +#define MCP23017_KEYMAP_7 7 +#define MCP23017_KEYMAP_8 8 +#define MCP23017_KEYMAP_9 9 +#define MCP23017_KEYMAP_10 10 +#define MCP23017_KEYMAP_11 11 +#define MCP23017_KEYMAP_12 12 +#define MCP23017_KEYMAP_13 13 +#define MCP23017_KEYMAP_14 14 +#define MCP23017_KEYMAP_15 15 + +#define LONG_PRESS_THRESHOLD 1000 +#define MULTI_TAP_THRESHOLD 2000 + +#endif + + + // Num chars per key +uint8_t MCP23017_TapMod[15] = {1, 6, 6, 6, 6, 6, 8, 6, 8, 1, 1, 1, 1, 1, 1}; + +// Tap Map +static const unsigned char MCP23017_TapMap[15][13] = { + {' '}, // 0: K_1 + {'a', 'b', 'c', 'A', 'B', 'C'}, // 1: K_2 + {'d', 'e', 'f', 'D', 'E', 'F'}, // 2: K_3 + {'g', 'h', 'i', 'G', 'H', 'I'}, // 3: K_4 + {'j', 'k', 'l', 'J', 'K', 'L'}, // 4: K_5 + {'m', 'n', 'o', 'M', 'N', 'O'}, // 5: K_6 + {'p', 'q', 'r', 's', 'P', 'Q', 'R', 'S'}, // 6: K_7 + {'t', 'u', 'v', 'T', 'U', 'V'}, // 7: K_8 + {'w', 'x', 'y', 'z', 'W', 'X', 'Y', 'Z'}, // 8: K_9 + {'*'}, // 9: K_10 + {' '}, // 10: K_11 + {KB_BSP}, // 11: K_12 + {KB_LEFT}, // 12: K_13 + {KB_SELECT}, // 13: K_14 + {KB_RIGHT} // 14: K_15 +}; + +// Long Press map +static const unsigned char MCP23017_LongPressMap[15] = { + '1', // 0: K_1 + '2', // 1: K_2 + '3', // 2: K_3 + '4', // 3: K_4 + '5', // 4: K_5 + '6', // 5: K_6 + '7', // 6: K_7 + '8', // 7: K_8 + '9', // 8: K_9 + '*' , // 9: K_10 + '0', // 10: K_11 + '#', // 11: K_12 + KB_UP, // 12: K_13 + KB_ESC, // 13: K_14 + KB_DOWN // 14: K_15 +}; + +// Bit position to logical index translation (0-14) +uint8_t MCP23017_KeyMap[16] = { + MCP23017_KEYMAP_0, MCP23017_KEYMAP_1, MCP23017_KEYMAP_2, MCP23017_KEYMAP_3, + MCP23017_KEYMAP_4, MCP23017_KEYMAP_5, MCP23017_KEYMAP_6, MCP23017_KEYMAP_7, + MCP23017_KEYMAP_8, MCP23017_KEYMAP_9, MCP23017_KEYMAP_10, MCP23017_KEYMAP_11, + MCP23017_KEYMAP_12, MCP23017_KEYMAP_13, MCP23017_KEYMAP_14, MCP23017_KEYMAP_15 +}; + +MCP23017Keyboard::MCP23017Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nullptr), writeCallback(nullptr) { + state = Init; + last_key = UINT8_MAX; + last_tap = 0L; + char_idx = 0; + queue = ""; +} + +void MCP23017Keyboard::begin(uint8_t addr, TwoWire *wire) { + m_addr = addr; + m_wire = wire; + m_wire->begin(); + reset(); +} + +void MCP23017Keyboard::begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr) { + m_addr = addr; + m_wire = nullptr; + writeCallback = w; + readCallback = r; + reset(); +} + +void MCP23017Keyboard::reset() { + LOG_DEBUG("MCP23017 Reset\n"); + // Configure I/O + writeRegister(_MCP23017_IODIRA, 0xFF); // All set as inputs + writeRegister(_MCP23017_IODIRB, 0xFF); + + writeRegister(_MCP23017_IPOLA, 0xFF); + writeRegister(_MCP23017_IPOLB, 0xFF); + + // Enable internal pull-ups + writeRegister(_MCP23017_GPPUA, 0xFF); + writeRegister(_MCP23017_GPPUB, 0xFF); + + // Configure interrupts: Trigger on any change + writeRegister(_MCP23017_INTCONA, 0x00); + writeRegister(_MCP23017_INTCONB, 0x00); + + // Enable interrupts for each pin + writeRegister(_MCP23017_GPINTENA, 0xFF); + writeRegister(_MCP23017_GPINTENB, 0x7F); + + // IOCON: Non-mirrored, standard configuration + writeRegister(_MCP23017_IOCONA, 0x00); + + // Initial clear + readRegister16(_MCP23017_INTCAPA); + readRegister16(_MCP23017_GPIOA); + + state = Idle; +} + +void MCP23017Keyboard::attachInterrupt(uint8_t pin, void (*func)(void)) const { + LOG_DEBUG("MCP23017 attached interrupt\n\n\n"); + pinMode(pin, INPUT_PULLUP); + ::attachInterrupt(digitalPinToInterrupt(pin), func, FALLING); +} + +void MCP23017Keyboard::detachInterrupt(uint8_t pin) const { + LOG_DEBUG("MCP23017 deattached interrupt\n\n\n"); + ::detachInterrupt(pin); +} + +uint16_t MCP23017Keyboard::status() const { + return readRegister16(_MCP23017_GPIOA); +} + +uint8_t MCP23017Keyboard::keyCount() const { + uint16_t keyRegister = readRegister16(_MCP23017_GPIOA); + return keyCount(keyRegister); +} + +uint8_t MCP23017Keyboard::keyCount(uint16_t value) const { + uint16_t buttonState = value & _KEY_MASK; + uint8_t numButtonsPressed = 0; + for (uint8_t i = 0; i < 15; ++i) { + if (buttonState & (1 << i)) { + numButtonsPressed++; + } + } + return numButtonsPressed; +} + +bool MCP23017Keyboard::hasEvent() { + return queue.length() > 0; +} + +void MCP23017Keyboard::queueEvent(char next) { + if (next == KB_NONE) return; + queue.concat(next); +} + +char MCP23017Keyboard::dequeueEvent() { + if (queue.length() < 1) return KB_NONE; + char next = queue.charAt(0); + queue.remove(0, 1); + return next; +} + +void MCP23017Keyboard::trigger() { + // Called periodically to poll the keyboard state + if (state != Init) { + uint16_t keyRegister = readRegister16(_MCP23017_GPIOA); // Read the state of all 16 pins (GPIO A & B) + readRegister16(_MCP23017_INTCAPA); // Clear INT latch + uint8_t keysPressed = keyCount(keyRegister); //Get key pressed + + if (keysPressed == 0) { // No button pressed + if (state == Held) + { + released(); + } + state = Idle; + return; + } + if (keysPressed == 1) { // Button pressed + if (state == Held || state == HeldLong) { + held(keyRegister); + } + if (state == Idle) { + pressed(keyRegister); + } + return; + } + if (keysPressed > 1) { // Multibutton pressed + state = Busy; + return; + } + } else { + reset(); + } +} + +void MCP23017Keyboard::pressed(uint16_t keyRegister) { + if (state == Init || state == Busy) return; + if (keyCount(keyRegister) != 1) { + return; + } else { + } + + uint16_t buttonState = keyRegister & _KEY_MASK; + uint8_t next_pin = 0; + for (uint8_t i = 0; i < 15; ++i) { + if (buttonState & (1 << i)) { + next_pin = i; + break; + } + } + + uint8_t next_key = MCP23017_KeyMap[next_pin]; + uint32_t now = millis(); + int32_t tap_interval = now - last_tap; + + if (tap_interval < 0) { + // long running, millis has overflowed. + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; + state = Held; + return; +} + +void MCP23017Keyboard::held(uint16_t keyRegister) { + if (state == Init || state == Busy) return; + if (keyCount(keyRegister) != 1) return; + + LOG_DEBUG("Held"); + uint16_t buttonState = keyRegister & _KEY_MASK; + uint8_t next_pin = 0; + for (uint8_t i = 0; i < 15; ++i) { + if (buttonState & (1 << i)) { + next_pin = i; + break; + } + } + + uint8_t next_key = MCP23017_KeyMap[next_pin]; + uint32_t now = millis(); + int32_t held_interval = now - last_tap; + + if (held_interval < 0 || next_key != last_key) { + last_tap = 0; + state = Busy; + return; + } + + if (held_interval > LONG_PRESS_THRESHOLD) { + state = HeldLong; + queueEvent(MCP23017_LongPressMap[last_key]); + last_tap = now; + } +} + +void MCP23017Keyboard::released() { + + if (state != Held) return; + + // Check valid index + if (last_key >= _NUM_KEYS) { + last_key = UINT8_MAX; + state = Idle; + return; + } + + // Multitap - clear old character + if (char_idx > 0 && MCP23017_TapMod[last_key] > 1) { + queueEvent(KB_BSP); + LOG_DEBUG("Multi Press, Backspace"); + } + + // Send new character + queueEvent(MCP23017_TapMap[last_key][(char_idx % MCP23017_TapMod[last_key])]); +} + +uint8_t MCP23017Keyboard::readRegister8(uint8_t reg) const { + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); + m_wire->requestFrom(m_addr, (uint8_t)1); + if (m_wire->available() < 1) return 0; + return m_wire->read(); + } + if (readCallback) { + uint8_t data; + readCallback(m_addr, reg, &data, 1); + return data; + } + return 0; +} + +uint16_t MCP23017Keyboard::readRegister16(uint8_t reg) const { + uint8_t data[2] = {0}; + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->endTransmission(); + m_wire->requestFrom(m_addr, (uint8_t)2); + if (m_wire->available() < 2) return 0; + data[0] = m_wire->read(); + data[1] = m_wire->read(); + } else if (readCallback) { + readCallback(m_addr, reg, data, 2); + } + return (data[1] << 8) | data[0]; +} + +void MCP23017Keyboard::writeRegister(uint8_t reg, uint8_t value) { + if (m_wire) { + m_wire->beginTransmission(m_addr); + m_wire->write(reg); + m_wire->write(value); + m_wire->endTransmission(); + } else if (writeCallback) { + writeCallback(m_addr, reg, &value, 1); + } +} \ No newline at end of file diff --git a/src/input/MCP23017Keyboard.h b/src/input/MCP23017Keyboard.h new file mode 100644 index 00000000000..f66b564f127 --- /dev/null +++ b/src/input/MCP23017Keyboard.h @@ -0,0 +1,55 @@ +#pragma once + +#include "configuration.h" +#include +#include + +#define MCP23017_KB_ADDR 0x20 // Default address (A0, A1 y A2 to GND) + +class MCP23017Keyboard { + public: + typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); + + enum States { Init = 0, Idle, Held, HeldLong, Busy }; + + States state; + + uint8_t last_key; + uint32_t last_tap; + uint8_t char_idx; + + String queue; + + MCP23017Keyboard(); + + void begin(uint8_t addr = MCP23017_KB_ADDR, TwoWire *wire = &Wire); + void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = MCP23017_KB_ADDR); + + void reset(void); + + void attachInterrupt(uint8_t pin, void (*func)(void)) const; + void detachInterrupt(uint8_t pin) const; + + void trigger(void); + void pressed(uint16_t value); + void held(uint16_t value); + void released(void); + + uint16_t status(void) const; + uint8_t keyCount(void) const; + uint8_t keyCount(uint16_t value) const; + + bool hasEvent(void); + char dequeueEvent(void); + void queueEvent(char); + + uint8_t readRegister8(uint8_t reg) const; + uint16_t readRegister16(uint8_t reg) const; + void writeRegister(uint8_t reg, uint8_t value); + + private: + TwoWire *m_wire; + uint8_t m_addr; + i2c_com_fptr_t readCallback; + i2c_com_fptr_t writeCallback; +}; \ No newline at end of file diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index cb03eb4ff66..d0e95d0ddf3 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -12,7 +12,7 @@ void CardKbI2cImpl::init() #if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) if (cardkb_found.address == 0x00) { LOG_DEBUG("Rescan for I2C keyboard"); - uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; + uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MCP23017_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; #if defined(T_LORA_PAGER) uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); #else @@ -43,6 +43,10 @@ void CardKbI2cImpl::init() // assign an arbitrary value to distinguish from other models kb_model = 0x11; break; + case ScanI2C::DeviceType::MCP23017: + // assign an arbitrary value to distinguish from other models + kb_model = 0x20; + break; case ScanI2C::DeviceType::MPR121KB: // assign an arbitrary value to distinguish from other models kb_model = 0x37; diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 510fb1e31df..bbd32db6ea7 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -79,6 +79,9 @@ int32_t KbI2cBase::runOnce() Q10keyboard.begin(BBQ10_KB_ADDR, &Wire); Q10keyboard.setBacklight(0); } + if (cardkb_found.address == MCP23017_KB_ADDR) { + MCPkeyboard.begin(MCP23017_KB_ADDR, &Wire); + } if (cardkb_found.address == MPR121_KB_ADDR) { MPRkeyboard.begin(MPR121_KB_ADDR, &Wire); } @@ -388,6 +391,67 @@ int32_t KbI2cBase::runOnce() break; } case 0x00: // CARDKB + case 0x20: { // MCP23017 (Dirección I2C por defecto) + MCPkeyboard.trigger(); + InputEvent e = {}; + + while (MCPkeyboard.hasEvent()) { + char nextEvent = MCPkeyboard.dequeueEvent(); + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x00; + e.source = this->_originName; + + switch (nextEvent) { + case 0x00: // KB_NONE + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + case 0xb4: // KB_LEFT + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0x00; + break; + case 0xb5: // KB_UP + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0x00; + break; + case 0xb6: // KB_DOWN + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0x00; + break; + case 0xb7: // KB_RIGHT + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0x00; + break; + case 0x1b: // KB_ESC + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; + break; + case 0x08: // KB_BSP + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0x08; + break; + case 0x0d: // KB_SELECT + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; + break; + default: + if (nextEvent > 127) { // Invalid key, ignore it + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0x00; + break; + } + // Normal character + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = nextEvent; + break; + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + } + break; + } case 0x10: { // T-DECK i2cBus->requestFrom((int)cardkb_found.address, 1); diff --git a/src/input/kbI2cBase.h b/src/input/kbI2cBase.h index ae769dff856..6fe652973cf 100644 --- a/src/input/kbI2cBase.h +++ b/src/input/kbI2cBase.h @@ -2,6 +2,7 @@ #include "BBQ10Keyboard.h" #include "InputBroker.h" +#include "MCP23017Keyboard.h" #include "MPR121Keyboard.h" #include "Wire.h" #include "concurrency/OSThread.h" @@ -23,6 +24,7 @@ class KbI2cBase : public Observable, public concurrency::OST TwoWire *i2cBus = 0; BBQ10Keyboard Q10keyboard; + MCP23017Keyboard MCPkeyboard; MPR121Keyboard MPRkeyboard; TCA8418KeyboardBase &TCAKeyboard; bool is_sym = false; diff --git a/src/main.cpp b/src/main.cpp index cc7c239a4e0..90ca07b05ed 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -716,6 +716,10 @@ void setup() // assign an arbitrary value to distinguish from other models kb_model = 0x11; break; + case ScanI2C::DeviceType::MCP23017: + // assign an arbitrary value to distinguish from other models + kb_model = 0x20; + break; case ScanI2C::DeviceType::MPR121KB: // assign an arbitrary value to distinguish from other models kb_model = 0x37;