Back
📦 macOS AVR Toolchain Installation Guide
Reference

Bare-Metal C: Writing Code the Arduino Way — Minus Arduino

Programming the ATmega328P Without the Arduino Framework

DE6417 Microcontrollers 2  ·  Week 14

1.5h Lecture 1.5h Hands-on Lab ATmega328P Required

What We Are Doing Today

Week 13 — what you already know

  • The ATmega328P can be programmed with 4 SPI wires
  • An Arduino Uno running ArduinoISP acts as the programmer
  • avrdude handles the serial protocol and writes the .hex to flash
  • No USB socket, no bootloader needed on the target chip
Week 13 answer: how to get code onto the chip.

Week 14 — today's goal

  • Write the code yourself — in plain C, no Arduino framework
  • Compile a .c file into a .hex file using avr-gcc
  • Flash that hex using the ISP setup from Week 13
  • No Arduino IDE for the code step — just a text editor and a terminal
Key insight: All semester you wrote TCCR1A = ... and OCR1A = .... Those are C statements. Today we remove the wrapper and write just those statements.

Learning Outcomes

🔗

Describe the pipeline

Explain the AVR toolchain: .c.elf.hex → flash.

🛠️

Install & verify

Install and verify avr-gcc and avrdude on your machine.

💻

Write bare-metal C

Write a minimal ATmega328P C program using register names from avr/io.h.

Compile, convert, flash

Compile, convert, and flash the program to the standalone chip via Arduino as ISP.

What Arduino IDE Is Actually Doing

Arduino IDE wraps three existing tools

  • avr-gcc — the C/C++ compiler for AVR microcontrollers (exists independently of Arduino)
  • avr-libc — the C standard library for AVR; provides avr/io.h, util/delay.h, and every register name you have used all semester
  • avrdude — the flasher that talks to the chip over serial/ISP

What Arduino.h adds on top

  • setup() / loop() call structure
  • pinMode(), digitalWrite(), analogRead(), delay()
  • All of these are C functions — they call the same registers you write directly
pinMode(13, OUTPUT) compiles to DDRB |= (1 << 5).
They are identical at the hardware level.
Your Code (.ino / .c file)
Arduino.h / Arduino Core
setup(), loop(), pinMode(), digitalWrite()…
avr-gcc  ·  avr-libc  ·  avrdude
the real tools (today we use these directly)
ATmega328P (silicon)

Quick Check — The Toolchain

Q1: What does avr-gcc do?

Q2: What does avr/io.h provide?

The AVR Toolchain Pipeline

1. Write main.c

Plain C source file. Any text editor. No Arduino IDE.

2. Compile avr-gcc

C → ELF binary with debug symbols & section info.

main.elf
3. Convert avr-objcopy

ELF → Intel HEX: addresses + data bytes only. No debug info.

main.hex
4. Flash avrdude

HEX → chip via Arduino as ISP (same setup as Week 13).

5. Run ATmega328P

Program executes from flash on every reset. No PC needed.

At which stage does the code become specific to the ATmega328P?
At avr-gcc — the -mmcu=atmega328p flag sets the memory map, linker script, and device header. Before that, it is just C source.

Installing avr-gcc: macOS and Windows

macOS

Option A — Homebrew:

brew tap osx-cross/avr
brew install avr-gcc
brew install avrdude

Option B — Arduino IDE bundled path (always works):

~/Library/Arduino15/packages/arduino/tools/avr-gcc/
export PATH="$HOME/Library/Arduino15/packages/arduino/\
tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin:$PATH"

Verify: avr-gcc --version

Windows

Option A — WinAVR:

  • Download from winavr.sourceforge.net
  • Installer adds avr-gcc, avr-libc, avrdude to PATH automatically
  • Open a new Command Prompt after install

Option B — Arduino IDE bundled path:

C:\Program Files (x86)\Arduino\hardware\tools\avr\bin\

Add that folder to System PATH. Open a new Command Prompt.

Verify (both OS):
avr-gcc --version
avrdude -v
Any version number = success.

The Minimal C Program

#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-by-line

LineWhat 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_CPUMust 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
Every register name (DDRB, PORTB, PB5) is one you have already used all semester. The only new thing is the build pipeline.

Code Annotation — What Does Each Line Do?

The blink program

#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;
}

Match the line to its effect

DDRB |= (1 << PB5)
PORTB |= (1 << PB5)
PORTB &= ~(1 << PB5)
#define F_CPU 16000000UL

avr/io.h: What It Gives You

How it works

  • avr/io.h is the device-selection header — it includes the correct device-specific file based on the -mmcu flag at compile time
  • For ATmega328P, it includes avr/iom328p.h which defines every register as a macro pointing to a fixed memory address
  • This is the same header file Arduino uses internally — it is not an Arduino invention
  • volatile tells the compiler: do not optimise this read/write away — the hardware can change the register value independently of your code
"DDRB is not an Arduino thing. It is the Data Direction Register B, documented on page 90 of the ATmega328P datasheet. avr/io.h simply gives it a name."

The actual macro definitions

/* From avr/iom328p.h */
#define DDRB  (*(volatile uint8_t *)0x24)
#define PORTB (*(volatile uint8_t *)0x25)
#define PB5   5

What this means:

  • 0x24 — the hardware address of Data Direction Register B
  • volatile uint8_t * — pointer to a 1-byte hardware register
  • The dereference *(...) makes it behave like a variable
  • PB5 = 5 — just the bit number, used in bit-shift expressions
Open the ATmega328P datasheet and find DDRB at address 0x24. The header file and the datasheet agree — this is the bridge between software and silicon.

F_CPU and _delay_ms()

How _delay_ms() works

  • _delay_ms(500) is not a runtime function call — it is a compile-time calculation
  • The compiler expands it to a counted loop of NOP instructions
  • Number of NOPs = (500 ms × F_CPU Hz) ÷ cycles_per_loop
  • Result: the CPU wastes exactly 500 ms worth of cycles before continuing
Rule: F_CPU must always match the actual hardware oscillator frequency.

Where to set it

Either in source (before the include):

#define F_CPU 16000000UL
#include <util/delay.h>

Or as a compiler flag:

-DF_CPU=16000000UL

What happens when F_CPU is wrong

F_CPU valueCrystalEffect on _delay_ms(500)
16000000UL16 MHz✅ Delays 500 ms as intended
8000000UL16 MHz❌ Delays 1000 ms (twice as long)
1000000UL16 MHz❌ Delays 8000 ms (16× too long)
The compiler cannot measure hardware. You must tell it the clock frequency. If you get the wrong value, all delays are proportionally wrong — the chip still runs, but timing is broken.
Note: The UL suffix means unsigned long. Required because 16 000 000 overflows a plain int on AVR (which is 16-bit).

F_CPU Check

Q1: Your hardware uses a 16 MHz crystal. What is the correct value of F_CPU?

Q2: If F_CPU is set to 8000000UL but your crystal runs at 16 MHz, what happens to _delay_ms(500)?

The Compile Command, Explained

The full command

avr-gcc -mmcu=atmega328p -DF_CPU=16000000UL -Os -Wall -o main.elf main.c
FlagMeaning
-mmcu=atmega328pTarget microcontroller — sets memory map, linker script, and device header. This is the flag that makes the output specific to the ATmega328P.
-DF_CPU=16000000ULPreprocessor define — same as writing #define F_CPU 16000000UL in the source. Must match hardware oscillator.
-OsOptimise for code size — important on a chip with only 32 KB of flash.
-WallEnable all warnings — catches bugs before they reach the chip.
-o main.elfOutput filename — the compiled ELF binary.
main.cInput source file.
Success indicator: No output, prompt returns, main.elf appears in the folder. Any error message means something needs fixing before continuing.

Convert to HEX with avr-objcopy

Why a second step?

  • main.elf — the output of avr-gcc — contains code, data, debug symbols, and section information. The chip cannot use this directly.
  • main.hex — Intel HEX format: just addresses and data bytes. No debug info. This is what avrdude sends to the chip.

The command

avr-objcopy -O ihex -R .eeprom main.elf main.hex
FlagMeaning
-O ihexOutput format = Intel HEX
-R .eepromRemove (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.elfInput — the compiled binary from avr-gcc
main.hexOutput — Intel HEX file for avrdude

Sanity check

Run cat main.hex (macOS) or type main.hex (Windows). Every line starts with a colon:

:10000000...
:10001000...
:00000001FF
A valid HEX file always starts with : on every line. The last line is :00000001FF — the end-of-file record.
Common mistake: Passing main.elf directly to avrdude instead of main.hex. avrdude will reject it with a confusing error. The pipeline is: compile → convert → flash.

Flash via avrdude

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

Expected output on success

avrdude: 176 bytes of flash written
avrdude: verifying flash memory against main.hex
avrdude done.  Thank you.

Flag reference

FlagMeaning
-c stk500v1Programmer protocol — ArduinoISP uses STK500v1
-p m328pTarget device — m328p = ATmega328P
-P /dev/tty...Serial port of the programmer Uno (Uno A from Week 13)
-b 19200Baud rate — ArduinoISP uses 19200, not 115200. If wrong: "not in sync."
-U flash:w:main.hex:iWrite (w) main.hex in Intel HEX format (:i) to flash memory
The baud rate catches everyone. Commands copied from the internet often use 115200. ArduinoISP uses 19200 for reliability. Use -b 19200 — always.

Arduino Blink vs Bare-Metal Blink

Arduino Blink (.ino)
void setup() {
    pinMode(13, OUTPUT);
}

void loop() {
    digitalWrite(13, HIGH);
    delay(500);
    digitalWrite(13, LOW);
    delay(500);
}
Compiled .hex size: ~1,700 bytes
Includes Arduino core init, timer setup, UART init, full runtime.
Bare-Metal Blink (.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);
    }
}
Compiled .hex size: ~170 bytes
Only the code you wrote. Nothing else.
Behaviour: identical. Both programs blink pin 13 (PB5) at 0.5 Hz. Arduino is a convenience layer — the hardware sees the same result from both. The bare-metal version is 10× smaller.

Lecture Summary

What we covered

  • Arduino IDE wraps avr-gcc, avr-libc, and avrdude — you can use these tools directly
  • avr/io.h maps register names (DDRB, PORTB, PB5) to hardware addresses — the same header Arduino uses
  • F_CPU must match the hardware oscillator or all delays are wrong
  • Pipeline: avr-gcc.elfavr-objcopy.hexavrdude → chip
  • Baud rate: -b 19200 for ArduinoISP — not 115200
  • Bare-metal code is 10× smaller than the same program with the Arduino framework

Lab preview

In the next 1.5 hours you will:

  1. Verify avr-gcc and avrdude are installed and reachable
  2. Create main.c in a text editor
  3. Compile it to main.elf with avr-gcc
  4. Convert to main.hex with avr-objcopy
  5. Flash to the standalone ATmega328P via Arduino as ISP
  6. Observe the LED blink — no Arduino IDE involved
The ISP circuit from Week 13 is still the programmer. Today the code going in is yours, written from scratch in plain C.

Lab Block

Write, Compile, Convert, Flash

Goal: bare-metal C → avr-gcc → avr-objcopy → avrdude → ATmega328P blinks

Lab Brief

Goal

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.

Do NOT open Arduino IDE for the code step. The terminal is the IDE today.

Materials

  • Arduino Uno (programmer — running ArduinoISP from Week 13)
  • Standalone ATmega328P-PU on breadboard with ISP wiring
  • 10 µF capacitor on programmer's RESET → GND
  • 6 ISP jumper wires (D10→Pin1, D11→Pin17, D12→Pin18, D13→Pin19, 5V, GND)
  • LED + 220Ω resistor → connect to Pin 19 (PB5) and GND
  • Push button + 10kΩ resistor → for Part 2 (interrupt-driven control)
  • Any text editor (VS Code, Notepad++, TextEdit in plain text mode)
  • Terminal / Command Prompt
⚠️ Hardware from Week 13 must still be wired. If you disassembled the ISP circuit, rebuild it now using the Week 13 wiring table before starting the software steps.

Time box

TimeTask
0:00 – 0:10Verify toolchain — every student must get a version number
0:10 – 0:30Part 1: Write main.c, compile, convert, flash (basic blink)
0:30 – 0:50Part 2: 3-state interrupt-driven button control
0:50 – 1:20Verify results, troubleshoot, attempt extensions
1:20 – 1:30Debrief and course wrap-up
Do not proceed to Step 2 until every student has a working avr-gcc --version. Toolchain problems discovered at compile time cost 30 minutes.

Hardware Setup: Arduino as ISP (Week 13 Recap)

Before starting the lab, confirm:

Programmer Uno has ArduinoISP sketch uploaded
File → Examples → 11. ArduinoISP → ArduinoISP (uploaded in Week 13)
10 µF capacitor on programmer RESET → GND
Negative leg (stripe) to GND — prevents auto-reset
ATmega328P seated on breadboard
Pin 1 notch at top-left, VCC to 5V rail, GND to GND rail
LED + 220Ω resistor on Pin 19 (PB5) → GND
Anode (long leg) to Pin 19, resistor to cathode, cathode to GND rail
All 6 ISP connections wired correctly
D10→Pin1, D11→Pin17, D12→Pin18, D13→Pin19, 5V→Pin7+20, GND→Pin8+22

ISP Wiring Quick Reference

Programmer Pin Signal Target Pin
D10RESETPin 1 (PC6)
D11MOSIPin 17 (PB3)
D12MISOPin 18 (PB4)
D13SCKPin 19 (PB5)
5VVCCPin 7 + Pin 20
GNDGNDPin 8 + Pin 22
If any wire is missing or wrong, avrdude will fail. Double-check every connection before running commands. The D13 LED will flicker during programming — this is normal.

Step 1: Verify the Toolchain

Run in terminal

avr-gcc --version
avrdude -v
Expected output:
avr-gcc (GCC) 12.x.x
avrdude version 7.x
Any version number = success. The exact version does not matter.

If avr-gcc is not found

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:

C:\Program Files (x86)\Arduino\hardware\tools\avr\bin
Do not proceed to Step 2 until avr-gcc --version returns a version number. This is the single most important check of the lab.

Step 2: Write main.c

Instructions

  1. Create a new folder on your Desktop: blink_baremetal
  2. Open any text editor (VS Code, Notepad++, Notepad, TextEdit in plain text mode)
  3. Create a new file: main.c inside blink_baremetal/
  4. Type the program from Slide 8 — type it, don't paste it
  5. Save the file
Students who type this once remember it. Students who paste it do not. Three minutes of typing beats one second of paste.
macOS: Open Terminal → cd ~/Desktop/blink_baremetal → verify: ls shows main.c
Windows: Address bar in Explorer → type cmd → Enter → verify: dir shows 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;
}

Command Builder — Fill in the Flags

1 — Compile

Fill in the two missing flags:

avr-gcc [mmcu flag] [F_CPU flag] -Os -Wall -o main.elf main.c

2 — Convert

Fill in the missing flag:

avr-objcopy -O ihex [flag] main.elf main.hex

3 — Flash (macOS)

Fill in the missing baud flag:

avrdude -c stk500v1 -p m328p -P /dev/tty.usbmodemXXXX [baud flag] -U flash:w:main.hex:i

Step 3: Compile and Convert

Run these two commands in order

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
You should see main.c, main.elf, and main.hex. A clean compile with no output = success.

Common errors and fixes

ErrorCauseFix
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

Step 4: Find Your Port

macOS

ls /dev/tty.*

Look for tty.usbmodem... or tty.usbserial...

If multiple entries appear: unplug Uno A, run the command again. The entry that disappears is Uno A's port. Re-plug and use that name.
This is the port of Uno A (the programmer) — the same board running ArduinoISP from Week 13.

Windows

Open Device Manager → expand Ports (COM & LPT) → find USB Serial Device or Arduino Uno. Note the COM number (e.g., COM3).

Same trick: unplug Uno A, observe which COM port disappears from Device Manager. That is the correct port.

Substitute into the avrdude command

macOS
-P /dev/tty.usbmodem1234
Windows
-P COM3

Pre-Flash Checklist

Toolchain & files

Hardware & avrdude

Step 5: Flash and Observe

Run avrdude (substitute your port)

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

Expected output

avrdude: 176 bytes of flash written
avrdude: verifying flash memory against main.hex
avrdude done.  Thank you.
✅ After successful flash: The LED on Pin 19 (PB5) should blink every 500 ms. If you see "done. Thank you." but no LED, check your LED wiring (anode to Pin 19, resistor + cathode to GND).

Observation questions

  • Which tool — avr-gcc, avr-objcopy, or avrdude — was the last to touch your code before the chip?
  • The Arduino IDE was never opened during this process. What did it provide before?
  • Could you use a USBasp programmer instead of Uno A? (Yes — change -c stk500v1 to -c usbasp and remove -b 19200)
  • What would you change to make the LED blink 3× faster?
Students who get this working have completed the full pipeline used in professional embedded development: text editor → compiler → converter → flasher → chip.

Part 2

Interrupt-Driven 3-State Button Control

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.

Part 2 Brief: 3-State Button Control with Interrupts

The Challenge

Extend your blink program to respond to button presses. Each press cycles through three states:

State 0
LED OFF
💡
State 1
Slow blink (1 sec)
State 2
Fast blink (200 ms)
No polling allowed. Use INT0 external interrupt to detect button presses. The main loop only handles blinking based on the current state.

Hardware Setup

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)
Keep the LED on Pin 19 (PB5). You're adding the button circuit, not replacing the LED.

Button behavior:

  • Not pressed: Pin 4 reads LOW (pulled to GND by 10kΩ)
  • Pressed: Pin 4 reads HIGH (connected to 5V)
  • Interrupt triggers on rising edge (LOW → HIGH)

Part 2: The Code

#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;
}

Key Concepts

volatile uint8_t state
volatile tells the compiler this variable can change at any time (via ISR). Prevents optimization bugs.
ISR(INT0_vect)
Interrupt service routine. Executes automatically when INT0 detects a rising edge on PD2.
EICRA register
External Interrupt Control Register A. ISC01=1, ISC00=1 → rising edge trigger.
EIMSK register
External Interrupt Mask Register. Setting bit INT0 enables the INT0 interrupt.
sei()
Set Enable Interrupts — enables the global interrupt flag (I-bit in SREG).

Part 2: Compile, Flash, and Test

Steps

  1. Wire the button: One leg to Pin 4, other leg to 5V. Add 10kΩ from Pin 4 to GND.
  2. Update main.c: Replace the Part 1 code with the Part 2 code from Slide 30.
  3. Compile:
    avr-gcc -mmcu=atmega328p -DF_CPU=16000000UL -Os -Wall -o main.elf main.c
  4. Convert:
    avr-objcopy -O ihex -R .eeprom main.elf main.hex
  5. Flash:
    avrdude -c stk500v1 -p m328p -P /dev/tty.usbmodemXXXX -b 19200 -U flash:w:main.hex:i

Expected Behavior

✅ Initial state: LED is OFF (state = 0)
✅ After 1st button press: LED blinks slowly (1 second on, 1 second off)
✅ After 2nd button press: LED blinks fast (200 ms on, 200 ms off)
✅ After 3rd button press: LED turns OFF again (state wraps to 0)
Debouncing: The 50 ms delay in the ISR prevents multiple state changes from a single button press. Mechanical buttons "bounce" — they make and break contact several times when pressed.
Challenge: Can you modify the code to add a 4th state (very fast blink at 50 ms)? Change the modulo from % 3 to % 4 and add another else if branch.

Troubleshooting — What Do You See?

Select your symptom

Debug order

  1. Toolchain: avr-gcc --version returns a version number?
  2. Files: Both main.elf and main.hex exist in the project folder?
  3. Port: Is the correct /dev/tty... or COM# in the avrdude command?
  4. Baud: Is -b 19200 in the avrdude command (not 115200)?
  5. Wiring: All 6 ISP wires correct? Cap on programmer Uno?
  6. LED: LED + 220Ω resistor on Pin 19 (PB5) → GND? Anode (long leg) to Pin 19?
  7. F_CPU: #define F_CPU 16000000UL is in main.c?
Read the exact avrdude error message. Each one points to a different cause.

Extension Tasks + Course Wrap-Up

Extension tasks (if you finish early)

1

Change blink speed

Change _delay_ms(500) to _delay_ms(100). Recompile, reconvert, reflash. Observe the change in real time.

2

Toggle with XOR

Replace the two PORTB lines with PORTB ^= (1 << PB5);. How many bytes smaller is the new hex?

3

Two LEDs

Add a second LED on PB4 (physical pin 18). Set DDRB |= (1 << PB4) and blink it in opposition to PB5.

4

Timer 1 instead of delay

Use Timer 1 CTC mode + overflow ISR instead of _delay_ms(). The same registers you used all semester — now in bare-metal C.

Two-week summary

  • Week 13: The ATmega328P can be programmed with 4 SPI wires + an Arduino running ArduinoISP. No USB socket, no bootloader, no Uno board needed on the target.
  • Week 14: Code for that chip can be written in plain C, compiled with avr-gcc, converted with avr-objcopy, and flashed with avrdude. Arduino IDE is optional — it is a convenience, not a requirement.
"All semester, every time you wrote TCCR1A |= (1 << WGM11), you were writing embedded C. The only difference between then and today is one layer of wrapper. Now you know what was in that wrapper — and you can build without it."
Exit question:
"Name the three tools that the Arduino IDE hides from you, and one reason you might want to use them directly."