15 minutes
Open AGB Display - An Open Source IPS Display Mod for the Game Boy Advance
TL;DR
- Fully open-source IPS display mod for the Game Boy Advance
- Based on RP2350B for signal capture + rendering
- Requires custom PCB, case cuts, and optional custom lens
- Integer upscaling on 480x480 display resolution (pixel-perfect)
- Effects support (scanlines, color tweaks, pixelation)
- Power efficient: ~10-12h on NiMH at 50% brightness
- Hardware & software files linked below
Introduction
After designing the Open DMG Display, I became curious whether I had missed anything by never owning a Game Boy Advance. At a recent retro fair in my city, I found a good deal on a standard Game Boy Advance and took it home. I was genuinely surprised by how poor the display quality was. Once again, I couldn’t find any affordable open-source option for a display replacement. I found a nice 480×480 IPS module and already had some RP2350B breakout boards lying around, so I decided to run some experiments to see if I could capture the Game Boy Advance image and display it at full framerate with low latency.
Those experiments eventually escalated into this very project.
What to Expect – and What Not To
This is an open project, not a finalized product. All data and materials are provided as is. If you have questions, I’m happy to help when I have the time — however, support is not guaranteed and responses may be delayed. Bugs in the code and potential hardware issues are to be expected. If you encounter any problems, I’d appreciate it if you report them so the project can continue to improve. If you’re able to fix an issue yourself, contributions are very welcome!
Disclaimer: Use of the provided information, files, and build instructions is entirely at your own risk. No guarantee is given regarding accuracy, completeness, or functionality. Any liability for damage to hardware or software, consequential damages, or other claims arising from use is excluded.
Signals and Timings
In order to understand how this project works, it is essential to first understand the signals we receive from the Game Boy Advance and how we process this data. I will begin with a brief introduction to the Game Boy Advance display signals before explaining how we translate these signals for the TFT.
Gameboy Advance Display Signals
If you look at the schematic of the Game Boy Advance, you will see many data lines going to the display. Fortunately, not many of them are relevant for capturing the image data. The most important lines are the DCK, SPL, and SPS lines, along with the 16 data lines (LDRx/LDGx/LDBx).
Regarding the data lines, the Game Boy Advance uses an RGB565 color scheme where the least significant bit of the green channel is tied to ground. This means there is no loss of information if RGB555 is used. In my case, it was easier to use RGB565 to have clean 16-bit data for storage and transport.
The data is transmitted line-by-line from the upper left to the lower right. The start of a frame is indicated by the SPS line, which acts as a vertical synchronization (or start-of-frame) signal. The frame rate directly corresponds to the frequency of this signal.
Between two low pulses on the SPS (VSYNC), there are a total of 161 pulses on the SPL (HSYNC) signal, which translates to 161 lines. If you know why there is an additional line, please contact me. In practice, the last line can be ignored.
Now let’s take a closer look at a single line. It begins with a high pulse on the SPL (HSYNC) line. The DCK (pixel clock) is only active during the transfer of line data. There are 241 clock cycles within a line, which makes one additional pulse or 241 pixels per row. The first pixel can be ignored. For implementation purposes, the pixel data is valid only when SPL is low (meaning that SPL is high during the first clock pulse).
Finally, down at the single-pixel level, the data on the data lines changes on the falling edge of the pixel clock (DCK) and should therefore be latched or captured on the rising edge.
If you want you can download the captures (can be used with Saleae Logic 2): AGB_Signal_Captures.zip
TFT
The TFT uses a non-buffered RGB565 interface, which is quite similar to what I described above. Unfortunately, it differs in ways that prevent us from simply passing the signal through. One major difference is the resolution, meaning we have to handle more data per unit of time. Another difference is in the signal itself. I will not go into detail, but for example, the TFT’s pixel clock must run continuously, whereas the Game Boy signal does not. There are also parameters such as front porches, back porches, and so on.
Therefore, we first buffer part of the frame in the RP2350’s RAM before transferring it to the TFT. More precisely, we capture half of the Game Boy Advance frame before starting the transfer to the display. This approach has two consequences:
- We get a synchronized signal with the same frame rate as the original.
- We introduce a fixed offset to the original signal, also known as latency (or some might call it lag).
Since the display controller has no buffer, the signal almost directly drives the TFT’s line and column drivers. However, the display still has a response time, which is a physical characteristic also present in the original Game Boy Advance screen. I do not have the exact figures for the original, but the panel used in this project has a worst-case response time of around 25 ms for a white-to-black transition, which should be relatively rare in most games.
The latency caused by buffering the signal in the RP2350’s RAM measures around 6.5 ms. This is the time between the moment the last pixel of a Game Boy Advance frame is received and the moment the TFT signal reaches the last active line of the display area used for rendering the Game Boy image. This figure assumes that the 240×160 image is scaled to 480×320 and centered on a 480×480 TFT. In this case, 80 pixels at both the top and bottom of the screen remain unused, so the last line containing content would be line 400.
Signal timings of the Gameboy and the TFT can be downloaded here: agb_tft_timings.sal
Capturing and Rendering
The capture driver (agb_capture.c) uses one of the PIO units to capture the full frame and stores the data into a buffer via a DMA channel. This buffer, therefore, holds 16-bit RGB565 pixel values in a linear fashion, starting from the upper-left pixel and ending at the lower-right pixel. It is able to generate a callback after receiving a specified number of lines. For example, we can configure this callback to trigger after receiving 50% of the lines respectively after capturing half of the frame. We use this callback to trigger the other critical driver responsible for generating the display signal: the TFT driver (tft.c). This driver generates a standard RGB interface signal commonly used in modern TFT display controllers. One advantage of this interface is that it is unbuffered, allowing direct control over timing. We leverage this by using the capture driver’s callback to initiate a new frame sequence in the TFT driver. Once started, the TFT driver automatically requests the pixel data line by line as it renders the frame. The TFT driver uses its own callback to request this data, effectively acting as the link between the capture driver and the TFT driver. This callback is responsible for transfering the captured image to the TFT display.
In the simplest implementation, this rendering function simply passes the pixel data through to the display, resulting in a 240x160 image displayed on the TFT. Since both systems use the same RGB565 format, the pixel data can be used directly. However, to better utilize the 480x480 display, we can upscale the image to 480x320 using simple integer scaling. While basic, this allows the rendered image to fill the full width of the TFT module.
The rendering function also has significant potential for implementing custom effects and additional functionality, which I will describe in the next chapter.
Effects
Each pixel’s color is expressed as a 16-bit color value. We have enough RAM to store lookup tables that allow direct mapping from each color to a different value. For example, we can store a slightly more saturated version of each color, resulting in a more vibrant and colorful image. Alternatively, we can fix the hue to a single color and only modify the saturation and brightness, which would produce a single-color tinted image. If green is used, this would make every Game Boy Advance game look more like a high-resolution DMG version.
Since the TFT resolution is twice that of the source image, we have four pixels available for each source pixel. I used this for another class of effect: introducing a second lookup table intended to store a less bright or less saturated version of the values in the first lookup table. Each of the four pixels is linked to one of the two lookup tables. For example, if the two upper pixels are linked to lookup table one and the lower two to lookup table two, this creates a horizontal scanline effect. If the same pattern is applied to the left and right pixels, it produces vertical scanlines. If only the upper-left pixel of the four-pixel block uses values from lookup table one, it results in a kind of pixelation effect.
The color adjustments are calculated using the HSV model. The values are computed once and stored in the lookup tables. This approach uses significant RAM but enables real-time rendering of the described effects.
Build
Before you start the build, I recommend using an aftermarket shell to keep the original device intact. You should also print the 3D patterns and the display dummy to make your life easier.
- TFT Display Module HD34013C40 3.4" 480x480 with 40-pin RGB interface (https://de.aliexpress.com/item/1005007703060475.html)
- Assembled Open AGB Display PCB
- Pi Pico Debug Adapter for flashing
- 50mm-150mm FFC, 0.5 pos FFC for connecting the programming adapter
- 50mm 32- or 40-pin FFC, 0.5mm pitch
- 3D prints:
- Cutting patterns
- Display dummy
- Link port dummy
- Bottom spring
- Thin double-sided tape
- Non-conductive foam
- Screen lens without prints to build the custom screen lens (optional)
- Vinyl film or spray paint
Case modification
Start with the upper half of the shell. Use the provided pattern to cut the walls at the correct spot. File down any high points on the case that prevent the display from sitting flat. You can also use a sharp knife for scraping. During this process, use the display dummy to check the fit and protect the actual display.
If you want to use the full display area for classic games, you’ll need to cut a new screen opening. A 3D-printable pattern is provided for this as well. Use the pattern to cut a groove first, then remove the excess material with a wire cutter. Finally, file down any burrs for a clean finish.
PCB assembly and firmware flashing
You’ll find all information about the required components in the provided BOM. The PCB can be ordered from your preferred supplier. You’ll need to decide whether to place the 32-pin or 40-pin connector for the connection to the AGB PCB. I was only able to test the 32-pin version, so use the 40-pin connector with caution, as it is still untested. A cost-effective method for assembling components onto the PCB is explained in this blog post: Manual Assembly of Modern PCBs Using Low-Cost Tools.
In addition to the main PCB, there is a separate adapter board used for flashing the firmware and/or connecting to the debug interface. You’ll need to assemble this board as well. Connect it to the AGB main board via a 10-pin FFC and power the OpenAGB Display board with an external 3V supply. Alternatively, you can power the Open AGB Display board through the adapter board by bridging the solder jumpers, but be careful not to connect it to the AGB mainboard at the same time.
To flash the firmware, connect the USB-C port of the adapter board to your PC while holding the push button. An external drive should appear in your file system. Copy the provided UF2 file onto this drive. Once the drive disappears, the flashing process is complete.
TFT assembly
First, connect the display to the 40-pin connector on the bottom of the Open AGB Display PCB. Clean the back of the TFT with IPA. Apply double-sided tape to the back of the PCB and remove the protective film. Attach the PCB to the back of the TFT, using the display edges for alignment.
Next, solder the wires to the corresponding pads on the board. Use an appropriate wire gauge for the power connections. To avoid confusion, mark the three input wires, either with a permanent marker (as I did) or by using different wire colors. Then insert the 32- or 40-pin FFC into the upper connector and lower the assembly into the case. Make sure to remove the protective film from the display before installation.
Insert the 3D-printed spring at the bottom of the TFT, and place pieces of non-conductive foam on the back of the display. This will keep the TFT pressed firmly against the shell.
Now move on to soldering the AGB mainboard. Begin by removing the link port from the top of the PCB. Connect the wire from pad 3 on the Open AGB Display to TP2. Since the remaining soldering will be done on the back of the AGB mainboard, this is a good time to tidy up the cables and secure the PCB in place with screws. It is also the right moment to connect the FFC between the Open AGB Display board and the AGB PCB. Next, connect the wire from pad 2 to the left shoulder button, and the wire from pad 1 to the right shoulder button. Finally, connect the supply wires to the power switch.
Custom Lens
If you want to take full advantage of the display size, especially when it comes to classic Game Boy games, you’ll need to use a custom lens for the screen. Fortunately, there are clear glass and plastic lenses available that do not have any printed graphics.
To achieve a cleaner look, it’s a good idea to add a frame that hides the adhesive and the rough edges from the cutouts we previously made.
I tried two methods for creating a frame on the lens. Both use a 3D-printed pattern (included in the project files) to make it easier to mark the transparent area.
Method 1: Spray Paint
For the first attempt, I used spray paint. I cleaned the glass lens and masked off the area that needed to remain transparent. I applied two coats of black spray paint and removed the masking tape while the paint was still slightly wet. If the paint dries completely, it may peel off when removing the tape. I let the lens dry overnight.
Method 2: Vinyl Film
For the second attempt, I used black vinyl film, typically used with vinyl cutters. I first cleaned the glass lens and covered one side with the film. It is important to be very careful during this step to avoid creating bubbles in the area that will remain on the lens.
Next, I used the 3D-printed pattern to mark the area where the film should be removed, and cut along the marked edges with an X-Acto knife. I then carefully removed the rectangular cutout.
To assemble the lens into the case, I used double-sided tape for both methods. It can easily be applied along the edges of the black frame and trimmed to the right shape using the lens itself as a guide.
OSD Menu and Settings
Press the Select button for 2 seconds to change the brigthness level. Use the left and right shoulder buttons to change the value.
If you hold Select for 5 seconds, you will enter the settings menu. In this menu, you can:
- Adjust the color brightness
- Apply a tint to the display using a single color
- Enable visual effects, such as pixelation or scanlines
- View the About section, which shows the current software version
- And maybe more in future software revisions
This allows you to customize the display to your preference and explore the available effects.
Supply and power consumtion
The Open AGB Display is powered directly from the battery and does not rely on the AGB’s internal voltage regulators. The board includes two regulators (in addition to the internal regulator of the RP2350). The first is a SEPIC converter that generates the 3.3V rail, accepting input voltages from 1.8V up to 5.5V. The second is a step-up converter that generates 18V for the backlight, operating from 1.8V to 6V.
Both regulators are connected to the battery through the power switch. Since this switch is usually somewhat aged and subject to wear, a voltage drop across it is to be expected. With the backlight fully enabled, the system draws around 400mA at 2.2V. In my measurements, the power switch introduced a voltage drop of about 0.08V, corresponding to roughly 200mΩ resistance. This could be worth revisiting in future revisions.
The regulators have been tested to start up reliably at 2.1V input at the battery terminals, which aligns with the lower discharge voltage of NiMH cells. To reduce current draw, and therefore losses, you may want to consider Li-Ion AA cells with 1.5V nominal voltage. This also brings the regulators closer to their efficiency sweet spot. However, the original AGB regulators may have been optimized for slightly lower voltages, so the measured system power consumption does not provide a clear picture.
Brightness | Voltage | Current | Power (W) |
---|---|---|---|
20% | 2.2V | 178mA | 0.392 W |
2.5V | 161mA | 0.403 W | |
3.0V | 131mA | 0.393 W | |
50% | 2.2V | 234mA | 0.515 W |
2.5V | 210mA | 0.525 W | |
3.0V | 172mA | 0.516 W | |
80% | 2.2V | 406mA | 0.894 W |
2.5V | 358mA | 0.895 W | |
3.0V | 294mA | 0.882 W | |
100% | 2.2V | 418mA | 0.920 W |
2.5V | 398mA | 0.995 W | |
3.0V | 366mA | 1.098 W |
(The power consumtion at the time of release with the system running a GBA game)
In practice, 50% brightness feels like a comfortable setting for indoor play. With modern NiMH cells, you can expect around 10–12 hours of runtime from two fully charged batteries, which, in my opinion, is quite reasonable. There may still be potential for software optimizations to reduce power consumption further, but as long as you’re using the maximum backlight setting, the impact of such tweaks will be limited.
Thanks
-
AGB board scans
-
Graphics Library for the OSD
- LVGL Graphics Library
- https://lvgl.io/
Files
Sourcecode and release binaries are hosted on codeberg.org
-
Hardware
- Open AGB Display
- Debug adapter
-
Software
3140 Words
2025-08-25 02:00