Project 15: Bare Metal LED Blinker

Write a bare-metal program that toggles an LED by writing to hardware registers.

Quick Reference

Attribute Value
Difficulty Expert
Time Estimate 2-4 weeks
Language C (Alternatives: Assembly, Rust)
Prerequisites Memory-mapped I/O, toolchain setup
Key Topics Registers, linker scripts, startup code

1. Learning Objectives

By completing this project, you will:

  1. Configure a microcontroller toolchain.
  2. Write startup code and linker scripts.
  3. Control hardware via memory-mapped registers.
  4. Debug bare-metal code with OpenOCD/GDB.

2. Theoretical Foundation

2.1 Core Concepts

  • Memory-mapped I/O: Hardware registers appear as addresses.
  • Reset vectors: The CPU jumps to a known address on boot.
  • Linker scripts: Control memory layout of firmware.

2.2 Why This Matters

Bare-metal programming is the purest form of C systems work. It teaches how software directly controls hardware without an OS.

2.3 Historical Context / Background

Embedded systems have used C for decades because it offers deterministic control over memory and registers.

2.4 Common Misconceptions

  • “You can use printf”: Not without a runtime and I/O setup.
  • “Registers are variables”: They are fixed memory addresses.

3. Project Specification

3.1 What You Will Build

A firmware binary that:

  • Configures a GPIO pin for output
  • Toggles the pin on a fixed interval
  • Runs without any OS or standard library

3.2 Functional Requirements

  1. Provide startup and vector table.
  2. Configure GPIO registers correctly.
  3. Implement a simple delay loop or timer.
  4. Blink LED at a visible rate.

3.3 Non-Functional Requirements

  • Reliability: Correct register addresses.
  • Portability: Target a specific MCU family.
  • Safety: Use volatile for registers.

3.4 Example Usage / Output

$ arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -nostdlib \
    -T linker.ld -o blink.elf startup.c main.c
$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
    -c "program blink.elf verify reset exit"
# Physical result: LED blinks at 1Hz

3.5 Real World Outcome

You flash your board and see the LED blink at a steady rate. This is proof that your C code is running on bare hardware without any OS.


4. Solution Architecture

4.1 High-Level Design

reset -> startup -> main -> gpio init -> loop toggle

4.2 Key Components

Component Responsibility Key Decisions
Startup code Set stack, init data Minimal runtime
Linker script Place sections Match MCU memory
GPIO driver Set pin mode Use register defines

4.3 Data Structures

#define GPIO_BASE 0x40020000
#define GPIO_MODER (*(volatile unsigned int *)(GPIO_BASE + 0x00))
#define GPIO_ODR   (*(volatile unsigned int *)(GPIO_BASE + 0x14))

4.4 Algorithm Overview

Key Algorithm: Blink loop

  1. Configure GPIO pin as output.
  2. Set pin high, delay.
  3. Set pin low, delay.

Complexity Analysis:

  • Time: O(1) per loop
  • Space: O(1)

5. Implementation Guide

5.1 Development Environment Setup

# Install ARM GCC toolchain and OpenOCD
arm-none-eabi-gcc --version
openocd --version

5.2 Project Structure

blink/
├── src/
│   ├── startup.c
│   ├── main.c
│   └── linker.ld
├── scripts/
│   └── flash.sh
└── README.md

5.3 The Core Question You’re Answering

“How does C code control physical hardware without an operating system?”

5.4 Concepts You Must Understand First

Stop and research these before coding:

  1. Memory-mapped I/O
    • Why volatile is required.
  2. Startup code
    • What happens before main() runs?
  3. Linker scripts
    • How code/data map to flash and RAM.

5.5 Questions to Guide Your Design

Before implementing, think through these:

  1. What GPIO pin is connected to the LED?
  2. What clock setup is required for that GPIO?
  3. Will you use a busy-wait or hardware timer?

5.6 Thinking Exercise

Boot Sequence

What does the CPU execute first after reset, and how does it reach main()?

5.7 The Interview Questions They’ll Ask

Prepare to answer these:

  1. “What is memory-mapped I/O?”
  2. “Why does bare-metal code need a linker script?”
  3. “What does volatile do for hardware registers?”

5.8 Hints in Layers

Hint 1: Start from a known board Use an STM32 or similar dev board.

Hint 2: Use reference manuals Find GPIO register addresses in the datasheet.

Hint 3: Start with vendor startup code Simplify bring-up by reusing a template.

5.9 Books That Will Help

Topic Book Chapter
Embedded systems “Making Embedded Systems” Ch. 1-4
ARM Cortex-M “The Definitive Guide to ARM Cortex-M3/M4” GPIO chapters

5.10 Implementation Phases

Phase 1: Foundation (5-7 days)

Goals:

  • Toolchain and flashing

Tasks:

  1. Build a minimal main.
  2. Flash and verify execution.

Checkpoint: Board runs code (even if LED not yet).

Phase 2: Core Functionality (7-10 days)

Goals:

  • GPIO control

Tasks:

  1. Configure pin mode.
  2. Toggle pin in loop.

Checkpoint: LED blinks.

Phase 3: Polish & Edge Cases (4-6 days)

Goals:

  • Timing accuracy and debug

Tasks:

  1. Replace busy-wait with timer.
  2. Use GDB to inspect registers.

Checkpoint: Stable blink rate and debug visibility.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Delay Busy-wait vs timer Busy-wait first Simpler bring-up
Startup Custom vs template Template Reduce risk

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Smoke Tests Program runs LED toggles
Debug Tests Register state GDB inspect
Timing Tests Blink rate Measure frequency

6.2 Critical Test Cases

  1. GPIO configured: Output mode set correctly.
  2. Toggle loop: Pin changes state.
  3. Startup: main executes.

6.3 Test Data

GPIO register addresses from datasheet

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Missing volatile No LED change Use volatile pointers
Wrong pin register No output Verify datasheet
Bad linker script Crash on boot Validate memory regions

7.2 Debugging Strategies

  • Use OpenOCD + GDB to read registers.
  • Toggle a second pin for debug.

7.3 Performance Traps

Busy-wait loops waste CPU, but are fine for this learning project.


8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a second LED pattern.
  • Read a button input.

8.2 Intermediate Extensions

  • Use hardware timers for accurate delays.
  • Add UART logging.

8.3 Advanced Extensions

  • Build a minimal bootloader.
  • Add interrupt-driven GPIO.

9. Real-World Connections

9.1 Industry Applications

  • Embedded devices: Sensor nodes, controllers.
  • Firmware: Boot code and hardware init.
  • libopencm3: Open-source MCU library.

9.3 Interview Relevance

Bare-metal work proves deep understanding of hardware/software boundaries.


10. Resources

10.1 Essential Reading

  • “Making Embedded Systems” - Ch. 1-4
  • “The Definitive Guide to ARM Cortex-M3/M4” - GPIO sections

10.2 Video Resources

  • Microcontroller bring-up tutorials

10.3 Tools & Documentation

  • OpenOCD documentation
  • ARM GCC toolchain docs
  • Memory Allocator: Memory reasoning skills.
  • OS Kernel: Uses low-level hardware control.

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain memory-mapped I/O.
  • I understand linker scripts.
  • I can describe the boot sequence.

11.2 Implementation

  • LED blinks reliably.
  • Startup code runs correctly.
  • Registers are configured properly.

11.3 Growth

  • I can use timers instead of busy-wait.
  • I can explain this project in an interview.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Board runs code and LED toggles.

Full Completion:

  • Correct startup code and stable blink rate.

Excellence (Going Above & Beyond):

  • Interrupt-driven GPIO and UART logging.

This guide was generated from C_PROGRAMMING_COMPLETE_MASTERY.md. For the complete learning path, see the parent directory.