DE6417 Microcontrollers 2 · Week 14
Explain the AVR toolchain: .c → .elf → .hex → flash.
Install and verify avr-gcc and avrdude on your machine.
Write a minimal ATmega328P C program using register names from avr/io.h.
Compile, convert, and flash the program to the standalone chip via Arduino as ISP.
avr-gcc do?avr/io.h provide?Plain C source file. Any text editor. No Arduino IDE.
C → ELF binary with debug symbols & section info.
main.elfELF → Intel HEX: addresses + data bytes only. No debug info.
main.hexHEX → chip via Arduino as ISP (same setup as Week 13).
Program executes from flash on every reset. No PC needed.
Option A — Homebrew:
brew tap osx-cross/avr
brew install avr-gcc
brew install avrdude
Option B — Arduino IDE bundled path (always works):
export PATH="$HOME/Library/Arduino15/packages/arduino/\
tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin:$PATH"
Verify: avr-gcc --version
Option A — WinAVR:
Option B — Arduino IDE bundled path:
Add that folder to System PATH. Open a new Command Prompt.
#include <avr/io.h>
#include <util/delay.h>
#define F_CPU 16000000UL /* must match oscillator */
int main(void) {
DDRB |= (1 << PB5); /* PB5 = output */
while (1) {
PORTB |= (1 << PB5); /* drive HIGH — LED on */
_delay_ms(500);
PORTB &= ~(1 << PB5); /* drive LOW — LED off */
_delay_ms(500);
}
return 0; /* never reached */
}
| Line | What it does |
|---|---|
| <avr/io.h> | Maps DDRB, PORTB, PB5 to their hardware addresses for ATmega328P |
| <util/delay.h> | Provides _delay_ms() — uses F_CPU to count cycles |
| F_CPU | Must equal 16 000 000 — your 16 MHz crystal |
| int main(void) | Entry point — the chip jumps here after reset |
| while (1) | Infinite loop — the equivalent of Arduino's loop() |
| DDRB |= (1<<PB5) | Set bit 5 of DDRB → PB5 is an output |
| PORTB |= (1<<PB5) | Drive PB5 HIGH |
| PORTB &= ~(1<<PB5) | Drive PB5 LOW |
#include <avr/io.h>
#include <util/delay.h>
#define F_CPU 16000000UL
int main(void) {
DDRB |= (1 << PB5);
while (1) {
PORTB |= (1 << PB5);
_delay_ms(500);
PORTB &= ~(1 << PB5);
_delay_ms(500);
}
return 0;
}
avr/io.h: What It Gives You/* From avr/iom328p.h */
#define DDRB (*(volatile uint8_t *)0x24)
#define PORTB (*(volatile uint8_t *)0x25)
#define PB5 5
What this means:
F_CPU and _delay_ms()_delay_ms() worksEither in source (before the include):
#define F_CPU 16000000UL
#include <util/delay.h>
Or as a compiler flag:
-DF_CPU=16000000UL
| F_CPU value | Crystal | Effect on _delay_ms(500) |
|---|---|---|
| 16000000UL | 16 MHz | ✅ Delays 500 ms as intended |
| 8000000UL | 16 MHz | ❌ Delays 1000 ms (twice as long) |
| 1000000UL | 16 MHz | ❌ Delays 8000 ms (16× too long) |
F_CPU?F_CPU is set to 8000000UL but your crystal runs at 16 MHz, what happens to _delay_ms(500)?avr-gcc -mmcu=atmega328p -DF_CPU=16000000UL -Os -Wall -o main.elf main.c
| Flag | Meaning |
|---|---|
| -mmcu=atmega328p | Target microcontroller — sets memory map, linker script, and device header. This is the flag that makes the output specific to the ATmega328P. |
| -DF_CPU=16000000UL | Preprocessor define — same as writing #define F_CPU 16000000UL in the source. Must match hardware oscillator. |
| -Os | Optimise for code size — important on a chip with only 32 KB of flash. |
| -Wall | Enable all warnings — catches bugs before they reach the chip. |
| -o main.elf | Output filename — the compiled ELF binary. |
| main.c | Input source file. |
avr-objcopyavr-objcopy -O ihex -R .eeprom main.elf main.hex
| Flag | Meaning |
|---|---|
| -O ihex | Output format = Intel HEX |
| -R .eeprom | Remove (exclude) the EEPROM section from the output — flash memory holds program code; EEPROM is a separate storage area for persistent data. We only need the flash content. |
| main.elf | Input — the compiled binary from avr-gcc |
| main.hex | Output — Intel HEX file for avrdude |
Run cat main.hex (macOS) or type main.hex (Windows). Every line starts with a colon:
:10000000...
:10001000...
:00000001FF
avrdudeavrdude -c stk500v1 -p m328p \
-P /dev/tty.usbmodemXXXX \
-b 19200 \
-U flash:w:main.hex:i
avrdude -c stk500v1 -p m328p ^
-P COM3 ^
-b 19200 ^
-U flash:w:main.hex:i
avrdude: 176 bytes of flash written
avrdude: verifying flash memory against main.hex
avrdude done. Thank you.
| Flag | Meaning |
|---|---|
| -c stk500v1 | Programmer protocol — ArduinoISP uses STK500v1 |
| -p m328p | Target device — m328p = ATmega328P |
| -P /dev/tty... | Serial port of the programmer Uno (Uno A from Week 13) |
| -b 19200 | Baud rate — ArduinoISP uses 19200, not 115200. If wrong: "not in sync." |
| -U flash:w:main.hex:i | Write (w) main.hex in Intel HEX format (:i) to flash memory |
void setup() {
pinMode(13, OUTPUT);
}
void loop() {
digitalWrite(13, HIGH);
delay(500);
digitalWrite(13, LOW);
delay(500);
}
#include <avr/io.h>
#include <util/delay.h>
#define F_CPU 16000000UL
int main(void) {
DDRB |= (1 << PB5);
while (1) {
PORTB ^= (1 << PB5);
_delay_ms(500);
}
}
In the next 1.5 hours you will:
Goal: bare-metal C → avr-gcc → avr-objcopy → avrdude → ATmega328P blinks
Write a main.c file in a text editor, compile it with avr-gcc, convert it with avr-objcopy, and flash it to the standalone ATmega328P using the Arduino as ISP setup from Week 13.
| Time | Task |
|---|---|
| 0:00 – 0:10 | Verify toolchain — every student must get a version number |
| 0:10 – 0:30 | Part 1: Write main.c, compile, convert, flash (basic blink) |
| 0:30 – 0:50 | Part 2: 3-state interrupt-driven button control |
| 0:50 – 1:20 | Verify results, troubleshoot, attempt extensions |
| 1:20 – 1:30 | Debrief and course wrap-up |
| Programmer Pin | Signal | Target Pin |
|---|---|---|
| D10 | RESET | Pin 1 (PC6) |
| D11 | MOSI | Pin 17 (PB3) |
| D12 | MISO | Pin 18 (PB4) |
| D13 | SCK | Pin 19 (PB5) |
| 5V | VCC | Pin 7 + Pin 20 |
| GND | GND | Pin 8 + Pin 22 |
avr-gcc --version
avrdude -v
macOS — add Arduino IDE bundled path:
export PATH="/Applications/Arduino.app/Contents/Java/\
hardware/tools/avr/bin:$PATH"
Windows — add to system PATH, then reopen terminal:
main.c#include <avr/io.h>
#include <util/delay.h>
#define F_CPU 16000000UL
int main(void) {
DDRB |= (1 << PB5);
while (1) {
PORTB |= (1 << PB5);
_delay_ms(500);
PORTB &= ~(1 << PB5);
_delay_ms(500);
}
return 0;
}
Fill in the two missing flags:
Fill in the missing flag:
Fill in the missing baud flag:
avr-gcc -mmcu=atmega328p -DF_CPU=16000000UL \
-Os -Wall -o main.elf main.c
avr-objcopy -O ihex -R .eeprom main.elf main.hex
After both commands, verify files exist:
ls -lh # macOS
dir # Windows
| Error | Cause | Fix |
|---|---|---|
| avr/io.h: No such file | avr-libc not found on PATH | Use Arduino IDE bundled path (slide 20) |
| warning: implicit declaration | Missing #include <util/delay.h> | Check includes at top of file |
| undefined reference to main | File saved with wrong name or wrong directory | Confirm ls / dir shows main.c in current folder |
| No such file: main.c | Not in the project folder in terminal | Run cd ~/Desktop/blink_baremetal |
ls /dev/tty.*
Look for tty.usbmodem... or tty.usbserial...
Open Device Manager → expand Ports (COM & LPT) → find USB Serial Device or Arduino Uno. Note the COM number (e.g., COM3).
macOS:
avrdude -c stk500v1 -p m328p \
-P /dev/tty.usbmodemXXXX \
-b 19200 \
-U flash:w:main.hex:i
Windows:
avrdude -c stk500v1 -p m328p -P COM3 -b 19200 -U flash:w:main.hex:i
avrdude: 176 bytes of flash written
avrdude: verifying flash memory against main.hex
avrdude done. Thank you.
Add a push button that cycles the LED through three states: OFF → Slow Blink → Fast Blink. Use external interrupts to detect button presses without polling.
Extend your blink program to respond to button presses. Each press cycles through three states:
Add these components:
| Component | Connection | Notes |
|---|---|---|
| Push button (one leg) | Pin 4 (PD2/INT0) | External interrupt 0 |
| Push button (other leg) | 5V rail | Active-high configuration |
| 10kΩ resistor | Pin 4 → GND | Pull-down (LOW when not pressed) |
Button behavior:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#define F_CPU 16000000UL
volatile uint8_t state = 0; // 0=OFF, 1=SLOW, 2=FAST
// INT0 interrupt service routine (button press)
ISR(INT0_vect) {
_delay_ms(50); // Simple debounce
state = (state + 1) % 3; // Cycle: 0 → 1 → 2 → 0
}
int main(void) {
// Configure PB5 (LED) as output
DDRB |= (1 << PB5);
// Configure PD2 (INT0) as input with pull-down external resistor
DDRD &= ~(1 << PD2);
// Enable INT0 on rising edge (LOW → HIGH)
EICRA |= (1 << ISC01) | (1 << ISC00); // Rising edge
EIMSK |= (1 << INT0); // Enable INT0
sei(); // Enable global interrupts
while (1) {
if (state == 0) {
// State 0: LED OFF
PORTB &= ~(1 << PB5);
_delay_ms(100); // Small delay to keep loop responsive
}
else if (state == 1) {
// State 1: Slow blink (1 second)
PORTB ^= (1 << PB5);
_delay_ms(1000);
}
else if (state == 2) {
// State 2: Fast blink (200 ms)
PORTB ^= (1 << PB5);
_delay_ms(200);
}
}
return 0;
}
volatile tells the compiler this variable can change at any time (via ISR). Prevents optimization bugs.
avr-gcc -mmcu=atmega328p -DF_CPU=16000000UL -Os -Wall -o main.elf main.c
avr-objcopy -O ihex -R .eeprom main.elf main.hex
avrdude -c stk500v1 -p m328p -P /dev/tty.usbmodemXXXX -b 19200 -U flash:w:main.hex:i
% 3 to % 4 and add another else if branch.
Change _delay_ms(500) to _delay_ms(100). Recompile, reconvert, reflash. Observe the change in real time.
Replace the two PORTB lines with PORTB ^= (1 << PB5);. How many bytes smaller is the new hex?
Add a second LED on PB4 (physical pin 18). Set DDRB |= (1 << PB4) and blink it in opposition to PB5.
Use Timer 1 CTC mode + overflow ISR instead of _delay_ms(). The same registers you used all semester — now in bare-metal C.