~nkali/mv950toy

mv950toy/docs/crash_course.md -rw-r--r-- 19.2 KiB
2bfd0a2c — Nina Kalinina Plotter simulation 25 days ago

#My first transistorised computer: A Crash Course

This document is a short introduction to programming and using the MV 950 emulator, a program that simulates a Metrovick 950-compatible computer, the first commercial computer using transistors (1956). The simulation is not perfect, but if you figure out how to use this program, you will only need a troubleshooting manual and a time machine to program and operate the real machine.

If you feel lost, please check other documentation files in this folder, as well as built-in help for the emulator.

#Unpacking the computer

Thanks to the latest advancement in digital electronics, Metrovick 950 is a very compact machine. It will barely take more space than a couple of average-sized wardrobes, and it only requires 2 kilowatts of power (about as much as three NVidia GTX 4090 Ti). The standard package includes a teleprinter, a tape punch, and a CRT that can display the contents of the computer's memory.

Metrovick 950 operates on 32-bit integer numbers and can store up to 4096 of them on a magnetic drum. The mean access time for a memory address is 10 milliseconds (100 Hz), but the computer allows you to access memory much more efficiently through programming. This means MV 950 is capable of reaching and even surpassing 200 operations per second.

MV 950 is a stored-program computer. This means not all 4096 words of memory are available for data; some of them will be required to store the program. However, this is actually great: if your program or data doesn't fit on the memory drum, you can offload a part of it on the paper tape, and then load it back when you need it again.

#The basics of the operation

The computer is mostly operated through the control panel. The control panel shows the numbers stored in the computer's register memory. It also has Hand Switches that can be set to any 32-bit value. Hand Switches can be written into the Accumulator or Instruction registers.

The most important register of MV950 is the Instruction register. The control logic surrounding the Instruction register is also the most notable difference between magnetic drum computers and other types of computers. Computers with semiconductor or core memory often have a register called "Instruction Pointer" or "Program Counter". A computer with a PC register would read memory from the address stored in the PC register, loads the data from it into the Instruction register (completely invisible to the programmer), execute it, and increment the value of the PC register by 1. Metrovick 950 doesn't work like that.

There is no "Instruction Pointer". Instead, there is an Instruction register, directly accessible to the programmer (or the computer operator). A number stored in the Instruction register is the command that the computer will execute, not a pointer to it. In the case of Metrovick 950, the command contains four components: Function Digit, B-store Digit, address S1 and address S2.

  • FD - Function Digits - is 5 bits. FD encodes the instruction type (e.g. 0 for "Clear Accumulator" and 21 for "STOP").
  • BD - B-store Digits - is 3 bits. BD is used for operations with a special type of fast memory, B-store. You can ignore it for now.
  • S1 - address S1 - is 12 bits. This is the memory address the command will use. For example, if you want to store a number at address 1234, then 1234 is going to be the S1 part of the command.
  • S2 - address S2 - is also 12 bits. When the computer finishes executing the command, it will replace the current Instruction register with the data read from the address S2.

Note: the manual for Metrovick 950 also mentions 4 bits of "Engineering Digits", but those are not accessible to the user.

#Your first program

There is little benefit in using the teleprinter for small programs; it is much easier to input numbers into the computer using Hand Switches and read the output from the contents of the registers displayed on the CRT. Thus, a very useful first program is a program that loads a number from Hand Switches to a fixed address.

The program will perform only two commands:

  1. Load a 32-bit number from Hand Switches to the memory at address 100,
  2. Stop the computer.

The emulator comes with a simple "autocode" program, also known as "assembler", but in the interest of education, it is worth converting this simple program into the machine code manually. We will store the first command at the memory address 0, and the second at the address 1. You can find the FD numbers for commands the MV 950 supports in the "Report on Metrovick", "Original Specification" or the assembler manual.

# FD - Command BD S1 address S2 address
0 Load H.Switch - 19 0 100 Next command - 1
1 STOP - 21 0 0 0

Naturally, the entire program needs to be converted into 32-bit words. Note: on a real physical Metrovick 950 the program words need to be entered backwards, but the MV 950 emulator simplifies this aspect of operation.

# FD BD S1 S2 Hexadecimal
0 10011 000 0000 0110 0100 0000 0000 0001 98 064 001
1 10101 000 0000 0000 0000 0000 0000 0000 a8 000 000

#Using the emulator

Now let's power up the computer, load the program and execute it. Uhm, I mean, let's start the emulator:

$ python3 emu.py

You will see the state of the computer's registers, including the Instruction register and a mnemonic of the command stored in it. "All zeroes" in the Instruction register for the computer means "Clear Accumulator, ignore B-store, the next instruction is at the address 0".

MV950 is ready. Press Enter to perform a single step, or enter a command.
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x00000000 [FD:0 BD:0 S1:0 S2:0] 	 CLR &0 0x0 0x0
>

As you started the emulator without any arguments, its RAM will be empty. This is the normal state for the computer after its magnetic drum was formatted. Worry not, there is no need for an operating system. We can load our program from the computer's control panel. The emulator offers a built-in shell that supplants the control panel, and it is fairly simple to use.

#Writing Instruction 0 to RAM

Thanks to the Instruction register, we can input a command into the computer and execute it without the need to store the command in the RAM first. A command to store the contents of the Hand Switches in the RAM is a very useful command that be executed directly.

A command to write to memory address 0 is:

# FD BD S1 S2 Hexadecimal
0 10011 000 0000 0000 0000 0000 0000 0000 98 000 000
  • To load our first command to RAM, I set Hand Switches to 0x98000000. It is done by typing hs 0x98000000 into the emulator.
  • Then I record this command into the Instruction register. It can be done by typing wi.
> hs 0x98000000
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x00000000 [FD:0 BD:0 S1:0 S2:0] 	 CLR &0 0x0 0x0
> wi
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98000000 [FD:19 BD:0 S1:0 S2:0] 	 HS &0 0x0 0x0
  • The built-in shell shows the contents of the Instruction register - "Store Hand Switches at address 0, then jump 0".
  • Now we can set our Hand Switches to the 32-bit word we want to store at the address 0, in our case 0x98064001
  • Then, a simple "Enter" press on your keyboard will make the computer to execute the command stored in the Instruction register.
> hs 0x98064001
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98000000 [FD:19 BD:0 S1:0 S2:0] 	 HS &0 0x0 0x0
>
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1

You can see that the value in the Instruction register has changed to the command we just loaded. This is because we told the computer to read the next command from the address 0. But we are not done yet!

#Writing Instruction 1 to RAM

Now, let's repeat the same operations for the second command.

  • First, set hs 0x98001000 on the Hand Switches - "Store Hand Switches at address 1, then jump 0"
  • Then write it to the instruction register with wi
> hs 0x98001000
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1
> wi
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98001000 [FD:19 BD:0 S1:1 S2:0] 	 HS &0 0x1 0x0
  • Now, set the Hand Switches to the 32-bit word of the "STOP" command - hs 0xa8000000
  • Execute a single instruction to amend the computer's memory.
> hs 0xa8000000
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98001000 [FD:19 BD:0 S1:1 S2:0] 	 HS &0 0x1 0x0
>
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1

The computer is now ready to execute the instruction from the address 0 again.

#Examine the memory

The emulator allows you to examine any address in RAM. This is very convenient for debugging. Let's confirm that our little first program is stored correctly in the memory by typing p 0 and p 1:

> p 0
[0] = 2550546433 | HS &0 0x64 0x1
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1
> p 1
[1] = 2818572288 | STOP &0 0x0 0x0
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1

#Rock and roll!

Now we are ready to execute our little program.

  • Let's set Hand Switches to 0x138d5. This number is just an example, it doesn't have any significance.
  • Then we need to execute two steps of the program by pressing Enter twice - until we see the "STOP" command executed.
> hs 0x138d5
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1
>
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0xa8000000 [FD:21 BD:0 S1:0 S2:0] 	 STOP &0 0x0 0x0
>

[STOP command encountered]

===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1

Let's examine the contents of the memory cell 100 to confirm that the number from the Hand Switches was recorded into the computer correctly.

> p 100
[100] = 80085 | CLR &0 0x13 0x8d5
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1

Success!

#Restarting the program

The "STOP" instruction we programmed sets S2 to 0. This means the program simply restarts after completion. This means we can set different Hand Switches, and execute the program again by pressing Enter twice. You can execute the program again and again, as many times as you need. That's a stored-program computer for you!

> hs 2989
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1
>
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0xa8000000 [FD:21 BD:0 S1:0 S2:0] 	 STOP &0 0x0 0x0
>

[STOP command encountered]

===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1
> p 100
[100] = 2989 | CLR &0 0x0 0xbad
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x98064001 [FD:19 BD:0 S1:100 S2:1] 	 HS &0 0x64 0x1

#Using the assembler / autocode system

This MV950 toy comes with a simple autocode system, commonly known as "assembler". It is written in Python and thus not self-hosted. The assembler implementation is very simple, and does not perform any optimisations for memory allocation. Therefore, you might want to use a more advanced autocode system on a real Metrovick 950.

Let's take a look at a simple program (001_add.asm). The Function Digits are represented through letter mnemonics. The B-digits can be omitted when unused. Addresses for S1 and S2 can be labels. S2 can be omitted altogether if you want the instructions to be executed one after the other.

;; Add two numbers
    CLR ; register A = 0
    ADD num1 ; A += [num1]
    ADD num2 ; A += [num2]
    STOL num3 ; [num3] = A (without the carry)
    STOP

:num1
    #DW 123
:num2
    #DW 456
:num3
    #DW 0

Let's compile this program. Normally, the output of the assembler is the copy of the magnetic drum needed to execute the program. For the debugging purposes, the assembler can generate a mapfile - a list of all labels used in the program. The MV950 emulator can make a use of the mapfile.

$ python3 ../host/asm.py -i 001_add.asm -o 001_add.ram -m 001_add.map

The contents of the RAM file is the list of 32-bit words on the computer's magnetic drum:

1
134238210
134242307
939552772
2818572293
123
456
0

The map file stores addresses for the labels - NUM1, NUM2 and NUM3 in our case:

{"NUM1": 5, "NUM2": 6, "NUM3": 7}

Let's execute the program in the emulator. We can type g to make the computer run until it encounters the STOP command. Please use Ctrl+C to terminate the program early if needed.

python3 ../host/emu.py -r 001_add.ram -m 001_add.map
MV950 is ready. Press Enter to perform a single step, or enter a command.
===  A: 0 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x00000001 [FD:0 BD:0 S1:0 S2:1] 	 CLR &0 0x0 0x1
> g

Entering interactive mode. Press Ctrl+C to return to the shell


[STOP command encountered]

[Computer hit the STOP instruction]

===  A: 579 B: [0, 0, 0, 0, 0, 0, 0, 0]
===  I: 0x0000007b [FD:0 BD:0 S1:0 S2:123] 	 CLR &0 0x0 0x7b
>

The computer has completed its calculation. Let's see how many cycles it took:

> c
Performed 5 cycles so far.

We asked the computer to execute CLR, ADD, ADD, STOL and STOP - five commands in total. Our expectation matches the actual number of instructions executed. Nice, isn't it?

Let's confirm that the computer added NUM1 to NUM2 and stored the result in NUM3:

> p NUM1
[5] = 123 | CLR &0 0x0 0x7b
...
> p NUM2
[6] = 456 | CLR &0 0x0 0x1c8
...
> p NUM3
[7] = 579 | CLR &0 0x0 0x243

Now you are ready to write your own programs for the MV 950, and you can do it with ease!

#Isn't this computer, like, really slow?

I am glad that you asked! At the beginning of the Crash Course, I mentioned that MV 950 can execute over 200 operations per second with optimal programming (285 op/s, if we were to believe the original report). For a modern computer operator, it can be difficult to put this number into perspective. Modern computer frequencies are measured in GHz, and performance in tens of billions of operations per second.

This, naturally, raises a question: Is there any practical use for a computer that can only run at 285 op/s?

To answer this question, I implemented a simple benchmark. The benchmark program 002_gcd.asm calculates the Greatest Common Denominator for two integers. MV 950 executes this program in 229 cycles. With 3.5ms for an average cycle with an optimal drum format, this program should execute in about 0.8 second. With less-than-optimal programming and 20ms per cycle, it would take about 4.5 seconds to complete.

I have implemented the same algorithm for microprocessor computers, Dragon 32 (using 6809 @ 750 KHz) and Commodore 64 (6502 @ 1 MHz), using built-in BASIC. Both computers had similar performance, with the GCD calculation taking about 0.25s. MV 950 from the mid-1950s programmed in assembly is only 4 times slower than BASIC on microprocessor computers from the early 1980s. Obviously, both Dragon and Commodore could deliver much more number-crunching per clock if programmed in assembly, but they shipped with BASIC because even with BASIC they were fast enough for practical usage.

Even with non-optimal programming, MV 950 can compete with microprocessor systems. IllinoisU BASIC for Intel 8008 (1974) reportedly performs at 20ms per step, and the authors call their BASIC "not a number-crunching giant, but more than adequate".

It probably cannot run Doom in real time, though.

#Calculating GCD of two numbers

VAR_A is 27776; VAR_B is 1612. The result of calculation (124), after 252 cycles, is stored in VAR_A.

$ python3 ../host/asm.py -i 002_gcd.asm -o 002_gcd.ram -m 002_gcd.map
$ python3 ../host/emu.py -r 002_gcd.ram -m 002_gcd.map
MV950 is ready. Press Enter to perform a single step, or enter a command.
...
> p VAR_A
[17] = 27776 | CLR &0 ADJUST_1 0xc80
...
> p VAR_B
[18] = 1612 | CLR &0 BEGIN 0x64c
...
> g
Entering interactive mode. Press Ctrl+C to return to the shell
...
[Computer hit the STOP instruction]
> p VAR_A
[17] = 124 | CLR &0 BEGIN 0x7c
...
> c
Performed 229 cycles so far.

#Tips and tricks

The assembler language for MV 950 makes programming the computer no more difficult than programming a modern microcontroller. But there are still a few tricks that you should know about.

#Constants

There is no command for loading a register directly from the command. If you want to load a number into a register, you have to define a constant:

;; Load 1234 into Accumulator
    CLR ; a = 0
    ADD const_1234 ; a = [const_1234]

; ...

:CONST_1234
    #DW 1234

#Unconditional jumps

Sometimes the code calls for an unconditional jump. In assembly for modern microprocessors, you probably would use a command like this:

JMP address

MV 950 always has an address of the next instruction in the instruction word. This means you can simply put the address you need in the S2 part of the instruction:

STOL variable address ; [variable] = Accumulator; then JMP to address

#Comparisons and conditional jumps

The computer does not have a "flag" register; it cannot detect carry, overflows, and so on. Thus, the only check the computer can reliably perform is a "negative" check, indicated by the Most Significant Bit in the Accumulator (or B-store register) set. Thus, the only conditional jump the system can do is "Jump If Negative".

It is possible to simulate other comparisons, of course.

;; Jump if A == B
	CLR ; a = 0
	ADD VAR_A ; a = VAR_A
	SUB VAR_B ; a = VAR_A - VAR_B
	JNEG NOT_EQUAL ; if negative, B > A, early exit
	SUB CONST_1 ; a = VAR_A - VAR_B - 1. There is no immediate load, so we keep a constant in RAM
	JNEG EQUAL ; if negative, A==B
:NOT_EQUAL
 	; ... [program continues]

:CONST_1
	#DW 1 ; just a number "1"

#Indexed operations

One fascinating aspect of MV 950 is its fast-access B-store memory. Five types of operations can be done with the B-store:

  1. Load a variable into B-store (function 12, LODB)
  2. Store a variable from the B-store (function 13, STOB)
  3. Decrement B-store (function 14, SUBB)
  4. Conditional jump if the value in the B-store is negative (function 16, BJNEG)
  5. Add B-store to the Instruction register (virtually for free, no special command needed, just specify the B-store for any command that isn't 12, 13, 14 or 16)

When you specify a non-zero B-store in an instruction (any instruction), the value of the B-store will be added to the full 32-bit value of the Instruction register. You can use it this way:

LODB &1 OFFSET ; load offset into the B-store &1
STOL &1 1 ; store the Accumulator at the address OFFSET+1

:OFFSET
	#DW 4190208 ; 1023 << 12 = 4190208

Keep in mind the 32-bit instruction format (FD, BD, S1, S2) to calculate the correct offset values for the B-store.

#What's next?

If you are ready to embrace the MV 950 assembly, make sure to check out Notes on programming. This document will teach you about the most frustrating aspects of MV 950 machine code and thus will save you a lot of time.