2 // Apple 2 floppy disk support
5 // (c) 2005-2019 Underground Software
7 // JLH = James Hammons <jlhamm@acm.org>
10 // --- ---------- -----------------------------------------------------------
11 // JLH 12/03/2005 Created this file
12 // JLH 12/15/2005 Fixed nybblization functions to work properly
13 // JLH 12/27/2005 Added blank disk creation, fixed saving to work properly
16 #include "floppydrive.h"
26 #include "video.h" // For message spawning... Though there's probably a
27 // better approach than this!
31 enum { IO_MODE_READ, IO_MODE_WRITE };
33 // Misc. arrays (read only) that are needed
35 static const uint8_t bitMask[8] = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
36 static const uint8_t sequencerROM[256] = {
37 0x18, 0x18, 0x18, 0x18, 0x0A, 0x0A, 0x0A, 0x0A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
38 0x2D, 0x38, 0x2D, 0x38, 0x0A, 0x0A, 0x0A, 0x0A, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
39 0x38, 0x28, 0xD8, 0x08, 0x0A, 0x0A, 0x0A, 0x0A, 0x39, 0x39, 0x39, 0x39, 0x3B, 0x3B, 0x3B, 0x3B,
40 0x48, 0x48, 0xD8, 0x48, 0x0A, 0x0A, 0x0A, 0x0A, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
41 0x58, 0x58, 0xD8, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58,
42 0x68, 0x68, 0xD8, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68,
43 0x78, 0x78, 0xD8, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78,
44 0x88, 0x88, 0xD8, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0x08, 0x88, 0x08, 0x88, 0x08, 0x88, 0x08, 0x88,
45 0x98, 0x98, 0xD8, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
46 0x29, 0xA8, 0xD8, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8,
47 0xBD, 0xB8, 0xCD, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0xB9, 0xB9, 0xB9, 0xB9, 0xBB, 0xBB, 0xBB, 0xBB,
48 0x59, 0xC8, 0xD9, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8, 0xC8,
49 0xD9, 0xA0, 0xD9, 0xD8, 0x0A, 0x0A, 0x0A, 0x0A, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8,
50 0x08, 0xE8, 0xD8, 0xE8, 0x0A, 0x0A, 0x0A, 0x0A, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8, 0xE8,
51 0xFD, 0xF8, 0xFD, 0xF8, 0x0A, 0x0A, 0x0A, 0x0A, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8, 0xF8,
52 0x4D, 0xE0, 0xDD, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x08, 0x88, 0x08, 0x88, 0x08, 0x88, 0x08
55 static char nameBuf[MAX_PATH];
58 // Static in-line functions, for clarity & speed, for swapping variables
59 static inline void Swap(uint8_t & a, uint8_t & b)
67 static inline void Swap(uint32_t & a, uint32_t & b)
75 static inline void Swap(bool & a, bool & b)
83 static inline void Swap(uint8_t * & a, uint8_t * & b)
91 // FloppyDrive class implementation...
93 FloppyDrive::FloppyDrive(): motorOn(0), activeDrive(0), ioMode(IO_MODE_READ), ioHappened(false), diskImageReady(false)
95 phase[0] = phase[1] = 0;
96 headPos[0] = headPos[1] = 0;
97 trackLength[0] = trackLength[1] = 51200;
98 disk[0] = disk[1] = NULL;
99 // woz[0] = woz[1] = NULL;
100 diskSize[0] = diskSize[1] = 0;
101 diskType[0] = diskType[1] = DT_EMPTY;
102 imageDirty[0] = imageDirty[1] = false;
103 imageName[0][0] = imageName[1][0] = 0; // Zero out filenames
107 FloppyDrive::~FloppyDrive()
117 bool FloppyDrive::LoadImage(const char * filename, uint8_t driveNum/*= 0*/)
119 WriteLog("FLOPPY: Attempting to load image '%s' in drive #%u.\n", filename, driveNum);
123 WriteLog("FLOPPY: Attempted to load image to drive #%u!\n", driveNum);
127 // Zero out filename, in case it doesn't load
128 imageName[driveNum][0] = 0;
129 //prolly should load EjectImage() first, so we don't have to dick around with crap
130 uint8_t * buffer = ReadFile(filename, &diskSize[driveNum]);
134 WriteLog("FLOPPY: Failed to open image file '%s' for reading...\n", filename);
139 free(disk[driveNum]);
141 disk[driveNum] = buffer;
143 diskImageReady = false;
144 DetectImageType(filename, driveNum);
145 strcpy(imageName[driveNum], filename);
146 diskImageReady = true;
148 WriteLog("FLOPPY: Loaded image '%s' for drive #%u.\n", filename, driveNum);
154 bool FloppyDrive::SaveImage(uint8_t driveNum/*= 0*/)
156 // Various sanity checks...
159 WriteLog("FLOPPY: Attempted to save image to drive #%u!\n", driveNum);
163 if (diskType[driveNum] == DT_EMPTY)
165 WriteLog("FLOPPY: No image in drive #%u to save\n", driveNum);
169 if (!imageDirty[driveNum])
171 WriteLog("FLOPPY: No need to save unchanged image in drive #%u...\n", driveNum);
175 char * ext = strrchr(imageName[driveNum], '.');
177 if ((ext != NULL) && (diskType[driveNum] != DT_WOZ))
178 memcpy(ext, ".woz", 4);
180 return SaveWOZ(imageName[driveNum], (WOZ2 *)disk[driveNum], diskSize[driveNum]);
184 bool FloppyDrive::SaveImageAs(const char * filename, uint8_t driveNum/*= 0*/)
186 strncpy(imageName[driveNum], filename, MAX_PATH);
187 // Ensure a NULL terminated string here, as strncpy() won't terminate the
188 // string if the source length is >= MAX_PATH
189 imageName[driveNum][MAX_PATH - 1] = 0;
190 return SaveImage(driveNum);
194 void FloppyDrive::CreateBlankImage(uint8_t driveNum/*= 0*/)
196 if (disk[driveNum] != NULL)
197 free(disk[driveNum]);
199 disk[driveNum] = InitWOZ(&diskSize[driveNum]);
200 diskType[driveNum] = DT_WOZ;
201 strcpy(imageName[driveNum], "newblank.woz");
202 SpawnMessage("New blank image inserted in drive %u...", driveNum);
206 void FloppyDrive::SwapImages(void)
208 char imageNameTmp[MAX_PATH];
210 memcpy(imageNameTmp, imageName[0], MAX_PATH);
211 memcpy(imageName[0], imageName[1], MAX_PATH);
212 memcpy(imageName[1], imageNameTmp, MAX_PATH);
214 Swap(disk[0], disk[1]);
215 Swap(diskSize[0], diskSize[1]);
216 Swap(diskType[0], diskType[1]);
217 Swap(imageDirty[0], imageDirty[1]);
219 Swap(phase[0], phase[1]);
220 Swap(headPos[0], headPos[1]);
221 Swap(currentPos[0], currentPos[1]);
222 SpawnMessage("Drive 0: %s...", imageName[0]);
227 Need to add some type of error checking here, so we can report back on bad images, etc. (basically, it does by returning DFT_UNKNOWN, but we could do better)
229 void FloppyDrive::DetectImageType(const char * filename, uint8_t driveNum)
231 diskType[driveNum] = DFT_UNKNOWN;
233 uint8_t wozType = CheckWOZType(disk[driveNum], diskSize[driveNum]);
237 // Check WOZ integrity...
238 CheckWOZIntegrity(disk[driveNum], diskSize[driveNum]);
240 // If it's a WOZ type 1 file, upconvert it to type 2
244 uint8_t * buffer = UpconvertWOZ1ToWOZ2(disk[driveNum], diskSize[driveNum], &size);
246 free(disk[driveNum]);
247 disk[driveNum] = buffer;
248 diskSize[driveNum] = size;
249 WriteLog("FLOPPY: Upconverted WOZ type 1 to type 2...\n");
252 diskType[driveNum] = DT_WOZ;
254 else if (diskSize[driveNum] == 143360)
256 const char * ext = strrchr(filename, '.');
261 WriteLog("FLOPPY: Found extension [%s]...\n", ext);
263 if (strcasecmp(ext, ".po") == 0)
264 diskType[driveNum] = DT_PRODOS;
265 else if ((strcasecmp(ext, ".do") == 0) || (strcasecmp(ext, ".dsk") == 0))
267 // We assume this, but check for a PRODOS fingerprint. Trust, but
269 diskType[driveNum] = DT_DOS33;
271 uint8_t fingerprint[4][4] = {
272 { 0x00, 0x00, 0x03, 0x00 }, // @ $400
273 { 0x02, 0x00, 0x04, 0x00 }, // @ $600
274 { 0x03, 0x00, 0x05, 0x00 }, // @ $800
275 { 0x04, 0x00, 0x00, 0x00 } // @ $A00
278 bool foundProdos = true;
280 for(uint32_t i=0; i<4; i++)
282 for(uint32_t j=0; j<4; j++)
284 if (disk[driveNum][0x400 + (i * 0x200) + j] != fingerprint[i][j])
293 diskType[driveNum] = DT_PRODOS;
296 // Actually, it just might matter WRT to nybblyzing/denybblyzing
297 // (and, it does... :-P)
298 WOZifyImage(driveNum);
300 else if (diskSize[driveNum] == 143488)
302 diskType[driveNum] = DT_DOS33_HDR;
303 WOZifyImage(driveNum);
306 #warning "Should we attempt to nybblize unknown images here? Definitely SHOULD issue a warning!"
307 // No, we don't nybblize anymore. But we should tell the user that the loading failed with a return value
309 WriteLog("FLOPPY: Detected image type %s...\n", (diskType[driveNum] == DT_DOS33 ?
310 "DOS 3.3" : (diskType[driveNum] == DT_DOS33_HDR ?
311 "DOS 3.3 (headered)" : (diskType[driveNum] == DT_PRODOS ? "ProDOS" : (diskType[driveNum] == DT_WOZ ? "WOZ" : "unknown")))));
316 // Write a bitstream (source left justified to bit 7) to destination buffer.
317 // Writes 'bits' number of bits to 'dest', starting at bit position 'dstPtr',
318 // updating 'dstPtr' for the caller.
320 void FloppyDrive::WriteBits(uint8_t * dest, const uint8_t * src, uint16_t bits, uint16_t * dstPtr)
322 for(uint16_t i=0; i<bits; i++)
324 // Get the destination location's bitmask
325 uint8_t dstMask = bitMask[*dstPtr % 8];
327 // Set the bit to one if there's a corresponding one in the source
328 // data, otherwise set it to zero
329 if (src[i / 8] & bitMask[i % 8])
330 dest[*dstPtr / 8] |= dstMask;
332 dest[*dstPtr / 8] &= ~dstMask;
339 void FloppyDrive::WOZifyImage(uint8_t driveNum)
341 // hdr (21) + nybbles (343) + footer (48) = 412 bytes per sector
342 // (not incl. 64 byte track marker)
343 // let's try 394 per sector... & see what happens
344 // let's go back to what we had, and see what happens :-)
345 // [still need to expand them back to what they were]
347 const uint8_t ff10[2] = { 0xFF, 0x00 };
348 uint8_t addressHeader[14] = {
349 0xD5, 0xAA, 0x96, 0xFF, 0xFE, 0x00, 0x00, 0x00,
350 0x00, 0x00, 0x00, 0xDE, 0xAA, 0xEB };
351 const uint8_t sectorHeader[3] = { 0xD5, 0xAA, 0xAD };
352 const uint8_t footer[3] = { 0xDE, 0xAA, 0xEB };
353 const uint8_t diskbyte[0x40] = {
354 0x96, 0x97, 0x9A, 0x9B, 0x9D, 0x9E, 0x9F, 0xA6,
355 0xA7, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB2, 0xB3,
356 0xB4, 0xB5, 0xB6, 0xB7, 0xB9, 0xBA, 0xBB, 0xBC,
357 0xBD, 0xBE, 0xBF, 0xCB, 0xCD, 0xCE, 0xCF, 0xD3,
358 0xD6, 0xD7, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE,
359 0xDF, 0xE5, 0xE6, 0xE7, 0xE9, 0xEA, 0xEB, 0xEC,
360 0xED, 0xEE, 0xEF, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6,
361 0xF7, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF };
362 const uint8_t doSector[16] = {
363 0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4, 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF };
364 const uint8_t poSector[16] = {
365 0x0, 0x8, 0x1, 0x9, 0x2, 0xA, 0x3, 0xB, 0x4, 0xC, 0x5, 0xD, 0x6, 0xE, 0x7, 0xF };
368 // Save current image until we're done converting
369 uint8_t * tmpDisk = disk[driveNum];
371 // Set up track index...
372 disk[driveNum] = InitWOZ(&diskSize[driveNum]);
373 WOZ2 & woz = *((WOZ2 *)disk[driveNum]);
375 // Upconvert data from DSK & friends format to WOZ tracks :-)
376 for(uint8_t trk=0; trk<35; trk++)
378 uint16_t dstBitPtr = 0;
379 uint8_t * img = disk[driveNum] + (Uint16LE(woz.track[trk].startingBlock) * 512);
380 //printf("Converting track %u: startingBlock=%u, %u blocks, img=%X\n", trk, Uint16LE(woz.track[trk].startingBlock), Uint16LE(woz.track[trk].blockCount), img);
382 // Write self-sync header bytes (16, should it be 64? Dunno.)
383 for(int i=0; i<64; i++)
384 WriteBits(img, ff10, 10, &dstBitPtr);
386 // Write out the following sectors
387 for(uint8_t sector=0; sector<16; sector++)
389 // Set up the sector address header
390 addressHeader[5] = ((trk >> 1) & 0x55) | 0xAA;
391 addressHeader[6] = (trk & 0x55) | 0xAA;
392 addressHeader[7] = ((sector >> 1) & 0x55) | 0xAA;
393 addressHeader[8] = (sector & 0x55) | 0xAA;
394 addressHeader[9] = (((trk ^ sector ^ 0xFE) >> 1) & 0x55) | 0xAA;
395 addressHeader[10] = ((trk ^ sector ^ 0xFE) & 0x55) | 0xAA;
397 WriteBits(img, addressHeader, 14 * 8, &dstBitPtr);
399 // Write 5 self-sync bytes for actual sector header
400 for(int i=0; i<5; i++)
401 WriteBits(img, ff10, 10, &dstBitPtr);
403 // Write sector header (D5 AA AD)
404 WriteBits(img, sectorHeader, 3 * 8, &dstBitPtr);
405 uint8_t * bytes = tmpDisk;
407 //Need to fix this so it writes the correct sector in the correct place *and* put the correct sector # into the header above as well. !!! FIX !!!
408 // Figure out location of sector data in disk image
409 if (diskType[driveNum] == DT_DOS33)
410 bytes += (doSector[sector] * 256) + (trk * 256 * 16);
411 else if (diskType[driveNum] == DT_DOS33_HDR)
412 bytes += (doSector[sector] * 256) + (trk * 256 * 16) + 128;
413 else if (diskType[driveNum] == DT_PRODOS)
414 bytes += (poSector[sector] * 256) + (trk * 256 * 16);
416 bytes += (sector * 256) + (trk * 256 * 16);
418 // Convert the 256 8-bit bytes into 342 6-bit bytes.
419 for(uint16_t i=0; i<0x56; i++)
421 tmpNib[i] = ((bytes[(i + 0xAC) & 0xFF] & 0x01) << 7)
422 | ((bytes[(i + 0xAC) & 0xFF] & 0x02) << 5)
423 | ((bytes[(i + 0x56) & 0xFF] & 0x01) << 5)
424 | ((bytes[(i + 0x56) & 0xFF] & 0x02) << 3)
425 | ((bytes[(i + 0x00) & 0xFF] & 0x01) << 3)
426 | ((bytes[(i + 0x00) & 0xFF] & 0x02) << 1);
429 tmpNib[0x54] &= 0x3F;
430 tmpNib[0x55] &= 0x3F;
431 memcpy(tmpNib + 0x56, bytes, 256);
433 // XOR the data block with itself, offset by one byte, creating a
434 // 343rd byte which is used as a checksum.
437 for(uint16_t i=342; i>0; i--)
438 tmpNib[i] = tmpNib[i] ^ tmpNib[i - 1];
440 // Using a lookup table, convert the 6-bit bytes into disk bytes.
441 for(uint16_t i=0; i<343; i++)
442 tmpNib[i] = diskbyte[tmpNib[i] >> 2];
444 WriteBits(img, tmpNib, 343 * 8, &dstBitPtr);
446 // Done with the nybblization, now add the epilogue...
447 WriteBits(img, footer, 3 * 8, &dstBitPtr);
449 // (Should the footer be 30 or 48? would be 45 FF10s here for 48)
450 for(int i=0; i<27; i++)
451 WriteBits(img, ff10, 10, &dstBitPtr);
454 // Set the proper bit/byte lengths in the WOZ for this track
455 woz.track[trk].bitCount = Uint16LE(dstBitPtr);
458 // Finally, free the non-WOZ image now that we're done converting
463 const char * FloppyDrive::ImageName(uint8_t driveNum/*= 0*/)
465 // Set up a zero-length string for return value
470 WriteLog("FLOPPY: Attempted to get image name for drive #%u!\n", driveNum);
474 // Now we attempt to strip out extraneous paths/extensions to get just the filename
475 const char * startOfFile = strrchr(imageName[driveNum], '/');
476 const char * startOfExt = strrchr(imageName[driveNum], '.');
478 // If there isn't a path, assume we're starting at the beginning
479 if (startOfFile == NULL)
480 startOfFile = &imageName[driveNum][0];
484 // If there isn't an extension, assume it's at the terminating NULL
485 if (startOfExt == NULL)
486 startOfExt = &imageName[driveNum][0] + strlen(imageName[driveNum]);
488 // Now copy the filename (may copy nothing!)
491 for(const char * i=startOfFile; i<startOfExt; i++)
500 void FloppyDrive::EjectImage(uint8_t driveNum/*= 0*/)
503 if (diskType[driveNum] == DT_EMPTY)
506 // Probably want to save a dirty image... ;-)
507 if (SaveImage(driveNum))
508 WriteLog("FLOPPY: Ejected image file '%s' from drive %u...\n", imageName[driveNum], driveNum);
511 free(disk[driveNum]);
513 disk[driveNum] = NULL;
514 // woz[driveNum] = NULL;
515 diskSize[driveNum] = 0;
516 diskType[driveNum] = DT_EMPTY;
517 imageDirty[driveNum] = false;
518 imageName[driveNum][0] = 0; // Zero out filenames
522 bool FloppyDrive::IsEmpty(uint8_t driveNum/*= 0*/)
526 WriteLog("FLOPPY: Attempted DriveIsEmtpy() for drive #%u!\n", driveNum);
530 return (diskType[driveNum] == DT_EMPTY ? true : false);
534 bool FloppyDrive::IsWriteProtected(uint8_t driveNum/*= 0*/)
538 WriteLog("FLOPPY: Attempted DiskIsWriteProtected() for drive #%u!\n", driveNum);
542 WOZ2 & woz = *((WOZ2 *)disk[driveNum]);
543 return (bool)woz.writeProtected;
547 void FloppyDrive::SetWriteProtect(bool state, uint8_t driveNum/*= 0*/)
551 WriteLog("FLOPPY: Attempted set write protect for drive #%u!\n", driveNum);
555 WOZ2 & woz = *((WOZ2 *)disk[driveNum]);
556 woz.writeProtected = (uint8_t)state;
560 int FloppyDrive::DriveLightStatus(uint8_t driveNum/*= 0*/)
562 int retval = DLS_OFF;
564 if (activeDrive != driveNum)
568 retval = (ioMode == IO_MODE_READ ? DLS_READ : DLS_WRITE);
575 void FloppyDrive::SaveState(FILE * file)
577 // Internal state vars
578 fputc(motorOn, file);
579 fputc(activeDrive, file);
581 fputc(dataRegister, file);
582 fputc((ioHappened ? 1 : 0), file);
587 WriteLong(file, diskSize[0]);
588 WriteLong(file, diskType[0]);
589 fputc(phase[0], file);
590 fputc(headPos[0], file);
591 WriteLong(file, currentPos[0]);
592 fputc((imageDirty[0] ? 1 : 0), file);
593 fwrite(disk[0], 1, diskSize[0], file);
594 fwrite(imageName[0], 1, MAX_PATH, file);
602 WriteLong(file, diskSize[1]);
603 WriteLong(file, diskType[1]);
604 fputc(phase[1], file);
605 fputc(headPos[1], file);
606 WriteLong(file, currentPos[1]);
607 fputc((imageDirty[1] ? 1 : 0), file);
608 fwrite(disk[1], 1, diskSize[1], file);
609 fwrite(imageName[1], 1, MAX_PATH, file);
616 void FloppyDrive::LoadState(FILE * file)
618 // Eject images if they're loaded
622 // Read internal state variables
623 motorOn = fgetc(file);
624 activeDrive = fgetc(file);
625 ioMode = fgetc(file);
626 dataRegister = fgetc(file);
627 ioHappened = (fgetc(file) == 1 ? true : false);
629 diskSize[0] = ReadLong(file);
633 disk[0] = new uint8_t[diskSize[0]];
634 diskType[0] = (uint8_t)ReadLong(file);
635 phase[0] = fgetc(file);
636 headPos[0] = fgetc(file);
637 currentPos[0] = ReadLong(file);
638 imageDirty[0] = (fgetc(file) == 1 ? true : false);
639 fread(disk[0], 1, diskSize[0], file);
640 fread(imageName[0], 1, MAX_PATH, file);
643 diskSize[1] = ReadLong(file);
647 disk[1] = new uint8_t[diskSize[1]];
648 diskType[1] = (uint8_t)ReadLong(file);
649 phase[1] = fgetc(file);
650 headPos[1] = fgetc(file);
651 currentPos[1] = ReadLong(file);
652 imageDirty[1] = (fgetc(file) == 1 ? true : false);
653 fread(disk[1], 1, diskSize[1], file);
654 fread(imageName[1], 1, MAX_PATH, file);
659 uint32_t FloppyDrive::ReadLong(FILE * file)
663 for(int i=0; i<4; i++)
664 r = (r << 8) | fgetc(file);
670 void FloppyDrive::WriteLong(FILE * file, uint32_t l)
672 for(int i=0; i<4; i++)
674 fputc((l >> 24) & 0xFF, file);
680 // Memory mapped I/O functions + Logic State Sequencer
683 The DSK format is a byte-for-byte image of a 16-sector Apple II floppy disk: 35
684 tracks of 16 sectors of 256 bytes each, making 143,360 bytes in total. The PO
685 format is exactly the same size as DSK and is also organized as 35 sequential
686 tracks, but the sectors within each track are in a different sequence. The NIB
687 format is a nybblized format: a more direct representation of the disk's data
688 as encoded by the Apple II floppy drive hardware. NIB contains 35 tracks of
689 6656 bytes each, for a total size of 232,960 bytes. Although this format is
690 much larger, it is also more versatile and can represent the older 13-sector
691 disks, many copy-protected disks, and other unusual encodings.
693 N.B.: Though the NIB format is *closer* to the representation of the disk's
694 data, it's not *quite* 100% as there can be zero bits lurking in the
695 interstices of the bytes written to the disk. There's room for another
696 format that takes this into account (possibly even take phase 1 & 3
697 tracks into account as well).
699 As luck would have it, not long after I wrote that, I found out that some enterprising people have created it already--WOZ format. Which is now supported by apple2. :-D
701 According to Beneath Apple DOS, DOS checks the data register to see if it changes when spinning up a drive: "A sufficient delay should be provided to allow the motor time to come up to speed. Shugart recommends one second, but DOS is able to reduce this delay by watching the read latch until data starts to change." Which means, we can simulate an empty/off drive by leaving the data register alone.
704 uint64_t stepperTime = 0;
705 bool seenReadSinceStep = false;
707 void FloppyDrive::ControlStepper(uint8_t addr)
713 The stepper motor has 4 phase solenoids (numbered 0-3) which corresponds to bits 1-2 of the address. Bit 0 tells the phase solenoid to either energize (1) or de-energize (0). By energizing the phase solenoids in ascending order, the stepper motor moves the head from a low numbered track to a higher numbered track; conversely, by energizing the solenoids in descending order, the stepper motor moves the head from a high numbered track to a lower one. Given that this is a mechanical device, it takes a certain amount of time for the drum in the stepper motor to move from place to place--though pretty much all software written for the Disk II takes this into account.
715 Tracks can apparently go from 0 to 79, though typically only 0 to 69 are usuable. Further, because of the limitations of the read/write head of the drive, not every track can be written to, so typically (about 99.99% of the time in my guesstimation) only every *other* track is written to (phases 0 and 2); some disks exist that have tracks written on phase 1 or 3, but these tend to be the exception rather than the rule.
717 Taking into account the head slew time: ATM nothing seems to look at it, though it could be problematic as how we emulate it is different from how it actually works; namely, the emulator zaps the head to a new track instantly when the write to the phase happens while in the real thing, obviously this takes a non-zero amount of time. As such, none of the states where more than one phase solenoid is active at a time can be written so that they come on instantaneously; it would be fairly easy to write things that work on the real thing that don't on the emulator because of this. But most software (pretty much everything that I've ever seen) is pretty well behaved and this isn't an issue.
719 If it ever *does* become a problem, doing the physical modeling of the head moving at a real velocity shouldn't be that difficult to do.
722 // This is an array of stub positions crossed with solenoid energize
723 // patterns. The numbers represent how many quarter tracks the head will
724 // move given its current position and the pattern of energized solenoids.
725 // N.B.: Patterns for 11 & 13 haven't been filled in as I'm not sure how
726 // the stub(s) would react to those patterns. :-/
727 int16_t step[16][8] = {
728 { 0, 0, 0, 0, 0, 0, 0, 0 }, // [....]
729 { 0, -1, -2, 0, 0, 0, +2, +1 }, // [|...]
730 { +2, +1, 0, -1, -2, 0, 0, 0 }, // [.|..]
731 { +1, 0, -1, -2, -3, 0, +3, +2 }, // [||..]
732 { 0, 0, +2, +1, 0, -1, -2, 0 }, // [..|.]
733 { 0, -1, 0, +1, 0, -1, 0, +1 }, // [|.|.]
734 { +3, +2, +1, 0, -1, -2, -3, 0 }, // [.||.]
735 { +2, +1, 0, -1, -2, -3, 0, +3 }, // [|||.]
736 { -2, 0, 0, 0, +2, +1, 0, -1 }, // [...|]
737 { -1, -2, -3, 0, +3, +2, +1, 0 }, // [|..|]
738 { 0, +1, 0, -1, 0, +1, 0, -1 }, // [.|.|]
739 { 0, 0, 0, 0, 0, 0, 0, 0 }, // [||.|] ???
740 { -3, 0, +3, +2, +1, 0, -1, -2 }, // [..||]
741 { 0, 0, 0, 0, 0, 0, 0, 0 }, // [|.||] ???
742 { 0, +3, +2, +1, 0, -1, -2, -3 }, // [.|||]
743 { -1, +2, +1, 0, -1, -2, +1, 0 } // [||||]
747 if (diskType[activeDrive] == DT_EMPTY)
750 // Convert phase solenoid number into a bit from 1 through 8 [1, 2, 4, 8]:
751 uint8_t phaseBit = 1 << ((addr >> 1) & 0x03);
753 // Set the state of the phase solenoid accessed using the phase bit
755 phase[activeDrive] |= phaseBit;
757 phase[activeDrive] &= ~phaseBit;
760 uint8_t oldHeadPos = headPos[activeDrive] & 0x07;
761 int16_t newStep = step[phase[activeDrive]][oldHeadPos];
762 int16_t newHeadPos = (int16_t)headPos[activeDrive] + newStep;
765 if ((newHeadPos >= 0) && (newHeadPos <= 140))
766 headPos[activeDrive] = (uint8_t)newHeadPos;
768 // See if the new phase solenoid is energized, & move the stepper/head
770 // N.B.: The head stub is located by bits 1 & 2 of the headPos variable
771 uint8_t oldHeadPos = headPos[activeDrive];
772 uint8_t nextUp = 1 << (((oldHeadPos >> 1) + 1) & 0x03);
773 uint8_t nextDown = 1 << (((oldHeadPos >> 1) - 1) & 0x03);
775 // We simulate cogging here by seeing if there's a valid up and/or down
776 // position to go to. If both are valid, the head goes nowhere.
777 if (phase[activeDrive] & nextUp)
778 headPos[activeDrive] += (headPos[activeDrive] < 140 ? 2 : 0);
780 if (phase[activeDrive] & nextDown)
781 headPos[activeDrive] -= (headPos[activeDrive] > 0 ? 2 : 0);
784 if (oldHeadPos != headPos[activeDrive])
786 WOZ2 & woz = *((WOZ2 *)disk[activeDrive]);
787 uint8_t newTIdx = woz.tmap[headPos[activeDrive]];
788 float newBitLen = (newTIdx == 0xFF ? 51200.0f
789 : Uint16LE(woz.track[newTIdx].bitCount));
791 uint8_t oldTIdx = woz.tmap[oldHeadPos];
792 float oldBitLen = (oldTIdx == 0xFF ? 51200.0f
793 : Uint16LE(woz.track[oldTIdx].bitCount));
794 currentPos[activeDrive] = (uint32_t)((float)currentPos[activeDrive] * (newBitLen / oldBitLen));
796 trackLength[activeDrive] = (uint16_t)newBitLen;
797 SpawnMessage("Stepping to track %u...", headPos[activeDrive] >> 2);
800 // only check the time since the phase was first set ON
803 stepperTime = mainCPU.clock;
804 seenReadSinceStep = false;
806 WriteLog("FLOPPY: Stepper phase %d set to %s [%c%c%c%c] (track=%2.2f) [%u]\n", (addr >> 1) & 0x03, (addr & 0x01 ? "ON " : "off"), (phase[activeDrive] & 0x08 ? '|' : '.'), (phase[activeDrive] & 0x04 ? '|' : '.'), (phase[activeDrive] & 0x02 ? '|' : '.'), (phase[activeDrive] & 0x01 ? '|' : '.'), (float)headPos[activeDrive] / 4.0f, mainCPU.clock & 0xFFFFFFFF);
810 void FloppyDrive::ControlMotor(uint8_t addr)
818 driveOffTimeout = 2000000;
820 WriteLog("FLOPPY: Turning drive motor %s\n", (motorOn ? "ON" : "off"));
824 void FloppyDrive::DriveEnable(uint8_t addr)
828 WriteLog("FLOPPY: Selecting drive #%hhd\n", addr + 1);
833 So for $C08C-F, we have two switches (Q6 & Q7) which combine to make four states ($C-D is off/on for Q6, $E-F is off/on for Q7).
835 So it forms a matrix like so:
838 +-----------------------------------------------------------------------
839 $C08C |Enable READ sequencing |Data reg SHL every 8th clock while writing
840 +----------------------------+------------------------------------------
841 $C08D |Check write prot./init write|Data reg LOAD every 8th clk while writing
843 Looks like reads from even addresses in $C080-F block transfer data from the sequencer to the MPU, does write from odd do the inverse (transfer from MPU to sequencer)? Looks like it.
848 void FloppyDrive::SetShiftLoadSwitch(uint8_t state)
855 void FloppyDrive::SetReadWriteSwitch(uint8_t state)
862 // MMIO: Reads from $C08x to $C0XX on even addresses
863 uint8_t FloppyDrive::DataRegister(void)
866 if (diskType[activeDrive] != DT_EMPTY)
868 WOZ2 & woz = *((WOZ2 *)disk[activeDrive]);
869 uint8_t tIdx = woz.tmap[headPos[activeDrive]];
870 uint32_t bitLen = (tIdx == 0xFF ? 51200
871 : Uint16LE(woz.track[tIdx].bitCount));
872 SpawnMessage("%u:Reading $%02X from track %u, sector %u...",
873 activeDrive, dataRegister, headPos[activeDrive] >> 2, (uint32_t)(((float)currentPos[activeDrive] / (float)bitLen) * 16.0f));
874 ioMode = IO_MODE_READ;
877 if ((seenReadSinceStep == false) && (slSwitch == false) && (rwSwitch == false) && ((iorAddr & 0x0F) == 0x0C))
879 seenReadSinceStep = true;
880 WriteLog("%u:Reading $%02X from track %u, sector %u (delta since seek: %lu cycles) [%u]...\n",
881 activeDrive, dataRegister, headPos[activeDrive] >> 2, (uint32_t)(((float)currentPos[activeDrive] / (float)bitLen) * 16.0f), mainCPU.clock - stepperTime, mainCPU.clock & 0xFFFFFFFF);
889 // MMIO: Writes from $C08x to $C0XX on odd addresses
890 void FloppyDrive::DataRegister(uint8_t data)
893 ioMode = IO_MODE_WRITE;
899 OFF switches ON switches
900 Switch Addr Func Addr Func
901 Q0 $C080 Phase 0 off $C081 Phase 0 on
902 Q1 $C082 Phase 1 off $C083 Phase 1 on
903 Q2 $C084 Phase 2 off $C085 Phase 2 on
904 Q3 $C086 Phase 3 off $C087 Phase 3 on
905 Q4 $C088 Drive off $C089 Drive on
906 Q5 $C08A Select Drive 1 $C08B Select Drive 2
907 Q6 $C08C Shift data register $C08D Load data register
908 Q7 $C08E Read $C08F Write
910 From "Beneath Apple ProDOS", description of combinations of $C0EC-EF
912 $C08C, $C08E: Enable read sequencing
913 $C08C, $C08F: Shift data register every four cycles while writing
914 $C08D, $C08E: Check write protect and initialize sequencer for writing
915 $C08D, $C08F: Load data register every four cycles while writing
920 LDX #SLOT Put slot number times 16 in X-register.
922 LDA $C08E, X Sense write protect.
923 BMI ERROR If high bit set, protected.
928 PRODOS 8 MLI ERROR CODES
931 $01: Bad system call number
932 $04: Bad system call parameter count
933 $25: Interrupt table full
935 $28: No device connected
936 $2B: Disk write protected
938 $40: Invalid pathname
939 $42: Maximum number of files open
940 $43: Invalid reference number
941 $44: Directory not found
942 $45: Volume not found
944 $47: Duplicate filename
946 $49: Volume directory full
947 $4A: Incompatible file format, also a ProDOS directory
948 $4B: Unsupported storage_type
949 $4C: End of file encountered
950 $4D: Position out of range
951 $4E: File access error, also file locked
953 $51: Directory structure damaged
954 $52: Not a ProDOS volume
955 $53: Invalid system call parameter
956 $55: Volume Control Block table full
957 $56: Bad buffer address
958 $57: Duplicate volume
959 $5A: File structure damaged
962 // N.B.: The WOZ documentation says that the bitstream is normalized to 4µs.
963 // Which means on the //e that you would have to run it at that clock
964 // rate (instead of the //e clock rate 0.9799µs/cycle) to get the
965 // simulated drive running at 300 RPM. So, instead of doing that, we're
966 // just gonna run it at twice the clock rate of the base 6502 clock,
967 // which will make the simulated drive run in the neighborhood of around
968 // 306 RPM. Should be close enough to get away with it. :-) (And it
969 // seems to run OK, for the most part.)
972 static bool logSeq = false;
974 // Logic State Sequencer & Data Register
976 void FloppyDrive::RunSequencer(uint32_t cyclesToRun)
978 static uint32_t prng = 1;
983 else if (diskType[activeDrive] == DT_EMPTY)
985 else if (motorOn == false)
987 if (driveOffTimeout == 0)
993 WOZ2 & woz = *((WOZ2 *)disk[activeDrive]);
994 uint8_t tIdx = woz.tmap[headPos[activeDrive]];
995 uint8_t * tdata = disk[activeDrive] + (Uint16LE(woz.track[tIdx].startingBlock) * 512);
997 // It's x2 because the sequencer clock runs twice as fast as the CPU clock.
1000 //extern bool dumpDis;
1001 //static bool tripwire = false;
1003 //static uint32_t lastPos = 0;
1006 WriteLog("DISKSEQ: Running for %d cycles [rw=%hhd, sl=%hhd, reg=%02X, bus=%02X]\n", cyclesToRun, rwSwitch, slSwitch, dataRegister, cpuDataBus);
1009 while (cyclesToRun-- > 0)
1011 // pulseClock = (pulseClock + 1) & 0x07;
1012 pulseClock = (pulseClock + 1) % 8;
1013 // 7 doesn't work... Is that 3.5µs? Seems to be. Which means to get a 0.25µs granularity here, we need to double the # of cycles to run...
1014 // pulseClock = (pulseClock + 1) % 7;
1016 if (pulseClock == 0)
1018 uint16_t bytePos = currentPos[activeDrive] / 8;
1019 uint8_t bitPos = currentPos[activeDrive] % 8;
1023 if (tdata[bytePos] & bitMask[bitPos])
1025 // According to Jim Sather (Understanding the Apple II),
1026 // the Read Pulse, when it happens, is 1µs long, which is 2
1027 // sequencer clock pulses long.
1035 currentPos[activeDrive] = (currentPos[activeDrive] + 1) % trackLength[activeDrive];
1037 // If we hit more than 2 zero bits in a row, simulate the disk head
1038 // reader's Automatic Gain Control (AGC) turning itself up too high
1039 // by stuffing random bits in the bitstream. We also do this if
1040 // the current track is marked as unformatted.
1042 N.B.: Had to up this to 3 because Up N' Down had some weird sync bytes (FE10). May have to up it some more.
1044 if ((zeroBitCount > 3) || (tIdx == 0xFF))
1048 // This PRNG is called the "Galois configuration".
1057 // Find and run the Sequencer's next state
1058 uint8_t nextState = (sequencerState & 0xF0) | (rwSwitch << 3)
1059 | (slSwitch << 2) | (readPulse ? 0x02 : 0)
1060 | ((dataRegister & 0x80) >> 7);
1062 WriteLog("[%02X:%02X]%s", sequencerState, nextState, (chop == 15 ? "\n" : ""));
1063 chop = (chop + 1) % 20;
1064 sequencerState = sequencerROM[nextState];
1066 switch (sequencerState & 0x0F)
1076 // CLR (clear data register)
1081 // NOP (no operation)
1084 // SL0 (shift left, 0 fill LSB)
1088 if (rwSwitch && (tIdx != 0xFF)
1089 && !woz.writeProtected)
1091 imageDirty[activeDrive] = true;
1092 uint16_t bytePos = currentPos[activeDrive] / 8;
1093 uint8_t bitPos = currentPos[activeDrive] % 8;
1095 if (dataRegister & 0x80)
1096 // Fill in the one, if necessary
1097 tdata[bytePos] |= bitMask[bitPos];
1099 // Otherwise, punch in the zero
1100 tdata[bytePos] &= ~bitMask[bitPos];
1103 if (dumpDis || tripwire)
1106 WriteLog("[%s]", (dataRegister & 0x80 ? "1" : "0"));
1107 if (lastPos == currentPos[activeDrive])
1108 WriteLog("{STOMP}");
1109 else if ((lastPos + 1) != currentPos[activeDrive])
1111 lastPos = currentPos[activeDrive];
1119 // SR (shift right write protect bit)
1121 dataRegister |= (woz.writeProtected ? 0x80 : 0x00);
1125 // LD (load data register from data bus)
1126 dataRegister = cpuDataBus;
1129 if (rwSwitch && (tIdx != 0xFF) && !woz.writeProtected)
1131 imageDirty[activeDrive] = true;
1132 uint16_t bytePos = currentPos[activeDrive] / 8;
1133 uint8_t bitPos = currentPos[activeDrive] % 8;
1134 tdata[bytePos] |= bitMask[bitPos];
1136 if (dumpDis || tripwire)
1139 WriteLog("[%s]", (dataRegister & 0x80 ? "1" : "0"));
1140 if (lastPos == currentPos[activeDrive])
1141 WriteLog("{STOMP}");
1142 else if ((lastPos + 1) != currentPos[activeDrive])
1144 lastPos = currentPos[activeDrive];
1151 // SL1 (shift left, 1 fill LSB)
1153 dataRegister |= 0x01;
1166 FloppyDrive floppyDrive[2];
1168 static uint8_t SlotIOR(uint16_t address)
1170 uint8_t state = address & 0x0F;
1182 floppyDrive[0].ControlStepper(state);
1186 floppyDrive[0].ControlMotor(state & 0x01);
1190 floppyDrive[0].DriveEnable(state & 0x01);
1194 floppyDrive[0].SetShiftLoadSwitch(state & 0x01);
1198 floppyDrive[0].SetReadWriteSwitch(state & 0x01);
1202 //temp, for debugging
1204 // Even addresses return the data register, odd (we suppose) returns a
1205 // floating bus read...
1206 return (address & 0x01 ? ReadFloatingBus(0) : floppyDrive[0].DataRegister());
1210 static void SlotIOW(uint16_t address, uint8_t byte)
1212 uint8_t state = address & 0x0F;
1224 floppyDrive[0].ControlStepper(state);
1228 floppyDrive[0].ControlMotor(state & 0x01);
1232 floppyDrive[0].DriveEnable(state & 0x01);
1236 floppyDrive[0].SetShiftLoadSwitch(state & 0x01);
1240 floppyDrive[0].SetReadWriteSwitch(state & 0x01);
1244 // Odd addresses write to the Data register, even addresses (we assume) go
1247 floppyDrive[0].DataRegister(byte);
1251 // This slot function doesn't need to differentiate between separate instances
1253 static uint8_t SlotROM(uint16_t address)
1255 return diskROM[address];
1259 void InstallFloppy(uint8_t slot)
1261 SlotData disk = { SlotIOR, SlotIOW, SlotROM, 0, 0, 0 };
1262 InstallSlotHandler(slot, &disk);