MatTris Arcade

by Arnov Sharma in Circuits > LEDs

295 Views, 3 Favorites, 0 Comments

MatTris Arcade

IMG-20261005-WA0028.jpg
IMG-20261005-WA0026.jpg
IMG-20261005-WA0042.jpg
I Built My Own Mini Game Console! | MatTris Arcade

Meet MatTris, a DIY Tetris console built completely from scratch.

There’s something about those old handheld game consoles that I still really like.

I was born in 1998, and my first introduction to Tetris was through those small monochrome LCD handhelds that came with 50, 100, sometimes even 999 built-in games. Most of them were pretty basic, but somehow you could spend hours playing them.

A few years ago, I decided to recreate that feeling using the kind of hardware I work with now. That project became MatTris, a small Tetris machine built using a WS2812B LED matrix and a Waveshare RP2040 Tiny.

You can check out the original project here: MatTris – Tetris on an LED Matrix.

I was pretty happy with how it turned out, but after using it for a while, I started thinking about the things I could have done differently. The enclosure was one of them. Also, having only Tetris on it felt like a bit of a missed opportunity.

So I decided to rebuild the whole thing.

For the new version, I wanted to keep the same basic idea but make it feel more like a small arcade console. I designed a completely new case, made some changes to the overall build and added a few more games that work with the same LED matrix.

The electronics are still based around a WS2812B mini LED matrix and a Waveshare RP2040 Tiny. The custom PCB handles the LED matrix, buttons and power management, and the whole thing runs from a rechargeable LiPo battery.

The main challenge was actually getting different games to work on such a small display. With only a 14×20 matrix, you don't have a lot of space to work with, so I had to keep the games and controls simple while still making them fun to play.

This is basically the second version of the project, but with a lot more stuff packed into it.

So let's take a look at how I built the new MatTris Arcade

let's get started.

Supplies

The following materials were used in this project:

  1. Custom PCB (Provided by HQ NextPCB)
  2. RP2040 Tiny Waveshare board
  3. WS2812B 2020 Package LEDs
  4. 100 nF Capacitors 0603 Package
  5. 10 uF Capacitor 1206 Package
  6. IP5306
  7. 10 uH Inductor
  8. Indicator LED 0805 Package
  9. USB Type-C port
  10. Push Buttons 6x6 SMD
  11. 2 Ohms 1206 Package
  12. RP2040 Tiny Programmer
  13. 3D Printed Enclosure

Hardware

41fuIItkKnL.jpg

The main controller for the MatTris Arcade is the Waveshare RP2040 Tiny. I kept the same controller from the previous version because the RP2040 has more than enough processing power for these small games and it is also quite compact.

The RP2040 is based on the Raspberry Pi RP2040 dual-core ARM Cortex-M0+, running at up to 133 MHz. In this project, one of its cores isn't really being pushed that hard, but having the extra processing power does make things like the LED updates, button input and game logic pretty comfortable to handle.

The RP2040 Tiny also keeps the overall PCB small, which was important here because I wanted to leave as much space as possible for the 14 × 20 LED matrix.

The controller handles the WS2812B LEDs, reads all the game buttons and runs the complete game logic. The code and all six games are stored directly on the RP2040, so no phone or external device is needed while playing.

PCB DESIGN

Screenshot 2025-12-19 101255.jpg
Screenshot 2025-09-07 211506.jpg
Screenshot 2025-09-07 211358.jpg
Screenshot 2025-12-19 101649.jpg
Screenshot 2025-12-19 101709.jpg

The PCB design process begins with preparing the schematic for the project. In our case, we had previously worked with WS2812B 2020 package LEDs. These LEDs are not the easiest to work with because of their small size, but that same compact form factor is exactly what makes them ideal for matrix-based projects.

We reused the schematic from our earlier 8×8 Tiny Matrix project as a starting point. Instead of an 8×8 layout, we expanded the design to a 14×20 matrix by adding more LEDs.

We then added another section to the schematic for the RP2040 Tiny board, which was connected to six push buttons. All buttons are connected to GND and individual GPIO pins. When a button is pressed, it pulls the corresponding GPIO pin low, and the microcontroller registers this as a button press.

We used the Waveshare's RP2040 Tiny, which is an RP2040-based SMD development board designed to be mounted directly onto a PCB as a module, similar to ESP-12F or ESP32 modules. The RP2040 Tiny features castellated pads, which allow it to be soldered onto the backside of the PCB and integrated cleanly into the overall design.

Check out its wiki for more info—https://www.waveshare.com/wiki/RP2040-Tiny

For power, we’ve included our trusted IP5306—a power management IC we’ve used in many previous projects. It provides a stable 5V output from a 3.7V lithium cell and includes features like charging indication, low battery warning, and high/low voltage cutoffs, all essential for safe lithium cell operation.

NextPCB PCB SERVICE

03.gif
04.gif
IMG_0218.JPG
IMG_0217.JPG

After completing the PCB design, Gerber data was sent to HQ NextPCB, and an order was placed for a green solder mask with white silkscreen; we even ordered a stencil this time.

After placing the order, the PCBs and stencil were both received within a week, and the PCB quality was pretty great.

In addition, I have to bring in HQDFM to you, which helped me a lot through many projects. Huaqiu’s in-house engineers developed the free Design for Manufacturing software, HQDFM, revolutionizing how PCB designers visualize and verify their designs.

Take advantage of NextPCB's Accelerator campaign and get 2 free assembled RP2040-based PCBs for your innovative projects.

https://www.nextpcb.com/blog/rp2040-free-pcba-prototypes-nextpcb-accelerator

This offer covers all costs, including logistics, making it easier and more affordable to bring your ideas to life. SMT services can be expensive, but NextPCB is here to help you overcome that hurdle. Simply share your relevant project, and they'll take care of the rest. Don't miss out on this amazing opportunity to advance your tech creations!

STENCIL PROCESS

06.gif
05.gif
07.gif

We started the circuit assembly process by applying solder paste to the PCB, but there was a slight problem. The number of components used in this project is quite large, which also means a very high number of pads. Because of this, using a solder paste dispensing syringe would be impractical. Instead, we used a custom stencil made specifically for this circuit.

A stencil is essentially a thin metal sheet with laser-cut slots and holes that match the component pads on the PCB. Using a stencil allows solder paste to be applied evenly across all pads. In industrial settings, where circuits are mass-produced, stencils are commonly used for this purpose.

  1. We first set up a makeshift PCB and stencil holder using extra PCBs. This setup helped keep the main PCB stationary and ensured it did not move during the process.
  2. After aligning the stencil openings with the PCB component pads, we applied solder paste over the stencil and used a scraper to spread it evenly across the slots. This process deposited a thin, uniform layer of solder paste onto each component pad, preparing the board for component placement.

SMD COMPONENTS PLACEMENT PROCESS

08.gif
10.gif
11.gif

Now comes another tedious process: SMD component placement. It is tedious because of the sheer number of LEDs used in this project. In total, we used 280 LEDs along with several other SMD components, bringing the overall component count to over 300. All of these components had to be placed manually.

  1. We started by placing the 100 nF decoupling capacitors in their designated locations. For the pick-and-place process, we used ESD-safe tweezers.
  2. Next, we placed all 280 SMD RGB LEDs onto their pads, making sure each LED was oriented correctly, as incorrect orientation would prevent the LEDs from working.
  3. After the LED placement, we added the remaining SMD components, which included the SMD push buttons and all the parts related to the power management circuit.
  4. Once all components were placed, the entire board was carefully lifted and placed onto an SMD reflow hotplate. The hotplate heats the PCB from below up to the solder paste melting temperature, which is around 200°C. As a result, all the components are soldered securely to their pads.

TESTING SMD LEDs

13.gif
12.gif

Before we move ahead with the placement of the RP2040 board on the backside of the PCB, we tested whether the LEDs were working or not. If the LEDs were not working and the RP2040 was already mounted on the back side of the PCB, we would not be able to use a hot plate to correct the LEDs.

We added jumper wires to the 5V, GND, and GPIO0 terminals of the RP2040. These jumper wires were then connected to a XIAO SAMD21 M0 development board.

We uploaded the sketch below to the XIAO, which turns on each LED in the chain one after another in green. This sketch was taken from the example sketches of the Adafruit NeoPixel library.

#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#endif
#define PIN 0
#define NUMPIXELS 280
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
#define DELAYVAL 50
void setup() {
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
clock_prescale_set(clock_div_1);
#endif
pixels.begin();
}
void loop() {
pixels.clear();
for(int i=0; i<NUMPIXELS; i++) { // For each pixel...
pixels.setPixelColor(i, pixels.Color(0, 150, 0));
pixels.show();
delay(DELAYVAL);
}
}

RP2040 ASSEMBLY

14.gif

After confirming that the LEDs were functioning, we moved onto the RP2040 assembly process, which begins by turning the board over, placing 2040 Tiny in its location, and then soldering all the pads with its castellated terminals with a soldering iron.

Power Source

20261005_202352.jpg
20261005_190057(0).jpg
InShot_20261008_193320830.gif

One of the things I wanted to improve from the previous version was the battery life. The earlier MatTris used a much smaller battery, and the runtime was honestly not great, especially when the LEDs were running continuously.

For MatTris Arcade, I went with two 2200mAh LiPo cells instead. This gives the console a much larger battery capacity and makes it more practical to actually use for a longer gaming session.

The two cells are connected to the power management circuit, which takes care of charging and supplying power to the rest of the system.

Designing the Enclosure

IMG-20261007-WA0024.jpg
IMG-20261007-WA0029.jpg
IMG-20261007-WA0030.jpg
IMG-20261007-WA0026.jpg
IMG-20261007-WA0031.jpg
IMG-20261007-WA0025.jpg
IMG-20261007-WA0028.jpg
IMG-20261007-WA0027.jpg

For this version, I decided to completely redesign the enclosure in Fusion 360. I wanted it to feel more like a small arcade console and less like a regular 3D printed electronics project.

I first modeled the PCB and the main components so I could design the case around the actual hardware. The enclosure is made in two main parts, a 3D printed body that holds the electronics and a separate front panel.

For the front panel, I used a tinted black transparent acrylic sheet. I laser cut it to the required shape and made the openings for all the buttons. The LED matrix sits directly behind this panel, so when the device is turned off, the front looks almost completely black. Once the LEDs turn on, the matrix shows through the tinted sheet, which gives it a really nice look.

The buttons are also mounted through the front panel, so everything stays in place without needing a complicated mounting system.

After a couple of small adjustments to get the panel, buttons and PCB to fit properly, I was happy with how the enclosure turned out. It ended up looking much closer to the little arcade-style device I had in mind when I started the redesign.

3D Printing

20261005_185651.jpg
20261005_185643.jpg
20261005_185803.jpg

Once the electronics were ready, it was time to make the enclosure.

I printed the main body using blue Hyper PLA on my Anycubic Kobra S1. I also printed the buttons in the same material so they matched the body.

The print came out pretty clean, and I didn't need to do much post-processing. After cleaning up the small bits from the print, I did a quick test fit with the PCB and buttons before moving on to the front panel. adahi

Laser Cutting the Front Panel

InShot_20261008_193125442.gif
InShot_20261008_193147656.gif
20261005_203738.jpg

For the front panel, I used a tinted black transparent acrylic sheet. I exported the design from Fusion 360 and cut the panel along with all the button openings on the laser cutter.

I used 70% power at around 15 mm/s, and it was enough to cut the sheet cleanly in a single pass.

After cutting, I removed the protective film and checked the panel on the enclosure. The tint is fairly light, so you can still see the LED matrix and some of the components behind it, but it gives the front a much cleaner finish than leaving the LEDs completely exposed. The buttons also sit directly through the panel, which keeps the front pretty simple.xz

Final Assembly

InShot_20261008_193649662.gif
InShot_20261008_194249318.gif
InShot_20261008_194612563.gif
InShot_20261008_194754583.gif
InShot_20261008_194916868.gif

Once all the parts were ready, I started putting everything into the enclosure.

The PCB is held in place with a few screws, so it sits properly inside the case. The battery was already connected to the power circuit, and I fixed the two cells underneath the PCB using double-sided tape.

For the front panel, I used a thin layer of double-sided tape around the edges and stuck the tinted acrylic sheet onto the enclosure. The buttons were already fitted into the panel, so after fixing the panel in place, the front was basically done.

After putting the enclosure together, there wasn't much left to do apart from turning it on and checking everything one last time.

Code Part

For the software, I kept the main sketch responsible for the menu, buttons, display and switching between games. The actual game logic is kept in separate .h files, which made it much easier to work on each game without making the main file too messy.

There are six games in this version: Tetris, Snake, Pong, Breakout, Car Dodge and Space Invaders. Each game has three difficulty levels.

The buttons use INPUT_PULLUP, so they are normally HIGH and go LOW when pressed. I also added a small debounce routine because without it, one press could sometimes get detected multiple times.

struct Button {
int pin;
bool raw = false;
bool down = false;
bool pressed = false;
uint32_t changed = 0;

Button(int p): pin(p) {}

void begin() {
pinMode(pin, INPUT_PULLUP);
}

void poll(uint32_t now) {

pressed = false;

bool v = digitalRead(pin) == LOW;

if (v != raw) {
raw = v;
changed = now;
}

if (v != down && uint32_t(now - changed) >= 25) {
down = v;
pressed = v;
}
}
};

Main Code and Menu

InShot_20261008_195101215.gif

The main menu is kept pretty simple. Left and Right move through the six games and A opens the difficulty selection. B can be used to resume a game that was left running.

I also added the A + B shortcut. Holding both buttons for three seconds opens the menu, which is useful when you're already inside a game.

const int MENU = 0;
const int MODES = 1;
const int PLAY = 2;
const int OVER = 3;

int screen = MENU;
int selected = 0;
int mode = 1;
int game = 0;
int difficulty = 1;

const char* labels[] = {
"TET",
"SNK",
"PON",
"BRK",
"CAR",
"INV"
};

const char* modes[] = {
"EZ",
"MED",
"HI"
};

The game itself is selected through a small dispatcher. This is what keeps the main loop from becoming a huge collection of if statements for every game.

void updateGame(uint32_t now) {

switch (game) {

case 0:
tetris(now);
break;

case 1:
snake(now);
break;

case 2:
pong(now);
break;

case 3:
breakout(now);
break;

case 4:
car(now);
break;

case 5:
invaders(now);
break;
}
}

Starting a game also resets its own variables depending on which game was selected.

void startGame(uint32_t now) {

game = selected;
difficulty = mode;
score = 0;
lives = 3;
won = false;

tick = moveAt = shotAt = now;

enter(PLAY);
hasGame = true;

if (game == 0) {
memset(board, 0, sizeof(board));
lines = 0;
spawn();
}

else if (game == 1) {
length = 4;
dx = nextDx = 1;
dy = nextDy = 0;
turnQueued = false;

for (int i = 0; i < 4; i++) {
sx[i] = 6 - i;
sy[i] = 10;
}

food();
}

else if (game == 2) {
paddle = enemy = 5;
bx = 7;
by = 10;
vx = vy = 1;
}

else if (game == 3) {
paddle = 5;
memset(bricks, 1, sizeof(bricks));
resetBall();
}

else if (game == 4) {
lane = 1;
roadStep = 0;

for (int i = 0; i < 6; i++)
carY[i] = -99;
}

else {
ship = 6;
wave = 0;
newWave(now);
}
}


Tetris

I Built Tetris on a 14&times;20 LED Matrix! | MatTris Arcade

Tetris is still the most involved game in the project. Each tetromino is stored as a small 4 × 4 bit pattern. Before moving or rotating a piece, fits() checks whether the new position would collide with the edge of the board or an already placed block.

const uint16_t shapes[7] = {
0x00F0,
0x0066,
0x0072,
0x0036,
0x0063,
0x0071,
0x0074
};

bool block(int p, int rot, int x, int y) {

for (int i = 0; i < rot; i++) {
int old = x;
x = y;
y = 3 - old;
}

return (
shapes[p] &
(uint16_t(1) << (y * 4 + x))
) != 0;
}

The controls are handled separately from the falling timer. This means you can move the piece while the automatic falling speed is controlled by the selected difficulty.

if (L.pressed && fits(px - 1, py, rotation))
px--;

if (R.pressed && fits(px + 1, py, rotation))
px++;

if (A.pressed || U.pressed) {

int r = (rotation + 1) % 4;

if (fits(px, py, r))
rotation = r;
}

if (B.pressed) {

while (fits(px, py + 1, rotation))
py++;

lockPiece();
}

When a piece is locked, the program checks each row for a complete line and removes it.

for (int y = 19; y >= 0; y--) {

bool full = true;

for (int x = 0; x < 14; x++)
if (!board[y][x])
full = false;

if (full) {

for (int j = y; j > 0; j--)
memcpy(board[j], board[j - 1], 14);

memset(board[0], 0, 14);

score += 100;
lines++;

y++;
}
}


Snake

I Built Snake on a 14&times;20 LED Matrix! | MatTris Arcade

for Snake, I used two arrays to store the X and Y coordinates of every part of the snake. Every movement shifts the body positions by one and then moves the head.

int sx[280];
int sy[280];

int length = 4;

int dx = 1;
int dy = 0;

int nextDx = 1;
int nextDy = 0;

I also added a small turn queue so the snake doesn't immediately turn back into itself if two direction buttons are pressed very quickly.

if (U.pressed && dy != 1) {
nextDx = 0;
nextDy = -1;
turnQueued = true;
}

else if (D.pressed && dy != -1) {
nextDx = 0;
nextDy = 1;
turnQueued = true;
}

else if (L.pressed && dx != 1) {
nextDx = -1;
nextDy = 0;
turnQueued = true;
}

else if (R.pressed && dx != -1) {
nextDx = 1;
nextDy = 0;
turnQueued = true;
}

When the snake eats the food, its length increases and another free position is picked for the next food.

if (eat) {

length++;
score += 10;

food();
}

for (int i = length - 1; i > 0; i--) {
sx[i] = sx[i - 1];
sy[i] = sy[i - 1];
}

sx[0] = x;
sy[0] = y;

Pong

I Built Pong on a 14&times;20 LED Matrix! | MatTris Arcade

Pong and Breakout share some of their basic ball and paddle logic, so I moved the common parts into BallGamesCommon.h.

For Pong, the bottom paddle is controlled by Left and Right while the top paddle is moved by a simple AI.

if (L.pressed && paddle > 0)
paddle--;

if (R.pressed && paddle < 10)
paddle++;

if (bx > enemy + 1 && enemy < 10)
enemy++;

else if (bx < enemy + 1 && enemy > 0)
enemy--;

The ball position is updated using its X and Y velocity. When it reaches the sides, the X direction is reversed.

int nx = bx + vx;
int ny = by + vy;

if (nx < 0 || nx > 13) {
vx = -vx;
nx = bx + vx;
}

A paddle hit reverses the Y direction and increases the score.


Breakout

I Built Breakout on a 14&times;20 LED Matrix! | MatTris Arcade

breakout uses the same paddle and ball system, but instead of an AI paddle, the top part of the display contains 28 bricks.

Each brick is just stored as either present or cleared, which keeps the logic pretty small.

uint8_t bricks[4][7];

...

if (
ny >= 2 &&
ny <= 5 &&
bricks[ny - 2][nx / 2]
) {

bricks[ny - 2][nx / 2] = 0;

score += 10;

vy = -vy;
ny = by + vy;
}

After every hit, the program checks whether any bricks are still left. If there aren't any, the game ends with a win.

Road Fighter

I Built Road Fighter on a 14&times;20 LED Matrix! | MatTris Arcade

Car Dodge uses three lanes. The player's car stays near the bottom while the other cars are spawned above and move downwards.

if (L.pressed && lane > 0)
lane--;

if (R.pressed && lane < 2)
lane++;

I kept six obstacle slots in the game. Whenever one becomes free, another car can be spawned at the top.

for (int i = 0; i < 6; i++) {

if (carY[i] != -99) {

carY[i]++;

if (
carLane[i] == lane &&
carY[i] + 2 >= 16 &&
carY[i] <= 18
) {
finish(false);
return;
}

if (carY[i] > 19) {
carY[i] = -99;
score++;
}
}
}

The difficulty mainly changes how quickly the traffic moves.

Space Invaders

I Built Space Invaders on a 14&times;20 LED Matrix! | MatTris Arcade

For Space Invaders, I had to simplify the original idea quite a bit to make it work on a 14 × 20 matrix. The aliens are arranged in a 3 × 3 formation and move from side to side.

bool aliens[3][3];

int alienX = 1;
int alienY = 2;
int alienDir = 1;

int ship = 6;

int bulletX = 0;
int bulletY = -1;

int enemyX = 0;
int enemyY = -1;

The player moves the ship horizontally and can have one active bullet at a time.

if (L.pressed && ship > 1)
ship--;

if (R.pressed && ship < 12)
ship++;

if (
A.down &&
bulletY < 0 &&
uint32_t(now - shotAt) >= 220
) {

bulletX = ship;
bulletY = 17;

shotAt = now;
}

The aliens are moved whenever their timer expires. If they reach the edge, the whole formation changes direction and moves down.

if (
alienX + alienDir < 0 ||
alienX + alienDir + 10 > 13
) {

alienDir = -alienDir;
alienY++;
}

else {
alienX += alienDir;
}

When a bullet overlaps an alien, that alien is removed and the score goes up. Once all nine are gone, a new wave starts.

if (
bulletX >= xx &&
bulletX < xx + 3 &&
bulletY >= yy &&
bulletY < yy + 2
) {

aliens[y][x] = false;
bulletY = -1;

score += 10;

return;
}

That gives the reader enough of the actual implementation to understand how you built each game, while the complete .ino and .h files remain available as the project source.

Conclusion

IMG-20261005-WA0024.jpg

This version of MatTris was mostly about taking the original idea and pushing it a little further. I wanted a better enclosure, more games and a build that felt more like an actual little arcade console rather than just a Tetris machine.

I'm pretty happy with how this version turned out, especially with the tinted front panel and the w ay the different games work on the same small matrix.

There are still a few things I would change in the next version. Right now I'm using WS2812B 2020 LEDs, which are already pretty small, but I'd like to go even smaller next time. I'm thinking of using 1 × 1 mm addressable LEDs so I can fit a lot more pixels into roughly the same space. That would give me a much higher pixel density and hopefully make it possible to create more detailed graphics and games.

For now though, I'm calling this version finished.