Game Boy Printer Packets: Checksums, Image Tiles, and Recovery
Trace the Game Boy Printer's serial packet framing, status exchange, tile payloads, compression, timing windows, and safe error recovery.
The Game Boy Pocket Printer is not a text printer reached through a generic UART. A Game Boy supplies the clock for a bidirectional serial transfer, and software layers a small packet protocol on top of that byte stream. The packets carry connection control, tile-based monochrome image data, print settings, and checksums; the printer clocks back a device identifier and status byte while the Game Boy sends dummy bytes. Keeping those layers separate makes an emulator, accessory adapter, or protocol analyzer much easier to reason about.
This guide follows Chapter 9 of Nintendo’s Game Boy Programming Manual version 1.1 (released December 3, 1999) and compares its wire format with the pinned mGBA printer implementation. The manual’s archival cover labels the document confidential and proprietary. The article paraphrases its protocol and uses newly written examples; it does not reproduce Nintendo sample code. A source listing for an emulator is useful implementation evidence, but it does not certify hardware timing or prove that every printer revision behaves identically.
Separate serial bytes from printer packets
At the lowest layer, the Game Boy and printer exchange bits through the Game Boy serial interface. The console provides the shift clock and both sides transfer most-significant bit first. A cable or adapter therefore has to reproduce the console’s synchronous transfer behavior; putting these bytes into a host UART stream without implementing the original clocked exchange is not equivalent.
At the packet layer, every message begins with the two-byte preamble 88 33. Four header bytes follow: command, compression indicator, and a 16-bit payload length in little-endian order. The payload follows, then a 16-bit checksum that is the sum of the four header bytes and payload, followed by two dummy transfer bytes. Those final clocks let the Game Boy receive the printer’s response: device number first, status second. The Pocket Printer identifies itself as 81; the manual describes FF FF as the response when it is not connected or powered.
The core framing rule can be tested on a host without pretending that the host code implements the physical link:
def printer_packet(command: int, payload: bytes = b"", compressed: int = 0) -> bytes:
if command not in (0x01, 0x02, 0x04, 0x08, 0x0F):
raise ValueError("unsupported printer command")
if compressed not in (0, 1) or (command != 0x04 and compressed != 0):
raise ValueError("compression is only defined for data packets")
if len(payload) > 0x280:
raise ValueError("payload exceeds the documented packet limit")
if command in (0x01, 0x08, 0x0F) and payload:
raise ValueError("this command has no payload")
if command == 0x02 and len(payload) != 4:
raise ValueError("print instruction data is four bytes")
if command == 0x04 and not payload and compressed:
raise ValueError("data-end packet is uncompressed")
header = bytes((command, compressed)) + len(payload).to_bytes(2, "little")
checksum = (sum(header) + sum(payload)) & 0xFFFF
return b"\x88\x33" + header + payload + checksum.to_bytes(2, "little") + b"\x00\x00"
init_packet = printer_packet(0x01)
assert init_packet == bytes.fromhex("88 33 01 00 00 00 01 00 00 00")
This fixture matches the manual’s initialization example byte for byte. A real adapter must collect the bytes clocked back during the two dummy positions; the zeroes in the host-side packet builder are outgoing placeholders, not a synthesized printer response. The snippet also does not create Game Boy clock pulses, handle cable voltage levels, or enforce transfer delays.
Commands describe a small state machine
The command byte identifies the packet’s role: 01 initializes and checks the connection; 02 carries a print instruction; 04 transfers image data; 08 requests a break; and 0F is a NUL packet used to query status. The data-end packet is also command 04, distinguished by a zero payload length. That detail matters to a parser: “data ended” is not a separate command number.
An initialization packet has no payload and invalidates previously transferred print data. A print-instruction payload is four bytes: requested sheet count, before/after feed counts packed into nibbles, palette mapping, and density adjustment. A count of zero means feed-only operation. A normal full-screen print is assembled from nine data packets, then terminated by a zero-length data packet before the print instruction is issued. The manual also allows a shorter transfer followed by data-end, in which case the printer prints the lines received rather than inventing the remainder of the screen.
The printer protocol’s image units are not arbitrary chunks of a PNG. Each Game Boy character tile contributes two bitplane bytes for each of eight vertical pixels. The manual describes the print payload as rows of 20 characters and two character rows per data packet; nine such packets cover a 160-by-144 image. Uncompressed data for one packet is bounded at 0x280 bytes. A parser should validate lengths before allocating memory, preserve packet order, and keep tile/bitplane interpretation separate from the outer serial framing.
Compression, checksums, and returned status
For data packets, the header’s compression byte selects compressed or uncompressed transfer. Nintendo documents a run-length scheme with raw-run and repeated-byte control codes, applied one print line at a time. A compressed representation that expands beyond the documented line structure is not a harmless visual glitch: the manual says incomplete line expansion produces a packet error. If implementing compression, build a decoder first and test round trips for short runs, maximum runs, literal blocks, and boundary crossings. Do not let a run span a line simply because the next bytes happen to match.
Every packet checksum covers header plus payload; an incorrect checksum is reported in the status response. The manual’s status byte assigns bit 0 to checksum error, bit 1 to busy, bit 2 to full image buffer, bit 3 to unprocessed data, bit 4 to packet error, bit 5 to paper jam, bit 6 to other error, and bit 7 to low battery. Treat the returned status as a response to each packet, not as a continuously updated host-side variable. A NUL packet has no payload and requests current status; the Nintendo flow recommends checking it before sending more image data because paper jams or low battery can interrupt printing.
Recovery should be bounded and state-aware. If the status indicates a checksum error, the documented flow permits resending the packet; do not resend indefinitely if the cable is disconnected or the device reports another fault. On a protocol error, stop the transfer and report what the device returned. If a packet was interrupted, re-establish the connection and initialize rather than assuming both endpoints kept the same parser state. The manual describes a 100 ms NUL keepalive cadence after connection is confirmed and says the printer returns to an initialized wait state if that synchronization check is missed.
Timing is part of the protocol
Nintendo specifies a 270-microsecond to 5-millisecond interval between bytes and 270-microseconds to 117-milliseconds between packets. Those windows are easy to violate if an interrupt, emulator scheduler, USB bridge, or host event loop inserts unbounded delay. Do not busy-wait inside a latency-sensitive game loop without considering interrupt effects; schedule transfers against the device timing contract and measure the actual adapter path.
The manual also warns not to issue a new print command within 100 milliseconds after the motor stops because the print starting position may be wrong. For consecutive sheets, expose a cancellation path and obey the line-feed guidance. Its final-debugging note says developers should test at least one printer from each of the two identified manufacturers, Seiko Systems and Hosiden; that is period-specific compatibility advice, not evidence that all currently available units are identical.
Use emulators as implementations, not as the specification
The pinned mGBA source has an explicit parser-state sequence for preamble, command, compression, length, payload, checksum, dummy/keepalive, and returned status. That is useful when designing a debugger or comparing a packet trace. However, the same source contains a TODO in its print-command processing. It is therefore not proof of faithful paper rendering, complete checksum rejection, exact motor timing, or every status transition. Compare emulator behavior with the Nintendo manual and, where possible, captured hardware traces; record the exact emulator revision used for tests.
A practical test set includes the initialization fixture above, a valid NUL status query, a zero-length data-end packet, a full nine-packet image, a checksum error, an overlength payload, a truncated packet, cable removal during transfer, a full buffer, and print interruption. Keep pure byte-framing tests separate from timing/hardware tests. The former can run on a host; the latter require a real console/printer or an explicitly scoped emulator test. This separation keeps a passing unit test from being mistaken for proof that a Game Boy can print the image correctly.
Related:
- Game Boy Link Cable Serial Transfer: Registers, Clocking, and Timeouts
- Game Boy Camera Cartridge: Banked Registers, Capture, and Image Processing
Sources:
- Nintendo, Game Boy Programming Manual, version 1.1, Chapter 9, pp. 237–251; Internet Archive item and scan
- mGBA printer state-machine implementation, pinned revision
143adf5aa2a60c6036810b0cd72445e13d74b0e6 - mGBA printer command and status definitions, pinned revision
143adf5aa2a60c6036810b0cd72445e13d74b0e6 - Pan Docs, Game Boy Printer protocol, community technical cross-reference