This is only a preview of the March 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:
Articles in this series:
Items relevant to "Power LCR Meter Part 2":
|
Max’s Cool Beans
By Max the Magnificent
Weird & Wonderful
Arduino Projects
Part 15: gentlepersons, start your cartridges!
E
eek (and I mean that most sincerely)! I can’t believe that it’s
almost March 2026. Even when
I was a little kid, I always knew when
it was the first day of March.
This wasn’t because I was unusually precocious (well, not only that), but
rather because my mum would bounce
into my bedroom, shout “White Rabbits” at the top of her voice and bounce
out again, leaving my ears ringing and
my head spinning. One day, I’ll have
to ask her why she used to do this and
why I’m now conditioned to carry on
the tradition.
Making connections
Let’s commence this column by
dabbling on the hardware front. Last
month, we finally finished wiring the
internals of our retro games console (a
thousand hurrahs!).
Since we first commenced work on
this bodacious beauty, the idea has
been to end up with the main console
accompanied by a collection of plugin game cartridges, much the way
things were before the days
of the internet, digital distribution and downloadable
content.
In traditional 1980s and
1990s implementations, the
‘brains’ of the system (the
main processor) lived in
the console, while the game
cartridges were essentially
dumb read-only memory
(ROM) modules. By comparison, we dare to be different
because our console is more
of a shared, unintelligent
Screen 1: our Arduino Uno-based game cartridge board design.
backplane that supplies power, buttons, and displays.
In our implementation, each cartridge will contain its own ‘brain’ in
the form of an Arduino microcontroller,
with the game stored in the Arduino’s
flash memory.
As you may recall, our console has a
25-way female D-sub
panel-mount connector
at the top (see Fig.1).
Our game cartridges will plug into this
connector. The way
we tackled things last
time, to get things up
and running as quickly as possible, was to
use a DB25 male-tobare-wire connector
that I happened to find
lurking in my treasure
chest of parts (https://
pemag.au/link/ac9q).
Photo 1: a 25-way D-sub right-angle
male PCB-mount connector.
I plugged the connector end into
my console and the appropriate wires
from the other end into my Arduino.
I then verified that the 7-segment display modules, the 14 × 10 tricolour LED
array and the push-button switches
worked as planned. They did. I performed my happy dance. I’m still wearing a foolish grin (but we shouldn’t
read too much into that because it’s
my usual look).
Creating a cartridge
When it comes to creating one of our
game cartridges, the first thing we’ll
need is a 25-way D-sub right-angle male
printed circuit board (PCB) mounting
connector (Photo 1). Actually, we will
Photo 2: the actual Arduino Unobased game cartridge board.
48
Practical Electronics | March | 2026
Photo 3: an Arduino
Nano microcontroller
module.
25-way D-sub connector (game cartridges plug in here)
7-Segment
Displays
Power
Jack
need one of these for each of our game
cartridges. I just found a pack of two
for £3.15 on Amazon (https://pemag.
au/link/ac9w).
You can probably get them cheaper
if you buy them by the bushel (eg, from
DigiKey or Mouser [DB25-PL-24]). You
can start with a single cartridge that you
can reprogram with different games and
add more cartridges later if you wish.
The next thing we’ll need is a
cartridge-sized PCB to solder our connector to. Happily, my friend Joe Farr
used the DipTrace Schematic and
PCB Design Software package (https://
diptrace.com) to create just such a board
(see Screen 1). This shows a bird’s-eye
view of the top side of the board. Does
anything look unusual to you? You’re
right; at first glance, all the pins appear
to be the wrong way around!
This takes a little thought to wrap
one’s brain around. Our 25-way connector is inserted into the top side of
the board and soldered on the bottom.
Similarly, a bunch of male header pins
are inserted on the top side and soldered on the bottom.
We use male header pins on this
board because the Arduino Uno comes
with female headers. This means we
need to flip the Arduino upside-down
to mount it on our cartridge. Photo 2
shows a completed cartridge, with the
Arduino’s underside facing us, plugged
into my console (the Arduino is powered by 9V supplied via the 25-way
connector from the console).
While I’m developing a new game,
I simply leave this cartridge plugged
into my console and reprogram it in
place by connecting a USB cable from
my host computer to the Arduino.
If you want to make your own Unobased cartridges, Joe has kindly provided us with all the files needed to
fabricate the boards. These are available
in the file named CB-mar26-uno-cartridge-v1.zip, part of the March 2026
download package on the PE website
at https://pemag.au/link/ac9x You can
also get these boards pre-made from
our shop (see page 69).
Honey, I shrunk the board
It has to be acknowledged that our
Practical Electronics | March | 2026
Reset
Button
14 x 10
Tricolor
LED Array
R
Y
Action
Controls
N
W
G
B
E
Direction
Controls
N
15-way D-sub connector (additional controls can plug in here)
Fig.1: a view from the top of the main console.
Uno-based game cartridges are a little board’s, or we would quickly find
on the ‘clunky’ side (I hope I’m not ourselves in a bit of a pickle. Since
being too technical).
the Nano comes equipped with male
When I’m creating space-constrained header pins (you may have to solder
projects, I often turn to the Arduino these on yourself), we mount female
Nano (https://pemag.au/link/ac9y), header pins on the Nano game carwhich uses the same 8-bit proces- tridge board.
sor as the Uno. In fact, it’s identical
In this case, we don’t need to flip the
in almost all respects, including its
Nano board over; we simply insert it
16MHz clock, 32kiB of
flash memory (for programs), 2kiB of RAM (for
working memory/live
data), 14 digital input/
output (I/O) pins, and
so on.
The main difference is
its smaller form factor/
footprint (see Photo 3).
Happily, Joe was on
a roll when he created
our Uno game cartridge
board because he also
threw one together for the
Nano (Screen 2). Once
again, this is a bird’s-eye
view of the top of the
board.
The connector’s pinout
is identical to the Uno Screen 2: the Nano-based game cartridge board design.
49
into the cartridge’s headers. Photo 4
shows a Nano-based cartridge plugged
into my console. As we see, these new
cartridges are decidedly less clunky
and much easier on the eye.
Once again, Joe kindly provided
the files if you want to build some of
these boards yourself (file CB-mar26nano-cartridge-v1.zip) and we also
have these in our shop.
In our previous column (February
2026), we reused existing code to create
three independent test programs: one
for the 7-segment displays, one for the
array, and one for the switches. The
purpose of these programs was simply
to make sure that things still worked as
expected once we’d wired everything
up in our console.
Now that we’ve created our first game
cartridge, it behooves us to reload and
rerun these programs to verify that the
cartridge concept works as expected.
I’ve been framed
Let’s turn our attention to the software side. Rather than starting from
scratch every time we sit down to create
a new game, it makes far more sense
to begin with a ‘framework’. This is a
sort of “master scaffolding” that does
all the heavy lifting behind the scenes.
In our case, this framework includes
the general-purpose code needed to
drive our six 7-segment displays, the
graphics primitives (draw pixel, draw
line, draw rectangle) to control the
14×10 tri-colour LED matrix, and all
the switch-debouncing and eventhandling routines for the eight pushbutton switches.
These buttons are split into two
groups: the action controls [red (R),
yellow (Y), green (G) and blue (B)] and
the direction controls [north (N), south
(S), east (E) and west (W)].
The framework frees us up to build
games, not infrastructure. Once the
boring-but-essential ‘plumbing’ has
been taken care of, each new game
becomes a matter of writing only the
fun bits: gameplay logic, animations,
scoring and whatever other scamps of
whimsy our imaginations can conjure.
Even better, whenever we improve
the framework in the future—add a
new graphics routine, speed
up the display refresh, refine
the switch handler routines—every game benefits.
Over time, our retro console
Photo 4: the Arduino Nanobased game cartridge board.
50
evolves, and we get to spend more of
our lives doing what we wanted to do
in the first place: making gob-smacking
games with gusto and abandon (and
aplomb, of course).
I love housework
If you look at my first framework
pass (in the file named CB-mar26code-01.txt), you’ll see that I’ve gathered our three test programs together.
Also, I’ve taken this opportunity to do
a little ‘housework’ and tidy up some
things that were niggling me.
Starting at the top of the program, I’ve
organised things into three tidy, selfcontained sections of definitions and
global variables: one for the 7-segment
displays, one for the 14×10 LED array
and one for the push-button switches. Keeping these domains separated
makes the code much easier to understand, maintain and extend.
I’ve also created a dedicated “pin
declaration” section where all the
input and output pins are defined in
one place. This means that if I ever
decide to reroute a signal or change
a connector, I can find things in a
single, well-labelled block rather than
spelunking through multiple devicespecific sections.
This is a simple organisational step,
but it pays dividends in clarity and
sanity—both of which are in short
supply when you’re juggling displays,
LEDs, buttons and gameplay logic all
at once.
I realised that we might want to use
our existing UpdateScore() function to
modify a variety of other metrics, so I
renamed it as UpdateCounter(), which
is a little more generic.
I also renamed our original LoadSR()
function to DigitToDisplay( ) because
that better aligns with our other function names.
I also deleted the original ClearSR()
function we used to clear the 7-segment
displays, because this was a bit of a
‘one-trick’ outlier. Instead, I created a
new FillBuffer() function. This new routine accepts an 8-bit integer, looks up
the corresponding 7-segment bit pattern in our CharSegMap[] table, and
writes that same pattern into every position of our six-digit
DisplayBuffer[].
Once the buffer
is filled, it calls our
BufferToDisplay( )
function to push the
updated patterns to
the physical display.
This means that we can now clear our
display by calling FillBuffer(SPACE).
Last, but certainly not least, I deleted our original LoadSR() function because, following the aforementioned
reorganisations, it ended up being
called from only one place. Thus, this
functionality has been subsumed by
the BufferToDisplay() function.
We live in testing times
It’s always reassuring when you
apply power to something like our
retro games console to get a quick
visual ‘all systems go’ before the real
action begins. A brief self-test gives
us instant feedback that the hardware is alive and responding, saving
us from chasing phantom software
bugs caused by something as simple
as a loose wire.
At this point, I’ll freely confess that
I was sorely tempted to make my power-on self-test fabulously flamboyant. For example, I could have had
the 7-segment displays cycle through
random numbers and then ‘spin down’
one by one to the number ‘8’, slot-
machine style.
Likewise, on the LED array, I could
have walked vertical and horizontal
bars across the display, creating something reminiscent of a retro diagnostics
demo from a 1980s arcade cabinet. All
very pretty… the first couple of times.
There are two reasons I decided
against this ‘over-the-top’ approach.
First, every extra animation consumes valuable memory, and we
want to conserve as much flash and
(especially) RAM as possible for our
actual games.
Second, while flashy startup sequences are fun during development,
they become teeth-grindingly annoying when you are forced to sit through
them every single time you power up
the console. Trust me. I’ve been there,
done that, bought the T-shirt and got
the tattoo. I still have the emotional
scars.
This explains why I opted for two
simple, functional power-on tests that
are called at the end of the Arduino’s
standard setup() function.
The first routine, TestDisplay
Modules( ), uses our new FillBuffer( )
function to load all six 7-segment displays with the character ‘8’, pauses for
a second, then clear them by writing
spaces. This makes it trivially easy to
confirm that every segment of every
display is alive and kicking.
The second routine, TestArray( ),
calls our existing DrawRectSolid( )
function to set the entire 14×10 LED
array to WHITE, pauses for a second,
and then sets it back to BLACK. If
any LED (or sub-LED) fails to fire
Practical Electronics | March | 2026
(0,9)
(13,9)
(0,9)
(13,9)
From
MCU
:
:
From
MCU
(0,0)
(13,0)
(a) Raster
(0,0)
(13,0)
(b) Reverse serpentine
Fig.2: alternative Neopixel wiring schemes.
during this test, it will stand out like
a sore thumb.
Short, sweet and sensible. No
wasted memory, no unnecessary
theatrics, and no irritated users (including me) drumming their fingers
while waiting for the console to stop
showing off and let them start to play.
Now would be a good time to load
and run this first pass at our framework program and observe these two
tests run.
Remember that at this stage, in
addition to testing the hardware,
we’re also verifying that our framework works and we haven’t messed
anything up. As part of this, the Arduino’s main loop( ) function is still
cycling around, repeatedly calling
our UpdateSwitches( ) and Process
Switches( ) functions.
In turn, our ProcessSwitches( )
function is still writing messages
to the host computer’s serial monitor, reporting whenever one of our
push-button switches is pressed or
released. This would be a good time
to activate the serial monitor in the
Arduino’s integrated development
environment (IDE) and verify that
everything still works as expected.
Cut the chit-chat!
Once we are happy that our core
framework is as ‘sound as a pound’,
we need to streamline it as much as
possible. You can follow along by
perusing and pondering our second
iteration (in the file named CB-mar26code-02.txt).
My first step was to comment out
the Serial.begin() call in the setup routine and all the Serial.println( ) calls
in the ProcessSwitches( ) function.
This will speed up the gameplay.
More importantly, it saves us a bunch
of valuable memory bytes.
Practical Electronics | March | 2026
The reason for commenting these
statements out rather than deleting them is that comments don’t
consume any space in the final machine code, and we may need one or
more of them for debugging in the
future.
Oh, the shame!
what I now refer to as a ‘reverse serpentine’ approach, as illustrated in
Fig.2(b).
As we discussed previously, we
need a function we decided to call
GetNeoNum( ) that accepts a pair of
(x, y) values specifying the position
of a pixel in the array and returns
that pixel’s index n in the physical
string.
The idea was that, regardless of
how we each physically organise our
pixels, all we need to do is swap out
our GetNeoNum( ) functions to run
each other’s games.
Joe did what I didn’t: figure things
out before he started wiring everything together.
Apart from anything else, Joe
doesn’t have to differentiate between
odd and even rows. As a result, assuming that NUM_COLS has been defined as 14, the body of his GetNeo
Num() function can be summarised as
n = (y * NUM_COLS) + x (easy peasy
lemon squeezy).
Now, let’s return to my wiring
scheme from Fig.2(b). My algorithm
does need to differentiate between
odd and even rows, and it requires
some mental gymnastics to work out
what’s going on.
Our initial incarnation deep in the
mists of time looked a little like Listing A (the lack of a listing number
indicates that this code isn’t derived
from any of the programs in this
issue).
The modulo division operation
on line 127 is used to detect whether the row (y) we’re dealing with is
even or odd.
I hail from the days when C compilers were as dumb as a bag of rocks
and would have implemented a statement like this as-is.
On an Arduino Uno, which doesn’t
have a hardware divider (meaning divisions and modulo operations must
be performed using multiple simple
instructions), this would consume
60-80 clock cycles.
Now we come to the point in the
story where I am once again obliged
to hang my head in shame.
As you may recall, our LED array
boasts 140 tricoloured ‘pixels’. These
are wired as a single physical string
numbered from 0 to 139, but they’re
presented to the viewer as a rectangular matrix of 10 rows (in the Ydimension) and 14 columns (in the
X-dimension).
In my programs, I predominantly
work in (x, y) coordinates, with (0,
0) at the bottom-left corner of the
display, (13, 0) at the bottom-right,
(0, 9) at the top-left, and (13, 9) at
the top-right.
This makes it easy to think of the
array as a conventional grid, even
though the underlying pixels are
wired as a linear strip.
It’s this wiring that
is the source of my 122 // Return the number of the pixel in the string
123 uint8_t GetNeoNum (uint8_t x, uint8_t y)
chagrin. When he 124 {
created his person- 125
uint8_t n;
al prototype of this 126
if ( (y % 2) == 0 )
console, my friend 127
128
{
Joe opted for what I 129
// Even Row
think of as a ‘raster’ 130
n = MAX_NEO - (y * NUM_COLS) - MAX_X + x;
schema, illustrated 131
}
132
else
in Fig.2(a).
{
For reasons I no 133
134
// Odd Row
l o n g e r r e c a l l , a l - 135
n = MAX_NEO - (y * NUM_COLS) - x;
t h o u g h I ’ m s u r e 136
}
they were jolly good 137
return n;
and extremely well 138
139 }
thought out at the
time, I plumbed for Listing A: our original GetNeoNum() function.
51
Thus, our first optimisation was to
replace this modulo division with a
bitwise AND, as illustrated on line
509 in Listing 1(a). Remember, we’re
using the convention that the listing
number (1 in this case) corresponds
to the numerical part of the matching
code file name (“01” here).
In addition to using less memory,
this new approach consumes only
two clock cycles if y is even (and the
branch isn’t taken) or three cycles if
y is odd (and the branch is taken).
Sad to relate, when we compiled
this new version of our routine (March
2025), we discovered that it consumed
the same amount of memory (and the
same number of clock cycles) as the
original. What? How could this be?
Well, it turns out that modern C/C++
compilers are aware of tricks like replacing (y % 2) == 0 with (y & 1) == 0,
and they automatically implement
these techniques without our knowing. I’m not sure whether to say “Rats!”
or “Hurrah!”.
And that’s where we left things…
until now.
Why GetNeoNum() matters
The word “furtling” is a delightfully imprecise British term meaning
to fiddle, tinker, or poke about with
something, typically without a grand
plan, but with great enthusiasm and
the faint hope that enlightenment (or
at least functionality) will emerge.
Bearing this in mind, the reason for
our furtling with our GetNeoNum( )
function is that it sits at the very bottom
of our ‘graphics stack’. Every higherlevel drawing function eventually funnels through this one routine.
DrawPixel( ) calls it directly, while
all the other helpers—DrawLine( ),
DrawRectOutline( ), DrawRectSolid( ),
DrawRectFilled( ) and any additional
routines we may add in the future—are
essentially loops that call DrawPixel()
over and over.
That means any time we touch the
14×10 NeoPixel array, we pay the
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
Optimising our socks off
When we look at lines 512 and 517
in Listing 1(a), we see that we perform
the same base calculation (MAX_NEO (y * NUM_COLS)) irrespective of whether the row is odd or even. There’s no
point in performing the same calculation twice, so we can rearrange things as
shown in Listing B (the reason this listing doesn’t have an associated number
is that we aren’t actually going to use
it in our code).
Upon perusing the compilation reports, I discovered that doing this saved
us six bytes of memory. That may not
seem like a lot, but every byte counts
(especially when you find you are running short).
The final version of our routine is
shown in Listing 2(a). In this case,
we’re using the C/C++ ternary operator (?:). This is a compact shorthand
for an if-else expression. It selects one
of two values depending on whether
a condition is true or false. The generic case is condition ? value_if_true
: value_if_false, where ‘true’ is any
non-zero value and ‘false’ equates to
a value of zero.
The ternary operator does not introduce any overhead or offer any advantages in terms of memory or clock cycles.
It’s essentially syntactic sugar because
the compiler typically generates the
// Return the number of the pixel in the string
uint8_t GetNeoNum (uint8_t x, uint8_t y)
{
uint8_t n;
if ( (y & 1) == 0 )
{
// Even Row
n = MAX_NEO - (y * NUM_COLS) - MAX_X + x;
}
else
{
// Odd Row
n = MAX_NEO - (y * NUM_COLS) - x;
}
return n;
}
Listing 1(a): our initial optimisation attempt.
52
cost of any inefficiencies in GetNeo
Num(). A single full-screen fill like the
DrawRectSolid(0, 0, MAX_X, MAX_Y,
WHITE) used in our TestArray() function calls GetNeoNum() once for every
one of the 140 pixels. Any future animations, scrolling, and suchlike might
easily rack up hundreds or thousands
of such calls.
On a little 8-bit Arduino Uno processor running at 16MHz with no cache, no
pipeline and no speculative execution,
even a couple of wasted instructions
in a basic function like this quickly
add up. That’s why we want to keep
GetNeoNum( ) as small, simple, and
straightforward as possible.
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
same machine code as an equivalent
if-else expression. So why did we do
it? To be honest, the only reason is to
impress our friends and make it look
like we know what we are doing!
Having said this, observe the use of
the static and inline keywords on line
503. Declaring a function as static restricts its visibility to this source file
only. This allows the compiler to optimise it more aggressively, since it
knows the function cannot be referenced from elsewhere.
Meanwhile, the inline keyword provides a hint to the compiler that this
function is a good candidate to be expanded in-place rather than called
through a normal function call. Inlining removes the function-call overhead
(pushing/popping registers, branching
etc) and often results in faster, tighter
assembly code.
If a function is declared static and is
only called once or twice, even without the inline keyword, the compiler
may decide to inline it anyway and not
generate any separate function code if
that’s more efficient.
More meandering musings
I did ponder (and discard) a couple
of other ideas. For example, consider
the calculation on line 505 in Listing
2(a). One thing we could do would be
to pre-calculate these values and store
them in an array, along the lines of:
uint8_t BaseTable[NUM_ROWS] = {139,
125, 111, 97, 83, 69, 55, 41, 27, 13};
This would cost us ten bytes of RAM
(one byte per value) but we could
change line 505 to base = BaseTable[y].
The reason I didn’t bother with this
is that the current calculation on line
505 conceptually requires only a single
8-bit multiplication (which the AVR’s
hardware multiplier performs in two
clock cycles) and a single 8-bit subtraction (one clock cycle), plus a few
bookkeeping instructions.
I think there’s a better-than-even
chance that accessing the data in the
array would consume at least as many,
// Return the number of the pixel in the string
uint8_t GetNeoNum (uint8_t x, uint8_t y)
{
uint8_t base = MAX_NEO - (y * NUM_COLS);
uint8_t xr;
if ( (y & 1) == 0 )
{
xr = (MAX_X - x);
}
else
{
xr = x;
}
return base - xr;
}
Listing B: extracting the common base calculation.
Practical Electronics | March | 2026
they are still
copied into
RAM at startup, so they end
up consuming
RAM anyway
(rats!).
Listing 2(a): the final version of GetNeoNum() (for now).
To keep such
and possibly more, clock cycles (we’d tables in flash and avoid burning RAM,
have to check the assembly code gen- we would need to augment their declaerated by the compiler to make a full rations with the PROGMEM keyword
determination).
and use special functions to read them,
Having said this, this is a useful tech- which is a rabbit hole we really don’t
nique for us to remember for future want to go down here.
projects involving similar concepts
As a parting thought, just to place an
and more complex calculations. Re- allegorical stake in the metaphorical
member that neither the Arduino Uno sand, our current framework program
nor the Nano has a hardware divider. (the file named CB-mar26-code-02.txt)
So, if our calculation involved a divi- consumes 3494 bytes (about 10%) of the
sion operation, it could consume tens flash program storage space available
of bytes of flash and take on the order to us (the maximum is 32,256 bytes.
of 60 to 100 clock cycles.
Meanwhile, our global variables use
In such a case, moving everything 95 bytes (about 4%) of the dynamic
over into a pre-calculated table would memory (RAM), leaving 1953 bytes for
make a lot of sense.
local variables (the maximum is 2048
Furthermore, if we were to use a bytes). To be honest, that’s not bad…
pre-calculated table, what about the not bad at all.
ten bytes of RAM?
Well, with many microcontrollers, Fabulously flashy
Before we move on to creating our
prefixing our BaseTable[] declaration
with the const keyword (which in- first honest-to-goodness game, let’s start
structs the compiler to prevent the as- by taking baby steps. We’ll begin by
sociated value from being modified in using our framework code as the basis
our code) would automatically cause for creating a ‘flashing pixel’ program.
I’m envisioning our starting pixel
the table to be stored in flash, thereby
to be at location (6, 6) for no other
freeing up RAM. Furthermore, when
running below ~25MHz, there would reason than 6 is my lucky number.
typically be little or no timing penalty This pixel will commence by flashing
between black and one of our RYGB
for reading from flash.
Unfortunately, with the AVR-based action button colours; let’s say green,
Uno and Nano, things are a bit more because that’s my favourite colour.
‘interesting’. Arrays and lookup tables We’ll consider black and green to be
defined with the const keyword are our ‘background’ and ‘foreground’ colours, respectively.
effectively read-only but, by default,
502
503
504
505
506
507
508
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// Return the number of the pixel in the string
static inline uint8_t GetNeoNum (uint8_t x, uint8_t y)
{
uint8_t base = MAX_NEO - (y * NUM_COLS);
uint8_t xr
= (y & 1) ? x : (MAX_X - x);
return base - xr;
}
// ============ GAME-RELATED STUFF ============
// Fine cycle time
#define TICK
#define FLASH
1 // Main clock tick
250 // Flash on/off times
#define SAME
#define EAST
#define WEST
#define NORTH
#define SOUTH
0
1
-1
1
-1
#define STANDARD
#define FORCED
0
1
#define START_X
6
#define START_Y
6
#define START_COLOR GREEN
uint8_t X = START_X;
uint8_t Y = START_Y;
uint32_t ForeGndColor = START_COLOR;
uint32_t BackGndColor = BLACK;
bool
FlashOn;
uint32_t TimeThen = micros(); // Tick-related
uint32_t TimeNow;
// Tick-related
uint32_t FlashThen;
// Flash-related
Listing 3(a): definitions and declarations.
Practical Electronics | March | 2026
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
Let’s say the on and off flash times
are both 250ms (milliseconds), resulting in a total flash cycle of 0.5 seconds
or a flash rate of 2Hz.
If we press any of the RYGB action
buttons, the pixel will start flashing that
colour. If we press any of the NSEW direction buttons, the current pixel will be
forced to the background colour (black
in this case), and we will start flashing
the new pixel in the specified direction.
Finally, if we reach any edge of the
array, we will ‘wrap around’ to the
other side. For example, if our current location is (13, 6) on the righthand side of the array and we press
the E (east) direction button, our new
flashing pixel will appear at (0, 6) on
the left-hand side of the array.
Our first flash
We will take things one step at a time
because it’s easier to track down any
problems that way (you’ll find our first
pass at this program in the file named
CB-mar26-code-03.txt). We start by
adding some game-specific definitions
and declarations, as seen in Listing 3(a).
Next, we create the UpdateFlash( )
function that will make our pixel flash.
We want this to have two modes: ‘standard’ and ‘forced’. In standard mode, it
turns the current pixel on and off with
the appropriate flash timing. In forced
mode, it will immediately turn the pixel
on (to our foreground colour) and reset
the flash timing. My realisation of this
function is shown in Listing 3(b).
On line 408, we check whether the
flash type is forced. If so, we set FlashOn
to true, set the current pixel to the foreground colour, use the Neos.show( )
function to load the physical array,
and reset the FlashThen time to the
current time.
// ============ GAME-SPECIFIC ROUTINES ============
void UpdateFlash (bool flashType)
{
if (flashType == FORCED)
{
FlashOn = true;
DrawPixel(X, Y, ForeGndColor);
Neos.show();
FlashThen = millis();
}
else
{
if ( (TimeNow - FlashThen) >= FLASH)
{
FlashOn = !FlashOn;
if (FlashOn)
DrawPixel(X, Y, ForeGndColor);
else
DrawPixel(X, Y, BackGndColor);
Neos.show();
FlashThen = millis();
}
}
}
Listing 3(b): our UpdateFlash() function.
53
pressing one of our RYGB action
buttons. I bet this is going to be
far easier than you might think
(see the file named CB-mar26code-04.txt). We start by creatif ( (TimeNow - TimeThen) >= TICK)
ing an UpdateColor( ) function,
{
which accepts the new colour
UpdateFlash(STANDARD);
UpdateSwitches();
as a parameter, as illustrated in
ProcessSwitches();
Listing 4(a).
If the new colour differs from
TimeThen = TimeNow;
the existing foreground colour,
}
}
we set the foreground colour to
the new colour and then call
Listing 3(c): modifying the loop() function.
our UpdateFlash( ) function in
its forced mode.
435 void UpdateColor(uint32_t color)
Next, we need to modify the
436 {
switch-handling code associated
437
if (ForeGndColor != color)
438
{
with each of our RYGB action
439
ForeGndColor = color;
switches. When any of these
440
UpdateFlash(FORCED);
switches is pressed, it will enter
441
}
its JUST_GONE_ACTIVE state,
442 }
at which point we call our new
Listing 4(a): our UpdateColor() function.
UpdateColor( ) function passing
Alternatively, if this is a standard
in the desired colour.
flash, on line 417 we check whether
This involves only a one-line adwe’ve reached the end of the flashing dition to each of our RYGB switch
pixel’s on or off duration. If so, we
routines. Consider the red switch,
invert the state of our FlashOn variable as illustrated in Listing 4(b), for ex(if it’s true, it becomes false, and vice ample. The new statement appears
versa), set the pixel to the appropriate on line 328.
on or off colour, update the physical
array, and reset the FlashThen time Changing location
Now let’s ponder the problem of
to the current time.
We call UpdateFlash(FORCED) at changing the pixel’s location in rethe end of our setup( ) function to sponse to pressing one of our NSEW
start things rolling, and we add an direction buttons.
Once again, I bet this is going to
UpdateFlash(STANDARD) call to our
main loop( ) function, as illustrated be easier than you might fear (see
the file named CB-mar26-code-05.
in Listing 3(c).
txt). We start by creating an Update
When we run our program, we
see the pixel at (6, 6) happily flash- Location( ) function, as illustrated in
ing away in a cheerful green. I’m Listing 5(a).
This function accepts two paramhappily watching it on my desk as I
eters, deltaX and deltaY, which repen these words, which means we’re
flect whatever X-Y changes we wish
both happy.
to make to the flashing pixel’s poChanging colours
sition. Observe that, in this case,
Still picking the ‘low-hanging fruit’, we’ve declared these parameters as
let’s turn our attention to chang- being of type int8_t (signed 8-bit inteing the pixel’s colour in response to gers) as opposed to our usual uint8_t
(unsigned 8-bit
320
// Switch Red
integers). This
321
if (Switches[SW_RED].swState == JUST_GONE_ACTIVE)
is because we
322
{
want to use
323
// Mandatory
them to repre324
Switches[SW_RED].swState = WAITING_FOR_ALL_ONES;
sent both posi325
326
// Optional (game-specific)
tive and nega327
// Serial.println("Switch Red just went active");
tive values.
328
UpdateColor(RED);
On line 450,
329
}
we set the pixel
Listing 4(b): modifying our RYGB switch handling.
at the current (x,
y) location to
448 void UpdateLocation(int8_t deltaX, int8_t deltaY)
the background
449 {
colour. On lines
450
DrawPixel(X, Y, BackGndColor);
451 and 452,
451
X = (X + NUM_COLS + deltaX) % NUM_COLS;
452
Y = (Y + NUM_ROWS + deltaY) % NUM_ROWS;
we update our
453
UpdateFlash(FORCED);
X and Y values.
454 }
Then on line
Listing 5(a): our UpdateLocation() function.
453, we call our
201
202
203
204
205
206
207
208
209
210
211
212
213
214
54
// Do this over and over again
void loop()
{
TimeNow = millis();
UpdateFlash( ) function in its forced
mode, which causes the cursor to
appear in its new location.
As a reminder, we’re using the %
operator on lines 451 and 452 to perform ‘wrap-around’ (modular) arithmetic so our cursor never leaves the
14 × 10 playfield. Adding NUM_COLS
or NUM_ROWS before the % ensures
that the resulting value is non-negative, even if deltaX or deltaY is -1.
Next, we shall modify the switch-
handling code associated with each of
our NSEW direction switches. When
any of these switches is pressed, it
will enter its JUST_GONE_ACTIVE
state, at which point we call our new
UpdateLocation( ) function, passing
in the desired direction.
Once again, this involves only
a one-line addition to each of our
NSEW switch routines. For example,
consider the north switch, as illustrated in Listing 5(b). The new statement appears on line 256.
As you may recall from the gamerelated definitions shown in Listing
3(a), SAME is defined as 0, NORTH
and EAST are defined as 1 (ie, +1),
and SOUTH and WEST are defined
as -1. These are the delta values that
we end up passing into our Update
Location( ) function.
Even though this is a simple
‘pseudo-game’ program, it’s strangely satisfying to press the RYGB and
NSEW buttons and watch the flashing pixel change colour and location.
Keeping count
How many times will we press the
RYGB and NSEW buttons before we
get bored? I’m glad you asked. Obviously, we will need to keep count.
On my console, I have six 7-segment
displays. I am going to consider the
least-significant digit (0) to be on the
right and the most-significant digit
(5) to be on the left.
I’m going to use digits 5, 4, and 3
to keep track of the number of times I
click one of the action buttons, while
digits 2, 1, and 0 will be used to reflect the number of times I click one
of the direction buttons. Both values
will range from 000 to 999.
If you look at our latest and greatest program (in the file named CBmar26-code-06.txt), you’ll see that we
start by adding two new definitions,
MIN_COUNT and MAX_COUNT, which
we set to 0 and 999, respectively. We
also add two new variables, ActionCtr
and DirectionCtr, and initialise them
with our MIN_COUNT value.
Next, we add a FillBuffer(ZERO) call
to the end of our setup( ) function.
This loads and presents 0 values on
all of our 7-segment displays.
Practical Electronics | March | 2026
248
249
250
251
252
253
254
255
256
257
// Switch North
if (Switches[SW_NORTH].swState == JUST_GONE_ACTIVE)
{
// Mandatory
Switches[SW_NORTH].swState = WAITING_FOR_ALL_ONES;
// Optional (game-specific)
// Serial.println("Switch North just went active");
UpdateLocation(SAME, NORTH);
}
Listing 5(b): modifying our NSEW switch handling.
All that remains is surprisingly
simple. Regarding the direction buttons, we simply add three lines to
our UpdateColor( ) function, which
is called whenever we press one of
them. This modification is shown in
Listing 6(a).
On line 454, we call our Update
Counter( ) function to increment the
contents of our ActionCtr variable,
ensuring it stays within the specified 0 to 999 range.
On line 455, we call our Number
ToBuffer() function to copy the 3-digit
value stored in the ActionCtr variable
into our display buffer. We specify
an offset of 3 digits (from digit 0),
thereby ensuring that this value is
copied into digits 3, 4, and 5 in the
buffer. And on line 456, we call our
BufferToDisplay( ) function to present
the contents of the entire buffer on
the display.
We perform a similar treatment in
our UpdateLocation( ) function, as illustrated in Listing 6(b). In this case,
however, we modify the value in the
DirectionCtr variable. Also, when we
446
447
448
449
450
451
452
453
454
455
456
457
copy this value into our
display buffer, we specify
an offset of 0, thereby ensuring it ends up in the
buffer’s 2, 1 and 0 digits.
Did we do well?
So, how well did we do with this
first test case of our framework program? Well, before we started, our
core framework required 531 lines of
source code while consuming 3494
bytes of flash and 95 bytes of SRAM.
Our new program, with all its ‘bells
and whistles’, is now 618 lines of
source code, which means we’ve
added only 87 lines, several of which
were blank. Also, this latest incarnation consumes 4430 bytes (~13%) of
flash and 126 bytes (~6%) of RAM.
I don’t know about you, but I, for
one, am feeling pretty good about
all this. Apart from anything else,
all the foundational routines we’ve
created seem to be working well for
us (touch wood).
I also think it’s important to say
that nothing we’ve done here is time
void UpdateColor(uint32_t color)
{
if (ForeGndColor != color)
{
ForeGndColor = color;
UpdateFlash(FORCED);
}
Next time
I was planning to start a new project next month, but we still have a
lot more functionality to explore with
our retro games console, not least
implementing our first real ‘win-orlose’ game.
I have a great one in mind, but I’d
love to hear your suggestions.
Even when we do start a new project, we can still return to this Retro
Games Console and add a new game
cartridge now and then!
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
5-year
collections
ActionCtr = UpdateCounter (ActionCtr, 1, MIN_COUNT, MAX_COUNT);
NumberToBuffer(ActionCtr, 3, 3);
BufferToDisplay();
}
2019-2023
£49.95
Listing 6(a): adding action button count functionality.
459
460
461
462
463
464
465
466
467
468
469
wasted. The new routines we’ve created here—UpdateFlash( ), Update
Color( ) and UpdateLocation( )—will
undoubtedly come in handy (perhaps
in modified form) for more ambitious
programs in the future.
void UpdateLocation(int8_t deltaX, int8_t deltaY)
{
DrawPixel(X, Y, BackGndColor);
X = (X + NUM_COLS + deltaX) % NUM_COLS;
Y = (Y + NUM_ROWS + deltaY) % NUM_ROWS;
UpdateFlash(FORCED);
DirectionCtr = UpdateCounter (DirectionCtr, 1, MIN_COUNT, MAX_COUNT);
NumberToBuffer(DirectionCtr, 3, 0);
BufferToDisplay();
}
Listing 6(b): adding direction button count functionality.
2018-2022
£49.95
2017-2021
£49.95
2016-2020
£44.95
Devices and components used in this issue
Arduino Uno R3 microcontroller module
Arduino Nano R3 microcontroller module
PCB spacer kit (black, M3)
Practical Electronics | March | 2026
https://pemag.au/link/ac2g
https://pemag.au/link/ac9y
https://pemag.au/link/ac4u
Purchase and download at:
www.electronpublishing.com
55
|