Chapter 4. A Little Exploration

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.

4.1. On Being Based On make

4.1.1. Dno Rebuilds Only What It Needs To

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.

4.1.2. Dno "Commands" Are make "Targets"

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.

4.1.3. Dno Accepts All Make Options

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.

4.1.4. Dno Variables

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.

  • DEVICE_PATH

    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.

  • VERBOSE

    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.

4.2. The Project Directory

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].

4.3.  The BOARD_TYPE File, And Board Types

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.

4.4. Cleaning

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.

4.4.1. Tidy

The tidy target removes all files that look like garbage. This includes Emacs' backup and auto-save files.

4.4.2. Clean

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.

4.4.3. Pristine

The pristine target does all that the clean target does, plus it removes any BOARD_INFO files.

Use this before committing changes into your repository, or before building source distribution tarballs.

4.5. The BOARD_INFO file

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.

4.6. The BOARD_OPTIONS file

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.

4.7. Connecting to Physical Arduino Boards

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.

4.7.1. Identifying Devices

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.

4.7.2. Uploading Software

Software is uploaded using dno's upload target. More can be found here.

4.7.3. Communicating With The Arduino

The equivalent to the Arduino IDE's Serial Monitor is invoked by the monitor target. More here.

4.7.4. Writing EEPROM data

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.

4.8. Performance

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.

4.8.1. Quantified Performance Comparison

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.

4.8.1.1. Comparing Build Times

Arduino CLI

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$ 	
	    

Dno

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$ 
	    

Conclusion

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.

4.8.1.2. Comparing Parse and Build Times

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.

Arduino CLI

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$ 
	    

Dno

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$ 
	    

Conclusion

In this test, dno does even better, achieving around 65% better performance. This is quite surprising since the parsing for dno is essentially done by dumb scripting tools rather than a compiled executable.

4.8.1.3. Final Conclusions

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.



[2] Yes. I know.