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
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 )
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.
- 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.
- The unit determines which of 4 possible values to use for Carry in the following calculation.
- 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.
- 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.
- 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.
- 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.
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
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