This draws hypotrochoid spirals (like Spirograph) for a picodvi display on Fruit Jam or Metro RP2350. I tested this on a Fruit Jam rev B prototype and a Metro RP2350 (no PSRAM version). The 320x480 8-bit video mode works on both, but 640x480 8-bit seems to need a board that has a PSRAM chip. The curves get plotted as individual pixels using a color ramp that cycles through the entire 8-bit RGB332 palette. This might be useful to people looking for sample code showing how to configure picodvi displays for RP2350 boards.
For more details on the math for Hypotrochoid curves, you can check out the Wikipedia or Wolfram MathWorld Hypotrochoid pages.
Transparency note: Adafruit provided the Fruit Jam rev B prototype board I used for this guide (Thanks Adafruit!).
Parts
I used a pre-release revision B Fruit Jam prototype board and a Metro RP2350 (no PSRAM version). You could probably get very similar results to the Fruit Jam using a Metro RP2350 with PSRAM once you add an HSTX to DVI adapter. For more information on setting up a Metro RP2350 as a pseudo-Fruit Jam, you can check out some of the recent guides by Tim C. and M. LeBlanc-Williams.
Updating CircuitPython
As I write this (June 13, 2025), CircuitPython 10.0.0-alpha.6 is the most recent build for Fruit Jam. I'm not sure what will change by the time 10.0.0 is released. To keep your board up to date, you can use the "DOWNLOAD .UF2 NOW" buttons on the appropriate download page of circuitpython.org:
- Metro RP2350 Download page
- Fruit Jam Download page
To install the UF2 File:
- Connect your board to a computer with a USB data cable (charge-only cables won't work! )
- Press and hold the board's boot button (Button 1 on Fruit Jam)
- Press and release the reset button
- Release the boot button
- When the removable drive named RP2350 appears, copy the UF2 file onto it. After the copy finishes, you should see the RP2350 drive disappear and soon after that a new CIRCUITPY drive should appear.
CircuitPython Code
You can view the code at the samblenny/fruit-jam-spirals GitHub repository. To download a zip archive project bundle with the code and all the necessary libraries, use the "Download Project Bundle" button:
Install Project Bundle
To copy the project bundle files to your CIRCUITPY drive:
- Download the project bundle .zip file using the "Download Project Bundle" button above.
- Expand the zip file by opening it, or use unzip in a Terminal. The zip archive should expand to a folder. When you open the folder, it should contain a README.txt file and a CircuitPython 10.x folder.
- Open the CircuitPython 10.x folder and copy all of its contents to your CIRCUITPY drive.
To learn more about copying libraries to your CIRCUITPY drive, check out the CircuitPython Libraries section of the Welcome to CircuitPython! learn guide.
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
from board import CKP, CKN, D0P, D0N, D1P, D1N, D2P, D2N
import displayio
from displayio import Bitmap, Group, Palette, TileGrid
import framebufferio
import gc
import math
import picodvi
import supervisor
from time import sleep
def draw_spiral(a, b, h, cycles=64, oversample=2, delay_ms=1):
"""Draw a Hypotrochoid spiral
a: outer circle radius in pixels
b: inner circle radius in pixels
h: drawing pen radius in pixels from center of inner circle
cycles: how many 360 degree cycles to run the equations through
oversample: increase this to make the pixel density higher
delay_ms: increase this to draw slower (unit: ms)
"""
global bitmap
global display
global palette
# Alias trig functions with short names to improve equation readability
rad = math.radians
sin = math.sin
cos = math.cos
# Translate origin of graph to center of the display
(x0, y0) = (width / 2, height / 2)
# Clear the bitmap, then start drawing the hypotrochoid spiral
bitmap.fill(0)
max_color = len(palette)
for t in range(cycles * 360 * oversample):
color = (t // oversample) % max_color # Cycle through all colors
tr = rad(t/oversample)
x = round(x0 + (a - b) * cos(tr) + h * cos((a - b) / b * tr))
y = round(y0 + (a - b) * sin(tr) - h * sin((a - b) / b * tr))
if (0 <= x < width) and (0 <= y < height):
bitmap[x, y] = color
display.refresh()
# This delay controls the pen movement speed
sleep(0.001 * delay_ms)
# This delay pauses at the end so you can see the final image
sleep(5)
def init_display(width, height, color_depth):
"""Initialize the picodvi display
Video mode compatibility (only tested these--unsure about other boards):
| Video Mode | Fruit Jam | Metro RP2350 No PSRAM |
| -------------- | --------- | ------------------------ |
| 320x240, 8-bit | Yes! | Yes! |
| 640x480, 8-bit | Yes! | MemoryError exception :( |
"""
displayio.release_displays()
gc.collect()
fb = picodvi.Framebuffer(width, height, clk_dp=CKP, clk_dn=CKN,
red_dp=D0P, red_dn=D0N, green_dp=D1P, green_dn=D1N,
blue_dp=D2P, blue_dn=D2N, color_depth=color_depth)
display = framebufferio.FramebufferDisplay(fb)
supervisor.runtime.display = display
return display
# Pick a video mode (comment out the one you don't want):
(width, height, color_depth) = (320, 240, 8)
#(width, height, color_depth) = (640, 480, 8) # This needs board with PSRAM
# Detect if an existing display matches requested video mode
display = supervisor.runtime.display
if (display is None) or (width, height) != (display.width, display.height):
# Didn't find a display configured as we need, so initialize a new one
try:
display = init_display(width, height, color_depth)
except MemoryError as e:
# Fall back to low resolution so the error message will be readable
display = init_display(320, 240, 8)
print("---\nREQUESTED VIDEO MODE NEEDS A BOARD WITH PSRAM\n---")
raise e
# Use manual refresh for better performance
display.auto_refresh = False
grp = Group(scale=1)
display.root_group = grp
# Arrange a bitmap + palette + tilegrid so we have a canvas to draw on. The
# pallet gets initialized with all 256 of the RGB332 colors.
palette = Palette(256)
bitmap = Bitmap(width, height, 256)
for i in range(256):
# Expand 8-bit RGB332 (i) into a 24-bit RGB888 tuple (r, g, b)
r = i & 0xE0
g = (i & 0x1C) << 3
b = (i & 0x03) << 6
palette[i] = (r, g, b)
tilegrid = TileGrid(bitmap, pixel_shader=palette)
grp.append(tilegrid)
# Draw stuff (includes a brief pause after each spiral is done)
while True:
draw_spiral(a=110, b=12, h=11, cycles=6, oversample=3, delay_ms=3)
draw_spiral(a=111, b=25, h=9, cycles=6, delay_ms=3)
draw_spiral(a=111, b=25, h=29, cycles=27, oversample=3)
Avoiding MemoryError
When I tried to use picodvi with 640x480 and 8-bit color depth on my Metro RP2350 (no PSRAM version), I got a MemoryError exception. It works fine on Fruit Jam though. I expect the Metro RP2350 with PSRAM also works, but I haven't tested that. If you get an error that looks like this,
then you need to either use a different board or switch to a video mode that use less RAM.
This page (Fruit Jam Hypotrochoid Spiral Maker) was last updated on June 14, 2025.
Text editor powered by tinymce.