Working on projects with an Arduino – or with other microcontrollers – can be a lot of fun. It can also be a frustrating experience; you write some code and then you need to wait for it to be compiled, packaged up, downloaded to your microcontroller, and then run. And when it doesn’t work, it can be difficult to figure out why it isn’t working; generally, the best you can do look at the output from Serial.println() or look at the signals on an oscilloscope, if you own one.
As a (now former) professional developer, I was sure that there was a better way, and after some experimentation I came up with the method described in these posts.
Basically, the approach is pretty simple. We are going to structure our software so that there are two discrete parts; a first part that is directly dependent on the microprocessor and libraries and a second part that is generic. The second part will contain the bulk of the code that we will be writing.
And then, we are going to use a generic C++ environment to run the code from the second part and verify that it works. Once we have it working in that environment, we can then move over to the arduino environment and test it on the real hardware.
In addition to making it easier to write and verify code, this will also break our code into parts and make it simpler to understand.
Tools
We will be using two different development environments. For our Arduino code, we need an Arduino environment – what I would call an “IDE”. I’m going to be using Visual Studio Code with the Platform IO package, but you can use the Arduino IDE if you’d rather. Both of those are free.
We will also need a desktop/laptop environment that can run C++ code. There are several good options here; I’m going to stay true to my roots and use Visual Studio Community with C++ support installed (also free) for that development, but you can use whatever environment you would like.
The project
A quick look at my blog will indicate that I am quite devoted to LEDs, so that’s what we’re going to build. Specifically a Larson Scanner:
Or at least something like that; I’m not sure we’re going to get to the dimming trail part. If you want to follow along, you’ll need a microcontroller that can run the FastLed library (I’m going to use an ESP8266 board), and a strip of WS2812/Neopixel LEDs. There’s a nice intro to using FastLED in their wiki here, and I suggest getting something working in your environment using their directions before trying to follow along.
I’ll add a note here that if you are using the ESP8266, Makuna’s NeoPixelBus is a better choice than FastLed as it has hardware support, but FastLed is more popular so that’s why I’m using it here.
First Version
All of the code lives in the LarsonScanner repository. I will try to keep my commits small and informative so you can see what the changes are.
I started by writing a quick and minimal version of the code. It looks like this:
#include <Arduino.h>
#define FASTLED_ESP8266_NODEMCU_PIN_ORDER
#include <fastled.h>
#define NUM_LEDS 15
#define DATA_PIN 3
CRGB leds[NUM_LEDS];
void setup() {
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
}
int last = 0;
int current = 0;
int increment = 1;
void loop() {
leds[last] = CRGB::Black;
leds[current] = CRGB::Red;
FastLED.show();
last = current;
current = current + increment;
if (current == 0 || current == NUM_LEDS – 1)
{
increment = -increment;
}
delay(30);
}
Most of this is boilerplate FastLED code. The code itself is simple; it has three variables:
- current defines the next LED that we need to turn on
- last defines the LED that is currently on that we will need to turn off
- increment defines the direction we are moving with the animation
The loop code sets the previous LED off and turns the new one on, and then increments to move to the next LED. If that puts us at the ends of the strip – either LED 0 or LED NUM_LEDS-1 – then that tells us we need to start going the other direction and we negate increment to do that.
Running this code on the desktop
Our goal is to be able to run the code that we wrote – the code to do the animation – on the desktop. But we can’t do this because that code calls the FastLED library, and there’s no FastLED library on the desktop. What we will need to do is provide a way for our animation code to *use* the FastLED code indirectly rather than referring to it directly.
In software development terms, we’re doing what is called “encapsulation”; taking all of the code related to a specific operation and separating it from the rest of our code.
We will take all the FastLED code and move it into a separate class. It looks like this:
#define FASTLED_ESP8266_NODEMCU_PIN_ORDER
#include <fastled.h>
#define NUM_LEDS 15
#define DATA_PIN 3
class LedStrip
{
CRGB _leds[NUM_LEDS];
public:
void setup()
{
FastLED.addLeds<NEOPIXEL, DATA_PIN>(_leds, NUM_LEDS);
}
void setColor(int ledIndex, int red, int green, int blue)
{
_leds[ledIndex] = CRGB(red, green, blue);
}
void show()
{
FastLED.show();
}
};
This new class now keeps track of the details of dealing with FastLED. Note that the “leds” array has been renamed “_leds”; that is a naming convention to make it easier to know that it belongs to this class.
Our main code now looks like this:
#include <Arduino.h>
#include <LedStrip.h>
LedStrip ledStrip;
void setup() {
ledStrip.setup();
}
int last = 0;
int current = 0;
int increment = 1;
void loop() {
ledStrip.setColor(last, 0, 0, 0);
ledStrip.setColor(current, 255, 0, 0);
ledStrip.show();
last = current;
current = current + increment;
if (current == 0 || current == NUM_LEDS – 1)
{
increment = -increment;
}
delay(30);
}
Better. There are no FastLED details in here, but there are still arduino details that would get in the way of using it from the desktop. Just as we took all the FastLED details and put them in a class, we will now put all of the animation details into a separate class. It looks like this:
#define NUM_LEDS 15
class Animater
{
int _last = 0;
int _current = 0;
int _increment = 1;
public:
void doAnimationStep(LedStrip &ledStrip)
{
ledStrip.setColor(_last, 0, 0, 0);
ledStrip.setColor(_current, 255, 0, 0);
ledStrip.show();
_last = _current;
_current = _current + _increment;
if (_current == 0 || _current == NUM_LEDS – 1)
{
_increment = -_increment;
}
}
};
That puts all the code in a method named doAnimationStep; we pass in an LedStrip, and it does whatever it needs to do.
Our main program code gets even simpler:
#include <Arduino.h>
#include <LedStrip.h>
#include <Animater.h>
LedStrip ledStrip;
Animater animater;
void setup() {
ledStrip.setup();
}
void loop() {
animater.doAnimationStep(ledStrip);
delay(30);
}
This demonstrates quite well why encapsulation is a good thing; instead of having one main program with different things going on, we have three sections of code; the code that only deals with FastLED operations, the code that deals with the animation, and then a very simple bit of code that hooks them together. If your arduino code is getting complicated and hard to understand, using encapsulation will help immensely.
The desktop version
We are now ready to run our animation code. I’ve created a Visual Studio C++ project named “ConsoleTest” next to the arduino project. My goal is to run the code in Animater.h in this environment, and to do that, I’m going to need a different implementation of LedStrip.h. Here’s what I create in the ConsoleTest project:
class LedStrip
{
public:
void setColor(int ledNumber, int red, int green, int blue)
{
printf(“LED %d: (%d, %d, %d) \n”, ledNumber, red, green, blue);
}
void show()
{
printf(“Show: \n”);
}
};
Instead of talking to an LEDStrip, it just writes out the information it is called with to the console.
The ConsoleTest.cpp file in this project looks like this:
#include “stdafx.h”
#include “LedStrip.h”
#include “..\Arduino\Larson\src\Animater.h”
int main()
{
LedStrip ledStrip;
Animater animater;
for (int i = 0; i < 30; i++)
{
animater.doAnimationStep(ledStrip);
}
return 0;
}
It includes the printing version of LedStrip in the test project, but it then includes Animater.h from the arduino project. The main() function then calls the animation code the same way the arduino code would call it. It generates the following output:
LED 0: (0, 0, 0)
LED 0: (255, 0, 0)
Show:
LED 0: (0, 0, 0)
LED 1: (255, 0, 0)
Show:
LED 1: (0, 0, 0)
LED 2: (255, 0, 0)
Show:
LED 2: (0, 0, 0)
LED 3: (255, 0, 0)
Show:
LED 3: (0, 0, 0)
LED 4: (255, 0, 0)
Show:
LED 4: (0, 0, 0)
LED 5: (255, 0, 0)
Show:
LED 5: (0, 0, 0)
LED 6: (255, 0, 0)
Show:
LED 6: (0, 0, 0)
LED 7: (255, 0, 0)
Show:
LED 7: (0, 0, 0)
LED 8: (255, 0, 0)
Show:
LED 8: (0, 0, 0)
LED 9: (255, 0, 0)
Show:
…
We are now able to see the calls the animation code would make to the FastLED library when it runs on the arduino and see if it is behaving as expected.
Digression for experienced developers
If you aren’t an experienced developer, you can safely ignore this section.
This technique – which I call “abstraction by include file” – likely looks a little weird. The “right” way to do this in C++ is to define a pure abstract class named ILedStrip with pure virtual functions that are then overwridden by LedStrip in the arduino code and by a LedStripTest class in the console project.
I’ve implemented this technique both my way and the “right” way, and I’ve found that the right way requires an extra interface definition and doesn’t really help the resulting code. And it requires understanding virtual methods. But that’s an aesthetic choice; feel free to make the opposite choice.
Modifying our animation…
Let’s say that we now want our animation to change colors each time it switches direction. Can we write that code and test it without downloading it to the Arduino?
Here’s my crappy implementation:
#define NUM_LEDS 15
class Animater
{
int _last = 0;
int _current = 0;
int _increment = 1;
int _color = 0;
public:
void doAnimationStep(LedStrip &ledStrip)
{
ledStrip.setColor(_last, 0, 0, 0);
if (_color == 0)
{
ledStrip.setColor(_current, 255, 0, 0);
}
else if (_color == 1)
{
ledStrip.setColor(_current, 0, 255, 0);
}
else
{
ledStrip.setColor(_current, 0, 0, 255);
}
ledStrip.show();
_last = _current;
_current = _current + _increment;
if (_current == 0 || _current == NUM_LEDS – 1)
{
_increment = -_increment;
_color = _color + 1;
if (_color == 3)
{
_color = 0;
}
}
}
};
Basically, it has a color variable that increments each time we switch directions, and we check that variable to decide what color to set.
By examining the output, we can see if the program is doing what we expect. Or we can use the debugger that is built into Visual Studio Community to have the program stop at any line in our code so that we can see what the values of variables are and what code is being executed. That is much much easier than trying to figure out what is going on in code running on the arduino. There’s a nice introduction to using the debugger here.
Tracking state
To verify an animation, we have to read a lot of output and keep track of which LEDs are which colors. We can make that a little easier by modifying our test LedStrip class so that it keeps track for us. First, we’ll need class that can hold the state of one LED:
class LedColor
{
public:
int Red;
int Green;
int Blue;
LedColor() : LedColor(0, 0, 0)
{
}
LedColor(int red, int green, int blue)
{
Red = red;
Green = green;
Blue = blue;
}
};
And then we can use that class in our LedStrip class:
class LedStrip
{
LedColor _colors[15];
public:
void setColor(int ledNumber, int red, int green, int blue)
{
_colors[ledNumber] = LedColor(red, green, blue);
}
void show()
{
printf(“Show: “);
for (int i = 0; i < 15; i++)
{
printf(“(%d,%d,%d)”, _colors[i].Red, _colors[i].Green, _colors[i].Blue);
}
puts(“”);
}
};
This generates the following output:
Show: (255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)
Show: (0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(0,0,0)(255,0,0)(0,0,0)(0,0,0)(0,0,0)
I find this approach to be a bit more visual and easier to understand.
That’s a good place to stop for this post. The next post will explore automated testing using this approach.