Working with arrays in Arduino environments is where most hobbyist code graduates from blinking LEDs to actual data processing. But arrays are also the number one source of silent memory corruption, random reboots, and hard crashes in embedded C++. Unlike desktop environments, microcontrollers lack an operating system to catch out-of-bounds memory access. If you write past the end of an array, you overwrite critical system variables, the stack, or hardware registers.

This guide cuts through the abstraction. We will map out exactly where to store your arrays, build a 4-channel rolling-average sensor logger, and debug the exact panic strings that occur when array bounds are violated.

The Decision Tree: Where Should Your Arrays Live?

Before you declare int myData[100];, you must decide which memory pool will hold it. The ESP32 and AVR architectures segment memory into SRAM (volatile, fast), Flash/PROGMEM (non-volatile, read-only at runtime), and EEPROM (non-volatile, slow write). Putting a static lookup table in SRAM wastes precious RAM; putting a live sensor buffer in Flash will cause a compile error or a crash.

Use this decision matrix to terminate your memory selection process:

Data Characteristic Memory Pool Declaration Syntax (ESP32/AVR) Concrete Pick
Static lookup tables (e.g., thermistor coefficients, sine waves) Flash (PROGMEM) const float lookup[] PROGMEM = {...}; PROGMEM: Use for any array that never changes after compilation.
Live sensor buffers, rolling averages, state machines SRAM float buffer[SIZE]; SRAM: Use for data that is read/written in the main loop.
Calibration offsets, user settings, boot counters EEPROM / NVS EEPROM.put(addr, val); EEPROM: Use only for data that must survive a power cycle.
Massive buffers (audio, large images) > 50KB PSRAM (ESP32 only) ps_malloc(size); PSRAM: Use when SRAM (typically 320KB-520KB) is exhausted.
Callout Tip: The sizeof() Trap
When you pass an array to a function in C++, it decays into a pointer. If you run sizeof(myArray) inside the function, it returns the size of the pointer (4 bytes on AVR, 8 bytes on ESP32), not the array length. Always pass the array length as a separate size_t argument.

Project Build: 4-Channel Thermistor Array Logger

To demonstrate proper array handling, bounds checking, and memory management, we will build a 4-channel temperature logger. It reads four NTC thermistors, stores the last 10 readings in a 2D SRAM array, calculates a rolling average, and displays the results on an I2C OLED.

Target Board: ESP32-WROOM-32 DevKit V1 (Programmed via Arduino IDE with ESP32 Core 3.x).
Difficulty Rating: Intermediate | Time: 45 Minutes

Parts List

  • 1x ESP32-WROOM-32 DevKit V1 (30-pin or 38-pin variant)
  • 4x 10K NTC Thermistors (B-value 3950)
  • 4x 10K 1% Tolerance Pull-up Resistors (for voltage divider)
  • 1x 0.96" I2C OLED Display (SSD1306 driver, 128x64)
  • 1x Half-size breadboard and jumper wires

Pin Mapping Table

Component ESP32 GPIO Pin Notes
Thermistor 1 (Analog) GPIO 36 (VP) ADC1 channel, safe to use with WiFi
Thermistor 2 (Analog) GPIO 39 (VN) ADC1 channel
Thermistor 3 (Analog) GPIO 34 ADC1 channel
Thermistor 4 (Analog) GPIO 35 ADC1 channel
OLED SDA GPIO 21 Default I2C SDA
OLED SCL GPIO 22 Default I2C SCL

Complete Compilable Code with Bounds Checking

This code explicitly defines array sizes using const size_t and implements strict bounds checking before every array write. It targets the ESP32 DevKit V1. Ensure you have the Adafruit_SSD1306 and Adafruit_GFX libraries installed via the Library Manager.

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// --- PIN DEFINITIONS ---
const int THERM_PINS[4] = {36, 39, 34, 35};
const int NUM_CHANNELS = 4;

// --- ARRAY SIZING & MEMORY ---
// Using const size_t prevents magic numbers and ensures type safety
const size_t BUFFER_SIZE = 10; 
float tempHistory[NUM_CHANNELS][BUFFER_SIZE];
size_t historyIndex = 0;

// --- DISPLAY CONFIG ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// --- STEINHART-HART COEFFICIENTS (Stored in Flash/PROGMEM) ---
// These never change, so we save SRAM by putting them in PROGMEM
const float PROGMEM SH_A = 1.009249522e-03;
const float PROGMEM SH_B = 2.378405444e-04;
const float PROGMEM SH_C = 2.019202697e-07;

float calculateTemp(int rawADC) {
  if (rawADC <= 0 || rawADC >= 4095) return -999.0; // Error handling for ADC saturation
  
  float resistance = 10000.0 * ((4095.0 / rawADC) - 1.0);
  float logR = log(resistance);
  
  // Read from PROGMEM using pgm_read_float (Required for AVR, good practice for ESP32)
  float A = pgm_read_float(&SH_A);
  float B = pgm_read_float(&SH_B);
  float C = pgm_read_float(&SH_C);
  
  float tempK = 1.0 / (A + B * logR + C * logR * logR * logR);
  return tempK - 273.15; // Convert Kelvin to Celsius
}

void setup() {
  Serial.begin(115200);
  
  // Error handling: Halt if display fails to initialize
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed. Halting."));
    while(true) { delay(1000); } 
  }
  
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  
  // Initialize the 2D array to zero
  for (int ch = 0; ch < NUM_CHANNELS; ch++) {
    for (size_t i = 0; i < BUFFER_SIZE; i++) {
      tempHistory[ch][i] = 0.0;
    }
  }
  analogReadResolution(12); // ESP32 12-bit ADC (0-4095)
}

void loop() {
  // --- BOUNDS CHECKING: Prevent index overflow ---
  if (historyIndex >= BUFFER_SIZE) {
    historyIndex = 0; // Wrap around safely
  }

  for (int ch = 0; ch < NUM_CHANNELS; ch++) {
    int raw = analogRead(THERM_PINS[ch]);
    float currentTemp = calculateTemp(raw);
    
    // Safe array write
    tempHistory[ch][historyIndex] = currentTemp;
    
    // Calculate rolling average
    float sum = 0.0;
    for (size_t i = 0; i < BUFFER_SIZE; i++) {
      sum += tempHistory[ch][i];
    }
    float avgTemp = sum / BUFFER_SIZE;
    
    // Output to Serial Plotter
    Serial.print("Ch"); Serial.print(ch); Serial.print(":");
    Serial.print(avgTemp); Serial.print("\t");
  }
  Serial.println();

  // Update OLED (Simplified for brevity)
  display.clearDisplay();
  display.setCursor(0,0);
  display.println("4-Ch Rolling Avg");
  for (int ch = 0; ch < NUM_CHANNELS; ch++) {
    display.print("T"); display.print(ch); display.print(": ");
    display.print(tempHistory[ch][historyIndex], 1);
    display.println(" C");
  }
  display.display();

  historyIndex++; // Increment AFTER the bounds check and write
  delay(500);
}

Debugging Array Crashes: Exact Errors and Ranked Causes

When you violate array boundaries on a desktop, the OS throws a segmentation fault. On a microcontroller, you overwrite adjacent memory. If you overwrite a return address on the stack, the CPU jumps to garbage memory and crashes. If you overwrite a global variable, your logic silently fails.

The First Three Things to Check When It Fails

  1. Loop Boundary Conditions: Check every for loop. Did you write i <= BUFFER_SIZE instead of i < BUFFER_SIZE? An array of size 10 has valid indices 0 through 9. Index 10 is out of bounds.
  2. Stack Size Limits: Did you declare a massive local array inside a function? The ESP32 default task stack is 8KB; the AVR ATmega328P has only 2KB of total SRAM. A local array of int data[2000]; will instantly cause a stack overflow. Move large arrays to the global scope (heap/BSS segment).
  3. Pointer Decay in Functions: Are you passing an array to a helper function and using sizeof(arr)/sizeof(arr[0]) to find its length? As noted in the tip above, this will return 1 or 2, causing your loop to terminate early or read garbage data. Pass the length explicitly.

Exact Error Strings and Ranked Causes

If you are running an ESP32 via the Arduino IDE and you write out of bounds, you will eventually trigger the hardware memory protection unit. The serial monitor will spit out this exact string:

Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.

On classic AVR boards (Uno/Nano), there is no MMU. You won't get a panic string. Instead, you will experience a silent watchdog reset or the board will lock up entirely. If you have the bootloader configured for debug, you might see stack smashing detected.

Rank Cause of LoadProhibited / Silent Reset The Fix
1 Off-by-one in 2D arrays: Accessing array[x][y] where x equals the row count. Implement the if (index >= SIZE) index = 0; wrap-around logic shown in the code above.
2 String buffer overflow: Using sprintf() to format text into a char array that is too small. Replace sprintf() with snprintf(buf, sizeof(buf), ...) to enforce hard limits.
3 Dynamic Array Fragmentation: Using malloc() and free() in the main loop, eventually exhausting the heap and returning a null pointer that you write to. Pre-allocate arrays globally at boot. Avoid dynamic allocation in loop().

Extending and Simplifying the Build

Once you have the core array logic working, you will inevitably need to scale the project up or strip it down for production. Here is how to modify the architecture without breaking memory bounds.

How to Extend (Scaling Up)

  • Add SD Card Logging: Arrays in SRAM are volatile. To persist the rolling buffer, add an SPI SD card module. Write the array to a CSV file every time historyIndex wraps back to 0. Use File.write() with the array pointer to dump the binary data in one operation.
  • Move to std::vector: If you absolutely need dynamic sizing, you can use #include <vector> in the ESP32 Arduino core. However, be warned: pushing to a vector causes memory reallocation. On a microcontroller, this fragments the heap. If you use vectors, call myVector.reserve(100) in setup() to pre-allocate the memory block.
  • Utilize PSRAM: If you are building an audio sampler or oscilloscope that requires arrays larger than 50,000 elements, upgrade to an ESP32-WROVER module with 4MB PSRAM. Use ps_malloc() instead of standard malloc() to allocate directly in external RAM (Espressif Memory Allocation Docs).

How to Simplify (Stripping Down)

  • Drop the OLED: The SSD1306 display library allocates a 1024-byte framebuffer in SRAM. If you only need to tune your sensor thresholds, delete the display code and rely entirely on the Arduino IDE Serial Plotter. This instantly frees up 1KB of SRAM for larger sensor buffers.
  • Use 1D Arrays for Single-Channel: If you only have one thermistor, flatten the 2D array tempHistory[NUM_CHANNELS][BUFFER_SIZE] into a simple 1D array tempHistory[BUFFER_SIZE]. This removes the nested loop overhead and saves CPU cycles during the rolling average calculation.
Default Recommendation: If you are strictly logging data for calibration and do not need a standalone physical display, drop the OLED and use the Arduino IDE Serial Plotter. This immediately frees up 1024 bytes of SRAM, allowing you to double your BUFFER_SIZE for smoother rolling averages without risking stack collisions. For static calibration data, always default to PROGMEM to keep your SRAM clean for live operations.