Your First Bare-Metal Project on ESP32 | Blinking of LED

Blinking an LED with Bare-Metal Programming | ESP-IDF Tutorial for Beginners
0
0
4 min read

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:

  1. Full Hardware Control: Direct access to microcontroller registers.
  2. Performance Optimization: No overhead from frameworks or RTOS.
  3. Deeper Understanding: Learn how GPIOs, timers, and memory actually work.
  4. Portability: Enables you to develop for any MCU or architecture easily.
  5. 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:

πŸ”Œ 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:

  1. Enables GPIO2 as output.
  2. Writes directly to the GPIO output register to turn the LED ON and OFF.
  3. 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

Leave a Reply