Lab 1: LED Blink (Basic GPIO Output)
Welcome to your first STM32 embedded systems program! In this lab, you'll master the fundamental concept of GPIO output control by creating a blinking LED using bare-metal C programming.
Learning Objectivesβ
By the end of this lab, you will understand:
- π― RCC (Reset and Clock Control) - Clock distribution to peripherals
- π― GPIO Initialization - Configure pins for output
- π― Register Manipulation - Direct hardware control via memory addresses
- π― Output Data Register (ODR) - Control pin voltage levels
- π― Software Delays - Create timing in embedded systems
Prerequisitesβ
- Basic C programming (variables, loops, functions)
- Understanding of hexadecimal notation
- Basic digital logic (HIGH/LOW, binary)
Hardware Requiredβ
| Component | Details |
|---|---|
| Microcontroller | STM32F407VG or STM32F407ZG (STM32F4 Discovery) |
| LED | Onboard LED at GPIOD pin 12 (typically red) |
| Connection | Already soldered on the discount board |
| Power Supply | USB or external 5V/3.3V adapter |
Theory: How GPIO Worksβ
The GPIO Block Diagramβ
Clock Source (RCC)
β
GPIO Port
β β β
Input Output Special
3-Step Processβ
Step 1: Enable Clock (RCC)β
Before any GPIO operation, you must ENABLE the clock to that port.
Without clock, the GPIO port is powered down (low power mode).
RCC_AHB1ENR |= (1 << 3); // Enable clock to GPIOD (bit 3)
Step 2: Configure Pin as Output (MODER)β
Each GPIO pin can be:
- 00: Input
- 01: General Purpose Output
- 10: Alternate Function
- 11: Analog
For 2-bit configuration per pin, pin N uses bits [2N+1:2N]
GPIOD pin 12 uses bits [25:24]
GPIOD_MODER |= (1 << 24); // Set PD12 as output
Step 3: Write to Output Data Register (ODR)β
Once configured as output, write to the ODR register to control voltage:
- 1 = Pin goes HIGH (3.3V) β LED ON
- 0 = Pin goes LOW (0V) β LED OFF
GPIOD_ODR |= (1 << 12); // Set PD12 HIGH
GPIOD_ODR &= ~(1 << 12); // Clear PD12 (LOW)
Register Referenceβ
| Register | Full Name | Purpose | Address |
|---|---|---|---|
| RCC_AHB1ENR | AHB1 Enable Register | Control clock to GPIO ports | 0x40023830 |
| GPIOD_MODER | Port Mode Register | Configure pin direction | 0x40020C00 |
| GPIOD_ODR | Output Data Register | Control pin output value | 0x40020C14 |
Demo: Visual Outputβ

The onboard LED blinks on and off repeatedly
The Codeβ
// ==================== REGISTER DEFINITIONS ====================
// Base address for RCC (Reset and Clock Control)
#define RCC_BASE 0x40023800UL
// AHB1 Enable Register (clock distribution)
#define RCC_AHB1ENR *(volatile unsigned int*)(RCC_BASE + 0x30U)
// ==================== GPIO PORT D ====================
// Base address for GPIO Port D
#define GPIO_D_BASE 0x40020C00UL
// Mode Register (configure as input/output)
#define GPIOD_MODER *(volatile unsigned int*)(GPIO_D_BASE + 0x00U)
// Output Data Register (control pin voltage)
#define GPIOD_ODR *(volatile unsigned int*)(GPIO_D_BASE + 0x14U)
// ==================== DELAY FUNCTION ====================
/**
* Simple software delay using CPU loop
* Creates visible delay for LED blinking
* Approximate: 150000 iterations β 1 second (varies by clock speed)
*/
void led_delay(void) {
for (volatile int i = 0; i < 150000; i++);
}
// ==================== MAIN PROGRAM ====================
int main(void) {
// ========== STEP 1: ENABLE CLOCK TO PORT D ==========
// Set bit 3 in RCC_AHB1ENR to enable GPIOD clock
// Without this, GPIO D cannot function
RCC_AHB1ENR |= (1 << 3);
// ========== STEP 2: CONFIGURE PIN AS OUTPUT ==========
// Pin number we're using
int pin = 12;
// Clear the 2 bits for this pin in MODER
// For pin 12: clear bits [25:24]
GPIOD_MODER &= ~(3 << (pin * 2));
// Set bits to 01 (General Purpose Output mode)
GPIOD_MODER |= (1 << (pin * 2));
// Initialize pin to LOW (LED OFF state)
GPIOD_ODR &= ~(1 << pin);
// ========== STEP 3: BLINK FOREVER ==========
while (1) {
// Turn LED ON (set pin HIGH = 3.3V)
GPIOD_ODR |= (1 << pin);
// Wait (visible to human eye)
led_delay();
// Turn LED OFF (clear pin = 0V)
GPIOD_ODR &= ~(1 << pin);
// Wait before next cycle
led_delay();
}
return 0; // Never reached (infinite loop)
}
Step-by-Step Execution Walkthroughβ
Initialization Phaseβ
1. RCC_AHB1ENR |= (1 << 3)
ββ Powers on GPIO Port D
2. GPIOD_MODER configuration
ββ Sets pin 12 as output mode (01)
3. GPIOD_ODR &= ~(1 << 12)
ββ Initializes pin to LOW (safe state)
Main Loop (Repeats Forever)β
Cycle 1:
ββ GPIOD_ODR |= (1 << 12) β Pin HIGH β LED ON
ββ led_delay() β Hold for ~1 second
ββ GPIOD_ODR &= ~(1 << 12) β Pin LOW β LED OFF
ββ led_delay() β Hold for ~1 second
β
Cycle 2: (Repeat infinitely)
Understanding the Macrosβ
What is volatile unsigned int*?β
#define GPIOD_ODR *(volatile unsigned int*)(GPIO_D_BASE + 0x14U)
β
Memory pointer to hardware register
// This means:
// 1. (GPIO_D_BASE + 0x14U) = Memory address (0x40020C14)
// 2. (volatile unsigned int*) = Cast to pointer type
// 3. * = Dereference to get the value
// 4. Using |= or &= = Modify the register
Why volatile?β
volatile tells the compiler:
- Don't optimize this variable away
- This value can change unexpectedly (hardware modifies it)
- Always read/write from actual memory, not a cached register
Expected Outputβ
Timeline:
ββ t=0s: LED ON (bright red)
ββ t=1s: LED OFF (dark)
ββ t=2s: LED ON (bright red)
ββ t=3s: LED OFF (dark)
ββ ...continues indefinitely
Visual: A smoothly blinking red LED at approximately 0.5 Hz (one blink per second)
Common Mistakes & Fixesβ
| Mistake | Problem | Fix |
|---|---|---|
Forgot AHB1_ENR | GPIO port has no clock, won't work | Add clock enable for GPIOD |
| Wrong pin number | Controlling different LED or wrong pin | Verify with board schematic |
Forgot ~(3 << ...) | Pin not cleared before setting | Always clear bits first |
No volatile keyword | Compiler optimizes away register access | Add volatile to all hardware registers |
| Delay too short | LED blinks too fast to see | Increase delay iterations |
Troubleshootingβ
LED Doesn't Blinkβ
β Check: Is the board powered? (LED should be dim even before code)
β Check: Is GPIOD clock enabled in RCC?
β Check: Is pin 12 actually an output? (MODER configuration)
β Check: Did you flash the code correctly?
LED Always ON or Always OFFβ
β Check: Are the ON/OFF commands reversed?
β Check: Is the delay so short it appears always on?
β Check: Is pin 12 available on your specific board?
Strange Behaviorβ
β Reduce frequency of changes (increase delay)
β Verify register addresses match your STM32 model
β Use a debugger to inspect register values
Key Takeawaysβ
β¨ Remember:
- GPIO requires 3 essential steps: Clock β Config β Control
- Register addresses map directly to hardware
- Bitwise operations efficiently control individual pins (no bit-banging overhead)
volatileprevents the compiler from optimizing away hardware operations- Delays are basic timing in embedded systems (later we'll use timers)
Challenge Exercisesβ
Challenge 1: Faster Blinkingβ
// Modify the delay function to blink twice as fast
// Hint: Reduce the loop iteration count
Challenge 2: Multiple LEDsβ
// The board has LEDs at pins 12, 13, 14, 15
// Modify to blink each LED sequentially:
// 12 β 13 β 14 β 15 β repeat
// Don't turn on the next until previous is off
Challenge 3: Custom Patternβ
// Create a pattern: 2 quick blinks, 1 second pause, repeat
// Sequence: ON (100ms) β OFF (100ms) β ON (100ms) β OFF (1000ms)
Next Stepsβ
π Ready for Lab 1.1? You'll refactor this code into modular functions for better code organization and reusability!
Prerequisites for Lab 1.1β
- β Understand GPIO initialization
- β Comfortable with bitwise operations
- β Can modify delays and pin numbers
Tips for Success:
- Type the code manually (don't copy-paste)βthis builds muscle memory
- Experiment with the delay value
- Try different pin numbers (12, 13, 14, 15)
- Use the board's reset button to restart the program