The Floppy Drive
2005-05-08
Introduction
Accessing the floppy drive on a PC is one of the most complex tasks. Besides programming the floppy drive controller (FDC), it requires programming the DMA controller, because the FDC uses it to transfer data to and from memory. In this part, we develop all the code necessary to read data from the floppy drive.
DMA
Recall that, in the keyboard handler, we were reading bytes from the keyboard
controller with in al, 60h. This instruction uses the CPU to read one byte from the keyboard
controller and store it in al. We can then transfer this byte to memory with a mov
instruction.
The DMA controller allows us to transfer data between peripherals and memory without using the CPU (Well, almost. We use the CPU to program the DMA controller first.) This has the advantage that the CPU can work on other things while data is being transfered in the background. Originally, the DMA controller could also transfer data much faster than the CPU, but the extreme increase of CPU speeds has reversed that picture.
In order to use the DMA controller to transfer data from the FDC to memory, we must initialize the DMA controller with the right parameters. The controller has four channels, 0 through 3. Channel 2 is connected to the FDC. Before we start fiddling with it, we disable the channel. This can be done by writing to the mask register of the DMA controller, located at port 0x0a. We pass the number of the channel to mask in the lower 2 bits, and set bit 2 (this indicates we want to disable the channel).
; Disable channel 2
mov al, 110b
out 0ah, al
Now we can set the type of transfer. We are going to perform a write transfer (meaning that it writes to memory), in single mode (meaning the DMA controller will stop after the transfer is done). For this, we need to write a byte to port 0x0b, which is the controller's mode register. FIXME: Explain value.
; Set single write mode for channel 2
mov al, 01000110b
out 0bh, al
We also need to set the address to transfer data to. When talking to the DMA controller, we specify the address as a 64KB page and an offset within that page. For channel 2, the page number is written to port 0x81, and the offset to port 4. Both registers are 8-bit. For the page register, this is not a problem, as 4 bits are enough to encode any page accessible to the CPU. However, we need 16 bits to encode the offset. The trick used by the DMA controller is that the offset register is written to twice, once for the low byte and once for the high byte. The controller keeps track of which one it expects by means of a flip-flop, which we will need to initialize first. This is accomplished by writing any byte to port 0x0c. The full sequence is as follows:
; Set start address
out 0ch, al ; Initialize flip-flop
; Write offset
mov al, [low_offset]
out 04h, al
mov al, [high_offset]
out 04h, al
; Write page
mov al, [page]
out 081h, al
Next, we specify how much data we want to transfer by writing to port 5. The value to write is the number of 16-bit words, minus one. Just as with the start offset, we write a low and a high byte.
; Set count
out 0ch, al ; Initialize flip-flop
mov al, [low_count]
out 05h, al
mov al, [high_count]
out 05h, al
Finally, we enable the channel. This works just like disabling it, except that bit 2 is cleared.
; Enable channel 2
mov al, 10b
out 0ah, al
That's it for the DMA controller. Now comes the floppy drive.
The Floppy Drive Controller
The floppy drive controller (FDC) is a complex beast, sporting a number of registers and several commands. This section describes how to access some basic functions of the NEC PD765, which other FDCs are compatible with, at least enough for our purposes.
Magic Numbers
Although the FDC has several ports, for now we only need the digital
output register (DOR) at 0x3f1, the main status register
(MSR) at 0x3f4 and the data register at 0x3f5.
All these registers are 8 bits wide. These registers are for the primary
controller; ports are also reserved for a secondary controller at
addresses 80h lower than the ones for the primary
controller. However, most PCs don't have a secondary
FDC, so we'll focus on the primary FDC here. In addition
to the aforementioned ports, the FDC uses IRQ 6 and
DMA channel 2. Let's define all these magic values as
constants:
FDC_BASE EQU 3f0h
FDC_DOR EQU FDC_BASE + 1
FDC_MSR EQU FDC_BASE + 4
FDC_DATA EQU FDC_BASE + 5
FDC_IRQ EQU 6
FDC_DMA EQU 2
IRQ Handler
Now that we know the registers, IRQ, and DMA channel to use, we can go on to program the FDC. First of all, we will set up a handler for the IRQ. IRQ 6 is triggered when the FDC completes a command. Our IRQ handler will increment a byte in memory, to signal that the IRQ has been triggered:
fdcIRQHandler:
inc byte [cs:_fdcDone]
mov al, 60h + FDC_IRQ
out 20h, al
iret
Now we can write a routine that waits for that byte to be incremented:
_waitFDCDone:
test [cs:_fdcDone], byte 0xff
jnz .end
hlt
jmp _waitFDCDone
.end:
mov [cs:_fdcDone], byte 0
ret
The routine simply waits until the byte is nonzero, and
once it is, it sets it back to zero and returns. Now, whenever we
need to wait for the FDC to complete a command, we can call
_waitFDCDone.
Sending and Receiving Data
Next, we will want to be able to send and receive data to and from the FDC. In principle, this is as simple as reading and writing the data regsiter, but we may only do so when the FDC is ready for it. The FDC indicates this by setting the most significant bit in the main status register. In addition, the next most significant bit of that register indicates whether the FDC expects us to read (1) or write (0) the data register. Thus, the procedures for reading and writing the data register can be coded as follows:
_FDCRecvByte:
mov dx, FDC_MSR
.loop0:
in al, dx
test al, 11000000b
jnz .end
hlt
jmp .loop0
.end:
mov dx, FDC_DATA
in al, dx
ret
_FDCSendByte:
push bp
mov bp, sp
mov dx, FDC_MSR
.loop0:
in al, dx
test al, 10000000b
jnz .end
hlt
jmp .loop0
.end:
mov dx, FDC_DATA
mov al, [bp + 4]
out dx, al
pop bp
ret
Spinning Up the Drive
We're almost ready to start sending commands to the FDC, but first there is one very important step: spinning up the disk drive! This is where the digital output register comes in. Its structure is as follows:
| bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| meaning | mot3 | mot2 | mot1 | mot0 | dma | rst | drive | |
The motx fields address the
motors of the disk drives. The PD765 supports up to four drives, so
there are four motx fields. A drive motor is
activated by setting the corresponding field to 1, and disabled by
setting it to 0. The dma field can be used to enable (1) or
disable (0) the use of DMA. The FDCs in most PCs won't function without
DMA, so the field should be set to 1. The rst field can be used to reset the controller; this is
done by setting it to 0. Since we don't want to reset the FDC now, we
will set it to 1. Finally, the last two bits select the disk drive. I
don't know what effect that has, but it seems a good idea to select the
disk drive whose motor you're activating. Finally, after instructing the
FDC to spin up a drive, we need to wait a bit while it gets up to speed.
The FDC does not seem to give any sign when this is done, so we'll just
have to guess how long it takes. Half a second seems long enough. The
code for starting the motor of the first drive, then, looks as
follows: (FIXME: timer has not been explained yet)
mov dx, FDC_DOR
mov al, 00011100b ; start motor 0, activate DMA/IRQ, no reset,
drive 00
out dx, al
push word 9 ; Wait for 9 jiffies (half a second)
mov ax, SYSCALL_WAIT_JIFFIES
int SYSCALL_INTERRUPT
pop ax
Files
The file dma.inc contains code for working with the DMA controller. The file floppy.inc contains code to work with the FDC. syscall.inc contains updated system call numbers for use with the DMA controller and the FDC. Finally, the file floppy_kernel.asm contains a kernel which reads a messages from sectors 18 and 36 on the diskette and displays them.
An example message is contained in secretmessage.txt. This message can be
written to the diskette with a command like dd if=secretmessage.txt
of=/dev/fd0 bs=512 seek=18. Compiling the kernel and writing it to
the diskette works as usual.