Lab 10: Accelerometer Bubble
Pixel says...
Time to put the sensor in charge of the lights! Tip your kit and watch a glowing dot slide toward the low side.
Let's light this up!
Program file: 10-accel-bubble.py
What you'll learn
- How to turn a tilt number into a pixel position
- How a small function does the math for both x and y
- How
FLIP_X,FLIP_Y, andSWAP_XYfix the direction - How nested loops draw a 2×2 dot
What you'll need
- Your whole kit: the matrix and the accelerometer, wired as shown in the Kit Guide
config.pysaved on the Pico- Thonny open and connected to your Pico
- The
xyfunction from Lab 7 and the readings from Lab 9
The program
This program draws a small blue-green dot. The dot slides toward whichever side of the kit is lowest.
# Test 10: Accelerometer Bubble
# Filename: 10-accel-bubble.py
# Version: 1.0.0
#
# A 2x2 dot on the matrix acts like a bubble in a level: tilt the kit
# and the dot slides toward the low side. If it slides the wrong way,
# change FLIP_X or FLIP_Y. If left-right and up-down are swapped,
# change SWAP_XY.
# Not yet tested on hardware.
from machine import Pin, I2C
from neopixel import NeoPixel
from utime import sleep
import ustruct
import config
print("Test 10: Accelerometer Bubble (version 1.0.0)")
# hardware settings from config.py
NEOPIXEL_PIN = config.NEOPIXEL_PIN
NUMBER_PIXELS = config.NUMBER_PIXELS
MATRIX_WIDTH = config.MATRIX_WIDTH
MATRIX_HEIGHT = config.MATRIX_HEIGHT
SERPENTINE = config.SERPENTINE
ACCEL_I2C_ID = config.ACCEL_I2C_ID
ACCEL_SDA_PIN = config.ACCEL_SDA_PIN
ACCEL_SCL_PIN = config.ACCEL_SCL_PIN
ACCEL_ADDRESS = config.ACCEL_ADDRESS
# LIS3DH registers
CTRL_REG1 = 0x20
CTRL_REG4 = 0x23
ACCEL_DATA_REGISTER = 0x28 | 0x80 # the 0x80 bit makes the chip step through x, y, z
COUNTS_PER_G = 16384
# how the sensor is mounted relative to the matrix
FLIP_X = False
FLIP_Y = True
SWAP_XY = False
strip = NeoPixel(Pin(NEOPIXEL_PIN), NUMBER_PIXELS)
i2c = I2C(ACCEL_I2C_ID, sda=Pin(ACCEL_SDA_PIN), scl=Pin(ACCEL_SCL_PIN), freq=400000)
i2c.writeto_mem(ACCEL_ADDRESS, CTRL_REG1, b'\x57') # 100 readings a second, x y z on
i2c.writeto_mem(ACCEL_ADDRESS, CTRL_REG4, b'\x88') # high resolution, +/- 2 g
def xy(x, y):
if SERPENTINE and y % 2 == 1:
return y * MATRIX_WIDTH + (MATRIX_WIDTH - 1 - x)
return y * MATRIX_WIDTH + x
def to_column(g, size):
# -1 g -> 0, 0 g -> middle, +1 g -> size - 2 (leaves room for the 2x2 dot)
position = round((g + 1) / 2 * (size - 2))
return max(0, min(size - 2, position))
while True:
raw = i2c.readfrom_mem(ACCEL_ADDRESS, ACCEL_DATA_REGISTER, 6)
ax, ay, az = ustruct.unpack('<hhh', raw)
gx = ax / COUNTS_PER_G
gy = ay / COUNTS_PER_G
if SWAP_XY:
gx, gy = gy, gx
if FLIP_X:
gx = -gx
if FLIP_Y:
gy = -gy
col = to_column(gx, MATRIX_WIDTH)
row = to_column(gy, MATRIX_HEIGHT)
for i in range(NUMBER_PIXELS):
strip[i] = (0, 0, 0)
for dx in range(2):
for dy in range(2):
strip[xy(col + dx, row + dy)] = (0, 40, 40)
strip.write()
sleep(0.03)
Run it and tilt the kit. The 2×2 dot slides toward the low side. Hold the kit level and the dot sits near the middle.
Test 10: Accelerometer Bubble (version 1.0.0)

This picture was drawn by a computer simulator, so your real matrix may look a little different.
How it works
Turn g into a column
def to_column(g, size):
# -1 g -> 0, 0 g -> middle, +1 g -> size - 2 (leaves room for the 2x2 dot)
position = round((g + 1) / 2 * (size - 2))
return max(0, min(size - 2, position))
The sensor gives a tilt between -1 and +1. The matrix needs a column between 0 and 14 (14 leaves room for a two-pixel-wide dot). The math in the middle stretches one range onto the other:
- Add 1, so the tilt goes from 0 to 2.
- Divide by 2, so it goes from 0 to 1.
- Multiply by
size - 2, so it goes from 0 to 14.
Here is the math for a 16-pixel-wide matrix:
| Tilt (g) | Math | Column |
|---|---|---|
| -1 | (-1 + 1) / 2 × 14 | 0 |
| 0 | (0 + 1) / 2 × 14 | 7 |
| 0.25 | (0.25 + 1) / 2 × 14 = 8.75 | 9 |
| 1 | (1 + 1) / 2 × 14 | 14 |
The last line, max(0, min(size - 2, position)), keeps the answer between 0 and 14, even if a hard shake pushes g past 1.
The same function works for the rows. We give it the tilt in y and the matrix height.
Fix the direction
FLIP_X = False
FLIP_Y = True
SWAP_XY = False
How you mount the sensor decides which way its x and y numbers point. These three settings turn them around so the dot slides down the hill. FLIP_Y = True makes the program use -gy instead of gy. SWAP_XY would trade x and y. If your dot slides the wrong way, change one of these settings and run it again.
Draw the 2×2 dot
for dx in range(2):
for dy in range(2):
strip[xy(col + dx, row + dy)] = (0, 40, 40)
This is a nested loop from Lab 8. Both dx and dy count 0, 1. So the loop colors four pixels: the one at (col, row) and the three next to it. Together they make a 2×2 square.
Key idea
A mapping takes numbers from one range and stretches them onto another range. Sensors, games, and graphs all use mappings.
Try it yourself
- Change the color of the dot from
(0, 40, 40)to something you like. Keep the numbers small. - Flip an axis. Change
FLIP_X = FalsetoFLIP_X = True. Predict what changes, then tilt the kit left and right. - Draw a bigger dot. A 3×3 dot needs four changes. Use
range(3)in each of the two loops. Changesize - 2tosize - 3in two places insideto_column. Can you find all four?
Check your understanding
- What range of tilt numbers does the sensor give? What range of columns does the matrix need?
- In
to_column, what column comes out when the tilt is 0? - What does
FLIP_Y = Truedo? - How many pixels light up in the dot? How does a nested loop make them?
Lab complete!
You made lights follow gravity! That mapping idea shows up in every game you will ever play.
What's next: In Lab 11: Sloshing Water, the whole matrix becomes a pan of water that sloshes.