Chip-8 on the COSMAC VIP: Arithmetic and Logic Instructions

This is part of a series of posts analysing the Chip-8 interpreter on the RCA COSMAC VIP computer. These posts may be useful if you are building a Chip-8 interpreter on another platform or if you have an interest in the operation of the COSMAC VIP. For other posts in the series refer to the index or instruction index.

INSTRUCTION GROUP: 7XNN
Add NN to VX

INSTRUCTION GROUP: 8XY1
OR VY with VX

INSTRUCTION GROUP: 8XY2
AND VY with VX

INSTRUCTION GROUP: 8XY3
XOR VY with VX

INSTRUCTION GROUP: 8XY4
Add VY to VX, load VF with carry bit

INSTRUCTION GROUP: 8XY5
Subtract VY from VX, load VF with carry bit

INSTRUCTION GROUP: 8XY6
Shift VY one bit right, load VX with result, load VF with bit 0

INSTRUCTION GROUP: 8XY7
Subtract VX from VY, load VX with result

INSTRUCTION GROUP: 8XYE
Shift VY one bit left, load VX with result, load VF with bit 7

The first of these instruction groups, 7XNN, adds the immediate operand NN to VX. It’s simple enough to not warrant a flowchart, so here’s the code:

Address (hex) Code (hex) Labels Assembly Comments
01B7 45 7XNN: LDA 5 Get the value in the second byte of the instruction into the accumulator (D) and then advance the Chip-8 programme counter to the next instruction
01B8 E6 SEX 6 Set the VX pointer to be used for register indirect addressing instructions
01B9 F4 ADD Add the value in VX to the accumulator (D), which currently holds the immediate operand from the second byte of the Chip-8 instruction
01BA 56 STR 6 Store the result back in VX
01BB D4 SEP 4 Return to the fetch and decode routine

An important thing to note about this routine is that any carry is ignored. If the result is more than 0xFF then only the least significant byte will be stored. For example, if V0 is 0xF1 and the instruction is 7010 then adding 0x10 to 0xF1 should give 0x101. However, as the carry condition is ignored, VX will contain 0x01 after this operation. If you need addition that preserves the carry bit, use instruction group 8XY4 instead (see below).

This instruction requires 10 machine cycles (45.4 microseconds).

The instruction groups that begin 8X?? all work in the same way, using a single routine. They make use of the fact that there is a series of 1802 instructions (0xF1 to 0xF7 and 0xF9 to 0xFF) that perform arithmetic and logic instructions. Instructions 0xF9, 0xFA, 0xFB, 0xFC, 0XFD and 0xFF all use immediate operands, so they cannot be used. The remaining instructions 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7 and 0xFE all operate on either the accumulator alone or the accumulator and whatever memory location is currently pointed to by the register indicated by X, so these instructions can be made use of by the Chip-8 interpreter. The flowchart shows how the routine works:

A flowchart showing the execution sequence for Chip-8 arithmetic and logic instructions

Here’s the code:

Address (hex) Code (hex) Labels Assembly Comments
01BC AF 8XYN: LDA 5 Get the value in the second byte of the instruction into the accumulator (D) and then advance the Chip-8 programme counter to the next instruction
01BD FA 0F ANI 0x0F Mask the byte to save just the second hex digit
01BF 3A C4 BNZ DECODE_ AL_ INSTR If the second digit is not zero, it’s an arithmetic and logic instruction, so branch to the decode routine for these
01C1 – 01C3 There’s a short routine here to copy VY into VX. This was analysed in a previous post
01C4 AF DECODE_ AL_ INSTR: PLO F Temporarily save the last digit of the instruction in RF.0
01C5 22 DEC 2 Decrement the stack pointer, ready for a push operation
01C6 F8 D3 LDI 0xD3 Load a 0xD3 1802 instruction (SEP 3) into the accumulator
01C8 73 STXD Push this onto the stack and decrement the stack pointer
01C9 8F GLO F Restore the last digit of the Chip-8 instruction to the accumulator
01CA F9 F0 ORI 0xF0 OR this with 0xF0 to create a 1802 instruction of the form 0xFN, where N is the last hex digit of the Chip-8 instruction
01CC 52 STR 2 Push this onto the stack
01CD E6 SEX 6 Set the VX pointer to be used for register indirect addressing memory instructions
01CE 07 LDN 7 Load the value in VY into the accumulator
01CF D2 SEP 2 Execute the two instructions at the top of the stack (the first of these will be the AL instruction, the second will be a SEP 3 instruction to return control to this routine at the instruction following this one
01D0 56 STR 6 Save the result of the operation in VX
01D1 F8 FF LDI 0xFF 0xFF is the low-order byte of the address of Chip-8 variable VF
01D3 A6 PLO 6 The VX pointer now points to VF
01D4 F8 00 LDI 0x00 Clear the accumulator
01D6 7E SHLC Move the carry flag into the least significant bit of the accumulator
01D7 56 STR 6 Save this in VF
01D8 D4 SEP 4 Return to the fetch and decode routine

There are a few interesting things to note about this routine. Firstly it cleverly uses the stack as a temporary scratch pad for a dynamically created, two-instruction routine that performs the actual arithmetic or logic instruction. You may notice that although two instructions are pushed onto the stack at the start of the routine, they are never popped off again. This is because, when the routine is run, R2 (the stack pointer) is incremented after each instruction is executed. So when control is returned to the Chip-8 interpreter routine, R2 has been restored to its original position.

Note that all these instructions overwrite variable VF. This is used to show the status of the carry bit, which is copied into the least significant bit of VF. All other bits of VF will be set to 0. This means, if you use VF as the VX argument, the result will be overwritten by the flag status.

Only the first five of these instructions were officially documented by RCA. 8XY6, 8XY7 and 8XYE are all undocumented. Unfortunately an erroneous assumption about 8XY6 and 8XYE, the two shift instructions, seems to have crept into Chip-8 lore at some point. Most recent documentation on Chip-8 suggests these instructions shift VX rather than VY. Some documents suggest that the correct format for them is 8X06 and 8X0E. However, when these instructions are run on the original Chip-8 interpreter, they will shift VY not VX! An instruction of the form 8X06 or 8X0E will not work as expected. The programmer would be expecting this to shift VX and store the result back in VX. What would actually happen is that V0 would be shifted and the result stored back in VX. This leaves the programmer of a contemporary interpreter with a bit of a dilemma. Do you make these instructions function as they would have done in the original interpreter or do you make them function in accordance with the erroneous assumption? Or do you offer both modes and a way to switch between them? This post documents the way the instructions worked in the original Chip-8 interpreter.

All these instructions require 44 machine cycles (199.76 microseconds) to execute.

The table below shows the register state for VX, VY and VF for each of these instructions.

Instruction VX (entry) VY (entry) VX (exit) VY (exit) VF (exit)
8XY1 VX argument VY argument Bitwise OR of VX with VY Unchanged Undefined
8XY2 VX argument VY argument Bitwise AND of VX with VY Unchanged Undefined
8XY3 VX argument VY argument Bitwise XOR of VX with VY Unchanged Undefined
8XY4 VX argument VY argument VX added to VY Unchanged 0x00 if no carry occurred, 0x01 if a carry occurred
8XY5 VX argument VY argument VY subtracted from VX Unchanged 0x00 if a borrow occurred, 0x01 if no borrow occurred
8XY6 VX argument (initial value is irrelevant) VY argument VY shifted right by one bit. Most significant bit of VX will be set to 0 VY shifted right by one bit. Most significant bit of VY will be set to 0 0x00 if least significant bit of VY before shift was 0, 0x01 if least significant bit of VY before shift was 1
8XY7 VX argument VY argument VX subtracted from VY Unchanged 0x00 if a borrow occurred, 0x01 if no borrow occurred
8XYE VX argument (initial value is irrelevant) VY argument VY shifted left by one bit. Least significant bit of VX will be set to 0 VY shifted left by one bit. Least significant bit of VY will be set to 0 0x00 if most significant bit of VY before shift was 0, 0x01 if most significant bit of VY before shift was 1

Note that when VF is shown as undefined, VF will still have been set to either 0x00 or 0x01, but this will reflect whatever 1802 instruction last affected the DF register and will bear no relation to the outcome of the current instruction.

Another thing to note is that, although some of these instructions set VF to reflect whether a carry or borrow occurred or to reflect the bit that was lost in a shift operation, none of these instructions take that status of VF into account when they are executed. This means the programmer must explicitly take whatever action is necessary as a result of the value in VF. For example, if the programmer was adding two four-byte integers, they would have to check VF after each addition operation and, if necessary, manually increment one of the addends for the next addition operation before performing it.

Here are examples of each instruction:

8XY1 (VX OR VY)
VX (entry): 0x00101101 (0x2D)
VY (entry): 0x01001011 (0x4B)
VX (exit): 0x01101111 (0x6F)

8XY2 (VX AND VY)
VX (entry): 0x00101101 (0x2D)
VY (entry): 0x01001011 (0x4B)
VX (exit): 0x00001001 (0x09)

8XY3 (VX XOR VY)
VX (entry): 0x00101101 (0x2D)
VY (entry): 0x01001011 (0x4B)
VX (exit): 0x01100110 (0x66)

8XY4 (VX + VY resulting in no carry)
VX (entry): 0x00101101 (0x2D)
VY (entry): 0x01001011 (0x4B)
VX (exit): 0x01111000 (0x78)
VF (exit): 0x00000000 (0x00)

8XY4 (VX + VY resulting in carry)
VX (entry): 0x11101101 (0xED)
VY (entry): 0x01001011 (0x4B)
VX (exit): 0x00111000 (0x38)
VF (exit): 0x00000001 (0x01)

8XY5 (VX – VY resulting in no borrow)
VX (entry): 0x01001011 (0x4B)
VY (entry): 0x00101101 (0x2D)
VX (exit): 0x00011110 (0x1E)
VF (exit): 0x00000001 (0x01)

8XY5 (VX – VY resulting in borrow)
VX (entry): 0x00101101 (0x2D)
VY (entry): 0x01001011 (0x4B)
VX (exit): 0x11100010 (0xE2)
VF (exit): 0x00000000 (0x00)

8XY6 (SHR VY with 0 as LSB)
VY (entry): 0x00101100 (0x2C)
VX (exit): 0x00010110 (0x16)
VF (exit): 0x00000000 (0x00)

8XY6 (SHR VY with 1 as LSB)
VY (entry): 0x00101101 (0x2D)
VX (exit): 0x00010110 (0x16)
VF (exit): 0x00000001 (0x01)

8XY7 (VY – VX resulting in no borrow)
VX (entry): 0x00101101 (0x2D)
VY (entry): 0x01001011 (0x4B)
VX (exit): 0x00011110 (0x1E)
VF (exit): 0x00000001 (0x01)

8XY7 (VY – VX resulting in borrow)
VX (entry): 0x01001011 (0x4B)
VY (entry): 0x00101101 (0x2D)
VX (exit): 0x11100010 (0xE2)
VF (exit): 0x00000000 (0x00)

8XYE (SHL VY with 0 as MSB)
VY (entry): 0x00101101 (0x2D)
VX (exit): 0x01011010 (0x5A)
VF (exit): 0x00000000 (0x00)

8XYE (SHR VY with 1 as MSB)
VY (entry): 0x10101101 (0xAD)
VX (exit): 0x01011010 (0x5A)
VF (exit): 0x00000001 (0x01)

If you are writing an interpreter to strictly mirror the documented functionality of the original Chip-8 interpreter, then you only need to implement instruction groups 7XNN and 8XY1 through 8XY5. However, if you do so, you will almost certainly find some more recent Chip-8 programmes that don’t function correctly.

If you do decide to implement the undocumented instructions, then you need to decide whether to implement the shift instructions as they are shown here (i.e. mirror the behaviour of the original interpreter) or implement them as they are documented by most recent descriptions of the Chip-8 instruction set (i.e. with VX being shifted rather than VY). Alternatively, you could provide both behaviours and then allow users to switch between them if they need to. The reality is that you will probably find more Chip-8 programmes that depend on the erroneous behaviour, so that may be a better choice as a default.

This entry was posted in Chip-8, Retro Computing and tagged , , , , , , , . Bookmark the permalink.

3 Responses to "Chip-8 on the COSMAC VIP: Arithmetic and Logic Instructions"

Leave a reply