A first look at the ESP-IDF v5.0-beta1 and C++20

For my current projects, I have been using the ESP-IDF v4.3.2. And to be honest, I don’t update the IDF frequently since I fear breaking something.

Recently, I discovered that Espressif had published the first beta of the ESP IDF v5, which now supports C++20 language features thanks to the switch to GCC 11. Since I have always liked to use the most up-to-date C++ versions, I wanted to try it out.

Installing the beta alongside the old installation

If you don’t want to break your existing projects, you can install the new version alongside the old one. My current version resides in ~/esp/esp-idf/, so I went into the esp folder to clone the beta version.

git clone -b v5.0-beta1 --recursive https://github.com/espressif/esp-idf.git esp-idf-v5.0-beta1

Afterwards, run the install script to set up all the required dependencies.

. esp-idf-v5.0-beta1/install.sh esp32

Once the install script finishes successfully, everything should be ready to go. In case you run into any issues, check out the official guide.

Verifying the installation

Now you could try to build one of your existing projects, but I choose to create a fresh one for testing purposes. Therefore, I copied the Hello World example and opened the directory in VS Code.

cp -r /Users/julian.schroden/esp/esp-idf-v5.0-beta1/examples/get-started/hello_world idf-v5-test

To make the IDF available to the path, open a new terminal window in VS Code and run the following command. Ensure to reuse this terminal instance for the upcoming idf.py commands.

. /Users/julian.schroden/esp/esp-idf-v5.0-beta1/export.sh

Next up, verify that the active IDF Version is correct by running echo $IDF_PATH, which should output the path to ESP IDF v5. Then, set the target to esp32 and start a build.

# Set the target
idf.py set-target esp32

# Build the project
idf.py build

Verifying C++20 features are available

C++20 introduced many exciting new language features, like coroutines, concepts and modules. But some of these features are still not fully implemented by the compiler or supported by the build tools (e. g modules). Let’s check out concepts and using enum to verify that C++20 language features are usable.

For more information on compiler support, check out this page on cppreference.com.

Concepts

Concepts can be used to define constraints on template arguments. Let’s imagine we are writing a program which should control a motor. Using concepts, we can define the interface a motor type needs to comply with by specifying requirements.

#include <concepts>

template <typename T>
concept Motor = requires(T motor, uint8_t speed) {
  motor.rotateLeft(speed);
  motor.rotateRight(speed);
  motor.stop();
  { motor.getSpeed() } -> std::same_as<uint8_t>;
};

If you want to learn more about concepts and how to define them, check out this post on cppstories.com.

But for now, let’s write a dummy class which satisfies the Motor concept and use it in app_main.

class DCMotor {
 private:
  uint8_t speed = 0;

 public:
  void rotateLeft(uint8_t speed) {
    this->speed = speed;
    printf("Starting to rotate left at the speed: %d\n", speed);
  }

  void rotateRight(uint8_t speed) {
    this->speed = speed;
    printf("Starting to rotate right at the speed: %d\n", speed);
  }

  void stop() {
    this->speed = 0;
    printf("Stop rotating");
  }

  uint8_t getSpeed() {
    return speed;
  }
};

void app_main(void) {
  // ...

  Motor auto motor = DCMotor();
  motor.rotateLeft(100);
  motor.stop();

  // ...
}

Here we are using the Motor concept to add a constraint on the auto keyword.

To see the power of concepts in action, remove any method of the DCMotor class and try to compile the code. The following compiler output was printed out after I removed the getSpeed() method.

hello_world_main.cpp:69:30: note: constraints not satisfied
hello_world_main.cpp:29:9:   required for the satisfaction of 'Motor<auto [requires ::Motor<<placeholder>, >]>' [with auto [requires ::Motor<<placeholder>, >] = DCMotor]
hello_world_main.cpp:29:17:   in requirements with 'T motor', 'uint8_t speed' [with T = DCMotor]
hello_world_main.cpp:33:19: note: the required expression 'motor.getSpeed()' is invalid
   33 |   { motor.getSpeed() } -> std::same_as<uint8_t>;
      |     ~~~~~~~~~~~~~~^~

As you can see, the compiler printed out a very descriptive error message.

Using enum

Another welcome addition to C++20 is bringing enum members into scope, which improves readability, especially in long switch case statements.

namespace gpio {
enum class Mode {
  disabled,
  input,
  output,
  outputOpenDrain,
  inputOutput,
  inputOutputOpenDrain,
};
};

void printGpioMode(gpio::Mode mode) {
  switch (mode) {
    using enum gpio::Mode;
    case disabled:
      printf("disabled\n");
      break;
    case input:
      printf("input\n");
      break;
    // ...
  }
}

Instead of repeating gpio::Mode for each entry over and over, the entries are directly accessible.

C++20 language features finally arrived

With the switch to GCC 11, promising new language features are now available when working with the ESP IDF. Over the last year, I have been reading many articles on C++20 features, and it is great we can finally explore them in the microcontroller world.

After the first successful experiments with IDF v5 beta and C++20, I cannot wait to update to a stable release once available.