Skip to content
HN On Hacker News ↗

The Art of Chip-8

▲ 58 points 11 comments by surprisetalk 2w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,771
PEAK AI % 0% · §1
Analyzed
Aug 31
backend: pangram/v3.3
Segments scanned
1 windows
avg 1771 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,771 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

The Art of CHIP-8 CHIP-8 is a programming language originally developed for the 1977 COSMAC VIP kit computer. CHIP-8 programs are composed of a series of two-byte instructions resembling machine-code for a simple virtual instruction-set architecture, so CHIP-8 interpreters are often also referred to as “emulators”. Indeed, writing a CHIP-8 interpreter is an excellent way to learn the principles underlying emulators for antique computers and game consoles, and as a result there are a dizzying array of thousands of CHIP-8 runtimes available for almost every conceivable platform. For historical platforms to live, rather than simply be preserved, we must write new software for them. The profusion of CHIP-8 implementations comes in turn with a great deal of confusion, as a half-century-long game of telephone has produced a wide variety of diverging behaviors in interpreters. Over the course of developing Octo, my high-level CHIP-8 assembler, I helped popularize and standardize a variety of “quirks flags” which capture common divergences between extant CHIP-8 flavors, and investigated many dark, unspecified corners of influential interpreters. There are now mature test suites available for CHIP-8 interpreters and their variants, so there’s no excuse for modern interpreters to get the details wrong. Still, the reality of CHIP-8 in the wild is fragmented: many interpreters for obscure platforms are written by beginners unaware of any broader hobbyist community and abandoned as soon as they (appear to) correctly run PONG.CH8. In this article I will examine CHIP-8 as an instruction set and its practical implications for writing new programs, distilling a number of scattered tutorials, examples, and FAQs I’ve written in the past. I will specifically point out approaches which are portable across all but the buggiest and least complete existing CHIP-8 interpreters. Example code will use Octo’s notation; this document is not intended as a complete reference manual for Octo assembly language, but I will endeavor to explain new ideas as we encounter them. Index System Overview Arithmetic Instructions Memory Subroutines Control Flow Input Random Numbers Timing Output Where To Go From Here Further Reading System Overview CHIP-8 operates in a 12-bit address space. The original CHIP-8 interpreter resided in the first 512 bytes of this space, with programs starting at address 0x200. It also reserved some of the upper region of the address space for a stack, a framebuffer, and several scratchpads. As a result, we are left with a maximum of 3232 bytes for our code and data. Modern CHIP-8 interpreters are often more generous, leaving up to 3584 bytes for user programs, and they will often use the low 512 bytes of memory to store their hex font(s) or nothing at all, leaving it available for programs to manipulate. Your code and data should fit within 3232 bytes for maximum portability. We have a file of 16 general-purpose 8-bit registers named v0-vf, giving the platform a pleasantly RISC-ey feel. The 12-bit1 “index register” i is used for all operations which reference or manipulate memory. There is an internal stack for threading subroutine return addresses, but it is opaque: the instruction set does not allow programs to freely push or pop temporary values or inspect the contents of the stack. Programs take input from a hexadecimal keypad. For output, we have a 64x32 pixel 1-bit bitmapped display and a simple piezo buzzer for making noise. We are also afforded a non-interrupting delay timer and a random number generator. There are 34 elementary CHIP-8 instructions. In the descriptions below, vx and vy are any v-register, and NNN, NN, and N represent an immediate 12-bit, 8-bit, or 4-bit value, respectively: Machine Code Octo Syntax Notes 00E0 clear Clear the display. 00EE ; or return Exit a subroutine. 1NNN jump NNN 2NNN NNN or :call NNN Call a subroutine. 3XNN if vx != NN then Conditional skip. 4XNN if vx == NN then Conditional skip. 5XY0 if vx != vy then Conditional skip. 6XNN vx := NN 7XNN vx += NN 8XY0 vx := vy 8XY1 vx |= vy Bitwise OR. 8XY2 vx &= vy Bitwise AND. 8XY3 vx ^= vy Bitwise XOR. 8XY4 vx += vy vf gets 1 on carry, otherwise 0. 8XY5 vx -= vy vf gets 0 on borrow, otherwise 1. 8XY6 vx >>= vy vf gets old least significant bit. 8XY7 vx =- vy vf gets 0 on borrow, otherwise 1. 8XYE vx <<= vy vf gets old most significant bit. 9XY0 if vx == vy then Conditional skip. ANNN i := NNN BNNN jump0 NNN Jump to address NNN + v0. CXNN vx := random NN Random byte bitwise ANDed with NN. DXYN sprite vx vy N Draw on display; vf gets 1 on collision, otherwise 0. EX9E if vx -key then Is a key not pressed? EXA1 if vx key then Is a key pressed? FX07 vx := delay FX0A vx := key Wait for a keypress. FX15 delay := vx FX18 buzzer := vx FX1E i += vx FX29 i := hex vx Set i to a hex character sprite. FX33 bcd vx Decode vx into binary-coded decimal. FX55 save vx Save v0-vx to memory address i through i+x. FX65 load vx Load v0-vx from memory address i through i+x. Observe that all instructions are two bytes wide, and component fields are nybble-aligned: these characteristics facilitate hand-assembling programs with a pen and paper and also help simplify writing some forms of self-modifying code. The following sections will discuss these instructions in more detail, grouped by their functional purpose. Arithmetic Instructions Two arithmetic instructions take immediate arguments: Machine Code Octo Syntax 6XNN vx := NN 7XNN vx += NN The remainder manipulate two registers, with vx storing the result. Machine Code Octo Syntax 8XY0 vx := vy 8XY1 vx |= vy 8XY2 vx &= vy 8XY3 vx ^= vy 8XY4 vx += vy 8XY5 vx -= vy 8XY6 vx >>= vy 8XY7 vx =- vy 8XYE vx <<= vy Octo uses syntax similar to C-family languages for these operations, with a Pascal-style := assignment operator for symmetry. The ^= =- <<= and >>= instructions were present in the original CHIP-8 interpreter, but not documented; they arose as a natural consequence of the RCA-1802 instruction encoding. The =- instruction is like -= except it subtracts vx from vy instead of vy from vx; as always the result is stored in vx. The shift instructions are intended to set vx to vy shifted left or right by 1 place, storing the shifted-out bit in vf. Many modern interpreters incorrectly implement these instructions as ignoring vy and shifting vx in-place. Only use shift instructions of the form vx <<= vx and vx >>= vx. Using the same register for both arguments will produce consistent behavior with or without the "shift quirks". In some interpreters, the bitwise operations |=, &=, and ^= modify vf as a side effect. This problem is less well-known than the “shift quirks”, and can be quite a surprise when trying to run programs on an emulated COSMAC VIP. Assume the bitwise instructions vx |= vy , vx &= vy, and vx ^= vy destroy the contents of vf. If you need to do a bitwise NOT, you can use XOR with an appropriate constant in a register. The vf register is an ideal choice if you only need that constant once: vf := 0xFFv0 ^= vf # invert the bits in v0 Many interpreters are inconsistent as to whether they write a carry-flag result before or after the main result of an arithmetic instruction, leading to ambiguity if the destination register (vx) is vf. Never use the vf register as the destination of arithmetic instructions except vf := NN, vf += NN, or vf := vx. If you ever need a “no-op” instruction, the best options are instructions of the form vx := vx, such as v0 := v0. Instructions of the form vx += 0 work, too; this leads to an important potential “gotcha”. Say you’re trying to increment a 16-bit counter: v0 += 1 # increment low bytev1 += vf # carry into the high byte The behavior of that snippet is undefined, because adding an immediate value to a v-register does not alter the carry flag vf. The fact that the original version may appear to work, sometimes- depending on whatever happened to already be in vf- is all the more infuriating. You meant to do this instead: vf := 1 # put 1 in a temporary registerv0 += vf # increment low byte with our constant 1v1 += vf # carry into the high byte Octo lets you use negative numbers for literals, which are interpreted as their two’s complement equivalents. Whether you use this feature or not, remember that vx += NN can be used both for incrementing and (via overflow) decrementing v-registers: v0 += -1v0 += 255 Taking the bitwise OR of N with N+1 will have the effect of setting the least significant (or rightmost) zero bit in the byte: vf := v0v0 += 1v0 |= vf Similarly, the bitwise AND of N with N-1 will clear the least significant (or rightmost) one bit in the byte: vf := v0v0 += -1v0 &= vf If a (nonzero) number becomes zero after performing this operation you know it had exactly one bit set and was thus a power of two. Memory Now we’re ready to discuss manipulating the index register i and using it to access memory: Machine Code Octo Syntax ANNN i := NNN FX1E i += vx FX29 i := hex vx FX33 bcd vx FX55 save vx FX65 load vx The first thing you’re likely to notice about these instructions is that our ability to modify the i register is constrained: we can set it, and we can advance it with i += vx, but we cannot decrement it, and there’s no provision for reading it out and stashing it for later. The save and load instructions are also a bit unusual: they write or read a range of bytes from memory, starting at i. Let’s say we want to stash the contents of v0 through v3 to a reserved buffer of 4 bytes. Octo lets us define a label with : which we can then refer to when setting i: i := buffer # initialize the index registersave v3 # write out the bottom four registers# ...: buffer 0 0 0 0 A save vf or load vf will stash or restore 16 registers in two instructions; very handy! This is especially useful at program startup. Most CHIP-8 interpreters will zero the v-registers before your program executes, but this behavior is not universal. Initializing four registers directly with vx := NN