Background
This project is my attempt to build a classic arcade-style fighting game on a custom handheld console powered by an STM32H7A3 microcontroller.
The aim is not to reproduce Street Fighter exactly. I am using it as a reference because a fighting game brings together many interesting engineering problems: responsive controls, sprite animation, collision detection, audio, real-time timing, memory management and fast graphics.
Everything runs directly on the microcontroller. There is no Linux, dedicated GPU or conventional game engine underneath it.
The Hardware
The console is built around an STM32H7A3 with an Arm Cortex-M7 processor running at 280 MHz.
The display has a resolution of 160 × 128 pixels and is used in landscape orientation. It uses the RGB565 colour format, which stores each pixel in 16 bits. The display is connected to the STM32 through an 8-bit parallel interface.
The screen resolution is small by modern standards, but it suits this type of game surprisingly well. It also keeps the amount of data that must be rendered and transferred for each frame at a manageable level.
Player controllers are connected over CAN bus. This gives me a reliable way to connect external hand controllers and makes it possible to support more than one player without using many additional pins on the main console.
The main hardware features are:
- STM32H7A3 running at 280 MHz
- 160 × 128-pixel LCD in landscape mode
- RGB565 graphics
- 8-bit parallel display interface
- External flash memory for graphics, audio and other assets
- CAN bus for external hand controllers
- I²S digital audio output
- USB mass-storage support
- FreeRTOS
Game Engine
I wrote the game engine specifically for this console and its hardware limitations. It handles player input, fighter movement, animation, attacks, collision detection, camera movement, graphics and sound.
The game uses a simple screen coordinate system. X increases towards the right and Y increases downwards.
The main game logic runs at a fixed interval of 20 milliseconds, which gives 50 game updates per second. Controller input is sampled every 5 milliseconds so that quick button presses and direction changes are not missed.
Using fixed timing makes movement and attacks predictable. A punch should have the same start-up and recovery time regardless of how long it took to draw the previous frame.
Input, game logic, graphics, CAN communication and audio are handled separately. This prevents a slow graphics update from delaying controller input or interrupting the sound.
Fighter State Machine
Each fighter is controlled by a state machine. The current state describes what the fighter is doing and which actions are allowed next.
Typical states include:
- Idle
- Walking
- Crouching
- Jumping
- Attacking
- Blocking
- Performing a special move
- Throwing
- Taking damage
- Knockdown
- Recovery
A jump is not treated as a single action. It is divided into a jump start, time in the air and a landing phase. Attacks are divided in a similar way.
A typical attack contains three parts:
- Start-up, before the attack can hit.
- Active frames, where the hitbox can make contact with the opponent.
- Recovery, where the fighter finishes the movement before another action can begin.
This is an important part of how a fighting game feels. Even a difference of one or two frames can make an attack feel fast, slow, powerful or unsafe.
Sprite Animation
The fighters are animated using sprite sheets containing frames for standing, walking, jumping, punching, kicking, blocking, taking damage and performing special moves.
The source artwork is not arranged on a regular grid. Frames have different sizes, inconsistent spacing and different positions relative to the ground. This makes automatic frame detection difficult and means that each frame needs some manual preparation.
Every frame has a pivot point that determines how it should be positioned relative to the fighter’s location in the game world. For example, placing the pivot at the feet prevents the character from appearing to jump up and down when the dimensions of the sprite change between frames.
An animation is stored as a sequence of frames, with an individual duration for each frame. Frames can also contain gameplay information such as:
- Pivot position
- Hurtboxes
- Attack hitboxes
- Pushboxes
- Movement during the frame
- Damage
- Hitstun
- Knockback
- Damage scaling
- Projectile spawn position
Keeping this information together makes it easier to synchronize the visual animation with what is happening in the game.
Movement and Camera
The fighting stage is wider than the physical display, so the engine uses a virtual camera to decide which part of the stage should be visible.
The camera follows the midpoint between the two fighters. It is clamped at the edges of the stage so that it never shows an area outside the background.
Fighter positions are stored in world coordinates rather than screen coordinates. Before drawing a fighter, the camera position is subtracted to calculate where the sprite should appear on the display.
The engine also keeps the fighters inside the playable area and uses pushboxes to prevent them from walking through each other.
Hitboxes and Collision Detection
Combat uses rectangular collision boxes. They are simple to calculate and are a good fit for a microcontroller.
Each fighter has one or more hurtboxes covering the parts of the body that can be hit. During the active part of an attack, a hitbox is placed around the fist, foot or weapon.
If an attack hitbox overlaps the opponent’s hurtbox, the engine registers a hit. It can then:
- Apply damage
- Place the opponent in hitstun
- Push the opponent backwards
- Start a hit animation
- Play a sound effect
- Trigger a knockdown
- Add screen shake or another visual effect
Pushboxes are handled separately. Their job is to stop the fighters from occupying the same physical space.
The boxes can change from one animation frame to another. A crouching fighter, for example, has a different hurtbox from a standing fighter.
Player Controls
The external hand controllers send their state to the console over CAN bus at regular intervals.
The engine does not only look at the current buttons. It also keeps a short history of recent directional and button inputs. This is needed to recognise special-move sequences.
A projectile move might use a sequence such as:
- Down
- Down and forward
- Forward
- Punch
The input does not have to be perfect. The engine accepts a limited amount of timing variation and can ignore neutral samples between parts of the sequence. Without this tolerance, special moves would be unnecessarily difficult to perform.
The same input system can also distinguish between short and long button presses or between different combinations of attack buttons.
Graphics and Framebuffer Handling
The console uses two framebuffers. While one framebuffer is being shown on the display, the game draws the next frame into the other one.
When the new frame is ready, the buffers are swapped. The display’s synchronization signal is used to choose a suitable time for the transfer, reducing visible tearing between frames.
The STM32’s DMA and MDMA peripherals help move and convert pixel data. These peripherals can work in parallel with the processor, leaving more CPU time for the game logic, animations and audio.
The complete screen contains 20,480 RGB565 pixels, or 40,960 bytes. Even at this relatively low resolution, moving a full frame efficiently is important when it must happen many times per second.
Audio
Audio is sent over I²S to a digital amplifier.
Music and sound effects are mixed in software before being transferred to the audio hardware using DMA. The DMA uses a double buffer, so one part of the buffer can be played while the next part is being prepared.
Audio runs independently from the graphics system. A complicated frame should therefore not cause the music to pause or a sound effect to break up.
The audio system is intended to support background music, punches, kicks, menu sounds and short voice samples.
FreeRTOS
The firmware uses FreeRTOS to divide the system into several tasks.
Separate tasks handle functions such as:
- Game logic
- Controller input
- CAN communication
- Display updates
- Audio mixing
- USB communication
The time-critical tasks are given suitable priorities. Input and audio, for example, must continue to run at predictable intervals even when the game is drawing a complex scene.
FreeRTOS is not strictly required to make a game like this, but it helps keep the different parts of the console separated and easier to develop.
Custom Development Tools
Preparing the content turned out to be almost as important as writing the game engine. General-purpose graphics tools can edit the artwork, but they do not understand fighter pivots, hitboxes, attack timing or the memory layout of an embedded system.
For that reason, I created two browser-based tools specifically for this project.
Street Fighter Animation Editor
The Street Fighter Animation Editor is a local, browser-based tool for preparing fighter animations.
It was designed around irregular sprite sheets like the one used during development. Instead of assuming that every frame has the same dimensions or sits on a fixed grid, the editor lets me draw the boundary of each frame manually.
Frames can then be grouped into categories and assembled into animations such as idle, walking, jumping, punching, kicking, blocking and taking damage.
For each frame, I can define:
- The frame boundary on the source image
- The pivot point
- Frame duration
- Hurtboxes
- Hitboxes
- Pushboxes
- Damage
- Hitstun
- Knockback
- Scaling
- Other attack properties
The editor includes an animation timeline, making it possible to preview the result and adjust the timing without recompiling the firmware after every small change.
All editing takes place locally in the browser. The project is stored using localStorage, so it remains available between sessions without requiring a server or database.
When an animation is complete, the editor exports clean C header files containing the frame coordinates, timing, pivots, collision boxes and attack properties. These files can be included directly in the STM32 project.
The normal workflow is:
- Load a sprite sheet.
- Mark the individual frame boundaries.
- Set the pivot point for each frame.
- Group the frames into animations.
- Adjust the frame timing.
- Add hitboxes, hurtboxes and pushboxes.
- Enter the attack properties.
- Preview the animation.
- Export the result as C data.
This removes a large amount of repetitive manual work and makes it much easier to fine-tune an attack while testing the game.
Embedded Asset Pipeline
The second tool is the Embedded Asset Pipeline. It prepares graphics and audio for use by the console.
Normal creative files such as PNG images and WAV audio cannot simply be copied into the STM32 firmware. They must be converted into formats that the console can read quickly and stored in a known layout inside the external flash memory.
The pipeline provides one place to import, convert, preview and package the project’s assets. Files are added using drag and drop.
It supports content such as:
- Images
- Sprite sheets
- Sound effects
- Tracker music
- Colour palettes
- Tilesets
- Background graphics
The files are converted directly in the browser. Images can, for example, be converted to RGB565 or indexed colour, while audio can be resampled and converted into the format expected by the audio mixer.
The converted result can be previewed before it is added to the final package. This is useful because a technically correct conversion may still look or sound poor on the actual hardware.
The pipeline also creates an asset index. Each asset receives a name and a known location in the final image, allowing the firmware to find it without hard-coded source addresses.
When everything is ready, the complete asset package can be exported as either a binary image or a UF2 file.
The tool is built with React, TypeScript and shadcn/ui. All conversion and packaging take place locally in the browser, so the original project files do not need to be uploaded to a server.
Updating Assets by Drag and Drop
One of the more convenient features of the console is its USB asset update system.
When the console is connected to a Windows computer, it appears as a removable drive, much like a USB memory stick. No separate STM32 programmer or installation utility is needed to update the game content.
The Embedded Asset Pipeline can export the converted graphics, music and sound effects as a UF2 file. Updating the console is then a simple process:
- Prepare and preview the assets in the Embedded Asset Pipeline.
- Export the package as a UF2 file.
- Connect the console to the computer using USB.
- Open the console drive in Windows.
- Drag and drop the UF2 file onto the drive.
The console reads the UF2 blocks and writes the asset data into its external flash memory. The game can then access the new assets by name or address.
The game firmware and asset data are kept separate. This means I can update sprites, backgrounds, palettes or audio without rebuilding and programming the complete application.
It also makes development faster. A new sprite or sound effect can be converted, packaged and transferred to the console using the same familiar drag-and-drop workflow as copying a file to a USB drive.
How the Tools Work Together
The two tools handle different parts of the content workflow.
The Street Fighter Animation Editor describes how the fighter behaves. It defines frame timing, pivots, collision boxes and attack properties.
The Embedded Asset Pipeline handles the actual data used by the console. It converts the images and audio, builds the asset index and packages everything for the external flash memory.
Together, the workflow looks like this:
- Define the fighter frames and animations in the Animation Editor.
- Export the animation data as C header files.
- Convert the graphics and audio in the Embedded Asset Pipeline.
- Preview the converted assets.
- Build a UF2 asset package.
- Drag the UF2 file onto the console’s USB drive.
- Compile the exported animation definitions into the game firmware.
- Run and test the result on the console.
These tools were created for this project, but most of the asset pipeline could also be reused for other STM32 graphics or audio projects.
Current Status
The project currently contains the main systems needed to build a small fighting game:
- Animated fighters
- Walking, crouching and jumping
- Fighter state machines
- Basic attacks
- Hitbox and hurtbox collision detection
- Pushboxes
- A scrolling stage camera
- External CAN controllers
- Input history and special-move detection
- Double-buffered graphics
- Music and sound-effect playback
- External flash asset storage
- USB mass-storage support
- Drag-and-drop UF2 asset updates
- Custom animation and asset-development tools
There is still plenty left to do. The next steps include adding more attacks, improving combat behaviour, introducing health and round management, refining the special moves and turning the current systems into a complete playable match.
What I Have Learned
This project has shown me that a capable 2D game can run directly on a microcontroller, but only when the software is designed around the hardware from the beginning.
Memory transfers matter. Asset formats matter. The timing of input, graphics and audio matters. Even the way a sprite sheet is prepared can affect how much work the microcontroller must perform while the game is running.
The limited display resolution, memory and processing power are challenges, but they are also what make the project interesting. There is no large framework hiding the details. Every frame that appears on the display has passed through systems that I designed specifically for the console.
For me, that combination of embedded hardware, real-time software and game development is the main point of the project.
This is an independent technical and educational project inspired by classic arcade fighting games. Street Fighter and its characters are trademarks and copyrighted works belonging to their respective owners. Recognisable artwork used during development is temporary prototype material and is not original project artwork.
