Reviving the NEO6502: My Journey Back Into a Unique SBC Game System

After spending a lot of time developing my NEO6502-firmware project, I took a break for a while. But recently, I felt the urge to bring it back to life and get it working again. This post is a deep dive into what the NEO6502 is, the vision behind it, the technical challenges, and the current state of the project.

My NEO6502 sleeping in its box

What is the NEO6502?

The NEO6502 is a single-board computer (SBC) that combines modern and retro computing in a fascinating way. At its heart is the RP2040 microcontroller (the same chip used in the Raspberry Pi Pico), which acts as the main CPU. But what makes it special is its connection to a classic 6502 CPU—a legendary 8-bit processor that powered iconic computers like the Commodore 64, Apple II, and Nintendo Entertainment System (NES).

The RP2040 handles all the periphery and system management, while the 6502 is intended to run the actual game logic, much like how the Gameboy Advance used a main CPU and a co-processor for games.


Firmware and HDMI Output

When I started, there was no official firmware—just a baremetal minimal framework. One of the most interesting hardware features is the HDMI connector. However, there’s no GPU; instead, the RP2040 must generate the DVI-over-HDMI signal on the fly. This is achieved using the RP2040’s PIO (Programmable I/O) co-processors and the excellent PicoDVI project, which lets you “ride the beam” and output video in real time.


The Vision: A Game Framework for the 6502

My goal was to create a game framework reminiscent of the Gameboy Advance, but with a twist: the RP2040 acts as the system’s periphery, and the 6502 is the game CPU. The two communicate via a 64KB memory-mapped array on the RP2040, directly connected to the 6502’s address bus.

This setup allows for both low-level memory-mapped access and high-level function calls (like loadSprites or tilemaps). The game applications are written in C, compiled using the MOS LLVM project, and can be run either natively or via emulation.


Emulator Modes for Easy Testing

To make development and debugging easier, I implemented two emulator modes that run on the desktop:

  • Native Mode: Runs the game code directly on x86 without 6502 emulation, for fast debugging.
  • 6502 Emulation Mode: Runs the game code as a 6502 binary using a 6502 emulator library.

Both modes support three backends:

  • neo6502: For the actual hardware.
  • x86-native-code: Game code compiled for x86.
  • x86-6502-code: Game code compiled as a 6502 binary and executed by the emulator.

Asset Conversion Tools

A key part of the workflow is a tool that converts images and assets for the game system. Here’s a summary of its main functions:

  • convert-palette: Converts a PNG image to a C header file with RGB565 pixel data, optionally exporting as binary.
  • convert-tilesheet: Converts a tilesheet image to a header file, supporting tile size, palette, transparency, and output format options (including cropping transparent borders).
  • pack-files: Packs multiple files into a single binary, with support files for C.
  • create-gluecode: Generates glue code to connect 6502 game system calls to the RP2040.
  • convert-tilemap: Converts tilemaps from formats like Tiled, LDtk, or SpriteFusion to binary.
  • parse-defines: Resolves mapped addresses from header files for memory mapping.

Memory Map

The memory map is a 64KB array in the RP2040, mapped to the 6502’s address bus. Here’s a sample of the memory map:

Name Address (hex) Bytes
FUNCTION_RETURN_OK $0001 1
FUNCTION_RETURN_ERROR $0002 1
MM_SIZE $00B5 1
MM_SCANLINE $014B 1
MM_GAMEPAD1_STATE_RELEASED $0152 2
MM_MOUSE_X $017C 2
MM_LAST_KB_CHAR $017F 1
MM_FUNC_CALL $01B1 1

(For the full memory map, see the project documentation.)


Function Map

The system exposes a rich set of functions for input, graphics, and debugging. Here are some highlights:

  • Keyboard/Gamepad Input:
    • io_keyboard_is_pressed(uint8_t keycode)
    • io_gamepad_is_active(uint8_t gamepad_id)
  • Graphics:
    • gfx_renderqueue_add_id(uint8_t id)
    • gfx_spritebuffer_create(gfx_sprite_t* spritedata, uint8_t spriteamount)
    • gfx_draw_tile(int16_t x, int16_t y, uint8_t tile_number)
    • gfx_draw_text(uint16_t x, uint16_t y, char* txt, uint8_t color_idx, uint8_t bg_index)
  • Debugging:
    • ng_debug_value(uint16_t v1, uint16_t v2)
    • gfx_debug_drawinfo_keyboard(uint16_t x, uint16_t y, keyboard_mapping_t* keyb, uint8_t coltext, uint8_t col_bg)

(For the full function map, see the project documentation.)


Challenges and Lessons Learned

One of the biggest challenges was memory mapping between the 32-bit RP2040 and the 8-bit 6502. Differences in memory padding and access patterns made mapping tricky and sometimes caused crashes. I also wanted to allow dynamic allocation of memory for code and graphics, but this nearly derailed the project due to mapping issues.


Features Implemented

  • Audio: Support for MOD and WAV files.
  • Sprites: Animated sprite handling and tilemaps.
  • Flexible Game Loading: Game code can be compiled directly into the system or loaded from USB at startup.

Setup working again!!!
0:00
/0:26

Flappy Bird as Emulator

0:00
/0:23

"Flappy Bird" on the NEO6502

Source-Code to this "Flappy Bird":
https://github.com/dertom95/neo6502-gameframework/tree/master/rp2040-firmware/mod-flappy/source


Not sure if I will put too much effort in this project. My goal today was to make it at least work (within its limitations) again.

The project is not ready for anyone to use, but it is available here:
https://github.com/dertom95/neo6502-gameframework