Skip to content

Instantly share code, notes, and snippets.

@nevercast
Last active November 20, 2022 17:34
Show Gist options
  • Save nevercast/9c48505cc6c5687af59bcb4a22062795 to your computer and use it in GitHub Desktop.
Save nevercast/9c48505cc6c5687af59bcb4a22062795 to your computer and use it in GitHub Desktop.
Simple MicroPython ESP32 RMT NeoPixel / WS2812B driver.
# 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')
@nevercast
Copy link
Author

nevercast commented Feb 4, 2020

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.

@carstenblt
Copy link

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...

@nevercast
Copy link
Author

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.

@carstenblt
Copy link

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...

@tech-shubham
Copy link

@carstenblt

Can you please share the final firmware you are integrating in esp32 in which you have ported FastLed driver to C.
(Micropython Firmware)

@carstenblt
Copy link

@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.

@tech-shubham
Copy link

@carstenblt

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.

@tech-shubham
Copy link

@carstenblt

Can you please share the compiled .bin file which I can use it as a bootloader.

@tech-shubham
Copy link

@carstenblt

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.

@tech-shubham
Copy link

@carstenblt

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.

@carstenblt
Copy link

@tech-shubham Are you sure you are using neopixel2? I did not touch the original neopixel library.

@tech-shubham
Copy link

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.

@carstenblt
Copy link

Yep... The interrupt handler could handle multiple strips, it would just need some more work.

@tech-shubham
Copy link

Well, I will be waiting for your next update. Kindly do tell me when its done.

Thanks!

@tech-shubham
Copy link

@carstenblt

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.

@vjdw
Copy link

vjdw commented May 15, 2020

Thanks for this, solved my flickering neopixels on ESP32.

@martinjo
Copy link

@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...

@vjdw
Copy link

vjdw commented Jun 21, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment