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. Board Specifiers

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.

4.1.1. Components of Board-Specifiers

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.

4.1.2. Specifying Board Options

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.

4.2. Dno is Based on make

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.

4.2.1. 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: 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.

4.2.2. 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.2.3. 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.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.

  • 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;
    • 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.

  • OPTIONS

    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.

  • VERBOSE

    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.

  • QUIET

    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.

4.3. The Project Directory

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.

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

4.5. 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 descendants.

4.5.1. Tidy

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

4.5.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.5.3. Pristine

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

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

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

4.8. 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.8.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.8.2. Uploading Software

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

4.8.3. Communicating With The Arduino

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

4.8.4. Writing EEPROM data

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.

4.8.5. Building and burning bootloaders

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.

4.9. Performance

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.

4.9.1. Quantified Performance Comparison

A number of comparison benchmarks were performed between the standard Arduino IDE and dno. The details of these are captured in the benchmarks section.

4.9.2. Benchmarking Observations

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

4.9.3. Final Conclusions

Even in relatively simple comparisons with simple sketches, dno easily outperforms the standard Arduino IDE. For more complex programs using multiple libraries, and for more complex architectures, such as the ESP32, we can expect similar or greater performance gains.