Arithmetic Instructions

This is where most of the work of a program is accomplished. But before the actual functions and syntax is covered, we need understand how the CPU handles these instructions. Most importantly, Arithmetic functions not only modify AC’s, they can also test the results of a computation or the contents of an AC and skip the next instruction if certain criteria are met. This is what makes the Nova instruction set so powerful and compact.

Every arithmetic instruction involves a Source AC (ACS in the docs) and a Destination AC (ACD in the docs). For some instructions both the ACS and ACD are involved in the computation (e.g. ADD), while in other instructions only the ACS contributes to the “computation”. But either way, the result is sent to the ACD (unless explicitly prevented). In the syntax, the AC’s are simply separated by a comma, as shown below, with ACS first and ACD last.

The eight actual instructions available and their 3-letter syntax are listed here with a couple of simple examples. Additional modifiers will be shown later.

  • NEG  ; Negate: perform a two’s complement on ACS and store the result in ACD
    ( This is what we usually think of when we negate a value. Treats ACS as a signed integer )
NEG 1,3  ; Store the 2's complement of (AC1) in AC3
NEG 1,1  ; Perform 2's complement on (AC1) store in AC1
  • COM  ; Complement: perform a one’s complement on ACS and store the result in ACD
    ( Toggles all the bits in ACS value. Zeros become Ones, and Ones become Zeros )
    If the value in ACS is considered a signed integer, then the result is one off from a “negate” command. For example complementing +5 will yield -6, and complementing -6 will yield +5. To put this another way, a number plus its complement will equal minus one, while a number plus its negated value will equal zero.
COM 1,2  ; complement (AC1) and store result in AC2
COM 1,1  ; complement (AC1) and store back in AC1
  • MOV  ; Move: copy ACS to ACD
MOV 0,3  ; copy contents of AC0 to AC3
MOV 1,1  ; copy (AC1) back into AC1. Effectively a no-op.
  • INC  ; Increment: add one to ACS and store the result in ACD
INC 1,2  ; increment (AC1), store in AC2
INC 3,3  ; incr (AC3), store in AC3
; this is a great way to use AC3 as an index in a list
  • ADC  ; Add Complement: add the one’s complement of ACS to ACD
ADC 1,0  ; Add one's complement of (AC1) to (AC0)
ADC 2,2  ; Add one's complement to self (always results in 177777)
  • SUB  ; Subtract: subtract ACS from ACD.
SUB 1,3  ; subtract (AC1) from (AC3)
SUB 2,2  ; subtract (AC2) from itself (always results in zero)
  • ADD  ; Add: add ACS to ACD
ADD 3,2  ; add (AC3) to (AC2)
ADD 1,1  ; add (AC1) to itself. Effectively multiplies by 2.
  • AND ; perform a logical bit-wise AND on the two AC’s and store the result in ACD
AND 1,3  ; do logical AND of (AC1) and (AC3), store in AC3.
LDA 1,C377  ; load constant
AND 1,2     ; preserve bits 0-7, zero the rest

 

Carry Bit

In addition to the four accumulators of 16 bits each, there is a register called ‘Carry’ which holds just 1 bit. Since it is a single-bit register, the only two values it can hold are zero and one.

Carry should be thought of as a 17th highest-order bit that works in conjunction with the 16 bit AC’s. Numbering the bits in decimal, bit 0 (zero) is the lowest value bit in the AC, bit 15 is the highest order bit, and bit 16 is Carry. The highest possible octal value in an AC is 177777. But during a computation, the highest possible result is 377777, a 17 bit value.

Carry provides a number of valuable features. For example, if you add two AC’s together, the binary result may not fit in 16 bits, but will never be more than 17 bits. Carry acts like a 17th bit in the addition. If the addition overflows to 17 bits, that extra bit is added to carry which effectively causes it to complement (if Carry was at zero, adding an overflow bit makes it one. If Carry was already one, adding an overflow bit makes it zero).

This can be very helpful, because the results of arithmetic instructions can be tested for zero or non-zero. Moreover, the resulting ACD can be tested independently of the Carry bit, and Carry can be tested independent of ACD. More on testing results later.

One’s Complement vs Two’s Complement Arithmetic

Assume that for a given word, the high order bit indicates whether it is a positive or negative value. That is, values 0-77777 are positive, while 100000-177777 are negative (and 177777 is considered to be  minus 1). The two’s complement of that value is what we normally think of as negating, and is accomplished by the NEG command. So if AC2 = 12, then NEG 2,2 yields 177766, or minus 12. The one’s complement is more of a bit-complementing function where all zero’s are set to one and all one’s are set to zero, and is done with the COM command. If AC2 = 12, then COM 2,2 yields 177765. Notice that if you add the complement of a number to itself, it always yields 177777. Thus, ADC 2,2 results in AC2 = 177777.

Internally in the CPU, subtraction is actually accomplished by adding the two’s complement of the ACS to the value in ACD. So if AC1 = 40 and AC2 = 50, then SUB 1,2 adds the two’s complement of 40 to 50 and stores the result in AC2. Note that the two’s complement of 40 is 177740. Adding that to 50 results in a 17 bit result of 200010. The 17th bit will be added to carry causing it to complement. To put this another way, when using the SUB command, carry will always be complemented if ACS <= ACD. It will not complement if ACS > ACD. On the other hand, when using ADC (add one’s complement), carry will complement if ACS < ACD, and it will not compliment if ACS >= ACD. Given that we can then test the Carry bit for zero or one (see below) these commands give us the ability to do all four tests of … greater-than, less-than, greater-than-or-equal, less-than-or-equal.

Other Capabilities of Arithmetic Instructions

While performing any of the above eight arithmetic functions, the Nova also handles 4 optional features which can greatly enhance the power of these instructions.

Carry‘ : Determine what value will be supplied to the computation as bit 17 of the source value.

The Carry register retains its value until changed, just like AC registers. Carry participates in all arithmetic instructions as bit 17 of the computation. However, the programmer has four options regarding how Carry is used.

  • (no special syntax) Use the current value of Carry Register for bit 17.
  • (Z) Use zero for bit 17 (ignore the Carry register).
  • (O) Use one for bit 17 (ignore the Carry register).
  • (C) Use the complement of the carry register for bit 17.

For syntax, if any of the last 3 options are used, the corresponding letter is appended to the Command.

ADDZ 1,2  ; set carry to zero and add (AC1) to (AC2) 
; if the result > 177777, carry will predictably be set to 1.
MOVO 0,0  ; since AC0 is both ACS and ACD, the AC does not chg
;           but Carry will be set to 1
SUBC 1,1  ; provide the complement of Carry to the subtraction.
; in this case, the subtraction will complement carry again due to overflow
;        so the resulting carry will be the same as it was before the SUB command.

 

‘Shift’ : rotate result or swap bytes

After the computation is complete, the results can be shifted right or left. Carry participates in this rotation. Shifting ‘Right’ causes the resulting Carry bit to shift into bit 15, and bit 0 of the result to shift into Carry. Shifting  ‘Left’ causes Carry to shift into bit 0, and bit 15 of the result to shift ‘left’ into the Carry bit. The following example is a LEFT shift.

carry  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
  x    | a | b | c | d | e | f | g | h | j | k | m | n | p | q | r | s |
       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

carry  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
  a    | b | c | d | e | f | g | h | j | k | m | n | p | q | r | s | x |
       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

In the syntax, the shift character ( ‘L’ or ‘R’ ) is entered following the Carry code, if any.

INCZR 1,1  ; increment (AC1) and then shift right, 
;  effectively this means add 1 and divide by 2. 
;  Carry will then be zero if the result of INC was even, 
;  or one if the result was odd.
; C was forced to zero before the INC to prevent 
;    shifting in a '1' bit into the high order bit of ACD.

MOVZL 1,2 ; shift (AC1) 1 bit left, zero C coming in as bit 0
; that effectively stores (AC1)*2 in AC2. If followed by
ADD 1,2  ; now (AC2) is the original (AC1)*3.

MOVL 1,1  ; move high order bit of (AC1) into carry
MOVL 2,2  ; move that carry bit into low order bit of (AC2)
; (of course, other things are happening to the AC's)

Instead of a shift left or right, the resulting 16 bit word can do a byte swap. The upper 8 bits and the lower 8 bits swap positions.

177400  ; constant, which acts as a masks for upper 8 bits
LDA 1,.-1  ; get mask
ANDS 1,0   ; do an AND with upper 8 bits and swap to lower half
; this effectively shifted the left byte into the right half
; and cleared out the left half.

Skip‘ : Test for Zero or Non-Zero Results

After the computation and rotation is complete, the resulting Carry bit and 16 bit value can be tested for zero or non-zero. If the test results in a true, then the next instruction is skipped. The following examples are all functionally no-op instructions (except for the test and possible skip) which show the syntax for various possible tests.

MOV 1,1,SZR ; skip next instr if AC1 is a zero result
MOV 1,1,SNR ; skip if AC1 is a non-zero result
MOV 1,1,SZC ; skip if zero carry result
MOV 1,1,SNC ; skip if non-zero carry result
MOV 1,1,SEZ ; skip if either carry or AC1 result is zero
MOV 1,1,SBN ; skip if both carry and AC1 results are non-zero
MOV 1,1,SKP ; always skip next instruction

In practice, the skip is usually part of a more meaningful test. For example:

LDA 1,C215  ; get carriage return constant from page zero
SUB 0,1,SZR  ; skip next instruction if AC0 was a 215
JMP <addr>   ; character was not a 215
<any>        ; yes it was 215, process end of line

; using generic symbols ... 
SUB ACS,ACD,SZR  ; in general terms, test for ACS = ACD 
SUB ACS,ACD,SNR  ; in general terms, test for not-equal
SUBZ ACS,ACD,SNC ; skip if ACS <= ACD (see why below)
; a lot more possibilities exist for comparisons, see below

 

No Load‘ : Prevent storing result in ACD

It would be nice if a lot of those above comparisons could be made without destroying the contents of ACD. So there is a ‘No-Load’ option that can be appended to any arithmetic command, which allows all of the processing to occur except when completed, the Carry and ACD are left unchanged. This is done by appending a pound sign (hash tag) to the command.

SUBZ# 1,2,SZC  ; skip if (AC1) > (AC2), do not change anything
INC# 1,1,SZR   ; skip if (AC1) = 177777, but no change

; suppose if a value is negative (high order bit set)
; we want to negate it (make it positive)
; otherwise if it is positive we want to increment it
MOVL# 1,1,SZC  ; is it negative?
NEG 1,1,SKP    ; yes, make it positive, and skip next instr
INC 1,1        ; increment the postive value
; ( shows one way to use an unconditional skip )

 

Summary

That is probably an overload at first take. It gets clearer with time. This may also be a little clearer by walking through the actual sequence of events that take place inside the arithmetic unit.

How the Arithmetic Unit processes instructions

The Nova CPU has a special subsystem (arithmetic unit) in the hardware that handles all arithmetic instructions. The unit processes every arithmetic instruction within a fixed set of steps.

  1. CPU sends the instruction along with the contents of ACS and ACD and Carry to the arithmetic unit, and then increments the Program Counter (PC) to the next instruction.
  2. The unit determines which of 4 possible values to use for Carry in the following calculation.
  3. Depending on the requested function (ADD, COM, etc) the Unit uses the two AC’s and the Carry to produce a single 17-bit result.
  4. If a shift Left or Right is requested, the unit rotates the 17 bits accordingly (see above). If instead a byte swap is requested, the unit swaps bits 0-7 with bits 8-15.
  5. If a Skip test is part of the instruction, the unit performs the test on the 17 bit result, and increments the Program Counter (PC) if needed. This effectively skips the instruction that would have normally followed.
  6. If a No-Load was indicated, then the unit is done. Otherwise, it stores the highest order (17th) bit in the Carry register and the other 16 bits in ACD.

Program Example

Here is a subroutine that will move any number of words from one place to another in memory, purging out any zero values in the process. The calling routine sets AC0 = # words, AC1 = source address, AC2 = destination address, and does a JSR to the ENTRY instruction (the JSR sets AC3 = return address).

RTNX:  0
ENTRY: STA 3,.-1    ; save return address
       MOV 1,3      ; move source to an index AC
       NEG 0,0      ; set # words to a negative value
LOOP:  LDA 1,0,3    ; get a word from source
       MOV# 1,1,SNR ; is it zero?
       JMP NONMV    ; yes, skip over dest stuff
       STA 1,0,2    ; store in destination
       INC 2,2      ; increment dest
NONMV: INC 3,3      ; increment source
       INC 0,0,SZR  ; have we checked all the words?
       JMP LOOP     ; no, get the next one
       JMP @RTNX    ; yes, return to the caller

Special Tests

Putting this all together, it is now possible to standardize the methods of comparing 2 AC’s without changing any registers.

SUBZ# 1,2,SNC  ; skip if AC1 <= AC2
SUBZ# 1,2,SZC  ; skip if AC1 > AC2
ADCZ# 1,2,SNC  ; skip if AC1 < AC2
ADCZ# 1,2,SZC  ; skip if AC1 >= AC2

; the assembler designers were nice enough to give us shortcut
; syntax for all the above items, and more.
SLE 1,2  ; same as SUBZ# 1,2,SNC
SGR 1,2  ; same as SUBZ# 1,2,SZC
SLS 1,2  ; same as ADCZ# 1,2,SNC
SGE 1,2  ; same as ADCZ# 1,2,SZC
SKZ 1,1  ; same as MOV# 1,1,SZR == skip if zero
SNZ 1,1  ; same as MOV# 1,1,SNR == skip if non-zero

Leave a Reply