/**
* Originally from: https://wokwi.com/projects/459779191640181761
* modified to esp32
* MCP23017 Custom Chip – Wokwi Test Sketch
*
* Hardware connections (see diagram.json):
* ESP32 D21 → MCP23017 SDA (+ 4.7kΩ pull-up to 5V)
* ESP32 D22 → MCP23017 SCL (+ 4.7kΩ pull-up to 5V)
* Not connected → MCP23017 INTA (for interrupt demo)
* ESP32 D19 → MCP23017 RESET (active-low; held HIGH normally)
* MCP23017 VDD → 3.3V | GND → GND
* A0/A1/A2 → GND → I2C address = 0x20
*
* GPA0-GPA7: 8 LEDs via 220Ω resistors to GND
* GPB0-GPB3: 4 push-buttons to GND (internal pull-ups enabled)
*
* Tests performed:
* 1. Basic direction & output – blink LEDs one by one
* 2. Knight-Rider sweep on PORTA
* 3. Read PORTB buttons and reflect on upper LED nibble
* 4. Disabled - Interrupt-on-change on GPB0 (button 0) → INTA on Arduino D2
* 5. Sequential multi-byte read/write
*/
#include <Wire.h>
/* ─── MCP23017 register addresses (BANK=0) ──────────────────────────────── */
#define MCP_IODIRA 0x00
#define MCP_IODIRB 0x01
#define MCP_IPOLA 0x02
#define MCP_IPOLB 0x03
#define MCP_GPINTENA 0x04
#define MCP_GPINTENB 0x05
#define MCP_DEFVALA 0x06
#define MCP_DEFVALB 0x07
#define MCP_INTCONA 0x08
#define MCP_INTCONB 0x09
#define MCP_IOCONA 0x0A
#define MCP_GPPUA 0x0C
#define MCP_GPPUB 0x0D
#define MCP_INTFA 0x0E
#define MCP_INTFB 0x0F
#define MCP_INTCAPA 0x10
#define MCP_INTCAPB 0x11
#define MCP_GPIOA 0x12
#define MCP_GPIOB 0x13
#define MCP_OLATA 0x14
#define MCP_OLATB 0x15
#define MCP_ADDR 0x20 /* A2=A1=A0=0 */
/* ─── I/O helpers ────────────────────────────────────────────────────────── */
void writeReg(uint8_t reg, uint8_t val) {
Wire.beginTransmission(MCP_ADDR);
Wire.write(reg);
Wire.write(val);
uint8_t err = Wire.endTransmission();
if (err) {
Serial.print(" !! I2C ERROR on write reg 0x");
Serial.print(reg, HEX);
Serial.print(": ");
Serial.println(err);
}
}
uint8_t readReg(uint8_t reg) {
Wire.beginTransmission(MCP_ADDR);
Wire.write(reg);
Wire.endTransmission(false); /* repeated-start */
Wire.requestFrom((uint8_t)MCP_ADDR, (uint8_t)1);
return Wire.available() ? Wire.read() : 0xFF;
}
/* Sequential multi-byte write starting at 'reg' */
void writeBurst(uint8_t reg, const uint8_t *data, uint8_t len) {
Wire.beginTransmission(MCP_ADDR);
Wire.write(reg);
for (uint8_t i = 0; i < len; i++) Wire.write(data[i]);
Wire.endTransmission();
}
/* Sequential multi-byte read starting at 'reg' */
void readBurst(uint8_t reg, uint8_t *out, uint8_t len) {
Wire.beginTransmission(MCP_ADDR);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom((uint8_t)MCP_ADDR, len);
for (uint8_t i = 0; i < len && Wire.available(); i++) out[i] = Wire.read();
}
/* ─── Interrupt service routine ──────────────────────────────────────────── */
volatile bool intFlag = false;
void INTA_ISR() { intFlag = true; }
/* ─── Setup ──────────────────────────────────────────────────────────────── */
void setup() {
Serial.begin(115200);
Serial.println("Boot");
Wire.begin();
Wire.setClock(400000); /* 400 kHz Fast-Mode */
/* Hardware RESET pulse */
pinMode(19, OUTPUT);
digitalWrite(19, LOW);
delay(100);
digitalWrite(19, HIGH);
delay(100);
/* Attach interrupt on Arduino D2 for MCP INTA */
// pinMode(2, INPUT_PULLUP);
// attachInterrupt(digitalPinToInterrupt(2), INTA_ISR, FALLING);
/* ── Verify device responds ── */
Wire.beginTransmission(MCP_ADDR);
uint8_t ack = Wire.endTransmission();
if (ack == 0) {
Serial.println("\n[PASS] MCP23017 found at 0x20");
} else {
Serial.println("\n[FAIL] MCP23017 NOT found – check wiring!");
while (1);
}
/* ── TEST 1: Register read-back ── */
Serial.println("\n--- TEST 1: Register defaults ---");
uint8_t iodira = readReg(MCP_IODIRA);
uint8_t iodirb = readReg(MCP_IODIRB);
Serial.print(" IODIRA = 0x"); Serial.print(iodira, HEX);
Serial.println((iodira == 0xFF) ? " [PASS] (all inputs)" : " [FAIL] expected 0xFF");
Serial.print(" IODIRB = 0x"); Serial.print(iodirb, HEX);
Serial.println((iodirb == 0xFF) ? " [PASS] (all inputs)" : " [FAIL] expected 0xFF");
/* ── TEST 2: Configure ports ── */
Serial.println("\n--- TEST 2: Configure PORTA=output, PORTB=input+pullup ---");
writeReg(MCP_IODIRA, 0x00); /* PORTA all outputs */
writeReg(MCP_IODIRB, 0xFF); /* PORTB all inputs */
writeReg(MCP_GPPUB, 0xFF); /* PORTB pull-ups ON */
uint8_t dir_a = readReg(MCP_IODIRA);
uint8_t dir_b = readReg(MCP_IODIRB);
uint8_t pup_b = readReg(MCP_GPPUB);
Serial.print(" IODIRA read-back = 0x"); Serial.println(dir_a, HEX);
Serial.print(" IODIRB read-back = 0x"); Serial.println(dir_b, HEX);
Serial.print(" GPPUB read-back = 0x"); Serial.println(pup_b, HEX);
Serial.println((dir_a == 0x00 && dir_b == 0xFF && pup_b == 0xFF) ?
" [PASS]" : " [FAIL]");
/* ── TEST 3: Burst write/read ── */
Serial.println("\n--- TEST 3: Burst write OLATA+OLATB ---");
uint8_t burst_wr[2] = {0xAA, 0x55};
writeBurst(MCP_OLATA, burst_wr, 2);
uint8_t burst_rd[2] = {0, 0};
readBurst(MCP_OLATA, burst_rd, 2);
Serial.print(" OLATA=0x"); Serial.print(burst_rd[0], HEX);
Serial.print(" OLATB=0x"); Serial.println(burst_rd[1], HEX);
Serial.println((burst_rd[0] == 0xAA && burst_rd[1] == 0x55) ?
" [PASS]" : " [FAIL]");
writeReg(MCP_OLATA, 0x00); /* clear */
/* ── TEST 4: Interrupt-on-change on GPB0 (change mode) ── */
Serial.println("\n--- TEST 4: Interrupt-on-change (GPB0, change vs previous) ---");
writeReg(MCP_GPINTENB, 0x01); /* Enable IOC on GPB0 */
writeReg(MCP_INTCONB, 0x00); /* Compare vs. previous value */
writeReg(MCP_IOCONA, 0x02); /* INTPOL=0 (active-low), MIRROR=0 */
Serial.println(" IOC configured – press button 0 (GPB0) to trigger INTA");
/* ── TEST 5: Input polarity inversion ── */
Serial.println("\n--- TEST 5: Input polarity inversion ---");
writeReg(MCP_GPINTENA, 0x00); /* disable all PORTA interrupts */
writeReg(MCP_IPOLA, 0xFF); /* invert all PORTA reads */
uint8_t pol_read = readReg(MCP_GPIOA);
Serial.print(" GPIOA (inverted, PORTA=outputs at 0): 0x");
Serial.println(pol_read, HEX);
writeReg(MCP_IPOLA, 0x00); /* restore */
Serial.println("\n=== Setup done – entering main loop ===\n");
}
/* ─── Main loop ──────────────────────────────────────────────────────────── */
uint8_t phase = 0;
uint32_t lastMs = 0;
uint8_t ledState = 0;
void loop() {
uint32_t now = millis();
/* ── Handle interrupt flag from MCP INTA ── */
if (intFlag) {
intFlag = false;
uint8_t intf = readReg(MCP_INTFB);
uint8_t intcap = readReg(MCP_INTCAPB); /* also clears interrupt */
Serial.print("[INT] INTF=0x"); Serial.print(intf, HEX);
Serial.print(" INTCAP=0x"); Serial.println(intcap, HEX);
}
if (now - lastMs < 120) return;
lastMs = now;
/* ── Phase 0: One-bit scan (light each LED once) ── */
if (phase == 0) {
writeReg(MCP_OLATA, 1 << (ledState & 7));
ledState++;
if (ledState >= 8) { phase = 1; ledState = 0; }
/* ── Phase 1: Knight-Rider sweep ── */
} else if (phase == 1) {
static int8_t dir = 1;
static int8_t pos = 0;
writeReg(MCP_OLATA, 0x03 << pos);
pos += dir;
if (pos >= 6) dir = -1;
if (pos <= 0) dir = 1;
ledState++;
if (ledState >= 30) { phase = 2; ledState = 0; }
/* ── Phase 2: Mirror PORTB buttons → upper 4 LEDs of PORTA ── */
} else if (phase == 2) {
uint8_t buttons = readReg(MCP_GPIOB); /* active-low (pull-ups) */
uint8_t mirror = (~buttons & 0x0F) << 4; /* invert & shift to [7:4] */
uint8_t lower = (1 << (ledState & 3)); /* free-running lower nibble */
writeReg(MCP_OLATA, mirror | lower);
ledState++;
if (ledState >= 40) { phase = 0; ledState = 0; }
}
}