Thursday, May 25, 2017

Arduino IDE: Best practices and gotchas

Programming for the Arduino is designed to be easy for beginners. The Integrated Development Environment (IDE) provides a safe place to write code, and handles the make and compiler steps that are required to create processor instructions from your C++ code.

This is fine for trivial applications and school exercises. But as soon as you try to use structured code (including classes and custom libraries) on a larger project, mysterious errors and roadblocks become the order of the day.

This article will consider best practices for working within the IDE. I will document a number of common errors and their workarounds. My perspective is of an experienced Python coder who finds C++ full of needless obfuscation. But we can make it work!

Why not switch?

On encountering limitations with the Arduino IDE, the natural thing to do is switch to a mature development environment. For example, you could use Microsoft Visual Studio by way of Visual Micro, a plugin that enables Arduino coding. Or, use Eclipse with one of several available plugins: Sloeber, PlatformIO, or AVR-eclipse.

But there are cases when it is advantageous to stick with the Arduino IDE. For example, I might be working on a team with other less-experienced developers. While I might wish to carry the cognitive burden of Eclipse plus plugins plus project management, they might not.

Or I could be in a teaching environment where my code must be developed with the same tools my students will be using.

Language features... and what's missing

The Arduino IDE gives you many basic C++ language features plus hardware-specific functions. Control structures, values, and data types are documented in the Reference.

But you don't get modern features such as the Standard Template Library (STL). If you want to use stacks, queues, lists, vectors, etc. you must install a library. Start with those by Marc Jacobi (last updated 2 years ago) and Andy Brown (updated 1 year ago). I am sure there are plenty of articles discussing the relative merits of these or other solutions.

You also don't get new and delete operators, and there's good reason. Dynamic memory management is discouraged on microprocessor boards, since RAM and other resources are limited. There are libraries that add these to your toolkit, but the IDE encourages us to use C++ as though it was little more than plain vanilla C. It can be frustrating, but my advice is to adapt.

Code structure

As you know, when using the Arduino IDE you start coding with a sketch that is your application's entry point. As an example, I'll use project.ino.

Inside this file are always two functions, setup() and loop(). These take no parameters and return no values. There's not much you can do with them... except populate them with your code. These functions are part of an implicit code structure that could be written as follows:

void main() {

    // declaration section

    setup();               // initialisation (runs once)

    while (true) {
        loop();            // process-oriented code (runs forever)
    }
}

In the IDE you never see the main() function and neither can you manipulate it.

Declaration section

The declaration section comes at the top of your project.ino. It is effectively outside any code block. Yes, even though it is in an implicit main() function. This means that only declarations and initializations are valid here. You cannot call methods of a class, nor access properties. This is our first rule:

Rule 1. The declaration section should contain only includes, initialisations of variables, and instantiations of classes.

This restriction can result in subtle errors when using classes. The declaration section is naturally where you will be instantiating classes you wish to use throughout the sketch. This means that the same restrictions just stated must apply to each and every class constructor. For this reason, you cannot use instances or methods of other classes in a constructor. No, not even for built-in libraries like Serial or Wire. Because the order of instantiation of classes is non-deterministic. All instances must be constructed before any instances are used.

Rule 2. A class constructor should have no arguments and do nothing but set default values for any properties.

Follow the example of the library classes for your own custom classes. Provide a begin() method that does take needed parameters and performs any initialization tasks. In other words, begin() should do everything you might otherwise expect the constructor to do. Call this method in the setup() block.

By the way, this solves another problem. A class that might be passed to another class requires a constructor that takes no parameters. Normally you would provide this in addition to another constructor template. But if you follow the rule two, this condition is already met.

Care with instantiation

The next discussion will prevent a syntax error. When instantiating a class with a constructor, you would normally do something like the following, assuming class Foo is defined elsewhere.

const byte pin = 10;
Foo bar(pin);

void setup() {}

void loop() {
    int result = bar.read();
}
But following our previous rule, constructors will never have arguments. You might quite naturally write this instead:

const byte pin = 10;
Foo bar();

void setup() {
    bar.begin(pin);
}
void loop() {
    int result = bar.read();
}
This generates the error "request for member 'read' in 'bar' which is of non-class type 'Foo'. That appears nonsensical, because Foo is most definitely a class. Spot the syntax error?

To the compiler, bar() looks like a function call. You need to rewrite that line as:

Foo bar;

Abandoning the sketch

Before you even get to this point of sophistication in your code, you will be seeing all sorts of mystifying compiler output. "Error: 'foo' has not been declared" for a foo that most certainly has been declared. "Error: 'foo' does not name a type" for a foo that is definitively a type. And so on.

These errors occur because the compiler is generating function prototypes for you, automatically, even if you don't need them. These prototypes will even over-ride your own perfectly good code. The only thing to do is abandon the sketch! Move to the lifeboats! Compiler error! Danger, Will Robinson!

Ahem.

Do the following:

1. Create a new .cpp file, ensuring it is not named the same as your sketch, and also not named main.cpp. These are both name conflicts. As an example, let's call it primary.cpp.

2. Copy all the code from project.ino to primary.cpp.

3. Add #include <Arduino.h> to the top of primary.cpp, before your other includes. This ensures that your code can access the standard prototypes.

4. In project.ino leave only your #include statements. Delete everything else.

This will solve all those mysterious issues. You can now prototype your own functions and classes without the IDE getting in your way. You will, however, need to remember that every time you add an #include to primary.cpp, you need to also add it to project.ino. But it's a small price to pay.

Rule 3. Use a top-level C++ file instead of a sketch file.

Simple includes

It's easy to get confused about include files. But all an include represents is a copy and paste operation. The referenced code is inserted at the point where the include directive is located.

Here are the rules.

1. You need a .h header file for each .cpp code file.

2. The .cpp should have only one include, that being its corresponding header (a file with the same name but different extension).

3. The header file must have all the includes necessary for the correct running of the .h and .cpp code. And, in the correct order, if there are dependencies.

4. A header guard is required for each .h file. This prevents the header from being included in your project multiple times. It doesn't matter what variable name you choose for the test, so long as it is unique.
#ifndef LIB_H
#define LIB_H

// everything else

#endif
5. If you have any sort of complex chaining, with circular pointer referencing, you may have to use forward referencing. But you should be avoiding this complexity in the sort of projects likely to run on an Arduino. So I won't count this rule in our next meta-rule.

Rule 4. Follow the four rules of correct header use.

Using libraries

The IDE limits how you use libraries to the very simplest case. Libraries get installed in one standard location across all your projects. You can put a library nowhere else. Why might you want to?

I am currently developing three modules as part of a single project. The code for each module is in its own folder. They have shared library code that I would like to put in a parallel folder, so I would have a folder hierarchy something like this:

/myproject
    /module-1
    /module-2
    /module-3
    /common
Then I could easily archive "myproject" into a ZIP file to share with the rest of the team.

Can I do this? No. It is not possible, since relative paths cannot be used in the IDE. And absolute paths are evil.

Rule 5. There is no rule to help manage libraries. Sorry.

Final Words

I have personally wasted dozens of hours before discovering these tips and working methods. It has been an enormous process of trial-and-error. If you are lucky enough to read this article first, you will never know the pain.

I have a donate button in the sidebar, in case you wish to thank me with a coffee.

In turn I'd like to thank Nick Gammon for an article I wish I'd read a bit sooner.

If there's interest, I might follow up with some words about general C++ syntax and issues that are not so Arduino-centric.

RELATED POSTS

No comments:

Post a Comment