Advanced ESP32 development with ESP-IDF

For electronics projects that outgrow Arduino

Many electronics hobbyists begin with the Arduino ecosystem. That's how I got started!

My first project was a tripod mount that could hold an iPhone for filming and pan using a motor. Early iterations used an Arduino Nano 33 BLE, a servo motor, and 4x AA batteries.

Arduino controlling a servo motor

When writing firmware for the Arduino Nano, I started with the Android IDE + SDK.

The code for communicating over Bluetooth and controlling the servo fit into a few hundred line .ino sketch file. The wiring was pretty straightforward and only needed a few pins connected. The Arduino Servo library for motor control and ArduinoBLE for comms made it easy to implement.

When I finished working on that project, I moved to increasingly complex projects. The Bitclock project needed to connect to an e-ink screen, mount three I2C sensors, and fit into a tiny enclosure. At this point, I was looking to graduate from the Arduino Nano and hand soldering everything together.

Bitclock photo

Bitclock - E-ink clock + air quality monitor

To meet these requirements, I decided to design a custom PCB (printed circuit board). By designing a PCB specific to the device, Bitclock electronics could have a smaller footprint that would fit snugly into an enclosure. Additionally, manufacturing the product at scale would be simplified through PCB assembly services like πŸ‡¨πŸ‡³ JLCPCB, PCBWay, or πŸ‡ΊπŸ‡Έ MacroFab.

I should disclose that one of my main goals when starting Bitclock was to learn custom PCB design, so my approach was going to end up there anyway!

Bitclock PCB photo

Bitclock PCB assembly, featuring an ESP32-S3 microcontroller

An early question when designing Bitclock was which microcontroller to use.

I settled on the ESP32-S3 module, which you can see mounted to the top-right corner of the PCB. These ESP32 SoC (system-on-a-chip) modules are popular for a few reasons:

With the Bitclock project, my firmware grew to thousands of lines and many files. The firmware needed to render graphics for a display, communicate over bluetooth + Wi-Fi, communicate with sensors. These features needed to multi-task on a single core and meet the constrained memory & flash of the ESP32.

While you can technically write code for ESP32 using the Arduino IDE + SDK, I settled on an alternative approach for this project using Espressif's ESP-IDF framework.

Switching to ESP-IDF

Technically, you can program an ESP32 using Arduino IDE & SDK.

The generic Arduino toolchain supports different hardware platforms through Arduino Cores. Each Arduino Core implements the standard APIs like Serial and Wire. In the case of ESP32 devices, Espressif actively maintains the arduino-esp32 core implementation.

Under the hood, the arduino-esp32 core is largely implemented using the ESP-IDF framework. The ESP-IDF framework was developed specifically for ESP32 SoCs, and has many features and APIs specific to that family of devices.

Demonstration of Arduino leveraging ESP-IDF

arduino-esp32 is implemented using ESP-IDF

This brought about a major design decision for Bitclock. Continue using the generic and familiar Arduino SDKs, or switch to the manufacturer's ESP-IDF framework?

For the Bitclock project, hardware cross-compatibility was not a priority. Instead, hardware control was more important especially given the hardware constraints of:

On the other hand, the ESP-IDF framework offers equivalent high and low level APIs, and is very well documented.

Some of the ESP-IDF features Bitclock leverages include:

Given the above, I decided to use ESP-IDF directly and bypass the Arduino abstraction.

Switching to VSCode

While I had enjoyed using the Arduino IDE previously, it no longer seemed the obvious choice given the ESP-IDF choice. VSCode was the general code editor I used the most, and has many appealing features:

In order to make the switch to VSCode, it was necessary to ensure that I would have replacements for a few crucial Arduino IDE features:

  1. Compile + flash + serial log monitoring
  2. Static code analysis + code completion
  3. Interactive GDB debugging

PlatformIO is a popular VSCode extension that supports all of these features, so I experimented with that before ultimately deciding not to use it and instead configure VSCode manually.

Evaluating PlatformIO

On paper, PlatformIO supported all the features I wanted for embedded development in VSCode. I'd be able to compile, flash, debug, and get static analysis of my code.

However, after installing the extension I was less enthused. I was greeted with a landing page showing news, social links, and project creation wizards. It's certainly a personal preference, but PlatformIO was delivering me an entire IDE-within-IDE when I was looking for a few basic debugging and code highlighting features.

PlatformIO screenshot

VSCode after PlatformIO extension installed

Fortunately, there is an alternative way to get the functionality I desired using only VSCode, the C/C++ extension by Microsoft, and the ESP-IDF CLI toolchain.

ESP-IDF toolchain setup

The ESP-IDF toolchain includes the SDK, as well as a command line front-end for build, flash, and monitor. If you use PlatformIO it likely installs it for you, but since we're doing things manually we'll install it ourselves.

Follow the instructions from Espressif here.

On macOS, you'll need create a directory to install the SDK to and the process goes something like this:

brew install cmake ninja dfu-util python3
git clone -b v5.3 --recursive https://github.com/espressif/esp-idf.git
./install.fish esp32s3
source export.sh

Make sure to read the latest docs for up-to-date installation instructions.

ESP-IDF project bootstrapping

With the toolchain now installed, create a separate directory to house the source code of your new project.

You can use ESP-IDF frontend to create a barebone project for you:

source ~/your/path/to/esp-idf/export.sh
idf.py create-project example

This will create a directory structure with the following:

β”œβ”€β”€ CMakeLists.txt          Top-level build configuration
β”œβ”€β”€ main
β”‚   β”œβ”€β”€ CMakeLists.txt      Defines main build target + dependencies
β”‚   └── example.c           Main run function for app

Make sure to tell ESP-IDF what device you are building for:

idf.py set-target esp32s3

With that complete, build and run is a simple via the CLI:

Static analysis + code completion

The ESP-IDF CLI tool solves build + run, but for VSCode to be a real IDEℒ️ it needs to know where to look for ESP-IDF libraries referenced by your code.

We'd need code static analysis to get library auto-completion, CMD+click to jump to function definitions, and a red squiggly underlines when we add a bug.

Auto-completion from ESP-IDF SDK

First, make sure you have the C/C++ extension by Microsoft installed.

Then we can configure IntelliSense by adding a .vscode/c_cpp_properties.json file to the root of the project. VSCode will read this to configure the extension.

Here's an example that worked for Bitclock's ESP32-S3 on macOS:

{
  "env": {
    "esp-version": "esp-13.2.0_20240530"
  },
  "configurations": [
    {
      "name": "esp32s3",
      "compilerPath": "${HOME}/.espressif/tools/xtensa-esp-elf/${esp-version}/xtensa-esp-elf/bin/xtensa-esp-elf-gcc",
      "compileCommands": "${workspaceFolder}/build/compile_commands.json",
      "includePath": [
        "${HOME}/.espressif/tools/xtensa-esp-elf/${esp-version}/xtensa-esp-elf/xtensa-esp-elf/include/**",
        "${HOME}/code/esp-idf/components/**",
        "${workspaceFolder}/**"
      ],
      "browse": {
        "path": [
          "${HOME}/.espressif/tools/xtensa-esp-elf/${esp-version}/xtensa-esp-elf/xtensa-esp-elf/include/**",
          "${HOME}/code/esp-idf/components/**",
          "${workspaceFolder}/**"
        ],
        "limitSymbolsToIncludedHeaders": false
      }
    }
  ],
  "version": 4
}

It does the following:

Make sure the paths match the locations of ESP-IDF's install location and ~/.espressif workspace for your system. See the ESP-IDF tools.json to see what version you should be pointing to. For example, ESP32-S3 uses the xtensa-esp-elf compiler while ESP32-C3 uses riscv32-esp-elf.

To be honest, I'm not entirely sure how critical each of these fields is to IntelliSense but I do know that in its entirety the configuration worked for my project! I used the Espressif's VSCode extension as a guide (yet another VSCode extension that I opted to skip and configure manually 🧘).

Interactive debugging

Last but not least, we'd like to have GDB functionality within VSCode. The C/C++ extension we've already installed supports this but we need to tell it how to connect.

ESP32 interactive debugging using OpenOCD

For Bitclock, the following debugging configuration worked by this file to your project's .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "esp-openocd",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/build/example.elf",
      "MIMode": "gdb",
      "miDebuggerPath": "/full/path/to/.espressif/tools/xtensa-esp-elf-gdb/14.2_20240403/xtensa-esp-elf-gdb/bin/xtensa-esp32s3-elf-gdb",
      "cwd": "${workspaceFolder}",
      "setupCommands": [
        { "text": "target extended-remote :3333" },
        { "text": "set remote hardware-watchpoint-limit 2" },
        { "text": "mon reset halt" },
        { "text": "maintenance flush register-cache" },
        { "text": "thb app_main" }
      ]
    }
  ]
}

Make sure .miDebuggerPath points to the correct ESP-IDF toolchain for this project (see tools.json tip from above). Also ensure .program points to the .elf build of your project.

Once setup, connect your ESP32 hardware, flash it with the latest version of your app, and start the local OpenOCD server. This will start a debugging connection over USB with your device:

idf.py openocd

The last step is to connect VSCode to your debugging session. Open the Run & Debug Panel (on macOS: ⌘ + Shift + D), then the click the ▢️ button to start debugging (F5). VSCode allows you to visually set breakpoints, inspect memory, and the usual debugging IDE features you'd expect.

Parting thoughts

For projects that grow too complex for the Arduino ecosystem, consider switching to ESP-IDF & VSCode. If you take that path, I hope this write-up proves a helpful resource in getting things going.

Bitclock photo

There is more I'd love to share from working on this project. Topics include:

If you've got feedback, or want to see anything specifically described further, don't hesitate to reach out at [email protected]!

And if you're interested in the gadget that inspired this post, consider ordering a Bitclock! Either use it as clock + air quality monitor, or write your own apps. The code is open source on GitHub.

Happy hacking...

--Brady

© 2024 Goat Hill Electronics LLC. All rights reserved. Privacy Policy 𐀟 Terms of Service