Introducing the Instruction Set Part 1

From Intellivision Wiki
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.

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.

One can think of addresses as being similar to street addresses and ZIP codes and each memory or peripheral as being similar to a city—each city gets its own range of ZIP codes that's separate from any other, and the ZIP code makes sure that mail gets to the right cities. The street address says where in the city the mail gets delivered. Addresses for accesses on the system bus work similarly: Some portion of the address uniquely identifies what item is being addressed, and the rest of the address says what to access within the item.

The CPU interacts with memory by reading and writing specific address 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.

RegisterGeneral PurposeShift InstructionsIndirect PointerReturn Address
R0 X X   
R1 X X X  
R2 X X X  
R3 X X X  
R4 X  Auto-incrementX
R5 X  Auto-incrementX
R6 X  StackX
R7 *  Program Counter 

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:

SSign FlagIf set, indicates that the previous operation resulted in negative value, which is determined by a one (1) in bit 15 of the result.
CCarry FlagIf set, indicates that the previous operation resulted in unsigned integer overflow.
ZZero FlagIf set, indicates that the previous operation resulted in zero.
OOverflow FlagIf set, indicates that the previous operation gave a signed result that is inconsistent with the signs of the source operands. (Signed overflow.)
IInterrupt Enable FlagIf 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.
DDouble Byte Data FlagIf set, it causes the next instruction to read a 16-bit operand with two 8-bit memory accesses, if it supports it.

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 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  
  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 R4

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 02f2 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 at 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), and at locations $300, $301 and $302 (first 3 positions in the third row).

Moving On

At this point, you may wish to continue with the remaining parts of this tutorial:

Or, you can return to the Programming Tutorials index.