The Hidden Trap in Arduino Var Types: Why Your Code Fails at 32,767
If you have ever left an Arduino project running overnight only to find your sensor counters reporting negative numbers or your timing loops locking up, you have fallen victim to variable overflow. The most common mistake makers make with arduino var types is assuming an int is always a 32-bit integer. On classic 8-bit AVR boards like the Uno R3, an int is strictly 16 bits, capping out at 32,767. When your pulse counter or millis() timer crosses that threshold, it wraps around to -32,768, silently destroying your data.
The direct answer to avoiding this is simple: never use generic int or long declarations for time or cumulative counts. Instead, use explicit fixed-width types like uint32_t for unsigned 32-bit integers. This guarantees your variable holds up to 4,294,967,295 regardless of whether you compile for an 8-bit ATmega328P or a 32-bit ARM Cortex-M4.
In this guide, we will build a long-duration pulse logger to demonstrate proper variable sizing, debug the exact compiler and runtime errors that occur when you get it wrong, and map out the memory differences between classic and modern Arduino architectures.
Project Build: Long-Duration Pulse Logger
To see variable limits in action, we are building a high-resolution rotary encoder logger. This project tracks cumulative pulses and uptime over long periods, requiring variables that will not overflow during a multi-day test run.
Parts List
- Microcontroller: Arduino Uno R4 Minima (or Uno R3)
- Sensor: KY-040 Rotary Encoder Module (with breakout board)
- Resistors: 2x 10kΩ pull-up resistors (if breakout lacks them)
- Wiring: Breadboard and standard 22 AWG solid-core jumper wires
Pin Mapping Table
| Encoder Pin | Arduino Pin | Function | Notes |
|---|---|---|---|
| CLK (Clock) | D2 | Interrupt / State Read | Must be a hardware interrupt-capable pin |
| DT (Data) | D3 | Direction Read | Read during CLK state change |
| SW (Switch) | D4 | Button Press | Active LOW, requires internal pull-up |
| + (VCC) | 5V | Power | Use 3.3V if running on a 3.3V logic board |
| GND | GND | Ground | Common ground required |
The Code: Bulletproof Typing with Overflow Handling
Below is the complete, compilable C++ code. Notice the strict use of uint32_t and int32_t from the <stdint.h> library. This eliminates the guesswork of arduino var types across different architectures. The code includes error handling for Serial initialization and safe math operations.
#include <stdint.h>
// Pin Definitions
const uint8_t PIN_ENCODER_CLK = 2;
const uint8_t PIN_ENCODER_DT = 3;
const uint8_t PIN_ENCODER_SW = 4;
// Explicitly sized variables to prevent cross-board overflow
// uint32_t guarantees 32 bits (0 to 4,294,967,295) on both AVR and ARM
volatile uint32_t pulseCount = 0;
uint32_t lastReportTime = 0;
const uint32_t REPORT_INTERVAL_MS = 5000; // 5 seconds
void setup() {
// Error handling: Ensure Serial is actually available before proceeding
Serial.begin(115200);
uint32_t serialTimeout = millis();
while (!Serial && (millis() - serialTimeout < 3000)) {
// Wait up to 3 seconds for native USB boards like Uno R4 to enumerate
}
if (!Serial) {
// Fallback for headless operation; blink LED to indicate fault
pinMode(LED_BUILTIN, OUTPUT);
while(1) { digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); delay(100); }
}
Serial.println(F("Pulse Logger Initialized."));
Serial.print(F("Size of int on this board: "));
Serial.print(sizeof(int));
Serial.println(F(" bytes"));
pinMode(PIN_ENCODER_CLK, INPUT_PULLUP);
pinMode(PIN_ENCODER_DT, INPUT_PULLUP);
pinMode(PIN_ENCODER_SW, INPUT_PULLUP);
// Attach interrupt to CLK pin
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_CLK), handleEncoder, FALLING);
}
void loop() {
uint32_t currentMillis = millis();
// Safe time-delta calculation that handles the 49-day millis() rollover
if (currentMillis - lastReportTime >= REPORT_INTERVAL_MS) {
lastReportTime = currentMillis;
// Safely read the volatile 32-bit variable by disabling interrupts momentarily
noInterrupts();
uint32_t safeCount = pulseCount;
interrupts();
Serial.print(F("Uptime: "));
Serial.print(currentMillis / 1000);
Serial.print(F("s | Total Pulses: "));
Serial.println(safeCount);
}
// Check for button press to reset counter
if (digitalRead(PIN_ENCODER_SW) == LOW) {
delay(50); // Simple debounce
if (digitalRead(PIN_ENCODER_SW) == LOW) {
noInterrupts();
pulseCount = 0;
interrupts();
Serial.println(F("Counter Reset by User."));
while(digitalRead(PIN_ENCODER_SW) == LOW); // Wait for release
}
}
}
// Interrupt Service Routine (ISR)
void handleEncoder() {
// Read DT pin to determine direction
uint8_t dtState = digitalRead(PIN_ENCODER_DT);
if (dtState == HIGH) {
pulseCount++;
} else {
if (pulseCount > 0) pulseCount--; // Prevent underflow below zero
}
}
Debugging: When Variable Sizing Goes Wrong
When you misuse arduino var types, the compiler might not stop you, but the runtime behavior will be catastrophic. Here is how to identify and fix the most common variable sizing failures.
The Exact Error Strings and Symptoms
Symptom 1: Compiler Warning on Assignment
If you try to assign a large constant to a 16-bit integer on an Uno R3, the GCC compiler will throw this exact warning:
warning: overflow in implicit constant conversion [-Woverflow]
Fix: Change the variable declaration from int to uint32_t or append an UL suffix to your constant (e.g., 40000UL).
Symptom 2: Runtime Serial Output Anomaly
Your serial monitor suddenly prints:
Total Pulses: -32768
Fix: Your variable is a signed 16-bit int. The 32,768th pulse flipped the sign bit (two's complement overflow). Switch to uint32_t.
The First Three Things to Check When It Fails
- Check
sizeof()on your specific board: AddSerial.println(sizeof(myVar));to your setup. If it prints2, you are limited to 16 bits. If it prints4, you have 32 bits. - Look for implicit casting in math operations: If you multiply two 16-bit integers (e.g.,
int a = 1000; int b = 1000; long c = a * b;), the multiplication happens in 16-bit space and overflows before being assigned to the 32-bitlong. You must cast first:long c = (long)a * b;. - Verify signed vs. unsigned boundaries: Time deltas and physical counts should almost always be unsigned (
uint32_t). Using signed types halves your maximum positive capacity and invites negative wrap-around.
Arduino Var Types Reference Matrix (AVR vs ARM)
The transition from 8-bit AVR (Uno R3, Nano, Mega) to 32-bit ARM (Uno R4, Nano 33 BLE, ESP32) fundamentally changed how arduino var types behave. According to the Arduino Programming Documentation and standard C++ ABI specifications, here is how memory allocation differs.
| Variable Type | AVR (Uno R3 / Mega) | ARM (Uno R4 / ESP32) | Best Practice / Use Case |
|---|---|---|---|
bool | 1 byte (8 bits) | 1 byte (8 bits) | Flags, states, true/false logic. |
byte / uint8_t | 1 byte (0 to 255) | 1 byte (0 to 255) | Raw I2C/SPI data, PWM values, small counters. |
int | 2 bytes (-32k to 32k) | 4 bytes (-2.1B to 2.1B) | AVOID for cross-platform code. Use int16_t or int32_t instead. |
long / int32_t | 4 bytes (-2.1B to 2.1B) | 4 bytes (-2.1B to 2.1B) | Signed math, large sensor calculations. |
unsigned long / uint32_t | 4 bytes (0 to 4.29B) | 4 bytes (0 to 4.29B) | millis(), micros(), cumulative pulse counts. |
float | 4 bytes (6-7 digits) | 4 bytes (6-7 digits) | Analog sensor scaling, PID loops. Never use for currency or exact counts. |
double | 4 bytes (Same as float) | 8 bytes (15-17 digits) | High-precision math on ARM. Wastes memory on AVR. |
float in Arduino (IEEE 754 single-precision) only guarantees 6 to 7 significant decimal digits. If you try to store a millis() value of 12,345,678 in a float, it will round to 12,345,680. Always use integers for time and counting; reserve floats strictly for physical measurements like temperature or voltage.
How to Extend or Simplify the Build
To Simplify: If you only need to track short-term events (under 5 minutes), you can strip out the uint32_t declarations and use standard int variables, reducing cognitive load for absolute beginners. Remove the interrupt routine and simply poll the encoder pins in the loop() with a 5ms delay().
To Extend: Add an I2C OLED display (SSD1306) to visualize the pulse count in real-time. When doing so, you will need to cast your uint32_t variables to strings for the display library. Use snprintf() instead of the Arduino String class to prevent heap fragmentation:
char buffer[20];
snprintf(buffer, sizeof(buffer), "Pulses: %lu", safeCount);
display.println(buffer);
Frequently Asked Questions (FAQ)
What is the difference between int and int16_t in Arduino var types?
The int keyword is platform-dependent. On an 8-bit Arduino Uno R3, int is 16 bits. On a 32-bit Arduino Uno R4 or ESP32, int is 32 bits. The int16_t type (defined in <stdint.h>) is explicitly guaranteed to be exactly 16 bits on every architecture. Using fixed-width types like int16_t and int32_t ensures your code behaves identically when you migrate from an AVR board to an ARM board.
Why does my float variable lose precision after 6 digits?
Standard Arduino boards use 32-bit IEEE 754 single-precision floating-point numbers for the float type. This format allocates 23 bits for the significand, which translates to roughly 6 to 7 decimal digits of precision. If you need higher precision on a 32-bit board (like the ESP32 or Uno R4), use the double type, which maps to 64-bit double-precision (15-17 digits). Note that on classic AVR boards, double is just an alias for float and offers no extra precision.
Should I use String or char arrays for text in Arduino var types?
For production or long-running embedded projects, always use C-style char arrays (e.g., char myText[32];). The Arduino String object (capital 'S') dynamically allocates memory on the heap. Frequent creation and destruction of String objects causes heap fragmentation, eventually leading to memory exhaustion and random reboots. Use snprintf() to format data into pre-allocated char buffers safely.
How do I choose the right Arduino var types for I2C sensor data?
I2C and SPI protocols transmit data in 8-bit chunks (bytes). When reading a 16-bit sensor register (like an accelerometer axis), you must read two uint8_t bytes and combine them using bitwise shift operations into a int16_t (if the sensor outputs signed data) or uint16_t. Never use standard int for bitwise assembly, as sign-extension on 32-bit boards can corrupt the upper 16 bits if you are not careful with your casting.
References: Arduino Programming Documentation, C Standard Arithmetic Types (cppreference).






