Overview

On the Minecraft Forums Eloraam said, "...RP Control contains not a vanilla 6502..., but an extended variants that contains the instructions of 6502 and 65C02, plus about half the instructions and addressing modes of the 65C816 (including 16 bit mode), in addition to a set of completely new instructions and two addressing modes (which I've labelled the 65EL02, because I like the pun). And yes, it includes a 16x16 single cycle multiply and a 32/16 hardware divide. I'm just odd that way."

On IRC Eloraam described Control's CPU as "...a 65EL02, it supports all the 6502, 65C02, and part of the 65C816 instruction set."

Machine architecture

CPU

The 65EL02 CPU is mostly compatible with the 65816, the earlier 65C02, and the even earlier 6502. The 65816 is documented in the datasheet from Western Design Center. The 65EL02 supports most of the features of the 65816, with the following notable exceptions:
  • As of RP2PR5b2, the 65EL02 does not support emulated-6502 mode in quite the same way as a 65816. The 65EL02 implements the XCE instruction that would normally be used to switch between native-65816 and emulated-6502 modes, and switching to emulated mode appears to set the M and X bits in the processor status register to 1 as it should (switching to 8-bit mode), but it is possible to later clear the M and X bits and use 16-bit instructions without first returning to native mode. It is possible this is a bug which might be fixed sometime. In any case, there is no reason to ever use emulated mode in RedPower Control because the 65EL02 also does not implement an interrupt service routine vector, making the B bit that would normally replace the X bit in emulated mode irrelevant; instead of running in emulated mode, one can simply run in native mode with 8-bit registers.
  • The 65EL02 omits a number of 65816 instructions, replacing them instead with other instructions not provided by the 65816. The omitted 65816 instructions are COP (not useful because the 65EL02 does not have a coprocessor), TCS and TSC (you must use TXS and TSX instead), WDM (which is a reserved NOP on the 65816), MVP and MVN (though the new NXA instruction may help implementing similar operations), all instructions that use long addressing mode or that affect the program bank register or data bank register (not useful because the 65EL02 never has more than 64 kB RAM), all instructions that affect the D register (the 65EL02 instead assumes the D register is always zero, as is effectively the case on the older 65C02). In return, the 65EL02 adds a number of new instructions and registers: the TXR, TRX, RH*, RL*, and RER instructions and the R register act as a second stack; the TXI, TIX, ENT, NXA, and NXT instructions and the I register provide additional flexibility in indirect memory accesses; and the MUL, DIV, ZEA, SEA, TAD, TDA, PHD, and PLD instructions and the D register (important: no relationship to the D register described earlier in this paragraph!) provide multiplication, division, and related supporting utility functionality.
  • According to the datasheet, the 65816 uses a postdecrement stack—that is, when pushing a value to the stack, the stack pointer is decremented after the push, and when popping (“pulling”, in 65816 terminology), the stack pointer is incremented before the pull. This means the stack pointer always points to the next byte to be pushed into, and the last byte pushed onto the stack is found at S+1. As of RP2PR5b2, the 65EL02 uses a predecrement stack like the Intel x86, where the stack pointer is decremented before a push and incremented after a pull, meaning the last byte pushed to the stack is found at S. This is likely a bug in the 65EL02. Given the amount of existing software that might break were this bug fixed, it is not clear whether Eloraam would feel this bug is beneficial to fix. However, fixing this would improve compatibility with existing compilers targeting 6502 series CPUs. Eloraam had the following to say when asked about this in the #redpower IRC channel on December 20, 2012 (timestamps in UTC−8):
    "(23:27:20) Eloraam: i could have sworn i got that right... unless of course the 816 changed from the 02, and i changed it from one to the other already
    (23:27:29) Eloraam: ... on the other hand, the current design is MUCH more friendly to Forth
    (23:27:59) Eloraam: the 65EL02 is after all primarily for running the RP Control FORTH language"
A table of all 65EL02 instructions and their encodings is available on Eloraam’s website, and also links to a version of the ACME assembler modified to support the 65EL02; to use the modified ACME assembler for 65EL02 code, either pass “--cpu 65el02” on the command line or place the following line at the top of the assembly source file:
    !cpu 65el02

Performance

The 65EL02 CPU is capable of running 20,000 instructions per second in the long term, or 1,000 instructions per world tick. Eloraam has stated that this cycle quota is flexible and that a program that regularly executes fewer than 1,000 instructions per tick (by invoking the WAI instruction) will be able to occasionally execute more than 1,000 instructions within a single tick; 1,000 instructions per tick is the long-term average performance of a fully CPU-bound program.

Experimental methodology

A computer was attached to an I/O expander. The white output of the I/O expander was attached to the set input of an RS latch. The reset (~Q) output of the latch was attached to a timer. The output of the timer was attached to the reset input of the latch. The set (Q) output of the latch was attached to the orange input of an I/O expander. The following code was executed, with the Redbus window at 0x2000 and addressing the I/O expander:
    ; Clear counter memory locations
    stz 10
    stz 12
    ; Hit the latch
    lda #1
    sta 0x2002
    ; Wait until the latch activates
    lda #2
loop1
    bit 0x2000
    bne loop1
    ; Deactivate output
    stz 0x2002
    ; Go into a loop until the latch deactivates
loop2
    lda #1
    clc
    adc 10
    sta 10
    lda #0
    adc 12
    sta 12
    lda #2
    bit 0x2000
beq loop2
Afterwards, the values in locations 10 through 13 were printed, yielding the number of loop iterations executed. This code was executed with the timer set to values ranging from 5 seconds to 3 minutes to amortize out any nonlinear components such as setup instructions, timing jitter due to the length of the loop, and one-to-two-tick overhead of managing the I/O expander and external logic gates. The results were as follows, showing a clear trend toward a 20 kHz long-term average instruction rate:
Seconds
Iterations
Instructions
Hz
5
9900
99000
19800
30
59900
599000
19966.67
60
119900
1199000
19983.33
90
179900
1799000
19988.89
120
239900
2399000
19991.67
150
299900
2999000
19993.33
180
359900
3599000
19994.44

Interrupts

The 65EL02 CPU partially implements the 65816's interrupt capabilities. The WAI instruction is implemented and halts execution until an interrupt is received. However, unlike the 65816, an incoming interrupt of this type never causes a control transfer to an interrupt vector; it is not possible to be asynchronously notified of interrupt arrival (the I bit in the processor status register appears to have no effect).
At the time of this writing, there appears to be just one interrupt source: one interrupt is delivered on each Minecraft world tick, which means at approximately 20 Hz.
There are, however, two events which act somewhat like interrupts: BRK and POR. BRK is effectively a “software interrupt”: when code executes the BRK instruction, the address of the following byte of code and the state of the CPU flags are pushed to the stack and control is transferred to the BRK vector (which can be set by loading the address of the handler into the 16-bit A register and executing MMU #0x05 and read by executing MMU #0x85, which stores the vector address into A); if the BRK handler wishes to return to the calling code, it can do so with the RTI instruction. POR is the so-called “power on reset” event, which is not quite what its name suggests, since power on resets cause the BIOS to run and load the operating system from floppy disk; instead, this event occurs when a player pushes the START button on the CPU front panel while the CPU is already running (pushing the button when the CPU is not running simply causes it to resume execution); when the POR event is delivered, execution jumps to the POR handler (which can be set by loading the address of the handler into the 16-bit A register and executing MMU #0x06 and read by executing MMU #0x86, which stores the vector address into A). Unfortunately the return address is not saved anywhere, so it is impossible to use the POR mechanism as a general-purpose interrupt, though a sufficiently carefully written system could potentially define a convention that allows the POR handler to jump to some safe, predetermined location that will continue doing useful work with minimal disruption.

Memory

The 65EL02 CPU implements a flat, 16-bit memory address space, covering 64 kB from 0x0000 to 0xFFFF. Any particular byte within this address space may be backed by physical RAM, backed by the Redbus window, or unbacked. Addresses 0x0000 through 0x00FF are referred to as the zero page, and they can be accessed by the smaller instructions from the instruction set that are marked as having "Zp" as an operand. These addresses are otherwise the same as all other addresses, and they can be accessed with normal direct and indirect addressing modes in the usual way.
The address space is divided into eight blocks of 8 kB each. The first block, from 0x0000 to 0x1FFF, always has physical RAM available to back it (this RAM is provided by the CPU block). The remaining seven blocks map to the seven possible backplane slots behind the CPU; each backplane slot containing a block of RAM will cause its corresponding region of address space to have backing RAM usable by software, while each empty or absent backplane slot will cause its corresponding region of address space to not have backing RAM.
Any attempt to write data to an unbacked address will cause the data to be discarded. All reads from unbacked addresses return 0xFF. This determinism can be exploited to scan the address space and determine the amount of RAM installed: for each of the eight blocks, simply write a byte to a value other than 0xFF and read it back; if it kept the written value, RAM is installed; if it read back as 0xFF, RAM is not installed.

Redbus

Redbus is a communication network standard that 65EL02 computers use to communicate with the rest of the world. At the physical layer, Redbus is carried over copper ribbon cables, which can carry messages up to 255 metres from the initiating device. Devices physically adjacent to a CPU are implicitly connected by Redbus; actual copper ribbon cable does not need to be laid. However, Redbus signals do not conduct through devices, only to them, so to communicate with distant devices, the ribbon cable must still be connected to the CPU block and not an adjacent peripheral.
At the link layer, Redbus has a master/slave topology with multiple-master capability. Each slave device is identified by an 8-bit ID number which serves as its address; a master wishing to talk to a particular slave must address its messages to the slave's ID number. The ID number of a computer can be set on its front panel, accessible simply by right-clicking; ID numbers of screens, disk drives, and IO expanders can be set by shift-right-clicking them with a screwdriver (sonic or normal) to open them up and access their DIP switches.
In operation, a Redbus network carries two types of messages: read cycles and write cycles. A read cycle is issued by a master to a specific slave and also carries an 8-bit byte address; upon receipt of the byte address, the slave returns a reply containing either 8 or 16 bits of data. A write cycle is issued by a master to a specific slave and carries both an 8-bit byte address and 8 or 16 bits of data; the slave does not return a reply. A read cycle issued to a nonexistent slave will be seen by the master as returning the idle state of the bus, which is all zeroes (0x00 for an 8-bit read or 0x0000 for a 16-bit read).
A message addressed to a particular slave address will always be accepted by the closest matching slave to the issuing master, where the distance in question is length of cable between the devices. It is thus safe to place multiple slaves with the same ID on a single Redbus network segment as long as the designer is careful with the geometry of the ribbon cables.
Note that the ID number of a master device never has any impact on the operation of Redbus; it is completely ignored. Thus, it is safe to give all computers on a network the same ID number as long as they only need to talk to peripherals and not directly to each other—any master can address a particular slave, even if there is a different master with the same ID closer to the slave. Redbus is also completely immune to collisions at the link level; multiple masters may communicate with various slaves over the same network at the same time. Of course if two masters write to the same slave, writes will be ordered arbitrarily and whichever master writes later will win the race, which may lead to undesirable behaviour, but the Redbus network itself will not introduce collisions.

The Redbus window

The "Redbus window" is one of the two ways a computer interacts with Redbus; this mechanism enables a computer to act as a Redbus master. The Redbus window must first be mapped into a particular location in the memory address space; this is done by loading the memory location into the 16-bit A register and invoking the MMU #0x01 instruction (MMU #0x81 copies the current window location into A). The window can then be enabled with the MMU #0x02 instruction (it can later be disabled with MMU #0x82). At this point, the Redbus master transceiver maps itself into the computer's memory starting at the requested address and covering a space of 256 bytes, acting as a memory-mapped I/O device: any read or write operations into the given address range are redirected to the Redbus master transceiver which generates Redbus cycles, instead of accessing the RAM or empty space which would normally exist at that location. A read from memory will generate a Redbus read cycle and will return the data provided by the Redbus slave; a write to memory will generate a Redbus write cycle and send the written data to the Redbus slave. The choice of which slave to address is made by loading the slave's ID number into the A register (which may for this purpose be 8-bit or 16-bit) and invoking the MMU #0x00 instruction; the choice of which byte address within the slave to read or write is made by choosing which byte within the 256-byte-long Redbus window to read or write.

Note that because the Redbus window acts as a memory-mapped I/O device, it essentially impersonates physical RAM at the specified addresses. As such, there is no need for actual physical RAM to exist at the locations where the Redbus window is mapped! Therefore, the 0x0300 address chosen by MineOS for mapping the Redbus window wastes 256 bytes of usable physical RAM provided by the CPU block. While this is unavoidable on computers with all seven 8 kB expansion modules installed (assuming one wants the Redbus window somewhere), when writing custom operating systems, one may prefer to place the Redbus window at a higher address where it does not overlap any physical RAM (for example, 0xFF00) on less-equipped computers.

The external memory mapped window

The "external memory mapped window" is the second way a computer interacts with Redbus; this mechanism enables a computer to act as a Redbus slave (contrast this to the Redbus window, which allows a computer to act as a master). The external memory mapped window acts as a direct memory access (DMA) device that is activated by incoming Redbus read and write cycles. The ID switches on the front panel of a computer select at which slave ID number the computer responds when using the external memory mapped window. To be used, the external memory mapped window must be provided (by loading the address into the 16-bit A register and executing MMU #0x03—MMU #0x83 reads back the current address into A) with a block of 256 bytes of RAM which it will expose over Redbus, then enabled with the MMU #0x04 instruction (MMU #0x84 disables the window). Once the external memory mapped window is enabled, any read cycle directed at the computer's own ID (presumably from another computer) will cause the external memory mapped window to read from the computer's RAM at the target address and return the data found there as the response to the read cycle; any write cycle directed at the computer's ID will cause the external memory mapped window to write the bus cycle's data into the computer's RAM at the target address. In both cases, the target address is the sum of the window location specified by the MMU #3 instruction and the 8-bit byte address encoded in the bus cycle.

Because the external memory mapped window acts as a DMA device (in contrast to the Redbus window), the memory region provided for use by the window must be backed by physical RAM. Incoming bus cycles received by the computer are satisfied by reads and writes into the actual physical RAM found at the specified locations, so that RAM must exist in order for anything useful to be done with the data.

Real-time clock

Every 65EL02 CPU is equipped with a free-running 32-bit timer. This timer starts at zero when the CPU block is placed into the world and thereafter increments by one every tick (1/20 of a second), no matter what the computer is doing (actively running code, waiting for an interrupt, or even halted), providing a rollover period of almost seven real-world years. The timer never resets, even if the computer’s reset button is pushed, though presumably after its almost-seven-year period has passed it will roll over from 0xFFFFFFFF to zero. Executing the MMU #0x87 instruction reads the current value of the timer, storing the upper 16 bits into the D register and the lower 16 bits into the A register. The timer continues to increment when the computer is in an unloaded world chunk, but does not adjust for time spent asleep.

Boot sequence

The boot sequence of a RedPower Control 65EL02 CPU consists of three basic components: a hardware component, a BIOS component, and an operating system component. Each component takes responsibility for doing just enough work to prepare the machine for the next component.

Hardware

The hardware boot sequence is performed automatically by the hardware whenever the CPU is reset—when it is first placed into the world, and when the reset button is pushed. The hardware boot sequence:
  1. copies the value of the eight "disk ID" switches from the front panel into RAM at address 0x0000
  2. copies the value of the eight "console ID" switches from the front panel into RAM at address 0x0001
  3. copies the BIOS from the file "eloraam/control/rpcboot.bin" in RedPowerMechanical-version.zip into RAM starting at address 0x0400
  4. sets the program counter to 0x0400 so the BIOS will start executing

BIOS

The BIOS is responsible for loading and launching the operating system from a floppy disk. Unlike modern 32- and 64-bit x86 computers, where the BIOS also provides services to the operating system, the RedPower Control BIOS does nothing but load and jump to the OS. The code fragment below is not the original source code to rpcboot.bin, which presumably only Eloraam has access to; it is, however, a hand-constructed, commented code fragment which assembles to a byte-for-byte-identical output to rpcboot.bin.
; Memory locations for variables
front_panel_disk_id=0      ; Set by the hardware to the disk ID number from
                           ; the front panel (8 bits)
front_panel_console_id=1   ; Set by the hardware to the console ID number from
                           ; the front panel (8 bits)
next_sector_to_load=2      ; The sector number of the next disk sector to load
                           ; (16 bits)
next_location_to_load_to=4 ; The memory address to which the next sector will
                           ; be copied once loaded (16 bits)
 
    !cpu 65el02
    *=0x0400
 
    ; Switch to native mode
    clc
    xce
 
    ; Load the front panel disk ID and map that device in on redbus
    lda front_panel_disk_id
    mmu #0x00
 
    ; Switch to 16-bit accumulator, memory accesses, and index registers
    rep #0x30
    !al
    !rl
 
    ; Set the redbus window to appear at address 0x0300
    lda #0x0300
    mmu #0x01
 
    ; Enable redbus window
    mmu #0x02
 
    ; Initialize variables: next sector to load is sector 0 and it will be
    ; stored to memory address 0x0500
    stz next_sector_to_load
    lda #0x0500
    sta next_location_to_load_to
 
 
 
    ; Loop to load as many sectors as there are on the disk
load_disk_loop
 
    ; Copy the next sector number into the disk drive's sector number register
    lda next_sector_to_load
    sta 0x0380
 
    ; Switch to 8-bit accumulator and memory accesses
    sep #0x20
    !as
 
    ; Issue the "read sector" command to the disk drive
    lda #4
    sta 0x0382
 
    ; Wait until the command is completed (noted by the command register no
    ; longer containing the original command)
wait_for_interrupt_loop
    wai
    cmp 0x0382
    beq wait_for_interrupt_loop
 
    ; If the "read sector" command completed successfully, copy the sector from
    ; the disk drive's sector buffer into main memory
    lda 0x0382
    beq copy_sector_to_main_memory
 
jump_to_loaded_code
    ; The "read sector" command did not complete successfully; this means we
    ; have reached the end of the disk and should jump to the loaded code
    ; Disable redbus window
    mmu #0x82
    ; Switch to 8-bit accumulator, memory accesses, and index registers
    sep #0x30
    !as {
        !rs {
            ; Switch to emulated mode
            sec
            xce
            ; Jump to the loaded code
            jmp 0x0500
        }
    }
 
 
 
    ; Copies the data from the disk drive's sector buffer into main memory
copy_sector_to_main_memory
    ; Switch to 16-bit accumulator and memory accesses
    rep #0x20
    !al
 
    ; Point the I register at the sector buffer
    ldx #0x0300
    txi
 
    ; Set the Y index register to hold the number of words to copy (0x40 = 64
    ; words = 128 bytes, the size of a sector)
    ldy #0x40
 
    ; Go into a loop copying the data
copy_sector_to_main_memory_loop
 
    ; Load a word from I (pointing into the sector buffer) into A,
    ; postincrementing I by 2 bytes
    nxa
 
    ; Store the loaded word into main memory at the appropriate location
    sta (next_location_to_load_to)
 
    ; Increment the location to store to by 2 bytes
    inc next_location_to_load_to
    inc next_location_to_load_to
 
    ; Loop
    dey
    bne copy_sector_to_main_memory_loop
 
    ; Check if the address counter has wrapped around to 0, indicating the disk
    ; is so big it doesn't fit in RAM; if that happens, only as much as fits in
    ; RAM is loaded and jumped to
    lda next_location_to_load_to
    beq jump_to_loaded_code
 
    ; Go back and try to load another sector
    inc next_sector_to_load
    jmp load_disk_loop
As one can see, the above code simply loads all available sectors from the floppy disk chosen by the front panel switches, copying the data to consecutive bytes starting at address 0x0500, then, upon running out of either disk sectors or address space, jumps to address 0x0500 where the operating system is expected to now reside. On entry to the operating system at address 0x0500, the Redbus window and external memory mapped window are both disabled, the CPU is operating in emulated mode with 8-bit registers, and the disk and console ID numbers from the front panel can still be found at addresses 0x0000 and 0x0001 respectively.

Operating system

An operating system is free to do what it chooses once it has been loaded by the BIOS. An operating system may overwrite the disk and console ID numbers at addresses 0x0000 and 0x0001 with its own data if desired; those addresses are only special during hardware initialization and can be used as ordinary memory locations subsequently. The same is true of addresses 0x0400 through 0x04FF; while the hardware copies the BIOS code into these locations during initialization, once the operating system is running the BIOS is generally no longer needed and may be erased and the RAM reused for other purposes (though the OS may prefer to keep the BIOS intact in order to provide a “reboot” command, which would simply disable the Redbus window and external memory window and jump to address 0x0400).

The Forth MineOS operating system sets up a P stack at 0x0100 through 0x01FF using the S register as a stack pointer, an R stack at 0x0200 through 0x02FF using the R register as a stack pointer, the Redbus window at 0x0300 through 0x03FF, and the external memory mapped window at 0x0400 through 0x04FF (as documented here), but there is no requirement that a custom operating system use these same addresses.