Skip to content

C65 F011 emulation

LGB edited this page Sep 10, 2016 · 20 revisions

Note: I don't emulate the unbuffered operations and it seems DOS doesn't use that either, maybe at some cases only like disk formatting has something to do with this in case of write.

F011 has an 512 bytes SRAM attached for buffered read/write operations. There are two 9 bits (thus the size of cache buffer) pointers maintained in relation of the buffer. It seems they warps around of course, though I couldn't notice this behaviour actually "exploited" by C65 DOS (other than to become zero again). One pointer (let's call that "FDC pointer") is used by the FDC to read a byte from the buffer and write onto the disk in case of a (buffered) write operation or to read one byte from the disk and place into the buffer in case of a (buffered) read operation (that is, FDC pointer is the byte in buffer FDC can access to read/write buffer at). After this task, the FDC's buffer pointer is incremented by one and DRQ bit is set to signal, that the F011 was the last who accessed the buffer. It's currently unknown to me, that EQ bit should be altered in case of FDC pointer change only, in my emulation I only do this, if CPU pointer (see below) changes. So DRQ means something like the FDC was the one (DRQ=1) who accessed the buffer, or the CPU (DRQ=0).

CPU can read/write buffer with register 7. Every time it does so, DRQ is cleared to signal, that CPU was the last who accessed the buffer and the other buffer pointer ("CPU pointer", let's say) is incremented. Then after the incrementation, FDC and CPU pointers are compared, if they are the same EQ bit is set, otherwise EQ bit is cleared. I think just before the CPU buffer address if EQ bit was already set, LOST bit set, as it would mean some byte will be lost in the buffer. It seems, EQ is never set at the beginning even not when you would think that the two pointers are equal at the start point. Doing so, would cause DOS to freeze hanging in a tight loop waiting for EQ to clear. Note about LOST: it's not so useful, as normally the max operation size is a physical block ie the size of buffer (512 bytes). I don't even noticed that it's ever used by DOS, however it is useful and important in case of unbuffered disk operations (though it's not the scope of this writing at all).

Maybe later ROM versions use the SWAP bit, but the version of ROM I use (and it seems M65 as well) does not use it, at all. In case if DOS needs an odd numbered (logical!) block it simply reads the physical 512 bytes block and does 256 dummy reads (to position the CPU buffer pointer to the second half of the buffer) and then only read and store result in C65 memory for real. That's true for write operations as well, where DOS reads the physical on-disk block (512 bytes block size) then doing 256 dummy reads of buffer to position to the second half, then writing 256 bytes to overwrite the second half of the physical sector in the buffer, if write operation is initiated for an even numbered (logical) block. After that, write block command is issued. As the FDC pointer is at #0 (as it filled 512 bytes on disk read in buffer, thus wrapped around to 0) the next $80 (write) command will write the whole 512 bytes buffer back to the disk, whose lower half is unmodified (the same as it was read) and the higher half was overwritten in the buffer by the DOS code. Thus the result, that we have an on-disk physical block which has the second 256 bytes are modified by the CPU only.

It seems that FDC never (?) resets the buffer pointers by its own (even not between commands?), always continue to use bytes where the pointers was (so if you read only one byte from the buffer on reg 7 followed by a new block read $40 operation, you will continue with byte at position 1 in the buffer to be read by the CPU and not at zero!). Unless if pointer reset command is sent which DOS do quite often according to my inspections. This is command $01, it causes to set CPU and FDC buffer pointers to zero (but it seems again, EQ bit should not be set then?). As it wouldn't mean sense to have a "reset pointer" command, I have the suspect, that pointers should be reset (to zero, I mean) on $01 command request and never done otherwise.

Moreover, there is important role of RDREQ. In case of a read operation, of course BUSY bit is set, then after some time either the FDC located the block, so RDREQ is set, or eg RFN is set instead, to indicate the failure of locating the needed one. DOS ROM waits for either RDREQ goes high (indicates that block is found and reading into the buffer right now by FDC) or RFN is set, to signal a problem. Interestingly, I haven't emulated WTREQ too much, but it seems DOS doesn't care about that. In theory it would mean, that data is right now written onto the disk from the buffer.

In Xemu, I emulated only a subset of this, ie the one-by-one byte read/write from/to the disk process is done in "one step", that is an "infinite fast drive". But since it seems DOS uses correctly the RDREQ/EQ for reading for example to tell if there is byte in the buffer and how many works in either case, so it doesn't matter. As EQ/DRQ/RDREQ should gives enough information for example read, it's enough to handle any of the cases, whatever amount of bytes and how fast FDC could handle in relation of the physical disk operation, so my primitive emulation is as good as the real one, where only bytes by bytes are transferred between the disk and the buffer by FDC "quite slowly" maybe at least compared to a fast enough CPU :)

Note about RDREQ: in theory with my primitive algorithm to have a crazy fast drive, doing every physical block read/write to/from the FDC buffer with no time passed, I wouldn't need RDREQ to set ever, as it would signal the "about reading ..." situation which I have for zero CPU time actually. However DOS ROM code waits at some point to RDREQ goes high. My approach was a workaround: I set RDREQ, but once the status byte is read by CPU where RDREQ bit is, I reset it. So I know, that CPU is "notified" the RDREQ event as it read the corresponding status byte. Without this, again, C65 DOS would "freeze" in a loop waiting for RDREQ. It seems C65 DOS does only use this information to know that the operation is OK and going on, and it does not care this anymore, since it's used only to signal that buffer reading can be started. After this point, only the buffer related status bit (ie EQ) is used to examine that it's OK to read one more byte from the buffer or not.

About the PROT bit: as I have some experience with WD177x emulation, I thought it's some kind of "error bit" in response of a write command for example. However, it seems, F011 sets this constantly (not as a response of an operation) if disk is write protected.

About the SWAP bit: so it seems, SWAP bit was introduced later and even earlier DOS versions didn't use it (also not by the currently used one by M65 project?). In theory - I think - it was designed to overcome the complexity of the "256 bytes dummy read" that is to bridge the logical/physical sector size differences in case of an operation meant to be an odd numbered logical sector. I'm not sure what it does exactly. The documentation states that it exchanges the buffer "halves" to be seen as the CPU. But how? Maybe just the 8th bit is inverted in the CPU buffer pointer in one step? So if you had CPU pointer for position #0 it becomes position #256, thus you can access the second half without needing the "dummy read" trick to "jump over" the lower half of the physical sector and being positioned at the second in the buffer cache. It can be tested on a real C65 (which I don't have) ie, $01 command to reset buffer pointer than fill (writing reg 7) the buffer with some bytes, then again $01 to reset pointers and try to use SWAP bit, and see, what is reading buffer (reg 7 again) will read ... Etc, so in theory it's possible to write some test program that can be used on a real C65 to figure things out (even some corner cases of the EQ bit etc). Though since I don't have C65, I could only see what DOS does by the DOS disassembly, and then trying to "mimic" the hardware in Xemu, and watching if at least that works :) However this does not cover some inner things DOS doesn't use anyway. It's a good question if it worth to emulate F011 at all at a very precise level or C65 DOS compatibility is enough, specially because it seems even F011 has several versions, with/without SWAP bit, and maybe other chip revisions as well?

Still missing but seems DOS does not care too much: RUN bit, WGATE bit. And the ability that F011 can generate an IRQ.

Example

Some ugly C example (not mentioning the other parts of F011 emulation though!). Note, that it's a draft written by me here, my emulation does not do this way exactly, but it's maybe more clear to show this way as an exmple:

static unsigned char cache[512];  // the 512 bytes long buffer cache used _internally_ by F011
static int cache_p_cpu = 0; // the "CPU pointer", NEVER sets to zero by "hand", ONLY in case of a $01 command!
static int cache_p_fdc = 0; // the "FDC pointer", NEVER sets to zero by "hand", ONLY in case of a $01 command!
static unsigned char status_a;  // FDC status-A register
static unsigned char status_b;  // FDC status-B register
// CPU: Writing disk buffer (that is, writing FDC register 7)
static void fdc_write_buffer_by_cpu ( unsigned char data )
{
    status_a &= ~64;  // Clears DRQ
    // Additional code here maybe: check if EQ was set already, if so, set LOST here!
    cache[cache_p_cpu] = data;
    cache_p_cpu = (cache_p_cpu + 1) & 511;
    if (cache_p_cpu == cache_p_fdc)
        status_a |=  32; // sets EQ
    else
        status_a &= ~32; // resets EQ
}
// CPU: Reading disk buffer (that is, reading FDC register 7)
static unsigned char fdc_read_buffer_by_cpu ( void )
{
    unsigned char data;
    status_a &= ~64; // Clears DRQ
    // Additional code here maybe: check if EQ was set already, if so, set LOST here!
    data = cache[cache_p_cpu];
    cache_p_cpu = (cache_p_cpu + 1) & 511;
    if (cache_p_cpu == cache_p_fdc)
        status_a |=  32;  // sets EQ
    else
        status_a &= ~32;  // resets EQ
    return data;
}
// FDC: Writing disk buffer (that is, FDC _reads_ disk, and fills buffer)
static void fdc_write_buffer_by_fdc ( unsigned char data )
{
    status_a |= 64; // Sets DRQ
    // Note about the LOST bit in the previous functions
    cache[cache_p_fdc] = data;
    cache_p_fdc = (cache_p_fdc + 1) & 511;
    // NOTE: it's not tested by me, if FDC pointer change modified EQ bit as well, I guess *it DOES*
    // however, as my emulator does "disk-op" in one step, I don't need this too much!
}
// FDC: Reading disk buffer (that is, FDC _writes_ disk, using one byte from buffer)
static unsigned char fdc_read_buffer_by_fdc ( void )
{
    unsigned char data;
    status_a |= 64;  // Sets DRQ
    // Note about the LOST bit in the previous functions
    data = cache[cache_p_fdc];
    cache_p_fdc = (cache_p_fdc + 1) & 511;
    // NOTE: it's not tested by me, if FDC pointer change modified EQ bit as well, I guess *it DOES*
    // however, as my emulator does "disk-op" in one step, I don't need this too much!
    return data;
}
Clone this wiki locally