This is only a preview of the December 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
W
Flashing LEDs and drooling engineers – Part 10
ell hello there. I hope you’re having as awesome
a day when you read this as I’m having while I write
it. Just to make sure we are all tap-dancing to the same
‘skirl of the pipes*,’ let’s briefly remind ourselves that we are
currently playing with a 12x12 array of ping-pong balls, each
containing a WS2812-based tricolour LED (*I know whereof I
speak, because my dear old dad was a dancer on the variety hall
stage prior to WWII, and he was in the Reconnaissance Unit of
the 15th Scottish Infantry Division during WWII, as part of which
he earned many beers performing Scottish sword dances to the
sound of the bagpipes).
For the past few columns, we’ve been experimenting with
‘virtual drips’ randomly falling on, and lighting up, pixels in
our array. At the end of our previous column (PE, November
2020), we noted that – up until now – we’ve worked with only
a single drip at a time (Fig.1a). We also conjectured that it would
be more exciting if we were to allow multiple drips to be active
concurrently, and for their start and end times to be randomly
determined such that they overlap in interesting and unpredictable ways (Fig.1b).
Like most things, of course, implementing a cornucopia of
contemporaneous drips sounds easy if you say it quickly and
gesticulate furiously. Sad to say, however, the underlying way
in which we’ve been implementing things in our code thus far
will prove to be rather limiting. But turn that frown upside down
and into a smile, because we won’t let anything prevent us from
achieving our multi-drip extravaganza, or my name isn’t Max
the Magnificent.
In a bit of a state
Consider the following interpretation of the main loop() function used in the Arduino’s classic ‘Blink’ sketch (program). Let’s
assume we are using this program to control a yellow LED. In
this particular example, we are cycling around turning the LED
on and off at a frequency of 1Hz (one cycle per second).
void loop()
{
digitalWrite(PinLed, LOW);
delay(500);
digitalWrite(PinLed, HIGH);
delay(500);
}
The term ‘finite-state machine’ (FSM), or simply ‘state machine’,
refers to a mathematical model of computation. The underlying
idea is that we have an abstract machine that can be in only one
of a finite number of states at any particular time. The reason
I mention this here is that the code presented above might be
considered to implement a rudimentary state machine, whose
operation we could depict graphically as illustrated in Fig.2a.
Now, suppose we decide to add a red LED, and have the two
LEDs turning on and off at different rates. Let’s say the red LED
has a frequency of 1Hz, while the yellow LED has a frequency
of 2Hz. The main loop() code for this could be as follows, with
a graphical equivalent as depicted in Fig.2b (the full sketch is
58
presented in file CB-Dec20-01.txt – it and the other files associated with this article, are available on the December 2020 page
of the PE website).
void loop ()
{
// State 0
digitalWrite(PinRedLed,
digitalWrite(PinYellowLed,
delay(250);
LOW);
LOW);
// State 1
digitalWrite(PinRedLed,
LOW);
digitalWrite(PinYellowLed, HIGH);
delay(250);
// State 2
digitalWrite(PinRedLed,
HIGH);
digitalWrite(PinYellowLed, LOW);
delay(250);
// State 3
digitalWrite(PinRedLed,
HIGH);
digitalWrite(PinYellowLed, HIGH);
delay(250);
}
In a classic FSM, we would have some way to remember the current context (state) of the machine. This could be a register containing the state variables in the case of a hardware implementation, or an enumerated type in the case of a software realisation
(see this month’s Tips and Tricks column for more information
on enumerated types). By comparison, when it comes to our example code shown above, apart from using comments, we don’t
have any way to explicitly define the current state. Instead, the
state is implied by where we are in the code.
In order to illustrate why this is a problem, let’s suppose I were
to ask you to add a green LED with a frequency of 3Hz into the
mix. Take a moment to think about how you would implement the
( a) One drip at a tim e
( b) M ultiple drips at the sam e tim e
Fig.1. Single versus multiple drips.
Practical Electronics | December | 2020
code for this. I can imagine you smiling because, even though you
know that everything is so intertwined it will undeniably make
things trickier, you are sure that – if push came to shove – you
could do this. How about if, instead of simply turning the three
LEDs on and off, I ask you to fade them on, hold them steady,
and fade them off, with each fade taking 10 steps over 100 milliseconds (ms). You aren’t smiling now, are you?
When we come to think about it, this is pretty much where
we are with our existing drip programs. Although it’s true that
we’ve implemented some very tasty fading effects using different
colours, we’ve only achieved this with one drip at a time. We’re
going to have to adopt a new approach if we wish to have multiple
drips active concurrently in random relationships to each other.
milliseconds that have passed since the Arduino powered
up and the program started running.
void loop ()
{
uint32_t currentTime = millis();
if ( (currentTime - LastTimeRedLedChanged) >=
OnOffDelayRedLed )
{
if (StateRedLed == LED_OFF)
{
StateRedLed = LED_ON;
}
else
{
StateRedLed = LED_OFF;
}
Dump the delay()!
The delay() function shown in the code examples above is a
blocking function, which means it completely ties up the processor, thereby preventing (or blocking) anything else from happening. While the processor is executing a delay(), it can’t respond to changes on any of its inputs, it can’t perform any calculations or make any decisions, and it can’t change the state of
any of its outputs.
The bottom line is that, in order to achieve multiple drips,
we need to dump the delay() and implement our code using
some other approach. One technique we can employ is to cycle
around checking the system clock to determine when it’s time to
act. Let’s look at a simple example of this in action. What we are
going to do is create a new version of our 2-LED program using
this new method. As you will see if you look at the code (file
CB-Dec20-02.txt), we start by defining LED_OFF and LED_ON as
LOW and HIGH, respectively. We also declare two global variables
StateRedLed and StateYellowLed to hold the current states
(LED_OFF or LED_ON) of their respective LEDs.
For the purposes of these examples, each LED has a 1:1 markspace ratio, which means it’s on for the same amount of time as
it’s off. Since we wish the red LED to have a frequency of 1Hz,
which equates to a period of 1,000ms, this means it will alternate
between being on for 500ms and off for 500ms. Similarly, as we
wish the yellow LED to have a frequency of 2Hz, which equates
to a period of 500ms, this means it will alternate between being
on for 250ms and off for 250ms.
All of this explains why we declare a global variable called OnOffDelayRedLed, which we set to 500ms, and a global variable
called OnOffDelayYellowLed, which we set to 250ms. Furthermore, we also declare two global variables LastTimeRedLedChanged and LastTimeYellowLedChanged, which – as their
names suggest
– we will use to
keep track of the
S tate
S tate
last time their asL ed = Off
L ed = On
0
1
sociated LEDs
changed state.
S 0
S 1
S 0
S 1
S 0
The code for
L ed
the first half of
( a) S im ple 2 - state F S M
the main loop
is shown below.
We start by loadL ed1 = Off S tate
L ed1 = Off
S tate
0
1
L ed2 = Off
L ed2 = On
ing the local variable currentT i m e with the
L ed1 = On S tate
L ed1 = On
S tate
value returned
3
2
L ed2 = On
L ed2 = Off
from the Arduino’s millis()
S 0 S 1 S 2 S 3 S 0 S 1 S 2 S 3 S 0 S 1 S 2
L ed1
function, which
L ed2
will be a 32-bit
unsigned inte( b) S im ple 4 - state F S M
ger representing
the number of
Fig.2. Simple state machines
Practical Electronics | December | 2020
digitalWrite(PinRedLed, StateRedLed);
LastTimeRedLedChanged = currentTime;
}
// More code goes here
}
Next, we perform a test to see if the current time minus the last
time the red LED changed is greater than or equal to the red LED’s
on/off delay, which we previously set to 500ms. If not, we don’t
do anything. However, if it has been 500ms or more since the
red LED changed, we flip its state (from off to on, or vice versa),
then we write this new state to the pin driving the red LED and
we reset the variable storing the last time this LED changed state
to be the current time.
Your first reaction may be to scream ‘Arrgggh!’ Your second reaction may be to say in menacing tones, ‘Forgive me for saying so,
but this appears to be a tad more complicated than simply using
calls to the delay() function.’ Well, yes and no. Although this
takes a little more effort to set up, it makes our lives a lot easier
in the long run. For example, the code to handle the yellow LED
(which will appear where we show the ‘// More code goes here’
comment) is simply a modified copy of the if () statement
we used to handle the red LED. Similarly, if we decided to add
a green LED with a frequency of 3Hz, all we would need to do
would be to add StateGreenLed, OnOffDelayGreenLed, and
LastTimeGreenLedChanged global variables and also add a
new if () statement into our main loop. Trust me – the more
you think about this, the easier it gets.
My register floweth over
Earlier, we noted that the Arduino’s millis() function returns
a 32-bit unsigned integer representing the number of milliseconds that have passed since the Arduino powered up and the
program started running.
This value is stored in a 32-bit counter/timer register buried
deep in the Arduino’s internal architecture. One question you
were doubtless asking yourself is, ‘What happens when this register overflows?’ By this we mean that when we power up the Arduino, this register contains 0 (or 0x00000000 in hexadecimal).
If we keep on incrementing this register every millisecond, then
it will eventually contain 232 = 4,294,967,296 (or 0xFFFFFFFF
in hexadecimal).
How long will this take and what happens next? Well, since
the register increments every millisecond (one thousandth of a
second), we can divide 4,294,967,296 by 1,000 to get seconds,
then divide by 60 to get minutes, and by 60 again to get hours,
and by 24 to get days. By this, we discover that it will take close
to 50 days before the register fills up.
59
N o drip
D rip fades on
DRIP_WAITING says that we’ve scheduled this drip to
commence at some time in the future, and DRIP_RISING,
DRIP_SUSTAINING, and DRIP_FALLING govern the pixel
fading up, holding, and fading away again, respectively.
Next, we declare a structure called Pixel, which contains all of the attributes we wish to associate with each of
our pixels:
D rip fades off
( a) R udim entary drip effec t
N o drip
D rip fades on
S plash fades on
D rip fades off
S plash fades off
typedef struct Pixel
{
PixelState currentState;
uint32_t
waterColor;
uint32_t
oldColor;
uint32_t
newColor;
int
numSteps;
int
currentStep;
};
( b) A ugm enting eac h drip with an assoc iated splash
Fig.3. Rudimentary drip effect compared to a ‘drip plus splash’ effect.
Once the register contains 4,294,967,296 (0xFFFFFFFF), the
next tick of the millisecond clock will cause it to overflow and
return to containing 0 (0x00000000), and we start all over again.
So, when we pass through this wraparound case, what will
happen to our test (currentTime - LastTimeRedLedChanged)
>= OnOffDelayRedLed)? Might we see a glitch or something
worse? On the one hand, it’s unlikely that we will be running
our drip program for 50 days or more at a stretch. Also, the
world wouldn’t end if there were a glitch in an application of
this ilk. On the other hand, suppose we wished to use a similar
technique to control a safety-critical or mission-critical system
in which any form of glitch, no matter how slight, would not
be considered to be a good thing to occur?
Well, due to the magic of binary numbers and operations,
our code will happily continue to perform its task of flashing
the LEDs without any change in delay or any other disruption,
even when the millis() register overflows back to 0. The reasoning behind all this takes a bit of time to digest and we don’t
want to delve into it here. Happily, I wrote two columns some
time ago that discuss all of this in excruciating detail (https://bit.
ly/3cPSSBo and https://bit.ly/2GrlNQ2).
A deluge of drips
Our first incarnation of a multi-drip program just focuses on the
drips themselves. There’s quite a lot to this, so I really do advise
you to download the text version of this program and print it out
so you can follow along (file CB-Dec20-03.txt).
When you peruse this program, you will see many familiar
faces in the form of the little utility functions we created in earlier drip sketches, such as GetNeoNum(), CrossFadeColor(),
BuildColor(), GetRed(), GetGreen(), and GetBlue(). In
fact, apart from these functions and our setup() and loop()
functions, we have only two other functions: StartNewDrip()
and ProcessDrips().
Before we look at these new functions in a little more depth,
there are some new constructs and definitions we need to consider in the form of typedef (type definitions), enum (enumerated types), and struct (structures). The nitty-gritty of these constructs is explored in more depth in this month’s Tips and Tricks
column. For our purposes here, all we need to know is that we’ve
declared an enumerated type called PixelState as follows:
typedef enum PixelState
{
NONE,
DRIP_WAITING,
DRIP_RISING,
DRIP_SUSTAINING,
DRIP_FALLING
};
These are the states that we are going to associate with each
of our pixels: NONE says that this pixel is currently inactive,
60
Observe that the first of these attributes is the state of the pixel.
We will commence with all of the pixels having a state of NONE,
where these values are assigned as part of our setup() function.
There are many different ways in which we might decide to
implement our program. One realisation might involve including a lastTimeLedChanged field in our Pixel structure (similar in concept to the way in which we implemented our 2-LED
program earlier in this column). As we will see, however, I decided to adopt a slightly different approach.
The final piece of this portion of the puzzle is where we
declare an array called Pixels[][] of our Pixel structure,
as shown below:
Pixel Pixels[NUM_COLS][NUM_ROWS];
Although it may take a bit of effort to wrap our brains around all
this, it’s really not as bad as it seems. If we look at things in reverse order, we have an array called Pixels[][] that contains
the data associated with each our pixels. This data includes
things like the state of the pixel, the colour associated with the
pixel, and so on.
As we see below, the loop() function is actually simpler than
the one we employed in our 2-LED program:
void loop ()
{
uint32_t currentTime = millis();
if ( (currentTime - LastTickTime) > TICK)
{
StartNewDrip();
ProcessDrips();
Neos.show();
LastTickTime = currentTime;
}
}
As you may recall from previous programs, we are using a
master clock whose TICK is set to 10ms. This means that every
ten milliseconds we call our StartNewDrip() function followed by our ProcessDrips() function, after which we display the current values of our pixels and update the variable
storing the current time.
If you look at the code, you will see that the StartNewDrip()
function doesn’t always initiate a new drip. We have a global
variable NumActiveDrips, which stores the number of active
drips, and we have a constant NUM_MAX_DRIPS, which defines the maximum number of drips that can be active at any
Practical Electronics | December | 2020
particular time. Our StartNewDrip() function will only initiate a new drip if we aren’t already fully loaded and – even
then – it will schedule the new drip to commence at some
random time in the future.
Meanwhile, the cunning way in which we’ve architected the
ProcessDrip() function means that every stage of the drip is
implemented in the same way, fading from one colour to another
over a series of steps. When we are waiting for a drip to drop, for
example, we spend our time fading from black to black, which
– not surprisingly – ends up looking like black. When we fade
a pixel up, we fade from black to the randomly selected colour
for that pixel. When we hold a pixel in its current colour, we actually fade from that colour to itself. And when we fade a pixel
down, we fade from its randomly selected colour back to black.
We aren’t going to examine this code in any more detail here.
Suffice to say, we can feast our eyes on all of this in action in a
video I just captured (https://bit.ly/33ufl3V).
Galoshes on!
pixels at the outside edges of the array, thereby relieving us of
having to perform any jiggery-pokery with regard to any splash
pixels that might otherwise appear outside of the array.
The next change is that we’ve added some additional states to
our PixelState enumerated type (the new states are shown in bold):
typedef enum PixelState
{
NONE,
DRIP_WAITING,
DRIP_RISING,
DRIP_SUSTAINING,
DRIP_FALLING,
SPLASH_WAITING,
SPLASH_RISING,
SPLASH_SUSTAINING,
SPLASH_FALLING
};
We’ve also added a FadeColor() function that we use to take
Thus far, we’ve been experiencing only rudimentary drip effects
the main drip colour and fade it down to a specified percentage
(Fig.3a). The final step on our trek through driptopia, the land of
of its original value – this muted version is what we use for our
drips – at least for the moment – is to add the concept of a splash
splash pixels.
(Fig.3b). The idea here is that shortly after a primary drip drops, a
Last but not least, we’ve modified the StartNewDrip() funcmuted version of the drip colour will appear in the pixels to the
tion to also launch any associated splash pixels, and we’ve augnorth, south, east, and west. These muted versions will persist
mented the ProcessDrips() function to display these splash
for a short time after the primary drip fades, after which they too
pixels. As you will see, the cunning way in which we architectwill fade away (‘All we are is drips in the wind,’ as the progresed the original (pre-splash) version of our program means that
sive rock band Kansas might have sung).
adding the splash effect is really not as difficult as you might
Once again, I strongly advise you to download the text verhave supposed. Once again, we can feast our orbs on all of this
sion of this program and print it out so you can follow along
in action in a video I just captured (https://bit.ly/3ljkcer).
(file CB-Dec20-04.txt).
The first change to the previous program
is that we’ve modified our MIN_XY and
Cool bean Max Maxfield (Hawaiian shirt, on the right) is emperor
MAX_XY definitions from 0 to 1 and 11
of all he surveys at CliveMaxfield.com – the go-to site for the
to 10, respectively. We did this to ensure
latest and greatest in technological geekdom.
that our StartNewDrip() function won’t
Comments or questions? Email Max at: max<at>CliveMaxfield.com
launch any primary drips in any of the
Max’s Cool Beans cunning coding tips and tricks
I
n this month’s main Cool Beans column, we
employed some new concepts in the form of typedef
(type definitions), enum (enumerated types), and struct
(structures). Let’s look at these in a little more detail.
Enumerated types (enum)
currentState = NONE;
And we can perform tests like:
if we wish to implement a finite-state machine (FSM), we will
need some way to store its current context (state). One way to do
this would be to identify a set of states and associate them with
numbers using a set of #define statements:
#define NONE
#define DRIP_WAITING
#define DRIP_RISING
#define DRIP_SUSTAINING
#define DRIP_FALLING
Later, we might declare a variable called currentState as being
of type int (integer), after which we can perform assignments like:
0
1
2
3
4
Practical Electronics | December | 2020
if (currentState == NONE)
{
// More stuff goes here
}
This technique is fine and it’s not difficult to add more states. However, if you are anything like me, you can easily end up spending
a lot of time reorganising things and changing the numbers associated with different states because you want things to be ‘just so.’
61
The enum keyword allows us to create a user-defined type comprising a set of named constants called enumerators:
myFavoritePixel.currentState = NONE;
myArrayOfPixels[6].currentState = NONE;
enum PixelState
{
NONE,
DRIP_WAITING,
DRIP_RISING,
DRIP_SUSTAINING,
DRIP_FALLING
};
Observe that when we are dealing with an array, as in the second
example, we also have to provide an integer index to specify
which element of the array we are talking about (element 6 in
this example).
Observe that no comma is required after the final enumerator,
but a semicolon is required after the ‘}’ (that is, the closing curly
bracket). By default, the enumerators are assigned integer values
by the compiler starting with 0. This means that, in the above example, NONE will be assigned a value of 0, DRIP_WAITING will
be assigned a value of 1, and so forth. It’s also possible for us to
assign our own values. It’s even possible for multiple enumerators to be assigned the same value, but that’s beyond the scope
of our discussions here.
Once we’ve defined an enum, we can declare one or more variables of this enum type:
Type definitions (typedef)
The typedef keyword is used to assign alternative names to
existing data types. If we really dislike the int keyword, for example, we could use the following statement, where int is the
existing data type name and simon is the alias:
typedef int simon;
After this, we can declare new variables with a data type of
simon if we wish. Obviously, this particular example is a tad
nonsensical, but using typedef with existing data types can be
useful on occasion. Where the typedef keyword really comes
into its own is when it’s used in conjunction with user-defined
enum and struct statements. Let’s start by using a typedef in
conjunction with an enum:
enum PixelState oldState = NONE;
enum PixelState newState = DRIP_RISING;
typedef enum PixelState
{
NONE,
DRIP_WAITING,
DRIP_RISING,
DRIP_SUSTAINING,
DRIP_FALLING
};
Elsewhere in our program, we can assign new values to these
variables as we wish:
Now, when we come to declare one or more variables of this
enum type, we can simply say something like:
oldState = DRIP_RISING;
newState = DRIP_SUSTAINING;
PixelState oldState;
PixelState newState;
Structures (struct)
The struct keyword is used to define a collection of data items,
each of which may have its own type:
Compare this to our earlier example where we had to reuse the
enum keyword. Next, let’s use a typedef in conjunction with
a struct:
struct Pixel
{
PixelState currentState;
uint32_t
waterColor;
uint32_t
oldColor;
uint32_t
newColor;
int
numSteps;
int
currentStep;
};
typedef struct Pixel
{
PixelState currentState;
uint32_t
waterColor;
uint32_t
oldColor;
uint32_t
newColor;
int
numSteps;
int
currentStep;
};
Observe that semicolons are required both after the final field and
after the closing curly bracket. Once we’ve defined a struct, we
can declare one or more variables of this struct type, where
these variables may be scalar values or arrays:
Now, when we come to declare one or more variables of this
struct type, we can simply say something like:
enum PixelState oldState;
enum PixelState newState;
Also, we can assign values as part of the declaration; for example:
struct Pixel myFavoritePixel;
struct Pixel myArrayOfPixels[100];
Observe that the second example declares an array with 100
elements numbered from 0 to 99. In the case of our multidrip programs, we actually declared multi-dimensional arrays
of these structures, because that’s just the sort of guys and
gals we are.
Unlike arrays, the individual fields (items) in a struct are accessed by name instead of using an integer index:
62
Pixel myFavoritePixel;
Pixel myArrayOfPixels[100];
Compare this to our earlier example where we had to reuse the
struct keyword.
But wait, there’s more...
As always, we’ve really only scratched the surface with regard
to the way in which the enum, struct, and typedef keywords
can be combined and deployed, but I think we can all bask in
the glow of knowing that we now know enough to be just a little
bit dangerous.
Practical Electronics | December | 2020
|