Skip to the content
PCBKitNew board
2 layers132 × 88 mmTetrominiKicad, the printed circuit board in 3D

TetrominiKicad

roserha/tetromini

Handheld FreeRTOS-Based Console on Feather STM32F405!

Size
132 × 88 mm
Copper layers
2
Thickness
1.6 mm
Parts
45
Nets
37
Pads
135
Tracks, vias
429, 78
Pours
1
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
15 Sep 2026
Updated
7 Nov 2026

Overview

TetrominiKicad is an open-source printed circuit board by roserha, from the roserha/tetromini repository on GitHub. It is a 2-layer board of 132 × 88 mm with 45 parts on 37 nets, routed with 429 tracks and 78 vias.

It is built around the MJTP1234. Its parts include 16 resistors, 9 capacitors, 1 IC and 1 switch.

Main components

1

README

README.md

Tetromini - STM Edition (In active development)

Tetromini Header

This is a refactor of the old Nordic/Zephyr version of the project for the STM32F405/freeRTOS pipeline that is archived in dev!

About

Tetromini is a portable console based on an Adafruit Feather STM32F405 Express running a custom firmware inside freeRTOS that plays a fan-made version of a falling-block puzzle game!

Tetromini WEBP

The project consists of multiple parts. As of writing, the firmware that controls the console itself and the game inside the firmware have a working version!

Future developments may want to explore things such as:

  • Game over screens
  • High score tracking (?)
  • A custom enclosure shaped like a T-Block Tetromino

Features

  • Sprite-based rendering
  • 4 directional inputs (move left, move right, soft drop, hard drop)
  • Clockwise and counterclockwise rotation
  • Dynamic gravity and boosted score based on level
  • Combo points per consecutive line clears
  • Pseudorandomly chosen pieces
  • Dropped pieces and 4 different line clear sound effects
  • Multi-channel soundtrack played via synthesized notes with custom notes system
  • Multithreaded parallel execution

Schematics, PCB and BOM

The below KiCad schematic is based on Rev 1, running on a breadboard, and set to run on a PCB under Rev 1a. (last updated Oct 28th 2026)

KiCAD Schematic KiCAD PCB KiCAD PCB

Bill Of Materials for Rev 1a (last updated Oct 28th 2026):

ReferenceDescriptionMPNDigiKey PNManufacturer
U1Feather STM32F405 Express43821528-4382-NDAdafruit
DS1Generic 2.42in SSD1309 OLED SPI Display----HiLetgo
DS2I2S Audio Breakout148091568-14809-NDSparkfun
DOWN1,LEFT1,RIGHT1,ROT_LEFT1,ROT_RIGHT1,UP1,START_BTN,POWER_BTNPush button switch, generic, two pinsMJTP1234679-2442-NDAPEM Inc.
C1,C2,C3,C4,C5,C6,C7,C8100nF CapacitorC320C104K5R5TA399-4264-NDKEMET
C9100UF Capacitor16ZLH100MEFC5X111189-1148-NDRubycon
R1,R2,R5,R6,R9,R10,R13,R1410kOhm ResistorROX05SJ10KA142728CT-NDTE Connectivity Passive Product
R3,R4,R7,R8,R11,R12,R15,R16100kOhm ResistorWK202070A1003J2200749-WK202070A1003J2200CT-NDVishay
SW1SPDT SwitchSS12SDP2360-2133-NDNKK Switches
--8ohm 700MW SpeakerCMS-181325-078L100B-672223-CMS-181325-078L100B-67-NDSame Sky
--Lithium Battery2581528-1838-NDAdafruit
--SMD SWD Header for Feather40481528-2827-NDAdafruit
--Headers for Feather28861528-1560-NDAdafruit
--Headers for OLED and I2S BreakoutPPTC071LFBN-RCS7005-NDSullins Connector Solutions

Firmware and Multithreading

The project's firmware is based on FreeRTOS, and is set up by multiple concurrent threads that run in parallel. Their execution and basic behaviours can be seen below:

mermaid
---
config:
  look: neo
  theme: redux-color
---
sequenceDiagram
    participant App as Main Application
    participant Game as Game Thread
    participant Render as Render Thread
    participant Battery as Battery Level Thread
    participant ButtonISR as Button Interrupts
    participant Audio as Audio Thread
    participant Melody as Melody Thread
    participant Video as Video Thread
    participant Decoder as Decoder Thread
    participant MCU as STM32F405

    MCU->>App: Spawn Main Application inside FreeRTOS

    App->>Render: Spawn thread
    App->>Battery: Spawn thread
    App->>Audio: Spawn thread
    App->>Melody: Spawn thread
    App->>Game: Spawn thread

Controllers

Most of the threads' behaviour (specifically the Melody, Audio and Game Threads) are abstracted away inside of Controller Classes. This is the overview of what controllers exist inside those threads, and how they interact with each other. It isn't an expressly formal Design Structure, but the closest I can think is that it's akin to singleton decorators, in a way? Controllers can "engulf" others to add their behaviours to their own, and we only should have one controller instance existing at a time.

mermaid
---
config:
  look: neo
  theme: redux-color
---
classDiagram
  class GraphicsController {
    +kScreenWidth : uint_fast8_t 
    +kScreenHeight : uint_fast8_t 
    +kFrameBufferSize : uint16_t 
    -_frame_buffer : std::array<uint8_t, kFrameBufferSize>* 
    -_xFrameBufferMutex : SemaphoreHandle_t* 
    +setPixel(const uint_fast8_t x, const uint_fast8_t y, const bool on) : void 
    +unfilledRect(const uint_fast8_t x, const uint_fast8_t y, const uint_fast8_t w, const uint_fast8_t h, const bool on) : void 
    +filledRect(const uint_fast8_t x, const uint_fast8_t y, const uint_fast8_t w, const uint_fast8_t h, const bool on) : void 
  }

  class SpritesController {
    -_sprite_array : std::array<uint8_t, 749> 
    +drawSprite(const uint_fast8_t sprite_id, const uint_fast8_t x, const uint_fast8_t y) : uint8_t 
    +drawBlock(const uint_fast8_t x, const uint_fast8_t y, const Tetromino_t block_type) : bool 
    +drawText(const uint_fast8_t x, const uint_fast8_t y, const char *sentence) : uint_fast8_t 
    +drawNumber(const uint_fast8_t x, const uint_fast8_t y, const int32_t number, const uint_fast8_t base) : uint_fast8_t 
  }

Sprite Rendering

Given we only have 1MB of flash and 128KB of RAM, the 6x6 px sprites need to take up as little space as possible. Since the OLED display is monochrome, they are stored in binary pixel format.

Given a source bitmap tilemap of all sprites, for each one, a count of their collumns (and therefore their horizontal resolution) is stored, and then each collumn is stored in a byte, with two leftover bits, for a total of 7 bytes per sprite. The actual arrays are programatically calculated and written down via a Python script run at build time.

When rendering sprites, a caret system is used to allow for interchanging and nested text and number rendering by leveraging the column size parameter.

Each sprite, when rendered, is stored in a framebuffer that gets sent via SPI + DMA to the OLED display through a custom SSD1309 driver. Each pixel drawing and render command protects the buffer via a Mutex.

Melody Playback

Given that a singular second of stored audio would be 96KB given an audio playback of 48kHz and 16-bit amplitude resolution, it is impractical (to put it lightly) to store full scores. As such, given the programmed behaviour of the Audio Task, songs are stored as several "play note" and "sleep" commands calculated from a given MIDI file.

Commands are arranged as follows: a 32-bit word arranged with masks as follows: 0x1F for note (N), 0xE0 for octave (C), 0xFFF00 for length (L), 0x7F00000 for volume (V), 0x78000000 for waveId (W) for a final word 0b0WWW WVVV VVVV LLLL LLLL LLLL CCCN NNNN. The most significant bit is restricted to indicate a wait time on stored songs, followed by a 31-bit integer indicating how many milliseconds to sleep before the next command.

This made it such that, for example, the main_song.mid file, whose raw uncompressed wave would occupy 3.686MB for a 38.4s playback, and MIDI file occupies 2.4KB, takes up around 1.932KB in flash! It does depend on the arrangement, however, because a 20.5KB piano arrangement MIDI takes up ~25KB. It still beats its theoretical uncompressed size of ~20MB!

Audio Synthesization

To generate the sound waves for the different instruments, the raw amplitudes of a single wave are stored as 600 16-bit integers spread equidistant across the single wavelength, and a "jump" floating point number is calculated based on the wave's desired pitch and the 48kHz playback. This means each instrument takes up around 1.2KB. Upon playback, we use that number to "jump" across amplitudes during playback to simulate pitch shifting. Each wave has an attack and decay envelope to avoid crackling on consecutive playbacks. The audio system can support up to kChannelNumber different waves to be played at the same time. A 256-integer-long buffer is then sent via I2S + Circular DMA to the I2S Breakout, and each half is filled based on ISR routines sent by the DMA coroutine.

Video Playback

For the Main Menu screen, a 3D animation plays. This would be computationally intensive to generate on the fly, and as such, a video encoding system was devised via a double circular buffer. The video task reads from two buffers of 36 frames each, and whenever one's reading is finished, it alerts the decoding task that that buffer is ready for new frames, and goes onto the next, so on ad infinitum (or as infinitum as a game console can be). Because GameTask initially was the only one that rendered information to the screen, VideoTask and GameTask now take turns having ownership of a DisplayControl mutex, so one task doesn't render over another.

Video Encoding/Decoding

Given the size of video files and our RAM, it would be impractical to store the entire video in the heap (let alone any task's stack). As such, we store two buffers of decoded output and processing buffers inside the decoder itself. Video frames are encoded to be in the same bit resolution and pixel order as the framebuffer, and groups of 36 (or less for the leftovers) are stored together and DEFLATEd together using ZLIB. For decoding, I utilize zlib_turbo for faster performance, with some modifications for it to fit my firmware's intended behaviour. 72 frames of animation get stored in 6.5KB in flash, compared to the overall 73.74KB that both blocks (comprised of 72 frames) take up.

With Video and Audio available, I hid a little fun easter egg inside of the project! You can read the source code to figure out what music video gets played, but I'm happy to say that its ~6500 frames only take up 479KB of space in flash!

"Random" Piece Generation

Since RTOSs are to be deterministic machines, true randomness isn't recommended. However, it is wished that games don't look the same across gameplays. As such, a custom pseudorandom number generation system was created in addition to a traditional "bag system" wherein each of the 7 pieces are pulled from a metaphorical "bag" before being reshuffled when all of them are pulled. This bag system ensures that no same piece is ever pulled more than twice in a row.

As soon as gameplay begins, a salt is initialized and incremented by 26489 every frame, and set to wrap at 2147457158. To further randomize initial conditions, a random number of pieces are pulled out and discarded, and two pieces are chosen at random. Whenever it is time to choose a piece at (seemingly) random, this salt is added to 3739 times the current ticks since boot for an internal random variable, and then the salt is incremented by 14929. By this nature, under extremely precise and calculated circumstances, gameplay COULD be fully deterministic, but under normal gameplay, it is seemingly random.

Debugging

Debugging is achieved live on the board using a Segger J-Link EDU! This project is purely for learning's sake, so yay!

Disclaimer

Tetris® is a registered trademark of The Tetris Company, LLC. This project is not affiliated with or endorsed by The Tetris Company.

Parts

45
QtyValueFootprintReferences
8100KΩResistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_HorizontalR3, R4, R7, R8, R11, R12, R15, R16
8100nFCapacitor_THT:C_Disc_D5.0mm_W2.5mm_P5.00mmC1, C2, C3, C4, C5, C6, C7, C8
810KΩResistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_HorizontalR1, R2, R5, R6, R9, R10, R13, R14
4MountingHole_2.2mm_M2MountingHole:MountingHole_2.2mm_M2H5, H6, H7, H8
4MountingHole_3.2mm_M3MountingHole:MountingHole_3.2mm_M3H1, H2, H3, H4
2~Connector_PinSocket_2.54mm:PinSocket_1x07_P2.54mm_VerticalDS1, DS2
1100uFCapacitor_THT:CP_Radial_D5.0mm_P2.00mmC9
1DOWN_BTNButton_Switch_THT:SW_PUSH_6mmDOWN1
1LEFT_BTNButton_Switch_THT:SW_PUSH_6mmLEFT1
1POWER_BTNButton_Switch_THT:SW_PUSH_6mmPOWER1
1POWER_PUSH_SPDTConnector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_VerticalSW1
1RIGHT_BTNButton_Switch_THT:SW_PUSH_6mmRIGHT1
1ROT_CCW_BTNButton_Switch_THT:SW_PUSH_6mmROT_CCW1
1ROT_CW_BTNButton_Switch_THT:SW_PUSH_6mmROT_CW1
1START_BTNButton_Switch_THT:SW_PUSH_6mmSTART1
1UP_BTNButton_Switch_THT:SW_PUSH_6mmUP1
1~Module:Adafruit_FeatherU1

Questions

How big is the TetrominiKicad PCB?

It measures 132 × 88 mm and has 2 copper layers, 1.6 mm thick.

What parts does the TetrominiKicad use?

45 parts, built around the MJTP1234. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the TetrominiKicad KiCad files?

From the roserha/tetromini repository on GitHub, at the commit this page was made from: the board is Resources/TetrominiKicad/TetrominiKicad.kicad_pcb and its schematic Resources/TetrominiKicad/TetrominiKicad.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the TetrominiKicad design in my own project?

Its repository states no licence, so its author keeps all rights: ask roserha before reusing the design.

Similar boards

  • 2 layers30 × 61.3 mmModuleV440, the printed circuit board in 3D

    ModuleV440

    stuartpittaway/diyBMSv4

    Version 4 of the diyBMS

    45 parts1.1k
  • 2 layers97 × 78.5 mmDumbpad, the printed circuit board in 3D

    Dumbpad

    imchipwood/dumbpad

    Simple 4x4 numpad with rotary encoder. Powered by QMK via ATmega32u4 Pro Micro

    45 parts517GPL-2.0
  • 2 layers49.8 × 88.9 mmLDG WING FOLD PLACARD, the printed circuit board in 3D

    LDG WING FOLD PLACARD

    jrsteensen/OpenHornet

    OpenHornet 1:1 F/A-18C Simulator Repository

    45 parts431
  • 2 layers26.5 × 34.4 mmPicoLoaderNSB, the printed circuit board in 3D

    PicoLoaderNSB

    makeo/PicoLoader

    A cheap and easy to install modchip for the Nintendo GameCube!

    45 parts320GPL-2.0
  • 2 layers40.2 × 19 mmOMGS3 Carrier R1, the printed circuit board in 3D

    OMGS3 Carrier R1

    UnexpectedMaker/esp32s3

    Assorted files for my ESP32-S3 development boards

    45 parts296
  • 2 layers63.5 × 63.5 mmMain pcb, the printed circuit board in 3D

    Main pcb

    peterkrull/quad

    Firmware, hardware and documentation for my autonomous quad copter project

    45 parts210GPL-3.0
  • 2 layers99.1 × 81.9 mmRC6502 Terminal, the printed circuit board in 3D

    RC6502 Terminal

    tebl/RC6502-Apple-1-Replica

    An expandable SBC-version of the Apple 1 computer, easy to get started with and lot of cool stuff that can be added to it!

    45 parts193GPL-3.0
  • 2 layers116.1 × 94.7 mmMiniDox, the printed circuit board in 3D

    MiniDox

    That-Canadian/MiniDox_PCB

    MiniDox Keyboard, PCB files

    45 parts169WTFPL