This is only a preview of the May 2026 issue of Practical Electronics. You can view 0 of the 80 pages in the full issue. Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
|
Max’s Cool Beans
By Max the Magnificent
Weird & Wonderful
Arduino Projects
Part 17: Chronological contraptions and solid snakes
Goodness gracious me; this month,
I’m celebrating the 23rd anniversary of
the 23rd anniversary of my 23rd birthday. That’s not something I expected
to hear myself say deep in the mists
of time when I was completing my
maiden voyage around the sun!
All I can say is, “Where did all those
years go?” Well, that’s not entirely
true; I have lots more to say. Later in
this column, we will embark on a new
project to create a Curiously Complex
Chronological Countdown Contraption to mark the years, months, days,
hours, minutes, and seconds until the
start of my 100th birthday. But first…
head at a specified location in the array,
(X, Y) = (7, 5), and move it in a specified direction across the array (EAST).
This means that every time we press
the reset button on our game console,
the snake will respawn at the same spot
and start slithering in the same direction.
You can refresh your recollections
of this program with the code in the
file named CB-may26-code-01.txt. As
usual, all files mentioned in this column
are available as part of the May 2026
download package from the PE website
at https://pemag.au/link/acb7
Let’s add a bit of visual and user interest by modifying this routine so the
snake appears at a random location
Variety is the spice of life
and starts off moving in a random diWe closed last month’s column with rection. The new code file is named
a four-segment snake randomly mean- CB-may26-code-02.txt.
dering around our retro game console’s
Our InitializeSnake( ) function now
14 × 10 array of tricolour light-emitting commences as illustrated in Listing
diodes (LEDs). To keep things simple, 2(a). We’re using the convention that
we started by using our InitializeSnake() the listing number (2 in this case) corfunction to start drawing our snake’s responds to the numerical part of the
matching code file
430 void InitializeSnake ()
name (“02” here).
431 {
432
uint8_t
tmpX;
We’re starting
433
uint8_t
tmpY;
with a 4-segment
434
int8_t deltaX;
snake: one head seg435
int8_t deltaY;
ment and three body
436
segments. On lines
437
// Start with a 4-segment snake
438
SnakeHeadPtr
= START_HEAD_PTR;
443 and 444, we
439
SnakeTailPtr
= START_TAIL_PTR;
use the Arduino’s
440
SnakeLength
= START_LENGTH;
random( ) function
441
to randomly gener442
// Generate random head location and direction
443
tmpX = random(5, 10);
ate the X & Y coordi444
tmpY = random(4, 6);
nates for the snake’s
445
SnakeDirection = random(0, NUM_DIRECTIONS);
initial head position
446
such that it will
447
// Generate delta values (opposite to direction)
appear somewhere
448
if (SnakeDirection == NORTH)
449
{deltaX = 0; deltaY = -1;}
in a small central
450
else if (SnakeDirection == SOUTH)
zone of the display;
451
{deltaX = 0; deltaY = 1;}
close enough to the
452
else if (SnakeDirection == EAST)
middle to feel fair,
453
{deltaX = -1; deltaY = 0;}
454
else if (SnakeDirection == WEST)
but random enough
455
{deltaX = 1; deltaY = 0;}
to keep things interListing 2(a): the modified start of the snake initialization routine. esting.
6
On line 445, we randomly select the
direction in which the snake is heading.
On lines 448 through 455, we generate the delta values we’ll use to build
the body of our snake. In this case,
we want these values to be opposite
to the snake’s direction. For example,
if the snake starts off moving east, we
want its body segments to appear to
the west of the head.
The remainder of this function remains unchanged from last month:
first generating the snake, loading it
into our circular buffer, then drawing
it on our array.
The problem with randomness
There’s a classic Dilbert cartoon in
which our hero visits the accounting
department and is introduced to their
“random number generator.” The creature assigned to this role is monotonously chanting, “Nine nine nine nine
nine nine…”, clearly repeating the
same number over and over.
Dilbert asks, “Are you sure that’s
random?” And the manager of the
department replies, “That’s the problem with randomness. You never can
be sure.”
With our current Snake program, if
you look closely, you’ll see that every
time we press the reset button, the snake
appears in the same “random” position and moves in the same “random”
direction. Hmm, that’s perhaps not
quite as “random” as we might have
been expecting.
Generating a truly random number is
a lot harder than one might think. The
Arduino Uno (or Nano) does not generate truly random numbers. Instead,
it generates pseudo-random numbers.
Although these appear to be statistically random, they are actually produced by a deterministic mathematical algorithm called a pseudo-random
number generator (PRNG).
Practical Electronics | May | 2026
This means that if we start the algorithm with the same initial (‘seed’)
value, it will always produce the same
sequence of “random” numbers.
We can easily verify this using the
simple test program shown in Listing
3(a), from the file named CB-may26code-03.txt. On line 5, we use the Arduino’s built-in randomSeed() function
to specify the seed value that will be
used by the random() function.
Remember to open the serial monitor window in the Arduino’s integrated development environment (IDE) so
you can see what’s happening, then
upload this program into your Arduino.
As you’ll see, we get exactly the same
six-number sequence (38, 52, 78, 17,
41, 58) every time we reset the console
and rerun the program.
One point of interest here is that
good pseudo-random number generators exhibit a strong sensitivity to initial conditions, meaning that even a
one-bit change in the seed produces a
completely different output sequence.
Try this for yourself by changing our
seed value from 1234 (10011010010
in binary) to 1235 (10011010011 in
binary), which involves flipping only
the least significant bit, and compare
the new sequence (45, 1, 4, 28, 71, 30)
to the old one.
On a standard Arduino (eg, Uno
or Nano), the default seed value is 1.
More precisely, if we don’t call the
randomSeed( ) function ourselves,
then the internal PRNG is initialised
as though we had called random
Seed(1). This is why, unless we intervene, every “random” game begins
in exactly the same way.
In many embedded applications,
pseudo-random values are perfectly adequate; repeatability can sometimes help with debugging. In other
cases, however, we may want things
to appear to be a tad more random. A
cheap-and-cheerful approach to increasing the perceived randomness is
illustrated in Listing 4(a), from the file
named CB-may26-code-04.txt.
All we’ve done is modify the way
we specify the seed value on line 5.
If analog input A0 is left floating (unconnected), it will pick up electrical
noise. That means the voltage on this
pin will vary each time we read it,
yielding a different seed and, hence,
a different sequence.
Try running this new version of our
test code several times and observe
that the generated sequence differs
each time you upload the program and
each time you press the reset button
on the console.
I’ve just added this analog-based
random seed generation to the setup()
function in our Snake program (CBPractical Electronics | May | 2026
may26-code-05.txt). Now, whenever
we press the reset button to start a new
game, the snake really does appear at a
random location heading in a random
direction (well, random enough for us).
Taking control
The modified code for the north
switch in the ProcessSwitches( ) routine is shown in Listing 6(a) (CB-may26code-06.txt). The only change occurs on
line 267, which we’ve added to set the
snake’s direction to NORTH when the
north switch is pressed (goes active).
Similar changes are required for the
south, east, and west switches.
Now, it’s time to take our snake’s
reins in our hands, which is another
thing I didn’t expect to hear myself
1
void setup()
saying when I woke up this morning. 2
{
You might recall that our current 3
Serial.begin(9600);
UpdateSnake() function starts as illus- 4
randomSeed(1234);
trated in Listing 5(a). The key part to 5
observe here is that, until now, using 6
7
Serial.println();
lines 497 through 502, we’ve been 8
Serial.println("======");
randomly generating the snake’s new 9
for (int i = 0; i < 6; i++)
{
direction. After this, the function gen- 10
Serial.println(random(100));
erates the next X/Y location for the 11
12
}
head, removes the tail segment, draws 13 }
the new head segment, and so forth.
Since we now plan to take control Listing 3(a): using a fixed random seed.
void setup()
ourselves, we can remove lines 497 1
{
through 502 and line 486, which we 2
3
Serial.begin(9600);
no longer need.
4
All that remains is to bring our di- 5
randomSeed(analogRead(A0));
rection control switches into play. Re- 6
Serial.println();
member that our loop() function repeat- 7
Serial.println("======");
edly calls UpdateSnake(), which up- 8
9
for (int i = 0; i < 6; i++)
dates the snake’s position on the array, 10
{
UpdateSwitches(), which debounces 11
Serial.println(random(100));
}
our switches, and ProcessSwitches(), 12
which performs any game-related ac- 13 }
tions associated with the switches Listing 4(a): a more random random seed.
being pressed or
483 void InitializeSnake ()
released.
484 {
What we are 485
uint8_t tmpSnakeHeadPtr;
uint8_t tmpDirection;
going to do is 486
uint8_t
tmpX;
tweak the Pro 487
uint8_t
tmpY;
cessSwitches( ) 488
489
int8_t deltaX;
routine so that 490
int8_t deltaY;
pressing any of 491
if ( (TimeNow - SnakeThen) >= SNAKE_MOVE_TICKS)
the north, south, 492
{
east, or west but- 493
494
// Pointer for new head in circular buffer
tons updates the 495
tmpSnakeHeadPtr = (SnakeHeadPtr+1) % NUM_SEGMENTS;
SnakeDirection 496
// Pick a new direction (can't be 180 degrees)
variable. It’s this 497
do {
new value that the 498
499
tmpDirection = random(0, NUM_DIRECTIONS);
UpdateSnake( ) 500
} while (tmpDirection == (SnakeDirection ^ 1));
function will use 501
the next time it’s 502
SnakeDirection = tmpDirection;
called.
Listing 5(a). automatically generating the snake’s direction.
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// Switch North
if (Switches[SW_NORTH].swState == JUST_GONE_ACTIVE)
{
// Mandatory
Switches[SW_NORTH].swState = WAITING_FOR_ALL_ONES;
Added line (similar lines added
// Optional (game-specific)
for the other three switches)
SnakeDirection = NORTH;
// Serial.println("Switch North just went active");
}
else if (Switches[SW_NORTH].swState == JUST_GONE_INACTIVE)
{
// Mandatory
Switches[SW_NORTH].swState = WAITING_FOR_ALL_ZEROS;
// Optional (game-specific)
// Serial.println("Switch North just went inactive");
}
Listing 6(a): tweaking the switch-handling routine.
7
Try running the latest version of our
program and bask in the glow of controlling your snake.
But what about…
I’m afraid that there’s a fly in the
soup and an elephant in the room
(I never metaphor I didn’t like). Did
you spot the obvious omission with
respect to the latest and greatest version of our code?
Previously, when our snake was
wending its way around the array
under its own steam, we ensured it
didn’t perform an ‘about-face’ (ie, a
180° change in direction). For example, if the snake was heading east, we
allowed it to keep on heading east, or
change to moving north or south, but
not west.
Now that we are controlling the snake
ourselves, we have no such safeguard.
So, what will happen if the snake is
heading east and we press the west
button? Why don’t you look at the
code to see if you can predict what
will occur, then run the program and
perform this test to see if what takes
place is what you expected?
As you’ll see, if we perform a 180°
about-face, the snake simply passes
through itself, which seems a little
unrealistic. At this point, we have
three main options. First, we could
leave things as-is, which many players
would find logically and aesthetically
unpleasing. Second, we could cause
the snake to ‘die’ and end this level of
the game, which many players would
consider overkill (no pun intended).
The third option, which was adopted by many classic Snake implementations, was to simply ignore any attempt by the user to reverse direction,
so that’s what we’ll do.
Implementing this is easy-peasy,
lemon squeezy. All we have to do is
slightly refine our existing switch processing code in the ProcessSwitches()
routine. The modified code for the
north switch is shown in Listing 7(a)
(CB-may26-code-07.txt).
All we’ve done is add a check on
line 267 to ensure that, when the north
button is pressed, we only set the
snake’s direction to NORTH if it’s not
already going SOUTH. Corresponding
changes are required for the south, east
and west switches.
PEEK and POKE
On classic 8-bit machines like the
Apple ][, Commodore 64, BBC Micro
etc, memory wasn’t abstracted away.
Hardware registers, screen memory,
sound chips and system flags all lived
at known memory addresses.
Also, on most early computers, the
BASIC language (Beginner’s All-purpose
8
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// Switch North
if (Switches[SW_NORTH].swState == JUST_GONE_ACTIVE)
{
// Mandatory
Switches[SW_NORTH].swState = WAITING_FOR_ALL_ONES;
// Optional (game-specific)
if (SnakeDirection != SOUTH) SnakeDirection = NORTH;
// Serial.println("Switch North just went active");
}
else if (Switches[SW_NORTH].swState == JUST_GONE_INACTIVE)
{
// Mandatory
Switches[SW_NORTH].swState = WAITING_FOR_ALL_ZEROS;
// Optional (game-specific)
// Serial.println("Switch North just went inactive");
}
Listing 7(a): the refined switch-handling routine.
597
598
599
600
601
602
603
604
605
606
607
608
609
// Set the color of pixel at [x, y] (i.e., [column, row])
void PokePixel (uint8_t x, uint8_t y, uint32_t color)
{
uint8_t n = GetNeoNum(x, y);
Neos.setPixelColor(n, color);
}
// Read the color of pixel at [x, y] (i.e., [column, row])
uint32_t PeekPixel (uint8_t x, uint8_t y)
{
uint8_t n = GetNeoNum(x, y);
return(Neos.getPixelColor(n));
}
Listing 8(a): peeking and poking pixels.
537
538
539
540
541
542
543
544
545
546
547
548
void PlaceFood()
{
uint8_t tmpX;
uint8_t tmpY;
do {
tmpX = random(0, NUM_COLS);
tmpY = random(0, NUM_ROWS);
} while (PeekPixel(tmpX, tmpY) != EMPTY_COLOR);
PokePixel(tmpX, tmpY, FOOD_COLOR);
}
Listing 8(b): placing a random piece of food.
Symbolic Instruction Code) wasn’t just
a programming language; it was the operating environment. When you powered the machine on, you were dropped
straight into the BASIC prompt.
Two foundational, ubiquitous and
slightly dangerous BASIC commands
are PEEK() and POKE(). PEEK(address)
returns the numeric value stored
at a given memory location, while
POKE( address, value) writes a numeric value directly into the specified
memory location.
Currently, we use our DrawPixel( )
function to set (write) the colour of a
pixel at a specified (x, y) location on the
array. Thus far, we haven’t felt the need
to be able to access (read) the colour
of a pixel, but that’s about to change.
As a first step, I’ve decided to rename
our DrawPixel( ) function to Poke
Pixel() (see CB-may26-code-08.txt). If
you are modifying your own code, I
suggest changing the function name
and all calls to it, then verifying that
everything still works by recompiling
before proceeding further.
Next, I’ve added a new PeekPixel()
function. Both of these functions are
shown in Listing 8(a).
Ooh, tasty!
A core concept of the Snake game is
that the snake grows larger as it munches on tasty morsels of ‘food’. As usual,
we’ll take this step-by-step. We’ll start
by defining EMPTY_COLOR as BLACK
and FOOD_COLOR as YELLOW (or any
colour of your choice, remembering that
we’ve already defined HEAD_COLOR
as MAGENTA and BODY_COLOR as
GREEN).
Next, we’ll create a utility function
called PlaceFood() that places a pixel
of ‘food’ at a randomly selected unoccupied location. I toyed with the idea
of calling this function ServeSnack(),
but I decided that would be super silly
(as you must already know, I’m a very
serious person). This function is shown
in Listing 8(b).
On line 542, we initiate a do loop.
On lines 543 and 544, we generate
random X and Y values. We keep on
Practical Electronics | May | 2026
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
void DealWithDeadSnake ()
{
uint8_t tmpPtr = SnakeHeadPtr;
// Color the snake red
delay(1000);
for (uint8_t i = 0; i < SnakeLength; i++)
{
PokePixel(Snake[tmpPtr].x, Snake[tmpPtr].y, DEAD_COLOR);
tmpPtr = (tmpPtr + NUM_SEGMENTS - 1) % NUM_SEGMENTS;
Neos.show();
delay(100);
}
delay(1400);
// Start a new game
InitializeSnake();
// Generate and draw snake
InitializeDisplays(); // 7-Segment displays
}
Listing 9(a): snake funeral arrangements.
doing so until we find an unoccupied
pixel using the test on line 545.
I should point out that this is far
from being the most efficient way to
achieve this functionality, but it does
give us an opportunity to take our new
PeekPixel() and PokePixel() functions
for a spin.
One more minor modification is to
call our new PlaceFood() function at
the end of the InitializeSnake( ) routine. We do this just after we’ve generated our initial snake and drawn it
on the array.
Try running this new version of the
program and observe that every time
you press the console’s reset button,
both the snake and the yellow food
pixel appear at random locations on
the array.
Feed me!
At the moment, our snake doesn’t
know what to do with any food it encounters. If we use our direction controls to guide the snake to the food,
it simply tramples it underfoot (you
know what I mean). So, the next step
is to modify our UpdateSnake() function so it can ‘see’ and ‘eat’ the food.
To be honest, this takes a bit of mental
gymnastics to wrap one’s head around.
The way the UpdateSnake() function
currently works is as follows:
1. Based on the present location and direction of the snake’s head segment,
determine the X and Y array values
for the new head segment location.
2. Remove the old tail segment.
3. Set the old head segment's location
to the snake’s body colour.
4. Set the new head segment's location
to the snake’s head colour.
As we discussed last month, the
reason we remove the old tail segment
before drawing the new head segment,
thereby allowing the head to move into
the square the tail has just vacated, is
that this reflects the behaviour of the
classic Snake game.
By removing the tail first, we avoid
Practical Electronics | May | 2026
falsely detecting a collision with the
tail-end of the snake’s own body and
allow the familiar smooth, continuous motion players expect. The thing
is that if the snake’s head moves into
a pixel containing food and eats it, the
way the snake grows is by not removing
the old tail segment. This means that
we need to modify the flow as follows:
1. Based on the present location and direction of the snake’s head segment,
determine the X and Y array values
for the new head segment location.
2. If the new head segment location
contains food, don’t delete the tail
segment; otherwise, delete it.
3. Take note of whatever is in the new
head segment location: options are
nothing (empty), food, or a segment
of the snake’s body.
4. Set the old head segment to the
snake’s body colour.
5. Set the new head segment to the
snake’s head colour.
6. Perform appropriate actions according to what was in the new head
segment location before we moved
the head.
Regarding point 6, we now have
three options:
i. If the location was empty, then
there’s nothing left for us to do.
ii. If the location contained food, we
need to increment and display the
new score (ie, the new length of
the snake). We also need to call our
PlaceFood() function to present a
new pixel snack.
iii. If the location contained one of
the snake’s body segments, we set
a status flag to say that the snake
just died (we’ll handle this outside
of this routine).
I’ve just implemented all of these
changes in the file named CB-may26code-09.txt. I have also added a global
variable called SnakeStatus. This starts
off being set to SNAKE_IS_ALIVE. When
the player causes the snake to run into
itself, the UpdateSnake() function sets
this flag to SNAKE_JUST_DIED.
I’ve also modified the end of the
loop() function. After calling Update
Snake(), we test whether the snake has
just died. If so, we call a new DealWith
DeadSnake( ) function, as illustrated
in Listing 9(a).
You can make your version as fancy
as you wish, or you can keep it ‘mean
and lean’. In my case, I opted to pause
for a second to let me reflect on my mistake, then colour the snake’s segments
red, starting with the head and ending
with the tail, with a 1/10th of a second
(100ms) delay between each segment.
Finally, on lines 593 and 597, we
reinitialise the snake and the 7-segment
displays, after which we’ll start the
game anew. I’m running the latest version of the program as we speak, and
it’s starting to look very tasty indeed.
It’s the final countdown (do do do dooo)
I suddenly find The Final Countdown, the 1986 hit by the Swedish
rock band Europe, rattling around
my noggin.
This is because I’ve decided to tweak
our InitializeDisplays() function. At the
moment, this kicks off by displaying
the game level (we’re starting at one,
but I envisage we’ll eventually support
nine levels) and the length of our starting snake (currently four):
L1 ☐0 04
We are using the three least significant digits to display the snake’s length,
which has a maximum possible value
of 140. I’m using the ☐ character above
to represent a blank, unlit digit on the
7-segment displays.
What we’re going to do is add a
countdown before the game (or a new
level) starts. In the case of our current L1
implementation, this will look like the
following on the 7-segment displays:
55 55 55
44 44 44
33 33 33
22 22 22
11 11 11
L1 ☐0 04
I’ve just added this to our code (in
the file named CB-may26-code-10.txt)
and I think it looks great. Of course,
you may decide to omit this completely
or to do it differently. You’re in charge
of your own console and the master
or mistress of your own destiny (just
don’t let this power run to your head).
The world is our Oyster Kilpatrick
At this point, we have so many options open to us that I barely know
where to start. One thing we could do
is define the characteristics of the first
9
three levels: L1, L2, and L3, and what
it takes to progress from one level to
the next.
For example, in L1, we currently
update the snake’s position every 500ms
(twice per second). We could reduce
this to 400ms in L2 and 300ms in L3.
We could also say that if we are in L1,
we automatically proceed to L2 when
the snake reaches 30% of its maximum
possible length (140), which would be
42 segments. If the snake dies before
we reach this magic number, we have
to replay the level.
Similarly, if we are in L2, we could
say that we need the snake to reach
40% of its maximum length before
proceeding to L3. And if we are in L3,
the snake must reach 50% of its maximum length to progress to L4.
Levels L4, L5, and L6 could be identical to levels L1, L2, and L3, respectively, except that the food only persists at its current location for a random
number of cycles before disappearing
and reappearing elsewhere in the array.
In the same vein, levels L7, L8, and
L9 could be identical to levels L4, L5,
and L6, except that, in addition to regular food pixels (YELLOW), we are also
occasionally presented with poisoned
food pixels (CYAN), the eating of which
will cause the snake to slither off this
mortal coil.
That seems appropriate given that
cyanide (HCN) is quite poisonous.
Anyway, the suggestions above are
just “top of my head” ideas. You can
construct your levels any way you wish.
The world is truly your oyster (or sea
creature of your choice).
There are many other possibilities.
Some are aesthetic, like making the
food and poison pixels flash, as we did
in our Etch A Sketch program. Others
are more functional, such as removing
the wraparound capability and making
the array’s walls (edges) ‘solid’.
This would present a new choice:
should the snake die if it runs headfirst into a wall, or should it simply
stop moving and wait for the user to
turn its head before setting off again?
More ideas are popping into my
head, like having occasional ‘special’
food pixels (GREEN) appear that, if
eaten, make the snake grow by two
or three segments at a time. We could
also keep track of how many moves
the snake makes between eating food
particles. If the player takes too long,
the score (and the snake’s length) could
start to shrink.
I should probably point out that I
won’t be adding any of the aforementioned functionality in future columns,
because we have bigger fish to fry.
Having said this, I may occasionally
surprise you by writing something like,
10
Binary
27 26 25 24 23 22 21 20
Dec
Hex
Binary
7
6
5
4
3
2
1
0
‒2 2 2 2 2 2 2 2
Decimal
Hex
0 0 0 0 0 0 0 0
+0
0 0
0 0 0 0 0 0 0 0
+0
0 0
0 0 0 0 0 0 0 1
+1
0 1
0 0 0 0 0 0 0 1
+1
0 1
0 0 0 0 0 0 1 0
+2
0 2
0 0 0 0 0 0 1 0
+2
0 2
0 0 0 0 0 0 1 1
+3
0 3
0 0 0 0 0 0 1 1
+3
0 3
:
:
:
:
:
:
0 1 1 1 1 1 0 1
+125
7D
0 1 1 1 1 1 0 1
+125
7D
0 1 1 1 1 1 1 0
+126
7 E
0 1 1 1 1 1 1 0
+126
7 E
0 1 1 1 1 1 1 1
+127
7 F
0 1 1 1 1 1 1 1
+127
7 F
1 0 0 0 0 0 0 0
+128
8 0
1 0 0 0 0 0 0 0
‒128
8 0
1 0 0 0 0 0 0 1
+129
8 1
1 0 0 0 0 0 0 1
‒128 + 1 = ‒127
8 1
1 0 0 0 0 0 1 0
+130
8 2
1 0 0 0 0 0 1 0
‒128 + 2 = ‒126
8 2
:
:
:
:
:
:
1 1 1 1 1 1 0 0
+252
FC
1 1 1 1 1 1 0 0
‒128 + 124 = ‒4
FC
1 1 1 1 1 1 0 1
+253
FD
1 1 1 1 1 1 0 1
‒128 + 125 = ‒3
FD
1 1 1 1 1 1 1 0
+254
FE
1 1 1 1 1 1 1 0
‒128 + 126 = ‒2
FE
1 1 1 1 1 1 1 1
+255
F F
1 1 1 1 1 1 1 1
‒128 + 127 = ‒1
F F
(a) Unsigned binary
(b) Signed binary
Fig.1: unsigned vs signed binary representations.
“You can download a new version of
the snake program that…”
The sound of silence
I love the song The Sound(s) of Silence by Simon and Garfunkel (oddly,
its title was plural on the album but
became singular for the single release).
My point is that, while playing the current version of my snake game, I realised that the sound (or lack thereof)
was deafening.
It struck me that this game, and others
to come, would be greatly enhanced by
the addition of some simple sound effects, like a cheerful ‘cheep’ when the
snake eats food, or a mournful melody
when the snake dies, for example. The
more I think about this, the more I want
to do it, so that’s one of the things we
will implement next month.
A sign of the times
Way back in the mists of time (the
January 2026 issue of PE, to be more
precise), we introduced the concepts
of logical and arithmetic shifts. At that
time, I wrote, “The reason we might
prefer an arithmetic shift right over
a logical shift right with signed integers is too involved to go into here
(we’ll discuss this further in our next
column)”.
Sad to relate, we never did that. Well,
it’s time to stop “prevaricating around
the bush”, as Wallace might say.
The differences between unsigned
and signed integers, and the many ramifications that flow from them, can be
confusing to the uninitiated. But the
deeper we dig into tasks like bit manipulation (eg, for use with the games
for our console), the more we need to
understand what’s going on; otherwise,
many things won’t make much sense.
For the sake of simplicity, we’ll focus
on eight-bit values. Let’s start with the
fact that eight bits can be used to represent 28 = 256 different combinations of
0s and 1s. We can use these patterns to
represent anything we wish, including
numbers, letters, and symbols.
In the case of a variable of type uint8_t
(unsigned 8-bit integer), we can represent positive decimal (dec) values
in the range 0 to +255, as illustrated
in Fig.1(a). These correspond to hexadecimal (hex) values from 00 to FF.
Now suppose we wish to represent
both positive and negative values. There
are several ways we could do this, but
the technique used by today’s computers is commonly referred to as ‘signed
binary’, although the formal terminology is “two’s complement representation” (we’ll look into one’s complement
and two’s complement representations
more next month).
In the case of a variable of type int8_t
(signed 8-bit integer), we can represent
values in the range -128 to +127, as illustrated in Fig.1(b). These still correspond to hexadecimal (hex) values in
the range 00 to FF, although there is
nothing stopping you from thinking of
half that range as negative hexadecimal numbers, if you prefer.
The signed binary representation can
be tricky to wrap your brain around
the first time you see it but it’s actually amazingly efficient and efficacious.
The key concept is that the most significant bit (bit 7, in the case of an 8-bit
value) doesn’t represent only the sign
Practical Electronics | May | 2026
uint8_t uA = 204;
int8_t sB = -1;
uint8_t uY = uA + sB;
int8_t sA = -52;
int8_t sB = -1;
int8_t sY = sA + sB;
1 1 0 0 1 1 0 0
(uA = 204)
+
1 1 1 1 1 1 1 1
(sB = –1)
=
1 1 0 0 1 0 1 1
(uY = 203)
1 1 0 0 1 1 0 0
(sA = –52)
+
1 1 1 1 1 1 1 1
(sB = –1)
=
1 1 0 0 1 0 1 1
(uY = –53)
(a) Unsigned + signed
(b) Signed + signed
Fig.2: adding unsigned and signed integers.
Original value in variable c
c = c >> 1
Result in variable c
1 1 1 0 0 1 1 1
0 1 1 1 0 0 1 1
LSB “falls off the end”
0 shifted into MSB
(a) logical shift right on signed 8-bit value (int8_t)
Original value in variable c
c = c >> 1
Result in variable c
1 1 1 0 0 1 1 1
Old MSB shifted into new MSB
1 1 1 1 0 0 1 1
LSB “falls off the end”
(b) arithmetic shift right on signed value (int8_t)
Fig.3: logical versus arithmetic right shifts.
of the number; instead, it represents a
negative quantity (-128, in the case of
an 8-bit value).
The remaining bits continue to represent their regular positive values.
Although this may, at first glance,
appear to be an outrageously complicated solution to a seemingly simple
problem, there’s method to its madness. For example, exactly the same
underlying operations are used by the
computer to add and subtract unsigned
and signed integers.
Furthermore, it’s possible to ‘mix
and match’, adding and subtracting
one type to/from another, just so long
as we’re confident the result will fall
within the valid range of the destination variable.
For example, we can add or subtract a variable of type int8_t (where
this variable may contain a positive
or negative value) to a variable of type
uint8_t, just so long as we know that
the result won’t be less than 0.
In Fig.2(a), we add an unsigned integer containing +204 to a signed integer containing -1 and store the result
in another unsigned integer. Not surprisingly, 204 + -1 equals 203. By comparison, in Fig.2(b), we add a signed
integer containing -52 to another signed
integer containing -1. In this case, we
aren’t surprised to discover that -52
+ -1 = -53.
The real point here is that the bit patterns of the variables and the operations
performed are identical in both cases.
The only difference is in how we interpret what these bit patterns represent.
Practical Electronics | May | 2026
You may recall that we’ve already
seen examples of Fig.2(a) in our console code. Consider Listings 2(a) and
5(a). In both cases, we declare tmpX
and tmpY as uint8_t, and deltaX and
deltaY as int8_t. Later, we add deltaX
to tmpX and deltaY to tmpY.
One important point worth noting
is that it’s usually a good idea to use
signed integer variables as for( ) loop
control variables. Suppose you get into
the habit of using unsigned integers as
your control variables; for example,
for (uint8_t i = 0; i < 10; i++) {}. This
will work perfectly well.
The problem only arises when
you subsequently implement a loop
like for (uint8_t i = 10; i >= 0; i--) {}.
Because the control variable is unsigned, it can never be less than 0.
When i reaches 0 and is decremented, it wraps around to 255, and the
loop continues forever.
Ooh, shifty!
The bitwise logical operations &, |,
^, and ~ (AND, OR, XOR and NOT, respectively) do not care about numeric
interpretations; they simply manipulate the underlying bit patterns. That
means they operate on signed and unsigned integers in exactly the same way
at the bit level.
In most CPU conversations, shift
instructions are grouped with logical
(bitwise) instructions because they
operate on bits rather than performing
arithmetic in the mathematical sense.
However, there’s a wrinkle when it
comes to shifts.
For the sake of simplicity, let’s
assume we’re working with single-bit
shifts. In practice, left shifts behave the
same at the bit level for signed and unsigned integers, provided we remain
within the representable range. In this
case, a 0 is shifted into the least significant bit (LSB) and the most significant bit (MSB) ‘falls off the end’. This
is known as a ‘logical shift’.
A right shift performed on an unsigned integer also acts as a logical shift.
In this case, a 0 is shifted into the MSB
and the LSB ‘falls off the end’. However, things get trickier when performing a right shift on a signed integer. In
this case, there are two possibilities.
The first is to perform a logical shift,
in which case a 0 will be copied into
the MSB, as illustrated in Fig.3(a). The
second is to perform an arithmetic shift,
in which case the original MSB (0 or
1) will be copied into the new MSB,
as illustrated in Fig.3(b).
The problem is that the silly sausages
who created the original C (and later
C++) specifications left this behaviour
implementation-defined, meaning the
creators of the compiler must document
their choice, but the standard does not
require a specific one.
Most modern compilers do the logical thing (no pun intended) and perform arithmetic right shifts on signed
integers but that isn’t mandatory. So
we cannot rely on the result, especially if we port our code to a different compiler.
Why does this matter? Early microprocessors and microcontrollers didn’t
have hardware multipliers or dividers,
so these operations were implemented
using many simple instructions and
many clock cycles. Savvy programmers
knew that they could implement certain multiplication and division operations using shift instructions.
Let’s start with some unsigned integer examples. Suppose we have a
uint8_t value of 00001100 in binary
(12 in decimal). Shifting our original
value left by one bit gives 00011000
(24 in decimal), which is equivalent
to multiplying it by two. Similarly,
shifting it by two bits gives 00110000
(48 in decimal), which is equivalent
to multiplying it by four.
Similarly, shifting our original value
right by one bit gives 00000110 (6 in
decimal), which is equivalent to dividing by two, while shifting it right
by two bits gives 00000011 (3 in decimal), which is equivalent to dividing
it by four.
The upshot of all this is that multiplications or divisions by powers of two
can be performed in a single clock cycle.
There are lots of variations on this.
For example, a common requirement
11
is to multiply by ten, and N * 10 is the
same as (N * 8) + (N * 2), which is the
same as (N << 3) + (N << 1).
This means that multiplying an 8-bit
integer by ten can be performed using
two shifts and one addition, consuming only three clock cycles. As we’ve
previously discussed, in the absence
of a hardware multiplier, modern compilers will do this for us, but it’s still
nice to know what’s going on ‘under
the hood’ (or bonnet, if you prefer).
Things also work as expected with
signed integers when shifting positive
or negative numbers to the left (assuming we don’t exceed the range of values
that can be supported).
Suppose we have an int8_t value of
11101100 binary (-128 + 108 = -20).
Shifting this value by one bit to the
left yields 11011000 (-128 + 88 = -40),
which is equivalent to multiplying by
two, while shifting it two bits to the
left will result in 10110000 (-128 + 48
= -80), which is equivalent to multiplying by four.
Now this is where things get tricky.
This will also work with signed integers when shifting positive or negative numbers to the right, just so long
as the compiler implements an arithmetic shift. For example, shifting our
original value one bit to the right yields
11110110 (-128 + 118 = -10), which is
the same as dividing by two.
However, if the compiler implements this as a logical shift, we’ll end
up with 01110110, which is equivalent
to +118, which isn’t what we want at
all. The bottom line is that with unsigned values, shifts are obedient, but
with signed values, they demand supervision.
Chronological contraptions
As noted earlier, we are poised to
embark on a new project to create a
Chronological Contraption to mark the
years, months, days, hours, minutes,
and seconds until the start of my 100th
birthday celebrations. Of course, you
can use it to count down to anything
you want. It can also act as a clock.
As usual, I’m collaborating on this
project with my friend Joe Farr, because ‘two heads are better than one’
when it comes to making decisions and
designing things. Also, it’s more fun
to have someone to bounce ideas off.
For this project, we’ve decided to use
Soviet-era vacuum fluorescent display
(VFD) tubes (Photo 1).
We can think of VFDs as something
of a cross between traditional vacuum
tubes and Nixie displays. Like tubes,
VFDs use thermionic emission within
a vacuum envelope, and, like Nixies,
they display glowing digits behind
glass. The key difference is that VFDs
12
I still have no idea why
they call me "Mad Max"!
use phosphors rather than gas discharge
and operate at far lower voltages.
VFDs are attractive for this project because they work well over a wide temperature range, offer excellent brightness and viewing angle, and have that
distinctive ‘retro-tech’ aesthetic beloved by your humble narrator.
The IV-6 and IV-11 versions of these
tubes are readily available as surplus
NOS (new old stock) parts from hobby
electronics sellers and on eBay, where
IV (ИВ in Cyrillic) stands for “Indicator, Vacuum-fluorescent” (or “Indikator Vakuumny” in Russian).
I opted for the IV-11 model/type because these are bigger, and bigger is
better. Having said this, these tubes are
still only around two inches (50mm)
tall and 0.8-0.9 inches (about 20-22mm)
in diameter.
I’m using four digits to represent
the year and two digits each to represent each of the remaining five fields,
so 14 digits in all. I’ve purchased 20
tubes, just in case.
However, I understand that not all
readers will want to undertake a ‘full
Monty’ implementation, so we’ve come
up with a modular design in which each
tube sits on its own circuit board. You
can connect as many (or as few) of these
boards together as you wish. We’ve
also designed small spacer boards to
separate the various fields.
Next time
In addition to adding sound effects
to our retro games console, we’ll delve
deeper into the design of the Chronological Contraption’s circuits and circuit boards.
As always, if you have any thoughts
you’d care to share on anything you’ve
read here, please feel free to email me
PE
at max<at>clivemaxfield.com
Photo 1:
a vacuum
fluorescent
display (VFD)
inside a glass
tube. They
also come
in multi-digit
rectangular
assemblies.
Source:
https://
displaytubes.org/
vfd/reflectoriv-12/
5-year
collections
2019-2023
£49.95
2018-2022
£49.95
2017-2021
£49.95
2016-2020
£44.95
Purchase and download at:
www.electronpublishing.com
Practical Electronics | May | 2026
|