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:
- configures PB5 as an output;
- drives PB5 high;
- waits in a software delay loop;
- drives PB5 low;
- repeats forever.
Then:
- build the program;
- create a map file and HEX file;
- dump disassembly;
- 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, andblink.lst.
What to Inspect in the Artifacts
blink.map
Check:
- total Flash and RAM usage;
- whether
mainanddelay_cyclesappear as expected; - whether unexpected libraries were pulled in.
blink.lst
Check:
- the
DDRBwrite that configures the pin direction; - the
PORTBset 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
- Confirm the compile step emits no warnings.
- Confirm the linker creates
blink.elf. - Confirm
blink.hexexists and is non-empty. - Flash the image to the target.
- Observe the LED blinking repeatedly.
- Open
blink.lstand verify there are explicit instructions corresponding to the port writes. - Open
blink.mapand 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.lstbefore 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_MASKand 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.