Silicon ChipMax’s Cool Beans - May 2026 SILICON CHIP
  1. Contents
  2. Publisher's Letter: Product codes should be specific to the hardware
  3. Feature: Net Work by Alan Winstanley
  4. Feature: Max’s Cool Beans
  5. Feature: The Fox Report
  6. Feature: Teach-In 2026 by Mike Tooley
  7. Project: SSB Shortwave Receiver by Kosina, VKEBAR
  8. Feature: Circuit Surgery by Ian Bell
  9. Project: 433MHz Transmitter Module by TIM BLYTHMAN
  10. Feature: AUDIO OUT by Jake Rothman
  11. Project: Tool Safety Timer by Phil Prosser
  12. Feature: Techno Talk by Max the Magnificent
  13. Project: Power Electronics by Andrew Levido
  14. Feature: Techno Talk by Max the Magnificent
  15. PartShop
  16. Advertising Index
  17. Market Centre
  18. Back Issues

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

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

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