I’ve just released an epic eight-and-a-half hour video where I live code an assembler and simulator for the OBP, the On-Board Processor used by a variety of spacecraft, designed way back in 1968.

Introduction

The OBP is a rather elegant 18-bit, two’s-complement machine from about 1968. It’s a full Von Neumann architecture with code and data living in the same address space, of which there can be up to 64kw, divided into 4kw pages. The instruction set includes hardware multiplication and division, multiple-bit shifts, a decent set of comparison instructions, system calls, simple memory protection, rich I/O including DMA, and sixteen different individually maskable interrupts — it’s actually quite a nice thing to work with. Admittedly, running at 250kHz and with most instructions taking multiple cycles, it’s not the fastest thing around. But then it did use non-volatile core memory and was built out of raw NOR gates.

The OBP was used on a variety of spacecraft, including the OAO-3 Copernicus space telescope, which was in operation from 1972 to 1981.

The OBP is interesting because it begat the Advanced On-Board Processor, or AOP; and the AOP begat the Nasa Standard Spaceflight Computer, or NSSC-1, which is used by the Hubble Space Telescope. At time of writing there’s a hardware fault somewhere in the vicinity of the Hubble’s NSSC-1 which might render it useless. Here’s hoping that ground control finds a way to fix it.

I based my simulator on these two documents:

The first document describes the OBP itself, but is unfortunately missing some core details. As the AOP looks extremely similar to the OBP but with more instructions, I just copied the relevant parts from there; hopefully they’re right. There is mention of a diagnostics suite for verifying the real hardware, but I’ve been unable to find it. If it ever shows up I should be able to test my implementation against that. Until then, I’m going to stick with my guess.

You can find the simulator (and assembler) on GitHub.

obpemu on GitHub

See the tools/obpemu directory in the Cowgol repository (linked here) for the emulator. You don't need Cowgol to use it, but you will need Cowgol for the assembler, because I was lazy.

The assembler

The original OBP assembler as described in the first document is a bizarre thing. It ran on a series of non-integrated-circuit mainframe machines, primarily the SDS 920 (from 1962). These assembled off punched cards, writing the program out the magnetic tape. The tape could then be loaded into a real OBP or run on a simulator.

While the original assembler did support fairly modern-looking syntax, it was really intended to be programmed in what looks like natural language. Here’s an example from the manual:

Defining for external use Factorial.
If it is less than 0 then go to (return from) Factorial.
If it is equal to 0 then go to answer equals one;
otherwise let it yield N and also yield result.

Defining Jail. Let N minus 1 yield N.
If it is less than 2 then go to Answer;
otherwise, let result times N yield result.
Go to Jail.

Defining Answer. Let result be. Return from Factorial.

Defining Answer equals one. Let 1 be. Return from Factorial.

End of this program segment.

The assembler doesn’t actually understand any of what’s written here. Instead, key phrases map on to particular assembler instructions. For example, the Jail paragraph can be rewritten in more modern syntax like this:

jail:             ; Defining Jail.
  lda [n]         ; Let N
  sub 1           ; minus 1
  sta [n]         ; yield N.
  ilt 2           ; If it is less than 2
  brc answer      ; then go to Answer; otherwise,
  lda [result]    ; let result
  mul [n]         ; times N
  sta [result]    ; yield result.
  bru jail        ; Go to Jail.

This is one of those ideas that keeps coming back, and each time it does it’s tried for a while before going away again. It’s interesting to note that the AOP document, for the OBP’s successor, doesn’t mention the natural language syntax at all! The problem is that English is fundamentally context-sensitive and ambiguous, while computer languages are the complete opposite. While the natural language looks very readable, it’s easy to come up with phrases which are perfectly unambiguous to a human but which don’t conform to the assembler’s parsing rules and so do the wrong thing — consider the phrase yield N minus 1, for example (this will become sta [n]; sub 1).

(The only time I’ve ever seen this work reasonably well is in the interactive fiction language Inform 7, and it uses an incredibly complex parser.)

The assembler I’ve written was hacked together in no time flat and is part of my Cowgol compiler suite — simply because it was the easiest way to get an assembler fast. It uses the non-natural language syntax, and I’ve also made some changes for clarity. You can find it at archobp.cow, but it might not be a lot of use unless you want to build the Cowgol toolchain.

The machine

The OBP itself has three 18-bit registers, A, E and X; most work is done in A, with E being used for double-word arithmetic. X is used for indexing into tables and dereferencing pointers.

Instructions are 18-bits wide, and either take no operands or are split into a 6-bit opcode and a 12-bit operand. This operand is always an address in the current page of memory. This is then (nearly) always dereferenced, so the instruction operates on the value in memory. So, to increment a value in memory, you have to do:

  lda [n]         ; load A from the variable n
  add [one]       ; add one to A
  sta [n]         ; store A to the variable n
  ...
one: dw 1
n:   dw 0

The add instructions needs to refer to a variable containing the constant 1. This seems odd, but it allows the instruction to work on full 18-bit values even though the address field in the instruction is only twelve bits wide. Both my and the original assembler allow you to do add 1 and they will automatically allocate a constant value in memory and assemble the add instruction to refer to it, which makes programming much easier.

The E register is used by the mul, div, dsh and dcy instructions. These all operate on a 35-bit value, made up of AE. There are also ste and lde instructions but as there’s no way to refer to E directly by other instructions it’s surprisingly non-useful. The AOP added an instruction to swap A and E, making E vastly more useful.

X, however, is invaluable for doing anything with arrays and pointers. All instructions contain a bit which causes X to be added to the effective address before derefencing:

  ldx [index]     ; load the index to an array
  lda [array, x]  ; load the Xth item in the array

Pointers can be dereferenced by simply doing lda [0, x]. There are also adx and xngt instructions which allow addition and comparisons on X without needing to move the value to A first, allowing reasonably fast loops:

  lda 0           ; initial sum
  ldx 0           ; loop counter
loop:
  add [array, x]  ; accumulate sum
  adx 1           ; move to next item
  xngt [length]   ; check for end of array
  brc loop        ; go again if not at the end
  ; sum is in A

One oddity is the way comparisons are done. Before doing a comparison, you tell the machine whether this is an and comparison or an or comparison, allowing code like this for ‘if x is 1 and y is 2’:

  lda [x]         ; load x into accumulator
  iet 1           ; if equal to 1
  andd            ; send AND bit
  lda [y]         ; load y
  iet 2           ; ...and if equal to 2
  brc yes         ; perform branch if both are true
  ...no code...

This makes writing multi-branch comparisons in machine code easy. It also works well with the natural language syntax too, as the above code simply becomes:

If x equals 1 and if y equals 2 then go to yes

However there’s a lot of hidden state; the and/or bit and the decision bit, which stores the result of a comparison, get reset after the conditional branch. So, if that branch doesn’t happen, or you do something which modifies that state unexpectedly such as calling a subroutine, bad things will happen. These days we use compilers to generate decision trees, which would look something like this:

 lda [x]          ; load x
 inet 1           ; if not equal to 1 (the OBP doesn't actually have this instruction)
 brc no           ; then go to no
 lda [y]          ; load y
 inet 2           ; if not equal to 2
 brc no           ; then go to no
 ...yes code...

This is faster, as it doesn’t bother comparing y if x wasn’t 1, and involves less hidden state.

One other interesting feature to the OBP is that it has no stack. This makes subroutine calls interesting. On modern machines, when you call a subroutine the current program counter is pushed onto the stack so that when the subroutine returns it can be reloaded. On the OBP, this isn’t possible. Instead, the return address is stored next to the subroutine which is being called:

  brm [subroutine] ; call 'subroutine'
  ...
subroutine:
  dw 0             ; used to store return address
  ...code here...
  bru [subroutine] ; returns from the subroutine

The brm instruction stores the program counter at [subroutine+0] and then jumps to subroutine+1. The subroutine can then return by loading the stored program counter at its head.

The disadvantage of this approach is that recursion isn’t possible; if you try, the second call will overwrite the first call’s return address, and your program will crash and burn. This is actually how Cowgol, my own programming language, is supposed to work; not having recursion isn’t as limiting as you might think. It’s possible to do static analysis to ensure that recursion never happens, although I suspect the original OBP assembler didn’t do this. Not having a stack also means you have much tighter control over memory usage in the machine, which for aerospace software is important.

There are some other interesting features, but they get increasingly exotic: there’s built in support for fixed-point arithmetic; you can set the scale factor in a special register and mul and div will automatically adjust. (There was no floating support.) The memory protection register and the system call instruction, along with the page structure, allows a simple multitasking kernel. Programs can be written independently, loaded into different pages, and then the kernel can timeshare between them. Memory protection means that one task can’t corrupt another one. As code was stored in RAM, this was important.

Conclusion

It’s a much nicer machine than I thought it was going to be. If it weren’t for the odd word size it would make a perfectly adequate embedded controller by modern standards (although you might want to speed it up a bit!). It’s certainly vastly cleaner and more elegant than the Apollo Guidance Computer. Given the OBP document I have is dated 1968, the OBP is undoubtedly contemporary with the AGC. The OBP is actually more powerful, with a larger word size and a larger address space (up to 64kw, all of which could be used for code, while the AGC had… less, it’s a bit complicated). There’s probably a story there.

I might expand this simulator in the future to work with the AOP, the OBP’s successor. This is essentially the same machine but with more instructions and some of the less useful ones swapped out for better ones. If anyone knows of specifications for the NSSC-1, I’d love to give that a try, too. It would also be really interesting, if not very useful, to do Cowgol compiler ports for these architectures. They are, sadly, too small to run the compiler on, but I bet Cowgol could generate reasonable code for them.