[TUTORIAL]: How to read (and supposedly write) floppies.

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
User avatar
mystran
Member
Member
Posts: 670
Joined: Thu Mar 08, 2007 11:08 am

[TUTORIAL]: How to read (and supposedly write) floppies.

Post by mystran »

Take your time to read it. There's lots of stuff to say. Easy-to-print HTML and/or PDF version will come at some point.

Legal issues:

Please do not distribute this version of the document. It is still work-in-process, and I don't want different version around the web yet. You can take copies or print it for your personal use if you want, but do not otherwise distribute this version. I will make a distributable version at some point in the future.

The code is free to use, however under the following license:

Code: Select all

/******************************************************************************
* Copyright (c) 2007 Teemu Voipio                                             *
*                                                                             *
* Permission is hereby granted, free of charge, to any person obtaining a     *
* copy of this software and associated documentation files (the "Software"),  *
* to deal in the Software without restriction, including without limitation   *
* the rights to use, copy, modify, merge, publish, distribute, sublicense,    *
* and/or sell copies of the Software, and to permit persons to whom the       *
* Software is furnished to do so, subject to the following conditions:        *
*                                                                             *
* The above copyright notice and this permission notice shall be included in  *
* all copies or substantial portions of the Software.                         *
*                                                                             *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  *
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    *
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL    *
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER  *
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING     *
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER         *
* DEALINGS IN THE SOFTWARE.                                                   *
*                                                                             *
******************************************************************************/
What is covered:

How to read (and supposedly write) normal floppies with DMA, one cylinder at a time (though you can read/write less, if you understand the text below).

My code is in C, and assumes that you have macros in8_p(port) and out8_p(port,byte) which read and write to IO ports with your usual iodelay (dummy port access or usleep() or whatever you have).

The prerequisite here is to be able to wait for an interrupt (and in production code preferably timeout) so make sure you have working interrupt routines first. It would be possible to write the floppy driver to do it's work from the interrupt routines (Linux does something like this), but it makes code a LOT less readable, so I'll just assume that when we call a function irq_wait(number), the scheduler will go pick another thread, and will wakeup us when an interrupt has occured.

You will also need to be able to sleep() for certain amounts of time, so you also need some sort of timer support. The version of sleep() I'll be using takes it's arguments as number of ticks, which are 10ms each, but I'll put comment on each sleep() with the real time, so you don't have to remember this. The normal rule with timers is that they never wake you before the requested time has elapsed, so sleep(1) really waits for two ticks: one so we know a point in time to count from, and another so we know a full period between ticks has elapsed.. but that's how every good timer works. ;)

Basic concepts:

Floppy addressing is done in CHS format. C means cylinder, H means head and S means sector. Each cylinder has one track for each head. Typically there are one or two heads, one for each side of the floppy. Each track has a number of sectors, which varies depending on how the floppy was formatted. For the common case of 1.44MB floppy with normal formatting, there are 80 cylinders (addressed from 0 to 79), 2 heads (0 and 1) and 18 sectors per track (from 1 to 18, yes).

To find the correct track, one issues a SEEK command for the head in question, with a cylinder number to seek for. Sectors need not be seeked, the drive will do that for you when you issue a READ or WRITE command. In order for SEEK to work, one is supposed to first calibrate the drive, with a special command, that will move the head 79 times towards track 0. This is normally necessary only after media change (and certain errors?).

Once a READ or WRITE request is issued, the drive will wait until the specified sector is under the (relevant) head, and starts reading or writing data. It normally does this using ISA DMA (specifically channel 2). It will continue to read until it gets and error, or DMA tells it's happy. Errors can be anything from end of cylinder to bad or write-protected floppy. So one only needs to tell the floppy controller where to start, and DMA takes care of the rest, assuming everything goes right.

The important thing here is that DMA controls how much data we transfer. Bochs happens to be kind enough to not report an error if you run into the end of track before DMA says it's time to stop, but real hardware will (at least mine does). We'll get to the DMA details (which several sources actually get wrong) later.

Commands, registers and all that stuff:

There can be several floppy controllers, each with up to four drives. On most modern computers you'll find one controller, with one or two drives (=one cable), so whether or not you want to support more controllers is up to you. All of them will normally use the same DMA 8-bit channel (2), and the same IRQ (6). This obviously means that you can only access one drive at any given time.

Following is a list of the registers of the floppy controller, relative to the base address, and a list of commands. I will only list those commands and registers that we'll be using. You can get the rest from various sources of documentation.

Code: Select all

// standard base address of the primary floppy controller
static const int floppy_base = 0x03f0;
// standard IRQ number for floppy controllers
static const int floppy_irq = 6;

// The registers of interest. There are more, but we only use these here.
enum floppy_registers {
   FLOPPY_DOR  = 2,  // digital output register
   FLOPPY_MSR  = 4,  // master status register, read only
   FLOPPY_FIFO = 5,  // data FIFO, in DMA operation for commands
   FLOPPY_CCR  = 7   // configuration control register, write only
};

// The commands of interest. There are more, but we only use these here.
enum floppy_commands {
   CMD_SPECIFY = 3,            // SPECIFY
   CMD_WRITE_DATA = 5,         // WRITE DATA
   CMD_READ_DATA = 6,          // READ DATA
   CMD_RECALIBRATE = 7,        // RECALIBRATE
   CMD_SENSE_INTERRUPT = 8,    // SENSE INTERRUPT
   CMD_SEEK = 15,              // SEEK
};
What drive I have?

There are various ways to figure out what kind of drive you have. We'll use the simplest method here: ask CMOS. So the drive detection looks like this:

Code: Select all

static const char * drive_types[8] = {
    "none",
    "360kB 5.25\"",
    "1.2MB 5.25\"",
    "720kB 3.5\"",

    "1.44MB 3.5\"",
    "2.88MB 3.5\"",
    "unknown type",
    "unknown type"
};

// Obviously you'd have this return the data, start drivers or something.
void floppy_detect_drives() {

   out8_p(0x70, 0x10);
   unsigned drives = in8_p(0x71);

   printk(" - Floppy drive 0: %s\n", drive_types[drives >> 4]);
   printk(" - Floppy drive 1: %s\n", drive_types[drives & 0xf]);

}
Rest of the code will assume that we are dealing with a 1.44MB 3.5" drive, and for simplicity I'll hardcode stuff for the first drive, but will give the details of accessing the other drives. That way there'll be less book-keeping.

Writing commands and reading results

These are simple helper functions, since we'll have to check the MSR register in order to know if the controller is ready. I'll give the full meaning of the MSR as well, so you can have documentation together with the code to avoid having to lookup a lot of information later if you wonder what the details are.

Code: Select all

//
// The MSR byte: [read-only]
// -------------
//
//  7   6   5    4    3    2    1    0
// MRQ DIO NDMA BUSY ACTD ACTC ACTB ACTA
//
// MRQ is 1 when FIFO is ready (test before read/write)
// DIO tells if controller expects write (1) or read (0)
//
// NDMA tells if controller is in DMA mode (1 = no-DMA, 0 = DMA)
// BUSY tells if controller is executing a command (1=busy)
//
// ACTA, ACTB, ACTC, ACTD tell which drives position/calibrate (1=yes)
//
//

void floppy_write_cmd(int base, char cmd) {
    int i; // do timeout, 60 seconds
    for(i = 0; i < 600; i++) {
        timer_sleep(1); // sleep 10 ms
        if(0x80 & in8_p(base+FLOPPY_MSR)) {
            return (void) out8_p(base+FLOPPY_FIFO, cmd);
        }
    }
    panic("floppy_write_cmd: timeout");    
}

unsigned char floppy_read_data(int base) {

    int i; // do timeout, 60 seconds
    for(i = 0; i < 600; i++) {
        timer_sleep(1); // sleep 10 ms
        if(0x80 & in8_p(base+FLOPPY_MSR)) {
            return in8_p(base+FLOPPY_FIFO);
        }
    }
    panic("floppy_read_data: timeout");
    return 0; // not reached
}
You could also poll for 0xC0 in write_cmd, but 0x80 is fine since we'll know what the controller expects anyway. My timeout is probably far too long, and the correct thing if it fails would be to reinitialize the controller or stop the driver (and not panic() the whole kernel), but we'd need to check every return value then which would make rest of the code impossible to follow.

Initialization

The FLOPPY_DOR registers controls various things, including whether the controller is enabled. It also controls the state of motors for all 4 drives. One of the bits is NOT_RESET which must be 1 for the controller to be enabled. So in order to reset the controller, one pulls all the bits to 0, then sets the NOT_RESET bit. We'll also set the DMA bit to enable interrupts and DMA. You'll need that to get interrupts even if you don't care for DMA (which is the simplest method anyway). We'll see the details of the register later...

Once the controller has been re-enabled, you'll get an interrupt, and you have to send a SENSE_INTERRUPT to get status from the controller. Note that you can't blindly do SENSE_INTERRUPT after each interrupt, because after READ/WRITE you'll get an interrupt but they have their own special way of reporting status (with more information). However we'll do SENSE_INTERRUPT from a couple of places, so make it a separate function. It needs to return two pieces of information, so we'll use output parameters. Here we just ignore the results (though you could check for error in st0).

We'll then set transfer speed in CCR. Most bits need to be zero, so valid values are 0=500kbps, 1=300kbps, 2=250kbps and 3=1Mbps. For 1.44MB floppies the correct setting is 00=500kbps. Other settings can be found for example in from http://www.isdaman.com/alsos/hardware/fdc/floppy.htm but we'll ignore such details.

You're also supposed to send a SPECIFY command, specifying mechanical timing such as "steprate", "head unload time" and "head load time". SPECIFY also sets whether we really wanna use DMA, or if we just set DMA bit in DOR for the purpose of having interrupts. One way to get the timings is to ask BIOS. Another way is to calculate them yourself to have values that you hope the drive to be able to do with. The simplest thing is to hardcode something that's likely to be sane (which I'll do here, for simplicity).

Finally the drive needs to be calibrated, so that it knows on which cylinder it's on. I'll skip the mechanical details (the controller knows) but after successful calibration we'll be at track 0. If we are not, then we'll retry a few times (my retry counts are seriously overkill). If we can't calibrate at all, then there's probably no floppy in the drive... or it's time to find a new floppy. Once we're done, the drive is ready to serve us.

Notice that curiously the calibration command doesn't take a head-number so supposedly it calibrates both heads at once? No idea, but it really does not take a head-number.

Code: Select all

void floppy_check_interrupt(int base, int *st0, int *cyl) {
    
    floppy_write_cmd(base, CMD_SENSE_INTERRUPT);

    *st0 = floppy_read_data(base);
    *cyl = floppy_read_data(base);
}

// Move to cylinder 0, which calibrates the drive..
int floppy_calibrate(int base) {

    int i, st0, cyl = -1; // set to bogus cylinder

    floppy_motor(base, floppy_motor_on);

    for(i = 0; i < 10; i++) {
        // Attempt to positions head to cylinder 0
        floppy_write_cmd(base, CMD_RECALIBRATE);
        floppy_write_cmd(base, 0); // argument is drive, we only support 0

        irq_wait(floppy_irq);
        floppy_check_interrupt(base, &st0, &cyl);
        
        if(st0 & 0xC0) {
            static const char * status[] =
            { 0, "error", "invalid", "drive" };
            printk("floppy_calibrate: status = %s\n", status[st0 >> 6]);
            continue;
        }

        if(!cyl) { // found cylinder 0 ?
            floppy_motor(base, floppy_motor_off);
            return 0;
        }
    }

    printk("floppy_calibrate: 10 retries exhausted\n");
    floppy_motor(base, floppy_motor_off);
    return -1;
}


int floppy_reset(int base) {

    out8_p(base + FLOPPY_DOR, 0x00); // disable controller
    out8_p(base + FLOPPY_DOR, 0x0C); // enable controller

    irq_wait(floppy_irq); // sleep until interrupt occurs

    {
        int st0, cyl; // ignore these here..
        floppy_check_interrupt(base, &st0, &cyl);
    }

    // set transfer speed 500kb/s
    out8_p(base + FLOPPY_CCR, 0x00);

    //  - 1st byte is: bits[7:4] = steprate, bits[3:0] = head unload time
    //  - 2nd byte is: bits[7:1] = head load time, bit[0] = no-DMA
    // 
    //  steprate    = (8.0ms - entry*0.5ms)*(1MB/s / xfer_rate)
    //  head_unload = 8ms * entry * (1MB/s / xfer_rate), where entry 0 -> 16
    //  head_load   = 1ms * entry * (1MB/s / xfer_rate), where entry 0 -> 128
    //
    floppy_write_cmd(base, CMD_SPECIFY);
    floppy_write_cmd(base, 0xdf); /* steprate = 3ms, unload time = 240ms */
    floppy_write_cmd(base, 0x02); /* load time = 16ms, no-DMA = 0 */

    // it could fail...
    if(floppy_calibrate(base)) return -1;
    
}

Notice how in calibration we actually check for the return status. "Error" means some error happened, "invalid" means the command was invalid, and "drive" means some error happened and drive isn't ready. Floppies are known to fail a lot, so checking and retrying is good idea.

Motor control:

The other thing you should notice, is the "floppy_motor" commands. For the drive to do much of anything, the motor must be on, and this detail must be taken care of by software. So before you can actually run the above code, you need something like this (with the functions and enum declared above previous code):

Code: Select all

//
// The DOR byte: [write-only]
// -------------
//
//  7    6    5    4    3   2    1   0
// MOTD MOTC MOTB MOTA DMA NRST DR1 DR0
//
// DR1 and DR0 together select "current drive" = a/00, b/01, c/10, d/11
// MOTA, MOTB, MOTC, MOTD control motors for the four drives (1=on)
//
// DMA line enables (1 = enable) interrupts and DMA
// NRST is "not reset" so controller is enabled when it's 1
//
enum { floppy_motor_off = 0, floppy_motor_on, floppy_motor_wait };
static volatile int floppy_motor_ticks = 0;
static volatile int floppy_motor_state = 0;

void floppy_motor(int base, int onoff) {

    if(onoff) {
        if(!floppy_motor_state) {
            // need to turn on
            out8_p(base + FLOPPY_DOR, 0x1c);
            timer_sleep(50); // wait 500 ms = hopefully enough for modern drives
        }
        floppy_motor_state = floppy_motor_on;
    } else {
        if(floppy_motor_state == floppy_motor_wait) {
            printk("floppy_motor: strange, fd motor-state already waiting..\n");
        }
        floppy_motor_ticks = 300; // 3 seconds, see floppy_timer() below
        floppy_motor_state = floppy_motor_wait;
    }
}

void floppy_motor_kill(int base) {
    out8_p(base + FLOPPY_DOR, 0x0c);
    floppy_motor_state = floppy_motor_off;
}

//
// THIS SHOULD BE STARTED IN A SEPARATE THREAD.
//
//
void floppy_timer() {
    while(1) {
        // sleep for 500ms at a time, which gives around half
        // a second jitter, but that's hardly relevant here.
        timer_sleep(50);
        if(floppy_motor_state == floppy_motor_wait) {
            floppy_motor_ticks -= 50;
            if(floppy_motor_ticks <= 0) {
                floppy_motor_kill(floppy_base);
            }
        }
    }
}

Oh well, my code is documented well enough that I won't have to explain that, I hope. But'll explain anyway: We'll have a state machine with three states. When you turn the motor on, it goes to the "on" state, and if it was in "off" state, we'll tell the controller to turn the motor. When we supposedly turn it off, we don't turn off anything, but instead make the state "wait" and set a timeout. If the state is still "wait" when the timeout expires, we'll have a background thread actually kill the motor. This saves the longish wait if we issue lots of motor-on, motor-off commands all the time. The "strange" is there because if we get two motor-off commands in a row, then some code somewhere probably doesn't know how to keep track of whether it wants the motor on or off, and should be fixed.

You should really have 4 state machines, one for each drive, and you should really also have it select the right drive when we want to operate with one. And somehow somewhere you'd have to make sure you don't try to operate on more than one drive at the same time. But I'll skip all that, and just have my code assume there's one drive.

Seeking for the data

So now that we have done the initialization and know how to get it spin the disk, we'll next concentrate on figuring out how to get away from cylinder 0, where we ended with the calibration request. This command takes two parameters of interest: the head that we want to move, and the cylinder we want to end up at. Other than that it looks a LOT like the calibration command:

Code: Select all

// Seek for a given cylinder, with a given head
int floppy_seek(int base, unsigned cyli, int head) {

    unsigned i, st0, cyl = -1; // set to bogus cylinder

    floppy_motor(base, floppy_motor_on);

    for(i = 0; i < 10; i++) {
        // Attempt to position to given cylinder
        // 1st byte bit[1:0] = drive, bit[2] = head
        // 2nd byte is cylinder number
        floppy_write_cmd(base, CMD_SEEK);
        floppy_write_cmd(base, head<<2);
        floppy_write_cmd(base, cyli);

        irq_wait(floppy_irq);
        floppy_check_interrupt(base, &st0, &cyl);

        if(st0 & 0xC0) {
            static const char * status[] =
            { "normal", "error", "invalid", "drive" };
            printk("floppy_seek: status = %s\n", status[st0 >> 6]);
            continue;
        }

        if(cyl == cyli) {
            floppy_motor(base, floppy_motor_off);
            return 0;
        }

    }

    printk("floppy_seek: 10 retries exhausted\n");
    floppy_motor(base, floppy_motor_off);
    return -1;
}
Ready to transfer?

Well, DMA is not ready to transfer. So next thing is to have a routine to setup it. There's a whole total of 2 bits difference depending on whether we are reading or writing, so we'll only have one routine. However, this whole thing is complicated by some unfortunate details about memory management: we'll need a buffer which is below 16MB and doesn't cross 64k boundary. You probably knew that already? Well, I'll leave it up to you to figure out one, but I'll cheat and have my linker give me a buffer aligned at 32k boundary. We'll need a big buffer because we'll do big transfers at once.

Basicly, you just give DMA the buffer address, the number of bytes to transfer, and whether we're transferring to or from memory. The devil is in the details. You need to give 16-bit addresses to a 8-bit device, which has to be told "I'm going to write another low-byte" every time you want to write a low-byte. At least after a low-byte it knows to expect a high-byte. But that is not the most interesting detail about this little beast.

The important thing to remember, is that DMA counts in a strange way. You give it a number to count down from. After every byte, it decrements this value. You'd think that it says "done" once it reaches 0. Nope, it does not. It's happy (done) when it wraps around from 0 to 0xffff. So it transfers one byte more than the count that was programmed into it. That's actually pretty nice, because you can do full 64k transfer by using "0xffff" as the count and it's impossible (some would say fnord) to transfer 0 bytes even if DMA didn't make it so, so nothing is lost. But it's easy to forget this detail. Many people actually do.

And the reason we want to care about a single byte, is that the floppy drive won't stop before DMA tells it to. So if we program too long count, we'll get too long transfer, or "end-of-track" error. I'd guess this is common reason people's code works in emulators but not on real hardware: at least Bochs doesn't report end-of-sector.

The other important thing to remember, is that you give DMA address is physical memory. This won't touch my example code when it's running in my own kernel, because my kernel is in the same place in both virtual and physical memory. But if you have something like a "higher-half" kernel, then you have to take care of finding the physical address and having continuous physical memory. And remember it can't cross 64k in physical memory. For ISA DMA, there is no virtual memory whatsoever.

Anyway, with these details we can have the following routine:

Code: Select all

// Used by floppy_dma_init and floppy_do_track to specify direction
typedef enum {
    floppy_dir_read = 1,
    floppy_dir_write = 2
} floppy_dir;


// we statically reserve a totally uncomprehensive amount of memory
// must be large enough for whatever DMA transfer we might desire
// and must not cross 64k borders so easiest thing is to align it
// to 2^N boundary at least as big as the block
#define floppy_dmalen 0x4800
static const char floppy_dmabuf[floppy_dmalen]
                  __attribute__((aligned(0x8000)));

static void floppy_dma_init(floppy_dir dir) {

    union {
        unsigned char b[4]; // 4 bytes
        unsigned long l;    // 1 long = 32-bit
    } a, c; // address and count

    a.l = (unsigned) &floppy_dmabuf;
    c.l = (unsigned) floppy_dmalen - 1; // -1 because of DMA counting

    // check that address is at most 24-bits (under 16MB)
    // check that count is at most 16-bits (DMA limit)
    // check that if we add count and address we don't get a carry
    // (DMA can't deal with such a carry, this is the 64k boundary limit)
    if((a.l >> 24) || (c.l >> 16) || (((a.l&0xffff)+c.l)>>16)) {
        panic("floppy_dma_init: static buffer problem\n");
    }

    unsigned char mode;
    switch(dir) {
        // 01:0:0:01:10 = single/inc/no-auto/to-mem/chan2
        case floppy_dir_read:  mode = 0x46; break;
        // 01:0:0:10:10 = single/inc/no-auto/from-mem/chan2
        case floppy_dir_write: mode = 0x4a; break;
        default: panic("floppy_dma_init: invalid direction");
                 return; // not reached, please "mode user uninitialized"
    }

    out8_p(0x0a, 0x06);   // mask chan 2

    out8_p(0x0c, 0xff);   // reset flip-flop
    out8_p(0x04, a.b[0]); //  - address low byte
    out8_p(0x04, a.b[1]); //  - address high byte

    out8_p(0x81, a.b[2]); // external page register

    out8_p(0x0c, 0xff);   // reset flip-flop
    out8_p(0x05, c.b[0]); //  - count low byte
    out8_p(0x05, c.b[1]); //  - count high byte

    out8_p(0x0b, mode);   // set mode (see above)

    out8_p(0x0a, 0x02);   // unmask chan 2
}
So first I talk a lot about how you must have the count right, and then I hardcode it? Well, that's because I'm lazy, and I'm only going to show how to read/write full cylinders. You can lookup the stupid datasheet for more info about the various bits. And like said, the linker-trick doesn't work (at least not unmodified) if you relocate your kernel. You'll need something a bit more clever. But this is just a tutorial, and "it works for me(tm)".

However, in order to make that more easily turned into generic code, I've pulled the relevant parts out into variables, so they can be made parameters easily.

Notice that I don't use the auto-reset. That's because when trying to figure out this stuff, I noticed that if you get a read error while the DMA is half-way done, you'll have to reinitialize it anyway, since you don't know where exactly it was. So we'll just do a full reset every time, so we don't need to keep track of what was done before.

Reading / writing 0x4800 (=2*18*512) bytes at once:

With the above support code, we can finally read/write to a floppy. If we're writing, you better hope you've got the data in the buffer. If we're reading, you can find the data in the buffer after this function return success. It's a bit of a monster, but quite simple in operation. Just lots of code to extract error codes. Since we just hardcoded DMA to 0x4800 bytes, we'll expect 18 sectors of 512 bytes each, and we'll have to start at sector 1 (yes they count from 1) to get them all, so we'll hardcode that as well.

But that only gives us 0x2400 bytes, so we'll throw in some extra fancy magic: MULTITRACK! Namely, you can tell the controller to start reading with head 0, and when it gets to the end of track, it should start reading with head 1. Once it then reaches end-of-track, it'll report and error, unless the DMA just told it "enough" (or if it's Bochs, it'll just give you success anyway, possibly doing "something" else, no idea). Multi-tracking means you'll only need to call the function 80 times (one for each cylinder) to read the whole floppy into the memory. Ofcourse normally you'd not do that. You'd read the cylinders that you need. But there's no point in reading single sectors.

Because the disk rotates the same speed whether you are reading or not, it'll take about the same time to read one sector or the whole track. Well, technically reading a single sector takes on average 37% of the average time to read a full track, or about 20% of the time to read both tracks, assuming the second track starts where the second track ends. So if we end up reading the other sectors as well, we'll waste around 6.6 (single track) or 7.2 times (multi-track) by reading sector at a time.

So .. at least for reading... go with multi-track for the whole cylinder. And you do it like this:

Code: Select all

// This monster does full cylinder (both tracks) transfer to
// the specified direction (since the difference is small).
//
// It retries (a lot of times) on all errors except write-protection
// which is normally caused by mechanical switch on the disk.
//
int floppy_do_track(int base, unsigned cyl, floppy_dir dir) {
    
    // transfer command, set below
    unsigned char cmd;

    // Read is MT:MF:SK:0:0:1:1:0, write MT:MF:0:0:1:0:1
    // where MT = multitrack, MF = MFM mode, SK = skip deleted
    // 
    // Specify multitrack and MFM mode
    static const int flags = 0xC0;
    switch(dir) {
        case floppy_dir_read:
            cmd = CMD_READ_DATA | flags;
            break;
        case floppy_dir_write:
            cmd = CMD_WRITE_DATA | flags;
            break;
        default: 

            panic("floppy_do_track: invalid direction");
            return 0; // not reached, but pleases "cmd used uninitialized"
    }

    // seek both heads
    if(floppy_seek(base, cyl, 0)) return -1
    if(floppy_seek(base, cyl, 1)) return -1

    int i;
    for(i = 0; i < 20; i++) {
        floppy_motor(base, motor_on);

        // init dma..
        floppy_dma_init(dir);

        timer_sleep(10); // give some time (100ms) to settle after the seeks

        floppy_write_cmd(base, cmd);  // set above for current direction
        floppy_write_cmd(base, 0);    // 0:0:0:0:0:HD:US1:US0 = head and drive
        floppy_write_cmd(base, cyl);  // cylinder
        floppy_write_cmd(base, 0);    // first head (should match with above)
        floppy_write_cmd(base, 1);    // first sector, strangely counts from 1
        floppy_write_cmd(base, 2);    // bytes/sector, 128*2^x (x=2 -> 512)
        floppy_write_cmd(base, 18);   // number of tracks to operate on
        floppy_write_cmd(base, 0x1b); // GAP3 length, 27 is default for 3.5"
        floppy_write_cmd(base, 0xff); // data length (0xff if B/S != 0)
        
        irq_wait(floppy_irq); // don't SENSE_INTERRUPT here!

        // first read status information
        unsigned char st0, st1, st2, rcy, rhe, rse, bps;
        st0 = floppy_read_data(base);
        st1 = floppy_read_data(base);
        st2 = floppy_read_data(base);
        /*
         * These are cylinder/head/sector values, updated with some
         * rather bizarre logic, that I would like to understand.
         *
         */
        rcy = floppy_read_data(base);
        rhe = floppy_read_data(base);
        rse = floppy_read_data(base);
        // bytes per sector, should be what we programmed in
        bps = floppy_read_data(base);

        int error = 0;

        if(st0 & 0xC0) {
            static const char * status[] =
            { 0, "error", "invalid command", "drive not ready" };
            printk("floppy_do_sector: status = %s\n", status[st0 >> 6]);
            error = 1;
        }
        if(st1 & 0x80) {
            printk("floppy_do_sector: end of cylinder\n");
            error = 1;
        }
        if(st0 & 0x08) {
            printk("floppy_do_sector: drive not ready\n");
            error = 1;
        }
        if(st1 & 0x20) {
            printk("floppy_do_sector: CRC error\n");
            error = 1;
        }
        if(st1 & 0x10) {
            printk("floppy_do_sector: controller timeout\n");
            error = 1;
        }
        if(st1 & 0x04) {
            printk("floppy_do_sector: no data found\n");
            error = 1;
        }
        if((st1|st2) & 0x01) {
            printk("floppy_do_sector: no address mark found\n");
            error = 1;
        }
        if(st2 & 0x40) {
            printk("floppy_do_sector: deleted address mark\n");
            error = 1;
        }
        if(st2 & 0x20) {
            printk("floppy_do_sector: CRC error in data\n");
            error = 1;
        }
        if(st2 & 0x10) {
            printk("floppy_do_sector: wrong cylinder\n");
            error = 1;
        }
        if(st2 & 0x04) {
            printk("floppy_do_sector: uPD765 sector not found\n");
            error = 1;
        }
        if(st2 & 0x02) {
            printk("floppy_do_sector: bad cylinder\n");
            error = 1;
        }
        if(bps != 0x2) {
            printk("floppy_do_sector: wanted 512B/sector, got %d", (1<<(bps+7)));
            error = 1;
        }
        if(st1 & 0x02) {
            printk("floppy_do_sector: not writable\n");
            error = 2;
        }

        if(!error) {
            floppy_motor(base, floppy_motor_off);
            return 0;
        }
        if(error > 1) {
            printk("floppy_do_sector: not retrying..\n");
            floppy_motor(base, floppy_motor_off);
            return -2;
        }
    }

    printk("floppy_do_sector: 20 retries exhausted\n");
    floppy_motor(base, floppy_motor_off);
    return -1;

}
No wait.. I mean, like this:

Code: Select all

int floppy_read_track(int base, unsigned cyl) {
    return floppy_do_track(base, cyl, floppy_dir_read);
}

int floppy_write_track(int base, unsigned cyl) {
    return floppy_do_track(base, cyl, floppy_dir_write);
}
Was that funny?

Like you probably noticed, most of the code is simply to get human readable error messages.

The only possible oddity with the above code is the fact that indeed head is set in two places within the same read/write command. First in the same byte that specifies the drive, and then again as a separate byte after cylinder. This is indeed correct, even if odd. The cylinder specified in the read/write doesn't cause a seek (we don't try to figure out if the controller would support implied-seeks) but supposedly we'll get an error (at least somewhere, maybe) if we try to pass in something other than where we just seeked to. What you do with the errors is up to you. The above retries if something other than "not writable" happened. No point for retrying that, since it's probably write protected disk.

That's about it. Hope you understood how this works. To read/write single sectors, or other smaller regions, you can set a sector other than 1 and set the DMA to stop earlier. Drop the multi-track bit if you only want to do it on one track, and remember to set the head to 1 in both places if you want that head only.

As far as I understand, it doesn't matter much if you set or clear the "skip deleted" bit. If you don't set it (I don't) the read will fail when it hits a deleted address mark (which you can write with a special command it seems) while if you set it, the sector with such a mark will get skipped. So what are these deleted address marks then? I have no idea, but normal floppies seem to work quite fine whatever the setting is, so I'd guess it's something that's useful for tapes. There would be a READ_TRACK command for reading the whole track, deleted or not, but you can't do that multi-tracked on both heads.

For real reliablity you'd probably want single sector reads/writes so that you can deal with cases where some of the sectors are bad and such. No idea if anybody cares these days, I'll throw floppies away the first time I see an error with one.

----


So .. hopefully that helped someone understand a bit of something.
Last edited by mystran on Mon Apr 02, 2007 2:55 pm, edited 13 times in total.
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.
User avatar
~
Member
Member
Posts: 1227
Joined: Tue Mar 06, 2007 11:17 am
Libera.chat IRC: ArcheFire

Post by ~ »

May I take and organize it in a document repository?
User avatar
Combuster
Member
Member
Posts: 9301
Joined: Wed Oct 18, 2006 3:45 am
Libera.chat IRC: [com]buster
Location: On the balcony, where I can actually keep 1½m distance
Contact:

Post by Combuster »

... or, just load it into the osdev wiki? - I don't mind replacing the bbcode with mediawiki tags for you :)

Nice work, BTW :wink:
"Certainly avoid yourself. He is a newbie and might not realize it. You'll hate his code deeply a few years down the road." - Sortie
[ My OS ] [ VDisk/SFS ]
User avatar
mystran
Member
Member
Posts: 670
Joined: Thu Mar 08, 2007 11:08 am

Post by mystran »

I'll move it to the Wiki myself once it's a few days(/week?) old, and I've had a chance to do possible corrections, and had people actually try to follow it.

For now, please don't copy it elsewhere. I'll tell when I'm sufficiently confident with it that you can start spreading it around more actively. :)

Please tell me if there's any problem in the above, or if you have trouble understanding something.
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.
User avatar
~
Member
Member
Posts: 1227
Joined: Tue Mar 06, 2007 11:17 am
Libera.chat IRC: ArcheFire

Post by ~ »

mystran wrote:Please tell me if there's any problem in the above, or if you have trouble understanding something.
It requires a mid-high level to understand the code. That's not even a complain, just an opinion. I guess that just like Linux this system code is unavoidable like that, like this bit play, which anyway it's not very extensive here:

Code: Select all

if(u.w[3] || l.w[2] || l.w[3] 
      || ((u.q+l.q)&~(0xffff)) != (u.q&~(0xffff))
User avatar
mystran
Member
Member
Posts: 670
Joined: Thu Mar 08, 2007 11:08 am

Post by mystran »

~ wrote:
mystran wrote:Please tell me if there's any problem in the above, or if you have trouble understanding something.
It requires a mid-high level to understand the code. That's not even a complain, just an opinion. I guess that just like Linux this system code is unavoidable like that, like this bit play, which anyway it's not very extensive here:

Code: Select all

if(u.w[3] || l.w[2] || l.w[3] 
      || ((u.q+l.q)&~(0xffff)) != (u.q&~(0xffff))
Well... ok.. well.. that's just a quick hack to test all the possible things that can go wrong with DMA, and panic if one of them fails (since somewhere there's a problem because you tried an invalid buffer).

Specifically it checks that buffer address is at most 24 bits (under 16MB) that length is at most 16-bit (since that's what DMA accepts), and that if you add the buffer address and the length, you don't get need a carry.

Could be written a bit more clear actually (pseudocode follows):

Code: Select all

if(address & 0xff000000 || length & 0xffff0000 || ((address+length)&0xffff0000)!=(address&0xffff0000))
That relies on the fact that in little endians the union would work like this:

Code: Select all

   u.q = 0x87654321;

now:
  u.b[0] -> 0x21
  u.b[1] -> 0x43
  u.b[2] -> 0x65
  u.b[3] -> 0x87
But well, that code doesn't do anything, just sanitychecks, and isn't very important. As a bonus it's wrong, and wouldn't go through the compiler, 'cos I changed the union members name to a more sane value without changing the code. Oh well.

Will fix, and name the members and variables a bit more clear.
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.
User avatar
mystran
Member
Member
Posts: 670
Joined: Thu Mar 08, 2007 11:08 am

Post by mystran »

Actually I figure out an even nicer way to test those conditions..
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.
User avatar
mystran
Member
Member
Posts: 670
Joined: Thu Mar 08, 2007 11:08 am

Post by mystran »

And found another problem: can't declare the array by having "static const int" size, if compiling as C in GCC for whatever reason. Updating..

edit: and another small typo in the comments in the code, fixed.
edit: some more stylistic issues fixed, and now turns motor off if write-protected
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.
User avatar
Brynet-Inc
Member
Member
Posts: 2426
Joined: Tue Oct 17, 2006 9:29 pm
Libera.chat IRC: brynet
Location: Canada
Contact:

Post by Brynet-Inc »

Code: Select all

--- new1.c      Mon Apr  2 16:01:30 2007
+++ new.c       Mon Apr  2 16:01:00 2007
@@ -16,7 +16,7 @@
    out8_p(0x70, 0x10);
    unsigned drives = in8_p(0x71);

-   printk(" - Floppy drive 0: %s\n", drive_types[drive >> 4]);
-   printk(" - Floppy drive 1: %s\n", drive_types[drive & 0xf]);
+   printk(" - Floppy drive 0: %s\n", drive_types[drives >> 4]);
+   printk(" - Floppy drive 1: %s\n", drive_types[drives & 0xf]);

 }
Also this line:
"One a READ or WRITE request is issued" in Basic Concepts..

I'm pretty sure "One" should be "Once" 8)
Image
Twitter: @canadianbryan. Award by smcerm, I stole it. Original was larger.
User avatar
mystran
Member
Member
Posts: 670
Joined: Thu Mar 08, 2007 11:08 am

Post by mystran »

Fixed... this is the kind of stuff why I said "wait until I'm happy a few days later." :)
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.
User avatar
mystran
Member
Member
Posts: 670
Joined: Thu Mar 08, 2007 11:08 am

Post by mystran »

I'll see if I can add some media change detection magic into the code at some point, since that's probably the most important thing missing from the above.

I obviously have a local copy of the text so it's rather painless for me to edit it. :)

edit: yet more minor details with the text fixed...

edit: floppy_do_track had a nonsensical comment that was an artifact of it's origin. It originally did just error checking, then also the parameters for commands, and finally the whole whole thing, as I refractored my code. In the last iteration I obviously forgot to update the comment. Fixed.
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.
anon19287473
Member
Member
Posts: 97
Joined: Thu Mar 15, 2007 2:27 pm

Post by anon19287473 »

Anyone converted the C into ASM?
urxae
Member
Member
Posts: 149
Joined: Sun Jul 30, 2006 8:16 am
Location: The Netherlands

Post by urxae »

anon19287473 wrote:Anyone converted the C into ASM?
Isn't that what compilers are for? :P
User avatar
mystran
Member
Member
Posts: 670
Joined: Thu Mar 08, 2007 11:08 am

Post by mystran »

Please do not start the C vs. ASM here as well.
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.
anon19287473
Member
Member
Posts: 97
Joined: Thu Mar 15, 2007 2:27 pm

Post by anon19287473 »

mystran wrote:Please do not start the C vs. ASM here as well.
Sorry, I didn't mean to, I was wondering if anybody else (presumably writing in ASM) had used the code.

Is there a C compiler that compiles into NASM?
Post Reply