Skip to content

Instantly share code, notes, and snippets.

@darconeous
Last active October 25, 2023 19:34
Show Gist options
  • Save darconeous/ab552187f9b7394b9036ddd7bf654047 to your computer and use it in GitHub Desktop.
Save darconeous/ab552187f9b7394b9036ddd7bf654047 to your computer and use it in GitHub Desktop.
PIO-based GE-ColorEffects G35 driver for MicroPython on RP2
# PIO-based GE-ColorEffects driver for MicroPython on RP2
# MIT license; Copyright (c) 2023 Robert Quattlebaum
import array, time
from machine import Pin
import rp2
import machine
# Pin 15 is the default on the [pimoroni plasma stick][1].
#
# [1]: https://shop.pimoroni.com/products/plasma-stick-2040-w
default_pin = machine.Pin(15)
# Each bulb has an address numbering from zero to fourty-nine, with
# bulb zero being the bulb closest to the control box.
#
# The protocol on the data line is simple and self-clocked. Here
# are the low-level details:
#
# * Idle bus state: Low
# * Start Bit: High for 10µSeconds
# * 0 Bit: Low for 10µSeconds, High for 20µSeconds
# * 1 Bit: Low for 20µSeconds, High for 10µSeconds
# * Minimum quiet-time between frames: 30µSeconds
# * Each frame is 26 bits long and has the following format:
#
# NOTE: You can actually clock out the bits significantly faster
# than this (a little more than twice as fast) and still have
# it work, as long as you keep the quiet period between frames
# to 30µSeconds.
#
# * Start bit
# * 6-Bit Bulb Address, MSB first
# * 8-Bit Brightness, MSB first
# * 4-Bit Blue, MSB first
# * 4-Bit Green, MSB first
# * 4-Bit Red, MSB first
#
# From this we can see that we have a color depth of 12 bits.
# Not terribly great, but this should still be plenty for our
# purposes. What is interesting is the Brightness field. This
# field acts a bit like a multiplier and enables smooth
# fade-ins and fade-outs.
#
# Bulb address 63 can be thought of as the "broadcast" bulb,
# except that it only changes the brightness level—the color
# fields are ignored. Bulb addresses 50-62 are ignored under
# normal circumstances. You can give a bulb address 63 if you
# want to have a 64 bulbs (8x8 matrix, perhaps?), but changing
# the brightness on bulb 63 will still change the brightness
# on all bulbs.
#
# Each frame takes 820 µSeconds to transmit (using the timing
# described above). Since there are 50 bulbs, that means that
# takes a minimum of 41 milliseconds to individually update
# every bulb. This gives us a maximum refresh rate of slightly
# more than 24Hz. Not bad.
#
# With the faster timing, you can easily get over 50Hz.
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, fifo_join=rp2.PIO.JOIN_TX)
def gecoloreffects_pio():
pull() # Block until data available, 5us low
set(x, 26) # Initialise bit counter, 5us low
set(pins, 1) [1] # Start bit, 10us of high
label("bitloop")
set(pins, 0) [1] # 10us of low
out(pins, 1) [1] # 10us of the bit value
set(pins, 1) # 5us of high
jmp(x_dec, "bitloop") # 5us of high & jump
set(pins, 0) [31]
class GeColorEffects:
BLACK = (0,0,0)
RED = (255,0,0)
GREEN = (0,255,0)
BLUE = (0,0,255)
WHITE = (255,255,255)
FAST = 550_000
SLOW = 275_000
SLOWEST = 140_000
def __init__(self, pin=default_pin, count=50, freq=FAST):
self.sm = rp2.StateMachine(0, gecoloreffects_pio, freq=freq, out_base=pin, set_base=pin)
self.sm.active(1)
self.count = count
self.buf = array.array("I", [0 for _ in range(count)])
self.brightness = 255
# Initialize all of the pixels.
#self.fill(GeColorEffects.BLACK)
#self.write()
def __setitem__(self, i, v):
self.buf[i] = GeColorEffects.encode_pixel(i, v, self.brightness)
def __getitem__(self, i):
return GeColorEffects.BLACK
def set_brightness(self, level):
self.brightness = level
self.update_pixel(63, GeColorEffects.BLACK, brightness=level)
def fill(self, color=BLACK):
for i in range(self.count):
self.buf[i] = GeColorEffects.encode_pixel(i, color, self.brightness)
def clear(self, color=BLACK):
for addr in range(self.count):
self.update_pixel(addr, color)
@micropython.native # noqa: F821
def encode_pixel(addr, color, brightness=255):
return ~(((addr) << 20) + ((brightness) << 12) + (((color[2] &0xF0))<<4) + (color[1] &0xF0) + ((color[0] >> 4)))
def update_raw(self, pixel):
self.sm.put(pixel, 6)
def write(self):
self.sm.put(self.buf, 6)
def update_pixel(self, addr, color, brightness=None):
if brightness is None:
brightness = self.brightness
raw_value = GeColorEffects.encode_pixel(addr, color, brightness)
self.update_raw(raw_value)
if addr < self.count:
self.buf[addr] = raw_value
# For Pimoroni Compatability
def set_rgb(self, addr, r,g,b):
self.update_pixel(addr, (r,g,b))
# For Pimoroni Compatability
def start(self):
self.clear()
@micropython.native # noqa: F821
def wheel(pos):
# Input a value 0 to 255 to get a color value.
# The colours are a transition r - g - b - back to r.
if pos < 0 or pos > 255:
return (0, 0, 0)
if pos < 85:
return (255 - pos * 3, pos * 3, 0)
if pos < 170:
pos -= 85
return (0, 255 - pos * 3, pos * 3)
pos -= 170
return (pos * 3, 0, 255 - pos * 3)
def rainbow_demo(pixels=None):
import gc
if pixels is None:
pixels = GeColorEffects(default_pin, count=50)
j = 0
while True:
j += 1
for i in range(pixels.count):
rc_index = (i * 256 // pixels.count) + j
color = wheel(rc_index & 255)
pixels.update_pixel(i,color)
gc.collect()
machine.idle()
if __name__=='__main__':
rainbow_demo()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment