This is only a preview of the October 2020 issue of Practical Electronics. You can view 0 of the 72 pages in the full issue. Articles in this series:
|
Max’s Cool Beans
By Max the Magnificent
Flashing LEDs and drooling engineers – Part 8
Beauty and the Beast – but which is which?
I
don’t know about you, but I can barely keep up
with everything that’s happening. There’s so much fun
stuff to do, but never enough time to do it. At the moment,
for example, I’m happily experimenting with my 12 × 12
ping-pong ball array, where each ball contains a tricolor
LED called a NeoPixel.
In my previous column (PE, September 2020), we pretended that the array was lying flat on the floor and that
occasional drips of virtual water were randomly falling
from the sky. Whenever a virtual drip landed on one of our
pixels (ping-pong balls), that pixel lit up bright white for a
short period of time.
Next, we decided to represent our drips using random colours selected from a palette comprising three primary colours (red, green and blue), three secondary colours (yellow,
cyan and magenta) and six tertiary colours (flush orange,
chartreuse, spring green, azure, electric indigo and rose).
The thing is, simply turning our pixels hard on and hard
off is not very subtle. In order to add a soupçon of sophistication, we decided to fade the colour up, hold it steady,
and then fade it back down again, so that’s what we are
going to do this month.
Keep it simple
In a moment, we’re going to create a small suite of functions that will perform the fading effect for us. One trick to
writing functions is to make them as simple and as generalpurpose as possible. As part of this, it can be advantageous
to split parts of our algorithm out into sub-functions that
can be reused by other functions in the future.
As usual, it may be advantageous for you to download a copy
of this sketch in order to follow along with my meandering musings (just mosey over to the October 2020 page of the PE web50
site and download
CB-Oct20-01.txt).
32 bits
8 bits
The only sizes
31 24 23 16 15 8 7
0
of fixed-width unsigned integers we
have available to
> > 8
Discard
us are 8, 16, 32 and
> > 16
64-bits wide. Our
red, green and blue
Fig.1. Deconstructing a 32-bit colour value
colour channels are
into three 8-bit fields.
each 8-bits wide. As
we discussed earlier, we are predominantly representing
our colours as single hexadecimal values. For example, we
define the COLOR_WHITE as 0xFFFFFFU (remember that
adding a ‘U’ or ‘u’ character on the end of a value directs
the compiler to regard it as being unsigned).
Did you spot the fact that FFFFFF is only 24 bits wide?
Since the smallest unsigned integer we can use to hold
this value is 32-bits wide, this means we are leaving the
most-signifi cant eight bits unused. Apart from anything
else, we should really have specified our COLOR_WHITE as
0x00FFFFFFU. The reason we didn’t do so is that we know
the compiler will add any required leading zeros when it
performs its magic.
Winkle-picking colour channels
Keeping all of this in mind, let’s start with three low-level
functions called GetRed(), GetGreen(), and GetBlue().
In each case, we are going to pass in a 32-bit colour value
and the function will return the appropriate 8-bit colour
to us (Fig.1).
Let’s start with GetBlue(). We pass this a 32-bit value
called tmpColor. We then use a bitwise & (AND) operator
to mask out the least-significant eight bits and return the
result, which we cast as an 8-bit value (the topic of casting
is discussed in this month’s Tips and Tricks column at the
end of this article).
uint8_t GetBlue (uint32_t tmpColor)
{
return (uint8_t) (tmpColor & 0xFFU);
}
There are a couple of things to note here. Let’s start with the
fact that using (tmpColor & 0xFFU) to perform the mask
operation is the same as saying (tmpColor & 0x000000FFU)
because, once again, the compiler will insert the leading
zeros for us.
More importantly, we don’t actually need to perform the
mask operation in the first place because the most-significant 24 bits of our 32-bit value will be discarded when we
perform the cast. Following on from the previous point,
believe it or not, we actually don’t even need to use the
(uint8_t) to cast the result as an 8-bit value, because
we already declared the GetBlue() function as returning
Practical Electronics | October | 2020
a uint8_t value, which means the compiler will automatically perform this cast for us. As a result of all this, we
could, if we wished, write our function as follows:
uint8_t GetBlue (uint32_t tmpColor)
{
return tmpColor;
}
32-bit unsigned integer and we shift the result eight bits to
the left. When it comes to our 8-bit blue channel, all we need
do is cast it to be a 32-bit value. Finally, we use | (bitwise
OR) operators to merge these three values together to form
a single 32-bit colour, which we return to whatever called
this function in the first place.
Watch me fade!
The advantage of including both the mask and the cast is that
it makes our intent clear to anyone who has to understand
and maintain this program in the future (remembering that
this could well be us). Furthermore, explicitly doing this sort
of thing throughout our code can help prevent hard-to-detect
bugs from creeping in while we aren’t looking.
And, just in case you were wondering (or worrying) about
any overhead imposed by these instructions, you may rest
assured that the compiler’s optimisation knowhow should
enable it to recognise any operations that are superfluous to
requirements and remove them (if it fails to do so, you can
take any additional execution time out of my vacation).
Extracting the blue channel was the low-hanging fruit, because it was already where we needed it to be with regard
to its position in our 32-bit colour value. In the case of the
green channel and our GetGreen() function, we’re going
to have to shift our 32-bit value eight bits to the right before
performing the mask operation.
For reasons that will become apparent in a future column,
we’re going to implement a ‘master clock’ with a duration of
10ms (milliseconds). This is defined by TICK in our sketch.
Purely for the sake of discussion, let’s suppose we decide to
take 100ms to perform our fade. Using our 10ms master clock,
this means we’re going to require 100/10 = 10 steps
When it comes to actually performing the fade, the next
function we require is one that can calculate a new colour
value that’s formed as a specified proportion of two different colours. Also, just for giggles and grins, we will need to
perform this calculation on each of the red, green and blue
channels independently.
If you look at our sketch, you’ll see a function called
CrossFadeColor(). When we call this function, we pass
in four arguments, the:
32-bit startColor
32-bit endColor
Total number of steps in this fade: numSteps
Step we’re currently on currentStep.
uint8_t GetGreen (uint32_t tmpColor)
{
return (uint8_t) ( (tmpColor >> 8) & 0xFFU );
}
Let’s look at the statement we use to process the red channel
as follows (observe that this statement calls our GetRed()
function two times to extract the red channels from startColor
and endColor):
Similarly, in the case of the red channel and our GetRed()
function, we’re going to have to shift our 32-bit value 16 bits
to the right before performing the mask operation.
tmpRed = ((GetRed(startColor) * (numSteps currentStep)) + (GetRed(endColor) *
currentStep)) / numSteps;
uint8_t GetRed (uint32_t tmpColor)
{
return (uint8_t) ( (tmpColor >> 16) & 0xFFU );
}
Now, this can be a little tricky to wrap one’s brain around.
The best way for you to comprehend the nuances of all this
will be to draw a table that contains columns for each of the
possible currentStep values, which would be 0 to numStep
(ie, 0 to 10). Your table should also contain four rows; one
each for the:
Results of the startColor calculations
Results of the endColor calculations
Results of adding these two values
Ultimate results following the final division operations.
As an aside, using the & (bitwise AND), | (bitwise OR), and
^ (bitwise XOR) operands is an interesting topic in its own
right, especially when it comes to performing masking operations. Sad to relate, we don’t have the time to delve into
this here, but for those who are excited to learn more, I discuss this in excruciating detail on my Cool Beans website –
see: https://bit.ly/3itQGCa
Regenerating colours
The counterpoint to extracting the red, green and blue channels from a 32-bit colour value is to take a triad, trio, or
troika of 8-bit colour channels and use them to construct a
32-bit colour value (Fig.2). We perform this magic using a
BuildColor() function:
uint32_t BuildColor (uint8_t red, uint8_t green,
uint8_t blue)
{
return ( (((uint32_t) red) << 16) | (((uint32_t)
green) << 8) | ((uint32_t) blue) );
}
A little thought shows that we’re doing things in reverse to
our GetColour() functions. First, we cast our 8-bit red channel to be a 32-bit unsigned integer and we shift the result 16
bits to the left. Next, we cast our 8-bit green channel to be a
Practical Electronics | October | 2020
I suggest you begin by assuming that the red component of
startColor has a value of 0 (fully off), while the red component
of endColor has a value of 255 (fully on). Next, create a second
table using a startColor value of 255 and an endColor value
of 0. Finally, do the whole thing again with both values set
to 255, which could easily happen if the main colours were
magenta (red and blue) and yellow (red and green).
Do you recall earlier on when I said, ‘One trick to writing
functions is to make them as simple as possible’? I don’t know
about you, but I
think the calculation
8 bits
32 bits
we just introduced
31 24 23 16 15 8 7
0
is rather cunning.
If you look at our
< < 16
CrossFadeColor()
< < 8
function, you’ll see
< < 0
that it contains only
four lines of code
that are doing the Fig.2. Using three 8-bit fields to construct
‘heavy lifting.’ Three a 32-bit value.
51
Time
10ms 30ms 50ms 70ms 90ms
0ms
20ms 40ms 60ms 80ms 100ms
S teps
S tart C o l o r
1
2
3
4
5
6
7
8
9 10
E nd C o l o r
Computations
U pload NeoPix els
Pad Delay
Your best bet since MAPLIN
Chock-a-Block with Stock
Visit: www.cricklewoodelectronics.com
O r phone our f riendly know ledgeable staf f on 02 0 8 4 5 2 01 6 1
Fig.3. Graphical representation of 100ms fade with 10ms
master clock.
Components • Audio • Video • Connectors • Cables
Arduino • Test Equipment etc, etc
of these lines calculate the new red, green, and blue channel
values, while the fourth calls our BuildColor() function to
merge everything back together again.
All that is required now is one more function we’ll call
CrossFade(), which we’ll use to orchestrate our fade effect. If
you look at the sketch, you’ll see that the first thing we do in this
function is to calculate the number of fade steps (numFadeSteps)
based on the required fade duration and the value of our master
clock tick. The next thing we do is to perform a loop that calls
our CrossFadeColor() function as follows:
for (int iStep = 1; iStep <= numFadeSteps; iStep++)
{
fadeColor = CrossFadeColor(startColor,
endColor, numFadeSteps, iStep);
// More stuff here
}
Before we proceed, let’s pause to ponder the fact that there
are four main options for the range of values we could assign
to iStep to control our loop:
0 to < numFadeSteps
0 to <= numFadeSteps
1 to < numFadeSteps
1 to <= numFadeSteps.
So, why did we choose the latter option? Well, when it comes
to this sort of loop construct, the initial value and terminating
condition will depend on our algorithm and what we’re trying
to do with it. There’s a common issue in computer programming that’s called the ‘off-by-one error’ or ‘off-by-one bug.’
This is also commonly known as a ‘fence post error’ based on
the so-called ‘fence post problem.’ For example, assuming we
have fence posts spaced 10 feet apart, how many posts will be
required to support a 100-foot length of fence? The answer is
11 – there is always one more fence post than there are fence
spans – because we need a post at the beginning of the fence.
Another way to look at this is that a single 10-foot length of
fence will require two posts – one at either end. A similar condition can occur in our programs when an iterative loop iterates one time too few or one time too many.
I’m sure professional computer programmers have no problem with this sort of thing, but it typically makes my head
hurt. On the bright side, I’m a visually oriented person, so
I usually find it helps me to draw things out. Let’s create
a graphical representation of our 100ms fade with a 10ms
master clock (Fig.3).
Before we commence our fade, we can
assume that we already have 100% of the
original (start) colour and 0% of the new
(end) colour. By the time we finish the
fade, we wish to have 0% of the original
52
Vis it o u r S h o p , C al l o r B u y o nl ine at:
w w w . crick l ew o o d el ectro nics . co m
02 0 8 4 5 2 01 6 1
Vis it o u r s h o p at:
4 0- 4 2 C rick l ew o o d B ro ad w ay
L o nd o n NW 2 3 E T
colour and 100% of the new colour. The ‘per cent’ values of
the intermediate colours will depend on the current step when
compared to the total number of steps.
As defined by the NeoPixel library, the clock used to upload
our NeoPixel string is running at 800kHz. Each NeoPixel requires 24 bits of data, which takes (1/800,000) * 24 = 30µs.
Since we have 145 NeoPixels (including our ‘sacrificial pixel’),
this equates to a total delay of 30 * 145 = 4,350µs = 4.35ms. If
we ‘guestimate’ that all of our computations require 0.65ms
(we will return to consider this in more detail in a future
column), then this means we need to add a padding delay of
10 – (0.65 + 4.35) = 5ms to each of our steps to build things
up to our 10ms master clock tick (you will see this defined as
INTER_TICK_PAD_DELAY in our sketch).
Once again, do you recall earlier on when I said, ‘One trick to
writing functions is to make them as general-purpose as possible’?
Well, the beauty of our CrossFade() and CrossFadeColor()
functions is that it doesn’t matter whether we are ‘fading up’ (from
black to a colour) or ‘fading down’ (from a colour to black), because black is just another colour. In turn, this means that we can
use these functions to fade from any colour to any other colour.
For your delectation and delight, I’ve created a video showing our colour-fading sketch in action (https://bit.ly/2EXQ20k).
In my next column, we will delve more deeply into various
multi-colour combinations we might decide to employ. Until
that frabjous day, please Callooh! Callay! – in other words
have a good one!
Cool bean Max Maxfield (Hawaiian shirt, on the right) is emperor
of all he surveys at CliveMaxfield.com – the go-to site for the
latest and greatest in technological geekdom.
Comments or questions? Email Max at: max<at>CliveMaxfield.com
Practical Electronics | October | 2020
Max’s Cool Beans cunning coding tips and tricks
O
n the one hand, I keep on saying I’m a hardware
designer by trade and my software skills are rudimentary
at best. On the other hand, I constantly amaze myself
with all the tidbits of trivia and nuggets of knowledge that
have managed to take root in my poor old noggin.
Want an argument?
As one example of the above, someone just emailed me to ask
why I sometimes use the term ‘parameter’ and at other times
say ‘argument’. Well, consider the following declaration of a
meaningless function:
int SillyFunction (int a, int b)
{
int y = a + b;
return y;
}
When we declare a function, part of this declaration is a list
of parameters associated with the function. In this case, our
function has two parameters: a and b. Of course, it’s also possible for a function to be declared with an empty parameter list.
Now consider when we call our function from somewhere
else in the program; for example:
the compiler will automatically convert one of the operands
into the same type as the other. If the operand goes from a
smaller domain (eg, short) to a larger domain (eg, long), this is
called promotion. By comparison, if the operand goes from a
larger domain to a smaller domain, this is known as demotion.
Promotion typically isn’t a problem because the range of values
associated with the smaller integer forms a subset of the larger
domain to which it is being promoted. In the case of demotion,
however, problems may occur if the value being demoted is too
large to fit into the target type, in which case the result will be
truncated (this may, of course, be just what we want to occur).
Different languages deal with all of this in different ways.
In the case of C/C++, we can perform explicit-type conversion
using a cast operator, which involves the name of the new
data type surrounded by parentheses. For example, suppose
we declare two unsigned integer variables: one 8-bits wide
and the other 32-bits wide, as follows:
uint8_t myInt8;
uint32_t myInt32;
Now suppose that, somewhere in our program, we wish to copy
the contents of the 32-bit value into the 8-bit value. In order to
achieve this, we would cast the 32-bit value into its 8-bit counterpart as follows:
int sillyResult = SillyFunction(40, 2);
myInt8 = (uint8_t) myInt32;
When we call a function, we pass in a list of arguments. In this
case, we pass two arguments, 40 and 2, into SillyFunction().
Of course, if a function is declared with an empty parameter list,
then when we call it we will pass in an empty argument list.
In the real world, a lot of people use the terms ‘parameters’
and ‘arguments’ interchangeably. So long as the person you are
talking to understands the message you are trying to convey,
then there’s ‘no harm, no foul,’ as they say.
The problem comes when you are talking with professional programmers who will take great delight in painstakingly
instructing you in the error of your ways. Personally, I don’t
care to give the little rascals the satisfaction.
Curiouser and curiouser
Towards the end of Chapter 1 in Alice’s Adventures in Wonderland, Alice foolishly guzzles the contents of a small bottle
marked ‘Drink Me’ and shrinks to only ten inches in height. A
little later, the little scamp cannot restrain herself from munching
down on a small cake with ‘Eat Me’ marked in currants. At the
start of Chapter 2, Alice cries ‘Curiouser and curiouser!’ as she
rapidly grows to giant size. If this ever happens to me (again),
I’m sure I will say much the same (or words to that effect).
The reason I’m waffling on about this here is that something similar can happen with integers in C/C++ (actually, it
can happen with all data types, but we will focus on integers
for the purpose of these discussions). This is known as ‘type
conversion’, of which there are two flavors: implicit and explicit. In the case of implicit-type conversion, the compiler accomplishes this automatically without our having to instruct
it to do so. Alternatively, we can use explicit type conversion
to ‘cast’ (transform) a value from one data type to another.
If you take a look at last month’s Tips and Tricks column (PE,
September 2020), you’ll see that we have short, regular and long
versions of signed and unsigned integers (we also have fixedwidth data types like uint8_t and uint32_t). Whenever we
try to perform a binary operation on operands of different types,
Practical Electronics | October | 2020
One of the purposes of the cast operator is to inform the compiler that we know what we’re doing. In this case, the most-significant 24 bits of the 32-bit myInt32 will be discarded, and
only its least-significant eight bits will be copied into myInt8.
As we’ve seen, the cast operator has only one operand, which
is to the right of the operator. As always, problems lurk for the
unwary. Suppose we want to shift the value in myInt32 eight
bits to the right and then copy the least-significant eight bits
of the result into myInt8. Consider the following statement:
myInt8 = (uint8_t) myInt32 >> 8;
What do you expect this to do? Many beginners would expect
this to first perform the shift operation followed by the cast.
In reality, the cast operator has a higher precedence than the
bitwise shift operator, so the operation that will actually be
performed can be represented as follows:
myInt8 = ((uint8_t) myInt32) >> 8;
That is, the 32-bit value will first be cast into an 8-bit value,
which will subsequently be shifted eight bits to the right.
The result will be to leave myInt8 containing 0 (00000000
in binary, 0x00 in hexadecimal), which is not what we were
hoping for. In order to achieve the desired result, we would
actually have to write our statement as follows:
myInt8 = (uint8_t) (myInt32 >> 8);
Adding these parentheses forces the shift to be implemented
first, after which the cast is performed on the result. You may
not be surprised to learn that the reason we are talking about
this here is that in my Cool Beans columns we are poised to
start casting like champions (I’ve just dispatched the butler
to fetch my casting trousers).
53
|