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.
Pro Tip: If you need to store large lookup tables or static text strings (like CSV headers for an SD card), do not use standard variables. Use the 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)

  1. Block Boundary Violation: You declared float temperature = bme.readTemperature(); inside an if statement or a for loop, but attempted to print it outside that block's closing curly brace }. Variables cease to exist the moment the compiler passes their closing brace.
  2. Case-Sensitivity Typos: C++ is strictly case-sensitive. Declaring float Temperature but calling Serial.print(temperature) will trigger this exact error, as the compiler sees them as two entirely different identifiers.
  3. 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:

  1. 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.
  2. 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.
  3. 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, before void 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 SdFat library instead of the stock SD library for faster, non-blocking file writes.
  • Implement PROGMEM: Store your CSV header strings in Flash memory. Instead of char header[] = "Time,Temp,Press,Hum";, use const char header[] PROGMEM = "Time,Temp,Press,Hum";. You will need to use strcpy_P to 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 long watchdog timer that resets the board via NVIC_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.