

TetrominiKicad
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
1README
README.mdTetromini - STM Edition (In active development)

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!

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)

Bill Of Materials for Rev 1a (last updated Oct 28th 2026):
| Reference | Description | MPN | DigiKey PN | Manufacturer |
|---|---|---|---|---|
| U1 | Feather STM32F405 Express | 4382 | 1528-4382-ND | Adafruit |
| DS1 | Generic 2.42in SSD1309 OLED SPI Display | -- | -- | HiLetgo |
| DS2 | I2S Audio Breakout | 14809 | 1568-14809-ND | Sparkfun |
| DOWN1,LEFT1,RIGHT1,ROT_LEFT1,ROT_RIGHT1,UP1,START_BTN,POWER_BTN | Push button switch, generic, two pins | MJTP1234 | 679-2442-ND | APEM Inc. |
| C1,C2,C3,C4,C5,C6,C7,C8 | 100nF Capacitor | C320C104K5R5TA | 399-4264-ND | KEMET |
| C9 | 100UF Capacitor | 16ZLH100MEFC5X11 | 1189-1148-ND | Rubycon |
| R1,R2,R5,R6,R9,R10,R13,R14 | 10kOhm Resistor | ROX05SJ10K | A142728CT-ND | TE Connectivity Passive Product |
| R3,R4,R7,R8,R11,R12,R15,R16 | 100kOhm Resistor | WK202070A1003J2200 | 749-WK202070A1003J2200CT-ND | Vishay |
| SW1 | SPDT Switch | SS12SDP2 | 360-2133-ND | NKK Switches |
| -- | 8ohm 700MW Speaker | CMS-181325-078L100B-67 | 2223-CMS-181325-078L100B-67-ND | Same Sky |
| -- | Lithium Battery | 258 | 1528-1838-ND | Adafruit |
| -- | SMD SWD Header for Feather | 4048 | 1528-2827-ND | Adafruit |
| -- | Headers for Feather | 2886 | 1528-1560-ND | Adafruit |
| -- | Headers for OLED and I2S Breakout | PPTC071LFBN-RC | S7005-ND | Sullins 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:
---
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 threadControllers
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.
---
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| Qty | Value | Footprint | References |
|---|---|---|---|
| 8 | 100KΩ | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal | R3, R4, R7, R8, R11, R12, R15, R16 |
| 8 | 100nF | Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P5.00mm | C1, C2, C3, C4, C5, C6, C7, C8 |
| 8 | 10KΩ | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal | R1, R2, R5, R6, R9, R10, R13, R14 |
| 4 | MountingHole_2.2mm_M2 | MountingHole:MountingHole_2.2mm_M2 | H5, H6, H7, H8 |
| 4 | MountingHole_3.2mm_M3 | MountingHole:MountingHole_3.2mm_M3 | H1, H2, H3, H4 |
| 2 | ~ | Connector_PinSocket_2.54mm:PinSocket_1x07_P2.54mm_Vertical | DS1, DS2 |
| 1 | 100uF | Capacitor_THT:CP_Radial_D5.0mm_P2.00mm | C9 |
| 1 | DOWN_BTN | Button_Switch_THT:SW_PUSH_6mm | DOWN1 |
| 1 | LEFT_BTN | Button_Switch_THT:SW_PUSH_6mm | LEFT1 |
| 1 | POWER_BTN | Button_Switch_THT:SW_PUSH_6mm | POWER1 |
| 1 | POWER_PUSH_SPDT | Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical | SW1 |
| 1 | RIGHT_BTN | Button_Switch_THT:SW_PUSH_6mm | RIGHT1 |
| 1 | ROT_CCW_BTN | Button_Switch_THT:SW_PUSH_6mm | ROT_CCW1 |
| 1 | ROT_CW_BTN | Button_Switch_THT:SW_PUSH_6mm | ROT_CW1 |
| 1 | START_BTN | Button_Switch_THT:SW_PUSH_6mm | START1 |
| 1 | UP_BTN | Button_Switch_THT:SW_PUSH_6mm | UP1 |
| 1 | ~ | Module:Adafruit_Feather | U1 |
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 mm

- 2 layers97 × 78.5 mm

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 mm

- 2 layers26.5 × 34.4 mm

PicoLoaderNSB
makeo/PicoLoader
A cheap and easy to install modchip for the Nintendo GameCube!
45 parts320GPL-2.0 - 2 layers40.2 × 19 mm

- 2 layers63.5 × 63.5 mm

Main pcb
peterkrull/quad
Firmware, hardware and documentation for my autonomous quad copter project
45 parts210GPL-3.0 - 2 layers99.1 × 81.9 mm

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 mm
