This is only a preview of the March 2021 issue of Practical Electronics. You can view 0 of the 72 pages in the full issue. Articles in this series:
|
Max’s Cool Beans cunning coding tips and tricks
I
’m quite excited as I start to pen these words
because we are poised to gather a bunch of disparate
concepts together and – at the end of this column – we
are all going to say ‘Wow!’ (If you fail to say ‘Wow!’ with
sufficient enthusiasm, I’ll have to send my dear old mother
to visit you to explain the error of your ways.)
Bits and bytes and nybbles, Oh my!
The smallest piece of information that can be stored and manipulated inside a digital computer is a binary digit (a.k.a. ‘bit’), which
can be used to embody two values. We typically think of these
values as representing the numbers 0 and 1. As we discussed in
my previous Tips and Tricks column (PE, February 2020), and assuming we are working with an Arduino microcontroller, these 0
and 1 values are equivalent to LOW and HIGH, respectively, when
we are reading from or writing to the digital input/outputs (I/O).
They are also equivalent to false and true, respectively, if we
are treating them as Boolean logic values.
Since a single bit is limited with regard to the amount of information it can represent, we usually find it more convenient to work
with groups of bits. Some groupings are common, so we’ve given
them special names. For example, the term ‘byte’ refers to an 8-bit
group, while the term ‘nybble’ (or ‘nibble’– I prefer ‘nybble’!) refers
to a 4-bit group. This means that two nybbles make a byte, thereby
demonstrating that computer engineers do have a sense of humor,
albeit one that’s not tremendously sophisticated.
Generally speaking, humans find it hard to wrap our brains
around large numbers presented in binary. For example, 10101100
in binary doesn’t mean much to most people, while its decimal
equivalent of 172 is easier to comprehend.
The binary number system has two digits, 0 and 1. The decimal number system has 10 digits, 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9.
For reasons that will be made clear in future columns, mapping
(translating) values back and forth between binary and decimal is
not as convenient as one might hope. We find it much more efficacious to use the hexadecimal number system, which boasts 16
digits, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, and F. The beauty of
this system is that each hexadecimal digit maps directly onto a
4-bit binary nybble (Fig.5.)
In previous Tips and Tricks columns (PE, August and September
2020) we discussed the difference between signed and unsigned
integers. Also, the fact that in the case of the int (integer) data
type, its size – ie, the number of bits used to represent it – depends
on the computer you are working with. In the case of an Arduino
Uno, for example, an int occupies two bytes (16 bits) of memory.
Let’s suppose we declare an int called intA. Let’s further suppose that we assign it a value of 0xCA35, where the ‘0x’ prefix indicates that this is a hexadecimal value:
AND and OR gates
Decimal Hex adecimal
B inary
0
0
0 0 0 0
At its lowest level, a digital computer
1
0 0 0 1
1
is composed of a humongous number
2
2
0 0 1 0
0 0 1 1
3
3
of on/off switches. These switches
4
4
0 1 0 0
can be implemented using a variety
5
5
0 1 0 1
6
0 1 1 0
6
of technologies, including mechanical,
7
7
0 1 1 1
electromechanical (relays), pneumat8
1 0 0 0
8
9
1 0 0 1
9
ic, and semiconductors (transistors).
1 0
A
1 0 1 0
These days, of course, transistors are
1 1
B
1 0 1 1
1 2
C
1 1 0 0
all the rage – and that’s what we will
1 1 0 1
1 3
D
assume here – but who knows what
1 4
E
1 1 1 0
1 1 1 1
1 5
F
may come tomorrow?
The next level up is to gather small Fig.5. Mapping binary
groups of transistors and use them to and hexadecimal.
implement simple logic functions –
a.k.a. primitive logic gates – and to then connect these gates
together in cunning ways. The two gates we will focus on in
this column are the AND and OR gates (Fig.7).
Observe that we use the & character to represent the AND,
while the | character is used to represent the OR. As we see,
the output (y) from the AND gate is a 1 only if both of its inputs
(a and b) are 1; otherwise, the output is 0. By comparison, the
output of the OR gate is 1 if either of its inputs are 1.
The && and || logical operators
Previously, we were talking about AND and OR in the context
of hardware; that is, the physical logic gates used to build the
computer. Now we are going to consider related functions in
software; that is, our programs.
In last month’s Tips and Tricks (PE, February 2021), we introduced the concept of Boolean variables, which can be assigned values of false and true. We also noted that these
variables are often treated similar to integers in that false
equates to 0 and true equates to 1 or – to be more precise –
true equates to any non-zero value.
The && and || logical operators allow us to construct complex conditional statements. For example, assuming that we’ve
declared integer variables called intA and intB, we could
write a test as follows:
if (((intA == 6) && (intB == 4)) == true)...
Note that we don’t actually need all of the parentheses I’ve
used here because the two == relational (comparison) operators on the left have a higher precedence than the && logical
AND operator, so we could have written this as follows:
if ((intA == 6 && intB == 4) == true)...
int intA = 0xCA35;
16 b its (2 b ytes, 4 nyb b les)
Using Fig.5 as a reference, we see that we’ve
actually loaded intA with a binary value of
8 b its
8 b its
1100101000110101, as illustrated in Fig.6.
15
8
7
0
Note that, rather than writing all 16 bits together in the form 1100101000110101, we
intA 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1
may find it easier to think of them in terms
of 4-bit nybbles and write them in the form
C
A
3
5
1100 1010 0011 0101 (this is what we will
do in the remainder of this column).
Fig.6. An example 16-bit value.
62
AN D
a
y
&
b
a
0
0
1
1
b
0
1
0
1
y
0
0
0
1
OR
a
y
|
b
a
0
0
1
1
b
0
1
0
1
y
0
1
1
1
Fig.7. AND and OR gates.
Practical Electronics | March | 2021
However, if you aren’t sure about operator precedence (https://bit.ly/355G9rC),
then it’s always best to use parentheses
to force things to happen in the order you
want them to, with the added advantage
that parentheses usually make it easier
for other people to work out what you
are trying to achieve.
Let’s think about how the above expression will be evaluated by the compiler,
and therefore how it will be executed by
the computer. If intA is equal to 6, then
this sub-expression will return true (1),
otherwise it will return false (0). Similarly, if intB is equal to 4, then this subexpression will return true (1), otherwise
it will return false (0). The && operator
will only return true (1) if both of its
inputs are true (1), otherwise it will return
false (0). Finally, we use the rightmost
== operator to compare the result from
the && to a value of true (1). Once again,
this comparison will only return a value
of true (1) if both of its inputs are true
(1), otherwise it will return false (0).
Actually, a little thought reveals we can
simplify this test by writing it as follows:
if ((intA == 6) && (intB == 4))...
Although we are in danger of wandering
off into the weeds, a little more thought
reveals that – assuming we’ve also declared
an integer variable called intY – the following would also be a valid statement:
intY = intA && intB;
In this case, if intA contains 0, the &&
operator will regard it as being false
(0); otherwise, if intA contains a nonzero value, the && operator will regard
it as being true (1). Similarly, for intB.
As a result, the && operator will return
true (1) only if both intA and intB contain non-zero values; otherwise, it will
return false (0). And whatever value is
returned from the && operator will be assigned to intY.
We can think of the && logical operator
as being a giant software AND (Fig.8a);
similarly, we can think of the || logical operator as being a giant software
OR (Fig.8b).
The & and | bitwise operators
intA
intA
1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1
1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1
0 1 1 0 1 1 0 1 1 0 1 0 0 1 1 1
0 1 1 0 1 1 0 1 1 0 1 0 0 1 1 1
intB
intB
&
B itwise OR
0 1 0 0 1 0 0 0 0 0 1 0 0 1 0 1
1 1 1 0 1 1 1 1 1 0 1 1 0 1 1 1
(a) B itwise AN D
(b ) B itwise OR
Fig.9. Visualising the & and | bitwise operators.
operator uses a bunch of physical OR
gates to perform an OR on each pair of
corresponding bits.
Assume that we declare three integers,
intA, intB, and intY. Remember that
we are also assuming that we are working with an Arduino Uno, so each of these
integers will be two bytes (16 bits) wide.
Suppose we assign a value of 0xCA35 in
hexadecimal (1100 1010 0011 0101 in
binary) to intA and a value of 0x6DA7
in hexadecimal (0110 1101 1010 0111
in binary) to intB. Now suppose we use
the following statement in our program
to perform a bitwise AND:
(???? ???? ???? ???? in binary). Now suppose that we wish to perform a test to determine if bit 7 of intA is a 1 and, if so,
perform some actions. We could achieve
this using the bitwise AND operator and
a reference value of 0x0080 (0000 0000
1000 0000) as follows:
if ((intA & 0x0080) == true)...
15
The least significant bit of intA is 1, as
is the least significant bit of intB, so an
AND of these means the least significant
bit of the result will also be a 1. Similar operations are performed for all of
the other bits (Fig.9a), resulting in intY
containing 0x4825 in hexadecimal (0100
1000 0010 0101 in binary).
Now assume that we start with intA
and intB containing the same values as
before and we use the following statement
in our program to perform a bitwise OR:
intY = intA | intB;
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0
R eference value of 0x 008 0
&
B itwise AN D
0 0 0 0 0 0 0 0 ?
0 0 0 0 0 0 0
(a) U sing & to test b it 7 of a 16 -b it value
15
0
intA
1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1
0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0
R eference value of 0x 008 0
|
B itwise OR
1 1 0 0 1 0 1 0 1 0 1 1 0 1 0 1
(b ) U sing | to set b it 7 of a 16 -b it value
15
0
intA
1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1
In this case, we perform an OR operation on each pair of corresponding bits
(Fig.9b), resulting in intY containing
0xEFB7 in hexadecimal (1110 1111 1011
0111 in binary).
1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1
R eference value of 0x FFF7F
&
B itwise AN D
1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 1
Testing bits
Suppose we know that intA currently
contains a valid value, but we don’t actually know what this value is. Purely for
the purpose of these discussions, we could
visualise this as 0x???? in hexadecimal
0
intA
intY = intA & intB;
When we are writing programs, we can
also use the & (bitwise AND) and | (bitwise OR) operators. The bitwise AND accepts two integer values
as inputs and performs
I s this true?
I s this true?
I s this true?
I s this true?
(ie, non-z ero)
(ie, non-z ero)
(ie, non-z ero)
(ie, non-z ero)
an AND on each pair of
& &
||
L ogical AN D
L ogical OR
corresponding bits. In
the computer, this is
I f so, then the result is true (1),
I f so, then the result is true (1),
actually implemented
otherwise the result is false (0)
otherwise the result is false (0)
using a bunch of physi(a) L ogical AN D
(b ) L ogical OR
cal AND gates. Similarly, the bitwise OR Fig.8. Visualising the && and || logical operators.
Practical Electronics | March | 2021
|
B itwise AN D
(c) U sing & to clear b it 7 of a 16 -b it value
15
?
0
?
intA
?
?
?
?
?
?
?
?
?
?
?
?
?
?
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1
R eference value of 0x 000F
&
B itwise AN D
0 0 0 0 0 0 0 0 0 0 0 0 ?
?
?
?
(d) U sing & as a mask to isolate the L S nyb b le
Fig.10. Testing, setting, clearing, and
masking bits.
63
A graphical representation of this operation is shown in Fig.10a.
For every bit that is 0 in the reference value, the corresponding output bit from the bitwise AND is forced to 0. Since bit 7
in the reference value is 1, the corresponding output bit will
reflect the value of bit 7 in intA.
If bit 7 in intA is 0, the value returned from (intA &
0x0080) will be zero, which is considered to be false, so
our test will boil down to if (false == true). Obviously,
this will fail, and the statements encompassed by the if()
will not be executed.
Alternatively, if bit 7 in intA is 1, the value returned from
(intA & 0x0080) will be 0x0080 – that is, non-zero – which
is considered to be true, so the test will boil down to if (true
== true). Obviously, this will pass, and the statements encompassed by the if() will therefore be executed.
From our earlier discussions, we know that we could achieve
the same effect using the following statement:
if (intA & 0x0080)...
Now suppose that we wish to perform the actions encompassed by the if() only if bit 7 of intA contains a 0. In this
case, we could use the following statement:
As a reminder, we used various combinations of masks and shifts
to extract the three 8-bit red, green, and blue channels from a
32-bit color value (the most significant 8 bits aren’t used) in an
earlier Cool Beans column (PE, October 2020). So, why are masking operations of interest to us here? Well, I’m glad you asked...
Just call me ‘Lord of the Rings’
Returning to the rings of LEDs we’ve been discussing in our
Cool Beans columns, when it comes to rotating a pixel around
the ring, there’s a trick we can use if the number of pixels in
the ring is a power of two. In the case of my Prognostication
Engine project, for example, the rings each have 16 pixels,
where 16 = 24. As we know, these pixels are numbered from
0 in decimal (0 in hexadecimal; 0000 in binary) to 15 in decimal (F in hexadecimal; 1111 in binary).
Cast your mind back to the Chickens and eggs topic in this
month’s Cool Beans column. Suppose we wished to employ
this approach with the Prognostication Engine’s 16-pixel rings.
With our newfound knowledge of masks, when it comes to
incrementing the pointer in our clockwise rotation (file CBMar21-07.txt), we could replace the original modulo calculation (CurrentP = (CurrentP + 1) % NUM_P;) in our
main loop() function with a masking operation as follows
(the modified portion is shown in bold):
if ((intA & 0x0080) == false)...
Alternatively, we could achieve the same effect as follows:
if (!(intA & 0x0080))...
void loop ()
{
// Turn old pixel off
Neos.setPixelColor(RingXref[CurrentP], COLOR_OFF);
We can read this latter version as ‘If not (intA & 0x0080)
then...’ or ‘If the bitwise AND of intA and 0x0080 doesn’t return
a true (non-zero) value, then…’ I know that this can be a tad
confusing at first, but it won’t be long before you can read and
write this sort of thing without thinking (too hard) about it.
// Increment the pointer
CurrentP = (CurrentP + 1) & MAX_P;
Setting and clearing bits
// Display the result
Neos.show();
delay(PadDelay);
Let’s suppose that intA contains some value like 0xCA35
(1100 1010 0011 0101) and we wish to set its bit 7 to 1 while
leaving its other bits unchanged. We could achieve this using
the bitwise OR operator and a reference value of 0x0080 (0000
0000 1000 0000) as follows.
intA = intA | 0x0080;
As illustrated in Fig.10b, the result is to leave intA containing
0xCAB5 (1100 1010 1011 0101). Now suppose that we change
our minds, and we decide that we really want to clear bit 7 of
intA to 0 while, once again, leaving the other bits unchanged.
We could achieve this using the bitwise AND operator and a
reference value of 0xFF7F (1111 1111 0111 1111) as follows.
intA = intA & 0xFF7F;
As illustrated in Fig.10c, the result, in this case, is to leave
intA containing 0xCA35 (1100 1010 0011 0101).
Masking bits
As you will see, this is a logical extension of what we’ve seen
before. As we did while testing bits, let’s suppose that we
know that intA currently contains a valid value, but that we
don’t actually know what this value is, so we will visualise
this as 0x???? in hexadecimal (???? ???? ???? ???? in binary).
Now suppose that we wish to ‘extract’ only the least-significant nybble and copy this into intY. We could achieve this
as follows (Fig.10d):
intY = intA & 0x000F;
64
// Turn new pixel on
Neos.setPixelColor(RingXref[CurrentP], COLOR_ON);
}
How does this work? Well, if you look at my source code (file
CB-Mar21-13.txt), you’ll see that we’ve defined NUM_P as 16.
Since MAX_P is defined as (NUM_P – 1), this means that
MAX_P is 15 in decimal (F in hexadecimal; 1111 in binary).
For CurrentP equals 0 to CurrentP equals 14, (CurrentP
+ 1) will equal 1 to 15, respectively. In the context of our 16-bit
integers, this corresponds to (CurrentP + 1) values of 0x0001
to 0x000F in hexadecimal, or 0000 0000 0000 0001 to 0000 0000
0000 1111 in binary. If we use the bitwise AND operator to perform a mask operation on these values with a reference value of
MAX_P, which will be automatically promoted to 0x000F (0000
0000 0000 1111), then they will all pass through unscathed.
Now consider what happens when CurrentP equals 15, which
means (CurrentP + 1) will equal 16 in decimal (0x0010 in
hexadecimal; 0000 0000 0001 0000 in binary). In this case, our
bitwise AND will mask out the three most-significant nybbles
and return 0. As a result, our count progresses from 0 to 15 and
then returns to 0 again, which is just what we want.
As an aside, you may have noticed that whenever we call the
Neos.setPixelColor() function in the above code snippet,
instead of the first argument being CurrentP, we’ve changed this
to be RingXref[CurrentP].The reason for this is that, as discussed in a previous Cool Beans column (PE, January 2021), the
physical pixels on the 16-element ring in my prototype aren’t oriented and connected the way we want them to be, so we use the
RingXref[] array to translate what we have in our virtual world
(in the form of CurrentP) into what we need in the real world.
Practical Electronics | March | 2021
Finally, let’s consider using the mask technique for an anticlockwise rotation. Before we proceed, let’s remind ourselves
that an integer in a computer occupies a limited number of bits
(16 in the case of an Arduino Uno). So, what happens if our integer already contains 0xFFFF (1111 1111 1111 1111) and we
add 1 to this? The result is an overflow condition where the integer ends up containing 0x0000 (0000 0000 0000 0000). Contrawise, if our integer contains 0x0000 (0000 0000 0000 0000)
and we subtract 1, we get an underflow condition leaving the
integer containing 0xFFFF (1111 1111 1111 1111).
Now consider the new implementation of our anticlockwise
rotation (file CB-Mar21-14.txt) and compare it to our original implementation (file CB-Mar21-08.txt). In this case, we can replace
the original modulo calculation (CurrentP = (CurrentP +
MAX_P) % NUM_P;) in our main loop() function with a masking operation as follows (the modified portion is shown in bold):
void loop ()
{
// Turn old pixel off
Neos.setPixelColor(RingXref[CurrentP], COLOR_OFF);
// Decrement the pointer
CurrentP = (CurrentP - 1) & MAX_P;
// Turn new pixel on
Neos.setPixelColor(RingXref[CurrentP], COLOR_ON);
// Display the result
Neos.show();
delay(PadDelay);
If you compare this to our clockwise version, you’ll see that
all we’ve done is change the ‘+’ to a ‘–’. So, how does this
one work?
Well, for CurrentP equals 15 to CurrentP equals 1,
(CurrentP - 1) will equal 14 to 0, respectively. This corresponds to (CurrentP - 1) values of 0x000E to 0x0000
in hexadecimal, or 0000 0000 0000 1110 to 0000 0000 0000
0000 in binary. As before, our bitwise AND mask operation
will pass all of these values through unscathed.
Now consider what happens when CurrentP equals 0,
which means (CurrentP - 1) will equal 0xFFFF in hexadecimal; 1111 1111 1111 1111 in binary. In this case, when
our bitwise AND masks out the three most-significant nybbles, the result will be 0x000F (0000 0000 0000 1111), which
is 15 in decimal. As a result, our count progresses from 15
down to 0 and then returns to 15 again, which is what we
are looking for.
The advantage of the mask approach is that, in the case of
a simple microcontroller, replacing a modulo division with
a simple bitwise AND can potentially save a bunch of clock
cycles. The disadvantage is that the modulo approach works
with rings containing an arbitrary number of pixels, while
the mask technique works only if the number of pixels is a
power of two.
What say you?
Phew! We’ve certainly covered a lot of ground this month,
but I hope you’ve found these discussions to be both interesting and useful. As always, I welcome your comments,
questions, and suggestions.
}
Teach-In 8 CD-ROM
Exploring the Arduino
This CD-ROM version of the exciting and popular Teach-In 8 series
has been designed for electronics enthusiasts who want to get to
grips with the inexpensive, immensely popular Arduino microcontroller,
as well as coding enthusiasts who want to explore hardware and
interfacing. Teach-In 8 provides a one-stop source of ideas and
practical information.
The Arduino offers a remarkably effective platform for developing a
huge variety of projects; from operating a set of Christmas tree lights
to remotely controlling a robotic vehicle wirelessly or via the Internet.
Teach-In 8 is based around a series of practical projects with plenty of
information for customisation. The projects can be combined together
in many different ways in order to build more complex systems that can
be used to solve a wide variety of home automation and environmental
monitoring problems. The series includes topics such as RF technology,
wireless networking and remote web access.
PLUS: PICs and the PICkit 3 – A beginners guide
The CD-ROM also includes a bonus – an extra 12-part series based around the popular
PIC microcontroller, explaining how to build PIC-based systems.
EE
FR -ROM
CD
ELECTRONICS
TEACH-IN 8
£8.99
FREE
CD-ROM
SOFTWARE
FOR
THE TEACH-IN
8
SERIES
FROM THE PUBLISHERS OF
INTRODUCING THE ARDUINO
• Hardware – learn about components and circuits
• Programming – powerful integrated development system
• Microcontrollers – understand control operations
• Communications – connect to PCs and other Arduinos
PLUS...
PIC n’MIX
PICs and the PICkit 3 - A beginners
guide. The why and how to build
PIC-based projects
Teach In 8 Cover.indd 1
04/04/2017 12:24
PRICE
£8.99
Includes P&P to UK if
ordered direct from us
SOFTWARE
The CD-ROM contains the software for both the Teach-In 8 and PICkit 3 series.
ORDER YOUR COPY TODAY!
JUST CALL 01202 880299 OR VISIT www.epemag.com
Practical Electronics | March | 2021
65
|