This is copied from November 1977 Byte magazine and is included without permission. Special thanks to Harry Dodgson for finding the original article for me.
While writing Apple BASIC for a 6502 microprocessor I repeatedly encountered a variant of Murphy's Law. Briefly stated, any routine operating on 16 bit data will require at least twice the code that it should. Programs making extensive use of 16 bit pointers (such as compilers, editors and assemblers) are included in this category. In my case, even the addition of a few double byte instructions to the 6502 would have only slightly alleviated the problem. What I really needed was a hybrid of the MOS Technology 6502 and RCA 1800 architectures, a powerful 8 bit data handler complemented by an easy to use processor with an abundance of 16 bit registers and excellent pointer capability. My solution was to implement a nonexistent 16 bit "metaprocessor" in software, interpreter style, which I call SWEET16. This metaprocessor was sketched at the end of my article in May 1977 BYTE, and the purpose of this article is to fill in the details of SWEET16.
SWEET16 is based around sixteen16 bit registers called RO to R15, actually implemented as 32 memory locations. RO doubles as the SWEET16 accumulator (ACC), R15 as the program counter (PC), and R14 as the status register. R13 holds compare instruction results and R12 is the subroutine return stack pointer if SWEET16 subroutines are used. All other SWEET16 registers are at the user's unrestricted disposal.
SWEET16 instructions fall into register and nonregister categories. The register operations specify one of the 16 registers to be used as either a data element or a pointer to data in memory depending on the specific instruction. For example, the instruction INR R5 uses R5 as data and ST @R7 uses R7 as a pointer to data in memory. Except for the SET instruction, register operations only require one byte. The nonregister operations are primarily 6502 style branches with the second byte specifying a +/-127 byte displacement relative to the address of the following instruction. If a prior register operation result meets a specified branch condition, the displacement is added to SWEET16's program counter, effecting a branch.
SWEET16 is intended as a 6502 enhancement package, not a stand alone processor. A 6502 program switches to SWEET16 mode with a subroutine call, and subsequent code is interpreted as SWEET16 instructions. The nonregister operation RTN returns the user program to the 6502's direct execution mode after restoring the internal register contents (A, X, Y, P and S). The example of listing 1 illustrates how to use SWEET16 in some program segment.
300 B9 00 02 LDA IN, Y Get a char. 303 C9 CD CMP "M" "M" for move. 305 D0 09 BNE NOMOVE No, skip move. 307 20 00 08 JSR SW16 Yes, call SWEET16 30A 41 MLOOP LD @R1 R1 holds source address 30B 52 ST @R2 R2 holds dest. address 30C F3 DCR R3 Decrement length. 30D 07 FB BNZ MLOOP Loop until done. 30F 00 RTN Return to 6502 mode. 310 C9 C5 NOMOVE CMP "E" "E" char? 312 D0 13 BEQ EXIT Yes, exit. |
Listing 1: Use of SWEET16 within an assembly language program is accomplished by executing a subroutine call to the SWEET16 entry point (address 307 here). This call preserves the processor registers at the time of entry and begins interpretive execution. End of interpretive execution is signaled by a RTN operation code of SWEET16, at which point all the processor registers will be restored. |
Instruction Descriptions
The SWEET16 op code list is short and uncomplicated. Excepting relative branch displacements, hand assembly is trivial. All register op codes are formed by combining two hexadecimal digits, one for the op code and one to specify a register. For example, op codes 15 and 45 both specify register R5 while codes 23, 27 and 29 are all ST (store) operations. Most register operations of SWEET16 are assigned to numerically adjacent pairs to facilitate remembering them. Thus LD and ST are op codes 2n and 3n respectively, while LD @ and ST @ are codes 4n and 5n.
Operation codes 00 to 0C (hexadecimal) are assigned to the 13 nonregister operations. Except for RTN (op code 0), BK (OA), and RS (B), the nonregister operations are 6502 style relative branches. The second byte of a branch instruction contains a +/- 127 byte displacement value (in two's complement form) relative to the address of the instruction immediately following the branch. If a specified branch condition is met by the prior register operation result, the displacement is added to the program counter effecting a branch. Except for BR (Branch always) and BS (Branch to Subroutine), the branch operation codes are assigned in complementary pairs, rendering them easily remembered for hand coding. For example, Branch if Plus and Branch if Minus are op codes 04 and 05, while Branch if Zero and Branch if NonZero are op codes 06 and 07.
Theory of Operation
SWEET16 execution mode begins with a subroutine call to SW16 (see listing 2, an assembly of SWEET16). The user must insure that the 6502 is in hexadecimal mode upon entry. [For those unfomilior with the 6502, arithmetic is either decimal or hexadecimal (binary) depending on a programmable flag...CH] All 6502 registers are saved at this time, to be restored when a SWEET16 RTN instruction returns control to the 6502. If you can tolerate indefinite 6502 register contents upon exit, approximately 30 us may be saved by entering SWEET16 at location SW16 + 3. Because this might cause an inadvertent switch from hexadecimal to decimal mode, it is advisable to enter at SW16 the first time through.
After saving the 6502 registers, SWEET16 initializes its program counter (R15) with the subroutine return address off the 6502 stack. SWEET16's program counter points to the location preceding the next instruction to be executed. Following the subroutine call are 1 byte, 2 byte, or 3 byte long SWEET16 instructions, stored in ascending memory locations like 6502 instructions. The main loop at SW16B repeatedly calls the "execute instruction" routine at SW16C which examines one op code for type and branches to the appropriate subroutine to execute it.
Subroutine SW16C increments the program counter (R15) and fetches the next op code which is either a register operation of the form OP REG (2 hexadecimal digits) with OP between hexadecimal 1 and F, or a nonregister operation of the form 0 OP with OP between hexadecimal 0 and D. Assuming a register operation, the register specification is doubled to account for the 2 byte SWEET16 registers and placed in the X register for indexing. Then the instruction type is determined. Register operations place the doubled register specification in the high order byte of R14 indicating the "prior result register" to subsequent branch instructions. Nonregister operations treat the register specification (right-hand half-byte) as their op code, increment the SWEET16 PC to point at the displacement byte of branch instructions, load the A-Reg with the "prior result register" index for branch condition testing, and clear the Y Reg.
When Is an RTS Really a JSR?
Each instruction type has a corresponding subroutine. The subroutine entry points are stored in a table which is directly indexed by the op code. By assigning all the entries to a common page, only a single byte of address need be stored per routine. The 6502 indirect jump might have been used as follows to transfer control to the appropriate subroutine:
LDA #ADRH High order address byte STA IND+1 LDA OPTBL,X Low order byte STA IND JMP (IND)To save code the subroutine entry address (minus 1) is pushed onto the stack, high order byte first. A 6502 RTS (Return from Subroutine) is used to pop the address off the stack and into the 6502 program counter (after incrementing by 1). The net result is that the desired subroutine is reached by executing a subroutine return instruction! [This ironic situation is an example of what is common/y referred to as "cleverness."]
Op Code Subroutines
The register operation routines make use of the 6502 "zero page indexed by X" and "indexed by X indirect" addressing modes to access the specified registers and indirect data. The "result" of most register ops is left in the specified register and can be sensed by subsequent branch instructions since the register specification is saved in the high order byte of R14. This specification is changed to indicate R0 (ACC) for ADD and SUB instructions and R13 for the CPR (compare) instruction.
Normally the high order R14 byte holds the "prior result register" index times 2 to account for the 2 byte SWEET16 registers, and thus the least significant bit is zero. If ADD, SUB or CPR instructions generate carries, then this index is incremented, setting the least significant bit, which becomes a carry flag.
The SET instruction increments the program counter twice, picking up data bytes for the specified register. In accordance with 6502 convention, the low order data byte precedes the high order byte.
Most SWEET16 nonregister operations are relative branches. The corresponding subroutines determine whether or not the "prior result" meets the specified branch condition and if so update the SWEET16 program counter by adding the displacement value ( -128 to +127 bytes).
The RTN operation restores the 6502 register contents, pops the subroutine return stack and jumps indirect through the SWEET16 program counter register. This transfers control to the 6502 at the instruction immediately following the RTN instruction.
The BK operation actually executes a 6502 break instruction (BRK), transferring control to the interrupt handler.
Any number of subroutine levels may be implemented within SWEET16 code via the BS (Branch to Subroutine) and RS (Return from Subroutine) instructions. The user must initialize and otherwise not disturb R12 if the SWEET16 subroutine capability is used since it is utilized as the automatic subroutine return stack pointer.
SWEET16 OP CODE SUMMARY | |||||
Register Ops | Nonregister Ops | ||||
00 | RTN | (Return to 6502 mode) | |||
1n | SET Rn | Constant (set) | 01 | BR ea | (Branch always) |
2n | LD Rn | (Load) | 02 | BNC ea | (Branch if No Carry) |
3n | ST Rn | (Store) | 03 | BC ea | (Branch if Carry) |
4n | LD @Rn | (Load indirect) | 04 | BP ea | (Branch if Plus) |
5n | ST @Rn | (Store indirect) | 05 | BM ea | (Branch if Minus) |
6n | LDD @Rn | (Load double indirect) | 06 | BZ ea | (Branch if Zero) |
7n | STD @Rn | (Store double indirect) | 07 | BNZ ea | (Branch if NonZero) |
8n | POP @Rn | (Pop indirect) | 08 | BM1 ea | (Branch if Minus 1) |
9n | STP @Rn | (Store Pop indirect) | 09 | BNM1 ea | (Branch if Not Minus 1) |
An | ADD Rn | (Add) | 0A | BK ea | (Break) |
Bn | SUB Rn | (Subtract) | 0B | RS | (Return from Subroutine) |
Cn | POPD @Rn | (Pop double indirect) | 0C | BS ea | (Branch to Subroutine) |
Dn | CPR Rn | (Compare) | 0D | (Unassigned) | |
En | INR Rn | (Increment) | 0E | (Unassigned) | |
Fn | DCR Rn | (Decrement) | 0F | (Unassigned) | |
SWEET16 Operation Code Summary: Table 1 summarizes the list of SWEET16 operation codes, which are explained in further detail one by one in the descriptions which follow this table. The program in Listing 2 implements the execution of these interpretive codes after a call to the entry point SW16. Return to the calling program and normal noninterpretive operation is accomplished with the RTN mnemonic of SWEET16. |
Memory Allocation and User Modifications
The only storage that must be allocated for SWEET16 variables are 32 consecutive locations in page zero for the SWEET16 registers, four locations to save the 6502 register contents, and a few levels of the 6502 subroutine return address stack. If you don't need to preserve the 6502 register contents, delete the SAVE and RESTORE subroutines and the corresponding subroutine calls. This will free the four page zero locations ASAV, XSAV, YSAV and PSAV.
You may wish to add some of your own instructions to this implementation of SWEET16. If you use the unassigned op codes $0E and $0F, remember that SWEET16 treats these as 2 byte instructions. You may wish to handle the break instruction as a SWEET16 call, saving two bytes of code each time you transfer into SWEET16 mode. Or you may wish to use the SWEET16 BK (Break) operation as a "CHAROUT" call in the interrupt handler. You can perform absolute jumps within SWEET16 by loading the ACC (R0) with the address you wish to jump to (minus 1) and executing a ST R15 instruction.
And as a final thought, the ultimate modification for those who do not use the 6502 processor would be to implement a version of SWEET16 for some other microprocessor design. The idea of a low level interpretive processor can be fruitfully implemented for a number of purposes, and achieves a limited sort of machine independence for the interpretive execution strings. I found this technique most useful for the implementation of much of the software of the Apple II computer. I leave it to readers to explore further possibilities for SWEET16.