Raspberry Pi Pico Dice

YouTube Video

Introduction

In this project tutorial, we are going to make a Raspberry Pi Pico Dice. The dice allows you to emulate two dice rolls. It is a fun beginner project for anyone starting out learning the Raspberry Pi Pico.

Disclaimer - JLCPCB was generous enough to sponsor this project and provide the PCB used in this project.

Components + Tools Breakdown

ComponentQuantityQuantity
Raspberry Pi Pico11
Custom PCB (JLCPCB)11
14 - 5mm LEDs14Few
330-1k Ohm Resistor145
3D printed parts1Few printed parts
3mm screw4Explained in steps
Tools / Equipment
Soldering Iron + Solder
Computer + Thonny IDE
Screw driver

Breadboard Prototype

A simple dice was first made on breadboard for testing. The following circuit diagram was used.

We need 7 LEDs to mimic all the possible shapes for a dice pattern as shown in the following figure

We will recreate this patterns later when designing the PCB.

Prototype Code

The code for the breadboard is as follows

from machine import Pin
import utime
import urandom

urandom.seed(utime.ticks_us())

# Define the LED pins for the Dice
dice1_leds = [Pin(i, Pin.OUT) for i in range(0, 7)]


# Define the button pin
button1 = Pin(14, Pin.IN, Pin.PULL_DOWN)


# Define the LED patterns for each number
numbers = [
    [0, 0, 0, 1, 0, 0, 0],  # 1
    [1, 0, 0, 0, 0, 0, 1],  # 2
    [1, 0, 0, 1, 0, 0, 1],  # 3
    [1, 1, 0, 0, 0, 1, 1],  # 4
    [1, 1, 0, 1, 0, 1, 1],  # 5
    [1, 1, 1, 0, 1, 1, 1],  # 6
]

# Function to turn off LEDs of a specific dice
def turn_off_leds(dice_leds):
    for led in dice_leds:
        led.value(0)

# Function to show a number on the dice
def show_number(dice_leds, number):
    # Get the LED pattern for the number
    pattern = numbers[number - 1]
    
    # Loop over each LED in the dice
    for i in range(len(dice_leds)):
        # Get the corresponding LED and its value from the pattern
        led = dice_leds[i]
        value = pattern[i]
        
        # Set the LED to the value from the pattern
        led.value(value)

# Main loop
while True:
    if button1.value() == 1:
        utime.sleep(0.3)  # Wait for 2 seconds to ensure the button is pressed long enough
        if button1.value() == 1:  # Check again if the button is still pressed
            utime.sleep_ms(urandom.randint(0, 200))  # Random delay before generating the number
            show_number(dice1_leds, urandom.randint(1, 6))
            utime.sleep(2)  # Keep the LEDs on for 2 seconds
            turn_off_leds(dice1_leds)  # Then turn them off

    utime.sleep_ms(10)  # Short delay to debounce the button

Code - Breakdown

Importing Libraries

machine import Pin
import utime
import urandom

urandom.seed(utime.ticks_us())

In these lines, three libraries are imported: Pin from machine, utime, and urandom. Pin is used to control GPIO pins, utime is used for access to time-related functions, and urandom is used to generate random numbers. The seed for random numbers is set using the current time in microseconds.

Defining LED and Button Pins

# Define the LED pins for the 
Dicedice1_leds = [Pin(i, Pin.OUT) for i in range(0, 7)]
# Define the button 
pinbutton1 = Pin(14, Pin.IN, Pin.PULL_DOWN)

Here, LED pins for the dice are set as outputs ranging from 0 to 6. The button pin is set as an input- 14, with a pull-down resistor. This means that when the button is not pressed, the button pin reads as 0 or False.

Defining LED Number Patterns

# Define the LED patterns for each number
numbers = [
    [0, 0, 0, 1, 0, 0, 0],  # 1
    [1, 0, 0, 0, 0, 0, 1],  # 2
    [1, 0, 0, 1, 0, 0, 1],  # 3
    [1, 1, 0, 0, 0, 1, 1],  # 4
    [1, 1, 0, 1, 0, 1, 1],  # 5
    [1, 1, 1, 0, 1, 1, 1],  # 6
]

These lines define the patterns for each number as it would appear on the dice. 0 would turn an LED off, and 1 would turn it on.

Functions to Handle LEDs

# Function to turn off LEDs of a specific dice
def turn_off_leds(dice_leds):
    for led in dice_leds:
        led.value(0)

This function turns off all the LEDs on the dice by setting their values to 0.

# Function to show a number on the dice
def show_number(dice_leds, number):
    # Get the LED pattern for the number
    pattern = numbers[number - 1]
    
    # Loop over each LED in the dice
    for i in range(len(dice_leds)):
        # Get the corresponding LED and its value from the pattern
        led = dice_leds[i]
        value = pattern[i]
        
        # Set the LED to the value from the pattern
        led.value(value)

The show_number() function takes in the diceโ€™s LED info and a number, then sets the appropriate LEDs on to display that number.

Main Loop

# Main loop
while True:
    if button1.value() == 1:
        utime.sleep(0.3)  # Wait for 2 seconds to ensure the button is pressed long enough
        if button1.value() == 1:  # Check again if the button is still pressed
            utime.sleep_ms(urandom.randint(0, 200))  # Random delay before generating the number
            show_number(dice1_leds, urandom.randint(1, 6))
            utime.sleep(2)  # Keep the LEDs on for 5 seconds
            turn_off_leds(dice1_leds)  # Then turn them off

    utime.sleep_ms(10)  # Short delay to debounce the buttons

In the main loop, a check is done as to whether the button has been pressed. If yes, a sleep of 0.3 seconds is implemented (to avoid phantom readings), after which itโ€™s confirmed if the button is still pressed. If still pressed, a randomly timed delay is executed then the dice number gets displayed using show_number. The LEDs stay illuminated for 2 seconds, then get turned off. The button is then debounced with a short delay before the loop repeats.

PCB ( JLCPCB )

PCB Design

With a working prototype we can create a custom PCB using EasyEda. The following figure is the schematic diagram for the Pico Dice PCB. Since we have access to 26 GPIO pins on the Pico we can connect all the LEDs to its own GPIO pin and donโ€™t require any special drivers to control all the LEDs.

The following figure is the layout of the components on the PCB, which as shown above mimics the patterns of a typical dice.

The PCB was ordered through JLCPCB. They offer great PCBs at a low cost and have promotions and coupons available throughout the year. You can sign up using here, or using the following link:

https://jlcpcb.com/?from=Nerd that will support me as a creator to keep making content that is accessible and open source at no charge to you.

Ordering the PCB is very simple:

Download the Gerber file here.

Click on Add Gerber file

leave all the settings as default given. You might want change the PCB color which you can do here:

Enter you shipping details, save to cart

Then after a few days depending on your location you will receive your great quality PCB.

Final Code

from machine import Pin
import utime
import urandom
from neopixel import Neopixel

urandom.seed(utime.ticks_us())

# ============================
# NeoPixel Setup
# ============================
NUM_LEDS = 14
PIN_NUM = 0
STATE_MACHINE = 0

np = Neopixel(NUM_LEDS, STATE_MACHINE, PIN_NUM, "GRB")
np.brightness(80)  # 1โ€“255

# Dice layout
DICE_LEN = 7
DICE1_START = 0
DICE2_START = 7

OFF = (0, 0, 0)

# ============================
# Buttons (Pico: GP1/2/3)
# ============================
button1 = Pin(1, Pin.IN, Pin.PULL_DOWN)  # roll dice 1
button2 = Pin(2, Pin.IN, Pin.PULL_DOWN)  # roll both
button3 = Pin(3, Pin.IN, Pin.PULL_DOWN)  # roll dice 2

# Dice pip patterns for 1โ€“6 (7 LEDs per die)
numbers = [
    [0, 0, 0, 1, 0, 0, 0],  # 1
    [1, 0, 0, 0, 0, 0, 1],  # 2
    [1, 0, 0, 1, 0, 0, 1],  # 3
    [1, 1, 0, 0, 0, 1, 1],  # 4
    [1, 1, 0, 1, 0, 1, 1],  # 5
    [1, 1, 1, 0, 1, 1, 1],  # 6
]


def show():
    np.show()

def clear_all():
    np.clear()
    np.show()

def clear_range(start, length):
    for i in range(start, start + length):
        np.set_pixel(i, OFF)

def show_number(start, number, color):
    pattern = numbers[number - 1]
    for i in range(DICE_LEN):
        np.set_pixel(start + i, color if pattern[i] else OFF)

def other_die_start(dice_start):
    return DICE2_START if dice_start == DICE1_START else DICE1_START


DEBOUNCE_MS = 60
_last_press_ms = {1: 0, 2: 0, 3: 0}
_last_state = {1: 0, 2: 0, 3: 0}

def _read_pin(btn: Pin) -> int:
    return 1 if btn.value() else 0

def was_pressed(pin_num: int, btn: Pin) -> bool:
    """
    Returns True only on a rising edge (0->1), debounced.
    """
    now = utime.ticks_ms()
    cur = _read_pin(btn)
    prev = _last_state[pin_num]
    _last_state[pin_num] = cur

    if cur == 1 and prev == 0:
        # rising edge
        if utime.ticks_diff(now, _last_press_ms[pin_num]) > DEBOUNCE_MS:
            _last_press_ms[pin_num] = now
            return True
    return False

def any_button_down():
    return button1.value() or button2.value() or button3.value()

# ------------------------------------------------------------
# Rainbow (idle animation)
# ------------------------------------------------------------
def wheel(pos):
    pos = 255 - (pos & 255)
    if pos < 85:
        return (255 - pos * 3, 0, pos * 3)
    if pos < 170:
        pos -= 85
        return (0, pos * 3, 255 - pos * 3)
    pos -= 170
    return (pos * 3, 255 - pos * 3, 0)

def rainbow_step(offset):
    for i in range(NUM_LEDS):
        np.set_pixel(i, wheel((i * 256 // NUM_LEDS + offset) & 255))
    np.show()

# ------------------------------------------------------------
# Dice Roll Animation
# ------------------------------------------------------------
def roll_animation(dice1=False, dice2=False,
                   color1=(255, 255, 255), color2=(255, 0, 255),
                   steps=16, start_delay_ms=40, end_delay_ms=180):
    """
    Shows quick changing faces and slows down near the end (ease-out).
    """
    for s in range(steps):
        if dice1:
            show_number(DICE1_START, urandom.randint(1, 6), color1)
        if dice2:
            show_number(DICE2_START, urandom.randint(1, 6), color2)

        np.show()

        delay = start_delay_ms + (end_delay_ms - start_delay_ms) * s // max(1, steps - 1)
        utime.sleep_ms(delay)

# ------------------------------------------------------------
# Roll functions
# ------------------------------------------------------------
def roll_one(dice_start, color, hold_s=2, blank_other=True):
    # Turn off the other die during the single roll (less distracting)
    if blank_other:
        clear_range(other_die_start(dice_start), DICE_LEN)
        np.show()

    roll_animation(
        dice1=(dice_start == DICE1_START),
        dice2=(dice_start == DICE2_START),
        color1=color,
        color2=color,
        steps=16
    )

    final = urandom.randint(1, 6)
    show_number(dice_start, final, color)
    np.show()

    utime.sleep(hold_s)

    # Clear both dice after showing result
    clear_range(dice_start, DICE_LEN)
    if blank_other:
        clear_range(other_die_start(dice_start), DICE_LEN)
    np.show()

def roll_both(color1=(0, 255, 0), color2=(255, 0, 255), hold_s=3):
    roll_animation(dice1=True, dice2=True, color1=color1, color2=color2, steps=18)

    d1 = urandom.randint(1, 6)
    d2 = urandom.randint(1, 6)
    show_number(DICE1_START, d1, color1)
    show_number(DICE2_START, d2, color2)
    np.show()

    utime.sleep(hold_s)

    clear_range(DICE1_START, DICE_LEN)
    clear_range(DICE2_START, DICE_LEN)
    np.show()

# ============================================================
# Main loop
# ============================================================
clear_all()

rainbow_offset = 0
last_idle_tick = utime.ticks_ms()
IDLE_FRAME_MS = 40  # lower = faster rainbow

while True:
    # Idle animation (only when no buttons are held down)
    if not any_button_down():
        now = utime.ticks_ms()
        if utime.ticks_diff(now, last_idle_tick) > IDLE_FRAME_MS:
            last_idle_tick = now
            rainbow_step(rainbow_offset)
            rainbow_offset = (rainbow_offset + 2) & 255

    # Button actions (edge-triggered)
    if was_pressed(1, button1):
        # Dice 1 only (other die off)
        roll_one(DICE1_START, (255, 0, 255), hold_s=2, blank_other=True)

    if was_pressed(2, button2):
        # Both dice
        roll_both(color1=(0, 255, 0), color2=(255, 0, 255), hold_s=3)

    if was_pressed(3, button3):
        # Dice 2 only (other die off)
        roll_one(DICE2_START, (255, 0, 255), hold_s=2, blank_other=True)

    utime.sleep_ms(5)

Enclosure

The enclosure was designed in Fusion 360.

You can download all the 3D files here: https://github.com/Guitarman9119/Raspberry-Pi-Pico-/tree/main/Pico%20Dice/3D%20model

Conclusion

This is a simple project, but it a perfect project for beginners that just started out with soldering, coding and 3D modelling.

If you have any questions you can comment on my YouTube video, and while you are on my video consider subscribing to the channel.