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:
- Configure a microcontroller toolchain.
- Write startup code and linker scripts.
- Control hardware via memory-mapped registers.
- 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
- Provide startup and vector table.
- Configure GPIO registers correctly.
- Implement a simple delay loop or timer.
- Blink LED at a visible rate.
3.3 Non-Functional Requirements
- Reliability: Correct register addresses.
- Portability: Target a specific MCU family.
- Safety: Use
volatilefor 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
- Configure GPIO pin as output.
- Set pin high, delay.
- 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:
- Memory-mapped I/O
- Why
volatileis required.
- Why
- Startup code
- What happens before
main()runs?
- What happens before
- Linker scripts
- How code/data map to flash and RAM.
5.5 Questions to Guide Your Design
Before implementing, think through these:
- What GPIO pin is connected to the LED?
- What clock setup is required for that GPIO?
- 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:
- “What is memory-mapped I/O?”
- “Why does bare-metal code need a linker script?”
- “What does
volatiledo 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:
- Build a minimal
main. - Flash and verify execution.
Checkpoint: Board runs code (even if LED not yet).
Phase 2: Core Functionality (7-10 days)
Goals:
- GPIO control
Tasks:
- Configure pin mode.
- Toggle pin in loop.
Checkpoint: LED blinks.
Phase 3: Polish & Edge Cases (4-6 days)
Goals:
- Timing accuracy and debug
Tasks:
- Replace busy-wait with timer.
- 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
- GPIO configured: Output mode set correctly.
- Toggle loop: Pin changes state.
- Startup:
mainexecutes.
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.
9.2 Related Open Source Projects
- 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
10.4 Related Projects in This Series
- 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.