Local build using gcc-arm

It is possible to build both application firmware and Device OS locally using the gcc-arm toolchain on Windows, Mac and Linux. There are a few reasons you might be interested in doing this:

  • To make a custom modification to Device OS
  • To test out Device OS changes that haven't been released yet
  • To work entirely offline, with no Internet access
  • To use the GDB debugger

While you can still use these instructions, a much easier way to use gcc-arm is to install Particle Workbench. It provides an automated way to install local development tools.

This note explains how to do a fully native installation. It's also possible to do a Docker installation. And, of course, you can use the cloud compilers Particle Build, Particle Dev, or Particle CLI which are much easier to install and use.

Select computer operating system:


Selecting a source directory

One of the things you need to do is decide where to store your source. For the examples here, I've used the following.

Check out the source

  • Use cd to position yourself into the src directory, as described above.

  • Once you have Git installed, getting the source is easy:

git clone https://github.com/particle-iot/device-os.git
cd device-os
git checkout release/stable
  • The checkout command used to specify "latest" instead of "release/stable" but you should now use "release/stable" instead.

You may prefer to check out a specific version instead:

git checkout release/v0.7.0
  • Then do a build. This will build all of the Device OS firmware, plus Tinker, for the Photon.

For Gen3 devices (Argon, Boron, Xenon)

For Gen3 (mesh) devices, you should checkout either:

  • A specific release such as release/v0.9.0
  • The branch mesh-develop

The develop and stable branches do not support mesh devices at this time.

You must also update the submodules after checkout of the device-os tree:

cd device-os
git submodule update --init


cd modules
make all PLATFORM=photon

These commands work for Windows, Linux and Mac. For Windows, remember to use the Cygwin Terminal or MinGW Shell, not the regular Windows Command Prompt.

  • If you update the source, make sure you add the clean option:
cd modules
make clean all PLATFORM=photon
  • If you're building for the Electron:
cd modules
make all PLATFORM=electron
  • If you're building for the P1:
cd modules
make all PLATFORM=P1

These commands build the Device OS firmware and Tinker. You can find the built binaries in the following locations after a successful build.


  • firmware/build/target/system-part1/platform-6-m/system-part1.bin
  • firmware/build/target/system-part2/platform-6-m/system-part2.bin
  • firmware/build/target/user-part/platform-6-m/user-part.bin


  • firmware/build/target/system-part1/platform-10-m/system-part1.bin
  • firmware/build/target/system-part2/platform-10-m/system-part2.bin
  • firmware/build/target/system-part3/platform-10-m/system-part3.bin
  • firmware/build/target/user-part/platform-10-m/user-part.bin

Building using APPDIR

I like to use the APPDIR feature to keep my code separate from the firmware repository.

  • For this example I created a new directory in my src directory, as described above.
cd src
mkdir blinkled
  • And I created the file blinkled.cpp.
cd firmware/modules
make all PLATFORM=photon APPDIR=../../blinkled
  • This builds everything, including Device OS firmware. You will probably want to do this once, but for subsequent builds, you can save a lot of time by using the command:
cd firmware/main
make all PLATFORM=photon APPDIR=../../blinkled

Here's what a build window might look like in the Cygwin Terminal:

Windows build

If you look in the target directory in the blinkled directory you'll find a blinkled.bin. This is the user firmware binary that you can flash using the Particle CLI:

particle flash YOUR_DEVICE_NAME target/blinkled.bin

Or, with the device in DFU mode (blinking yellow)

particle flash --usb target/blinkled.bin

Or, with the device in listening mode (blinking dark blue):

particle flash --serial target/blinkled.bin

Building with libraries

To use libraries with local build you should use the extended project structure within the APPDIR.

It looks like this:


You can start out with an empty file for project.properties.

To add a library you typically cd into myappdir then use a CLI library copy command for example:

particle library copy neopixel

This copies the source to the neopixel library to lib/neopixel.

If you have a project in the extended format, building with APPDIR automatically builds all of the libraries in lib as well, and all of the src directories in the libraries are added to the include search path, so for example you can just:

#include "neopixel.h"

Including additional header directories

Sometimes you want to include other directories as locations for header files. For example, say you are using the extended project structure within the APPDIR with the following layout:


The project.properties can be an empty file; it just signifies that you are using the extended project layout.

This is my main.cpp. Of note is that it just includes testinc.h, not inc/testinc.h.

#include "Particle.h"

#include "testinc.h"

void setup() {

void loop() {

Adding additional include directories requires adding a build.mk file in your src directory. The first part should be the standard boilerplate.

But after that, you can add other things, such as adding additional include directories.

# Standard behavior must be included here
INCLUDE_DIRS += $(SOURCE_PATH)/$(USRSRC)  # add user sources to include path
CPPSRC += $(call target_files,$(USRSRC_SLASH),*.cpp)
CSRC += $(call target_files,$(USRSRC_SLASH),*.c)

APPSOURCES=$(call target_files,$(USRSRC_SLASH),*.cpp)
APPSOURCES+=$(call target_files,$(USRSRC_SLASH),*.c)

# Custom stuff can be added here

Flashing using DFU during build

This technique is handy with an Electron where you want to flash over USB, though it also works with the Photon:

cd firmware/main
make all PLATFORM=electron program-dfu APPDIR=../../blinkled

If you used firmware/modules instead, it would DFU the system and the user firmware, which is handy because it will load both system and user firmware at once with a single command.