Non-blocking lighting effects in Arduino

Arduino tinkering

There's been some occasions when I needed to perform few operations on an Arduino/Genuino board, but these had to happen along each other. This platform does not support parallel execution, and there isn't any built-in higher level tools to support asynchronous execution, but you can emulate asynchronous execution by changing the composition of your code.

In this particular case my requirements for the product were:

  1. The lighting is to be following some predefined, sometimes elaborate patterns of intensities depending on elapsed time
  2. The product would react to user interactions via a number of buttons and a potentiometer

Some of the problems requiring non-blocking processing can be tackled with interrupts but the number of pins providing this feature is limited on the traditional boards (Uno, Nano, Mega, Micro and others). For a potentiometer some additional circuitry would be needed to identify user's input. Interrupts wouldn't (without making it a really awkward solution) help to control the output simultaneously.

I however opted-in for a solution where the task is broken down into many short steps (as a function of time) and that allows to perform other, but also short, activities in the meantime.

Here's the code:

 1const int LED = 5;
 2
 3long startedAt;
 4
 5void setup() {
 6  pinMode(LED, OUTPUT);
 7}
 8
 9const int lightingPatternOnOffSmooth[][2] = {
10  {0, 0},
11  {2000, 255},
12  {4000, 0},
13};
14
15const int lightingPatternOnOffRapid[][2] = {
16  {0, 0},
17  {1999, 0},
18  {2000, 255},
19  {3999, 255},
20  {4000, 0},
21};
22
23int intensityAsAFunctionOfTime(const int f[][2], int sizeOfArray, const long time) {
24  int noOfRefPts = sizeOfArray / sizeof(f[0]);
25  int period = f[noOfRefPts - 1][0];
26
27  long t = time % period;
28  int indexBefore = -1;
29  int indexAfter = -1; 
30  long totalTime = 0;
31  int nextPt[2];
32
33  for (int refPt = noOfRefPts - 1; refPt >= 0 ; refPt--) {
34    if (t >= f[refPt][0]) {
35      if (refPt == noOfRefPts - 1) {
36        nextPt[0] = f[0][0] + period;
37        nextPt[1] = f[0][1];
38      } else {
39        nextPt[0] = f[refPt+1][0];
40        nextPt[1] = f[refPt+1][1];
41      }
42      long timeDiff = nextPt[0] - f[refPt][0];
43      float timeFraction = (t - f[refPt][0]) / float(timeDiff);
44      int intensityDiff = nextPt[1] - f[refPt][1];
45      int intensityFraction = intensityDiff * timeFraction;
46      return f[refPt][1] + intensityFraction;
47    }
48  }
49  return 0;
50}
51
52void loop() {
53
54  int pwm = intensityAsAFunctionOfTime(
55    lightingPatternOnOffRapid, 
56    sizeof(lightingPatternOnOffRapid), 
57    millis());
58
59  analogWrite(LED, pwm);
60
61  // capture user's input or handle other stuff
62
63}

The above listing contains two lighting patterns:

 1const int lightingPatternOnOffSmooth[][2] = {
 2  {0, 0},
 3  {2000, 255},
 4  {4000, 0},
 5};
 6
 7const int lightingPatternOnOffRapid[][2] = {
 8  {0, 0},
 9  {1999, 0},
10  {2000, 255},
11  {3999, 255},
12  {4000, 0},
13};

These two-dimensional arrays define two simple lighting patterns. Each provides characteristic function values at their times. The first index of nested array denotes a point in time, the second is the 8 bit value that will be written to an output pin (make sure to use PWM pin). The pattern will loop (jump back to first value when last one was processed). The function intensityAsAFunctionOfTime() will smooth out the values as it hits the times in-between defined points. The above two patterns demonstrate how to achieve smooth or rapid transition.

If you wonder why I don't perform sizeof within the intensityAsAFunctionOfTime() function itself it is because the array variable decays into a pointer as I am passing it into the function, which looses the information about its size.

And here you have more fancy patterns:

 1const int lightingPatternHeartbeat[][2] = {
 2  {0, 0},
 3  {50, 80},
 4  {250, 0},
 5  {300, 0},
 6  {350, 100},
 7  {500, 0},
 8  {1500, 0}
 9};
10
11const int lightingPatternGlow[][2] = {
12  {0, 5},
13  {1000, 50},
14  {1500, 5},
15  {1750, 10},
16  {2000, 5},
17  {2500, 50},
18  {3000, 5},
19};