MCP23x1y Port Expander

About this post

In one of my first posts, I had reported on the MCP23017 port expander and my associated library. Meanwhile, I have adapted the library so that you can also use it for other members of the MCP23x1y family. That’s why I’m coming back to this topic. In this post, I will primarily discuss the differences between the MCP23x1y ICs. For the details, which are the same for all representatives of the MCP23x1y port expanders, please to my post about the MCP23017.

By the way: The somewhat cumbersome naming MCP23x1y is not official. “MCP23x” would also have included the models MCP23008 and MCP23S08, which I will not go into here. They only have 8 GPIOs.

Here’s what’s in store for you:

Overview of the MCP23x1y family

The MCP23x1y family has five members:

MCP23x1y Family
MCP23x1y family (PDIP version)

Here is an overview of the most important features of the MCP23x1y port expanders:

Key differences of the MCP23x1y port expanders at a glance
Key differences of the MCP23x1y port expanders at a glance

The most commonly used representative is the MCP23017. Like the MCP23018, it is I2C controlled. If you want to switch or read the GPIOs (General Purpose Input Output) at a higher speed, then you should use one of the SPI controlled “S” variants.

The maximum current for a single pin is 25 mA for all representatives of the MCP23x1y family. However, this current may only flow simultaneously on all pins in the “18” variants. With these, there is again a restriction regarding the current direction. The GPIOs of the MCP23018 and MCP23S18 are “open drain” inputs/outputs. This means that they behave like a simple transistor. You can open it (set it to OUTPUT) and let current flow into the pin to ground (sink), but you can’t use it as a current source.

The MCP23016 has a special position. It is the only one with a different register structure. It also requires an external clock. Since it was also marked as “not recommended for new designs” by the manufacturer Microchip, I did not bother to adapt my library for it. But I list it for the sake of completeness.

Use of the MCP23x1y port expanders in detail

In the following, I will discuss the below topics for the members of the MCP23x1y family:

  • Pinout
  • Connection to the microcontroller
  • Simple input/output operations with the library MCP23017_WE

As you can see, I kept the name of the library. This is because I didn’t want to confuse those who already use the library.

MCP23017

Pinout

A representative of the MCP23x1y family - Pinout of the MCP23017

We start with the MCP23017. You supply it via VDD/VSS with 1.8 to 5.5 volts. You connect the I2C pins of your microcontroller with SDA and SCL of the MCP23017. I2C often works without pull-up resistors, but I would recommend to use them. We don’t need the interrupt pins for the sketches used in this post. You connect the reset pin to an I/O pin of your microcontroller.

The I2C address is set using the scheme: 0b00100-A2-A1-A0. If Ax IS HIGH, replace it with a 1, otherwise with a zero. For example, if you connect A0, A1 and A2 to LOW, the address is 0b00100000 = 0x20 = 32.

Example circuit

To try out the sample sketches, connect each GPIO to an LED. When choosing the LEDs, keep in mind that the total current of VDD should not exceed 125 milliamps. This means that when all LEDs are on, each one may only consume about 7.8 milliamperes on average. The circuit on an Arduino Nano could look like this:

The MCP23017 connected to an Arduino Nano
The MCP23017 connected to an Arduino Nano

If, like me, you don’t feel like wiring sixteen resistors and LEDs, then I recommend LED bars with integrated resistors as shown below. They are available with a common anode or cathode:

LED bars for testing the MCP23x1y port expanders
LED bars with common cathode (left) or common anode (right)

By the way, I found the LED bars on AliExpress. This makes the set-up fast and easy. And they consume only 2 to 3 milliamperes per LED. This is how it looked on my breadboard (without pull-ups):

Example circuit with LED bars connected to the MCP23017

Example sketch for the MCP23017

The following example sketch shows how to create your MCP23017 object and control the LEDs with various functions.

#include <Wire.h>
#include <MCP23017.h>
#define RESET_PIN 5 
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)

/* There are several ways to create your MCP23017 object:
 * MCP23017 myMCP = MCP23017(MCP_ADDRESS)            -> uses Wire / no reset pin (if not needed)
 * MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN)  -> uses Wire / RESET_PIN
 * MCP23017 myMCP = MCP23017(&wire2, MCP_ADDRESS)    -> uses the TwoWire object wire2 / no reset pin
 * MCP23017 myMCP = MCP23017(&wire2, MCP_ADDRESS, RESET_PIN) -> all together
 */
 
MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);

int wT = 500; // wT = waiting time

void setup(){ 
  Wire.begin();
  myMCP.Init(); 
  delay(wT);
  myMCP.setPortMode(0b11111111, A);  // Port A: all pins are OUTPUT 
  myMCP.setPortMode(0b11111111, B);  // Port B: all pins are OUTPUT
  myMCP.setAllPins(A,ON);            // Port A: all pins are HIGH
  myMCP.setAllPins(B,ON);            // Port B: all Pins are HIGH
  delay(wT);
  myMCP.setAllPins(A,OFF);           // Port A: all pins are LOW
  myMCP.setAllPins(B,OFF);           // Port B: all pins are LOW 
  delay(wT);
  byte portValue = 0;
  for(int i=0; i<8; i++){
    portValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc.
    myMCP.setPort(portValue, A);
    delay(wT);
  }
  portValue = 0;
  for(int i=0; i<8; i++){
    portValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc.
    myMCP.setPort(portValue, B);
    delay(wT);
  }
  myMCP.setAllPins(A,OFF);           // Port A: all pins are LOW
  myMCP.setAllPins(B,OFF);           // Port B: all pins are LOW 
  delay(wT);
  myMCP.setPin(3, A, HIGH);          // Pin 3 / PORT A is HIGH
  myMCP.setPin(1, B, HIGH);          // Pin 1 / PORT B is HIGH
  delay(wT);
  myMCP.setAllPins(A,OFF);           // Port A: all pins are LOW
  myMCP.setAllPins(B,OFF);           // Port B: all pins are LOW 
  myMCP.setPortMode(0,A);            // Port A: all pins are INPUT 
  myMCP.setPortMode(0,B);            // Port B: all pins are INPUT
  myMCP.setPinX(1,A,OUTPUT,HIGH);    // A1 HIGH 
  delay(wT);
  myMCP.togglePin(1, A);             // A1 LOW
  delay(wT);
  myMCP.togglePin(1, A);             // A1 HIGH
  delay(wT); 
  // the following two lines are similar to setPinX(2,B,OUTPUT,HIGH);
  myMCP.setPinMode(2,B,OUTPUT);      // B2 is OUTPUT
  myMCP.setPin(2,B,HIGH);            // B2 is HIGH
  delay(wT); 
  myMCP.setPortX(0b00001111,0b10001111,B); // B0-B4: OUTPUT/HIGH, B7: INPUT, HIGH;
}

void loop(){ 
} 

 

This post is not meant to be a repeat of my first post about the MCP23017, but a few explanations are certainly useful, so you don’t have to jump back and forth between posts.

The GPIOs of the MCP23017 work in principle the same way as those of an Arduino or any other microcontroller. You can set them as INPUT or OUTPUT and you can switch them HIGH or LOW. If you power the LEDs via the GPIOs, the pins must be OUTPUT/HIGH for the LEDs to light up.

I have implemented a number of functions in the library that allow you to address individual pins or entire ports. Where “0” is basically INPUT or LOW. “1” corresponds to OUTPUT or HIGH. Instead of HIGH or LOW, you can also use “ON” or “OFF”. These are all just synonyms for 0 or 1.

  • setPinMode(): determines the mode (INPUT / OUTPUT) for a pin.
  • setPortMode(): sets INPUT / OUTPUT for an entire port, but individually for each pin.
  • setPin(): sets a pin to HIGH or LOW.
  • setPort(): HIGH/LOW for a whole port, but individually for each pin.
  • setAllPins(): sets all pins of a port uniformly to HIGH or LOW.
  • setPinX(): combined setPinMode() and setPin() in one function.
  • setPortX(): combined setPortMode() and setPort() in one function.
  • togglePin(): changes a pin from HIGH to LOW or vice versa.

MCP23S17

Pinout

In contrast to the MCP23017, the MCP23S17 is controlled by SPI. Accordingly, it has an SCK (Serial Clock), SI (Slave In), SO (Slave Out) and a CS (Chip Select) pin.

At first, it is surprising that it also has the three address pins A0, A1 and A2. If you address the MCP23S17 via SPI, you must first pull the CS pin to LOW as usual. Then you send a control byte consisting of the address and the read/write bit. This has the advantage that you can attach eight MCP23S17 ICs to a single CS line. This is followed by the register byte and then the data. Of course, you don’t have to worry about these things in detail, as the library takes care of that.

Example circuit

The circuit for the control via Arduino Nano is not particularly surprising:

The MCP23S17 connected to an Arduino Nano
The MCP23S17 connected to an Arduino Nano

Example sketch

Since the MCP23S17 differs from the MCP23017 only by the SPI communication, the example sketch is only slightly different. A function is added, and that is setSPIClockSpeed(). This allows you to select the SPI clock rate within the capabilities of your microcontroller and the MCP23S17 (max. 10 MHz). The default setting of the library is 8 MHz. Otherwise, the sketch probably needs no further explanation.

#include <SPI.h>
#include <MCP23S17.h>
#define CS_PIN 7   // Chip Select Pin
#define RESET_PIN 5 
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)

/* There are two ways to create your MCP23S17 object:
 * MCP23S17 myMCP = MCP23S17(CS_PIN, RESET_PIN, MCP_ADDRESS);
 * MCP23S17 myMCP = MCP23S17(&SPI, CS_PIN, RESET_PIN, MCP_ADDRESS);
 * The second option allows you to create your own SPI objects,
 * e.g. in order to use two SPI interfaces on the ESP32.
 */
 
MCP23S17 myMCP = MCP23S17(CS_PIN, RESET_PIN, MCP_ADDRESS);

int wT = 500; // wT = waiting time

void setup(){ 
  SPI.begin();
  myMCP.Init(); 
  // myMCP.setSPIClockSpeed(8000000); // Choose SPI clock speed (after Init()!)
  delay(wT);
  myMCP.setPortMode(0b11111111, A);  // Port A: all pins are OUTPUT 
  myMCP.setPortMode(0b11111111, B);  // Port B: all pins are OUTPUT
  myMCP.setAllPins(A,ON);            // Port A: all pins are HIGH
  myMCP.setAllPins(B,ON);            // Port B: all Pins are HIGH
  delay(wT);
  myMCP.setAllPins(A,OFF);           // Port A: all pins are LOW
  myMCP.setAllPins(B,OFF);           // Port B: all pins are LOW 
  delay(wT);
  byte portValue = 0;
  for(int i=0; i<8; i++){
    portValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc.
    myMCP.setPort(portValue, A);
    delay(wT);
  }
  portValue = 0;
  for(int i=0; i<8; i++){
    portValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc.
    myMCP.setPort(portValue, B);
    delay(wT);
  }
  myMCP.setAllPins(A,OFF);           // Port A: all pins are LOW
  myMCP.setAllPins(B,OFF);           // Port B: all pins are LOW 
  delay(wT);
  myMCP.setPin(3, A, HIGH);          // Pin 3 / PORT A is HIGH
  myMCP.setPin(1, B, HIGH);          // Pin 1 / PORT B is HIGH
  delay(wT);
  myMCP.setAllPins(A,OFF);           // Port A: all pins are LOW
  myMCP.setAllPins(B,OFF);           // Port B: all pins are LOW 
  myMCP.setPortMode(0,A); // Port A: all pins are INPUT 
  myMCP.setPortMode(0,B);  // Port B: all pins are INPUT
  myMCP.setPinX(1,A,OUTPUT,HIGH); // A1 HIGH 
  delay(wT);
  myMCP.togglePin(1, A);             // A1 LOW
  delay(wT);
  myMCP.togglePin(1, A);             // A1 HIGH
  delay(wT); 
  // the following two lines are similar to setPinX(2,B,OUTPUT,HIGH);
  myMCP.setPinMode(2,B,OUTPUT);      // B2 is OUTPUT
  myMCP.setPin(2,B,HIGH);            // B2 is HIGH
  delay(wT); 
  myMCP.setPortX(0b00001111,0b10001111,B); // B0-B4: OUTPUT/HIGH, B7: INPUT, HIGH;
}

void loop(){ 
} 

 

MCP23018

The MCP23018 is again I2C controlled. But there is a fundamental difference to the MCP23017. To light up an LED on the MCP23018, you have to use it as a current sink. This means that you do not supply the LED with power via the MCP23018, but let the current flow into the MCP23018. For this to happen, the respective pin must be OUTPUT/LOW.

Pinout

But the MCP23018 has another special feature. In addition to the different assignment of GPIOs, it has only a single address pin. The address is set via the voltage level applied to the address pin. A rather unusual concept, the advantage of which is not really apparent to me.

The scale for the “address voltage” is determined by the supply voltage VDD. If you apply 1/16th of the supply voltage to the address pin, then the address is 0x20. At 3/16th it is 0x21, at 5/16th it is 0x22, at 7/16th it is 0x23, etc. You set the highest address 0x27 by applying a voltage of 15/16th VDD. You can also set 0x20 by going to VSS level at the address pin (0/16th) and 0x27 by going to VDD level. For the other addresses, the tolerances are lower. An overview of the tolerances and example tables can be found in the data sheet, page 11.

If you want to run eight MCP23018 ICs, then a circuit (reduced to the I2C part) could look like this:

Example circuit

In my example circuit, the LEDs are powered by the 5 volt output of the Arduino Nano. The current flows to GND via the MCP23018. I set the address via a voltage divider. Since I only use a single MCP23018 here, I could have alternatively connected the address pin to GND or VDD.

The MCP23018 connected to an Arduino Nano
The MCP23018 connected to an Arduino Nano

As before, I made it easy and used LED bars with integrated resistors. In this case, of course, the version with common anode:

Example circuit with LED bars attached to the MCP23018

Again, I have omitted the pull-ups for the I2C lines (in case you miss them). However, I added them in the later speed tests.

Example sketch

At first glance, the sketch doesn’t look much different from the previous ones. If you take a closer look, however, you will see that I control the LEDs via OUTPUT/INPUT and not via HIGH/LOW.

#include <Wire.h>
#include <MCP23018.h>
#define RESET_PIN 5 
#define MCP_ADDRESS 0x20 

/* There are several ways to create your MCP23018 object:
 * MCP23018 myMCP = MCP23018(MCP_ADDRESS)            -> uses Wire / no reset pin (if not needed)
 * MCP23018 myMCP = MCP23018(MCP_ADDRESS, RESET_PIN)  -> uses Wire / RESET_PIN
 * MCP23018 myMCP = MCP23018(&wire2, MCP_ADDRESS)    -> uses the TwoWire object wire2 / no reset pin
 * MCP23018 myMCP = MCP23018(&wire2, MCP_ADDRESS, RESET_PIN) -> all together
 * Successfully tested with two I2C busses on an ESP32
 */
 
MCP23018 myMCP = MCP23018(MCP_ADDRESS, RESET_PIN);

int wT = 500; // wT = waiting time

void setup(){ 
  Wire.begin();
  myMCP.Init(); 
  delay(wT);
  myMCP.setAllPins(A,OFF);            // Port A: all pins are LOW
  myMCP.setAllPins(B,OFF);            // Port B: all Pins are LOW
  myMCP.setPortMode(0b11111111, A);   // Port A: all pins are OUTPUT = LEDs are on!
  myMCP.setPortMode(0b11111111, B);   // Port B: all pins are OUTPUT
  delay(wT);
  myMCP.setPortMode(0b00000000, A);   // Port A: all pins are INPUT = LEDs are off
  myMCP.setPortMode(0b00000000, B);   // Port B: all pins are INPUT
  delay(wT);
  byte portModeValue = 0; // = 0b00000000
  for(int i=0; i<8; i++){
    portModeValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc.
    myMCP.setPortMode(portModeValue, A);
    delay(wT);
  }
  portModeValue = 0;
  for(int i=0; i<8; i++){
    portModeValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc.
    myMCP.setPortMode(portModeValue, B);
    delay(wT);
  }
  myMCP.setPortMode(0,A);           // Port A: all pins are INPUT
  myMCP.setPortMode(0,B);           // Port B: all pins are INPUT 
  delay(wT);
  myMCP.setPinMode(3, A, OUTPUT);          // Pin 3 / PORT A is OUTPUT/LOW
  myMCP.setPinMode(1, B, OUTPUT);          // Pin 1 / PORT B is OUTPUT/LOW
  delay(wT);
  myMCP.setPortMode(0,A);           // Port A: all pins are INPUT
  myMCP.setPortMode(0,B);           // Port B: all pins are INPUT
  myMCP.setPinX(1,A,OUTPUT,LOW);    // A1 HIGH 
  delay(wT);
  myMCP.togglePin(1, A);             // A1 LOW
  delay(wT);
  myMCP.togglePin(1, A);             // A1 HIGH
  delay(wT); 
  // the following two lines are similar to setPinX(2,B,OUTPUT,LOW);
  myMCP.setPinMode(2,B,OUTPUT);     // B2 is OUTPUT/LOW
  myMCP.setPin(2,B,LOW);            // B2 is still OUTPUT/LOW
  delay(wT); 
  myMCP.setPortX(0b10001111,0b10000000,B); // B0-B4: OUTPUT/LOW, B7: OUTPUT, HIGH;
}

void loop(){ 
} 

 

If a pin is in the OUPUT/LOW state, the connected LED is on. Instead of switching them off by with INPUT/LOW, you can do the same by switching to OUTPUT/HIGH.

MCP23S18

Pinout

The MCP23S18 does not offer any big surprises. It is simply the SPI version of the MCP23018. Unlike the MCP23S17, however, it does not have the option of being operated with different addresses. Accordingly, you have to connect each individual MCP23S18 with its own CS line.

Nevertheless, the MCP23S18 expects a “device opcode” consisting of the 0x20 and the read/write bit as the first byte during write or read operations. This is what the library takes care of. However, when creating the MCP23S18 object, you must always pass the 0x20 (MCP_SPI_CTRL_BYTE). This is simply for compatibility reasons.

Example circuit

For the sake of completeness, I show here again an example circuit:

The MCP23S18 connected to an Arduino Nano
The MCP23S18 connected to an Arduino Nano

Example sketch

Even the example sketch is no longer surprising.

#include <SPI.h>
#include <MCP23S18.h>
#define CS_PIN 7   // Chip Select Pin
#define RESET_PIN 5 
#define MCP_SPI_CTRL_BYTE 0x20 // Do not change

/* There are two ways to create your MCP23S18 object:
 * MCP23S18 myMCP = MCP23S18(CS_PIN, RESET_PIN, MCP_CTRL_BYTE);
 * MCP23S18 myMCP = MCP23S18(&SPI, CS_PIN, RESET_PIN, MCP_CTRL_BYTE);
 * The second option allows you to create your own SPI objects,
 * e.g. in order to use two SPI interfaces on the ESP32.
 */
 
MCP23S18 myMCP = MCP23S18(CS_PIN, RESET_PIN, MCP_SPI_CTRL_BYTE);

int wT = 500; // wT = waiting time

void setup(){ 
  SPI.begin();
  myMCP.Init(); 
  // myMCP.setSPIClockSpeed(8000000); // Choose SPI clock speed (after Init()!)
  delay(wT);
  myMCP.setAllPins(A,OFF);            // Port A: all pins are LOW
  myMCP.setAllPins(B,OFF);            // Port B: all Pins are LOW
  myMCP.setPortMode(0b11111111, A);   // Port A: all pins are OUTPUT = LEDs are on!
  myMCP.setPortMode(0b11111111, B);   // Port B: all pins are OUTPUT
  delay(wT);
  myMCP.setPortMode(0b00000000, A);   // Port A: all pins are INPUT = LEDs are off
  myMCP.setPortMode(0b00000000, B);   // Port B: all pins are INPUT
  delay(wT);
  byte portModeValue = 0; // = 0b00000000
  for(int i=0; i<8; i++){
    portModeValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc.
    myMCP.setPortMode(portModeValue, A);
    delay(wT);
  }
  portModeValue = 0;
  for(int i=0; i<8; i++){
    portModeValue += (1<<i); // 0b00000001, 0b00000011, 0b00000111, etc.
    myMCP.setPortMode(portModeValue, B);
    delay(wT);
  }
  myMCP.setPortMode(0,A);           // Port A: all pins are INPUT
  myMCP.setPortMode(0,B);           // Port B: all pins are INPUT 
  delay(wT);
  myMCP.setPinMode(3, A, OUTPUT);          // Pin 3 / PORT A is OUTPUT/LOW
  myMCP.setPinMode(1, B, OUTPUT);          // Pin 1 / PORT B is OUTPUT/LOW
  delay(wT);
  myMCP.setPortMode(0,A);           // Port A: all pins are INPUT
  myMCP.setPortMode(0,B);           // Port B: all pins are INPUT
  myMCP.setPinX(1,A,OUTPUT,LOW);    // A1 HIGH 
  delay(wT);
  myMCP.togglePin(1, A);             // A1 LOW
  delay(wT);
  myMCP.togglePin(1, A);             // A1 HIGH
  delay(wT); 
  // the following two lines are similar to setPinX(2,B,OUTPUT,LOW);
  myMCP.setPinMode(2,B,OUTPUT);     // B2 is OUTPUT/LOW
  myMCP.setPin(2,B,LOW);            // B2 is still OUTPUT/LOW
  delay(wT); 
  myMCP.setPortX(0b10001111,0b10000000,B); // B0-B4: OUTPUT/LOW, B7: OUTPUT, HIGH;
}

void loop(){ 
} 

 

MCP23016

Pinout

As you can see on the right, the MCP23016 stands out compared to the other members of the MCP23x1y family. The GPIOs are named differently, and there is no reset pin at all. It also has a CLK pin and a TP pin.

CLK must be connected to GND via a capacitor and to VDD via a resistor. This is used to set the clock speed. The data sheet recommends the combination of 33 pF / 3.9 kΩ. With 30 pF / 3.9 kΩ it also worked for me. At TP, you can check the clock signal. Otherwise, you leave it unconnected.

The most significant difference to the other family members is its different register architecture. Since it is also no longer recommended, I have refrained from introducing it to my library. Instead, I tried the CyMCP23016 library, which you can find here on GitHub. Overall, the selection of libraries for the MCP23016 is not very large.

Example circuit with an Arduino Nano

The CyMCP23016 library is equipped with a sample sketch, which I tried with the following circuit. In the sketch, only one LED attached to GPIO 0.0 is switched. However, this makes the principle sufficiently clear. I’m not reprinting the sketch here.

The MCP23016 on an Arduino Nano
The MCP23016 on an Arduino Nano

I2C vs. SPI – Speed Tests

Personally, if I have a choice between I2C and SPI, I tend to use I2C because fewer wires are needed. This is especially true if several devices are to be controlled. However, SPI has an advantage in applications that require a high transmission speed. I did a few tests for this.

I2C using the Arduino Nano

The MCP23017 supports an I2C clock speed of 1.7 MHz, the MCP23018 even 3.4 MHz. The problem is that most microcontrollers do not support these high clock speeds, including the ATmega328P based Arduinos such as the UNO, Nano or Pro Mini. On the Arduino Nano, I tested 100 kHz (standard) and 400 kHz (Fast I2C). To do this, I have determined the time it takes to read a port 10000 times (digitalRead() for a whole port, so to speak).

Here is the sketch:

#include <Wire.h>
#include <MCP23017.h>
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)
#define RESET_PIN 5  

MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);

void setup(){ 
  Serial.begin(115200);
  long startTime = 0;
  long readTime = 0;
  byte portStatus = 0;
  Wire.begin();
  myMCP.Init();  
  startTime = millis();
  for(long i=0; i<10000; i++){
    portStatus = myMCP.getPort(A);
  }
  readTime = millis() - startTime;
  Serial.print("Duration@100kHz [ms]: ");
  Serial.println(readTime);
  Serial.print("Duration@100kHz per Read [ms]: ");
  Serial.println(readTime/10000.0);

  Wire.setClock(400000);
  startTime = millis();
  for(long i=0; i<10000; i++){
    portStatus = myMCP.getPort(A);
  }
  readTime = millis() - startTime;
  Serial.print("Duration@400kHz [ms]: ");
  Serial.println(readTime);
  Serial.print("Duration@400kHz per Read [ms]: ");
  Serial.println(readTime/10000.0);
}
    
void loop(){ 
} 

 

And here is the result:

Output of I2C_speed_test_mcp23017_nano.ino
Output of I2C_speed_test_mcp23017_nano.ino

I2C using the ESP32

Then I repeated the test with the ESP32 and tried to set 1.7 MHz, because I thought the ESP32 could do that.

#include <Wire.h>
#include <MCP23017.h>
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)
#define RESET_PIN 18  

MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);

void setup(){ 
  Serial.begin(115200);
  long startTime = 0;
  long readTime = 0;
  byte portStatus = 0;
  Wire.begin();
  myMCP.Init();  
  
  startTime = millis();
  for(long i=0; i<10000; i++){
    portStatus = myMCP.getPort(A);
  }
  readTime = millis() - startTime;
  Serial.print("Duration@100kHz [ms]: ");
  Serial.println(readTime);
  Serial.print("Duration@100kHz per Read [ms]: ");
  Serial.println(readTime/10000.0);

  Wire.setClock(400000);
  startTime = millis();
  for(long i=0; i<10000; i++){
    portStatus = myMCP.getPort(A);
  }
  readTime = millis() - startTime;
  Serial.print("Duration@400kHz [ms]: ");
  Serial.println(readTime);
  Serial.print("Duration@400kHz per Read [ms]: ");
  Serial.println(readTime/10000.0);

  Wire.setClock(1700000);
  startTime = millis();
  for(long i=0; i<10000; i++){
    portStatus = myMCP.getPort(A);
  }
  readTime = millis() - startTime;
  Serial.print("Duration@1.7MHz [ms]: ");
  Serial.println(readTime);
  Serial.print("Duration@1.7MHz per Read [ms]: ");
  Serial.println(readTime/10000.0);
}
  
void loop(){ 
} 

 

The meager speed increase at 1.7 MHz surprised me at first: 

Output of I2C_speed_test_mcp23017_esp32.ino
Output of I2C_speed_test_mcp23017_esp32.ino

However, a measurement of the actual clock rate with the Logic Analyzer showed that even the ESP32 cannot deliver the 1.7 MHz clock rate. 655 kHz was the maximum. This is (approximately) consistent with what I’ve found in other places on the subject, e.g. here on GitHub.

Actual I2C clock using the ESP32

SPI using Arduino Nano

The Arduino Nano and all other boards based on the ATmega328P can deliver an SPI clock rate up to 16 MHz. The MCP23S17 supports an SPI clock rate of 10 MHz. However, you cannot set 10 MHz with the Arduino Nano, but only quotients of 16 MHz and integer dividers, i.e. 8 MHz, 4 MHz, 2 MHz, etc. So 8 MHz is the maximum. If you try to apply other values, the next possible lower frequency is taken.

This is the sketch:

#include <SPI.h>
#include <MCP23S17.h>
#define CS_PIN 7   // Chip Select Pin
#define RESET_PIN 5 
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)

MCP23S17 myMCP = MCP23S17(CS_PIN, RESET_PIN, MCP_ADDRESS);

void setup(){ 
  Serial.begin(115200);
  long startTime = 0;
  long readTime = 0;
  byte portStatus = 0;
  SPI.begin();
  myMCP.Init(); 
  
  myMCP.setSPIClockSpeed(2000000); // Choose SPI clock speed (after Init()!
  startTime = millis();
  for(long i=0; i<10000; i++){
    portStatus = myMCP.getPort(A);
  }
  readTime = millis() - startTime;
  Serial.print("Duration@2MHz [ms]: ");
  Serial.println(readTime);
  Serial.print("Duration@2MHz per Read [µs]: ");
  Serial.println(readTime/10.0);
  
  myMCP.setSPIClockSpeed(4000000); // Choose SPI clock speed (after Init()!)
  startTime = millis();
  for(long i=0; i<10000; i++){
    portStatus = myMCP.getPort(A);
  }
  readTime = millis() - startTime;
  Serial.print("Duration@4MHz [ms]: ");
  Serial.println(readTime);
  Serial.print("Duration@4MHz per Read [µs]: ");
  Serial.println(readTime/10.0);

  myMCP.setSPIClockSpeed(8000000);
  startTime = millis();
  for(long i=0; i<10000; i++){
    portStatus = myMCP.getPort(A);
  }
  readTime = millis() - startTime;
  Serial.print("Duration@8MHz [ms]: ");
  Serial.println(readTime);
  Serial.print("Duration@8MHz per Read [µs]: ");
  Serial.println(readTime/10.0);
}
  
void loop(){} 

 

And here is the result:

When using an ATmega328P based board, you can achieve almost ten times the read rate if you switch from the MCP23017 to the MCP23S17.

Reading an Arduino’s own pins is still much faster. One digitalRead() needs only about 3.5 μs on the Arduino Nano. And the direct reading of the PINB/PIND registers is even faster. Only about 0.44 μs are needed for this. For details, check out my post about port manipulation.

Maximum “toggle” speed

Finally, I tried how quickly you can toggle a GPIO of the MCP23S17 between HIGH and LOW. I selected the pin A1 and executed the following short sketch on the Arduino Nano:

#include <SPI.h>
#include <MCP23S17.h>
#define CS_PIN 7   // Chip Select Pin
#define RESET_PIN 5 
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)

MCP23S17 myMCP = MCP23S17(CS_PIN, RESET_PIN, MCP_ADDRESS);

void setup(){ 
  Serial.begin(115200);
  SPI.begin();
  myMCP.Init();
  myMCP.setSPIClockSpeed(8000000);
  myMCP.setPinX(1,A,OUTPUT,LOW);
}
  
void loop(){ 
  myMCP.setPort(0b00000010,A);
  myMCP.setPort(0,A);
} 

The maximum frequency is about 16 kHz. For comparison: With digitalRead() you reach 127 kHz, via port manipulation even about 2.3 MHz.

Maximum "toggle" frequency with the MCP23S17 on the Arduino Nano
Maximum “toggle” frequency with the MCP23S17 on the Arduino Nano

6 thoughts on “MCP23x1y Port Expander

  1. Hi
    The test code in library GPIO reading – have lines

    myMCP.setPortMode(0b11111111, A); // Port A: all pins are OUTPUT
    myMCP.setPortMode(0b11111111, B); // Port B: all pins are OUTPUT
    myMCP.setPort(0b10010011,A); //
    if all ports and pins are OUTPUT can we be able to test and read switch input on these ports ?
    we not need INPUT on these ports if we connect switch ?
    regards

    ATHAR PAKISTAN

    1. This is just to show if you set e.g. myMCP.setPort(0b10010011,A) and you query myMCP.getPort(A)that you will receive 0b1001011. If want to read external logic statusses all pins should be input.

      1. Wolfgang
        I understand well library GPIO reading code, i changed and performed the work again.

        myMCP.setPortMode(0b00000000, A); // Port A: all pins are INPUT
        myMCP.setPortMode(0b11111111, B); // Port B: all pins are OUTPUT
        myMCP.setPort(0b00000000,A); //
        myMCP.setPort(0b00000000,B); //
        myMCP.setPortPullUp(0b00000000,A);

        I have switch connected to PORT A from 0 to 7 and other + (HIGH) – A all INPUT and B all OUTPUT.
        Module is following PCB

        https://www.ebay.de/itm/264739247628?hash=item3da3b0560c:g:PSgAAOSwzStex51q

        When i run sketch and operate switch it work fine few inputs and then some time stuck and then smooth but give false INPUT on nearby pins. If A3 is push it give A4 or A2 also HIGH.

        Is there timing or speed of chip problem – how to control or do that so exact INPUT be monitor on switch.

        What is function of PIN 5 reset and variable wT Wait. Reset to connect on pin 5 is necessary

        Regards Athar Kaludi (UOK) Pakistan

        1. Dear Athar,
          why inputs on some pins lead to false results on neighbor pins is something I can’t tell you. At least this is not related with the library.
          wT stands for waiting Time. My example sketch shows how LEDs are switched using the different functions. Some people might like to see the LEDs switching fast, some might like to perform it slowly, so that they can read the code and see the effect in parallel. So it is just for the example sketch.
          The (hardware) reset is performed during init(). It ensures that alle registers are set back to default. Without a reset the registers would keep their settings as long as you do not disconnect the MCP23017 from power. That can lead to strange results, in particular when you develop sketches. In the newest version of the library, which I launched a week ago, you can just connect the reset Pin to HIGH, and pass a reset pin of 99 or a higher number like this:
          #define RESET_PIN 99
          …..
          MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN)

          Then library will then do a software reset as part of the init function, which means all registers are set to default by I2C/SPI commands.

          1. Wolfdang
            Thanks for quick reply – Well explained.
            Taking grasp of things now, registers and other variables functions.
            Reading Datasheet and your library have Interrupts.(Interrupts on change)
            Is that meaning that any of the pin on any of MCP23017 GPIO when if used as INPUT via a [push button or touch switch or IR switch] will cause INT to feed to Arduino or MCU as soon as pin get change [Either Low to High OR High to Low]. We can write code in INT and compare with last state to know which pin caused or PUSH occurred where.
            Is there any built-in way we know which pin cause Interrupt.
            Any sketch you provided which use Interrupts change.
            How to control speed of chip to use it in 100Khz.
            Regards
            Athar Kaludi

Leave a Reply

Your email address will not be published.