Advertisement

Coding tips & tricks for LED ring lighting effects

October 22, 2015

Max The Magnificent-October 22, 2015

Before we start, I should remind you that I am a hardware design engineer by trade -- I am in no way a paid-up member of the software cognoscenti -- so when it comes to creating code, I'm largely reduced to making things up as I go along.

Of course, this sometimes ends up with my feeling as though I'm on the losing end of a birling competition, but I love the adrenalin rush of excitement and anticipation when I finally track a recalcitrant bug -- typically in the form of a stupid mistake on my part -- down to its lair.

The point is that, even though I do walk the path of hardware righteousness, I have picked up a few tricks from the software dark-side along the way, and I'm quite proud of some of these little rascals, so I thought I'd dare to share them here.

Now, if you are one who strides the corridors of power in the software domain, you'll probably see this article as an amphigory describing paltry parlor tricks of no account, in which case may I bid you good day (don’t let the door bang into you on the way out). We hardware guys and gals have enough on our plates wrestling the underlying systems into submission, so when it comes to subduing scoundrelly software, all we can do is do the best we can with what we've got.

The topic of this article is implementing interesting effects on LED rings -- in particular, the NeoPixel-based rascals from Adafruit that I've been using in my Vetinari Clock and Cunning Chronograph projects (see also Using WS2812-based NeoPixels in embedded systems).

In the case of the Vetinari Clock, we're working with a 16-element NeoPixel ring. For the purposes of simplicity, however, let's pretend it's an 8-element ring, and that these elements are numbered as follows (I've shown element 0 as being a darker color for future reference):


(Source: Max Maxfield/Embedded.com)

Let's assume that we've declared our ring and -- because we don’t have much imagination -- we've called it myRing. Let's also assume that we've already defined our 8-bit RGB values, which we will refer to as R, G, and B in the code (I hope this naming convention won't prove to be too confusing :-)

Now, let's suppose that we wish to light up the elements forming our ring one after the other, starting with element 0 and working out way up to element 7, with a 100 ms delay between each of the elements turning on. After this, we wish to turn them off again in the same order. The code we use to do this could be as follows:

for (i = 0; i < 8; i++) {
  myRing.setPixelColor(i,R,G,B);
  myRing.show();
  delay(100);
}

for (i = 0; i < 8; i++) {
  myRing.setPixelColor(i,0,0,0);
  myRing.show();
  delay(100);
}

The way Adafruit's NeoPixel library works is that the setPixelColor() function stores the RGB data in an array in memory (we might visualize this as a two-dimensional array of bytes called myRing[8][3], where the first dimension reflects the number of elements forming the ring and the second dimension refers to the three RGB values associated with each element). Later, we use the show() function to stream this data out of memory and into the ring. (Note that the 8 and 3 values in the array declaration result in elements numbered 0-to-7 and 0-to-2, respectively.)

OK so far, but now let's suppose that every time we light up a new element (i), we also wish to turn off the previous element (i - 1), perhaps using something like the following:

for (i = 0; i < 8; i++) {
  myRing.setPixelColor(i,R,G,B);
  myRing.setPixelColor(i-1,0,0,0);
  myRing.show();
  delay(100);
}

The idea is that if we were to call this snippet of code over and over again, we would hope to see a single element racing round and round the ring. The problem is that the code shown above won’t work (cue "sad sound" effect). The underlying theory is reasonable enough, but it's always the "end conditions" that bite you in the nether regions when you're least expecting it. In order to see what the issue is, consider the following table:


(Source: Max Maxfield/Embedded.com)

As we see, the problem occurs right at the beginning when we illuminate element 0 and attempt to turn off the previous element. We actually wish to turn off element 7 (00000111 in binary). The problem is that when i = 0, our i - 1 operation will result in -1 (11111111 in binary).

If you aren’t sure where this 11111111 value came from, but you really wish to know, then you need to perform a Google search on "Two's Complement." For the purposes of this column, however, let's simply assume that -1 in our computer is represented using an all 1s value.

Now, we could perform a test to see if we are working with bit 0, and address the situation accordingly. Consider the following snippet of code, for example:

for (i = 0; i < 8; i++) {
  myRing.setPixelColor(i,R,G,B);
  if (i == 0)
    myRing.setPixelColor(7,0,0,0);
  else
    myRing.setPixelColor(i-1,0,0,0);
  myRing.show();
  delay(100);
}

This is certainly a serviceable solution, but I regard it as being messy and aesthetically unpleasing. Fortunately, there's a cunning trick we can employ as follows:

for (i = 0; i < 8; i++) {
  myRing.setPixelColor(i,R,G,B);
  myRing.setPixelColor((i-1) & 0x7,0,0,0);
  myRing.show();
  delay(100);
}

The & is known as the bitwise AND operator. This operator performs a logical AND operation on each pair of corresponding bits in the values on either side of the operator. If either of the bits is 0, the associated output bit is 0; it's only when both bits are 1 that the output is 1. Consider what would happen if we were to perform the bitwise AND on two 8-bit variables called a and b containing binary values of 00001111 and 01010101, respectively:


(Source: Max Maxfield/Embedded.com)

If we look at the two digits in the bit 7 positions, we have 0 & 0 = 0. The two digits in bit 6 give us 0 & 1 = 0; the two digits in bit 3 give us 1 & 0 = 0; and the two digits in bit 2 give us 1 & 1 = 1.

Returning to our code snippet above, we're using the & 0x7 as a "mask" to clear the most-significant (MS) bits to 0 and to only return the values in the three LS bits (where 0x7 in hexadecimal represents 00000111 in binary).

What this means in practice is that when i = 1 through 7 (00000001 through 00000111 in binary), and i - 1 = 0 through 6 (00000000 through 00000110 in binary), then the & 0x7 has no effect whatsoever. However, when i = 0 (00000000 in binary), and i - 1 = -1 (11111111 in binary), then using the bitwise AND (& 0x7, or 00000111 in binary) results in 00000111, which equates to element 7, which is what we wanted in the first place (cue "happy music").

Our new solution also works "the other way round," as it were. In the examples above, our illuminated element has been racing round in a clockwise direction, but suppose we wished to make it rotate the other way round. Once again, we might start with the following code:

for (i = 7; i >= 0; i--) {
  myRing.setPixelColor(i,R,G,B);
  myRing.setPixelColor(i+1,0,0,0);
  myRing.show();
  delay(100);
}

And, once again, this won’t work due to a problem with the end condition as illustrated below:


(Source: Max Maxfield/Embedded.com)

Happily, the same solution we came up with to address the clockwise end condition problem also works for its anticlockwise counterpart:

for (i = 7; i >= 0; i--) {
  myRing.setPixelColor(i,R,G,B);
  myRing.setPixelColor((i+1) & 0x7,0,0,0);
  myRing.show();
  delay(100);
}

In this case, when i = 6 through 0 (00000110 through 00000000 in binary), and i + 1 = 7 through 1 (00000111 through 00000001 in binary), then the & 0x7 has no effect. However, when i = 7 (00000111 in binary), and i + 1 = 8 (00001000 in binary), then using the bitwise AND (& 0x7, or 00000111 in binary) results in 00000000, which equates to element 0, which -- once again -- is what we wanted in the first place.

This is the point when I feel like enthusiastically exclaiming "Tra-la" in strident tones; but wait, because there's more (Oh, so much more)...

To Page 2 >

< Previous
Page 1 of 3
Next >

Loading comments...