DIY Electronics

Pybotchi & MCP for Beginners: What I Wish I Knew Sooner

Running out of GPIO pins for your Python project? This beginner's guide to the MCP I/O expander is what I wish I had. Learn to build your Pybotchi and avoid common pitfalls.

D

Daniel Carter

A maker and software developer passionate about demystifying hardware for fellow hobbyists.

7 min read10 views

There’s a unique kind of magic in the world of DIY electronics. It’s the moment a mess of wires and components on your desk suddenly blinks to life, running code you wrote. My first "Pybotchi" project—a homemade Tamagotchi-style digital pet powered by Python—was supposed to be one of those magical moments. Instead, it started with a sinking feeling of defeat.

I had it all planned out: a cute pixel-art creature on a small screen, a few buttons for feeding and playing, a buzzer for alerts, and an LED for status. I laid out my Raspberry Pi Pico, my breadboard, and my components, only to realize I was playing a losing game of musical chairs with my GPIO pins. There just weren't enough to go around. For a moment, I thought my ambitious little pet project was dead on arrival. That is, until I discovered a tiny, 28-pin chip that changed everything: the MCP23017.

What is a Pybotchi? (And Why It’s the Perfect Weekend Project)

A "Pybotchi" is simply a DIY digital pet built with Python—usually on a microcontroller like a Raspberry Pi Pico or a single-board computer like a Raspberry Pi. Think of it as a 90s nostalgia trip fueled by modern, accessible technology. It’s the perfect project because it touches on so many core maker skills in a fun, engaging way:

  • Basic Electronics: Wiring up screens, buttons, and LEDs.
  • Python Programming: You'll handle game loops, state management (is your pet happy, hungry, or asleep?), and user input.
  • Hardware Interfacing: This is where you make your software talk to the physical world, reading button presses and updating the display.

But as I quickly learned, the more interactive you want your pet to be, the more pins you’re going to need.

The Great GPIO Shortage: When Your Project Outgrows Your Board

A standard Raspberry Pi Pico has about 26 GPIO pins. Sounds like a lot, right? Let's do the math for a moderately complex Pybotchi:

  • SPI Screen (like a ST7789): ~7 pins (VCC, GND, SCL, SDA, RST, DC, BL)
  • Four Buttons (Up, Down, A, B): 4 pins
  • Status LED: 1 pin
  • Buzzer/Speaker: 1 pin

That’s 13 pins gone already, and we haven't even considered adding more features like a light sensor, more LEDs, or a dedicated audio chip. You can see how quickly you run out of real estate. This is the exact wall I hit.

Meet Your New Best Friend: The MCP23017 I/O Expander

The MCP23017 is an I/O (Input/Output) expander. In simple terms, it's a specialist chip that gives you 16 extra GPIO pins while only using two of your microcontroller's pins to control them all. It communicates using a protocol called I2C (Inter-Integrated Circuit), which is a two-wire bus designed for chips to talk to each other.

Instead of wiring every single button to your Pico, you wire them to the MCP23017. Then, you just tell your Pico, "Hey, ask the MCP chip if button #5 is being pressed." It’s like delegating a task to a very efficient assistant.

Wiring It Up Without Losing Your Mind

Advertisement

Wiring an I2C device for the first time can feel intimidating. Here’s the simple breakdown for connecting an MCP23017 to your Pico.

Key MCP23017 Pin Connections
MCP23017 Pin Connects To... Purpose
VCC Pico 3.3V (OUT) Power for the chip.
GND Pico GND Common ground.
SCL Pico I2C SCL Pin Serial Clock - The "heartbeat" of I2C communication.
SDA Pico I2C SDA Pin Serial Data - The actual data line.
A0, A1, A2 GND or 3.3V Address pins. More on this crucial step below!

The remaining 16 pins (GPA0-GPA7 and GPB0-GPB7) are now your new GPIO ports, ready for your buttons, LEDs, and more!

The 5 Game-Changing Tips I Wish I Knew Sooner

Here’s where we get to the good stuff. These are the five things that caused me hours of frustration. Learn them now and thank me later.

1. I2C Addresses Aren't Suggestions, They're Rules

The problem: My code kept crashing with an `OSError` or `ValueError`, saying it couldn't find a device. I thought my chip was dead.

What I wish I knew: The MCP23017 needs a unique address on the I2C bus so your Pico knows who to talk to. You set this address physically by connecting the A0, A1, and A2 pins to either Ground (0) or Power (1). By default, with all three floating or tied to Ground, the address is `0x20`. If you connect A0 to 3.3V, the address becomes `0x21`. This is how you can use up to 8 of these chips on the same two I2C wires!

How to fix it: First, wire A0, A1, and A2 to Ground. Then, in your Python code, you must specify this address when you initialize the chip. On a Raspberry Pi, you can run `i2cdetect -y 1` in the terminal to scan the bus and see the address of any connected device. This is the single most common point of failure for beginners.

2. Internal Pull-ups Are Magic (Use Them!)

The problem: My button presses were unreliable. Sometimes they worked, sometimes they didn't. The input seemed to be "floating" randomly between high and low.

What I wish I knew: When a button isn't pressed, its input pin is not connected to anything, leaving it in an undefined state. You need a "pull-up" or "pull-down" resistor to pull the pin to a known state (either HIGH or LOW). But soldering resistors for every button is a pain. The MCP23017 has built-in pull-up resistors for every pin that you can enable with a single line of code!

How to fix it: When you set up a pin for a button, just enable the pull-up. In CircuitPython, it looks like this: `pin.pull = digitalio.Pull.UP`. That’s it! No extra wiring, no soldering. It's a lifesaver.

3. Understanding Banks: It's Not Just 16 Pins

The problem: I was trying to access pin 9 and my code wasn't working. I was thinking of the pins as a simple 0-15 list.

What I wish I knew: The MCP23017's 16 pins are internally split into two "banks" of 8: Port A (GPA0-GPA7) and Port B (GPB0-GPB7). When using libraries like the Adafruit CircuitPython MCP230xx library, you access them sequentially. Pin 0 is GPA0, Pin 7 is GPA7, but Pin 8 is GPB0. Understanding this structure is key to not getting confused about which physical pin corresponds to which software pin number.

4. Your Button Isn't Broken, It's "Bouncing"

The problem: I finally got my button working, but every time I pressed it once, my Pybotchi thought I'd pressed it 5-10 times. Its happiness level went from 0 to 100 instantly.

What I wish I knew: This is called "bouncing." Inside a physical button are tiny metal contacts. When you press it, they don't just make a clean connection. They bounce against each other for a few milliseconds, causing the microcontroller to read a rapid series of on-off signals. You need to "debounce" your input, which means ignoring those extra signals for a short period after the first one.

How to fix it: You can do this easily in software. Just record the time of the last button press and ignore any new presses that happen within a short window (e.g., 200 milliseconds).

5. Test the Chip, Not Your Patience: Start with an LED

The problem: I wired up everything at once—the screen, four buttons, the MCP—and nothing worked. I had no idea where the problem was. Was it the wiring? The I2C address? The button logic? My screen code?

What I wish I knew: Isolate and test. Before you even think about your Pybotchi's game logic, test the MCP23017 on its own. The simplest way? Make it blink an LED. Write a tiny script that does nothing but configure one pin as an output and turn it on and off. If the LED blinks, you know your wiring is correct, your I2C address is right, and the chip is working. Now you can add your buttons with confidence.

Putting It All Together: A Simple Pybotchi Button Example

Here’s a simple CircuitPython example that brings some of these lessons together. This code sets up one button on the MCP23017 (pin 0) and prints a message when you press it, using a simple debounce logic.


import time
import board
import busio
import digitalio
from adafruit_mcp230xx.mcp23017 import MCP23017

# --- Setup ---
i2c = busio.I2C(board.SCL, board.SDA)

# Initialize the MCP23017 with the default address 0x20
mcp = MCP23017(i2c, address=0x20)

# --- Configure a pin for our button ---
# Get a reference to pin 0 (GPA0)
button_pin = mcp.get_pin(0)

# Set it as an input
button_pin.direction = digitalio.Direction.INPUT

# Enable the internal pull-up resistor (Magic!)
button_pin.pull = digitalio.Pull.UP

# --- Debounce variables ---
last_press_time = 0
debounce_delay = 0.2 # 200 milliseconds

print("Ready for button presses!")

# --- Main Loop ---
while True:
    # The pin is pulled HIGH by default. When pressed, it connects to ground,
    # so its value becomes LOW (False).
    if not button_pin.value:
        current_time = time.monotonic()
        if (current_time - last_press_time) > debounce_delay:
            print("Button Pressed! Feed the Pybotchi!")
            last_press_time = current_time
    
    # Add a small delay to prevent the loop from running too fast
    time.sleep(0.01)
    

Conclusion: Was It Worth It?

Absolutely. That initial frustration of running out of pins almost made me scale back my project, but learning to use the MCP23017 didn't just solve the problem—it opened up a new world of possibilities. Suddenly, my projects could be bigger, more complex, and more interactive. The learning curve was steep for a day or two, but it was really just a few key concepts that, once understood, made everything click into place.

So if you're sketching out your next big project and find yourself counting GPIO pins on your fingers, don't despair. Grab an I/O expander like the MCP23017. It's an invaluable, inexpensive little chip that will pay for itself a hundred times over in the creative freedom it gives you. Now, go build something amazing.

Tags

You May Also Like