Last active
October 25, 2023 19:34
-
-
Save darconeous/ab552187f9b7394b9036ddd7bf654047 to your computer and use it in GitHub Desktop.
PIO-based GE-ColorEffects G35 driver for MicroPython on RP2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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