The most common mistake makers make with variable types in Arduino is using a 16-bit int for pulse counts or millis() timestamps, causing silent rollover bugs the moment a counter hits 32,767. On 8-bit AVR boards like the Uno or Nano, an int is only two bytes. If you are logging sensor data or tracking uptime, you must use unsigned long for time and volatile unsigned long for interrupt counters. Choosing the wrong data type doesn't just waste RAM; it introduces catastrophic math errors that only appear after hours of runtime.
In this guide, we will break down the exact memory footprints of Arduino variable types, compare 8-bit AVR architectures to 32-bit ARM (ESP32) boards, and build a water flow sensor logger that specifically demonstrates how to avoid type-mismatch overflows in production code.
The Core Variable Types in Arduino (AVR vs ARM)
Before writing a single line of code, you must understand that variable sizes are not universal across all Arduino-compatible boards. The Arduino IDE abstracts a lot, but the underlying C/C++ compiler allocates memory based on the target architecture. An int on an Arduino Uno (ATmega328P) is 16 bits, but on an ESP32 (Xtensa LX6), it is 32 bits. Relying on generic types like int or long makes your code non-portable and prone to hidden bugs when you upgrade your hardware.
int and long in favor of the <stdint.h> standard types like uint16_t and uint32_t. These guarantee exact bit-widths regardless of whether you compile for an Uno, Nano, or ESP32.
| Data Type | AVR (Uno/Nano) Size | ARM (ESP32) Size | Min Value | Max Value | Best Use Case |
|---|---|---|---|---|---|
bool |
1 byte | 1 byte | 0 (false) | 1 (true) | Flags, button states |
byte / uint8_t |
1 byte | 1 byte | 0 | 255 | PWM values, I2C registers |
int / int16_t |
2 bytes | 4 bytes* | -32,768 | 32,767 | Small math, pin numbers |
unsigned int |
2 bytes | 4 bytes* | 0 | 65,535 | ADC readings (10-bit/12-bit) |
long / int32_t |
4 bytes | 4 bytes | -2,147,483,648 | 2,147,483,647 | Large calculations, GPS coords |
unsigned long / uint32_t |
4 bytes | 4 bytes | 0 | 4,294,967,295 | millis(), pulse counters, epoch time |
float |
4 bytes | 4 bytes | -3.4028235E38 | 3.4028235E38 | Sensor math, PID loops |
double |
4 bytes** | 8 bytes | -3.4028235E38 | 3.4028235E38 | High-precision ARM math only |
* On ESP32/ARM, int is 32-bit. Always use int16_t if you specifically need 16-bit behavior.
** On 8-bit AVR boards, double is implemented identically to float (4 bytes). You do not gain precision by using double on an Uno/Nano.
Project Build: YF-S201 Flow Sensor & Uptime Logger
To demonstrate why variable types matter, we are building a water flow logger. The YF-S201 sensor outputs a PWM pulse train (roughly 4.5 pulses per second per liter/minute). If you use a standard 16-bit int to count these pulses, your counter will overflow and roll over to -32768 after just a few hours of continuous flow. Furthermore, tracking uptime with millis() requires an unsigned long to prevent the 49-day rollover bug from breaking your time-delta calculations.
Parts List
- Microcontroller: Arduino Nano v3 (ATmega328P, 16MHz, 5V logic)
- Sensor: YF-S201 Water Flow Sensor (1/2" NPT threads, 5V powered)
- Display: 16x2 I2C LCD with PCF8574 backpack (Address 0x27)
- Passives: 10kΩ pull-up resistor (for the sensor data line)
- Wiring: 22 AWG solid core jumper wires, breadboard
Pin Mapping Table
| Nano Pin | Component | Function / Notes |
|---|---|---|
| D2 | YF-S201 Yellow Wire | Hardware Interrupt 0 (Pulse counting) |
| D3 | LED (via 220Ω) | Status indicator (blinks on pulse) |
| A4 | LCD SDA | I2C Data line |
| A5 | LCD SCL | I2C Clock line |
| 5V | YF-S201 Red / LCD VCC | Sensor and display power |
| GND | Common Ground | Sensor Black, LCD GND, Nano GND |
Complete Code with Type-Safe Error Handling
The following code is explicitly written for the Arduino Nano v3 (AVR 8-bit). Notice the strict use of volatile uint32_t for the interrupt counter and uint32_t for time tracking. We also include I2C error handling to ensure the LCD is actually present on the bus before attempting to write to it, preventing the sketch from hanging indefinitely.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <stdint.h>
// --- PIN DEFINITIONS ---
const uint8_t FLOW_SENSOR_PIN = 2; // Must be an interrupt-capable pin (D2 or D3 on Nano)
const uint8_t STATUS_LED_PIN = 3;
// --- I2C LCD SETUP ---
// PCF8574 backpacks usually default to 0x27 or 0x3F
LiquidCrystal_I2C lcd(0x27, 16, 2);
bool lcd_present = false;
// --- TYPE-SAFE VARIABLES ---
// CRITICAL: Use volatile for variables modified inside an ISR
// CRITICAL: Use uint32_t (unsigned 32-bit) to prevent 16-bit int overflow at 32,767
volatile uint32_t pulse_count = 0;
uint32_t last_millis = 0;
uint32_t uptime_seconds = 0;
// Calibration factor: YF-S201 outputs ~4.5 pulses per second per L/min
const float CALIBRATION_FACTOR = 4.5;
void setup() {
Serial.begin(115200);
// Initialize I2C and check for LCD presence
Wire.begin();
Wire.beginTransmission(0x27);
if (Wire.endTransmission() == 0) {
lcd.init();
lcd.backlight();
lcd_present = true;
lcd.print("System Ready");
} else {
Serial.println(F("ERROR: I2C LCD not found at 0x27!"));
}
pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP);
pinMode(STATUS_LED_PIN, OUTPUT);
// Attach hardware interrupt (FALLING edge for YF-S201)
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), pulseISR, FALLING);
last_millis = millis();
}
void loop() {
uint32_t current_millis = millis();
// Calculate delta time safely using unsigned math (handles 49-day rollover automatically)
uint32_t delta_ms = current_millis - last_millis;
if (delta_ms >= 1000) {
last_millis = current_millis;
uptime_seconds++;
// Safely copy volatile variable by disabling interrupts momentarily
noInterrupts();
uint32_t safe_pulse_count = pulse_count;
interrupts();
// Calculate flow rate (L/min) and total volume (Liters)
// Note: Multiplying by 60.0 forces floating-point math, avoiding integer truncation
float flow_rate_lpm = (safe_pulse_count / CALIBRATION_FACTOR) * 60.0;
float total_liters = safe_pulse_count / (CALIBRATION_FACTOR * 60.0);
// Output to Serial
Serial.print(F("Uptime: ")); Serial.print(uptime_seconds);
Serial.print(F("s | Pulses: ")); Serial.print(safe_pulse_count);
Serial.print(F(" | Flow: ")); Serial.print(flow_rate_lpm, 2);
Serial.println(F(" L/min"));
// Output to LCD if present
if (lcd_present) {
lcd.setCursor(0, 0);
lcd.print("Flow: ");
lcd.print(flow_rate_lpm, 1);
lcd.print(" L/m ");
lcd.setCursor(0, 1);
lcd.print("Vol: ");
lcd.print(total_liters, 2);
lcd.print(" L ");
}
}
}
// --- INTERRUPT SERVICE ROUTINE (ISR) ---
// Keep this as short as possible. No Serial.print, no delay, no floating point math.
void pulseISR() {
pulse_count++;
// Optional: Toggle LED to visualize pulses (fast, direct port manipulation is better but digitalWrite works for low freq)
digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
}
Debugging Type Mismatches and Overflows
When working with sensor data and timing, type-related bugs rarely crash the board; instead, they produce garbage data that looks plausible until it doesn't. If your flow counter suddenly drops to a negative number, or your millis() logic stops triggering, here are the first three things to check:
- Verify the variable type matches the expected maximum value: If you used
int pulse_counton an AVR board, it will max out at 32,767. The next pulse rolls it over to-32768. Always useuint32_tfor cumulative counts. - Ensure interrupt variables are declared as
volatile: Without thevolatilekeyword, the GCC compiler will optimize the code by caching the variable in a CPU register, meaning the mainloop()will never see the updates made by the ISR. Your serial monitor will just print0forever. - Check for integer division truncation: If you write
float liters = pulse_count / 270;, the compiler performs integer division first (truncating the decimal) before assigning it to the float. You must force float math by writingpulse_count / 270.0.
Common Error Strings and Ranked Causes
If you are seeing weird behavior, look for these specific compiler warnings or runtime outputs:
warning: overflow in implicit constant conversion [-Woverflow]Ranked Causes:
1. You tried to assign a literal value larger than 32,767 to a 16-bit
int (e.g., int timeout = 60000;). Fix: Use long or uint32_t.2. You performed a math operation that exceeded the type's bounds before assigning it. Fix: Cast one of the operands to
long (e.g., (long)a * b).
Runtime Symptom: Serial monitor prints Pulse Count: -32768 or Flow: -nan.
Ranked Causes:
1. 16-bit signed integer overflow (the classic AVR int trap).
2. Dividing by zero in your flow calculation because the calibration factor was accidentally set to 0 or an uninitialized variable.
3. Reading a volatile multi-byte variable without disabling interrupts, resulting in a "torn read" where the upper and lower bytes are captured at different times.
Extending and Simplifying the Build
Depending on your project constraints, you may need to scale this hardware up or down. Here is how to adapt the variable types and hardware for different scenarios.
How to Simplify the Build
If you are prototyping on a breadboard and don't want to wire up the I2C LCD, simply delete the LiquidCrystal_I2C library includes and the lcd_present logic. Rely entirely on the Serial.print() statements. To save even more RAM on the ATmega328P (which only has 2KB of SRAM), replace any remaining string literals with the F() macro (e.g., Serial.print(F("Flow: "));). This forces the compiler to keep the strings in Flash memory (32KB) rather than loading them into precious SRAM at runtime.
How to Extend the Build
To turn this into a permanent data logger, add a MicroSD card module (like the Adafruit MicroSD Breakout). When logging to FAT32 file systems, file sizes and byte offsets require 32-bit unsigned integers. You will need to use uint32_t for tracking file sizes. Furthermore, if you decide to upgrade the microcontroller to an ESP32 DevKit v1 to add WiFi/MQTT telemetry, be aware that the ESP32's int is 32 bits. While this accidentally "fixes" the 16-bit overflow bug without you changing the code, it wastes RAM. It is always best practice to explicitly declare int16_t or uint32_t so your memory intent is clear, regardless of the silicon you compile for.
For deeper reading on standard integer types and architecture-specific behaviors, refer to the avr-libc stdint documentation and the official Arduino Language Reference.






