Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

AVR Assembly LED Blink — ATmega328P

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.


Why Assembly?

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.


Hardware

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.


How It Works

GPIO Control

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 (binary 00100000) to PORTB — bit 5 high, all others low

To turn it off:

  • Write 0x00 to 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.

Delay Routine

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.


Program Structure

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.


Build Environment

  • 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

Upload Command

avrdude -C "path\to\avrdude.conf" -v -p atmega328p -c arduino -P COM8 -b 115200 -D -U flash:w:"path\to\led_blink.hex":i

Replace COM8 with your Arduino's assigned COM port (check Device Manager on Windows).


Key Concepts Demonstrated

  • Direct register manipulation — DDRB and PORTB controlled without any library abstraction
  • Bit masking — isolating PB5 using 0x20 (binary 00100000)
  • Software delay timing — deriving loop counts from clock frequency arithmetic
  • Subroutine structurercall / ret pattern, placement relative to jump instructions
  • AVR instruction setldi, out, dec, brne, rjmp, rcall, ret
  • Harvard architecture — separate flash (code) and SRAM (data) memory spaces

Modifying the Blink Rate

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.


Related Projects


Author

Logan — Electrical Engineer pivoting to embedded systems and robotics.
GitHub: TheThriftyLog

About

Bare-metal AVR assembly LED blink on ATmega328P — direct register manipulation, no Arduino libraries.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages