I have written a PDP-11 assembly language simulator in Javascript. There will be three posts for this project:
Post 1 (this post) gives an outline of the PDP-11 assembly language.
Post 2 contains some example programs for the CRC-16, MD4 Hash and TEA (Tiny Encryption Algorithm) algorithms.
Post 3 covers the actual simulator (written in HTML/Javascript).
The PDP-11
The PDP-11 was a 16-bit mini-computer manufactured by the Digital Equipment Corporation (DEC) during the 70’s and 80’s. The design for the architecture of this machine was one of the most successful and innovative of it’s era. The instruction set for the PDP-11 had a reputation among programmers as being one of the most elegant and well designed assembly languages, and was compact but powerful.
Both the PDP-11 architecture and instruction set introduced several new ideas, which influenced the design of subsequent generations of micro-processors (such as the Intel 386).
The 16 bit word size enabled access to 65,536 bytes of memory (but this was expandable). Memory was usually referenced in terms of 16 bit words, for a total of 32,768 words. The low byte of each word was always at an even memory location, and the high byte at an odd memory location. In other words, the byte ordering was little endian. The top 4096 words in the memory space (from 177777 downwards) were reserved for the system, giving 28,672 words for programming.
The PDP-11 had 8 16-bit registers that could be used by the programmer. They were numbered from 0 to 7. Registers R0 to R5 were general purpose registers. Register R6 (also known as SP) normally functioned as a stack pointer, while register R7 was the program counter (PC).
Also available to the programmer was the 16 bit Processor Status Word (PSW). Only the lower 5 bits of the PSW are relevant to the simulator that I have written. These are the TNZVC bits, being the Trap bit (T) and the Condition Code bits (NZVC). The condition code bits are set by the most recently executed instruction and contain information about the result of that instruction:
N = 1 if the result was negative.
Z = 1 if the result zero.
V = 1 if there was an arithmetic overflow.
C = 1 if there was a carry out of the msb.
Each PDP-11 machine language instruction has a length of 16 bits (1 word) and is always located at an even memory location. The flow of execution of a program is managed by the Program Counter (PC) register. This register stores the 16 bit address of the next instruction that is to be executed. It works like this:
– The current instruction pointed to by the PC is loaded into the CPU.
– The PC is incremented by 2 bytes (or 1 word) to point to the next word in memory.
– The instruction that was just loaded into the CPU is executed.
Note that whenever the PC is used to read a word from memory, it must be incremented by 2 bytes.
References
The PDP 11-40 Processor Handbook can be found at: http://pdos.csail.mit.edu/6.828/2005/readings/pdp11-40.pdf.
This handbook contains the instruction set for the pdp-11. My simulator implements most (but not all) of this instruction set. Refer to the tables below to see which instructions are implemented by my simulator.
The reference manual for the MACRO-11 assembler can be found at:
My simulator only implements a few of the most basic directives and features of the MACRO-11 assembler, as described below.
The PDP-11 Instruction Set
I won’t describe all of the instructions available in the PDP-11 instruction set here. Full descriptions can be found in the PDP 11/40 Processor Handook. My simulator supports most (but not all) of the instructions decribed in this handbook.
One thing to note is that most of the single and double operand instructions have both a byte mode and a word mode. Byte mode effects either a single byte in main memory or the low byte of a (16 bit) register. Word mode effects an entire word in main memory or the entire contents of a register.
The PDP-11 Addressing Modes
The key to understanding the PDP-11 instruction set is in understanding how the addressing modes work. In a PDP-11 instruction, the source and/or destination operands can refer to either a location in main memory or a register. When the instruction is assembled, just 6 bits are set aside to represent these operands. The 3 high bits are used to specify a mode (0-7), and the 3 low bits a register number (0-7). Through this mechanism, an instruction is able to reference any location within the machine’s 16 bit address space.
As illustrated in the table here, an operand references a register, but how the contents of that register are interpreted depends upon the mode. So a register can contain either a data value, an address in main memory, or an address of an address. Apart from mode 0, where the register itself stores the data, the address in main memory that the operand ultimately points to is known as the effective address (EA). The 3 mode bits determine how the value in the register is used to calculate the effective address.
Note that the description in the Action column is for the case of a source operand. In the case of a destination operand, Data will be to the right of the equals sign. For example, Data = Rn (the data is read from the source register) becomes Rn = Data (the data is saved to the destination register).
The Extra Word column indicates for which addressing modes an extra word (following the 16 bit machine code for the instruction) needs to be assembled. This will be explained shortly.
Syntax | Mode | Action | Machine Code | Extra Word |
---|---|---|---|---|
Rn | Register | Data = Rn | 0n | – |
(Rn)+ | Autoincrement | Data = (Rn) Rn++ |
2n | – |
-(Rn) | Autodecrement | Rn– Data = (Rn) |
4n | – |
X(Rn) | Index | Offset address X = (PC) PC += 2 Base address = Rn Data = (Rn + X) |
6n | Yes |
@Rn or (Rn) | Register Deferred | Data = (Rn) | 1n | – |
@(Rn)+ | Autoincrement Deferred | Data =((Rn)) Rn++ |
3n | – |
@-(Rn) | Autodecrement Deferred | Rn– Data =((Rn)) |
5n | – |
@X(Rn) | Index Deferred | Offset address X = (PC) PC += 2 Base address = Rn Data = ((Rn + X)) |
7n | Yes |
#n | Immediate | Data = (PC) = n | 27 | Yes |
@#A | Immediate Deferred (Absolute) | Data = ((PC)) = (A) | 37 | Yes |
A or X(PC) | Relative | Offset address X = (PC) PC += 2 Data = (PC + X) = (A) |
67 | Yes |
@A or @X(PC) | Relative Deferred | Offset address X = (PC) PC += 2 Data = ((PC + X)) = ((A)) |
77 | Yes |
In the above table, if N is an address in memory then (N) is the data stored at the address N.
For the autoincrement and autodecrement modes, the size of the increment/decrement depends on whether the instruction is a byte operation or a word operation. The size is 1 byte for a byte operation, or 2 bytes for a word operation. There are some exceptions to this. The increment/decrement is always 2 bytes for modes 3 and 5, or if the register being used is R6 (the stack pointer SP).
Also note that modes 4 and 5 cannot be used with the PC. The PC can only be incremented, not decremented. The PC always increments by 2 when it is used to read a word from memory e.g. data = (PC).
Register Mode
In register mode, the contents of a register is just a 16 bit data value. For example the operation mov r0 r1 just copies the data in register 0 to register 1.
Register Deferred Mode
In register deferred mode, the contents of the register are read as an address in main memory. Since a register stores 16 bits, any location in memory can be accessed with this mode. Usually the @ character is used to indicate register deferred mode, but the parentheses can also be used.
Some examples:
(i) MOV R0 @R1 The source operand uses register mode, while the destination operand uses register deferred mode. This instruction copies the contents of register 0 to the memory location stored in register 1. So if the value 1234 is stored in R0 and the value 100 is stored in R1, then the instruction will copy the value 1234 into the word at memory address 100. (ii) MOV R0 (R1) This is the same as (i). (iii) ADD @R0 @R1 Both operands use register deferred mode. The instruction adds the value in the memory location pointed to by R0 to the value in the memory location pointed to by R1. Let there be two locations in memory at 100 and 200. Location 100 stores the word 000004, while location 200 stores the word 000003. The result of the add operation will be that the value at location 200 becomes 000007. This can also be written as: ADD (R0) (R1) or even add (r0) (r1) (case only matters for user defined labels and symbols). |
Autoincrement Mode
In autoincrement mode, as for register deferred mode, the contents of the register is an address. The difference is that once the address in the register is read, it is then incremented (by either 1 or 2 bytes, depending on the situation).
(i) movb r0 (r1)+ This operation will copy the low byte of the value stored in r0 to the byte at the memory location pointed to by r1. It will then increment the address stored in r1 by 1 byte. Let r0 hold the value 177777 and r1 the address 100. Execution of the instruction will cause the value 377 to be stored in the byte at location 100. The contents of r1 will then be incremented to 101. (ii) mov r0 (r1)+ Copy the word value stored in r0 to the word at the memory location pointed to by r1. The address in r1 is then incremented by 2 bytes. For the example in (i), the word at 100 becomes 177777 and the address in r1 is incremented to 102. (iii) movb (sp)+ r0 Copy the byte at the memory location pointed to by sp to the low byte of the register r0. Then increment the contents of the register sp. But since the register here is r6 (the stack pointer) the increment will be by 2 bytes, even though the instruction movb is a byte operation. |
Autodecrement Mode
In autodecrement mode, as for autoincrement mode, the contents of the register is an address. The difference is that the address in the register is decremented, and that the decrement occurs before the address is read. The decrement can be either 1 or 2 bytes, depending on the situation. This mode cannot be used with the PC register (which only ever increments).
(i) mov r0 -(r1) Decrement the address stored in r1. Then copy the word in r0 to the (new) address pointed to by r1. Let r0 store the value 1234 and r1 the address 100. The result of this instruction is that the value 1234 is copied to the word in memory at the address 76, because the address 100 has been decremented by 2 bytes to a value of 76. Note that the default number system for the PDP-11 is octal. Thus, 100 - 2 = 76. |
Autoincrement Deferred Mode
In autoincrement deferred mode, the contents of the register is the address of an address. As for the autoincrement mode, the increment occurs after the address in the register has been read. However, unlike for the autoincrement mode, the increment here is always by 2 bytes. This is because the storage for an address value is always 16 bits (and this is what the address in the register is pointing to).
loop: mov r0 @(r1)+ dec r0 bne loop What effect will this code fragment have? Let: r0 = 3 r1 = 100 Let: (100) = 120 (102) = 144 (104) = 164 and: (120) = 0 (144) = 0 (164) = 0 The loop repeats as long as the value in r0 is not zero. Each iteration copies the current value in r0 to the location in memory pointed to by the destination operand. The value in r0 is then decremented by 1 (by the dec instruction). The loop executes 3 times: Iteration 1: r0 = 3, r1 = 100 effective address of the destination operand = (100) = 120 mov --> (120) = 3 increment r1, decrement r0 Iteration 2: r0 = 2, r1 = 102 effective address of the destination operand = (102) = 144 mov --> (144) = 2 increment r1, decrement r0 Iteration 3: r0 = 1, r1 = 104 effective address of the destination operand = (104) = 164 mov --> (164) = 1 increment r1, decrement r0 Final state: r0 = 0, r1 = 106 |
Autodecrement Deferred Mode
In autodecrement deferred mode, the contents of the register is the address of an address. As for the autodecrement mode, the decrement occurs before the address in the register is read. However, unlike for the autodecrement mode, the decrement here is always by 2 bytes. This is because the storage for an address value is always 16 bits (and this is what the address in the register is pointing to).
clear: clr @-(r1) dec r0 bne clear Let: r0 = 3 r1 = 106 As long as r0 is reset to a value of 3, this code fragment will clear the contents of the memory locations 120, 144 and 164 that were set in the previous example. |
Index Mode
In Index Mode, the operand specifies both the register and an offset value X. The value X can be either a number, a symbol, or an expression. Adding X to the value in the register
gives an effective address. This mode can be used to randomly access elements in an array, where the register contains a base address that marks the start of the array, and X is an offset from this base address.
When an instruction that contains extra information in an operand is asembled, the 6 bit address field is not able to store this extra value. Therefore, extra storage needs to be allocated for this information. For the addressing modes that require extra storage, the extra data is stored in memory just after the 16 bit machine code for the assembled instruction. A word of storage is allocated for each piece of extra data. Some examples:
(i) clr 1234(r0) Assembles to: 005060 <-- clr dest: mode=6 R=0 001234 <-- 1234 (ii) mov 66(r0) 24(r1) Assembles to: 016061 <-- mov src: mode=6 R=0 dest: mode=6 R=1 000066 <-- 66 000024 <-- 24 (iii) mov r0 24(r1) Assembles to: 010061 <-- mov src: mode=0 R=0 dest: mode=6 R=1 000024 <-- 24 |
Example usage:
(i) loop: movb r0 pArr(r1) inc r1 dec r0 bne loop Let: r0 = 5 r1 = 0 pArr = 60 The loop will execute 5 times and populate an array of byte values that starts at location 60: (60) = 5 (61) = 4 (62) = 3 (63) = 2 (64) = 1 In this case, X (pArr) is fixed and the array elements are iterated through by incrementing r1. (ii) movb 2(r1) r2 movb 3(r1) r3 movb 7(r1) r4 movb 11(r1) r5 Let: r1 = 60 This code will randomly access the array starting at location 60, and copy the byte values from the array to the low bytes of the specified registers. In this case, r1 is kept fixed while X varies. Note that when MOVB is used to copy a byte to a register, it extends the msb of the byte up through the high byte of the register. |
Index Deferred Mode
Index Deferred Mode works in the same way as Index Mode, except that Rn + X gives the address of an address. As before, X is evaluated as a 16 bit word and allocated storage in a memory word immediately following the assembled instruction.
Example usage:
mov r0 @6(r1) Let: r0 = 1234 r1 = 40 (r1) + X = 40 + 6 = 46 is an address of an address. Let: (46) = 100 The mov instruction will copy 1234 to the word in memory at location 100. (100) = 1234 |
Program Counter Addressing Modes
There are 4 addressing modes that use the PC as the register. These are the immediate, absolute, relative and relative deferred modes.
Immediate Mode
The syntax for the operand in immediate mode is #N. When the instruction is assembled, the value N will be assembled as an extra word. N can be a number, user defined symbol or expression. Immediate mode allows a numerical value to be loaded directly into a register or memory location. For example:
mov #1234 r1 This instruction will store the value 1234 in register 1. It assembles as: 012701 <-- mov src: mode=2 R=7 dest: mode=0 R=1 001234 <-- 1234 What happens when this instruction is executed? - The MOV instruction is loaded into the CPU. - The PC increments and is now pointing to the location in memory that is storing the value 1234. - The MOV instruction is executed. This causes the address in the PC to be used to read the value 1234. - As a result the PC increments again. - The value 1234 is copied into register 1. |
Immediate mode is equivalent to autoincrement mode, but with the PC as the register. The address in the PC is used to read a value and then the PC is incremented (by 2 bytes).
Most of the time it won’t make sense to use immediate mode for a destination operand. But what happens if you do?
Consider the example: CLR #1234 This assembles to: 005027 001234 After the CLR instruction is executed, the same 2 words in memory look like this: 005027 000000 |
In a PDP-11 assembly language instruction, the source and destination operands point to storage in memory (which is either a register or a location in main memory). So where does the #1234 operand point to? It points to the location in memory immediately following the machine code for the CLR operation (005027), where the value specified in the operand (1234) has been stored. So the result of the CLR operation is that this location in memory is cleared.
Immediate Deferred Mode
Immediate Deferred Mode is also known as absolute mode. The syntax for the operand is @#A. A can be a number, user defined symbol or expression. The value A is assembled into memory as an extra word following the machine code for the instruction. When the instruction is executed, A is interpreted as an address.
Immediate deferred mode is equivalent to autoincrement deferred mode, but with the PC as the register. The address pointed to by the PC is the location that stores the address value A i.e. the PC contains the address of an address.
mov #1234 @#100 In this instruction, the 1st operand uses immediate mode and the 2nd operand uses immediate deferred mode. When executed, the operation will copy the value 1234 into the memory location 100. If this instruction is assembled at location 40 in memory, it will look like this: 000040 012737 <-- mov src: mode=2 R=7 dest: mode=3 R=7 000042 001234 <-- the value 1234 000044 000100 <-- the address 100 The instruction executes as follows: - The instruction is loaded into the CPU. - The PC increments and now points to the location 42 that holds the value 1234 (i.e. addressing mode 2). - The PC is used to read the value 1234 from memory, and therefore increments. - The PC now points to the location 44 that holds the address 100 (i.e. addressing mode 3). - The PC is used to read the address 100 from memory, and therefore increments. - The instruction has completed execution and the PC is left pointing to the location 46, containing the next instruction. |
Relative Mode
The syntax for the operand is just A, where A can be a number, user defined symbol, or expression. The operand A represents an address in memory.
Relative mode causes an extra word to be asembled in memory, but A is not the value stored in this extra word. Instead a value X is calculated, which is the offset of the address A relative to the location being pointed to by the PC. But where is the PC pointing to at the time the offset X is used to calculate A?
Consider the examples:
(i) CLR 10 When assembled at location 0, it looks like this: 000000 005067 <-- clr dest: mode=6 R=7 000002 000004 <-- X = 4 The instruction executes as follows: - The instruction is loaded into the CPU and the PC increments to 2. - The PC is used to read the value X and increments to 4. - At this point the address A is calculated: A = PC + X = 4 + 4 = 10 (in octal). So, as required, the memory at location 10 is cleared. (ii) CLR 20 If this is assembled at location 40, it looks like this: 000040 005067 <-- clr dest: mode=6 R=7 000042 177754 <-- X = -24 In this case, the offset X is negative. At the point the address A is calculated, the value X has just been loaded and the PC incremented such that it is pointing to the location 44. A = PC + X = 44 + (-24) = 20 (in octal). |
Relative Deferred Mode
The syntax for the operand is @A, where A can be a number, user defined symbol, or expression. The operand A is the address of an address in memory.
As for Relative mode, an extra word is assembled in memory containing the value X (where X is the offset of the address A relative to the location being pointed to by the PC). When the instruction is executed, A is calculated in exactly the same way as for relative mode.
CLR @10 This assembles to: 000000 005077 <-- clr dest: mode=7 R=7 000002 000004 <-- X = 4 When executed: A = PC + X = 4 + 4 = 10 (in octal). Let the location 10 contain the value 100. This means that the effective address of the operand is 100. Therefore, the instruction will clear the contents of memory location 100. |
Macro-11 Symbols
Any line in the PDP-11 assembly language source code can include a label field. A label must begin with a letter, and can only contain letters and numbers. On the first pass of the compiler, the label is placed into the user defined symbol table. Only the first 6 characters of a label are recognised by the compiler and this is what will be placed into the symbol table. The value assigned to the label is the address of the CLC (current location counter) of the line that contains the label. Once stored in the symbol table, a label can be used as part of an expression anywhere in the program. A label is terminated by the colon character ‘:’. For example:
A label should be used to mark the beginning of a loop: loop: dec r0 bne loop The symbol 'loop' can then be used as an operand to the bne instruction. Multiple labels can appear on one line: A: B: C: mov r0 r1 Or alternatively: A: B: C: mov r0 r1 In this example each label is assigned the address of the mov instruction. Labels by themselves do not increment the value of the CLC. |
Symbols or labels can only be defined once in the source code.
Macro-11 Directives
My simulator supports only a few of the Macro-11 directives:
.odd Add 1 if the CLC is even. Do nothing if the CLC is already odd. .even Add 1 if the CLC is odd. Do nothing if the CLC is already even. .blkb Usage: .blkb exp - where exp can be an expression, number or symbol. Increment the CLC by exp bytes, to reserve a block of bytes in memory. .blkw Usage: .blkw exp - where exp can be an expression, number or symbol. Increment the CLC by exp words, to reserve a block of words in memory. .byte Usage: .byte exp1, exp2, ... This directive can have multiple arguments, where each argument exp can be an expression, number or symbol. Each exp evaluates to an 8 bit value which is stored into memory at the address of the CLC. The CLC is incremented by 1 byte for each argument. If there are no arguments, a single zero byte is placed into memory. .word Usage: .word exp1, exp2, ... This directive can have multiple arguments, where each argument exp can be an expression, number or symbol. Each exp evaluates to a 16 bit value which is stored into memory at the address of the CLC. The CLC is incremented by 1 word for each argument. If there are no arguments, a single zero word is placed into memory. .ascii Usage: .ascii "text string" Store a text string in memory, as a sequence of ascii values. Two 8 bit ascii values will be stored in each word of memory. The string is enclosed in double quotes. Double quotes cannot be used within a string, but single quotes can. The string can only contain printable characters. My implementation of the .ascii directive is very different from how it was done in Macro-11. .end Usage: .end exp This directive tells the compiler to stop. Optionally, it also sets the entry point for the execution of the code. If the argument exp is included, it will be evaluated as a 16 bit value. The PC will then be set to the value of exp, which can be an expression, number or symbol. If exp is not present, the PC is just set to 0. |
I have also added my own directive:
.hex Usage: .hex h1, h2, ... This directive can have multiple arguments, where each argument h has to be a number in hexadecimal format that will evaluate to a 16 bit value which is stored into memory at the address of the CLC. The CLC is incremented by 1 word for each argument. If there are no arguments, a single zero word is placed into memory. |
Macro-11 Expressions
My simulator supports most of the Macro-11 approach to the evaluation of assignments and expressions, with a few differences.
The operators that can be used in an expression are:
addition: + subtraction: - multiplication: * division: / AND: & OR: ! |
There are no operator precedence rules. An expression is just evaluated from left to right.
Brackets can be used in an expression : < and >. Parentheses cannot be used in an expression, because they are already reserved for use within instruction operands to indicate register modes. Any part of the expression that is enclosed in brackets is evaluated first. If there are multiple brackets, then the inner brackets will be evaluated first.
Some examples: (i) 2 + 4 * 7 / 6 & 4 ! 7 = 6 * 7 / 6 & 4 ! 7 = 42 / 6 & 4 ! 7 = 7 & 4 ! 7 = 4 ! 7 = 7 (ii) < 2 + 4 > * < < 6 / 3 > & < 4 ! 7 > > = 6 * < < 6 / 3 > & < 4 ! 7 > > = 6 * < 2 & 7 > = 6 * 2 = 14 (in octal) (iii) Expressions can also include user defined symbols: A * B + C / < < E / F > - 2 * 7 > |
A user defined symbol can be created via an assignment. For example:
(i) A = 2 * 3 - 7 B = A + 5 (ii) Create multiple symbols and set them to the same value: A = B = C = 5 * 6 - D |
An assignment is evaluated during the first pass of the compiler. In this case, any symbol or label used within an expression that is on the right hand side of an equals operator must have already been defined on a preceding line of code.
All other expressions (those that appear as part of an operand for an instruction) will be evaluated during the second pass of the compiler.
Decimal And Binary Numbers
By default, any number appearing in the PDP-11 assembly language code is interpreted as an octal number. Macro-11 has the .radix directive and the ^O, ^D, ^B operators to change the number base, but my simulator does not support any of these features.
Instead, a decimal number can be specified in my simulator by appending either a ‘d’ or a dot ‘.’ to the end of a number. A binary number can be specified by appending a ‘b’ to the end of a binary value. For example:
A = 56789d B = 89065. C = 1001110110b 5 * 3 + 7 * 89d + 101b * 8948. |
The CLC Symbol
The dot ‘.’ is the symbol for the current location counter (except when it is appended to the end of a number). It can be used in an expression, in which case the current value of the CLC will be substituted. An assignment can be used to change the value of the CLC, however it cannot be used in a multiple assignment statement. Some examples:
(i) . = 500 (ii) A = . + 4 * 12 (iii) . = . + 6 (iv) mov .+10 r0 (v) mov #. . (vi) Cannot do this: A = B = . = C = 4 * 5 |
The Branch Instructions
The branch instructions have the format: bxx dest. These instructions test the condition code bits (N, Z, V and C) to determine whether to branch to the specified location. If a branch occurs, the PC will be set to the effective address of the destination operand.
The limitation of the branch instructions is that dest must be within -128 to +127 words of the current PC. This is because the branch instructions do not use the addressing modes as described above (the operand is just a symbol or number). Instead, 8 bits within the instruction machine code are set aside to specify the offset from the current location in words.
The table below shows how each instruction tests the NZVC bits.
Branch | NZVC |
---|---|
br | – |
bne | Z == 0 |
beq | Z == 1 |
bpl | N == 0 |
bmi | N == 1 |
bvc | V == 0 |
bvs | V == 1 |
bcc | C == 0 |
bcs | C == 1 |
Branch – Signed Conditional | NZVC |
bge | N ^ V == 0 |
blt | N ^ V == 1 |
bgt | Z | (N ^ V) == 0 |
ble | Z | (N ^ V) == 1 |
Branch – Unsigned Conditional | NZVC |
bhi | C == 0 && Z == 0 |
blos | C | Z == 1 |
bhis | C == 0 |
blo | C == 1 |
Most of the PDP-11 instructions set the NZVC bits depending on what the outcome of the instruction was. The table below shows how. Note that src and dst refer to the state of the operands before the operation is executed, while result refers to the state of the dst operand after completion of the operation.
Unless otherwise specified in the table, most of the operations below will set the N bit to 1 if the result is negative, or the Z bit to 1 if the result is zero.
Single Op | NZVC |
---|---|
clr | N = V = C = 0, Z = 1 |
com | V = 0, C = 1 |
inc | V = (dst == 077777) C = C |
dec | V = (dst == 0100000) C = C |
neg | V = (result == 0100000) C = (result != 0) |
tst | V = C = 0 |
asr | V = N ^ C C = dst & 1 |
asl | V = N ^ C C = dst & 0100000 |
ror | V = N ^ C C = dst & 1 |
rol | V = N ^ C C = dst & 0100000 |
swab | V = C = 0 |
adc | V = (dst == 077777) & (C == 1) C = (dst == 0177777) & (C == 1) |
sbc | V = (dst == 0100000) C = !(dst == 0 && C == 1) |
sxt | N = N Z = (N == 0) V = V C = C |
Double Op | NZVC |
mov | V = 0 C = C |
cmp | V = msb(src ^ dst) && !msb(dst ^ result) C = !(result & 0200000) |
add | V = !msb(src ^ dst) && msb(src ^ result) C = (result & 0200000) |
sub | V = msb(src ^ dst) && !msb(src ^ result) C = !(result & 0200000) |
bit | V = 0 C = C |
bic | V = 0 C = C |
bis | V = 0 C = C |
Double Op – Register | NZVC |
ash | V = msb(R ^ R’) C = last bit out of R |
ashc | V = msb(R,R+1 ^ (R,R+1)’) C = last bit out of R,R+1 |
xor | V = 0 C = C |
The condition code bits can also be set or cleared by the programmer, by using the instructions: sen, sez, sev, sec, scc, cln, clz, clv,clc, ccc. These instructions have no operands.
A couple of the instructions in the table above can be used to directly test and compare operands, and set the NZVC bits accordingly:
(i) The TST(B) instruction: tst 177777 --> NZVC = 1000 tst 77777 --> NZVC = 0000 tst 0 --> NZVC = 0100 tstb 377 --> NZVC = 1000 (ii) The CMP(B) instruction: Tests src and dst by calculating result = (src - dest) = src + ~dest + 1. Let: src = 177777 dst = 100000 result = 177777 + 77777 + 1 = 277777 cmp #177777 #100000 --> NZVC = 0001 |
Loops
Loops can be easily constructed by using the INC(B) or DEC(B) instructions:
(i) mov #10 r5 Loop: .... dec r5 bne Loop ; branch as long as r5 is not zero. (ii) mov #-10 r5 Loop: .... inc r5 bne Loop ; branch as long as r5 is not zero. |
Another instruction available for loop control is SOB. This has the syntax: SOB Rn dest. When this instruction executes the contents of the register Rn are decremented by 1. If Rn is not equal to 0, program control is transfered to the destination address.
The regular addressing modes are not used for the destination operand here. It is just a symbol or number that represents an address. When the SOB instruction is assembled, the offset of this address (in words) relative to the PC is calculated. In the machine code for the instruction, 6 bits are set aside for this offset, which is interpreted as a positive number. When executed, twice this offset is subtracted from the PC to get the destination address. Therefore, the destination must always be less than the PC and must also be within 63 words (126 bytes).
Stacks
A stack is a last in, first out data structure. Only 2 operations can be performed on a stack, push and pop. Push adds a data element to the stack, while pop removes a data element. A stack needs a stack pointer, which points to the most recent element pushed onto the stack. In the PDP-11 a stack is just an area in memory, with a register acting as the stack pointer. Any register apart from the PC (register 7) can be used as a stack pointer. On the PDP-11 a stack grows downward in memory.
The PDP-11 has its own processor stack. The register used for this stack is R6. Therefore R6 is also be referred to as SP (stack pointer). In my program, only the JSR and RTS instructions make use of the processor stack through the SP register. By default in my program, when the first word is pushed onto the processor stack it will be stored at memory location 177776, and the stack will grow downward in memory from there. Of course, the user can set the value of SP to any memory location they desire before using it.
The processor stack always contains words, therefore SP can only be incremented or decremented by 2 bytes. The byte instructions can be used with the processor stack, but they will only operate on the lower byte of each word in the stack.
Other stacks however (using registers 0 to 5) can contain either words or bytes. They can be placed anywhere in memory, and multiple stacks can be maintained at any one time (using one register per stack).
The autoincrement, autodecrement and index register modes are used to respectively pop, push and randomly access data bytes or words belonging to a stack on the PDP-11. Some examples:
(i) ; use r5 as the stack pointer mov #170 r5 ; build the stack mov #1 -(r5) mov #2 -(r5) mov #3 -(r5) ; access the top of the stack mov (r5) r0 ; randomly access a stack element mov 2(r5) r1 ; pop data from the top of the stack mov (r5)+ r2 ; reset the stack pointer add #4 r5 halt (ii) Two stacks: ; use r4 and r5 as stack pointers mov #170 r4 mov #220 r5 ; build the r4 stack mov #1 -(r4) mov #2 -(r4) mov #3 -(r4) ; copy to build the r5 stack mov 4(r4) -(r5) mov 2(r4) -(r5) mov 0(r4) -(r5) halt (iii) A byte stack: mov #170 r5 ; build the stack movb #1 -(r5) movb #2 -(r5) movb #3 -(r5) movb #4 -(r5) ; the top of the stack movb (r5) r0 ; random access movb 3(r5) r1 halt |
Jumps and Subroutines
The unconditional jump instruction is: JMP dest. Unlike the branch instructions, JMP can transfer program control to any location in memory. However, address mode 0 (register mode) cannot be used for the destination operand here, because there is no way to calculate an effective address.
The instructions for jumping to and returning from subroutines are: JSR and RTS.
The usage for the JSR instruction is: JSR Rn dest. When this instruction is executed, 3 things happen:
(i) The contents of the register Rn are pushed onto the processor stack. (ii) The contents of the PC register are copied to Rn. (iii) The effective address pointed to by the destination operand is loaded into the PC. |
Execution of the program will continue from the new location pointed to by the PC until a RTS instruction is encountered. The usage for the RTS instruction is: RTS Rn. When this instruction is executed:
(i) The contents of Rn are copied to the PC. (ii) The top of the processor stack is popped into the register Rn. |
In other words, the original contents of the PC and Rn are restored and the program resumes execution from the next instruction after the original JSR call. Usually the register Rn will be the same for both the JSR and RTS instructions. This register is known as the Linkage Pointer. Some examples:
(i) Without arguments: mov #1 r0 mov #2 r1 jsr r5 SUB_0 clr r0 clr r1 halt .=40 SUB_0: add r0 r2 add r1 r2 rts r5 (ii) With arguments: In this example there are 3 arguments, that are stored in the 3 words following the JSR instruction. Just before the JSR instruction is executed the PC is incremented as usual, and therefore points to the first argument. Execution of JSR results in the PC being copied to r5. As a result, the arguments can be accessed inside the subroutine SUB_1 through the linkage register (r5). SUB_1 just uses r5 to load the arguments into the registers 0, 1 and 2. When the SUB_1 subroutine has finished executing, the register r5 should be left pointing to the instruction following the last argument, so that the RTS instruction will return program execution to the right place. jsr r5 SUB_1 arg1: .word 10 arg2: .word 20 arg3: .word 30 clr r0 clr r1 clr r2 halt .=60 SUB_1: mov (r5)+ r0 mov (r5)+ r1 mov (r5)+ r2 rts r5 If random access to the arguments is required inside the subroutine SUB_0, index mode can be used on the linkage register r5. In this case the add instruction is used just before the RTS instruction, to make sure that program execution returns to the right place. jsr r5 SUB_1 arg1: .word 10 arg2: .word 20 arg3: .word 30 clr r0 clr r1 clr r2 halt .=60 SUB_1: mov (r5) r0 mov 2(r5) r1 mov 4(r5) r2 add #6 r5 rts r5 (iii) PC as the linkage pointer: It is alright to use the PC as the linkage register. The value of the PC is pushed onto the processor stack when JSR is executed, and restored (popped off the stack) when RTS is called. mov #1 r0 mov #2 r1 jsr pc SUB_0 clr r0 clr r1 halt .=40 SUB_0: add r0 r2 add r1 r2 rts pc (iv) Passing arguments via the processor stack: If the PC is also the linkage register, arguments can still be passed by using the processor stack. They must be added to the processor stack before the JSR instruction is executed. Inside the SUB_0 subroutine, the arguments can then be randomly accessed using index mode on the SP register. Just remember that the PC value needed to restore program execution is also on the processor stack. mov arg1 -(sp) mov arg2 -(sp) mov arg3 -(sp) jsr r7 SUB_1 clr r0 clr r1 clr r2 add #6 sp halt .= 40 SUB_1: mov 6(sp) r0 mov 4(sp) r1 mov 2(sp) r2 rts r7 .=100 arg1: .word 10 arg2: .word 20 arg3: .word 30 Mixing data and instructions, as in (ii) above, is undesirable. Passing arguments via the stack avoids this. If the arguments do not need to be on the stack after the subroutine returns, just use the add instruction to adjust the stack pointer (sp). |
Function Pointers
Function pointers are easy to implement, as the following example shows:
mov #fn_A F_ptr jsr r7 @F_ptr mov #fn_B F_ptr jsr r7 @F_ptr mov #fn_C F_ptr jsr r7 @F_ptr halt ; function A fn_A: mov #-1 r0 rts r7 ; function B fn_B: mov #-1 r1 rts r7 ; function C fn_C: mov #-1 r2 rts r7 ; the function pointer F_ptr: .word .end 0 |
Recursion
Through the use of JSR and RTS, recursion is possible, as shown in the example:
; compute Total = 1 + 2 + ... + N ; initialise r0 to N mov N r0 ; call the subroutine jsr r5 SUM ; save the result mov r1 Total halt .=40 ; the subroutine SUM: ; if r0 is 0, stop tst r0 beq stop ; add r0 and then decrement add r0 r1 dec r0 ; call the subroutine again jsr r5 SUM stop: rts r5 ; storage for N and Total N: .word 5 Total: .word 0 .end 0 |
Bitwise AND
A bitwise AND operation that leaves the result in the destination location is not available in the PDP-11 instruction set. There is the BIT (bit test) instruction that uses the bitwise AND operation to test a pair of operands, but this instruction only modifies the condition code bits. However, a bitwise AND operation can be constructed by using the bitwise complement (COM) and bit set (BIS) instructions. Note that the BIS instruction is just a bitwise OR. The following identity expresses the bitwise AND in terms of the bitwise complement and OR operations:
A & B = ~(~A | ~B)
The following code demonstrates that this identity is correct:
; 0 and 0 clr r0 clr r1 jsr r7 AND mov r1 AND_00 ; 0 and 1 clr r0 mov #1 r1 jsr r7 AND mov r1 AND_01 ; 1 and 0 mov #1 r0 clr r1 jsr r7 AND mov r1 AND_10 ; 1 and 1 mov #1 r0 mov #1 r1 jsr r7 AND mov r1 AND_11 halt AND: ; Bitwise AND r0 and r1 com r0 com r1 bis r0 r1 com r1 rts r7 ; truth table AND_00: .word AND_01: .word AND_10: .word AND_11: .word .end 0 |