-
Notifications
You must be signed in to change notification settings - Fork 32
C65 F011 emulation
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 access 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 (though those ROM version has "issues" otherwise) use the SWAP bit, but the version of ROM I use (and it seems M65 project 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. Of course, if an even numbered logical block is needed to be write, the issue is more easy. Then DOS reads again the whole 512 bytes block (of course, it cannot read only a part of it) and simply, only the first half of the buffer is written by the CPU, leaving the second half as-is. Thus, again, issuing the $80 write command, the physical disk block is written to the disk, having the first half in the modified form, and second half with the original content.
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 does 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 sane to have a "reset pointer" command (if they are reset otherwise at every operations without this command this), I have the suspect, that pointers should be reset (to zero, I mean) on $01 command request and never done otherwise. Also it can explain, why DOS uses command $01 often, which is the case by logging FDC commands set in my emulator.
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, especially 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.
For me, it seems, FDC allows basically free access to the 512 bytes buffer. That is, you can read/write it via register 7 without any disk operations as well, even for using your own custom RAM storage :-) EQ bit only gives you the "hint" to be able to remain in sync with buffer usage of the FDC itself, but nothing keeps you away to simply ignore what EQ bit or anything other means! However that surely means if you are not careful with EQ bit (with using it in a C65 software, or by emulating it well enough in case of an emulator) it will cause data corruption or something very odd, as FDC also reads/writes the buffer! EQ is meant to provide the ability for the user program to know, that you are at the "limit" where FDC doesn't referred data yet (ie if you would read/write buffer via reg 7, it would cause to overwrite a byte which is not yet written to the disk, or in case of read, you would read buffer which is not read from the disk by FDC to the buffer yet, that is, it would be invalid! Invalid yes, but you can READ the buffer, just the content will be not so sane, but the old byte in the buffer at that position!)
Also, it seems EQ is always cleared at the beginning of disk operations. Even that it means, the two pointers are equal to set EQ bit rule is not universal. Moreover, I am REALY NOT SURE if EQ bit set/reset is done by FDC only if CPU pointer is incremented, or by the FDC pointer change as well. This is very important in fact. Imagine, that you issued a disk block read command but your program is slow (that is the case with my emulator, since it reads one block with no emulated CPU time elapsed, that is, everything is slow compared to this speed). FDC happily read 512 bytes, thus FDC pointer wrapped to 0. However you haven't read anything (assume CPU pointer was zero before the operation) and it's still zero. Eg EQ should be set. What can be interpreted by the software that buffer is empty, but in fact buffer is FULL NOW. This is even documented (c65manual.txt):
"Buffered operations can be monitored by reading status register A. The DRQ and EQ bits indicate the immediate status of the buffer pointers. During any operation, the EQ bit, when set, indicates that both the disk and CPU buffer pointers are pointing to the same location. This can mean that the buffer is full or empty, depending on what operation is, or will be performed. The DRQ bit set indicates that the disk was last to access the buffer, and clear indicates that CPU was last to access the buffer."
However still, according to my tests in emulator C65 DOS hangs waiting for EQ, if set EQ signalling that 512 bytes is read, so the reason of EQ is because being full, but DOS thinks it's empty. It's possible that the confusion in my case is given by the fact, that I do things in "one step" otherwise DOS would notice that there is one byte to read, then another etc, etc, but with my current emulation it would face with full filled buffer already. Again, with my current implementation which does not sets EQ in case of FDC pointer change (only CPU pointer change) seems to solve the problem. Again, these things should be tested on a real C65 with some test cases and checking EQ bit then ...
And again, the buffer with the CPU pointer is read as-is. What I mean here: if you make read a block by FDC then read a single byte from the buffer (read reg 7), then you make FDC to read another block into the buffer from the disk, another read from the buffer (reg 7) will access the buffer at position 1 and not zero, so you will read actually the second byte of the latter block read operation! That is, either you need to read the full buffer (512 bytes) after a block read FDC command to have CPU buffer pointer zero, or you need to utilize the buffer pointer reset command ($01). It seems C65 DOS often use this command, btw, as I've written before too.
FDC pointer itself "on long term" is zero. Why? Because FDC can read a physical block or write which is always 512 bytes long, thus the size of buffer exactly. Of course, in the real world, while reading or writing (RDREQ/WTREQ is high) that is incrementing, but at the end, it will be zero again. It's not the case with the CPU pointer, as you can read/write reg 7 as many times as you want.
Note, that these two pointers are "virtual", in the manner, that you can't query or set them directly (well, the $01 command can reset both of them to zero, at least, also the SWAP bit may able to manipulate the MSB of the CPU pointer?). The relation of these two pointers can be monitored though with inspecting the EQ bit.
Please note, that these "facts" are my ideas as well included, not so much "real facts". Again, a test program should be written for a real C65 to check these!
Though I had some words on this, but ... One problem here, that it seems, the whole stuff works more or less if the speed of disk read (ie, for write, it's not so much an issue, as disk write command can go as its own, already filled buffer, but in case of read there are more issues of the possible concurrent access of the buffer from CPU and FDC) is assumed to be "floppy speed". I mean here about RDREQ for example. It's should hold high during the reading from the disk (to the buffer). But, what if, the process is very fast, ie the emulation doing 'once' approach, or even the real M65, with SD card, having much more speed than a floppy drive? It can be the issue, that RDREQ is already over, before the code will hit the point in the DOS ROM code which checks if it goes high. Then it hangs that point, waiting for RDREQ which is already over ... As I've written my workaround not to clear RDREQ (it should be cleared after the end of the read from disk to buffer) but leaving so, for example till the CPU reads status-B where RDREQ bit is. So now, we can know, that CPU saw that RDREQ, we can clear that. The other approach, is an artificial extended RDREQ=1 state which would be the case of a floppy drive more or less. This one is maybe more correct.
The other issue here is the following: RDREQ signals the disk reading, but data is read from the disk to the buffer one by one. CPU can starts reading the result but should be careful to check EQ bit, not to "pass over" the position in buffer where FDC is about. So even with buffered read, it's almost real time, that is, you can read the result before the whole 512 bytes physical sector is actually read. The problem here again, the speed: if an emulator is really fast, or an SD-card, then the emulated FDC may read all the bytes before CPU started its work at all. And because the FDC and CPU pointers are equal (in case of CPU pointer was zero before the operation, the FDC pointer is zero, because 512 bytes read, wrapped around to zero) EQ bit is set. That is, CPU may wait for a byte in the buffer thinking buffer is empty, while it's actually full. My somewhat ugly solution, that may be not true (should be checked with a real C65) that I never set EQ at the beginning of a read operation even pointers are the same. The more sane realization: do as FDC would do (but again: should be checked how EQ is set actually on a real C65 initially) but then the about "floppy drive speed" of getting bytes should be emulated. Though my version is not so "nice" it has the value that I don't need to emulate ugly "slowing down" things.
Otherwise, if the two pointers are different, in theory, the speed does not matter, etc, and really EQ is enough without any workaround. We have only the "initial problems" with RDREQ and initial state of EQ bits. The EQ bit "issue" is from the fact that it can mean buffer is empty, but also, that buffer is full. Again, maybe my workaround (with EQ setting initially) is the way how FDC behaves for real! I can't be sure.
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
}
// 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
return data;
}
// Used with $01 FDC command. Note; it seems NO OTHER cases the pointers needs to be reset by "hand"!!!!
static void fdc_execute_reset_pointer_command ( void )
{
cache_p_fdc = 0;
cache_p_cpu = 0;
}