The MCP23017 is one of the most versatile I/O expanders on the market. It has 16 input/output pins and lots of settings to setup the behaviour of the device.
I'm using it extensively for my home automation system to detect push button presses and to activate relays, to name a few of the functionalities.
It's an I2C based device and you can have up to 8 devices on the same bus (3 address pins). That means: 8 * 16 pins = 128 I/O pins available on only one I2C bus!
If you combine this with an I2C bus mux like the PCA9548 where you can have up to 8 I2C busses, you end up with a whopping 128 * 8 = 1024 I/O pins! Can it be more crazy? Don't think so...
The below explanation is valid for both GPIOA as well as GPIOB, unless stated otherwise. Also, Bank 0 is assumed (default bank).
IODIR (0x00 / 0x01)
This register sets the corresponding expander pin(s) as input (corresponding register bit set to 1) or output (corresponding register bit set to 0).
Register defaults to 0xFF (all pins are set as input pins).
IPOL (0x02 / 0x03)
The bits in this register are only relevant when the corresponding pin is set as input!
When the corresponding bit in the register is set, the value at the physical pin is inverted.
That is, if the value at the physical input pin is '1' and the IPOL bit in the register is '1', the value read when reading the corresponding GPIO register is '0'.
Register defaults to 0x00 (level read at the corresponding pin(s) is not inverted).
GPINTEN (0x04 / 0x05)
The bits in this register are only relevant when the corresponding pin is set as input!
This register controls the interrupt-on-change feature. When the corresponding bit in this register is set, a level change on the physical pin will trigger an interrupt.
There are two different interrupt-on-change modes, based on the value of the corresponding bit in the INTCON register:
When the corresponding bit in the INTCON register is set, then the interrupt for that pin is only generated when the level on the physical pin is the opposite of the level for that particular bit defined in the DEFVAL register.
Example: if the bit in DEFVAL is 1 and the level on the corresponding physical pin is also '1', no interrupt is generated. If the level on the corresponding physical pin is '0', an interrupt is generated.
When the corresponding bit in the INTCON register is not set, then an interrupt is generated when the level on the physical pin is different from the previous level.
Register defaults to 0x00 (no interrupts enabled).
DEFVAL (0x06 / 0x07)
The bits in this register are only relevant when the corresponding pin is set as input!
This register can set a reference value for a given pin to generate an interrupt-on-change by changing the value of the corresponding bit in the register to '0' or '1'.
The content of this register is only relevant when the corresponding bit(s) in register INTCON are set to '1'.
When the bit in the register is set to '0', the value on the corresponding physical pin must be '1' before an interrupt is generated.
When the bit in the register is set to '1', the value on the corresponding physical pin must be '0' before an interrupt is generated.
Register defaults to 0x00.
INTCON (0x08 / 0x09)
The bits in this register are only relevant when the corresponding pin is set as input!
When a bit is set, the value of corresponding bit in the DEFVAL register determines when an interrupt is generated. See DEFVAL section above.
When a bit is cleared, an interrupt is generated when the current value on a pin is the opposite of the previous value on that pin.
Register defaults to 0x00.
IOCON (0x0A / 0x0B)
Note: although there are 2 addresses provided for IOCON (0x0A and 0x0B if the BANK bit is set to 0), there's only one IOCON register. So, addressing 0x0A or 0x0B is the same.
This register can configure the MCP23017 in different ways.
BANK bit:
Determines the addresses of all the MCP23017 registers.
When reset (default behaviour) the addresses of A and B registers are consecutive (paired).
Example: IODIRA = 0x00 and IODIRB = 0x01 / GPIOA = 0x12 and GPIOB = 0x13
Mapping for all registers is from 0x00 - 0x15.
When set, the addresses of A and B registers have an offset of 0x10 (segregated).
Example: IODIRA = 0x00 and IODIRB = 0x10 / GPIOA = 0x09 and GPIOB = 0x19
Mapping for PORTA is from 0x00 - 0x0A, mapping for PORTB is from 0x10 - 0x1A.
MIRROR bit:
Controls how the INTA and INTB pins of the MCP23017 behave. Both ports have their own INT pin and can act individually.
That is, whenever a pin of PORTA changes value, INTA is triggered. Whenever a pin of PORTB changes value, INTB is triggered.
This is the default behaviour (bit is set to '0').
But there's a possibility that, whenever an interrupt occurs on either port, both pins are activated ("OR"-ed together).
SEQOP
Controls if the MCP23017 works in Byte Mode or in Sequential Mode.
When the bit is set Byte Mode is selected. That is, after reading an address, the internal address pointer is not changed. This is useful when you want to poll the GPIO register, for instance.
When the bit is not set (default behaviour) Sequential Mode is enabled. After reading an address, the internal address pointer is moved to the next register address.
DISSLW
Controls the slew rate of the SDA signal. If set, the SDA slew rate will be controlled when driving from high to low. Defaults to '0'.
HAEN
Only for the MCP23S17 (SPI-based device).
ODR
This bit controls the open-drain configuration for the INT pin. When set, the pin acts as open drain (and also overrides the INTPOL bit). When reset (default behaviour) the INT pin is configured as active driver output.
INTPOL
This bit controls the polarity of the INT output pin.
When set, the output is active high.
When reset (default behaviour), the output is active low.
GPPU (0x0C / 0x0D)
This register can activate the weak internal pull-up resistors when a pin is set as input. The value of the internal pull-up resistors is 100kOhm.
When a bit is set, the corresponding internal pull-up resistor is activated.
When a bit is reset (default behaviour) the corresponding internal pull-up resistor is disabled.
INTF (0x0E / 0x0F)
This register reflects which pin has triggered the interrupt. From what I experienced, there is always only one bit set in the INTF register. After the first pin triggered the interrupt, that bit is set. If more pins are set they won't set additional bits in the register until the GPIO register is read.
Note that you can have two interrupts when pressing a button, if the corresponding bit in the INTCON register is set to '0': one interrupt when going from, say, low to high. If you read out the pin through GPIO before the pin goes from high to low, you will get another interrupt since the state of the pin is gain different from its previous state.
INTCAP (0x10 / 0x11)
This register stores the state of the GPIO port value the moment an interrupt occurs. The value of the register remains unchanged until the interrupt is cleared via a read of the INTCAP or GPIO register.
GPIO (0x12 / 0x13)
This register reflects the status of the physical port.
Reading from it reads the status of the pin. Writing to it modifies the corresponding bits in the OLAT register.
OLAT (0x14 / 0x15)
This register provides access to the output latches.
Reading from it reads the content of the OLAT register, not the port itself.
So, when you want to read the status of the port itself, you have to use the GPIO register, not the OLAT register!
Writing to it modifies the output latches that modify the pins configured as output.
So, writing to GPIO or OLAT both will change the level of the pins configured as output. Writing to GPIO indirectly changes the port pin through the OLAT register.
To make use of the interrupt facilities of the MCP23017, one has to fulfil the following requirements:
One or more pins have to be set as input
The corresponding bit(s) in the register GPINTEN have to be set to 1
The register INTCON has to be set correctly:
0: compare to previous value. If you check a switch, for instance, an interrupt will be generated when the switch brings the corresponding pin to high and again when the pin goes to low. So, one switch press/release will create two interrupts
1: value of the associated pin is compared to de value defined in the DEFVAL register. Only when the values differ, the interrupt will be generated.
So, suppose the following situation:
a pull-up resistor (internal or external) is connected to the input pin
the input pin can generate an interrupt (GPINTEN bit is set)
the input pin is connected to a switch
the other end of the switch is connected to ground
steady state is high on the input pin
the DEFVAL register contains a '1' for that particular pin
the switch is pressed, so the input pin is connected to ground
since the value of the pin in DEFVAL is set to 1 and the level on the pin is 0, the interrupt is generated
reading the INTF register will indicate which input pin has generated the interrupt
the INTCAP register will contain the PORT status the moment the interrupt occurred: the INTCAP register content is NOT changed as long as the GPIO register is not read or the INTCAP register itself is not read. Only then the INTF register will clear its interrupt flag
The pin that caused the interrupt has to be checked in the INTF register: the bit remains set as long as the pin has not be read by the GPIO register
The corresponding INT pin goes low (default behaviour) whenever an interrupt has been detected.
The INTF register reflects the first input pin that has caused the interrupt. Even if more pins detect a change, those will not be reflected in the INTF register.