Using the MCP23017 IO Expansion Board

So marvellous code, illustrations and explanation I hope I can do just 50% of that.
That’s why I have to drop everything I’m currently doing to add some little mods to your code.
Hope you don’t mind.

#define BLYNK_PRINT Serial
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>

#include <Wire.h>
#include "Adafruit_MCP23017.h"  // https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library

char auth[] = "REDACTED";
char ssid[] = "REDACTED";
char pass[] = "REDACTED";

// For more info, See https://community.blynk.cc/t/using-the-mcp23017-io-expansion-board/44525
// Connect pin SCL of the first expander (mcp_A) to SCL (D1)
// Connect pin SDA of the first expander (mcp_A) to SDA (D2)
// Connect pin INTA or INTB of the first expander (mcp_A) to ESP8266 pin D5 (GPIO14)
// don't solder A0,A1,A2 (default ID of 7) on the first expander (mcp_A)
// Connect 8 physical momentary push buttons to mcp_A pins 0 to 7 inclusive (labelled PA0 to PA7), with the other side of the button connected to GND
// Connect an 8-way Active LOW relay board to mcp_A pins 8 to 15 inclusive (labelled PB0 to PB7 inclusive)
// Attach Blynk button widgets (in Switch mode) to virtual pins 0 to 7 inclusive. These mirror the physical momentary buttons of mcp_A pins PA0 to PA7

// Solder 0.1" pitch pins to the connections at the far end of mcp_A and
// connect the corresponding wires from mcp_B on to these (INTB to INTB, INTA to INTA etc)
// solder A0 (to give an ID of 6) on the second expander (mcp_B)
// Connect 8 physical momentary push buttons to mcp_B pins 0 to 7 inclusive (labelled PA0 to PA7), with the other side of the button connected to GND
// Connect a second 8-way Active LOW relay board to mcp_B pins 8 to 15 inclusive (labelled PB0 to PB7 inclusive)
// Attach Blynk button widgets (in Switch mode) to virtual pins 8 to 15 inclusive. These mirror the physical momentary buttons of mcp_B pins PA0 to PA7

// If additional mcp boards are added (C, D, E etc) then connectors need to be soldered to the previous board in the daisy chain to allow them to connect together
// and each board needs to have a unique I2C address/ID, see below for details..

/*
  The begin() param can be 0 to 7, the default param is 7 (no solder bridges on A0, A1 & A2) giving an address of 0x27.
  Addr(BIN)  Addr(hex)   ID   A2  A1  A0
  010 0111    0x27        7      0   0   0   <--- Default for WaveShare Board
  010 0110    0x26        6      0   0   1
  010 0101    0x25        5      0   1   0
  010 0100    0x24        4      0   1   1
  010 0011    0x23        3      1   0   0
  010 0010    0x22        2      1   0   1
  010 0001    0x21        1      1   1   0
  010 0000    0x20        0      1   1   1
*/

#define MCP23017_I2C_ADDRESS_0     0x20
#define MCP23017_I2C_ADDRESS_1     0x21
#define MCP23017_I2C_ADDRESS_2     0x22
#define MCP23017_I2C_ADDRESS_3     0x23
#define MCP23017_I2C_ADDRESS_4     0x24
#define MCP23017_I2C_ADDRESS_5     0x25
#define MCP23017_I2C_ADDRESS_6     0x26                       // solder pad A0 bridged
#define MCP23017_I2C_ADDRESS_7     0x27                       // default address, no solder pads bridged

#define MCP23017_ID_0     ( MCP23017_I2C_ADDRESS_0 & 0x07 )
#define MCP23017_ID_1     ( MCP23017_I2C_ADDRESS_1 & 0x07 )
#define MCP23017_ID_2     ( MCP23017_I2C_ADDRESS_2 & 0x07 )
#define MCP23017_ID_3     ( MCP23017_I2C_ADDRESS_3 & 0x07 )
#define MCP23017_ID_4     ( MCP23017_I2C_ADDRESS_4 & 0x07 )
#define MCP23017_ID_5     ( MCP23017_I2C_ADDRESS_5 & 0x07 )
#define MCP23017_ID_6     ( MCP23017_I2C_ADDRESS_6 & 0x07 )     // solder pad A0 bridged
#define MCP23017_ID_7     ( MCP23017_I2C_ADDRESS_7 & 0x07 )     // default address, no solder pads bridged

typedef struct
{
  Adafruit_MCP23017 mcp;
  int               device_ID;
} Adafruit_MCP23017_Array;

// To add more devices, just add more instances of Adafruit_MCP23017 and entries in Adafruit_MCP23017_Array
// For example, see the commented lines

// Create the instances of the mcp objects
Adafruit_MCP23017 mcp_0;
Adafruit_MCP23017 mcp_1;
// Uncomment this and the below line to add one more device and so on. Remember to change device_ID
//Adafruit_MCP23017 mcp_2;

Adafruit_MCP23017_Array mcpArray[  ] =
{
  { mcp_0, MCP23017_ID_7 },
  { mcp_1, MCP23017_ID_6 },
  // this line
  //{ mcp_2, MCP23017_ID_5 },
};

#define NUM_MCP23017   ( sizeof(mcpArray) / sizeof(Adafruit_MCP23017_Array) )

// duplicate the above code if additional mcp boards are added to the daisy chain

// Define the interrupt pin on the MCU
int esp_interrupt_pin = 14; // GPIO14 (D5)

volatile unsigned long last_interrupt; // Used to store the time in millis of the last interrupt, for debouncing
int debounce_delay = 100;              // The minimum delay in milliseconds between button presses, for debouncing

volatile int mcp_board;     // when an interrupt is detected and decoded the device which generated the interrupt is stored here
volatile int mcp_device_ID; // when an interrupt is detected and decoded the device which generated the interrupt is stored here
volatile uint8_t pin_num;   // when an interrupt is detected and decoded the pin which generated the interrupt  is stored here

// Define friendly names for the pins...

// These mcp_A pins will have physical buttons connected to them, with the other side connected to ground.
//  the pins will be pulled-up when we declare them later...

enum
{
  mcp_pin_P0 = 0,
  mcp_pin_P1,
  mcp_pin_P2,
  mcp_pin_P3,
  mcp_pin_P4,
  mcp_pin_P5,
  mcp_pin_P6,
  mcp_pin_P7,
  mcp_pin_P8,
  mcp_pin_P9,
  mcp_pin_P10,
  mcp_pin_P11,
  mcp_pin_P12,
  mcp_pin_P13,
  mcp_pin_P14,
  mcp_pin_P15,
  mcp_pin_max

} MCP23017_PIN;

#define NUM_RELAY_PER_MCP23017    ( mcp_pin_max / 2 )

// duplicate the above definitions if additional mcp boards are added to the daisy chain

BLYNK_CONNECTED()
{
  // This code runs when the MCU connects or re-connects to the Blynk server
  // We want to get the latest widget switch values and update the relays so that they match this

  // Loop through pins 0-15 and do a Blynk.syncVirtual
  // If more pins are used that relate to additional mcps, or different patterns of virtual pins are used on the existing mcps
  // the this for loop range will need to be adjusted...

  // Loop through pins 0-NUM_RELAY_PER_MCP23017 of NUM_MCP23017 boards, and do a Blynk.syncVirtual
  for (int board = 0; board < NUM_MCP23017; board++)
  {
    for (int v_pin = 0; v_pin < NUM_RELAY_PER_MCP23017; v_pin++)
    {
      Blynk.syncVirtual(v_pin); // This forces the BLYNK_WRITE_DEFAULT callback function to trigger for each virtual pin in turn
    }
  }
}

BLYNK_WRITE_DEFAULT()
{
  int widget_pin = request.pin;      // Which virtual pin triggered this BLYNK_WRITE_DEFAULT callback?
  int widget_value = param.asInt();  // Get the value from the virtual pin (O = off, 1 = on)

  // if it was a virtual pin in the range 0-7 then it's a command meant for mcp_A
  // if it was a virtual pin in the range 8-15 then it's a command meant for mcp_B
  // Write the opposite state to the pin on mcp_A to toggle the relay
  mcpArray[ (int) (widget_pin / NUM_RELAY_PER_MCP23017) ].mcp.digitalWrite(widget_pin + NUM_RELAY_PER_MCP23017, !widget_value);
}

void clear_mcp_interrupts()
{
  for (int board = 0; board < NUM_MCP23017; board++)
  {
    for (int mcp_pin = mcp_pin_P0; mcp_pin <= mcp_pin_P7; mcp_pin++) // Loop through the 8 pins on mcp_A that have interrupts attached
    {
      mcpArray[ board ].mcp.digitalRead(mcp_pin);    // Read the pin to clear the interrupt register for that pin
    }
  }
}

// Interrupt handler for pins mcb_A pins 0-7 and mcb_B pins 0-7
void ICACHE_RAM_ATTR intCallBack()
{
  noInterrupts();

  // If the interrupt occurred on mcp_A then reading the mcp_A interrupt register will return the pin number
  // (0-15 or 0-7 in this case as we aren't using interrupts on pins 8-15),
  // otherwise it will return 255.
  // Therefore a value >15 means that the interrupt didn't occur on mcp_A but on one of the other mcp boards in the daisy chain so we
  // repeat the process for each board until we get a value in the range of 0 to 15
  // To later reference which board the interrupt came from we use mcp_device_ID

  for (int board = 0; board < NUM_MCP23017; board++)
  {
    if (mcpArray[ board ].mcp.getLastInterruptPin() < mcp_pin_max)
    {
      pin_num = mcpArray[ board ].mcp.getLastInterruptPin();
      mcp_device_ID = mcpArray[ board ].device_ID;
      mcp_board = board;
      break;
    }
  }

  if (millis() - last_interrupt >= debounce_delay) // Debounce routine
  {
    // for debugging only...
    Serial.println("_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-");
    Serial.println("A button has been pressed!!!");
    Serial.print("Device = ");
    Serial.println(mcp_device_ID);
    Serial.print("Pin = ");
    Serial.println(pin_num);
    Serial.println("_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-");
    Serial.println();

    process_button_press();  // Call the function that will activate the correct relay etc.
  }
  last_interrupt = millis();
  interrupts();
  clear_mcp_interrupts(); // Call the function that clears all of the interrupt registers
}

void process_button_press()
{
  // This code assumes that the switch on mcp_A pin 0 controls the relay on mcp_A pin 8 (= NUM_RELAY_PER_MCP23017)
  // so we just add 8 to the number of the pin that triggered the interrupt
  int current_pin_state = mcpArray[ mcp_board ].mcp.digitalRead(pin_num + NUM_RELAY_PER_MCP23017);     // Get the current state of the pin
  mcpArray[ mcp_board ].mcp.digitalWrite(pin_num + NUM_RELAY_PER_MCP23017, !current_pin_state);        // Write the opposite state to the pin to toggle it

  // This code assumes that we have Blynk button widgets (in Switch mode) on virtual pins that are numbered the same
  // as the ones used for the physical buttons (V0 = MCP Pin 0, V1 = MCP Pin 1 etc)

  // As the relays are Active LOW then we need to write the opposite state to the Blynk switch widget than we do to the MCP pin...
  // Relay = Off (HIGH or 1) then Blynk button widget = 0
  // Relay = On (LOW or 0) then Blynk button widget = 1
  Blynk.virtualWrite(pin_num, current_pin_state);

  // additional case statements would be added if more expander boards are added to the daisy chain,
  // assuming that interrupts are used on those additional boards
}

void setup() 
{

  Serial.begin(115200);

  pinMode(esp_interrupt_pin, INPUT); // Initialise the MCU pin used for the Interrupt line (INTA or INTB on the Waveshare board)

  for (int board = 0; board < NUM_MCP23017; board++)
  {
    mcpArray[ board ].mcp.begin(mcpArray[ board ].device_ID);

    // We mirror INTA and INTB, so that only one line is required between MCP and Arduino for int reporting
    // The INTA/B will not be Floating
    // INTs will be signalled with a LOW
    mcpArray[ board ].mcp.setupInterrupts(true, false, LOW);
  }

  // duplicate the above code if more expanders are added to the daisy chain (assuming that more pins with interrupts are needed on these boards)

  // In this next section I could have used the friendly names (0-15) defined in the declarations, but chose to stick with the
  // names screen-printed on the mcp board to aid wiring and debugging.

  // In the following blocks of code we declare each of our pins that will be used for interrupts (Pins 0-7) on mcb_A
  // If other pins have interrupts assigned to them then they must be added to the clear_mcp_interrupts() function

  for (int board = 0; board < NUM_MCP23017; board++)
  {
    for ( int mcp_pin = mcp_pin_P0; mcp_pin < mcp_pin_max; mcp_pin++)
    {
      // configuration for a button PA0 on mcb_A
      // interrupt will trigger when the pin is taken to ground by a pushbutton
      mcpArray[ board ].mcp.pinMode(mcp_pin, INPUT);
      mcpArray[ board ].mcp.pullUp(mcp_pin, HIGH);                  // turn on a 100K pullup internally
      mcpArray[ board ].mcp.setupInterruptPin(mcp_pin, FALLING);
    }
  }

  // duplicate the above code if more expander boards are added to the daisy chain and use the correct
  // mode definition etc for the type of use for that pin (input, output, input with interrupt etc)
  // If other board/pin combinations have interrupts assigned to them then they must be added to the clear_mcp_interrupts() function

  clear_mcp_interrupts(); // Call the function that clears all of the interrupt registers

  Blynk.begin(auth, ssid, pass);

  // Here we attach an interrupt to the MCU pin that will be used to listen for interrupts from the mcp_A...
  attachInterrupt(digitalPinToInterrupt(esp_interrupt_pin), intCallBack, FALLING);
  // It's not necessary to do this for the other mcp's, as it's only mcp_A that's directly connected to the NodeMCU's interrupt pin
}

void loop()
{
  Blynk.run();
}

2 Likes