Silicon ChipMax’s Cool Beans - March 2026 SILICON CHIP
  1. Contents
  2. Publisher's Letter: Quantity kinds, tagging and units
  3. Subscriptions: ETI Bundles
  4. Feature: Audio Out by Jake Rothman
  5. Feature: The Fox Report by Barry Fox
  6. Project: USB-Programmable Frequency Divider/Counter by Nicholas Vinen
  7. Feature: Teach-In 2026 by Mike Tooley
  8. Feature: Circuit Surgery by Ian Bell
  9. Back Issues
  10. Project: Rotating Light for Models by Nicholas Vinen
  11. Feature: Max’s Cool Beans by Max the Magnificent
  12. Feature: Techno Talk by Max the Magnificent
  13. Feature: Data Centres, Servers & Cloud Computing by Dr David Maddison
  14. PartShop
  15. Project: Power LCR Meter Part 2 by Phil Prosser
  16. Advertising Index
  17. Market Centre
  18. Back Issues

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:
  • Audio Out (January 2024)
  • Audio Out (February 2024)
  • AUDIO OUT (April 2024)
  • Audio Out (May 2024)
  • Audio Out (June 2024)
  • Audio Out (July 2024)
  • Audio Out (August 2024)
  • Audio Out (September 2024)
  • Audio Out (October 2024)
  • Audio Out (March 2025)
  • Audio Out (April 2025)
  • Audio Out (May 2025)
  • Audio Out (June 2025)
  • Audio Out (July 2025)
  • Audio Out (August 2025)
  • Audio Out (September 2025)
  • Audio Out (October 2025)
  • Audio Out (November 2025)
  • Audio Out (December 2025)
  • Audio Out (January 2026)
  • Audio Out (February 2026)
  • Audio Out (March 2026)
  • Audio Out (April 2026)
Articles in this series:
  • The Fox Report (July 2024)
  • The Fox Report (September 2024)
  • The Fox Report (October 2024)
  • The Fox Report (November 2024)
  • The Fox Report (December 2024)
  • The Fox Report (January 2025)
  • The Fox Report (February 2025)
  • The Fox Report (March 2025)
  • The Fox Report (April 2025)
  • The Fox Report (May 2025)
  • The Fox Report (July 2025)
  • The Fox Report (August 2025)
  • The Fox Report (September 2025)
  • The Fox Report (October 2025)
  • The Fox Report (October 2025)
  • The Fox Report (December 2025)
  • The Fox Report (January 2026)
  • The Fox Report (February 2026)
  • The Fox Report (March 2026)
Articles in this series:
  • Teach-In 12.1 (November 2025)
  • Teach-In 2026 (December 2025)
  • Teach-In 2026 (January 2026)
  • Teach-In 2026 (February 2026)
  • Teach-In 2026 (March 2026)
  • Teach-In 2026 (April 2026)
Articles in this series:
  • STEWART OF READING (April 2024)
  • Circuit Surgery (April 2024)
  • Circuit Surgery (May 2024)
  • Circuit Surgery (June 2024)
  • Circuit Surgery (July 2024)
  • Circuit Surgery (August 2024)
  • Circuit Surgery (September 2024)
  • Circuit Surgery (October 2024)
  • Circuit Surgery (November 2024)
  • Circuit Surgery (December 2024)
  • Circuit Surgery (January 2025)
  • Circuit Surgery (February 2025)
  • Circuit Surgery (March 2025)
  • Circuit Surgery (April 2025)
  • Circuit Surgery (May 2025)
  • Circuit Surgery (June 2025)
  • Circuit Surgery (July 2025)
  • Circuit Surgery (August 2025)
  • Circuit Surgery (September 2025)
  • Circuit Surgery (October 2025)
  • Circuit Surgery (November 2025)
  • Circuit Surgery (December 2025)
  • Circuit Surgery (January 2026)
  • Circuit Surgery (February 2026)
  • Circuit Surgery (March 2026)
  • Circuit Surgery (April 2026)
Articles in this series:
  • Max’s Cool Beans (January 2025)
  • Max’s Cool Beans (February 2025)
  • Max’s Cool Beans (March 2025)
  • Max’s Cool Beans (April 2025)
  • Max’s Cool Beans (May 2025)
  • Max’s Cool Beans (June 2025)
  • Max’s Cool Beans (July 2025)
  • Max’s Cool Beans (August 2025)
  • Max’s Cool Beans (September 2025)
  • Max’s Cool Beans: Weird & Wonderful Arduino Projects (October 2025)
  • Max’s Cool Beans (November 2025)
  • Max’s Cool Beans (December 2025)
  • Max’s Cool Beans (January 2026)
  • Max’s Cool Beans (February 2026)
  • Max’s Cool Beans (March 2026)
  • Max’s Cool Beans (April 2026)
Articles in this series:
  • Techno Talk (February 2020)
  • Techno Talk (March 2020)
  • (April 2020)
  • Techno Talk (May 2020)
  • Techno Talk (June 2020)
  • Techno Talk (July 2020)
  • Techno Talk (August 2020)
  • Techno Talk (September 2020)
  • Techno Talk (October 2020)
  • (November 2020)
  • Techno Talk (December 2020)
  • Techno Talk (January 2021)
  • Techno Talk (February 2021)
  • Techno Talk (March 2021)
  • Techno Talk (April 2021)
  • Techno Talk (May 2021)
  • Techno Talk (June 2021)
  • Techno Talk (July 2021)
  • Techno Talk (August 2021)
  • Techno Talk (September 2021)
  • Techno Talk (October 2021)
  • Techno Talk (November 2021)
  • Techno Talk (December 2021)
  • Communing with nature (January 2022)
  • Should we be worried? (February 2022)
  • How resilient is your lifeline? (March 2022)
  • Go eco, get ethical! (April 2022)
  • From nano to bio (May 2022)
  • Positivity follows the gloom (June 2022)
  • Mixed menu (July 2022)
  • Time for a total rethink? (August 2022)
  • What’s in a name? (September 2022)
  • Forget leaves on the line! (October 2022)
  • Giant Boost for Batteries (December 2022)
  • Raudive Voices Revisited (January 2023)
  • A thousand words (February 2023)
  • It’s handover time (March 2023)
  • AI, Robots, Horticulture and Agriculture (April 2023)
  • Prophecy can be perplexing (May 2023)
  • Technology comes in different shapes and sizes (June 2023)
  • AI and robots – what could possibly go wrong? (July 2023)
  • How long until we’re all out of work? (August 2023)
  • We both have truths, are mine the same as yours? (September 2023)
  • Holy Spheres, Batman! (October 2023)
  • Where’s my pneumatic car? (November 2023)
  • Good grief! (December 2023)
  • Cheeky chiplets (January 2024)
  • Cheeky chiplets (February 2024)
  • The Wibbly-Wobbly World of Quantum (March 2024)
  • Techno Talk - Wait! What? Really? (April 2024)
  • Techno Talk - One step closer to a dystopian abyss? (May 2024)
  • Techno Talk - Program that! (June 2024)
  • Techno Talk (July 2024)
  • Techno Talk - That makes so much sense! (August 2024)
  • Techno Talk - I don’t want to be a Norbert... (September 2024)
  • Techno Talk - Sticking the landing (October 2024)
  • Techno Talk (November 2024)
  • Techno Talk (December 2024)
  • Techno Talk (January 2025)
  • Techno Talk (February 2025)
  • Techno Talk (March 2025)
  • Techno Talk (April 2025)
  • Techno Talk (May 2025)
  • Techno Talk (June 2025)
  • Techno Talk (July 2025)
  • Techno Talk (August 2025)
  • Techno Talk (October 2025)
  • Techno Talk (November 2025)
  • Techno Talk (December 2025)
  • Techno Talk (January 2026)
  • Techno Talk (February 2026)
  • Techno Talk (March 2026)
  • Techno Talk (April 2026)
Items relevant to "Power LCR Meter Part 2":
  • Power LCR Meter PCB [04103251] (AUD $10.00)
  • PIC32MK0128MCA048 programmed for the Power LCR Meter [0410325A.HEX] (Programmed Microcontroller, AUD $20.00)
  • Software & STL files for the Power LCR Tester (Free)
  • Power LCR Meter PCB pattern (PDF download) [04103251] (Free)
  • Power LCR Meter panel artwork and drilling diagrams (Free)
Articles in this series:
  • Power LCR Meter, part one (February 2026)
  • Power LCR Meter Part 2 (March 2026)
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. Update­Color( ) 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 Update­Location( ) 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