mirror of
https://github.com/digarok/gsplus.git
synced 2024-11-30 16:50:45 +00:00
240 lines
11 KiB
Plaintext
240 lines
11 KiB
Plaintext
|
|
||
|
KEGS's Apple //gs IWM emulation routines.
|
||
|
|
||
|
The IWM code does 5.25" and 3.5" reads & writes, and updates the Unix disk
|
||
|
image on writes. It is also nearly cycle-accurate--Let me know if you
|
||
|
have a program which can detect it's not a real Apple II. There are
|
||
|
a few 5.25" features missing (No 1/4 or 1/2 tracks, no support for Unix nibble
|
||
|
images, limited disk switching), but what's there is pretty accurate.
|
||
|
The low-level code support 1/4 and 1/2 tracks--it's the arm movement
|
||
|
and image-handling routines which don't. And lack of Unix nibble images
|
||
|
are also due to lack of higher-level routines to make those features work.
|
||
|
|
||
|
How my disk emulation works: The routines have a nibblized image of each
|
||
|
track of each drive (two 5.25" and two 3.5" drives are supported) in memory.
|
||
|
The nibble images are declared as arrays, but it could be made to use
|
||
|
more dynamic memory allocation.
|
||
|
|
||
|
Each track's data format is a series of two-byte pairs. The first byte
|
||
|
of the pair is the number of bits in this disk byte, and the second byte
|
||
|
is the value. So a size of 8 is normal. A size of 10 means that there
|
||
|
are 2 sync bits written before this byte on the disk. So for 5.25" disk
|
||
|
accesses, 40 cycles need to pass in the simulator before providing a
|
||
|
valid nibble. Partial nibbles are correctly formed if a read happens
|
||
|
too early (this actually makes things slower, but is required if you
|
||
|
want to make nibble copiers work). Similarly, writing to the disk
|
||
|
watches timing carefully to write out the correct number of bits per
|
||
|
disk byte. These routines will definitely test out your emulator's cycle
|
||
|
counting ability.
|
||
|
|
||
|
If a long delay occurs between a read (or a write) the routines skips
|
||
|
the correct number of bits to return the correctly formed disk byte.
|
||
|
After a long delay, for efficiency, I always return a full disk byte,
|
||
|
instead of a partial one, even if the timing would put it in the middle
|
||
|
of a disk byte.
|
||
|
|
||
|
The arm stepping is really lame. I will clean it up soon.
|
||
|
|
||
|
Smartport support is sufficient to claim that there are no smartport
|
||
|
devices. This is necessary since the ROM tries to see if there are
|
||
|
smartport devices at power-on.
|
||
|
|
||
|
I tested my 5.25" drive routines on EDD, which could correctly measure
|
||
|
drive speed and other disk factors. I also nibble-copied some disks,
|
||
|
which also worked fine. I tested the 3.5" routines using Copy II+,
|
||
|
which successfully nibble-copied several disks.
|
||
|
|
||
|
|
||
|
Code description:
|
||
|
|
||
|
Most code is in iwm.c, with some defines in iwm.h, and some stuff in
|
||
|
iwm_35_525.h.
|
||
|
|
||
|
Code only supports DOS3.3 ordered 5.25" images now, and ProDOS-ordered 3.5"
|
||
|
images. Well, the code supports ProDOS-order 5.25" also, but has no
|
||
|
mechanism to tell it an image is prodos-order yet. :-)
|
||
|
|
||
|
Iwm state is encoded in the Iwm structure.
|
||
|
|
||
|
drive525[2]: Disk structure for each 5.25" disk
|
||
|
drive35[2]: Disk structure for each 3.5" disk
|
||
|
smarport[32]: Disk structure for each "smartport" device emulated
|
||
|
via slot 7 (this code not included)
|
||
|
motor_on: True if IWM motor_on signal (c0e9) is asserted. Some
|
||
|
drive is on.
|
||
|
motor_off: True if motor has been turned off in software, but the
|
||
|
1 second timeout has not expired yet.
|
||
|
motor_on35: True if 3.5" motor is on (controlled differently than
|
||
|
5.25" c0e9).
|
||
|
motor_off_vbl_count: VBL count to turn motor off.
|
||
|
head35, step_direction35: 3.5" controls, useless.
|
||
|
iwm_phase[4]: Has '1' for each 5.25" phase that is on.
|
||
|
iwm_mode: IWM mode register.
|
||
|
drive_select: 0 = drive 1, 1 = drive 2.
|
||
|
q6, q7: IWM q6, q7 registers.
|
||
|
enable2: Smartport /ENABLE2 asserted.
|
||
|
reset: Smartport /RESET asserted.
|
||
|
previous_write_val: Partial write value.
|
||
|
previous_write_bits: How many bits are valid in previous_write_val.
|
||
|
|
||
|
Each disk (3.5" and 5.25") is encoded in the Disk struct:
|
||
|
fd: Unix file descriptor. If < 0, no disk.
|
||
|
name_ptr: Unix file name for this disk.
|
||
|
image_start: offset from beginning of file for this partition.
|
||
|
image_size: size of this partition.
|
||
|
smartport: 1 if this is a smartport image, 0 if it is 5.25" or 3.5"
|
||
|
disk_525: 1 if this is a 5.25" image, 0 if it is 3.5"
|
||
|
drive: 0 = drive 1, 1 = drive 2.
|
||
|
cur_qtr_track: Current qtr track. So track 1 == qtr_track 4.
|
||
|
For 3.5", cur_qtr_track encodes the side also, so track 3
|
||
|
side 1 would be qtr_track 7.
|
||
|
prodos_order: True if Unix image is ProDOS order.
|
||
|
vol_num: DOS3.3 volume number to use. Always 254.
|
||
|
write_prot: True if disk is write protected.
|
||
|
write_through_to_unix: True if writes should be passed through to
|
||
|
the unix image. If this is false, you can write
|
||
|
to the image in memory, but it won't get reflected
|
||
|
into the Unix file. If you create a non-DOS3.3
|
||
|
or ProDOS format image, it automatically sets this
|
||
|
false.
|
||
|
disk_dirty: Some track has dirty data that need to be flushed.
|
||
|
just_ejected: Ejection flag.
|
||
|
dcycs_last_read: Cycle count of last disk data register access.
|
||
|
last_phase: Phase number last accessed.
|
||
|
nib_pos: Nibble offset ptr--points to a byte.
|
||
|
num_tracks: Number of tracks: 140 for 5.25" and 160 for 3.5"
|
||
|
track[MAX_TRACKS]: nibble image of all possible tracks.
|
||
|
|
||
|
Each track is represented by the Track structure:
|
||
|
track_dirty: Contains data that needs to be written back to
|
||
|
the Unix image file.
|
||
|
overflow_size: Count of overflow bits, used in writing.
|
||
|
track_len: Number of nibbles on this track.
|
||
|
dsk: Handy pointer to parent Disk structure.
|
||
|
nib_area[]: ptr to memory containing pairs of [size,data],
|
||
|
encoding disk data bytes.
|
||
|
pad1: If the structure is 32 bytes long, some array
|
||
|
indexing is done better by my compiler.
|
||
|
|
||
|
|
||
|
Externally callable routines:
|
||
|
iwm_init(): Init various data structures at simulation start.
|
||
|
iwm_reset(): Called at Apple //gs reset time.
|
||
|
iwm_vbl_update(): Called every VBL (60 Hz) period. Used to turn motor
|
||
|
off, and flush out dirty data.
|
||
|
g_vbl_count is the count of VBL ticks (so it counts
|
||
|
at 60 times a second).
|
||
|
iwm_read_c0ec(double dcycs): Optimized routine to handle reading $C0EC
|
||
|
faster. Exactly the same as read_iwm(0xc, dcycs);
|
||
|
read_iwm(loc, dcycs):
|
||
|
Read from 0xc0e0 + loc. Loc is between 0x0 and 0xf.
|
||
|
Dcycs is an artifact from my simulator. Dcycs is a
|
||
|
double holding the number of Apple //gs cycles since the
|
||
|
emulator started. Dcycs always counts at 1.024MHz. If
|
||
|
you are running at 2.5MHz, it increments by 0.4 every
|
||
|
"cycle". This is a very convenient timing strategy. It
|
||
|
also allows emulating the delay caused by synchronizing
|
||
|
the fast part of a real Apple //gs with slow memory,
|
||
|
which means my emulator knows that reading softswitches
|
||
|
takes longer than reading fast memory.
|
||
|
write_iwm(int loc, int val, double dcycs):
|
||
|
Write to 0xc0e0 + loc. Just like read_iwm, but write "val" into
|
||
|
loc.
|
||
|
|
||
|
|
||
|
Tricky routines:
|
||
|
|
||
|
IWM_READ_ROUT(): called by read_iwm() if q6,q7 = 0,0.
|
||
|
This is actually in the file iwm_35_525.h. This is so I
|
||
|
write the basic code once for 5.25" and 3.5" disk reads,
|
||
|
but then include the file with some macros set to create
|
||
|
the correct function optimized for 5.25" or 3.5"
|
||
|
accesses. The function for 5.25" is called
|
||
|
iwm_read_data_525, and iwm_read_data_35 for 3.5".
|
||
|
Returns next disk byte.
|
||
|
Takes three arguments: ptr to the Disk structure for
|
||
|
the active drive, fast_disk_emul, and dcycs. dcycs is
|
||
|
so that it can see how many cycles have passed since
|
||
|
the last read (stored in dsk->dcycs_last_read).
|
||
|
16.0 dcycs need to pass for an 8 bit nibble for 3.5"
|
||
|
accesses, and 32.0 dcycs for an 8 bit nibble for 5.25".
|
||
|
Fast_disk_emul == 1 says don't mess around with accuracy,
|
||
|
and always return the next fully-formed nibble.
|
||
|
There is a lot of complexity in this routine. All IWM
|
||
|
routines must skip over nibbles (stored as byte pairs in
|
||
|
dsk->nib_area[]) which have a size of 0 (special padding
|
||
|
trick, described later). It then determines how much
|
||
|
time has passed, and so how many bits are valid.
|
||
|
If too many bits have gone by (11 cycs is almost 3 5.25"
|
||
|
bit times, which is about the nibble valid time in
|
||
|
the Apple //gs IWM hardware latch), it tries to skip
|
||
|
to the correct position.
|
||
|
Handles IWM latch mode for 3.5" or 5.25" accesses. If a
|
||
|
partial read is indicated, it ensures that the high bit
|
||
|
is clear by shifting the nibble to the right
|
||
|
appropriately. Again, nib_area[] is an array of bytes,
|
||
|
which are treated as pairs. Byte 0 = size, byte 1 =
|
||
|
disk nibble.
|
||
|
|
||
|
IWM_WRITE_ROUT(): called by write_iwm() if q6,q7 = 1,1.
|
||
|
Similar to above. Handles async and sync mode writes.
|
||
|
Handles partial writes. Handles the ROM writing
|
||
|
0xff, 0x3f, 0xcf, 0xf3, 0xfc to be four 10-bit nibbles.
|
||
|
Routine disk_nib_out(dsk, val, bits_read) does the
|
||
|
actual work of merging the bits into the track image.
|
||
|
|
||
|
disk_nib_out(): called by IWM_WRITE_ROUTE() and iwm_nibblize_track_*().
|
||
|
Writes byte into nib_area[]. If size > 10, makes it 10.
|
||
|
If high order bit not set, it sets it (makes certain routines
|
||
|
in EDD happy).
|
||
|
|
||
|
overflow_size:
|
||
|
Writing to the disk creates some problems. I need to
|
||
|
maintain 2 things at all times on the track:
|
||
|
1) Constant number of bits for the entire track.
|
||
|
2) know where each synchronized byte starts on
|
||
|
the track.
|
||
|
If the track was just stored as raw bits, then correctly
|
||
|
simulating a delay of 300*4 cycles is tough, since it has to
|
||
|
be done by reading through all 300 bits on the track,
|
||
|
so that we keep in sync with where bytes really start.
|
||
|
But if you just store the bytes themselves, then sync
|
||
|
bytes look like every other byte. And if you now add
|
||
|
the size field, you have a situation where a track could
|
||
|
gain or lose bits when rewritten. Here's the case:
|
||
|
Assume the track contains: 10,ff 10,ff 10,ff 10,ff.
|
||
|
(That is 4 self-sync disk bytes of 10 bits each).
|
||
|
If we rewrite that area of the track with 'D5 AA 96 FF',
|
||
|
where each byte is 8 bits, we would have:
|
||
|
8,D5 8,AA, 8,96, 8,FF.
|
||
|
Looks OK, but we just lost 8 bits! The original 4 nibbles
|
||
|
were using 40 bits of space on the disk. Our new 4 nibbles
|
||
|
are using 32 bits. 8 bits are lost.
|
||
|
Solution: log these missing bits via overflow_size.
|
||
|
When writing, if overflow_size gets > 8, force out a 0,0
|
||
|
nibble. So sync bytes get written as:
|
||
|
10,FF 10,FF 10,FF 10,FF 0,0 10,FF 10,FF 10,FF 10,FF, 0,0.
|
||
|
So when they get re-written with 8,xx, we don't lose any
|
||
|
bytes on the disk.
|
||
|
|
||
|
Unfortunately, it doesn't quite work that easily, and bits
|
||
|
can still be lost when sync fields are partially overwritten.
|
||
|
This happens when all the 0,0's end up in a place on the
|
||
|
track where few overwrites occur, but other sync bytes
|
||
|
are being turned into 8,xx. So overflow_size goes negative,
|
||
|
saying we've got too much on the track.
|
||
|
The code prints an error when it gains more than 64 bits.
|
||
|
If someone can come up with a better scheme, I'd love to
|
||
|
hear it. A partial solution would be to have a routine
|
||
|
re-space the track to spread the needed 0,0's around
|
||
|
a little better when overflow_size gets too negative.
|
||
|
|
||
|
|
||
|
In iwm_nibblize_track_35(), the comments with hex numbers correspond
|
||
|
to the ROM 01 addresses which I disassembled to determine the checksum
|
||
|
algorithm. The code is not well written--it's basically hand-translated
|
||
|
65816 assembly code. I'll clean it up someday.
|
||
|
|
||
|
Much of the code is not well-optimized. I'll get to that someday, but
|
||
|
the speed has been adequate for me so far.
|