2 // Apple 2 floppy disk support
5 // (c) 2005 Underground Software
7 // JLH = James L. 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
22 #include "applevideo.h" // For message spawning... Though there's probably a better approach than this!
24 //using namespace std;
28 enum { IO_MODE_READ, IO_MODE_WRITE };
30 // FloppyDrive class variable initialization
32 uint8_t FloppyDrive::header[21] = {
33 0xD5, 0xAA, 0x96, 0xFF, 0xFE, 0x00, 0x00, 0x00,
34 0x00, 0x00, 0x00, 0xDE, 0xAA, 0xFF, 0xFF, 0xFF,
35 0xFF, 0xFF, 0xD5, 0xAA, 0xAD };
36 uint8_t FloppyDrive::doSector[16] = {
37 0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4, 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF };
38 uint8_t FloppyDrive::poSector[16] = {
39 0x0, 0x8, 0x1, 0x9, 0x2, 0xA, 0x3, 0xB, 0x4, 0xC, 0x5, 0xD, 0x6, 0xE, 0x7, 0xF };
40 char FloppyDrive::nameBuf[MAX_PATH];
42 // FloppyDrive class implementation...
44 FloppyDrive::FloppyDrive(): motorOn(0), activeDrive(0), ioMode(IO_MODE_READ), phase(0), track(0)
46 disk[0] = disk[1] = NULL;
47 diskSize[0] = diskSize[1] = 0;
48 diskType[0] = diskType[1] = DT_UNKNOWN;
49 imageDirty[0] = imageDirty[1] = false;
50 writeProtected[0] = writeProtected[1] = false;
51 imageName[0][0] = imageName[1][0] = 0; // Zero out filenames
54 FloppyDrive::~FloppyDrive()
63 bool FloppyDrive::LoadImage(const char * filename, uint8_t driveNum/*= 0*/)
65 WriteLog("FLOPPY: Attempting to load image '%s' in drive #%u.\n", filename, driveNum);
69 WriteLog("FLOPPY: Attempted to load image to drive #%u!\n", driveNum);
73 imageName[driveNum][0] = 0; // Zero out filename, in case it doesn't load
75 FILE * fp = fopen(filename, "rb");
79 WriteLog("FLOPPY: Failed to open image file '%s' for reading...\n", filename);
84 delete[] disk[driveNum];
86 fseek(fp, 0, SEEK_END);
87 diskSize[driveNum] = ftell(fp);
88 fseek(fp, 0, SEEK_SET);
89 disk[driveNum] = new uint8_t[diskSize[driveNum]];
90 fread(disk[driveNum], 1, diskSize[driveNum], fp);
93 //printf("Read disk image: %u bytes.\n", diskSize);
94 DetectImageType(filename, driveNum);
95 strcpy(imageName[driveNum], filename);
98 WriteLog("FLOPPY: Opening image for drive #%u.\n", driveNum);
99 FILE * fp2 = fopen("bt-nybblized.nyb", "wb");
102 WriteLog("FLOPPY: Failed to open image file 'bt-nybblized.nyb' for writing...\n");
105 fwrite(nybblizedImage[driveNum], 1, 232960, fp2);
109 //writeProtected[driveNum] = true;
110 WriteLog("FLOPPY: Loaded image '%s' for drive #%u.\n", filename, driveNum);
115 bool FloppyDrive::SaveImage(uint8_t driveNum/*= 0*/)
119 WriteLog("FLOPPY: Attempted to save image to drive #%u!\n", driveNum);
123 if (!imageDirty[driveNum])
125 WriteLog("FLOPPY: No need to save unchanged image...\n");
129 if (imageName[driveNum][0] == 0)
131 WriteLog("FLOPPY: Attempted to save non-existant image!\n");
135 if (diskType[driveNum] == DT_NYBBLE)
136 memcpy(disk[driveNum], nybblizedImage[driveNum], 232960);
138 DenybblizeImage(driveNum);
140 FILE * fp = fopen(imageName[driveNum], "wb");
144 WriteLog("FLOPPY: Failed to open image file '%s' for writing...\n", imageName[driveNum]);
148 fwrite(disk[driveNum], 1, diskSize[driveNum], fp);
151 WriteLog("FLOPPY: Successfully wrote image file '%s'...\n", imageName[driveNum]);
156 bool FloppyDrive::SaveImageAs(const char * filename, uint8_t driveNum/*= 0*/)
158 //WARNING: Buffer overflow possibility
159 #warning "Buffer overflow possible--!!! FIX !!!"
160 strcpy(imageName[driveNum], filename);
161 return SaveImage(driveNum);
164 void FloppyDrive::CreateBlankImage(uint8_t driveNum/*= 0*/)
166 if (disk[driveNum] != NULL)
167 delete disk[driveNum];
169 disk[driveNum] = new uint8_t[143360];
170 diskSize[driveNum] = 143360;
171 memset(disk[driveNum], 0x00, 143360);
172 memset(nybblizedImage[driveNum], 0x00, 232960); // Set it to 0 instead of $FF for proper formatting...
173 diskType[driveNum] = DT_DOS33;
174 strcpy(imageName[driveNum], "newblank.dsk");
175 writeProtected[driveNum] = false;
176 SpawnMessage("New blank image inserted in drive %u...", driveNum);
179 void FloppyDrive::SwapImages(void)
181 uint8_t nybblizedImageTmp[232960];
182 char imageNameTmp[MAX_PATH];
184 memcpy(nybblizedImageTmp, nybblizedImage[0], 232960);
185 memcpy(nybblizedImage[0], nybblizedImage[1], 232960);
186 memcpy(nybblizedImage[1], nybblizedImageTmp, 232960);
188 memcpy(imageNameTmp, imageName[0], MAX_PATH);
189 memcpy(imageName[0], imageName[1], MAX_PATH);
190 memcpy(imageName[1], imageNameTmp, MAX_PATH);
192 uint8_t * diskTmp = disk[0];
196 uint32_t diskSizeTmp = diskSize[0];
197 diskSize[0] = diskSize[1];
198 diskSize[1] = diskSizeTmp;
200 uint8_t diskTypeTmp = diskType[0];
201 diskType[0] = diskType[1];
202 diskType[1] = diskTypeTmp;
204 uint8_t imageDirtyTmp = imageDirty[0];
205 imageDirty[0] = imageDirty[1];
206 imageDirty[1] = imageDirtyTmp;
208 uint8_t writeProtectedTmp = writeProtected[0];
209 writeProtected[0] = writeProtected[1];
210 writeProtected[1] = writeProtectedTmp;
211 SpawnMessage("Drive 0: %s...", imageName[0]);
214 void FloppyDrive::DetectImageType(const char * filename, uint8_t driveNum)
216 diskType[driveNum] = DT_UNKNOWN;
218 if (diskSize[driveNum] == 232960)
220 diskType[driveNum] = DT_NYBBLE;
221 memcpy(nybblizedImage[driveNum], disk[driveNum], 232960);
223 else if (diskSize[driveNum] == 143360)
225 const char * ext = strrchr(filename, '.');
229 WriteLog("FLOPPY: Found extension [%s]...\n", ext);
231 //Apparently .dsk can house either DOS order OR PRODOS order... !!! FIX !!!
232 //[DONE, see below why we don't need it]
233 if (strcasecmp(ext, ".po") == 0)
234 diskType[driveNum] = DT_PRODOS;
235 else if ((strcasecmp(ext, ".do") == 0) || (strcasecmp(ext, ".dsk") == 0))
237 diskType[driveNum] = DT_DOS33;
238 //WriteLog("Detected DOS 3.3 disk!\n");
240 This doesn't seem to be accurate... Maybe it's just a ProDOS disk in a DOS33 order...
241 That would seem to be the case--just because it's a ProDOS disk doesn't mean anything
242 WRT to the disk image itself.
243 // This could really be a ProDOS order disk with a .dsk extension, so let's see...
244 char fingerprint[3][4] = {
245 { 0x04, 0x00, 0x00, 0x00 }, // @ $500
246 { 0x03, 0x00, 0x05, 0x00 }, // @ $700
247 { 0x02, 0x00, 0x04, 0x00 } }; // @ $900
249 if ((strcmp((char *)(disk[driveNum] + 0x500), fingerprint[0]) == 0)
250 && (strcmp((char *)(disk[driveNum] + 0x700), fingerprint[1]) == 0)
251 && (strcmp((char *)(disk[driveNum] + 0x900), fingerprint[2]) == 0))
252 diskType[driveNum] = DT_PRODOS;
256 // Actually, it just might matter WRT to nybblyzing/denybblyzing
257 // Here, we check for BT3
259 //diskType[driveNum] = DT_PRODOS;
261 NybblizeImage(driveNum);
264 #warning "Should we attempt to nybblize unknown images here? Definitely SHOULD issue a warning!"
266 WriteLog("FLOPPY: Detected image type %s...\n", (diskType[driveNum] == DT_NYBBLE ?
267 "Nybble image" : (diskType[driveNum] == DT_DOS33 ?
268 "DOS 3.3 image" : (diskType[driveNum] == DT_PRODOS ? "ProDOS image" : "unknown"))));
271 void FloppyDrive::NybblizeImage(uint8_t driveNum)
273 // Format of a sector is header (23) + nybbles (343) + footer (30) = 396
274 // (short by 20 bytes of 416 [413 if 48 byte header is one time only])
275 // Hmph. Who'da thunk that AppleWin's nybblization routines would be wrong?
276 // This is now correct, BTW
277 // hdr (21) + nybbles (343) + footer (48) = 412 bytes per sector
278 // (not incl. 64 byte track marker)
280 uint8_t footer[48] = {
281 0xDE, 0xAA, 0xEB, 0xFF, 0xEB, 0xFF, 0xFF, 0xFF,
282 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
283 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
284 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
285 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
286 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
288 uint8_t diskbyte[0x40] = {
289 0x96, 0x97, 0x9A, 0x9B, 0x9D, 0x9E, 0x9F, 0xA6,
290 0xA7, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB2, 0xB3,
291 0xB4, 0xB5, 0xB6, 0xB7, 0xB9, 0xBA, 0xBB, 0xBC,
292 0xBD, 0xBE, 0xBF, 0xCB, 0xCD, 0xCE, 0xCF, 0xD3,
293 0xD6, 0xD7, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE,
294 0xDF, 0xE5, 0xE6, 0xE7, 0xE9, 0xEA, 0xEB, 0xEC,
295 0xED, 0xEE, 0xEF, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6,
296 0xF7, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF };
298 uint8_t * img = nybblizedImage[driveNum];
299 memset(img, 0xFF, 232960); // Doesn't matter if 00s or FFs...
301 for(uint8_t trk=0; trk<35; trk++)
303 memset(img, 0xFF, 64); // Write gap 1, 64 bytes (self-sync)
306 for(uint8_t sector=0; sector<16; sector++)
308 memcpy(img, header, 21); // Set up the sector header
310 img[5] = ((trk >> 1) & 0x55) | 0xAA;
311 img[6] = (trk & 0x55) | 0xAA;
312 img[7] = ((sector >> 1) & 0x55) | 0xAA;
313 img[8] = (sector & 0x55) | 0xAA;
314 img[9] = (((trk ^ sector ^ 0xFE) >> 1) & 0x55) | 0xAA;
315 img[10] = ((trk ^ sector ^ 0xFE) & 0x55) | 0xAA;
318 uint8_t * bytes = disk[driveNum];
320 if (diskType[driveNum] == DT_DOS33)
321 bytes += (doSector[sector] * 256) + (trk * 256 * 16);
322 else if (diskType[driveNum] == DT_PRODOS)
323 bytes += (poSector[sector] * 256) + (trk * 256 * 16);
325 bytes += (sector * 256) + (trk * 256 * 16);
327 // Convert the 256 8-bit bytes into 342 6-bit bytes.
329 for(uint16_t i=0; i<0x56; i++)
331 img[i] = ((bytes[(i + 0xAC) & 0xFF] & 0x01) << 7)
332 | ((bytes[(i + 0xAC) & 0xFF] & 0x02) << 5)
333 | ((bytes[(i + 0x56) & 0xFF] & 0x01) << 5)
334 | ((bytes[(i + 0x56) & 0xFF] & 0x02) << 3)
335 | ((bytes[(i + 0x00) & 0xFF] & 0x01) << 3)
336 | ((bytes[(i + 0x00) & 0xFF] & 0x02) << 1);
341 memcpy(img + 0x56, bytes, 256);
343 // XOR the data block with itself, offset by one byte,
344 // creating a 343rd byte which is used as a cheksum.
348 for(uint16_t i=342; i>0; i--)
349 img[i] = img[i] ^ img[i - 1];
351 // Using a lookup table, convert the 6-bit bytes into disk bytes.
353 for(uint16_t i=0; i<343; i++)
354 //#define TEST_NYBBLIZATION
355 #ifdef TEST_NYBBLIZATION
357 WriteLog("FL: i = %u, img[i] = %02X, diskbyte = %02X\n", i, img[i], diskbyte[img[i] >> 2]);
359 img[i] = diskbyte[img[i] >> 2];
360 #ifdef TEST_NYBBLIZATION
361 //WriteLog(" img[i] = %02X\n", img[i]);
366 // Done with the nybblization, now for the epilogue...
368 memcpy(img, footer, 48);
374 void FloppyDrive::DenybblizeImage(uint8_t driveNum)
376 uint8_t decodeNybble[0x80] = {
377 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
378 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
379 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
380 0x00, 0x00, 0x08, 0x0C, 0x00, 0x10, 0x14, 0x18,
381 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x20,
382 0x00, 0x00, 0x00, 0x24, 0x28, 0x2C, 0x30, 0x34,
383 0x00, 0x00, 0x38, 0x3C, 0x40, 0x44, 0x48, 0x4C,
384 0x00, 0x50, 0x54, 0x58, 0x5C, 0x60, 0x64, 0x68,
385 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
386 0x00, 0x00, 0x00, 0x6C, 0x00, 0x70, 0x74, 0x78,
387 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x80, 0x84,
388 0x00, 0x88, 0x8C, 0x90, 0x94, 0x98, 0x9C, 0xA0,
389 0x00, 0x00, 0x00, 0x00, 0x00, 0xA4, 0xA8, 0xAC,
390 0x00, 0xB0, 0xB4, 0xB8, 0xBC, 0xC0, 0xC4, 0xC8,
391 0x00, 0x00, 0xCC, 0xD0, 0xD4, 0xD8, 0xDC, 0xE0,
392 0x00, 0xE4, 0xE8, 0xEC, 0xF0, 0xF4, 0xF8, 0xFC };
395 if (disk[driveNum] == NULL || diskSize[driveNum] < 143360)
397 WriteLog("FLOPPY: Source disk image invalid! [drive=%u, disk=%08X, diskSize=%u]\n",
398 driveNum, disk[driveNum], diskSize[driveNum]);
402 uint8_t * srcImg = nybblizedImage[driveNum];
403 uint8_t * dstImg = disk[driveNum];
404 uint8_t buffer[345]; // 2 extra bytes for the unpack routine below...
406 for(uint8_t trk=0; trk<35; trk++)
408 uint8_t * trackBase = srcImg + (trk * 6656);
410 for(uint8_t sector=0; sector<16; sector++)
412 uint16_t sectorStart = (uint16_t)-1;
414 for(uint16_t i=0; i<6656; i++)
416 if (trackBase[i] == header[0]
417 && trackBase[(i + 1) % 6656] == header[1]
418 && trackBase[(i + 2) % 6656] == header[2]
419 && trackBase[(i + 3) % 6656] == header[3]
420 && trackBase[(i + 4) % 6656] == header[4])
422 //Could also check the track # at +5,6...
423 uint8_t foundSector = ((trackBase[(i + 7) % 6656] & 0x55) << 1)
424 | (trackBase[(i + 8) % 6656] & 0x55);
426 if (foundSector == sector)
428 sectorStart = (i + 21) % 6656;
435 if (sectorStart == (uint16_t)-1)
437 WriteLog("FLOPPY: Failed to find sector %u (track %u) in nybble image!\n",
442 // Using a lookup table, convert the disk bytes into 6-bit bytes.
444 for(uint16_t i=0; i<343; i++)
445 buffer[i] = decodeNybble[trackBase[(sectorStart + i) % 6656] & 0x7F];
447 // XOR the data block with itself, offset by one byte.
449 for(uint16_t i=1; i<342; i++)
450 buffer[i] = buffer[i] ^ buffer[i - 1];
452 // Convert the 342 6-bit bytes into 256 8-bit bytes (at buffer + $56).
454 for(uint16_t i=0; i<0x56; i++)
456 buffer[0x056 + i] |= ((buffer[i] >> 3) & 0x01) | ((buffer[i] >> 1) & 0x02);
457 buffer[0x0AC + i] |= ((buffer[i] >> 5) & 0x01) | ((buffer[i] >> 3) & 0x02);
458 buffer[0x102 + i] |= ((buffer[i] >> 7) & 0x01) | ((buffer[i] >> 5) & 0x02);
461 uint8_t * bytes = dstImg;
463 if (diskType[driveNum] == DT_DOS33)
464 bytes += (doSector[sector] * 256) + (trk * 256 * 16);
465 else if (diskType[driveNum] == DT_PRODOS)
466 bytes += (poSector[sector] * 256) + (trk * 256 * 16);
468 bytes += (sector * 256) + (trk * 256 * 16);//*/
470 memcpy(bytes, buffer + 0x56, 256);
475 const char * FloppyDrive::GetImageName(uint8_t driveNum/*= 0*/)
477 // Set up a zero-length string for return value
482 WriteLog("FLOPPY: Attempted to get image name for drive #%u!\n", driveNum);
486 // Now we attempt to strip out extraneous paths/extensions to get just the filename
487 const char * startOfFile = strrchr(imageName[driveNum], '/');
488 const char * startOfExt = strrchr(imageName[driveNum], '.');
490 // If there isn't a path, assume we're starting at the beginning
491 if (startOfFile == NULL)
492 startOfFile = &imageName[driveNum][0];
496 // If there isn't an extension, assume it's at the terminating NULL
497 if (startOfExt == NULL)
498 startOfExt = &imageName[driveNum][0] + strlen(imageName[driveNum]);
500 // Now copy the filename (may copy nothing!)
503 for(const char * i=startOfFile; i<startOfExt; i++)
511 void FloppyDrive::EjectImage(uint8_t driveNum/*= 0*/)
513 // Probably want to save a dirty image... ;-)
516 WriteLog("FLOPPY: Ejected image file '%s' from drive %u...\n", imageName[driveNum], driveNum);
519 delete[] disk[driveNum];
521 disk[driveNum] = NULL;
522 diskSize[driveNum] = 0;
523 diskType[driveNum] = DT_UNKNOWN;
524 imageDirty[driveNum] = false;
525 writeProtected[driveNum] = false;
526 imageName[driveNum][0] = 0; // Zero out filenames
527 memset(nybblizedImage[driveNum], 0xFF, 232960); // Doesn't matter if 00s or FFs...
530 bool FloppyDrive::DriveIsEmpty(uint8_t driveNum/*= 0*/)
534 WriteLog("FLOPPY: Attempted DriveIsEmtpy() for drive #%u!\n", driveNum);
538 // This is kinda gay, but it works
539 return (imageName[driveNum][0] == 0 ? true : false);
542 bool FloppyDrive::DiskIsWriteProtected(uint8_t driveNum/*= 0*/)
546 WriteLog("FLOPPY: Attempted DiskIsWriteProtected() for drive #%u!\n", driveNum);
550 return writeProtected[driveNum];
553 void FloppyDrive::SetWriteProtect(bool state, uint8_t driveNum/*= 0*/)
557 WriteLog("FLOPPY: Attempted set write protect for drive #%u!\n", driveNum);
561 writeProtected[driveNum] = state;
565 // Memory mapped I/O functions
568 The DSK format is a byte-for-byte image of a 16-sector Apple II floppy disk: 35 tracks of 16
569 sectors of 256 bytes each, making 143,360 bytes in total. The PO format is exactly the same
570 size as DSK and is also organized as 35 sequential tracks, but the sectors within each track
571 are in a different sequence. The NIB format is a nybblized format: a more direct representation
572 of the disk's data as encoded by the Apple II floppy drive hardware. NIB contains 35 tracks of
573 6656 bytes each, for a total size of 232,960 bytes. Although this format is much larger, it is
574 also more versatile and can represent the older 13-sector disks, many copy-protected disks, and
575 other unusual encodings.
578 void FloppyDrive::ControlStepper(uint8_t addr)
582 What I can gather here:
583 bits 1-2 are the "phase" of the track (which is 1/4 of a full track (?))
584 bit 0 is the "do something" bit.
588 uint8_t newPhase = (addr >> 1) & 0x03;
589 //WriteLog("*** Stepper change [%u]: track = %u, phase = %u, newPhase = %u\n", addr, track, phase, newPhase);
591 if (((phase + 1) & 0x03) == newPhase)
592 phase += (phase < 79 ? 1 : 0);
594 if (((phase - 1) & 0x03) == newPhase)
595 phase -= (phase > 0 ? 1 : 0);
599 track = ((phase >> 1) < 35 ? phase >> 1 : 34);
602 //WriteLog(" track = %u, phase = %u, newPhase = %u\n", track, phase, newPhase);
603 SpawnMessage("Stepping to track %u...", track);
606 // return something if read mode...
609 void FloppyDrive::ControlMotor(uint8_t addr)
615 void FloppyDrive::DriveEnable(uint8_t addr)
621 uint8_t FloppyDrive::ReadWrite(void)
623 SpawnMessage("%u:%sing %s track %u, sector %u...", activeDrive,
624 (ioMode == IO_MODE_READ ? "Read" : "Write"),
625 (ioMode == IO_MODE_READ ? "from" : "to"), track, currentPos / 396);
628 I think what happens here is that once a track is read its nybblized form
629 is fed through here, one byte at a time--which means for DO disks, we have
630 to convert the actual 256 byte sector to a 416 byte nybblized data "sector".
633 if (ioMode == IO_MODE_WRITE && (latchValue & 0x80))
635 // Does it behave like this?
636 #warning "Write protection kludged in--investigate real behavior!"
637 if (!writeProtected[activeDrive])
639 nybblizedImage[activeDrive][(track * 6656) + currentPos] = latchValue;
640 imageDirty[activeDrive] = true;
643 //doesn't seem to do anything
644 return 0;//is this more like it?
647 uint8_t diskByte = nybblizedImage[activeDrive][(track * 6656) + currentPos];
648 currentPos = (currentPos + 1) % 6656;
650 //WriteLog("FL: diskByte=%02X, currentPos=%u\n", diskByte, currentPos);
654 uint8_t FloppyDrive::GetLatchValue(void)
660 void FloppyDrive::SetLatchValue(uint8_t value)
666 void FloppyDrive::SetReadMode(void)
669 ioMode = IO_MODE_READ;
672 void FloppyDrive::SetWriteMode(void)
675 ioMode = IO_MODE_WRITE;
679 PRODOS 8 MLI ERROR CODES
682 $01: Bad system call number
683 $04: Bad system call parameter count
684 $25: Interrupt table full
686 $28: No device connected
687 $2B: Disk write protected
689 $40: Invalid pathname
690 $42: Maximum number of files open
691 $43: Invalid reference number
692 $44: Directory not found
693 $45: Volume not found
695 $47: Duplicate filename
697 $49: Volume directory full
698 $4A: Incompatible file format, also a ProDOS directory
699 $4B: Unsupported storage_type
700 $4C: End of file encountered
701 $4D: Position out of range
702 $4E: File access error, also file locked
704 $51: Directory structure damaged
705 $52: Not a ProDOS volume
706 $53: Invalid system call parameter
707 $55: Volume Control Block table full
708 $56: Bad buffer address
709 $57: Duplicate volume
710 $5A: File structure damaged