/* xdrv_67_mcp23xxx.ino - MCP23008/MCP23017/MCP23S17 GPIO Expander support for Tasmota SPDX-FileCopyrightText: 2023 Theo Arends SPDX-License-Identifier: GPL-3.0-only */ #if defined(USE_I2C) || defined(USE_SPI) #ifdef USE_MCP23XXX_DRV /*********************************************************************************************\ * MCP23008/17 (I2C) and MCP23S17 (SPI) GPIO Expander to be used as virtual button/switch/relay only * * Docs at https://www.microchip.com/wwwproducts/en/MCP23008 * https://www.microchip.com/wwwproducts/en/MCP23017 * * I2C Address: 0x20 - 0x26 (0x27 is not supported) * * The goal of the driver is to provide a sequential list of pins configured as Tasmota template * and handle any input and output as configured GPIOs. * * Restrictions: * - Uses incremental I2C addresses / SPI Chip select until template pin count reached * - Max support for 28 switches (input), 32 buttons (input), 32 relays (output) * * Supported template fields: * NAME - Template name * BASE - Optional. 0 = use relative buttons and switches (default), 1 = use absolute buttons and switches * GPIO - Sequential list of pin 1 and up with configured GPIO function * Function Code Description * ------------------- -------- ---------------------------------------- * None 0 Not used * Button1..32 B 32..63 Button to Gnd with internal pullup * Button_n1..32 Bn 64..95 Button to Gnd without internal pullup * Button_i1..32 Bi 96..127 Button inverted to Vcc with internal pullup * Button_in1..32 Bin 128..159 Button inverted to Vcc without internal pullup * Switch1..28 S 160..187 Switch to Gnd with internal pullup * Switch_n1..28 Sn 192..219 Switch to Gnd without internal pullup * Relay1..32 R 224..255 Relay * Relay_i1..32 Ri 256..287 Relay inverted * Output_Hi Oh 3840 Fixed output high * Output_lo Ol 3872 Fixed output low * * Prepare a template to be loaded either by: * - a rule like: rule3 on file#mcp23x.dat do {"NAME":"MCP23017 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} endon * - a script like: -y{"NAME":"MCP23017 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} * - file called mcp23x.dat with contents: {"NAME":"MCP23017 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} * * S3 S2 B2 B3 Oh B1 S1 R1 R4 R2 R3 S4 * {"NAME":"MCP23S17 Shelly Pro 4PM","GPIO":[194,193,65,66,3840,64,192,0,224,0,0,0,227,225,226,195]} * * Inverted relays and buttons Ri8 Ri7 Ri6 Ri5 Ri4 Ri3 Ri2 Ri1 B1 B2 B3 B4 B5 B6 B7 B8 * {"NAME":"MCP23017 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} * * Inverted relays and buttons Ri1 Ri2 Ri3 Ri4 Ri5 Ri6 Ri7 Ri8 B1 B2 B3 B4 B5 B6 B7 B8 * {"NAME":"MCP23017 A=Ri1-8, B=B1-8","GPIO":[256,257,258,259,260,261,262,263,32,33,34,35,36,37,38,39]} * * Relays and buttons R1 R2 R3 R4 R5 R6 R7 R8 B1 B2 B3 B4 B5 B6 B7 B8 * {"NAME":"MCP23017 A=R1-8, B=B1-8","GPIO":[224,225,226,227,228,229,230,231,32,33,34,35,36,37,38,39]} * * Buttons and relays B1 B2 B3 B4 B5 B6 B7 B8 R1 R2 R3 R4 R5 R6 R7 R8 * {"NAME":"MCP23017 A=B1-8, B=R1-8","GPIO":[32,33,34,35,36,37,38,39,224,225,226,227,228,229,230,231]} * * Buttons, relays, buttons and relays B1 B2 B3 B4 B5 B6 B7 B8 R1 R2 R3 R4 R5 R6 R7 R8 B9 B10B11B12B13B14B15B16R9 R10 R11 R12 R13 R14 R15 R16 * {"NAME":"MCP23017 A=B1-8, B=R1-8, C=B9-16, D=R9-16","GPIO":[32,33,34,35,36,37,38,39,224,225,226,227,228,229,230,231,40,41,42,43,44,45,46,47,232,233,234,235,236,237,238,239]} * * {"NAME":"MCP23017 A=R1-8, B=B1-8, C=R9-16, D=B9-16","GPIO":[224,225,226,227,228,229,230,231,32,33,34,35,36,37,38,39,232,233,234,235,236,237,238,239,40,41,42,43,44,45,46,47]} * * 32 relays R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 R12 R13 R14 R15 R16 R17 R18 R19 R20 R21 R22 R23 R24 R25 R26 R27 R28 R29 R30 R31 R32 * {"NAME":"MCP23017 A=Ri1-8, B=Ri9-16, C=Ri17-24, D=Ri25-32","GPIO":[256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287]} * {"NAME":"MCP23017 A=R1-8, B=R9-16, C=R17-24, D=R25-32","GPIO":[224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255]} * \*********************************************************************************************/ #define XDRV_67 67 #define XI2C_77 77 // See I2CDEVICES.md #define MCP23XXX_ADDR_START 0x20 // 32 #define MCP23XXX_ADDR_END 0x26 // 38 #define MCP23XXX_MAX_DEVICES 6 #define MCP23XXX_SPI_CLOCK 1000000 // SPI clock speed set to 1MHz in case of signal interference at higher speed (Max is 10MHz) /*********************************************************************************************\ * MCP23017 support \*********************************************************************************************/ enum MCP23S08GPIORegisters { MCP23X08_IODIR = 0x00, MCP23X08_IPOL = 0x01, MCP23X08_GPINTEN = 0x02, MCP23X08_DEFVAL = 0x03, MCP23X08_INTCON = 0x04, MCP23X08_IOCON = 0x05, MCP23X08_GPPU = 0x06, MCP23X08_INTF = 0x07, MCP23X08_INTCAP = 0x08, MCP23X08_GPIO = 0x09, MCP23X08_OLAT = 0x0A, }; enum MCP23X17GPIORegisters { // A side MCP23X17_IODIRA = 0x00, MCP23X17_IPOLA = 0x02, MCP23X17_GPINTENA = 0x04, MCP23X17_DEFVALA = 0x06, MCP23X17_INTCONA = 0x08, MCP23X17_IOCONA = 0x0A, MCP23X17_GPPUA = 0x0C, MCP23X17_INTFA = 0x0E, MCP23X17_INTCAPA = 0x10, MCP23X17_GPIOA = 0x12, MCP23X17_OLATA = 0x14, // B side MCP23X17_IODIRB = 0x01, MCP23X17_IPOLB = 0x03, MCP23X17_GPINTENB = 0x05, MCP23X17_DEFVALB = 0x07, MCP23X17_INTCONB = 0x09, MCP23X17_IOCONB = 0x0B, MCP23X17_GPPUB = 0x0D, MCP23X17_INTFB = 0x0F, MCP23X17_INTCAPB = 0x11, MCP23X17_GPIOB = 0x13, MCP23X17_OLATB = 0x15, }; enum MCP23XInterruptMode { MCP23XXX_NO_INTERRUPT, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING }; enum MCP23XInterfaces { MCP23X_I2C, MCP23X_SPI }; typedef struct { uint8_t olata; uint8_t olatb; uint8_t address; uint8_t interface; uint8_t pins; // 8 (MCP23x08) or 16 (MCP23x17) int8_t pin_cs; int8_t pin_int; } tMcp23xDevice; struct MCP230 { tMcp23xDevice device[MCP23XXX_MAX_DEVICES]; uint32_t relay_inverted; uint32_t button_inverted; uint8_t chip; uint8_t max_devices; uint8_t max_pins; uint8_t relay_max; uint8_t relay_offset; uint8_t button_max; uint8_t switch_max; int8_t button_offset; int8_t switch_offset; bool base; bool interrupt; } Mcp23x; uint16_t *Mcp23x_gpio_pin = nullptr; /*********************************************************************************************\ * MCP23x17 - SPI and I2C \*********************************************************************************************/ #ifdef USE_SPI void MCP23xEnable(void) { SPI.beginTransaction(SPISettings(MCP23XXX_SPI_CLOCK, MSBFIRST, SPI_MODE0)); digitalWrite(Mcp23x.device[Mcp23x.chip].pin_cs, 0); } void MCP23xDisable(void) { SPI.endTransaction(); digitalWrite(Mcp23x.device[Mcp23x.chip].pin_cs, 1); } #endif void MCP23xDumpRegs(void) { uint8_t data[22]; for (Mcp23x.chip = 0; Mcp23x.chip < Mcp23x.max_devices; Mcp23x.chip++) { uint32_t data_size = sizeof(data); if (8 == Mcp23x.device[Mcp23x.chip].pins) { data_size /= 2; } #ifdef USE_SPI if (MCP23X_SPI == Mcp23x.device[Mcp23x.chip].interface) { MCP23xEnable(); SPI.transfer(Mcp23x.device[Mcp23x.chip].address | 1); SPI.transfer(0); for (uint32_t i = 0; i < data_size; i++) { data[i] = SPI.transfer(0xFF); } MCP23xDisable(); } #endif #ifdef USE_I2C if (MCP23X_I2C == Mcp23x.device[Mcp23x.chip].interface) { I2cReadBuffer(Mcp23x.device[Mcp23x.chip].address, 0, data, data_size); } #endif AddLog(LOG_LEVEL_DEBUG, PSTR("MCP: Intf %d, Address %02X, Regs %*_H"), Mcp23x.device[Mcp23x.chip].interface, Mcp23x.device[Mcp23x.chip].address, data_size, data); } } uint32_t MCP23xRead16(uint8_t reg) { // Read 16-bit registers: (regb << 8) | rega uint32_t value = 0; #ifdef USE_SPI if (MCP23X_SPI == Mcp23x.device[Mcp23x.chip].interface) { MCP23xEnable(); SPI.transfer(Mcp23x.device[Mcp23x.chip].address | 1); SPI.transfer(reg); value = SPI.transfer(0xFF); // RegA value |= (SPI.transfer(0xFF) << 8); // RegB MCP23xDisable(); } #endif #ifdef USE_I2C if (MCP23X_I2C == Mcp23x.device[Mcp23x.chip].interface) { value = I2cRead16LE(Mcp23x.device[Mcp23x.chip].address, reg); } #endif return value; } uint32_t MCP23xRead(uint8_t reg) { uint32_t value = 0; #ifdef USE_SPI if (MCP23X_SPI == Mcp23x.device[Mcp23x.chip].interface) { MCP23xEnable(); SPI.transfer(Mcp23x.device[Mcp23x.chip].address | 1); SPI.transfer(reg); value = SPI.transfer(0xFF); MCP23xDisable(); } #endif #ifdef USE_I2C if (MCP23X_I2C == Mcp23x.device[Mcp23x.chip].interface) { value = I2cRead8(Mcp23x.device[Mcp23x.chip].address, reg); } #endif return value; } bool MCP23xValidRead(uint8_t reg, uint8_t *data) { #ifdef USE_SPI if (MCP23X_SPI == Mcp23x.device[Mcp23x.chip].interface) { MCP23xEnable(); SPI.transfer(Mcp23x.device[Mcp23x.chip].address | 1); SPI.transfer(reg); *data = SPI.transfer(0xFF); MCP23xDisable(); return true; } #endif #ifdef USE_I2C if (MCP23X_I2C == Mcp23x.device[Mcp23x.chip].interface) { return I2cValidRead8(data, Mcp23x.device[Mcp23x.chip].address, reg); } return false; #endif } void MCP23xWrite(uint8_t reg, uint8_t value) { #ifdef USE_SPI if (MCP23X_SPI == Mcp23x.device[Mcp23x.chip].interface) { MCP23xEnable(); SPI.transfer(Mcp23x.device[Mcp23x.chip].address); SPI.transfer(reg); SPI.transfer(value); MCP23xDisable(); } #endif #ifdef USE_I2C if (MCP23X_I2C == Mcp23x.device[Mcp23x.chip].interface) { I2cWrite8(Mcp23x.device[Mcp23x.chip].address, reg, value); } #endif } /*********************************************************************************************/ void MCP23xUpdate(uint8_t pin, bool pin_value, uint8_t reg_addr) { // pin = 0 - 15 uint8_t bit = pin % 8; uint8_t reg_value = 0; if (reg_addr == MCP23X17_OLATA) { reg_value = Mcp23x.device[Mcp23x.chip].olata; if (8 == Mcp23x.device[Mcp23x.chip].pins) { reg_addr = MCP23X08_OLAT; } } else if (reg_addr == MCP23X17_OLATB) { reg_value = Mcp23x.device[Mcp23x.chip].olatb; } else { reg_value = MCP23xRead(reg_addr); } if (pin_value) { reg_value |= 1 << bit; } else { reg_value &= ~(1 << bit); } MCP23xWrite(reg_addr, reg_value); if ((8 == Mcp23x.device[Mcp23x.chip].pins) && (reg_addr == MCP23X08_OLAT)) { reg_addr = MCP23X17_OLATA; } if (reg_addr == MCP23X17_OLATA) { Mcp23x.device[Mcp23x.chip].olata = reg_value; } else if (reg_addr == MCP23X17_OLATB) { Mcp23x.device[Mcp23x.chip].olatb = reg_value; } } /*********************************************************************************************/ uint32_t MCP23xSetChip(uint8_t pin) { // Calculate chip based on number of pins per chip. 8 for MCP23x08, 16 for MCP23x17 // pin 0 - 63 for (Mcp23x.chip = 0; Mcp23x.chip < Mcp23x.max_devices; Mcp23x.chip++) { if (Mcp23x.device[Mcp23x.chip].pins > pin) { break; } pin -= Mcp23x.device[Mcp23x.chip].pins; } return pin; // relative pin number within chip (0 ... 7 or 0 ... 15) } void MCP23xPinMode(uint8_t pin, uint8_t flags) { // pin 0 - 63 pin = MCP23xSetChip(pin); uint8_t iodir; uint8_t gppu; if (8 == Mcp23x.device[Mcp23x.chip].pins) { iodir = MCP23X08_IODIR; gppu = MCP23X08_GPPU; } else { iodir = pin < 8 ? MCP23X17_IODIRA : MCP23X17_IODIRB; gppu = pin < 8 ? MCP23X17_GPPUA : MCP23X17_GPPUB; } switch (flags) { case INPUT: MCP23xUpdate(pin, true, iodir); MCP23xUpdate(pin, false, gppu); break; case INPUT_PULLUP: MCP23xUpdate(pin, true, iodir); MCP23xUpdate(pin, true, gppu); break; case OUTPUT: MCP23xUpdate(pin, false, iodir); break; } // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: MCP23xPinMode chip %d, pin %d, flags %d, regs %d,%d"), Mcp23x.chip, pin, flags, iodir, gppu); } void MCP23xPinInterruptMode(uint8_t pin, uint8_t interrupt_mode) { // pin 0 - 63 pin = MCP23xSetChip(pin); uint8_t gpinten; uint8_t intcon; uint8_t defval; if (8 == Mcp23x.device[Mcp23x.chip].pins) { gpinten = MCP23X08_GPINTEN; intcon = MCP23X08_INTCON; defval = MCP23X08_DEFVAL; } else { gpinten = pin < 8 ? MCP23X17_GPINTENA : MCP23X17_GPINTENB; intcon = pin < 8 ? MCP23X17_INTCONA : MCP23X17_INTCONB; defval = pin < 8 ? MCP23X17_DEFVALA : MCP23X17_DEFVALB; } switch (interrupt_mode) { case MCP23XXX_CHANGE: MCP23xUpdate(pin, true, gpinten); MCP23xUpdate(pin, false, intcon); break; case MCP23XXX_RISING: MCP23xUpdate(pin, true, gpinten); MCP23xUpdate(pin, true, intcon); MCP23xUpdate(pin, true, defval); break; case MCP23XXX_FALLING: MCP23xUpdate(pin, true, gpinten); MCP23xUpdate(pin, true, intcon); MCP23xUpdate(pin, false, defval); break; case MCP23XXX_NO_INTERRUPT: MCP23xUpdate(pin, false, gpinten); break; } } void MCP23xSetPinModes(uint8_t pin, uint8_t flags) { // pin 0 - 63 MCP23xPinMode(pin, flags); if (Mcp23x.device[Mcp23x.chip].pin_int > -1) { // Mcp23x.chip is updated by call to MCP23xPinMode MCP23xPinInterruptMode(pin, MCP23XXX_CHANGE); } } bool MCP23xDigitalRead(uint8_t pin) { // pin 0 - 63 pin = MCP23xSetChip(pin); uint8_t bit = pin % 8; uint8_t reg_addr; if (8 == Mcp23x.device[Mcp23x.chip].pins) { reg_addr = MCP23X08_GPIO; } else { reg_addr = pin < 8 ? MCP23X17_GPIOA : MCP23X17_GPIOB; } uint8_t value = MCP23xRead(reg_addr); return value & (1 << bit); } void MCP23xDigitalWrite(uint8_t pin, bool value) { // pin 0 - 63 pin = MCP23xSetChip(pin); uint8_t reg_addr = pin < 8 ? MCP23X17_OLATA : MCP23X17_OLATB; // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: MCP23xDigitalWrite chip %d, pin %d, state %d, reg %d"), Mcp23x.chip, pin, value, reg_addr); MCP23xUpdate(pin, value, reg_addr); } /*********************************************************************************************\ * Tasmota \*********************************************************************************************/ int MCP23xPin(uint32_t gpio, uint32_t index = 0); int MCP23xPin(uint32_t gpio, uint32_t index) { uint16_t real_gpio = gpio << 5; uint16_t mask = 0xFFE0; if (index < GPIO_ANY) { real_gpio += index; mask = 0xFFFF; } for (uint32_t i = 0; i < Mcp23x.max_pins; i++) { if ((Mcp23x_gpio_pin[i] & mask) == real_gpio) { return i; // Pin number configured for gpio } } return -1; // No pin used for gpio } bool MCP23xPinUsed(uint32_t gpio, uint32_t index = 0); bool MCP23xPinUsed(uint32_t gpio, uint32_t index) { return (MCP23xPin(gpio, index) >= 0); } uint32_t MCP23xGetPin(uint32_t lpin) { if (lpin < Mcp23x.max_pins) { return Mcp23x_gpio_pin[lpin]; } else { return GPIO_NONE; } } /*********************************************************************************************/ String MCP23xTemplateLoadFile(void) { String mcptmplt = ""; #ifdef USE_UFILESYS mcptmplt = TfsLoadString("/mcp23x.dat"); #endif // USE_UFILESYS #ifdef USE_RULES if (!mcptmplt.length()) { mcptmplt = RuleLoadFile("MCP23X.DAT"); } #endif // USE_RULES #ifdef USE_SCRIPT if (!mcptmplt.length()) { mcptmplt = ScriptLoadSection(">y"); } #endif // USE_SCRIPT return mcptmplt; } bool MCP23xLoadTemplate(void) { String mcptmplt = MCP23xTemplateLoadFile(); uint32_t len = mcptmplt.length() +1; if (len < 7) { return false; } // No McpTmplt found JsonParser parser((char*)mcptmplt.c_str()); JsonParserObject root = parser.getRootObject(); if (!root) { return false; } // rule3 on file#mcp23x.dat do {"NAME":"MCP23017","GPIO":[32,33,34,35,36,37,38,39,224,225,226,227,228,229,230,231]} endon // rule3 on file#mcp23x.dat do {"NAME":"MCP23017","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} endon // rule3 on file#mcp23x.dat do {"NAME":"MCP23017 A=Ri8-1, B=B1-8","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39]} endon // rule3 on file#mcp23x.dat do {"NAME":"MCP23017 A=Ri8-1, B=B1-8, C=Ri16-9, D=B9-16","GPIO":[263,262,261,260,259,258,257,256,32,33,34,35,36,37,38,39,271,270,269,268,267,266,265,264,40,41,42,43,44,45,46,47]} endon // {"NAME":"MCP23017","GPIO":[32,33,34,35,36,37,38,39,224,225,226,227,228,229,230,231]} // {"NAME":"MCP23017","GPIO":[32,33,34,35,36,37,38,39,224,225,226,227,228,229,230,231,40,41,42,43,44,45,46,47,232,233,234,235,236,237,238,239]} JsonParserToken val = root[PSTR(D_JSON_BASE)]; if (val) { Mcp23x.base = (val.getUInt()) ? true : false; } val = root[PSTR(D_JSON_NAME)]; if (val) { AddLog(LOG_LEVEL_DEBUG, PSTR("MCP: Base %d, Template '%s'"), Mcp23x.base, val.getStr()); } JsonParserArray arr = root[PSTR(D_JSON_GPIO)]; if (arr) { uint32_t pin = 0; for (pin; pin < Mcp23x.max_pins; pin++) { // Max number of detected chip pins JsonParserToken val = arr[pin]; if (!val) { break; } uint16_t mpin = val.getUInt(); if (mpin) { // Above GPIO_NONE if ((mpin >= AGPIO(GPIO_SWT1)) && (mpin < (AGPIO(GPIO_SWT1) + MAX_SWITCHES_SET))) { Mcp23x.switch_max++; MCP23xSetPinModes(pin, INPUT_PULLUP); } else if ((mpin >= AGPIO(GPIO_SWT1_NP)) && (mpin < (AGPIO(GPIO_SWT1_NP) + MAX_SWITCHES_SET))) { mpin -= (AGPIO(GPIO_SWT1_NP) - AGPIO(GPIO_SWT1)); Mcp23x.switch_max++; MCP23xSetPinModes(pin, INPUT); } else if ((mpin >= AGPIO(GPIO_KEY1)) && (mpin < (AGPIO(GPIO_KEY1) + MAX_KEYS_SET))) { Mcp23x.button_max++; MCP23xSetPinModes(pin, INPUT_PULLUP); } else if ((mpin >= AGPIO(GPIO_KEY1_NP)) && (mpin < (AGPIO(GPIO_KEY1_NP) + MAX_KEYS_SET))) { mpin -= (AGPIO(GPIO_KEY1_NP) - AGPIO(GPIO_KEY1)); Mcp23x.button_max++; MCP23xSetPinModes(pin, INPUT); } else if ((mpin >= AGPIO(GPIO_KEY1_INV)) && (mpin < (AGPIO(GPIO_KEY1_INV) + MAX_KEYS_SET))) { bitSet(Mcp23x.button_inverted, mpin - AGPIO(GPIO_KEY1_INV)); mpin -= (AGPIO(GPIO_KEY1_INV) - AGPIO(GPIO_KEY1)); Mcp23x.button_max++; MCP23xSetPinModes(pin, INPUT_PULLUP); } else if ((mpin >= AGPIO(GPIO_KEY1_INV_NP)) && (mpin < (AGPIO(GPIO_KEY1_INV_NP) + MAX_KEYS_SET))) { bitSet(Mcp23x.button_inverted, mpin - AGPIO(GPIO_KEY1_INV_NP)); mpin -= (AGPIO(GPIO_KEY1_INV_NP) - AGPIO(GPIO_KEY1)); Mcp23x.button_max++; MCP23xSetPinModes(pin, INPUT); } else if ((mpin >= AGPIO(GPIO_REL1)) && (mpin < (AGPIO(GPIO_REL1) + MAX_RELAYS_SET))) { Mcp23x.relay_max++; MCP23xPinMode(pin, OUTPUT); } else if ((mpin >= AGPIO(GPIO_REL1_INV)) && (mpin < (AGPIO(GPIO_REL1_INV) + MAX_RELAYS_SET))) { bitSet(Mcp23x.relay_inverted, mpin - AGPIO(GPIO_REL1_INV)); mpin -= (AGPIO(GPIO_REL1_INV) - AGPIO(GPIO_REL1)); Mcp23x.relay_max++; MCP23xPinMode(pin, OUTPUT); } else if (mpin == AGPIO(GPIO_OUTPUT_HI)) { MCP23xPinMode(pin, OUTPUT); MCP23xDigitalWrite(pin, 1); } else if (mpin == AGPIO(GPIO_OUTPUT_LO)) { MCP23xPinMode(pin, OUTPUT); MCP23xDigitalWrite(pin, 0); } else { mpin = 0; } Mcp23x_gpio_pin[pin] = mpin; } if ((Mcp23x.switch_max >= MAX_SWITCHES_SET) || (Mcp23x.button_max >= MAX_KEYS_SET) || (Mcp23x.relay_max >= MAX_RELAYS_SET)) { AddLog(LOG_LEVEL_INFO, PSTR("MCP: Max reached (S%d/B%d/R%d)"), Mcp23x.switch_max, Mcp23x.button_max, Mcp23x.relay_max); break; } } Mcp23x.max_pins = pin; // Max number of configured pins } // AddLog(LOG_LEVEL_DEBUG, PSTR("MCP: Pins %d, Mcp23x_gpio_pin %*_V"), Mcp23x.max_pins, Mcp23x.max_pins, (uint8_t*)Mcp23x_gpio_pin); return true; } uint32_t MCP23xTemplateGpio(void) { String mcptmplt = MCP23xTemplateLoadFile(); uint32_t len = mcptmplt.length() +1; if (len < 7) { return 0; } // No McpTmplt found JsonParser parser((char*)mcptmplt.c_str()); JsonParserObject root = parser.getRootObject(); if (!root) { return 0; } JsonParserArray arr = root[PSTR(D_JSON_GPIO)]; if (arr.isArray()) { return arr.size(); // Number of requested pins } return 0; } void MCP23xModuleInit(void) { int32_t pins_needed = MCP23xTemplateGpio(); if (!pins_needed) { AddLog(LOG_LEVEL_DEBUG, PSTR("MCP: Invalid template")); return; } #ifdef USE_SPI if ((SPI_MOSI_MISO == TasmotaGlobal.spi_enabled) && PinUsed(GPIO_MCP23SXX_CS, GPIO_ANY)) { #ifdef ESP8266 SPI.begin(); #endif #ifdef ESP32 SPI.begin(Pin(GPIO_SPI_CLK), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_MOSI), -1); #endif while ((Mcp23x.max_devices < MCP23XXX_MAX_DEVICES) && PinUsed(GPIO_MCP23SXX_CS, Mcp23x.max_devices)) { Mcp23x.chip = Mcp23x.max_devices; Mcp23x.device[Mcp23x.chip].pin_int = (PinUsed(GPIO_MCP23XXX_INT, Mcp23x.chip)) ? Pin(GPIO_MCP23XXX_INT, Mcp23x.chip) : -1; Mcp23x.device[Mcp23x.chip].pin_cs = Pin(GPIO_MCP23SXX_CS, Mcp23x.max_devices); digitalWrite(Mcp23x.device[Mcp23x.chip].pin_cs, 1); pinMode(Mcp23x.device[Mcp23x.chip].pin_cs, OUTPUT); Mcp23x.device[Mcp23x.chip].interface = MCP23X_SPI; Mcp23x.device[Mcp23x.chip].address = MCP23XXX_ADDR_START << 1; MCP23xWrite(MCP23X08_IOCON, 0x80); // Attempt to set bank mode - this will only work on MCP23x17, so its the best way to detect the different chips 23x08 vs 23x17 uint8_t buffer; if (MCP23xValidRead(MCP23X08_IOCON, &buffer)) { if (0x00 == buffer) { // MCP23S08 AddLog(LOG_LEVEL_INFO, PSTR("SPI: MCP23S08 found at CS%d"), Mcp23x.chip +1); Mcp23x.device[Mcp23x.chip].pins = 8; MCP23xWrite(MCP23X08_IOCON, 0b00011000); // Enable INT mirror, Slew rate disabled, HAEN pins for addressing Mcp23x.device[Mcp23x.chip].olata = MCP23xRead(MCP23X08_OLAT); } else if (0x80 == buffer) { // MCP23S17 AddLog(LOG_LEVEL_INFO, PSTR("SPI: MCP23S17 found at CS%d"), Mcp23x.chip +1); Mcp23x.device[Mcp23x.chip].pins = 16; MCP23xWrite(MCP23X17_IOCONA, 0b01011000); // Enable INT mirror, Slew rate disabled, HAEN pins for addressing Mcp23x.device[Mcp23x.chip].olata = MCP23xRead(MCP23X17_OLATA); Mcp23x.device[Mcp23x.chip].olatb = MCP23xRead(MCP23X17_OLATB); } } Mcp23x.max_devices++; Mcp23x.max_pins += Mcp23x.device[Mcp23x.chip].pins; pins_needed -= Mcp23x.device[Mcp23x.chip].pins; if (!pins_needed) { break; } } } else { #endif // USE_SPI #ifdef USE_I2C uint8_t mcp23xxx_address = MCP23XXX_ADDR_START; while ((Mcp23x.max_devices < MCP23XXX_MAX_DEVICES) && (mcp23xxx_address < MCP23XXX_ADDR_END)) { Mcp23x.chip = Mcp23x.max_devices; if (I2cSetDevice(mcp23xxx_address)) { Mcp23x.device[Mcp23x.chip].pin_int = (PinUsed(GPIO_MCP23XXX_INT, Mcp23x.chip)) ? Pin(GPIO_MCP23XXX_INT, Mcp23x.chip) : -1; Mcp23x.device[Mcp23x.chip].interface = MCP23X_I2C; Mcp23x.device[Mcp23x.chip].address = mcp23xxx_address; MCP23xWrite(MCP23X08_IOCON, 0x80); // Attempt to set bank mode - this will only work on MCP23x17, so its the best way to detect the different chips 23x08 vs 23x17 uint8_t buffer; if (MCP23xValidRead(MCP23X08_IOCON, &buffer)) { if (0x00 == buffer) { I2cSetActiveFound(mcp23xxx_address, "MCP23008"); Mcp23x.device[Mcp23x.chip].pins = 8; MCP23xWrite(MCP23X08_IOCON, 0b00011000); // Slew rate disabled, HAEN pins for addressing Mcp23x.device[Mcp23x.chip].olata = MCP23xRead(MCP23X08_OLAT); Mcp23x.max_devices++; } else if (0x80 == buffer) { I2cSetActiveFound(mcp23xxx_address, "MCP23017"); Mcp23x.device[Mcp23x.chip].pins = 16; MCP23xWrite(MCP23X08_IOCON, 0x00); // Reset bank mode to 0 (MCP23X17_GPINTENB) MCP23xWrite(MCP23X17_IOCONA, 0b01011000); // Enable INT mirror, Slew rate disabled, HAEN pins for addressing Mcp23x.device[Mcp23x.chip].olata = MCP23xRead(MCP23X17_OLATA); Mcp23x.device[Mcp23x.chip].olatb = MCP23xRead(MCP23X17_OLATB); Mcp23x.max_devices++; } Mcp23x.max_pins += Mcp23x.device[Mcp23x.chip].pins; pins_needed -= Mcp23x.device[Mcp23x.chip].pins; } } if (pins_needed) { mcp23xxx_address++; } else { mcp23xxx_address = MCP23XXX_ADDR_END; } } #endif // USE_I2C #ifdef USE_SPI } #endif // USE_SPI if (!Mcp23x.max_devices) { return; } Mcp23x_gpio_pin = (uint16_t*)calloc(Mcp23x.max_pins, 2); if (!Mcp23x_gpio_pin) { return; } if (!MCP23xLoadTemplate()) { AddLog(LOG_LEVEL_INFO, PSTR("MCP: No valid template found")); // Too many GPIO's Mcp23x.max_devices = 0; return; } Mcp23x.relay_offset = TasmotaGlobal.devices_present; Mcp23x.relay_max -= UpdateDevicesPresent(Mcp23x.relay_max); Mcp23x.button_offset = -1; Mcp23x.switch_offset = -1; } void MCP23xServiceInput(void) { // I found no reliable way to receive interrupts; noise received at undefined moments - unstable usage Mcp23x.interrupt = false; // This works with no interrupt uint32_t pin_offset = 0; uint32_t gpio; for (Mcp23x.chip = 0; Mcp23x.chip < Mcp23x.max_devices; Mcp23x.chip++) { if (8 == Mcp23x.device[Mcp23x.chip].pins) { gpio = MCP23xRead(MCP23X08_GPIO); // Read MCP23x08 gpio } else { gpio = MCP23xRead16(MCP23X17_GPIOA); // Read MCP23x17 gpio } // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("MCP: Chip %d, State %04X"), Mcp23x.chip, gpio); uint32_t mask = 1; for (uint32_t pin = 0; pin < Mcp23x.device[Mcp23x.chip].pins; pin++) { uint32_t state = ((gpio & mask) != 0); uint32_t lpin = MCP23xGetPin(pin_offset + pin); // 0 for None, 32 for KEY1, 160 for SWT1, 224 for REL1 uint32_t index = lpin & 0x001F; // Max 32 buttons or switches lpin = BGPIO(lpin); // UserSelectablePins number if (GPIO_KEY1 == lpin) { ButtonSetVirtualPinState(Mcp23x.button_offset + index, (state != bitRead(Mcp23x.button_inverted, index))); } else if (GPIO_SWT1 == lpin) { SwitchSetVirtualPinState(Mcp23x.switch_offset + index, state); } mask <<= 1; } pin_offset += Mcp23x.device[Mcp23x.chip].pins; } } static void IRAM_ATTR MCP23xInputIsr(void) { Mcp23x.interrupt = true; } void MCP23xInit(void) { if (Mcp23x.button_max || Mcp23x.switch_max) { uint32_t gpio; for (Mcp23x.chip = 0; Mcp23x.chip < Mcp23x.max_devices; Mcp23x.chip++) { if (Mcp23x.device[Mcp23x.chip].pin_int > -1) { if (8 == Mcp23x.device[Mcp23x.chip].pins) { gpio = MCP23xRead(MCP23X08_GPIO); // Clear MCP23x08 interrupt } else { gpio = MCP23xRead16(MCP23X17_GPIOA); // Clear MCP23x17 interrupt } pinMode(Mcp23x.device[Mcp23x.chip].pin_int, INPUT_PULLUP); attachInterrupt(Mcp23x.device[Mcp23x.chip].pin_int, MCP23xInputIsr, CHANGE); } } } } void MCP23xPower(void) { // XdrvMailbox.index = 32-bit rpower bit mask // Use absolute relay indexes unique with main template power_t rpower = XdrvMailbox.index; uint32_t relay_max = TasmotaGlobal.devices_present; if (!Mcp23x.base) { // Use relative and sequential relay indexes rpower >>= Mcp23x.relay_offset; relay_max = Mcp23x.relay_max; } for (uint32_t index = 0; index < relay_max; index++) { power_t state = rpower &1; if (MCP23xPinUsed(GPIO_REL1, index)) { uint32_t pin = MCP23xPin(GPIO_REL1, index) & 0x3F; // Fix possible overflow over 63 gpios MCP23xDigitalWrite(pin, bitRead(Mcp23x.relay_inverted, index) ? !state : state); } rpower >>= 1; // Select next power } } bool MCP23xAddButton(void) { // XdrvMailbox.index = button/switch index uint32_t index = XdrvMailbox.index; if (!Mcp23x.base) { // Use relative and sequential button indexes if (Mcp23x.button_offset < 0) { Mcp23x.button_offset = index; } index -= Mcp23x.button_offset; if (index >= Mcp23x.button_max) { return false; } } else { // Use absolute button indexes unique with main template if (!MCP23xPinUsed(GPIO_KEY1, index)) { return false; } Mcp23x.button_offset = 0; } XdrvMailbox.index = (MCP23xDigitalRead(MCP23xPin(GPIO_KEY1, index)) != bitRead(Mcp23x.button_inverted, index)); return true; } bool MCP23xAddSwitch(void) { // XdrvMailbox.index = button/switch index uint32_t index = XdrvMailbox.index; if (!Mcp23x.base) { // Use relative and sequential switch indexes if (Mcp23x.switch_offset < 0) { Mcp23x.switch_offset = index; } index -= Mcp23x.switch_offset; if (index >= Mcp23x.switch_max) { return false; } } else { // Use absolute switch indexes unique with main template if (!MCP23xPinUsed(GPIO_SWT1, index)) { return false; } Mcp23x.switch_offset = 0; } XdrvMailbox.index = MCP23xDigitalRead(MCP23xPin(GPIO_SWT1, index)); return true; } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv67(uint32_t function) { bool spi_enabled = false; bool i2c_enabled = false; #ifdef USE_SPI spi_enabled = (SPI_MOSI_MISO == TasmotaGlobal.spi_enabled); #endif // USE_SPI #ifdef USE_I2C i2c_enabled = I2cEnabled(XI2C_77); #endif // USE_I2C if (!spi_enabled && !i2c_enabled) { return false; } bool result = false; if (FUNC_SETUP_RING2 == function) { MCP23xModuleInit(); } else if (Mcp23x.max_devices) { switch (function) { case FUNC_LOOP: case FUNC_SLEEP_LOOP: if (!Mcp23x.interrupt) { return false; } AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("MCP: Interrupt")); MCP23xServiceInput(); break; case FUNC_EVERY_100_MSECOND: if (Mcp23x.button_max || Mcp23x.switch_max) { MCP23xServiceInput(); } break; case FUNC_SET_POWER: MCP23xPower(); break; case FUNC_INIT: MCP23xInit(); break; case FUNC_ADD_BUTTON: result = MCP23xAddButton(); break; case FUNC_ADD_SWITCH: result = MCP23xAddSwitch(); break; case FUNC_ACTIVE: result = true; break; } } return result; } #endif // USE_MCP23XXX_DRV #endif // USE_I2C or USE_ESP_SPI