Skip to content

Exercise 2: Macro Definitions & Build Automation

This section is still a work-in-progress!

In the last exercise, you blinked an LED, and in this lesson, you’ll be doing the same thing. The difference is that in this exercise, I’ll cover things to make you a far more effective programmer.

What I’m about to say may be surprising to some of you, but the “best” code is code that is easiest to read and maintain. “But wait!” I hear one of you yelling, “Isn’t the best code the code that runs the fastest!?” I too was once under this impression, but soon you realize this isn’t the rule, rather the exception.

Programming is an art form. It is a constant balance between efficiency and elegance. Is it worth spending hours writing something that will run marginally faster? I don’t know about you, but I value my time over the computer’s time. The only time you should be writing the most efficient code is when you’ve settled on a final design, and you know it will be worth the investment to write marginally more efficient code. If this is the case, then yes, spend that extra time perfecting your code.

I’ll explain more in the breakdown, for now, create sketch ex2 and type out the following code in the appropriate files.

The Code

ex2.ino

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/*
 * Exercise 2
 * Macro Definitions & Build Automation
 */

#define LED   LED_BUILTIN
#define DELAY 1000

void setup()
{
    pinMode(LED, OUTPUT);
}

void loop()
{
    // Toggle LED state every DELAY
    digitalWrite(LED, !digitalRead(LED));
    delay(DELAY);
}

Makefile

Note

If you are using the Arduino IDE, you can safely ignore this file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
PORT ?=

all:
    arduino-cli compile --fqbn arduino:avr:uno $(PWD)

clean:
    @rm -vrf $(PWD)/build

flash: all
ifndef PORT
    $(error make variable: [PORT] not defined)
endif
    arduino-cli upload -p $(PORT) --fqbn arduino:avr:uno $(PWD)

.PHONY: clean flash

Automating Builds

There isn’t much to do in regards to automating builds when using the Arduino IDE because it already does this for you. Although builds are automated, it doesn’t hurt to learn some keybinds to save a second or two every time you want to compile or flash your program.

To compile your program press Ctrl+R

To flash your program press Ctrl+U

Up until now, using Arduino CLI over the Arduino IDE has been more work. And this is where I would like to introduce you to your new friend, make.

Note

make is a UNIX tool, if you are a Windows user, you’ll have to look elsewhere for a build automation system. Sorry!

To use make you need something called a Makefile. A Makefile has targets and dependencies, for instance all:. All you need to understand is that make will let us automate typing out long commands that are prone to user error.

To compile your program, open a terminal at the root of your sketch, in this case ./ex2. What you should see when running the tree command is as follows:

.
├── ex2.ino
└── Makefile

0 directories, 2 files

From the output we can see that the current directory . has files ex2.ino and Makefile.

To compile your program simply run make. You should have output like this:

arduino-cli compile --fqbn arduino:avr:uno ../ex2
Sketch uses 960 bytes (2%) of program storage space. Maximum is 32256 bytes.
Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes.

This should look familiar as we can see the compile command from the previous exercise, along with compiler output.

To flash your program to the Arduino, we must first determine the device port like in execrise 1. Next we simply run make PORT=/dev/ttyACM1 flash.

arduino-cli compile --fqbn arduino:avr:uno ../ex2
Sketch uses 960 bytes (2%) of program storage space. Maximum is 32256 bytes.
Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes.
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:uno ../ex2

From the output we see the flash command we ran in the previous exercise as well as the output from Arduino CLI.

The Breakdown

ex2.ino

ex2.ino:6-7: #define is a directive that allows for the definition of macros within your source code. In this instance, we are creating constants for our source code. LED corresponds to LED_BUILTIN and DELAY corresponds to 1000. When the program gets compiled, the C preprocessor will first evaluate each macro before moving on to the next steps of the compiling process.

ex2.ino:11: After the C preprocessor runs through this line, it will evaluate to pinMode(LED_BUILTIN, OUTPUT);.

ex2.ino:17: This line may look cryptic to a beginner, but it’s simple. We know from the last lesson that digitalWrite() takes two arguments, the pin and the state. The first argument, LED is a macro which will evaluate to LED_BUILTIN. The second argument is the inverse of the value returned by digitalRead(). This function takes one parameter, a digital pin, and returns an int value that is either 0 or 1. The prefix ! will then get the inverse of the return value, or in other words, enable us to toggle the pin. In the end, this line will evaluate to either digitalWrite(LED_BULITIN, 0); or digitalWrite(LED_BULITIN, 1);.

ex2.ino:18: This line shouldn’t be too difficult; it will just evaluate the DELAY macro to delay(1000);.

Makefile

Note

If you don’t understand the structure of a Makefile don’t stress it. Makefile‘s are beyond the scope of this course, all you need to know is it automates builds.

Makefile:1: Here we declare a variable called PORT. The ?= will declare the variable to nothing if it is not declared a value at runtime.

Makefile:3: Here we have our first (and default) make target called all:.

Makefile:4: Here we have the command that will be run for the all: target. You can see that it is the same command we ran in the previous command with arduino-cli to compile the sketch. The only thing that might be unfamiliar is the $(PWD). This will evaluate the make variable PWD to get the current working directory.

Makefile:6-7: This is our second target clean: which will clean out any compiled binaries from our working directory. The @ idicates to not print the command being run to stdout1.

Makefile:


  1. Standard Output