I have a TMS9900 based system on eurocards, FTI990 made by TEP. With this system, I have a copy of Eyring Research Institute's PDOS on floppy disks.
To save this OS, and to be able to share it with fellow TMS9900 enthousiasts, I have built a disk emulator for use with the FTI990, using my Digilent XUPV5 FPGA card.
Emulator (mounted on wooden plank) on top of the FTI990 system. From the left top side, clockwise: XUPV5 FPGA card, wire-wrap board with level shifters to interface with 5V logic, card with bus buffer IC's plugged into the FTI990, PCIe interface to laptop.
Above is a block diagram of the emulator: on the left is a PCIe interface to talk to the laptop used to capture the disk images. On the right is some logic to talk to the TMS9900 bus.
The emulator works as follows: part of the Boot ROM in the FTI990 is a table with four pointers to Boot-ROM provided disk device drivers; in my boot ROM, only the first pointer points to an actual driver, for that for the floppy disc controller. The other three pointers point to a table that indicates "no device here". When the FPGA sees a memory read transaction on the TMS9900 bus to read the second pointer in the table, it hijacks the bus to change the value read from the Boot ROM. It can do so, because the bus is held high with resistors, and pulled low by open-collector TTL logic outputs. This means that even though another device on the bus (the boot ROM) is already responding to that read transaction, we can change 1's into 0's in the read value by pulling those bits low. In this manner, we change the second pointer from F83A to E800.
+----------+
>0000 | |
| |
| RAM |
| |
>DFFF | |
+----------+
>E000 | | >E100->E1FF : sector buffer (256 bytes)
| Devices | >E200->E201 : sector# register (write) / status register (read)
| | >E202->E203 : single/double sided register
>EFFF | | >E800->E8FF : driver ROM
+----------+
>F000 | |
| Boot ROM | >FC2A->FC2B : hijacked driver pointer
>FFFF | |
+----------+
At address >E800 (in TMS 9900 assembly, '>' is the prefix for hexadecimal values), our emulator provides a driver for the emulated disk. when reading or writing a sector, the driver sends the sector# (logical block address) of the required sector to a sector# register at >E200. Then waits for that sector to become available in the buffer. If needed, the program running on the laptop will save the track that was previously in the buffer, and then load the buffer with the requested track. Once the status register indicates that the sector is available, the driver will copy the data to or from the 256-byte sector buffer located at >E100. Whenever sector 0 is read or written, the driver looks at the single/double-sided flag in that sector, and writes it to the single/double sided register at >E202. This is important, because when booting, the disk is always read in single-sided mode, even if it's a double-sided disk. Booting starts by reading a number of sectors from the disk, starting at sector 1136. Because the disk is treated as single-sided, sector 1136 is on track 71, not on track 35. However, after sector 0 is read, if it's a double-sided disk, whenever PDOS addresses sector 1136, we want to access track 35.
On the laptop, we continuously poll two registers on the PCIe side; one is the "current generation number" register. Every time a sector is written to by the FTI990, the value in this register increments by one. The software on the laptop keeps track of the generation number, and when it changes, it knows it has to read the track data back from the track buffer to keep it's disk image up to date. Once that's done, it writes the updated generation number to the "last seen generation number" register. The second register that's read is the "wanted track" register. If it's different from the track currently loaded in the track buffer, the software will write the requested track to the track buffer, then update the "loaded track" register.
The emulator will tell the FTI990-side driver that it's busy when either the "wanted track" doesn't match the "loaded track", or when the "current generation number" doesn't match the "last seen generation number".
PDOS boot floppy
PDOS 2.4 booting from the emulated disk
Emulator with test leads for debugging (with a logic analyzer)
Finally, here is the driver code as contained in the ROM:
; -------------------------------------
; Driver linkage table
;--------------------------------------
E800 100D JMP INIT ; init vector
E802 1010 JMP READ ; read vector
E804 1014 JMP WRIT ; write vector
E806 100D JMP MOFF ; motor-off vector
E808 0470 DATA >1136 ; boot sector
E80A 0470 DATA >1136
E80C 0470 DATA >1136
E80E 0470 DATA >1136
E810 4645 4939 3930 TEXT "FTI990-EMU"
2D45 4D55
E81A 0000 BYTE >00
; -------------------------------------
; Initialization routine
;--------------------------------------
E81C 04E0 E202 INIT: CLR @>E202 ; Clear double-sided register
E820 045B RT
; -------------------------------------
; Periodic routine
;--------------------------------------
E822 045B MOFF: RT
; -------------------------------------
; Read sector routine
;--------------------------------------
E824 C06D 0004 READ: MOV @4(R13),R1 ; destination: memory
E828 0202 E100 LI R2, >E100 ; source: sector buffer
E82C 1004 JMP COMN ; continue with read/write common code
; -------------------------------------
; Write sector routine
;--------------------------------------
E82E C0AD 0004 WRIT: MOV @4(R13),R2 ; source: memory
E832 0201 E100 LI R1, >E100 ; destination: sector buffer
; -------------------------------------
; Common read/write code
; -------------------------------------
E836 C020 E200 COMN: MOV @>E200, R0 ; ready ?
E83A 11FD JLT COMN ; no; wait
E83C C82D 0002 E200 MOV @2(R13),@>E200 ; request sector
E842 C020 E200 SEEK: MOV @>E200, R0 ; ready ?
E846 11FD JLT SEEK ; no; wait
E848 1304 JEQ DOIT ; ready, no error; continue
E84A 0200 0065 LI R0, >101 ; error; return error #101 (invalid track number)
E84E 0460 E870 B RETN
E852 0200 0080 LI R0, 128 ; number of words to copy
E856 CC72 COPY: MOV @R2+, @R1+ ; copy 1 word
E858 0600 DEC R0 ; are we done?
E85A 16FD JNE COPY ; no; copy next word
E85C C02D 0002 MOV @2(R13), R0 ; yes; sector is zero?
E860 1605 JNE SUCC ; no; we're done
E862 C06D 0004 MOV @4(R13), R1 ; yes, get address of buffer
E866 C821 001E E202 MOV @30(R1), >E202 ; copy double-sided flag from sector buffer to double sided register
E86C 04C0 SUCC: CLR R0 ; indicate success
E86E 05CE INCT R14 ; increment return address by two (no error)
E870 04E0 2FE8 RETN: CLR @L3LOCK ; clear OS lock
E873 0380 RTWP ; return to OS