Close

COSMAC

A project log for Novasaur Retrocomputer

Retrocomputer with serial and video built from only 1978-era TTL logic

Alastair HewittAlastair Hewitt 03/08/2020 at 22:320 Comments

The hardware abstraction layer contains a virtual CPU for executing application code. The plan has always been to emulate an existing CPU, but which one? 8-bits for sure, and since the Intel-derived CPUs (8085/Z80) were too complex, the initial approach was towards a Motorola-derived CPU (68XX/6502). There is a third option though; the RCA 1802 COSMAC.

This chip is often overlooked because it didn't gain the same visibility as the other 8-bit micros. RCA started its precipitous decline soon after the CPU was launched and their commercial products were all flops. The CPU did find success in the embedded market, from GM's first ECU to space probes.

The COSMAC is closer to RISC rather than the typical CISC processors of the era. This minimalistic design makes it even easier to implement that the Motorola-derived chips. The chip contains a total of 16 index registers, each with 16 bits, a single 8-bit accumulator, and a few other status registers.

One issue with chips like the 6502 is their register constrained design (they rely heavily on a zero page to expand a limited set of registers). The emulator has access to its own zero page and can implement up to 256-bytes of registers at no additional cost. There is no benefit to implementing a register constrained design. In fact, a lot of 6502 code would be working around this limitation for no reason. COSMAC code tends to work within its own set of registers and this means it works within the emulator's zero page, so is far more efficient virtual CPU.

The COSMAC uses a fetch and execute sequence, with a single fetch, and typically one, or sometime two execute cycles. The 1976 COSMAC CPU would run with a 4-5uS machine cycle, so comparable to the 5.2uS machine cycle of the hardware abstraction layer (HAL).

The fetch code of the HAL just fits in the 43 clock cycles of the virtual machine cycle. There is one caveat: the program counter is pre-incremented. This is the only way to make it fit, so this will break binary compatibility of assembled machine code. However, the code can be easily fixed via static analysis - absolute jump locations need to be reduced by one.

For the fetch cycle, any one of the index registers can be assigned to the program counter (the PC is essentially an indirect address). This address is used to reference the two bytes of the PC in the emulator's zero page. The lower byte is incremented and the upper byte is either loaded or incremented if the lower byte overflows. The memory location at this address is read and copied to an instruction cache in the zero page. This instruction is then used, along with the virtual machine state, to decode the next page jump.

Most of the execution will fit in a single virtual machine cycle, but there are a few exceptions. One is the long branch - this is where the next two bytes referenced by the PC need to be read and then used to update the PC. This requires a double length virtual machine cycle (86-clock cycles).

This is where the indirect location of the PC is used to find and then increment the PC (two step process with conditional jump), load the value at that memory location and then save it in a temporary location. It has to be cached because the PC needs to be incremented again to load the second byte. Both bytes are then used to update the two bytes of the PC (indirectly). This is a lot of work with such limited hardware, as can be seen in the assembly code below:

# Long Branch (LBR)
INCLUDE ../inc/unary.nsa
INCLUDE ../inc/zpage.nsa
INCLUDE ../inc/pages.nsa
PAGE LBR_PG

# $PREG - zero page location of the P register
# $PREG:  10> 100
# $REG0H: 100> 222 Big-endian
# $REG0L: 101> 254
# assume: Y = $VMS
LDZ HL, $INC2$NULL
FNH DZ, HLD        # double inc state
LD HL, $INC$FORK
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = lower byte address (y=101)
FNFH DZ, XD        # inc value of lower byte put in X ([101]->254->255->X)
FNEL A, PC         # fork based on X
#16

ADDR 0x40          # if X=0xFF : iden Y, inc X, inc Y
LD HL, $IDEN$NULL
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = upper byte address (y=100)
FNH DZ, Y          # get value of upper byte put in ([100]->222->Y)
FNFH B, NA         # get value of memory put in A (A=[Y,X]=[222,255])
LDZ Y, $TEMP
FNFH AZ, ND        # store A in temp
#32
LD HL, $INC$IDEN
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = lower byte address (y=101)
FNFH DZ, XD        # inc value of lower byte put in X ([101]->255->0->X)
LD Y, $PREG        # zero page address of PC (Y=10, [10]->100)
FNFL DZ, Y         # Y = upper byte address (y=100)
FNFH DZ, ND        # inc value of upper byte leave in A ([100]->222->223->Y)
LD HL, $IDEN$INC
FNH A, Y           # move A to Y
FNFH B, NA         # get value of memory put in A (A=[Y,X]=[223,0])
#56
LD Y, $PREG        # zero page address of PC (Y=10, [10]->100)
FNFL DZ, Y         # Y = lower byte address (y=101)
FNFH AZ, ND        # save A to lower byte of P (A->[101])
LDZ Y, $TEMP
FNFH DZ, NA        # copy temp to A
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = upper byte address (y=100)
FNH AZ, HLD        # save A to upper byte of P (A->[100])
#75
NOP
NOP
NOP
LD HL, $NOP
LD Y, $VMS         # set Y = $VMS on exit
VMPHL DZ, PGA      # jump to next VMC
#86

ADDR 0x80          # if X=0 : inc Y, inc X, iden Y
LD HL, $INC$IDEN
LD Y, $PREG        # zero page address of PC (Y=10, [10]->100)
FNFL DZ, Y         # Y = upper byte address (y=100)
FNH DZ, HLD        # inc value of upper byte leave in A ([100]->222->223->Y)
LD HL, $IDEN$INC
FNH A, Y           # move A to Y
FNFH B, NA         # get value of memory put in A (A=[Y,X]=[223,0])
LDZ Y, $TEMP
FNH AZ, HLD        # store A in temp
#36
LD HL, $INC$IDEN
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = lower byte address (y=101)
FNFH DZ, XD        # inc value of lower byte put in X ([101]->255->0->X)
LD HL, $IDEN$INC
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = upper byte address (y=100)
FNH DZ, Y          # get value of upper byte leave in A ([100]->223->Y)
FNFH B, NA         # get value of memory put in A (A=[Y,X]=[223,0])
#56
LD Y, $PREG        # zero page address of PC (Y=10, [10]->100)
FNFL DZ, Y         # Y = lower byte address (y=101)
FNFH AZ, ND        # save A to lower byte of P (A->[101])
LDZ Y, $TEMP
FNFH DZ, NA        # copy temp to A
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = upper byte address (y=100)
FNH AZ, HLD        # save A to upper byte of P (A->[100])
#75
NOP
NOP
NOP
LD HL, $NOP
LD Y, $VMS         # set Y = $VMS on exit
VMPHL DZ, PGA      # jump to next VMC
#86

ADDR 0xC0          # if else : iden Y, inc X, iden Y
LD HL, $IDEN$NULL
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = upper byte address (y=100)
FNH DZ, Y          # get value of upper byte put in ([100]->222->Y)
FNFH B, NA         # get value of memory put in A (A=[Y,X]=[222,255])
LDZ Y, $TEMP
FNFH AZ, ND        # store A in temp
#32
LD HL, $INC$IDEN
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = lower byte address (y=101)
FNFH DZ, XD        # inc value of lower byte put in X ([101]->255->0->X)
LD HL, $IDEN$INC
LD Y, $PREG        # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = upper byte address (y=100)
FNH DZ, Y          # get value of upper byte leave in A ([100]->222->Y)
FNFH B, NA         # get value of memory put in A (A=[Y,X]=[223,0])
#52
LD Y, $PREG        # zero page address of PC (Y=10, [10]->100)
FNFL DZ, Y         # Y = lower byte address (y=101)
FNFH AZ, ND        # save A to lower byte of P (A->[101])
LDZ Y, $TEMP
FNFH DZ, NA        # copy temp to A
LDZ Y, $PREG       # zero page address of PC (Y=10, [10]->100)
FNH DZ, Y          # Y = upper byte address (y=100)
FNH AZ, HLD        # save A to upper byte of P (A->[100])
#71
NOP
NOP
NOP
NOP
NOP
NOP
NOP
LD HL, $NOP
LD Y, $VMS         # set Y = $VMS on exit
VMPHL DZ, PGA      # jump to next VMC
#86

There's a lot going on in that code, but the main structure revolves around the FORK function. The first step is to increment the lower byte of COSMAC PC (referenced by the value in the zero page at $PREG). The result of this increment is loaded in to the X register and passed to the FORK operation. This then jumps to one of the following locations in the page:

The code in these three sections then handles the necessary incrementing of Y to reference the following memory locations:

The rest of the code is basically the same in each section. The optimization is having the FORK take care of all the conditional jumping in a single step.

Discussions