MCP23017 I/O expander

Sixteen more pins over I2C, in a 28-pin DIP. Aliases: 16-bit expander, port expander, MCP23S17.

Where the PCF8574 is a latch with a weak pull-up, this is a proper pair of ports: direction, pull-ups, polarity, push-pull outputs and interrupts, each set by a register.

Pins

Pin What it does
GPA0 to GPA7, GPB0 to GPB7 The two ports.
VDD, VSS Supply, 1.8 to 5.5 V, and ground.
SCL, SDA I2C.
A0, A1, A2 The low three bits of the address, 0x20 to 0x27.
RESET Active low.
INTA, INTB Interrupt outputs for port A and port B.
NC1, NC2 Not connected.

Three things must be wired or the chip is not there. RESET has no pull-up and must be tied to VDD. A0, A1 and A2 have no pull-downs and must each go to VDD or VSS. And the bus needs 4.7 kΩ pull-ups on SDA and SCL, because this is a bare chip. Left floating, the part does what a real one does on a bad day: nothing at all.

The library

Adafruit_MCP23X17.h works as its tutorials use it; pins 0 to 7 are GPA0 to GPA7 and 8 to 15 are GPB0 to GPB7:

#include <Adafruit_MCP23X17.h>

Adafruit_MCP23X17 mcp;

void setup() {
  if (!mcp.begin_I2C()) { while (1); }   // 0x20
  mcp.pinMode(0, OUTPUT);                 // GPA0
  mcp.pinMode(8, INPUT_PULLUP);           // GPB0
}

void loop() {
  mcp.digitalWrite(0, !mcp.digitalRead(8));
}

The registers

At power-up IOCON.BANK is 0 and the A and B registers alternate: IODIRA at 0x00, IODIRB at 0x01, then IPOL, GPINTEN, DEFVAL, INTCON, IOCON, GPPU, INTF, INTCAP, GPIO and OLAT, a pair each, to 0x15. Every pin powers up an input. Setting BANK moves port B's registers to 0x10 to 0x1A at once, which is how a sketch that sets it ends up writing to the wrong register with its next byte.

After each byte the register pointer steps on, so a sketch writes IODIRA and IODIRB in one transfer. With SEQOP set it toggles between the A and B register of a pair instead, or with BANK also set stays where it is.

What the model gets right

Both register maps and the pointer's three behaviors. Outputs from OLAT, 100 kΩ pull-ups from GPPU, and IPOL inverting what GPIO reads. Interrupt-on-change: compare against the last value (INTCON 0) or against DEFVAL (INTCON 1), INTF naming the pin, INTCAP catching the port at that moment, and a read of INTCAP or GPIO clearing it. With DEFVAL, a pin that is still different fires again at once. INTA and INTB are push-pull active low at power-up; INTPOL, ODR and MIRROR change that as the datasheet says.

What it does not model

SPI, which is the MCP23S17. Output current limits. The slew-rate control. Microchip's current datasheet says GPA7 and GPB7 should be outputs only, because a change on either during a read can corrupt SDA on real silicon; here they work as inputs like the rest, so a circuit that relies on that works in Mokxi and may not on the bench.

Common mistakes

Leaving RESET or an address pin floating. Forgetting the bus pull-ups. Using the old Adafruit library's begin(0), which took 0 to 7; the new one takes the address, begin_I2C(0x20).