Organizing Large Arduino Code Bases

Eli Fatsi, Former Development Director

Article Category: #Code

Posted on

So you went ahead and bought yourself an Arduino UNO, some sensors, a few buttons, and you're having a blast learning about all the things you can make. But let's say you're ready for the next level. This guide will explain some of the concepts you need to be familiar with in order to write larger, more complicated code that you can come back to in 6 months without raising two clenched fists and cursing your former self.

Introduction

We're going to begin with a simple "blink" application. If you've held an Arduino, you've probably written this already. It's the "Hello World" of microcontrollers:

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

void loop() {
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
}

We'll slowly improve on this to demonstrate some helpful concepts. Here's the run down of topics that we'll cover:

Let's get to it!

IDE

The first thing you're going to want to do is get yourself out of the Arduino IDE. The Arduino editor is great for getting started, but lacks many features which make writing code quicker, easier, and have become quite standard in modern text editors. At a minimum, you can check the "User external editor" setting in the Arduino Preferences and modify your code elsewhere while using the IDE to Build/Upload code. However, I would suggest getting comfortable in a separate coding environment altogether, one that's a little more featured.

I personally use the Atom editor and make use of the PlatformIO library, you can reference this post for getting set up there. Eclipse, codebender, and even Gedit are a few other IDE alternatives. You can find a detailed list here. Take the time to get a working environment set up, it is oh so worth it.

Now that we have our housekeeping out of the way, let's get into the code!

Variables

Just a quick note here: use them. For configuration, for arbitrary numbers, for anything you might want to change ever.

int ledPin = 13;
int ledDelay = 1000;

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

void loop() {
  digitalWrite(ledPin, HIGH);
  delay(ledDelay);
  digitalWrite(ledPin, LOW);
  delay(ledDelay);
}

This isn't Arduino specific, it's a good coding practice in general. Despite the overhead it adds, it provides many benefits. In this simple app of ours, if we needed to change the LED pin or the speed of the blink, we only need to change the variable instead of hunting down all the implementation instances.

Timers

Using timers instead of delays is a necessary building block for writing more complex code. Unfortunately, our Blink example is simple enough that delays work just fine. But let's add one more feature to demonstrate the value and necessity of timers.

Add a button to the application. The LED should be ON if the button is pressed, and BLINKING if not pressed.

With the delay() calls we have now, we can't take a reading of the button while we're waiting for the light to switch states. With a timer, we can.

int buttonPin = 9;
int ledPin = 13;
int ledDelay = 1000;
bool ledBlinkState = HIGH;
long ledTimer;

void setup() {
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
  
  ledTimer = millis();
}

void loop() {
  if (millis() - ledTimer > ledDelay) {
    ledTimer = millis();
    
    ledBlinkState = !ledBlinkState;
    digitalWrite(ledPin, ledBlinkState);
  }
  
  if (digitalRead(buttonPin) == HIGH) {
    digitalWrite(ledPin, HIGH);
  }
}

It takes a little more brain cycles to understand what's going on here. millis() returns the number of milliseconds that have passed since the program started, so we use it against an ledTimer to determine when 1 second has passed. Since we're never pausing code execution, our light is blinking and we're simultaneously checking for button presses.

You might have noticed that there was a small bug in the last bit of code. There's nothing in place to turn the LED off if you release the button. It will eventually turn off when the ledTimer comes around, but let's mock up the quick fix. We'll change the last if statement to check with the ledBlinkState before checking the button's situation:

void loop() {
  if (millis() - ledTimer > ledDelay) {
    ledTimer = millis();
    
    ledBlinkState = !ledBlinkState;
    digitalWrite(ledPin, ledBlinkState);
  }
  
  if (ledBlinkState == LOW) {
    if (digitalRead(buttonPin) == HIGH) {
      digitalWrite(ledPin, HIGH);
    } else {
      digitalWrite(ledPin, LOW);
    }
  }
}

Separating State and Display

Unfortunately, our code is now becoming a bit tougher to reason about. To understand what the code is doing, you need a full understanding of every line and how it behaves in the different states. This was no problem when our loop was 4 lines long, but even at this scale, some useful abstractions can make this much easier to reason about. Let's start this refactoring by simplifying our loop function:

void loop() {
  determineState();
  display();
}

Keeping state management isolated from display logic is a powerful separation that makes understanding and updating your code easier in the long run. With this separation, you don't have to hold the entire application flow in your head when trying to understand it, making it easier to track down bugs or make changes. Let's take a look at what these sub functions would look like for this application.

void loop() {
  determineState();
  display();
}

void determineState() {
  checkBlinkTimer();
  checkButtonPress();
  determineLedState();
}

void checkBlinkTimer() {
  if (millis() - ledTimer > ledDelay) {
    ledTimer = millis();
    ledBlinkState = !ledBlinkState;
  }
}

void checkButtonPress() {
  buttonPressed = (digitalRead(buttonPin) == HIGH);
}

void determineLedState() {
  if (ledBlinkState || buttonPressed) {
    ledState = HIGH;
  } else {
    ledState = LOW;
  }
}

void display() {
  digitalWrite(ledPin, ledState);
}

Ahhh, nice and clean! This code has a couple things going for it:

  • Small functions
  • Descriptively named functions/variables
  • State maintained across a few variables
  • Very simple function managing display logic

Changes to this code are now much easier to implement. If we want to invert the light behavior, or add another button, or make multiple lights turn on, we only need to change the code in one obvious place.

Creating Your Own Libraries

Organizing your code more deeply into custom libraries is another great tool for keeping your code clean and maintainable. Particularly if you have some code that is used in multiple places, or if you just want to isolate functionality into testable units.

I won't go over the ins and outs of creating a library yourself as it is a lot of information to add to this already lengthy post, but there is a great tutorial on Arduino's website for getting started: https://www.arduino.cc/en/Hacking/LibraryTutorial.

Sharpening C/C++ Skills

Given that the Arduino language is based on C/C++, improving your skills there will have a direct impact on your ability to write Arduino code. Here are some good references if you're interested:

Conclusion

Coding on the Arduino platform can be an absolute blast. There's so much potential and the barrier to entry is getting lower and lower as the years go on. With great power comes great responsibility however, and keeping your code clean and maintainable is indeed a great responsibility.

I hope you've found some of these tips useful. Happy hacking!

Related Articles