diff --git a/include/apple2.dd.h b/include/apple2.dd.h index 226e946..8021cbf 100644 --- a/include/apple2.dd.h +++ b/include/apple2.dd.h @@ -1,10 +1,18 @@ #ifndef _APPLE2_DISK_DRIVE_H #define _APPLE2_DISK_DRIVE_H +/* + * Forward declaration of apple2dd for some files (e.g. apple2.h) which + * want to know about us before we have actually defined the struct. + */ +struct apple2dd; +typedef struct apple2dd apple2dd; + #include #include #include +#include "apple2.h" #include "vm_bits.h" #include "vm_segment.h" @@ -38,7 +46,44 @@ enum apple2_dd_mode { */ #define MAX_SECTOR_POS 4095 -typedef struct { +struct apple2dd { + /* + * Inside the disk drive there is a stepper motor, and it's + * controlled through four "phases", which are a bit hard to + * describe. Imagine four points on a wheel; suppose in order to + * to roll the wheel forward on the ground, you could only do so by + * controlling which point is facing the ground. A smooth rotation + * would always require you choose the point which is + * counter-clockwise from the ground at a 90º angle; and the point + * which was on the ground, is now clockwise from the ground, at + * 90º. Going backwards is similar, except you choose the point + * clockwise from the ground; and the point on the ground now goes + * counter-clockwise. + * + * To advance the motor, you need to turn on an adjacent phase; if + * phase 1 is on, turn on phase 2 and turn off phase 1; this allows + * you to go "forward"; and then turn on phase 3, and turn off phase + * 2; and you wrap around, so you turn on phase 0 and turn off phase + * 3. And vice versa for going "backward". In this field, then, we + * really care about only four bits; 0x1, 0x2, 0x4, and 0x8; and, in + * particular, we care about the adjacent relations of those high + * and low bits. + * + * It's really like if you unrolled the surface of the wheel and + * laid it out as a flat line, but still with those points defined. + * Does that make sense? + */ + vm_8bit phase_state; + vm_8bit last_phase; + + /* + * Data is written via a "latch", and happens in two steps; one, you + * set the latch; two, you commit the write. By steps, I mean two + * separate instructions--not necessarily adjacent to each other, + * but in some sequence and in that order. + */ + vm_8bit latch; + /* * Disk II drives allow the stepper to move in half-tracks, so we * track (pun intended) the position of the head in those @@ -99,19 +144,26 @@ typedef struct { * that you can enable or disable on the drive. */ bool write_protect; -} apple2dd; +}; +extern SEGMENT_READER(apple2_dd_switch_read); +extern SEGMENT_WRITER(apple2_dd_switch_write); extern apple2dd *apple2_dd_create(); extern int apple2_dd_insert(apple2dd *, FILE *); extern int apple2_dd_position(apple2dd *); extern vm_8bit apple2_dd_read(apple2dd *); extern void apple2_dd_eject(apple2dd *); extern void apple2_dd_free(apple2dd *); +extern void apple2_dd_map(vm_segment *); extern void apple2_dd_set_mode(apple2dd *, int); extern void apple2_dd_shift(apple2dd *, int); extern void apple2_dd_step(apple2dd *, int); +extern void apple2_dd_switch_drive(apple2 *, size_t); +extern void apple2_dd_switch_latch(apple2dd *, vm_8bit); +extern void apple2_dd_switch_phase(apple2dd *, size_t); +extern vm_8bit apple2_dd_switch_rw(apple2dd *); extern void apple2_dd_turn_on(apple2dd *, bool); -extern void apple2_dd_write(apple2dd *, vm_8bit); +extern void apple2_dd_write(apple2dd *); extern void apple2_dd_write_protect(apple2dd *, bool); #endif diff --git a/include/apple2.h b/include/apple2.h index 8a84f3f..e7c5550 100644 --- a/include/apple2.h +++ b/include/apple2.h @@ -1,6 +1,13 @@ #ifndef _APPLE2_H_ #define _APPLE2_H_ +/* + * A forward declaration is needed to avoid some errors in dd.h where we + * need to define a function that accepts an apple2 pointer. + */ +struct apple2; +typedef struct apple2 apple2; + #include "apple2.dd.h" #include "mos6502.h" #include "vm_bitfont.h" @@ -236,7 +243,7 @@ enum bank_switch { BANK_ALTZP = 0x8, }; -typedef struct { +struct apple2 { /* * The apple 2 hardware used an MOS-6502 processor. */ @@ -320,7 +327,13 @@ typedef struct { */ apple2dd *drive1; apple2dd *drive2; -} apple2; + + /* + * The Apple II machine allows you to "select" a drive, and the + * operations you perform are (mostly) targeting that drive. + */ + apple2dd *selected_drive; +}; extern apple2 *apple2_create(int, int); extern bool apple2_is_double_video(apple2 *); diff --git a/src/apple2.dd.c b/src/apple2.dd.c index efdbbe2..14dba2f 100644 --- a/src/apple2.dd.c +++ b/src/apple2.dd.c @@ -2,6 +2,7 @@ * apple2.disk_drive.c */ +#include "apple2.h" #include "apple2.dd.h" /* @@ -30,6 +31,8 @@ apple2_dd_create() drive->online = false; drive->write_protect = true; drive->mode = DD_READ; + drive->phase_state = 0; + drive->last_phase = 0; return drive; } @@ -78,6 +81,51 @@ apple2_dd_insert(apple2dd *drive, FILE *stream) return OK; } +/* + * Evaluate the value of the phase_state against the last known phase, + * and decide from there whether we should step forward or backward by a + * half-track. This function, as a side-effect, will update the last + * phase to be the current phase state if the step was successful. + */ +void +apple2_dd_phaser(apple2dd *drive) +{ + int phase = drive->phase_state; + int last = drive->last_phase; + + // This is a bit of trickery; there is no phase state for 0x10 or + // 0x0, but we want to pretend like the phase is "next" to the bit + // we're operating with for the purpose of establishing a direction. + if (phase == 0x1 && last == 0x8) { + phase = 0x10; + } else if (phase == 0x8 && last == 0x1) { + phase = 0x0; + } + + // We only want to respond to adjacent phases, so if the last phase + // shifted in _any_ direction is not equal to the phase state, then + // we should do nothing. + if (phase != (last << 1) || + phase != (last >> 1) + ) { + return; + } + + // If phase > last, then we must move the head forward by a half + // track. If it's < last, then we move the head backward, again by a + // half track. + if (phase > last) { + apple2_dd_step(drive, 1); + } else if (phase < last) { + apple2_dd_step(drive, -1); + } + + // Recall our trickery above with the phase variable? Because of it, + // we have to save the phase_state field into last_phase, and not + // the pseudo-value we assigned to phase. + drive->last_phase = drive->phase_state; +} + /* * Return the segment position that the drive is currently at, based * upon track and sector position. @@ -214,9 +262,9 @@ apple2_dd_turn_on(apple2dd *drive, bool online) * shift the drive position forward by one byte. */ void -apple2_dd_write(apple2dd *drive, vm_8bit byte) +apple2_dd_write(apple2dd *drive) { - vm_segment_set(drive->data, apple2_dd_position(drive), byte); + vm_segment_set(drive->data, apple2_dd_position(drive), drive->latch); apple2_dd_shift(drive, 1); } @@ -231,3 +279,206 @@ apple2_dd_write_protect(apple2dd *drive, bool protect) { drive->write_protect = protect; } + +/* + * Half of all the disk drive switches deal with turning on and off the + * different phases of the stepper. We handle all of them here. + */ +void +apple2_dd_switch_phase(apple2dd *drive, size_t addr) +{ + switch (addr & 0xF) { + case 0x0: drive->phase_state &= ~0x1; break; + case 0x1: drive->phase_state |= 0x1; break; + case 0x2: drive->phase_state &= ~0x2; break; + case 0x3: drive->phase_state |= 0x2; break; + case 0x4: drive->phase_state &= ~0x4; break; + case 0x5: drive->phase_state |= 0x4; break; + case 0x6: drive->phase_state &= ~0x8; break; + case 0x7: drive->phase_state |= 0x8; break; + } +} + +/* + * This function handles all of the switch behavior that handles drive + * metadata, like what drive is turned on, or which one is selected. + */ +void +apple2_dd_switch_drive(apple2 *mach, size_t addr) +{ + switch (addr) { + case 0x8: + apple2_dd_turn_on(mach->drive1, false); + apple2_dd_turn_on(mach->drive2, false); + break; + + case 0x9: + apple2_dd_turn_on(mach->selected_drive, true); + break; + + case 0xA: + mach->selected_drive = mach->drive1; + break; + + case 0xB: + mach->selected_drive = mach->drive2; + break; + + case 0xE: + mach->selected_drive->mode = DD_READ; + break; + + case 0xF: + mach->selected_drive->mode = DD_WRITE; + break; + } +} + +/* + * If the disk drive is in write mode, we are allowed (!) to set the + * latch value to whatever is passed in here. + * + * What's a latch value? Good question! It's basically a placeholder for + * data to be committed (written) to disk. Writing via the Disk II drive + * is a two-legged process; one, set the latch value; two, actually + * write the latch value to the disk. As to why things are done this + * way, I can only imagine that there were technical reasons for + * essentially requiring the data to be written to be onboard the disk + * drive itself. + */ +void +apple2_dd_switch_latch(apple2dd *drive, vm_8bit value) +{ + if (drive->mode == DD_WRITE) { + drive->latch = value; + } +} + +/* + * This function handles the logic for reading and/or writing to a Disk + * II drive. What exactly happens here depends very much on the drive + * mode, as well as whether or not we consider the disk to be + * write-protected. + */ +vm_8bit +apple2_dd_switch_rw(apple2dd *drive) +{ + // If we are in read mode OR if we are working with a + // write-protected disk, then all operations are interpreted as + // read operations. If we are specifically in write mode, and in the + // else condition we can say that the drive is not write-protected, + // then we will write the latch data to the drive. + if (drive->mode == DD_READ || drive->write_protect) { + return apple2_dd_read(drive); + } else if (drive->mode == DD_WRITE) { + apple2_dd_write(drive); + } + + return 0; +} + +/* + * This function handles reads to any of the disk II controller + * addresses. Note that it's possible to write to a disk with a call to + * this "read" function! Pay less attention to where these functions are + * mapped, and pay more attention to the specific behavior of the + * address switches being used. + */ +SEGMENT_READER(apple2_dd_switch_read) +{ + apple2 *mach = (apple2 *)_mach; + apple2dd *drive = mach->selected_drive; + + // A nibble is a half-byte... not to be confused with the .NIB file + // format + size_t nib = addr & 0xF; + + // This might not be _right_... a better solution might be to bail + // out unless the address indicates that the operation is not on a + // specific drive, like turning drives off, or selecting a new + // drive. + if (drive == NULL) { + drive = mach->drive1; + } + + // In the first if block, we will handle 0x0..0x8; in the second if, + // we'll do 0x9..0xB, 0xE, and 0xF. + if (nib < 0x9) { + apple2_dd_switch_phase(drive, nib); + } else if (nib < 0xC || nib > 0xD) { + apple2_dd_switch_drive(mach, nib); + } + + // This is the read/write address... various states of the disk + // drive will dictate what we do here. + if (nib == 0xC) { + return apple2_dd_switch_rw(drive); + } else if (nib == 0xD) { + // In a read context, accessing the latch switch will pass a + // zero value into the latch. (The latch value will only be + // committed if the drive itself is in write mode.) + apple2_dd_switch_latch(drive, 0); + } + + return 0; +} + +/* + * A decent portion of the logic in this function is similar to the + * switch_read function, the defining difference being that here we have + * a potentially-nonzero value to pass into the switch_latch function. + * As such I have not commented much on the code here outside of what + * can happen specifically in switch_write(); in the future it wouldn't + * be a bad idea to refactor this common code into its own function. + */ +SEGMENT_WRITER(apple2_dd_switch_write) +{ + apple2 *mach = (apple2 *)_mach; + apple2dd *drive = mach->selected_drive; + size_t nib = addr & 0xF; + + if (drive == NULL) { + drive = mach->drive1; + } + + if (nib < 0x9) { + apple2_dd_switch_phase(drive, nib); + } else if (nib < 0xC || nib > 0xD) { + apple2_dd_switch_drive(mach, nib); + } + + // It's possible to attempt a "read" from a disk drive while doing a + // write to the $C0nC address; all this does in effect is to shift + // the disk forward. The more likely thing is that, if we are in + // write mode, we will commit the latch value to disk; but that can + // happen from either this particular function or from the + // switch_read function. + if (nib == 0xC) { + apple2_dd_switch_rw(drive); + } else if (nib == 0xD) { + // The only way to get a latch value that is non-zero is to + // write to the $C0nD address, where n is the address of one of + // the disk controller ROMs. And even then, the drive needs to + // be in write mode. + apple2_dd_switch_latch(drive, value); + } +} + +/* + * Map the Disk II drive switch addresses. + */ +void +apple2_dd_map(vm_segment *seg) +{ + size_t addr; + + for (addr = 0xC0E0; addr < 0xC0F0; addr++) { + vm_segment_read_map(seg, addr, apple2_dd_switch_read); + vm_segment_write_map(seg, addr, apple2_dd_switch_write); + } + + for (addr = 0xC0F0; addr < 0xC100; addr++) { + vm_segment_read_map(seg, addr, apple2_dd_switch_read); + vm_segment_write_map(seg, addr, apple2_dd_switch_write); + } +} diff --git a/src/apple2.mem.c b/src/apple2.mem.c index f8ad095..ed720f8 100644 --- a/src/apple2.mem.c +++ b/src/apple2.mem.c @@ -4,6 +4,7 @@ #include "apple2.bank.h" #include "apple2.dbuf.h" +#include "apple2.dd.h" #include "apple2.h" #include "apple2.kb.h" #include "apple2.mem.h" @@ -62,6 +63,9 @@ apple2_mem_map(apple2 *mach, vm_segment *segment) // And this handles our keyboard soft switches apple2_kb_map(segment); + // Map our disk drive switches + apple2_dd_map(segment); + // We will do the mapping for the zero page and stack addresses. // Accessing those addresses can be affected by bank-switching, but // those addresses do not actually exist in the capital