Non-cringify
All checks were successful
Deploy to Pages / build (pull_request) Successful in 1m48s

This commit is contained in:
Andras Schmelczer 2026-05-25 21:31:09 +01:00
parent 0be50b6c24
commit 2c37e7fa62
39 changed files with 410 additions and 397 deletions

View file

@ -1,6 +1,6 @@
---
title: A 50 FPS Game Engine on an ATtiny85
description: Building a tiny embedded game engine around an ATtiny85V, OLED display, IR input, EEPROM persistence, and a custom PCB.
title: A 50 FPS Game Engine on an 8-Bit Microcontroller
description: A handheld game built from the PCB up — ATtiny85V, OLED, IR receiver, EEPROM, 8 MHz 8-bit ALU. 50 FPS floor.
date: 2026-05-06
projectPeriod: 'Spring 2020'
thumbnail:
@ -8,9 +8,9 @@ thumbnail:
alt: The Ad Astra game running on a small OLED display.
tags: ['embedded', 'games', 'systems']
role: Hardware and firmware author
stack: ['C', 'ATtiny85V', 'OLED', 'EEPROM', 'PCB design']
scale: 8-bit microcontroller, 8 MHz clock, 15-20 ms maximum frame times during gameplay
outcome: A working low-power handheld game engine and game built from the circuit board up
stack: ['C', 'ATtiny85V', 'SPI OLED', 'IR receiver', 'EEPROM', 'KiCad']
scale: 8 MHz, 8-bit ALU, ~31 mW at full brightness, ~1.5 mA standby, 1520 ms frame budget
outcome: A handheld built from schematic to firmware, with a 50 FPS game on it
audience: technical
links:
- label: Source
@ -22,42 +22,31 @@ media:
mp4: /media/video/ad_astra.mp4
captions: /media/video/ad_astra.vtt
alt: Video demonstration of the embedded game running on a small OLED display.
caption: The game engine ran on an ATtiny85V with an OLED display and IR input.
transcript: No spoken dialogue. The demonstration shows the Ad Astra handheld board running its OLED game, with the player moving through the small display while the IR input controls gameplay.
caption: The whole thing — board, firmware, sprites, game loop — runs on a single ATtiny85V at 8 MHz.
transcript: No spoken dialogue. The handheld board runs its OLED game; the player moves through the small display while the IR input controls gameplay.
---
Ad Astra came from wanting to combine graphics and microcontrollers without hiding behind a large development board. The result was a small embedded game engine and game built around an ATtiny85V, an OLED display, IR input, EEPROM persistence, and a custom PCB.
**The short version:**
The fun part was that every layer mattered. The circuit, display driver, memory layout, object model, sprite tooling, and game loop all had to fit inside a tiny system.
- A handheld game built from the PCB up: ATtiny85V, 0.96" SPI OLED, TSOP4838 IR receiver, 3.3V regulator, battery.
- 8-bit ALU at 8 MHz. Every byte and every cycle has a price tag, and that's the whole point.
- 1520 ms peak frame time, so gameplay never drops below 50 FPS. Power draw ~31 mW peak, ~1.5 mA standby.
## The Problem
## Why the PCB changed the project
The hardware setup was intentionally constrained: an ATtiny85V, a D096-12864-SPI7 OLED display, a TSOP4838 IR receiver, and a 3.3V regulator. The system was low power, with peak consumption around 31 mW at full brightness and a standby mode around 1.5 mA.
I'd done microcontroller work on dev boards before and it always felt like I was renting the hardware. As soon as I had a real board with my soldering on it, bugs stopped feeling like software inconveniences and started feeling like consequences of choices I'd made in KiCad. That shift was most of the value of doing it this way.
Those numbers made the project feel physical. Performance was not an abstract target. Every frame and every byte had a cost.
The constraint that mattered: an 8-bit ALU at 8 MHz, with no FPU, no SIMD instruction set, and 8 KB of flash. Anything I built had to fit inside that, or I'd be staring at a brick.
## Constraints
## The bits worth showing
The engine ran at 8 MHz on an 8-bit ALU. That meant the display driver and game loop had to avoid expensive generality.
- **SIMD-on-an-8-bit-ALU display driver.** The OLED is 128×64 monochrome, 1024 bytes per frame. The driver packs four pixels into a byte and processes them with bit-parallel tricks. That's how the frame budget stayed under 20 ms with room for game logic.
- **Prototype-based inheritance, in C.** Entities share behaviour by pointing at a struct of function pointers. No vtable, no class, no allocator. Cheap dispatch and the whole object model fits on one screen.
- **Atomic EEPROM commits.** Sprite data and save state both live in EEPROM. The commit path writes a new region, then swaps a tiny header pointer. Pull the battery mid-write and the previous version is intact.
- **PNG-to-C sprite pipeline.** A Python script turns PNG artwork into static C arrays the firmware can include directly. Asset workflow without ever leaving the source tree.
Even the programming model needed restraint. I wrote the firmware in C, but used a balance of structured and object-oriented ideas to keep game object behaviour manageable without paying for a runtime that did not exist.
## What I'd change
## Design
The display driver was the most performance-sensitive layer. I used SIMD-like techniques on the 8-bit ALU to process four pixels at once. That helped keep maximum frame times between 15 and 20 milliseconds during gameplay, so the lowest gameplay frame rate stayed above 50 FPS.
For game objects, I used prototype-based inheritance. It was a pragmatic way to reuse behaviour while keeping the implementation simple enough for the target.
Persistent state used the built-in EEPROM with an atomic commit approach. Sprite data also lived in EEPROM, and I wrote scripts to convert PNG sprites into C array definitions so assets could move into firmware cleanly.
## What Worked
The project worked because the abstraction level stayed close to the hardware. The engine had reusable pieces, but none of them pretended the platform was larger than it was.
The custom PCB also changed the project. Once the system had a real board, bugs felt less like software inconveniences and more like design consequences. That made the final result much more satisfying.
## What I Would Change
Today I would write a more explicit development log around the display driver and persistence layer. Those are the parts that still feel technically interesting, and they deserve diagrams and measurements.
I would also add a small emulator or host-side harness. Debugging firmware directly on constrained hardware is useful, but a fast feedback loop would have made the engine easier to evolve.
- **A host-side emulator.** Debugging firmware directly on hardware was character-building and slow. A small SDL-based simulator linking the same C code would have shortened the iteration loop from "reflash and hope" to "rebuild and run."
- **Power numbers I'd actually trust.** I have peak and standby draw. I don't have a curve over a real gameplay session, so I honestly can't say how long the battery lasts under load. I can only say it outlasted my patience.
- **A development log for the driver.** The display driver and the EEPROM commit protocol are the parts I'd still defend. They deserved diagrams and measurements at the time, not the half page of comments I left them with.