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
- Use of the MCP23x1y port expanders in detail
- I2C v. SPI – Speed Tests
Overview of the MCP23x1y family
The MCP23x1y family has five members:
- the MCP23016 (Link to the data sheet)
- MCP23017 / MCP23S17 (Link to data sheet)
- and finally the two MCP23018 / MCP23S18 (link to data sheet)

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

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
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.
Important notice!!!The design of the MCP23017 has changed 2022. Unfortunately, Microchip has taken away the input function of the pins GPA7 and GPB7. The pins are now pure OUTPUT pins. What a bad idea! Visually, there’s no difference, so you’ll have to test which version you have. I have not adjusted the example sketches and the post text so far!

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:

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:
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 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()
: combinedsetPinMode()
andsetPin()
in one function.setPortX()
: combinedsetPortMode()
andsetPort()
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:

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.

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

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:

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.

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:
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:

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.

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.
Wolfgang,
Not sure what is happening during port read, but it appears that MSBs get latched and not cleared from the GPIO registers. We are reading a rotary switch to select a hardware “channel” , one of four positions and should read 0b10000000, 0b01000000, etc. However, once “1” is present in the lesser bit, it remains as the switch is rotated to another position. Hence, returned value from port is 0b11000000, etc. if switch is rotated to a lesr position.
Code is: channel = myMCP.getPort(B); So channel variable returns decimal 240 if all four positions are read as switch is rotated.
Thoughts? I don’t want to hard reset after each read, soft reset does nothing.
Hi MarKo, are you using MCP23017 port expanders (in contrast to MCP23S17, MCP23S18 and MCP23018)? And have you bought these chips in 2022/2023? Then the bad news is that Microchip has changed the design of the MCP23017. GPA7 and GPB7 have lost their input function. They are output by default. You find this information in the MCP23017 section of this article. Extremely bad idea, no reason why they did this. And it’s also a risk: when these Pins are set LOW and you apply a positive voltage you produce a shortcut.
In theory, that shouldn’t impact GPA6/GPB6 which you are also using. Can you please try other pins, at least to test if the issue is related to new design or something else? If it still does not work with other pins, then I would ask you to try another library (e.g. the one from Adafruit). If this should work, then it’s something I might be able to change in the lib. Should the issue be the new version of the chip, then take other pins or take another MCP23x1y version.
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
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.
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
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.
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
The use of interrupts is explained here:
https://wolles-elektronikkiste.de/en/port-expander-mcp23017-2?lang=en
Sorry, I can’t go more into details. Please have a look there.