Before we begin, 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.
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):
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:
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:
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:
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)...
Using cross-reference tables
Let’s assume that we are still playing with an 8-element ring and that we've spent countless yonks developing copious amounts of capriciously cunning code. Unfortunately, something dire happens to our ring and we have to replace it. Even worse, when we come to "light up" our new ring, we discover that we've inserted it incorrectly such that it's rotated clockwise by 90° as illustrated below.
In this case, the red numbers outside the ring are the actual indices to the physical pixels, while the green numbers inside the ring reflect the indices our code is expecting to use. It probably goes without saying that this is the time when one is prone to murmur something like "Oh dear, I wish I'd listened to my dear old mother and refrained from supergluing this little scamp into the system" (or words to that effect).
Of course, this won't really matter if all we want to do is have one element chasing itself around and around the ring ad infinitum, but it's going to be a real pain if we wish to control specific elements and showcase sophisticated sequences (I tell you, the words are just tripping and trilling off my tongue today). One solution is to create a cross-reference table (array) as illustrated below:
int xref[8] = {6,7,0,1,2,3,4,5};
Now, let's take our original "single element chasing itself clockwise around the ring" code snippet as an example:
for (i = 0; i < 8; i++) { myRing.setPixelColor(i,R,G,B); myRing.setPixelColor((i-1) & 0x7,0,0,0); myRing.show(); delay(100); }
All we have to do is to modify any references to i
in our setPixelColor()
function call such that they are redirected by our cross-reference array as illustrated below:
for (i = 0; i < 8; i++) { myRing.setPixelColor(xref,R,G,B); myRing.setPixelColor((xref[i-1]) & 0x7,0,0,0); myRing.show(); delay(100); }
Well, I don’t know about you, but I think this is pretty nifty!
Handling rings that don’t have 2^n elements
The reason the & 0x7
solution worked in our previous examples is that our rings had eight elements, and eight is a power of two (2^3 = 8). Similar solutions can be employed for any other power of two; for example, 2^4 = 16, so & 0xF
(00001111 in binary) would work with a 16-element ring.
Unfortunately, our solution will not work with rings that don’t have 2^n elements. Consider a ring with only six elements, as shown below, for example:
In this case, the only legal values we have to address our elements are 0 through 5 (00000000 through 00000101 in binary). In the case of a clockwise rotation, when we are lighting element i = 0
, our & 0x7
solution to extinguish element i -1
would return 7 (00000111), not the 5 (00000101) we desire.
Once again, our cross-reference table (array) comes to the rescue. In this case, we could use the following array to support clockwise rotations:
int xref[7] = {5,0,1,2,3,4,5};
Observe that we've created a 7-element array, even though our ring boasts only 6 elements. This is because we've added an extra entry at the beginning of the array to handle the i - 1
case for when we are illuminating element 0 and extinguishing element 5. In this case, our code snippet would look like the following:
for (i = 1; i < 7; i++) { myRing.setPixelColor(xref,R,G,B); myRing.setPixelColor(xref[i-1],0,0,0); myRing.show(); delay(100); }
Note especially that we now use i = 1; i < 7;
in our for
loop, as opposed to the i = 0; i < 6;
we might have used if we hadn’t added the extra element into the beginning of the array.
Similarly, if we wish to rotate the illuminated element in an anticlockwise fashion, we would add one more element onto the end of our cross reference array as follows:
int xref[8] = {5,0,1,2,3,4,5,0};
Well, things are looking pretty sweet so far, but I'm about to drop a wrench into the works. The above solutions are great if all we wish to do is light up one pixel and extinguish its adjacent companion, but what happens if we wish to do more?
Let's suppose, for example, that we are performing a clockwise rotation on a ring containing eight elements. Furthermore, let's suppose that, when we light up element i
with RGB values of 255, we also wish to assign element i - 1
RGB values of 127, element i - 2
RGB values of 63, element i - 3
RGB values of 31, and that we wish to extinguish element i - 4
by assigning it RGB values of 0.
The overall effect we're trying to achieve is something like a vapor trail fading away. We could, of course, extend our cross-reference array to look like the following:
int xref[12] = {4,5,6,7,0,1,2,3,4,5,6,7};
And then we could change our corresponding code snippet to look like the following:
for (i = 4; i < 12; i++) { myRing.setPixelColor(xref, 255,255,255); myRing.setPixelColor(xref[i-1],127,127,127); myRing.setPixelColor(xref[i-2], 63, 63, 63); myRing.setPixelColor(xref[i-3], 31, 31, 31); myRing.setPixelColor(xref[i-4], 0, 0, 0); myRing.show(); delay(100); }
I think you'll agree, however, that things are starting to get a little frayed around the edges. Can you think of a solution to this conundrum? What we're looking for is an approach that will let us work with arbitrary numbers of arbitrarily-sized rings (i.e., varying number of elements), and that will facilitate our implementing effects such as having arbitrarily-long "vapor trails," for example.
By some strange quirk of fate, I happen to have just such a solution. Let's take a look, shall we...
A plan so cunning...
What we are talking about here is a cunning plan. Indeed, "A plan so cunning we could pin a tail on it and call it a weasel," as Black Adder might say. Now, a real programmer would probably have come up with something like this solution right from the get-go, and a real programmer would probably have decided to use pointers.
Let's start by assuming we are working with a single ring and that this ring has six elements numbered 0 through 5. Based on this, our hypothetical programmer might have defined a structure containing "previous" and "next" pointers, along with any other desired data fields. Our software guru might subsequently have instantiated a bunch of these structures and connected them together as illustrated below:
Using this construct, it would be possible to implement all sorts of engaging effects relatively easily. Sad to relate, however, there are several problems with this approach, not the least being the fact that I no longer remember how to use pointers. (I used to be a diva with these little rascals 30+ years ago, but a lot of Pooh Sticks have flowed down the river of time since those halcyon days.)
Another issue is memory utilization. I'm not sure how big a pointer is in an Arduino Uno -- these things are system dependent -- but it's got to be at least two bytes. This isn’t really an issue if you are playing with a single ring containing only six elements, but things start to mount up when you are working with something like my Cunning Chronograph.
For all these reasons, my solution is to implement something like the structure shown above, but using arrays of bytes instead of pointers. (The reason for using 8-bit bytes as opposed to the Arduino Uno's 16-bit integers is that it saves memory. Each byte can happily represent values from 0 to 255, and I have only 96 elements in my Cunning Chronograph -- a 60-pixel ring, a 24-pixel ring, and a 12-pixel ring.)
To be continued in Part 2
文章评论(0条评论)
登录后参与讨论