- 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