Beyond the Upload Button: The Reality of Arduino Execution

Most beginners learn how to run code in Arduino by simply clicking the right-facing arrow in the IDE and watching an LED blink. But treating the Arduino ecosystem as a magic black box severely limits your ability to debug, optimize, and scale embedded projects. As we navigate the embedded landscape in 2026, professional makers and firmware engineers must understand the underlying architecture that bridges human-readable C++ and the raw silicon of the microcontroller.

This library deep dive bypasses the basic 'Hello World' tutorials. Instead, we will dissect the ArduinoCore-avr library, expose the hidden execution pipeline, and analyze the exact CPU cycle overhead of standard library functions versus direct hardware manipulation.

The Illusion of setup() and loop()

When you write an Arduino sketch (a .ino file), you are not writing a complete C++ program. Standard C++ requires a main() function as the entry point. So, how does the microcontroller know where to start?

The Arduino IDE (and the underlying arduino-cli toolchain) performs a pre-processing step before compilation. It concatenates all .ino tabs, automatically generates function prototypes, and wraps your code into a hidden main.cpp file provided by the core library. If you examine the ArduinoCore-avr GitHub Repository, you will find the actual entry point located in cores/arduino/main.cpp:

#include <Arduino.h>

int main(void) {
    init();
    initVariant();
#if defined(USBCON)
    USBDevice.attach();
#endif
    setup();
    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }
    return 0;
}

Notice the init() function. This is where the core library configures the hardware timers, sets up the ADC (Analog-to-Digital Converter) prescalers, and initializes the UART serial buffers before your setup() function ever runs. Understanding this sequence is critical when dealing with early-boot edge cases or watchdog timer resets.

Deconstructing Arduino.h and the Wiring Library

The core of the Arduino API relies heavily on the 'Wiring' framework. When you call digitalWrite() or millis(), you are invoking functions defined in wiring_digital.c and wiring.c.

The millis() Timer0 Dependency

A common point of failure in long-running Arduino projects is the misunderstanding of timekeeping. The millis() function does not query a real-time clock (RTC). Instead, it relies on Timer0, an 8-bit hardware timer on the ATmega328P. The core library configures Timer0 with a prescaler of 64. At a 16MHz clock speed, the timer overflows every 1.024 milliseconds. The library's Interrupt Service Routine (ISR) catches this overflow and increments a 32-bit unsigned integer.

Edge Case Warning: Because millis() uses a 32-bit unsigned long, it will overflow and roll back to zero after exactly 49.71 days. If your code uses subtraction for timing (e.g., if (currentMillis - previousMillis >= interval)), the math gracefully handles the rollover. If you use addition (e.g., if (currentMillis >= previousMillis + interval)), your code will lock up on day 49.

The Execution Pipeline: From Sketch to Silicon

To truly master how to run code in Arduino environments, you must understand the toolchain. The IDE uses avr-gcc and avr-g++ to compile, and avrdude to flash. Here is the exact pipeline:

  1. Preprocessing: The IDE appends #include <Arduino.h> and generates prototypes.
  2. Compilation: avr-g++ translates the C++ into AVR assembly language, applying optimization flags (default is -Os for size optimization).
  3. Linking: The linker combines your object files with the pre-compiled core library (libcore.a) and the standard C library (avr-libc).
  4. Hex Conversion: avr-objcopy strips debugging symbols and converts the ELF binary into an Intel HEX file.
  5. Flashing: avrdude communicates with the Optiboot bootloader via UART (at 115200 baud) to write the HEX file into the microcontroller's Flash memory.

Library Overhead vs. Direct Port Manipulation

The Arduino core library prioritizes ease of use and cross-board compatibility over raw execution speed. This abstraction comes at a measurable cost in CPU cycles. When you need to run code in Arduino at high frequencies (e.g., bit-banging a custom protocol or driving high-speed LED matrices), standard library functions become a bottleneck.

Operation Method CPU Cycles (Approx) Execution Time (at 16MHz) Memory Overhead
Set Pin HIGH digitalWrite(13, HIGH) ~50 - 60 cycles ~3.5 µs High (Lookup tables)
Set Pin HIGH PORTB |= (1 << PB5) 1 - 2 cycles ~0.125 µs Zero (Inline ASM)
Read Pin digitalRead(2) ~40 - 50 cycles ~2.8 µs High
Read Pin PIND & (1 << PD2) 1 - 2 cycles ~0.125 µs Zero

By bypassing the Arduino.h abstraction layer and writing directly to the AVR I/O registers (PORTx, DDRx, and PINx), you achieve a 30x to 50x speed increase. However, this sacrifices portability; code written for the ATmega328P's PORTB will not compile on an ESP32 or an ARM-based Arduino Zero without a complete rewrite.

Modern Execution: Running Code via Arduino CLI

In 2026, relying solely on the graphical IDE is inefficient for version control, CI/CD pipelines, and remote headless servers. The industry standard for professional firmware deployment is the Arduino CLI. Here is how to compile and run your code entirely from the terminal:

Step 1: Identify Your Board's FQBN

Connect your board and run:

arduino-cli board list

Note the Fully Qualified Board Name (FQBN). For a standard Uno, it is arduino:avr:uno.

Step 2: Compile with Verbose Output

To see the exact avr-gcc flags and memory usage, run:

arduino-cli compile --fqbn arduino:avr:uno -v ./my_sketch

This will output the exact Flash and SRAM utilization. On the ATmega328P, you have 32KB of Flash and 2KB of SRAM. If your global variables and the heap exceed the 2KB SRAM limit, the code will compile successfully but will suffer from stack collisions at runtime, causing random reboots.

Step 3: Flash to the Microcontroller

arduino-cli upload -p /dev/ttyUSB0 --fqbn arduino:avr:uno ./my_sketch

Troubleshooting Execution Failures and Edge Cases

When your code compiles but fails to run correctly on the hardware, the issue usually lies in one of three hardware-level edge cases:

  • Watchdog Timer (WDT) Infinite Loops: If you enable the hardware watchdog to reset the board on lockups, you must clear the WDT flag in the bootloader. Older bootloaders fail to do this, causing the board to enter an infinite reset loop immediately upon powering on. Always ensure you are using Optiboot v8.0 or newer.
  • Interrupt Starvation: If an ISR (Interrupt Service Routine) takes longer to execute than the interval between interrupts, the system will hang. Keep ISRs under 50 clock cycles. Never use delay(), Serial.print(), or millis() inside an ISR.
  • EEPROM Wear Leveling: The ATmega328P EEPROM is rated for 100,000 write cycles. If your code writes to the EEPROM inside the loop() function without a conditional trigger, you will physically destroy the EEPROM silicon within minutes. Always use the EEPROM.update() function, which only writes if the new value differs from the existing value.

Conclusion

Knowing how to run code in Arduino is not just about uploading a sketch; it is about understanding the translation of C++ into hardware registers, managing the limitations of the AVR architecture, and leveraging the core libraries efficiently. By mastering the underlying toolchain, utilizing direct port manipulation where necessary, and adopting modern CLI workflows, you transition from a casual hobbyist to a capable embedded systems engineer.