-
-
Save nevercast/9c48505cc6c5687af59bcb4a22062795 to your computer and use it in GitHub Desktop.
# Copyright public licence and also I don't care. | |
# 2020 Josh "NeverCast" Lloyd. | |
from micropython import const | |
from esp32 import RMT | |
# The peripheral clock is 80MHz or 12.5 nanoseconds per clock. | |
# The smallest precision of timing requried for neopixels is | |
# 0.35us, but I've decided to go with 0.05 microseconds or | |
# 50 nanoseconds. 50 nanoseconds = 12.5 * 4 clocks. | |
# By dividing the 80MHz clock by 4 we get a clock every 50 nanoseconds. | |
# Neopixel timing in RMT clock counts. | |
T_0H = const(35 // 5) # 0.35 microseconds / 50 nanoseconds | |
T_1H = const(70 // 5) # 0.70 microseconds / 50 nanoseconds | |
T_0L = const(80 // 5) # 0.80 microseconds / 50 nanoseconds | |
T_1L = const(60 // 5) # 0.60 microseconds / 50 nanoseconds | |
# Encoded timings for bits 0 and 1. | |
D_ZERO = (T_0H, T_0L) | |
D_ONE = (T_1H, T_1L) | |
# [D_ONE if ((channel >> bit) & 1) else D_ZERO for channel in channels for bit in range(num_bits - 1, -1, -1)] | |
# Reset signal is low for longer than 50 microseconds. | |
T_RST = const(510 // 5) # > 50 microseconds / 50 nanoseconds | |
# Channel width in bits | |
CHANNEL_WIDTH = const(8) | |
class Pixels: | |
def __init__(self, pin, pixel_count, rmt_channel=1, pixel_channels=3): | |
self.rmt = RMT(rmt_channel, pin=pin, clock_div=4) | |
# pixels stores the data sent out via RMT | |
self.channels = pixel_channels | |
single_pixel = (0,) * pixel_channels | |
self.pixels = [D_ZERO * (pixel_channels * CHANNEL_WIDTH)] * pixel_count | |
# colors is only used for __getitem__ | |
self.colors = [single_pixel] * pixel_count | |
def write(self): | |
# The bus should be idle low ( I think... ) | |
# So we finish low and start high. | |
pulses = tuple() | |
for pixel in self.pixels: | |
pulses += pixel | |
pulses = pulses[:-1] + (T_RST,) # The last low should be long. | |
self.rmt.write_pulses(pulses, start=1) | |
def __setitem__(self, pixel_index, colors): | |
self_colors = self.colors | |
self_pixels = self.pixels | |
if isinstance(pixel_index, int): | |
# pixels[0] = (r, g, b) | |
self_colors[pixel_index] = tuple(colors) | |
self_pixels[pixel_index] = tuple(clocks for bit in (D_ONE if ((channel >> bit) & 1) else D_ZERO for channel in colors for bit in range(CHANNEL_WIDTH - 1, -1, -1)) for clocks in bit) | |
elif isinstance(pixel_index, slice): | |
start = 0 if pixel_index.start is None else pixel_index.start | |
stop = len(self.pixels) if pixel_index.stop is None else pixel_index.stop | |
step = 1 if pixel_index.step is None else pixel_index.step | |
# Assume that if the first colors element is an int, then its not a sequence | |
# Otherwise always assume its a sequence of colors | |
if isinstance(colors[0], int): | |
# pixels[:] = (r,g,b) | |
for index in range(start, stop, step): | |
self_colors[index] = tuple(colors) | |
self_pixels[index] = tuple(clocks for bit in (D_ONE if ((channel >> bit) & 1) else D_ZERO for channel in colors for bit in range(CHANNEL_WIDTH - 1, -1, -1)) for clocks in bit) | |
else: | |
# pixels[:] = [(r,g,b), ...] | |
# Assume its a sequence, make it a list so we know the length | |
if not isinstance(colors, list): | |
colors = list(colors) | |
color_length = len(colors) | |
for index in range(start, stop, step): | |
color = colors[(index - start) % color_length] | |
self_colors[index] = tuple(color) | |
self_pixels[index] = tuple(clocks for bit in (D_ONE if ((channel >> bit) & 1) else D_ZERO for channel in color for bit in range(CHANNEL_WIDTH - 1, -1, -1)) for clocks in bit) | |
else: | |
raise TypeError('Unsupported pixel_index {} ({})'.format(pixel_index, type(pixel_index))) | |
def __getitem__(self, pixel_index): | |
# slice instances are passed through | |
return self.colors[pixel_index] | |
### All code below this point is test code and can be safely deleted in your own application. | |
def TEST(): | |
from machine import Pin | |
pin = Pin(18) | |
p = Pixels(pin, 5) | |
try: | |
assert(isinstance(p, Pixels)) | |
assert(p.channels == 3) | |
assert(len(p.colors) == 5) | |
assert(len(p.pixels) == 5) | |
assert(len(p.colors[0]) == 3) | |
assert(sum(p.colors[0]) == 0) | |
assert(p[0] == (0, 0, 0)) | |
old_pixels = p.pixels[0] | |
assert(p.pixels[0] == old_pixels) | |
p[0] = (1, 2, 3) | |
assert(p[0] == (1, 2, 3)) | |
assert(p.colors[0] == p[0]) | |
assert(p.pixels[0] != old_pixels) | |
assert(p[0:1] == [(1,2,3)]) | |
assert(p[0:2] == [(1,2,3), (0,0,0)]) | |
p[0:1] = (1, 3, 5) | |
assert(p.colors[0] == (1, 3, 5)) | |
assert(p.colors[1] == (0, 0, 0)) | |
assert(p.colors[0:2] == [(1,3,5), (0,0,0)]) | |
p[0:4] = [(1,1,1), (2,2,2)] | |
assert(p.colors[0:4] == [(1,1,1), (2,2,2)]*2) | |
finally: | |
p.rmt.deinit() | |
p = Pixels(pin, 5, pixel_channels=4) | |
try: | |
assert(isinstance(p, Pixels)) | |
assert(p.channels == 4) | |
assert(len(p.colors) == 5) | |
assert(len(p.pixels) == 5) | |
assert(len(p.colors[0]) == 4) | |
assert(sum(p.colors[0]) == 0) | |
assert(p[0] == (0, 0, 0, 0)) | |
old_pixels = p.pixels[0] | |
assert(p.pixels[0] == old_pixels) | |
p[0] = (1, 2, 3, 4) | |
assert(p[0] == (1, 2, 3, 4)) | |
assert(p.colors[0] == p[0]) | |
assert(p.pixels[0] != old_pixels) | |
assert(p[0:1] == [(1,2,3,4)]) | |
assert(p[0:2] == [(1,2,3,4), (0,0,0,0)]) | |
p[0:1] = (1, 3, 5, 7) | |
assert(p.colors[0] == (1, 3, 5, 7)) | |
assert(p.colors[1] == (0, 0, 0, 0)) | |
assert(p.colors[0:2] == [(1,3,5,7), (0,0,0,0)]) | |
p[0:4] = [(1,1,1,1), (2,2,2,2)] | |
assert(p.colors[0:4] == [(1,1,1,1), (2,2,2,2)]*2) | |
finally: | |
p.rmt.deinit() | |
def RAINBOW(): | |
from machine import Pin | |
from time import ticks_ms, ticks_diff | |
last = ticks_ms() | |
def delta(title=None): | |
nonlocal last | |
if title is not None: | |
print(title,ticks_diff(ticks_ms(), last),'ms') | |
last = ticks_ms() | |
p = Pin(15) | |
pix = Pixels(p, 60) | |
rainbow = [[126 , 1 , 0],[114 , 13 , 0],[102 , 25 , 0],[90 , 37 , 0],[78 , 49 , 0],[66 , 61 , 0],[54 , 73 , 0],[42 , 85 , 0],[30 , 97 , 0],[18 , 109 , 0],[6 , 121 , 0],[0 , 122 , 5],[0 , 110 , 17],[0 , 98 , 29],[0 , 86 , 41],[0 , 74 , 53],[0 , 62 , 65],[0 , 50 , 77],[0 , 38 , 89],[0 , 26 , 101],[0 , 14 , 113],[0 , 2 , 125],[9 , 0 , 118],[21 , 0 , 106],[33 , 0 , 94],[45 , 0 , 82],[57 , 0 , 70],[69 , 0 , 58],[81 , 0 , 46],[93 , 0 , 34],[105 , 0 , 22],[117 , 0 , 10]] | |
while True: | |
rainbow = rainbow[-1:] + rainbow[:-1] | |
pix[:] = rainbow | |
delta('update') | |
pix.write() | |
delta('write') | |
You've both touched on a very obvious point which is that RMT is a memory hog. I'll try @fstengel's fix, every bit of saving counts. Best case scenario the entire buffer wouldn't need to be generated ahead of time, instead it should be shifted in as its consumed. That's what FastLED does, and any sane implementation should do the same. For now, I can't do that with the MicroPython implementation of RMT.
I don't think you will ever be able to do that with Python. For 150 pixels the calculation of pulses with @fstengels generator expression took 50 ms, so a live calculation is not an option. Maybe you can still write it a little bit faster (I guess the expression does a lot of copying and no preallocation), but I don't think you can keep up with the RMT. I outsourced the pulse calculation to a C module because of that.
In-time pulse calculation is done for example in the FastLED library - but using this implementation breaks the current RMT implementation, because it defines its own interrupt handlers. However this is the only way to ever have a memory-efficient and fast driver. I'm working with PSRAM, so memory is not a problem for me...
My intent was still to have all the colours in a python buffer before writing, but this can be made significantly smaller as they can be compacted in a bytearray as just RGB values and not rmt items.
It would then just be memory copying while streaming. I do not know if it will work though. I'm just hopeful.
I couldn't get rid of timing glitches when using the current RMT implementation. I don't know why, but apparently the interrupt copying from buffer to RMT memory couldn't catch up all the time. Increasing the RMT memory helped a bit but couldn't resolve it completely.
I ended up "porting" the FastLED driver to C. Haven't had any glitches so far. It's blazingly fast and uses very little RAM. You can do animations with 200 Hz refresh rate if you get the rendering done in time...
Can you please share the final firmware you are integrating in esp32 in which you have ported FastLed driver to C.
(Micropython Firmware)
@tech-shubham
You can find it here: https://github.com/carstenblt/micropython
Some details on how to enable it and why the code looks like crap: micropython/micropython#5623 (comment)
In my own codebase I modified it a bit further and let it use the full RMT memory (therefore you can only use 1 RMT instead of 8 - FastLED had the ability to run several neopixel strips in parallel, but I never fully implemented that anyways). I had another glitch after several hours of running but that modification should make it even more stable.
Thank you so much for your support.
The thing is I just need a favour if you can compile it and share with me .bin file as I have my windows laptop and somehow its not compiling through esp idf tools.
If you share the .bin file then I can directly put it into my esp32 board.
Your efforts are much appreciated, thanks.
Can you please share the compiled .bin file which I can use it as a bootloader.
Makefile shows multiple problem while compiling. It shows this error and number of more errors.
"Makefile:427: *** target pattern contains no `%'. Stop."
Kindly Please help out.
I did able to compile and run finally.
But still the problem remains, in my code I am running it for 64 Leds and there is still random flickering.
But I appreciate your efforts.
@tech-shubham Are you sure you are using neopixel2? I did not touch the original neopixel library.
Yes, I miss something previously.
But,Thanks @carstenblt now its working just fine.
Just a problem is, we can't run two led strips simultaneously with this update.
Yep... The interrupt handler could handle multiple strips, it would just need some more work.
Well, I will be waiting for your next update. Kindly do tell me when its done.
Thanks!
Does your micropython Firmware: https://github.com/carstenblt/micropython
Supports esp32 + psRam, I mean to ask either we have to change some settings or does it support external 4MB(or above) SPIRAM.
Thanks for this, solved my flickering neopixels on ESP32.
@tech-shubham, @vjdw Any chance that you can share a compiled version of the firmware that you got working? 🙏 Or even better, share instructions of what changes are necessary to build...
Sorry @martinjo, I'm only using a short strip and so all I needed was the original pixels.py
gist. Maybe @tech-shubham can help with the firmware.
'Yes', the calculation on integer numbers is correct, and each integer takes 2 bytes: According to RMT's documentation (v4.0-rc was used since 'latest' version seemed to have that image removed), each bit in LED strip data structure will have to consume a whopping 32 bits(4 bytes) of memory. If programs were to construct a full RMT-compatible array and output it(hopefully in daemon), the memory usage for such a 60-LED-RGB strip would be 5760 bytes(and for a 300-GRBW strip which I'm building, unfortunately, 38400bytes), still kind of high.
But anyway, hopes that more experienced developers could come out with some solutions balancing the memory and execution time, or something smaller than
rmt_item32_t
could be implemented.