When you start working with microcontrollers like the ESP32, one of the first and most essential experiments you perform is blinking an LED.
But have you ever wondered what actually happens beneath the framework β at the bare-metal level?
In this tutorial, weβll dive deep into bare-metal programming with ESP-IDF, and understand how to control hardware directly β without using any high-level APIs.
By the end of this guide, youβll know exactly how to blink an LED using register-level programming on the ESP32 β the true essence of embedded development.
π₯ Watch the complete hands-on tutorial here:
π» Get the source code from GitHub:
π LED-Blinking-Bare-Metal Repository
π What is Bare-Metal Programming?
Bare-Metal Programming means writing firmware that runs directly on hardware β without any operating system or abstraction layer in between.
In this approach, you work close to the hardware registers, manually configuring and controlling peripherals like GPIO, timers, and interrupts.
Unlike frameworks such as Arduino or ESP-IDFβs high-level APIs, bare-metal programming gives you complete control and maximum performance β ideal for developers who want to deeply understand how microcontrollers work.
βοΈ Why Learn Bare-Metal Programming?
Hereβs why understanding bare-metal development is essential for every embedded developer:
- Full Hardware Control: Direct access to microcontroller registers.
- Performance Optimization: No overhead from frameworks or RTOS.
- Deeper Understanding: Learn how GPIOs, timers, and memory actually work.
- Portability: Enables you to develop for any MCU or architecture easily.
- Strong Foundation: Helps you understand frameworks like ESP-IDF or FreeRTOS better.
π§ Understanding the ESP32 GPIO at Register Level
Before we jump into code, letβs understand how GPIOs work at the hardware register level.
In the ESP32, each GPIO is managed by memory-mapped registers, which control:
- GPIO direction (input/output)
- GPIO output value (high/low)
- GPIO function (alternate/peripheral)
When you write to these registers directly, you are effectively telling the ESP32βs hardware what to do β without any intermediate software layer.
π§© Project Overview
π― Objective:
Blink an LED connected to the ESP32βs GPIO2 using bare-metal register manipulation β without using ESP-IDFβs gpio_set_level() or gpio_set_direction() functions.
π§° Requirements:
- ESP32 Development Board
- USB Cable
- LED + 220Ξ© Resistor
- ESP-IDF Installed on your system
- VS Code / ESP-IDF Terminal
π Circuit Connection:
- Connect the anode (+) of the LED to GPIO2.
- Connect the cathode (-) to GND through a 220Ξ© resistor.
π§Ύ Step-by-Step Implementation
π§© Step 1: Create a New ESP-IDF Project
Create a new project folder named LED-Blinking-Bare-Metal, and set up your main/app_main.c file.
π§© Step 2: Include the Required Headers
Weβll include only the minimal required headers for register-level access.
// BLINKING OF LED
#include <stdio.h>
#include <stdint.h>
π§© Step 3: Define the GPIO Register Addresses
Each GPIO register is mapped to a specific memory address in the ESP32.
For GPIO2, weβll directly manipulate these registers.
#define GPIO_PIN 2 // builtin LED
#define GPIO_OUT_W1TS_REG 0x3FF44008
#define GPIO_OUT_W1TC_REG 0x3FF4400C
#define GPIO_ENABLE_REG 0x3FF44020
π§© Step 4: Write the Bare-Metal Code
void delay(volatile uint32_t cycles){
while (cycles--)
{
for (int i = 0; i < 10000; i++)
{
__asm__ volatile("nop"); // no operation
}
}
}
void app_main(void){
volatile uint32_t* gpio_out_w1ts_reg = (volatile uint32_t*)GPIO_OUT_W1TS_REG;
volatile uint32_t* gpio_out_w1tc_reg = (volatile uint32_t*)GPIO_OUT_W1TC_REG;
volatile uint32_t* gpio_enable_reg = (volatile uint32_t*)GPIO_ENABLE_REG;
*gpio_enable_reg = (1<<GPIO_PIN);
while (1)
{
*gpio_out_w1ts_reg = (1<<GPIO_PIN);
delay(500);
*gpio_out_w1tc_reg = (1<<GPIO_PIN);
delay(500);
}
}
This code:
- Enables GPIO2 as output.
- Writes directly to the GPIO output register to turn the LED ON and OFF.
- Uses
delay()function to create a delay between blinks.
π§© Step 5: Build and Flash the Firmware
Run the following commands in your ESP-IDF terminal:
idf.py build
idf.py -p COMx flash monitor
Replace COMx with your ESP32βs port.
Once flashed, youβll see the LED blinking continuously β driven directly by register-level code.
π§ͺ Output
When you open the serial monitor, youβll observe logs similar to:
I (303) main_task: Started on CPU0
I (313) main_task: Calling app_main()
And visually, your LED will blink every half second β controlled by bare-metal register writes.
π» GitHub Repository
You can find the complete source code, project structure, and configuration files in the GitHub repository below π
π https://github.com/ashus3868/LED-Blinking-Bare-Metal.git
π₯ Watch the Full Tutorial on YouTube
For a complete walkthrough with visual demonstration, watch the detailed video on our YouTube channel Innovate Yourself:
π¬ Blinking an LED with Bare-Metal Programming | ESP-IDF Tutorial for Beginners
π‘ Key Takeaways
- Bare-metal programming gives you ultimate control over your hardware.
- You directly interact with registers instead of using high-level APIs.
- Itβs the best way to understand microcontroller architecture and firmware design deeply.
- Once you grasp this, using frameworks like ESP-IDF or FreeRTOS becomes much easier.
π§ Whatβs Next?
Now that youβve mastered bare-metal LED blinking, try exploring:
- Controlling multiple LEDs simultaneously
- Creating a bare-metal timer interrupt
- Building your own custom bootloader
Each of these will help you advance toward professional embedded system design.
π Useful Links
- GitHub Code: LED-Blinking-Bare-Metal
- Video Tutorial: Watch on YouTube
- Official ESP-IDF Docs: https://docs.espressif.com





