More pins on an Arduino Uno
A PCF8574 and an MCP23017 on one I2C bus: a port with no registers walking a bit along its pins, and a port with a register for everything reading a button. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 25 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $33. On Mokxi they're free.
Step by step, with the wiring and the common mistakes: Arduino PCF8574 and MCP23017 I/O Expanders
What you'll learn
- I/O expanders add pins over I2C
- I2C addresses and address pins
- A port with no registers versus a full register chip
- Active-low LEDs and input pull-ups
- Difficulty: Intermediate
- Time: About 25 minutes
- Board: Arduino Uno R3
- Parts: 5
How it works
Two I/O expanders share the Uno's I2C bus on A4 and A5. A PCF8574 at address 0x20 has no registers: every byte written sets its eight pins. An MCP23017 at address 0x27 has proper ports, direction registers and pull-ups.
Every 250 ms the sketch writes a byte to the PCF8574 with a single zero bit, and moves that bit along pins P0 to P3. The LEDs on that chip light when their bit is 0.
On the MCP23017, GPB0 is an input with its pull-up on and a button to ground. GPA0 drives an LED and lights while the button is held. The serial monitor reports each state.
Parts list
5 parts, plus the jumper wires. They retail for about $33; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × I/O expander, MCP23017
- 1 × I/O expander, PCF8574
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| I/O expander, MCP23017 pin VDD | Arduino Uno R3 pin 5V |
| I/O expander, MCP23017 pin A0 | Arduino Uno R3 pin 5V |
| I/O expander, MCP23017 pin A1 | Arduino Uno R3 pin 5V |
| I/O expander, MCP23017 pin A2 | Arduino Uno R3 pin 5V |
| I/O expander, MCP23017 pin RESET | Arduino Uno R3 pin 5V |
| I/O expander, PCF8574 pin VCC | Arduino Uno R3 pin 5V |
| I/O expander, MCP23017 pin VSS | Arduino Uno R3 pin GND |
| I/O expander, PCF8574 pin GND | Arduino Uno R3 pin GND |
| Pushbutton pin 2b | Arduino Uno R3 pin GND |
| I/O expander, MCP23017 pin SDA | Arduino Uno R3 pin A4 |
| I/O expander, PCF8574 pin SDA | Arduino Uno R3 pin A4 |
| I/O expander, MCP23017 pin SCL | Arduino Uno R3 pin A5 |
| I/O expander, PCF8574 pin SCL | Arduino Uno R3 pin A5 |
| Pushbutton pin 1b | I/O expander, MCP23017 pin GPB0 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Two port expanders on one I2C bus: a PCF8574 at 0x20 and an MCP23017 at 0x27.
//
// The PCF8574 has no registers at all. Every byte written is its eight pins,
// and a pin written 1 is only weakly high, so the LEDs on it run from 5 V
// through a resistor *into* the pin and light when their bit is 0. The sketch
// walks a single dark bit along four of them.
//
// The MCP23017 is a proper pair of ports: a direction register, pull-ups and
// push-pull outputs. GPB0 is an input with its pull-up on and a button to
// ground; GPA0 drives an LED to ground, lit while the button is held. Its
// RESET is tied high and A0 to A2 high too, which is address 0x27: none of the
// four pins is pulled inside the chip, and left floating the chip is simply
// not on the bus.
#include <Wire.h>
#include <Adafruit_MCP23X17.h>
const uint8_t PCF8574 = 0x20;
Adafruit_MCP23X17 mcp;
uint8_t step = 0;
void setup() {
Serial.begin(9600);
Wire.begin();
Wire.beginTransmission(PCF8574);
Serial.println(Wire.endTransmission() == 0 ? "PCF8574 at 0x20" : "no PCF8574 at 0x20");
if (!mcp.begin_I2C(0x27)) {
Serial.println("no MCP23017 at 0x27");
} else {
Serial.println("MCP23017 at 0x27");
}
mcp.pinMode(0, OUTPUT); // GPA0: the LED
mcp.pinMode(8, INPUT_PULLUP); // GPB0: the button
}
void loop() {
// One dark bit walking along P0..P3; the other four stay high, which is
// also how they would be read as inputs.
Wire.beginTransmission(PCF8574);
Wire.write((uint8_t)~(1 << step));
Wire.endTransmission();
step = (step + 1) % 4;
bool pressed = mcp.digitalRead(8) == LOW;
mcp.digitalWrite(0, pressed ? HIGH : LOW);
Serial.println(pressed ? "button held" : "button up");
delay(250);
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Starts serial and I2C. It sends an empty message to the PCF8574 at 0x20 and prints whether it answered. Then it calls mcp.begin_I2C(0x27) and reports. Finally it makes GPA0 (pin 0) an output for the LED and GPB0 (pin 8) an input with pull-up for the button.
void setup() {
Serial.begin(9600);
Wire.begin();
Wire.beginTransmission(PCF8574);
Serial.println(Wire.endTransmission() == 0 ? "PCF8574 at 0x20" : "no PCF8574 at 0x20");
if (!mcp.begin_I2C(0x27)) {
Serial.println("no MCP23017 at 0x27");
} else {
Serial.println("MCP23017 at 0x27");
}
mcp.pinMode(0, OUTPUT); // GPA0: the LED
mcp.pinMode(8, INPUT_PULLUP); // GPB0: the button
}loop sketch.ino
Writes the inverse of 1 << step to the PCF8574, so one bit is low, then moves step on, wrapping at 4. It reads the button on the MCP23017, copies it to the LED on GPA0, prints "button held" or "button up" and waits 250 ms.
void loop() {
// One dark bit walking along P0..P3; the other four stay high, which is
// also how they would be read as inputs.
Wire.beginTransmission(PCF8574);
Wire.write((uint8_t)~(1 << step));
Wire.endTransmission();
step = (step + 1) % 4;
bool pressed = mcp.digitalRead(8) == LOW;
mcp.digitalWrite(0, pressed ? HIGH : LOW);
Serial.println(pressed ? "button held" : "button up");
delay(250);
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The serial monitor says no MCP23017 at 0x27 This project
The address pins A0 to A2 must be tied to 5V for 0x27, and RESET tied high. Floating pins leave the chip off the bus. - Two chips on the bus do not answer This project
Check that both share SDA on A4 and SCL on A5, and both have power and ground. - The chip is not found or behaves randomly. I/O expander
Tie RESET high to the supply, since it is active low and floats otherwise. Tie A0, A1 and A2 to ground or the supply to set the address, 0x20 to 0x27. Use pull-ups on SDA and SCL. - Pins do not output or inputs read wrong. I/O expander
Pins start as inputs, so set the direction registers (IODIRA and IODIRB) for outputs. Enable pull-ups (GPPU) for switches, since inputs float. Each pin sources only a few mA, so use resistors for LEDs. - Inputs read wrong, or LEDs stay dim. I/O expander
The pins are quasi-bidirectional. Write 1 to a pin before reading it as an input. Outputs sink better than they source, so wire LEDs to the positive supply. - The expander is not found on I2C. I/O expander
The address is 0x20 to 0x27 (PCF8574) or 0x38 to 0x3F (PCF8574A), set by A0 to A2. Add pull-ups on SDA and SCL if none exist. - The input reads random HIGH and LOW when the button is not pressed. Pushbutton
A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW. - The button seems always pressed, or never changes. Pushbutton
A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.