A bare-metal AVR assembly program that blinks the built-in LED on an Arduino Uno at approximately 1Hz, written entirely in AVR assembly language using Microchip Studio. No Arduino libraries, no C++, no abstraction layers — just direct hardware register manipulation at the instruction level.
Most Arduino projects are written in C++ using the Arduino IDE, which abstracts away the underlying hardware through libraries like digitalWrite() and delay(). This project strips all of that away to interact directly with the ATmega328P's hardware registers.
Writing in assembly provides a clear understanding of:
- How GPIO pins are controlled at the register level
- How software delay loops are derived from clock frequency math
- How the CPU executes instructions and manages program flow
- What happens on a microcontroller before any high-level language takes over
This kind of knowledge is directly applicable to embedded systems roles where firmware interacts with hardware at the lowest level — PIC microcontrollers, motor controllers, safety-critical systems, and any application where cycle-accurate timing matters.
| Component | Details |
|---|---|
| Microcontroller | ATmega328P (8-bit AVR) |
| Board | Arduino Uno |
| LED | Built-in LED on Pin 13 (PB5) |
| Clock Speed | 16 MHz |
| Programmer | Arduino bootloader via avrdude |
No external components required. The built-in LED connected to PB5 is used as the output target.
The ATmega328P controls pins through memory-mapped I/O registers. Two registers govern Port B:
- DDRB (Data Direction Register B) — sets each pin as input (0) or output (1)
- PORTB (Port B Data Register) — sets each output pin high (1) or low (0)
Each bit in these registers corresponds to one physical pin. Pin 13 maps to PB5, which is bit 5 of Port B.
To turn on only the LED on PB5:
- Set DDRB to
0xFF(all Port B pins as outputs) - Write
0x20(binary00100000) to PORTB — bit 5 high, all others low
To turn it off:
- Write
0x00to PORTB — all bits low
Because AVR instructions cannot load immediate values directly into I/O registers, a general-purpose register (R16 or R17) acts as an intermediary — a two-step "messenger" pattern required by the AVR instruction set architecture.
A software delay loop burns CPU cycles to create a visible blink rate. At 16 MHz, each clock cycle takes 62.5 nanoseconds. To create a 500ms delay, approximately 8,000,000 cycles must be consumed:
16,000,000 cycles/sec × 0.5 sec = 8,000,000 cycles
A single dec + brne pair consumes roughly 3 cycles per iteration. A single 8-bit counter maxes out at 255 iterations (~195,000 cycles, ~12ms) — far short of 500ms. Three nested loops are required:
255 × 255 × N × 3 cycles = 8,000,000
N ≈ 41
The outermost counter is loaded with 41, producing approximately 500ms per delay call. The LED is on for 500ms and off for 500ms, giving a 1Hz blink rate.
Start:
Set all Port B pins as outputs via DDRB ; runs once on reset
Loop:
Write 0x20 to PORTB ; LED on (PB5 high)
Call delay subroutine ; wait ~500ms
Write 0x00 to PORTB ; LED off
Call delay subroutine ; wait ~500ms
Jump back to Loop ; repeat forever
delay:
Three nested countdown loops ; burns ~8,000,000 cycles
Return to caller
The delay subroutine lives below the main loop's rjmp instruction — the CPU never falls into it accidentally since rjmp Loop acts as a permanent redirect. The subroutine is only reached via rcall delay, which saves the return address and restores it on ret.
- IDE: Microchip Studio 7 (formerly Atmel Studio)
- Assembler: AVR-AS (bundled with Microchip Studio)
- Programmer: avrdude 8.0.0 via Arduino bootloader
- Target Device: ATmega328P
- Clock: 16,000,000 Hz
avrdude -C "path\to\avrdude.conf" -v -p atmega328p -c arduino -P COM8 -b 115200 -D -U flash:w:"path\to\led_blink.hex":iReplace COM8 with your Arduino's assigned COM port (check Device Manager on Windows).
- Direct register manipulation — DDRB and PORTB controlled without any library abstraction
- Bit masking — isolating PB5 using
0x20(binary00100000) - Software delay timing — deriving loop counts from clock frequency arithmetic
- Subroutine structure —
rcall/retpattern, placement relative to jump instructions - AVR instruction set —
ldi,out,dec,brne,rjmp,rcall,ret - Harvard architecture — separate flash (code) and SRAM (data) memory spaces
To change the blink rate, adjust the outermost delay counter in the delay subroutine:
| Outermost Counter Value | Approximate Delay | Blink Rate |
|---|---|---|
| 21 | ~250ms | 2 Hz |
| 41 | ~500ms | 1 Hz |
| 82 | ~1000ms | 0.5 Hz |
For precise timing, a hardware timer (Timer0 or Timer1) should be used instead of a software delay loop. Hardware timers are interrupt-driven and not affected by other code execution time.
- acoustic-localization-robot — Autonomous robot using acoustic localization to detect human voices, built with Arduino
Logan — Electrical Engineer pivoting to embedded systems and robotics.
GitHub: TheThriftyLog