├── 5150CAXX.C ├── 5150CAXX.H ├── BINSTUB ├── BUILD.BAT ├── CASSETTE.BIN └── STUB.ASM ├── BUILD.BAT ├── EXAMPLE └── EXAMPLE.ASM └── README.md /5150CAXX.C: -------------------------------------------------------------------------------- 1 | /* 5150CAXX (c) J. Bogin 2 | Compile using BUILD.BAT */ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "5150caxx.h" 12 | 13 | int main(int argc, char* argv[]) 14 | { 15 | PrintSplash(); 16 | CheckCurrentSetup(); 17 | PrintMaximumSizeLimit(); 18 | ParseCommandLine(argc, argv); 19 | DoOperation(); 20 | 21 | return 0; 22 | } -------------------------------------------------------------------------------- /5150CAXX.H: -------------------------------------------------------------------------------- 1 | /* Tape data buffers - far used defaultly or near if there's not enough memory */ 2 | unsigned char* pNearTapeBuffer = NULL; 3 | unsigned char far* pFarTapeBuffer = NULL; 4 | unsigned int nTapeBufferSegment = 0; 5 | unsigned int nMaximumTapeDataSize = 0xffff; /* 64 K */ 6 | 7 | /* To detect if we're on an IBM PC or Jr */ 8 | unsigned char far* pEquipmentByte = (unsigned char far*)MK_FP(0xf000, 0xfffe); 9 | 10 | /* Used if tweaking recording speed. 11 | Bit1 usually requires to be within 25% deviation (750-1250Hz) 12 | for the BIOS "leader finding" code to work properly. 13 | */ 14 | unsigned char far* pFarBiosBuffer = NULL; /* 8K BIOS buffer, used to shadow INT 15h */ 15 | unsigned int far* pINT15PatchSeg = (unsigned int far*)MK_FP(0, 0x56); /* INT 15h vector segment */ 16 | unsigned int far* pINT15PatchOfs = (unsigned int far*)MK_FP(0, 0x54); /* INT 15h vector offset */ 17 | unsigned int nINT15SegOrig = 0xF000; /* On IBM PC/jr, INT 15h points to F000:F859 by default */ 18 | unsigned int nINT15OfsOrig = 0xF859; /* This can be overriden by DOS! */ 19 | 20 | unsigned int nBit0FrequencyHz = 2000; /* Bit 0 is a 0.5 ms (2000Hz) pulse by default */ 21 | unsigned int nBit1FrequencyHz = 1000; /* Bit 1 is a 1 ms (1000Hz) pulse by default */ 22 | const unsigned long nPITFrequencyHz = 1193180; /* PIT runs on 1.19318MHz off a divider */ 23 | 24 | /* Files related */ 25 | FILE* pFile = NULL; 26 | const char* pFileName = NULL; 27 | unsigned char cFileNameOnly[9] = {0}; 28 | unsigned char cFileExtOnly[5] = {0}; 29 | 30 | /* Options parsed from the command line */ 31 | unsigned char nWaitForPressPlay = 0; 32 | unsigned char cOperation = 0; 33 | unsigned char nCreateBASICRecord = 0; 34 | unsigned long nWantedBytes = 0; 35 | unsigned int nSpecifiedBytes = 0; 36 | unsigned char nTargetIsMemorySegment = 0; 37 | 38 | /* 244-byte BASIC stub */ 39 | unsigned char sBASICStub[244] = 40 | { 41 | 0xF5, 0x0C, 0x01, 0x00, 0x92, 0x2C, 0x1C, 0x00, 0x20, 0x3A, 0x20, 0x97, 42 | 0x20, 0x53, 0x45, 0x47, 0xE7, 0x0C, 0xAD, 0xDE, 0x3A, 0x20, 0x82, 0x20, 43 | 0x49, 0xE7, 0x11, 0x20, 0xCC, 0x20, 0x0F, 0x1F, 0x3A, 0x20, 0x87, 0x20, 44 | 0x4A, 0x3A, 0x20, 0x98, 0x20, 0x49, 0x2C, 0x4A, 0x3A, 0x20, 0x83, 0x20, 45 | 0x49, 0x3A, 0x20, 0x49, 0xE7, 0x11, 0x3A, 0x20, 0xB3, 0x20, 0x49, 0x00, 46 | 0x32, 0x0D, 0x02, 0x00, 0x84, 0x20, 0x26, 0x48, 0x42, 0x39, 0x2C, 0x26, 47 | 0x48, 0x44, 0x45, 0x2C, 0x26, 0x48, 0x41, 0x44, 0x2C, 0x26, 0x48, 0x42, 48 | 0x38, 0x2C, 0x26, 0x48, 0x44, 0x45, 0x2C, 0x26, 0x48, 0x41, 0x44, 0x2C, 49 | 0x26, 0x48, 0x42, 0x42, 0x2C, 0x26, 0x48, 0x44, 0x45, 0x2C, 0x26, 0x48, 50 | 0x41, 0x44, 0x2C, 0x26, 0x48, 0x38, 0x45, 0x2C, 0x26, 0x48, 0x44, 0x38, 51 | 0x00, 0x74, 0x0D, 0x03, 0x00, 0x84, 0x20, 0x26, 0x48, 0x38, 0x45, 0x2C, 52 | 0x26, 0x48, 0x43, 0x30, 0x2C, 0x26, 0x48, 0x38, 0x39, 0x2C, 0x26, 0x48, 53 | 0x43, 0x32, 0x2C, 0x26, 0x48, 0x38, 0x33, 0x2C, 0x26, 0x48, 0x45, 0x41, 54 | 0x2C, 0x26, 0x48, 0x33, 0x30, 0x2C, 0x26, 0x48, 0x38, 0x45, 0x2C, 0x26, 55 | 0x48, 0x44, 0x32, 0x2C, 0x26, 0x48, 0x42, 0x43, 0x2C, 0x26, 0x48, 0x30, 56 | 0x30, 0x2C, 0x26, 0x48, 0x30, 0x32, 0x00, 0xA7, 0x0D, 0x04, 0x00, 0x84, 57 | 0x20, 0x26, 0x48, 0x35, 0x30, 0x2C, 0x26, 0x48, 0x35, 0x33, 0x2C, 0x26, 58 | 0x48, 0x42, 0x34, 0x2C, 0x26, 0x48, 0x30, 0x32, 0x2C, 0x26, 0x48, 0x46, 59 | 0x41, 0x2C, 0x26, 0x48, 0x43, 0x44, 0x2C, 0x26, 0x48, 0x31, 0x35, 0x2C, 60 | 0x26, 0x48, 0x46, 0x42, 0x2C, 0x26, 0x48, 0x43, 0x42, 0x00, 0xAD, 0x0D, 61 | 0x05, 0x00, 0x81, 0x00 62 | }; 63 | 64 | 65 | /* Relay goes click... */ 66 | unsigned char TapeMotorOn() 67 | { 68 | union REGS regs; 69 | regs.x.ax = 0; 70 | int86(0x15, ®s, ®s); 71 | 72 | return regs.h.ah; 73 | } 74 | 75 | /* ... and clack */ 76 | void TapeMotorOff() 77 | { 78 | union REGS regs; 79 | regs.x.ax = 0x100; 80 | int86(0x15, ®s, ®s); 81 | } 82 | 83 | /* Read from tape to memory */ 84 | unsigned char ReadTape(unsigned int nBytesToRead, unsigned int* nBytesRead) 85 | { 86 | union REGS regs; 87 | struct SREGS sregs; 88 | 89 | regs.x.ax = 0x200; 90 | regs.x.cx = nBytesToRead; 91 | sregs.es = nTapeBufferSegment; 92 | regs.x.bx = 0; 93 | 94 | /* Disable hardware IRQ ISRs during tape operation */ 95 | asm cli 96 | int86x(0x15, ®s, ®s, &sregs); 97 | asm sti 98 | 99 | /* Number of successful bytes read is in DX */ 100 | *nBytesRead = regs.x.dx; 101 | 102 | return regs.h.ah; 103 | } 104 | 105 | /* Write from memory to tape */ 106 | unsigned char WriteTape(unsigned int nBytesToWrite, unsigned int* nBytesWritten) 107 | { 108 | union REGS regs; 109 | struct SREGS sregs; 110 | 111 | regs.x.ax = 0x300; 112 | regs.x.cx = nBytesToWrite; 113 | sregs.es = nTapeBufferSegment; 114 | regs.x.bx = 0; 115 | 116 | /* Disable hardware IRQ ISRs during tape operation */ 117 | asm cli 118 | int86x(0x15, ®s, ®s, &sregs); 119 | asm sti 120 | 121 | /* Offset of the last byte written in BX */ 122 | *nBytesWritten = regs.x.bx; 123 | 124 | return regs.h.ah; 125 | } 126 | 127 | /* Write from memory to tape and include a ROM-BASIC loader stub */ 128 | unsigned char WriteTapeWithBasicStub(unsigned int nBytesToWrite, unsigned int* nBytesWritten, 129 | unsigned int nSegment, unsigned int nOffset) 130 | { 131 | union REGS regs; 132 | struct SREGS sregs; 133 | unsigned char cBuf[17] = {0}; 134 | 135 | /* Prepare the BASIC header */ 136 | cBuf[0] = 0xA5; 137 | memset(&cBuf[1], 0x20, 8); /* name padded with spaces */ 138 | memcpy(&cBuf[1], cFileNameOnly, strlen(cFileNameOnly)); 139 | cBuf[9] = 0x80; /* tokenized BASIC */ 140 | cBuf[0xA] = sizeof(sBASICStub); /* 244 */ 141 | cBuf[0xC] = 0x60; /* BASIC loader will be at 0060:081E, same as IBM Advanced Diagnostics */ 142 | cBuf[0xE] = 0x1E; 143 | cBuf[0xF] = 0x08; 144 | 145 | /* Write the BASIC header */ 146 | regs.x.ax = 0x300; 147 | regs.x.cx = sizeof(cBuf); 148 | sregs.es = FP_SEG(cBuf); 149 | regs.x.bx = FP_OFF(cBuf); 150 | asm cli 151 | int86x(0x15, ®s, ®s, &sregs); 152 | asm sti 153 | 154 | /* Prepare the BASIC loader */ 155 | memset(cBuf, 0, 16); 156 | 157 | /* Binary loading location: nSegment:nOffset. nOffset equals 0x100 if .COM extension, otherwise 0. */ 158 | /* 2 bytes: set DEF SEG for a machine code stub loader, to load the rest of the tape, at nSegment-0x10 */ 159 | /* Stack segment is set inside to nSegment-0x30:0x200, i.e. right below said stub. */ 160 | { 161 | unsigned int* setDefSeg = (unsigned int*)&(sBASICStub[0x12]); 162 | *setDefSeg = nSegment - 0x10; 163 | } 164 | 165 | /* Ugly tokenized BASIC PEEK-POKE machine code follows. First, DATA &H(low byte), then &H(high byte) */ 166 | sprintf(cBuf, "&H%02X", (unsigned char)nBytesToWrite); /* MOV CX,nBytesToWrite. (CX for INT15h, AH=02) */ 167 | memcpy(&(sBASICStub[0x47]), cBuf, 4); 168 | sprintf(cBuf, "&H%02X", (unsigned char)(nBytesToWrite >> 8)); 169 | memcpy(&(sBASICStub[0x4C]), cBuf, 4); 170 | sprintf(cBuf, "&H%02X", (unsigned char)nSegment); /* MOV AX,nSegment. (Binary loading segment.) */ 171 | memcpy(&(sBASICStub[0x56]), cBuf, 4); 172 | sprintf(cBuf, "&H%02X", (unsigned char)(nSegment >> 8)); 173 | memcpy(&(sBASICStub[0x5B]), cBuf, 4); 174 | sprintf(cBuf, "&H%02X", (unsigned char)nOffset); /* MOV BX,nOffset. (Binary loading offset.) */ 175 | memcpy(&(sBASICStub[0x65]), cBuf, 4); 176 | sprintf(cBuf, "&H%02X", (unsigned char)(nOffset >> 8)); 177 | memcpy(&(sBASICStub[0x6A]), cBuf, 4); 178 | 179 | /* Write the BASIC stub */ 180 | regs.x.ax = 0x300; 181 | regs.x.cx = sizeof(sBASICStub); 182 | sregs.es = FP_SEG(sBASICStub); 183 | regs.x.bx = FP_OFF(sBASICStub); 184 | asm cli 185 | int86x(0x15, ®s, ®s, &sregs); 186 | asm sti 187 | 188 | /* And finally write the program binary */ 189 | return WriteTape(nBytesToWrite, nBytesWritten); 190 | } 191 | 192 | /* Dump tape buffer to screen */ 193 | void DumpMemory(unsigned int nLength) 194 | { 195 | unsigned int nOffset = 0; 196 | 197 | for (; nOffset < nLength; nOffset++) 198 | { 199 | unsigned char nByte = *((unsigned char far*)(MK_FP(nTapeBufferSegment, nOffset))); 200 | 201 | /* Skip ASCII BELL (control code 7) to stop the machine from honking */ 202 | if (nByte == 7) 203 | { 204 | nByte = 0; 205 | } 206 | 207 | putchar(nByte); 208 | } 209 | } 210 | 211 | /* PUSHA, CALL FAR tape buffer, POPA... 8088 and inline assembly "friendly" :) */ 212 | void CallMemory() 213 | { 214 | /* Store flags, DS, ES, BP, SI, DI, AX to DX (no PUSHA on 8088) */ 215 | asm pushf 216 | asm push ds 217 | asm push es 218 | asm push bp 219 | asm push si 220 | asm push di 221 | asm push ax 222 | asm push bx 223 | asm push cx 224 | asm push dx 225 | 226 | /* Set DS and ES to be the same as CS of tape buffer (will be CS=DS=ES) */ 227 | _BX = nTapeBufferSegment; 228 | _DS = _BX; 229 | _ES = _BX; 230 | 231 | /* BX = tape buffer offset (always 0!) */ 232 | _BX = 0; 233 | 234 | /* Get current instruction pointer address... (by doing a near CALL +0) */ 235 | asm db 0e8h, 0, 0 236 | 237 | /* AX = IP from stack */ 238 | asm pop ax 239 | /* 1 byte */ 240 | 241 | /* Push return address to stack (CS:IP+9 bytes to the end of function - hack) */ 242 | asm push cs 243 | asm add ax,9 244 | asm push ax 245 | /* 5 bytes */ 246 | 247 | /* Push segment and offset (0) of tape buffer */ 248 | asm push ds 249 | asm push bx 250 | /* 2 bytes */ 251 | 252 | /* Far call */ 253 | asm retf 254 | /* 1 byte */ 255 | 256 | /* It shall return here... or should, at least :) */ 257 | dummy_return_label: 258 | 259 | /* Restore registers */ 260 | asm pop dx 261 | asm pop cx 262 | asm pop bx 263 | asm pop ax 264 | asm pop di 265 | asm pop si 266 | asm pop bp 267 | asm pop es 268 | asm pop ds 269 | asm popf 270 | } 271 | 272 | /* Terminate with exit code, do cleanup beforehand */ 273 | void Quit(int nStatus) 274 | { 275 | if (pFile) 276 | { 277 | fclose(pFile); 278 | 279 | /* If there was a fatal error and we wrote a temporary file to disk, erase it */ 280 | if ((cOperation == 'R') && (nStatus == EXIT_FAILURE)) 281 | { 282 | remove(pFileName); 283 | } 284 | } 285 | 286 | /* Deallocate memory used with disk I/O */ 287 | if (pNearTapeBuffer) 288 | { 289 | free(pNearTapeBuffer); 290 | } 291 | else if (pFarTapeBuffer) 292 | { 293 | farfree(pFarTapeBuffer); 294 | } 295 | 296 | /* Did we tweak the recording speed? */ 297 | if (pFarBiosBuffer) 298 | { 299 | /* Restore original INT 15h vector */ 300 | *pINT15PatchSeg = nINT15SegOrig; 301 | *pINT15PatchOfs = nINT15OfsOrig; 302 | 303 | farfree(pFarBiosBuffer); 304 | } 305 | 306 | /* Make sure the tape motor is off before passing control */ 307 | TapeMotorOff(); 308 | exit(nStatus); 309 | } 310 | 311 | /* Clear the screen, set 80x25 video mode and print the first line */ 312 | void PrintSplash() 313 | { 314 | asm mov ax,3 315 | asm int 10h 316 | 317 | printf("5150CAXX - DOS cassette tape interface for the IBM PC, (c) J. Bogin\n"); 318 | } 319 | 320 | /* If executed with /A, wait for confirmation from the user (press PLAY or RECORD+PLAY) */ 321 | void WaitForKeypress() 322 | { 323 | if (nWaitForPressPlay) 324 | { 325 | printf("Press %sPLAY on tape and then press a key to continue...", 326 | (cOperation == 'W') ? "RECORD+" : ""); 327 | 328 | asm xor ax,ax 329 | asm int 16h 330 | } 331 | } 332 | 333 | void DelLine() 334 | { 335 | int index = 0; 336 | printf("\r"); 337 | 338 | for (; index < 79; index++) 339 | { 340 | putchar(' '); 341 | } 342 | 343 | printf("\r"); 344 | } 345 | 346 | /* Determine maximum free memory */ 347 | void PrintMaximumSizeLimit() 348 | { 349 | unsigned char nAllocateNearBuffer = 0; 350 | unsigned long nAddress = 0; 351 | 352 | /* Allocate 64K RAM (+ some reserve for the zero offset alignment)... */ 353 | unsigned long nAllocSize = (unsigned long)nMaximumTapeDataSize + 16; 354 | pFarTapeBuffer = farmalloc(nAllocSize); 355 | 356 | /* Not enough memory? */ 357 | if (!pFarTapeBuffer) 358 | { 359 | /* Get free memory (minus some reserve for the standard library) for both heaps */ 360 | const unsigned int nNearFree = coreleft(); 361 | const unsigned long nFarFree = farcoreleft(); 362 | unsigned long nFree = 0; 363 | const unsigned int nReserve = 3*1024; 364 | 365 | /* Which one is greater? */ 366 | if ((unsigned long)nNearFree < nFarFree) 367 | { 368 | nFree = nFarFree; 369 | } 370 | else 371 | { 372 | nFree = nNearFree; 373 | nAllocateNearBuffer = 1; 374 | } 375 | 376 | /* Subtract the reserve */ 377 | if (nFree <= nReserve) 378 | { 379 | _nomem: 380 | printf("\nNot enough memory to run the application.\n"); 381 | Quit(EXIT_FAILURE); 382 | } 383 | 384 | /* Try again, with reduced maximum size */ 385 | nAllocSize = nFree - nReserve; 386 | 387 | /* near or far... */ 388 | if (!nAllocateNearBuffer) 389 | { 390 | pFarTapeBuffer = farmalloc(nAllocSize); 391 | if (!pFarTapeBuffer) 392 | { 393 | asm jmp _nomem 394 | } 395 | } 396 | else 397 | { 398 | pNearTapeBuffer = malloc((unsigned int)nAllocSize); 399 | if (!pNearTapeBuffer) 400 | { 401 | asm jmp _nomem 402 | } 403 | } 404 | } 405 | 406 | /* Success. Determine address of heap buffer in memory */ 407 | if (!nAllocateNearBuffer) 408 | { 409 | nAddress = (FP_SEG(pFarTapeBuffer) * (unsigned long)0x10) + FP_OFF(pFarTapeBuffer); 410 | } 411 | else 412 | { 413 | nAddress = (_DS * (unsigned long)0x10) + (unsigned int)(pNearTapeBuffer); 414 | } 415 | 416 | /* Not aligned to offset 0? (most of the time) */ 417 | if (nAddress % 0x10) 418 | { 419 | /* Align to 0 and subtract the offset difference from the maximum available size */ 420 | unsigned long nAlignedAddress; 421 | nTapeBufferSegment = (unsigned int)((nAddress / 0x10) + 1); 422 | nAlignedAddress = nTapeBufferSegment * (unsigned long)0x10; 423 | 424 | nAllocSize -= labs(nAlignedAddress - nAddress); 425 | } 426 | 427 | /* Aligned perfectly, use as is */ 428 | else 429 | { 430 | nTapeBufferSegment = (unsigned int)(nAddress / 0x10); /* set segment */ 431 | } 432 | 433 | /* Round to 64K and set maximum data size */ 434 | if (nAllocSize > 0xffff) 435 | { 436 | nAllocSize = 0xffff; 437 | } 438 | nMaximumTapeDataSize = (unsigned int)nAllocSize; 439 | 440 | /* Clear the buffer */ 441 | _fmemset(MK_FP(nTapeBufferSegment, 0), 0, nMaximumTapeDataSize); 442 | 443 | printf("\nMaximum data size: "); 444 | if (nMaximumTapeDataSize == 0xffff) 445 | { 446 | printf("64K\n"); 447 | } 448 | 449 | else 450 | { 451 | printf("%u bytes (64K, if given existing RAM segment)\n", nMaximumTapeDataSize); 452 | } 453 | } 454 | 455 | /* Will fail to run on a non IBM-5150 or PCjr, or without a proper 4.77MHz 8088 */ 456 | void CheckCurrentSetup() 457 | { 458 | /* Error messages */ 459 | const char sPCRequired[] = "\nAn original IBM 5150 or PCjr is required to run this application.\n"; 460 | const char s8088Required[] = "\nA genuine Intel 8088 CPU is required to run this application.\n"; 461 | 462 | /* Must be an IBM PC or PCjr */ 463 | if ((*pEquipmentByte != 0xff) && (*pEquipmentByte != 0xfd)) 464 | { 465 | printf(sPCRequired); 466 | Quit(EXIT_FAILURE); 467 | } 468 | 469 | /* Detect 8088 */ 470 | asm push sp 471 | asm pop ax 472 | 473 | /* The 8088 pushes the incremented value of SP on stack */ 474 | if (_SP == _AX) 475 | { 476 | printf(s8088Required); 477 | Quit(EXIT_FAILURE); 478 | } 479 | 480 | /* Rule out NECs */ 481 | _AX = 0x100; 482 | asm aad 2 483 | 484 | /* AX shall be 2 on an Intel (NECs ignore the immediate and return 10) */ 485 | if (_AX != 2) 486 | { 487 | printf(s8088Required); 488 | Quit(EXIT_FAILURE); 489 | } 490 | 491 | /* We are on an IBM, increase text performance a little */ 492 | directvideo = 1; 493 | } 494 | 495 | /* Printed on incorrect or no command line arguments */ 496 | void PrintUsage() 497 | { 498 | /* /F argument (tweak baudrate) not allowed on PCjr due to 64k ROM BIOS */ 499 | unsigned char nAllowFArgument = (*pEquipmentByte != 0xfd); 500 | unsigned char cTabs[] = "\t\t "; 501 | unsigned char cFArgument[] = " [/F bit0 bit1] "; 502 | 503 | if (!nAllowFArgument) 504 | { 505 | strcpy(cTabs, " "); 506 | strcpy(cFArgument, " "); 507 | } 508 | 509 | /* Print out possible input arguments... */ 510 | printf("\n" 511 | "5150CAXX [/A]%s/R target [bytes]\n" 512 | "\t [/A]%s/D [bytes]\n" 513 | "\t [/A]%s/X [bytes]\n" 514 | "\t [/A]%s/W source [bytes]\n" 515 | "\t [/A]%s/WB source [bytes]\n" 516 | "where:\n" 517 | " /R - reads number of [bytes] from tape and saves them to target,\n" 518 | " /D - reads number of [bytes] from tape and dumps them to screen as raw ASCII,\n" 519 | " /X - reads number of [bytes] from tape and executes them as code at org 0,\n" 520 | " /W - records source up to [bytes] length to tape,\n" 521 | " /WB - records source up to [bytes] to tape as ROM-BASIC launchable binary,\n" 522 | " /A - optionally asks to press play on tape before the selected operation", 523 | cTabs, cTabs, cTabs, cFArgument, cFArgument); 524 | 525 | if (nAllowFArgument) 526 | { 527 | printf(",\n /F - sets bit0/bit1 freq. in Hz; tweaks recording speed. Default: 2000 1000.\n\n"); 528 | } 529 | else 530 | { 531 | printf(".\n\n"); 532 | } 533 | 534 | /* ... and the rest */ 535 | printf("Seek your tape to the desired point from which to begin the operation, first.\n" 536 | "The source and target can be a file name or a hex memory segment (eg. 0xF600).\n" 537 | "Passing a segment to /R can cause a crash if the memory is already occupied.\n" 538 | "/X will crash the system if illegal instructions are read from the tape.\n" 539 | "Using [bytes] is optional. In any case, the max size above cannot be exceeded.\n"); 540 | } 541 | 542 | /* Tweak the recording speed by "shadowing" ROM-BIOS and patching the timers */ 543 | void SetRecordingSpeed() 544 | { 545 | /* Stuff we'll use */ 546 | unsigned int nSegment = 0; 547 | unsigned long nAddress = 0; 548 | unsigned int far* pPatchBit0Div = NULL; 549 | unsigned int far* pPatchBit1Div = NULL; 550 | 551 | /* Won't work on a PCjr, because we'd have to shadow its 64K BIOS in addition to having our 64K tape data buffer */ 552 | /* And, we are using a "small" memory model here */ 553 | if (*pEquipmentByte == 0xfd) 554 | { 555 | printf("\nCannot shadow PCjr BIOS to set custom speeds. Using default.\n"); 556 | return; 557 | } 558 | 559 | /* Allocate an 8K buffer for BIOS + 16-byte overhead for offset alignment at 0xE000 */ 560 | pFarBiosBuffer = farmalloc(8192 + 16); 561 | if (pFarBiosBuffer) 562 | { 563 | nAddress = (FP_SEG(pFarBiosBuffer) * (unsigned long)0x10) + FP_OFF(pFarBiosBuffer); 564 | } 565 | 566 | /* Allocation failed, or we won't be able to align the starting offset for some weird reason */ 567 | if (nAddress < 0xe000) 568 | { 569 | if (pFarBiosBuffer) 570 | { 571 | farfree(pFarBiosBuffer); 572 | pFarBiosBuffer = NULL; 573 | } 574 | 575 | /* Continue, just warn */ 576 | printf("\nNot enough memory to shadow PC BIOS to set custom speeds. Using default.\n"); 577 | return; 578 | } 579 | 580 | /* Shadowed BIOS code needs to start at offset 0xE000 exactly - perform alignment */ 581 | nSegment = (nAddress - 0xE000) / 0x10; /* 16-byte leeway, ah well */ 582 | 583 | /* Copy the ROM BIOS into our RAM so we can do patching */ 584 | _fmemcpy(MK_FP(nSegment, 0xE000), MK_FP(0xF000, 0xE000), 8192); 585 | 586 | /* Look where to patch: the bit0 frequency divisor is 592, for bit1 it is 1184. 587 | PC BIOS assumes a 1184000 dividend instead of the more proper 1193180, which is closer to the real PIT freq. 588 | Actual frequency in Hz = PIT freq. (1193180) Hz / divisor 589 | The lower the frequency, the longer the time period for a bit, and thus, the recording speed is slower. 590 | */ 591 | 592 | /* IBM 5150 - 2 possible memory locations, depending on BIOS */ 593 | /* bit0 freq divisor is at address 0xFA2D or 0xFA34 */ 594 | pPatchBit0Div = (unsigned int far*)MK_FP(nSegment, 0xFA2D); 595 | if (*pPatchBit0Div != 592) 596 | { 597 | pPatchBit0Div = (unsigned int far*)MK_FP(nSegment, 0xFA34); 598 | } 599 | 600 | /* bit1 freq divisor address at 0xFA28 or 0xFA2F */ 601 | pPatchBit1Div = (unsigned int far*)MK_FP(nSegment, 0xFA28); 602 | if (*pPatchBit1Div != 1184) 603 | { 604 | pPatchBit1Div = (unsigned int far*)MK_FP(nSegment, 0xFA2F); 605 | } 606 | 607 | /* Sanity check */ 608 | if ((*pPatchBit0Div != 592) || (*pPatchBit1Div != 1184)) 609 | { 610 | printf("\nNon-standard BIOS detected, cannot set custom speeds.\n"); 611 | farfree(pFarBiosBuffer); 612 | pFarBiosBuffer = NULL; 613 | return; 614 | } 615 | 616 | /* Now patch the 2 divisors according to the chosen frequencies. */ 617 | printf("\nUsing custom recording speed: bit0 frequency %u Hz, bit1 frequency %u Hz.\n", 618 | nBit0FrequencyHz, nBit1FrequencyHz); 619 | 620 | *pPatchBit0Div = nPITFrequencyHz / nBit0FrequencyHz; 621 | *pPatchBit1Div = nPITFrequencyHz / nBit1FrequencyHz; 622 | 623 | /* Store the old INT 15h pointer and set the new vector to use our shadow-patch. And we're done! */ 624 | nINT15SegOrig = *pINT15PatchSeg; 625 | nINT15OfsOrig = *pINT15PatchOfs; 626 | *pINT15PatchSeg = nSegment; 627 | *pINT15PatchOfs = 0xF859; /* orig. IBM BIOS offset */ 628 | } 629 | 630 | void ParseCommandLine(int argc, char* argv[]) 631 | { 632 | int indexArgs = 0; 633 | int indexSec = 0; 634 | 635 | /* Display usage information */ 636 | if ( (argc < 2) || (argc > 8) ) 637 | { 638 | PrintUsage(); 639 | Quit(EXIT_SUCCESS); 640 | } 641 | 642 | /* Ugly parsing follows */ 643 | for (indexArgs = 1; indexArgs < argc; indexArgs++) 644 | { 645 | char* pArgument = argv[indexArgs]; 646 | nWantedBytes = strtoul(pArgument, NULL, 10); 647 | 648 | if (stricmp(pArgument, "/A") == 0) 649 | { 650 | nWaitForPressPlay = 1; 651 | } 652 | else if ((stricmp(pArgument, "/F") == 0) && (argc > indexArgs+2)) 653 | { 654 | nBit0FrequencyHz = strtoul(argv[indexArgs+1], NULL, 10); 655 | nBit1FrequencyHz = strtoul(argv[indexArgs+2], NULL, 10); 656 | 657 | /* 600-6000 Hz */ 658 | if ((nBit0FrequencyHz < 600) || 659 | (nBit1FrequencyHz < 600) || 660 | (nBit0FrequencyHz > 6000) || 661 | (nBit1FrequencyHz > 6000)) 662 | { 663 | printf("\nBit frequencies must be between 600 to 6000 Hz.\n"); 664 | Quit(EXIT_FAILURE); 665 | } 666 | 667 | /* Filler so this does not get mistaken for "wanted bytes" if at the end */ 668 | argv[indexArgs+1][0] = 'A'; 669 | argv[indexArgs+2][0] = 'B'; 670 | } 671 | else if ( pFileName && (indexArgs == argc-1) && (nWantedBytes > 0) ) 672 | { 673 | for (indexSec = 0; indexSec < strlen(pArgument); indexSec++) 674 | { 675 | if (isdigit(pArgument[indexSec]) == 0) 676 | { 677 | nWantedBytes = 0; 678 | break; 679 | } 680 | } 681 | } 682 | else if ( (argc > indexArgs+1) && (stricmp(pArgument, "/R") == 0) ) 683 | { 684 | cOperation = 'R'; 685 | pFileName = argv[indexArgs+1]; 686 | } 687 | else if ( (argc > indexArgs+1) && (stricmp(pArgument, "/W") == 0) ) 688 | { 689 | cOperation = 'W'; 690 | pFileName = argv[indexArgs+1]; 691 | } 692 | else if ( (argc > indexArgs+1) && (stricmp(pArgument, "/WB") == 0) ) 693 | { 694 | cOperation = 'W'; 695 | pFileName = argv[indexArgs+1]; 696 | nCreateBASICRecord = 1; 697 | } 698 | else if (stricmp(pArgument, "/D") == 0) 699 | { 700 | cOperation = 'D'; 701 | } 702 | else if (stricmp(pArgument, "/X") == 0) 703 | { 704 | cOperation = 'X'; 705 | } 706 | } 707 | 708 | /* Is the target not a filename, but a memory segment? */ 709 | nTargetIsMemorySegment = pFileName && (strnicmp(pFileName, "0x", 2) == 0); 710 | if (nTargetIsMemorySegment) 711 | { 712 | unsigned long nCmdLine = strtoul(pFileName, NULL, 16); 713 | 714 | /* Sanity check */ 715 | if ((nCmdLine > 0xffff) || ((nCmdLine == 0) && 716 | (strcmp(pFileName, "0") != 0) && 717 | (strcmp(pFileName, "0x0000") != 0))) 718 | { 719 | printf("\nInvalid memory segment specified\n"); 720 | Quit(EXIT_FAILURE); 721 | } 722 | 723 | /* Deallocate memory reserved file usage */ 724 | if (pNearTapeBuffer) 725 | { 726 | free(pNearTapeBuffer); 727 | pNearTapeBuffer = NULL; 728 | } 729 | else if (pFarTapeBuffer) 730 | { 731 | farfree(pFarTapeBuffer); 732 | pFarTapeBuffer = NULL; 733 | } 734 | 735 | /* Set the int 15h buffer segment manually */ 736 | nTapeBufferSegment = (unsigned int)nCmdLine; 737 | nMaximumTapeDataSize = 0xffff; /* full segments are 64K max. */ 738 | 739 | /* Correct maximum data size if going past the 1MB boundary */ 740 | nCmdLine *= 0x10; 741 | if (nCmdLine+nMaximumTapeDataSize > 0xfffff) 742 | { 743 | nMaximumTapeDataSize = (unsigned int)(0x100000 - nCmdLine); 744 | printf("Given RAM segment.\nReducing 64K maximum to %u bytes, for it not overlap the 1MB boundary.\n", nMaximumTapeDataSize); 745 | } 746 | 747 | /* Used with /WB */ 748 | sprintf(cFileNameOnly, "0x%04X", nTapeBufferSegment); 749 | } 750 | else if (pFileName) 751 | { 752 | /* Obtain file name without ext or path */ 753 | _splitpath(pFileName, NULL, NULL, cFileNameOnly, cFileExtOnly); 754 | 755 | /* Determine extension */ 756 | if (nCreateBASICRecord && (strlen(cFileExtOnly) > 0) 757 | && (stricmp(cFileExtOnly, ".com") != 0) 758 | && (stricmp(cFileExtOnly, ".bin") != 0)) 759 | { 760 | printf("\nA .COM or other <=64K binary with no DOS function calls is required.\n"); 761 | Quit(EXIT_FAILURE); 762 | } 763 | } 764 | 765 | /* Size to read or write manually specified on commandline. Check if it is 0 < arg < 64K */ 766 | if (nWantedBytes > 0) 767 | { 768 | if (nWantedBytes <= nMaximumTapeDataSize) 769 | { 770 | nSpecifiedBytes = (unsigned int)nWantedBytes; 771 | } 772 | else 773 | { 774 | printf("\nSpecified data size of %lu bytes exceeds memory/segment limit\n", nWantedBytes); 775 | Quit(EXIT_FAILURE); 776 | } 777 | } 778 | 779 | /* Tweak recording speed */ 780 | if ((cOperation == 'W') && ((nBit0FrequencyHz != 2000) || (nBit1FrequencyHz != 1000))) 781 | { 782 | SetRecordingSpeed(); 783 | } 784 | 785 | /* Still unknown operation */ 786 | if (cOperation == 0) 787 | { 788 | PrintUsage(); 789 | Quit(EXIT_SUCCESS); 790 | } 791 | } 792 | 793 | /* Check result of INT 15h, allow continue on non-fatal errors */ 794 | void CheckForErrors(unsigned char nResult) 795 | { 796 | DelLine(); 797 | 798 | switch(nResult) 799 | { 800 | case 0: 801 | printf("Success!"); 802 | break; 803 | case 1: 804 | printf("Reached a CRC error (or end of a valid data stream) here. Stop."); 805 | break; 806 | case 2: 807 | printf("Reached bad tape signal (or end of a valid data stream) here. Stop."); 808 | break; 809 | case 4: 810 | printf("No data leader on tape, or no valid tape present.\n"); 811 | Quit(EXIT_FAILURE); 812 | default: 813 | break; 814 | } 815 | } 816 | 817 | /* Main funtionality */ 818 | void DoOperation() 819 | { 820 | unsigned int nSuccessfulBytes = 0; 821 | unsigned char nMainOperationResult = 0; 822 | long nFileSize = 0; 823 | unsigned char cBuffer[1024] = {0}; 824 | 825 | /* Will be reading or writing a file ? Open it in the correct mode */ 826 | if (!nTargetIsMemorySegment && ((cOperation == 'R') || (cOperation == 'W'))) 827 | { 828 | pFile = fopen(pFileName, (cOperation == 'R') ? "wb" : "rb"); 829 | if (!pFile) 830 | { 831 | printf("\nCannot open file \"%s\" for %s\n", pFileName, (cOperation == 'R') ? "writing" : "reading"); 832 | Quit(EXIT_FAILURE); 833 | } 834 | } 835 | 836 | /* No manually specified number of bytes to read/write (or invalid) */ 837 | if (nSpecifiedBytes == 0) 838 | { 839 | nSpecifiedBytes = nMaximumTapeDataSize; 840 | } 841 | 842 | /* If we're going to write to the tape, inform about the boundary */ 843 | if (pFile && (cOperation == 'W')) 844 | { 845 | fseek(pFile, 0, SEEK_END); 846 | nFileSize = ftell(pFile); 847 | rewind(pFile); 848 | 849 | if (nFileSize == 0) 850 | { 851 | printf("\nThe file is empty\n"); 852 | Quit(EXIT_FAILURE); 853 | } 854 | else if (nFileSize > nSpecifiedBytes) 855 | { 856 | printf("\nReading the first %u bytes of the file only.", nSpecifiedBytes); 857 | } 858 | else 859 | { 860 | nSpecifiedBytes = (unsigned int)nFileSize; 861 | } 862 | } 863 | 864 | /* Form the message */ 865 | printf((cOperation == 'W') ? "\nWill record " : "\nWill load "); 866 | printf("%u bytes ", nSpecifiedBytes); 867 | printf((cOperation == 'W') ? "to tape" : "from tape"); 868 | 869 | switch(cOperation) 870 | { 871 | case 'R': 872 | if (!nTargetIsMemorySegment) 873 | { 874 | printf(" to file %s.\n\n", pFileName); 875 | } 876 | else 877 | { 878 | printf(" to memory segment 0x%04X.\n\n", nTapeBufferSegment); 879 | } 880 | break; 881 | case 'W': 882 | if (!nTargetIsMemorySegment) 883 | { 884 | printf(" from file %s.\n\n", pFileName); 885 | } 886 | else 887 | { 888 | printf(" from memory segment 0x%04X.\n\n", nTapeBufferSegment); 889 | } 890 | break; 891 | case 'D': 892 | printf(", and display them as ASCII.\n\n"); 893 | break; 894 | case 'X': 895 | printf(", and execute them as machine code.\n\n"); 896 | } 897 | 898 | /* Warn about possible extra dummy data at the end of the file */ 899 | if ((cOperation != 'W') && (nWantedBytes == 0)) 900 | { 901 | printf("Warning: no data length specified. Will read until end-of-tape or data error.\n" 902 | "There might be some extra data padded at the end of the buffer.\n\n"); 903 | } 904 | 905 | /* Read file/memory segment and write to tape */ 906 | if (cOperation == 'W') 907 | { 908 | unsigned int nOffset = 0; 909 | unsigned int nBinaryLoadingOfs = (stricmp(cFileExtOnly, ".com") == 0) ? 0x100 : 0; 910 | 911 | if (nTargetIsMemorySegment) 912 | { 913 | asm jmp _skipread 914 | } 915 | 916 | printf("Reading file..."); 917 | 918 | /* In 1K increments (no far fread) */ 919 | while (nOffset != nSpecifiedBytes) 920 | { 921 | const int nSize = ((nSpecifiedBytes-nOffset) < 1024) ? nSpecifiedBytes-nOffset : 1024; 922 | size_t nResult = fread(cBuffer, sizeof(unsigned char), nSize, pFile); 923 | 924 | if (nResult < nSize) 925 | { 926 | DelLine(); 927 | printf("File read error: Could not read %s from the disk.\n", pFileName); 928 | Quit(EXIT_FAILURE); 929 | } 930 | 931 | _fmemcpy(MK_FP(nTapeBufferSegment, nOffset), cBuffer, nSize); 932 | nOffset += nSize; 933 | } 934 | 935 | DelLine(); 936 | _skipread: 937 | WaitForKeypress(); 938 | DelLine(); 939 | 940 | printf("Recording tape..."); 941 | TapeMotorOn(); 942 | 943 | if (!nCreateBASICRecord) 944 | { 945 | nMainOperationResult = WriteTape(nSpecifiedBytes, &nSuccessfulBytes); 946 | } 947 | else 948 | { 949 | /* Will be read to 0300:0000 (BIN) or 0300:0100 (.COM) */ 950 | nMainOperationResult = WriteTapeWithBasicStub(nSpecifiedBytes, &nSuccessfulBytes, 0x300, nBinaryLoadingOfs); 951 | } 952 | 953 | TapeMotorOff(); 954 | CheckForErrors(nMainOperationResult); 955 | 956 | printf("\n%u out of %u bytes recorded successfully.\n" 957 | "Be sure you note down this value, and also your current tape position.\n", 958 | nSuccessfulBytes, nSpecifiedBytes); 959 | 960 | if (nCreateBASICRecord) 961 | { 962 | printf("\nTo run in ROM-BASIC, execute following: LOAD \"%s\",R" 963 | "\nLoading this under DOS Disk BASIC/BASICA.COM will crash the system.\n", cFileNameOnly); 964 | } 965 | 966 | Quit(EXIT_SUCCESS); 967 | } 968 | 969 | /* Read from tape and write to file/memory segment/do an ASCII dump/whatever else is handled here */ 970 | WaitForKeypress(); 971 | DelLine(); 972 | 973 | printf("Loading tape..."); 974 | TapeMotorOn(); 975 | 976 | nMainOperationResult = ReadTape(nSpecifiedBytes, &nSuccessfulBytes); 977 | TapeMotorOff(); 978 | CheckForErrors(nMainOperationResult); 979 | 980 | printf("\n%u out of %u bytes read successfully.\n", nSuccessfulBytes, nSpecifiedBytes); 981 | 982 | /* Quit with an error */ 983 | if (nSuccessfulBytes == 0) 984 | { 985 | Quit(EXIT_FAILURE); 986 | } 987 | 988 | /* Do nothing more if the target is a custom memory segment */ 989 | if (nTargetIsMemorySegment) 990 | { 991 | printf("\nTape buffer stored at address 0x%04X:0.\n", nTapeBufferSegment); 992 | Quit(EXIT_SUCCESS); 993 | } 994 | 995 | /* Now decide what to do with the buffer */ 996 | switch(cOperation) 997 | { 998 | 999 | case 'R': 1000 | { 1001 | unsigned int nOffset = 0; 1002 | printf("\nWriting file..."); 1003 | 1004 | /* in 1K increments from the far memory (no far fwrite) */ 1005 | while (nOffset != nSuccessfulBytes) 1006 | { 1007 | const int nSize = ((nSuccessfulBytes-nOffset) < 1024) ? nSuccessfulBytes-nOffset : 1024; 1008 | 1009 | _fmemcpy(cBuffer, MK_FP(nTapeBufferSegment, nOffset), nSize); 1010 | 1011 | if (fwrite(cBuffer, sizeof(unsigned char), nSize, pFile) < nSize) 1012 | { 1013 | DelLine(); 1014 | printf("File write error: Could not write %s to the disk.\n", pFileName); 1015 | Quit(EXIT_FAILURE); 1016 | } 1017 | 1018 | nOffset += nSize; 1019 | } 1020 | 1021 | printf(" done!"); 1022 | } 1023 | break; 1024 | 1025 | case 'D': 1026 | { 1027 | printf("\nASCII dump of the memory buffer contents:\n\n"); 1028 | DumpMemory(nSuccessfulBytes); 1029 | } 1030 | break; 1031 | 1032 | case 'X': 1033 | { 1034 | printf("\nInvoking CALL FAR to the memory buffer contents...\n\n"); 1035 | CallMemory(); 1036 | } 1037 | } 1038 | 1039 | printf("\n"); 1040 | Quit(EXIT_SUCCESS); 1041 | } -------------------------------------------------------------------------------- /BINSTUB/BUILD.BAT: -------------------------------------------------------------------------------- 1 | @echo off 2 | if not exist cassette.bin goto end 3 | fasm stub.asm 4 | if not exist stub.com goto end 5 | if exist program.com del program.com 6 | copy /b stub.com+cassette.bin program.com 7 | del stub.com 8 | :end -------------------------------------------------------------------------------- /BINSTUB/CASSETTE.BIN: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/climatex/5150CAXX/36f83a9c1c14a009274058c665584043c32bbc1e/BINSTUB/CASSETTE.BIN -------------------------------------------------------------------------------- /BINSTUB/STUB.ASM: -------------------------------------------------------------------------------- 1 | ; Compile with FASM 2 | 3 | use16 4 | org 100h 5 | 6 | xor dx,dx 7 | mov ax,program_start 8 | mov bx,10h 9 | div bx 10 | 11 | mov bx,cs 12 | add bx,ax 13 | mov ds,bx 14 | mov es,bx 15 | 16 | push cs 17 | push word return 18 | push ds 19 | push word 0 20 | retf 21 | 22 | return: 23 | mov ax,cs 24 | mov ds,ax 25 | mov es,ax 26 | int 20h 27 | 28 | times 48d-($-$$) db 90h 29 | program_start: 30 | -------------------------------------------------------------------------------- /BUILD.BAT: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM *** Assumes DOS Turbo C v3 in C:\TC *** 4 | 5 | set TC=C:\TC 6 | 7 | if exist 5150caxx.exe del 5150caxx.exe 8 | del *.obj >nul 9 | 10 | %TC%\BIN\TCC -I%TC%\INCLUDE -c -f- -1- -ms -K 5150CAXX.C 11 | %TC%\BIN\TLINK /L%TC%\LIB /x C0FS 5150CAXX.OBJ,5150CAXX.EXE,,CS 12 | del *.obj >nul -------------------------------------------------------------------------------- /EXAMPLE/EXAMPLE.ASM: -------------------------------------------------------------------------------- 1 | ; 5150CAXX, /X command line option binary example 2 | ; Assemble with FASM or similar 3 | 4 | use16 ; 16-bit binary 5 | org 0 ; 5150CAXX.EXE allocates a new 64K segment with data starting at offset 0 6 | 7 | jmp start 8 | 9 | message db 'Hello world!',13,10,'$' 10 | 11 | start: 12 | xor ax,ax ;Set 40x25 13 | int 10h 14 | mov ah,9 ;Print the string 15 | lea dx,[message] 16 | int 21h 17 | xor ax,ax ;Wait for a keypress 18 | int 16h 19 | mov ax,0003 ;Set 80x25 again 20 | int 10h 21 | retf ;FAR return to parent that called it (5150CAXX in this case) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 5150CAXX 2 | DOS cassette tape interface for IBM PC. Check out my [website](http://boginjr.com/it/sw/dev/5150caxx/) for more info! 3 | 4 | --------------------------------------------------------------------------------