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.
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.
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 := 0xFF
v0 ^= 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 byte
v1 += 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 register
v0 += vf # increment low byte with our constant 1
v1 += 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 += -1
v0 += 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 := v0
v0 += 1
v0 |= vf
Similarly, the bitwise AND of N with N-1 will clear the least significant (or rightmost) one bit in the byte:
vf := v0
v0 += -1
v0 &= 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.
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 register
save 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 takes four instructions, and eight bytes:
v0 := 11
v1 := 22
v2 := 33
v3 := 44
Initializing four registers with a load also takes eight bytes, but only two instructions. Any more than four registers will save both program space and execution time:
i := startup
load v3
# ...
: startup 11 22 33 44
Don't assume the v-registers contain 0 when your program starts: initialize them explicitly.
Note that any load or save involves the v0 register. You’ll need this register available for temporary use throughout your program. Similarly, the vf register is mangled or altered by most of the arithmetic instructions and the sprite instruction- you can’t use it for long-term storage even if you wanted to. Since loads and saves always work on the low registers, you should organize your register usage from least to most persistent: v0, v1, v2… should contain information that is relevant only to a local subroutine, while ve, vd, vc… can contain information that tends to remain useful for the whole lifetime of a program. Be sure to use Octo’s :alias directive to give registers meaningful names, and thus give yourself flexibility for rearranging them while developing longer programs.
In practice, I recommend trying to keep at least v0 and v1 “free” for use in leaf subroutines along with vf; being able to write a pair of bytes in one go is necessary for a variety of self-modifying code tricks we’ll touch on later.
Slinging up to 16 bytes around is handy for doing fast array copies or fills. Unfortunately, if we want our programs to be portable we need to remain aware of another common bug in interpreters: the original CHIP-8 interpreter would automatically increment i after a load or save by the number of bytes read or written. The “SCHIP” interpreter for the HP-48 calculator- and many modern imitators- leaves i intact. The following fragment will zero 32 bytes starting from temp on vintage CHIP-8:
i := blank
load v7
save v7
save v7
save v7
save v7
# ...
: blank 0 0 0 0 0 0 0 0
: temp
But it won’t zero any bytes of the temp buffer on an interpreter with this misbehavior, because i never changes! A whole class of fun tricks is possible with either the original or the SCHIP behavior, but if we don’t know what we’ll get, we have to be conservative:
Never re-use the position of i after a load or save operation.
Supposing you have an array:
: data 1 1 2 3 5 8 11
Reading an element from the array by an index in v1 is straightforward. This approach can address up to 256 bytes:
i := data # set i to the base address
i += v1 # add an offset (in this case, the value in v1)
load v0 # load data[v1] into v0
If entries in the array are two bytes wide, we could add our offset to i twice; this approach can address up to 512 bytes:
i := data # set i to the base address
i += v1 # i is data+v1
i += v1 # i is data+2*v1
load v1 # v0 is data[2*v1], v1 is data[(2*v1)+1]
If we need to perform indirect addressing- looking up a pointer in a table and then indexing into it- we’ll need to write self-modifying code. Given a table of i := NNN instructions which represent our “pointers”, we can load an instruction into v0/v1 and then write it back to a different location, replacing a no-op instruction:
i := pointers # (assume the table index is in v0)
i += v0 # table entries are 2 bytes,
i += v0 # so add the index twice
load v1 # load the pointer into v0-v1
i := get-pointer # choose the destination
save v1 # overwrite the old instruction
: get-pointer
v0 := v0 # no-op which becomes an i := NNN
# ...
: pointers
i := 0xAAA
i := 0xBBB
i := 0xCCC
i := 0xDDD
i := 0xEEE
This technique is especially efficient if the instruction we’re overwriting is within a loop. The initial fetch-and-overwrite process takes 6 instructions, but on subsequent fetches it’s only necessary to execute the single i := NNN instruction.
If we happen to have a 12-bit address already in the v0 and v1 registers, we can skip a few steps and construct the i := NNN instruction on the fly:
vf := 0xA0 # constant
v0 |= vf # now v0-v1 contain our instruction
i := get-pointer # choose the destination
save v1 # overwrite the old instruction
: get-pointer 0 0
The i := hex vx instruction is a specialized built-in array lookup: it points the index register i at a 5-pixel-tall, 4-pixel wide sprite representing the lower hexadecimal digit of the contents of vx. The original CHIP-8 interpreter referenced a cleverly overlapped series of digits in the COSMAC VIP ROM, while modern interpreters tend to stash a font at 0x000 and set i to vx * 5. Beware: different interpreters use different fonts, and they don’t all perform careful bounds-checking.
Do not depend on the appearance of the built-in hex font, and do not execute i := hex vx with a value greater than 15 in vx.
The only other instruction which writes to memory is bcd vx. This is also a bit odd and specialized. If you point i at a three-byte buffer, bcd vx will decode the value in vx into digits- hundreds, then tens, then ones- in the cells of the buffer. Thankfully, there’s no indexing ambiguity for this instruction: it always leaves i unchanged. Many early CHIP-8 games used this instruction for implementing score displays:
va := 10 # horizontal position for the score counter
vb := 10 # vertical position for the score counter
i := decode-buffer # point to a temporary buffer
bcd v0 # decode a numeric value in v0
load v2 # v0 is hundreds (0-2), v1 is tens (0-9), v2 is ones (0-9)
i := hex v0 # sprite for hundreds
sprite va vb 5 # draw a 5-pixel tall sprite
va += 5 # advance 5 pixels horizontally, leaving a space between digits
i := hex v1 # sprite for tens
sprite va vb 5 # draw a 5-pixel tall sprite
va += 5 # advance 5 pixels horizontally, leaving a space between digits
i := hex v2 # sprite for ones
sprite va vb 5 # draw a 5-pixel tall sprite
: decode-buffer 0 0 0
Octo’s :macro facilities can help tidy up repetitive code like displaying each of these digits:
:macro digit REG {
i := hex REG
sprite va vb 5
}
digit v0 # hundreds
va += 5
digit v1 # tens
va += 5
digit v2 # ones
It is possible to leverage bcd vx as a way of dividing numbers by 10- or taking them modulo 10- but having to go through memory and modify i makes this awkward. When you need to display a number greater than 255, it is often best to store two digits in a byte (0–99) and only use the two low bytes of a bcd vx result. Alternatively, store each digit of the number in its own byte to begin with and thus entirely remove the need for the bcd vx instruction!
| Machine Code | Octo Syntax |
|---|---|
00EE |
; or return |
2NNN |
NNN or :call NNN |
Like Forth, Octo gives the lightest possible syntax to subroutines and subroutine calls. A colon (:) defines a label. Referencing a label name is a call to that label. A semicolon (;) returns from a subroutine2.
: washer wash spin rinse spin ;
It’s slightly more efficient to replace function calls which are immediately followed by a return with a jump instead:
: washer wash spin rinse jump spin
That’s Octo-style Tail Call Elimination!
The original CHIP-8 interpreter has a 12-level return stack, but most modern interpreters raise the limit to 16. Some crash if you over-nest, some nest indefinitely, and some quietly wrap and overwrite the oldest stack entries. In practice, well-formed programs will rarely even need 12 nested function calls, as recursion is impractical unless you furnish your own auxiliary parameter stack.
Don't write programs which make more than 12 nested subroutine calls.
One little trip-hazard to be aware of with Octo’s subroutine call syntax: Octo does not distinguish between labels and constants. If you define a constant and then use the name on its own, it will be assembled as a function call:
:const foo 0xAB
foo # 0x20 0xAB
If you want to assemble the value of a constant, use :byte:
:const foo 0xAB
:byte foo # 0xAB
If you want the name foo to assemble as a literal byte, you could also declare a :macro instead of a constant:
:macro foo { 0xAB }
foo # 0xAB
| Machine Code | Octo Syntax |
|---|---|
1NNN |
jump NNN |
3XNN |
if vx != NN then |
4XNN |
if vx == NN then |
5XY0 |
if vx != vy then |
9XY0 |
if vx == vy then |
BNNN |
jump0 NNN |
Conditional instructions in CHIP-8 are mostly conditional skips. Note that Octo’s syntax “inverts” the way conditions are shown relative to some other descriptions of these opcodes you may find online. The 3XNN opcode skips the next instruction if vx is equal to the literal NN. Octo writes the same instruction as performing the next instruction if vx is not equal to NN.
Octo also furnishes “pseudo-op” conditionals; you can use <, >, <=, or >= in conditional expressions like so:
if v0 > v1 then
And they will be expanded into logically equivalent constructions:
vf := v1
vf -= v0
if vf == 0x00 then
As we’ve noted previously, this type of construction can pose portability problems for buggy interpreters, because it uses arithmetic operators with vf as the destination register. It is also important to keep in mind that these comparisons are unsigned. While Octo will happily allow users to store immediate values like -1 in a v-register as its two’s-complement representation (0xFF), two’s-complement is merely a state of mind. 0xFE is greater than 0x03, even if you intend for those values to mean -2 and 3, respectively. If you want to compare values that could be “negative”, consider storing them with an added bias. Use the pseudo-op conditionals with caution, if at all.
jump NNN is straightforward enough. Octo encourages using structured programming, so we’ll generally write backward jumps as a loop ... again infinite loop. Inside such a loop you can have any number of while clauses which, if unsatisfied, branch forward to the end of the loop. You can also use an ordinary conditional to skip over the again at the end. Summarized,
| Octo syntax | C syntax |
|---|---|
loop ... again |
do{...}while(1); |
loop ... if a == 0 then again |
do{...}while(a==0); |
while a != 0 |
if(a==0)break; |
If we want to repeat a loop body 4 times, we write something like:
v0 := 0
loop
body
v0 += 1
if v0 != 4 then
again
Which we could also write explicitly with a label:
v0 := 0
: A
body
v0 += 1
if v0 != 4 then jump A
Keep in mind that repeated logic doesn’t necessarily mean we need a loop. If the loop body is factored as a subroutine, you can call it four times in sequence and avoid any conditionals or the need for an index variable:
body
body
body
body
This still spends an instruction for each subroutine call and return. If performance is paramount, directly inlining the instructions in this subroutine four times will be faster, at the cost of more program space.
Octo can synthesize nestable if ... begin ... (else ...) end conditionals from conditional skips and forward branches:
if v0 == v1 begin
va := 12
vb := 34
else
va := 56
vb := 78
end
Desugaring as:
if v0 != v1 then
jump A
va := 12
vb := 34
jump B
: A
va := 56
vb := 78
: B
This is very useful for writing programs that are easy to understand and modify, but over-reliance on these constructs can be inefficient. Consider this alternative which saves two bytes:
va := 56
vb := 78
if v0 != v1 then va := 12
if v0 != v1 then vb := 34
Redundant conditionals can be cheap!
It’s worth mentioning that Octo will automatically insert a jump instruction at address 0x200 to the main label if : main isn’t at the beginning of your program. This makes it more convenient to structure your program in a top-to-bottom reading order, but might be a surprise if you’re desperate for those last two bytes!
Rounding out our control flow rogues' gallery, the jump0 instruction performs a jump to address NNN+v0. The intended use is building jump-tables, dispatching to a variety of code fragments or subroutines based on an index:
v0 := va # take an index from va
v0 += v0 # entries are two bytes wide, so double the index
jump0 table
: table
jump func1
jump func2
jump func3
(Can you see a way to make this an instruction shorter if you used a non-portable vx <<= vy?)
There are some limitations to this approach. While we can repeatedly add v-registers to i to index large regions of memory, jump0 takes its offset from the 8-bit v0, so it can only address a total range of 255 bytes past the fixed label. Those bytes must be jump targets- valid instructions, which are always two bytes wide- so a table can only have a maximum of 128 uniform-stride entries if they’re jump instructions, or 64 entries if they’re pairs of instructions like i := NNN return.
As an extra inconvenience, the SCHIP interpreter has a buggy implementation of jump0 which bizarrely uses the 4 high bits of the destination address to select the offset register instead of always using v0, making it essentially unusable. The jump0 instruction was very rare in historical ROMs, which might explain why it was not thoroughly tested in some early interpreters, and why some of the first CHIP-8 variants removed jump0 to make room for different instructions with a 12-bit immediate operand.
Avoid the jump0 instruction for maximum portability.
At the cost of a few extra instructions, we can use the same idiom we saw for pointer indirection to replace jump0:
i := table
i += va # take an index from va
i += va # entries are two bytes wide, so double the index
load v1 # read the jump into v0/v1
i := trampoline
save v1 # overwrite the placeholder instruction
: trampoline
v0 := v0 # no-op to overwrite
: table
jump func1
jump func2
jump func3
Much like pointer indirection, we could use this technique to rewrite a jump or subroutine call in a loop, amortizing the cost of the self-modification. In the right situation, this technique can be faster and much more flexible than a jump0.
| Machine Code | Octo Syntax |
|---|---|
EX9E |
if vx -key then |
EXA1 |
if vx key then |
FX0A |
vx := key |
The CHIP-8 hexadecimal keypad has the following 4x4 layout:
1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F
There are two ways to get keyboard input: the vx := key instruction, which blocks until the user presses a key (and produces a somewhat jarring “beep” for each key read on the COSMAC VIP and some other interpreters), and a pair of conditional skip instructions which test whether a specific key is held down (key) or not held down (-key).
The former prevents any sort of animation or other game logic while it waits.
# wait for the user to press 'any key', blocking
v0 := key
The latter are more complex to use, but essential for any kind of action game.
# wait for the user to pess 'any key', polling
loop
# ... insert some idle animation here
vf := 0
loop
if vf key then jump done
vf += 1
if vf != 16 then
again
again
: done
The conditional skip instructions need the index of the key they’re testing to already be in a v-register. In a tight game loop, if you can afford it, it’s best to keep those constants resident in some of the upper v-registers so you don’t have to constantly re-initialize them. Otherwise, if you’re testing multiple keys in sequence, you might be able to load a bank of key-constants in one go.
# a flickery but highly responsive moveable object
: main
i := constants
load v6
i := hex v6
loop
clear
sprite v0 v1 5
if v2 key then v0 += -1
if v3 key then v0 += 1
if v4 key then v1 += -1
if v5 key then v1 += 1
again
: constants
30 # initial x position
15 # initial y position
:byte OCTO_KEY_A
:byte OCTO_KEY_D
:byte OCTO_KEY_W
:byte OCTO_KEY_S
0
Remember: you don’t have to use every key on the keypad! Simpler control schemes are easier for users to learn, and many CHIP-8 interpreters run on devices that have fewer than 16 gamepad keys.
Consider minimizing the number of distinct keys you use to control your programs.
If you’re polling keypad keys and waiting for a single keypress, it may be useful to frame it as waiting for the rising edge of a keypress:
vf := OCTO_KEY_E
loop if vf key then again # make sure the key isn't already pressed
loop if vf -key then again # wait for a press,
loop if vf key then again # wait for the release
You can add animation between the loops in this pattern to make your program feel more responsive to user input, but take care adding extra instructions to the polling loops: if they’re doing too much work, they can potentially miss very short key presses.
| Machine Code | Octo Syntax |
|---|---|
CXNN |
vx := random NN |
The vx := random NN instruction loads a random byte bitwise AND-ed with the mask NN into a v-register. Writing the mask constant in binary can help clarify your intent. A few examples:
| Constant | Possible values |
|---|---|
0b00000001 |
0, 1 |
0b00000011 |
0, 1, 2, 3 |
0b00000110 |
0, 2, 4, 6 |
0b00001010 |
0, 2, 8, 10 |
0b00001000 |
0, 8 |
0b10000001 |
0, 1, 128, 129 |
0b00001111 |
0–15 |
0b01111111 |
0–127 |
Sometimes there’s confusion around this instruction with implementers assuming it will generate a number between 0 and NN. Thankfully, I have not seen this misbehavior in any mainstream CHIP-8 interpreters (yet). If you wanted to be extra-safe you could strictly use 0xFF as the mask constant for this instruction- which would behave the same for either interpretation- and then perform an explicit bitwise AND with vx &= vy as needed.
Generating random numbers in some ranges with no relationship to powers of two can be tricky. The simplest approach might be to generate a number in a larger range, and retry several times if you get an invalid result:
: random-upto-9
v1 := 9
loop
v0 := random 0xF # 0-15 is the closest option to 0-9
if v0 > v1 then
again
;
For a non-uniform distribution, consider using the vx := random NN instruction to produce indices into a lookup table.
| Machine Code | Octo Syntax |
|---|---|
00E0 |
clear |
DXYN |
sprite vx vy N |
FX18 |
buzzer := vx |
Generating sound in CHIP-8 is constrained to a simple on-or-off noisemaker. The buzzer := vx instruction tells the interpreter to make some kind of noise for vx 60ths of a second. Re-issuing the instruction resets the countdown timer, so if vx is zero you can also use this instruction to immediately silence the buzzer. The noise made by the buzzer is implementation-dependent and varies wildly in volume and auditory discomfort. Octo defaults to using a “visual buzzer”, flashing the border color of the screen instead of making an audible sound.
The sprite instruction is the essential building-block of all CHIP-8 graphics. Using vx and vy to indicate a horizontal and vertical position on the screen in pixels, respectively, and i to indicate the image to draw, this instruction will draw an 8 pixel wide and N pixel tall image by bitwise XORing successive bytes of the data pointed to by i with the pixels of the screen. If any pixels on the screen are “flipped” by this process- that is, if our image’s “on” pixels overlapped with any pixels presently on the screen- we set vf to a nonzero value. Otherwise, vf will be set to zero. This can be useful for detecting whether an object we’ve drawn collides with anything else on the screen. Drawing the same image in the same place a second time will fully erase it.
There are several subtle variations on the behavior of this instruction across interpreters. Firstly, it is a common assumption that vf will be set to 1 when a “collision” occurs during sprite drawing. The SCHIP interpreter- among others- set vf to the number of rows containing a collision. This behavior is potentially an intriguing enhancement, but was not documented, and modern interpreters generally do not attempt to replicate it.
Don't compare vf to 1 after a sprite instruction; test whether it is zero or non-zero.
The SCHIP interpreter also offered an intentional alteration of the sprite instruction, using a size constant N of 0 to request drawing a 16x16 sprite, while the original CHIP-8 interpreter treated it as drawing an 8x0 pixel sprite (drawing nothing). The SCHIP extension is commonplace, but not strictly a CHIP-8 feature.
Don't draw a sprite with a height of zero in vanilla CHIP-8 programs.
There’s also some variation in how interpreters handle sprites overlapping the edges of the screen: some will only draw sprites within the 64x32 pixel window and clip anything beyond it, some will draw sprites so long as their starting x and y coordinates modulo 64 and 32 (respectively) fall within the 64x32 pixel window, and some will draw every pixel of every sprite modulo the dimensions of the display, “wrapping” graphics around instead of clipping them. (Octo defaults to the latter.) It can be a bit limiting for some kinds of programs, but for maximum portability it is best to avoid drawing over the edges of the screen.
Draw sprites with x coordinates between 0 and 56, and y coordinates between 0 and 32-N.
On the COSMAC VIP, the sprite instruction also waited for a vertical-sync on the display. In effect, this means every additional sprite instruction in a main loop directly reduced the framerate of a program! Modern interpreters tend to run programs much faster than the original interpreter, and should be designed to use the delay timer (as described in the following section) to help account for variation in execution speed. There’s no simple rule to follow here for compatibility, but you should be aware that sprite may have a large intrinsic delay and each individual sprite drawing operation may be visible- if only for a fraction of a second- to the user.
This is part of why programs generally should not use the clear instruction to erase and then completely re-draw the display on every update: it leads to flickery, unpleasant-looking visuals. Static elements of a display- like the dotted “net” line in Pong- should be drawn once and left on the screen. Changing elements should be kept to a minimum, and should be erased and then re-drawn with sprite instructions. If the old and new version of a changing element fit within the same 8x15 (or smaller) rectangle, it is possible to design “pre-XORed” images which will erase the old image and draw the new image in a single step, avoiding any chance of a user seeing the “flicker” of the erased image before it is replaced. This is the key to buttery-smooth animation with CHIP-8! It is, however, incompatible with using the sprite collision flag in vf: we’ll always be toggling some pixels in an erase-and-redraw operation.
EZ-Pack is a utility designed to work alongside Octo for cutting up images into sprite-sized chunks. The “XOR Frames” checkbox will instruct it to pre-XOR successive frames in an animation sequence together to support this kind of use-case, and the more specialized EZ-Bake Animator can help with drawing these kinds of sequences procedurally. It’s not unusual for authoring sophisticated CHIP-8 programs to require writing new supporting tools.
| Machine Code | Octo Syntax |
|---|---|
FX07 |
vx := delay |
FX15 |
delay := vx |
As we’ve mentioned previously, different CHIP-8 interpreters run at different speeds. Fortunately, we have a tool to help even things out: the delay timer. Write a value to the timer with delay := vx and it will asynchronously count down to zero at 60 ticks per second. The vx := delay instruction lets you poll the timer.
There are a number of ways this can be used for different types of programs, but there’s a common structure that applies to almost any action game: initialize the timer at the beginning of your main loop, execute the majority of your game’s per-frame logic, and then poll the timer until it returns 0:
loop
vf := 2
delay := vf
# ... game logic goes here ...
loop
vf := delay
if vf != 0 then
again
again
If your program executes too slowly, the overhead of setting and checking the delay timer will only make the situation worse by a few instructions. If your program executes faster than you expect, the delay loop will burn off the extra cycles.3 Initializing the timer to 1 will “expect” the main game loop to complete in less than 1/60th of a second, for a 60 frames per second target- maybe a bit ambitious for interpreters on older or more constrained hardware. Initializing it to 2 will “expect” it to take 2/60ths of a second, for 30 frames per second, and so on.
Very few historical CHIP-8 ROMs use the delay timer in this fashion, instead relying on the inherent slowness of the sprite instruction to regulate their framerate. As a result, they need special coddling by interpreters to stop them from running too fast. Use the delay timer and your programs will work consistently on the interpreters of yesterday and tomorrow.
That’s CHIP-8. 34 instructions which can easily fit in your head. Some odd and interesting features like the load and save instructions that can manipulate slabs of registers at once, or the sprite drawing instruction that encourages thinking of all your graphics and collision tests as deltas against an opaque framebuffer. You’ve learned everything you need to know about the sharp corners the platform has acquired over the decades- instructions and practices to avoid, and ways around them. You’re ready.
CHIP-8 offers many of the puzzles and challenges available in any form of assembly language, but with a much smaller surface area and a much broader range of targets than even 8088 boot sectors. It’s an inviting gateway for learning about bits, bytes, shifts, and cycles, and developing a feel for “bare metal” programming. Every programming enthusiast, computing student, and budding compiler developer should give it a spin. You might be surprised just how much you can do with 64x32 pixels and 3232 bytes.
What are you waiting for? Why not go write some software? If it runs on CHIP-8, it’s immortal.
Technically, i is a 16–bit register, and the i := hex vx instruction is permitted to set i to an address beyond the usual 4kb of the CHIP–8 virtual machine. Since we cannot read the contents of i, we cannot observe this behavior in our programs without making assumptions about the “quirky” index–post–increment behavior of load and save. ↩︎
If you're allergic to Forth you can use the more verbose :call NNN and/or return syntax. ↩︎
Clever interpreter authors might consider recognizing the three–instruction delay loop used in this example (it's optimal, and appears verbatim in many modern ROMs), replacing it with a more efficient situationally–appropriate “sleep()”! ↩︎