Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace PIO code with an inbuilt timer, based on https://learn.adafru… #20

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
133 changes: 87 additions & 46 deletions neopixel.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
import array, time
import array
from machine import Pin
import rp2


# PIO state machine for RGB. Pulls 24 bits (rgb -> 3 * 8bit) automatically
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
T1 = 2
T2 = 5
T3 = 3
# based on https://learn.adafruit.com/intro-to-rp2040-pio-with-circuitpython/advanced-using-pio-to-drive-neopixels-in-the-background by https://learn.adafruit.com/u/jepler
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=False)
def sk6812():
wrap_target()
pull(block) .side(0) # get fresh NeoPixel bit count value
out(y, 32) .side(0) # get count of NeoPixel bits

label("bitloop")
out(x, 1) .side(0) [T3 - 1]
jmp(not_x, "do_zero") .side(1) [T1 - 1]
jmp("bitloop") .side(1) [T2 - 1]
pull(ifempty) .side(0) # drive low
out(x, 1) .side(0) [5]
jmp(not_x, "do_zero") .side(1) [3] # drive high and branch depending on bit val
jmp(y_dec, "bitloop") .side(1) [4] # drive high for a one (long pulse)
jmp("end_sequence") .side(0) # sequence is over

label("do_zero")
nop() .side(0) [T2 - 1]
jmp(y_dec, "bitloop") .side(0) [4] # drive low for a zero (short pulse)

label("end_sequence")
pull(block) .side(0) # get fresh delay value
out(y, 32) .side(0) # get delay count

label("wait_reset")
jmp(y_dec, "wait_reset") .side(0) # wait until delay elapses
wrap()


# PIO state machine for RGBW. Pulls 32 bits (rgbw -> 4 * 8bit) automatically
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=32)
def sk6812():
T1 = 2
T2 = 5
T3 = 3
# based on https://learn.adafruit.com/intro-to-rp2040-pio-with-circuitpython/advanced-using-pio-to-drive-neopixels-in-the-background by https://learn.adafruit.com/u/jepler
@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=False)
def ws2812():
wrap_target()
pull(block) .side(0) # get fresh NeoPixel bit count value
out(y, 32) .side(0) # get count of NeoPixel bits

label("bitloop")
out(x, 1) .side(0) [T3 - 1]
jmp(not_x, "do_zero") .side(1) [T1 - 1]
jmp("bitloop") .side(1) [T2 - 1]
jmp(not_osre, "bit_out") .side(0) # if OSR still contains data, skip this section
pull(ifempty) .side(0) # pull new 32bits of RGB data
out(x, 8) .side(0) # skip first 8 as RGB is 24bits

label("bit_out")
pull(ifempty) .side(0) # drive low
out(x, 1) .side(0) [5]
jmp(not_x, "do_zero") .side(1) [3] # drive high and branch depending on bit val
jmp(y_dec, "bitloop") .side(1) [4] # drive high for a one (long pulse)
jmp("end_sequence") .side(0) # sequence is over

label("do_zero")
nop() .side(0) [T2 - 1]
jmp(y_dec, "bitloop") .side(0) [4] # drive low for a zero (short pulse)

label("end_sequence")
pull(block) .side(0) # get fresh delay value
out(y, 32) .side(0) # get delay count

label("wait_reset")
jmp(y_dec, "wait_reset") .side(0) # wait until delay elapses
wrap()


Expand All @@ -46,10 +71,6 @@ def __getitem__(self, slc):
slice_maker = slice_maker_class()


# Delay here is the reset time. You need a pause to reset the LED strip back to the initial LED
# however, if you have quite a bit of processing to do before the next time you update the strip
# you could put in delay=0 (or a lower delay)
#
# Class supports different order of individual colors (GRB, RGB, WRGB, GWRB ...). In order to achieve
# this, we need to flip the indexes: in 'RGBW', 'R' is on index 0, but we need to shift it left by 3 * 8bits,
# so in it's inverse, 'WBGR', it has exactly right index. Since micropython doesn't have [::-1] and recursive rev()
Expand All @@ -64,16 +85,17 @@ class Neopixel:
# to describe the data members...
# __slots__ = [
# 'num_leds', # number of LEDs
# 'pixels', # array.array('I') of raw data for LEDs
# 'pixels', # array.array('I') of raw data for LEDs (with header and trailer)
# 'mode', # mode 'RGB' etc
# 'W_in_mode', # bool: is 'W' in mode
# 'sm', # state machine
# 'shift', # shift amount for each component, in a tuple for (R,B,G,W)
# 'delay', # delay amount
# 'brightnessvalue', # brightness scale factor 1..255
# 'header', # header - num of data bits in the data stream
# 'trailer', # trailer - delay at the end
# ]

def __init__(self, num_leds, state_machine, pin, mode="RGB", delay=0.0003):
def __init__(self, num_leds, state_machine, pin, mode="RGB"):
"""
Constructor for library class

Expand All @@ -82,26 +104,38 @@ def __init__(self, num_leds, state_machine, pin, mode="RGB", delay=0.0003):
:param pin: pin on which data line to led-strip is connected
:param mode: [default: "RGB"] mode and order of bits representing the color value.
This can be any order of RGB or RGBW (neopixels are usually GRB)
:param delay: [default: 0.0001] delay used for latching of leds when sending data
"""
self.pixels = array.array("I", [0] * num_leds)
self.mode = mode
self.W_in_mode = 'W' in mode
bpp = 3
if self.W_in_mode:
# RGBW uses different PIO state machine configuration
self.sm = rp2.StateMachine(state_machine, sk6812, freq=8000000, sideset_base=Pin(pin))
self.sm = rp2.StateMachine(state_machine, sk6812, freq=12_800_000, sideset_base=Pin(pin))
# tuple of values required to shift bit into position (check class desc.)
self.shift = ((mode.index('R') ^ 3) * 8, (mode.index('G') ^ 3) * 8,
(mode.index('B') ^ 3) * 8, (mode.index('W') ^ 3) * 8)
bpp = 4
else:
self.sm = rp2.StateMachine(state_machine, ws2812, freq=8000000, sideset_base=Pin(pin))
self.sm = rp2.StateMachine(state_machine, ws2812, freq=12_800_000, sideset_base=Pin(pin))
mcarlson marked this conversation as resolved.
Show resolved Hide resolved
self.shift = (((mode.index('R') ^ 3) - 1) * 8, ((mode.index('G') ^ 3) - 1) * 8,
((mode.index('B') ^ 3) - 1) * 8, 0)
self.sm.active(1)
self.num_leds = num_leds
self.delay = delay
self.brightnessvalue = 255

# from https://learn.adafruit.com/intro-to-rp2040-pio-with-circuitpython/advanced-using-pio-to-drive-neopixels-in-the-background
byte_count = bpp * num_leds
bit_count = byte_count * 8

# array of 32bit integers, with header and trailer at the beginning and end
self.pixels = array.array("I", [0] * (num_leds + 2))
# header - num of bits in the data stream
self.header = bit_count - 1
# trailer - delay at the end (12.8 Mhz clk -> 1 period = 7.8125e-8, 3840 * 7.8125e-8 = 0.0003s delay)
self.trailer = 3840
self.pixels[0] = self.header
self.pixels[-1] = self.trailer

def brightness(self, brightness=None):
"""
Set the overall value to adjust brightness when updating leds
Expand Down Expand Up @@ -195,9 +229,11 @@ def set_pixel(self, pixel_num, rgb_w, how_bright=None):
# set some subset, if pixel_num is a slice:
if type(pixel_num) is slice:
for i in range(*pixel_num.indices(self.num_leds)):
self.pixels[i] = pix_value
# offset of 1 due to header
self.pixels[i + 1] = pix_value
else:
self.pixels[pixel_num] = pix_value
# offset of 1 due to header
self.pixels[pixel_num + 1] = pix_value

def get_pixel(self, pixel_num):
"""
Expand All @@ -206,7 +242,9 @@ def get_pixel(self, pixel_num):
:param pixel_num: Index of pixel to be set
:return rgb_w: Tuple of form (r, g, b) or (r, g, b, w) representing color to be used
"""
balance = self.pixels[pixel_num]
# offset of 1 due to header
balance = self.pixels[pixel_num + 1]

sh_R, sh_G, sh_B, sh_W = self.shift
if self.W_in_mode:
w = (balance >> sh_W) & 255
Expand Down Expand Up @@ -317,7 +355,10 @@ def rotate_left(self, num_of_pixels=None):
"""
if num_of_pixels is None:
num_of_pixels = 1
self.pixels = self.pixels[num_of_pixels:] + self.pixels[:num_of_pixels]
# offset of 1 due to header
num_of_pixels += 1
#self.pixels = self.pixels[num_of_pixels:] + self.pixels[:num_of_pixels]
self.pixels = array.array("I", [self.pixels[0]] + list(self.pixels[num_of_pixels:-1]) + list(self.pixels[1:num_of_pixels]) + [self.pixels[-1]])

def rotate_right(self, num_of_pixels=None):
"""
Expand All @@ -328,23 +369,21 @@ def rotate_right(self, num_of_pixels=None):
"""
if num_of_pixels is None:
num_of_pixels = 1
# offset of 1 due to header
num_of_pixels += 1
num_of_pixels = -1 * num_of_pixels
self.pixels = self.pixels[num_of_pixels:] + self.pixels[:num_of_pixels]
#self.pixels = self.pixels[num_of_pixels:] + self.pixels[:num_of_pixels]
self.pixels = array.array("I", [self.pixels[0]] + list(self.pixels[num_of_pixels:-1]) + list(self.pixels[1:num_of_pixels]) + [self.pixels[-1]])

def show(self):
"""
Send data to led-strip, making all changes on leds have an effect.
This method should be used after every method that changes the state of leds or after a chain of changes.
:return: None
"""
# If mode is RGB, we cut 8 bits of, otherwise we keep all 32
cut = 8
if self.W_in_mode:
cut = 0

self.sm.put(self.pixels, cut)
# put entire pixel data along with header and trailer into TX FIFO
self.sm.put(memoryview(self.pixels))

time.sleep(self.delay)

def fill(self, rgb_w, how_bright=None):
"""
Expand All @@ -363,4 +402,6 @@ def clear(self):

:return: None
"""
self.pixels = array.array("I", [0] * self.num_leds)
self.pixels = array.array("I", [0] * (self.num_leds + 2))
self.pixels[0] = self.header
self.pixels[-1] = self.trailer