Variables in Arduino are named memory allocations that store data in the microcontroller's SRAM (for standard mutable variables) or Flash memory (for const or PROGMEM declarations). Choosing the correct variable type is not just about syntax; it dictates your memory footprint, prevents silent rollover bugs in timing functions, and ensures mathematical precision. For instance, assigning a 32-bit millis() value to a 16-bit int on an AVR board will cause a silent overflow every 32 seconds, crashing your logic without triggering a compiler warning.
This guide breaks down exactly how memory is allocated across modern Arduino architectures, provides a complete environmental logging build to demonstrate strict typing, and walks through the exact compiler errors you will face when variable scope is mismanaged.
Memory Limits and Data Type Sizes
Before declaring variables, you must understand the hardware constraints. A common trap is assuming an int is always 16 bits. On legacy 8-bit AVR boards (like the classic Uno R3), an int is 2 bytes. On 32-bit ARM boards (like the Uno R4 WiFi or Nano 33 IoT), an int is 4 bytes. This architectural difference breaks code portability if you rely on implicit overflow behaviors.
Below is the definitive reference for variable types, their memory footprints, and their practical applications on the Arduino Uno R4 WiFi (Renesas RA4M1 ARM Cortex-M4), which features 32KB of SRAM and 256KB of Flash.
| Data Type | Size (Bytes) | Value Range | Best Use Case & Hardware Notes |
|---|---|---|---|
byte / uint8_t |
1 | 0 to 255 | Digital pin states, I2C register addresses, raw ADC byte mapping. |
int / int16_t |
4 (on ARM) 2 (on AVR) |
-2,147,483,648 to 2,147,483,647 (ARM) | General math. Warning: Use explicit int16_t or int32_t if porting code between AVR and ARM boards. |
unsigned long |
4 | 0 to 4,294,967,295 | Mandatory for millis() and micros() timestamps to handle 49-day rollovers safely. |
float |
4 | ±3.4028235E+38 | Sensor math (temperature, pressure). Note: AVR float lacks double precision; ARM supports true 64-bit double. |
char[] vs String |
Static vs Dynamic | Fixed size vs Heap allocated | Always prefer char[] for formatting. The String object causes SRAM heap fragmentation and eventual crashes on long-running loggers. |
PROGMEM macro to force the compiler to store the data in Flash memory, freeing up your limited 32KB SRAM for runtime operations. See the official Arduino Variables and Memory Reference for architecture-specific quirks.
Project Build: Environmental Logger with Strict Typing
To demonstrate proper variable management, we will build a non-blocking environmental data logger. This code explicitly avoids the String class to prevent heap fragmentation, uses unsigned long for timing, and implements strict error handling for sensor initialization.
Parts List
- Microcontroller: Arduino Uno R4 WiFi (Target Board Variant: Renesas RA4M1, 32KB SRAM)
- Sensor: Adafruit BME280 I2C Temperature, Humidity, and Pressure Sensor (Product ID: 2652)
- Wiring: 4x 26 AWG silicone jumper wires (M-F)
- Hardware: 4x M2.5 brass standoffs for mounting
Pin Mapping Table
| BME280 Pin | Arduino Uno R4 WiFi Pin | Function |
|---|---|---|
| VIN | 5V | Power Input (3.3V to 5V tolerant) |
| GND | GND | Common Ground |
| SCL | A5 (or dedicated SCL header) | I2C Clock Line |
| SDA | A4 (or dedicated SDA header) | I2C Data Line |
Complete Compilable Code
This sketch targets the Uno R4 WiFi. It uses snprintf with a statically allocated char array to format serial output, entirely bypassing the memory-hungry String class.
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// --- Pin & Hardware Definitions ---
const int I2C_SDA_PIN = A4;
const int I2C_SCL_PIN = A5;
const int LED_STATUS_PIN = LED_BUILTIN;
const uint8_t BME_I2C_ADDRESS = 0x76; // Adafruit boards default to 0x77, some clones use 0x76
// --- Timing Variables (Must be unsigned long to prevent 32-bit overflow) ---
unsigned long lastReadTime = 0;
const unsigned long READ_INTERVAL_MS = 2000;
// --- Buffer Variables (Static allocation to avoid heap fragmentation) ---
char logBuffer[80];
// Instantiate sensor object
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
while (!Serial) { delay(10); } // Wait for serial port on native USB boards
pinMode(LED_STATUS_PIN, OUTPUT);
digitalWrite(LED_STATUS_PIN, LOW);
// Initialize I2C with explicit pin definitions
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
// Error Handling: Verify sensor presence
if (!bme.begin(BME_I2C_ADDRESS)) {
Serial.println("FATAL: Could not find a valid BME280 sensor. Check I2C address and wiring.");
// Blink LED rapidly to indicate hardware fault
while (1) {
digitalWrite(LED_STATUS_PIN, !digitalRead(LED_STATUS_PIN));
delay(100);
}
}
Serial.println("BME280 initialized successfully. Logging data...");
}
void loop() {
unsigned long currentMillis = millis();
// Non-blocking delay using unsigned long math
if (currentMillis - lastReadTime >= READ_INTERVAL_MS) {
lastReadTime = currentMillis;
digitalWrite(LED_STATUS_PIN, HIGH);
// Read sensor data into strictly typed float variables
float temperature = bme.readTemperature();
float pressure = bme.readPressure() / 100.0F; // Convert Pa to hPa
float humidity = bme.readHumidity();
// Format data into static char array (Avoids String class heap fragmentation)
snprintf(logBuffer, sizeof(logBuffer),
"T: %.2f C | P: %.2f hPa | H: %.1f %% | Uptime: %lu ms",
temperature, pressure, humidity, currentMillis);
Serial.println(logBuffer);
digitalWrite(LED_STATUS_PIN, LOW);
}
}
Debugging Variable Scope Errors
When learning how variables in Arduino interact with C++ block structures, you will inevitably hit scope errors. The compiler halts and throws an error when you attempt to read or write a variable outside the block {} where it was born.
The Exact Error String
error: 'temperature' was not declared in this scope
Ranked Causes (Most Likely First)
- Block Boundary Violation: You declared
float temperature = bme.readTemperature();inside anifstatement or aforloop, but attempted to print it outside that block's closing curly brace}. Variables cease to exist the moment the compiler passes their closing brace. - Case-Sensitivity Typos: C++ is strictly case-sensitive. Declaring
float Temperaturebut callingSerial.print(temperature)will trigger this exact error, as the compiler sees them as two entirely different identifiers. - Forward Referencing: You attempted to use the variable on line 45, but didn't declare it until line 50. Unlike some interpreted languages, the Arduino GCC compiler reads top-to-bottom and requires declaration before usage within the same scope.
The First Three Things to Check When It Fails
When the compiler throws a scope error, do not immediately rewrite your logic. Run this 3-step diagnostic:
- Trace the Curly Braces: Count your
{and}pairs. Use the Arduino IDE's auto-format tool (Ctrl+T / Cmd+B). If the line where you declared the variable is indented deeper than the line where you are trying to use it, you have a scope mismatch. - Verify Exact Spelling: Highlight the variable name in the error message and use the Find tool (Ctrl+F) to ensure the declaration matches the usage exactly, including capitalization.
- Check Global vs. Local Intent: If the variable needs to be accessed by multiple functions (e.g., inside both
loop()and a custom interrupt service routine), it must be declared globally at the very top of the sketch, beforevoid setup().
Extending and Simplifying the Build
Depending on your project phase, you may need to strip this code down for quick prototyping or scale it up for permanent deployment.
How to Simplify (For Quick Bench Testing)
If you are just testing the sensor on your workbench and don't care about long-term memory stability, you can strip out the char array and snprintf logic. Replace the buffer formatting block with raw serial calls:
Serial.print("T: "); Serial.print(temperature);
Serial.print(" | P: "); Serial.print(pressure);
Serial.print(" | H: "); Serial.println(humidity);
Trade-off: This is faster to type and easier to read for beginners, but it increases the compiled binary size slightly and introduces minor timing jitter due to multiple sequential I2C/UART buffer flushes.
How to Extend (For Permanent Data Logging)
To turn this into a robust, long-term field logger, you need persistent storage and memory optimization.
- Add SPI Storage: Wire an Adafruit MicroSD SPI breakout (Product ID: 254) to the Uno R4's hardware SPI pins (MOSI: 11, MISO: 12, SCK: 13, CS: 10). Use the
SdFatlibrary instead of the stockSDlibrary for faster, non-blocking file writes. - Implement PROGMEM: Store your CSV header strings in Flash memory. Instead of
char header[] = "Time,Temp,Press,Hum";, useconst char header[] PROGMEM = "Time,Temp,Press,Hum";. You will need to usestrcpy_Pto pull it into a RAM buffer before writing to the SD card, but this saves precious SRAM for the file system cache. - Add Watchdog Variables: Implement an
unsigned longwatchdog timer that resets the board viaNVIC_SystemReset()if the I2C bus locks up (a common issue with long BME280 cable runs in high-EMI environments).
Understanding how variables map to physical silicon is what separates a sketch that runs for five minutes on a desk from a firmware that survives for five years in the field. Always declare with intent, respect your SRAM boundaries, and let the compiler's scope errors guide you toward cleaner architecture. For deeper reading on sensor integration, refer to the Adafruit BME280 Wiring and Test Guide.






