DIY 8-bit handheld multiplayer gaming device

Erik van Zijst
11 min readMar 24, 2022

Building a super basic 8-bit, retro handheld gaming device with wireless infrared multiplayer capability.

After wrapping up my earlier Arduino-run Tetris project using a breadboard for a controller, I wanted to take a stab at packing it all onto a custom PCB and creating a super basic handheld gaming device, leveraging my existing code and matrix LED displays.

Multiplayer Tetris battle between 2 devices using wireless infrared communication

This post offers a summary of the design and construction of this project for which I had used Twitter as a more detailed lab notebook.

Display

I wanted to carry the matrix panels from the other project, except that I wanted the display to be a little bigger and so I added 2 more panels for a total resolution of 24x16, or 384 discrete LEDs.

I didn’t want to use an off the shelf LCD display. It would have been great for gaming, but I felt I hadn’t pushed the custom display far enough yet. On the Arduino about half of all cycles went to driving the screen. This screen would be 50% larger and so I wanted to find a more efficient design.

Matrix displays don’t have individual pins for each LED. Instead, the LEDs are connected in a grid pattern, exposing only the row and column terminals. Only a single row or column can be addressed at a time and so to display a steady image covering the entire display, you need to quickly scan over the rows/columns, refreshing them faster than the eye can see.

One performance improvement over the Arduino project is using a parallel bus to the display. On the Arduino, the column bits for the currently active row were clocked out in serial over a single line to a 16 bit shift register. Going to 24 cols would make that 50% slower.

Instead, if we divide the screen in 3 logical sections; columns [0–7], [8–15] and [16–23] and we use an 8 lane bus, we can clock out 8 bits in a single instruction. By giving each section a dedicated 1-byte register or octal-latch chip on the board, we can write distinct values to each section.

Instead of serially, column values are pushed out 8 bits at a time over an 8 lane bus

We put the value for the columns [0–7] on the bus, then toggle the clock line of the first latch. Then put the value for columns [8–15] on the bus, toggle the second latch, and repeat for the third.

If we create a frame buffer data structure that reflects this segmentation (uint8_t screen[16][3]), we can clock out 24 column bits in just 9 register operations (VPORT3.OUT is the 8 bit data bus):

VPORT3.OUT = screen[row][0];     // put 8 bits on the bus
VPORT2.OUT |= SEGCLK0; // bring clk of latch #1 high
VPORT2.OUT &= ~SEGCLK0; // bring clk of latch #1 low

VPORT3.OUT = screen[row][1]; // put 8 bits on the bus
VPORT2.OUT |= SEGCLK1; // bring clk of latch #2 high
VPORT2.OUT &= ~SEGCLK1; // bring clk of latch #2 low

VPORT3.OUT = screen[row][2]; // put 8 bits on the bus
VPORT2.OUT |= SEGCLK2; // bring clk of latch #3 high
VPORT2.OUT &= ~SEGCLK2; // bring clk of latch #3 low

Microcontroller

For the microcontroller I wanted something close to an Arduino so I’d be able to benefit from the enormous community and freely available libraries and components.

An actual ATmega328 didn’t have enough IO pins. The display would be needing a lot more pins and I wanted more buttons. There’s the ATmega2560 which is basically an Arduino with more IO pins, but I picked the ATxmega256c3 instead.

The Atmel XMEGA in 64 pin QFP package

The XMEGA is still an 8 bit AVR chip, but it runs at double the clock speed (32MHz), has more sophisticated IO port registers and depending on the variant, more peripherals like on-chip USB and hardware CRC.

It also has IrDA (infrared) support for one UART. It would be awesome to try and use that for wireless multiplayer support between 2 devices.

Battery powered

Because we’re building a handheld device, it needs to be battery powered. Since we’ll also have a USB port that can deliver power, it’d be great if it would only draw from the battery if USB is not connected and run off USB otherwise.

USB provides 5V, so I opted to use four standard 1.5V AA batteries in series to get into the same ballpark as USB. AA batteries come with different chemistries and voltage curves. Starting at 1.5V for a typical fresh Alkaline battery (but ranging from 1.2V for NiCd to 1.6V for NiZn), the actual voltage drops substantially under load and during discharge.

Typical alkaline battery discharge curve

The xmega needs a stable 3.3V and so we need decent voltage regulation. I picked a step-down DC-DC converter that can take in up to 5.5V and provide 600mA at 3.3V output.

While 600mA is sufficient to also power the display (one row of 24 LEDs at 0.15mA each is 360mA), 3.3V probably is not. At least not for green and blue LEDs that have a large voltage drop. Because I want to be able to use green LED panels, I chose to run the LEDs directly off the board’s unregulated power supply, relying on 24 constant current drivers.

The board would have two power nets: 3.3V and unregulated ~3–5.5V.

Automatic switching between USB and battery power

PCB assembly

I decided to go with a 4 layer PCB and all SMT components this time. At 240x76mm it’s quite large and with 4 layers, it got really expensive at OSHPark ($289.30) so instead I used JLCPCB who quoted $41 for a 5 board batch.

KiCad PCB layout

This was also the first time ordering a stencil and solder paste, as I would do the assembly myself using my rework station’s hot air gun.

PCBs manufactured by JLCPCB
Black solder mask
Fancy silkscreen art on the back
The target PCB surrounded by other PCBs to provide a level surface for the stencil
Stencil aligned over the PCB and secured with masking tape
Solder paste applied to the stencil

Properly applying solder paste through a stencil is craft. After reflow, the first board had quite a few solder bridges that needed careful testing and touching up. The second and third went better.

Solder paste applied, not too shabby!
This is the largest board I’ve done with a lot of components
Ugly solder bridges
Quite pleased with the result
A tiny infrared transceiver
Fully assembled
The back has the USB port, on/off switch and 2 AA battery holders
Power rails nominal at bringup

PCB mistakes

After I ordered the PCBs but before they arrived, I noticed a problem with the boards that KiCad’s DRC report had been trying to tell me about. I had the ground and power plane touching!

Luckily I discovered it before applying power to the board and the fact that it was on the top layer meant I could “fix it” with a knife.

Making a cut between the overlapping ground and power planes

The next self-inflicted problem was me ordering the wrong package flip flops.

Make sure you order the package that matches your footprint

However, after I had ordered the properly sized 74LV273 and reworked the board, it didn’t work and I noticed it wasn’t even the right chip to begin with!

In the design I had used the LV573 which has a very different pinout and that meant waiting for another DigiKey order.

Comically, even with the proper 54LV573 chip, things still didn’t work and at this point I realized the first mistake had occurred months earlier when creating the schematic symbol. I don’t know where I got ~CLR for pin 1 from, as it should be ~OE. As such, it needs to be tied low and not high as on my board.

Incorrect pin 1 on the custom schematic symbol

Sadly this wasn’t the first schematic symbol I got wrong, but luckily it was easily patched.

Pin 1 of the octal latch IC bent up
Bodge wire from pin 1 to ground

Back plate

To give the large PCB a bit more rigidity, I laser-cut a wooden back plate. By using two layers, I could create embedded recesses for the through-hole parts.

Two-part laser-cut back plate
Gluing together top and bottom parts
Recesses for the through-hole parts
Final assembly

Tetris firmware

For the firmware I was hoping to start by just copying the Tetris code I had written on the Arduino earlier.

Earlier Tetris implementation on Arduino

Unfortunately, because the XMEGA is sufficiently different from the ATmega in terms of its peripheral memory mapping, GPIO capabilities, header files and other components, Arduino code does not run as-is. By extension, the Arduino IDE cannot be used, nor any Arduino software libraries.

This was a bummer, as Arduino compatibility was a factor in choosing the XMEGA microcontroller over, say, one of the many 32-bit ARM-based microcontrollers, many of which run at higher clock speeds too.

That said, Microchip/Atmel provide tools to bootstrap initial code for peripheral configuration, clock setup and pin assignment. I used Atmel START to generate a gcc/Makefile-based project archive as a starting point that I modified and built upon with my own tools.

Atmel START generates custom firmware templates online

8-bit sound

I included a piezo buzzer on the board so I could add some authentic retro style 8-bit sound effects. A piezo buzzer is different to a speaker in that it does not contain a coil or magnet, but a piezoelectric material that deforms when exposed to an electric field. Like a capacitor, it does not conduct DC current and can be connected directly to an IO pin.

A piezo buzzer next to the PDI programming header

To create a tone, we toggle the IO pin at the frequency of the desired note. To do this, we configure a hardware timer to trigger an interrupt routine toggling the pin at the two times the frequency of the note with a square wave. This works for single notes, but is impractical for chords.

Robson Cuoto had collected a bunch of tunes tunes to be played in this manner on Arduino, from which I lifted the Tetris melody.

IrDA: multiplayer over infrared

As the XMEGA comes with IrDA pulse modulation support for a UART, I decided to add an IrDA transceiver module to the board. Not knowing anything about infrared communication, I picked a component largely at random. It would be a stretch goal.

The tiny, tiny Vishay TFBS4711 IrDA transceiver
Mounted vertically at the very edge of the board, the transceiver faces directly forward

Getting this to work was a real challenge and worthy of a separate blog post.

Infrared light made visible on a digital camera

Because the infrared sensor is mounted directly next to the LED, effectively creating a loopback channel, a copy of each transmitted frame comes back in through the receiver. To make bidirectional communication work, we need to be able to distinguish these frames from those from the other device.

Next is the problem of collisions. When both stations start a transmission at the same time, everything gets corrupted and so stations need to coordinate who can safely transmit when.

Data gets corrupted easily over infrared

Think CB radio. You wait until the channel is clear and press the mic button to speak. You say “over” to signal you’re done transmitting.

Following this model, I ended up writing a simple protocol to synchronize two stations into a pattern of taking strict turns transmitting and receiving.

Debugging bidirectional infrared communication

When it’s a station’s turn, it can send a stream of data frames, followed by a special marker frame to indicate it’s done. This tells the peer it should switch to TX mode, while we move to RX, keeping the state machines synchronized while avoiding collisions.

If a station has nothing to send, it just sends the end-of-packet frame to hand back over.

During the game, the 16x10 Tetris field is stored as a bitmap of 16 uint16_t’s (ignoring the last 6 bits of each word) and at every turn it just sends the entire bitmap. At 115200 baud, transmitting such 32 byte packet, followed by receiving the peer’s response takes ~10ms, for ~100 real-time screen updates per second.

Stations take turns sending 32 byte packets at 115200 baud

Gameplay

The firmware boots up with a selection between single player or multiplayer. In single player mode the player is awarded points for completed lines, with the game speeding up as the score increases.

Classic single player Tetris with previews (right) and hold function (left)

The player can see the next 4 upcoming bricks and has a “hold” feature to temporarily save a brick for later use. The game stores the highest score in EEPROM.

High scores are stored in EEPROM

Mulitplayer games are quick with a winner and a loser. Whenever a line is completed, it gets added to the bottom of the opponent’s board, quickly filling up the screen. Whenever the connection is temporarily lost, the game pauses. With the transceiver’s rather limited < 0.7m range and narrow angle, this is not unimportant.

Two devices playing multiplayer Tetris over infrared communication

Resources

The entire projects is MIT-licensed as Open Source Hardware and Software and hosted on GitHub, which includes the KiCad schematic and PCB layout files. Also included is the full single and multiplayer firmware.

--

--