Play with ESP32 GPIO part 1: Blinking LED

OK, this is the second post about ESP32 programming. This post is the first post of Play with ESP32 GPIO series. And in this post, I'm going to show you how to blink a LED using ESP-IDF.

Hold on!

Before you read this tutorial, you must:

  • install the ESP32 toolchain and ESP-IDF on your system. Read this post first if you're using GNU/Linux.

  • have an ESP32 development board of any kind, especially the one with LED on board.

If you have done that before, feel free to skip this section.

Before we go to the main topic, I need to introduce some necessary concept that is used in ESP-IDF.

Components

Component is a building block of an ESP32 firmware. ESP-IDF provides necessary built-in components for building ESP32 firmware.

Let's take a look into the anatomy of a component.

component_name  
├── component.mk
├── Kconfig
├── component_source.h
└── component_source.c

Basically, a component is a directory that contains:

  • component.mk file that defines source directories, include directories, compilation & linking flags, etc.

  • Component's C / C++ source & header files.

  • Optionally, Kconfig file which defines set of component configurations. The configuration menu will appear inside Component config menu of make menuconfig.

Another common structure of a component is

component_name  
├── component.mk
├── component_source.c
├── include
│   └── component_source.h
└── Kconfig

In this kind of structure, all header files are placed under the include directory of the component.

I'll write about ESP-IDF component with more details later.

Main component

Main component is a mandatory component in every ESP32 firmware project. This component ties both ESP-IDF built-in components and user's components together.

Main component requires at least component.mk and a C or C++ source file. The component.mk can be empty if we decide not to provide any custom configuration.

Inside the source file, we should define the main function with following signature:

void app_main()  

If you're using C++, you need to enclose the function definition within extern "C" {} statement. For example

extern "C" {  
  void app_main() {
    // user's code here
    // ...
  }
}

Or simply

extern "C" void app_main() {  
  // user's code here
  // ...
}

Let's Code!

Alright, let's get down to the main event. We're gonna create the project structure from scratch.

Create a project directory. Then, move the the newly created directory.

$ mkdir esp32-gpio
$ cd esp32-gpio

Create a Makefile with the following content

PROJECT_NAME := esp32-gpio

include $(IDF_PATH)/make/project.mk  

We simply define the PROJECT_NAME variable and include the project.mk file from ESP-IDF.

Next, we create the main component.

$ mkdir main
$ touch main/component.mk
$ touch main/esp32_gpio.c

OK, now we have created the main component with only one C file and empty component.mk file.

Now, let's include some necessary header files.

// file: esp32_gpio.c

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include <stdio.h>

We included freertos component's headers, namely FreeRTOS.h and task.h. These headers are used for harnessing FreeRTOS's multitasking capability. I'll write more details about FreeRTOS in the future. So, please bear with me for a while if you have no idea what FreeRTOS is.

We also included gpio.h header from driver component. This header provides data structures and functions for manipulating GPIO.

And finally we included the infamous header, stdio.h. We're gonna use the printf function to print information to the serial monitor.

Then, we define the GPIO number of the LED

gpio_num_t led_pin = GPIO_NUM_16;  

My Nano32's LED is connected to GPIO 16. Your board's LED might be connected to a different GPIO pin, so you might need to change the led_pin variable value.

Next, we define the blinking LED task function.

void blinking_led_task(void* pvParameter) {  
  int level = 1;
  gpio_set_direction(led_pin, GPIO_MODE_OUTPUT);

  while (true) {
    printf("LED value: %d\n", level);

    gpio_set_level(led_pin, level);
    level = !level;
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

This function follows the FreeRTOS task function signature

void task_name(void* parameter);  

We can optionally pass an argument to the task function.

In the blinking_led_task function, we defined the GPIO output value as level.

Then, we set the mode or direction of the GPIO.

gpio_set_direction(led_pin, GPIO_MODE_OUTPUT);  

If you're familiar with Arduino, the above line is equal to

pinMode(led_pin, OUTPUT);  

Then, we do the blinking inside the infinite loop.

printf("LED value: %d", level);  

The first line of the loop is basically printing the current GPIO output value to the serial monitor.

Then, we send the signal to the GPIO.

gpio_set_level(led_pin, level);  

The above line is equal to the following in Arduino:

digitalWrite(led_pin, level);  

Then, we flip the level value.

level = !level;  

Finally, we block the execution for 500 ms using

vTaskDelay(500 / portTICK_PERIOD_MS);  

vTaskDelay function accepts an argument in tick unit. That's why we divide 500 by portTICK_PERIOD_MS to get the amount of ticks for 500 milliseconds.

Then, we define the main function.

void app_main() {  
  xTaskCreate(&blinking_led_task, "blinking_led_task", 2048, NULL, 5, NULL);
}

In the main function, we register the blinking_led_task to the FreeRTOS scheduler so that the task can be run.

Here's the list of arguments passed to xTaskCreate function:

  • Task function that will be run. Here we pass the blinking_led_task function.
  • Name of the task.
  • Size of stack allocated for the task in bytes unit.
  • Argument for the task function. We passed NULL because we don't need to pass anything for now.
  • Task' priority number. Greater number means higher priority.
  • The last argument is the task handle.

So, here's the final form of esp32_gpio.c

// file: esp32_gpio.c

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include <stdio.h>

gpio_num_t led_pin = GPIO_NUM_16;

void blinking_led_task(void* pvParameter) {  
  int level = 1;
  gpio_set_direction(led_pin, GPIO_MODE_OUTPUT);

  while (true) {
    printf("LED value: %d\n", level);

    gpio_set_level(led_pin, level);
    level = !level;
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

void app_main() {  
  xTaskCreate(&blinking_led_task, "blinking_led_task", 2048, NULL, 5, NULL);
}

Run the code!

Before running the code, we need to configure the project by running the following command:

$ make menuconfig

We only need to configure the serial flasher port and baud rate.

Then, we build and flash the firmware to the ESP32 board.

$ make -j4 flash monitor

You can replace 4 in -j4 flag with the number of your PC/Laptop/Macbook processor core.

The first build process will take a while since we need to compile the ESP-IDF built-in components first.

A serial monitor will open for you after the firmware is flashed successfully. You can press CTRL + ] to exit the serial monitor.

Conclusion

We have learned a little bit about ESP-IDF component as the building block of ESP32 firmware. We also learned about how to send signal to a GPIO using driver component. And finally we learned the basic of FreeRTOS' task to do the blinking.

What's next?

In the next post, we will learn about how to read GPIO value and handling interrupt using ISR (interrupt service routine) and FreeRTOS' queue.

I haven't enabled discussion feature yet, so if you have any thoughts you can ping me at alwin.ridd@gmail.com or twitter @alwin_winter.

Thank you for reading!