collage of Pragpub magazine covers
Take a step back in history with the archives of PragPub magazine. The Pragmatic Programmers hope you’ll find that learning about the past can help you make better decisions for the future.

FROM THE ARCHIVES OF PRAGPUB MAGAZINE APRIL 2011

Advanced Arduino Hacking: Bringing Serious Developer Tools and Techniques to Arduino, the Popular Single-Board Platform

By Maik Schmidt

23 min readMay 26, 2022

--

You want to get into this popular open-source electronics prototyping platform, but you don’t want to have to work with development tools designed for artists and hobbyists. Maik shows you how to develop software for Arduino in a professional way.

https://pragprog.com/newsletter/
https://pragprog.com/newsletter/

The Arduino was made for beginners. The microcontroller board has a lot of built-in mechanisms that prevent beginners from destroying it accidentally, and its development environment is as simple as possible. On the web and in your favorite book store you can find countless tutorials and books for beginners. Simplicity is the basis of the Arduino’s popularity.

But simplicity is a double-edged sword. As the Arduino became more popular, advanced developers and even experts started to have a look at it and quickly got disappointed by the development environment’s limited capabilities and the lack of advanced documentation.

In this article, I’ll try to fill some of those gaps. You’ll learn how to develop software for the Arduino in a professional way. For example, you’ll see how to manage your Arduino projects with a good old Makefile that you can easily integrate into your favorite IDE. In addition, you’ll learn that the Arduino platform supports nearly all features of the current C++ standard and that it’s advantageous to use these features for programming embedded systems.

But first, we’ll take a close look at the Arduino IDE’s internals. Then once we’ve seen how it turns our sketches into executable code, we’ll create a Makefile to bypass it completely.

Photo by ThisisEngineering RAEng on Unsplash

What’s Wrong With The Arduino IDE?

One of the reasons for the Arduino’s success is its beginner-friendly integrated development environment (IDE). For people who have never written any software, the Arduino IDE is a perfect starting point. It offers only features that you absolutely need: compile a program, transfer it to the Arduino board, and monitor the serial port. Also, the IDE has basic support for grouping a set of files into a project, it supports syntax coloring, and it helps you to quickly find example projects and documentation.

All this is helpful for beginners and useful for small projects, but as soon as your projects become more sophisticated you’ll need a more elaborate programming environment. You’ll probably want to use your favorite text editor or IDE and you may need a more flexible build process. For example, if you’re developing a video game for one of the great Arduino video game shields, you’ll need tools that turn bitmap graphics into C/C++ source code. These tools should be part of your automatic build process, and it’s difficult to add them to the original IDE.

So let’s see how the Arduino IDE turns your sketches into binary code for the Arduino board.

Behind the Scenes

People sometimes seem to be a bit irritated when it comes to the language the Arduino gets programmed in. That’s mainly because the typical sample sketches look as if they were written in a language that has been exclusively designed for programming the Arduino. But that’s not the case — it is plain old C++ (which implies that it supports C, too). To turn C++ code into machine code the Arduino can execute, we need a suitable compiler.

Every Arduino uses an AVR microcontroller designed by a company named Atmel. (Atmel says that the name AVR does not stand for anything.) These microcontrollers are very popular, and many hardware projects use them. One of the reasons for their popularity is their excellent toolchain, based on the GNU C++ compiler tools and optimized for generating code for AVR microcontrollers.

That means you feed C/C++ code to the AVR compiler and it does not translate it into machine code for your computer but for an AVR microcontroller. This is called cross-compiling and is the usual way to program embedded devices.

For nearly all GNU development tools — such as gcc, ld, or as — there’s an AVR pendant: avr-gcc, avr-ld, and so on. You can find them in the hardware/tools/bin directory of the Arduino IDE. The IDE is mainly a graphical wrapper that helps you to avoid using the command-line tools directly. Whenever you compile or upload a program using the IDE, it delegates all work to the AVR tools.

As a serious software developer, you should enable the IDE’s verbose output, so you can see all command-line tool invocations. Load an arbitrary sketch and hold down the Shift key when you click the Verify/Compile in the IDE’s toolbar. The output in the message panel should look similar to the picture.

Output when you enable the IDE’s verbose output
Output when you enable the IDE’s verbose output

The command invocations look a bit weird at first, because of the names of the many temporary files that are created. You should still be able to identify all compile and link steps that are necessary to build even a simple sketch like the blinking LED example. That’s the most important thing that the Arduino team did: they hid all these nasty details well behind the IDE, so even people with no software development experience are able to program the Arduino.

So to compile and upload Arduino programs yourself you have to perform the same steps as the IDE; that is, you have to invoke the AVR tools in the right order and for the right files. In principle, you could do this manually, but it’s much more efficient to use a Makefile for this task.

Do It Yourself

Now that we know how the Arduino IDE works internally we will bypass it and use make for compiling, uploading, and monitoring our programs. Make is a project management tool that has been around for decades and helps you to build executables from source files. It knows a lot of rules for turning C/C++ files into object files, for example, and it has some clever algorithms for calculating dependencies between your project’s files, so it only compiles source files if needed.

In contrast, the Arduino IDE always recompiles your sketch and all the system libraries it needs. This usually happens very fast on a modern computer, but it is not necessary and in some cases, it might be even annoying. Also, it’s hard to automate tasks with the IDE. For example, you might have completed a successful project and now you have to transfer the same software to a bunch of Arduinos. In this case, you want to compile your program only once and then upload it to as many devices as you like.

In a typical development cycle for an Arduino program, you compile the program and upload it to an Arduino board. Then you usually start a serial monitor to see what’s happening. So our perfect Makefile should support the following targets:

  • all: This is the default target in nearly all Makefiles on this planet and it builds the whole project.
  • clean: Sometimes it’s good to start from scratch and this target deletes all build artifacts.
  • upload: Upload the software to an Arduino board and compile it if necessary.
  • monitor: This target opens a serial monitor and connects it to the Arduino.
  • upload_monitor: This is a convenience task that uploads a program and opens a serial monitor.

Writing Makefiles isn’t fun, but fortunately, Alan Burlison did all the hard work for us already. He created a master Makefile that you can include in your own Makefile, so you only have to adjust some settings. I had to add some minor changes to the master Makefile, and you can download my version (together with all the other code I use in this article) from GitHub.

Let’s see how to actually use make to compile a simple Arduino program.

Hello, World!

One of the simplest Arduino programs possible is the following:

void setup() { 
Serial.begin(9600);
}
void loop() {
Serial.println("Hello, world!");
delay(1000);
}

It initializes the serial port and then it outputs the text “Hello, world!” endlessly in a loop. Here’s our project’s Makefile:

# Your Arduino environment.
ARD_REV = 22
ARD_HOME = /Applications/Arduino.app/Contents/Resources/Java AVR_HOME = $(ARD_HOME)/hardware/tools/avr
ARD_BIN = $(AVR_HOME)/bin AVRDUDE = $(ARD_BIN)/avrdude
AVRDUDE_CONF = $(AVR_HOME)/etc/avrdude.conf
# Your favorite serial monitor.
MON_CMD = screen MON_SPEED = 9600
# Board settings.
BOARD = diecimila
PORT = /dev/tty.usbserial-A60061a3
PROGRAMMER = stk500v1
# Where to find header files and libraries.
INC_DIRS = ./inc
LIB_DIRS = $(addprefix $(ARD_HOME)/libraries/, $(LIBS))
LIBS =
include ../Makefile.master

As you can see you have to define only a few variables: ARD_REV specifies the revision of the Arduino IDE you have installed. Although you no longer have to use the IDE to compile your programs, you still need an installation of the IDE to have all tools and libraries available. ARD_HOME has to point to the IDE’s installation directory.

If your program uses the serial port, set MON_CMD to your favorite serial monitor. Also, you should set MON_SPEED to the baud rate you’re using in your program. Unfortunately, serial monitors often differ in their invocation syntax, so you might have to change the master Makefile to get your preferred serial monitor running. Here’s the relevant portion of the master Makefile:

upload : all
- pkill -f '$(MON_CMD).*$(PORT)'
- sleep 1
- stty -f $(PORT) hupcl
- $(AVRDUDE) -V -C$(AVRDUDE_CONF) -p$(MCU) -c$(PROGRAMMER) \
-P$(PORT) -b$(UPLOAD_SPEED) -D -Uflash:w:$(IMAGE).hex:i
monitor :
$(MON_CMD) $(PORT) $(MON_SPEED)

Adjust the monitor target to your needs and notice that the upload target removes all running serial monitor processes from the process list using pkill. Depending on your UNIX flavor you also might have to change the pkill and stty calls. For example, Mac OS X usually does not have a pkill command, but you can install it. Also stty’s command switch ‘-f’ is named ‘-F’ on Linux systems.

Back to our master Makefile’s configuration. With the BOARD variable, you define which type of Arduino board you’re using. You can find a list of all supported boards in hardware/boards.txt in the IDE’s installation directory. PORT contains the name of the serial port you have connected your Arduino to.

Finally, you can tell the compiler where to look for header files and libraries using INC_DIRS and LIB_DIRS. With LIBS you can define a set of libraries that should be linked to your project. If you need the libraries for LCD and EEPROM, for example, set LIBS to “LiquidCrystal EEPROM”. You can find many useful libraries in the libraries folder of the IDE’s installation directory.

Don’t forget to include the master Makefile at the end and then run your Makefile for the first time:

maik> make
...
avr-size build/HelloWorld.hex
text data bss dec hex filename
0 2106 0 2106 83a build/HelloWorld.hex

If everything goes fine you will see a lot of output that is very similar to the IDE’s verbose output. In fact, it is nearly the same. A major difference is that you will see the output of the avr-size command at the end. This tool tells you how much memory your program will use on the Arduino and it also tells you which parts of the memory it occupies. This can be a helpful debugging aid if you ever use too much memory. And believe me: sooner or later you will!

You might have noticed that we did not specify a target when we invoked make. If you do not specify a target make will run the all target by default. In our case, this target builds the whole project. It creates a directory named build in your project’s directory. There you can find all artifacts that make created during the build process. Most files are system files and libraries needed by all Ardunio programs and they do not differ from project to project. Only files starting with “HelloWorld” were built specifically for our project.

All files ending with eep, elf, and lst are binary files that you probably know from other build processes. Only HelloWorld.hex might be new to you and you might wonder where the final executable file like a.out is? The answer is simple: HelloWorld.hex is the final executable. It contains all the data that we will upload to the Arduino.

Now run the upload target and you should see something like this (I’ve shortened some path names for clarity):

maik> make upload
pkill -f 'screen.*/dev/tty.usbserial-A60061a3'
sleep 1
stty -f /dev/tty.usbserial-A60061a3 hupcl
avrdude -V -Cavrdude.conf -patmega168 -cstk500v1 -b19200 \
-P/dev/tty.usbserial-A6 -D -Uflash:w:build/HelloWorld.hex:i
avrdude: AVR device initialized and ready to accept instructions
Reading | ############################################### | 100% 0.02s
avrdude: Device signature = 0x1e9406
avrdude: reading input file "build/HelloWorld.hex"
avrdude: writing
flash (2106 bytes):
Writing | ############################################### | 100% 1.60s
avrdude: 2106 bytes of flash written
avrdude: safemode: Fuses OK
avrdude done. Thank you.

Here you can see the avrdude command in action. This tool is responsible for loading code into the Arduino, and you can use it for programming many other devices, too. Study the output carefully to see where make uses our former variable definitions such as PORT.

Finally, we should test if our program actually does what it’s supposed to do. Invoke make monitor and your serial monitor should open and print lots of “Hello, world!” messages.

That’s it! You’ve compiled and uploaded your first Arduino program without starting the Arduino IDE. Also, you’ve monitored your program’s output from the command line and did not use the IDE’s serial monitor. You’re well prepared for the next steps!

So let’s build a more sophisticated project: a park distance control system. We will use some advanced C++ features such as namespaces and template programming, and we’ll have a look at dynamic memory management on the Arduino.

A Park Distance Control System

Many new cars come with a park distance control system (PDC) that helps you to enter or leave tight parking spaces. Whenever you are going to collide with an obstacle such as another car the PDC will give you an acoustic (and sometimes also visual) warning. Usually, the PDC starts to beep if it detects an object near your car and it increases the frequency of the tones the closer you get. If you’re getting too close it emits a non-stop warning tone.

If we’re going to build our own PDC we need a device for generating acoustic signals. A piezo buzzer is a perfect fit for our purposes. It’s cheap and easy to use because it only has two pins. Connect one to the Arduino’s ground and the other one to the Arduino’s digital pin #13 (see illustration). It doesn’t matter which of the buzzer’s pins you connect to ground.

A piezo buzzer (top) connected to Arduino pin 13 and an infrared proximity sensor connected to 5V pin

We also need a sensor for measuring the distance to the nearest object, and we have several options. Commercial PDCs use ultrasonic sensors, because they offer high accuracy and a large range. For our PDC I’ve chosen the SHARP GP2Y0A21YK0F infrared proximity sensor, because it’s much cheaper than most ultrasonic sensors. The sensor emits infrared light and measures the time it takes for the reflected light to get back to the sensor. It is easy to use, but has a very limited range (about 10 to 80 cm). You could not use it for a real car’s PDC, but for a small robot or an RC car it’s sufficient.

The infrared proximity sensor is an analog device that outputs a voltage corresponding to the detection distance. Connect its signal pin to one of the Arduino’s analog pins such as A0, connect the ground pin to the Arduino’s ground and its Vcc pin to the Arduino’s 5V pin (see illustration).

Wiring the circuit of our PDC was easy. Let’s write some code now to bring our hardware to life.

Using the Piezo Buzzer

We start with the code for the piezo buzzer, and without further ado, I present the Speaker class:

#include <stdint.h> 
#include "WProgram.h"
namespace arduino {
namespace actuators {
class Speaker {
public:
static const uint16_t DEF_FREQUENCY = 1000;
static const uint32_t DEF_DURATION = 200;
Speaker(const uint8_t speaker_pin) :
_speaker_pin(speaker_pin) {}
void beep(
const uint16_t frequency = DEF_FREQUENCY,
const uint32_t duration = DEF_DURATION) const
{
tone(_speaker_pin, frequency, duration);
}
private:
uint8_t _speaker_pin;
};
}
}

If you’re not very familiar with C++ this code might look a bit shocking at first, but we’ll dissect it piece by piece. First of all, we include two header files. stdint.h defines portable integer types, for example uint16_t for a 16 bit unsigned integer. These types are advantageous for several reasons, but most importantly they make your code more predictable because the length of data types in C/C++ is not strictly defined. In addition, they save you some typing, which is always a good thing.

The WProgram.h header file comes with the Arduino IDE and defines all its basic constants and functions such as OUTPUT or digitalWrite. We have to include it because we will use the tone function later on.

After we’ve included all the header files we need, we declare a nested namespace using the namespace keyword. Namespaces help you to prevent name clashes when using code from different sources. You have to separate nested namespace by a double colon (::), so everything we define from here belongs to the arduino::actuators namespace.

Then we define the Speaker class and start with two constants named DEF_FREQUENCY and DEF_DURATION. They contain the default frequency (in Hertz) and the default duration (in milliseconds) of our warning tone. Note that we’re using the types from stdint.h for the first time.

You should also note that we’re using the const keyword for defining the constants. Many people still use the preprocessor’s #define directive for defining constants and I strongly recommend that you get out of this habit! The compiler will never see the name of a constant defined using #define, so this can lead to strange error messages and long debugging sessions. Also, depending on your constant’s type, the compiler will often produce more effective code when using proper constant definitions using const.

Have you noticed that in modern C++ it’s possible to define static constants (aka class constants) like DEF_DURATION directly in a class declaration? This is a very handsome feature and also helps to keep namespaces clean.

The constructor of our Speaker class expects a single argument (the number of the digital pin it’s connected to) and it initializes a private class member named _speaker_pin. Private member names do not have to start with an underscore in C++, but I think it makes code more readable under certain circumstances.

You might wonder how we initialize _speaker_pin without using an assignment. We’ve used a member initialization list which in C++ is the preferred way for initializing class members. In most cases, member initialization is more efficient than assignment statements and in some cases, for example for const members or for references, you even have to use it. All in all, it is never a disadvantage and it’s often more efficient.

Next, we define a method named beep that takes two arguments, a frequency, and a duration. Both arguments have default values, so if you do not pass a duration, for example, it will automatically be set to DEF_DURATION. The method body is simple because it delegates all the hard work to the Arduino’s tone function.

There’s one thing worth mentioning, though: we declare the beep method as const. This tells the compiler that calling this method does not change the class’s state. It helps the compiler to optimize the machine code and it also helps to detect errors when passing around constant objects. Use const wherever possible!

The definition of the Speaker class is complete and you can use it as follows:

#include "speaker.h"
using namespace arduino::actuators; Speaker speaker(13); speaker.beep(1000, 500);

This code fragment uses a piezo buzzer connected to pin 13 and outputs a tone having a frequency of 1000 Hz for 500 milliseconds. Note that we import the arduino::actuators namespace with the using namespace directive. Without it, you’d have to use the fully qualified name arduino::actuators::Speaker.

Even in our small Speaker class, we can benefit from many useful C++ features. We have used namespaces, class constants, const function declarations, and member initialization lists. We’ll use even more features when we start to control the infrared proximity sensor.

Using the Infrared Proximity Sensor

Our infrared proximity sensor is an analog device, so it outputs a voltage corresponding to the current detection distance. Its datasheet contains some graphs explaining how a certain voltage can be mapped to a distance. We can simplify the distance calculation because the distance is approximately equal to 27 V * cm.

You might be tempted to read the sensor’s signal from analog pin A0 using analogRead and turn it into a distance immediately. Unfortunately, the real world is a bit more complicated, because sensor signals are often subject to jitter and distortion. So, it’s a much better idea to continuously append sensor data to a small buffer and calculate their average value. If the buffer is full and a new sensor value arrives, the oldest value will be removed from the buffer. We call such a data structure a ring buffer.

After I’d written countless ring buffer implementations using fixed-length arrays of various types, I decided to find a reusable solution. Here’s my dynamic RingBuffer class for all C++ integer types:

namespace arduino { namespace util {
template<typename T>
class RingBuffer {
private:
T* _samples;
uint16_t _sample_pos;
uint16_t _buffer_size;
public:
static const uint16_t DEF_SIZE = 16;
RingBuffer(const uint16_t buffer_size = DEF_SIZE) {
_sample_pos = 0;
_buffer_size = buffer_size != 0 ? buffer_size : DEF_SIZE;
_samples = static_cast<T*>(
malloc(sizeof(T) * _buffer_size)
);
}
RingBuffer(const RingBuffer& rhs) {
*this = rhs;
}
RingBuffer& operator=(const RingBuffer& rhs) {
if (this != &rhs) {
_sample_pos = rhs._sample_pos;
_buffer_size = rhs._buffer_size;
_samples = static_cast<T*>(
malloc(sizeof(T) * _buffer_size)
);
for (uint16_t i = 0; i < _buffer_size; i++)
_samples[i] = rhs._samples[i];
}
return *this;
}
~RingBuffer() {
free((void*)_samples);
}
void addValue(const T value) {
_samples[_sample_pos] = value;
_sample_pos = (_sample_pos + 1) % _buffer_size;
}
T getAverageValue() const {
float sum = 0.0;
for (uint16_t i = 0; i < _buffer_size; i++)
sum += _samples[i];
return round(sum / _buffer_size);
}
uint16_t getBufferSize() const {
return _buffer_size;
}
};
}
}

This class uses a lot of the features we’ve seen already in our Speaker class. Still, we have something new: template classes. You can do a lot of interesting things using template classes, but you’ll mainly use them for generating classes for different types. In our case, it helps us to make the RingBuffer work for all C++ integer types such as int or long.

Defining template classes is easy. Simply put a template<typename T> declaration in front of the class declaration. Now you can use “T” as a placeholder for every C++ type in your class (you can choose whatever name you like instead of “T”, but it’s a widespread convention).

We use the placeholder for the first time to declare the samples member. Here we declare a pointer to the actual memory buffer we are going to use. In a non-templated class this declaration would have been something like uint16_t*_samples;, but using templates we can be more generic. Then we define two more member variables. _sample_pos stores our current position in the buffer, so we know when we have to remove older data samples. _buffer_size contains the size of our sample buffer.

The public interface of our class starts with a class constant named DEF_SIZE that specifies a default size for the ring buffer. The constructor is a delicate piece of code. It expects the buffer size as an argument and initializes all private members. In this case, we cannot use member initialization lists, because we cannot use pure assignment statements to initialize the member variables. First of all, we set _sample_pos to 0. Then we initialize _buffer_size and make sure that the buffer size is greater than 0. If it’s not we use the default size.

You might think that it’d be more appropriate to raise an exception in such a case and I fully agree with you. Unfortunately, we’ve found one of the major restrictions when programming the Arduino: it does not support exceptions, because their runtime overhead would be too big. So we had to find a compromise and using the default size is a reasonable approach in this case. If you need more control, you have to add a separate initialize method that returns some kind of error code.

In the next line, we initialize the samples buffer and we find another restriction. Usually, you’d use the new operator in C++ to allocate objects dynamically and you’d use delete to give back the occupied memory. Both operators aren’t supported on the Arduino, so we had to use good old malloc. For our purpose, it does not make a difference, because we will only allocate memory for integer types. It makes a big difference when you allocate memory for “real” objects because the new operator automatically calls each object’s constructor while malloc doesn’t. The same is true for giving back the memory. delete automatically invokes all destructors and free does not.

Note that we use C++’s shiny new static_cast operator to turn the void pointer returned by malloc into a pointer to our generic T objects. Attentive readers have certainly noticed that we did not check if malloc returned NULL. We should have done this, and in the next version of this class, I’ll definitely add an initialize method!

The next three methods you’ll find in nearly every C++ class that encapsulates pointers to dynamic memory. We define a copy constructor, an assignment operator, and a destructor. These methods make sure that you can safely copy objects and pass them to methods without messing up the dynamic memory.

Finally, we implement the ring buffer’s business logic. addValue takes a data value having a generic type and appends it to the ring buffer. It uses the modulus operator to make sure that we do not exceed the data buffer’s bounds and to automatically throw out old values if necessary. getAverageValue calculates the average value of the current sample buffer and returns it as a generic type.

Now that we have a generic ring buffer implementation, let’s use it to implement an InfraredSensor class:

#include <stdint.h>
#include "ring_buffer.h" using namespace arduino::util;
namespace arduino {
namespace sensors {
class InfraredSensor {
private:
uint8_t _pin;
RingBuffer<uint16_t> _buffer;
public:
static const float SUPPLY_VOLTAGE = 4.7;
static const float VOLTS_PER_CM = 27.0;
InfraredSensor(const uint8_t pin);
void update(void);
float getDistance(void) const;
};
}
}

This class has only two data members. In _pin we store the number of the analog pin we have connected the sensor to and _buffer is the ring buffer we’re going to use to de-jitter the sensor’s signal. Here we have to instantiate a concrete implementation of our template class for the first time. When the compiler encounters the declaration RingBuffer<uint16_t> it replaces all occurrences of the generic type “T” with uint16_t and compiles the file afterward. If in your next project you’re working with a sensor that outputs smaller values, it might be sufficient to use RingBuffer<uint8_t>, for example.

The rest of the class declaration is fairly easy. We define a constructor and two methods for updating the sensor and for getting the distance to the nearest object. Their implementation is straightforward:

#include <WProgram.h>
#include <stdint.h>
#include "infrared_sensor.h"
namespace arduino {
namespace sensors {
InfraredSensor::InfraredSensor(const uint8_t pin) : _pin(pin) {
for (uint16_t i = 0; i < _buffer.getBufferSize(); i++)
update();
}
void InfraredSensor::update(void) {
_buffer.addValue(analogRead(_pin));
}
float InfraredSensor::getDistance(void) const {
const float voltage =
_buffer.getAverageValue() * SUPPLY_VOLTAGE / 1024.0;
return VOLTS_PER_CM / voltage;
}
}
}

The constructor uses a member initialization list to initialize the _pin variable and then it initializes the ring buffer, so we have some data available right from the start. update reads the sensor’s current output using Arduino’s analogRead function and adds it to the ring buffer. Then we use a simple formula to calculate the distance to the nearest object in getDistance.

Isn’t this code beautiful? It’s mostly because we’ve separated concerns and because we could keep all methods very small. Also RingBuffer is completely independent of all other classes and even of all Arduino stuff. This way you can easily test it and you can even use it in projects that aren’t related to the Arduino.

Minimizing dependencies is always a good idea and using classes helps a lot. The same is true for generic programming because it helps us to decouple algorithms (for example, calculating an average value) from concrete data types.

Building a PDC Application

The only thing missing is the PDC’s business logic, but that’s really a piece of cake now. We have already implemented all major classes and in the illustration, you can see a class diagram of our final product. Only the ParkDistanceControl class is missing and here is its interface:

#include "infrared_sensor.h"
#include "speaker.h"
using namespace arduino::sensors;
using namespace arduino::actuators;
namespace arduino {
class ParkDistanceControl {
private:
InfraredSensor _ir_sensor;
Speaker _speaker;
float _mounting_gap;
public:
static const float MIN_DISTANCE = 8.0;
static const float MAX_DISTANCE = 80.0;
ParkDistanceControl(
const InfraredSensor& ir_sensor,
const Speaker& speaker,
const float mounting_gap = 0.0) :
_ir_sensor(ir_sensor),
_speaker(speaker),
_mounting_gap(mounting_gap) {}
void check(void);
};
}

As shown in the class diagram this class uses an InfraredSensor object and an instance of the Speaker class. In addition, it stores a mounting gap. This is necessary because when calculating the distance to the nearest object you have to take into account that the sensor usually isn’t mounted directly to the car’s surface.

The class’s constructor only initializes all data members, so we could implement it in the class definition. With the check method you can check the PDC’s current state, and its implementation looks like this:

#include <WProgram.h> 
#include "pdc.h"
namespace arduino {
void ParkDistanceControl::check(void) {
_ir_sensor.update();
const float distance =
_ir_sensor.getDistance() - _mounting_gap;
if (distance <= MIN_DISTANCE) {
Serial.println("Too close!");
_speaker.beep();
} else if (distance >= MAX_DISTANCE) {
Serial.println("OK.");
} else {
Serial.print(distance);
Serial.println(" cm");
}
}
}

No big surprises here: we update the infrared sensor and then we calculate the distance to the nearest object. Then we check if the distance is less than the minimum distance we allow. If yes, we print an appropriate message to the serial port for debugging purposes and we make our speaker beep. If the distance is greater than the sensor’s range, we print “OK” to the serial port, and in the remaining case, we print the current distance.

Finally, we create a minimalistic Arduino sketch for our project:

#include <stdint.h>
#include "pdc.h"
const uint16_t BAUD_RATE = 57600;
const uint8_t IR_SENSOR_PIN = A0;
const uint8_t SPEAKER_PIN = 13;
const float MOUNTING_GAP = 3.0;
arduino::ParkDistanceControl pdc(
InfraredSensor(IR_SENSOR_PIN),
Speaker(SPEAKER_PIN),
MOUNTING_GAP
);
void setup(void) {
Serial.begin(BAUD_RATE);
}
void loop(void) {
pdc.check();
delay(50);
}

We initialize a global ParkDistanceControl object and pass it an InfraredSensor instance, a Speaker object, and a mounting gap. The setup function only initializes the serial port, and in loop, we check the PDC’s state every 50 milliseconds. We’re done!

You can use the following Makefile to build, upload, and monitor the PDC:

ARD_REV = 22
ARD_HOME = /Applications/Arduino.app/Contents/Resources/Java AVR_HOME = $(ARD_HOME)/hardware/tools/avr
ARD_BIN = $(AVR_HOME)/bin
AVRDUDE = $(ARD_BIN)/avrdude
AVRDUDE_CONF = $(AVR_HOME)/etc/avrdude.conf
PROGRAMMER = stk500v1
MON_CMD = screen
MON_SPEED = 57600
PORT = /dev/tty.usbmodemfa141
BOARD = uno
LIB_DIRS = $(addprefix $(ARD_HOME)/libraries/, $(LIBS)) include ../Makefile.master

The Makefile automatically compiles all files ending with pde, c, and cpp (it currently ignores files ending with cc). While the serial monitor is running play with your hands in front of the sensor and see how the output changes.

What About Performance?

You might wonder if it’s appropriate to build complete class hierarchies for these kinds of simple projects. From my experience, it is, because reusing classes such as RingBuffer helps to reduce programming and debugging time tremendously. Also, a clean interface design makes it much easier to replace hardware components.

On top of that, proper C++ code does not cost as much as you might think, because modern compilers optimize their output massively. Most of the features you’ve seen in this article have no performance or memory overhead at all and may even help to reduce your program’s size.

The article’s code repository contains a classless PDC implementation in a single file. It is as simple as possible and does the same as our object-oriented version. It needs 5,690 bytes while the object-oriented version needs 6,646 bytes. That’s a small overhead, especially taking into account that it’s usually much easier to refactor and optimize the object-oriented version.

Interestingly, the code size is 6,756 bytes when I use the IDE to build the project. Obviously, the IDE uses different settings for the compiler and the linker, which leads to a final important issue: portability. If you’re planning to release your project to the public you should keep in mind that people will expect that your project works with the Arduino IDE.

To achieve this you only have to follow a few conventions: your project’s main folder has to have the same name as your pde file (in our case it’s park_distance_control. All files have to be in the same directory, so you cannot put header files into a separate include folder, for example. Don’t use a cpp file that has the same name as your pde file. In our case, you should not have a file named park_distance_control.cpp, for example. If you follow these rules, your project should run fine with the IDE and your Makefile.

All in all, I think it’s well worthwhile to treat every Arduino project as a serious software development project. Just because the codebase might be small, that doesn’t mean it has to be sloppy.

Of course, there are still plenty of interesting and advanced techniques left. We did not talk about inheritance or unit testing, for example. Maybe we will deal with some of these issues in a future article!

About Maik Schmidt

Author Maik Schmidt
Author Maik Schmidt

Maik Schmidt has worked as a software developer for more than 20 years and makes a living creating complex solutions for large enterprises. Outside his day job, he writes book reviews and articles for computer science magazines. He is the author of Arduino: A Quick-Start Guide, Second Edition and Raspberry Pi: A Quick-Start Guide, Second Edition available from The Pragmatic Bookshelf.

Cover from PragPub magazine, April 2014
Cover from PragPub magazine, April 2014

--

--

PragPub
The Pragmatic Programmers

The Pragmatic Programmers bring you archives from PragPub, a magazine on web and mobile development (by editor Michael Swaine, of Dr. Dobb’s Journal fame).