The Art of CHIP-8
摘要
A deep-dive technical article exploring the CHIP-8 programming language, its instruction set, interpreter quirks, and strategies for writing portable CHIP-8 programs.
<p><a href="https://lobste.rs/s/aqmagt/art_chip_8">Comments</a></p>
查看缓存全文
缓存时间: 2026/08/14 09:32
Source: [https://beyondloom.com/blog/artofchip8.html](https://beyondloom.com/blog/artofchip8.html)
## The Art of CHIP\-8
[CHIP\-8](https://en.wikipedia.org/wiki/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](http://octo-ide.com/), 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](https://github.com/Timendus/chip8-test-suite)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](https://beyondloom.com/blog/artofchip8.html#systemoverview)
- [Arithmetic Instructions](https://beyondloom.com/blog/artofchip8.html#arithmeticinstructions)
- [Memory](https://beyondloom.com/blog/artofchip8.html#memory)
- [Subroutines](https://beyondloom.com/blog/artofchip8.html#subroutines)
- [Control Flow](https://beyondloom.com/blog/artofchip8.html#controlflow)
- [Input](https://beyondloom.com/blog/artofchip8.html#input)
- [Random Numbers](https://beyondloom.com/blog/artofchip8.html#randomnumbers)
- [Timing](https://beyondloom.com/blog/artofchip8.html#timing)
- [Output](https://beyondloom.com/blog/artofchip8.html#output)
- [Where To Go From Here](https://beyondloom.com/blog/artofchip8.html#wheretogofromhere)
- [Further Reading](https://beyondloom.com/blog/artofchip8.html#furtherreading)
## 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\-bit[1](https://beyondloom.com/blog/artofchip8.html#fn:1)“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 CodeOcto SyntaxNotes`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**AND**ed 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 CodeOcto Syntax`6XNN``vx := NN``7XNN``vx \+= NN`The remainder manipulate two registers, with`vx`storing the result\.
Machine CodeOcto 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](https://en.wikipedia.org/wiki/Two%27s_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 CodeOcto 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`takes four instructions, and eight bytes:
```
v0 := 11v1 := 22v2 := 33v3 := 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 := startupload 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`load`s and`save`s 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 := blankload v7save v7save v7save v7save 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 addressi += 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 addressi += v1 # i is data+v1i += v1 # i is data+2*v1load 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 countervb := 10 # vertical position for the score counteri := decode-buffer # point to a temporary bufferbcd v0 # decode a numeric value in v0load v2 # v0 is hundreds (0-2), v1 is tens (0-9), v2 is ones (0-9)i := hex v0 # sprite for hundredssprite va vb 5 # draw a 5-pixel tall spriteva += 5 # advance 5 pixels horizontally, leaving a space between digitsi := hex v1 # sprite for tenssprite va vb 5 # draw a 5-pixel tall spriteva += 5 # advance 5 pixels horizontally, leaving a space between digitsi := hex v2 # sprite for onessprite 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 # hundredsva += 5digit v1 # tensva += 5digit 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\!
## Subroutines
Machine CodeOcto 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 subroutine[2](https://beyondloom.com/blog/artofchip8.html#fn:2)\.
```
: 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](https://en.wikipedia.org/wiki/Tail_call)\!
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 0xABfoo # 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
```
## Control Flow
Machine CodeOcto 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 := v1vf -= v0if 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 syntaxC 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 := 0loop body v0 += 1 if v0 != 4 thenagain
```
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:
```
bodybodybodybody
```
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 := 34else va := 56 vb := 78end
```
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 := 56vb := 78if v0 != v1 then va := 12if 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`\.
## Input
Machine CodeOcto 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', blockingv0 := key
```
The latter are more complex to use, but essential for any kind of action game\.
```
# wait for the user to pess 'any key', pollingloop # ... insert some idle animation here vf := 0 loop if vf key then jump done vf += 1 if vf != 16 then againagain: 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_Eloop if vf key then again # make sure the key isn't already pressedloop 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\.
## Random Numbers
Machine CodeOcto 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:
ConstantPossible 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–127Sometimes 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\.
## Output
Machine CodeOcto 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**XOR**ing 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`sprite`s overlapping the edges of the screen: some will only draw`sprite`s within the 64x32 pixel window and clip anything beyond it, some will draw`sprite`s 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`sprite`s 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\-**XOR**ed” 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](http://beyondloom.com/tools/ezpack.html)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](http://beyondloom.com/tools/ezbake.html)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\.
## Timing
Machine CodeOcto 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 againagain
```
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](https://beyondloom.com/blog/artofchip8.html#fn: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\.
## Where To Go From Here
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](https://github.com/nanochess/bootBASIC)\. 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](http://johnearnest.github.io/Octo/)? If it runs on CHIP\-8, it’s immortal\.
## Further Reading
1. The[Octo Reference Manual](https://github.com/JohnEarnest/Octo/blob/gh-pages/docs/Manual.md)
2. [An Octo Implementation In C](https://github.com/JohnEarnest/c-octo/)
3. The[CHIP\-8 Archive](https://github.com/JohnEarnest/chip8Archive); modern games written for CHIP\-8
4. Tobias V\. Langhoff’s[Guide to making a CHIP\-8 Emulator](https://tobiasvl.github.io/blog/write-a-chip-8-emulator/)
[back](https://beyondloom.com/blog/index.html)
相似文章
我用自己编写的编程语言写了一个CHIP-8模拟器
一个使用自定义编程语言Spectre编写的CHIP-8模拟器实现,作为开源项目发布在GitHub上。
从前面板到程序:像PDP-8那样思考
本文介绍了PDP-8小型计算机的架构及其在教授基本计算机概念中的应用,并包含一个复刻模型的赠品活动。
AP8L - apl
AP8L 是一种受 APL 启发的编程语言,嵌入在 PICO-8 中,支持类似 APL 的函数和基本图形。
Gigatron 编程
一篇技术文章,详细描述了作者尝试理解和编程 Gigatron TTL 彩色微型计算机的过程,涵盖其哈佛架构、CPU 指令和编程技巧。
在PICO-8上模拟进化
一篇关于在PICO-8幻想游戏机上模拟进化过程的文章,可能展示了一个创意编码项目或教育工具。