PlatformIO and Marlin – and how to have separate config files, for different 3Dprinter while sharing the same code.

I am using Marlin 2.0 to control my 3D printers, Marlin 2.0 can still be compiled and installed from the Arduino-IDE,although I use the supplied Makefile. To take advantage of other processors you have to use PlatformIO. In this article we will compile Marlin for my 3D-printer, just as we did in Marlin 2.0 using Makefile (and reclaiming io-pins on atmega1284p based mainboard), My 3D printers:

  • MKS GEN v1.3 using atmega2560 as used on Tevo Tarantula
  • Melzi 2.0 using atmega1284p as used on Tronxy X1
  • SKR v1.1 using lpc1768 on home made Prusa i3 Clone

PlatformIO already have a separate directory tree for object files and binaries.

We will have a subdir per board which contains the configuration files, and the platformio.ini, The platformio.ini is comparable to a Makefile, just different syntactic suger.

To do this I want a way to have per printer Configuration.h and Configuration_adv.h files. while sharing the rest. A very simple way to achieve that is to just add -Iprinter_conf_dir fot the C-preprocessor and have it be first in the include search path – but how do we do that in the PlatformIO world?

There is an option build_flags most options will make it into C*FLAGS, but you have no control of the order so no luck here.

With the option extra_scripts you can write your own code to change environment variables and how the system works. To figure out what we can do we create a simple platformio.ini file

[platformio]
src_dir = ../Marlin-git/Marlin
boards_dir = ../Marlin-git/buildroot/share/PlatformIO/boards

[env]
platform = atmelavr
framework = arduino
board = sanguino_atmega1284p
src_filter =  +<src/*> -<src/config> -<src/HAL> +<src/HAL/shared>

[env:show_env]
extra_scripts = pre:show_env.py

print_env.py is a small script which looks like this

Import("env")
print(env.Dump())

When executing “pio run -e show_env” we will see something like this

Processing show_env (platform: atmelavr; framework: arduino; board: sanguino_atmega1284p)
-----------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
{ 'AR': 'ar',
  'ARCOM': '$AR $ARFLAGS $TARGET ${_long_sources_hook(__env__, SOURCES)}',
  'ARCOMSTR': 'Archiving $TARGET',
  'ARFLAGS': ['rc'],
  'AS': 'as',
  'ASCOM': '$AS $ASFLAGS -o $TARGET $SOURCES',
  'ASCOMSTR': 'Compiling $TARGET',
  'ASFLAGS': [],
  'ASPPCOM': '$CC $ASPPFLAGS $CPPFLAGS $_CPPDEFFLAGS '
             '${_long_incflags_hook(__env__, _CPPINCFLAGS)} -c -o $TARGET '
             '$SOURCES',
  'ASPPCOMSTR': 'Compiling $TARGET',
  'ASPPFLAGS': '$ASFLAGS',
  'BOARD': 'sanguino_atmega1284p',
  'BOARD_F_CPU': '16000000L',
  'BOARD_MCU': 'atmega1284p',
  'BUILDERS': { 'Library': ,
                'LoadableModule': ,
                'Object': ,
                'Program': ,
                'SharedLibrary': ,
                'SharedObject': ,
                'StaticLibrary': ,
                'StaticObject': },
  'BUILD_CACHE_DIR': None,
  'BUILD_DIR': '$PROJECT_BUILD_DIR/$PIOENV',
  'BUILD_SCRIPT': '/t470/peter/.platformio/platforms/atmelavr/builder/main.py',
  'BUILD_SRC_DIR': '$BUILD_DIR/src',
  'BUILD_TEST_DIR': '$BUILD_DIR/test',
  'CC': 'cc',
  'CCCOM': '$CC -o $TARGET -c $CFLAGS $CCFLAGS $_CCCOMCOM $SOURCES',
  'CCCOMSTR': 'Compiling $TARGET',
  'CCFLAGS': [],
  'CFILESUFFIX': '.c',
  'CFLAGS': [],
  'COMPILATIONDB_PATH': '$BUILD_DIR/compile_commands.json',
  'CONFIGUREDIR': '#/.sconf_temp',
  'CONFIGURELOG': '#/config.log',
  'CPPDEFPREFIX': '-D',
  'CPPDEFSUFFIX': '',
  'CPPSUFFIXES': [ '.c',
                   '.C',
                   '.cxx',
                   '.cpp',
                   '.c++',
                   '.cc',
                   '.h',
                   '.H',
                   '.hxx',
                   '.hpp',
                   '.hh',
                   '.F',
                   '.fpp',
                   '.FPP',
                   '.m',
                   '.mm',
                   '.S',
                   '.spp',
                   '.SPP',
                   '.sx'],
  'CXX': 'c++',
  'CXXCOM': '$CXX -o $TARGET -c $CXXFLAGS $CCFLAGS $_CCCOMCOM $SOURCES',
  'CXXCOMSTR': 'Compiling $TARGET',
  'CXXFILESUFFIX': '.cc',
  'CXXFLAGS': [],
  'DSUFFIXES': ['.d'],
  'Dir': ,
  'Dirs': ,
  'ENV': ...
  'ESCAPE': ,
  'FRAMEWORKPATH': [],
  'FRAMEWORKS': [],
  'File': ,
  'HOST_ARCH': None,
  'HOST_OS': None,
  'IDLSUFFIXES': ['.idl', '.IDL'],
  'INCPREFIX': '-I',
  'INCSUFFIX': '',
  'LDMODULE': '$SHLINK',
  'LDMODULECOM': '$LDMODULE -o $TARGET $LDMODULEFLAGS $__LDMODULEVERSIONFLAGS '
                 '$__RPATH $SOURCES $_LIBDIRFLAGS $_LIBFLAGS',
  'LDMODULEEMITTER': [],
  'LDMODULEFLAGS': '$SHLINKFLAGS',
  'LDMODULENOVERSIONSYMLINKS': '$SHLIBNOVERSIONSYMLINKS',
  'LDMODULEPREFIX': '$SHLIBPREFIX',
  'LDMODULESUFFIX': '$SHLIBSUFFIX',
  'LDMODULEVERSION': '$SHLIBVERSION',
  'LIBDIRPREFIX': '-L',
  'LIBDIRSUFFIX': '',
  'LIBLINKPREFIX': '-l',
  'LIBLINKSUFFIX': '',
  'LIBPATH': ['$BUILD_DIR'],
  'LIBPREFIX': 'lib',
  'LIBPREFIXES': ['$LIBPREFIX'],
  'LIBSOURCE_DIRS': [ '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/lib',
                      '$PROJECT_LIBDEPS_DIR/$PIOENV',
                      '/t470/peter/.platformio/lib'],
  'LIBSUFFIX': '.a',
  'LIBSUFFIXES': ['$LIBSUFFIX', '$SHLIBSUFFIX'],
  'LINK': '$SMARTLINK',
  'LINKCOM': '$LINK -o $TARGET $LINKFLAGS $__RPATH '
             '${_long_sources_hook(__env__, SOURCES)} $_LIBDIRFLAGS $_LIBFLAGS',
  'LINKCOMSTR': 'Linking $TARGET',
  'LINKFLAGS': [],
  'MAXLINELENGTH': 128072,
  'OBJPREFIX': '',
  'OBJSUFFIX': '.o',
  'PIOENV': 'show_env',
  'PIOFRAMEWORK': ['arduino'],
  'PIOPLATFORM': 'atmelavr',
  'PLATFORM': 'posix',
  'PLATFORM_MANIFEST': '/t470/peter/.platformio/platforms/atmelavr/platform.json',
  'PROGNAME': 'program',
  'PROGPREFIX': '',
  'PROGSUFFIX': '',
  'PROG_PATH': '$BUILD_DIR/$PROGNAME$PROGSUFFIX',
  'PROJECTDATA_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/data',
  'PROJECTSRC_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Marlin',
  'PROJECT_BUILD_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/.pio/build',
  'PROJECT_CONFIG': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/platformio.ini',
  'PROJECT_CORE_DIR': '/t470/peter/.platformio',
  'PROJECT_DATA_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/data',
  'PROJECT_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test',
  'PROJECT_INCLUDE_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/include',
  'PROJECT_LIBDEPS_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/.pio/libdeps',
  'PROJECT_PACKAGES_DIR': '/t470/peter/.platformio/packages',
  'PROJECT_SRC_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Marlin',
  'PROJECT_TEST_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/test',
  'PROJECT_WORKSPACE_DIR': '/net/y/y/3D/Marlin-2.0.x-git/Tronxy_X1/test/.pio',
  'PSPAWN': ,
  'PYTHONEXE': '/home/peter/.platformio/penv/bin/python',
  'RANLIB': 'ranlib',
  'RANLIBCOM': '$RANLIB $RANLIBFLAGS $TARGET',
  'RANLIBCOMSTR': 'Indexing $TARGET',
  'RANLIBFLAGS': [],
  'RDirs': ,
  'SCANNERS': [],
  'SHCC': '$CC',
  'SHCCCOM': '$SHCC -o $TARGET -c $SHCFLAGS $SHCCFLAGS $_CCCOMCOM $SOURCES',
  'SHCCFLAGS': ['$CCFLAGS'],
  'SHCFLAGS': ['$CFLAGS'],
  'SHCXX': '$CXX',
  'SHCXXCOM': '$SHCXX -o $TARGET -c $SHCXXFLAGS $SHCCFLAGS $_CCCOMCOM $SOURCES',
  'SHCXXFLAGS': ['$CXXFLAGS'],
  'SHELL': 'sh',
  'SHLIBEMITTER': [],
  'SHLIBPREFIX': '$LIBPREFIX',
  'SHLIBSUFFIX': '.so',
  'SHLINK': '$LINK',
  'SHLINKCOM': '$SHLINK -o $TARGET $SHLINKFLAGS $__SHLIBVERSIONFLAGS $__RPATH '
               '$SOURCES $_LIBDIRFLAGS $_LIBFLAGS',
  'SHLINKFLAGS': ['$LINKFLAGS', '-shared'],
  'SHOBJPREFIX': '$OBJPREFIX',
  'SHOBJSUFFIX': '.os',
  'SMARTLINK': ,
  'SPAWN': ,
  'SRC_FILTER': [ '+ - - + '
                  '+'],
  'STATIC_AND_SHARED_OBJECTS_ARE_THE_SAME': 0,
  'TARGET_ARCH': None,
  'TARGET_OS': None,
  'TEMPFILE': ,
  'TEMPFILEARGJOIN': ' ',
  'TEMPFILEPREFIX': '@',
  'TOOLS': [ 'ar',
             'as',
             'cc',
             'c++',
             'link',
             'platformio',
             'pioplatform',
             'pioproject',
             'piomaxlen',
             'piolib',
             'pioupload',
             'piomisc',
             'pioide',
             'piosize'],
  'UNIX_TIME': 1587916285,
  'UPLOAD_PROTOCOL': 'arduino',
  'UPLOAD_SPEED': 115200,
  '_CCCOMCOM': '$CPPFLAGS $_CPPDEFFLAGS ${_long_incflags_hook(__env__, '
               '_CPPINCFLAGS)}',
  '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, '
                  '__env__)}',
  '_CPPINCFLAGS': '$( ${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, '
                  'TARGET, SOURCE)} $)',
  '_LIBDIRFLAGS': '$( ${_concat(LIBDIRPREFIX, LIBPATH, LIBDIRSUFFIX, __env__, '
                  'RDirs, TARGET, SOURCE)} $)',
  '_LIBFLAGS': '${_stripixes(LIBLINKPREFIX, LIBS, LIBLINKSUFFIX, LIBPREFIXES, '
               'LIBSUFFIXES, __env__)}',
  '__DRPATH': '$_DRPATH',
  '__DSHLIBVERSIONFLAGS': '${__libversionflags(__env__,"DSHLIBVERSION","_DSHLIBVERSIONFLAGS")}',
  '__LDMODULEVERSIONFLAGS': '${__libversionflags(__env__,"LDMODULEVERSION","_LDMODULEVERSIONFLAGS")}',
  '__RPATH': '$_RPATH',
  '__SHLIBVERSIONFLAGS': '${__libversionflags(__env__,"SHLIBVERSION","_SHLIBVERSIONFLAGS")}',
  '__libversionflags': ,
  '_concat': ,
  '_defines': ,
  '_long_incflags_hook': ,
  '_long_sources_hook': ,
  '_stripixes': ,
  'toolpath': [ '/t470/peter/.platformio/penv/lib/python3.6/site-packages/platformio/builder/tools']}
CONFIGURATION: https://docs.platformio.org/page/boards/atmelavr/sanguino_atmega1284p.html
PLATFORM: Atmel AVR 2.0.0 > Sanguino ATmega1284p (16MHz)
HARDWARE: ATMEGA1284P 16MHz, 16KB RAM, 127KB Flash
PACKAGES: 
 - framework-arduino-avr 5.0.0 
 - toolchain-atmelavr 1.50400.190710 (5.4.0)
Converting Marlin.ino
LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 5 compatible libraries
Scanning dependencies...
Dependency Graph
|--  1.0
|--  1.0
Building in release mode
...

Lots of variables, focusing on CPP variables we note that ‘_CPPINCFLAGS’ most likely have the include-file-search-path, if we could only prepend -Imy_conf_dir we would be all set. Another variable PIOENV hold the name of the current environment, we can use that to our advantage, and have per environment config-files.

The script below called prepend_cpppath.py lets the compiler look in a directory with the environment name first, or if it is not present to look in the dir where platformio.ini was located. So if we put our include files there, they will be found first – mission accompliced

import os
Import("env")
if os.path.isdir(env['PIOENV']):
    incdir = env['PIOENV']
else:
    incdir = "."

env['_CPPINCFLAGS'] = "-I%s $( ${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE)} $)" % (incdir)

With this we can create a small setup where it is convenient to have the firmware for all my 3D-printers based on the same source-code-tree. Below is how I have organized my directory layout.

 Makefile               # I like Makefile, it is like my README file
 Marlin-git/            # git clone -b 2.0.x https://github.com/MarlinFirmware/Marlin.git Marlin-git
     platformio.ini
     buildroot/
     Marlin/
         src
         Configuration.h
         Configuration_adv.h
         platformio.ini
         ...
     ...
 platformio.ini         # to compile the default setup for the printers below.
 prepend_cpppath.py
 tronxy_x1/
     Configuration.h
     Configuration_adv.h
     platformio.ini
     tronxy_x1_tiny/
         Configuration.h
         Configuration_adv.h
         platformio.ini
 tarantula/
     Configuration.h
     Configuration_adv.h
     platformio.ini
 skr-v1.1/
     Configuration.h
     Configuration_adv.h
     platformio.ini
 build/                 # Files generated by PlatformIO
     skr-v1.1/
     tarantula/
     tronxy_x1/
 lib/                   # libraries put here will override those below
 libdeps/               # libraries downloaded by PlatformIO automagically
     skr-v1.1/
     tarantula/
     tronxy_x1/

The content of the tronxy_x1/platformio.ini

# Marlin-2 via PlatformIO for Tronxy X1
# tronxy_x1: using NewLiquidCrystal over I2C via Wire
# tronxy_x1_tiny: using LCD_I2C_Tiny via SoftI2CMaster

[my]
top          = ..
hal_dir      = ${my.top}/Marlin-git/Marlin/src/HAL
script_dir   = ${my.top}/Marlin-git/buildroot/share/PlatformIO/scripts

[platformio]
libdeps_dir  = ${my.top}/libdeps
build_dir    = ${my.top}/build
lib_dir      = ${my.top}/lib
src_dir      = ${my.top}/Marlin-git/Marlin
boards_dir   = ${my.top}/Marlin-git/buildroot/share/PlatformIO/boards
default_envs = tronxy_x1, tronxy_x1_tiny

[common]
default_src_filter = + - - + +
extra_scripts = pre:${my.script_dir}/common-cxxflags.py
build_flags = -fmax-errors=5 -g -D__MARLIN_FIRMWARE__ -fmerge-all-constants -DUSE_MAKE
lib_deps      = LiquidCrystal

[env]
platform      = atmelavr
framework     = arduino
board         = sanguino_atmega1284p
build_flags   = ${common.build_flags}
lib_deps      = ${common.lib_deps}
monitor_speed = 250000
upload_speed  = 115200
src_filter    = ${common.default_src_filter}
extra_scripts = pre:../prepend_cpppath.py

[env:show_env]
extra_scripts = pre:../show_env.py

[env:tronxy_x1]
build_flags   = ${common.build_flags}
  -DLCD_I2C
  -DCUSTOM_MACHINE_NAME="\"Tronxy X1 24Apr2020\""
lib_deps      = ${common.lib_deps}

[env:tronxy_x1_tiny]
# with platformio saves text=246 data=8 bss=1
# Arduino the saving were text=642 data=18 bss=182
build_flags   = ${common.build_flags}
  -DLCD_I2C_TINY
  -DCUSTOM_MACHINE_NAME="\"Tronxy x1 24Apr2020\""
lib_deps      =
  SoftI2CMaster=https://github.com/todbot/SoftI2CMaster.git
  LiquidCrystal_I2C_Tiny=https://github.com/SpenceKonde/LiquidCrystal_I2C_Tiny.git
lib_ignore    = Wire

The special thing here is that I can be working on a version using LCD_Tiny to save some flash which is the bottleneck on this CPU

The top platformio.ini have all that is needed to compile firmware for all my 3D-printers (envs), the file is below:

# Marlin Firmware for my 3D printers
# PlatformIO Configuration File
#
# For detailed documentation with EXAMPLES:
# http://docs.platformio.org/en/latest/projectconf.html

[my]
top          = .
hal_dir      = ${my.top}/Marlin-git/Marlin/src/HAL
script_dir   = ${my.top}/Marlin-git/buildroot/share/PlatformIO/scripts

[platformio]
libdeps_dir  = ${my.top}/libdeps
build_dir    = ${my.top}/build
lib_dir      = ${my.top}/lib
src_dir      = ${my.top}/Marlin-git/Marlin
boards_dir   = ${my.top}/Marlin-git/buildroot/share/PlatformIO/boards
default_envs = tronxy_x1, tarantula

[common]
default_src_filter = +<src/*> -<src/config> -<src/HAL> +<src/HAL/shared>
extra_scripts = pre:${my.script_dir}/common-cxxflags.py
build_flags = -fmax-errors=5 -g -D__MARLIN_FIRMWARE__ -fmerge-all-constants
lib_deps = LiquidCrystal

[env]
framework     = arduino
build_flags   = ${common.build_flags}
lib_deps      = ${common.lib_deps}
monitor_speed = 250000
upload_speed  = 115200
extra_scripts = pre:prepend_cpppath.py

[env:show_env]
extra_scripts = pre:show_env.py

[env:tarantula]
platform      = atmelavr
board         = megaatmega2560
src_filter    = ${common.default_src_filter} +<src/HAL/AVR>
build_flags   = ${common.build_flags}
  -DCUSTOM_MACHINE_NAME="\"Tarantula 26Apr2020\""

[env:tronxy_x1]
platform      = atmelavr
board         = sanguino_atmega1284p
src_filter    = ${common.default_src_filter} +<src/HAL/AVR>
build_flags   = ${common.build_flags}
  -DLCD_I2C
  -DCUSTOM_MACHINE_NAME="\"Tronxy X1 24Apr2020\""


#
# NXP LPC176x ARM Cortex-M3
#
[env:LPC1768]
platform          = https://github.com/p3p/pio-nxplpc-arduino-lpc176x/archive/0.1.2.zip
board             = nxp_lpc1768
build_flags       = -DU8G_HAL_LINKS -I${my.hal_dir}/LPC1768/include -I${my.hal_dir}/LPC1768/u8g ${common.build_flags}
# debug options for backtrace
#  -funwind-tables
#  -mpoke-function-name
lib_ldf_mode      = off
lib_compat_mode   = strict
extra_scripts     = ${my.hal_dir}/LPC1768/upload_extra_script.py
src_filter        = ${common.default_src_filter} +<src/HAL/LPC1768>
lib_deps          = LiquidCrystal

I haven’t made up my mind if I like PlatformIO yet.

My first reaction was: why bother, we have Makefiles, and OpenWRT and BSD-ports have showed us how to get/compile full systems long before PlatformIO was invented.

On the other hand it only took me a day or two to figure out a solution to my problem, and it only required a few lines of code, so it seems PlatformIO is highly flexible.

The real power with PlatformIO (and OpenWrt Makefiles) is that somebody already did most of the work and integrated everything for you, so you can create one of their precooked meals without much effort. I seem to always to end up with a desire to use it a little differently.

The good news is: if PlatformIO do not do work the way you think is should work – you can fix it, without too much effort !

If you like me prefer the commandline over the Arduino-IDE or other click-systems, you will be better of adopting the PlatformIO to compile Marlin, than try to get the supplied Makefile to do what you want.

Stay tuned, there might be more coming,

This entry was posted in 3D printer, Arduino. Bookmark the permalink.