Built-in project · Raspberry Pi Pico W

Pico W guess the number: keypad and LCD

MicroPython on a Pico W: a number game on a 4x4 keypad (rows GP21 to GP18, columns GP13 to GP10) and an I2C LCD that narrows the range after each guess. The whole build, a Raspberry Pi Pico W and 2 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.

Advanced About 25 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $11.50. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Scanning a 4x4 keypad by rows and columns
  • Pull-down inputs and driving rows as outputs
  • Talking to an I2C character LCD from MicroPython
  • Narrowing a range with a binary-search style game
  • Detecting a new key press so one press counts once
  • Difficulty: Advanced
  • Time: About 25 minutes
  • Board: Raspberry Pi Pico W
  • Parts: 4

How it works

This MicroPython game picks a secret number from 0 to 99. Press A on the keypad to start. Type one or two digits, then press D to guess.

The Pico drives each keypad row high, one at a time, and checks which column pin goes high. That tells it which key is down. The columns use pull-downs inside the chip, so a pin reads low until a key connects it to a high row.

After each guess the LCD narrows the range. A low guess raises the bottom, and a high guess lowers the top. A correct guess shows GAME OVER and the number. The LCD is on I2C with SDA on GP6 and SCL on GP7.

Parts list

4 parts, plus the jumper wires. They retail for about $11.50; on Mokxi they're free.

Kits that can build it

These 2 starter kits have every part in the circuit. Each kit's page lists what else it can build.

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 pinConnects to
Character LCD with I2C backpack pin SDARaspberry Pi Pico W pin GP6
Character LCD with I2C backpack pin SCLRaspberry Pi Pico W pin GP7
Keypad, 4x4 pin C4Raspberry Pi Pico W pin GP10
Keypad, 4x4 pin C3Raspberry Pi Pico W pin GP11
Keypad, 4x4 pin C2Raspberry Pi Pico W pin GP12
Keypad, 4x4 pin C1Raspberry Pi Pico W pin GP13
Keypad, 4x4 pin R1Raspberry Pi Pico W pin GP21
Keypad, 4x4 pin R2Raspberry Pi Pico W pin GP20
Keypad, 4x4 pin R3Raspberry Pi Pico W pin GP19
Keypad, 4x4 pin R4Raspberry Pi Pico W pin GP18
Character LCD with I2C backpack pin GNDGround
Character LCD with I2C backpack pin VCC5 V

The code

The MicroPython code exactly as the editor opens it. Change a line there and press Run: the board runs it on the official MicroPython firmware, with nothing to compile.

main.py · Raspberry Pi Pico W
# Guess number: press A to start. A secret number from 0 to 99 is picked;
# type a guess and press D. The LCD narrows the range until somebody
# hits the number. Keypad rows on GP21 to GP18, columns on GP13 to GP10.
from machine import I2C, Pin
from lcd1602 import LCD1602
import random
import time

KEYS = ["123A", "456B", "789C", "*0#D"]
rows = [Pin(n, Pin.OUT, value=0) for n in (21, 20, 19, 18)]
cols = [Pin(n, Pin.IN, Pin.PULL_DOWN) for n in (13, 12, 11, 10)]   # pull-downs inside the chip
lcd = LCD1602(I2C(1, sda=Pin(6), scl=Pin(7), freq=400000))

def key():
    for r, row in enumerate(rows):
        row.value(1)
        for c, col in enumerate(cols):
            if col.value():
                row.value(0)
                return KEYS[r][c]
        row.value(0)
    return None

secret, low, high, typed = None, 0, 99, ""
lcd.show("Guess number", "Press A to play")
last = None
while True:
    k = key()
    if k and k != last:
        if k == "A":
            secret, low, high, typed = random.randint(0, 99), 0, 99, ""
            lcd.show("Guess %d to %d" % (low, high), "")
            print("new game")
        elif secret is not None and k.isdigit() and len(typed) < 2:
            typed += k
            lcd.show("Guess %d to %d" % (low, high), typed)
        elif secret is not None and k == "D" and typed:
            guess = int(typed)
            typed = ""
            if guess == secret:
                lcd.show("GAME OVER", "It was %d" % secret)
                print("got it:", secret)
                secret = None
            else:
                if guess < secret:
                    low = max(low, guess + 1)
                else:
                    high = min(high, guess - 1)
                lcd.show("Guess %d to %d" % (low, high), "")
                print("between", low, high)
    last = k
    time.sleep_ms(30)
lcd1602.py · Raspberry Pi Pico W
# lcd1602.py: a 16x2 character LCD on a PCF8574 I2C backpack.
# The backpack's P0 is RS, P2 is E, P3 is the backlight and P4 to P7 are
# the display's D4 to D7, so every byte is sent as two 4-bit halves.
import time

class LCD1602:
    def __init__(self, i2c, addr=0x27):
        self.i2c = i2c
        self.addr = addr
        self.light = 0x08
        time.sleep_ms(50)
        for half in (0x30, 0x30, 0x30, 0x20):   # wake up, then 4-bit mode
            self._half(half, 0)
            time.sleep_ms(5)
        self.command(0x28)                      # two lines, 5x8 dots
        self.command(0x0C)                      # display on, no cursor
        self.command(0x06)                      # move right after a character
        self.clear()

    def _send(self, bits):
        self.i2c.writeto(self.addr, bytes([bits | self.light]))

    def _half(self, bits, rs):
        self._send(bits | rs | 0x04)            # E high...
        self._send(bits | rs)                   # ...and low: the LCD reads it

    def _byte(self, value, rs):
        self._half(value & 0xF0, rs)
        self._half((value << 4) & 0xF0, rs)

    def command(self, value):
        self._byte(value, 0)
        if value < 4:
            time.sleep_ms(2)                    # clear and home are slow

    def clear(self):
        self.command(0x01)

    def move_to(self, col, row):
        self.command(0x80 | (col + 0x40 * row))

    def write(self, text):
        for ch in text:
            self._byte(ord(ch), 1)

    def show(self, line1, line2=""):
        self.move_to(0, 0)
        self.write("%-16s" % line1[:16])
        self.move_to(0, 1)
        self.write("%-16s" % line2[:16])

Code walkthrough

The key functions of the sketch, and what each one does.

key main.py

Scans the keypad. For each row, it sets the pin high, then checks every column. If a column reads high, that row and column pick a character from KEYS. It sets the row low again and returns the key. If nothing is pressed it returns None.

def key():
    for r, row in enumerate(rows):
        row.value(1)
        for c, col in enumerate(cols):
            if col.value():
                row.value(0)
                return KEYS[r][c]
        row.value(0)
    return None

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • Wrong keys show up, or none at all This project
    Check the rows on GP21 to GP18 and the columns on GP13 to GP10. R1 goes to GP21 and C1 to GP13.
  • The LCD stays blank This project
    Check SDA on GP6 and SCL on GP7, and that the LCD has 5 V and ground.
  • Nothing happens when you type This project
    Press A first to start a game. Digits and D are ignored until a game has begun.
  • The screen stays blank or shows only a row of blocks. Character LCD with I2C backpack
    The I2C address is usually 0x27 or 0x3F. Run an I2C scanner to find yours. Then turn the contrast trimmer on the backpack slowly until text appears.
  • The backlight is on but there is no text, or text is garbled. Character LCD with I2C backpack
    Wire SDA and SCL to the right pins (A4 and A5 on an Uno), power it from 5 V, and call lcd.init() and the backlight call in setup.
  • Pressing one key reports several keys, or the wrong key. Keypad
    Match the keymap and the row and column pin order to your keypad's pinout. Without diodes, pressing three keys at once can ghost a fourth.
  • No keys are detected. Keypad
    Rows need pull-ups, or use INPUT_PULLUP, while one column is driven low at a time. The Keypad library does this for you if you pass the pins in the right order.
  • Pins are powered from 5 V, or a 5 V signal goes into a GPIO. The RP2040 pins are 3.3 V only and are not 5 V tolerant. Raspberry Pi Pico W
    Power the board from VBUS or VSYS at 5 V, but keep GPIO signals at 3.3 V. Use a level shifter or a divider for 5 V parts.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.