This is only a preview of the September 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 7
W
ell hello there! How nice it is to see you
again. It probably won’t surprise you to hear that
things have been progressing apace here at Maxfield
Manor with regard to my 12 × 12 ping-pong ball array. I should
perhaps warn you that we will be leaping from topic to topic
with the agility of young mountain goats in this column, so
please take a moment to make sure you’re wearing appropriate
clothing before we proceed further.
Blinded by the light
Let’s commence by reminding ourselves that each of our pixels
(ping-pong balls) contains a tricolour LED called a NeoPixel.
Furthermore, each NeoPixel contains red, green, and blue subpixels, each of which can be driven with 256 different levels
from 0 (0x00 in hexadecimal = 0% = fully off) to 255 (0xFF
in hexadecimal = 100% = fully on).
I don’t know about you, but whenever I’m working on a
project like this, questions pop into my mind and I realise
that there are all sorts of things I don’t know. For example,
suppose we were to light up one of our pixels 100% red. Now
suppose we were to light up an adjacent pixel with 100%
green and 100% blue, resulting in cyan. Does this mean that
the cyan pixel is twice as bright as the red pixel? In turn, does
this mean that we should really light the green and blue subpixels at 50% (128 in decimal = 0x80 in hexadecimal) so as
to achieve the same brightness as the red pixel?
Similarly, if we were to take another pixel that’s adjacent to
our original red pixel and light all three sub-pixels to generate
white, does this mean that we should really set the red, green,
and blue sub-pixels to 33% (85 in decimal = 0x55 in hexadecimal) in order to achieve the same brightness as the red pixel?
I thought it might be a good idea to test things out. As you
may recall from my previous column (PE, August 2020), our
NeoPixels are connected together as a single string numbered
from 1 to 144 (there’s also a sacrificial pixel 0, which we’re
using as a voltage-level converter, but this plays no part in
the visible portion of the display).
We’re considering our array to be an (X,Y) matrix with both
X (horizontal) and Y (vertical) positions numbered from 0 to
That’s what I call a pixel array... and the shirt’s not bad either!
11, and with pixel (0,0) in the bottom left-hand corner. Also,
we created a simple function called GetNeoNum(), which accepts X and Y values as arguments and returns the number
of the corresponding NeoPixel in the string.
As a first test of this brightness/contrast conundrum, I created a simple sketch (program) that fills the outer pixels with
100% white and an inner 4 × 4 square of pixels with 100%
red (Fig.1a). Then we drop the surrounding white to 33%.
Next, the sketch fills the outer pixels with 100% red and the
inner square with 100% white (Fig.1b). Once again, we subsequently drop the white to 33%. We then cycle back to perform the whole sequence over and over again. If you wish, you
can download this sketch and peruse and ponder it at your
leisure (it’s file CB-Aug20-01.txt – available for download,
along with the other three files discussed in this column, on
the September 2020 page of the PE website). For your delectation and delight, I’ve also created a short video showing all
of this in action (https://bit.ly/2OG9lgg).
As an aside, you may recall from an earlier column (PE,
July 2020) that the creators of Adafruit’s NeoPixel library
provided two ways for us to specify the colour of a pixel.
Let’s suppose our string is called Neos and that we want to
set the colour associated with pixel 42 to have red, green,
and blue colour components of 128, 0, and 255, respectively (these would be 0x80, 0x00, and 0xFF in hexadecimal). If we wish, we can specify the colours individually
using a statement like:
Neos.setPixelColor(42, 128, 0, 255)
(a) Red inner 100% ; White outer 100%
< -> 33%
(b ) Red outer 100% ; White inner 100%
Fig.1. First contrast test.
Practical Electronics | September | 2020
< -> 33%
Or:
Neos.setPixelColor(42, 0x80, 0x00, 0xFF)
53
Neos.setPixelColor(42, 8388863)
into here) it’s also exclusive, which means it won’t appear
in the result. Another way to look at this is to observe that
when this function is called it will return a result between
min and (max – 1).
where the number is specified in decimal, or:
A bit of a drip
As an alternative we can specify all three colours together as
a single numerical value using a statement like:
Neos.setPixelColor(42, 0x8000FF)
with the number specified in hexadecimal. Using a single
numerical value in hexadecimal makes our lives a lot easier,
so that’s what we’ll do. This explains why you will see the
colour definitions in my sketches looking something like
the following:
#define COLOR_WHITE 0xFFFFFFU
#define COLOR_BLACK 0x000000U
#define COLOR_RED
0xFF0000U
#define COLOR_GREEN 0x00FF00U
#define COLOR_BLUE 0x0000FFU
Note that adding ‘U’ (or ‘u’) characters on the end of these
values informs the compiler to regard them as being unsigned
(see this month’s Tips and Tricks column for more discussions
on the difference between signed and unsigned values). Most
of the time this won’t be necessary, but every now and then...
Next, I started to wonder if my 4 × 4 centre square was a tad
small (16 pixels) in comparison to the surrounding area (128
pixels), so I decided to increase its size to 6 × 6. Fortunately,
due to the way I created my original program, all I had to do to
generate the new version was to change two definitions: XY_
CENTER_MIN was changed from 4 to 3, while XY_CENTER_MAX
was changed from 7 to 8. Once again, you can download this
sketch (CB-Aug20-02.txt). And, once again, I’ve created a short
video showing all of this in action (https://bit.ly/2OG9lgg).
Personally, I think the 33% white looks a bit washed out.
My conclusion is that, if we were trying to use our array to
represent images or to play extremely low-resolution videos,
then we would probably vary the intensity of each sub-pixel
to balance everything out. By comparison, in the case of our
simple effects, we probably want to make everything as bright
as possible, which – happily – is my default position anyway.
For our first series of effects, we’re going to imagine that our
array is lying flat on the floor and that occasional drips of
water – virtual raindrops, if you will – are randomly falling
from the sky. Whenever a virtual drip lands on one of our
pixels (ping-pong balls), that pixel will light up bright white
for a short period of time.
You may find it advantageous to download the sketch associated with this effect (file CB-Aug20-03.txt) and reference
it in conjunction with these discussions.
As you will see, we declare two integer global variables
called OldX and OldY, which we will use to keep track of
the (X,Y) coordinates of the last pixel that was lit up. We initialise both of these variables with random values between 0
and 11 in our setup() function.
Most of this sketch is self-explanatory. The interesting part
is where we generate the (X,Y) coordinates associated with a
new drip as follows (the +1s associated with the maximum
values are there to offset the fact that, as we just discussed,
the random() function treats its maximum argument as
being exclusive):
do
{
newX = random(MIN_X, (MAX_X + 1));
newY = random(MIN_Y, (MAX_Y + 1));
} while (condition);
This loop will be executed at least one time, and it will keep
on executing until its condition is met (ie, returns true).
But what condition should we use? One alternative would
be to say:
((OldX == newX) && (OldY == newY))
To give our effects an ‘organic’ feel, we’re going to be using
the C/C++ random() function, which accepts two arguments
and returns a random result as follows:
In this case, the loop will keep on executing until the new
pixel is different from the old pixel, but there’s nothing to
prevent the new pixel from being horizontally or vertically
adjacent to the old pixel.
Personally, I prefer a tad more differentiation, so I opted to
swap the && (logical AND) operator for the || (logical OR)
operator and use:
result = random(min, max);
((OldX == newX) || (OldY == newY))
The min argument is optional. If this value is not specified,
it will default to 0. This value is also inclusive, which means
it may appear in the result. By comparison, the max argument is required and (for all sorts of reasons we won’t go
This means the new pixel cannot be on the same row or
column as the old pixel.
The only other things to note about this sketch are that we
also use the random() function to vary the duration of the
flashes associated with each of the drips and the gaps between
drips. Also, the last thing we do at the end of each cycle is
set the values of OldX and OldY to be the values of newX and
newY, respectively. This means that the pixel we just flashed
will be considered to be our old pixel the next time we pass
through the loop. Do you even need to ask?! Yes, of course
I’ve carefully crafted a video showing all of this in action (see:
https://bit.ly/2OG9lgg).
Feeling random?
Red
Ma g e n t a
Y ellow
G reen
Y e llo w
Cy an
Blue
(a) Sub tractiv e (ink s, paints, and pig ments)
R e d
G reen
C y a n
B lu e
(b ) Additiv e (lig ht)
Fig.2. Subtractive and additive primary colours.
54
Ma g e n t a
Do you believe in colour fairies?
When I was a little kid, I had a book that described how the
red, yellow, and blue colour fairies spent the night repainting
everything, which explained why all of the colours looked so
bright and fresh each morning.
Practical Electronics | September | 2020
0° Primary
Red
330° Tertiary
30° Tertiary
FF 00 00
Rose
Flush O rang e
FF 00 80
FF 80 00
300° Secondary
60° Secondary
Mag enta
FF 00 FF
11
0
Y ellow
2
10
270° Tertiary
Electric
Indig o
9
Chartreuse
80 FF 00
4
7
6
Your best bet since MAPLIN
Chock-a-Block with Stock
90° Tertiary
3
8
80 00 FF
FF FF 00
1
5
240° Primary
120° Primary
00 00 FF
00 FF 00
Blue
Visit: www.cricklewoodelectronics.com
O r phone our f riendly kn ow ledg eab le staf f on 0 2 0 8 4 5 2 0 16 1
Components • Audio • Video • Connectors • Cables
Arduino • Test Equipment etc, etc
G reen
210° Tertiary
Az ure
00 80 FF
150° Tertiary
180° Secondary
Cy an
Spring G reen
00 FF 80
00 FF FF
Fig.3. The colour wheel we will be using in our experiments.
This all made perfect sense to me, especially since I knew
from school that the fairies could generate other colours by
working together: red and yellow made orange, red and blue
made purple, yellow and blue made green, and so forth.
Later, my teacher explained that red (R), yellow (Y), and
blue (B) were primary colours. What she didn’t say was that
this was just one possible set of primaries, because the term
‘primary colours’ simply refers to a small collection of three
or more colours that can be combined to form a range of additional hues, shades, tints and tones. For example, printers use cyan (C), magenta (M), and yellow (Y) inks as their
primary colours. Furthermore, although it’s possible to mix
cyan, magenta, and yellow to form black, printers typically
use a special black pigment. We use the letter K to represent
this pigment because B could be confused with blue. This
explains why we use the term CMYK when we’re talking
about printer ink.
Now, the thing about inks, paints and pigments is that they
are subtractive in nature. One way to think about this is that
we start with a white background illuminated with white
light. The white background reflects all of the frequencies in
the white light. When we start to add our colours, each colour
absorbs (subtracts) some of the frequencies from the white
light (Fig.2a). Furthermore, when inks, paints, or pigments are
mixed together, they each absorb different frequencies. Basically, what we see is what’s left over; that is, what isn’t absorbed. When all of the colours are mixed together, they absorb
all of the frequencies leaving us with nothing to see (ie, black).
By comparison, light is additive. The set of primary light
colours with which we are most familiar are red (R), green
(G), and blue (B). One way to think about this is that we start
with a black room and add our primary light sources. Red and
green add together to form yellow, red and blue add to form
magenta, and green and blue add to form cyan. When they
are all combined, red, green, and blue are perceived as white.
Technicolor drips
The reason for our interest in primary colours is that the next
version of our drip program is going to illuminate our pixels
with randomly selected colours. Since we
have red, green, and blue subpixels, each
of which can be assigned values from 0 to
255, we have the potential for 256 × 256
× 256 = 16,777,216 (224) different hues,
shades, tints and tones.
Practical Electronics | September | 2020
V is it o u r S h o p , C a ll o r B u y o n lin e a t:
w w w .c r ic k le w o o d e le c tr o n ic s .c o m
0 2 0 8 4 5 2 0 16 1
V is it o u r s h o p a t:
4 0 -4 2 C r ic k le w o o d B r o a d w a y
Lo n d o n N W 2 3E T
Of course, some of these little rascals are going to be dim
and lacklustre but – from our experiments in contrast at the
beginning of this column – we know that we want our colours
to be as bright and shiny as they can be. One way to represent
colours is to use a colour wheel (Fig.3).
As we see, our three primary colours (red green and blue)
can be combined to form three secondary colours (yellow,
cyan, and magenta) and six tertiary colours (with more exotic
names: flush orange, chartreuse, spring green, azure, electric
indigo, and rose).
These 12 colours are the ones we will use in our sketch,
which you can download if you wish (CB-Aug20-04.txt). If
you compare this sketch to our previous version, you will observe that the only difference is that – as opposed to lighting
our pixels with white – for each virtual drip we now use the
random() function to select one of our 12 colour options.
Once again, I’ve created a video showing all of this in action
(https://bit.ly/2OG9lgg).
Next time
I must admit that I do like the coloured drips featured in our
last sketch, but simply turning our pixels hard on and hard
off is not very subtle and lacks a little je ne sais quoi. One
way in which we can add a soupçon of sophistication is by
fading the colour up, holding it steady, and then fading it
back down again, so this is what we’ll do in my next column.
Until then, 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
55
Max’s Cool Beans cunning coding tips and tricks
I
n my previous tips and tricks column (PE August
2020), we noted that when people are first introduced
to the C/C++ programming languages, one of the first
data types to which they are introduced is the int, which
stands for ‘integer’. We also noted that we may decide to
declare our integers as being signed or unsigned:
signed
int John;
unsigned int Jane;
A signed int can be used to represent both positive and negative values, while an unsigned int can be used to represent only
positive values. Interestingly enough, the C/C++ standards don’t
explicitly define the size of an int. All they say is that an int
should be a minimum of two bytes (16 bits). In the case of an Arduino Uno with its 8-bit data bus, the size of an int is indeed two
bytes; in other computers it can be two bytes, four bytes, or more.
A 2-byte (16-bit) variable can be used to represent 216 = 65,526
different patterns of 0s and 1s. In the case of a signed int, these
patterns can be used to represent both negative and positive
values in the range –32,768 to +32,767. By comparison, in the
case of an unsigned int, these patterns can be used to represent
only positive values in the range 0 to 65,535.
Hey, short stuff!
There are also short and long flavours of the int:
signed
short int Fred;
signed
long int Bert;
unsigned short int Mary;
unsigned long int Beth;
In the same way we assume a number like 42 to be positive without explicitly writing it as +42, the compiler will assume that an
unqualified int is signed, which means we may omit the signed
part of the declaration if we wish:
int
John;
short int Fred;
long int Bert;
Similarly, if we declare something as being of type short or long,
the compiler will happily assume that we are talking about integer values, so we may also omit the int part if we wish:
int
John;
short Fred;
long Bert;
The C/C++ specifications state that the minimum size of a short
int is two bytes (16 bits), while the minimum size of a long
int is four bytes (32 bits).
Fixing the width
It can be a tad disconcerting when one first discovers that the
size of the different types of integers can vary from one machine
to another. The reason behind this is to allow the compiler to
make use of its knowledge of the targeted processor’s internal architecture to use optimal data sizes for that particular machine.
Things aren’t too bad if you are a hobbyist writing code for only
one processer. However, depending on the size of the numbers
you are trying to represent, problems may arise if you try to port
your code from a machine that supports four-byte integers to one
56
whose integers are only two bytes in size, for example. In order
to get around this, we can use the fixed-width integer types that
have been available since the v11 release of C++; for example:
int8_t Cuthbert; // Unequivocally 8 bits (one byte)
uint8_t Clarence; // Indubitably 8 bits (one byte)
There are also 16-bit (int16_t and uint16_t), 32-bit (int32_t
and uint32_t), and 64-bit (int64_t and uint64_t) equivalents of these fixed-width types.
A dangerous thing
‘A little knowledge is a dangerous thing,’ as the old saying goes.
Suppose you are writing a program for an Arduino Uno and you
know that the counter you are using to control a for() loop will
only ever count up to 100. In this case, would you be better off
using an int8_t (8 bits) rather than an int (16 bits), thereby
saving a byte (8 bits) in memory? For example:
for (int8_t i = 0; i < 100; i++)
{
// Do something cunning here
}
Generally speaking, the answer is ‘no’. The problem is that using
a smaller variable in order to save memory may negatively impact
the performance of your program. Unless you are intimately familiar with the workings of your compiler and the internal architecture of your processor, you are best advised to use an int
and let the compiler use its detailed knowledge of the processor’s
architecture to make optimal decisions for you.
Having said this, if you do have specific requirements, then by
all means use the appropriate fixed-width types. In the case of
the programs for my 12 × 12 ping-pong ball array project, for example, you will observe that I declare all of my colour values as
being of type uint32_t (ie, unsigned 32-bit integers). The equivalent would be an unsigned long in the case of the Arduino
Uno, but I prefer to be explicit in the case of these declarations.
But wait, there’s more!
I come from the days where every memory location and every
clock cycle were considered to be precious commodities. Computer magazines used to have columns of cunning tricks for doing
things like swapping the upper and lower four bits in a byte in
the least number of clock cycles or using the smallest number of
instructions or consuming the least amount of memory.
Some designers of embedded systems are pushing the boundaries of the processors they’ve been instructed to use. In this case,
they may decide to use the minimum width and fastest minimum
width types that have been supported by C since 1999 (known
as C99-compliant compilers); for example:
int8_t Bob;
// Fixed width signed
// 8-bit integer
int_least8_t Dan; // Minimum width signed
// 8-bit integer
int_fast8_t Sam; // Fastest minimum width
// signed 8-bit integer
Of course, there are unsigned versions of these types, along with
16-, 32-, and 64-bit variants, but I’d advise holding off on using
these little scamps until you are 100% sure you are aware of any
and all implications.
Practical Electronics | September | 2020
|