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. |
sizeof() TrapWhen 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
- Loop Boundary Conditions: Check every
forloop. Did you writei <= BUFFER_SIZEinstead ofi < BUFFER_SIZE? An array of size 10 has valid indices 0 through 9. Index 10 is out of bounds. - 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). - 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
historyIndexwraps back to 0. UseFile.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, callmyVector.reserve(100)insetup()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 standardmalloc()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 arraytempHistory[BUFFER_SIZE]. This removes the nested loop overhead and saves CPU cycles during the rolling average calculation.
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.






