Loading header...

Exercise: Build Blink and Inspect Output

This exercise turns the first four lessons into a real workflow. You will write a tiny LED blink program, compile it, generate the usual build artifacts, and inspect what the toolchain produced.

The point is not just to make an LED blink. The point is to prove you can connect source code, build commands, and generated outputs.

Learning Objectives

By the end of this exercise, you should be able to:

  • build a minimal bare-metal AVR C program from the command line;
  • identify the resulting .elf, .hex, .map, and disassembly files;
  • explain which source lines became the visible LED behavior;
  • verify both firmware behavior and build artifacts.

Prerequisites

  • Basic familiarity with GPIO direction and output registers.
  • An AVR board such as Arduino Uno or any ATmega328P development board.
  • AVR-GCC toolchain installed.
  • USB cable and programmer or bootloader workflow appropriate for your board.

Hardware and Software Needed

Item Purpose
ATmega328P board target hardware
LED on PB5 / Arduino D13 visible output
AVR-GCC, avr-objcopy, avr-objdump, avr-size build tools
Optional serial terminal or oscilloscope extra verification

Concrete Task

Write a program that:

  1. configures PB5 as an output;
  2. drives PB5 high;
  3. waits in a software delay loop;
  4. drives PB5 low;
  5. repeats forever.

Then:

  1. build the program;
  2. create a map file and HEX file;
  3. dump disassembly;
  4. inspect the results.

Implementation

#include <avr/io.h>
#include <stdint.h>

#define F_CPU 16000000UL
#define LED_MASK (1u << PB5)

static void delay_cycles(volatile uint32_t cycles)
{
    while (cycles--) {
        __asm__ __volatile__("nop");
    }
}

int main(void)
{
    DDRB |= LED_MASK;

    for (;;) {
        PORTB |= LED_MASK;
        delay_cycles(800000UL);
        PORTB &= (uint8_t)~LED_MASK;
        delay_cycles(800000UL);
    }
}

This code is intentionally simple. It uses a crude busy-wait so you can inspect the generated loop easily.

Build Commands

avr-gcc -mmcu=atmega328p -DF_CPU=16000000UL -Os -Wall -Wextra -std=c11 -c blink.c -o blink.o
avr-gcc -mmcu=atmega328p -Wl,-Map=blink.map blink.o -o blink.elf
avr-objcopy -O ihex -R .eeprom blink.elf blink.hex
avr-objdump -d -S blink.elf > blink.lst
avr-size --format=avr --mcu=atmega328p blink.elf

If your board uses a bootloader workflow, flash with the usual uploader for that board. If you use an ISP programmer, flash blink.hex with your normal device-programming command.

Expected Behavior

  • The onboard LED connected to PB5 should blink steadily.
  • The blink period should be approximate, not precision-grade, because the delay is software-based.
  • The build should generate blink.o, blink.elf, blink.hex, blink.map, and blink.lst.

What to Inspect in the Artifacts

blink.map

Check:

  • total Flash and RAM usage;
  • whether main and delay_cycles appear as expected;
  • whether unexpected libraries were pulled in.

blink.lst

Check:

  • the DDRB write that configures the pin direction;
  • the PORTB set and clear operations;
  • the loop implementation inside delay_cycles.

avr-size output

Check:

  • code size is small;
  • RAM usage is near zero because there are no global buffers.

Verification Steps

  1. Confirm the compile step emits no warnings.
  2. Confirm the linker creates blink.elf.
  3. Confirm blink.hex exists and is non-empty.
  4. Flash the image to the target.
  5. Observe the LED blinking repeatedly.
  6. Open blink.lst and verify there are explicit instructions corresponding to the port writes.
  7. Open blink.map and confirm code sections fit comfortably in device memory.

Common Failure Symptoms

Symptom Likely cause
LED never turns on wrong pin, wrong board, DDR bit not set
LED stays always on clear operation missing or loop optimized unexpectedly
LED blinks too fast or too slow wrong clock assumption or different optimization outcome
build fails at compile step syntax error or missing AVR headers
build fails at link step wrong MCU option or broken toolchain install
flash succeeds but nothing runs wrong target device, fuse issue, bad bootloader/programmer setup

Debugging Guidance

  • If the LED never toggles, inspect blink.lst before changing random code.
  • If timing is wrong, check F_CPU, optimization level, and whether the board actually runs at 16 MHz.
  • If the LED pin is different on your board, update LED_MASK and register selection accordingly.
  • If compile warnings appear, fix them first; do not normalize them.

Extension Challenge

Replace the crude busy-wait with a timer-based blink so the CPU is not tied up in delay loops. Then compare:

  • code size;
  • accuracy of blink rate;
  • readability of the disassembly;
  • how much of the work moved from software looping to hardware timing.

Concise Explained Solution

The program works because DDRB |= LED_MASK makes PB5 an output, then the main loop alternates between setting and clearing the corresponding output bit in PORTB. The delay loop simply burns cycles between those writes. The generated artifacts prove the workflow end to end: source becomes object code, object code is linked into an ELF, and the final HEX is flashed to hardware.

Common Mistakes

  • Copying commands without checking the target MCU name.
  • Forgetting that the onboard LED pin differs by board family.
  • Assuming the delay loop duration is portable across clock rates.
  • Skipping artifact inspection after a successful flash.

Summary

This exercise establishes a baseline embedded C workflow: write simple hardware-facing code, build it from the command line, inspect the outputs, and verify real hardware behavior. That workflow will remain useful even when later examples become much more complex.

Further Reading

Mind Map

mindmap root((Blink Build Exercise)) Prerequisites ATmega328P board LED on PB5 AVR GCC tools Flash method known Task Set DDRB bit Toggle PORTB bit Delay between states Build artifacts Flash and observe Code checks LED_MASK correct F_CPU matches board No warnings Busy wait intentional Main loops forever Artifact checks ELF linked HEX nonempty MAP fits memory LST shows port writes Size is small Debugging Wrong MCU flag Wrong board pin Clock mismatch Missing avr headers Programmer issue Extension Timer blink Compare size Compare accuracy Review disassembly Reduce CPU waste