Blake Smith

create. code. learn.

»

Learning Homebrew Game Boy Game Development in Assembly

A few weeks ago, I shipped a mostly complete Breakout / Arkanoid clone called “Brickbasher”, written for the original Game Boy. You can download the ROM here.


The limitations of designing for small and constrained hardware proved to be really fun. You can’t yak-shave too much when you only have 4 bit colors, and 4 basic audio oscillators to work with! I played so many old-school Game Boy games growing up, so it’s been a rewarding nostalgia trip to dive in head first and do what my younger self could only dream of.

At a high level, the tools and building blocks for the project are:

  1. Game Boy flavored 6502 assembly for all the code, assembled with RGBDS.
  2. PNG tilemaps for game assets, layed out in Aseprite, and converted into ROM data with my own custom tool called gbtile.
  3. Music tracked with hUGETracker and played in game with hUGEDriver.
  4. Testing / emulation with the BGB Game Boy emulator (Ironically, ran via Wine, since I’m a Linux user, and BGB seems to have the best debugging tools I could find)

The game runs on any standard Game Boy emulator, and should theoretically run on real hardware too.

Tools and Hardware Ramp-Up

In today’s modern homebrew world, believe it or not, you can get your hands on a C compiler that targets the Game Boy. C might be considered “low level” these days, but it’s luxurious compared to raw 6502 asm! I wanted to learn by diving down “one more layer of abstraction”, so I chucked the C compiler aside and went straight for emacs and an assembler.

After tool setup, the next big step was getting familiar with the Game Boy hardware and how it works. There are great manuals floating around that go through the important details of the Game Boy hardware. About a year before Brickbasher, I built a very rough webasm Game Boy emulator in Rust, which was another great way to get familiar with the inner workings of the system.

Tutorials

I used this gbdev.io tutorial as a starting point for mapping out the rudimentary game structure. The tutorial walks you through a lot of the basics of Game Boy 6502 assembly, and basic concepts of the Game Boy hardware. You draw tilemaps, take some basic user input, and move the paddle around back and forth. By the end of the tutorial, you wind up with something that looks like this:


You get basic tile drawing, and a small paddle that you can move around, but that’s it. Unfortunately, the rest of the tutorial beyond these basics is incomplete. So now I needed to go off on my own and figure out the rest.

Level Data

The original tutorial showed how to draw the background tiles with the bricks, but it was all hardcoded into the ROM tilemap. I wanted to implement a “level draw” routine that could take input level data, and draw the bricks in the correct locations. Here’s what the level data looks like in assembly ROM:

Level0:
        db 0,0,0,0,0,0
        db 0,0,0,0,0,0
        db 0,0,0,0,0,0
        db 2,2,2,2,2,2
        db 2,2,2,2,2,2

The level data format I came up with is a bit wasteful on space, but gets the job done. Having bricks all be “byte aligned” avoids doing a bunch of bit-level manipulation later on in tight loops (trading data for CPU time). I use a single byte to represent each brick, with a maximum of 5 rows of bricks per level. Each byte counts how many “hits” an individual brick can take, with a ‘0’ representing “no brick”.

From this level data, I wrote a routine that reads through the level data and writes out the brick tiles in the correct locations.

Sprites and Tiles

I wanted to make the original paddle sprite larger, and add a basic bouncing ball. The original tutorial did not include a sprite or tile map, other than the raw converted assembly data for the game. So I recreated the sprites in a PNG based tilemap so I could change things around to my liking:

Background Tilemap

Background Tilesheet. Individual tiles in Game Boy hardware are each 8x8 pixels wide, which is why Game Boy games usually follow similar “block size” layouts.

Ball sprite

Ball sprite that bounces around the level.

Paddle sprite

The Paddle Sprite. In the game, this actually uses a total of three 8x8 pixel Game Boy sprites.

I run these PNG images through a small utility I wrote that converts the PNG images into Game Boy sprite and tile data that can be loaded directly into the assembler. The RGBDS toolkit comes with a more feature rich utility to accomplish a similar task, but writing my own simplified tool was a really great way to learn about how the Game Boy represents pixel data down to the bit level.

Collision Detection

By far the hardest part of the entire project was doing collision detection on the limited Game Boy hardware. The Game Boy has no built-in multiplication instructions (read: multiplication in a tight loop is going to be expensive).

I bashed my head against the wall a lot, and settled on precomputing collision lookup tables that store the upper X and Y coordinates of each brick. In retrospect, because there are only a maximum of 30 bricks on the screen, I probably could have just computed the tables “offline” with a calculator, and hardcoded them into the game, but I wanted to figure out how to compute the tables with the Game Boy hardware itself.

I ended up needing to borrow and modify the multiplication routines from the Game Boy C compiler, in order to precompute these tables when the level loads.

Like I said, next time I’m just going to break out the calculator, or a Python script and slap the output directly into ROM data!

Music

I stumbled upon hUGETracker, an old-school style music tracker that can export to a format that’s readable on Game Boy hardware.

hUGETracker Logo

My game only has a very basic 4 bar repeating track, but it was enough to learn how to go through the process of getting music into a game via the hUGEDriver asm routines.

After compiling hUGEDriver.asm along with the game code, the routine to initialize the audio track looks like:

InitAudio:
        ;; Turn audio on
        ld a, $80
        ld [rAUDENA], a
        ld a, $FF
        ld [rAUDTERM], a
        ld a, $77
        ld [rAUDVOL], a

        ;; Start playing audio track
        ld hl, first_track
        call hUGE_init
        ei
        ret

And then we just need to tick the audio on a fixed interrupt cycle:

SECTION "Timer overflow interrupt", ROM0[$0050]
    nop
    jp isr_wrapper

isr_wrapper:
    push af
    push hl
    push bc
    push de
    ;; Call into hUGE_dosound to 'tick' the audio
    call hUGE_dosound
    pop de
    pop bc
    pop hl
    pop af
    reti

I didn’t implement any sound effects, but I could reserve one oscillator for effects if I ever get there.

Polish and Finish

Beyond that, I added a lot of the bare bones features necessary to make the game actually playable:

  • A life count, and “Game Over” state.
  • Multiple levels, and level advancement.
  • A “win” state when you get through all the levels.
  • Ready screens with delays to give you time to orient before the ball starts bouncing around the screen.

Brickbasher Ready Screen

Final Thoughts

There’s a lot more polish that’d be fun to add at some point. Logo splash screens, final credits, game sound effects, more music tracks, multiple balls, all would make great additions.

This was a “Big Magic” project: I did it just for fun, to learn, and because creating new things is its own reward. I enjoyed building the game up from raw assembly, and making other tools along the way. The constraints of limited hardware made for new problems that are solvable with trivial one-liners in a modern programming language on modern hardware, but stretched me creatively in unique new ways in 8-bit land.

Feel free to check out the source code on GitHub, and give the ROM a spin in a Game Boy emulator!


»

Micropad: Final Prototype Update

I’ve recently started building the final prototype revision of micropad. Since the last post, I’ve been able to:

  • Finish the transition to a USB-C connector
  • Get the case in good alignment, including other minor mechanical changes
  • Fix top-plate buckling

Micropad Final Prototype 1.

Micropad Final Prototype 2.

Micropad Final Prototype 3.

Mechanical Fixes

I fixed the top-plate buckling issue, by including 2 different standoff sizes that the PCB is mounted to. This increased the overall macropad height, but gives the board enough clearance for a completely level top-plate mount.

I also changed the revision 1 screw fasteners from M2 to M3. M3 fasteners are much easier to find and purchase, and I wanted these to be easily repairable if fasteners are ever lost.

Part of what delayed this project’s finalization was waiting for my Prusa Mini+ 3D printer to arrive:

Prusa Mini+ 3D Printer.

Having a 3D printer at home allowed me to iterate on case tweaks really fast, and get all the case tolerances worked out. It was also a fun multi-week assembly that my oldest son helped with, which made for great fun!

Firmware

On the firmware side: There’s still a few blocks of unsafe Rust in the firmware. Most of this is caused by needing static mutable references to peripherals for interrupt handling, but I was able to slim down the unsafe blocks by wrapping more global peripherals in Mutex<RefCell<T>> types than before. I ended up not using RTIC, since my firmware size was already at the upper limit of the onboard 32K flash memory size, and adding another dependency sent it over the edge. A few unsafe blocks in a small firmware like this seems worth it to not have to buy a chip with more onboard memory, or reduce other user-visible functionality.

I’ve made 4 of these micropads in my (really messy) workshop, to give to friends, and am excited with how they’ve turned out!

PCB Assembly.

Workshop.

Micropad Final Prototype 4.

As always, the full source for micropad is available from GitHub.


»

Hardware Hacking: Building a Custom USB Media Controller

UPDATE: Click here for part 2.

I recently completed the first revision prototype of a USB media controller keyboard “macropad”. I’m not a fan of the touchbar based media control buttons on my work MacBook Pro, and wanted a way to control my music and audio volume (especially for video calls) with big chunky, tactile buttons instead.

The result is an Open Source Hardware USB Keyboard I call micropad.

Its specifications include:

  • A custom circuit and PCB designed in KiCAD.
  • A custom keyboard case and top plate designed in OpenSCAD.
  • Custom firmware, written in Rust for an STM32F042 microcontroller.
  • A command line USB-CDC command line client to control the device via a serial port protocol while the device is plugged into the host computer.
  • A chunky rotary encoder knob, for volume control. Forward, Backward, Play / Pause buttons to control media (mostly Spotify).

Finished Prototype Shop

Hardware Design

Finished Prototype Shot

While I’ve built my own full-sized USB keyboard before, I wanted to try a few new things hardware wise with this project.

  • USB-C: I’ve done plenty of USB Micro projects, but I wanted to support a USB-C connector for power and data for this project.
  • Replace the standard 10-pin 1.27mm pitch JTAG pin header I use on most projects with something that requires no additional pin header for firmware programming. I settled on these SKEDD connector cables that I really like. Check out the adapter board on GitHub.
  • Experiment with ENIG, gold plated PCB finish, and black PCB solder-mask.

KiCAD PCB Screenshot

USB C connector

Many of the other existing hardware design ideas were remixes of existing projects I’ve had success with in the past. I improved the rotary encoder accuracy by actually including a rotary encoder filter circuit into the schematic. I used standard Cherry MX keyboard switches I had laying around from a previous project for the media control buttons, but was able to skip the scan matrix that is normally required when you have more input buttons than microcontroller pins.

PCB with components assembled

The most difficult part of the hardware design has been getting the PCB and case design to fit correctly. I still haven’t found a good workflow to connect OpenSCAD and KiCAD so I can get exact / precise alignment with mounting holes and components tolerances. As much as I appreciate the “CAD as source code” nature of OpenSCAD, getting good component fits will probably push me to try something different like FreeCAD with my next hardware design project.

Micropad Case, PCB and Top plate

OpenSCAD 3D model

Firmware

Before this project, I built a small proof of concept board that used Rust and an ATSAMD51, so I was pretty confident I could get Rust going on an STM32F042, and make its USB hardware peripheral work with Rust. My previous keyboard’s firmware was written in C++, and I wanted to see if I could mimic a lot of that existing functionality in Rust.

I was able to leverage some really spectacular Rust crates, that made this project so much simpler and more intuitive than working in C++. I spent weeks in the past getting USB to work with crappy STM32 vendor examples that I could scour on the internet. Even after getting this C++ implementation to work, so much of that USB handling code wasn’t portable to other microcontrollers.

Instead, on this project, I used the usb-device crate, which provides an easy abstraction over common USB firmware implementations, including USB handshake handling, device descriptors, and communications polling.

unsafe {
    let bus_allocator = {
        USB_BUS_ALLOC = Some(UsbBus::new(usb));
        USB_BUS_ALLOC.as_ref().unwrap()
    };
    *USB_KEYBOARD.borrow(cs).borrow_mut() = Some(KeyboardHidClass::new(&bus_allocator));
    *USB_SERIAL.borrow(cs).borrow_mut() = Some(SerialPort::new(&bus_allocator));
    *USB_DEV.borrow(cs).borrow_mut() = Some(
        UsbDeviceBuilder::new(&bus_allocator, UsbVidPid(0xb38, 0x0003))
            .manufacturer("micropad")
            .product("micropad")
            .serial_number("MP00X")
            .max_packet_size_0(64)
            .build(),
    );

    core.NVIC.set_priority(Interrupt::USB, 1);
    NVIC::unmask(Interrupt::USB);
}

The STM32F042 microcontroller I used has a great Rust crate, that has well-integrated implementations of the usb-device interfaces, allowing me to interface with the USB peripheral in a relatively high-level, and microcontroller agnostic way.

Serial Control Interface

In addition to the USB HID device that the hardware would expose, I also implemented a simple bidirectional serial communication protocol between the hardware device and the host computer. This gets exposed as a USB-CDC serial device, which is essentially a way to send serial data over USB.

Command Line Client Interface

The result is a command-line client that can be used to control brightness, and other settings on the micropad itself. Right now, the messages it supports is pretty limited, but I have plans to expand this to support live changing the keyboard layout from the host computer.

What’s next

So far, I’ve done a first and second revision of the PCB and case. I’m waiting for a Prusa Mini+ 3D printer to arrive from backorder so I can iterate on the case design much more quickly, and without spending a bunch of money on externally printed parts. There is some other top plate buckling issues I plan to fix as well.

I also had to use a lot of unsafe Rust. This comes with the territory of embedded firmware, but I’d like to try some different idioms that give more safety guarantees. RTIC might be able to help, but I’m not sure if I want a ‘framework’ like this in the project. We’ll see.

In the meantime, check out the full source code, along with the Bill of Materials on Github!


» all posts