Electronics Software Development

Adding Digital I/O To Your Arduino: Part 3 – The MCP23017

Add More IO Green Graphic
Written by John Woolsey

Last Updated: October 26, 2023
Originally Published: March 18, 2021

Skill Level: Intermediate

WARNING:
Microchip discovered an issue with the MCP23017 chip, where under certain circumstances, the I2C SDA signal may become corrupted when the chip’s GPA7 and/or GPB7 pins are used as inputs. For this reason, they updated the datasheet to reflect that those pins should only be used as outputs.

This tutorial utilizes the MCP23017 GPB7 pin as an input. If you see strange or unexpected behavior while following this tutorial, you may want to adjust your circuit and code accordingly.

I am not planning to change the tutorial since, hopefully, a silicon change is in the works, but I at least want to notify readers of the potential issue.

Thank you to Günter Nowinski for notifying me about this issue.

Table Of Contents

Introduction

Sometimes, a project needs more digital I/O than what is available on your Arduino board. This often happens when you connect to components that require a lot of pins for their interface, e.g. some displays, or your project uses many discrete sensors and/or actuators.

This three-part tutorial teaches you how to add more digital inputs and outputs to your Arduino development board. Each part focuses on a specific integrated circuit (IC) chip.

Part 1 – The 74HC595 described how to add digital outputs using the 74HC595 8-bit serial-in parallel-out (SIPO) shift register IC.

Part 2 – The 74HC165 described how to add digital inputs using the 74HC165 8-bit parallel-in serial-out (PISO) shift register IC.

Part 3 – The MCP23017 (currently reading) describes how to add both digital inputs and outputs using the MCP23017 16-Bit I2C I/O Expander With Serial Interface IC.

A basic understanding of electronics and programming is expected along with some familiarity with the Arduino platform. If you are new to Arduino, or would just like to refresh your knowledge, please see our Blink: Making An LED Blink On An Arduino Uno tutorial before proceeding with this one. In addition, this tutorial will use a solderless breadboard to build a circuit from a schematic diagram. The All About Circuit’s Understanding Schematics, SparkFun’s How to Read a Schematic, Core Electronics’ How to Use Breadboards, and Science Buddies’ How to Use a Breadboard guides are good resources for learning how to translate a schematic to a breadboard.

The resources created for this tutorial are available on GitHub for your reference.

This tutorial is provided as a free service to our valued readers. Please help us continue this endeavor by considering a GitHub sponsorship or a PayPal donation.

What Is Needed

Background Information

If you need to add both digital inputs and outputs to your project, the MCP23017 IC from Microchip is a good choice to incorporate into your design. It is a 16-bit I/O port expander that adds a total of 16 additional digital GPIO pins, in two ports, PORTA and PORTB with 8 pins each, that communicates with your Arduino over an I2C serial interface. 8-bit and SPI port expander versions are also available if you prefer.

Each MCP23017 IC can be set to one of eight I2C addresses (0x20-0x27) giving you a total of 128 (8 ICs x 16 pins) additional digital I/O pins that you can add to your project.

Each GPIO pin of the MCP23017 can be configured as either an input or an output. Additional options can also be enabled for each pin, i.e., an internal pull-up resistor, triggering an interrupt, and polarity inversion. Interrupts can even be configured to not only trigger on a change but also trigger on a comparison to a default value set by the user.

The MCP23017 is highly configurable. I will be covering many, but not all, of its capabilities. To learn more, check out the MCP23017 datasheet.

There are multiple ways to configure and use the MCP23017 within an Arduino sketch to access and control GPIO pins. Each has its pros and cons and can be heavily dependent on the nature of the additional inputs or outputs required. For instance, are the additional I/O highly disparate and need to be manipulated separately, or are they more homogeneous and can be referred to as a single block? I will present four different approaches in this tutorial, all producing the same basic functionality, so that you can easily compare among them and choose the best approach for your own design. While all of these approaches eventually update the MCP23017’s output pins (LEDs) with associated changes in the input pins (switches), each approach provides a different way to visualize or manipulate individual pins.

My development system consists of the Arduino Uno WiFi Rev2 development board connected to a macOS based computer running the desktop Arduino IDE. If you are using a different Arduino board or computer setup, the vast majority of this tutorial should still apply, however, some minor changes may be necessary.

If you need assistance with your particular setup, post a question in the comments section below and I, or someone else, can try to help you.

Building The Circuit

Before connecting any circuitry to your Arduino board, disconnect it from power and your computer. This avoids accidental damage during wiring.

Place the components and wire up the circuit on a breadboard according to the schematic diagram shown below.

Schematic Diagram Of An MCP23017 Digital I/O Circuit Connected To An Arduino Uno
Schematic Diagram Of An MCP23017 Digital I/O Circuit Connected To An Arduino Uno

Eight LEDs are connected to PORTA‘s GPA0GPA7 (21-28) pins of the MCP23017 IC via 330 Ω resistors constituting the 8 digital outputs being added to the system. Please note, the MCP23017 has a maximum sink/source current capability per I/O pin of 25 mA and a total chip current capability of 125 mA into the VDD pin or 150 mA out of the VSS pin. Our use of 8 LEDs, with expected currents of 10 mA or less each, easily falls within the proper current range, but additional design constraints may need to be considered when using more outputs or higher current devices.

An 8-position dip switch is connected to PORTB‘s GPB0GPB7 (1-8) pins of the MCP23017 constituting the 8 digital inputs being added to the system. Eight individual standard single-pole single-throw (SPST) switches may be used instead of the integrated 8-position dip switch if you prefer. Typically, we would also need to incorporate either pull-up or pull-down resistors in series with the 8 switches. However, we will be utilizing the internal pull-up resistors of the MCP23017 so that we can eliminate the need for those external resistors. Since pull-up resistors are being used, the other side of the switches are all tied to ground.

The MCP23017’s three address pins, A0A2 (15-17), set the lowest three bits of the I2C address range (0x20-0x27) that will be used to communicate to the IC over the I2C serial bus. Pins tied low (ground) correspond to a value of 0 and pins tied high (power) correspond to a value of 1. All address pins must be tied to either power or ground and can not be left floating. Since we are tying all address pins to ground (000), this correlates to an address of 0x20 for our MCP23017 IC. If you prefer to use a different I2C address or plan to use more than one MCP23017 IC in your design, make sure to tie the address pins for each IC appropriately.

The MCP23017’s RESET (18) pin is an active low input and must be externally biased so that it is not left floating. I tied it directly to power since we do not plan to reset the chip directly.

A 0.1 µF bypass capacitor was placed across the power pins of the MCP23017 IC in order to reduce any power supply noise that may be present.

The I2C clock, SCK (12), and data, SDA (13), pins of the MCP23017 IC are connected to the SCL and SDA pins respectively of the Arduino board to establish the I2C serial bus connection. Be aware that not all Arduino boards have the analog pins A4 and A5 connected to the I2C port. For this reason, I am using the SCL and SDA pins on the Digital row of the Arduino board.

INTA (20) and INTB (19) are the interrupt output pins for PORTA and PORTB respectively of the MCP23017. Later, we will be utilizing interrupts on PORTB to detect switch changes, so INTB is connected to D2 on the Arduino. We will not be using interrupts with PORTA so I left the INTA pin unconnected.

The circuit should look similar to the one shown below once completed.

Completed Arduino Uno And MCP23017 Digital I/O Circuit
Completed Arduino Uno And MCP23017 Digital I/O Circuit

Once the circuit is built, connect your Arduino to your computer with the USB cable.

Reading And Writing Single Inputs And Outputs Using Familiar digitalRead() And digitalWrite() Functionality

This first approach implements the same calling mechanisms as Arduino’s standard digitalRead() and digitalWrite() functions. It should be very familiar to long time Arduino users and the easiest to understand. However, when reading and writing multiple inputs or outputs, it will involve more I2C communication than other approaches since only one input or output at a time can be read or written respectively.

I researched various libraries available for use with the MCP23017 and chose the MCP23017 library by Bertrand Lemasle due to its thoroughness and ability to write to all MCP23017 registers if needed.

Open the Arduino IDE and install the MCP23017 library from within the Library Manager.

Next, create a sketch named PortExpander with the code shown below.

#include <MCP23017.h>

#define MCP23017_I2C_ADDRESS 0x20  // I2C address of the MCP23017 IC

const uint8_t LED0 = 0;      // GPA0 (21) of the MCP23017
const uint8_t LED1 = 1;      // GPA1 (22) of the MCP23017
const uint8_t LED2 = 2;      // GPA2 (23) of the MCP23017
const uint8_t LED3 = 3;      // GPA3 (24) of the MCP23017
const uint8_t LED4 = 4;      // GPA4 (25) of the MCP23017
const uint8_t LED5 = 5;      // GPA5 (26) of the MCP23017
const uint8_t LED6 = 6;      // GPA6 (27) of the MCP23017
const uint8_t LED7 = 7;      // GPA7 (28) of the MCP23017
const uint8_t Switch0 = 8;   // GPB0 (1) of the MCP23017
const uint8_t Switch1 = 9;   // GPB1 (2) of the MCP23017
const uint8_t Switch2 = 10;  // GPB2 (3) of the MCP23017
const uint8_t Switch3 = 11;  // GPB3 (4) of the MCP23017
const uint8_t Switch4 = 12;  // GPB4 (5) of the MCP23017
const uint8_t Switch5 = 13;  // GPB5 (6) of the MCP23017
const uint8_t Switch6 = 14;  // GPB6 (7) of the MCP23017
const uint8_t Switch7 = 15;  // GPB7 (8) of the MCP23017

MCP23017 mcp23017 = MCP23017(MCP23017_I2C_ADDRESS);  // instance of the connected MCP23017 IC

void setup() {
   Wire.begin();     // initialize I2C serial bus
   mcp23017.init();  // initialize MCP23017 IC

   // Configure MCP23017 I/O pins
   configurePinsWithPinMode();  // familiar pinMode() style

   // Reset MCP23017 ports
   mcp23017.writeRegister(MCP23017Register::GPIO_A, 0x00);
   mcp23017.writeRegister(MCP23017Register::GPIO_B, 0x00);
}

void loop() {
   readAndWriteWithDigitalReadAndDigitalWrite();
}

void configurePinsWithPinMode() {
   // Configure output pins
   mcp23017.pinMode(LED0, OUTPUT);
   mcp23017.pinMode(LED1, OUTPUT);
   mcp23017.pinMode(LED2, OUTPUT);
   mcp23017.pinMode(LED3, OUTPUT);
   mcp23017.pinMode(LED4, OUTPUT);
   mcp23017.pinMode(LED5, OUTPUT);
   mcp23017.pinMode(LED6, OUTPUT);
   mcp23017.pinMode(LED7, OUTPUT);

   // Configure input pins with internal 100K pull-up resistors
   // Third argument inverts the polarity of the input value when read
   mcp23017.pinMode(Switch0, INPUT_PULLUP, true);
   mcp23017.pinMode(Switch1, INPUT_PULLUP, true);
   mcp23017.pinMode(Switch2, INPUT_PULLUP, true);
   mcp23017.pinMode(Switch3, INPUT_PULLUP, true);
   mcp23017.pinMode(Switch4, INPUT_PULLUP, true);
   mcp23017.pinMode(Switch5, INPUT_PULLUP, true);
   mcp23017.pinMode(Switch6, INPUT_PULLUP, true);
   mcp23017.pinMode(Switch7, INPUT_PULLUP, true);
}

void readAndWriteWithDigitalReadAndDigitalWrite() {
   // Read and write individual inputs and outputs
   mcp23017.digitalWrite(LED0, mcp23017.digitalRead(Switch0));
   mcp23017.digitalWrite(LED1, mcp23017.digitalRead(Switch1));
   mcp23017.digitalWrite(LED2, mcp23017.digitalRead(Switch2));
   mcp23017.digitalWrite(LED3, mcp23017.digitalRead(Switch3));
   mcp23017.digitalWrite(LED4, mcp23017.digitalRead(Switch4));
   mcp23017.digitalWrite(LED5, mcp23017.digitalRead(Switch5));
   mcp23017.digitalWrite(LED6, mcp23017.digitalRead(Switch6));
   mcp23017.digitalWrite(LED7, mcp23017.digitalRead(Switch7));
}

Line 3 sets the address we will use to communicate with the MCP23017 IC over the I2C bus. This address needs to match the address specified by the hardwired A0A2 pins discussed in the Building The Circuit section.

Lines 5-20 define the named references for our individual LEDs and switches. The integer values are how the MCP23017 library refers to the individual pins of PORTA (07) and PORTB (815). I have also included the specific MCP23017 port pins in the comments for correlation to the hardware.

Line 22 defines our instance, mcp23017, of the MCP23017 chip.

In the setup() routine, lines 25 and 26 initialize the I2C bus and the mcp23017 instance.

Lines 32-33 reset the ports by writing zeros directly to the MCP23017’s GPIO registers. This helps the sketch and the MCP23017 IC remain synced and is especially helpful between uploads.

Since we will be covering multiple configuration and operational approaches, I separated them into their own distinct functions.

The first configuration approach is the configurePinsWithPinMode() function that is called in setup(). It uses the familiar pinMode() functionality to configure the individual inputs and outputs of the MCP23017. Note the extra third argument, true, used when setting the inputs. This optional argument inverts the polarity of the value read from the input pin. In other words, if the real value is LOW, it will give you HIGH when read, and vice versa. This ability comes in quite handy since we had to tie the common sides of the dip switches to ground in order to take advantage of the internal pull-up resistors of the MCP23017 IC. With that being the case, the switches become LOW when turned on, but it would be nice to show them as HIGH instead. Enabling the polarity inversion option of the MCP23017 does just that.

Likewise, the readAndWriteWithDigitalReadAndDigitalWrite() function is the first operational approach and is called in the loop() routine. It simply reads the current value of an input (switch) and writes that value to the associated output (LED). Since this routine is run continuously, any flipped switches are immediately reflected in the LEDs.

Save, compile, and upload the sketch to give it a try. Turn on and off a few of the switches and watch the matching LEDs turn on and off.

If there is something that needs further explanation, please let me know in the comment section and I will try to answer your question.

Reading And Writing All Inputs Or Outputs Of A Port

This second approach takes advantage of the fact that we designed PORTA to be all outputs and PORTB to be all inputs giving us the ability to not only easily manage all pins of a port, but spend less time in I2C bus communication. It produces the most concise code, but does not indicate the meaning of each of the inputs or outputs.

Add the following functions to the end of your sketch.

void configurePinsWithPortMode() {
   // Configure PORTA (LEDs)
   mcp23017.portMode(MCP23017Port::A,
      0b00000000);  // direction (IODIRA) - set direction of all pins as outputs

   // Configure PORTB (switches)
   mcp23017.portMode(MCP23017Port::B,
      0b11111111,   // direction (IODIRB) - set direction of all pins as inputs
      0b11111111,   // pull-up (GPPUB) - enable 100K pull-up resistors on all inputs
      0b11111111);  // polarity (IPOLB) - invert logic polarity for all inputs
}

void portCopy() {
   // Copy values from PORTB (switches) to PORTA (LEDs)
   mcp23017.writePort(MCP23017Port::A, mcp23017.readPort(MCP23017Port::B));
}

Then replace the pins configuration line within the setup() function from

configurePinsWithPinMode();  // familiar pinMode() style

to

// configurePinsWithPinMode();  // familiar pinMode() style
configurePinsWithPortMode();  // concise portMode() style

thereby commenting out the old pins configuration routine and adding the new one underneath the original.

Likewise, replace the body of the loop() function with the following.

// readAndWriteWithDigitalReadAndDigitalWrite();
portCopy();

Notice that this approach drastically reduces the number of lines needed to configure the I/O pins thanks to the MCP23017 library’s portMode() method. All pin directions for an entire port are written as a single byte with 0 representing an output and 1 an input for each pin. The optional third and fourth arguments of portMode(), used for PORTB, enable the pull-up resistor and polarity inversion for each pin. All bits in each byte of this configuration are all of the same value, but that does not have to be the case. You can mix and match inputs, outputs, and enabled options for the various pins as you see fit. I deliberately designed the circuit and configured the ports in this manner so that I could take advantage of the fact that all pins of a given port are all of the same type. Thus, the portCopy() function consists only of a single statement that simply reads all the pins of PORTB and writes their values to the pins of PORTA.

Save your work and upload the updated sketch to test the code. It should function in exactly the same way as before, just with less code.

Port based reading and writing are good choices to use when all the pins of a port are the same type (inputs or outputs) and can be referred to as a single block, like a bank of LEDs or switches in our case.

As you can probably tell from our work so far, the digitalRead(), digitalWrite(), readPort(), and writePort() methods of the MCP23017 library are analogous to the isrDigitalRead(), osrDigitalWrite(), isrReadRegister(), and osrWriteRegister() functions we created and used in Part 1 – The 74HC595 and Part 2 – The 74HC165 of this series. Hence, all of the approaches and examples we covered in Part 1 – The 74HC595:

  • changeOutputsWithDigitalWrite(),
  • changeOutputsWithBinaryValues(),
  • changeOutputsWithDefinedNames(),
  • changeOutputsWithBitOperations(), and
  • cycleLEDs()

and Part 2 – The 74HC165:

  • readInputsWithDigitalRead(),
  • readInputsWithBinaryValues(),
  • readInputsWithDefinedNamesAndBitOperations(), and
  • readAndPrintInputsOnChange()

also apply to the MCP23017 when replacing the relevant operational functions of the shift registers with their MCP23017 library counterparts. So instead of reiterating those approaches, we are focusing on the differences and capabilities of the MCP23017 IC in general and the MCP23017 library in particular in this last part of the series.

Utilizing Interrupts For Greater Efficiency

The previous approaches continuously looped through reading and writing the I/O pins. This is not a very efficient use of the microcontroller’s time. We could use polling to read the current inputs and compare them with their last known values. That would be better, but still not that efficient. Fortunately, the MCP23017 chip is capable of triggering interrupts on input pin changes. Its interrupt system is quite extensive and we will cover many of its capabilities within these next two sections.

The approach covered in this section uses interrupts to alert us when an input has changed, thereby freeing up the microcontroller to perform other tasks as necessary until an interrupt is triggered. This approach can be quite useful when the inputs are not expected to change very often.

Add the following lines to the PortExpander sketch just below the I/O Pin Definitions and above the creation of the mcp23017 instance towards the top of the sketch.

const uint8_t MCP23017_INTB = 2;  // connected to MCP23017 INTB (19) pin

volatile bool switchDidChange = false;  // change status of MCP23017 input pins

INTB is the interrupt output pin that will be driven LOW by the MCP23017 when it detects a change in the PORTB input pins. The switchDidChange flag will be set by our interrupt service routine (ISR). The volatile keyword is used since the variable can be changed at any time by the ISR.

Next, add the following functions to the end of the PortExpander sketch.

void configureInterrupts() {
   // Configure MCP23017 interrupts
   mcp23017.interruptMode(MCP23017InterruptMode::Separated);  // INTA and INTB act independently
   mcp23017.interrupt(MCP23017Port::B, CHANGE);  // trigger an interrupt when an input pin CHANGE is detected on PORTB

   // Set up interrupt connection and attach interrupt service routine
   mcp23017.clearInterrupts();  // reset interrupt system
   pinMode(MCP23017_INTB, INPUT_PULLUP);  // utilize microprocessor's internal pull-up resistor
   attachInterrupt(digitalPinToInterrupt(MCP23017_INTB), mcp23017ChangeDetectedOnPortB, FALLING);  // INTB is active LOW
}

This function configures the MCP23017 interrupt system and attaches an interrupt service routine.

The interrupt mode is set to Separated to allow the INTA and INTB interrupt pins to act independently. An Or mode option is available that internally ors the the pins together so that an interrupt for either PORTA or PORTB will be reflected on both pins simultaneously.

The interrupt() method enables interrupts for all pins of PORTB, detecting any CHANGE that occurs. Other available options are FALLING and RISING.

The clearInterrupts() method resets the interrupt system by clearing any previous interrupts and initiates watching for further pin changes.

The mcp23017ChangeDetectedOnPortB() ISR is then attached for execution when the Arduino detects that the MCP23017_INTB pin is FALLING.

void mcp23017ChangeDetectedOnPortB() {
   switchDidChange = true;
}

This is the interrupt service routine that simply sets the switchDidChange flag when executed.

void readAndWritePortOnInputChange() {
   if (switchDidChange) {
      delay(100);  // allow time for MCP23017 to set interrupt registers
      portCopy();  // copy values from PORTB (switches) to PORTA (LEDs)
      mcp23017.clearInterrupts();  // clear interrupt
      switchDidChange = false;
   }
}

This function checks if the switchDidChange flag was set by the ISR, and if so, copies the values from PORTB (inputs) to PORTA (outputs). A small delay of 100 ms is used to ensure the MCP23017 has had enough time to update its internal interrupt registers. Once the copy is complete, the interrupts and flag are cleared.

Finally, we need to update the setup() and loop() functions to add the use of these new functions. Add the following line to the setup() function between configuring the MCP23017’s I/O pins and resetting of its ports.

configureInterrupts();

Comment out the portCopy() statement in the loop() function and add the readAndWritePortOnInputChange() to be executed instead. This latter function should now be the only thing being called within the loop() function.

Save your work and upload the updated sketch to test the code. It should function in exactly the same manner as before, but with much greater efficiency and far less I2C communication.

Extending Interrupts With More Granularity

This last approach utilizes more of the MCP23017 interrupt system’s capabilities to provide greater detail of specific pin states at the time an interrupt occurred.

Add the following function to the end of the sketch

void readAndWritePinOnInputChange() {
   uint8_t flagA, flagB;  // MCP23017 INTFA/B registers
   uint8_t capA, capB;    // MCP23017 INTCAPA/B registers

   if (switchDidChange) {
      delay(100);  // allow time for MCP23017 to set interrupt registers
      mcp23017.interruptedBy(flagA, flagB);  // retrieve pin causing interrupt
      mcp23017.clearInterrupts(capA, capB);  // clear interrupt and capture pin states at time of interrupt
      uint8_t pin = 0;  // input pin causing interrupt
      for (pin = 0; pin < 8; pin++) {
         if (bitRead(flagB, pin)) break;
      }
      uint8_t value = bitRead(capB, pin);  // new value of input pin causing interrupt
      mcp23017.digitalWrite(pin, value);  // set appropriate LED with new value
      switchDidChange = false;
   }
}

and then update the loop() function to run this function instead.

The MCP23017’s INTFA and INTFB registers flag the pins that caused the interrupt for PORTA and PORTB respectively. The INTCAPA and INTCAPB registers capture all the pin values at the time the interrupt was triggered. The flagA/flagB and capA/capB variables hold these register values that are retrieved with the library’s interruptedBy() method along with an overloaded version of the clearInterrupts() method. These register values are then used to retrieve the specific pin that was changed, along with its new value, and then update only the associated output pin with digitalWrite(). The PORTA interrupt registers are ignored since we are only interested in PORTB state changes at this time.

Save your work and upload the updated sketch to test the code. Again, it should function in exactly the same manner as before, but this time with more pin state granularity.

Before we end, I wanted to mention another very nice feature of the MCP23017 library is the ability to read from and write to any of the MCP23017 IC’s registers directly with the library’s readRegister() and writeRegister() methods. We saw an example of this in the setup() function for resetting the GPIO ports. This capability provides maximum flexibility in configuring and accessing individual GPIO pin data independent of the library’s default functionality. Check out the library’s header file for a summary of the methods and general capabilities available with the MCP23017 library.

Now would be a good time to upload the BareMinimum sketch (Main Menu > File > Examples > 01.Basics > BareMinimum) to reset all pins back to their default states. This ensures no outputs are being driven when plugging in your board for your next project.

Summary

In this tutorial, we learned how to add both digital inputs and outputs to your Arduino board using the MCP23017 16-Bit I2C I/O Expander With Serial Interface IC. We covered multiple approaches for how to represent and control the MCP23017 IC’s GPIO in your Arduino code so that you can compare and choose the right implementation in your own designs. They range from a simplistic approach that copies all values of one port to another to using the familiar digitalRead() and digitalWrite() mechanisms. We even covered how to use interrupts to allow for greater efficiency of the microcontroller’s time.

The final source code and schematic used for this tutorial are available on GitHub. The GitHub version of the code is fully commented to include additional information, such as the program’s description, circuit connections, code clarifications, and other details. The comments are also Doxygen compatible in case you want to generate the code documentation.

As a reminder, this tutorial was provided as a free service to our valued readers. Please help us continue this endeavor by considering a GitHub sponsorship or a PayPal donation.

Thank you for joining me along this journey and I hope you enjoyed the experience. Please feel free to share your thoughts or questions in the comments section below.

About the author

John Woolsey

John is an electrical engineer who loves science, math, and technology and teaching it to others even more.
 
He knew he wanted to work with electronics from an early age, building his first robot when he was in 8th grade. His first computer was a Timex/Sinclair 2068 followed by the Tandy 1000 TL (aka really old stuff).
 
He put himself through college (The University of Texas at Austin) by working at Motorola where he worked for many years afterward in the Semiconductor Products Sector in Research and Development.
 
John started developing mobile app software in 2010 for himself and for other companies. He has also taught programming to kids for summer school and enjoyed years of judging kids science projects at the Austin Energy Regional Science Festival.
 
Electronics, software, and teaching all culminate in his new venture to learn, make, and teach others via the Woolsey Workshop website.

50 Comments

  • Great information! Attempted to add 2nd mcp23017 without success. I know it has to do with the instance but I cannot figure it out. I can get one or the other mcp23017 to work, but not both at the same time. Any advice would be awesome.

    Added the following to your code to the digitalRead() & digitalWrite():

    #define MCP23017_I2C_ADDRESS 0x21 // I2C address of the MCP23017 IC
    const uint8_t LED8 = 0; // GPA0 (21) of the MCP23017

    void setup() {
    mcp23017.writeRegister(MCP23017Register::GPIO_A, 0x01);
    mcp23017.writeRegister(MCP23017Register::GPIO_B, 0x01);

    void loop() {
    mcp23017.digitalWrite(LED8,HIGH);
    delay(500);
    mcp23017.digitalWrite(LED8,LOW);
    delay(500);

    void configurePinsWithPinMode() {
    mcp23017.pinMode(LED8, OUTPUT);

    • Yes, two instances will be required to have two MCP23017 ICs operating simultaneously.

      You first need to make sure the I2C address for the second IC is different from the first one. Tie the A0 pin of the second MCP23017 IC to 5V, instead of ground, so that its I2C address becomes 0x21.

      Then add the following to the relevant sections of the initial PortExpander sketch. mcp23017 will continue referring to the original instance and we are defining mcp23017_2 to refer to the second instance.

      #define MCP23017_2_I2C_ADDRESS 0x21  // I2C address of the second MCP23017 IC
      
      const uint8_t LED8 = 0;      // GPA0 (21) of the second MCP23017
      
      MCP23017 mcp23017_2 = MCP23017(MCP23017_2_I2C_ADDRESS);  // instance of the second connected MCP23017 IC
      
      # within setup()
         mcp23017_2.init();  // initialize second MCP23017 IC
      
         // Reset second MCP23017 ports
         mcp23017_2.writeRegister(MCP23017Register::GPIO_A, 0x00);
         mcp23017_2.writeRegister(MCP23017Register::GPIO_B, 0x00);
      
      # within configurePinsWithPinMode()
         mcp23017_2.pinMode(LED8, OUTPUT);
      
      # within readAndWriteWithDigitalReadAndDigitalWrite()
         mcp23017_2.digitalWrite(LED8,HIGH);
         delay(500);
         mcp23017_2.digitalWrite(LED8,LOW);
         delay(500);

      Does that make sense?

  • Nice work, easy to understand and well laid out.
    I’m fairly new to code but got 2 instances running. 16 switches, 16 LEDs. One chip does switches with interupts, one with LEDs.
    I’ve been trying to get the outputs to latch or (toggle), Is there any fairly short way of doing that?
    Any help would be appreciated.

    • Wow, that’s great!

      The bitwise not (~) operator is used to flip, or toggle, bits in a number. You can toggle an individual output with
      mcp23017.digitalWrite(LED0, ~mcp23017.digitalRead(LED0));
      and use
      mcp23017.write(~mcp23017.read());
      to toggle all outputs.

  • Thanks for the swift reply John, Yes the (~) inverts the bit. Maybe I didn’t explain too well. I was looking for, Say; Press switch 4, LED 4 comes on, Press switch 4 again LED 4 goes off. Basically to control a bank of 16 Relays. I can obtain 2 bytes (Bank1 and Bank 2) but on releaseing buttons they revert to zero. Thanks again for the info.

    • If you are using dip or toggle switches, then your desired outcome happens naturally by just copying all the port values; turn the switch on and the LED turns on, turn the switch off and the LED turns off.

      If you are wanting to use momentary push buttons, then a bit more work is required. You need an extra variable of type uint16_t that keeps track of the current state of all the 16 switches and you need to implement button debouncing. The code would probably look something like the following:

      void readButtons() {  // invoke from loop()
         static uint16_t previousButtonStates = 0;  // static used to retain last value
         uint16_t currentButtonStates = mcp23017_buttons.read();
         static unsigned long previousTimeButtonsChanged = 0;  // static used to retain last value
         unsigned long currentTime = millis();
         if (currentTime - previousTimeButtonsChanged > 100  // button debounce time of 100-250 ms
            && currentButtonStates != previousButtonStates) {
               mcp23017_relays.write(currentButtonStates);
               previousTimeButtonsChanged = currentTime;
               previousButtonStates = currentButtonStates;
         }
      }
  • I am using momentary push buttons with LED’s built in, and hardware de-bounce which seems to work well. (nothing strange happening yet!)

    Thanks for your code, However I got in a bit of a knot with using both ports and 2 instances of the chip.
    I discovered the ‘Exclusive Or’ Operator could do what I needed.

    void portCopy() {
    // Copy values from PORTA and PORTB (switches) to trigger1 and trigger2 (Temporary variables)
    trigger1 =(mcp23017.readPort(MCP23017Port::A));
    trigger2 =(mcp23017.readPort(MCP23017Port::B));

    // Exclusive Or with the variable which holds the LED value and the Relay drive value
    relay1 = (relay1^trigger1);
    relay2 = (relay2^trigger2);

    //Write the Relay drive value to the second instance of the chip. This keeps LED’s on until switch is //pressed again (and the Relays)
    mcp23017_2.writePort(MCP23017Port::A,relay1);
    mcp23017_2.writePort(MCP23017Port::B,relay2);

    // Also can save a couple of (trigger) bytes by using this

    void portCopy() {
    relay1 = (relay1^mcp23017.readPort(MCP23017Port::A));
    relay2 = (relay2^mcp23017.readPort(MCP23017Port::B)); mcp23017_2.writePort(MCP23017Port::A,relay1);
    mcp23017_2.writePort(MCP23017Port::B,relay2);
    }

    I’m running this on an ESP32 (For the wifi) to have local and remote switching.
    The handy thing with ESP32 is you can use interupt on any pin.
    As my project advances I could send you the code, maybe use some of it on your Page or you might be able to tell me where I’m going wrong.
    Again thanks for all the help and code.
    Bri

    • I am not a Spanish speaker, so I may not have the correct translation.

      I believe you can use the same sketch for an ESP32 board. You just need to install and use the correct core for the ESP32 and use the appropriate I2C pins.

      I also found the following Getting started with ESP32 tutorial that may be helpful to you.

  • hello, first of all thank you for your sharing, your course is masterful, just a clarification, is the capacitor electrolytic or not, in the photo it looks like it is not, yet the link that sends on the page concerns electrolytic capacitors, in waiting for your reply, I wish you a good day

    • I’m happy to hear you liked the tutorial. No, the capacitor is not electrolytic, just a standard ceramic capacitor. The circuit will probably work just fine without it, but it does provide voltage stability in a noisy environment.

  • John, I had a previous comment about the project I’m working on. I just received the prototype board, and the VDD and VSS are flipped-flopped. The VSS has the +5VDC, and the VDD has the DGND.

    The breadboard model from your code works, but the Altium designer switched from what I submitted. Is there a simple solution with code? Or pull out the soldering iron?

    I see the VSS and VDD get used differently from my learning jpurney. Is there a reason for this?

    • Sorry to hear that, but these often end up being great learning experiences.

      I’m afraid you are going to have to pull out the soldering iron. Except in rare instances, VDD is always connected to the higher voltage and VSS to the lower voltage (or ground).

  • bonjour, j’ai besoin d’aide pour notre projet d’un simulateur d’un réseau,
    nous avons utiliser (un) Arduino uno et ( 14) MCP23017 pour contrôler des bouton poussoir.
    dans l’installation il y a des MCP23017 qui sont même adresse mais l’un en écriture et l’autre en lecture
    alors nous voudrions un peu d’aide pour le programme s’il vous plait.

  • et si tous les broche d’une MCP23017 sont en lecture ou en écriture , est ce qu’on devrais encore écrire cette code
    mcp23017.writeRegister(MCP23017Register::GPIO_A, 0x00);
    mcp23017.writeRegister(MCP23017Register::GPIO_B, 0x00);

    • Yes, it should work just fine with pushbuttons. With normally open momentary pushbuttons, the values will be read as high when the buttons are being pressed and will be read as low when the buttons are released due to the polarity inversion implemented in the sketches. For the LEDs used in the tutorial, this will work just fine. If your application requires you to read “clean” button presses, you probably also need to perform button debouncing and keep track of the button states.

    • dricardo
      20hpost #1
      Hi everone. I’m in a deliberate dilemma of wanting to expand the ESP32’s digital ports and connect to the network via RJ45 cable.
      First I installed the MCP23017 port expander including more than one (1) (Thus identifying the addresses) and it simply worked well with the example codes from the Adafruit-MCP23017 library itself.
      Separately, I configured the ENC28J60 shield to connect the esp32 to the network using the cable. Luckily I also managed to get it working.
      Finally I wanted to put together the codes so that I could control the inputs and outputs (Buttons and LEDs) and also through the home assistant via mqtt. I was only able to make the buttons and LEDs connected directly to the ESP32 work, I can control the LEDs through the buttons as well as through the home assistant via mqtt.
      The problem now is to control the LEDs connected to the GPIO expander. They don’t trigger anything. Neither through the home assistant nor through the buttons. The buttons connected to the expanders also show no effect when pressed. I was supposed to see it through the serial monitor, in the same way I see those connected to the ESP32.

      #include
      #include
      #include
      #include
      #include
      #include

      // Instanciar objeto mcp
      Adafruit_MCP23X17 mcp1;
      Adafruit_MCP23X17 mcp2;

      // Definir o endereço MAC e IP para o ENC28J60
      byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
      IPAddress ip(192, 168, 1, 177);

      // //Definir detalhes do MQTT broker
      const char* mqtt_server = “192.168.1.29”;
      const int mqtt_port = 1883;
      const char* mqtt_username = “mqtt_user”;
      const char* mqtt_password = “mqtt_pass”;

      // Define GPIO pins
      const int lampada_varanda = 2;
      const int interruptor_varanda = 4;
      const int interruptor_sala_de_estar = 0;
      const int lampada_sala_de_estar = 0;
      int lampada_varandaValue = LOW;
      int lampada_sala_de_estarValue = LOW;

      EthernetClient ethClient;
      PubSubClient client(ethClient);

      void callback(char* topic, byte* payload, unsigned int length) {
      // lidar com a mensagem chegou
      String content = “”;
      char character;
      for (int num = 0; num < length; num++) {
      character = payload[num];
      content.concat(character);
      }
      Serial.println(topic);
      Serial.println(content); // Mensagens enviadas por ações de botõo são retornadas pelo broker e impressas no monitor serial
      if (content == "1on") {
      lampada_varandaValue = HIGH;
      }

      if (content == "1off") {
      lampada_varandaValue = LOW;
      }

      if (content == "2on") {
      lampada_sala_de_estarValue = HIGH;
      }

      if (content == "2off") {
      lampada_sala_de_estarValue = LOW;
      }

      digitalWrite(lampada_varanda, lampada_varandaValue);
      digitalWrite(lampada_sala_de_estar, lampada_sala_de_estarValue);

      }
      Bounce bouncer1 = Bounce();
      Bounce bouncer2 = Bounce();
      void setup() {
      // Iniciar o Moitor Serial
      Serial.begin(115200);
      Wire.begin();

      // Inicializar as instâncias dos MCP23017
      if (!mcp1.begin_I2C(0x20)) {
      Serial.println("Erro ao inicializar o MCP23017 1. Por favor, verifique as conexções.");
      while (1);
      }

      if (!mcp2.begin_I2C(0x21)) {
      Serial.println("Erro ao inicializar o MCP23017 2. Por favor, verifique as conexções.");
      while (1);
      }

      // Setup Ethernet
      Ethernet.begin(mac, ip);

      // Setup MQTT
      client.setServer(mqtt_server, mqtt_port);
      client.setCallback(callback);

      // Setup GPIO
      pinMode(lampada_varanda, OUTPUT);
      pinMode(interruptor_varanda, INPUT_PULLUP);
      digitalWrite(interruptor_varanda, HIGH);
      bouncer1.attach(interruptor_varanda);
      bouncer1.interval(5);

      mcp1.pinMode(interruptor_sala_de_estar, INPUT);
      mcp2.pinMode(lampada_sala_de_estar, OUTPUT);
      digitalWrite(interruptor_sala_de_estar, HIGH);
      bouncer2.attach(interruptor_sala_de_estar);
      bouncer2.interval(5);
      }

      void loop() {
      // ConnectConectar ao MQTT
      if (!client.connected()) {
      reconnect();
      }

      //Lidando com as mensagens do mqtt broker de novo
      client.loop();

      if (bouncer1.update()) {
      if (bouncer1.read() == HIGH) {
      if (lampada_varandaValue == LOW) {
      lampada_varandaValue = HIGH;
      client.publish("casa/interruptores/varanda", "1on");
      } else {
      lampada_varandaValue = LOW;
      client.publish("casa/interruptores/varanda", "1off");
      }
      }
      }
      int buttonState = mcp1.digitalRead(interruptor_sala_de_estar);
      if (bouncer2.update()) {
      if (bouncer2.read() == HIGH) {
      if (lampada_sala_de_estarValue == LOW) {
      lampada_sala_de_estarValue = HIGH;
      client.publish("casa/interruptores/sala_estar", "2on");
      } else {
      lampada_sala_de_estarValue = LOW;
      client.publish("casa/interruptores/sala_estar", "2off");
      }
      }
      }

      }
      void reconnect() {
      // Repetição até se conectar
      while (!client.connected()) {
      Serial.println("Tentando conectar ao servidor MQTT…");

      // Tentativa de conecxão
      if (client.connect("ESP32Client", mqtt_username, mqtt_password)) {
      Serial.println("Conectado");
      // Subcrevendo – se ao tópico (se Necessário)
      client.subscribe("casa/interruptores/#");
      } else {
      Serial.print("Falha ao conectar, rc= ");
      Serial.println(client.state());
      Serial.println("Tentar de novo em 5 segundos");
      // Esperar 5 segundos antes de tentar outra vez
      delay(5000);
      }
      }
      }

      If anyone can take a look at my code and help me understand where I'm going wrong. It's a project to automate my home.
      Hugs.
      Dércio

    • Sorry, but I don’t have any experience with the Adafruit_MCP23X17 device library. Hopefully, someone else can offer assistance. Since this tutorial uses the MCP23017 library by Bertrand Lemasle, you might have better luck asking your question on one of Adafruit’s forums.

  • bonjour , j’ai besoin d’aide pour mon projet , j’utilise une cart arduino UNO multiplier par deux MCP23017 , l’autre MCP en lecture et l’autre en ecriture , mais il y a un problème de bibliothèque

  • These greatly helps me . But i have question please, I want to use it with blynk app as a smart home, the problem I found out is that, it has a limited distance, some meter away from the sensor, how can I extend the wires to about 200Metres . And was that possible?

    • I’m happy to hear that you found the tutorial beneficial.
      Ideally, wire lengths of 200 m with sufficiently large wire widths should work just fine. For instance, the resistance of a 12 gauge wire at 200 m is only about 1 ohm. Realistically, it depends on how electrically “noisy” your environment is and how quickly you are flipping digital states. I don’t have much experience in this area, but I suggest you just try it and see how it goes. You can also try using a repeater or buffer circuit in the middle of the long wire run to boost the signal if necessary. If none of that works in your environment, a wireless solution may be the answer.

  • John, the journey continues. The hardware fix has been completed (see above comment), and I’m trying to find a solution as to why the code just stops, which seems to be happening during the inputs to the MCP23017.

    I am not using interrupts since efficiency is not a priority at this moment. The MCP23017 is used for inputs only, my code minics your example expect my PORTA are inputs. I do have INTB to PIN13 of an Arduino2650, but I did not utilize INTA. I believe this can be internally connected per your explanation, correct?

    The code works for the first few cycles, then as the inputs change state, it seems to stop in the MCP23017, and the Arduino2650 is not responsive to any inputs/outputs. Power cycle Arduino with the same results.

    I’m confident this will be discovered as my error, but looking for guidance. The hardware is identical to your setup. Did I make a mistake by not using INTA? Are interrupts necessary for the code transmission on the I2C bus?

    Thanks,
    Dirk

    • I am a little confused. You stated you are not using interrupts, however, you mentioned INTB being connected to pin 13. You are correct that INTA and INTB can be internally ORed together using the MCP23017InterruptMode::Or mode. If you are not using interrupts, you don’t need to worry about the INTA or INTB pins. If you do end up using interrupts, I believe that pin 13 should work, however, it does have the built-in LED connected to that pin. I don’t think this would be a problem, but it may be an area of investigation.

      The use of interrupts is not required. The first sketch shown, within the Reading And Writing Single Inputs And Outputs Using Familiar digitalRead() And digitalWrite() Functionality section, does not use interrupts. You can just poll the pins with the mcp23017.digitalRead() method. Try that code again, without the outputs, and see if that works for you. If not, post your code somewhere and I will look at it for you.

  • Please be aware that there is a hardware change in the latest MCP23017, starting at some point in 2022. The pins GPA7 and GPB7 are now output only. This holds only for the I2C version. In the SPI version (MCP23S17) the pins are still bi-directional.
    Make sure to have the correct datasheet, dated 2022 or newer.

    • Wow, that changes things. It will definitely cause some consternation with existing designs.

      Thank you so much for letting us know! I will make a note in the affected tutorials soon.

    • I could not find that a silicon change had occurred, so a datasheet only change of this magnitude was quite disturbing. Upon further research, I found that Microchip discovered an issue with the part and updated the datasheet accordingly. Please see this article by Microchip for additional information.

  • Just to adapt the original code to be Implemented at ESP01

    #define GPIO0 0 // for GPIO 0 as SDA pin
    #define GPIO2 2 // for GPIO 2 as SCL pin
    void setup() {
    Wire.begin(GPIO0, GPIO2); // wake up I2C bus SDA, SCL // initialize I2C serial bus

    … /* Do not set pinmode for GPIO0 and GPIO2 */
    }

  • Thanks for great tutorial.
    mcp23017.portMode(MCP23017Port::B,
    0b11111111, // direction (IODIRB) – set direction of all pins as inputs
    0b11111111, // pull-up (GPPUB) – enable 100K pull-up resistors on all inputs
    0b11111111); // polarity (IPOLB) – invert logic polarity for all inputs

    I have buttons, then I press it shorted to ground, than button is released, its pullup to 5v
    if I set 0b00000000 polarity it will be sense the same – meaning push ->low, release high, its correct ?

    • You are welcome. I’m glad you liked it.

      Your assumption is correct. When all pins are inverted (0b11111111), pressing the button (closing the switch) drives the pin LOW but is read as HIGH. When all pins are not inverted (0b00000000), pressing the button drives the pin LOW and is read as LOW.

  • Your tutorial has been very helpful as I am new to coding. I am using the mcp23017 to detect up to 16 switches which will ultimately be used to position servos. I have used your last method to detect which switch changed and its current state. Generally the code works but I get random errors with the wrong switch indicated.

    I have disconnected the switches on GPA7 & GPB7 as suggested without improvement and suspect I need also to set the direction of these pins to output, but cannot fathom how to set individual pins.

    Can you advise if I’m making the correct diagnosis and advise how to set the direction of GPA7 & GPB7

    • I’m glad to hear that you are finding the tutorial helpful.

      The easiest way to set an individual pin as an output is to use the pinMode() method as was used in the sketch’s configurePinsWithPinMode() function.

      mcp23017.pinMode(7, OUTPUT);  // set GPA7 as an output
      mcp23017.pinMode(15, OUTPUT);  // set GPB7 as an output

      Since you will be using a combination of inputs and outputs within the same port, it is probably more understandable to use the configurePinsWithPinMode() function than the configurePinsWithPortMode() function to set all of the individual pins.

      When debugging issues, it is usually best to start with a known good state and progress from there, slowly making and testing changes until you have what you want. With that in mind, did the full functionality of the tutorial work for you before you began making changes? If so, I suggest ignoring interrupts for now and start by using the configurePinsWithPinMode() function to make all of the pins an input, except for the GPA7 and GPB7 pins, and then create a new function, that gets called from within the loop() function, that reads all of the pin values and prints them to the Serial Monitor for you to check their status.

      If you want me to look at your code for suggestions, please post it somewhere and send me a link to it.

  • Thanks for responding so promptly. I have done as you suggest and used pin mode to set the pins and not using 7 and 15 for input. it seems as though pins 7 & 15 were causing the erratic results previously.
    I would still prefer to use interrupts to detect which switch has changed as it seems the most efficient method, so I will persevere as my original code did compile and I think that it was the 7 & 15 issue causing the problem .
    I will change the port mode method of setting pin direction to pin mode and see if this cures the problem. unfortunately I have to go abroad for a few weeks so my have to put this on one side for now. Many thanks for you help.

    • You are very welcome.

      Sorry if I was not clear before. I was trying to suggest that we begin with a simpler sketch to make sure the fundamentals are working before including additional complexity, such as interrupts. I completely understand wanting to have that functionality. We can probably even simplify the interrupt and pin reading code since you are only interested in inputs.

      Enjoy your trip and we can revisit it once you return.

  • Hi
    I have two MCP23017 in my system
    Both configured as
    mcp_1.portMode(MCP23017Port::A, 0b11111111, 0b11111111, 0b00000000);
    mcp_1.portMode(MCP23017Port::B, 0b11111111, 0b11111111, 0b00000000);
    mcp_2.portMode(MCP23017Port::A, 0b11111111, 0b11111111, 0b00000000);
    mcp_2.portMode(MCP23017Port::B, 0b11111111, 0b11111111, 0b00000000);
    both MCP’s on different addresses.

    To MCP23017 1 I have 8 switches connected, when switch is on, it pulls down through 3.7k resistor
    On portA bits PA0 to PA6 the behavior as needed, but the PA7 gives me strange result
    Then I connect to port B, same behavior occurs: PB0 to PB6 is correct, PB7 strange results.

    The second MCP23017 works correctly on every bit

    Do you have any advice?
    Thanks

  • Hi
    After long research, pins GPA7, GPB7 are output only, according Microchip MCP23017 Datasheet Rev D. Please be aware. So only 14 inputs can be used.
    Thanks

    • Thank you for the heads up and I am sorry to hear you had to do so much research. Perhaps you are looking at an older version of the tutorial as I had already included a warning, in October of 2023, at the top of the tutorial about this issue.

    • Hello
      I was so happy to find such good tutorial, it help me a lot, I did not notice to this warning on top of the page.
      Again – Thank you for great tutorial

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.