1
0
mirror of https://github.com/pevans/erc-c.git synced 2024-09-29 11:55:01 +00:00
erc-c/src/apple2.dd.c
2018-02-04 14:15:24 -06:00

575 lines
16 KiB
C

/*
* apple2.disk_drive.c
*/
#include "apple2.dd.h"
#include "apple2.dec.h"
#include "apple2.enc.h"
#include "apple2.h"
/*
* Create a new disk drive. We do not create a memory segment for the
* drive right away, as the size of said data can be variable based on
* the disk format.
*/
apple2dd *
apple2_dd_create()
{
apple2dd *drive;
drive = malloc(sizeof(apple2dd));
if (drive == NULL) {
log_critical("Could not malloc space for apple2 disk drive");
return NULL;
}
// To begin with, we have no segment for data; that's something that
// will depend on the disk you insert. For example, a DOS 3.3 or
// ProDOS disk will have 140k, but a NIB file would have more.
drive->data = NULL;
drive->image = NULL;
drive->locked = false;
drive->track_pos = 0;
drive->sector_pos = 0;
drive->online = false;
drive->write_protect = true;
drive->mode = DD_READ;
drive->phase_state = 0;
drive->last_phase = 0;
drive->image_type = DD_NOTYPE;
return drive;
}
/*
* Insert a "disk" into the drive, such that a disk is delivered to us
* through a FILE stream. Return an error code if the disk format is
* something we cannot accept.
*/
int
apple2_dd_insert(apple2dd *drive, FILE *stream, int type)
{
struct stat finfo;
int err;
if (stream == NULL) {
log_critical("File stream is null");
return ERR_BADFILE;
}
// How large is this data set? Let's get the stat info.
if (fstat(fileno(stream), &finfo)) {
log_critical("Couldn't inspect file stream: %s", strerror(errno));
return ERR_BADFILE;
}
if (finfo.st_size != _140K_) {
log_critical("Unexpected file format (file size = %d)", finfo.st_size);
return ERR_BADFILE;
}
// If we have any data, get rid of it. We'll start fresh here.
apple2_dd_eject(drive);
drive->image = vm_segment_create(finfo.st_size);
drive->track_pos = 0;
drive->sector_pos = 0;
// Read the data from the stream and write into the memory segment
err = vm_segment_fread(drive->image, stream, 0, finfo.st_size);
if (err != OK) {
log_critical("Could not read data into disk drive");
return err;
}
drive->stream = stream;
drive->image_type = type;
// Now we need to build the data segment
apple2_dd_encode(drive);
return OK;
}
/*
* Encode the drive image segment into the drive data segment using
* 6-and-2 encoding, if necessary. (It is not necessary if the
* image_type is DD_NIBBLE.)
*/
int
apple2_dd_encode(apple2dd *drive)
{
switch (drive->image_type) {
case DD_NIBBLE:
drive->data = apple2_enc_nib(drive->image);
break;
case DD_DOS33:
case DD_PRODOS:
drive->data = apple2_enc_dos(drive->image);
break;
default:
log_critical("Unknown image type");
return ERR_INVALID;
}
return OK;
}
/*
* Save the contents of the drive back to the file system (given as the
* stream field in the drive struct).
*/
void
apple2_dd_save(apple2dd *drive)
{
// First bring the image segment back into sync with with the data
// segment.
apple2_dd_decode(drive);
if (drive->stream) {
rewind(drive->stream);
vm_segment_fwrite(drive->image, drive->stream, 0, drive->image->size);
}
}
/*
* Decode the drive data segment into the drive image segment, reversing
* the 6-and-2 encoding (if need be -- see note on DD_NIBBLE for the
* encode function).
*/
int
apple2_dd_decode(apple2dd *drive)
{
switch (drive->image_type) {
case DD_NIBBLE:
apple2_dec_nib(drive->image, drive->data);
break;
case DD_DOS33:
case DD_PRODOS:
apple2_dec_dos(drive->image, drive->data);
break;
default:
log_critical("Unknown image type");
return ERR_INVALID;
}
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 != 0 && (phase << 1) != last && (phase >> 1) != last) {
return;
}
// The above check works for non-zero phases, but for zero phases,
// the logic is a bit different.
if (phase == 0 && last != 0x1 && last != 0x8) {
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.
*/
int
apple2_dd_position(apple2dd *drive)
{
// Special case: they didn't load any image data into the "drive".
// Return zero.
if (drive->data == NULL) {
return 0;
}
// This is a normative DOS 3.3 / ProDOS disk. (Except ProDOS is
// separated into 512 byte blocks which _shouldn't_ matter for our
// purposes but let's not talk about that here do-de-doo.)
if (drive->data->size == _140K_) {
int track_offset;
track_offset = (drive->track_pos % 2) * ENC_ETRACK;
return track_offset + drive->sector_pos;
}
return 0;
}
/*
* Read a single byte from the disk drive, at its current position, and
* then shift the head by 1 byte.
*/
vm_8bit
apple2_dd_read(apple2dd *drive)
{
vm_8bit byte = vm_segment_get(drive->data, apple2_dd_position(drive));
apple2_dd_shift(drive, 1);
return byte;
}
/*
* Here we mean to "empty" the drive, essentially freeing the segment
* memory and resetting the head position.
*/
void
apple2_dd_eject(apple2dd *drive)
{
if (drive->data) {
// Save off anything else we have left
apple2_dd_save(drive);
vm_segment_free(drive->data);
vm_segment_free(drive->image);
drive->data = NULL;
drive->image = NULL;
}
drive->track_pos = 0;
drive->sector_pos = 0;
}
/*
* Free the memory taken up by the disk drive.
*/
void
apple2_dd_free(apple2dd *drive)
{
if (drive->data) {
vm_segment_free(drive->data);
}
free(drive);
}
/*
* Set the disk drive mode, which is either read or write. (It can only
* be one or the other at a time.)
*/
void
apple2_dd_set_mode(apple2dd *drive, int mode)
{
if (mode != DD_READ && mode != DD_WRITE) {
return;
}
drive->mode = mode;
}
/*
* Shift the head position in the drive by the given positions, which is
* in bytes. Pos may be a negative number; if so, the head essentially
* moves further away from the center of the magnetic wafer.
*/
void
apple2_dd_shift(apple2dd *drive, int pos)
{
// When locked is true, we shouldn't shift our position by any
// number.
if (drive->locked) {
return;
}
drive->sector_pos += pos;
while (drive->sector_pos > MAX_SECTOR_POS) {
// We need to reset the sector pos to zero, because...
drive->sector_pos -= (MAX_SECTOR_POS + 1);
// We also need to move to the next track, so let's adjust by
// two half-tracks.
apple2_dd_step(drive, 2);
}
}
/*
* When you step the drive, you are essentially moving the head in
* track positions. It's not really faster for _us_, but it's faster for
* a mechanical drive than a bunch of shifts if you know the data is far
* away track-wise. This function also safeguards (as the drive did!)
* against stepping too far out or too far in.
*/
void
apple2_dd_step(apple2dd *drive, int steps)
{
drive->track_pos += steps;
if (drive->track_pos > MAX_DRIVE_STEPS) {
drive->track_pos = MAX_DRIVE_STEPS;
} else if (drive->track_pos < 0) {
drive->track_pos = 0;
}
}
/*
* A really simple function to turn the drive "on".
*/
void
apple2_dd_turn_on(apple2dd *drive, bool online)
{
drive->online = online;
}
/*
* Write a byte to the disk in the drive. This is pretty similar to the
* read function in that, once we do what we need with the segment, we
* shift the drive position forward by one byte.
*/
void
apple2_dd_write(apple2dd *drive)
{
vm_segment_set(drive->data, apple2_dd_position(drive), drive->latch);
apple2_dd_shift(drive, 1);
}
/*
* Set the write-protect status for the disk. Note that it was _disks_
* that were write-protected in the past, sometimes by taping over a
* chunk that was clipped out of the disk. So this function is somewhat
* similar to just taping over or removing that tape.
*/
void
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 < 0xC100; addr++) {
vm_segment_read_map(seg, addr, apple2_dd_switch_read);
vm_segment_write_map(seg, addr, apple2_dd_switch_write);
}
}