├── LICENSE ├── README.md ├── c64convert.c └── C64Tape.ino /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C64Tape 2 | 3 | Homebrew device for loading .tap files from an SD card on a real Commodore 64. It connects to the C64's cassette port and acts like a Commodore Datasette (1530). Instead of real tapes though you can load compressed tap files (tape images used by emulators) off the SD card. 4 | 5 | Video of it in action is here: 6 | https://www.youtube.com/watch?v=QHEHTlryBxw 7 | 8 | Components 9 | - Teensy 3.2 (Massively overkill, any reasonably quick Arduino compatible would do) 10 | - Microchip 23LC1024 128Kb Serial SRAM 11 | - SD card module from eBay (Only used as it was easy to solder but also provides 5V conversion if needed) 12 | - Couple of 10KOhm resistors 13 | - Push Button 14 | 15 | Schematic in ASCII art at top of Arduino source. 16 | 17 | Tap files are approximately 8 times larger than the data they represent so tend to be pretty large. Using a simple LZ77 like compressor I got approximately a 10 to 1 reduction in file size. This combined with reloading new data whenever there's a long pause (>1s) means that we can get away with a relatively small amount of SRAM (128Kb) even for tap files several megabytes long. c64compress.c needs to be used to compress the files before you put them on the SD card to allow C64Tape to read them. 18 | 19 | User interface: Short button presses switch between different tap files on the SD card. A long press plays the tape. 20 | -------------------------------------------------------------------------------- /c64convert.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define WINDOW_SIZE 1024 6 | 7 | typedef signed char s8; 8 | typedef unsigned char u8; 9 | typedef signed short s16; 10 | typedef unsigned short u16; 11 | 12 | u8* LZLikeDecode(u8 *out, u8 *data, u8 *end) 13 | { 14 | u8 window[WINDOW_SIZE]; 15 | u16 head=0; 16 | while (data>6)<<8))+WINDOW_SIZE; 28 | a&=0x3F; 29 | for (int i=0; i<=a; i++) 30 | { 31 | u8 v=window[(idx+i)&(WINDOW_SIZE-1)]; 32 | window[(head++)&(WINDOW_SIZE-1)]=v; 33 | *(out++)=v; 34 | } 35 | } 36 | } 37 | return out; 38 | } 39 | 40 | u8* LZLikeEncode(u8 *out, u8 *data, u8 *end) 41 | { 42 | int eidx=(int)(end-data); 43 | int start=0; 44 | while (start=0) 52 | { 53 | int len=0; 54 | while (data[k]==data[k-1-o] && len<64 && kbestLen) 60 | { 61 | bestLen=len; 62 | bestOffset=o; 63 | } 64 | } 65 | } 66 | *(out++)=(bestLen-1)|((bestOffset>>8)<<6); 67 | *(out++)=bestOffset; 68 | start+=bestLen; 69 | } 70 | return out; 71 | } 72 | 73 | int main(int argc, char **argv) 74 | { 75 | if (argc!=3) 76 | { 77 | printf("Usage: c64convert source.tap dest.tpz\n"); 78 | return 1; 79 | } 80 | FILE *f=fopen(argv[1], "r"); 81 | if (f) 82 | { 83 | fseek(f, 0, SEEK_END); 84 | long size=ftell(f); 85 | printf("Loaded %s (%d bytes)\n", argv[1], (int)size); 86 | fseek(f, 0, SEEK_SET); 87 | u8 *data=(u8*)malloc(size); 88 | fread(data, 1, size, f); 89 | fclose(f); 90 | 91 | printf("Read done\n"); 92 | u8 *out=(u8*)malloc(2*size); // worst case 93 | u8 *eout=LZLikeEncode(out, data, data+size); 94 | printf("Compressed:%d/%d\n", (int)(eout-out), (int)size); 95 | 96 | FILE *g=fopen(argv[2], "w"); 97 | if (g) 98 | { 99 | fwrite(out, 1, eout-out, g); 100 | fclose(g); 101 | } 102 | else 103 | { 104 | printf("Couldn't open '%s' for write\n", argv[2]); 105 | return 1; 106 | } 107 | 108 | u8 *dest=(u8*)malloc(size); 109 | u8 *edest=LZLikeDecode(dest, out, eout); 110 | if (memcmp(dest, data, size)!=0) 111 | { 112 | printf("Decompress error %d/%d\n", (int)(edest-dest), (int)size); 113 | return 1; 114 | } 115 | } 116 | else 117 | { 118 | printf("Couldn't open '%s' for read\n", argv[1]); 119 | return 1; 120 | } 121 | return 0; 122 | } 123 | 124 | -------------------------------------------------------------------------------- /C64Tape.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // Schematic: 5 | // 6 | // +---------------------+ 7 | // --------| | | 8 | // | | |------------| | 9 | // 5V|---+----|Vin | | |-------------| |----------------| 10 | // | | | +--|5V | | | 11 | // READ|--------|19 11|------|MISO SRAM |--------|MISO | 12 | // | | 12|------|SOMI |--------|SOMI | 13 | // | | 13|------|SCK |--------|SCK | 14 | // WRITE|--------|18 9|------|CS | +-----|CS SD Card | 15 | // | | | +--|GND | | +---|3.3V | 16 | // SENSE|--------|16 | | |-------------| | | +-|GND | 17 | // | | | | | | | | | 18 | // MOTOR|---+ | Teensy 3.2 | --- | | | |----------------| 19 | // | | | | - | | | 20 | // | 10K | | | | | 21 | // C64 | | | | | | | 22 | // Tape | +----|21 2|-----------------------+ | | 23 | // Port | | | | | | 24 | // | 10K | 3.3V|-------------------------+ | 25 | // | | | | | 26 | // GND|---+----|GND 7|-----BUTTON----+ | 27 | // | | |------------| | | 28 | // --------| | | | 29 | // +---------------------------------+-----------+ 30 | // | 31 | // --- 32 | // - 33 | // 34 | 35 | //#define LOGGING 36 | 37 | #define DATA_OUT 19 // READ 38 | #define DATA_IN 18 // WRITE 39 | #define MOTOR_IN 21 40 | #define SENSE_OUT 16 41 | #define RAM_CS 9 42 | #define BUTTON 7 43 | #define SD_CS 2 44 | 45 | #define WINDOW_SIZE 1024 46 | 47 | #define WRSR 1 48 | #define WRITE 2 49 | #define READ 3 50 | 51 | #define LONG_PRESS 500 52 | #define SHORT_PRESS 20 53 | 54 | #define s8 signed char 55 | #define s16 signed short 56 | #define s32 signed int 57 | #define u8 unsigned char 58 | #define u16 unsigned short 59 | #define u32 unsigned int 60 | 61 | SPISettings RAMSPISettings(20000000, MSBFIRST, SPI_MODE0); 62 | 63 | typedef struct TapHdr_s 64 | { 65 | char sig[12]; 66 | u8 version; 67 | u8 dummy[3]; 68 | u8 size[4]; 69 | } TapHdr; 70 | 71 | //Header 72 | TapHdr hdr; 73 | 74 | //LZ stuff 75 | u8 window[WINDOW_SIZE]; 76 | u16 head; 77 | u16 offset; 78 | u8 length; 79 | 80 | File tape; 81 | u32 filePos; 82 | bool bDoneLoad; 83 | 84 | void LZLikeReset() 85 | { 86 | head=0; 87 | length=0; 88 | filePos=0; 89 | } 90 | 91 | u8 LZLikeDecode() 92 | { 93 | if (length==0) 94 | { 95 | u8 a=SPI.transfer(0); 96 | u8 b=SPI.transfer(0); 97 | filePos+=2; 98 | if (a==0) 99 | { 100 | window[(head++)&(WINDOW_SIZE-1)]=b; 101 | return b; 102 | } 103 | else 104 | { 105 | offset=head-1-(b+((a>>6)<<8))+WINDOW_SIZE; 106 | length=(a&0x3F)+1; 107 | offset+=length-1; 108 | } 109 | } 110 | length--; 111 | u8 v=window[(offset-length)&(WINDOW_SIZE-1)]; 112 | window[(head++)&(WINDOW_SIZE-1)]=v; 113 | return v; 114 | } 115 | 116 | void ReadToRAM(u32 fileOffset) 117 | { 118 | u8 buf[1024]; 119 | u32 address=0; 120 | tape.seek(fileOffset); 121 | while (tape.available() && address<128*1024) 122 | { 123 | for (u32 i=0; i>16)&0xFF); // Top address 129 | SPI.transfer((address>>8)&0xFF); // Mid address 130 | SPI.transfer(address&0xFF); // Low address 131 | for (u32 i=0; i=1000000) // If a second gap or more and not fully loaded to memory yet then load fresh chunk from SD 193 | ReadMore(); 194 | } 195 | while (micros()1) 215 | { 216 | return 2; 217 | } 218 | return 0; 219 | } 220 | 221 | bool isLongButtonPress() 222 | { 223 | while (true) 224 | { 225 | while (digitalRead(BUTTON)==HIGH); 226 | u32 startTime=millis(); 227 | while (millis()-startTime=LONG_PRESS) 229 | return true; 230 | if (millis()-startTime>SHORT_PRESS) 231 | return false; 232 | } 233 | } 234 | 235 | void LoadTape() 236 | { 237 | SPI.beginTransaction(RAMSPISettings); 238 | digitalWrite(RAM_CS, LOW); 239 | SPI.transfer(READ); 240 | SPI.transfer(0); // Top address 241 | SPI.transfer(0); // Mid address 242 | SPI.transfer(0); // Low address 243 | if (LoadAndCheckHeader()==0) 244 | { 245 | if (isLongButtonPress()) 246 | { 247 | #ifdef LOGGING 248 | Serial.println("Playing tape"); 249 | #endif 250 | digitalWrite(SENSE_OUT, LOW); 251 | PlayTape(); 252 | digitalWrite(SENSE_OUT, HIGH); 253 | } 254 | } 255 | digitalWrite(RAM_CS, HIGH); 256 | SPI.endTransaction(); 257 | } 258 | 259 | void LoadToRAM() 260 | { 261 | while (true) 262 | { 263 | File dir = SD.open("/"); 264 | while (true) 265 | { 266 | tape = dir.openNextFile(); 267 | if (!tape) 268 | break; 269 | if (tape.isDirectory()) 270 | continue; 271 | #ifdef LOGGING 272 | Serial.print(tape.name()); 273 | Serial.println(": About to read"); 274 | #endif 275 | ReadToRAM(0); 276 | LoadTape(); 277 | tape.close(); 278 | } 279 | dir.close(); 280 | } 281 | } 282 | 283 | void InitialiseRAM() 284 | { 285 | SPI.beginTransaction(RAMSPISettings); 286 | digitalWrite(RAM_CS, LOW); 287 | SPI.transfer(WRSR); // Write status 288 | SPI.transfer(0x40); // Sequential mode 289 | digitalWrite(RAM_CS, HIGH); 290 | SPI.endTransaction(); 291 | } 292 | 293 | void setup() 294 | { 295 | pinMode(DATA_OUT, OUTPUT); 296 | pinMode(DATA_IN, INPUT); 297 | pinMode(MOTOR_IN, INPUT); 298 | pinMode(SENSE_OUT, OUTPUT); 299 | pinMode(RAM_CS, OUTPUT); 300 | pinMode(SD_CS, OUTPUT); 301 | pinMode(BUTTON, INPUT_PULLUP); 302 | digitalWrite(SENSE_OUT, HIGH); 303 | digitalWrite(DATA_OUT, LOW); 304 | digitalWrite(RAM_CS, HIGH); 305 | digitalWrite(SD_CS, HIGH); 306 | #ifdef LOGGING 307 | Serial.begin(9600); 308 | while (!Serial); // Uncomment to debug 309 | #endif 310 | SPI.begin(); 311 | SD.begin(SD_CS); 312 | InitialiseRAM(); 313 | LoadToRAM(); 314 | } 315 | 316 | void loop() 317 | { 318 | // Do nothing 319 | } 320 | --------------------------------------------------------------------------------