In addition to Arduino you can also build a fidget spinner tachometer with Circuit Playground Express and CircuitPython.  This Python-powered tachometer works just like the Arduino version and can detect the speed of a spinner held in front of the board's light sensor.  However instead of using low level Arduino programming code this tachometer is programmed with simpler Python code!

To build the CircuitPython version of the tachometer you must use the Circuit Playground Express board.  The older Circuit Playground classic board unfortunately can't run CircuitPython so you'll need the latest express board.

Once you have the board you'll need to load it with the latest version of CircuitPython firmware.  Follow the steps in this Metro M0 Express guide to see how to load CircuitPython onto a board--the steps are exactly the same for Circuit Playground Express except you'll download the Circuit Playground Express .uf2 firmware from the latest CircuitPython release.

After you've loaded CircuitPython onto the board you should see it appear as a USB drive named CIRCUITPY when connected to your computer.  This is where you can copy Python code and other files for the board to run.

First you'll need to copy a CircuitPython NeoPixel module that allows code to control the NeoPixels on the board.  Go to the releases tab of the CircuitPython NeoPixel module and download the neopixel.mpy file from the latest release.  Then drag this neopixel.mpy file onto the board's CIRCUITPY drive.

Next download the code for this project below and save it as a file on the board's CIRCUITPY drive:

Download: file
# Adafruit Circuit Playground Express Fidget Spinner Tachometer
# This code uses the light sensor built in to Circuit Playground Express
# to detect the speed (in revolutions per second) of a fidget spinner.
# Save this code as on a Circuit Playground Express board running
# CircuitPython (see  You will
# also need to load neopixel.mpy onto the board's filesystem (from
# When the first three NeoPixels light up white you're ready to read the speed # of a spinner. Hold a spinning fidget spinner very close to (but not touching)
# the light sensor (look for the eye graphic on the board, it's right
# below the three lit NeoPixels) and look at the serial terminal from the board
# at 115200 baud to see the speed of the spinner printed.  This works best
# holding the spinner perpendicular to the sensor, like:
#       ||
#       || <- Spinner
#       ||
#    ________  <- Circuit Playground
# Author: Tony DiCola
# License: MIT License (
import array

import board
import analogio
import time

import neopixel

# Configuration:
SPINNER_ARMS           = 3       # Number of arms on the fidget spinner.
                                 # This is used to calculate the true
                                 # revolutions per second of the spinner
                                 # as one full revolution of the spinner
                                 # will actually see this number of cycles
                                 # pass by the light sensor.  Set this to
                                 # the value 1 to ignore this calculation
                                 # and just see the raw cycles / second.

SAMPLE_DEPTH           = 256     # How many samples to take when measuring
                                 # the spinner speed.  The larger this value
                                 # the more memory that will be consumed but
                                 # the slower a spinner speed that can be
                                 # detected (larger sample depths mean longer
                                 # period waves can be detected).  You're
                                 # limited by the amount of memory on the
                                 # board for this value.

TARGET_SAMPLE_RATE_HZ  = 150     # Target sample rate for sampling the light
                                 # sensor.  This in combination with the sample
                                 # depth above controls how slow and fast of
                                 # a signal you can detect.  Note that the
                                 # sample rate can only go so high before it's
                                 # too fast for the Python interpreted code
                                 # to run.  If that happens the board will
                                 # light up LEDs red to indicate the 'underflow'
                                 # condition (drop the sample rate down to
                                 # a lower value and try again).
                                 # A value of 150 times a second means you can
                                 # measure a spinner going up to 25 revolutions
                                 # per second.

THRESHOLD              = 40000   # How big the magnitude of a cyclic
                                 # signal has to be before the measurement
                                 # logic kicks in.  This is a value from
                                 # 0 to 65535 and might need to be adjusted
                                 # up or down if the detection is too
                                 # sensitive or not sensitive enough.
                                 # Raising this value will make the detection
                                 # less sensitive and require a very large
                                 # difference in amplitude (i.e. a very close
                                 # or highly reflective spinner), and lowering
                                 # the value will make the detection more
                                 # sensitive and potentially pick up random
                                 # noise from light in the room.

# Configure NeoPixels and turn them all off at the start.
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10)

# Configure analog input for light sensor.
light = analogio.AnalogIn(board.LIGHT)

# Take an initial set of readings and measure how long it takes.
# This is used to calculate a delay between readings to hit the desired
# target sample rate.  Use the array module to preallocate an array of 16-bit
# unsigned samples with lower memory overhead vs. a simple python list.
readings = array.array('H', [0]*SAMPLE_DEPTH)
start = time.monotonic()
for i in range(SAMPLE_DEPTH):
    readings[i] = light.value
stop = time.monotonic()

# Calculate how long it took to take all the readings above, then figure out
# the difference from the target period to actual period.  This difference is
# the amount of time to delay between sample readings to hit the desired target
# sample rate.
target_period = 1.0/TARGET_SAMPLE_RATE_HZ
actual_period = (stop-start)/SAMPLE_DEPTH
delay = 0
# Check that we can sample fast enough to hit the target rate.
if actual_period > target_period:
    # Uh oh can't sample fast enough--print a warning and light up pixels red.
    print('Could not hit desired target sample rate!')
    # No problem hitting target sample rate so calculate the delay between
    # samples to hit that desired rate.  Then turn on the first three pixels
    # to white full brightness.
    delay = target_period - actual_period
    pixels[0] = (255, 255, 255)
    pixels[1] = (255, 255, 255)
    pixels[2] = (255, 255, 255)

# Main loop:
while True:
    # Pause for a second between tachometer readings.
    # Grab a set of samples and measure the time it took to do so.
    start = time.monotonic()
    for i in range(SAMPLE_DEPTH):
        readings[i] = light.value
        time.sleep(delay)  # Sleep for the delay calculated to hit target rate.
    stop = time.monotonic()
    elapsed = stop - start
    # Find the min and max readings from the samples.
    minval = readings[0]
    maxval = readings[0]
    for r in readings:
        minval = min(minval, r)
        maxval = max(maxval, r)
    # Calculate magnitude or size of the signal.  If the magnitude doesn't
    # pass the threshold then start over with a new sample (run the loop again).
    magnitude = maxval - minval
    if magnitude < THRESHOLD:
    # Calculate the midpoint of the signal, then count how many times the
    # signal crosses the midpoint.
    midpoint = minval + magnitude/2.0
    crossings = 0
    for i in range(1, SAMPLE_DEPTH):
        p0 = readings[i-1]
        p1 = readings[i]
        # Check if a pair of points crossed the midpoint either by hitting it
        # exactly or hitting it going up or down.
        if p1 == midpoint or p0 < midpoint < p1 or p0 > midpoint > p1:
           crossings += 1
    # Finally use the number of crosssings and the amount of time in the sample
    # window to calculate how many times the spinner arms crossed the light
    # sensor.  Use that period to calculate frequency (rotations per second)
    # and RPM (rotations per minute).
    period = elapsed / (crossings / 2.0 / SPINNER_ARMS)
    frequency = 1.0/period
    rpm = frequency * 60.0
    print('Frequency: {0:0.5} (hz)\t\tRPM: {1:0.5}\t\tPeriod: {2:0.5} (seconds)'.format(frequency, rpm, period))

You can also grab this code from a gist on GitHub.

Make sure both neopixel.mpy and the above code saved as are on the board's filesystem, it should look something like this:

Once the file is copied to the board it should restart and run the tachometer code.  You'll see the first three NeoPixels light up bright white to indicate the board is ready to detect the speed of a fidget spinner.  If you don't see the lights turn on then eject the CIRCUITPY drive, disconnect the board, and reconnect it to your computer to reset it.

Once the code is running and the lights are lit up you can open the board's serial terminal at 115200 baud.  Hold a spinning fidget spinner in front of the light sensor and you should see the speed printed out every second.  You need to hold the spinner very close to the light sensor to make sure it can get a good reading.  Watch the video at the top of this page to see an example of using the tachometer.

That's all there is to the CircuitPython version of the fidget spinner tachometer!

This guide was first published on Jul 07, 2017. It was last updated on Jul 07, 2017. This page (CircuitPython) was last updated on Jul 21, 2019.