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