From the bench

MCP23017: It's Not 'Just Wire It Up' — What Actually Matters on This GPIO Expander

August 27, 2026 · AI-generated from the datasheet, fact-checked by two independent LLM critics

The MCP23017 is a 16-bit I²C GPIO expander from Microchip — two independent 8-bit ports (PORTA/PORTB) that add 16 extra digital I/O pins to any microcontroller over a two-wire bus, for boards where I²C is available but spare GPIO pins aren't. It runs directly on a 1.8–5.5 V rail with no level translation needed for 3.3 V or 5 V logic, and up to eight devices can share one bus via three hardware address pins, giving a 7-bit address range of 0x20–0x27. Every input pin supports interrupt-on-change, with INTA/INTB outputs configurable active-high, active-low, or open-drain, so a host can react to a button press or sensor trip without polling. It's the standard part for driving LEDs/relays and reading switches behind I²C instead of spending scarce MCU pins directly on them.

Next step
Start building with MCP23017 → This guide comes from the same grounded, cited datasheet answers — ask the assistant your own MCP23017 questions.

The myth: "It's a dumb GPIO expander — just wire it to the bus and go"

That assumption causes most MCP23017 failures. The part is not dumb, and its defaults are what bite you: every pin powers up as an input, sequential mode is on, the register map is "paired" rather than banked, and the 1.7 MHz mode everyone quotes only works at 5 V. None of this is a bug — it's documented behavior you have to design around. Here's the full picture.

What the MCP23017 actually is

It's a 16-bit general-purpose parallel I/O expansion over I²C — two independent 8-bit ports (PORTA/PORTB) behind a serial bus, for when your MCU has spare I²C but few pins. Key facts:

Integration: how to wire and configure it

Power and straps. Tie A2:A1:A0 solidly to VDD or VSS — the datasheet explicitly requires external biasing; a floating strap is the classic "no ACK" cause. Same for RESET if you use it: drive it with a defined source or pull-up, don't float it. The datasheet specifies a maximum VDD rise time for POR, so a slow-ramping rail can prevent clean reset release.

I²C pull-ups. All AC timing is characterized at RPU = 1 kΩ with CL = 135 pF — that's the condition the guarantees are anchored to, so start there. Bus capacitance is capped at 400 pF at 100/400 kHz. The SDA/SCL lines have an internal 50 ns spike-suppression filter at ≤400 kHz, so no extra series resistors needed for glitch rejection at normal speeds. For marginal rising-edge timing or long/many-device buses, IOCON.DISSLW enables slew-rate control on the SDA pin (applied when driving high to low) — a part-specific tuning knob worth knowing about.

First writes. On power-up, IODIRA/B = 0xFF — all pins are inputs. Write IODIR before expecting any output to drive. For inputs, set GPPU bits to enable the internal 100 kΩ weak pull-ups (there is no internal pull-down — add your own for a low default). Use IPOL to invert active-low switch readings in hardware instead of software. Leave IOCON.BANK = 0 (the default) if you want the ports addressed as one contiguous 16-bit block — BANK=0 pairs the A and B registers back-to-back at consecutive addresses; only set BANK=1 if you specifically want the ports split into separate A/B register banks.

Interrupts. Configure GPINTEN per pin; interrupts fire on pin change or deviation from a preconfigured DEFVAL. The port value is latched into INTCAP at the moment of change, and the interrupt clears only when GPIO or INTCAP is read. With MIRROR = 1 both INT pins OR together — read the associated port or the line stays asserted. Note the ODR/INTPOL interaction: ODR = 1 (open-drain) overrides INTPOL; active-high INT requires ODR = 0 and INTPOL = 1 — with the default INTPOL = 0, ODR = 0 alone still yields an active-low INT.

Gotchas to design around

  1. 1.7 MHz is 4.5–5.5 V only. At 3.3 V, treat 400 kHz as the ceiling. Fast+ mode also drops bus capacitance to 100 pF (vs 400 pF) and the spike filter to effectively off (10 ns vs 50 ns).

  2. THD:DAT is long at low supply — up to 3.45 µs at 1.8–5.5 V (0.9 µs at 2.7 V+, 0.15 µs at 4.5 V+). A host that releases SDA early after SCL falls can corrupt bytes. Similarly, TAA (output valid) is up to 3.45 µs at 1.8–5.5 V — don't read back too fast at low VDD.

  3. Sequential mode is on by default (SEQOP = 0) — the pointer auto-increments after every ACK and rolls over to 0x00 after the last register. Critically: if the host issues Stop or Restart mid-transfer, the in-flight data is not written. Don't abort a block write expecting partial writes to stick. If you want to poll one register, disable auto-increment with SEQOP.

  4. BANK=0 is the default map (IODIRA=0x00, IODIRB=0x01, whole map 0x00–0x15). Changing BANK re-partitions the map the instant the byte clocks in — the pointer can land on an invalid address, so the datasheet advises byte-writes only when changing BANK.

  5. HAEN does nothing here. It's marked "MCP23S17 only" — don't waste time toggling it expecting an address change on the I²C part.

  6. Drive limits. Per-pin drive is capped at 25 mA sink or source, with 700 mW total package power and 125 mA into VDD / 150 mA out of VSS package limits. Clamp current on any pin driven outside 0–VDD is ±20 mA, and absolute pin voltage is −0.6 V to VDD+0.6 V — series resistance on exposed I/O is your protection. ESD rating is 4 kV HBM / 400 V MM, so connector-exposed lines need external protection.

    Known limitation: these are absolute-max stress limits, not recommended DC drive levels — our verification pass couldn't fully read the DC Characteristics table, so the VOH/VOL and recommended supply-current numbers are deliberately omitted here. Confirm datasheet §1.1 directly before sizing LED/relay loads.

  7. GPIO glitch filter is 150 ns — shorter pulses get suppressed. And an I/O change propagates to INT within ~600 ns (register valid in up to 450 ns), so don't read GPIO instantly from your ISR.

Testing: a bring-up sequence

  1. Before I²C: verify A0/A1/A2 and RESET are biased, VDD/VSS correct, exposed pad tied to VSS or left open.
  2. Link check: scope SCL/SDA at 100 kHz, confirm the device ACKs its address. Verify SDA is held valid past the SCL falling edge per THD:DAT.
  3. POR defaults: read IODIRA/B — expect 0xFF at 0x00/0x01. GPINTEN, IPOL, GPPU, GPIO, OLAT should all read 0x00. That's a fast "is it alive and did it reset" check.
  4. Round-trip: write a pattern to OLAT, read back via GPIO. Remember sequential mode auto-increments — a Stop mid-block drops the in-flight byte.
  5. Direction: write IODIR to make pins outputs, drive OLAT, measure. Confirm GPA7/GPB7 aren't assigned as inputs. Enable GPPU on an input and confirm it reads high.
  6. Interrupts: enable GPINTEN, toggle the pin, watch INTA. Confirm INT clears only after reading GPIO/INTCAP — if it never re-arms, the read-back step was missed. With MIRROR=1, read the associated port.
  7. Speed sweep: step up through 100 kHz (THIGH 4.0 µs min), 400 kHz (0.6 µs min at 2.7 V+), and only to 1.7 MHz (0.12 µs min) if you're at 4.5–5.5 V with ≤100 pF bus. A fast-mode failure at 3.3 V is an invalid test, not a bug.

Troubleshooting: order of attack

Strap bias → ACK/address → sequential-pointer behavior → IODIR defaults → OLAT vs GPIO → interrupt clear. Most "dead chip" reports trace to one of these:

Fix the documented behaviors first; only then suspect the silicon.

Part page: MCP23017.