Can AI Handle Firmware-Level Development? Lessons from Building a Breakout Game in the UEFI Shell
Firmware and BIOS circles have long assumed that AI coding tools are fine for application-layer code but fall short once you reach register access, protocol stacks, and bare-metal environments. This article reports an experiment that put that assumption to the test: a graphical Breakout game running in the UEFI Shell with working Chinese text rendering, built almost entirely by AI. It walks through the constraints that were deliberately tightened, the five real technical obstacles (the EFI build chain, GOP pixel formats and modes, an OS-free event loop, bitmap font rendering, and source-encoding mismatches), a two-round iteration log, the AI's stable strengths versus its recurring failure modes, how the three main tool paradigms compare for firmware work, and the time and token costs involved. It closes with ten reusable practices for making AI-driven low-level development actually deliverable.
1. Why “Breakout in the UEFI Shell” Is a Good Stress Test for AI Coding
In firmware and BIOS circles, a long-standing belief holds that AI coding tools do fine on application-layer code, but fall short as soon as the work involves register access, protocol stacks, or bare-metal environments. That belief has real grounding: public corpora for firmware are scarce, most of the knowledge lives in private codebases, chip manuals, and internal documents, and — critically — there is no operating system to fall back on. No standard library, no system calls, no graphics framework, no exception messages. A single null pointer can show up as a black screen or a hang.
But “not enough training data” does not mean “impossible”. It is largely a problem of prompt design and environment constraints. To quantify where the boundary actually sits, this experiment picked a deliberately harsh target: a graphical Breakout game running under the UEFI Shell, with Chinese text display support. Three reasons:
- The environment is genuinely bare. The UEFI stage has no OS support at all; every capability comes from firmware protocols and runtime libraries, which is the truest reflection of low-level development.
- The feature surface is broad enough. Graphics rendering (framebuffer, pixel formats, mode switching), user input (keyboard/pointer events), collision detection, time stepping, and file I/O (loading resources) together cover nearly every core element of a graphics program.
- The acceptance criteria are objective. Whether it compiles, whether the firmware loads it, whether the picture renders correctly, whether Chinese glyphs actually form characters — none of that requires subjective judgment.
2. Experiment Rules: Locking Down the Variables First
To keep the conclusions credible, the rules were tightened as far as possible:
| Constraint | Setting |
|---|---|
| Host environment | Desktop dev machine with one general-purpose commercial IDE installed; no firmware-specific toolchain preinstalled |
| Local codebase | Zero starting point; no existing project, framework, or sample code provided |
| Human-written code | Zero lines on the host side; everything generated by AI |
| Expert guidance | Goal-level and constraint-level prompts only; no protocol usage hints, no build flags, no debugging strategy |
| Acceptance | Copy the executable to a USB stick → boot into the firmware Shell → run it directly → GUI plus Chinese text renders correctly |
Project scaffolding, build flags, directory layout, resource packaging, and error localization were all left to the AI to decide on its own. Human intervention was limited to two things: correcting false premises (for example, when it assumed some operating-system API exists) and feeding back verbatim compiler or runtime errors.
3. Breaking Down the Difficulty: Where Low-Level Work Actually Hurts
3.1 The Build Chain: From “Code” to a Loadable .efi
This was the first place the AI fell apart at scale. A firmware executable is not an ordinary PE program; it has to satisfy all of the following:
- The output must be an EFI Application image type, with the link subsystem set to the EFI application subsystem.
- No C runtime dependency: calls the compiler may implicitly generate, such as
memcpy,strcpy, andmemset, must be implemented manually, or you get undefined symbols at link time. - Stack protection off, RTTI and C++ exceptions off, runtime security checks off, and any compiler-inserted calls disabled.
- The entry point must use the standard signature
EFI_STATUS EFIAPI Entry (EFI_HANDLE, EFI_SYSTEM_TABLE *). - The build framework needs module type, entry point, library classes, and include paths declared.
The AI’s first version of the project showed a gap between “the code compiles” and “the image can be loaded” — the classic symptom being zero compile errors while the firmware either refuses to load the image or resets on load. The fix is not to make the AI guess. It is to make it converge stage by stage through “compile error → link error → load failure → runtime failure”, solving one class of problem per stage and requiring it to state how each step is verified.
3.2 Graphics Output: The Twin Traps of Pixel Format and Mode
UEFI exposes graphics through the Graphics Output Protocol (GOP). There are only two core operations: setting a video mode and block transfer (Blt). The difficulty is not the small number of APIs but the details:
// Direct framebuffer write: the offset must be computed from the firmware's// actual pixel format and scanline strideSTATICVOIDDrawPixel (UINT32 X, UINT32 Y, UINT32 Argb){EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;UINT32 Stride;UINT8 *Base;Info = mGop->Mode->Info;Stride = Info->PixelsPerScanLine; // note: stride != horizontal resolutionBase = (UINT8 *)(UINTN)mGop->Mode->FrameBuffer + Y * Stride * 4 + X * 4;// Under PixelBlueGreenRedReserved8BitPerColor, the BGR order must not be assumed*(UINT8 *)(Base + 0) = (UINT8)(Argb); // Blue*(UINT8 *)(Base + 1) = (UINT8)(Argb >> 8); // Green*(UINT8 *)(Base + 2) = (UINT8)(Argb >> 16); // Red}
Two high-frequency traps:
PixelsPerScanLineis not the horizontal resolution. There is alignment padding, so computing the address from the width produces diagonal streaks and overall misalignment.- There are three possible pixel formats (different combinations of RGB/BGR and reserved bits). The AI’s first implementation hard-coded the channel order, which turned “red bricks” blue. The correct approach is to detect the format at runtime and branch on it.
Mode selection cannot be hard-coded either. Different platforms support different resolution sets, so all modes must be enumerated with priority-based fallback:
STATIC CONST UINT32 mWidthOrder[] = { 1280, 1024, 800, 640 };STATIC CONST UINT32 mHeightOrder[] = { 720, 768, 600, 480 };// Step down level by level; return as soon as any level matches.// If every level fails, fall back to the current mode and keep running.
3.3 Input and the Event Loop: A “Main Loop” With No OS
Without an operating system, input has to be driven by hand. Keyboard input can be read through the simple text input protocol, but the more refined approach is synchronous event waiting: hand the console input device’s event handle to WaitForEvent so the CPU is released while waiting instead of busy-polling.
// Event-driven skeleton (pseudocode)while (!Finished) {gBS->WaitForEvent (1, &mInputEvent, &Index); // block instead of burning CPU in a spin loopif (Index == 0) {gST->ConIn->ReadKeyStroke (gST->ConIn, &Key); // consume the inputHandleKey (&Key); // left / right / pause / quit}TickPhysics (ElapsedMs ()); // advance physics by real elapsed time, not by frame countRenderScene (); // commit the whole frame via double buffering to avoid tearing and flicker}
This is where a key engineering watershed appears: frame-based progression versus time-based progression. The AI’s first version computed displacement from loop iterations, which produced wildly different game speeds on machines of different performance. Only after switching to measuring actual elapsed time with a timer and computing displacement from delta time did the experience become consistent across hardware.
3.4 Chinese Text Display: The Biggest Challenge in This Experiment
Firmware console text output does support Unicode wide characters, but the built-in fonts in the vast majority of firmware implementations only cover the ASCII range, so printing Chinese directly yields blank space or question marks. There are three viable paths:
| Approach | Idea | Cost |
|---|---|---|
| Rely on the firmware font | Output wide characters directly | Coverage is uncontrollable; Chinese is missing on most platforms, so it is unusable for delivery |
| Roll your own bitmap font | Build a 16×16 bitmap set, index by code point, draw pixel by pixel | Requires generating and packaging font resources; the largest implementation effort but the most controllable |
| Register a system font service | Implement the font protocol and ship simplified/traditional font packages | Best generality, but the interface is complex and hard to converge on short-term |
The experiment ultimately chose option two. The implementation points: generate 16×16 monochrome bitmaps (2 bytes per row, 32 bytes per glyph), sort them in ascending code-point order to support binary search, place the font binary in the same directory as the executable on the USB stick, and load it at runtime through the shell file interface:
typedef struct {UINT16 CodePoint; // Unicode code point, sorted ascendingUINT8 BytesPerRow;UINT8 Rows;UINT8 Bitmap[32]; // 16x16 monochrome bitmap} GLYPH_16X16;STATIC EFI_STATUSDrawText16 (CHAR16 *Text, UINT32 X, UINT32 Y, UINT32 Argb){while (*Text != 0) {GLYPH_16X16 *G = BinarySearchGlyph (*Text);if (G != NULL) { BlitGlyph (G, X, Y, Argb); X += 16; }else { BlitGlyph (GetAsciiGlyph (*Text), X, Y, Argb); X += 8; }Text++;}return EFI_SUCCESS;}
Generating the font was handed to a script: read a system font file → render each code point → emit a binary resource → generate the matching C header index. That is the classic “AI writes the tool, the tool feeds the data” division of labor.
3.5 Source Encoding: A Purely Engineering Trap That Is Still Fatal
This problem has nothing to do with firmware; it is a mismatch between the source character set and the execution character set. When a source file is saved as UTF-8 without a BOM, the compiler may parse it according to the local code page, and wide string literals get converted incorrectly. The symptoms:
- Compile-time warnings that characters cannot be represented in the source and execution character sets;
- Chinese in comments triggering character-parsing anomalies, with error line numbers that do not match reality;
- Chinese turning into mojibake or question marks at runtime.
There are three remedies: save all sources as UTF-8 with a BOM and explicitly specify the source and execution character sets; or standardize on the regional encoding; or, on GCC-based toolchains, additionally verify that wide-character length matches the firmware’s 16-bit wide characters. This kind of issue has nothing to do with the AI’s reasoning ability — it is missing environmental fact, and the AI can only work backwards from error messages. It is therefore exactly where a human should intervene early: one sentence stating the encoding convention can save an entire iteration cycle.
4. Two Rounds of Iteration
Round One: Zero to One (Goal: “Make It Run”)
- Project skeleton and build configuration: multiple failures, concentrated on image type, runtime dependencies, and entry-point signature.
- Graphics initialization: the black-screen hunt took the longest; the root cause was that the return value of the mode-setting call was never checked and the fallback logic was missing.
- First successful run: white background + grey rectangle + a movable paddle, with no text, no sound, and no boundary feedback.
The defining trait of this round was that failures clustered at the environment layer rather than the algorithm layer. Error messages were sparse, the feedback cycle was long (edit code → compile → copy to USB → reboot the machine), and the cost of a single iteration was high.
Round Two: From Working to Playable (Goal: “Playable, Good-Looking, Chinese Support”)
| Stage | Main work | Key takeaway |
|---|---|---|
| Feature build-out | Multi-row bricks, score, lives, level reset, pause | AI is consistently strong on structured logic and state-machine decomposition |
| Visual polish | Offscreen double buffering with whole-frame commit, palette tuning, boundary gradient | Flicker is fundamentally “computing while writing to video memory”; double buffering is the real cure |
| Display hardening | Multi-resolution probing, mode fallback, stride and pixel-format adaptation | Every hardware-related assumption must be determined at runtime |
| Chinese breakthrough | Font generation script, code-point indexing, localized title and prompts | Externalizing resources plus script generation is the general pattern for introducing non-ASCII in low-level scenarios |
5. Capability Scorecard: What the AI Got Right and Where It Tripped
What held up steadily: module decomposition and directory organization; boilerplate protocol-call code; standard algorithms such as state machines and collision detection; bulk refactoring and style unification; classifying and explaining error text; scripts and resource-generation tooling; documentation and comment generation.
Recurring error patterns (and the most valuable part of any retrospective):
- Implicitly assuming an operating system exists: calling non-existent memory-allocation, file, or time APIs, producing code that looks complete but will not compile.
- Interface hallucination: inventing plausible-sounding protocol names, structure fields, and constant values — syntactically valid, semantically nonexistent.
- Ignoring hardware variation: hard-coding resolution, pixel format, and framebuffer base address.
- Not checking return values: protocol-location and mode-setting failures get ignored, and the fault only surfaces later as a black screen.
- Forgetting early constraints as the context grows: after a long session, re-introducing dependencies that were explicitly banned.
The corresponding countermeasures are clear: write “no operating system support” into the global constraints and restate it every round; require the AI to list every external dependency for human review; enforce a status check on every protocol acquisition; use staged tasks rather than one long instruction; and periodically reset the context and restate the acceptance criteria.
6. How the Three Tool Paradigms Compare for Firmware Work
Setting specific products aside, mainstream AI coding tools fall into three paradigms, and their suitability in low-level firmware scenarios differs sharply:
- In-editor inline completion: low latency, no interruption, well suited to boilerplate and familiar domains; but it lacks project-level context and struggles with cross-file build-configuration problems.
- Conversational Q&A: good for clarifying principles, interpreting errors, and weighing options; it does not own the execution loop, so a human has to move code and results around.
- Agentic command-line tools: able to read and write files, run commands, invoke the compiler, and read logs, forming a closed “modify → compile → read error → fix” loop. This is the only form that could carry the current challenge.
The selection advice is not “strongest wins” but matching the tool to the task: environment setup and thorny error diagnosis call for a flagship model with strong long-chain reasoning; bulk rewrites, comment generation, formatting, and unit-test supplementation are fine on an economy model. Cost should be proportional to technical necessity.
7. Cost and Retrospective
Time cost: roughly 80% of the time went into the first 20% — environment adaptation (build configuration, encoding, mode initialization). That time has nothing to do with a human’s coding speed and everything to do with whether environmental facts were disclosed up front. Stating the compiler version, target image type, and source-encoding convention at kickoff would compress this phase substantially.
Monetary cost: under pay-per-token pricing, cost = input tokens (context length) × output tokens × unit price, so context accumulation in a long session keeps driving up the cost of each individual request. In testing, one continuous two-hour session of intensive autonomous development ran into the range of a couple hundred RMB; the same task on a subscription plan or an economy model can cost an order of magnitude less. Actionable cost reductions:
- Split work into independent small tasks instead of replaying a long context over and over.
- Write key constraints into a project-level description file instead of restating them by hand every round.
- Feed back only the relevant fragments of compile logs, not entire dumps.
- Validate on a small sample first (light up one rectangle, render one Chinese character) before expanding features.
- Use a local knowledge base or retrieval augmentation to fill gaps around private protocols and chip-manual material.
- Tier the model routing: strong reasoning only for architecture decisions, economy models for implementation work.
Output volume: a firmware application of roughly a dozen-plus source files and several thousand lines of effective code, including build configuration, a graphics abstraction layer, an input layer, a font module, game logic, and a resource-generation script — with zero lines of code written by hand.
8. Ten Reusable Practices
- Disclose all environmental facts once, up front: compiler version, target image type, character-set convention, and the range of available protocols.
- State negative constraints explicitly: “do not assume an operating system exists”, “do not use unverified interfaces”.
- Require a dependency list and a risk list before any code is written, and review them by hand before letting it proceed.
- Drive with the “minimum viable verification” mindset: get the smallest runnable skeleton working first, then add features layer by layer.
- Build a strong verification loop: compile errors, load results, and runtime behavior must be fed back verbatim; no guessing.
- Require runtime probing plus a fallback branch for every hardware-related parameter, and forbid hard-coding.
- Externalize resources: fonts, sprites, and configuration should all become binary resources plus generation scripts.
- Reset the context at segment boundaries in long-running tasks, and produce a short status summary at the end of each segment.
- Distill the frequent error patterns into a project-level checklist and self-audit against it at the end of every round.
- Route models by task difficulty so that cost matches capability.
9. Conclusion
In firmware low-level development — an area widely regarded as an “AI dead zone” — the experiment’s conclusion is this: AI can independently deliver a complete, working result from zero, but whether it succeeds depends mainly on constraint design, the completeness of environmental information, and the strength of the verification loop, not on the model’s raw code-generation ability.
The real bottleneck is not “can it write firmware code” but three engineering questions: can environmental facts be communicated accurately, can errors be fed back efficiently, and can context be managed effectively. Once those three are solved, the AI’s efficiency advantage in protocol calls, resource generation, logic implementation, and bulk refactoring is enough to reshape how firmware is developed — with the human role shifting from “line-by-line author” to “architecture decision-maker, constraint setter, and verification gatekeeper”. Low-level development is not weakened here. What is weakened is the repetitive labor that never should have fallen on humans in the first place.
