A Guided Tour through a Build Execution
Finally I want to make a guided tour of how exactly the compiler options are collected for a simple PlatformIO Arduino project.
We start a standard project created by pio init -b due
for the Arduino Due board, that gives the platformio.ini
of
[env:due]
platform = atmelsam
board = due
framework = arduino
and an empty src\main.cpp
sketch with
#include <Arduino.h>
void setup() {}
void loop() {}
Per above we already know that that will then be using platform-atmelsam
together with the Due’s due.json board definition file. Note some important options in there like "core": "arduino"
, "mcu": "at91sam3x8e"
, "cpu": "cortex-m3"
, "extra_flags": "-D__SAM3X8E__ -DARDUINO_SAM_DUE"
, "variant": "arduino_due_x"
, (upload) "protocol": "sam-ba"
. They will be referred to later.
Upon building of the project (pio run
), the platform’s platform.py
will be invoked. Most notabaly we setup the needed framework package
Which, with the info given above, will be framework-arduino-sam
.
Next, the builder/main.py
of the platform is invoked.
Note that the toolchain package (toolchain-gccarmnoneeabi
) is not modified, so the standard version will apply.
We first grab create / grab some variables from SCons and PlatformIO
We then go on to tell the SCons construction environment what our compiler and other tools are. Refer to the above chapter about SCons and its documentation to understand that better.
We teach SCons how to convert from a .elf
to a .bin
file. This will be important later as the upload programm will need a .bin
file, not an .elf
. We do so using $OBJCOPY
, which, through SCons’s variable substition, will go to arm-none-eabi-objcopy
.
We tell SCons that we want the firmware (as ELF) and additionally the .bin
from it (remember upload_protocol = "sam-ba"
)
We also inject variables in the SCons environment needed for uploading the project, triggering this code
with a small addition.
That completes main.py
. Then, since we specified framework = arduino
, and the platform.json
has mapped
we will now be executing arduino.py.
This wrapper script will check what sub-SCons-script to execute
leading us into arduino-sam.py
.
One of the first things that script will do is execute arduino-common.py
.
In that file then, we start doing actual compiler flags setup with flags that are apparently common to all the Arduino cores that AtmelSAM will be compiling.
(click above to see the full code). This includes CCFLAGS
for C and C++ options, CXXFLAGS
, CPPDEFINES
and `CFLAGS´.
It continues with some library path and linker settings. These will be used in the final link command. We can e.g. see that it links agsinst libm
(math) by default. It also sets up the libraries/
folder, in which an Arduino core typically has the list of all its internal libraries, to LIBSOURCE_DIRS
.
Due to the attributes set in the board definition file, we will also be triggering this code
which sets up our linker script as flash.ld
from the due’s variant folder.
and again since the board definition file has the needed flags for USB, we will be hitting this code
That finishes execution of arduino-common.py
, so we are back in arduino-sam.py
.
In there, we add more CPPPATH
(include folders!), CPPDEFINES
, LINKFLAGS
and LIBS
, just like the original Arduino core would do.
We finish up by adding a reference to the source code of the Arduino core variant (arduino_due_x
) source files, typically variant.cpp
and friends, using env.BuildLibrary()
and modifications to env
.
And finally, the Arduino core files (source) are added to the build system.
Now the SCons environment is set up. Additionally, per links above, PlatformIO now processes special platformio.ini
like build_flags
for additional compiler flags, discovers used libraries and adds those to the build system etc. In this projects we are having none of those. PlatformIO will however add the extra_flags
attribute of the board JSON file as flags, as listed above.
With all of this, SCons / PlatformIO can finally start building the firmware. In a verbose compilation (pio run -v
), we see all the flags we have collected.
arm-none-eabi-g++ -o .pio\build\due\src\main.cpp.o -c -fno-rtti -fno-exceptions -std=gnu++11 -fno-threadsafe-statics -Os -ffunction-sections -fdata-sections -Wall -mcpu=cortex-m3 -mthumb -nostdlib --param max-inline-insns-single=500 -DPLATFORMIO=50200 -D__SAM3X8E__ -DARDUINO_SAM_DUE -DARDUINO=10805 -DF_CPU=84000000L -DUSBCON -DUSB_VID=0x2341 -DUSB_PID=0x003E “-DUSB_PRODUCT="Arduino Due"” -DUSB_MANUFACTURER="Arduino" -DARDUINO_ARCH_SAM -Iinclude -Isrc -IC:\Users\Max\.platformio\packages\framework-arduino-sam\cores\arduino -IC:\Users\Max\.platformio\packages\framework-arduino-sam\system\libsam -IC:\Users\Max\.platformio\packages\framework-arduino-sam\system\CMSIS\CMSIS\Include -IC:\Users\Max\.platformio\packages\framework-arduino-sam\system\CMSIS\Device\ATMEL -IC:\Users\Max\.platformio\packages\framework-arduino-sam\variants\arduino_due_x src\main.cpp
Is e.g. the invocation for building the main.cpp
, all others are equivalent to that. You can go ahead and double check that every single compilation option listed here really comes from the pieces of code discussed above.
The final linker command would be
arm-none-eabi-g++ -o .pio\build\due\firmware.elf -T flash.ld -Os -mcpu=cortex-m3 -mthumb -Wl,–gc-sections -Wl,–check-sections -Wl,–unresolved-symbols=report-all -Wl,–warn-common -Wl,–warn-section-align -Wl,–entry=Reset_Handler -u _sbrk -u link -u _close -u _fstat -u _isatty -u _lseek -u _read -u _write -u _exit -u kill -u _getpid .pio\build\due\src\main.cpp.o -L.pio\build\due -LC:\Users\Max.platformio\packages\framework-arduino-sam\variants\arduino_due_x\linker_scripts\gcc -LC:\Users\Max.platformio\packages\framework-arduino-sam\variants\arduino_due_x -Wl,–start-group .pio\build\due\libFrameworkArduinoVariant.a .pio\build\due\libFrameworkArduino.a -lm -lsam_sam3x8e_gcc_rel -lgcc -Wl,–end-group
Which again matches what we’ve seen above, e.g. regarding the LIBS
and link flags, etc.
As for uploading, when we do a pio run -t upload -v
(verbose upload), we see
Auto-detected: COM13
Forcing reset using 1200bps open/close on port COM13
bossac --info --debug --port “COM13” --write --verify --reset --erase -U false --boot .pio\build\due\firmware.bin
As the upload command. The first bit is about the PlatformIO core using COM port autodetection (and reset into bootloader mode), the second part is, as defined by the main.py
code, the invocation for bossac
as configured for that board.
Do I need to understand all this to develop with PlatformIO?
No. I’d say in 99% of cases, users do need to know how PlatformIO works internally, beyond how it is described in the documentation. Having to touch a platform manifest, platform script, a builder script or board definition file is rare. Most things can be solved with simple platformio.ini
instructions without ever touching those things above. For more advanced use cases, scripting is also supported.
- Don’t like a particular compiler flag that the builder script set for you?
build_unflags
it. - Want a particular compiler flag (e.g., a define, include directory, optimization flag, …) that the builder script didn’t set for you? Add it via
build_flags
. - Don’t like a specific attribute that was set it the board definition file? Change it dynamically using
board_<section>.<attribute> = <value>
(example). - Need to exclude certain files from the build that were auto-included?
srcFilter
them. - Need to add a flag to the upload execution?
upload_flags
. - Need to modify the SCons environment (and thus have access to all the compiler settings via a script) for some really specially stuff? Use
extra_scripts
and Advanced Scripting. - You want to do something special in the project’s build process, like executing some action before or after a file is compiled? Use above Advanced Scripting with Pre- and Post actions.
- Need some custom upload method that PlatformIO does not implement by default? Use the
upload_command
in theplatformio.ini
or Advanced Scripting. - Need to invoke some custom debug server with flags that PlatformIO doesn’t implement? Use
debug_server
and friends. - Don’t like the package version or source that the build script gives you? Use
platform_packages
or specify a different version of platform. - Your library needs to be built in a special way (e.g., compiler flags, linking precompiled file, custom logic at each compilation,…)? Use a library.json, which also has the possibility to use advanced scripting.
- etc., etc…
Documentation and further reading
Refer to the official documentation
platform_packages
- Custom Platform (explains well how in general platforms work)
- Custom boards
- using different releases of a platform, e.g. Atmel SAM
- Advanced Scripting
- Platform configuration options
- Framework configuration options
And these topics
- Custom framework? or platform? - #2 by maxgerhardt
- How to specify desired version of an installed framework? - #6 by maxgerhardt
- the lower half of Help Creating Custom Platform that builds and uploads custom firmware - #4 by maxgerhardt
- How to import/use custom arduino board for STM32L4 - #5 by maxgerhardt
And of course the source code for the linked entities, such as the PlatformIO core, platforms, builder scripts, etc, for a more info on these specific topics.