4th of Aug. 2026 by Michael

Embedded Projects

Myth

This is my successor project to Sonne.

What was Sonne again?

Sonne is a discrete micro-controller I designed and built from scratch using about one-hundred 74HC series logic chips, some CMOS memory and passive-components only. It runs about 1 million instructions per second on a four-layer PCB fabricated by JLCPCB. See the link for more details and the KiCad files I used for ordering.

A demonstration video shows the Sonne controller board with a companion I/O-board I designed stacked on top of it. In the video, it loads a program for multiplying 7 by 13 from a serial EEPROM and executes it. A boot loader program is stored in a parallel EEPROM (large chip top-center marked “App”) mapped into the CPU’s address space. When reset is applied, the boot loader “bit-bangs” the SPI interface of the IO board to load the multiplication routine from a Serial EEPROM (seen bottom center). Both programs are written in the CPU’s native assembly language. The object code is generated by an assembler I wrote for the project. The multiplication routine then takes over, computes the result and displays it on the I/O boards 7-segment display. What is interesting about this is that none of the electronics components used is an Arduino or micro-controller or CPU – the circuit is the CPU described in this project.

Working PCB based CPU with similar design
Working PCB based CPU with similar design

From Sonne to Myth

Myth is a heavily revised version of Sonne. Although I am not planning to build a version using old-fashioned logic chips, care has been taken to retain the simplicity of it, so that I could still do so in principle. Being able to build it and being able to visualise the circuit on a basic component level (as opposed to writing Verilog like in my 16-bit controller project) was a design goal.

The schematics have been stripped of everything I thought wasn’t essential, for it to be easier to read. In other words, although it’s still possible to load the schematics file into KiCad, its intended use is only for reference and tinkering with the design.

Simulator

A minimal reference implementation of the CPU in C is provided in the download package (see end of article). It shows the intended workings of the hardware and has very little code, so have a look.

Myth POC Schematics
Myth POC Schematics

Schematics PDF

The component count is now higher than for the prototype, because in this project I’ve implemented the ALU (top and top-right in schematics) in discrete logic, where it used to be just look-up tables in a big PROM.

Status

There is now a command-line tool (my) for exploring Myth. It includes an assembler.

Code Example

Here is an example of a native multiplication routine which multiplies the two 8-bit numbers in the accumulator, leaving the accumulator with the 16-bit result (high-order in A). The listing is “wide” output of the assembler using my -la. The hex numbers on the left show the generated object code. As you can see, the label “3@MUL8” instructs the assembler to emit opcodes into code page 0x03.

    ADDR:  OBJCODE:                  LIN:  SOURCE:
                                     0001  
                                     0002  (Multiply 4x7)
    0000:  84 04 84 07 80 03         0003  fa 4, fa 7 - fc >MUL8. ; Sets A to 7, X to 4 and calls mul8
    0006:  8C 06                     0004  @idle fj <idle         ; my-tool stops at 64k cycles
                                     0005                         ; Check the result using: "my -p"
                                     0006  
                                     0007  ; Multiply A times X, result in A:X
                                     0008  ; A:X accumulator acts as a two-element push-down stack when writing to A
                                     0009  ; Both regs are the implied ALU operands, primary result in A, secondary in X
                                     0010  
                                     0011  ; Using page 3: "my"-tool uses p1 for persisting regs, p2 as IO-buffer
                                     0012  
                                     0013  3@MUL8
    0300:  68                        0014      a1          (Save multiplicand into L1 - turns into low order result)
    0301:  17 6B                     0015      xa a4       (Save multiplier into L4)
    0303:  84 00 69                  0016      fa 0, a2    (Set high-order result to 0, keep in L2)
    0306:  85 07                     0017      fd 7        (Initialise loop counter, 8 bits to process)
                                     0018      @loop
                                     0019          fa b0000_0001, 1a AND  (Check LSB of multiplicand)
    030C:  8E 12                     0020          fz >a                  (Skip if zero)
    030E:  63 61 1D 69               0021              4a 2a ADDC a2      (Add multiplier to high order result)
                                     0022          @a
                                     0023          1a SHR, a1             (Shift low-order result right)
    0315:  61 1B 69                  0024          2a SHR, a2             (Shift high-order result right, LSB saved to X)
    0318:  17 60 15 68               0025          xa 1a IOR, a1          (Carry high-order LSB into low-order MSB)
    031C:  8B 08                     0026      fw <loop
    031E:  60                        0027      1a          (Push low-order result)
    031F:  61                        0028      2a          (Push high-order result)
    0320:  05                        0029      RTS

The following is a screenshot that demonstrates a syntax highlighting script I made for Sublime Text. It shows the same routine but as editable source code as opposed to assembler output.

 
 

A division/modulus routines looks like this:

 
 

The following parts are the reference documentation for the CPU.

Myth CPU/Micro-Controller

OVERVIEW

Myth is an educational 8-bit CPU with a reduced, but hopefully enjoyable feature set. It has no microcoded complex instructions, but care has been taken to allow for practicable programming that should be intuitive to anyone who has some familiarity with assembly language.

The project is based on an earlier prototype, built successfully using just under 100 74HC series chips, some CMOS memory and passive components.

PART 1

Basic CPU

Power-up and Reset

When the CPU is reset, registers L (stack frame pointer), C (program page index), PC (program offset) and the BUSY flag (disable interrupts) are set to zero, and the first instruction is fetched from address C:PC. PC is then incremented for the next instruction fetch, and so on.

Accumulator and ALU

An accumulator with two registers (A and X) feeds into an Arithmetic Logic Unit (ALU), computes a result, then overwrites A and, in some cases, X with the results.

The primary result (the sum of both registers, for instance) is stored in A, the secondary result (carry bit of the sum) is stored in X.

When writing a value into A (instructions _A and GETA), the accumulator functions as a two-element push-down stack: The old value of A is saved into X before overwriting it with the new value. X can be saved into A with the XA instruction.

Here is an example that demonstrates the use of the ALU in conjunction with the accumulator:

; F_ fetches a literal and places it into whatever _ is

FA 4  (Pushes 4 onto the accumulator stack AX)
FA 5  (Pushes 5)

(A is now 5, X is 4)

ADDC  ; Add A and X; secondary result/side effect:
      ; carry bit in X

(A is now 9, X is 0)

SHR   ; Shift A right; secondary result/side effect:
      ; previous low order bit as 0 or 80h in X

(A is now 4, X is 128/80h)

AGX   ; Produce flag: A greater than X, no side effect

(A is 0 - false, X is 128/80h - unchanged)

The ALU can run the following opcodes:

 0 NOT   Set A to one's complement of A, X unchanged
 1 ALX   Flag (A<X) in A (255 if true, 0 if false), X unchanged
 2 AEX   Flag (A==X) in A (255 if true, 0 if false), X unchanged
 3 AGX   Flag (A>X) in A (255 if true, 0 if false), X unchanged
 4 AND   Set A to (A AND X), X unchanged
 5 IOR   Set A to (A OR X), X unchanged
 6 EOR   Set A to (A XOR X), X unchanged
 7 XA    Set A equal to X, X unchanged
 8 AX    Set X equal to A
 9 SWAP  Swap A and X
10 SHL   Shift A left, result in A, set X to previous MSB of A as LSB (0 or 1)
11 SHR   Shift A right logically, result in A, set X to previous LSB of A as MSB (0 or 80h)
12 ASR   Shift A right arithmetically, set X to previous LSB of A as MSB (0 or 80h)
13 ADDC  Add A to X, result in A, CARRY bit in X (0 or 1)
14 ADDV  Add A to X, result in A, OVERFLOW flag in X (255 if OVF, else 0)
15 SUBB  Subtract A from X, result in A, BORROW bit in X (0 or 1)

Memory Layout

Memory is accessed as 256 pages of 256 bytes each (64k).

A memory address is composed of a page index (high order address byte) and an offset (low order address byte) within that page. For example address 0x6502 has a page index of 0x65 and a byte offset of 0x02.

Offset Registers

There are exactly two address offset registers, one for fetching and storing data, and one for fetching code literals and pointing at the next instruction.

For data memory access, the value in the offset register (O) is used.

The value in the program counter register (PC) is used as the address offset of the current instruction or code literal in memory.

There are instructions that access memory with implied offsets, however, such as GETPUT instructions.

Page-Index Registers

There are four page-index registers: B for Base, C for Code, K for Key, and L for Local.

Register B

For data memory access, the base register (B) is used together with O as the memory pointer to the address where read or write operations occur.

The 16-bit value B:O is called the base pointer. The base pointer is the only means of composing complete 16-bit addresses (pointers) directly.

Register C

During code execution, the program counter register (PC) holds the byte offset of the current instruction in memory. The page index (address high byte) to which this offset is added is stored in C (Code). The current instruction or code literal is thus pointed to by C:PC. PC auto-increments and wraps without affecting C.

Register K

This is an amenity register that can be set to B using the KEY instruction. It is used in conjunction with the PAIR transfer target xK, an effect register. When writing into _K, B is set to the value in K, and then the source value is stored in O. This sets the base pointer to K:O.

The intended use for this instruction is to provide a shortcut:

;Load a system variable (offset MYVAR) from a table in page 2
fb 2 KEY (Set page-index in K to 2, setting the implied page for writing to the _K effect)
...
fk MYVAR ma (Set base pointer B:O to 2:MYVAR, then read from there into A)
Register L

The purpose of the L (local) page index register is to provide single page stack frames for subroutines. During subroutine calls, the page index in L is decremented, so that memory reads and writes using the L page index transparently access memory which is local to the currently running subroutine. When the subroutine returns, the page index in L is incremented, so that the previous stack frame (or L page) is restored to the calling subroutine. While conceptually each stack frame uses the whole page, a conservative memory map will probably reserve the initial dozens of bytes for subroutine code, so that the call stack can grow or shrink “overlaying” the code pages. See the following section on stack frames and GETPUT variables, where this is demonstrated by a design feature.

GETPUT Instructions and Stack Frames

Instructions of type GETPUT complement the behaviour explained in the previous section on L. GETPUT instructions have mnemonics that consist of a digit and a letter. The letters name one of the allowed registers for this type of instruction: B, O, A, or D. The digit is a number between 1 and 8, used as a short-hand to reference offsets 0xF8 to 0xFF in the local page (page index L) in ascending order. These memory locations are also referred to as L1 to L8. By means of the GETPUT instructions, L1 to L8 become quick-access memory locations that can be used as local variables within the current stack frame.

The position of the digit relative to the letter determines the direction of the data move. For example: 1a transfers the memory content at offset 0xF8 into register A. And d5 transfers register D (down-counter) into memory at offset 0xFC of the local page.

The local-page index can be manually decremented using ENTER, and manually incremented using LEAVE.

PAIR Instructions and Effect Registers

Data move instructions of type PAIR have mnemonics that consist of two letters, one for the source, followed by one for the target of the data move. For example, in order to write register B into A, there is an instruction with the mnemonic “ba”.

Some registers are “conventional” data registers, such as B and A, which correspond to physical registers, but there are also EFFECT registers, which source or distribute data indirectly, or trigger conditional actions. One of them is the F register (“fetch”). It extracts the next byte in the instruction stream, and then skips to the next instruction byte.

Example: “fa” fetches the byte following the current instruction in memory, and stores it into A. It then increments the program counter by 1 so that the next instruction is fetched instead of the literal.

Intrapage Control Flow and D Register

Branching inside of the current code page (page index C) is controlled by writing into the five effect registers J (Jump), W (While), H (Hot/Not zero), Z (Zero), or N (Negative). These instructions (conditionally) set the byte offset of the C:PC instruction pointer to an absolute value. You cannot leave the code page with them, which can be accomplished using the “COR” jump instruction, or any of the call/return mechanisms. Relative branching instructions can be implemented by writing a custom (TRAP) instruction that does this.

By writing a branch offset into J, the program counter is set to this new offset without condition. Writing to H loads the PC with the new offset only if register A (Accumulator) is non-zero (“hot”). Writing to Z loads PC with the new offset only if A is zero. W (while) works in conjunction with the D (down-counter) register; when a branch target offset is written into W, PC is loaded with the new offset only while/if the D register is non-zero. Then, in either case, the D register is decremented.

Interpage Control Flow and TRAP Instructions

On power-up and reset, registers C and PC are set to zero and the first instruction is fetched from address C:PC.

Branching to code in a different page is done by writing into effect register C (call), by executing a return instruction (RTS or RTI), the COR (coroutine) instruction, or executing a TRAP call instruction.

CALLS

Writing into the _C effect register (“call”) triggers the following sequence of events: C:PC is saved into B:O, the source value of the instruction (target page) is stored into C, PC is set to zero and L is decremented by 1.

TRAPS

TRAP instructions have opcodes that encode an immediate 5-bit target page-index for a call. When executed, an implicit subroutine call to this encoded page-index (0-31) occurs within a single instruction. C:PC is saved into IA, the trap page-index is stored into C, and PC is set to zero, so that just as for _C calls, the call goes to the head/first byte of the target page. A TRAP sets the BUSY flag and thus disables interrupts. The BUSY flag is cleared by executing RTI. Trap calls must be left by executing RTI (Return from Interrupt)!, since they are using another pointer (IA) instead of B:O for saving and restoring the return address.

COROUTINES

The COR (“coroutine”) instruction swaps C:PC and B:O and does not modify L. The instruction transfers control to the instruction pointed to by B:O, and then overwrites B:O with the return address (previous value of C:PC) upon closing the instruction. The behaviour that you get is a “ratcheting” back-and-forth execution that alternates between two routines using the same stack frame. Of course you are free at any time to modify the return address in B:O.

RETURNS

The RTS instruction conceptually reverses _C call instructions and return control to the calling routine. When executing RTS, the C:PC instruction pointer is restored from B:O, and the local-page index in register L is incremented.

The RTI instructions conceptually reverse TRAP calls and return control to the calling routine. When executing RTI, the C:PC pair is loaded from the IA amenity pointer, and the local-page index in register L is incremented.

PAIR Memory access - Effect Register M

Writing into M (M) stores the value into memory at page index B offset O. Conversely, reading from M (M) transfers the value stored into that memory cell into the target of the PAIR instruction. There are no other memory transfer instructions besides xMx, the GETPUT instructions, and F_.

Scrounged PAIR opcodes

Inherent NOP instructions such as BB, OO, AA, and DD, and impractical instructions such as FM and MM (same-cycle memory load-store) are repurposed (“scrounged”), and their respective opcodes execute different instructions.

FM routed to: KEY (Copy B into K)
MM routed to: CODE (set B:O to C:PC)
BB routed to: LOCAL (set B:O to L:0xF7 - "L0")
OO routed to: LEAVE (increment L)
AA routed to: ENTER (decrement L)
DD routed to: INC (increment A)
SS routed to: DEC (decrement A)
PP routed to: EA (Copy E to A)

B:O Pointer Register (BOPs) and Amenity Pointers

As mentioned, registers B (base) and O (offset) form a 16-bit pointer for memory access. The xU (update) instruction is used to add an 8-bit signed number to this pointer for doing address arithmetic.

There are four 16-bit amenity registers into which the B:O pointer can be saved, or from which it can be loaded in a single instruction (instruction group BOP). For instance: BOP1 stores the B:O pointer into P1, and P1BO stores P1 into B:O.

The fourth amenity pointer (P4 => IA) is reserved for trap and interrupt operation. See the note on usage conventions for these pointers in the “Programming” section.

Interrupts

An external device can make an interrupt request (IRQ) by asserting the IRQ signal.

At the beginning of each instruction cycle, the CPU checks whether an Interrupt must be serviced. There are two conditions which prevent an interrupt from being serviced by the microcontroller during a given instruction cycle. Firstly, when the CPU is running code within page 0, for example just after RESET, and secondly when the BUSY flag is set.

If the BUSY flag is not set, and the page index in C is not zero, the CPU injects a “fake” TRAP call instruction to page 0, instead of fetching a proper instruction opcode. By entering page 0, an interrupt service routine in page zero at address-offset 0 is run, and the busy flag is set, preventing the CPU from accepting (nesting) further interrupts. The service routine can poll registers attached to the GPIO bus and dispatch to second-level service handlers if needed and prioritise interrupts in this way.

To re-enable interrupts, the software must execute an RTI instruction (Return from Interrupt). RTI behaves like RTS (Return from Subroutine), but clears the BUSY flag and uses a separate pointer register (IA) to save/restore the return address. As long as the BUSY flag remains set, downstream service routines or other code will not be interrupted by interrupts.

The interrupt service subroutine, once it returns, resumes execution at the point in code where the interrupt occurred.

You can manually set BUSY by executing a TRAP instruction. Regular calls to trap destinations (by other instructions than TRAP) do not have this side effect.

PART 2 - I/O Functionality (Dedicated Registers and Instructions)

Device Selection

The CPU can control serial and parallel communication with external components. This is facilitated by dedicated hardware-registers and instructions.

E (Enable) register

The 8-bit E register is used to control the select state of devices attached to the serial or parallel bus lines. To this end, the register is divided into two independent four-bit groups for device selection.

Each four-bit group (L for the low-order, H for the high-order) drives a 4-to-16 line decoder, which maps the bit pattern encoded by that group to 1 of 16 possible, mutually exclusive select signals (SL0-15 and SH0-15) per group.

Special Purpose Selectors

Select signals SL0 and SH0 are reserved, and select a NULL device (“nothing”). These signals are selected on power-up or reset.

SL1 corresponds to the internal POR register output enable signal (POE). SH1 corresponds to the internal PIR register latch enable signal (PLE).

In order to latch the current value of the GPIO bus into the PIR, the PLE signal must be set by the high-order nybble of E. Selecting the POE signal by the low-order nybble of E enables the output of the POR register onto the GPIO bus.

All remaining selectors can be used freely.

Communication Registers

SOR (Serial Output Register)

A write-only parallel-to-serial shift register for serialising an output byte, modelled after a 74HC165 chip. Writing an output value for serialisation is done by writing the value into register S.

This value is clocked out/serialized by pulsing the SCLK clock line. This is achieved by alternating SCL and SCH instructions (set clock low/high).

SCL-SCH-SCL generates a positive clock edge. SCH-SCL-SCH generates an inverted clock. Eight clock cycles are required to send-out a byte.

SIR (Serial Input Register)

A read-only serial-to-parallel shift register for de-serialising an incoming bit stream into an input byte, modelled after a 74HC595 chip.

Receiving a byte is done by executing the SCL/SCH instructions eight times as explained above. Reading the deserialised input byte is done by reading register S.

POR (Parallel Output Register)

A tri-state register with 8-bit parallel output, modelled after a 74HC574 chip. Writing an output byte onto the parallel bus is a two step process. First, the data byte must be latched into the register by writing it into P. Then, the register output must be enabled by selecting POE in the E register, as described above.

PIR (Parallel Input Register)

A read-only 8-bit parallel input register, modelled after a 74HC574 chip. Latching the current 8-bit value of the parallel bus into the register is done by selecting PLE in E. The latched data byte can then be read from P. The bus operates in weak pull-down mode, so when all bus-devices are in tri-state mode, a zero value is registered.

Communication Instructions

SERIAL

The following instructions contained in the SYS group operate on the communication registers:

SSI (Shift Serial In)

This instruction receives a serial bit via the serial input line. It then shifts SIR left and sets its least significant bit (LSB) to the received bit state.

SSO (Shift Serial Out)

This instruction outputs the most significant bit (MSB) of SOR onto the serial output line and then shifts SOR left.

SCH (Serial clock high)

This instruction sets the clock line to HIGH.

SCL (Serial clock low)

This instruction sets the clock line to LOW.

PARALLEL

The CPU interfaces to an external bidirectional 8-bit wide bus (GPIO bus).

It can communicate on this bus by writing a data byte into P (POR register), and then enabling POE in the E register by setting its lower nybble to 1. Setting the bit to 1 switches the POR from tri-state output to active output, so that the byte value is output on the bus lines.

While the output is active, other devices on the bus can read the data byte. Usually, such a device will be controlled or synchronised by the Myth controller. It does this by enabling or disabling latches or outputs of the required device in E as explained above. This generates output signals made available to external devices on the micro-controller pins. Exactly two output signals (one per nybble in E) can be active at the same time.

Deselecting POE in E again (setting the low-order nybble to a value different from 1) tristates the POR output, so that other devices can put data bytes on the GPIO bus.

Enabling PLE in E (setting the high-order nybble to 1) latches a data byte into the PIR. This byte can then be read from the P register.

Once a data byte has been read, the PIR input should be deselected again in E by setting the high-order nybble to a value different from 1.

Serial Communication

The Serial Peripheral Interface (SPI) protocol can be implemented using the device enable register E, serial registers SIR and SOR, and instructions SCL, SCH, SSI, and SSO.

Device Selection

Before communicating with a specific device connected to the serial bus, the corresponding selector bit representing the device must be set in the E register.

Data Transmission

To transmit data to the selected device, the processor writes a data byte (8 bits) to be serialised for output into the SOR (Serial Output) register.

The SSO (Serial Shift Out) instruction is then used, which clocks the serial output shift register and produces a data bit on the MOSI line. Using the instruction sequence SCL SCH SCL (Serial Clock Low/High), a positive edge clock pulse is generated.

As each bit is shifted out, it is sent to the selected device through the serial bus. The passive device processes the transmitted bit and the cycle repeats.

Data Reception

To receive data from an external device, the SSI (Serial Shift In) instruction is used. It clocks the serial input shift register, allowing the processor to receive one bit of data at a time from the selected device via the MISO line. The received data can then be read from the S register. Clocking is done as above.

CPOL (Clock Polarity)

The CPOL parameter determines the idle state of the clock signal. The controller provides signals SCL (Serial Clock Low) and SCH (Serial Clock High) instructions which can be used to control the clock signal’s state.

To configure CPOL=0 (clock idles low), execute SCL to set the clock signal low during the idle state. To configure CPOL=1 (clock idles high), execute SCH to set the clock signal high during the idle state.

CPHA (Clock Phase)

The CPHA parameter determines the edge of the clock signal where data is captured or changed. The Myth controller provides instructions SSI (Serial shift in) and SSO (Serial shift out) to control data transfer on each clock transition.

To configure CPHA=0 (data captured on the leading edge), execute SSI before the clock transition to capture the incoming data. To configure CPHA=1 (data captured on the trailing edge), execute SSI after the clock transition to capture the incoming data.

Similarly, to transmit data on the leading or trailing edge, execute SSO before or after the clock transition, respectively.

Device Deselection

After data transmission is complete, the selected device needs to be deselected to allow other devices to communicate on the bus. This is done by updating the E register with the appropriate value.

Part 3 - Programming

Opcode Format

Operation codes fall into 6 format groups, which are decodable using a priority encoder.

                  -- Opcode Bits --
                     MSB     LSB
all 0: OPC_SYS       00000   xxx    See table @ SYS decoder
 else: OPC_BOP       00001   xxx    See table @ BOP decoder
       OPC_ALU       0001   xxxx    See table @ ALU
       OPC_TRAP      001   xxxxx    b0-4: DESTPAGE
       OPC_GETPUT    01 xx x xxx    b0-2: OFFS, b3: GET/PUT, b4-5: REG
       OPC_PAIR      1  xxx xxxx    b0-3: DST, b4-6: SRC

Assembler

Labels

  • Address labels are defined using identifiers prefixed with an at-sign (@labelname). A decimal number before the at sign (123@labelname) sets the page-index for emitting object code to that number, if the labelname is all-uppercase, or sets the page-offset to that number if the labelname contains lowercase letters.

  • A label may optionally be followed by a colon (:), like @FOO: — this marks it as a global label. Global labels are inserted into the native symbol table (inside the resulting binary image).

  • Labels must be unique unless they are a single lowercase letter (@a, @b, etc.), which may be defined multiple times (for generic labels such as short jumps). When defined multiple times, the nearest matching label in either direction will be used, see below.

Label References

  • Use <label for a backward reference to the closest matching label earlier in the file.

  • Use >label for a forward reference to the closest matching label later in the file.

  • Use #label for a general, first-match search.

Trap Call Syntax

  • The asterisk is used for trap call references: *label or *123, or *1Fh etc. all assemble trap instructions.

Constants (Data Labels)

  • You can define a constant using name=value. The value can be any valid number literal (see below).

  • Defined constants can be used later by referencing their name in the source code.

  • A colon after the label name (name:=value) defines a global label.

Special Tokens

  • PAGE — Page-index of the current instruction.

  • OFFSET — Page-offset of the current instruction.

Literals

  • Decimal: e.g., 42, -5

  • Hexadecimal: Suffix h (e.g., 2Ah, 0FFh)

  • Binary: Prefix b, underscores allowed (e.g., b1010_0001)

  • Character literal: Single character in quotes (e.g., 'A')

  • String literal: Double quotes, may contain spaces (e.g., " hello world ")

Mnemonics

  • Mnemonics are case insensitive — addc, ADDC, and AddC are all valid.

  • Example mnemonics: AND, ADDC, RET, 2r

Comments

  • Any text after a semicolon (;) is a comment.

  • Text enclosed in parentheses (A comment) is also treated as a comment — including the parentheses themselves. Useful for commenting out just one or two mnemonics.

Phrasing

Any assembly token can be followed by a comma (,) or a dot (.), and dashes are ignored.

fa 1, fa 2 (pushed onto A) - ADDC. ; These are fine

Syntax-Highlighting in Sublime

Place the syntax and color scheme definition files from the repo inside the folder: /Users/???/Library/Application Support/Sublime Text 3/Packages/User' (macOS).

Then in Sublime, press CMD-Shift-P. In the dialog, navigate to: Preferences: Settings- Syntax Specific and paste the following snippet.

// These settings override both User and Default settings for the myth-my8 syntax
{
    // Sets the colors used within the text area.
    // The value "auto" will switch between the "light_color_scheme" and
    // "dark_color_scheme" based on the operating system appearance.
    "color_scheme": "myth-dark-my8.sublime-color-scheme"
}

There is also a light theme (myth-light-my8.sublime-color-scheme) in the repo.

“My”-Tool for Native Development

The command line tool my (for Myth) can be used to set registers, print memory read-outs, and for assembling and running assembler code. The source-code for my is in the util folder of the Myth GitHub repo.

On each invocation, the program reads in a complete 64k RAM image (default name: ram.bin that is used as memory for a virtual Myth CPU. You can create this file by running my -N <filename>. Before the tool terminates, the (possibly modified) RAM is persisted back into the image file.

You can assemble a source file into this image with my -la <filename>. The l option in this example prints an additional assembly listing including the emitted object code by source line.

A memory read-out (dump) can be printed out with my -b 2 -d 2000h. This example prints 16 data bytes stored starting at address 0x2000, listing them in three number bases. See my -h for more options.

You can set individual CPU registers using my -w regname=value. The command my -p prints out a text block of all registers (“pulley”).

Individual instructions can be executed with my -o mnemonic, and the virtual CPU can be instructed to run n cycles with my -r n (for single-step only use my -s). Be mindful of setting C and PC to suitable values!

There is a special dialog mode, when my is run with a command line where the first character is not a ‘-’ (not a command line option). The command line (max 127 ascii bytes) is then copied into the RAM image at 0x2100 and the CPU is run in order to have it write an output string (max 127 bytes) at 0x2180. The CPU is stopped and the tool terminates as soon as the output string becomes not NULL, or once 64k cycles have elapsed. You can then run my -m to try for another 64k cycles.

Example my session:

The example sets the accumulator registers A and X, and executes the ADDC instruction, which produces the sum of A and X in A, and the carry generated by the addition into X.

(base) ➜  myth-tool git:(main) ✗ my -p

C:00 PC:A7          E:00(0000_0000) E_OLD:00(0000_0000)
SCLK:0 MISO:0 MOSI:0      SIR:00 SOR:00   PIR:00 POR:00
A:09(+009,0000_1001)  X:00(+000,0000_0000)   D:00  L:00
BO:0000  P1:0000 P2:0000 P3:0000 P4:0000   KEY:00 L0:00
L1:00(+000) L2:00(+000) L3:00(+000) L4:00(+000)   IRQ:0
L5:00(+000) L6:00(+000) L7:00(+000) L8:00(+000)  BUSY:0

(base) ➜  myth-tool git:(main) ✗ my -w a=4
(base) ➜  myth-tool git:(main) ✗ my -w x=253
(base) ➜  myth-tool git:(main) ✗ my -p

C:00 PC:A7          E:00(0000_0000) E_OLD:00(0000_0000)
SCLK:0 MISO:0 MOSI:0      SIR:00 SOR:00   PIR:00 POR:00
A:04(+004,0000_0100)  X:FD(-003,1111_1101)   D:00  L:00
BO:0000  P1:0000 P2:0000 P3:0000 P4:0000   KEY:00 L0:00
L1:00(+000) L2:00(+000) L3:00(+000) L4:00(+000)   IRQ:0
L5:00(+000) L6:00(+000) L7:00(+000) L8:00(+000)  BUSY:0

(base) ➜  myth-tool git:(main) ✗ my -o ADDC
(base) ➜  myth-tool git:(main) ✗ my -p

C:00 PC:A7          E:00(0000_0000) E_OLD:00(0000_0000)
SCLK:0 MISO:0 MOSI:0      SIR:00 SOR:00   PIR:00 POR:00
A:01(+001,0000_0001)  X:01(+001,0000_0001)   D:00  L:00
BO:0000  P1:0000 P2:0000 P3:0000 P4:0000   KEY:00 L0:00
L1:00(+000) L2:00(+000) L3:00(+000) L4:00(+000)   IRQ:0
L5:00(+000) L6:00(+000) L7:00(+000) L8:00(+000)  BUSY:0

Preliminary ROM image (“Firmware”)

Reserved Pages for My-Tool

Page 0 - Interrupts

Due to how interrupts work, code execution after power-on, reset or when an interrupt request is accepted, starts at address 0h. Page 0 should be reserved for handling these various cases, particularly the main interrupt service handler.

Pages 1-31 Trap Handlers

The TRAP instruction (*n) is a single-instruction subroutine call to an immediate address encoded in the opcode using 5 bits. The range of call target pages is therefore 0..31. Trap 0 is equivalent to causing an interrupt to happen: doing this calls page 0 and sets the BUSY flag.

Page 32 (2000h) - Register Store

My-Tool persists the CPU registers in the 64k firmware image starting at page index 32, address 2000h (see my-tool project files), with the whole page being reserved.

Page 33 (2100h) - Text Buffers

Further, page 33 is used for two text buffers which my-tool used to communicate with the Myth VM: In dialog mode, a maximum of 127 bytes of the command line text is stored as a zero terminated string at address 2100h (input buffer). The VM is expected to respond by writing a zero terminated string not exceeding 127 characters into the output buffer at address 2180h.

The first byte of the output buffer should be monitored; when it becomes non-zero, this is a termination/ready signal from the VM. This implies that the output string should be written, with the first character last, overwriting the initial zero at 2180h put there by My-Tool before running the VM.

Page 34 (2200h) - Key

The firmware currently sets K to page 34, so that xK instructions set the B:O pointer to 34:x. The xK instruction was implemented to have quick access to one “key” page of frequently used system variables.

(A table of these will be maintained here)

Page 35 (2300h) - Stack and P3 Pointer

The amenity pointer P3 is currently reserved as a system wide stack pointer, which serves as parameter stack pointer and threading token pointer, and it is set to 23FFh, growing towards lower addresses. If your routines use it for other purposes, you should restore its value on return.

Page 36 (2400h) - Threading Stack

Amenity pointer P2 is currently reserved as a system wide threading stack pointer, set to 24FFh.

P1 can be used as a scratch register for the base pointer (P:O).

Remarks

  • Local Page Frames Be aware that the local frame pointer in L decrements during subroutine nesting. Subroutines use the highest 9 bytes (F7h - FFh) for local variables L0-L8. The address of L0 is loaded into B:O by the LOCAL instruction. L1-L8 are accessible using GETPUT-instructions. In principal, the whole local page is available to the currently running subroutine. This implies a tradeoff between how much data you store in your local frames, and how long your subroutines are since they must stay clear of the local storage.

  • Threaded Code This is particular to this firmware only, but the subroutines implementing threaded code will use 80h as the implied page offset for interpreter-called code.

Symbol Table

The assembler outputs a “Global” symbol table. Entries in this table are formed as follows:

  • Link-Byte: Relative byte offset to the next entry, or zero for end-of-table.

  • Info-Byte: High-order nybble encodes the symbol type, low order is the length in bytes of the symbol name -1; hence a maximum length of 16.

  • Name-String Name of the symbol

  • Zero: Zero for string termination of the name string

  • Data-Bytes: Variable number of data-bytes, corresponding to the type of the symbol

The following types are currently used:

  • Assembly label: Type=1, data-bytes: none

  • Mnemonic: Type=2, data-bytes: opcode

Debugging Native Code

Don’t forget that you can place hooks directly into cpu.c for instance. As a temporary debugging aid, writing into E (_E) is caught in cpu.c und causes My-Tool to print-out a register dump.

Parameter passing

Use the accumulator (AX) for primary arguments in general.

There is a “hidden” local variable shortcut “L0”. You can obtain a pointer to this memory location by executing the instruction “LOCAL”. Local sets B to the Local page, and O to F7h, the byte offset just below L1. Then use MxM instructions such as “am” to read or store into the L0 variable:

; Store number 4 in L0:
(fa 2 shl,) local am.

Tables

Opcode Matrix

       x0    x1    x2    x3    x4    x5    x6    x7    x8    x9    xA    xB    xC    xD    xE    xF
0x    NOP   SSI   SSO   SCL   SCH   RTS   RTI   COR  P1BO  BOP1  P2BO  BOP2  P3BO  BOP3  IABO  BOIA
1x    NOT   ALX   AEX   AGX   AND   IOR   EOR    XA    AX  SWAP   SHL   SHR   ASR  ADDC  ADDV  SUBB
2x     *0    *1    *2    *3    *4    *5    *6    *7    *8    *9   *10   *11   *12   *13   *14   *15
3x    *16   *17   *18   *19   *20   *21   *22   *23   *24   *25   *26   *27   *28   *29   *30   *31
4x     1b    2b    3b    4b    5b    6b    7b    8b    b1    b2    b3    b4    b5    b6    b7    b8
5x     1o    2o    3o    4o    5o    6o    7o    8o    o1    o2    o3    o4    o5    o6    o7    o8
6x     1a    2a    3a    4a    5a    6a    7a    8a    a1    a2    a3    a4    a5    a6    a7    a8
7x     1d    2d    3d    4d    5d    6d    7d    8d    d1    d2    d3    d4    d5    d6    d7    d8
8x     fc   KEY    fb    fo    fa    fd    fs    fp    fe    fk    fu    fw    fj    fh    fz    fn
9x     mc  CODE    mb    mo    ma    md    ms    mp    me    mk    mu    mw    mj    mh    mz    mn
Ax     bc    bm LOCAL    bo    ba    bd    bs    bp    be    bk    bu    bw    bj    bh    bz    bn
Bx     oc    om    ob LEAVE    oa    od    os    op    oe    ok    ou    ow    oj    oh    oz    on
Cx     ac    am    ab    ao ENTER    ad    as    ap    ae    ak    au    aw    aj    ah    az    an
Dx     dc    dm    db    do    da   INC    ds    dp    de    dk    du    dw    dj    dh    dz    dn
Ex     sc    sm    sb    so    sa    sd   DEC    sp    se    sk    su    sw    sj    sh    sz    sn
Fx     pc    pm    pb    po    pa    pd    ps    EA    pe    pk    pu    pw    pj    ph    pz    pn

Opcode Descriptions

Group SYS

0x00: NOP   Pass the turn (no operation)
0x01: SSI   Shift serial bit in
0x02: SSO   Shift serial bit out
0x03: SCL   Set serial clock low
0x04: SCH   Set serial clock high
0x05: RTS   Return from subroutine
0x06: RTI   Return from interrupt
0x07: COR   Coroutine Jump
Group BOP

0x08: P1BO  Copy pointer P1 into B:O
0x09: BOP1  Copy B:O into pointer P1

0x0A: P2BO  Copy pointer P2 into B:O
0x0B: BOP2  Copy B:O into pointer P2

0x0C: P3BO  Copy pointer P3 into B:O
0x0D: BOP3  Copy B:O into pointer P3

0x0E: IABO  Copy pointer IA into B:O
0x0F: BOIA  Copy B:O into pointer IA
Group ALU

0x10: NOT   Set A to one's complement of A , X unchanged
0x11: ALX   Flag (A<X) in A (255 if true, 0 if false), X unchanged
0x12: AEX   Flag (A==X) in A (255 if true, 0 if false), X unchanged
0x13: AGX   Flag (A>X) in A (255 if true, 0 if false), X unchanged
0x14: AND   Set A to (A AND X), X unchanged
0x15: IOR   Set A to (A OR X), X unchanged
0x16: EOR   Set A to (A XOR X), X unchanged
0x17: XA    Set A equal to X, X unchanged
0x18: AX    Set X equal to A
0x19: SWAP  Swap A and X
0x1A: SHL   Shift A left, result in A, set X to previous MSB of A as LSB (0 or 1)
0x1B: SHR   Shift A right logically, result in A, set X to previous LSB of A as MSB (0 or 80h)
0x1C: ASR   Shift A right arithmetically, set X to previous LSB of A as MSB (0 or 80h)
0x1D: ADDC  Add A to X, result in A, CARRY bit in X (0 or 1)
0x1E: ADDV  Add A to X, result in A, OVERFLOW flag in X (255 if OVF, else 0)
0x1F: SUBB  Subtract A from X, result in A, BORROW bit in X (0 or 1)
Group TRAP

0x20: *0    Trap call to page 0, offset 0 - Set BUSY flag
0x21: *1    Trap call to page 1, offset 0
0x22: *2    Trap call to page 2, offset 0
0x23: *3    Trap call to page 3, offset 0
0x24: *4    Trap call to page 4, offset 0
0x25: *5    Trap call to page 5, offset 0
0x26: *6    Trap call to page 6, offset 0
0x27: *7    Trap call to page 7, offset 0
0x28: *8    Trap call to page 8, offset 0
0x29: *9    Trap call to page 9, offset 0
0x2A: *10   Trap call to page 10, offset 0
0x2B: *11   Trap call to page 11, offset 0
0x2C: *12   Trap call to page 12, offset 0
0x2D: *13   Trap call to page 13, offset 0
0x2E: *14   Trap call to page 14, offset 0
0x2F: *15   Trap call to page 15, offset 0
0x30: *16   Trap call to page 16, offset 0
0x31: *17   Trap call to page 17, offset 0
0x32: *18   Trap call to page 18, offset 0
0x33: *19   Trap call to page 19, offset 0
0x34: *20   Trap call to page 20, offset 0
0x35: *21   Trap call to page 21, offset 0
0x36: *22   Trap call to page 22, offset 0
0x37: *23   Trap call to page 23, offset 0
0x38: *24   Trap call to page 24, offset 0
0x39: *25   Trap call to page 25, offset 0
0x3A: *26   Trap call to page 26, offset 0
0x3B: *27   Trap call to page 27, offset 0
0x3C: *28   Trap call to page 28, offset 0
0x3D: *29   Trap call to page 29, offset 0
0x3E: *30   Trap call to page 30, offset 0
0x3F: *31   Trap call to page 31, offset 0
Group GETPUT

0x40: 1b    Load B from L1 (M[L:F8h])
0x41: 2b    Load B from L2 (M[L:F9h])
0x42: 3b    Load B from L3 (M[L:FAh])
0x43: 4b    Load B from L4 (M[L:FBh])
0x44: 5b    Load B from L5 (M[L:FCh])
0x45: 6b    Load B from L6 (M[L:FDh])
0x46: 7b    Load B from L7 (M[L:FEh])
0x47: 8b    Load B from L8 (M[L:FFh])

0x48: b1    Store B into L1 (M[L:F8h])
0x49: b2    Store B into L2 (M[L:F9h])
0x4A: b3    Store B into L3 (M[L:FAh])
0x4B: b4    Store B into L4 (M[L:FBh])
0x4C: b5    Store B into L5 (M[L:FCh])
0x4D: b6    Store B into L6 (M[L:FDh])
0x4E: b7    Store B into L7 (M[L:FEh])
0x4F: b8    Store B into L8 (M[L:FFh])

0x50: 1o    Load O from L1 (M[L:F8h])
0x51: 2o    Load O from L2 (M[L:F9h])
0x52: 3o    Load O from L3 (M[L:FAh])
0x53: 4o    Load O from L4 (M[L:FBh])
0x54: 5o    Load O from L5 (M[L:FCh])
0x55: 6o    Load O from L6 (M[L:FDh])
0x56: 7o    Load O from L7 (M[L:FEh])
0x57: 8o    Load O from L8 (M[L:FFh])

0x58: o1    Store O into L1 (M[L:F8h])
0x59: o2    Store O into L2 (M[L:F9h])
0x5A: o3    Store O into L3 (M[L:FAh])
0x5B: o4    Store O into L4 (M[L:FBh])
0x5C: o5    Store O into L5 (M[L:FCh])
0x5D: o6    Store O into L6 (M[L:FDh])
0x5E: o7    Store O into L7 (M[L:FEh])
0x5F: o8    Store O into L8 (M[L:FFh])

0x60: 1a    Load A from L1 (M[L:F8h])
0x61: 2a    Load A from L2 (M[L:F9h])
0x62: 3a    Load A from L3 (M[L:FAh])
0x63: 4a    Load A from L4 (M[L:FBh])
0x64: 5a    Load A from L5 (M[L:FCh])
0x65: 6a    Load A from L6 (M[L:FDh])
0x66: 7a    Load A from L7 (M[L:FEh])
0x67: 8a    Load A from L8 (M[L:FFh])

0x68: a1    Store A into L1 (M[L:F8h])
0x69: a2    Store A into L2 (M[L:F9h])
0x6A: a3    Store A into L3 (M[L:FAh])
0x6B: a4    Store A into L4 (M[L:FBh])
0x6C: a5    Store A into L5 (M[L:FCh])
0x6D: a6    Store A into L6 (M[L:FDh])
0x6E: a7    Store A into L7 (M[L:FEh])
0x6F: a8    Store A into L8 (M[L:FFh])

0x70: 1d    Load D from L1 (M[L:F8h])
0x71: 2d    Load D from L2 (M[L:F9h])
0x72: 3d    Load D from L3 (M[L:FAh])
0x73: 4d    Load D from L4 (M[L:FBh])
0x74: 5d    Load D from L5 (M[L:FCh])
0x75: 6d    Load D from L6 (M[L:FDh])
0x76: 7d    Load D from L7 (M[L:FEh])
0x77: 8d    Load D from L8 (M[L:FFh])

0x78: d1    Store D into L1 (M[L:F8h])
0x79: d2    Store D into L2 (M[L:F9h])
0x7A: d3    Store D into L3 (M[L:FAh])
0x7B: d4    Store D into L4 (M[L:FBh])
0x7C: d5    Store D into L5 (M[L:FCh])
0x7D: d6    Store D into L6 (M[L:FDh])
0x7E: d7    Store D into L7 (M[L:FEh])
0x7F: d8    Store D into L8 (M[L:FFh])
Group PAIR

0x80: FC    Take M[C:PC++] as page-index, load the index into C, set PC to 0. Save return pointer into B:O. Decrement L
0x81: KEY   Copy register B into K
0x82: FB    Take M[C:PC++] into B
0x83: FO    Take M[C:PC++] into O
0x84: FA    Push M[C:PC++] into Acc
0x85: FD    Take M[C:PC++] into D
0x86: FS    Take M[C:PC++] into SOR
0x87: FP    Take M[C:PC++] into POR
0x88: FE    Take M[C:PC++] into E, sets device enable signals
0x89: FK    Take M[C:PC++] into O, load K into B
0x8A: FU    Take M[C:PC++] as 8-bit signed number and add it to 16-bit pointer B:O
0x8B: FW    Take M[C:PC++] as page offset and store it into PC - while register D is not zero. In either case, decrement D
0x8C: FJ    Take M[C:PC++] as page offset and store it into PC - always
0x8D: FH    Take M[C:PC++] as page offset and store it into PC - if A is not equal to zero
0x8E: FZ    Take M[C:PC++] as page offset and store it into PC - if A is equal to zero
0x8F: FN    Take M[C:PC++] as page offset and store it into PC - if A is negative (has bit 7 set)
0x90: MC    Take M[B:O] as page-index, load the index into C, set PC to 0. Save return pointer into B:O. Decrement L
0x91: CODE  Copy pointer C:PC into B:O
0x92: MB    Take M[B:O] into B
0x93: MO    Take M[B:O] into O
0x94: MA    Push M[B:O] into Acc
0x95: MD    Take M[B:O] into D
0x96: MS    Take M[B:O] into SOR
0x97: MP    Take M[B:O] into POR
0x98: ME    Take M[B:O] into E, sets device enable signals
0x99: MK    Take M[B:O] into O, load K into B
0x9A: MU    Take M[B:O] as 8-bit signed number and add it to 16-bit pointer B:O
0x9B: MW    Take M[B:O] as page offset and store it into PC - while register D is not zero. In either case, decrement D
0x9C: MJ    Take M[B:O] as page offset and store it into PC - always
0x9D: MH    Take M[B:O] as page offset and store it into PC - if A is not equal to zero
0x9E: MZ    Take M[B:O] as page offset and store it into PC - if A is equal to zero
0x9F: MN    Take M[B:O] as page offset and store it into PC - if A is negative (has bit 7 set)
0xA0: BC    Take B as page-index, load the index into C, set PC to 0. Save return pointer into B:O. Decrement L
0xA1: BM    Take B into M[B:O]
0xA2: LOCAL Copy pointer L:F7h (L0) into B:O
0xA3: BO    Take B into O
0xA4: BA    PushB into Acc
0xA5: BD    Take B into D
0xA6: BS    Take B into SOR
0xA7: BP    Take B into POR
0xA8: BE    Take B into E, sets device enable signals
0xA9: BK    Take B into O, load K into B
0xAA: BU    Take B as 8-bit signed number and add it to 16-bit pointer B:O
0xAB: BW    Take B as page offset and store it into PC - while register D is not zero. In either case, decrement D
0xAC: BJ    Take B as page offset and store it into PC - always
0xAD: BH    Take B as page offset and store it into PC - if A is not equal to zero
0xAE: BZ    Take B as page offset and store it into PC - if A is equal to zero
0xAF: BN    Take B as page offset and store it into PC - if A is negative (has bit 7 set)
0xB0: OC    Take O as page-index, load the index into C, set PC to 0. Save return pointer into B:O. Decrement L
0xB1: OM    Take O into M[B:O]
0xB2: OB    Take O into B
0xB3: LEAVE Increment L
0xB4: OA    Push O into Acc
0xB5: OD    Take O into D
0xB6: OS    Take O into SOR
0xB7: OP    Take O into POR
0xB8: OE    Take O into E, sets device enable signals
0xB9: OK    Take O into O, load K into B
0xBA: OU    Take O as 8-bit signed number and add it to 16-bit pointer B:O
0xBB: OW    Take O as page offset and store it into PC - while register D is not zero. In either case, decrement D
0xBC: OJ    Take O as page offset and store it into PC - always
0xBD: OH    Take O as page offset and store it into PC - if A is not equal to zero
0xBE: OZ    Take O as page offset and store it into PC - if A is equal to zero
0xBF: ON    Take O as page offset and store it into PC - if A is negative (has bit 7 set)
0xC0: AC    Take A as page-index, load the index into C, set PC to 0. Save return pointer into B:O. Decrement L
0xC1: AM    Take A into M[B:O]
0xC2: AB    Take A into B
0xC3: AO    Take A into O
0xC4: ENTER Decrement L
0xC5: AD    Take A into D
0xC6: AS    Take A into SOR
0xC7: AP    Take A into POR
0xC8: AE    Take A into E, sets device enable signals
0xC9: AK    Take A into O, load K into B
0xCA: AU    Take A as 8-bit signed number and add it to 16-bit pointer B:O
0xCB: AW    Take A as page offset and store it into PC - while register D is not zero. In either case, decrement D
0xCC: AJ    Take A as page offset and store it into PC - always
0xCD: AH    Take A as page offset and store it into PC - if A is not equal to zero
0xCE: AZ    Take A as page offset and store it into PC - if A is equal to zero
0xCF: AN    Take A as page offset and store it into PC - if A is negative (has bit 7 set)
0xD0: DC    Take D as page-index, load the index into C, set PC to 0. Save return pointer into B:O. Decrement L
0xD1: DM    Take D into M[B:O]
0xD2: DB    Take D into B
0xD3: DO    Take D into O
0xD4: DA    Push D into Acc
0xD5: INC   Increment A
0xD6: DS    Take D into SOR
0xD7: DP    Take D into POR
0xD8: DE    Take D into E, sets device enable signals
0xD9: DK    Take D into O, load K into B
0xDA: DU    Take D as 8-bit signed number and add it to 16-bit pointer B:O
0xDB: DW    Take D as page offset and store it into PC - while register D is not zero. In either case, decrement D
0xDC: DJ    Take D as page offset and store it into PC - always
0xDD: DH    Take D as page offset and store it into PC - if A is not equal to zero
0xDE: DZ    Take D as page offset and store it into PC - if A is equal to zero
0xDF: DN    Take D as page offset and store it into PC - if A is negative (has bit 7 set)
0xE0: SC    Take SIR as page-index, load the index into C, set PC to 0. Save return pointer into B:O. Decrement L
0xE1: SM    Take SIR into M[B:O]
0xE2: SB    Take SIR into B
0xE3: SO    Take SIR into O
0xE4: SA    Push SIR into Acc
0xE5: SD    Take SIR into D
0xE6: DEC   Decrement A
0xE7: SP    Take SIR into POR
0xE8: SE    Take SIR into E, sets device enable signals
0xE9: SK    Take SIR into O, load K into B
0xEA: SU    Take SIR as 8-bit signed number and add it to 16-bit pointer B:O
0xEB: SW    Take SIR as page offset and store it into PC - while register D is not zero. In either case, decrement D
0xEC: SJ    Take SIR as page offset and store it into PC - always
0xED: SH    Take SIR as page offset and store it into PC - if A is not equal to zero
0xEE: SZ    Take SIR as page offset and store it into PC - if A is equal to zero
0xEF: SN    Take SIR as page offset and store it into PC - if A is negative (has bit 7 set)
0xF0: PC    Take PIR as page-index, load the index into C, set PC to 0. Save return pointer into B:O. Decrement L
0xF1: PM    Take PIR into M[B:O]
0xF2: PB    Take PIR into B
0xF3: PO    Take PIR into O
0xF4: PA    Push PIR into Acc
0xF5: PD    Take PIR into D
0xF6: PS    Take PIR into SOR
0xF7: EA    Push E into Acc
0xF8: PE    Take PIR into E, sets device enable signals
0xF9: PK    Take PIR into O, load K into B
0xFA: PU    Take PIR as 8-bit signed number and add it to 16-bit pointer B:O
0xFB: PW    Take PIR as page offset and store it into PC - while register D is not zero. In either case, decrement D
0xFC: PJ    Take PIR as page offset and store it into PC - always
0xFD: PH    Take PIR as page offset and store it into PC - if A is not equal to zero
0xFE: PZ    Take PIR as page offset and store it into PC - if A is equal to zero
0xFF: PN    Take PIR as page offset and store it into PC - if A is negative (has bit 7 set)

Instructions of Type PAIR

Pair instructions are best explained by looking at their mnemonics. A pair mnemonic consists of two letters, the first being the source register, the second being the target register. Pair instructions copy a value from their source to their target.

There are 8 sources: F, M, B, O, A, D, S, P.

All sources can also be targets, except F.

In addition to these 7 sources we can use as targets, there are 9 additional targets which in turn can’t be sources: C, E, K, U, W, J, H, Z, N.

So in total, there are 16 targets.

You can now combine these to form “pairs”: FK (Copy F to K), AE (Copy A to E), and so forth.

Regarding pair instructions, there is one additional rule, and one exception to this rule to remember.

Scrounging

The rule is that when both letters (source and target) are the same, that means something different! - another instruction entirely.

This is called “scrounging” the original pair. The simplified reason for this feature is that copying the value from one register into itself (AA - copy A to A, for example) generally has no effect, so we are doing something else instead!

Now, the exception to this rule of scrounging, just something to remember, is that since F is not a target register, we scrounge FM instead, even though F and M are not the same letters. The decision for implementing this was not arbitrary: F_ fetches the next byte in the instruction stream, a memory operation, and _M stores into memory. The design of the CPU does not permit these two operations to happen in the same instruction cycle.

Here are all the scrounged combinations, and what they “redirect” to, which instruction they do instead:

FM: KEY, MM: CODE, BB: LOCAL, OO: LEAVE, AA: ENTER, DD: INC, SS: DEC, PP: EA

Each of these will be explained in the appropriate section.

Effects

Some of the sources and targets are what are called effects. Effect means that instead of being a little physical storage location (called a “register”), reading or writing it triggers a specific side effect. It has an action. We could say that effects are just names for their actions, instead of names for a storage cell or register.

The actions can be quite surprising, they can enable hardware, or jump to a different subroutine, so be prepared, we will explain them all as we go on.

We are now going to take a systematic look at each of the 8 sources.

F

F stands for fetch. F is an effect, and it can’t be used as a target. Its action is that when you read from it, the computer looks at the byte that would be the next instruction in memory. It reads that byte and uses it as the source value. Then it skips over the byte, when it looks for the next instruction. This process is called “fetching”, and the byte is called a literal. The way you use this in your program is like so: “FB 2”, for example. The computer sees the FB pair instruction (copy F to B), and this triggers the read action of F. The next instruction would be the literal 2, but F reads and then skips it, instead of fetching it as an instruction in the next cycle. It then takes the 2 as the source value and copies it to B.

M

M stands for memory. M can be a source or a target. This computer has only one memory pointer, in the form of the B:O register pair. This register pair is called the base pointer, and B is called the base register. The O register is called the offset register. To read a sixteen bit address, you have to set B to the high order byte (the “base” address or “page”), and O to the low order byte (the “offset”) of the address. This is best viewed in hexadecimal: Setting B to 20h and O to 30h means setting B:O to the 16-bit address 2030h. To actually access the memory data pointed to by B:O, you then have to use a pair instruction that uses M as source or target. M is an effect. Its actions are as follows: When you read from M, you get as source the value of the memory cell at the 16-bit address formed by B:O as we said before. When you write to M, the written value is stored at the B:O location in memory.

A

A stands for accumulator. A can be source or target. The accumulator is the heart of the computer, and it is both a very important effect and a very important register. It takes its name from the fact that it accumulates results. But the accumulator actually consists of two registers working together. Apart from the “main” accumulator register A, there is its reclusive friend, the X register. X stands for mystery.

You may have noticed that we didn’t mention X when we listed the pair sources or targets. Can we access it somehow? We will explain more about A and X in the section about doing computations with the ALU. For now, let’s just explain one curious thing about A: When you read from it, it’s just a little storage cell with a value, but it has a write-“action”. Writing to it does two things. First, the computer copies A into X. And after that, it stores the source value of your instruction into A. It works like a little push down stack - you “push” the value into A, which in turn pushes its previous value down into X, to save it for later.

So register “A” is both a regular register, and an effect register! Finally, as this seems an appropriate place for it, let’s reveal three of the infamous “scrounge instructions” (remember, these are combinations of equal pair letters: BB, OO, and so one, that are replaced by other , more useful instructions): unspectacularly, INC increments A, DEC decrements A. And then there is EA, which pushes E into A, using A’s action that we have just explained. This instruction is the only way to get to the value of E!

We are getting ahead of ourselves, but for completeness let me spoil it and tell you that there is an “XA” instruction, which only looks like a pair instruction but is really an ALU instruction, for copying the value of X into A.

D

D stands for “down” or “decrement”. D can be source or target. D is a register, just a little storage location. What makes D interesting and is the reason for its name, is that it can count itself down. This happens when you write into _W effect (“while”), which effectively turns D into a loop counter.

S

S stands for serial. S can be source or target. Serial stands for two registers or storage locations, depending on whether you read or write. Reading S gets you what’s in the “serial input register” (SIR), and writing S puts a byte into the “serial output register” (SOR).

P

P stands for parallel. And what has been said for S applies verbatim to P, if you change the letter S for a P: SIR becomes PIR, SOR becomes POR, and so on.

So now that we have finished explaining the source registers and effects, let’s look at the targets, the second letters that occur in each pair instruction. As has been said, the 8 sources we have explained in the last section - except F! - can also be used as target. We will concentrate now on the 9 remaining targets: C, E, K, U, W, H, Z, N. These can’t occur as sources!

_C

Target “C” is another effect register, like A. It acts like a register and stores a value, but also has a powerful effect. C stands for “call”. When you write a value to C, it is understood that this value is the high-byte of a memory address where a subroutine is stored. A subroutine is a program that your code jumps to, and which jumps back to your own program at the exact place where you left off, once it’s done.

It can do this because it remembers the memory address in your program it needs to return to. The “C” target effect helps with that: it stores the high-byte of where the subroutine needs to return to in B (the base) and the low-byte in O (the offset). Then it “calls” the subroutine. Subroutines in this computer always start at the beginning of a page of memory, where the offset is zero. Therefore, when you write a value into C, the program counter (remember: it provides the offset of the next instruction in memory) is set to zero, and the C register is set to the value that is copied from the source of the pair instruction. This has the effect that the next instruction will be the first of the subroutine.

So to resume: when you write a value into the C effect register, the current value of C is copied into B, and PC is copied into O. This is so that the subroutine can return to you. Then comes the actual call: The source value is copied into C, and PC is set to zero.

Now, unless your subroutine is very simple, it will need to store both B and O somewhere (ideally using GETPUT instructions, since they store into a “local” memory region especially made for your subroutine), and then restore the values to B and O just before returning, so the CPU knows where to go.

_E

“E”, which stands for “enable” is another effect register. It stores a value, but it also has an action when you write to it. The bit pattern in the source byte selects or deselects hardware that is connected to your computer. This is explained in the section on input/output. Don’t write to E if you don’t understand yet how it works, just because the hardware may do strange things as a result. But don’t worry, it isn’t that complicated!

_K

The pair target “K”, for “key”, is purely an effect, but it’s tied to the K storage register via one of the infamous “scrounge” instructions we mentioned, aptly called KEY. The KEY instruction copies B into K. But we are here to discuss the target effect “K”. Its action is: Copy K into B, and set O to the source value. So it sets the memory pointer B:O to the address of a particular byte in page K - a shortcut you can use for accessing a table of variables in a less verbose way.

_U

The “U” effect (update) also has an action that involves the B:O pointer. It treats B:O as a 16-bit number and adds the source value to it as a signed number. So essentially it allows you to add or subtract a constant to or from B:O.

W, H, Z, N

The remaining four targets are all effects: W, H, Z and N. All of them are about jumping to a new offset within the memory page you are currently in.

_W

Perhaps the most interesting is W (while). As we mentioned, W works together with D (down-counter) register. When you write a value to W, it is understood that the source value is an address offset in the current program page, a new value that you want to set PC to. Now, the action of W is the following: If D is not zero, set PC to the source value (jump to this location within the page). Otherwise just continue what you are doing. Then, however it turned out, decrement D by 1. So you can see that this is essentially a while loop, with a dedicated register.

_H

The action of the H effect is similar to W, but PC is set to the source value and does the jump, if the accumulator register A is not zero. Otherwise we just continue. The letter H stands for “hot”. That’s a term used in electronics for when a data word has at least one bit that is not zero. On other computers the action of this instruction would be called “branch if not zero”.

_Z

The action of the Z effect is almost identical, but this time we jump if A is in fact zero.

_N

Then, finally, the N effect. N stands for negative, so we will jump if the value in A is negative. A byte is negative, by a very practical convention called the two’s complement, if its highest order bit (bit 7 if count from zero) is one. So this action can be used for checking that bit, too.

Now you know what all the pair instructions do! They make up half of the entire instruction set of this computer, there are just so many combinations - exactly 128: 8 sources x 16 targets, including the “scrounges”.

Getput

Getput instructions are the second largest group of instructions, after pair instructions. There are 64 of them, and just like pairs, their great number is only because there are exactly that many combinations of essentially the same type of instruction.

Local Variables

To understand what getput instructions do, you need to know what the L register is for, and what local variables are. L stands for local, and it contains a page index. Whenever you call a subroutine, that number is decremented, and whenever you return from a subroutine, the number increments.

So when you call a subroutine, it gets its own page, and when it returns, you get the same page number back that you had before the call.

This mechanism allows you to store “your stuff” into your page, and the subroutine can store “its stuff” in its page. This is where the name local comes from, it simply means “local” to a specific subroutine. The advantage of this, and this may not be obvious!, is that when the subroutine returns, your own local variables are guaranteed to be just as you left them, because the subroutine can’t even get to them.

But it also means that if you store, say, the value 2 into L1 and call a subroutine, it will be another, fresh L1, and the 2 will not be there, should you try to read it from within the subroutine. If you want to pass along data to a subroutine, you will need to either store it into a memory location both the calling routine and the subroutine agree on, or in a register. You can’t use B or O though, because the CPU uses it behind the scenes to carry out the call and return. The accumulator is an ideal place for this (it can hold two values, A and X, for example an address pointer).

L1 to L8

The 8 very last bytes at the end of a local page are special: They are called L1 to L8, and they are what getput instructions operate on. So L8 corresponds to the last offset in page L, at offset FFh. Then L7 is at FEh, and so on.

These instructions allow you to load (get) the registers B, O, A, or D from one of these 8 memory locations, or to store (put) these registers there.

The syntax is very simple, you combine the number of the location (1 to 8) with the name of one of the four registers. If the number comes first (1a - L1 into A) it means you want do store the memory variable into the register. If the register comes first (a1 - A into L1) then you’re storing the register into the memory variable.

Instruction LOCAL and L0

Time to disclose three more of the infamous “scrounge” instructions. Let’s start with LOCAL. This instruction sets the B register to L, and the offset to point to the memory cell just below L1. You guessed it, that memory cell is called L0.

So if you want to quickly store away A somewhere, you can say: “LOCAL AM”, and this will copy A to L0. If you are unsure how AM works, read the section on pair instructions. In a nutshell: A_ means “take A as a source”, so M is the target. M is an “effect”, the storage effect. When you write to it, it stores the value at address B:O (the register pair is set by LOCAL, as we said) in memory.

Note that you can obtain the page number stored in L by saying LOCAL and inspecting B.

ENTER and LEAVE

The other two scrounge instructions that belong in this section are ENTER (decrement L), and LEAVE (increment L). The words evoke entering a new local page (like a subroutine does), and leaving the page again (when the subroutine returns).

The L register cannot be set directly to a specific page, but if you really, really want to, you can say “LOCAL BD (puts L into D to know what page it is and then somehow calculate how far it is to where you want it), and then say”ENTER” or “LEAVE” to wind L to the page you need.

If you just say LEAVE inside a subroutine, your local variables will switch to those of the calling subroutine, be careful.

But if you need more local variable, you say ENTER in your subroutine, and get another page, with another set of L0 to L8 (but your previous ones will not be availabe unless you switch back to them doing LEAVE).

Now you can see why there are 64 getput instructions: 8 memory cells (L0-L8), 2 directions (get and put), and 4 registers (B, O, A, D). 8x2x4 combinations = 64.

BOPs

This group of 8 instructions is probably the easiest one to understand. BOP stands for “B:O Pointer”. As you know by now, the B:O register pair has a central place as the only address register in this computer.

In order to use it, you must copy suitable values into B and into O, before you can read or write memory. To reduce repetitive code, and make things faster, the BOP instructions copy two registers at once. The copy action is always between one of four “wide” amenity pointers, P1, P2, P3 and IA, and the B:O register pair.

All the BOP instructions do is the saving and restoring between B:O and these pointers. Example:

**P1BO**: copy P1 into BO.
**BOP1**: copy BO into P1.

Same for P2BO and BOP2, P3BO and BOP3, 
 and IABO and BOIA.

The IA amenity register is special, in exactly the same way that B:O itself is special regarding subroutine calls. While it can be meaningful to read or write it, you must only use it for its intended purpose, or your program can “crash”.

IA stores the return address during trap calls and interrupts, and the RTI instruction relies on this address to be there. Interrupts can happen at any time, if you haven’t disabled them. Let’s say you store a copy of BO into IA. Now, when an interrupt comes along, your program stops right in its tracks, and the address where you are is stored into IA, overwriting the value that you were counting on to be there. When the interrupt is done, while your program does successfully resume where it left off, IA will have changed and still have the left-over address in it. This will happen unbeknownst to you - your program will sometimes work (no interrupt happened), sometimes fail!

Doing Maths and Things to do with Bits

This section is all about the accumulator: register A and its reclusive sister register, X. Well, there is actually a third player that also has an important role: the Arithmetic Logic Unit, or ALU.

Accumulator review: When you read A, you get its value back. When you write A, its current value gets copied into X, and then the new value is stored in A (like a little push-down stack).

ALU

What’s an ALU? It’s a very common, central part in a computer that takes input from registers, performs one of a number of possible operations (the instruction tells it which one it is) and then stores the result back in some register.

In this computer, the ALU sees what is in A and X, so it bases its operation on those two values. There are 16 possible operations, 16 instructions. Let’s look at them in a systematic way.

Something noteworthy is that the first half of the ALU instructions in numerical order just produce a primary result (“the” result) and it gets stored in A. X remains unchanged.

The other half of the instructions gives you the primary result in A (the one you probably want, but also another useful aside, stored in X).

Operations that leave X alone

NOT

This instruction just inverts the bits in A. Those bits that are 0 become 1, those that are 1 become 0. This is also called the “one’s complement”. When you add 1 to it, it becomes “minus” the original number, the negative version of it. Look up how this works, it’s fascinating: This is called the two’s complement. It is how the computer actually does subtraction behind then scenes.

Did you know that doing “EOR FFh” to a byte has the same effect as NOT? This is beclause Exclusive-OR is 1 if and only if the two input bits are different. The number FFh has all its bits set to 1. So if a bit in the other number is 1, that is the same as the corresponding bit in FFh, so the result bit is 0. And if the bit is 0, well then it’s different from the 1 bit in FFh, so the result is 1.

ALX, AEX, AGX

These stand for A-less-than-X, A-equal-to-X, and A-greater-than-X. They produce a number in A that is 0, if the named condition is false, and 255 (all bits set) if the condition is true. If you are wondering why 255: You can say NOT and get the opposite, for example to turn “A-less-than-X” into “A-greater-than-or-equal-than-X”.

AND, IOR, EOR

These compute A AND X, A OR X (inclusive or), and A XOR X (exclusive or), and store their result in A.

XA

Aha! So you can get the value of X. With this instruction, it gets stored in A.

Operations with secondary result in X

AX SWAP

AX: Stores A into X. Swap: Swaps A and X.

SHL SHR ASR

  • SHL shifts every bit in A to the left by one position. Position one - the first bit - is set to zero. So far so good. At this point, importantly, X gets zeroed. Now. The highest order bit from the original number (the bit that got “pushed” out) is put back as the first bit of X. So at the end, X either contains 0 or 1.

    This instruction is exactly the same as multiplying A by two, or adding A to itself. Think about it.

  • SHR is the opposite. Every bit is shifted one position to the right. The highest position - bit 7 - is set to zero. So far so good. X gets cleared again. Now. The lowest order bit from the original number (the bit that got “pushed” out) is put back as the highest-order bit of X. So at the end, X contains either 0 or 80h (bit 7 set).

    This instruction is exactly the same as diving A by two, if you prefer to use the larger “unsigned” range of your bytes.

  • ASR is just like SHR, with one little difference:

    Every bit is shifted one position to the right. The highest position - bit 7 - is set to whatever the highest-order bit of the original number was. So far so good. X gets cleared again. Now. The lowest order bit from the original number (the bit that got “pushed” out) is put back as the highest-order bit of X. So at the end, X is either 0 or 80h.

    The reason why the high-order bit is copied down has to do with negative binary numbers. As we said in an earlier section, negative binary numbers have their highest bit set to 1. The highest bit is often abbreviated to MSB (Most Significant Bit). When you use SHR (the other shift-right instruction), the MSB changes from a possible 1 to a zero. Practically, let’s say you divide -6 by 2, with SHR you would get 3, not -3. Whereas ASR gives the correct result in this case. For this reason, ASR stands for Arithmetic Shift Right.

    This instruction is exactly the same as diving A by two, if you prefer to use the smaller, “signed” range of your bytes.

ADDC ADDV SUBB

  • ADDC is the easiest of the three. A receives the sum of A and X. If the sum didn’t fit into a byte, then X is set to 1. Else, X is set to zero.

    Example: I’m sure you know that the maximum unsigned number you can store in a byte is 255. So if A=60 and X=200, then ADDC will leave A=5 and X set to 1.

    Just like in decimal: Add 6 to 5, and you need another 1 to the left, because it’s greater than 9. That one is called the carry bit. ADDC stands for ADD and CARRY. This instruction is for when you are treating your bytes as “unsigned”.

    In preparation for explaining the next instruction, think about this. Our example from before, 200+60=5 looks like this in hexadecimal: C8+3C=5, CARRY=1. So the result (05h) is “wrong” in a way. But in some other way, it only can’t stand by itself, the carry must be part of it. So you can fix it by prefixing it with the carry: 105h = 255 + 5 = 260.

    The carry bit often comes into play when you have a 16-bit address, and add a small number to the low-order byte. If the carry bit is clear, you don’t have to do anything to the high-order byte. But if it is set, you must increment the high-order byte by 1 to fix the address. The _U effect does this for you. Remember, when you store a number info _U, that number gets added to the B:O register pair. And although you are just adding a byte to O, the result may spill over into B, or underflow O. And _U has your back and updates B as required.

  • ADDV also puts the sum of A and X into A. But the value that will be in X is somewhat less intuitive. X gets the “overflow flag”. This instruction is strictly for when you’re treating your bytes as signed.

    As we said, the carry bit is for fixing the result when the addition of two unsigned numbers overflows a byte.

    The Overflow flag is different - you won’t be able to use it directly to fix your result. All it tells you is that the addition didn’t work, the two operands when added are out of range. The overflow flag is an error flag. ADDV stands for ADD and OVERFLOW.

    You can tell that your result is wrong, when you add two positive numbers (A and X) but you get a negative result. And also a positive result when you add two negative numbers is clearly wrong. And that is what the overflow flag tells you, that one of these two cases occurred.

    The interesting thing, and this does take a bit of thought, is to realise this: when your two numbers have different signs, one positive and a negative number, there is just no way that you can go wrong when adding them as bytes.

  • SUBB is more easy! It means: subtract A from X, assume that they are both “unsigned” and store the “borrow” bit into X. What on earth is the borrow bit? The borrow bit is 1 if the result of the subtraction is negative.

    In other words, if A is greater than X. You are subtracting a larger positive number from a smaller positive number, so the result is negative? Boom, borrow bit is 1.

    Just like with ADDC and the carry bit, you can use the borrow bit to fix your result! It isn’t just an error flag. While the carry meant that you have to add it to the higher-order byte of your addition, the borrow flag means you have to subtract it from the higher-order byte to complete your calculation. (“You need a larger number to subtract from and come out positive”).

    The borrow bit often comes into play when you have a 16-bit address, and subtract a small number from the low-order byte. If the borrow bit is clear, you don’t have to do anything to the high-order byte. But if it is set, you must decrement the high-order byte by 1 to fix the address. The _U effect does this for you. Remember, when you store a number info _U, that number gets added to the B:O register pair. And although you are just subtracting a byte from O (oh, not 0), the result may spill over into B, or underflow O. And _U has your back and updates B as required.

    A good way to remember that A gets subtracted from X, and not the other way around, is the actual way in which you proceed. First you push a number into A, then you realize you need to subtract 5 from it, so you push the five. At this point, the number that is subtracted (the 5) is in A, and your orginal number (the one you want to subtract from) has been pushed into X. You then do SUBB and are left with the result in A. As a side-effect, the borrow bit is stored in X.

Traps

Trap instructions are single-cycle subroutine calls. There are 32 of them, and they all do the same thing, with just a different address.

They act like instruction set extensions, because it’s completely up to you to decide what happens, when their opcodes are executed.

You may or may not know this, but below what is called machine-code, many computers have another layer of even more primitive instructions, called microcode. The regular machine-code instructions are built from these microcode instructions, that tell the hardware exactly how the (macro-)instruction must perform.

So when you normally program in assembler, with each instruction, you are actually running a little microcode program.

The Myth computer is really primitive, so that it took fewer components to build it. It’s instructions are actually microcode.

And the idea with implementing trap instructions was that you then have 32 free opcodes to implement more complex custom instructions in the form of single-byte subroutine calls.

A trap instruction opcode encodes a page number between 0 and 31. That’s the base address for your call. The trap call always goes to offset zero of “its” page.

So if your goal is to write an instruction handler for “Trap5”, you need to put a subroutine into page 5, starting at the first byte.

A side-effect of trap calls is that they set the cpu “busy” flag for reasons explained in the section on interrupts. As a general rule, while you’re in page 0, or while the busy flag is on, your program can’t get interrupted; you’re in a protected zone.

In regular call-return type instructions (Call, COR, RTS), the base pointer (B:O) is used for saving and restoring the return address. But during trap calls and interrupts, the amenity pointer IA (Interrupt address) is used (we explained this in the section on BOPS), and you must use RTI (Return from Interrupt) to return from a trap or interrupt service routine. The advantage of this is that your trap or interrupt is completely transparent to the caller. Using trap instruction you can really build new, fully independent instructions.

SYS

Finally!, the last little block of instructions, the SYS group. There are only 8 of them: NOP, SSI - SSO, SCL - SCH, RTS - RTI, and COR. Let’s go through them systematically.

NOP (No operation)

Here, the computer just sits pretty and passes its turn for one cycle. It’s opcode is zero, so this is something like the “default” instruction.

Serial Port Control

The next four are for serial communication. In another document, there is a whole section on serial communication. But the basics are very simple.

When you transmit or receive over a serial data line, which is literally a wire, it goes just one bit at a time. So you put your data bit of electricity - low or high level voltage, 0 or 1 - on the line, so that the receiver can sample your data: is it a zero or a one bit?

Clock

Now that the received knows which kind of bit you sent, a good way for her to let you know know that she has read your bit and is ready for the next, is to use a second wire, the “clock” line.

Just like you have put a data bit on your line, your partner now puts a “tick” on the clock line. For example: low, then high, then low again. This is an encoded message to you! It says: I’m done with your data bit.

Then you - the sender - detect that tick on the clock line and send your next data bit. Rinse and repeat, that’s all there is to it.

Apart from the clock line, this computer uses two data lines, one for input, one for output.

SCL - SCH (Set Serial Clock Low / High)

These two instructions are for controlling the state of the clock line. When you say SCL, the clock signal is set to “low” (0) and when you say SCH, the clock line is set to “high” (1).

Now we need to figure out a way to convert a byte into a series of bits and vice-versa. There are two registers in this computer which do that: SIR and SOR.

SSI (Shift Serial Bit In)

SIR is the Serial Input Register. One bit of it is connected to the serial input wire, and it has what is called a shift register.

Every time you say “SSI”, the bit that is on the serial input line is shifted into SIR at the lowest bit position, pushing out its highest order bit into nothing.

So with every “SSI” instruction you execute, the SIR is slowly filling up with bits, one by one, until 8 of them have been read in. Since 8 bits are all that the SIR can hold, if you execute more SSIs than that, the first bits that you shifted in will be “pushed out” of the shift register and you would be losing them.

So the sensible thing after 8 SSI instructions is to read out the SIR and store your data byte somewhere. You can do this with a pair instruction like SD (store SIR into D).

Also, you in between issuing the SSI instructions, you should also tick the clock-line using SCL and SCH as we said, to synchronise your partner.

SSO (Shift Serial Bit Out)

SSO is the Serial Output Register. Just like the SIR is converting bits into bytes, the SOR is turning bytes into bits.

For this to work, you store a byte into the SOR with a pair instruction like DS (store D into SOR). Then you do 8 SSO instructions, and with each of them, the current high-order bit of SOR gets shifted out onto the serial output line, the low order places slowly filling with zeros.

Also, you in between issuing the SSI instructions, you should also tick the clock-line using SCL and SCH as we said, to synchronise your partner.

RTS (Return from Subroutine)

This instruction reverses a call instruction. RTS increments L, so that the caller will have his local page back. Then it puts whatever is in B into C, and whatever is in O into PC. So the base pointer (B:O) had better point to the exact address you need to return to, because the next instruction is going to be fetched from there.

RTI (Return from TRAPs and Interrupts)

RTI (Return from Interrupt) is similar in operation but “clears the BUSY flag”.

Let’s explain this in a little more detail: As long as this flag is set, the computer will not accept an interrupt. An interrupt, when it is accepted by the computer, is literally a “Trap0” instruction that you can’t predict will happen!

The computer - without you knowing - makes you run a call to an “interrupt service routine”, and in the process, it sets the BUSY flag.

In contrast to RTS, trap calls, interrupts (same as Trap0!) and RTI do not use B:O to store the return address. And since they don’t alter your base pointer, if the trap handler does a good job, you will be returned to where you left off to exactly the same state you were in and not notice a thing.

Traps and RTS use the amenity pointer “IA” (Interrupt Address), to store and retrieve the return address, instead of the base-pointer.

COR

COR stands for “Coroutine”. This word should always be in the plural, since when you are using COR, you become the coroutine of another subroutine, and you are its coroutine.

COR puts whatever is in the base pointer (B:O) into C:PC, but keeps a copy (the return address) before overwriting these registers. Then, just before the jump, it overwrites the base pointer with the return address, so your coroutine knows where you left off.

The idea is that you both do your thing, taking turns: you jump to the coroutine, it does a bit of work, jumps backs to you, you do a bit of work and so on.

This instruction acts very much like a call/return, but without changing local pages. Of course you can also just use it as a jump to a 16-bit address.

Downloads

Download the Source Code for this project.