Language: EN

4-consejos-para-programar-codigo-mas-limpio-en-arduino

4 tips for writing cleaner code in Arduino

So by now, you already know about Arduino and other MCUs. Analog inputs and outputs have no secrets for you, and you control PWM, LEDs, and motor movement like a pro. Congratulations, you know how to write code in Arduino!

But you, who always want to improve, now want to learn how to program (well) in Arduino. And, my friend, programming is much more than putting together a bunch of lines of code that “just work.” If you want to play in the major leagues, your code has to be clean.

Speaking in general about the world of programming, more and more people are programming. Which is good… but it also means that the average quality of programming is decreasing.

Within the world of Arduino and other MCUs, and all the many (so many!) things that we owe to this great ecosystem, the truth is that the lack of average quality is almost scandalous.

This is due, in large part, to a very marked and widespread existence of two types of user profiles:

  • Novice user: who learns with effort on their own, reads tutorials on the Internet, and evolves as best they can.
  • Expert user: who learned 40 years ago, using a PIC that had one and a half bits, and continues to maintain the same practices.

But, as we said, you want to program better. And that has brought you to this post where we are going to see a few tips and guidelines so that you can improve your way of programming.

1) Keep your code clean and organized

consejos-para-programar-arduino-limpio-spagetti

Your code must be clean, organized, and have a logical order. Clean code contributes to shorter development times, fewer errors, better maintainability, and greater ease of expansion…

Don’t be afraid to stop and refactor from time to time, that is, rewrite and reorder parts of your program to make it do the same thing, but with cleaner and more structured code.

Let’s see some tips on cleanliness and code order.

Be consistent with your style

Every master has his own style, and I won’t be the one to open the debate on style rules now. I don’t care if you use 3 spaces or a tab, if you put the ’{’ on the same line or the line below, if you call the constants IN_UPPERCASE or AsWritten.

But yes, whatever you choose, use it consistently throughout your program. Don’t mix styles, and now this way, and now that way.

Short functions

The most important advice I’m going to give you today, keep your functions short. You only need to see a program made up of a 5-screen loop to know that your code is not good.

This type of long, mixed, unstructured code is called Spaghetti Code.

A long function implies more difficulty in understanding and maintaining, both for the next person and for yourself in a few months, when you come back to open your code and have to understand what you have written so clearly today.

But, also, it denotes a conceptual problem in itself. A function should be responsible for performing a single action in the program. Divide your code into short functions like GetTemperature(), FillScreenBuffer(), ParseMessage()…

Divide your code into files

Divide and conquer! One of the programming guidelines par excellence. The Arduino IDE allows you to work with code in multiple files. Of course, other more advanced IDEs also. So divide your code into files, and add them with #include.

In the best case, your main Sketch should be practically empty. If you don’t feel like complicating yourself with .h and .cpp files and safeguards, etc, then don’t. Simply create a .hpp file, move part of your functions to it, and “include” it.

By dividing the code into files, try to group them into logical blocks. For example, the part related to measuring temperatures in one file, the RF part in another, the communication part in another, the display part in another.

And, if possible, try to create reusable modules, grouping in some files functions that do not depend on your particular program. This way, you can reuse the code for the next program with little or no modification.

Create a constants file

A good habit is to create a Constants.h file that contains all the fixed values that are involved in your program.

// example
const int NUM_CHANNELS = 3;
const int NUM_MEASURES = 10;
const float SENSOR_GAIN = 3.5;
   // .... more constants

Not only does it improve the readability of the code, but it allows you to control the behavior of your entire program by modifying a single file.

You can use objects

The use of objects is perfectly possible in Arduino. Contrary to what is usually believed, it is not necessary to create a library for this. You can create an object in any .hpp file, and even in the .ino file of your program.

2) Good code is understood by reading it

consejos-para-programar-arduino-limpio-words

Pure and tough Agile philosophy. A good programmer writes code that is understood by reading it. This is greatly contributed to by what we have seen in the previous code, that your code is not Spaghetti, the functions are short, and are responsible for a single action.

We will see it in these little tips that will help your code to be more Agile.

Minimize comments

How??? But if comments are good, I’ve been told that the code should be commented! Well, they lied to you. Comments are just another part of the code that takes up space and needs to be maintained.

There are few things more ridiculous than:

// leo la temperatura del sensor
int temperatura = analogRead(pin_sensor)

// multiplico la temperatura por dos
temperatura = temperatura * 2;

In the best case, the only comments needed are the ones that document classes and functions, just above them. But every time you use a comment within a function, a developer kitten cries.

If you follow the rest of the advice in this section and the previous one, you won’t need to use comments, because the goal is for your code to explain itself.

Use representative variable names

I love this, I will insist until the end of days, and I will not get tired of being annoying about this. Don’t be lazy, and use representative names for variables.

// nope
int v = analogRead(ps);
float r = v * f;
float t = r * fs;

// yes
int voltage = analogRead(PIN_SENSOR);
float raw_temperature = voltage * CONVERSION_C_MV;
float temperature = raw_temperature * SENSOR_FACTOR;

Shall we compare readability? In the first one, I have no idea what it’s doing, and in the second one, I can even understand it. And that’s an invented example! So, don’t skimp on the names of the variables, or you will end up spending them on comments.

Use representative function names

Cousin of the previous one, the same applies to function names. Although, in general, this error is less common and people tend to save fewer letters.

// nope
int DoMagic(int ps) { … }

// yes
int GetTemperature(int pin_sensor) { … }

But the same philosophy, give the functions names that explain what they are going to do. It helps a lot when the functions are short and perform a single action, as we have seen before.

Cleanliness in function parameters

Regarding function parameters, apart from the fact that by now it will not surprise you if we say that you should put representative and self-explanatory names to the parameters.

// nope
int GetTemperatura(int ps, int i, int d) { … }

// well
int GetTemperature(int pin_sensor, int interval, int delay) { … }

// yes
int GetTemperature(Config configuration) { … }

But also, it is also convenient to use the fewest number of parameters. Zero parameters is perfect. One is good. Two passable. Three, think about it. More than three, no. If you need three or more, consider creating a structure that contains them.

Avoid “magic numbers”

A magic number is one that you see in the middle of the code and you don’t know where it comes from. Avoid… no… run away from them like crazy. Always encapsulate them in a constant that identifies it, allows you to search for it, and change it from a single place.

// nope
int temperature = tension * 3 * 1.674;

// yes
int temperature = tension * NUM_CHANNELS * CONVERSION_C_MV

Don’t avoid intermediate variables

A typical rookie mistake, chaining function calls to avoid creating intermediate variables. In fact, in general, avoid excessively long lines.

// example 1
// nope
bool result = Validate(GetTemperature(GetVoltage(PIN_SENSOR)));

// yes
int voltage = GetVoltage(PIN_SENSOR);
float temperature = GetTemperature(voltage);
bool isValid = Validate(temperature);

// example 2
// nope
if(GetMeasure(pin_sensor)  { …} 

// yes
auto isValid = GetMeasure(PIN_SENSOR);
if(isValid)  { … }

By using intermediate variables, you are “telling the story” of what you are doing. You don’t need comments, I read it and understand it. If you’re concerned about efficiency, the compiler will do its magic and both functions will be exactly the same when they get to the processor.

Use enumerations

Enumerations are there for a reason, and it seems that in Arduino there is sometimes a phobia of them. It takes you 5 seconds to make an enumeration.

// nope
if( Key == 0) MotorUp();

// yes
enum Control_Key { UP, DOWN, LEFT, RIGHT }

if( Key == Control_Key.UP) MotorUp();

Not only does it make it better understood, and you are telling me what you want to do just by reading it. It also prevents me from coming up with a 5 because I don’t know what the Key is. And if one day you have to change the key assignments, you only have to modify the enumeration and all the code you do will continue to work.

A single exit per function

Avoid functions that have multiple return points. Combined with long functions, it makes the code very difficult to understand. If you have a function in which, depending on many things, it returns one value or another, create a result variable and thus you make it clear what you are doing.

// nope
int myFunction()
{ 
  if(this) return 5;
  //... more code

  for(something)
  {
    if(otherThing) return 10;
  }
  
   if(yet another conditional) return 12;
   //... much more code
}

// yes
int myFunction()
{ 
   int result;
   if(this) result = 5;
   //... more code

  for(something)
  {
    if(otherThing) result = 10;
  }
  
   if(yet another conditional) result = 12;
   //... much more code
   
   return result;
}

We accept as an exception (it would be debatable) the rejection returns. That is, a return placed at the beginning of the function that “bounces” the execution, as long as it is very clear what it does at a glance.

// acceptable
void MoveMotor(int parameter)
{
  if(parameter < 10) return;
  if(parameter > 210) return;
  
  //... move motor
}

Adjust the Scope of the variables

You have probably read that the use of global variables is evil. In some ways this is true, and in programming, they tend to be avoided. Or, at least, to disguise them so that it seems that we are not using them (ahem, ahem… singleton pattern… ahem).

In the case of an MCU, we don’t have as many tools to avoid the use of global variables, but it is clear that abusing them is bad. For example:

// nope
float raw;
float a;
float b;
float temperature;
void CalculateTemperatura()
{
  temperature= a * raw + b
}

// yes
float CalculateTemperature(float raw, float a, float b)
{
  return a * raw + b
}

However, on the other hand, you are programming an MCU that does not have the memory and power of a computer. So if you have a variable/object (let’s say) a servo, or an httpClient, and it really is something that is part of your entire program, don’t hesitate to define them as global variables (without abusing them).

3) Keep It Simple

consejos-para-programar-arduino-limpio-kiss

KISS, another programming guideline (and not just for programming). The simpler something is, the better. Less chance of errors, of someone making a mistake, of wasting time trying to understand what someone else has written, or even yourself when you open it three years later and say… was I drunk or what?

Get it through your head, simpler is better. And let’s see some tips about it.

Avoid bit operations and masks

There is nothing more ‘fashionable’ to an old school programmer, or one who has been seduced by the mathematical madness of his teacher, than throwing a code like

int T = (REG >> 2 & b00001001 ) || ( TMCR_6 >> 3 & b00100101 );

I don’t care what the code is, I made that one up. Every time you see something like that, I’m sure it could be replaced with 2-3 lines of arithmetic operations and a conditional.

Avoid pointers

Another, how? But C is based on pointers, and I’ve been told that it is “the top” to program in Arduino. Yes, but Arduino’s Wiring is based on C++, not C. And part of the improvements that C++ had was to minimize the use of pointers.

In fact, most of modern computing seems to be aimed at avoiding saying the word “pointer,” precisely because they quickly become a readability nightmare.

C++ incorporated references, and 80% of the things you do with pointers can be done with a reference. So don’t get used to using pointers “just because.” On the contrary, use pointers only when there is “no other choice.”

Avoid touching the Timers

In any traditional MCU programming, the Timers were a fundamental part of achieving different functionalities.

However, Arduino, at least the Atmega 328p 32u2, etc, are very short of Timers. In addition, these are used by many ecosystem libraries and functions (millis and delay, for example) both internally and externally.

So, trust me, don’t play with the Timers if you don’t want strange things to happen to you. Besides, you can do almost everything you could do by manipulating a Timer in other ways.

If you really need a fine adjustment (very fine!) of the time, choose an MCU with more Timers. And even then, you will have libraries so that you never, ever, have to play with the registers.

Pre-increment and post-increment

The ++i and i++ operator is a plague that plagues programming, possibly only surpassed by the disaster of null (another day I’ll tell you about that). They are a frequent source of errors, and how “fun” it is until you find it!

//nope
var miValor = miVector[i++] * 5.7;

//yes
i++;
auto miValor = miVector[i] * 5.7;

Basic rule, whenever you use an operator ++, —, on its own line and alone.

Use the right loops

It seems quite obvious, but you have three types of loops. By far the most used is the for loop, followed by while and then do while (and the first would be foreach, but speaking in MCUs we’ll leave it).

Use each one for what they are, and avoid doing “great things” like:

// nope, don't do that
for(;; condition)
{
}

Touch yourself. I will only comment that later see the section “just because it works doesn’t mean it’s right.”

Use the right conditionals

Similar to the previous one, you have the usual if conditional, the switch, and the ternary operator ?. Use each one when appropriate, and avoid putting nested conditionals like crazy.

Use the string class

It’s amazing the mess of code that is sometimes seen for something that could have been solved in one line with the String class. The String class has an undeserved bad reputation for causing problems with dynamic memory.

It has a point, String is an object, and to create a new String it needs to be positioned in memory. If we start creating Strings like crazy, and since Arduino doesn’t have a memory manager like a normal computer, you create objects in different memory positions. And, even if you release them, you are leaving it like a Swiss cheese and in the end, even though you have available memory, none of them fits your new String.

Then you have a nice memory collapse, which Arduino solves by restarting and getting rid of the little torture that your sketch has been, starting again with clean memory and brand new.

That’s how it is, and it can’t be argued. But it’s not the String class’s fault, it’s yours for creating objects like crazy. The String class, when used properly, is a great ally that will save you huge amounts of code.

4) Don’t freak out about efficiency

consejos-para-programar-arduino-limpio-smart

This is the most controversial point and, for many, the hardest to understand. Code cleanliness versus execution efficiency. We will agree that both are desirable, and you always have to keep an eye on both.

The good news is, many times both go hand in hand, and clean code is more efficient. But (there had to be a but), also often you will have a compromise between cleanliness and efficiency.

Attention, this can blow some people’s minds. When you have to choose between cleanliness and efficiency you should choose cleanliness, except on occasions that you can count on one hand.

Optimize with your head

What’s the point of writing a much more complex code fragment to save 10 nanoseconds, if right after you’re going to use a delay(…)?

// a round of applause for this lack of common sense
int T = (REG >> 2 & b00001001 ) || ( TMCR_6 >> 3 & b00100101 );
delay(5000);

Or, what’s the point of killing yourself to scratch nanoseconds here, when you then pass an object through the stack and that operation is a million times slower than anything you could have scraped before.

In any program, be it Arduino or another, most of the time is spent on certain especially slow operations. It is in those that you must pay attention. The rest, don’t waste time on them.

Let the compiler do its job

Long ago, compilers were not as smart as they are now and programmers did a lot of nasty tricks in the code to improve efficiency. There was no other way. But times have changed, and compilers are much smarter than they used to be.

Don’t try to “cheat” to try to make your program faster. In fact, if you try to “do the work” of the compiler, you will often find that your code ends up being slower and not faster, because you have cut certain optimizations that it could do, but since you wrote it in a weird way, it couldn’t do them.

Trust the compiler and let it do its job. Write your program in the cleanest way, and leave the machine language for the machines.

Avoid using #define

#define is a precompiler directive. It serves to tell the compiler, don’t compile this, compile something else, and generate different programs for the same code. However, in Arduino you will be very used to using #define to define constants. Don’t do it.

// nope

#define pin_sensor 5;

// yep
const int PIN_SENSOR = 5;

Differences? Well, apart from an important concept, because for you PIN_SENSOR is really a constant, the biggest difference is that with #define the compiler goes to where pin_sensor is and embeds a 5 without checking the typing, nor warning of a cast, etc.

On the other hand, if you are using a more advanced compiler than the standard IDE (like Visual Studio) the IDE will suggest the name of the variable, allow you to rename it, when you put the cursor over it, it will tell you the type it is, among many other advantages you have when you use each thing for what it is.

Leave the #define for when you really want to modify the program, for example, to compile a branch if it is an Arduino and another branch for an ESP32. For the rest, use const.

Consider if it is the right machine

At this point, if you are really making a program in which Arduino is very tight on power… maybe you should consider spending 1.5€ instead of 2.5 and buying something more powerful.

It is often said, iron (hardware) is cheap, programmers are expensive (taking into account development time, maintenance, etc). Without using it as an excuse to make slow code, the truth is that it is true.

Also, keep in mind, it is not the same when you do something in a run of 1-10 units, as when you are going to manufacture 10 thousand. There, if it is convenient to kill yourself in efficiency, because every penny counts.

And if the time is personal, because you do it at home for enjoyment, and there is no “cost”? Even more so. Don’t waste your time optimizing something to fit on a 1.5€ processor, if it would be easier on a larger one.

Not to mention, all these processors are going to evolve. In X years (it’s already happening) they will be more like “normal” programming (pc, web) than MCUs. What do you prefer to spend your time on, something that will disappear in a few years? Invest your time in programming well. If the machine limits you too much, don’t rule out changing machines.

That something works doesn’t mean it’s right

The code has to be clean, it’s not enough for it to work. If it’s not clean, it’s wrong, just like if it didn’t work. Drill this into your head. If you don’t believe it, repeat it over and over again until you convince yourself.

It’s not an excuse “I’ve been doing it this way for 30 years”, all the more reason for you to think that maybe the movie has changed a lot. The “I saw it on the Internet” or “my teacher told me” doesn’t work either. Develop your own opinion.

The excuse “I understand my super complex code because I’m very smart” doesn’t work either. In the first place… it’s not true. You spent 30 minutes trying a one-line code, when if you had written it in 3 lines it would have taken you 1 minute.

Also, if someone else has to read it, they won’t understand anything. But what’s more, when you pick it up again, you’ll wonder “what the heck does that line do”. Every time your program fails, you’ll reconsider… is this part here? And that’s not programming well.

And yes, it’s true, it works. That very long code with two-letter variables, no functions or objects, with numbers in the middle, taped together with duct tape, works. But no, it’s not right.

Programming is more than making complicated code that works. It’s structuring, abstracting, and making clean code that explains itself. Follow the advice in this post, short functions, variables with names, understandable code, not repetitive, etc.

Conclusion

We have seen some guidelines and tips for cleaner programming, and to help improve the quality of your code as a programmer. Of course, they are just advice, not laws.

We know it’s not the same when an Adafruit programmer uses a long loop in a library because they really need it, as when someone who is learning does it because they don’t know it’s better to avoid it.

It is also not the same when you make a prototype of 1 unit, or a series of 10 thousand. Nor when you make code to teach in a class, where it is logical to have more comments. Nor is it the same if you know how to make clean code and have to skip it in a section, as doing it out of ignorance.

Most of the guidelines we have seen come from the Agile world, and collect some of the main currents that have prevailed in “normal” programming (PC, web) for years. They are open to debate, but, in general, they have a high degree of consensus.

In Arduino and MCUs programming in general, these currents are more difficult to reach. Part of the reason is the user profile (very senior, or very new). Another important part is the difference in resources available in an MCU versus a computer.

However, the boundaries between both worlds are becoming increasingly blurred. And, just as it is important to program “cleanly” in “normal” development (PC, Web), it is also important for you to get used to doing it in MCUs, Arduinos, etc.

Final advice, it is advisable to see other programming languages. Learn C# or Java, learn web development. You will see how the code is structured in a high-level language, or in a framework like React, Angular or VueJs. This will change the way you program, and will improve the quality of your programs in Arduino and other MCUs.