Introducing the Instruction Set Part 1

From Intellivision Wiki
Revision as of 06:02, 8 October 2007 by Mr z (talk | contribs) (Implied Operand Instructions)
Jump to: navigation, search

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.

Primary Instructions

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
  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  
  MOVR    MVI    MVI@    MVII  
--  MVO    MVO@    MVOI  
  ADDR    ADD    ADD@    ADDI  
  SUBR    SUB    SUB@    SUBI  
  CMPR    CMP    CMP@    CMPI  
  ANDR    AND    AND@    ANDI  
  XORR    XOR    XOR@    XORI  

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:


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.

Program Listing

        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

        ; 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

Fire up jzintv with "jzintv -d ex1". (See Introducing_jzIntv's_Debugger for a tutorial on the debugger.)) After jzIntv loads, you'll be at the debugger prompt:

    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.

Implied Operand Instructions

These instructions don't operate on any register:

  NOP   No operation
  SETC  Set carry flag
  CLRC  Clear carry flag
  EIS   Enable interrupts
  DIS   Disable interrupts

SETC, CLRC and NOP are fairly self explanatory. EIS and DIS are also straightforward. Interrupts, though, are beyond the scope of this tutorial. They are important, and I will cover them in a future tutorial.

Shift and Rotate Instructions

Branch and Jump Instructions