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 our BOARD_TYPE file is
used to specify the type of board, along with options for that
board. We also saw that although the board-specifier could be
provided in a short abbreviated form, the
BOARD_TYPE file contained a longer form.
Let's take a closer look.
A fully-specified Arduino board definition consists of 3 parts separated by dots. These are:
vendor
This is the vendor or maintainer for a family of
boards. It is the top level of the hierarchical
directory structure defined by
the
Arduino platform specification. This might
typically be arduino or
esp32.
architecture
This describes an architecture for a family of boards. It is quite possible for one vendor/maintainer to support multiple architectures. Similarly some architectures are supported by multiple vendors/maintainers.
An example of this would be avr.
board
This identifies a specific board within a vendor and architecture.
Note that sometimes vendors and architectures might have
the same name as a board. For example
esp32 is the name of a vendor, its
architecture, and one of its boards. Where such
ambiguity exists, it is safer not to use abbreviated
board-specifiers.
If a board name is unique across all vendors and architectures
it need not be completely specified: it is enough in such
cases to simply use the board name: ie uno
rather than arduino.avr.uno.
As a final option, you can provide just the vendor or architecture in addition to the board name.
Board options can be appended to board-specifiers to provide
some or all of the board's options. In our previous example we
identified the 3.3v 8MHz variant of our arduino pro mini by
appending .8MHzatmega328 to our
board-specifier. Note that in our
BOARD_TYPE file this became
.cpu=8MHzatmega328. A full
option-specifier consists of
<option-name>=<option-value>.
Multiple option values can be provided by separating them with
commas, eg: optiboot32.8MHz,atmega328p
specifies an optiboot32 board with two selected options.
If options appear in the same order that are defined in their
platform.txt file (also the same order that
they are shown in dno's show_boards
target), their corresponding option names can be avoided.
Note that in our BOARD_TYPE file the
board-specifier is fully specified. The abbreviated forms are
simply a convenience.
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 whenever it needs to know them.
What all of this means is that dno will
only do as much work as it has to in order to ensure everything
is up to date.
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: if you are in a board
directory, it will rebuild the executable; if you are in a lib
directory, it will compile the lib and run unit tests; if you
are in a docs directory, it will rebuild your documentation.
This means that for many, or even most, things that you want
dno to do, you don't even need to provide a
target: the command will simply be dno.
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.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 options, those options can also be specified within this variable following a period ("."). See Specifiying Board Options for more.
Targets which make use of this variable are:
BOARD_TYPE;
BOARD_INFO;
show_boards.
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;
cat.
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 and
cattargets.
KILL_SCREEN
This can be provided to the noscreen target to kill any
screen instance that is connected to
your Arduino.
This variable is only used by the show_boards target.
If defined (OPTIONS=y) show_boards
will list any options that can be provided to the board
along with their values.
This causes dno's activities to become
more verbose. Usually the commands that dno executes
are summarised:
blink$ touch blink.ino blink$ dno build/blink.ino.o C++ ./blink.ino make: 'build/blink.ino.o' is up to date. blink$
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:
blink$ touch blink.ino blink$ dno build/blink.ino.o VERBOSE=y BUILDING TARGET[] build/blink.ino.d (arduino-ctags -u --language-force=c++ -f - --c++-kinds=spf --fields=STt /dev/null ./blink.ino | sed -e "s/^\([^[:space:]]*\).*ture:\(([^)]*)\).*type:\(.*\)/\3 \1\2;/") | cat - ./blink.ino | "/home/marc/.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=8000000L -DARDUINO=903 -DARDUINO_AVR_PRO -DARDUINO_ARCH_AVR -I /home/marc/.arduino15/packages/arduino/hardware/avr/1.8.6/cores/arduino -I /home/marc/.arduino15/packages/arduino/hardware/avr/1.8.6/variants/eightanaloginputs -I /home/marc/.arduino15/packages/arduino/hardware/avr/1.8.6/libraries/EEPROM/src -I /home/marc/.arduino15/packages/arduino/hardware/avr/1.8.6/libraries/HID/src -I /home/marc/.arduino15/packages/arduino/hardware/avr/1.8.6/libraries/SoftwareSerial/src -I /home/marc/.arduino15/packages/arduino/hardware/avr/1.8.6/libraries/SPI/src -I /home/marc/.arduino15/packages/arduino/hardware/avr/1.8.6/libraries/Wire/src -I . -x c++ --include Arduino.h - -o "build/blink.ino.o" sed 's/\([^:]*\).o\s*:/\1.d \1.o:/' <build/blink.ino.d >build/blink.ino.dd mv build/blink.ino.dd build/blink.ino.d make: 'build/blink.ino.o' is up to date. blink$
This can be useful if you want to run a compilation manually, to debug some unexpected behaviour etc: you can simply cut and paste the commands shown into a command line.
Setting QUIET on the command line
causes upload and related actions to be less verbose,
and program size information to not be displayed after
linking.
The name for a dno project comes from the name of the project directory.
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.
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 would be given when building the
BOARD_TYPE file (see Board Specifiers for more).
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
total 20
-rw-r--r-- 1 user user 0 May 10 13:10 blink.ino
-rw-r--r-- 1 user user 11086 May 10 13:04 BOARD_INFO
-rw-r--r-- 1 user user 34 May 10 13:03 BOARD_TYPE
drwxr-xr-x 3 user user 4096 May 10 13:10 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 4
-rw-r--r-- 1 user user 0 May 10 13:10 blink.ino
-rw-r--r-- 1 user user 34 May 10 13:03 BOARD_TYPE
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
file. 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
Program size: 924 out of 30720 (3%: 29796 remaining)
Data size: 9 out of 2048 (0%: 2039 remaining)
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 descendants.
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. Note that this will ignore any options that are
already specified by the board directory name or the
BOARD_TYPE file.
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.
Dno kind-of, mostly, supports writing to a devices eeprom. This works for AVR-architecture boards and may work for others but has not been tested. More here.
If your board is connected to a programmer, you can burn a
board's standard bootloader by simply running dno
burn in a board directory. For more on this see
Restoring (Burning)
the Standard Bootloader.
To create a custom bootloader, you should create a
boot directory below your board directory.
Here you can build and burn a custom bootloader image. For
more on this see Burning a Custom
Bootloader.
Generally speaking dno will easily outperform the Arduino IDE or CLI. This is because dno:
fully 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.
A number of comparison benchmarks were performed between the standard Arduino IDE and dno. The details of these are captured in the benchmarks section.
Performance of standard IDE
The benchmarking results are striking in their demonstration of how inefficient the standard IDE is. When no code has been changed since the last build, it will still recompile and rebuild your sketch. And do it slowly.
Full build comparisons
Dno, building from a totally clean directory, and with no
paralellism is about 25% faster than the standard IDE.
With BOARD_INFO already in place it is
nearly twice as fast (56.0% Real time), and in a more
realistic parallel invocation it is more than 3 times as fast
(31.3% Real time).
Minimal build comparisons
With only the sketch updated, dno was more than 3 times as fast as the Arduino IDE (29.1% Real time). This was without any paralellism.
Finally, with no updates to sources, ie with nothing that needed to be rebuilt, the Arduino IDE still recompiled and relinked the sketch, taking more than 5 times as long as dno's do-nothing build.
On determing what to build
The Arduino IDE has a very primitive view of what should
be updated when a compilation is actioned. It will always
recompile the sketch regardless of whether it needs to,
and will always try to re-use compiled and archived parts
of the core library, presumably regardless of whether the
sources for these have been updated. While this is very
likely to be safe there can be corner cases with such
policies leading to inconsistent builds. Dno, being based
on make, with a full understanding of
all dependencies does not have this
problem. It rebuilds everything that
needs to be rebuilt, and nothing that
does not.