PDP-8 on an FPGA II
Step-by-step
I began by following Prosser's design for the components and the state machine. Prosser's design contains both synchronous and edge-triggered elements. From my reading about FPGA design, fully-synchronous designes are more reliable and a more correct approach. As a result, I modified some of his states and signals to make everything synchronous to the pdp8_clk signal.
I also used what I called the "X; next_X" pattern when dealing with signals that need to be stable from one state to another. I'm not positive that this is the best or only way to do this, but it was a way to avoid inferred latches. My reading suggested that inferred latches were bad - since you're not specifying them specifically (perhaps) - and I couldn't find a way to make signals hold their values any other way. In the end it worked, as you'll see; Verilog professionals may take issue, though.
As I developed the Verilog, I made extensive use of three test files that I found in the accessory files of the Verilog PDP-8 - note that this is the earlier version; the version on Github doesn't have all the files. The binaries are in tests/diags/ and the manual for each is in tests/pdf:
-
MAINDEC-8I-D01C.mem - this is "Instruction Test 1"
-
MAINDEC-8I-D02B.mem - this is "Instruction Test 2" including interrupts
-
MAINDEC-08-D1GB.mem - this tests extended addressing
These allowed me to make major changes to the code while always being able to be sure that I hadn't broken anything that I had already fixed.​
Challenges along the way
While the documentation was clear, without formal training in Verilog, there were a bunch of challenges along the way that helped me learn some important things about the PDP-8, Verilog, FPGAs, and system integration:
Interrupts and Extended Memory
Prosser's PDP-8 only had 4K of RAM. I wanted to build a PDP-8/I which had the full 32K of RAM. This meant building in extended memory control and very careful treatment of interrupts. I made extensive use of the test program D02B and D1GB code and manuals - the documentation there was very helpful in figuring out what's supposed to happen.
-
Extended Memory - there are several registers related to extended memory. There are 8 4K "fields" of RAM. At a given time, only one field is active. The HD6120 datasheet has some details.
-
DF = "data field" - the 3-bit address of the current field for data access
-
IF = "instruction field" - the 3-bit address of the current field for instruction access. This is updated from the IB (see below) on a JMS or JMP instruction.
-
IB = "instruction buffer" - the 3-bit address set by the CIF instruction that sets up the pending instruction field. The IF is updated on a JMS or JMP instruction.
-
SF = "save field" - the 6-bit register that saves both the IF and DF on an interrupt.
-
-
Interrupts - there are several important steps and details in the process (details can be found on alt.sys.pdp8 and the HD6120 datasheet.
-
Interrupts are not allowed when there's a mismatch between the IB and the IF (for example, after a CIF or RMF instruction); this block (CIF_delay) is reset by a JMS or JMP instruction which copies IB to IF.
-
Interrupts must be enabled by the ION instruction. However, if there's an immediate interrupt, the instruction following the ION is allowed to complete before going to the ISR.​
-
On the interrupt, a JMS 0 instruction is forced. Note that this JMS instruction does not update IF. This is prevented by my interrupt_initiated signal.
-
On the interrupt, IF and DF are saved to SF; IF and DF are set to 000, the current PC is saved at 0000 and control transfers to 0001
-
A "double rotate" instruction?
When I finally got FOCAL-8 running using the .bn file that ships with the OS-X PDP-8 simulator, it opens with a congratulations message saying that I'm running FOCAL-8 on a "PDP-8 COMPUTER" as does simh. But some poking around on the web suggested that, since I was building a PDP-8/I, it should report that I'm using an 8/I. I found this program "WHATAMI" and it boils down to what instruction 7354 (CLA CLL CMA RAL RAR) sets the accumulator to. The other simulators leave the ac = 7777; but an 8/I should shift both left and right, leaving the ac = 3776. Once I added this code - which was not in Prosser - FOCAL-8 announces that I'm running an "8/I".
Input/Output
This was the most challenging part. Asynchronous I/O is tricky to program right and Prosser's documentation was hard for me to follow. After much trial and tribulation - and a lot of learning about handshaking signals - an e-mail from Vince Slyngstad at the alt.sys.pdp8 list cleared it all up (it is such a pleasure to work with excellent engineers like Vince):
The good news is that the device you are trying to emulate (device code 04)
is/was *very* simple:
At IOP1, it examines DONE, and requests a PC increment if it is set.
At IOP2, it clears DONE.
At IOP4, it initiates the transmission of whatever character is in the 8 LSB of AC.
The completion of the transmission of the character sets DONE. DONE also
asserts/implies "interrupt request".
That's it. IOP1, IOP2, and IOP4 will be seen (in that order) or not
depending on if the IOT has the corresponding bit set in the least
significant digit, and IOPx will be ignored unless the middle digits
are "04", indicating this device.
Similarly, if the device code is "03":
At IOP1, if DONE is set, request a PC increment.
At IOP2, clear DONE and request the CPU to clear AC.
At IOP4, OR the most recent byte received into AC.
The arrival of a complete character updates the most recent byte and
sets DONE. (This is device 03 DONE, which is independent of device
04 DONE. Either implies interrupt request via an open-collector
wire-OR.)
The 8/I is old enough that that's all there is. The I/O bus is horrifically
wide, burning *dozens of feet* of wire, just to keep things this simple.
By the time of the 8/E, they were more frugal with the wire vs transistor
tradeoff, and the IOPx system was changed so that there was a single
I/O strobe, and the I/O devices were expected to each decode the bits
of the IOT themselves to figure out what to do (and how to time it).
So, on the 8/E, device command 0 can do something, where on the
earlier machines, it can't (since no IOPx would be generated). Also,
the less popular codes (combining skip functions with something
else, for instance) were used instead to check errors, mask interrupts,
or whatever.
With that, I was finally able to get it going.