The previous section shows how to get started with
dno
for a simple project. It introduces a lot of
dno
's features and capabilities without really
explaining or delving into them. So, let's look a little closer.
We saw in the previous section that after everything had been
compiled and linked, running dno
again didn't
do anything.
This is because dno
is based on
make
(specifically GNU
make
) and understands the dependencies between
everything that it builds. If you update
a source file, dno
knows that the object
for that source file has to be recreated because the object
depends on that source file. Similarly,
if that object file is part of a library, then the library
depends on the object and must also be
rebuilt. And if the library or object is part of an image
file, then the image depends on the
library or object, and dno will re-link it.
All of these dependencies are discovered automatically by
dno
.
What all of this means, is that dno
will
only do as much work as it has to in order to achieve
anything. This makes compilations much faster and more
efficient, making dno
more pleasant to use
than the standard, much slower, tools.
We have been introduced to a number of different
dno
invocations:
dno BOARD_TYPE
;
dno show_boards
;
dno clean
;
dno devices
;
dno upload
.
Each of the "commands" after "dno
" are, in
make parlance, called targets.
A target is something that make
, and hence
dno
, knows how to build. Dno has a recipe
for building that target and a set of dependencies.
Providing a target on the dno command line, tells it what you
want it to do.
Some targets are files, eg BOARD_TYPE, some are higher level, more abstract. Some cause actions such as compilations, others simply provide information to the user.
To get a list of the most useful targets, use dno
help
.
If you run dno
without an explicit target,
it will attempt to build the default target. What this will
do, will depend on the type
of directory you are in.
Of particular interest here is the -j N
option. This allows make to perform multiple actions ("jobs")
in parallel. This can greatly reduce the compilation time for
a large piece of software. See the make
manual page for more on this.
There are a number of variables that can be defined on the dno
command line that provide information to
dno
or otherwise affect its behaviour. We
have already seen an example of this with the
command:
blink$ dno BOARD_TYPE BOARD=pro CPU=8MHzatmega328 Creating BOARD_TYPE... blink$
which creates a BOARD_TYPE
file containing
a definition matching the provided values for the
BOARD
and CPU
variables.
The most important and useful variables are:
BOARD
This is used to identify a specific type of Arduino board. For boards that have multiple CPU options, the cpu can also be specified within this variable following a period ("."). The following are therefore equivalent:
BOARD=pro CPU=8MHzatmega328
;
BOARD=pro.8MHzatmega328
.
Targets which make use of this variable are:
BOARD_TYPE
;
BOARD_INFO
;
show_boards
.
CPU
This is used to identify the cpu variant for board
specified by the BOARD
variable.
This is used to specify the host device to which a target Arduino board is connected. Generally, this will not be needed as dno will figure it out for itself, but if you have multiple serial devices connected, this allows you to specify which one you are interested in. Note that this overrides any value that dno might have discovered for itself.
Targets which make use of this variable are:
reset
;
upload
;
eeprom
;
monitor
.
MONITOR_BAUD
This is used to specify the baud rate to be used for a serial connection. This normally defaults to the correct value but sometimes dno will get this wrong.
This is used by the monitor
target.
KILL_SCREEN
This can be provided to the noscreen target to kill any
screen
instance that is connected to
your Arduino.
This causes dno
's activities to become
more verbose. Usually the commands that dno executes
are summarised:
8MHzatmega328$ touch ../blink.cpp pro.8MHzatmega328$ dno build/blink.o C++ [..] blink.cpp make: 'build/blink.o' is up to date. pro.8MHzatmega328$
Setting VERBOSE on the command line changes that, and causes the actual commands to be shown instead of the summary. Additionally, it identifies each target that is actually run:
pro.8MHzatmega328$ touch ../blink.cpp pro.8MHzatmega328$ dno build/blink.o VERBOSE=y BUILDING TARGET[] build/blink.o BUILDING TARGET[] prebuild BUILDING TARGET[] presketch BUILDING TARGET[] build/blink.d "/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-g++" -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -Wno-error=narrowing -MMD -flto -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=00508 -DARDUINO_AVR_PRO -DARDUINO_ARCH_AVR -I /home/user/.arduino15/packages/arduino/hardware/avr/1.8.6/cores/arduino -I /home/user/.arduino15/packages/arduino/hardware/avr/1.8.6/variants/eightanaloginputs -I ../Deferal -I /home/user/.arduino15/packages/arduino/hardware/avr/1.8.6///libraries/EEPROM/src -I /home/user/.arduino15/packages/arduino/hardware/avr/1.8.6///libraries/HID/src -I /home/user/.arduino15/packages/arduino/hardware/avr/1.8.6///libraries/SoftwareSerial/src -I /home/user/.arduino15/packages/arduino/hardware/avr/1.8.6///libraries/SPI/src -I /home/user/.arduino15/packages/arduino/hardware/avr/1.8.6///libraries/Wire/src "..//blink.cpp" -o "build/blink.o" BUILDING TARGET[] postsketch BUILDING TARGET[] prelib BUILDING TARGET[] postlib BUILDING TARGET[] precore BUILDING TARGET[1] prebuild BUILDING TARGET[1] presketch BUILDING TARGET[1] postsketch BUILDING TARGET[1] prelib BUILDING TARGET[1] postlib BUILDING TARGET[1] precore make: 'build/blink.o' is up to date. pro.8MHzatmega328$
This can be useful if you want to run a compilation manually, to debug some unexpected behaviour etc.
The name for a dno project comes from the name of the project directory. The Arduino IDE handles this in much the same way, but setting the name in the Arduino IDE is more cumbersome.
You can change the name of your project by simply changing the
name of the project directory. When you next run
dno
, it will figure out that the name has
changed and will rebuild everything [2].
A BOARD_TYPE
file identifies the type of
board that dno is going to build code for. Any time this
changes dno will know it has to rebuild everything.
As an alternative to an explicit BOARD_TYPE
file, dno allows board-specific subdirectories to be added to a
project. These directories are given the name of the board in
the same format as the contents of a
BOARD_TYPE
file.
This allows you to build code for multiple types of boards within a single project. Let's switch to this named-directory approach. We'll start by looking at our current directory:
blink$ ls -l - rw-r--r-- 1 user user 1233 Jan 6 11:04 blink.ino -rw-r--r-- 1 user user 12860 Jan 10 16:33 BOARD_INFO -rw-r--r-- 1 user user 18 Jan 10 16:33 BOARD_TYPE drwxr-xr-x 3 user user 4096 Jan 10 16:33 build blink$
Here we see that we have our source code,
blink.ino
, our BOARD_TYPE
and BOARD_INFO
files, and a build
directory
where all of our objects, libraries and
executables are stored.
We'll want to clean this up before we create our new directory,
as subsequent builds will be done there. We could do this
manually (with rm
commands), or we can use a
dno target to do this for us:
blink$ dno pristine Super-cleaning . blink$ ls -l total 8 -rw-r--r-- 1 user user 1233 Jan 6 11:04 blink.ino -rw-r--r-- 1 user user 18 Jan 10 16:42 BOARD_TYPE blink$ rm BOARD_TYPE blink$ ls -l total 4 -rw-r--r-- 1 user user 1233 Jan 6 11:04 blink.ino blink$
The pristine
target is a slightly souped-up
version of the clean
target. Clean will
remove the build
directory and its contents.
Pristine additionally removes the BOARD_INFO
directory. Note that we still had to manually remove
BOARD_TYPE
.
Now, we create the new directory, move into it and run
dno
again:
blink$ mkdir pro.8MHzatmega328 blink$ cd pro.8MHzatmega328 pro.8MHzatmega328$ dno Creating BOARD_INFO... C++ [..] blink.ino AS [core] wiring_pulse.S C [core] wiring_shift.c C [core] wiring_pulse.c C [core] wiring_digital.c C [core] wiring.c C [core] wiring_analog.c C [core] WInterrupts.c C [core] hooks.c C++ [core] WString.cpp C++ [core] WMath.cpp C++ [core] USBCore.cpp C++ [core] Tone.cpp C++ [core] Stream.cpp C++ [core] Print.cpp C++ [core] PluggableUSB.cpp C++ [core] new.cpp C++ [core] main.cpp C++ [core] IPAddress.cpp C++ [core] HardwareSerial.cpp C++ [core] HardwareSerial3.cpp C++ [core] HardwareSerial2.cpp C++ [core] HardwareSerial1.cpp C++ [core] HardwareSerial0.cpp C++ [core] CDC.cpp C++ [core] abi.cpp AR [libcore] abi.o... LD blink.ino.o OBJCOPY (hex) blink.elf pro.8MHzatmega328$
Having explicit board directories like this keeps the parent
code directory cleaner and eliminates the unsightly
BOARD_TYPE
file. This is the recommended way
to use dno
.
The section above introduced the "clean" and "pristine" targets. These, along with "tidy" are used for carefully cleaning up unwanted files.
Although it is easy enough to use the rm
command to remove unwanted files, it is very easy to mistype and
delete files that you actually want to keep. Using the
clean
and related targets allows you to
remove junk files safely.
Note that all of the cleaning targets work in the current directory as well as its descendant's.
The tidy
target removes all files that look
like garbage. This includes Emacs' backup and auto-save files.
The Clean
target does all that the
tidy
target does as well as removing
build
and html
directories, which will be recreated when
dno
is next run.
The BOARD_INFO
file is derived from the
files:
platform.txt
;
boards.txt
;
platform.local.txt
.
For more about these files please see the Arduino Platform Specification.
Dno parses these files, based on the user's
board selection
, creating a file that conforms with
makefile
syntax and provides all of the
definitions necessary for compiling, linking, etc, Arduino
programs.
The BOARD_OPTIONS
file describes selected,
non-default, board-specific configuration values.
It is created by running dno menu
, which
presents the user with menus of board-specific configuration
options. If the user selects any non-default options, a
BOARD_OPTIONS
file will be created to
document the selections.
Dno provides a number of commands for interfacing with Arduino boards. It can upload compiled code to a board, modify a board's eeprom (with some limitations), and communicate with a board using its serial interface.
Generally, when a single Arduino board is connected to your
computer, dno is able to identify the serial device it is
connected to. To see what devices dno thinks are in use, use
the devices
target:
pro.8MHzatmega328$ dno devices DEVICES: /dev/ttyUSB0 pro.8MHzatmega328$
Here we see that a single serial device is in use. In this
case dno will be able to identify the device for itself. If
there is more than one device is detected, you will have to
tell dno which device to use using the
DEVICE_PATH
variable.
Software is uploaded using dno's upload
target. More can be found here.
The equivalent to the Arduino IDE's Serial Monitor is invoked
by the monitor
target. More here.
Writing to an Arduino's EEPROM from outside of a program is a bit hit and miss. Dno kind-of, mostly, sort-of supports writing eeproms for AVR-architecture boards. More here.
Generally speaking dno will easily outperform the Arduino IDE or CLI. This is because dno:
understands dependencies, and so only does work that is necessary;
Compare this with the Arduino IDE, which although it caches some results, always attempts to recompile your code, even if nothing has changed.
retains work that it has already done;
For instance the Arduino IDE appears to parse the
boards.txt
and
platform.txt
files each time it is
run. Dno, keeps this information in its
BOARD_INFO
file in a form that
make
can understand.
automatically discovers everything it needs;
You don't have to tell dno what libraries to use or where to find them. This reduces the cognitive load for the developer, helping them stay in the zone.
uses very short commands, which are quick and easy to type;
This is generally faster than pointing and clicking through an IDE interface.
has very low overhead.
Dno does not require massive 3rd party libraries. It does not require a massive virtual machine or runtime environment. It is lightweight and uses mature, well-optimised tools.
The following tests were performed using the standard
blink.cpp
sketch, compiled for a
pro.8MHzatmega328
Arduino board on a
relatively old x86_64 GNU/Linux desktop machine.
Due to the difficulty of timing actions within the IDE, the
comparison is between running the commands that the IDE claims
to have issued, with a fresh dno directory containing only the
sketch and a BOARD_TYPE
file.
To do this we ran the Arduino IDE to show the build commands. These commands were extracted into a script, shown below:
#! /usr/bin/env bash rm -rf /tmp/arduino_build_22206/ mkdir /tmp/arduino_build_22206/ do_it () { arduino-builder -dump-prefs -logger=machine -hardware /usr/share/arduino/hardware -hardware /home/user/.arduino15/packages -tools /usr/share/arduino/hardware/tools/avr -tools /home/user/.arduino15/packages -libraries /home/user/Arduino/libraries -fqbn=arduino:avr:pro:cpu=16MHzatmega328 -vid-pid=0403_6015 -ide-version=10813 -build-path /tmp/arduino_build_22206 -warnings=none -build-cache /tmp/arduino_cache_578139 -prefs=build.warn_data_percentage=75 -prefs=runtime.tools.arduinoOTA.path=/home/user/.arduino15/packages/arduino/tools/arduinoOTA/1.3.0 -prefs=runtime.tools.arduinoOTA-1.3.0.path=/home/user/.arduino15/packages/arduino/tools/arduinoOTA/1.3.0 -prefs=runtime.tools.avr-gcc.path=/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7 -prefs=runtime.tools.avr-gcc-7.3.0-atmel3.6.1-arduino7.path=/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7 -prefs=runtime.tools.avrdude.path=/home/user/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17 -prefs=runtime.tools.avrdude-6.3.0-arduino17.path=/home/user/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17 -verbose /home/user/proj/cli_blink/blink.cpp time arduino-builder -compile -logger=machine -hardware /usr/share/arduino/hardware -hardware /home/user/.arduino15/packages -tools /usr/share/arduino/hardware/tools/avr -tools /home/user/.arduino15/packages -libraries /home/user/Arduino/libraries -fqbn=arduino:avr:pro:cpu=16MHzatmega328 -vid-pid=0403_6015 -ide-version=10813 -build-path /tmp/arduino_build_22206 -warnings=none -build-cache /tmp/arduino_cache_578139 -prefs=build.warn_data_percentage=75 -prefs=runtime.tools.arduinoOTA.path=/home/user/.arduino15/packages/arduino/tools/arduinoOTA/1.3.0 -prefs=runtime.tools.arduinoOTA-1.3.0.path=/home/user/.arduino15/packages/arduino/tools/arduinoOTA/1.3.0 -prefs=runtime.tools.avr-gcc.path=/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7 -prefs=runtime.tools.avr-gcc-7.3.0-atmel3.6.1-arduino7.path=/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7 -prefs=runtime.tools.avrdude.path=/home/user/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17 -prefs=runtime.tools.avrdude-6.3.0-arduino17.path=/home/user/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17 -verbose /home/user/proj/cli_blink/blink.cpp } do_it
This script, clears any cached build, rebuilds the build
directory, parses the platform.txt
file
(at least that's my assumption of what the first
arduino-builder
command is doing), and
then compiles and links the sketch. This last command is
executed using the Linux time
command,
which records how long things take.
We run this script 4 times. The timing results are shown below:
cli_blink$ ./cli.sh [ lots of output removed ] real 0m1.304s user 0m0.678s sys 0m0.732s cli_blink$ ./cli.sh [ lots of output removed ] real 0m1.373s user 0m0.754s sys 0m0.775s cli_blink$ ./cli.sh [ lots of output removed ] real 0m1.344s user 0m0.699s sys 0m0.744s cli_blink$ ./cli.sh [ lots of output removed ] real 0m1.263s user 0m0.668s sys 0m0.696s cli_blink$
We created a directory containing our sketch, and created
BOARD_TYPE
and
BOARD_INFO
files:
dno_blink$ ls blink.ino dno_blink$ dno BOARD_TYPE BOARD=pro.8MHzatmega328 Creating BOARD_TYPE... dno_blink$ ls blink.ino BOARD_TYPE dno_blink$
This is equivalent to the setup above. Then we run the following commands:
dno pristine; dno BOARD_INFO; time dno
The first invocation will remove any
build
directory and
BOARD_INFO
file (ensuring that the
contents of BOARD_INFO are not cached by the OS). The
second, rebuilds BOARD_INFO
, and the
third, provides our timing run of compilation and
linking. Here are the results:
dno_blink$ dno pristine; dno BOARD_INFO; time dno [ relatively little output removed ] real 0m1.252s user 0m1.044s sys 0m0.260s no_blink$ dno pristine; dno BOARD_INFO; time dno [ relatively little output removed ] real 0m1.237s user 0m1.009s sys 0m0.280s dno_blink$ dno pristine; dno BOARD_INFO; time dno [ relatively little output removed ] real 0m1.262s user 0m1.006s sys 0m0.310s dno_blink$ dno pristine; dno BOARD_INFO; time dno [ relatively little output removed ] real 0m1.213s user 0m1.011s sys 0m0.254s dno_blink$
In a pure compilation and linking invocation, with no cached results, dno is a little faster (about 6%) than the Arduino CLI. This might seem surprising given that most of the time should be spent by the compiler, linker, etc, which should be identical in both cases, however dno does optimise the way it generates archive files when compared to the Arduino tools. This may account for the consistent but small difference.
Let's now also consider parse times. This is the time for
arduino-builder
to dump its preferences,
or for dno
to create the
BOARD_INFO
file.
For this test, we modify our script as shown below:
#! /usr/bin/env bash rm -rf /tmp/arduino_build_22206/ mkdir /tmp/arduino_build_22206/ do_it () { arduino-builder -dump-prefs -logger=machine -hardware /usr/share/arduino/hardware -hardware /home/user/.arduino15/packages -tools /usr/share/arduino/hardware/tools/avr -tools /home/user/.arduino15/packages -libraries /home/user/Arduino/libraries -fqbn=arduino:avr:pro:cpu=16MHzatmega328 -vid-pid=0403_6015 -ide-version=10813 -build-path /tmp/arduino_build_22206 -warnings=none -build-cache /tmp/arduino_cache_578139 -prefs=build.warn_data_percentage=75 -prefs=runtime.tools.arduinoOTA.path=/home/user/.arduino15/packages/arduino/tools/arduinoOTA/1.3.0 -prefs=runtime.tools.arduinoOTA-1.3.0.path=/home/user/.arduino15/packages/arduino/tools/arduinoOTA/1.3.0 -prefs=runtime.tools.avr-gcc.path=/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7 -prefs=runtime.tools.avr-gcc-7.3.0-atmel3.6.1-arduino7.path=/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7 -prefs=runtime.tools.avrdude.path=/home/user/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17 -prefs=runtime.tools.avrdude-6.3.0-arduino17.path=/home/user/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17 -verbose /home/user/proj/cli_blink/blink.cpp arduino-builder -compile -logger=machine -hardware /usr/share/arduino/hardware -hardware /home/user/.arduino15/packages -tools /usr/share/arduino/hardware/tools/avr -tools /home/user/.arduino15/packages -libraries /home/user/Arduino/libraries -fqbn=arduino:avr:pro:cpu=16MHzatmega328 -vid-pid=0403_6015 -ide-version=10813 -build-path /tmp/arduino_build_22206 -warnings=none -build-cache /tmp/arduino_cache_578139 -prefs=build.warn_data_percentage=75 -prefs=runtime.tools.arduinoOTA.path=/home/user/.arduino15/packages/arduino/tools/arduinoOTA/1.3.0 -prefs=runtime.tools.arduinoOTA-1.3.0.path=/home/user/.arduino15/packages/arduino/tools/arduinoOTA/1.3.0 -prefs=runtime.tools.avr-gcc.path=/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7 -prefs=runtime.tools.avr-gcc-7.3.0-atmel3.6.1-arduino7.path=/home/user/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7 -prefs=runtime.tools.avrdude.path=/home/user/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17 -prefs=runtime.tools.avrdude-6.3.0-arduino17.path=/home/user/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17 -verbose /home/user/proj/cli_blink/blink.cpp } time do_it
The Linux time
command is now applied
to the entire do_it
function, rather
than just the second arduino-builder
invocation.
Running this 4 times:
cli_blink$ ./cli.sh [ output removed ] real 0m2.243s user 0m1.175s sys 0m1.350s cli_blink$ ./cli.sh [ output removed ] real 0m2.489s user 0m1.396s sys 0m1.426s cli_blink$ ./cli.sh [ output removed ] real 0m2.472s user 0m1.333s sys 0m1.498s cli_blink$ ./cli.sh [ output removed ] real 0m2.438s user 0m1.334s sys 0m1.464s cli_blink$
We slightly modify our previous invocation and, again, run 4 times:
dno_blink$ dno pristine; time dno [ output removed ] real 0m1.414s user 0m1.199s sys 0m0.308s dno_blink$ dno pristine; time dno [ output removed ] real 0m1.482s user 0m1.290s sys 0m0.255s dno_blink$ dno pristine; time dno [ output removed ] real 0m1.465s user 0m1.286s sys 0m0.270s dno_blink$ dno pristine; time dno [ output removed ] real 0m1.464s user 0m1.255s sys 0m0.301s dno_blink$
Even in relatively simple comparisons with simple sketches, dno outperforms the standard Arduino CLI. Given that: the Arduino IDE adds additional overhead, beyond that of the CLI; that parsing appears to be necessary on each invocation; and that much unnecessary rebuilding is often performed; on performance dno wins handsomely.