aboutsummaryrefslogtreecommitdiff
path: root/articles/2021-10-11_reproducible_development_environment_teensy.org
blob: e0fd62606698f91f8ece1b44fcbee5f3ec80c984 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
* Reproducible development environment for Teensy
So for a change of scenery I recently started to mess around with microcontrollers again.
Since the last time that I had any real contact with this area was probably around a decade ago --- programming an [[https://www.dlr.de/rm/en/desktopdefault.aspx/tabid-14006/#gallery/34068][ASURO]] robot --- I started basically from scratch.
Driven by the goal of building and programming a fancy mechanical keyboard (as it seems to be the trendy thing to do) I chose the Arduino-compatible [[https://www.pjrc.com/store/teensy40.html][Teensy 4.0]]
board. While I appreciate the rich and accessible software ecosystem for this platform, I don't really want to use some special IDE, applying amongst other things[fn:0]
weird non-standard preprocessing to my code. In this vein it would also be nice to use my accustomed [[https://nixos.org][Nix-based]] toolchain which leads me to this article.

Roughly following what [[https://rzetterberg.github.io/teensy-development-on-nixos.html][others did]] for Teensy 3.1 while adapting it to Teensy 4.0 and Nix flakes it is simple to build and flash
some basic C++ programs onto a USB-attached board. The adapted version of the Arduino library is available on [[https://github.com/PaulStoffregen/cores][Github]] and can
be compiled into a shared library using flags

#+BEGIN_SRC make
MCU     = IMXRT1062
MCU_DEF = ARDUINO_TEENSY40

OPTIONS  = -DF_CPU=600000000 -DUSB_SERIAL -DLAYOUT_US_ENGLISH
OPTIONS += -D__$(MCU)__ -DARDUINO=10813 -DTEENSYDUINO=154 -D$(MCU_DEF)

CPU_OPTIONS = -mcpu=cortex-m7 -mfloat-abi=hard -mfpu=fpv5-d16 -mthumb

CPPFLAGS = -Wall -g -O2 $(CPU_OPTIONS) -MMD $(OPTIONS) -ffunction-sections -fdata-sections
CXXFLAGS = -felide-constructors -fno-exceptions -fpermissive -fno-rtti -Wno-error=narrowing -I@TEENSY_INCLUDE@
#+END_SRC

included into a run-of-the-mill Makefile and relying on the =arm-none-eabi-gcc= compiler. Correspondingly, the
derivation for the core library [[http://code.kummerlaender.eu/teensy-env/tree/core.nix?id=44c1837717f748b891df1a6c88a72ec3a51470ce][=core.nix=]] is straight forward. It clones a given version of the library repository,
jumps to the =teensy4= directory, deletes the example =main.cpp= file to exclude it from the library and applies a Makefile
adapted from the default one. For the result only headers, common flags and the linker script =IMXRT1062.ld=
are exported.

As existing Arduino /sketches/ commonly consist of a single C++ file (ignoring some non-standard stuff for later) most
builds can be handled generically by a mapping of =*.cpp= files into flashable =*.hex= files. This is realized by the following
function based on the =teensy-core= derivation and a [[http://code.kummerlaender.eu/teensy-env/tree/Makefile.default?id=44c1837717f748b891df1a6c88a72ec3a51470ce][default makefile]]:

#+BEGIN_SRC nix
build = name: source: pkgs.stdenv.mkDerivation rec {
  inherit name;

  src = source;

  buildInputs = with pkgs; [
    gcc-arm-embedded
    teensy-core
  ];

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

    cp ${./Makefile.default} Makefile
    export TEENSY_PATH=${teensy-core}
    make
  '';

  installPhase = ''
    mkdir $out
    cp *.hex $out/
  '';
};
#+END_SRC

The derivation yielded by =build "test" ./test= results in a =result= directory containing a =*.hex= file for each
C++ file contained in the =test= directory. Adding a =loader= function to be used in convenient =nix flake run=
commands

#+BEGIN_SRC nix
loader = name: path: pkgs.writeScript name ''
  #!/bin/sh
  ${pkgs.teensy-loader-cli}/bin/teensy-loader-cli --mcu=TEENSY40 -w ${path}
'';
#+END_SRC

a reproducible build of the canonical /blink/ example[fn:1] is realized using:

#+BEGIN_SRC sh
nix flake clone git+https://code.kummerlaender.eu/teensy-env --dest .
nix run .#flash-blink
#+END_SRC

Expanding on this, the =teensy-env= flake also provides convenient =image(With)= functions for building
programs that depend on additional Arduino libraries such as for controlling servos. E.g. the build
of a program =test.cpp= placed in a =src= folder

#+BEGIN_SRC cpp
#include <Arduino.h>
#include <Servo.h>

extern "C" int main(void) {
  Servo servo;
  // Servo connected to PWM-capable pin 1
  servo.attach(1);
  while (true) {
    // Match potentiometer connected to analog pin 7
    servo.write(map(analogRead(7), 0, 1023, 0, 180));
    delay(20);
  }
}
#+END_SRC

is fully described by the flake:

#+BEGIN_SRC nix
{
  description = "Servo Test";

  inputs = {
    teensy-env.url = git+https://code.kummerlaender.eu/teensy-env;
  };

  outputs = { self, teensy-env }: let
    image = teensy-env.custom.imageWith
      (with teensy-env.custom.teensy-extras; [ servo ]);

  in {
    defaultPackage.x86_64-linux = image.build "servotest" ./src;
  };
}
#+END_SRC

At first I expected the build of [[http://www.ulisp.com/][uLisp]][fn:2] to proceed equally smoothly as this implementation of Lisp
for microcontrollers is provided as a single [[https://raw.githubusercontent.com/technoblogy/ulisp-arm/master/ulisp-arm.ino][=ulisp-arm.ino=]] file. However, the =*.ino= extension
is not just for show here as beyond even the replacement of =main= by =loop= and =setup= --- which
would be easy to fix --- it relies on further non-standard preprocessing offered by the
Arduino toolchain. I quickly aborted my efforts towards patching in e.g. the forward-declarations
which are automagically added during the build (is it really such a hurdle to at least declare stuff before
referring to it… oh well) and instead followed a less pure approach using =arduino-cli= to access
the actual Arduino preprocessor.

#+BEGIN_SRC sh
arduino-cli core install arduino:samd
arduino-cli compile --fqbn arduino:samd:arduino_zero_native --preprocess ulisp-arm.ino > ulisp-arm.cpp
#+END_SRC

The problematic line w.r.t. to reproducible builds in Nix is the installation of the =arduino:samd= toolchain
which requires network access and wants to install stuff to home. Pulling in arbitrary stuff over the
network is of course not something one wants to do in an isolated and hopefully reproducible build
environment which is why this kind of stuff is heavily restricted in common Nix derivations. Luckily
it is possible to misuse (?) a fixed-output derivation to describe the preprocessing of =ulisp-arm.ino=
into a standard C++ =ulisp-arm.cpp= compilable using the GCC toolchain.

The relevant file [[https://code.kummerlaender.eu/teensy-env/tree/ulisp.nix?id=44c1837717f748b891df1a6c88a72ec3a51470ce][=ulisp.nix=]] pulls in the uLisp source from Github and calls =arduino-cli= to install
its toolchain to a temporary home folder followed by preprocessing the source into the derivation's
output. The relevant lines for turning this into a fixed-output derivation are

#+BEGIN_SRC nix
outputHashMode = "flat";
outputHashAlgo = "sha256";
outputHash = "mutVLBFSpTXgUzu594zZ3akR/Z7e9n5SytU6WoQ6rKA=";
#+END_SRC

to declare the hash of the resulting file. After this point building and flashing uLisp using the =teensy-env=
flake works the same as for any C++ program. The two additional /SPI/ and /Wire/ library dependencies are
added easily using =imageWith=:

#+BEGIN_SRC nix
teensy-ulisp = let
  ulisp-source = import ./ulisp.nix { inherit pkgs; };
  ulisp-deps   = with teensy-extras; [ spi wire ];
in (imageWith ulisp-deps).build
  "teensy-ulisp"
  (pkgs.linkFarmFromDrvs "ulisp" [ ulisp-source ]);
#+END_SRC

So we are now able to build and flash uLisp onto a conveniently attached Teensy 4.0 board using only:

#+BEGIN_SRC sh
nix flake clone git+https://code.kummerlaender.eu/teensy-env --dest .
nix run .#flash-ulisp
#+END_SRC

Connecting finally via serial terminal =screen /dev/ttyACM0 9600= we end up in a LISP environment where we
can play around with the microcontroller at our leisure without reflashing.

#+BEGIN_SRC lisp
59999> (* 21 2)
42

59999> (defun blink (&optional x)
         (pinmode 13 t)
         (digitalwrite 13 x)
         (delay 1000)
         (blink (not x)))

59966> (blink)
#+END_SRC

As always, the code of everything discussed here is available via Git on [[https://code.kummerlaender.eu/teensy-env][code.kummerlaender.eu]].
While I only focused on Teensy 4.0 it should be easy to adapt to other versions by changing the
compiler flags using [[https://github.com/PaulStoffregen/cores][PaulStoffregen/cores]] as a reference.

[fn:0] e.g. forcing me to patch my XMonad [[http://code.kummerlaender.eu/nixos_home/tree/gui/conf/xmonad.hs][config]] to even get a usable UI…
[fn:1] Simply flashing the on-board LED periodically
[fn:2] Interactive development using a Lisp REPL on a microcontroller, how much more can you really ask for?