cookbook/core-concepts/timing.md
Difficulty Level: ⭐⭐ Beginner to Intermediate Time to Complete: 35-45 minutes (reading and experimenting with different timing approaches) Prerequisites:
You'll Learn:
Control animation speed and timing in FastLED.
The simplest timing method using delay():
void loop() {
updateLEDs();
FastLED.show();
delay(20); // ~50 FPS (20ms between frames)
}
Pros: Simple, easy to understand Cons: Blocks code execution - nothing else can run during delay
Frame rate (FPS) = 1000ms / delay_time
Examples:
- delay(20) = 50 FPS
- delay(10) = 100 FPS
- delay(50) = 20 FPS
- delay(100) = 10 FPS
Non-blocking timing using FastLED's built-in macro:
void loop() {
EVERY_N_MILLISECONDS(20) { // 50 FPS
updateLEDs();
FastLED.show();
}
// Other code can run here
checkButtons();
readSensors();
}
Pros: Non-blocking, other code can run Cons: Fixed interval, less precise than custom timing
For slower updates:
void loop() {
EVERY_N_SECONDS(1) {
Serial.println("One second elapsed");
}
EVERY_N_MILLISECONDS(20) {
updateLEDs();
FastLED.show();
}
}
Full control using millis():
uint32_t lastUpdate = 0;
const uint32_t updateInterval = 20; // 20ms = 50 FPS
void loop() {
uint32_t now = millis();
if (now - lastUpdate >= updateInterval) {
lastUpdate = now;
updateLEDs();
FastLED.show();
}
// Other code runs every loop
checkButtons();
}
Pros: Non-blocking, precise control Cons: More code to manage
Run different effects at different speeds:
uint32_t lastLEDUpdate = 0;
uint32_t lastSerialUpdate = 0;
const uint32_t LED_INTERVAL = 20; // 50 FPS
const uint32_t SERIAL_INTERVAL = 1000; // 1 Hz
void loop() {
uint32_t now = millis();
// Update LEDs at 50 FPS
if (now - lastLEDUpdate >= LED_INTERVAL) {
lastLEDUpdate = now;
updateLEDs();
FastLED.show();
}
// Print status at 1 Hz
if (now - lastSerialUpdate >= SERIAL_INTERVAL) {
lastSerialUpdate = now;
Serial.println("Status update");
}
}
FastLED provides its own delay function with built-in dithering:
void loop() {
updateLEDs();
FastLED.show();
FastLED.delay(20); // Like delay() but maintains dithering
}
Use FastLED.delay() instead of delay() for better color rendering.
Track actual FPS:
void loop() {
updateLEDs();
FastLED.show();
EVERY_N_SECONDS(1) {
uint16_t fps = FastLED.getFPS();
Serial.print("FPS: ");
Serial.println(fps);
}
delay(20);
}
Make animation speed configurable:
uint8_t animationSpeed = 10; // Adjustable speed
void loop() {
EVERY_N_MILLISECONDS(20) {
static uint8_t hue = 0;
fill_rainbow(leds, NUM_LEDS, hue, 7);
FastLED.show();
hue += animationSpeed; // Speed controls how fast hue changes
}
}
Use millis() directly for smooth effects:
void loop() {
uint32_t now = millis();
for (int i = 0; i < NUM_LEDS; i++) {
// Create time-based wave
uint8_t brightness = sin8(now / 10 + i * 20);
leds[i] = CHSV(160, 255, brightness);
}
FastLED.show();
}
Use FastLED's beat functions:
void loop() {
// Sawtooth wave (0-255 repeating)
uint8_t beat = beat8(60); // 60 BPM
// Sine wave (0-255 oscillating)
uint8_t sineBeat = beatsin8(60, 0, 255);
// Apply to LEDs
fill_solid(leds, NUM_LEDS, CHSV(beat, 255, 255));
FastLED.show();
}
millis() overflows after ~49 days. Handle gracefully:
// GOOD: Subtraction handles overflow correctly
uint32_t now = millis();
if (now - lastUpdate >= interval) {
// This works even after overflow
}
// BAD: Direct comparison fails after overflow
if (millis() >= lastUpdate + interval) {
// Don't use this pattern
}
Some operations can interfere with LED timing:
void loop() {
// Disable interrupts during critical LED update
noInterrupts();
FastLED.show();
interrupts();
// Other code
}
On ESP32, WiFi can interfere. Consider disabling WiFi during FastLED.show().
Don't update faster than necessary:
// WASTEFUL: 1000+ FPS (updates too fast)
void loop() {
updateLEDs();
FastLED.show(); // No delay
}
// BETTER: 50 FPS is plenty
void loop() {
EVERY_N_MILLISECONDS(20) {
updateLEDs();
FastLED.show();
}
}
void loop() {
EVERY_N_MILLISECONDS(20) {
// Only call show() when needed
FastLED.show();
}
// Calculate every loop (faster)
updateLEDs();
}
void fadeInOut() {
static uint8_t brightness = 0;
static int8_t direction = 1;
EVERY_N_MILLISECONDS(10) {
brightness += direction;
if (brightness == 0 || brightness == 255) {
direction = -direction;
}
FastLED.setBrightness(brightness);
fill_solid(leds, NUM_LEDS, CRGB::Purple);
FastLED.show();
}
}
uint8_t chaseSpeed = 50; // ms between steps
void chase() {
static uint8_t pos = 0;
static uint32_t lastMove = 0;
uint32_t now = millis();
if (now - lastMove >= chaseSpeed) {
lastMove = now;
FastLED.clear();
leds[pos] = CRGB::Red;
FastLED.show();
pos = (pos + 1) % NUM_LEDS;
}
}
void multiSpeed() {
static uint8_t hue1 = 0;
static uint8_t hue2 = 128;
// Fast effect (every 10ms)
EVERY_N_MILLISECONDS(10) {
leds[0] = CHSV(hue1, 255, 255);
hue1 += 2;
}
// Slow effect (every 50ms)
EVERY_N_MILLISECONDS(50) {
leds[NUM_LEDS - 1] = CHSV(hue2, 255, 255);
hue2++;
}
// Update display at fixed rate
EVERY_N_MILLISECONDS(20) {
FastLED.show();
}
}