PlatformIO the missing document...

September 02, 2022

Introduction

PlatformIO (www.platformio.org) is a great tool for building embedded applications. Supporting a wide range of MCU’s and frameworks. Highly simplified PlatformIO Arduino on steriods.

I have been using PlatformIO for a number of years as the basis for my embedded development. Mainly I use it as a build tool as it unifies the build experience across multiple platforms. Being a developer first I really dislike spending time with the build system. I want to write code. PlatformIO solves this in 99% of my cases very smoothly.

NOTE: I use the CLI of PlatformIO or the CLion Pluing, I don’t use the PlatformIO IDE.

Under the hood PlatformIO uses the SCons (www.scons.org) build-system. It is a Python based system and a natural fit for PlatformIO as it is also built in Python.

If you want to know more I suggest you visit: https://docs.platformio.org/en/latest/what-is-platformio.html

I won’t comment much on the specific variables/commands or PlatformIO names, you can look them up.

Multiple environments

The platformio.ini file has support for multiple environments. They can also inherit from each other. This can come in handy when you have several developers working together, compiling for different MCU target, building on multiple OS:es etc..

Section shows

  • Environment and inheritence
  • Library paths and quirks with environment variables
  • Library naming and handling

The following example builds a project for ESP32 using the Arduino Framework for ESP32 and then also the same project for NRF52 using the Zephyr OS. Linking some external library MyNiceLib.

## base configuration, valid throughout this project
[base]
# Define build flags
build_flags =
    -std=c++11

# support for Arduino libraries on Linux/Windows (default installation directories)
lib_extra_dirs = 
    ${sysenv.HOMEPATH}/Documents/Arduino/libraries
    ${sysenv.HOME}/Arduino/libraries

lib_ldf_mode = deep
lib_deps = MyNiceLib

#
# ESP32 Base Configuration
#
[espressif_base]
platform = espressif32
board = esp-wrover-kit
framework = arduino
build_flags = ${base.BUILD_FLAGS}

#
# ESP32 Release
#
[env:ESP32Release]
extends = espressif_base, base

#
# ESP32 Debug
#
[env:ESP32Debug]
extends = espressif_base, base
build_type = debug
build_flags =
    ${espressif_base.BUILD_FLAGS}
    -D DEBUG

#
# NRF52
#
[env:nrf52]
extends = base
platform = nordicnrf52
board = adafruit_feather_nrf52840
framework = zephyr
build_type = debug
upload_protocol = jlink

build_flags =
    ${base.BUILD_FLAGS}
    -D DEBUG

Base section

The first section (base) contains configuration valid for any platform and build use case.

The lib_extra_dirs tells PlatformIO where we have external dependencies installed. You can have multiple paths in this list, and you probably will (after a while).

Here is also the first quirk, ${sysenv.XXXXX} will expand to the environment variable specified by XXXXX. On Windows and Linux the home directory environment variable name is different so we need to specify it twice. PlatformIO will simply ignore non-existing directories.

The lib_ldf_mode is a little special. Generally PlatformIO parses all code to find what external libraries to include in the build. The LDF mode is a setting to the code parser. Deep will cause it to fully parse and expand macros in order to sort out what needs to be included. After playing around with this many times, I tend to keep deep in my project files.

lib_deps is just a list of libraries to search for. The way PlatformIO parses the name is quite powerful and extensive. In general the name should match what is present in the library.json or library.properties file of the library.

You can also post-fix the library name with version handling and so forth, like:
lib_deps = MyNiceLib@3.1.2.

Up-stream source for a library is supported like:
lib_deps = MyNiceLib=https://github.com/bla/lib

This is quite powerful when having big projects where you for example need approval of libraries before use.

[base]
build_flags =
    -std=c++11

# support for Arduino libraries on Linux/Windows (default installation directories)
lib_extra_dirs = 
    ${sysenv.HOMEPATH}/Documents/Arduino/libraries
    ${sysenv.HOME}/Arduino/libraries

lib_ldf_mode = deep
lib_deps = MyNiceLib

Espressif section

This section sets up the common configuration for debug and release builds for the ESP32 MCU.

[espressif_base]
platform = espressif32
board = esp-wrover-kit
framework = arduino
build_flags = ${base.BUILD_FLAGS}

In this case we have the espressif32 platform and family of MCU’s. The board is the esp-wrover-kit and we are using arduino as our framework, build flags are pass-through by reference to the base.

Nothing special happening here…

ESP32 Release section

Since the PlatformIO default build is Release we simply inherit this environment from base and espressif_base .
I think the order of inheritance matters but I haven’t explored it.

NOTE: This section starts with env, making it a build target!

[env:ESP32Release]
extends = base, espressif_base

ESP32 Debug section

The debug section is similar to the release section but we explicitly have to state the build_type as release build. I also normally add the DEBUG as a build flag for debug-builds. PlatformIO doesn’t do this so you have to.

NOTE: This section starts with env, making it a build target!

[env:ESP32Debug]
extends = base, espressif_base
build_type = debug
build_flags =
    ${espressif_base.BUILD_FLAGS}
    -D DEBUG

NRF52 section

The NRF52 section defines another build target for the NRF52840 MCU. This one is using a completely different framework anb also specifically defines the upload protocol.

In order for this to work on the same code-base you obviously need a way to handle the differences.

[env:nrf52]
extends = base
platform = nordicnrf52
board = adafruit_feather_nrf52840
framework = zephyr
build_type = debug
upload_protocol = jlink

build_flags =
    ${base.BUILD_FLAGS}
    -D DEBUG

Libraries with Custom build scripts

Assuming you have a library supporting multiple MCU’s with various options and so forth it you can utilize some advanced stuff from PlatformIO.

Section shows

  • Custom PlatformIO.ini options MUST start with custom_
  • PlatformIO first scans then it builds!
  • Build extension sripts are written in Python

It is important to understand that PlatformIO first scans your project and all dependencies and once done PlatformIO will start the build process.

In your library root folder you need to have a description file library.json. This file is read by PlatformIO as it compiles the application.

{
  "name": "MyNiceLib",
  "version": "1.0.0",
  "build" : {
      "extraScript" : "pio_prebuild.py",
      "libArchive": false
  }
}

Here our MyNiceLib has defined an extra script which is run during the scanning process. Build scripts are written in Python.

Since only the application defines a platformio.ini the settings from the application is forwarded to our build-script. You can extend the platformio.ini with custom options.

Assuming your library has vastly different implementations for some feature depending on target platform.

[env:ESP32]
custom_option_target=esp32

In your pre_build.py script you can now modify the source folders.

def resolveTarget():
    target = env.GetProjectOption("custom_option_target","")
    env.Append(SRC_FILTER=["+<Targets/%s/>" % platform])

# Disable all targets
env.Replace(SRC_FILTER=["-<Targets/>"])
resolveTarget()

So PlatformIO uses the SRC_FILTER as to define what to build. By default PlatformIO will search recusrively in MyNiceLib\src\* for files. Ergo, the Targets folder is assumed to be in MyNiceLib\src\Targets.

Then we resolve the target by digging out the custom_option_target and appending it the SRC_FILTER. Obviously you should check that it is a valid target and so forth - that code has been omitted.

This allows you to organize a single library with different implementations for different frameworks and MCU’s by organizing our code properly and then provide a build script that selects what portions of the library to include.

Arduino\libraries
    MyNiceLib\
        src\
            library.json
            pre_build.py
            Targets\
                esp32\
                    esp32_specific.cpp
                nrf52\
                    nrf52_specific.cpp

Rebuild Issue

So you have a rebuild issue. Sometimes (or always) PlatformIO rebuilds all your code. While there are several things that can cause this the most common is some script along the library path modifies something in the build environment and the modification is different each time.

First debug the issue, run a couple of verbose builds from the command line pio run -t upload -v -e <myenv> > build_1.txt

Diff those builds. Discard any SCons at 0x0000234324. Look for changes to CPP_DEFINES, SRC_FILTER and so forth.

Chances are you (or a dependency) are flattening a hash-table/dictionary or similar and the order differs from time to time.

I had this issue in a larger library. Solution was to simply sort the data before committing it to the environment. This made it go away.

Custom MCU Boards

You have done your own PCB boards with some MCU and what not. And you want PlatformIO to find this board and treat like any other.

In your platformio.ini you should set the boards_dir variable.

[platformio]
boards_dir = /path/to/my/project/boards
[esp32]
platform = espressif32
board = my_custom_mcu

This will tell PlatformIO to look in the above folder for a file named my_custom_mcu.json.

Zephyr

The Zephyr RTOS (https://www.zephyrproject.org) is an RTOS within the Linux Foundation umbrella with some quite impressive backing from leading industry players. While not being built on Linux as such it is definitley heavily influenced by it. This makes it quite a nice option for an RTOS. That being said, it is also not as mature. Sometimes API’s goes out of scope and it can be pretty hard to find migration guides.

Zephyr and PlatformIO plays nice together although PlatformIO can be a bit behind on the version of Zephyr they support I still prefer using Zephyr through PlatformIO than directly, as it uniforms my build experience. For PlatformIO Zephyr is just another framework. Essentially you can swap out Zephyr with a simple framework=<something else> and keep the rest.

You will find more industrial solutions supporting Zephyr (Nordic Semiconductor, SiLabs, NXP, etc..) than Arudino. While Arduino is more popular wihtin the DIY community (ESP32 and friends).

Zephyr offers trimmed and slimmed hardware abstraction layer on top of the MCU. You can also extend it from a wide range of optional components. I sometimes find sheer number of configuration options a bit humilating and sometimes have to spend quite a bit of time reading both source code and documentation before getting it right. However, it is extremely valueable that the bare-metal implementation is available.


Profile picture

Written by Fredrik Kling. I live and work in Switzerland. Follow me Twitter