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.
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:
- Support Software For The Space Electronics Branch On-Board Processor, 1968
- Advanced On-Board Processor, 1973
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.
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
but it might not be a lot of use unless you want to build the Cowgol toolchain.
The OBP itself has three 18-bit registers,
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
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
instruction to refer to it, which makes programming much easier.
E register is used by the
These all operate on a 35-bit value, made up of
AE. There are also
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
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
xngt instructions which allow addition and comparisons on
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
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
brm instruction stores the program counter at
[subroutine+0] and then
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
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.
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.