I'll just walk through various ways I played with doing this, as I learned some nice things. Intuitively, I started by thinking of modifying the code at the end of the last post:
void loop() { for(int dot = 0; dot < NUM_LEDS; dot++) { leds[dot] = CRGB::Blue; FastLED.show(); // clear this led for the next time around the loop leds[dot] = CRGB::Black; delay(30); } for(int dot = 0; dot < NUM_LEDS; dot++) { leds[NUM_LEDS - dot] = CRGB::Blue; FastLED.show(); // clear this led for the next time around the loop leds[NUM_LEDS - dot] = CRGB::Black; delay(30); } }
If we wanted the same dot, but with a trail, what are we saying? Let's look at what the array will hold, imagining that the moving dot has a brightness of 3, and the "trail" decreases in brightness behind it. Our time sequence might look like this:
|---+---+---+---+---+---+---+---+---| | 3 | | | | | | | | | |---+---+---+---+---+---+---+---+---| |---+---+---+---+---+---+---+---+---| | 2 | 3 | | | | | | | | |---+---+---+---+---+---+---+---+---| |---+---+---+---+---+---+---+---+---| | 1 | 2 | 3 | | | | | | | |---+---+---+---+---+---+---+---+---| |---+---+---+---+---+---+---+---+---| | | 1 | 2 | 3 | | | | | | |---+---+---+---+---+---+---+---+---| |---+---+---+---+---+---+---+---+---| | | | 1 | 2 | 3 | | | | | |---+---+---+---+---+---+---+---+---| |---+---+---+---+---+---+---+---+---| | | | | 1 | 2 | 3 | | | | |---+---+---+---+---+---+---+---+---|
We need some way to identify our initial dot location, set that dot to full bright, and in the next frame, decrease its brightness by some amount while setting the next pixel to full on, and so on. Intuitively, I started by thinking of something like this:
int lead_dot = 0; void loop() { for(int dot = 0; dot < NUM_LEDS; dot++) { leds[dot].b = leds[dot].b - 80; if(leds[dot].b < 0) { leds[dot].b = 0; } } leds[lead_dot].b = 255; lead_dot = lead_dot + 1; if(lead_dot == NUM_LEDS) { lead_dot = 0; } FastLED.show(); delay(50); }
That might seem reasonable, or at least it did to me, but it doesn't work at all! I used a
Serial.print()
statement to look at the value of leds[dot].b
in the for
loop to see
what was going on, and noticed that the values for .b
never go negative! So my check for
a negative value never resulted in a poitive hit and the leds just stayed blue with a
little bit of flickering. It was a good lesson learned; if you wanted to know if you were
subtracting too much, the time to check is before you do the subtraction; otherwise the
value will roll over. Here was my updated method after figuring that out:int lead_dot = 0; void loop() { for(int dot = 0; dot < NUM_LEDS; dot++)
{
if(leds[dot].b > 80) { leds[dot].b = leds[dot].b - 80; } else { leds[dot].b = 0; } } leds[lead_dot].b = 255; lead_dot = lead_dot + 1; if(lead_dot == NUM_LEDS) { lead_dot = 0; } FastLED.show(); delay(50); }
That works pretty slick! As we go down the line, we simply subtract 80 (~1/3) of the full bright value for the blue channel if we know we'll get a desired result, otherwise we just set it to 0. Then we set the leading pixel full blue, set it to use the next pixel on the following loop, make sure we roll back to 0 at the end of the strip, and push the data.
What if you wanted to make the tail longer or shorter? You'd change the 80 value above to subtract to something else, taking more/less steps to reach 0. One function I stumbled on in the FastLED wiki was
fadeToBlackBy()
. That sounds perfect. You just pass it a value
in the form of n/256
and it fades the brightness by that amount. We can use this method,
now:int lead_dot = 0; void loop() {
for(int dot = 0; dot < NUM_LEDS; dot++) {
leds[dot].fadeToBlackBy(64);
} leds[lead_dot].b = 255; lead_dot = lead_dot + 1; if(lead_dot == NUM_LEDS) { lead_dot = 0; } FastLED.show(); delay(50); }
Now we don't have to do any math in the
for
loop to make sure we're actually dimming the
trailing pixels vs. rolling over. When fiddling with the first code, I wasn't checking to
see if lead_dot
had reached the end of the strip (I didn't have the if()
statement
shown above). I got really weird results and posted to the FastLED Google+ community for
help. I got some great tips from Andrew Tuline, which I started playing with. Andrew's
comment was:
I hate counting up and counting down a strip in order to move pixels. I also hate that about as much as using delays (especially when I've got button polling routines to run).He gave a modified example using the functions
beatsin8()
and EVERY_N_MILLISECONDS
, so
I looked into those. EVERY_N_MILLISECONDS
is a way to run something every nth
millisecond without using the delay()
function. The effect is the same,
but it uses a check to the clock (elapsed milliseconds) to determine if it should run the
code or not. This is different than delay which literally instructs the processor to sit
there idly for n
milliseconds. This is important if you're reading a sensor in your
project. An Arduino won't be able to read the sensor during delay()
phases, so your
sampling rate will go way down. Encoders are a good example of where you can't deal with delays since
you have to constantly check the pin signals.
In any case, you basically just wrap whatever you want to run periodically inside of
EVERY_N_MILLISECONS(N) { ... }
to accomplish the same as delay(n)
:
EVERY_N_MILLISECONDS(50) { for(int dot = 0; dot < NUM_LEDS; dot++) { leds[dot].fadeToBlackBy(64); } leds[lead_dot].b = 255; lead_dot = lead_dot + 1; if(lead_dot == NUM_LEDS) { lead_dot = 0; } FastLED.show(); }
Andrew also posted code using a different form of
fadeToBlackBy
, namely
fadeToBlackBy(leds, NUM_LEDS, amount_to_fade)
. Interesting! So the function can act on an entire
array and on n
LEDS, fading by some value. Now we don't even need a for
loop at all:EVERY_N_MILLISECONDS(50) { fadeToBlackBy(leds, NUM_LEDS, 64); leds[lead_dot].b = 255; lead_dot = lead_dot + 1; if(lead_dot == NUM_LEDS) { lead_dot = 0; } FastLED.show(); }
What if we want the dot to go back and forth? I though of moving to an increment that could go positive or negative once we reached the end of the strip, like this:
int delta = 1; ... void loop() { EVERY_N_MILLISECONDS(50) { fadeToBlackBy(leds, NUM_LEDS, 64); leds[lead_dot].b = 255; lead_dot = lead_dot + delta; if(lead_dot == NUM_LEDS | lead_dot == 0) { delta = -delta; } FastLED.show(); } }
That works, though Andrew's comments pointed me in another direction. He used a function called
beatsin8()
, so I had a look at that in the
FastLED 3.1 docs and found this definition:LIB8STATIC uint8_t beatsin8 ( accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0 ) beatsin8 generates an 8-bit sine wave at a given BPM, that oscillates within a
given range.
So, sounds like we provide a beats per minute, a range for the output values, and it returns the proper number to oscillate between that range at the desired rate. Let's have a go!
int bpm = 40; ... void loop()
{ leds[lead_dot].b = 255; lead_dot = beatsin8(bpm, 0, NUM_LEDS); FastLED.show(); EVERY_N_MILLISECONDS(20) { fadeToBlackBy(leds, NUM_LEDS, 64); } }
The speed is definitely not the same (but could easily be tweaked by changing
bpm
), but
the effect is perfect! Can we see what's going
on? How about we collect the value of lead_dot
every millisecond to see how it's
achieving a sin wave with a frequency of 1/40 sec (40 BPM was passed to the
function). Doing that yielded this plot:
So as the loop runs, it's simply holding
lead_dot
for different amounts of time before
incrementing/decrementing in order to approximate a sin wave. Pretty nifty! How about our
fading? I tried to collect data on all of the LEDs at each update, but it appears that the
Serial.print()
command might be affecting the timing of the operations. The lights were
very spread out and "glitchy." From asking on the Google+ community, apparently printing
takes a lot of time. I just resolved to collect every 5 milliseconds and deal with the
fact that the lights wouldn't actually work but hoped the data would show reasonably well what's
going on:
void loop() { lead_dot = beatsin8(bpm, 0, NUM_LEDS); leds[lead_dot].b = 255; EVERY_N_MILLISECONDS(5) { fadeToBlackBy(leds, NUM_LEDS, 64); FastLED.show(); int now = millis(); for(int i = 0; i < NUM_LEDS; i++) { Serial.print(i); Serial.print(","); Serial.print(leds[i].b); Serial.print(","); Serial.println(now); } } }
And here's what you get (I narrowed it down to just the first pixels):
We can see each pixel spike to 255 and then dim down. Milliseconds later, the pixel that peaked is much lower and the subsequent one is being set to 255. Let's zoom in on just one peak:
I suspect I'm not capturing the data fast enough, or it might be how
beatsin8
works, but
some of the LEDs don't seem to appear on both sides (they should light up as lead_led
returns from the far end, passes through them to the first, and then through them
again. It's also possible that based on my ampling rate, somehow the colors have identical
values and the lines are simply on top of each other. In any case, I thought it was pretty
cool to be able to see the overlapping peaks/trailing values.
To make it more representative of a strand of LEDs, I arranged the data into a tile plot. Think of each column as the LED strip at a given time. Moving from left to right, we can watch the state of the set brightness:
Again, I think there's something wrong with the timing and what printing is doing since the transitions are a bit irregular and several pixels appear to jump to 255 at the same time, but the gist is definitely there! We get a brightness spike that propagates down the line and fades out. Nifty.
Looking around at the other fast math functions offered by FastLED, I noticed
cubicwave8
, quadwave8
, triwave8
, and sin8
. We can try those out, but need to set
the angle and map it to my led strip:
int lead_dot = 0; int angle = 0; void loop() { EVERY_N_MILLISECONDS(3) { uint8_t lead_dot = map(cubicwave8(angle), 0, 255, 0, NUM_LEDS - 1); angle = angle + 1; leds[lead_dot].b = 255; fadeToBlackBy(leds, NUM_LEDS, 32); } FastLED.show(); }
Give that a whirl and substitute in thevarious waveforms to see the effect. I'll leave it there for this post – happy lighting!
you've got a missing open brace in your 3rd code example above, in the first for-loop's first line.
ReplyDeletefor(int dot = 0; dot < NUM_LEDS; dot++)
instead of
for(int dot = 0; dot < NUM_LEDS; dot++){
@anonymous: Good catch, and corrected. Found the same error in the 4th block and fixed that too. Thanks!
ReplyDelete'tis I should be thanking you. I started with Adafruit libraries and their NeoPixel products with simple loops and single animation sequences run in series but I've been wanting to implement more pizzazz, with less code, more efficient code and overlapping, more complex animations. Your blog entries helped me get up to speed faster with FastLED and I hope you will be making more and soon.
DeleteIf you can recommend other sources of information on FastLED or animation examples that demonstration more of the "undocumented" features of the library, I'd appreciate it. I've been reading over the FastLED documentation, wiki and the Google+ group already.
Thanks again and keep up the good work,
Scott
Hi Scott -- wow, thanks! Happy that this could help others, and I started on the series because of a similar request over on the Google+ group (basically to have examples of lesser documented features). I'm pretty new myself to FastLED, so just took a stab at basic stuff.
DeleteFeel free to list some other things you'd want to see and I'd be happy to try something. I know these came up in the post I'm thinking of: fadeToBlackBy, fill_rainbow/fill_gradient, EVERY_N_MILLISECONDS.
- https://plus.google.com/116225765652479729260/posts/aXGJq4WidcV
The places you listed are the only ones I know of myself. The post above suggests it's just a byproduct of a busy development team focused on features for their actual work, which leaves some documentation by the wayside for now...
Good stuff. Thanks! Implementing in my project tonight. :)
DeleteBrilliant tutorial! Excellent research!
ReplyDeleteI really appreciated your two tutorials on FastLED. It's a shame you seemed to have stopped publishing articles on your blog. I wish you success in whatever else it is you're doing!
ReplyDeleteHi Pietro: glad you liked them, and indeed... I'm sorry I haven't written more as well! Not sure where the time goes, but it's just seemed quite busy as of late.
DeleteI'd gladly take suggestions on something else you'd like to see. At one point I thought about going through some of the fill_* functions (e.g. fill_rainbow), beatsin*, or some other family. Anything you'd like to see?
Great tutorials!!!! The way you stepped through everything really helped. I've been out of C++ for over a decade and just getting back into it. This helped big time!!! I'd like to learn some of the Fill functions like (rainbow, color palette, etc) If you have any links or a direction I could head, that would be greatly appreciated!!! Keep up the awesome work!
ReplyDelete