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
| Component | Quantity | Quantity | |
|---|---|---|---|
| Raspberry Pi Pico | 1 | 1 | |
| Custom PCB (JLCPCB) | 1 | 1 | |
| 14 - 5mm LEDs | 14 | Few | |
| 330-1k Ohm Resistor | 14 | 5 | |
| 3D printed parts | 1 | Few printed parts | |
| 3mm screw | 4 | Explained 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.