Silicon ChipMax’s Cool Beans - April 2026 SILICON CHIP
  1. Contents
  2. Publisher's Letter: The benefits of desktop computers
  3. Subscriptions: ETI Bundles
  4. Feature: Teach-In 2026 by Mike Tooley
  5. Back Issues
  6. Project: Versatile Battery Checker by Tim Blythman
  7. Feature: Power Electronics Part 1: DC-DC Converters by Andrew Levido
  8. Project: Power Rail Probe by Andrew Levido
  9. Feature: Max’s Cool Beans by Max the Magnificent
  10. Feature: Circuit Surgery by Ian Bell
  11. Feature: Techno Talk by Max the Magnificent
  12. Project: Pico 2 Audio Analyser by Tim Blythman
  13. Feature: Audio Out by Jake Rothman
  14. PartShop
  15. Market Centre
  16. Advertising Index
  17. Back Issues

This is only a preview of the April 2026 issue of Practical Electronics.

You can view 0 of the 80 pages in the full issue.

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:
  • 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:
  • 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:
  • 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)
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)
Max’s Cool Beans By Max the Magnificent Weird & Wonderful Arduino Projects Part 16: etching sketches and snake shenanigans H i there, I hope you are feeling anticipendulous and exhilarocious regarding the cunning code we are about to create concerning our retrodocious games console. Why yes, I have indeed delighted in both of the Wicked movies. How did you guess? As you may recall, my retro gaming console features a laser-cut front panel mounted in a 3D-printed enclosure. From the start, I noted that readers without access to these tools could construct their own consoles by simply attaching the various parts, such as the 7-segment display modules, 14 × 10 tricolour light-emitting diode (LED) array, and push-button switches, to a sheet of timber. I just heard from a reader we’ll call Ian (because that’s his name). Ian was keen to tell me about the current state of his console. He enclosed some snapshots of his handiwork (Photos 1 and 2). In his message, Ian commented: As you can see, I don’t have access to a CNC machine or 3D printer, so I had to resort to my other passion: woodworking. My console is formed from MDF mounted on a couple of pieces of wood, but it seems to be working out OK so far. Last month, I decided to deviate slightly from your design and build my console with an onboard Nano instead of separate cartridges. I have an internal extension cable, so I can plug a USB cable into the console to download games as required. I, for one, think Ian’s ingenious implementation has turned out brilliantly. This is just what I was envisaging. Hopefully, this will prompt other readers who have been sitting on the “pity me because I don’t have a CNC” fence to leap into action and join in the fun. Before we proceed, we should perhaps note that, in Ian’s realisation, the 40 four black north (N), south (S), east (E), and west (W) direction control buttons are located on the lower left-hand side of his console. (These buttons are white in my implementation.) Meanwhile, the four red (R), yellow (Y), green (G), and blue (B) action control buttons are located on the lower right-hand side of Ian’s panel. This was the way I’d initially planned to do things myself, and Ian was simply following my earlier instructions. However, due to an unexpected turn of events (I messed things up), I ended up with my action buttons on the left and my direction buttons on the right. Oh well, what can you do, eh? Ooh, shiny! Continuing our gaming console saga, in our previous column, we created a “framework program” that we can think of as the scaffolding that does all the heavy lifting behind the scenes. To ensure we’re all tap-dancing to the same skirl of the bagpipes, you can revisit this program in the file named CB-apr26-code-01.txt. As usual, all files mentioned in this column are available as part of the April 2026 download package from the PE website at https:// pemag.au/link/acau We subsequently used this framework as the basis for a simple program that let us use our RYGB action control buttons to modify the colour of a flashing pixel on our 14 × 10 array. Next, we augmented this program to use the four NSEW direction control buttons to move the flashing ‘pixel’ around the array. Finally, we taught the console to keep track of how many times we press any of the action control buttons and display this value in the three leftmost 7-segment digits located at the top of the console. Similarly, for the number of times we tap any of the direction control buttons, which we display in the three rightmost 7-segment digits. If your controls are arranged à la Ian, you might decide to swap which 7-­segment digits display the various values. You can peruse and ponder the way we left this program in the file named CB-apr26-code-02.txt. Electronic Etch A Sketch The Etch A Sketch was invented in 1959 by a French electrician called André Cassagnes. By the late 1960s and early 1970s, this toy was a staple Christmas stocking-stuffer in the UK. I think it’s fair to say that almost every kid I knew had one. The Etch A Sketch is one of those rare toys that never really went away. Over the decades, it smoothly transitioned into ‘timeless toy’ territory, eventually repositioning itself as a retro classic that’s still manufactured and sold to this day. I know what you’re thinking. Did he fire six shots or only five? Sorry, wrong script. You are thinking to yourself: “Why don’t we use our retro games console to implement an electronic Etch A Sketch?” I was just thinking the same thing. “Great minds think alike”, as they say. Of course, they also say, “Fools seldom differ”, but that certainly doesn’t apply to us! So, what will it take to convert our existing program into the first pass at a rudimentary electronic Etch A Sketch? Surprisingly little. In fact, all we need to do is tweak a single line of code. Consider the UpdateLocation( ) function in the current version of our program, as illustrated in Listing 2(a). Remember, we’re using the convention that the listing number (2 in this case) corresponds to the numerical part of the matching code file name (“02” here). This function is called whenever Practical Electronics | April | 2026 Photos 1 and 2: Ian’s MDFbased console. we press one of our NSEW direction control buttons. The first thing we do on line 461 is set the current pixel to the background colour (currently BLACK). On lines 462 and 463, we update our X-Y coordinates to reflect their new values. Then, on line 464, we call our UpdateFlash() function to force the pixel at our new X-Y location to start flashing with the current foreground colour. Can you spot which line we need to change? You’re right, it’s line 461. Instead of setting the original pixel to the background colour, we set it to the foreground colour. I’ve just made this change (in the file named CBapr26-code-03.txt), as illustrated in Listing 3(a). Now, as we press our direction control buttons, we leave a trail of glowing pixels behind us. Any colour you want The famous phrase, “You can have any colour you want, as long as it’s black”, is attributed to Henry Ford regarding his revolutionary Model T cars. This reflected his focus on mass production with a single, fast-drying black paint to maximise efficiency on the assembly line, thereby making cars affordable and widely available, alPractical Electronics | April | 2026 though the exact wording (and timing) of the quote is debated. Paradoxically, the only new colour we want for our Etch A Sketch program is black, but there’s no way for us to achieve it… or is there? The problem with the updated version of our code is that we cannot prevent ourselves from leaving a trail of illuminated pixels as we manoeuvre our 459 460 461 462 463 464 465 466 467 468 469 flashing cursor around the array. This is unfortunate because sometimes we may wish to move the cursor without marking the display as we go. My knee-jerk reaction when I first considered this conundrum was to think, “I wish I had decided to include an additional black (K) action button located in the centre of my existing RYGB action buttons.” 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 2(a): our original UpdateLocation() function. 459 460 461 462 463 464 465 466 467 468 469 void UpdateLocation(int8_t deltaX, int8_t deltaY) { DrawPixel(X, Y, ForeGndColor); 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 3(a): our modified UpdateLocation() function. 41 Of course, doing so would have introduced its own problems, not least the lack of available digital input/ output (I/O) pins. The Arduino Uno has 14 such pins. Pins 0 and 1 are off the table because they are used to communicate with the host PC, leaving us with 12. We are using one pin to drive our 14 × 10 array and three pins to drive the chain of 74HC595 8-bit serial-in, parallel-out (SIPO) shift registers that live in our 7-segment display modules, leaving us with eight pins to access our eight existing switches. This means that, using our current scheme, there would be no pins remaining to connect to any additional buttons. Load B Clock B Shift over! Fig.1: using PISO shift registers to support more switches. One solution would be to add two or more 74HC597 8-bit parallel-in, serial-out (PISO) shift registers. As we see from their data sheet (www.ti.com/ lit/ds/symlink/cd54hc597.pdf), these behave in a complementary manner to their 595 cousins. We can load the values from eight switches into a 597 simultaneously (in parallel), then clock these values out serially (one after the other) on a single data line. As with the 595s, it’s possible to daisy-­chain multiple 597s together. This means we can effectively add as many switches as we wish while using only three of our Arduino’s pins. For example, with six 597s, we could support up to 48 switches (see Fig.1). This is tempting for several reasons, but I’ve made the decision to make do with the eight switches we have. The reason for this will become clear shortly. Teaching old buttons new tricks In our current code, whenever we press one of our RYGB action buttons, our switch-handling routine calls the UpdateColor() function, passing it the colour associated with that button. The existing version of this function is shown in Listing 3(b). On line 448, we test whether the current foreground colour differs from the colour we’ve just passed in. If so, on line 450, we set the foreground colour to be the new colour. Then on line 451, we call our UpdateFlash() function to force the pixel to start flashing with the new foreground colour (ie, the one associated with the button we just pressed). Observe that we don’t mess with the background colour because, thus far, we’ve assumed that it’s always set to BLACK. Well, that’s about to change. I’ve just modified this function, as illustrated in Listing 4(a) from the file named CB-apr26-code-04.txt. 42 Data In Load A Clock A Module 2 Digit 5 Digit 4 Module 1 Digit 3 Digit 2 Module 0 Digit 1 Digit 0 595 595 595 595 From MCU 595 To MCU From Switches 597 446 447 448 449 450 451 452 453 454 455 456 457 595 597 597 597 597 597 Data Out void UpdateColor(uint32_t color) { if (ForeGndColor != color) { ForeGndColor = color; UpdateFlash(FORCED); } ActionCtr = UpdateCounter(ActionCtr, 1, MIN_COUNT, MAX_COUNT); NumberToBuffer(ActionCtr, 3, 3); BufferToDisplay(); } Listing 3(b): our original UpdateColor() function. 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 void UpdateColor(uint32_t color) { if (ForeGndColor == color) { ForeGndColor = BLACK; BackGndColor = WHITE; } else { ForeGndColor = color; BackGndColor = BLACK; } UpdateFlash(FORCED); ActionCtr = UpdateCounter(ActionCtr, 1, MIN_COUNT, MAX_COUNT); NumberToBuffer(ActionCtr, 3, 3); BufferToDisplay(); } Listing 4(a): our modified UpdateColor() function. As you can see, I’ve modified the test on line 448. In this new version, we check whether the current foreground colour is the same as the colour we’ve just passed in. If so (for example, the foreground colour is RED and we just pressed the R button), then we set the foreground colour to BLACK and the background colour to WHITE. Otherwise, we set the foreground colour to the colour of the button we just pressed, and we reset the background colour to BLACK. In effect, we’ve made our four action buttons context-sensitive. Pressing any of these buttons once selects that button’s colour; pressing it again toggles us into erase mode; and pressing any action button while we’re in erase mode returns us to that button’s colour. One more thing As a final tweak, when we created our original flashing pixel program that evolved into this month’s Etch A Sketch program, we could only display the four RYGB colours. This meant that we were obliged to select one of these colours as our default start colour. We picked green because that’s my favourite. However, now that we can display black as a colour, it makes sense to set our default foreground and background colours to BLACK and WHITE, respectively (see the file named CB-apr26code-05.txt). This means that we can move our Practical Electronics | April | 2026 flashing white pixel around the array without leaving a coloured trail until we reach the location from which we wish to commence our drawing. While this is only a minor modification in the scheme of things, I find it both aesthetically and logically pleasing. Although our Etch A Sketch implementation is simple, it’s surprisingly enjoyable to play with. Kids of all ages will love it (I know I do)! In fact, I was just thinking that we could all have a little competition to see who can create the best image using the current incarnation of our code. If you wish to email your entry to me, I may include some of the more interesting offerings in a future column. Start SR = 0xFFFF SR = 0xFFFF State = WFAZ SR == 0x0000? WFAZ = WAITING_FOR_ALL_ZEROS JGA = JUST_GONE_ACTIVE WFNAZ = WAITING_FOR_NOT_ALL_ZEROS WFAO = WAITING_FOR_ALL_ONES JGI = JUST_GONE_INACTIVE State = WFAZ Read Switch No States Start Performed by setup() function Functions UpdateSwitches() ProcessSwitches() Read Switch No SR == 0x0000? Yes SR = Shift Register RC = Repeat Counter GRA = Game-Related Action Yes RC = Start Val Additional state and logic to implement auto-repeat function State = JGA State = JGA Optional GRAs Optional GRAs State = WFAO State = WFAO State = WFNAZ Read Switch Read Switch Read Switch Thanks for the memory Before we proceed, it’s worth noting that our original flashing pixel program consumed 4430 bytes (~13%) of flash and 126 bytes (~6%) of RAM. How much more memory do you think we’ve consumed by transforming it into our Etch A Sketch program? The answer is surprisingly little. Due to the way we architected things, we required only small modifications to a couple of core functions. As a result, we added only 78 more bytes of flash consumption and 4 more bytes of RAM, resulting in a usage of 4508 bytes (still ~13%) of flash and 130 bytes (still ~6%) of RAM. Arrgggh (sincerely) I have a confession to make: I’ve made a mistake. I know, I know… I find this as hard to write as you do to read, but even awesome Albert (my uncle, not the other one) had his off days. In the past, we’ve noted that when we instantiate our array of NeoPixels, the system reserves 24 bits of memory for each array element. It uses these to store the three 8-bit RGB values associated with each pixel. In our case, since we are working with a 14 × 10 array, this amounts to 3 × 140 = 420 bytes of memory. What we’ve neglected to consider is where these memory locations live. While they reside in RAM, the problem is that, because the NeoPixel library allocates this memory dynamically (don’t ask), it is invisible to the Arduino’s integrated development environment (IDE), which is why we’ve failed to account for it. The bottom line is that although the IDE reports we’re using only 130 bytes of RAM, we’re actually using 130 + 420 = 550, which is ~27% of the 2048 bytes (2KiB) available to us. This isn’t as innocuous as we’d thought, but it’s Practical Electronics | April | 2026 No SR == 0xFFFF? No SR == 0xFFFF? No SR == 0x0000? Yes Yes Yes State = JGI State = JGI RC = RC - 1 Optional GRAs Optional GRAs RC == 0? No Yes Standard operation 'flowchart' (b) Augmented with are withactive-low). auto-repeat capability Fig.2:(a)our switch-handling (all switches still not worrisome enough to warrant us wearing our frowny faces. Idly pondering For reasons I no longer recall, I was idly pondering the possibility of us adding ‘auto-repeat’ functionality to our buttons. For example, rather than pressing the east (E) button multiple times, I was wondering if it would be advantageous to allow pressing and holding the button to automatically repeat the action. Before we plunge headfirst into the mire, it’s worth taking a few moments to remind ourselves how we are currently debouncing and processing our switches. Each switch is associated with a simple state machine, as illustrated in Fig.2(a). Fig.2 shows state machines presented as pseudo-flowcharts rather than traditional state diagrams or program execution flowcharts (that’s just the way they fell out when I was drawing them). This means we can’t directly translate these diagrams into code; they’re just a way of visualising what’s going on. Each major operating state is shown as a purple rectangle. White diamond shapes represent transition decisions, while white rectangles reflect actions to be executed. In our current code, as shown in Fig.2(a), we decided that our state machine should have four states: WAITING_FOR_ALL_ZEROS (ie, waiting for the switch to become fully/ stably active), JUST_GONE_ACTIVE, WAITING­_FOR_ALL_ONES (waiting for the switch to become fully/stably inactive), and JUST_GONE_INACTIVE. We can shorten these names to WFAZ, JGA, WFAO and JGI, respectively. Remember that a copy of the state machine shown in Fig.2(a) is applied to each switch. The setup( ) function preloads the shift register (SR) associated with each switch with 0xFFFF and sets its state to WFAZ, reflecting the assumption that all our switches commence in their inactive states. If you look at our loop( ) function, you’ll see that once every millisecond, it first calls the UpdateSwitches() function, followed by the Process­ Switches() function. The UpdateSwitches() function handles noise rejection and switch debouncing. It’s also responsible for transitioning the states from WFAZ to JGA and from WFAO to JGI. Meanwhile, the 43 ProcessSwitches() function performs any game-related actions (GRAs) associated with the switch being pressed or released. Once these actions have been performed, this function is also responsible for transitioning the states from JGA to WFAO and from JGI to WFAZ. When you come to a fork… Yogi Berra (1925-2015) was an iconic American baseball player—catcher, manager, and beloved cultural figure. He was famous for his paradoxical and humorous “Yogi-isms”, memorable sayings that appeared nonsensical at first, but often made profound sense the more you thought about them, such as “It’s tough to make predictions, especially about the future”. One of my favourites is, “When you come to a fork in the road, take it”. As with many of Yogi’s Zen-like offerings, there’s more to this than meets the eye. The most common reading is: don’t overthink decisions; just choose a path and move forward. However, there is a backstory here. When approaching Yogi’s house, the road split into two branches, but both forks eventually led to the same destination, so it didn’t actually matter which you chose. I tell you, you won’t find nuggets of knowledge and tidbits of trivia like this in other, lesser electronics magazines! The reason I mention this here is that, like a thief in a cutlery store, I feel the urge to take a fork. That’s not something I expected to hear myself say when I woke up this morning. To be slightly more specific, in software development, a ‘fork’ is an independent copy of an existing codebase created to explore or evaluate changes without affecting the main line of development. That’s what we’re going to do here. Can you repeat that? I don’t want to incorporate autorepeat into our main codebase at this time, so I’ve decided to take a fork. Our starting point will be our original flashing pixel program (CB-apr26-code-02. txt), which uses the RYGB action buttons to modify the colour of a flashing pixel and the NSEW direction buttons to move it around our 14 × 10 array. I’ve just forked this code into a test program named CB-apr26-code-06.txt. In our original sketch, as depicted in Fig.2(a), each switch uses a simple four-state debounce state machine that generates one action-triggering event per press. This event occurs when the input becomes stably active and the machine enters its JUST_GONE_ACTIVE state. In the case of the action buttons, 44 the event changes the flashing pixel’s colour, while the direction buttons modify its location. To add auto-repeat functionality to the direction buttons, we augment the per-switch state machines, as shown in Fig.2(b). First, we extended the state machine by adding a repeat counter, rptCtr (labelled RC in the diagram), to the SwitchStuff structure. We also need to introduce a new WAITING_FOR_NOT_ALL_ZEROS (aka WFNAZ) state, which we can think of as ‘waiting for the switch to stop being active and commence its transition to becoming stably inactive’. We’ll use this state to distinguish between the button being held down and it being released. The only change to the Update­ Switches( ) function is that, once a switch becomes stably active, the code sets rptCtr to its initial delay value. We then enter the JUST_GONE_ACTIVE state, as before. When the ProcessSwitches() function sees the JUST_GONE_ACTIVE state, it first performs any game-­related actions (GRAs) associated with that switch. For the action switches, which don’t require auto-repeat (at least, not in this instance), the state machine continues to transition to the WAITING­_FOR_ALL_ONES state, as shown in Fig.2(a). By contrast, for the direction switches, it transitions to our new WAITING_ FOR_NOT_ALL_ZEROS state. While the state machine remains in that state and the button is still pressed, the code decrements rptCtr on each tick. When the counter expires (reaches zero), the code reloads rptCtr and generates another JUST_GONE_ACTIVE event, resulting in repeated movement in the selected direction. As soon as the button is released, the state machine transitions to WAITING_FOR_ALL_ONES, after which it proceeds exactly as before. I just ran a quick test, and this works rather well. Pressing any of the action buttons changes the flashing pixel’s colour as before (pressing and holding the action buttons has no effect because we haven’t modified their state machines). Pressing and releasing any of the direction buttons moves the flashing pixel one step in that direction, while pressing and holding them keeps it moving until the button is released. As I noted earlier, I’m not sure if we will ever need this functionality, but now we know we can do it if we want to. puter game that emerged in the 1970s and has been continuously reinvented ever since. It belongs to a small, illustrious club of classic games, alongside titles like Pong and Tetris, that prove you don’t need fancy graphics or complex rules to create something engaging. Snake’s inclusion on Nokia mobile phones in the late 1990s made it one of the most widely played games in history. The rules are instantly understandable (move, eat, grow, don’t crash), yet the gameplay remains compelling. Snake still appears today in browsers, embedded systems demos, coding tutorials, and even as Easter eggs. At its heart, Snake is beautifully simple: a series of connected segments move across a grid, one step at a time, changing direction under the user’s control, growing longer as the game progresses and demanding ever more careful control. This simplicity makes it perfect for newbie programmers because every part of the game—position, direction, movement, and state—cleanly maps onto fundamental programming concepts. In our case, we’ll bring this timeless concept to life on the 14 × 10 array of tricolour LED ‘pixels’ on our retro game console. We’ll take this step-by-step (or slither-by-slither), starting by creating a short, four-segment snake at a known location. Later, we’ll learn how to make it move, turn, and eventually (in later stages) hunt for food and grow. But first, we need to set the scene… Circular thinking Let’s suppose that, at some time in the future, we end up with an 11-­segment snake, as illustrated in Fig.3. One segment will act as the head (we’ll colour this purple), with the remaining 10 segments acting as the body (we’ll colour them green). From Fig.3, we can deduce that, over the past couple of moves, the snake has been heading east. There are only three possible options for its next move: to carry on moving east, or to change direction and head north or south. It can’t move west because that would mean moving back into itself. Although the representation in Fig.3 appears simple, it hides underlying Tail Head Timeless classics Snake is an arcade and early com- Fig.3: an 11-segment snake. Practical Electronics | April | 2026 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 164 165 166 167 168 169 170 171 172 #define TICK #define SNAKE_MOVE_TICKS 1 500 #define NUM_SEGMENTS #define START_HEAD #define START_TAIL #define START_LENGTH #define START_LEVEL 140 3 0 4 1 #define NUM_DIRECTIONS #define NORTH #define SOUTH #define EAST #define WEST 4 0 1 2 3 #define HEAD_COLOR #define BODY_COLOR MAGENTA GREEN typedef struct { uint8_t x; uint8_t y; } SnakeSegment; SnakeSegment Snake[NUM_SEGMENTS]; int16_t SnakeHead; int16_t SnakeTail; int16_t SnakeLength; uint8_t SnakeDirection; uint8_t GameLevel = START_LEVEL; uint32_t TimeThen = micros(); uint32_t TimeNow; uint32_t SnakeThen = TimeThen; Listing 7(a): snake-related stuff. layers of complexity. For example, although we think of our ‘play area’ as a 2D array, the pixels (LEDs) themselves are wired as a single long serial string. This means that snake segments that occupy adjacent squares on the grid might be dozens of pixels apart in the underlying 1D string. Things become more complicated when we want the snake to advance. Fortunately, we don’t have to redraw every segment. In the absence of any “food” (the “eating” of which will make the snake grow longer), moving the snake simply involves placing the head segment in its new position, changing the old head segment into a body segment, and deleting the tail segment. On the downside, using a 2D array that ultimately maps onto a 1D string to track the segment locations can be a pain. The solution is to use a circular buffer. As a worst-case scenario, our snake can grow so large that it fills the grid, so we will create a circular buffer of 14 × 10 = 140 elements, numbered from 0 to 139, each capable of storing the X-Y location of one of the snake’s segments. Let’s imagine the 11-segment snake from Fig.3 being stored somewhere in a 140-element circular buffer, as depicted in Fig.4. Once we’ve generated the initial snake, stored it (in the circular buffer), and drawn it (on the array), all we need to do is keep track of the locations of the head and tail in the circular buffer. These can be represented as integers (I think of them as pointers), which we use to index into the buffer. Now, when the snake advances, in addition to updating the colours of any related pixels on the array, all we need to do is increment the head and tail pointers into the buffer. The reason we call this a circular buffer is that we manipulate it so that its end ‘wraps around’ to its beginning. We will also keep track of the snake’s length, which will eventually form the score in our game. The longer the snake, the higher the score. When you think about it, if we know the value of the head pointer and the length of the snake, then we can easily generate the value of the tail pointer on-the-fly whenever we feel the need. Personally, however, I prefer to maintain both head and tail pointers (it’s my ‘programmer’s prerogative’). Hello, snake! Traditionally, a programmer’s first tentative step when learning a new language or working with a new system is to coax the computer to print some variant of “Hello, world!” (the original C Programming Language book used “hello, world” without capitalisation or an exclamation mark). The underlying idea is both modest and powerful: to prove that the toolchain works, confirm that text can be produced, and celebrate the smallest possible success before attempting any­thing more ambitious. We are going to do something similar with our Snake program. We’ll start by generating a four-element snake, storing it in our circular buffer, and drawing it on our array. I just copied our fundamental frame- Tail Head Length = 11 x x y y x y Fig.4: a 140-element circular buffer. Practical Electronics | April | 2026 x y x y x y x y x y x x y y 139 x x y y 137 138 x y n+9 n + 10 x y n+7 n+8 x y n n+1 n+2 n+3 n+4 n+5 n+6 x y 0 1 2 x y work (named CB-apr26-code-01.txt) and made a few tweaks to implement a “Hello, snake!” program (in the file named CB-apr26-code-07.txt). Our first task is to add game-­related definitions and declarations, as illustrated in Listing 7(a). We still have our 1ms TICK definition on line 138, to which we add a 500ms (half-second) SNAKE_MOVE_TICKS definition on line 139 (we won’t be using this in this program, but it’s there for when we need it). On lines 141 through 145, we define the number of elements in our circular buffer (140), the values of the snake’s head and tail pointers into the buffer (3 and 0), the length of the snake (4 segments) and the game’s starting level (1). On lines 147 through 151, we define the number of directions the snake can move (4) and associate values of 0, 1, 2, and 3 with directions north, south, east and west, respectively. On lines 153 and 154, we define the colours associated with the snake’s head and body segments. On lines 156 through 160, we define a structure to store the X-Y coordinates associated with an individual snake segment. Then, on line 162, we instantiate our circular buffer as a 140-­element array of these structures. Finally, on lines 164 through 172, we declare the variables that hold the snake’s current state—head and tail indices (pointers) into the circular buffer, the snake’s length and direction, and the current game level—along with a couple of timing variables we’ll use to schedule events like snake motion without resorting to blocking delays. If you look at the setup() function in CB-apr26-code-07.txt, you’ll see that we’ve added calls to two new functions at the end, just after the calls to our power-on self-test functions. The first, InitializeSnake( ), generates our 4-element snake, stores it in our circular buffer, and draws it on our array. For simplicity, we’re starting with a horizontal snake pointing east (Fig.5). The second, InitializeDisplays(), sets up our 7-segment displays, presenting “L” (for level) in the most significant digit (digit 5), the current game level (1) in digit 4, and the current length of the snake (4) in the three least significant digits (2, 1, and 0). Fig.5: hello there, snake! 45 I just ran this program, and it looks very… static, but at least we’ve laid the groundwork for what’s to come. A simple slither Now, let’s make our snake a little more animated. We’ll start with a simple ‘slither’ by just making it move eastward. Furthermore, when the snake reaches the right-hand side of the array, it will ‘wrap around’ and reappear on the left-hand side. First, in the file CB-apr26-code-08. txt, we modify our loop() function, as seen in Listing 8(a). The only change here appears on line 217, where we call a new UpdateSnake() routine. It’s worth noting that we are still calling the functions to debounce and process our switches (lines 218 and 219, respectively); it’s just that we aren’t actually doing anything with them yet, but we will… Next, we add the new UpdateSnake() routine itself, as illustrated in Listing 8(b). One thing you’ll notice is that the controlling construct on line 474 is very similar to the one we used in the UpdateFlash( ) function in our Flashing Pixel and Etch A Sketch programs. The way this is currently set up, with SNAKE_MOVE_TICKS defined as 500ms, is that the snake will move every half-second. One thing we might do in the future is to gradually decrease this value, thereby speeding up the snake and increasing the difficulty in controlling it as we progress through the game’s levels. On line 476, we determine the location of the new head pointer for use with our circular buffer. We’ve seen this use of the modulo (%) operator before. In simple terms, this modulo operation makes the buffer behave as if its ends are joined together. 210 211 212 213 214 215 216 217 218 219 220 221 222 223 Each time we advance the head pointer, it increments by one. When it reaches the last element, the modulo calculation quietly wraps it around to zero so we can continue from the start without any special-case code. In the next iteration of the code, we’re going to allow the snake to move in any direction. For the moment, however, we’re assuming it always wants to move east, which explains lines 478-486. Observe the use of the modulo operators on lines 488 and 489. These allow the snake to wrap around the array in any direction, east to west and north to south. The reason we’ve added + NUM_COLS in line 488 and + NUM_ROWS in line 489 is to prevent problems if deltaX is -1 or deltaY is -1, respectively. I’ve just run this incarnation of the program, and it’s a wonder to behold, but it leaves me wanting more. // Do this over and over again void loop() { TimeNow = millis(); if ( (TimeNow - TimeThen) >= TICK) { UpdateSnake(); UpdateSwitches(); ProcessSwitches(); TimeThen = TimeNow; } } Listing 8(a): tweaking the loop() function. Cycle 0 Cycle 1 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 Put your snake on automatic If I had a coconut for every time I’ve heard someone say, “Put your snake on automatic”, I wouldn’t have many coconuts, but that’s not important here. What we’re going to do now is allow our snake to move randomly around the array. First, however, let’s return to the rationale for the order in which we perform the actions of removing the tail segment before moving the head segment, as shown in Listing 8(b), lines 495-496 and 498-502, respectively. There are two ways our snake could move. It could stretch out its head and then pull in its tail, or it could pull in its tail and then stretch out its head. We’ve chosen to implement the latter. This reflects the behaviour of the classic Snake game, where the head can move into the square the tail has just vacated. void UpdateSnake () { int16_t tmpSnakeHead; uint8_t tmpX; uint8_t tmpY; uint8_t deltaX; uint8_t deltaY; if ( (TimeNow - SnakeThen) >= SNAKE_MOVE_TICKS) { tmpSnakeHead = (SnakeHead + 1) % NUM_SEGMENTS; // Generate new X-Y location for the head // This part coming soon // Let's asume the direction is east if (SnakeDirection == EAST) { deltaX = 1; deltaY = 0; } tmpX = (Snake[SnakeHead].x + NUM_COLS + deltaX) % NUM_COLS; tmpY = (Snake[SnakeHead].y + NUM_ROWS + deltaY) % NUM_ROWS; // Remove tail segment DrawPixel(Snake[SnakeTail].x, Snake[SnakeTail].y, BLACK); SnakeTail = (SnakeTail + NUM_SEGMENTS + 1) % NUM_SEGMENTS; // Set old head segment to body color DrawPixel(Snake[SnakeHead].x, Snake[SnakeHead].y, BODY_COLOR); // Move head and set it to head color SnakeHead = tmpSnakeHead; Snake[SnakeHead].x = tmpX; Snake[SnakeHead].y = tmpY; DrawPixel(Snake[SnakeHead].x, Snake[SnakeHead].y, HEAD_COLOR); Neos.show(); SnakeThen = millis(); } } Listing 8(b): implementing a simple eastward slither. Cycle 2 Cycle 3 Cycle 4 Cycle 5 Fig.6: don’t eat my tail! 46 Practical Electronics | April | 2026 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 // Pick a new direction (can't be 180 degrees) do { tmpDirection = random(0, NUM_DIRECTIONS); } while (tmpDirection == (SnakeDirection ^ 1)); SnakeDirection = tmpDirection; // Generate new X-Y location for the head if (SnakeDirection == NORTH) {deltaX = 0; deltaY = 1;} else if (SnakeDirection == SOUTH) {deltaX = 0; deltaY = -1;} else if (SnakeDirection == EAST) {deltaX = 1; deltaY = 0;} else if (SnakeDirection == WEST) {deltaX = -1; deltaY = 0;} www.poscope.com/epe tmpX = (Snake[SnakeHead].x + NUM_COLS + deltaX) % NUM_COLS; tmpY = (Snake[SnakeHead].y + NUM_ROWS + deltaY) % NUM_ROWS; Listing 9(a): moving in a random direction. By removing the tail first, we avoid falsely detecting a collision with the snake’s own body and allow the familiar smooth, continuous motion players expect. It’s only when the snake eats food (which we’ll implement next month) that we skip the tail-removal step, thereby causing the snake to grow by one segment. All this is depicted in Fig.6. Our snake starts by progressing eastward (Cycle 0). Then it turns south (Cycle 1), then west (Cycle 2), then north (Cycle 3). This is the key point in the snake’s journey. If we didn’t remove the tail segment before drawing the new head segment, our snake would effectively try to eat its own tail, which would be a bad thing to happen (ask a real snake if you don’t believe me). The only changes we need to make to implement the snake’s random motion in the file named CB-apr26-code-09. txt are illustrated in Listing 9(a). The first thing we do on lines 480-482 is loop around, generating a new random direction (0, 1, 2 or 3), corresponding to north, south, east and west, respectively. The rule we need to implement here is that the snake cannot ‘go back on itself’. To put this another way, if it’s heading north, it cannot decide to head south (and vice versa). If it’s Practical Electronics | April | 2026 heading east, it cannot choose to head west (and vice versa). The key observation is that opposite directions form pairs: north ↔ south (0 ↔ 1) and east ↔ west (2 ↔ 3). This means each pair differs by only the least significant bit. Using the XOR operator (^) in the expression SnakeDirection ^ 1 in the conditional test on line 482 performs a delightful bit of digital sleight-ofhand, flipping the least-significant bit of the current direction to swap north with south and east with west. If the random choice matches this forbidden opposite, we simply try again until a legal direction appears. Once we’ve determined the snake’s new direction, we use lines 487-494 to set up appropriate deltaX and deltaY values, which we then use to generate the new X-Y location of the snake’s head. I don’t know about you, but I feel a little “Tra-la” is in order. As I pen these words, my four-segment snake is happily roaming around its little universe. Next time Our next task will be to take control of the snake’s path using our direction buttons. We’ll also start randomly adding food, the eating of which will make our snake grow longer. We w i l l k e e p adding games and performing experiments with our console in the months to come. At the same time, we will be starting a new project that’s so exciting I can barely contain myself. As always, if you have any thoughts you’d care to share, please feel free to drop me an email at max<at>clivemaxfield. PE com - USB - Ethernet - Web server - Modbus - CNC (Mach3/4) - IO - PWM - Encoders - LCD - Analog inputs - Compact PLC - up to 256 - up to 32 microsteps microsteps - 50 V / 6 A - 30 V / 2.5 A - USB configuration - Isolated PoScope Mega1+ PoScope Mega50 - up to 50MS/s - resolution up to 12bit - Lowest power consumption - Smallest and lightest - 7 in 1: Oscilloscope, FFT, X/Y, Recorder, Logic Analyzer, Protocol decoder, Signal generator 47