Arduino Zero Memory issues with large numbers of LED's

I am encountering Arduino memory issues with the following project.
I am using using individually addressable LED rings for my project. Each ring has 16 LEDs. I have 63 rings (1008 leds) connected in line for my setup. I already figured out the needed power so no problems there, but there is another problem with Arduino memory. When a button is pressed, each ring (16 LED) light up, and then once you press the button again, another one lights up and so on.....

I added candle effect to each ring (16 leds), and it works pretty well if only 4-5 rings are lit, but the more rings I light, the slower the effect becomes. After turning on 15 or more rings, even the button stops responding and the entire system is unusable. Is there another way to program this to avoid this issue? When applying the candle effect, you have to constantly write to each LED in each ring to achieve the effect, so i am not sure if there is a way to somehow group it, so it is not eating up all the memory?

Yes, I'm pretty sure there is. But it's not memory that's getting eaten up. It's time, I suspect.

Let's see that code and find out.

The 1008 LEDs needs about 3K memory to work; your arduino has a 32K - so it definitely not a memory problem

#include <LiquidCrystal.h>
#include <FastLED.h>

// Define LED ring parameters
#define NUM_LEDS 1008
#define DATA_PIN 9

// Define the array of LEDs
CRGB leds[NUM_LEDS];

// Initialize the LCD with the numbers of the interface pins
LiquidCrystal lcd(7, 6, 2, 3, 4, 5); // RS, E, D4, D5, D6, D7 for communication

const int buttonPin = 8; // Pin connected to the color change button
const int randomLedButtonPin = 10; // Pin connected to the random LED button
const int potentiometerPin = A0; // Pin connected to the potentiometer for changing how long lights stay on
int buttonState = 0;
int lastButtonState = 0;
int randomLedButtonState = 0;
int lastRandomLedButtonState = 0;
int colorIndex = 0;

const char* colors[] = {"Red", "Green", "Purple", "Yellow", "White"};
const int numColors = sizeof(colors) / sizeof(colors[0]);

CRGB colorValues[] = {
  CRGB::Red,
  CRGB::Green,
  CRGB(128, 0, 128), // Purple
  CRGB::Yellow,
  CRGB::White
};

unsigned long previousMillis = 0;
unsigned long interval = 5000; // Default interval (5 seconds)

unsigned long ledOnTime[NUM_LEDS] = {0};

// Noise variables for candle effect
uint16_t x = 0; // Used for candle effect noise generation
uint16_t y = 0;
uint16_t z = 0;

void setup() {
  // Initialize the LCD
  lcd.begin(16, 2);
  lcd.setCursor(0, 0);
  lcd.print("Color: ");
  lcd.print(colors[colorIndex]);

  // Initialize the button pins as input
  pinMode(buttonPin, INPUT);
  pinMode(randomLedButtonPin, INPUT);

  // Initialize the LED lights
  FastLED.addLeds<WS2812, DATA_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(150);

  // Set initial color
  setStripColor(CRGB::Black); // Turn off all LEDs initially

  // Initialize serial communication for debugging
  Serial.begin(9600);
}

void loop() {
  // Read the state of the buttons
  buttonState = digitalRead(buttonPin);
  randomLedButtonState = digitalRead(randomLedButtonPin);

  // Check if the color change button was pressed
  if (buttonState != lastButtonState) {
    if (buttonState == HIGH) { // HIGH indicates the button is pressed
      // Change the color index
      colorIndex = (colorIndex + 1) % numColors;

      // Clear the entire line on the LCD before printing the new color
      lcd.setCursor(7, 0); // Set cursor to the first character of the color display
      lcd.print("        "); // Clear the line by printing spaces

      // Update the LCD display with the new color
      lcd.setCursor(7, 0); // Set cursor back to the first character of the color display
      lcd.print(colors[colorIndex]); // Print the new color

      // Update the LED lights
      setStripColor(colorValues[colorIndex]);
    }

    // Debounce delay
    delay(50);
  }

  // Light up all LEDs in one ring if the random LED button is pressed
  if (randomLedButtonState != lastRandomLedButtonState && randomLedButtonState == HIGH) {
    // Randomly select a ring (0 for first ring, 1 for second ring)
    int selectedRing = random(NUM_LEDS/16);

    // Calculate the start and end indices for the selected ring
    int startIdx = selectedRing * 16;
    int endIdx = startIdx + 16;

    // Check if any LED in the selected ring is already on
    bool ringIsOn = false;
    for (int i = startIdx; i < endIdx; i++) {
      if (leds[i] != CRGB::Black) {
        ringIsOn = true;
        break;
      }
    }

    // If the selected ring is already on, choose the other ring
    if (ringIsOn) {
      selectedRing = (selectedRing == 0) ? 1 : 0;
      startIdx = selectedRing * 16;
      endIdx = startIdx + 16;
    }

    // Blink all LEDs in the selected ring three times with blue
    for (int i = 0; i < 3; i++) {
      for (int j = startIdx; j < endIdx; j++) {
        leds[j] = CRGB::Blue;
      }
      FastLED.show();
      delay(500);
      for (int j = startIdx; j < endIdx; j++) {
        leds[j] = CRGB::Black;
      }
      FastLED.show();
      delay(500);
    }

    // Light up all LEDs in the selected ring with the main color
    for (int j = startIdx; j < endIdx; j++) {
      leds[j] = colorValues[colorIndex];
      ledOnTime[j] = millis(); // Save the current time for each LED
    }
    FastLED.show();
  }

  // Apply candle effect to each ring
  for (int ring = 0; ring < NUM_LEDS / 16; ring++) {
    int startIdx = ring * 16;
    int endIdx = startIdx + 16;
    applyCandleEffect(startIdx, endIdx);
  }

  // Check if the interval has elapsed since the last LED lighting
  unsigned long currentMillis = millis();
  for (int i = 0; i < NUM_LEDS; i++) {
    if (currentMillis - ledOnTime[i] >= interval) {
      leds[i] = CRGB::Black;
    }
  }
  FastLED.show();

  // Save the current state as the last state for the next loop
  lastButtonState = buttonState;
  lastRandomLedButtonState = randomLedButtonState;

  // Read potentiometer value and map it to the desired intervals
  int potValue = analogRead(potentiometerPin);
  interval = map(potValue, 250, 950, 5000, 15000); // Map potentiometer value to interval range (5-15 seconds)

  // Debug: Print potentiometer value to serial monitor
  Serial.println(potValue);

  // Update interval on LCD
  lcd.setCursor(0, 1);
  lcd.print("Interval: ");
  lcd.print(interval / 1000); // Display interval in seconds
  lcd.print("s    "); // Clear any extra characters from previous value
}

void setStripColor(CRGB color) {
  for (int i = 0; i < NUM_LEDS; i++) {
    if (leds[i] != CRGB::Black) {
      leds[i] = color;
    }
  }
  FastLED.show();
}

void applyCandleEffect(int startIdx, int endIdx) {
  for (int i = startIdx; i < endIdx; i++) {
    if (leds[i] != CRGB::Black) {
      uint8_t flicker = random8(128, 255); 
      leds[i] = colorValues[colorIndex];
      leds[i].fadeLightBy(flicker);
    }
  }
  x += 1;
  y += 1;
  z += 1;
  FastLED.show();
}

The amount of time each candle stays on is also not seconds but days. This code above was just for testing if the timer works. So the values for time range from min 1day to max 5 days.

This is the final code that i have with the candle effect completely removed because it is causing problem. If one of your guys can help with adding something that would work. Please notice that the system can have one of 4 color set (Red, Green, Purple, White), so the candle effect should just be there to dim/blink the currently set color, not change it to different color.

Code below:

#include <Wire.h>
#include <hd44780.h>                   // Main hd44780 header
#include <hd44780ioClass/hd44780_I2Cexp.h> // i2c expander i/o class header
#include <FastLED.h>

// Define LED ring parameters
#define NUM_LEDS 1008
#define DATA_PIN 9
#define NUM_RINGS (NUM_LEDS / 16)  // Number of rings

// Define the array of LEDs
CRGB leds[NUM_LEDS];

// Initialize the LCD with the I2C address 0x27 (common for most I2C LCDs)
hd44780_I2Cexp lcd; // Declare lcd object

const int buttonPin = 8; // Pin connected to the color change button
const int randomLedButtonPin = 10; // Pin connected to the random LED button
const int durationButtonPin = 11; // Pin connected to the duration change button

int buttonState = HIGH; // Default to HIGH (input_pullup)
int lastButtonState = HIGH;
int randomLedButtonState = HIGH; // Default to HIGH (input_pullup)
int lastRandomLedButtonState = HIGH;
int durationButtonState = HIGH; // Default to HIGH (input_pullup)
int lastDurationButtonState = HIGH;
int colorIndex = 0;

const char* colors[] = {"RED", "GREEN", "PURPLE", "WHITE"};
const int numColors = sizeof(colors) / sizeof(colors[0]);

CRGB colorValues[] = {
  CRGB::Red,
  CRGB::Green,
  CRGB(255, 0, 255), // Darker Purple
  CRGB::White
};

unsigned long previousMillis = 0;
unsigned long interval = 86400000; // Default interval (1 day)

unsigned long ledOnTime[NUM_LEDS] = {0};

// Boolean array to keep track of whether each ring is lit
bool ringsLit[NUM_RINGS] = {false};

// Array to store ring indices and current index for picking
int ringIndices[NUM_RINGS];
int currentRingIndex = 0;

// Duration options in milliseconds
unsigned long durationOptions[] = {86400000, 172800000, 259200000, 345600000, 432000000}; // 1, 2, 3, 4, 5 days
int durationIndex = 0; // Index for the current duration option

// Debounce variables
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50; // Debounce time in milliseconds
unsigned long lastRandomLedDebounceTime = 0;
unsigned long lastDurationDebounceTime = 0;

void setup() {
  // Initialize the LCD
  lcd.begin(16, 2); // Initialize with the correct number of columns and rows
  lcd.setBacklight(1);
  lcd.setCursor(0, 0);
  lcd.print("Color: ");
  lcd.print(colors[colorIndex]);

  // Initialize the button pins as input pullups
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(randomLedButtonPin, INPUT_PULLUP);
  pinMode(durationButtonPin, INPUT_PULLUP);

  // Initialize the LED lights
  FastLED.addLeds<WS2812, DATA_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(75);

  // Set initial color
  setStripColor(CRGB::Black); // Turn off all LEDs initially

  // Initialize serial communication for debugging
  Serial.begin(9600);

  // Initialize ring indices
  for (int i = 0; i < NUM_RINGS; i++) {
    ringIndices[i] = i;
  }
  shuffleRingIndices(); // Shuffle the ring indices

  // Display initial interval on LCD
  lcd.setCursor(0, 1);
  lcd.print("Interval: ");
  lcd.print(durationOptions[durationIndex] / 86400000); // Display interval in days
  lcd.print(" DAYS");
}

void loop() {
  // Read the state of the buttons
  int reading = digitalRead(buttonPin);
  int randomLedReading = digitalRead(randomLedButtonPin);
  int durationReading = digitalRead(durationButtonPin);

  // Debounce the color change button
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != buttonState) {
      buttonState = reading;

      if (buttonState == LOW) { // Button is pressed
        // Change the color index
        colorIndex = (colorIndex + 1) % numColors;

        // Clear the entire line on the LCD before printing the new color
        lcd.setCursor(7, 0); // Set cursor to the first character of the color display
        lcd.print("        "); // Clear the line by printing spaces

        // Update the LCD display with the new color
        lcd.setCursor(7, 0); // Set cursor back to the first character of the color display
        lcd.print(colors[colorIndex]); // Print the new color

        // Update the LED lights
        setStripColor(colorValues[colorIndex]);
      }
    }
  }

  // Debounce the random LED button
  if (randomLedReading != lastRandomLedButtonState) {
    lastRandomLedDebounceTime = millis();
  }

  if ((millis() - lastRandomLedDebounceTime) > debounceDelay) {
    if (randomLedReading != randomLedButtonState) {
      randomLedButtonState = randomLedReading;

      if (randomLedButtonState == LOW) { // Button is pressed
        bool allRingsLit = true;

        // Check if any ring is not lit
        for (int i = 0; i < NUM_RINGS; i++) {
          if (!ringsLit[i]) {
            allRingsLit = false;
            break;
          }
        }

        // Only proceed if not all rings are lit
        if (!allRingsLit) {
          // Attempt to find an unlit ring by using the shuffled indices
          for (int i = 0; i < NUM_RINGS; i++) {
            int selectedRing = ringIndices[currentRingIndex];
            currentRingIndex = (currentRingIndex + 1) % NUM_RINGS;

            // If the ring is not lit, proceed to light it
            if (!ringsLit[selectedRing]) {
              int startIdx = selectedRing * 16;
              int endIdx = startIdx + 16;

              // Blink all LEDs in the selected ring three times with blue
              for (int i = 0; i < 3; i++) {
                for (int j = startIdx; j < endIdx; j++) {
                  leds[j] = CRGB::Blue;
                }
                FastLED.show();
                delay(500);
                for (int j = startIdx; j < endIdx; j++) {
                  leds[j] = CRGB::Black;
                }
                FastLED.show();
                delay(500);
              }

              // Light up all LEDs in the selected ring with the main color
              for (int j = startIdx; j < endIdx; j++) {
                leds[j] = colorValues[colorIndex];
                ledOnTime[j] = millis(); // Save the current time for each LED
              }
              FastLED.show();

              // Mark the ring as lit
              ringsLit[selectedRing] = true;
              break;
            }
          }
        }
      }
    }
  }

  // Debounce the duration button
  if (durationReading != lastDurationButtonState) {
    lastDurationDebounceTime = millis();
  }

  if ((millis() - lastDurationDebounceTime) > debounceDelay) {
    if (durationReading != durationButtonState) {
      durationButtonState = durationReading;

      if (durationButtonState == LOW) { // Button is pressed
        // Change the duration index
        durationIndex = (durationIndex + 1) % 5;
        interval = durationOptions[durationIndex];

        // Update the interval display on the LCD
        lcd.setCursor(0, 1);
        lcd.print("Interval: ");
        lcd.print(durationOptions[durationIndex] / 86400000); // Display interval in days
        lcd.print(" DAYS");
      }
    }
  }

  // Check if the interval has elapsed since the last LED lighting
  unsigned long currentMillis = millis();
  for (int i = 0; i < NUM_LEDS; i++) {
    if (leds[i] != CRGB::Black && currentMillis - ledOnTime[i] >= interval) {
      leds[i] = CRGB::Black;
      int ringIndex = i / 16;
      ringsLit[ringIndex] = false;  // Mark the ring as not lit
    }
  }
  FastLED.show();

  // Save the current state as the last state for the next loop
  lastButtonState = reading;
  lastRandomLedButtonState = randomLedReading;
  lastDurationButtonState = durationReading;
}

void setStripColor(CRGB color) {
  for (int i = 0; i < NUM_LEDS; i++) {
    if (leds[i] != CRGB::Black) {
      leds[i] = color;
    }
  }
  FastLED.show();
}

void shuffleRingIndices() {
  for (int i = NUM_RINGS - 1; i > 0; i--) {
    int j = random(0, i + 1);
    int temp = ringIndices[i];
    ringIndices[i] = ringIndices[j];
    ringIndices[j] = temp;
  }
}

FastLED.show() for your leds lasts about 30 ms. You do it every time you call the applyCandleEffect() function. So applying a candle effect to all 63 rings need about 2 seconds for EACH STEP of changing.

Try do not apply FastLED.show() after changing each rings, do it once after all rings changed.

b707 please also look at the latest final code I posted with the candle effect removed, so it's not causing slowness problem.

What for? Do you have any problem with it?

I looked your code with candle effect and suggest you a way to solution. But you decided to remove effect at all. So it means that I wasted my time?

Really!!! I wasted your time by asking for help. I posted the second final code so you can see what it is trying to do and i stated

"This is the final code that i have with the candle effect completely removed because it is causing problem. If one of your guys can help with adding something that would work."

If you think i wasted your time maybe you should just not comment at all instead to giving me"(expletive deleted) comments"

You asked for help with the candle effect being too slow. And when I suggested to you how to fix this problem, you simply threw the effect out of the code, thereby devaluing my efforts.
And now you expect me to waste time on your new code? - no thanks, try it yourself this time.

I'm confused, resting state but also by this comment

// If the selected ring is already on, choose the other ring

I thought there were 16 rings.

Can you remove the candle effect a bit less aggressively, that is to say just take the original code and just // comment out the call to applyCandleEffect()?

When you say the button(s) stop working, is that literal or are you saying they are sluggish, unresponsive?

You've got the serial monitor, crank up the baud rate to something your grandfather could only have dreamed of, like 115200.

Then out some printing in there to see, for example, tunings like whether the code is even reading the buttons, and if so how frequently.

I haven't begun to look because I see some eyes are already on this deeper than I can get just now, but if it literally goes south, you may somewhere be writing off the end of the pixel buffer array, which would mean your sketch could do almost anything and be be blameless for doing.

I also haven't looked at how hard it wou,d be, but you could try 16 rings of, say, 5 pixels, no matter how that looked, and see if you have the same issues.

a7

This looks weird, talk about it to me. I'm a bit baked, TBH.

    int selectedRing = random(NUM_LEDS/16);

    // Calculate the start and end indices for the selected ring
    int startIdx = selectedRing * 16;
    int endIdx = startIdx + 16;

This looks like 63 rings of 16 numbered 0 to 62, amIRight?

a7

This applies even without the "candle effect."
Update ALL of the ring display values, and then do ONE "show" per loop(), and you might have enough FPS to add the candle back in.
In theory, 30ms per update is better than 30 frames/sec, which should be plenty to be pretty smooth.

  • You were warned !

:sunglasses:

The pot goes from 0 to 1023. map() will extrapolate.

int potValue = analogRead(potentiometerPin);
  interval = map(potValue, 250, 950, 5000, 15000); // Map potentiometer value to interval range (5-15 seconds)

so you will get (too lazy to do the maths) a wider range than the comment suggests.

I am agreeing with @b707 and @westfw, this really calls for a once-per-loop output to all LEDs.

The "busy, select another" logic does not exist. If N = 0 is busy, 1 is selected. If N != 0 is busy, 0 is selected. Is that what you wanted? I was looking for a "find an free ring", which would be kinda complicated in this sketch right now. I would maintain the rings with some kind of data structure, busy, not, age; you'd save memory becuase all the LEDs in a ring are da same age. Pluis a few details.

@LarryD different kind of baked, so.

a7

The above can be written like the rest of your colorValues:

  CRGB::Purple, // 128, 0, 128