DEV Community

Cover image for Embedded Rust on the ESP32 : Blinking an LED
Vaishnav-sabari-girish
Vaishnav-sabari-girish

Posted on • Edited on

Embedded Rust on the ESP32 : Blinking an LED

In my previous blog, we coded our first Embedded Rust application which prints Hello World, now we will start a new application that is LED Blinking.
LED blinking in hardware is what Hello World is to software i.e it is the starting point of anyone who plans on starting microcontroller programming.

LED blinking gives you a basic idea of how to select which pin to toggle, make it an input or an output and how to set the pin as HIGH or LOW.

In this blog, we will be learning how to blink an LED in 2 ways :

  1. [Setting HIGH and LOW separately]
  2. [Toggling the LED]

Getting Started

Let us create a new project

esp-generate --chip=esp32 blinky
Enter fullscreen mode Exit fullscreen mode

Refer the previous blog , for the steps.

NOTE
The above link takes you directly to the section in the previous blog where steps are outlined

These steps are common for all projects.



After the project has been created, let us get into the programming aspect.

Open the src/bin/main.rs file in the project.

Code 1 - Mentioning the Level of the Pin separately.

In this code, the Logic Level of the LED pin is mentioned explicitly as HIGH or LOW.

Here's the full code

#![no_std]
#![no_main]
#![deny(
    clippy::mem_forget,
    reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
    holding buffers for the duration of a data transfer."
)]

use esp_backtrace as _;
use esp_hal::{
    delay::Delay,
    gpio::{Level, Output, OutputConfig},
    main,
};

use esp_println::println;
esp_bootloader_esp_idf::esp_app_desc!();

#[main]
fn main() -> ! {
    let peripherals = esp_hal::init(esp_hal::Config::default());

    println!("Hello World");

    let mut led = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
    let delay = Delay::new();

    loop {
        led.set_high();
        println!("LED HIGH");
        delay.delay_millis(1000);
        led.set_low();
        println!("LED LOW");
        delay.delay_millis(1000);
    }
}
Enter fullscreen mode Exit fullscreen mode

Into the code : A line-by-line explanation

#![no_std]
Enter fullscreen mode Exit fullscreen mode
  • This disables Rust's standard library, which is required in embedded environments where there is no OS.
#![no_main]
Enter fullscreen mode Exit fullscreen mode
  • Tells the Rust compiler that this binary has no standard entry point (main from std) and instead uses a custom entry point, suitable for embedded.
#![deny(
    clippy::mem_forget,
    reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
    holding buffers for the duration of a data transfer."
)]
Enter fullscreen mode Exit fullscreen mode
  • This directive disallows the use of mem::forget, which can lead to memory/resource leaks when dealing with HAL (Hardware Abstraction Layer) resources like GPIOs or DMA.
use esp_backtrace as _;
Enter fullscreen mode Exit fullscreen mode
  • This macro provides backtraces on panic, helping with debugging crashes.
use esp_hal::{
    delay::Delay,
    gpio::{Level, Output, OutputConfig},
    main,
};
Enter fullscreen mode Exit fullscreen mode
  • Brings in essential traits and modules from the esp_hal crate:

    • Delay: For implementing delay operations.
    • gpio::{Level, Output, OutputConfig}: For configuring and using GPIO pins.
    • main: Used to mark the entry point.
use esp_println::println;
Enter fullscreen mode Exit fullscreen mode
  • Provides the println! macro, which sends debug messages to a serial console.
esp_bootloader_esp_idf::esp_app_desc!();
Enter fullscreen mode Exit fullscreen mode
  • Macro that embeds application metadata like version, build date, etc., into the firmware binary.

#[main]
fn main() -> ! {
Enter fullscreen mode Exit fullscreen mode
  • Defines the custom entry point of the application, marked with #[main] for esp_hal. The -> ! return type means this function never returns (infinite loop or system reset).
    let peripherals = esp_hal::init(esp_hal::Config::default());
Enter fullscreen mode Exit fullscreen mode
  • Initializes the HAL with default configuration and gets access to hardware peripherals like GPIO.
    println!("Hello World");
Enter fullscreen mode Exit fullscreen mode
  • Prints "Hello World" over serial (for debugging).
    let mut led = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
Enter fullscreen mode Exit fullscreen mode
  • Configures GPIO2 as a digital output with default settings and initial level Low. Output::new() takes the pin, initial logic level, and an optional output config.
    let delay = Delay::new();
Enter fullscreen mode Exit fullscreen mode
  • Initializes the delay timer used for timing operations like delay_millis().
    loop {
Enter fullscreen mode Exit fullscreen mode
  • Infinite loop to blink the LED repeatedly.
        led.set_high();
Enter fullscreen mode Exit fullscreen mode
  • Sets the GPIO2 pin to logic high (turns LED on).
        println!("LED HIGH");
Enter fullscreen mode Exit fullscreen mode
  • Logs the LED state to the serial console.
        delay.delay_millis(1000);
Enter fullscreen mode Exit fullscreen mode
  • Waits for 1000 milliseconds (1 second).
        led.set_low();
Enter fullscreen mode Exit fullscreen mode
  • Sets the GPIO2 pin to logic low (turns LED off).
        println!("LED LOW");
Enter fullscreen mode Exit fullscreen mode
  • Logs the LED state to the serial console.
        delay.delay_millis(1000);
Enter fullscreen mode Exit fullscreen mode
  • Waits for another second before repeating.
    }
}
Enter fullscreen mode Exit fullscreen mode

Code 2 - Toggling the pin without mentioning it's level.

In this code, we directly toggle the pin instead of mentioning it's logic level. This reduces the lines of code.

#![no_std]
#![no_main]
#![deny(
    clippy::mem_forget,
    reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
    holding buffers for the duration of a data transfer."
)]

use esp_backtrace as _;
use esp_hal::{
    delay::Delay,
    gpio::{Level, Output, OutputConfig},
    main
};
use esp_println::println;

esp_bootloader_esp_idf::esp_app_desc!();

#[main]
fn main() -> ! {
    let peripherals = esp_hal::init(esp_hal::Config::default());

    println!("Hello World");

    let mut led = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
    let delay = Delay::new();

    loop {
        led.toggle();
        println!("LED TOGGLED");
        delay.delay_millis(2000);
    }
}

Enter fullscreen mode Exit fullscreen mode

Into the code : Line-by-line explanation

#![no_std] 
Enter fullscreen mode Exit fullscreen mode
  • Disables the standard Rust library, which isn't available in embedded systems.
#![no_main] 
Enter fullscreen mode Exit fullscreen mode
  • Replaces the standard main entry point with a custom one suitable for bare-metal applications.
#![deny(
    clippy::mem_forget,
    reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
    holding buffers for the duration of a data transfer."
)] 
Enter fullscreen mode Exit fullscreen mode
  • Forbids use of mem::forget, which can cause resource leaks in hardware abstractions.
use esp_backtrace as _; 
Enter fullscreen mode Exit fullscreen mode
  • Enables backtrace functionality for debugging panics in embedded context.
use esp_hal::{
    delay::Delay,
    gpio::{Level, Output, OutputConfig},
    main,
}; 
Enter fullscreen mode Exit fullscreen mode
  • Imports delay functionality, GPIO control types, and the custom main macro.
use esp_println::println; 
Enter fullscreen mode Exit fullscreen mode
  • Allows the use of println! for serial output, typically for debugging.
esp_bootloader_esp_idf::esp_app_desc!(); 
Enter fullscreen mode Exit fullscreen mode
  • Embeds application metadata into the binary (e.g., version, date, etc.).

#[main]
fn main() -> ! {
Enter fullscreen mode Exit fullscreen mode
  • Marks this as the custom entry point with an infinite loop (-> !).
    let peripherals = esp_hal::init(esp_hal::Config::default());
Enter fullscreen mode Exit fullscreen mode
  • Initializes hardware abstraction and retrieves access to peripherals (GPIO, timers, etc).
    println!("Hello World");
Enter fullscreen mode Exit fullscreen mode
  • Sends a "Hello World" message over serial for initial confirmation/debug.
    let mut led = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
Enter fullscreen mode Exit fullscreen mode
  • Initializes GPIO2 as a push-pull output pin, starting in a Low state.
    let delay = Delay::new();
Enter fullscreen mode Exit fullscreen mode
  • Creates a delay instance for blocking delay operations.
    loop {
Enter fullscreen mode Exit fullscreen mode
  • Starts an infinite loop for repeated toggling of the LED.
        led.toggle();
Enter fullscreen mode Exit fullscreen mode
  • Toggles the LED pin state (High <-> Low) each cycle.
        println!("LED TOGGLED");
Enter fullscreen mode Exit fullscreen mode
  • Logs that the LED state was toggled.
        delay.delay_millis(2000);
Enter fullscreen mode Exit fullscreen mode
  • Waits for 2000 milliseconds (2 seconds) before toggling again.
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's run

cargo run --release
Enter fullscreen mode Exit fullscreen mode

to get the output.

Outputs

Code 1 outputs

Terminal Output

terminal op

Hardware output

Code 2 outputs

Terminal output

terminal op 2

Hardware Output

Important Links

  1. Getting Started Blog
  2. Hello World Blog
  3. GitHub Repository

Thank You

Top comments (0)