UP | HOME
Published
2017-01-04

This article will show you how to setup a basic project on NixOS for developing embedded applications for the Teensy 3.1 without using the Arduino IDE.

At the end of the article you will have a complete project you can just run nix-shell inside and start working.

I will go through how to setup the project step-by-step, explaining as I go. If you are only interested in the source files, just scroll to the end.

For this article to make most sense you should be familiar with using NixOS, GNU Make and GCC.

Dependencies

I wanted to setup a project where I could write code for the device itself, but also unit tests that ran on my host.

Which meant I needed to have access to two compilers: gcc for my host and gcc-arm-none-eabi that is used for the device.

The nixpkgs for those two compilers have the attribute names gcc and gcc-arm-embedded respectively. At the time the Teensy core library could not be compiled with gcc-arm-embedded above version 4.7, so I needed to use the package gcc-arm-embedded-4_7 instead.

To upload the built hex-files to the Teensy device I also needed the Teensy Loader for CLI. Which has the attribute name teensy-loader-cli in nixpkgs.

I created a default.nix file with the dependencies my project needed:

default.nix

{ stdenv, lib, gcc, gcc-arm-embedded-4_7, teensy-loader-cli }:

stdenv.mkDerivation rec {
  name    = "teensy-dev-${version}";
  version = "1.0.0";
  src     = ./src;

  buildInputs = [
    gcc
    gcc-arm-embedded-4_7
    teensy-loader-cli
  ];
}

And then I created a simple shell.nix file that called my package definition:

shell.nix

{ system ? builtins.currentSystem }:

let
  pkgs = import <nixpkgs> { inherit system; };
in
  pkgs.callPackage ./default.nix {}

I could then run nix-shell and check that the needed tools was be available:

$ nix-shell

[nix-shell]$ whereis gcc
gcc: /nix/store/4mhq8ic8wk46wpwjyghap1x7xfdk2rj3-gcc-wrapper-5.4.0/bin/gcc /nix/store/haqh3hshg8w1779xgyzr4pk9q706ra30-gcc-5.4.0/bin/gcc

[nix-shell]$ whereis arm-none-eabi-gcc
arm-none-eabi-gcc: /nix/store/h085f3ksivc2183hfn1sfbikgi5yf5bx-gcc-arm-embedded-4.7-2013q3-20130916/bin/arm-none-eabi-gcc

[nix-shell]$ whereis teensy-loader-cli
teensy-loader-cli: /nix/store/q8q2va34kb064p4knm37d1kda5rp3k1g-teensy-loader-cli-2.1/bin/teensy-loader-cli

I could then start to figure out how to use my Teensy board without the Arduino IDE.

Teensy core library build

First off, to make a device Arduino compatible you need to create an Arduino core library implementation.

Turns out the Teensy implementation was available on the PaulStoffregen/cores Github repo. (Paul Stoffregen is the creator of the Teensy boards).

The repo had a teensy3 folder with the specific implementation along with a Makefile and a target main.cpp file, that served as a good example on how to build the library without the Arduino IDE.

After removing the Arduino specific stuff from the Makefile and simplifying it, I had this Makefile:

OPTIONS =  -DF_CPU=72000000 -DUSB_SERIAL -DLAYOUT_US_ENGLISH
OPTIONS += -DUSING_MAKEFILE -D__MK20DX256__

CPPFLAGS = -Wall -g -Os -mcpu=cortex-m4 -mthumb -MMD $(OPTIONS) -I.
CXXFLAGS = -std=gnu++0x -felide-constructors -fno-exceptions -fno-rtti

LDFLAGS =  -Os -Wl,--gc-sections,--defsym=__rtc_localtime=0
LDFLAGS += --specs=nano.specs -mcpu=cortex-m4 -mthumb -Tmk20dx256.ld

LIBS = -lm

CC      = arm-none-eabi-gcc
CXX     = arm-none-eabi-g++
OBJCOPY = arm-none-eabi-objcopy
SIZE    = arm-none-eabi-size

C_FILES   := $(wildcard *.c)
CPP_FILES := $(wildcard *.cpp)
OBJS      := $(C_FILES:.c=.o) $(CPP_FILES:.cpp=.o)

all: $(OBJS)

-include $(OBJS:.o=.d)

There are a number of different Teensy3 boards, the one I was using was the 3.1 with the MK20DX256VLH7 processor. At the time there was also the 3.0 with the MK20DX128VLH5 processor.

When building the core library for the Teensy3 boards you need to supply 3 things that change between the boards revisions:

  • Clock rate of the processor
  • The processor name
  • Path to processor specific linker script

These options are supplied as flags in the makefile variables OPTIONS and LDFLAGS.

The clock rate is set in OPTIONS using the flag -DF_CPU=[herz]. The processor on the Teensy3.1 has a clock rate of 72 Mhz, so I used the flag: -DF_CPU=72000000.

The processor name is set in OPTIONS using the flag -D__[short name]__. The full processor name of the Teensy3.1 is MK20DX256VLH7, so I used the short name in the flag: -D__MK20DX256__.

All processor specific linker scripts are situated in the root of the teensy3 source tree. They have the name [short name].ld where short name is the processor name in lower case. The linker script path is set in LDFLAGS using the flag -T[short name].ld, so I used the flag: -Tmk20dx256.ld.

Teensy core library derivation

The Makefile above produces object files for all C and C++ files it finds in the current directory.

To avoid having to include source code and building of the Teensy core library in my project, I created a separate nixpkg definition. That meant that the Teensy library was built once and then put in the nix store.

For simplicity I wanted to be able to use pkg-config when including and linking against the teensy library in my project. I also wanted to create a nixpkg definition that could be used for other revisions of the Teensy board.

To do this I created 4 files. First a part of the Makefile that contained all the flags, that could be included in the Makefile, but also that I could include in my projects Makefile:

teensy3-core/flags.mk

OPTIONS =  -DF_CPU=@clockRate@ -DUSB_SERIAL -DLAYOUT_US_ENGLISH
OPTIONS += -DUSING_MAKEFILE -D__@mcu@__

CPPFLAGS = -mcpu=cortex-m4 -mthumb -MMD $(OPTIONS)
CXXFLAGS = -std=gnu++0x -felide-constructors -fno-exceptions -fno-rtti

LDFLAGS =  -Wl,--gc-sections,--defsym=__rtc_localtime=0 --specs=nano.specs

LIBS = -lm

CC  = arm-none-eabi-gcc
CXX = arm-none-eabi-g++

Then the actual Makefile:

teensy3-core/Makefile

include flags.mk

CPPFLAGS += -I.
LDFLAGS  += -T@linkerScript@

C_FILES   := $(wildcard *.c)
CPP_FILES := $(wildcard *.cpp)
OBJS      := $(C_FILES:.c=.o) $(CPP_FILES:.cpp=.o)

all: $(OBJS)

-include $(OBJS:.o=.d)

After that I created a pkg-config file:

teensy3-core/libteensy3-core.pc

prefix=@out@
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${exec_prefix}/include

Name: libteensy3-core
Description: Teensy3 core static library and headers
Version: @version@

Libs: -L${libdir} -l:libteensy3-core.a -T${includedir}/@linkerScript@
Cflags: -I${includedir}

Note that values wrapped in @...@ are values that nix will replace/substitute when the package is built.

After that I created a nixpkg definition that tied everything together that you could supply two values (in addition to the dependencies):

  • clockRate
  • mcu

Using these values nix can take care of creating the right files, building the static library and installing everything into the nix store.

teensy3-core/default.nix

{ stdenv, lib, fetchFromGitHub, writeTextFile, gcc-arm-embedded-4_7
, clockRate, mcu
}:

let
  linkerScript = (lib.toLower mcu) + ".ld";
in
  stdenv.mkDerivation rec {
    name    = "teensy3-core-${version}";
    version = "1.3.1";
    src  = fetchFromGitHub {
      owner  = "PaulStoffregen";
      repo   = "cores";
      rev    = "3a6e99ca1ca3ed3948e90fa72d8e27f787f3dd03";
      sha256 = "0lqdnxqyvmgs1841z10nzzkp4cv8h1njfz3379nmm2xh3h4jhxck";
    };

    buildInputs = [
      gcc-arm-embedded-4_7
    ];

    phases = ["unpackPhase" "buildPhase" "installPhase"];

    buildPhase = ''
      rm -f teensy3/main.cpp

      substitute ${./flags.mk} ./teensy3/flags.mk \
        --subst-var-by clockRate ${toString clockRate} \
        --subst-var-by mcu ${mcu}

      substitute ${./Makefile} ./teensy3/Makefile \
        --subst-var-by linkerScript ${linkerScript}

      make -C teensy3
      ar rvs libteensy3-core.a *.o
    '';

    installPhase = ''
      mkdir -p $out/{include,lib}
      mkdir $out/lib/pkgconfig

      mv *.a $out/lib
      cp teensy3/* $out/include -R;

      rm $(find $out -name "*.c" -o -name "*.cpp")

      substitute ${./teensy3-core.pc} $out/lib/pkgconfig/teensy3-core.pc \
        --subst-var-by out $out \
        --subst-var-by version ${version} \
        --subst-var-by linkerScript ${linkerScript}
    '';
  }

Using the Teensy core library

I could then update my default.nix definition to include the teensy3 core library as a dependency with the right values for my board:

default.nix

{ stdenv, lib, gcc, gcc-arm-embedded-4_7, teensy-loader-cli, callPackage
, fetchFromGitHub, pkgconfig
}:

let
  teensy3-core = callPackage ./teensy3-core/default.nix {
    clockRate = 72000000;
    mcu       = "MK20DX256";
  };
in
  stdenv.mkDerivation rec {
    name    = "teensy-dev-${version}";
    version = "1.1.0";
    src     = ./src;

    buildInputs = [
      gcc
      gcc-arm-embedded-4_7
      teensy-loader-cli
      teensy3-core
      pkgconfig
    ];
  }

After that the teensy3-core library was readily available via pkg-config:

$ nix-shell

[nix-shell]$ pkg-config teensy3-core --libs
-L/nix/store/bs2q2x3fi2yn5hh3z6lf8kzrcly9nl04-teensy3-core-1.3.1/lib -l:libteensy3-core.a -T/nix/store/bs2q2x3fi2yn5hh3z6lf8kzrcly9nl04-teensy3-core-1.3.1/include/mk20dx256.ld

[nix-shell]$ pkg-config teensy3-core --cflags
-I/nix/store/bs2q2x3fi2yn5hh3z6lf8kzrcly9nl04-teensy3-core-1.3.1/include

Compiling the project

To actually build the project I created a Makefile that included the flags.mk file from teensy3-core, to avoid having to type all the flags again:

src/Makefile

include $(shell pkg-config --variable=includedir teensy3-core)/flags.mk

CPPFLAGS += -I. $(shell pkg-config --cflags teensy3-core)
LDFLAGS  += $(shell pkg-config --libs teensy3-core)

OBJCOPY = arm-none-eabi-objcopy
SIZE    = arm-none-eabi-size

DEVICE_ELF  = main.elf
DEVICE_HEX  = $(DEVICE_ELF:.elf=.hex)
C_FILES    := $(wildcard *.c)
CPP_FILES  := $(wildcard *.cpp)
OBJS       := $(C_FILES:.c=.o) $(CPP_FILES:.cpp=.o)

all: $(DEVICE_HEX)

$(DEVICE_ELF): $(OBJS)
        $(CC) $(CPPFLAGS) $(LDFLAGS) -o $@ $(OBJS)

$(DEVICE_HEX): $(DEVICE_ELF)
        $(SIZE) $(DEVICE_ELF)
        $(OBJCOPY) -O ihex -R .eeprom $(DEVICE_ELF) $@

And my basic application:

src/main.cpp

#include "WProgram.h"

extern "C" int main(void)
{
  Serial.begin(9600);

  Serial.println("look ma, no Arduino IDE!");

  for (;;) {}
}

I could then just run make inside the src directory:

nix-shell

[nix-shell]$ cd src

[nix-shell]$ make
arm-none-eabi-g++ -std=gnu++0x -felide-constructors -fno-exceptions -fno-rtti -mcpu=cortex-m4 -mthumb -MMD -DF_CPU=72000000 -DUSB_SERIAL -DLAYOUT_US_ENGLISH -DUSING_MAKEFILE -D__MK20DX256__ -I. -I/nix/store/bs2q2x3fi2yn5hh3z6lf8kzrcly9nl04-teensy3-core-1.3.1/include  -c -o main.o main.cpp
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -MMD -DF_CPU=72000000 -DUSB_SERIAL -DLAYOUT_US_ENGLISH -DUSING_MAKEFILE -D__MK20DX256__ -I. -I/nix/store/bs2q2x3fi2yn5hh3z6lf8kzrcly9nl04-teensy3-core-1.3.1/include -Wl,--gc-sections,--defsym=__rtc_localtime=0 --specs=nano.specs -L/nix/store/bs2q2x3fi2yn5hh3z6lf8kzrcly9nl04-teensy3-core-1.3.1/lib -l:libteensy3-core.a -T/nix/store/bs2q2x3fi2yn5hh3z6lf8kzrcly9nl04-teensy3-core-1.3.1/include/mk20dx256.ld -o main.elf  main.o
arm-none-eabi-size main.elf
   text    data     bss     dec     hex filename
   1124       0      28    1152     480 main.elf
arm-none-eabi-objcopy -O ihex -R .eeprom main.elf main.hex

After that my program was ready to be uploaded to the device:

[nix-shell]$ teensy-loader-cli --mcu=mk20dx256 -w -v main.hex

And there you have it! A minimal example of a fully working project for Teensy development on NixOS without the Arduino IDE.

Hopefully this article have been useful to someone else!

The code

If you didn't read the article and just want to get the code you can download it here.

That tar-file contains the following source tree:

code/teensy-development-on-nixos
├── default.nix
├── shell.nix
├── src
│   ├── main.cpp
│   └── Makefile
└── teensy3-core
    ├── default.nix
    ├── flags.mk
    ├── Makefile
    └── teensy3-core.pc

2 directories, 8 files

Then you just run nix-shell in the root and build the project inside the nix-shell:

cd src
make

After the build is complete, you can upload the application to your Teensy with:

teensy-loader-cli --mcu=mk20dx256 -w -v main.hex