CFT in your Browser

  • Alexios
  • hardware
  • projects
  • cpus
  • emulation
  • Javascript
  • jquery

Try it right now: set the switches to c700 (type it on your keyboard if you can't convert hexadecimal to binary in your head), then press the RUN switch or AltR. Read on for more details. Read on for more details.

You've seen all those images of the CFT front-panel (Coming Real Soon Now). You've seen screenshots of unit tests running, Forth executing exotic code, even the microcode. You've seen screenshots of the CFT emulator. But so far, you've never had a chance to play with a CFT processor yourself. It's time to remedy this, so here's a CFT emulator and Front Panel trainer rolled into one, with a bonus ROM full (for very, very sparse values of ‘full’) of goodies. What's best, this emulator isn't the C-based one. Oh no. This one runs in your browser.

Very, very slowly. On my computer, using the latest version of Chrome, it runs at about 250 Hz. The CFT is intended to run at 4 MHz, so the emulator runs 15,000 times slower than the actual processor. But this is a good thing because now you can see all those blinkenlights1 at proper Hollywood-style speeds.

This emulator requires a large-ish screen to use. It probably won't work on mobile devices or old browsers. It was tested on Chrome 24 and 25 (and above), Firefox 19 (and above) and Opera 12, but will work with any reasonable version of these browsers with varying degrees of eye candy. It does not currently work on Internet Explorer and I'm not even working on it. Drop me a comment below if you need it to run on Explorer. I reserve the right to laugh at you, though.

1. Obligatory Bullet Points — We Have Them

Here's a list of the features available:

  • Full CFT processor emulation at the microcode level.
  • 48 kWords of RAM, 16 kWords of ROM.
  • Seven panel-oriented test programs already in ROM, or toggle in your own!
  • Full programmer's control panel, except the power and panel lock switches.
  • Debugging board (partial emulation, to capture processor output and state).
  • Various light decoding aids.

And here's what we don't have.

  • No Memory Banking Unit.
  • No Interrupt Controller Unit.
  • No serial ports, printers, graphics, sound, bells or whistles.

In short, you're getting the glorious CFT processor as the Elder Gods intended: unadulterated, unexpanded, without bloat or feature creep. You'll find it's still pretty capable.

2. Future Versions

This version of the emulator is meant to demonstrate the microcode and front panel, and for that we need to be graphics-intensive and run at the microcode level, clock tick by clock tick. A better approach would be to implement instruction-level emulation, which will easily be a couple of orders of magnitude faster, approaching the speed of the real thing. It won't have a front panel, but it will have a serial console and may be able to run ROM Forth.

3. Programmer's Front Panel Crash Course

All computers used to have these once upon a time. These days, some explanation is required. You can use the Programmer's Front Panel (PFP) to operate, inspect, program and debug the machine. The CFT's PFP shows pretty much all of the computer's internal state from the microcode level up.

3.1. Lights

There are loads. If you're overwhelmed, set the ‘LTS’ switch to ‘OFF’ to extinguish the less used lights. A bunch of these are permanently off because the hardware that would drive them isn't emulated. Others are off and unlabelled because they haven't been assigned functions yet. Don't worry about them. Here are the really important ones.

Program Counter
Shows the address of the next instruction to be executed.
Accumulator

Shows the value of the Accumulator (AC register), the main register of the machine. All the magic happens here.

Output Register
Shows the value of the Front Panel's Output Register, which is used to output results to the user. To display this register, set the white ‘OR/DR/µADDR’ switch to the ‘OR’ position (up — this is the default).
Run/Stop
Shows whether the computer is running or stopped. Many panel switches only work with the computer stopped. The computer only works when it's running, of course.
Fetch/Execute
Next to the Run/Stop lights are the Fetch/Exec lights. When Fetch is lit, the computer is reading a new instruction from memory. When Exec is lit, it's executing the last instruction it fetched.

3.2. Switches

There are three groups of these. Computer and panel control switches on the left, the Switch Register in the middle, and programming switches on the right. Here's a quick description of the important ones.

Reset/Start
Push up (or press Alt+Shift+R) to reset the computer, push down (or press Alt+Shift+S) to reset it and start it if it was stopped (this is the same behaviour as the ‘reset’ button on PCs).
Run/Stop
Push up (or press Alt+R) to start the computer, push down (or press Alt+S) to stop it. The computer can stop itself by operating the panel programmatically, and you can also stop it to examine its state. You can use the panel to program and debug the computer when it's stopped.
Fast/Slow/Creep
Sets the computer's clock speed. This switch behaves differently to the real thing: in fast mode, the lights don't update as often, and the processor runs at around 250–900 Hz (the real CFT runs at 4 MHz). In slow and creep modes, the lights update all the time. Slow is good for watching code execute, creep is good for watching microcode execute.
Light Switch
The LTS ON/OFF switch disables most of the panel's lights, which is useful when not debugging. This emulator also runs slightly faster in LTS OFF mode.
OR/DR/µADDR
Controls what's displayed on the Multi-Function Display, the middle of the fourth row of lights.
The Switch Register
The 16 switches in the middle of the front panel are used to input values to the computer. The panel uses them for programming, and the computer reads them to find out the user's intentions. When a switch is up, the corresponding bit value is 1, otherwise it's 0. You can enter values using the hexadecimal keys 09 and AF. Each key press sets four switches at once (highlighted in red). You can use Backspace to go back, but the focus wraps around to the leftmost digit after the rightmost digit is entered.
SR→PC
With the computer stopped, push down (or press Shift+:) to set the Program Counter to the value of the Switch Register.
→AC
With the computer stopped, push up or down (or press =) to set the Accumulator from the Switch Register.
MEM W/W NEXT
With the computer stopped, push up to store the write the Switch Register to the memory location indicated by the Program Counter lights. Push down (or press Shift+Enter) to do the same and increment the PC, ready for the next value to be deposited. This is the switch most used when storing programs or data in memory. Note there are another three switches for reading memory and writing and reading from I/O space that work in exactly the same way.

3.3. Visual Aids

Many lights and switches are decoded to help you enter values. The decoding appears next to the register name in dark grey (hover over the register to make it brighter). Values like 0000 are in hexadecimal, sometimes followed by a CFT machine code disassembly of the same value. Decimal values may be shown as something like (42) after that.

4. Playing with the Emulator

When you first load the emulator in your browser, it will run briefly then halt with all Accumulator lights off and all Output Register lights on. I use this as a prompt. Set the switch register to an address by toggling it in manually or pressing, e.g. C700and press ‘RUN’ (Alt+R). The address identifies one of the programs in the ROM. Each program behaves differently.

4.1. Address C000: Reboot

This is the code that shows the prompt and waits for an entry address. You can get there by simply pressing ‘START’ (Alt+Shift+S) at any time. The program halts, then (when the computer starts again), reads the value of the Switch Register and jumps to it.

4.2. Address C100: Counter

Counts up from zero to the value of the Switch Register, which can be changed while the program is running. The Output Register displays the current count. When the count is reached, the computer stops. Resuming by pressing the ‘RUN’ switch (Alt+R) starts counting all over again.

4.3. Address C200: Adding Machine

This is a simple adding machine with printout (on the Debug Terminal, shown below the front panel). Key in a number using the Switch Register and press the ‘RUN’ switch (Alt+R) to add it to the running total. The running total is displayed in the Output Register and printed out on the Debug Terminal. The total is displayed modulo 65,536 since the CFT is a 16-bit machine.

4.4. Address C300: Echo

The Accumulator and Output Register follow the value set on the Switch Register in real time. This program never stops.

4.5. Address C400: Rolling Lights

Upon entry, the program halts, waiting for a bit pattern to be set on the Switch Register. Try 1111. Once this is done, press the ‘RUN’ switch (Alt+R) and the Accumulator and Output Register will roll this pattern to the left. The gap in the pattern is due to the ‘L’ register participating in the rolls as a 17th bit.

4.6. Address C500: Fibonacci Sequence

This program prints out (and displays on the Output Register) the first numbers of the Fibonacci Sequence: 0, 1, 1, 2, 3, 5, etc. Only numbers that fit in 16 bits are displayed, so the calculation will stop with 46,368.

4.7. Address C600: Eratosthenes' (Prime Number) Sieve

This program prints out prime numbers calculated using Eratosthenes' Sieve, an ancient algorithm, but very effective for generating shortish lists of primes. Upon entry, the program prints out (on a line of its own) the maximum integer to be tested, which is 30. Then, prime numbers are printed out one by one, as they are found, separated by spaces.

The maximum number of primes can be set manually by modifying the value at location 0017 (hexadecimal), and then entering the algorithm at address C602. To do this:

  1. Press ‘STOP’ (Alt+S). The machine stops (if it wasn't already).
  2. Key in 0017 on the Switch Register (manually, or by pressing 0017 — ensure the first digit focused is the right one).
  3. Press ‘→PC’ (Shift+:). The PC is set to 0017.
  4. On the Switch Register, toggle in the maximum number to test. Remember that the number is entered in hexadecimal.
  5. Press ‘MEM W’ or ‘MEM W NEXT’ (Shift+Enter). The memory location 0017 is set to the value you toggled in.
  6. Press ‘START’ (Alt+Shift+S).
  7. Key in C602 on the Switch Register (manually, or by pressing C602 — ensure the first digit focused is the right one).
  8. Press ‘RUN’ (Alt+R). The calculation starts.

You can generate any number of primes up to a maximum of 45,056 (the beginning of the ROM). Unless you have a lot of free time, I recommend sticking to numbers less than a few hundred. Remember, this simulator runs at around 250 Hz.

4.8. Address C700: Hello World

The Standard Program. No input is expected. Output is sent to the Debug Terminal, but the ASCII/UCS character codepoints are also shown on the Output Register as they're printed. The program halts on termination, but can be restarted by pressing ‘Run’ (Alt+R).

5. Questions I'll Never Have to Read

In time honoured fashion, here's a NAQ (Never Asked Questions) list:

5.1. Why are so many lights off?

Some have never been designated a function. The version 4 front panel is smaller by 2U than the version 3, but it has space for a good 48 more lights, left there for future expansion and debugging of the actual beast. These lights are usually in the peripheral section, to the right of the register display and above the programming toggles.

Some other lights are supposed to be driven by hardware that isn't present. The Memory Bank Unit (MBU) isn't included here. It's part of the processor boards as constructed, but it's not necessary for the processor to function and we have more memory than we'll ever need for a panel-programmed computer. This includes the ‘MBEN’ light, which is on when the MBU is enabled and remapping addresses.

Likewise, the Interrupt Controller Unit is absent, and its lights are undriven and off. In fact, there's no hardware that generates interrupts on the Javascript emulator.

5.2. What do the Rightmost Two Toggles Do?

Nothing in this version.

The second switch from the right controls the memory map of the computer at boot time, before the MBU is enabled. In the ‘RAM BNK’ position, all 64 kWords of memory are RAM. In the ‘ROM BNK’ position, the upper 32 kWords are ROM. This is all done by the MBU, which isn't installed in this virtual computer. So the switch does nothing and the mapping is hardwired: 48 kWords of RAM, 16 kWords of ROM.

The rightmost switch issues Interrupt Requests at levels 1 and 6 when pressed. This is meant to input simple asynchronous signals to the processor via the front panel, but it requires the Interrupt Controller Unit which (shock!) isn't installed. So the switch does nothing.

5.3. In Fast Mode, the Lights Don't Make Sense. What Gives?

Well spotted. In Fast mode, there are 10 processor ticks for every update of the front panel lights, so some status changes aren't shown. If you have a really quick eye, you can see the PC jumping around where there are no jump instructions. Switch to a slower mode and the panel updates every tick.

5.4. I Need to Know More! Where Do I Start?

The top of the CFT project is right here. There's also an regularly updated design and built log which outlines the hundreds of 180° turns I've made so far (more to come).

For proper documentation, you might try the CFT Book, which contains more information about the hardware and software than you'll ever need (or want) to know. Even if you're me. This is a draft, and full of typesetting mistakes, typos, information in need of review, and the like. But it should be informative nonetheless.

6. Assembly Listing of the ROM

Here's the Standard Assembly listing of the ROM used in the emulator right now:

;;; A test program for the Javascript CFT Emulator.
;;;
;;; Copyright © 2013–2015 Alexios Chouchoulas.
;;;
;;; This program is free software; you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 2, or (at your option)
;;; any later version.
;;; 
;;; This program is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;; 
;;; You should have received a copy of the GNU General Public License
;;; along with this program; if not, write to the Free Software Foundation,
;;; Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  


.include "testing.asm"			; Debugging board definitions
.include "dfp.asm"			; Panel register definitions
.include "macro-generic.asm"		; Load some basic helper macros


;;; Fill both RAM and ROM parts of the image with SENTINEL instructions, which
;;; cause the emulator to terminate. This is so we can catch addresses which
;;; don't contain valid code. On the real CFT, the DFP will also log the failure
;;; to any testing computer attached to the serial port.
        
&0000:
        .fill 65535 SENTINEL

;;; Simplify calling puts using a macro

.equ PUTS R &100
.macro puts(addr)
		
	LIA %addr			; Load address
	JSR I PUTS			; And jump to subroutine

.end


;;; C000: Entry point. Halts, then jumps to the address toggled in the Switch
;;;       Register.

&c000:
start:
.scope

	LOAD _aputs			; Make puts available globally
	STORE PUTS
		
	puts(msg)			; Print out the boot message
		
	LOAD minus1			; We're done. AC = &ffff
        SOR				; Set all the OR lights on
        LI 0				; Set all the AC lights off
        HALT				; Halt and wait for user.

        LSR				; Load the switch register
        STORE R 1			; Store it
        JMP I R 1			; And jump to that address

minus1: .word &ffff
msg:    .str "Ready. Set addr & press Run.\n" 0
_aputs:	.word _puts

.endscope


;;; Note: for the rest of the code, we avoid macros so that the inner workings
;;;       of CFT assembly are made obvious.

                
;;; C100: Counter. Counts up from zero to the value of the Switch Register
;;;       (which can be changed while the program is running). The Output
;;;       Register displays the count.

&c100:
.scope
		
count:  LI 0				; mem[&10] = 0
        STORE R 10

acloop: LOAD R 10			; mem[&10]++
        INC
        STORE R 10
        SOR				; Set the OR lights
        LSR				; Read the panel switches
        XOR R 10			; Compare to the count (mem[&10])?
        SZA				; Different?
        JMP acloop			; Yes. Increment again.
        SUCCESS				; They're equal. Log success.
        HALT				; And halt.
		
        JMP count			; When we continue, run again.

.endscope


;;; C200: Adding machine with printout. Toggle in value to add, then actuate
;;;       the Run switch. The running total is shown on the Output Register
;;;       and printed out to the debugging terminal.

&c200:
.scope

	puts(msg)			; Print out prompt.
        LI 0				; Mem[&10] holds the sum. Reset it to 0.
        STORE R 10
        SOR				; Clear the lights
loop:	HALT				; Wait for user input.
        LSR				; Read the panel switches
        ADD R 10			; Add them to mem[&10]
        STORE R 10			; Store it back
        SOR				; Set the OR lights to the result
        PRINTD				; Print out the result to the DFP.
        PRINTNL				; Print a newline
        JMP loop			; Go again.

msg:	.str "Adder. Set value to sum, press Run.\n" 0

.endscope

		
;;; C300: Echo. The Accumulator and Output Register follow the value set on the
;;;       switches. Extremely simplistic.

&c300:
.scope

loop:   LSR				; Read the switches
        SOR				; Set the lights to the same value
        JMP loop			; Go again.

.endscope
        

;;; C400: Rolling lights. The program halts. Set a starting light pattern using
;;;       the Switch Register and actuate the Run switch. The Accumulator and
;;;       Output Register display a rolling lights display using that pattern.
;;;       The added 17th bit (seen as a gap) is because the L register also
;;;       participates in the rolls, but isn't set from the initial pattern.

&c400:
.scope

	puts(msg)			; Print out prompt
	CLA CLL				; Clear both A and L PDP-8 style
        SOR				; Set the OR lights
        HALT				; Halt & wait the user to set the pattern
		
        LSR				; Load initial pattern
roll:   RBL				; Roll left through L & AC lights
        SOR				; Set OR lights too
        JMP roll			; Keep rolling

msg:	.str "Set initial pattern, press Run.\n" 0

.endscope


;;; C500: Fibonacci sequence. The Output Register shows the Fibonacci sequence.
;;;       The sequence is also printed out on the debug terminal.

&c500:
.scope

        .equ n1 R &10			; First number
        .equ n2 R &11			; Second number
        .equ tmp0 R &12			; Temporary

again:  CLA CLL				; Clear AC and L, PDP-8 style.
        STORE n1			; n1 = 0
        LI 1
        STORE n2			; n2 = 1
loop:   LOAD n2
        STORE tmp0
        ADD n1
        SCL				; Is L set?
        JMP done			; Yes. We've run out of bits, so stop.
        SOR				; Set the OR lights to the current value
        PRINTD				; Print it out to the DFP console
        PRINTSP
        STORE n2			; Store it for the next iteration
        LOAD tmp0
        STORE n1
        JMP loop			; And keep going
done:   SUCCESS
        HALT
        JMP again		        ; Keep running as long as the user wants.

.endscope
        


;;; C600: Eratosthenes' Sieve. Prime number generator. Shows primes < 30.
;;;       The maximum number (30) by default is first printed out on the debug
;;;       terminal, on a line of its own. The primes are then printed out as
;;;       they are found, separated by spaces. They are also shown on the
;;;       Output Register. On the Javascript emulator, the calculation is slow
;;;       enough that the numbers can be read out from the OR.
;;;
;;;       To calculate a different range of primes, write the maximum integer
;;;       needed to location &0017 and start the program from location &C602.


&c600:

.scope

        .equ ONE R &0F          ; Constant 1
        .equ I0 R &080          ; Autoincrement register
        .equ x R &010           ; Count register
        .equ pos R &011         ; Current position
        .equ posptr R &012      ; Pointer to value at current position
        .equ prime R &013       ; Last prime found
        .equ neglimit R &014    ; The last number (negated, for subtracting)
        .equ tmp R &015         ; Temporary register
        .equ tmp2 R &016        ; Temporary register
        .equ count R &017       ; 
		
eratosthenes:   
        ;; Prepare the pad

        LOAD sieve_cnt          ; Size of working memory
        STORE count

	puts(msg)
        LOAD count
        PRINTD
        PRINTNL
        
        NEG
        STORE x                 ; Prepare for clearing the working memory
        LOAD sieve_start        ; Autoindex pointer
        STORE I0

        LOAD count              ; Used for limit checking
        NEG
        STORE neglimit

        LI 1
        STORE ONE

        ;; The first prime we report, 2, is found at initialisation time.
        ;; Since it's the prime that takes the most to mark (half of the
        ;; pad has to be marked!), we join this step and the pad init
        ;; step and start looking for primes at 3.

        LI 2                    ; Report 2
        SOR
        PRINTD
        PRINTSP

sieve_clear:
        LOAD x
        AND ONE                 ; Heh. EBM reference.
        XOR ONE                 ; Thus we initialise the entire table for prime=2
        STORE I I0
        ISZ x
        JMP sieve_clear

sieve_init:
        LI 2                    ; pos = 2 (to be incremented soon)
        STORE pos
        ADD sieve_start
        STORE posptr            ; posptr = &pad[pos]

sieve_next_prime:
        ISZ pos                 ; Next position
        JMP @+1
        ISZ posptr
        JMP @+1

        LOAD pos                ; Past the end of the working memory?
        ADD neglimit
        SNA                     ; pos + neglimit < 0?
        JMP sieve_done          ; The algorithm is done!

        LOAD I posptr           ; Consider pad[pos].
        SNZ                     ; pad[pos] == 0?
        JMP sieve_got_prime     ; Yes. We found a prime.
        JMP sieve_next_prime    ; No. Loop again.
        
sieve_got_prime:
        LOAD pos
        SOR
        PRINTD
        PRINTSP

sieve_mark_mult:
        ADD pos                 ; Next multiple of pos
        STORE tmp

        ADD sieve_start
        STORE tmp2

        LOAD tmp
        ADD neglimit
        SNA                     ; pos + neglimit < 0?
        JMP sieve_next_prime    ; No. Done with the pad, find next prime.

        ;; Within limits. Mark as non-prime and loop.
        LI 1                    ; pad[x] = 1
        STORE I tmp2
        LOAD tmp
        JMP sieve_mark_mult

sieve_done:
        SUCCESS
        HALT
        JMP eratosthenes        ;; If the user continues here, restart.
        
sieve_start:
        .word &1000
sieve_cnt:
        .word 200

msg:	.str "Prime numbers up to " 0

.endscope



;;; C700: Hello world program. Prints out The Standard Message on the debug
;;;       terminal.

&c700:
.scope
hello:  LIA msg
        STORE R &80

loop:   LOAD I R &80
        SOR
        SNZ
        JMP done
        PRINTC
        JMP loop

done:   SUCCESS
        HALT
        JMP hello

msg:    .str "Hello, world!\n"
        .str "This is a microcode-level CFT emulator written in Javascript! "
        .str "It's a lot slower than \nthe real thing, but it works."
        .str "The emulator contains a small ROM of sample programs."
        .str "Read the description below \nfor a list of programs and "
	.str "instruction on how to use them. The source of the ROM "
	.str "is also available below.\n"
        .str 0

.endscope


;;; A basic subroutine to print out a message.
;;; 
;;; AC:	address of message.
;;; 
;;; Side-effects: Clobbers index register &00FF.

_puts:		
.scope

	STORE R &ff			; Store the string address
loop:   LOAD I R &ff			; Load next character
        SNZ				; Is it zero?
        JMP done			; Then we're done.
        PRINTC				; Print it to the debugging terminal
        JMP loop			; And go again
done:	RET				; Return

.endscope

		
;;; System vectors

&fff0:
        JMP I boot
boot:   .word start

;;; Just in case interrupts are enabled by mistake (and an interrupt somehow
;;; triggered), the ISR simply disables interrupts again.

&fff8:  RTI                     ; Interrupts do nothing but disable interrupts

;;; End of file.

  1. Not all blinkenlights implemented yet — many are permanently off because the emulated cards driving them aren't installed.