Introducing the Instruction Set Part 1
This tutorial focuses primarily on introducing the CP1610's instruction set. The instructions divide up into multiple categories that we will explore briefly below. You can visit the various instruction pages for details on specific instructions.
- 1 The CPU, Memory and Registers
- 2 Primary Instructions
- 3 Single Register Arithmetic Instructions
- 4 Implied Operand Instructions
- 5 Shift and Rotate Instructions
- 6 Branch and Jump Instructions
The CPU, Memory and Registers
The Central Processing Unit, aka. CPU, forms the brain of the system. It is connected to several memories, including the Executive ROM, Scratchpad RAM, System RAM, Graphics ROM and Graphics RAM. It's also connected to several peripherals, such as the Programmable Sound Generator (PSG) and Standard Television Interface Controller (STIC). On the inside, it has a small amount of storage for intermediate values called registers.
All of the RAMs, ROMs and peripherals are hooked to the CPU over a common interface known as the "system bus." Each of these items is assigned a range of addresses by which the CPU can refer to it. For example, the Scratch RAM is assigned addresses $0100 - $01EF. The System RAM is assigned addresses $0200 - $035F. The EXEC is assigned addresses $1000 - $1FFF. And so on. These assignments are referred to as the Memory Map.
The CPU interacts with memory by reading and writing specific locations in memory. The CPU does not know what it is accessing when it sends out a read or write. All it knows is the address, and whether it's reading or writing. If it's writing, it also knows the data it wishes to write. How the addressed item responds depends on the item itself.
RAMs (Random Access Memories) respond to reads by returning the contents of the addressed location. For example, if the CPU reads from location $0100, the Scratch RAM will return the contents of that location to the CPU. If the CPU writes to location $0100, the Scratch RAM will update the value stored in that location. ROMs (Read Only Memories) work similarly, except that they ignore writes. Peripherals such as the PSG and STIC modify their behavior based on what gets written to them, either generating various sounds (in the case of the PSG) or changing the display (in the case of the STIC).
Programming the CPU is primarily a process of figuring out what values need to be written where and when. ROMs typically hold the program code and fixed data. RAMs hold values that change during the game, or in case of the GRAM and BACKTAB, information displayed on the screen. Peripherals handle inputs (such as controllers) and provide outputs such as sound, voice and video.
The CPU Registers
The CPU has 8 16-bit registers. These registers act as a scratch pad, holding values for instructions to operate on. Some registers have special purposes. All of the registers can be used for general purpose computation. Here's a quick reference to what each register can be used for.
|Register||General Purpose||Shift Instructions||Indirect Pointer||Return Address|
R6 and R7 are special. R6 is the stack pointer. R7 is the program counter. The assembler accepts SP and PC as aliases for R6 and R7. You can perform arbitrary arithmetic on either, although performing math on the program counter usually is a bad idea unless you really know what you're doing.
In addition to the 8 16-bit registers, the CPU has a number of flag bits that track processor status:
|S||Sign Flag||If set, indicates that the previous operation resulted in negative value, which is determined by a one (1) in bit 15 of the result.|
|C||Carry Flag||If set, indicates that the previous operation resulted in unsigned integer overflow.|
|Z||Zero Flag||If set, indicates that the previous operation resulted in zero.|
|O||Overflow Flag||If set, indicates that the previous operation gave a signed result that is inconsistent with the signs of the source operands. (Signed overflow.)|
|I||Interrupt Enable Flag||If set, allows the INTRM line to trigger an interrupt, causing the CPU to jump to the interrupt subroutine located in the Executive ROM at $1004.|
|D||Double Byte Data Flag||If set, it causes the next instruction to read a 16-bit operand with two 8-bit memory accesses, if it supports it.|
The CP1610 has 7 primary instructions. The primary instructions have the most flexibility in terms of where they draw their inputs from. The 7 primary instructions are:
|MVI||"MoVe In": Read a value into a register from memory|
|MVO||"MoVe Out": Write a value from a register to memory|
|ADD||Add two values together|
|SUB||Subtract two values|
|CMP||Compare two values by subtracting them|
|AND||Bitwise logical AND|
|XOR||Bitwise logical XOR|
Each instruction takes two operands. The first operand is a source operand. The second is both source and destination, except in the case of CMP, which doesn't write a result. (CMP does, however, set flags.)
The primary instructions are available in 4 forms, each with a different addressing mode. The mnemonics for each form differs slightly from the others:
|Register Mode||Direct Mode||Indirect Mode||Immediate Mode|
Register Mode Instructions
Register mode instructions operate on two different registers. For instance, "ADDR R0, R1" adds the value in R0 to the value in R1, and writes the result to R1. It's equivalent to the expression "R1 = R0 + R1".
The move instructions are the simplest. "Move" is something of a misnomer, though. Move instructions really copy values from one place into another. For example, "MOVR R0, R1" copies the value in R0 to R1. After the CPU runs this instruction, both R0 and R1 will have the same value. The CPU will also set the Sign Flag and Zero Flag based on the value of the number it copied.
Move instructions are sometimes used with the program counter. "MOVR R5, R7" will jump to the location whose address is in R5. The assembler offers a pseudonym for this, "JR". The instruction "JR R5" is equivalent to "MOVR R5, R7."
; Add R0 to R1, leaving the result in R1 ADDR R0, R1
; Copy the value in R2 to R3 MOVR R2, R3
; Subtracts R1 from R2 setting flags. Does not change either of R1 or R2. CMPR R1, R2
Direct Mode Instructions
Direct mode instructions operate on a value in memory. For all but MVO, they read a value from the named location. The instruction "ADD $123, R0" reads the value from location $123 and adds it to R0. The MVO instruction works in the opposite direction: It copies the value from a register into a memory location. "MVO R0, $123" writes the value that's in R0 to location $123.
The address can either be a bare address, as shown above, or a label. Labeled addresses are much easier to read. For example, suppose the label "LIVES" points to the number of lives your player has remaining. You could read that value from memory into R0 with:
MVI LIVES, R0
You'll see this technique used heavily in assembly programs of appreciable size. It makes programs easier to write, read and follow.
Indirect Mode Instructions
Indirect mode instructions also operate on a value in memory. However, rather than specifying the address directly in the instruction, they instead get the address from a register. This is useful for reading through a range of memory. For example, the instruction "ADD@ R1, R2" reads from the memory location pointed to by R1, and adds that value into R2. As with direct mode, the MVO@ instruction works in the opposite direction by writing a value to memory. "MVO@ R0, R1" writes the value of R0 to the location pointed to by R1.
Indirect mode behaves specially when using R4, R5 or R6 as the pointer register. For R4 and R5, the CPU will increment their value after using it. This makes it easy to step through an array in memory. The instruction "MVI@ R4, R0" will copy the value in memory pointed to by R4 into R0, and then it will increment R4.
; Read memory pointed to by R3, ; and put the value in R0 MVI@ R3, R0
; Read memory pointed to by R2, ; and add the value to R1, leaving the result in R1 ADD@ R2, R1
; Read memory pointed to by R4, ; subtract the value from R3 writing the difference to R3. ; Then, increment R4. SUB@ R4, R3
R6 works even more specially. When reading via R6, the CPU will decrement R6 *before* using it. When writing via R6, the CPU will increment R6 *after* using it. This behavior is what makes R6 useful as a stack pointer. The instructions "PSHR" and "PULR" are just pseudonyms for MVI@ and MVO@ using R6. Thus "PSHR R0" means the same as "MVO@ R0, R6", and "PULR R0" means the same as "MVI@ R6, R0".
The stack pointer can be used with other instructions as well. The instruction "ADD@ R6, R0" reads the value from the top of stack and adds it to R0. This can eliminate many PULR instructions. If you've ever used a stack-based calculator (such as one of HP's RPN calculators), you may find this style of programming intuitive.
Indirect mode has one limitation: You can't use R0 as the indirect register. Therefore "MVI@ R0, R1" is illegal, as is "MVO@ R1, R0".
Immediate Mode Instructions
Immediate mode instructions operate on a constant value. "MVII #1234, R0" copies the number 1234 into R0. These are useful for adding or subtracting constants from registers, setting up values in registers and so on. Note that the instruction "MVOI" generally doesn't do anything useful. You can safely ignore it.
; Puts the value 1234 into R0 MVII #1234, R0
; Bitwise ANDs $FF with R1, leaving result in R1 ANDI #$FF, R1
Special Note: Compare Instructions
The CMP instruction bears special mention. Regardless of their addressing mode, the ADD, SUB and CMP instructions all set the flags based on the result of the computation. ADD and SUB also write the computed value back to a register. CMP works like SUB, but it doesn't write the result of the subtraction anywhere. Rather, it only sets flags. Its primary use is to control conditional branch instructions.
Program Example: ex1.asm
Lets see some of the primary instructions in action, shall we? The following example code (
ex1.asm) give a couple examples of each addressing mode on a couple instructions.
ROMW 16 ORG $5000 ;------------------------------------------------------------------------------ ; EXEC-friendly ROM header. ;------------------------------------------------------------------------------ ROMHDR: BIDECLE ZERO ; MOB picture base (points to NULL list) BIDECLE ZERO ; Process table (points to NULL list) BIDECLE MAIN ; Program start address BIDECLE ZERO ; Bkgnd picture base (points to NULL list) BIDECLE ONES ; GRAM pictures (points to NULL list) BIDECLE TITLE ; Cartridge title/date DECLE $03C0 ; Flags: No ECS title, run code after title, ; ... no clicks ZERO: DECLE $0000 ; Screen border control DECLE $0000 ; 0 = color stack, 1 = f/b mode ONES: DECLE 1, 1, 1, 1, 1 ; Color stack initialization ;------------------------------------------------------------------------------ TITLE DECLE 107, "Example 1", 0 MAIN ; Immediate mode instructions MVII #$1234, R0 ADDI #$5555, R0 ; Register mode instructions MOVR R0, R1 ADDR R0, R1 ; Direct mode instructions MVO R0, $300 MVI $300, R2 ADD $300, R2 MVII #$300, R1 MOVR R1, R5 ; Indirect mode instructions MVI@ R1, R3 MVO@ R3, R5 MVO@ R3, R5 MVO@ R3, R5 ; Stack instructions PSHR R0 ; put R0 on the stack PULR R4 ; pull the value back off into R3 here B here ; Spin forever.
Assemble this program with "
as1600 -o ex1 -l ex1.lst ex1.asm". Then take a quick peek at the symbol table in the listing file. (See the Hello World Tutorial if you need a refresher.)
00005000 ROMHDR 0000500d ZERO 0000501f MAIN 0000500f ONES 00005014 TITLE 0000502f here
As you can see, "MAIN", which is where our program starts, is at $501F. Since this program doesn't print anything, we'll need to watch it in action in the debugger. To save time, we can set a breakpoint at the start of our code so that we can skip watching the EXEC.
Observing in the Debugger
0000 0000 0000 0000 0000 0000 0000 1000 -------- JSRD R5,$1026 0 >
Set a breakpoint at MAIN by typing "b 501F" and pressing enter. Then run up until the breakpoint by typing "r" and enter. The result should look like this:
0000 0000 0000 0000 0000 0000 0000 1000 -------- JSRD R5,$1026 0 > b 501F Set breakpoint at $501F > r Starting jzIntv... Hit breakpoint at $501F 0000 C0C0 0291 8007 501F 1E87 02f2 501F ------ib MVII #$1234,R0 54064 >
As you can see, R7 = $501F, which is the first instruction of MAIN. The other registers all have values in them that were set up by the EXEC. We can ignore these values for the time being. Lets single step through our 10 instructions. (Note: I have highlighted register values that change among R0 - R6 and important memory accesses for clarity.)
0000 C0C0 0291 8007 501F 1E87 02f2 501F ------i- MVII #$1234,R0 54064 > RD a=501F d=02B8 CP-1610 (PC = $501F) t=54064 RD a=5020 d=1234 CP-1610 (PC = $501F) t=54064 1234 C0C0 0291 8007 501F 1E87 02f2 5021 ------i- ADDI #$5555,R0 54072 > RD a=5021 d=02F8 CP-1610 (PC = $5021) t=54072 RD a=5022 d=5555 CP-1610 (PC = $5021) t=54072 6789 C0C0 0291 8007 501F 1E87 02f2 5023 ------i-
See how the MVII sets R0, and the ADDI changes it.
6789 C0C0 0291 8007 501F 1E87 02f2 5023 ------i- MOVR R0,R1 54080 > RD a=5023 d=0081 CP-1610 (PC = $5023) t=54080 6789 6789 0291 8007 501F 1E87 02f2 5024 ------i- ADDR R0,R1 54086 > RD a=5024 d=00C1 CP-1610 (PC = $5024) t=54086 6789 CF12 0291 8007 501F 1E87 02f2 5025 S-O---i-
See how MOVR R0, R1 copies the value from R0 to R1, and how ADDR R0, R1 adds the value of R0 into R1, leaving the result in R1.
6789 CF12 0291 8007 501F 1E87 02f2 5025 S-O---i- MVO R0,$0300 54092 > RD a=5025 d=0240 CP-1610 (PC = $5025) t=54092 RD a=5026 d=0300 CP-1610 (PC = $5025) t=54092 WR a=0300 d=6789 CP-1610 (PC = $5025) t=54092 6789 CF12 0291 8007 501F 1E87 02f2 5027 S-O----- MVI $0300,R2 54103 > RD a=5027 d=0282 CP-1610 (PC = $5027) t=54103 RD a=5028 d=0300 CP-1610 (PC = $5027) t=54103 RD a=0300 d=6789 CP-1610 (PC = $5027) t=54103 6789 CF12 6789 8007 501F 1E87 02f2 5029 S-O---i- ADD $0300,R2 54113 > RD a=5029 d=02C2 CP-1610 (PC = $5029) t=54113 RD a=502A d=0300 CP-1610 (PC = $5029) t=54113 RD a=0300 d=6789 CP-1610 (PC = $5029) t=54113 6789 CF12 CF12 8007 501F 1E87 02f2 502B S-O---i-
Here, you can see the CPU write the value of R0 to location $300 with the first instruction. The next one reads location $300 into R2. The third one reads location $300, and adds its value to R2, leaving the result in R2.
The next two instructions set us up to read and write locations around $300 with indirect accesses:
6789 CF12 CF12 8007 501F 1E87 02f2 502B S-O---i- MVII #$0300,R1 54123 > RD a=502B d=02B9 CP-1610 (PC = $502B) t=54123 RD a=502C d=0300 CP-1610 (PC = $502B) t=54123 6789 0300 CF12 8007 501F 1E87 02f2 502D S-O---i- MOVR R1,R5 54131 > RD a=502D d=008D CP-1610 (PC = $502D) t=54131 6789 0300 CF12 8007 501F 0300 02f2 502E --O---i-
At this point, both R1 and R5 are set up to point to location $300. The only difference between the two is that R1 does not increment with each indirect access, but R5 does.
6789 0300 CF12 8007 501F 0300 02f2 502E --O---i- MVI@ R1,R3 54137 > RD a=502E d=028B CP-1610 (PC = $502E) t=54137 RD a=0300 d=6789 CP-1610 (PC = $502E) t=54137 6789 0300 CF12 6789 501F 0300 02f2 502F --O---i- MVO@ R3,R5 54145 > RD a=502F d=026B CP-1610 (PC = $502F) t=54145 WR a=0300 d=6789 CP-1610 (PC = $502F) t=54145 6789 0300 CF12 6789 501F 0301 02f2 5030 --O-----
Notice how both instructions accessed location $300. Also notice that R1 retained its value, but the CPU incremented R5. This is the main difference between R1-R3 and R4-R5 for indirect accesses.
6789 0300 CF12 6789 501F 0301 02f2 5030 --O----- MVO@ R3,R5 54154 > RD a=5030 d=026B CP-1610 (PC = $5030) t=54154 WR a=0301 d=6789 CP-1610 (PC = $5030) t=54154 6789 0300 CF12 6789 501F 0302 02f2 5031 --O----- MVO@ R3,R5 54163 > RD a=5031 d=026B CP-1610 (PC = $5031) t=54163 WR a=0302 d=6789 CP-1610 (PC = $5031) t=54163 6789 0300 CF12 6789 501F 0303 02f2 5032 --O-----
As you can see, with each "MVO@ R3, R5", the CPU writes the value of R3 out to the address that R5 currently points to, and it also increments R5. So, the first write goes to $300, the next goes to $301, and the third goes to $302. At the end, R5 points to $303.
That brings us to our last two instructions, PSHR and PULR:
6789 0300 CF12 6789 501F 0303 02f2 5032 --O----- PSHR R0 54172 > RD a=5032 d=0270 CP-1610 (PC = $5032) t=54172 WR a=02F2 d=6789 CP-1610 (PC = $5032) t=54172 6789 0300 CF12 6789 501F 0303 02f3 5033 --O----- PULR R4 54181 > RD a=5033 d=02B4 CP-1610 (PC = $5033) t=54181 RD a=02F2 d=6789 CP-1610 (PC = $5033) t=54181 6789 0300 CF12 6789 6789 0303 </B>02f2</B> 5034 --O---i- B $5034 54192 >
The PSHR instruction writes the value of R0 ($6789) to the "top of stack", location $2F2. This is the address held in R6, the stack pointer. It also advances the stack pointer to point to location $2F3.
The PULR instruction reverses the process. First, the CPU decrements R6, and then it reads the value R6 now points to from memory into R4. Notice that we pushed from one register and pulled back into a different one. While "push" and "pull" operations need to be paired so that the stack stays consistent, nothing prevents you from pushing from one register and pulling into another.
Before you quit jzIntv, let's take a peek a memory real quick, by typing "m 2F0" and pressing enter:
> m 2F0 02F0: 5014 106C 6789 1E7D 8007 0002 0000 0007 # P..lg........... 02F8: 02D2 17D0 18F9 0000 8007 3800 5012 0228 # .-.-......8.P... 0300: 6789 6789 6789 0000 0000 0000 0000 0000 # g.g.g........... 0308: 0000 0000 0000 0000 0000 0000 0000 0000 # ................ 0310: 0000 0000 0000 0000 0000 0000 0000 0000 # ................ 0318: 0000 0000 0000 0000 0000 0000 0000 0000 # ................ 0320: 0000 0000 0000 0000 0000 0000 0000 0000 # ................ 0328: 0000 0000 0000 0000 0000 0000 0000 0000 # ................ >
You'll notice that our value $6789 appears at location $2F2 (two words in on the top row), andlocations $300, $301 and $302 (first 3 positions in the third row).
Single Register Arithmetic Instructions
These instructions are fairly simple instructions that operate on a single register:
|TSTR||Check sign, zero on a register|
|CLRR||Set a register to 0||R = 0|
|INCR||Increment a register||R = R + 1|
|DECR||Decrement a register||R = R - 1|
|NEGR||Negate a register||R = -R|
|COMR||1s complement a register||R = R XOR $FFFF|
|ADCR||Add carry bit to register||R = R + C|
These instructions work equally on all 8 registers. That includes the program counter. For instance, "INCR R7" (aka. INCR PC) will skip the next instruction word. That can be useful for skipping single-word instructions. "DECR R7" (aka. DECR PC) is equivalent to "here: B here", but is one byte shorter.
Example: Consider the value of R0 at each step:
CLRR R0 ; Clear R0: R0 = 0 INCR R0 ; Add 1 to R0: R0 = 1 DECR R0 ; Subtract 1 from R0: R0 = 0 COMR R0 ; 1s complement R0: R0 = $FFFF NEGR R0 ; Negate R0: R0 = 1
The ADCR instruction is useful for extended precision arithmetic. The following example shows how to add the 32-bit number in R3:R2 to the 32-bit number in R1:R0. (R3 and R1 hold the upper halves of the 32-bit numbers, whereas R2 and R0 hold the lower halves.)
; Add lower halves together. This generates a carry in 'C' ADDR R2, R0 ; Add carry into upper half of result ADCR R1 ; Now add upper halves together. ADDR R3, R1
Implied Operand Instructions
These instructions don't operate on any register:
|SETC||Set carry flag|
|CLRC||Clear carry flag|
SETC, CLRC and NOP are fairly self explanatory. EIS and DIS are also straightforward, though interrupts are beyond the scope of this tutorial. They are important, and I will cover them in a future tutorial.
Shift and Rotate Instructions
The CP1610 offers a rich variety of shift and rotate instructions. These are useful for bit manipulation and certain types of mathematics. Shifting left by one position is equivalent to multiplying by 2. Shifting right by one position is roughly equivalent to dividing by 2. The shift instructions only operate on R0 through R3. You cannot use them with R4 through R7. Overall, CP-1600 offers 8 separate shift instructions. Each can shift by one or two positions.
Click on a given mnemonic to see how it operates.
|SLL Rx[, 2]||Shift Logical Left|
|SLR Rx[, 2]||Shift Logical Right|
|SAR Rx[, 2]||Shift Arithmetic Right|
|SWAP Rx[, 2]||Swap bytes|
|SLLC Rx[, 2]||SLL into Carry|
|SARC Rx[, 2]||SAR into Carry|
|RLC Rx[, 2]||Rotate Left thru Carry|
|RRC Rx[, 2]||Rotate Right thru Carry|
Logical shifts (SLL, SLR, SLLC) fill the newly-opened positions with zeros. Arithmetic shifts (SAR, SARC) fill the newly opened positions with copies of bit 15. Logical right shifts can be thought of as "unsigned divide by 2", whereas arithmetic right shifts can be thought of as "signed divide by 2, rounding toward negative."
You can combine shift and add instructions to perform simple multiplications. The following example shows how to multiply the value in R0 by 20, using R1 as a temporary variable.
SLL R0, 2 ; Multiply R0 by 4 MOVR R0, R1 ; Save a copy of original value * 4 SLL R0, 2 ; Multiply R0 by 4 again (original value * 16) ADDR R1, R0 ; Add (value * 4) to (value * 16), giving (value * 20)
Right shifts divide by powers of two, but they also truncate the fractional portion. You can combine a right shift with an ADCR to get a rounded result instead:
SARC R0, 1 ; Signed divide by 2. Shifted away bit goes to 'C' ADCR R0 ; Add 1 if shifted away bit was 1. (Round towards positive)
Rotate instructions combined with shift instructions make it easy to shift values longer than 16 bits. The following examples show how to shift the 32 bit number held in R1:R0 left and right by 1 and 2 positions. (R1 holds bits 16..31 of the 32-bit number.)
; Shift R1:R0 left by 1 position SLLC R0, 1 ; Shift lower half left, extra bit to 'C' RLC R1, 1 ; Shift upper half left, pulling lower bit from 'C' ; Shift R1:R0 left by 2 positions SLLC R0, 2 ; Shift lower half left, extra bits to 'C', 'O' RLC R1, 2 ; Shift upper half left, pulling lower bit from 'C', 'O' ; Shift R1:R0 right by 1 position (arithmetic right shift) SARC R1, 1 ; Shift upper half left, extra bit to 'C' RRC R0, 1 ; Shift lower half left, pulling lower bit from 'C' ; Shift R1:R0 left by 2 positions (arithmetic right shift) SARC R1, 2 ; Shift upper half right, extra bits to 'C', 'O' RRC R0, 2 ; Shift lower half right, pulling lower bit from 'C', 'O'