├── .gitignore ├── LICENSE.txt ├── README.md ├── cbmloader.js ├── commodore_sketch ├── Pengo.prg ├── atomic.h ├── cbmdefines.h ├── commodore_sketch.ino ├── epyxfastload.S ├── epyxfastload.h ├── iec_driver.cpp ├── iec_driver.h ├── interface.cpp ├── interface.h ├── log.h ├── promicro │ ├── epyxfastload.S │ └── epyxfastload.h └── uno │ ├── epyxfastload.S │ └── epyxfastload.h ├── d64driver.js ├── docs ├── app-instructions.png ├── app-start-screen.png ├── arduino-uno-to-6-pin-male-din.png ├── components.png ├── pro-micro-to-6-pin-male-din.png ├── pro-micro-usb-beetle-to-6-pin-male-din.png └── pro-micro-usb-beetle-with-case.png ├── index.html ├── index.js ├── prgdriver.js ├── resources ├── favicon.ico ├── files-available.png ├── files-empty.png ├── info_c64.js ├── info_vic20.js ├── logo_c64.png ├── logo_home.png ├── logo_vic20.png ├── search-web.png ├── usb-connected.png ├── usb-disconnected.png └── usb-handshake-ok.png ├── styles.css └── t64driver.js /.gitignore: -------------------------------------------------------------------------------- 1 | resources/art -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Stephen Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commodore computer loader using standalone HTML/JavaScript and Arduino 2 | This project offers a simple and cost-effective way of loading software onto a Commodore 64 or Commodore Vic-20 retro-computer. It consists of two main components, an Arduino microcontroller which connects to the Commodore serial port, and local HTML/JavaScript programs which provide a front-end to select programs from. 3 | 4 | ![App start screen](./docs/app-start-screen.png) 5 | 6 | Main features 7 | - Load D64, PRG and T64 program files, including multi-loaders to your Commodore 8 | - Select programs using a simple GUI 9 | - Works well for many C64 games and Vic-20 games requiring memory expansion e.g. a 35K switchable memory expansion 10 | - Works with the C64 EPYX fast-load cartridge 11 | - Runs from local HTML/JavaScript files. No webserver is required 12 | 13 | ## Hardware Requirements 14 | - A web browser which supports the Web Serial API. Chrome, Edge and Opera running on a Windows 11 PC were used for testing 15 | - For connecting to the Commodore, a 5V 16Mhz Arduino / clone microcontroller is used. This may be an ATmega32U4 device (Micro, Pro-Micro and variations) or ATmega328P device (Uno, Nano) 16 | - The connections required are simple, go to the Hardware Interface section for details and diagrams 17 | 18 | ## Installation 19 | - Copy this project to a folder on a PC 20 | - Download and install the [Arduino IDE](https://www.arduino.cc/en/software) 21 | - Use the Arduino IDE to open the [sketch](./commodore_sketch/commodore_sketch.ino) 22 | - If using the C64 EPYX fast-load cartridge, additional steps are needed. Refer to that section for details 23 | - Select the Arduino board and port. Pro-micro clones are often mentioned as being Arduino Leonardo compliant. If yours is like this, choose Arduino Leonardo as your board 24 | - Compile and upload the Arduino sketch 25 | - Edit the settings section at the top of the [cbmloader.js](./cbmloader.js) program file with the Arduino pins and serial device filters used 26 | - Box art images are optional, but if used must be located in the `resources/art/c64` and `resources/art/vic20` subfolders and given the same name as their corresponding program with a `-image.jpg` extension, e.g. `Blue Max.d64` should have an associated image called `Blue Max-image.jpg` 27 | - Optional help information can be added into the `resources/info_c64.js` and `resources/info_vic20.js` files for each program as shown in the example below 28 | ``` 29 | const info_c64 = { 30 | ,"Alien 3.d64":"Joystick port 2" 31 | ,"Blue Max.d64":"Joystick port 2" 32 | ,"Boulder Dash.d64":"Joystick port 1" 33 | } 34 | ``` 35 | 36 | ## Usage 37 | - Ensure all the physical connections are made between the Commodore, Arduino and PC 38 | - Remember that the Commodore should always be turned off when physically connecting to it 39 | - Open `index.html` in Chrome, Edge or Opera 40 | - Click the `Open files` icon then navigate to a folder containing D64, PRG and T64 programs 41 | - To select all files, click on one of them then press `CTRL + A` together and then choose `Open` 42 | - Turn the Commodore on and click the `Connect` icon to select an Arduino device and connect to it 43 | - On the Commodore, issue the required `LOAD` command. See commands section below 44 | 45 | | Command | Description | 46 | | ------- | ----------- | 47 | | `LOAD "*",8` | Used to load the selected D64, PRG or T64 based program. The disk drive number is always 8 | 48 | | `LOAD "*",8,1` | This version of the load command is needed for many Vic-20 games and some C64 games. The program will be loaded at the memory location given in the first two bytes of a PRG file | 49 | | `LOAD "ABC*",8` | For D64, T64 programs, used to load a program starting with certain characters (e.g. `ABC`) | 50 | | `/*` | Shortcut for `LOAD "*",8,1` with C64 EPYX fast-load cartridge only | 51 | | `RUN` | Runs the program following `LOAD` above | 52 | | `LOAD "$",8` | Loads a directory listing of the contents of a D64 or T64 file | 53 | | `LIST` | Lists the directory contents after the above | 54 | | `$` | Shortcut for `LOAD "$",8` with C64 EPYX fast-load cartridge only | 55 | 56 | - The `Search web` icon searches for the selected program on the Internet, providing a shortcut to get more information about it 57 | - Directory listings showing the contents of `D64` and `T64` files are displayed by clicking on the associated art image. They are automatically displayed if no art exists for a program 58 | 59 | > C64 sample list of working games 60 | ``` 61 | 1942, Abbaye Des Morts, Arc of Yesod, Archon 1 and 2, Arkanoid, Armalyte, Barbarian 1 and 2, Beach Head 1 and 2, Biggles, Blue Max, Bomb Jack, Boulder Dash series, Bruce Lee, Bruce Lee Return of Fury, Bubble Bobble, Burnin Rubber, Cauldron, Cliff Hanger, Commando, Dig Dug, Dino Eggs, Donkey Kong Junior, Donkey Kong, Druid, Emlyn Hughes International Soccer, Empire Strikes Back, Fist 2, Fix-it Felix Jr, Football Manager, Fort Apocalypse 2, Galencia, Ghostbusters, Ghosts 'n Goblins, Green Beret, Heli Rescue, Impossible Mission 1 and 2, International Football, International Karate, International Karate Plus, Lady Pac, Last Ninja 1 and 2, Lock 'n' Chase, Lode Runner, Mayhem in Monsterland, Microprose Soccer, Millie and Molly, Moon Cresta, Outrun, Pacman Arcade, Pac-Man, Paperboy, Paradroid, Pitstop 1 and 2, Pooyan, Popeye, Raid Over Moscow, Rambo First Blood Part 2, Rampage, Rick Dangerous, Robin of the Wood, Rodland, R-Type, Spy Hunter, Spy vs Spy series, Super Mario Bros, Super Pipeline 1 and 2, The Great Giana Sisters, The Train Escape to Normandy, Turrican, Uridium, Volfied, Way of the Exploding Fist, Who Dares Wins 1 and 2, Wizard of Wor, Wizball, Yie Ar Kung-Fu 62 | ``` 63 | > Vic-20 sample list of working games (most with a memory expansion) 64 | ``` 65 | Adventure Land, AE, Alien Blitz, Amok, Arcadia, Astro Nell, Astroblitz, Atlantis, Attack of the Mutant Camels, Avenger, Bandits, Battlezone, Black Hole, Blitz, Buck Rogers, Capture the Flag, Cheese and Onion, Choplifter, Cosmic Cruncher, Creepy Corridors, Defender, Demon Attack, Donkey Kong, Dragonfire, Escape 2020, Final Orbit, Galaxian, Get More Diamonds, Gridrunner, Help Bodge, Hero, Jelly Monsters, Jetpac, Lala Prologue, Laser Zone, Lode Runner, Manic Miner, Metagalactic Llamas, Mickey the Bricky, Miner 2049er, Mission Impossible, Moon Patrol, Moons of Jupiter, Mosquito Infestation, Mountain King, Ms Pac-Man, Nibbler, Omega Race, Pac-Man, Pentagorat, Perils of Willy, Pharaoh's Curse, Pirate Cove, Polaris, Pool, Pumpkid, Radar Rat Race, Rigel Attack, Robotron, Robots Rumble, Rockman, Rodman, Sargon 2 Chess, Satellite Patrol, Satellites and Meteorites, Scorpion, Seafox, Serpentine, Shamus, Skramble, Skyblazer, Spider City, Spiders of Mars, Squish'em, Star Battle, Star Defence, Super Amok, Sword of Fargoal, Tenebra Macabre, TenTen, Tetris Deluxe, The Count, Traxx, Tutankham, Video Vermin, Voodoo Castle, Zombie Calavera 66 | ``` 67 | 68 | Note that load times in disk drive mode are not fast by modern standards, taking just over a minute for most C64 programs. If the C64 EPYX fast-load cartridge is used, loading takes around 4-5 seconds. 69 | 70 | As this is not a 'true' disk drive emulator, there are some related downsides and some things which have not been tested. 71 | - Some program files, typically for the C64, do not load because they require features of the actual disk drive hardware 72 | - Has not been tested with C64 fast-load cartridges other than EPYX fast-load 73 | - Has not been fully tested with programs that use two or more D64 files 74 | - Saving programs or handling disk operations e.g. renaming a file is not currently supported 75 | - This project has used a PAL C64 and Vic-20 for testing, so it's uncertain how this might work on NTSC machines 76 | 77 | A program title may have many different roms. If having a problem, try other versions instead, especially for a favourite game. 78 | 79 | ## Hardware Interface 80 | **Important!** There is potential for variation and errors, so **only proceed if you are content to take on all risks involved**. Some specific points to be aware of: 81 | - The connections to the Commodore and Arduino are essential to get right. Damage to the Commodore and Arduino may result if this isn't done 82 | - This project involves soldering the pin connections for the Commodore and Arduino. Ensure you're confident to do this and that connections will be secure and will not leave pins touching one another 83 | 84 | For connecting the Commodore and Arduino, a 6 pin male DIN connector and 5 wire cable (or jumper wires) are needed, both are cheap and readily available on eBay. Soldering wires onto the DIN is a bit tricky, so it's worth checking YouTube videos for tips. Alternatively, 6 pin DIN cables are available but are not that common. 85 | 86 | Any digital pins may be used. The ones you choose are defined in the settings section at the top of the cbmloader.js program file. The digital pins used in these diagrams provide a working guide. If using the C64 EPYX fast-load cartridge, refer to the section below before deciding on which pins to use. 87 | 88 | > The Pro-Micro needs a standard micro-USB to USB cable to connect to your computer 89 | ![Pro-Micro to 6 pin male din](./docs/pro-micro-to-6-pin-male-din.png) 90 | 91 | > The Pro-Micro USB Beetle needs a USB female to USB male cable to connect to your computer 92 | ![USB Beetle to 6 pin male din](./docs/pro-micro-usb-beetle-to-6-pin-male-din.png) 93 | 94 | > The Uno needs a USB B-type male to USB male cable to connect to your computer 95 | ![Uno to 6 pin male din](./docs/arduino-uno-to-6-pin-male-din.png) 96 | 97 | Having a reset button is useful for restarting the Arduino which is occasionally needed if a program fails in loading. The Arduino Micro and Uno already have one but many Arduino clones do not. Making one is easy, a momentary button needs to be connected between the reset pin (usually labelled RES or RST) and ground pin (GND). Alternatively, the Arduino needs to be re-plugged in to reset it. 98 | 99 | > A completed, working example using a Pro-Micro USB Beetle with a reset button mounted in a Lego case 100 | ![Completed USB Beetle example in Lego case](./docs/pro-micro-usb-beetle-with-case.png) 101 | 102 | ## C64 EPYX fast-load cartridge installation steps 103 | The C64 EPYX fast-load cartridge requires cycle-exact timing to work. For this, AVR assembler code is needed which may need minor amendment for the choice of input/output pins used on the Arduino. 104 | 105 | ### Arduino board pin to AVR chip pin mappings 106 | Arduino board pins are mapped to pins on the AVR chip. The AVR assembler code requires the AVR-chip pin mappings not the Arduino board pins, so a slight code change is needed to accommodate this. 107 | 108 | The board pin to AVR-chip pin mappings are found in files created when the Arduino IDE is installed, examples below for Windows Arduino IDE v2.3.2. 109 | 110 | `Leonardo / Pro-Micro`: C:\Users\xxxx\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.6\variants\leonardo\pins_arduino.h 111 | 112 | `Standard / Uno`: C:\Users\xxxx\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.6\variants\standard\pins_arduino.h 113 | 114 | In the `~\leonardo\pins_arduino.h` file for instance, digital pin 9 (D9) maps to AVR pin 5 on port B (PB5). The AVR pin input and output port names are also needed (PINB and PORTB for PB5). 115 | 116 | ### Determine the AVR-chip mapping for the Arduino pins being used 117 | The AVR-chip pin and port mapping depend on the choice of Arduino board pins. They do not depend on the type of Arduino, so although an Uno and Pro-Micro are mentioned in the examples below, it is just the Arduino board pin choice that matters. 118 | 119 | The simplest scenario is where all board pins share the same AVR-chip input and output port names, such as the example below used for the Arduino Uno in this case. 120 | 121 | | Pin | Description | 122 | | ------- | ----------- | 123 | | `Atn` | board pin 2 (D2) is PD2. The PD pins have an input port name PIND, output PORTD, mode (not used here) DDRD | 124 | | `Clock` | board pin 3 (D3) is PD3 | 125 | | `Data` | board pin 4 (D4) is PD4 | 126 | | `Reset` | board pin 5 (D5) is PD5 | 127 | 128 | A split port name example is below, used in this case for the Pro-Micro / Leonardo. 129 | 130 | | Pin | Description | 131 | | ------- | ----------- | 132 | | `Atn` | board pin 9 (D9) is PB5. The PB pins have an input port name PINB, output PORTB, mode (not used here) DDRB | 133 | | `Clock` | board pin 18 (D20) is PF7. The PF pins have an input port name PINF, output PORTF, mode (not used here) DDRF | 134 | | `Data` | board pin 19 (D20) is PF6 | 135 | | `Reset` | board pin 20 (D20) is PF5 | 136 | 137 | Note. Data and clock pins must be on the same input/output port. If they are not, different board pins should be chosen. 138 | 139 | ### Apply the AVR chip mappings to the epyxfastload files 140 | From the above, find the scenario which best matches your pin selection, create a sketch subfolder and copy existing `epyxfastload.h` and `epyxfastload.cpp` files into it. The `epyxfastload.h` and `epyxfastload.cpp` will come from the `uno` subfolder (example where all pins share the same AVR chip input and output port names) or `promicro` subfolder (example where port names are split). 141 | 142 | Amend the `epyxfastload.h` with the pin choices. The `epyxfastload.cpp` may need amendment to change the port labels (to avoid confusion), for example IEC_INPUT_B, IEC_OUTPUT_B may become IEC_INPUT_D, IEC_OUTPUT_D where port D is being used instead of B. A global change/replace in `epyxfastload.h` and `epyxfastload.cpp` will easily do this. 143 | 144 | Copy both files into the main sketch folder and compile the sketch. 145 | 146 | ## Authors and Acknowledgement 147 | The information and code shared by the following developers and sources is gratefully acknowledged: 148 | - [New 1541 emulator for arduino via desktop computer: uno2iec - Commodore 64 (C64) Forum (lemon64.com)](https://www.lemon64.com/forum/viewtopic.php?t=48771&start=0&sid=667319bb48acd56b1d4e0c2296145a84), developer Lars Wadefalk 149 | - [SD2IEC project](https://www.sd2iec.de/), for EPYX fast-load cartridge additions 150 | - [How Does Epyx Fastload Make Loading Faster on a Commodore 64?](https://www.youtube.com/watch?v=pUjOLLvnhjE), YouTube video by Commodore History 151 | 152 | ## License 153 | [The software is provided under the terms of its MIT license](./LICENSE.txt) 154 | 155 | ## Project Status 156 | This project is considered working for disk / serial operations loading Commodore D64, PRG and T64 based programs. 157 | Development is ongoing for bug fixes and enhancements. 158 | -------------------------------------------------------------------------------- /cbmloader.js: -------------------------------------------------------------------------------- 1 | // Settings: Amend pins and device filters below as required 2 | 3 | const ATN_CLOCK_DATA_RESET_PINS = "9|18|19|20"; //Arduino Pro Micro 4 | //const ATN_CLOCK_DATA_RESET_PINS = "2|3|4|5"; //Arduino Uno 5 | const SERIAL_DEVICE_FILTERS = [ 6 | { usbVendorId: 0x2341, usbProductId: 0x8036 }, //Arduino Pro Micro 7 | { usbVendorId: 0x2341, usbProductId: 0x0001 }, //Arduino Uno 8 | { usbVendorId: 0x2341, usbProductId: 0x0043 } //Arduino Uno (another) 9 | ]; 10 | 11 | // End of settings 12 | 13 | const BUFFER_SIZE = 256; 14 | const MAX_BYTES_PER_REQUEST = 254; 15 | 16 | const HANDSHAKE_READY = "\r"; 17 | const HANDSHAKE_SEND = ""; 18 | const HANDSHAKE_OK = "\r"; 19 | 20 | const OPEN_CMD = 79; //'O' 21 | const READ_CMD = 82; //'R' 22 | const CLOSE_CMD = 67; //'C' 23 | const DIR_CMD = 76; //'L' 24 | const WRITE_FULL_CMD = 87; //'W' 25 | const WRITE_PART_CMD = 119; //'w' 26 | const DEBUG_CMD = 68; //'D' 27 | 28 | const OPEN_FNF = 88; //'X' 29 | 30 | const BUF_NORMAL = 66; //'B' 31 | const BUF_END = 98; //'b' 32 | 33 | const DIR_NORMAL = 76; //'L' 34 | const DIR_END = 108; //'l' 35 | 36 | const REVERSE_CHAR = 18; //Commodore reverse text colour code 37 | const SPACE_CHAR = 32; //' ' 38 | const QUOTE_CHAR = 34; //'"' 39 | const NON_SPACE_CHAR = 160; //' ' 40 | 41 | const FILE_TYPES = [[68, 69, 76],[83, 69, 81],[80, 82, 71],[85, 83, 82],[82, 69, 76]]; //"DEL", "SEQ", "PRG", "USR", "REL" 42 | 43 | class jsCBMLoader { 44 | constructor(baudRate) { 45 | 46 | if (!navigator.serial) { 47 | alert("WebSerial is not enabled in this browser"); 48 | return false; 49 | } 50 | 51 | this.baud = baudRate; 52 | 53 | //Setup event listener if caller passes in a message and handler 54 | this.on = (message, handler) => { 55 | parent.addEventListener(message, handler); 56 | }; 57 | 58 | //Setup serial connect, disconnect event handlers 59 | navigator.serial.addEventListener("connect", this.serialConnect); 60 | navigator.serial.addEventListener("disconnect", this.serialDisconnect); 61 | } 62 | 63 | //Open serial port using Arduino filters 64 | async openPort() { 65 | 66 | try { 67 | this.port = await navigator.serial.requestPort({ SERIAL_DEVICE_FILTERS }); //Open pop-up selection window showing available ports 68 | await this.port.open({ baudRate: this.baud, BUFFER_SIZE }); 69 | this.serialReadPromise = this.receiveArduino().catch(err => { 70 | console.error("Error on serial port:", err); 71 | }); 72 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "SERCON", msg: "Serial device connected"} })); 73 | 74 | } catch (err) { 75 | console.error("Error opening serial port:", err); 76 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "SERERR", msg: "Error opening serial port"} })); 77 | } 78 | 79 | } //openPort 80 | 81 | //Close serial port 82 | async closePort() { 83 | 84 | if (this.port) { 85 | this.reader.cancel(); //Stop the reader 86 | await this.serialReadPromise; //Wait for the receiveArduino function to stop 87 | await this.port.close(); 88 | this.port = null; 89 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "SERDIS", msg: "Serial device disconnected"} })); 90 | } 91 | 92 | } //closePort 93 | 94 | //Assign program to load 95 | async setDriverForFile(file, testMode) { 96 | 97 | const extension = file.name.split('.').pop().toUpperCase(); 98 | switch(extension) { 99 | case "D64": 100 | this.driver = new d64driver(file); 101 | await this.driver.readBinaryFile(); 102 | await this.driver.buildDirectory(); 103 | break; 104 | 105 | case "PRG": 106 | this.driver = new prgdriver(file); 107 | await this.driver.readBinaryFile(); 108 | break; 109 | 110 | case "T64": 111 | this.driver = new t64driver(file); 112 | await this.driver.readBinaryFile(); 113 | await this.driver.buildDirectory(); 114 | break; 115 | 116 | default: 117 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "DRVUNS", msg: "No driver found for selected program"} })); 118 | } 119 | 120 | //Update load progress 121 | parent.dispatchEvent(new CustomEvent('progress', { detail: { progress: "0%"} })); 122 | 123 | //Return directory listing 124 | if (this.driver) { 125 | let htmlDirList = ""; 126 | do { 127 | let { protocol, payload } = await this.driver.getDirectoryLine(); 128 | const fileBlocks = payload[0] | payload[1] << 8; 129 | htmlDirList += String(fileBlocks) + " " + (String.fromCharCode(...payload.slice(2))).replace(String.fromCharCode(REVERSE_CHAR),"").replace(/[^A-Z0-9 !"#%&'()+-/@*[\]:;=<>,.?]/g, "-") + "
"; 130 | if (testMode) { 131 | console.log(payload); 132 | } 133 | if (protocol[0] != DIR_NORMAL) { 134 | break; 135 | } 136 | } while (true); 137 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "DIRLIST", msg: htmlDirList} })); 138 | } 139 | 140 | //In test mode, return program data payloads 141 | if (testMode && this.driver) { 142 | 143 | const delay = millis => new Promise((resolve, reject) => { 144 | setTimeout(_ => resolve(), millis); 145 | }); 146 | 147 | const messagebuf = [42]; //* wildcard character 148 | if (await this.driver.openProgram(messagebuf)) { 149 | this.programSize = this.driver.subProgSize; 150 | this.bytesReceived = 0; 151 | do { 152 | let { protocol, payload } = await this.driver.getBuffer(); 153 | 154 | this.bytesReceived += protocol[1]; 155 | var progress = Math.ceil((this.bytesReceived / this.programSize) * 100); 156 | parent.dispatchEvent(new CustomEvent('progress', { detail: { progress: progress } })); //Update load progress 157 | console.log(`READ_CMD: Sent ${this.bytesReceived} of ${this.programSize} bytes`); 158 | 159 | if (testMode) { 160 | console.log(payload); 161 | } 162 | await delay(10); 163 | 164 | if (protocol[0] != BUF_NORMAL) { 165 | break; 166 | } 167 | } while (true); 168 | } 169 | } 170 | 171 | } //setDriverForFile 172 | 173 | //Receive data from Arduino and perform actions based on the protocol indicators 174 | async receiveArduino() { 175 | 176 | let validArduinoConnection = false; 177 | let readbuf = new ArrayBuffer(BUFFER_SIZE); 178 | let messagebuf = new Array(); 179 | let msg = 0; 180 | let channel = 0; 181 | let bufLen = 0; 182 | let ok = true; 183 | 184 | while (this.port.readable) { 185 | 186 | this.reader = this.port.readable.getReader({ mode: "byob" }); //Initialize reader, mode is bring-your-own-buffer 187 | 188 | const { value, done } = await this.reader.read(new Uint8Array(readbuf)); //Read serial buffer 189 | if (value) { 190 | readbuf = value.buffer; //Reset the buffer 191 | messagebuf = messagebuf.concat(Array.from(value)); //append the reader data to the message buffer 192 | 193 | if (validArduinoConnection && this.driver) { 194 | 195 | //Process the instructions in the message buffer 196 | ok = true; 197 | while (messagebuf.length > 0 && ok) { 198 | 199 | //Check first byte for the action required 200 | switch (messagebuf[0]) { 201 | 202 | //Handle open command 203 | case OPEN_CMD: 204 | ok = false; //Assume the open message is incomplete 205 | if (messagebuf.length > 1) { 206 | bufLen = messagebuf[1]; 207 | if (messagebuf.length >= bufLen) { 208 | ok = true; //Open message is complete, continue 209 | channel = messagebuf[2]; 210 | msg = messagebuf.splice(0,3); //remove the command, message length, channel used above 211 | msg = messagebuf.splice(0, bufLen-3); //next are the open command bytes 212 | 213 | if (msg.length > 0) { 214 | 215 | //Check the channel is supported (open to read) 216 | if ([0,2,8].includes(channel)) { 217 | 218 | //Return first payload, either a directory line or buffer load of program bytes 219 | if (msg == 36) { //$ directory character 220 | console.log("DIR_CMD: Start directory listing"); 221 | await this.sendArduino(Uint8Array.from([DIR_CMD,0])); 222 | } 223 | else { 224 | console.log(`OPEN_CMD: Open file [${msg}]`); 225 | if (await this.driver.openProgram(msg)) { 226 | this.programSize = this.driver.subProgSize; 227 | this.bytesReceived = 0; 228 | 229 | var { protocol, payload } = await this.driver.getBuffer(); //Get program bytes payload 230 | await this.sendArduino(protocol).then(await this.sendArduino(payload)); 231 | 232 | this.bytesReceived += protocol[1]; 233 | var progress = Math.ceil((this.bytesReceived / this.programSize) * 100); 234 | parent.dispatchEvent(new CustomEvent('progress', { detail: { progress: progress } })); //Update load progress 235 | console.log(`READ_CMD: Sent ${this.bytesReceived} of ${this.programSize} bytes`); 236 | } 237 | else { 238 | console.log(`OPEN_FNF: Error opening ${msg}`); 239 | await this.sendArduino(Uint8Array.from([OPEN_FNF,1])); 240 | messagebuf = []; //Clear the message buffer 241 | } 242 | } 243 | 244 | } 245 | else { 246 | const cmdName = String.fromCharCode(...msg); 247 | if (channel == 1 || (channel == 15 && cmdName.substring(0, 2) == "S:")) { 248 | console.log(`Pseudo-supported channel [${channel}] with command [${msg}]`); 249 | await this.sendArduino(Uint8Array.from([WRITE_FULL_CMD])); //Acknowlege write instruction 250 | } 251 | else { 252 | if (cmdName == "M-R\xc4\xe5\x04" || cmdName == "M-R\xc6\xe5\x04") { 253 | //M-R for the drive number, see dos1541 rom $25c6 ($e5c4 - $c000 + 2) 254 | await this.sendArduino(Uint8Array.from([BUF_END,2,52,177])); //Bytes from $25c6. Used in Bruce Lee 2 255 | } 256 | else { 257 | console.log(`Unsupported channel [${channel}] with command [${msg}]`); 258 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "CHAUNS", msg: "Unsupported channel"} })); 259 | messagebuf = []; //Clear the message buffer 260 | } 261 | } 262 | 263 | } 264 | } 265 | else { 266 | console.log(`Unsupported channel [${channel}] with command [${msg}]`); 267 | await this.sendArduino(Uint8Array.from([OPEN_FNF,1])); 268 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "CHAUNS", msg: "Unsupported channel"} })); 269 | messagebuf = []; //Clear the message buffer 270 | } 271 | } 272 | } 273 | break; 274 | 275 | //Handle read buffer command 276 | case READ_CMD: 277 | msg = messagebuf.shift(); //remove the command 278 | 279 | //Return next payload 280 | var { protocol, payload } = await this.driver.getBuffer(); 281 | await this.sendArduino(protocol).then(await this.sendArduino(payload)); 282 | 283 | this.bytesReceived += protocol[1]; 284 | var progress = Math.ceil((this.bytesReceived / this.programSize) * 100); 285 | parent.dispatchEvent(new CustomEvent('progress', { detail: { progress: progress } })); //Update load progress 286 | console.log(`READ_CMD: Sent ${this.bytesReceived} of ${this.programSize} bytes`); 287 | break; 288 | 289 | //Log close command 290 | case CLOSE_CMD: 291 | msg = messagebuf.shift(); //remove the command 292 | console.log("CLOSE_CMD: Closed file"); 293 | break; 294 | 295 | //Handle directory listing command 296 | case DIR_CMD: 297 | msg = messagebuf.shift(); //remove the command 298 | 299 | //Return directory line 300 | var { protocol, payload } = await this.driver.getDirectoryLine(); 301 | if (payload) { 302 | await this.sendArduino(protocol).then(await this.sendArduino(payload)); 303 | } 304 | else { 305 | await this.sendArduino(protocol); 306 | } 307 | console.log(`DIR_CMD: Line ${payload}`); 308 | break; 309 | 310 | case WRITE_FULL_CMD: 311 | case WRITE_PART_CMD: 312 | ok = false; //Assume the write message is incomplete 313 | if (messagebuf.length > 1) { 314 | bufLen = messagebuf[1]; 315 | if (messagebuf.length >= bufLen) { 316 | console.log(`WRITE_CMD: Received ${bufLen} bytes`); 317 | ok = true; //Write message is complete, continue 318 | msg = messagebuf.splice(0, bufLen); //ignore write/save data 319 | } 320 | } 321 | break; 322 | 323 | //Debug instruction ('D' with a CR means debug message to process) 324 | case DEBUG_CMD: 325 | ok = false; //Assume the debug message is incomplete 326 | let debugMsg = String.fromCharCode(...messagebuf); 327 | let cr_index = debugMsg.indexOf("\r\n"); 328 | if (cr_index > 0) { 329 | ok = true; //Debug message is complete, continue 330 | msg = messagebuf.splice(0, cr_index+2); //remove the message including the CR/LF on the end 331 | console.log(`DEBUG_CMD: ${debugMsg.substring(0,cr_index)}`); 332 | } 333 | break; 334 | 335 | //Disgard any other messages 336 | default: 337 | msg = messagebuf.shift(); //remove the command 338 | console.log(`Disgarded message: ${msg}`); 339 | } 340 | } //while 341 | 342 | } 343 | else { 344 | //Do the connection handshake 345 | let handshake = String.fromCharCode(...messagebuf); 346 | if (handshake.includes(HANDSHAKE_READY)) { 347 | console.log("Connecting to Arduino"); 348 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "ARDWIP", msg: "Connecting to Arduino"} })); 349 | 350 | await this.sendArduino(new TextEncoder().encode(HANDSHAKE_SEND + "0|8|" + ATN_CLOCK_DATA_RESET_PINS + "\r")); 351 | messagebuf = []; //Clear the message buffer, ready for next stage 352 | } 353 | else if (handshake.includes(HANDSHAKE_OK)) { 354 | console.log("Connected to Arduino"); 355 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "ARDCON", msg: "Connected to Arduino"} })); 356 | 357 | validArduinoConnection = true; 358 | messagebuf = []; //Clear the message buffer, ready for next stage 359 | } 360 | } 361 | 362 | } 363 | this.reader.releaseLock(); 364 | if (done) { 365 | console.log("DONE!"); 366 | break; 367 | } 368 | 369 | } //while 370 | 371 | } //receiveArduino 372 | 373 | //Write data to Arduino 374 | async sendArduino(data) { 375 | 376 | if (this.port.writable) { 377 | const writer = this.port.writable.getWriter(); //Initialize the writer 378 | await writer.write(data).then(writer.releaseLock()); //Send data and release the writer 379 | } 380 | 381 | } //sendArduino 382 | 383 | //Event handler for each time a new serial device connects 384 | serialConnect(event) { 385 | console.log(event.target); 386 | } 387 | 388 | //Event handler for each time a new serial device disconnects 389 | serialDisconnect(event) { 390 | console.log(event.target); 391 | parent.dispatchEvent(new CustomEvent('data', { detail: { code: "SERDIS", msg: "Serial device disconnected"} })); 392 | } 393 | 394 | } -------------------------------------------------------------------------------- /commodore_sketch/Pengo.prg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/commodore_sketch/Pengo.prg -------------------------------------------------------------------------------- /commodore_sketch/atomic.h: -------------------------------------------------------------------------------- 1 | /* sd2iec - SD/MMC to Commodore serial bus interface/controller 2 | Copyright (C) 2007-2022 Ingo Korb 3 | 4 | Inspired by MMC2IEC by Lars Pontoppidan et al. 5 | 6 | FAT filesystem access based on code from ChaN and Jim Brain, see ff.c|h. 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; version 2 of the License only. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program; if not, write to the Free Software 19 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 | 21 | 22 | atomic.h: Wrapper for atomic blocks 23 | 24 | */ 25 | 26 | #ifndef ATOMIC_H 27 | #define ATOMIC_H 28 | 29 | #include 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /commodore_sketch/cbmdefines.h: -------------------------------------------------------------------------------- 1 | #ifndef CBMDEFINES_H 2 | #define CBMDEFINES_H 3 | 4 | namespace CBM { 5 | 6 | // Largest Serial byte buffer request from / to arduino. 7 | #define MAX_BYTES_PER_REQUEST 256 8 | 9 | // Device OPEN channels. 10 | // Special channels. 11 | enum IECChannels { 12 | READPRG_CHANNEL = 0, 13 | WRITEPRG_CHANNEL = 1, 14 | CMD_CHANNEL = 15 15 | }; 16 | 17 | } // namespace CBM 18 | 19 | #endif // CBMDEFINES_H 20 | -------------------------------------------------------------------------------- /commodore_sketch/commodore_sketch.ino: -------------------------------------------------------------------------------- 1 | #include "iec_driver.h" 2 | #include "interface.h" 3 | 4 | #define DEFAULT_BAUD_RATE 115200 5 | #define SERIAL_TIMEOUT_MSECS 1000 6 | 7 | #define HANDSHAKE_READY "\r" 8 | #define HANDSHAKE_SEND "" 9 | #define HANDSHAKE_OK "\r" 10 | 11 | #define CONNECT_BLINKS 2 12 | 13 | static IEC iec(8); 14 | static Interface iface(iec); 15 | 16 | unsigned mode, deviceNumber, atnPin, clockPin, dataPin, resetPin; 17 | 18 | void setup() 19 | { 20 | 21 | //Initialize serial and wait for port to open 22 | Serial.begin(DEFAULT_BAUD_RATE); 23 | while (!Serial); 24 | Serial.setTimeout(SERIAL_TIMEOUT_MSECS); 25 | 26 | connectMediaHost(); 27 | iec.setDeviceNumber(deviceNumber); 28 | iec.setPins(atnPin, clockPin, dataPin, resetPin); 29 | iec.init(); 30 | 31 | } // setup 32 | 33 | void loop() 34 | { 35 | 36 | if(IEC::ATN_RESET == iface.handler()) { 37 | while(IEC::ATN_RESET == iface.handler()); // Wait to get out of reset 38 | } 39 | 40 | } // loop 41 | 42 | //Establish connection with the media host 43 | static void connectMediaHost() 44 | { 45 | char tempBuffer[64]; 46 | 47 | //Initial handshake 48 | while (true) { 49 | 50 | //Empty serial buffer 51 | while(Serial.available()) 52 | Serial.read(); 53 | 54 | //Send connect token to media host 55 | Serial.write(HANDSHAKE_READY); 56 | delay(1000); 57 | 58 | //Check for acknowledgement ok response and continue when received 59 | if (Serial.find(HANDSHAKE_SEND) > 0) 60 | break; 61 | 62 | } // while not connected 63 | 64 | //Receive the pins and other assignments from the media host 65 | while (true) { 66 | if(Serial.readBytesUntil('\r', tempBuffer, sizeof(tempBuffer))) { 67 | sscanf_P(tempBuffer, (PGM_P)F("%u|%u|%u|%u|%u|%u"), 68 | &mode, &deviceNumber, &atnPin, &clockPin, &dataPin, &resetPin); 69 | break; 70 | } 71 | } 72 | 73 | //Blink to indicate connected ok 74 | pinMode(LED_BUILTIN, OUTPUT); // initialize the onboard LED pin for output 75 | for (byte i = 0; i < CONNECT_BLINKS; ++i) { 76 | digitalWrite(LED_BUILTIN, LOW); 77 | delay(250); 78 | digitalWrite(LED_BUILTIN, HIGH); 79 | delay(250); 80 | } 81 | Serial.write(HANDSHAKE_OK); 82 | 83 | } // connectMediaHost 84 | -------------------------------------------------------------------------------- /commodore_sketch/epyxfastload.S: -------------------------------------------------------------------------------- 1 | #include "epyxfastload.h" 2 | #include 3 | 4 | #ifndef CONFIG_MCU_FREQ 5 | # error "CONFIG_MCU_FREQ must be defined for delay cycle calculation." 6 | #endif 7 | 8 | .section .text 9 | 10 | ;; =================================================================== 11 | ;; Utility routines 12 | ;; =================================================================== 13 | 14 | ; RCALL and RET each take an additional cycle on MCUs with >128kB 15 | ; program memory (and therefore a 22 bit PC). 16 | ; These are defined as offset to the (16 bit PC) base value instead of 17 | ; absolute cycles because that simplifies delay cycle calculation. 18 | #ifndef __AVR_3_BYTE_PC__ 19 | # define RCALL_OFFSET 0 20 | # define RET_OFFSET 0 21 | #else 22 | # define RCALL_OFFSET 1 23 | # define RET_OFFSET 1 24 | #endif 25 | 26 | ;; Used by the macros below, don't call directly 27 | nop 28 | nop 29 | delay_loop: 30 | dec r18 ; 1 (ldi) + 3*#r18 - 1 (last brne not taken) 31 | brne delay_loop 32 | delay_7: ; 3+4=7 (16 bit PC) / 4+5=9 (22 bit PC) 33 | ret 34 | 35 | ;; This macro waits for the specified number of cycles 36 | ;; Uses r18 37 | .macro delay_cycles num 38 | .if \num >= 10 + RCALL_OFFSET + RET_OFFSET 39 | ldi r18, (\num - (7 + RCALL_OFFSET + RET_OFFSET)) / 3 40 | rcall delay_loop - 2*((\num - (10 + RCALL_OFFSET + RET_OFFSET)) % 3) 41 | .elseif \num >= 7 + RCALL_OFFSET + RET_OFFSET 42 | rcall delay_7 43 | delay_cycles \num - (7 + RCALL_OFFSET + RET_OFFSET) 44 | .elseif \num >= 2 45 | rjmp .+0 46 | delay_cycles \num-2 47 | .elseif \num == 1 48 | nop 49 | .endif 50 | .endm 51 | 52 | 53 | ;; Delay in 0.5us resolution. Only used if the additional resolution is 54 | ;; necessary, most places use delay_us below (which calls this macro). 55 | ;; 56 | ;; offset is the number of cycles to add to the result of the 57 | ;; conversion from [us]. Can be either positive or negative, but a 58 | ;; negative value can result in an illegal (ie negative) delay. 59 | .macro delay_05us us05:req, offset=0 60 | .if ((\us05)*CONFIG_MCU_FREQ%(2*1000000) != 0) || \ 61 | ((\us05)*CONFIG_MCU_FREQ/(2*1000000)+\offset < 0) 62 | .error "Invalid delay value and/or CONFIG_MCU_FREQ" 63 | .endif 64 | delay_cycles (\us05) * CONFIG_MCU_FREQ/(2*1000000) + \offset 65 | .endm 66 | 67 | ;; Delay in 1us resolution. 68 | .macro delay_us us:req, offset=0 69 | delay_05us 2*(\us), \offset 70 | .endm 71 | 72 | ;; send bits 7 and 5 of r0 to clock/data 73 | ;; masked contents of IEC_OUTPUT expected in r19 74 | ;; 8 (or 9 for 22 bit PC MCUs) cycles from rcall to out, 4 (or 5) to return 75 | epyx_bitpair: 76 | ;; rcall - 3 or 4 77 | bst r0, 7 ; 1 ; Store bit 7 of r0 into T flag 78 | bld r19, IEC_OPIN_CLOCK ; 1 ; Load T flag into CLOCK bit of r19 79 | bst r0, 5 ; 1 ; Store bit 5 of r0 into T flag 80 | bld r19, IEC_OPIN_DATA ; 1 ; Load T flag into DATA bit of r19 81 | out _SFR_IO_ADDR(IEC_OUTPUT_F), r19 ; 1 82 | ret ; 4 or 5 83 | 84 | ;; 85 | ;; Send a byte using the Epyx Fastload cartridge protocol 86 | ;; 87 | .global asm_epyxcart_send_byte 88 | asm_epyxcart_send_byte: 89 | ;; DATA and CLOCK high 90 | sbi _SFR_IO_ADDR(IEC_OUTPUT_F), IEC_OPIN_DATA ;Set pin to high 91 | sbi _SFR_IO_ADDR(IEC_OUTPUT_F), IEC_OPIN_CLOCK ;Set pin to high 92 | delay_us 1 93 | 94 | ;; prepare data 95 | in r19, _SFR_IO_ADDR(IEC_OUTPUT_F) ;Read port F (set of pin values) into r19 96 | andi r19, ~(IEC_OBIT_DATA|IEC_OBIT_CLOCK|IEC_OBIT_SRQ) ;ORs the values of the pins (8 bit values each) and bitwise inverts the result. ATN is on its own port and excluded 97 | 98 | ;; wait for DATA high or ATN low 99 | 1: sbis _SFR_IO_ADDR(IEC_INPUT_B), IEC_OPIN_ATN 100 | rjmp epyxcart_atnabort 101 | sbis _SFR_IO_ADDR(IEC_INPUT_F), IEC_OPIN_DATA 102 | rjmp 1b 103 | 104 | com r24 ; 1 ; Flip all the bits / aka bitwise inversion / aka one's complement 105 | mov r0, r24 ; 1 106 | delay_us 10, -8-RCALL_OFFSET ; Used with the com r24 above 107 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 7 and 5 108 | 109 | lsl r0 ; 1 110 | delay_us 10, -13-RET_OFFSET-RCALL_OFFSET 111 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 6 and 4 112 | 113 | swap r24 ; 1 114 | mov r0, r24 ; 1 115 | delay_us 10, -14-RET_OFFSET-RCALL_OFFSET 116 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 3 and 1 117 | 118 | lsl r0 ; 1 119 | delay_us 10, -13-RET_OFFSET-RCALL_OFFSET 120 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 2 and 0 121 | 122 | delay_us 20, -RET_OFFSET ; final delay so the data stays valid long enough 123 | 124 | clr r24 125 | ret 126 | 127 | epyxcart_atnabort: 128 | ldi r24, 1 129 | ret 130 | 131 | .end 132 | -------------------------------------------------------------------------------- /commodore_sketch/epyxfastload.h: -------------------------------------------------------------------------------- 1 | //Mixed board pin to AVR chip pin mappings across port B and F 2 | # define IEC_INPUT_B PINB 3 | # define IEC_OUTPUT_B PORTB 4 | # define IEC_OPIN_ATN PB5 //pin 9 5 | 6 | # define IEC_INPUT_F PINF 7 | # define IEC_OUTPUT_F PORTF 8 | # define IEC_OPIN_SRQ PF5 //pin 20 9 | # define IEC_OPIN_DATA PF6 //pin 19 10 | # define IEC_OPIN_CLOCK PF7 //pin 18 11 | 12 | # define IEC_OBIT_ATN _BV(IEC_OPIN_ATN) 13 | # define IEC_OBIT_DATA _BV(IEC_OPIN_DATA) 14 | # define IEC_OBIT_CLOCK _BV(IEC_OPIN_CLOCK) 15 | # define IEC_OBIT_SRQ _BV(IEC_OPIN_SRQ) 16 | 17 | # define CONFIG_MCU_FREQ 16000000 18 | 19 | #ifdef __ASSEMBLER__ 20 | 21 | .global asm_epyxcart_send_byte 22 | 23 | #endif 24 | 25 | #ifndef __ASSEMBLER__ 26 | 27 | extern "C" uint8_t asm_epyxcart_send_byte(uint8_t byte); 28 | 29 | #endif -------------------------------------------------------------------------------- /commodore_sketch/iec_driver.cpp: -------------------------------------------------------------------------------- 1 | #include "iec_driver.h" 2 | #include "log.h" 3 | 4 | using namespace CBM; 5 | 6 | /****************************************************************************** 7 | * * 8 | * TIMING SETUP * 9 | * * 10 | ******************************************************************************/ 11 | 12 | 13 | // IEC protocol timing consts: 14 | #define TIMING_BIT 70 // bit clock hi/lo time (us) 15 | #define TIMING_NO_EOI 20 // delay before bits (us) 16 | #define TIMING_EOI_WAIT 200 // delay to signal EOI (us) 17 | #define TIMING_EOI_THRESH 20 // threshold for EOI detect (*10 us approx) 18 | #define TIMING_STABLE_WAIT 20 // line stabilization (us) 19 | #define TIMING_ATN_PREDELAY 50 // delay required in atn (us) 20 | #define TIMING_ATN_DELAY 100 // delay required after atn (us) 21 | #define TIMING_FNF_DELAY 100 // delay after fnf? (us) 22 | 23 | // Version 0.5 equivalent timings: 70, 5, 200, 20, 20, 50, 100, 100 24 | 25 | // TIMING TESTING: 26 | // 27 | // The consts: 70,20,200,20,20,50,100,100 has been tested without debug print 28 | // to work stable on my (Larsp)'s DTV at 700000 < F_CPU < 9000000 29 | // using a 32 MB MMC card 30 | // 31 | 32 | 33 | // The IEC bus pin configuration on the Arduino side 34 | // NOTE: Deprecated: Only startup values used in ctor, this will be defined from host side. 35 | #define DEFAULT_ATN_PIN 5 36 | #define DEFAULT_DATA_PIN 3 37 | #define DEFAULT_CLOCK_PIN 4 38 | #define DEFAULT_RESET_PIN 7 39 | 40 | // See timeoutWait below. 41 | #define TIMEOUT 65000 42 | 43 | IEC::IEC(byte deviceNumber) : 44 | m_state(noFlags), m_deviceNumber(deviceNumber), 45 | m_atnPin(DEFAULT_ATN_PIN), m_dataPin(DEFAULT_DATA_PIN), 46 | m_clockPin(DEFAULT_CLOCK_PIN), m_resetPin(DEFAULT_RESET_PIN) 47 | #ifdef DEBUGLINES 48 | ,m_lastMillis(0) 49 | #endif 50 | {} 51 | 52 | 53 | byte IEC::timeoutWait(byte waitBit, boolean whileHigh) 54 | { 55 | word t = 0; 56 | boolean c; 57 | 58 | while(t < TIMEOUT) { 59 | // Check the waiting condition: 60 | c = readPIN(waitBit); 61 | 62 | if(whileHigh) 63 | c = not c; 64 | 65 | if(c) 66 | return false; 67 | 68 | delayMicroseconds(2); // The aim is to make the loop at least 3 us 69 | t++; 70 | } 71 | 72 | // If down here, we have had a timeout. 73 | // Release lines and go to inactive state with error flag 74 | writeCLOCK(false); 75 | writeDATA(false); 76 | 77 | m_state = errorFlag; 78 | 79 | // Wait for ATN release, problem might have occured during attention 80 | while(not readATN()); 81 | 82 | // Note: The while above is without timeout. If ATN is held low forever, 83 | // the CBM is out in the woods and needs a reset anyway 84 | 85 | return true; 86 | } // timeoutWait 87 | 88 | 89 | // IEC Receive byte standard function 90 | // 91 | // Returns data received 92 | // Might set flags in iec_state 93 | // 94 | // FIXME: m_iec might be better returning bool and returning read byte as reference in order to indicate any error. 95 | byte IEC::receiveByte(void) 96 | { 97 | m_state = noFlags; 98 | 99 | // Wait for talker ready 100 | if(timeoutWait(m_clockPin, false)) { 101 | return 0; 102 | } 103 | 104 | // Say we're ready 105 | writeDATA(false); 106 | 107 | // Record how long CLOCK is high, more than 200 us means EOI 108 | byte n = 0; 109 | while(readCLOCK() and (n < 20)) { 110 | delayMicroseconds(10); // this loop should cycle in about 10 us... 111 | n++; 112 | } 113 | 114 | if(n >= TIMING_EOI_THRESH) { 115 | // EOI intermission 116 | m_state or_eq eoiFlag; 117 | 118 | // Acknowledge by pull down data more than 60 us 119 | writeDATA(true); 120 | delayMicroseconds(TIMING_BIT); 121 | writeDATA(false); 122 | 123 | // but still wait for clk 124 | if(timeoutWait(m_clockPin, true)) { 125 | return 0; 126 | } 127 | } 128 | 129 | // Sample ATN 130 | if(false == readATN()) 131 | m_state or_eq atnFlag; 132 | 133 | byte data = 0; 134 | // Get the bits, sampling on clock rising edge: 135 | for(n = 0; n < 8; n++) { 136 | data >>= 1; 137 | if(timeoutWait(m_clockPin, false)) { 138 | return 0; 139 | } 140 | data or_eq (readDATA() ? (1 << 7) : 0); 141 | if(timeoutWait(m_clockPin, true)) { 142 | return 0; 143 | } 144 | } 145 | 146 | // Signal we accepted data: 147 | writeDATA(true); 148 | 149 | return data; 150 | } // receiveByte 151 | 152 | 153 | // IEC Send byte standard function 154 | // 155 | // Sends the byte and can signal EOI 156 | // 157 | boolean IEC::sendByte(byte data, boolean signalEOI) 158 | { 159 | // Listener must have accepted previous data 160 | if(timeoutWait(m_dataPin, true)) 161 | return false; 162 | 163 | // Say we're ready 164 | writeCLOCK(false); 165 | 166 | // Wait for listener to be ready 167 | if(timeoutWait(m_dataPin, false)) 168 | return false; 169 | 170 | if(signalEOI) { 171 | // FIXME: Make this like sd2iec and may not need a fixed delay here. 172 | 173 | // Signal eoi by waiting 200 us 174 | delayMicroseconds(TIMING_EOI_WAIT); 175 | 176 | // get eoi acknowledge: 177 | if(timeoutWait(m_dataPin, true)) 178 | return false; 179 | 180 | if(timeoutWait(m_dataPin, false)) 181 | return false; 182 | } 183 | 184 | delayMicroseconds(TIMING_NO_EOI); 185 | 186 | // Send bits 187 | for(byte n = 0; n < 8; n++) { 188 | // FIXME: Here check whether data pin goes low, if so end (enter cleanup)! 189 | 190 | writeCLOCK(true); 191 | // set data 192 | writeDATA((data bitand 1) ? false : true); 193 | 194 | delayMicroseconds(TIMING_BIT); 195 | writeCLOCK(false); 196 | delayMicroseconds(TIMING_BIT); 197 | 198 | data >>= 1; 199 | } 200 | 201 | writeCLOCK(true); 202 | writeDATA(false); 203 | 204 | // FIXME: Maybe make the following ending more like sd2iec instead. 205 | 206 | // Line stabilization delay 207 | delayMicroseconds(TIMING_STABLE_WAIT); 208 | 209 | // Wait for listener to accept data 210 | if(timeoutWait(m_dataPin, true)) 211 | return false; 212 | 213 | return true; 214 | } // sendByte 215 | 216 | //Semi-fastload protocol "gijoe" read byte 217 | int16_t IEC::gijoe_read_byte(void) { 218 | uint8_t i; 219 | uint8_t value = 0; 220 | char buffer[80]; 221 | 222 | for (i=0;i<4;i++) { 223 | 224 | value >>= 1; 225 | if(timeoutWait(m_clockPin, true)) //Wait until clock becomes low/false (wait while high/true) 226 | return -1; 227 | if (readDATA() == false) 228 | value |= 0x80; 229 | 230 | value >>= 1; 231 | if(timeoutWait(m_clockPin, false)) //Wait until clock becomes high/true (wait while low/false) 232 | return -1; 233 | if (readDATA() == false) 234 | value |= 0x80; 235 | 236 | } 237 | 238 | return value; 239 | } 240 | 241 | void IEC::setClock(boolean state) 242 | { 243 | writeCLOCK(state); 244 | } // setClock 245 | 246 | void IEC::setData(boolean state) 247 | { 248 | writeDATA(state); 249 | } // setData 250 | 251 | byte IEC::getATN() 252 | { 253 | return readATN(); 254 | } // getATN 255 | 256 | byte IEC::getData() 257 | { 258 | return readDATA(); 259 | } // getData 260 | 261 | // IEC turnaround 262 | boolean IEC::turnAround(void) 263 | { 264 | // Wait until clock is released 265 | if(timeoutWait(m_clockPin, false)) 266 | return false; 267 | 268 | writeDATA(false); 269 | delayMicroseconds(TIMING_BIT); 270 | writeCLOCK(true); 271 | delayMicroseconds(TIMING_BIT); 272 | 273 | return true; 274 | } // turnAround 275 | 276 | 277 | // this routine will set the direction on the bus back to normal 278 | // (the way it was when the computer was switched on) 279 | boolean IEC::undoTurnAround(void) 280 | { 281 | writeDATA(true); 282 | delayMicroseconds(TIMING_BIT); 283 | writeCLOCK(false); 284 | delayMicroseconds(TIMING_BIT); 285 | 286 | // wait until the computer releases the clock line 287 | if(timeoutWait(m_clockPin, true)) 288 | return false; 289 | 290 | return true; 291 | } // undoTurnAround 292 | 293 | 294 | /****************************************************************************** 295 | * * 296 | * Public functions * 297 | * * 298 | ******************************************************************************/ 299 | 300 | // This function checks and deals with atn signal commands 301 | // 302 | // If a command is received, the cmd-string is saved in cmd. Only commands 303 | // for *this* device are dealt with. 304 | // 305 | // Return value, see IEC::ATNCheck definition. 306 | IEC::ATNCheck IEC::checkATN(ATNCmd& cmd) 307 | { 308 | ATNCheck ret = ATN_IDLE; 309 | byte i = 0; 310 | 311 | if(not readATN()) { 312 | // Attention line is active, go to listener mode and get message. Being fast with the next two lines here is CRITICAL! 313 | writeDATA(true); 314 | writeCLOCK(false); 315 | delayMicroseconds(TIMING_ATN_PREDELAY); 316 | 317 | // Get first ATN byte, it is either LISTEN or TALK 318 | ATNCommand c = (ATNCommand)receiveByte(); 319 | if(m_state bitand errorFlag) { 320 | return ATN_ERROR; 321 | } 322 | 323 | if(c == (ATN_CODE_LISTEN bitor m_deviceNumber)) { 324 | // Okay, we will listen. 325 | // Get the first cmd byte, the cmd code 326 | c = (ATNCommand)receiveByte(); 327 | if (m_state bitand errorFlag) { 328 | return ATN_ERROR; 329 | } 330 | 331 | cmd.code = c; 332 | 333 | // If the command is DATA and it is not to expect just a small command on the command channel, then 334 | // we're into something more heavy. Otherwise read it all out right here until UNLISTEN is received. 335 | if((c bitand 0xF0) == ATN_CODE_DATA and (c bitand 0xF) not_eq CMD_CHANNEL) { 336 | // A heapload of data might come now, too big for this context to handle so the caller handles this, we're done here. 337 | ret = ATN_CMD_LISTEN; 338 | } 339 | else if(c not_eq ATN_CODE_UNLISTEN) { 340 | // Some other command. Record the cmd string until UNLISTEN is sent 341 | for(;;) { 342 | c = (ATNCommand)receiveByte(); 343 | if(m_state bitand errorFlag) { 344 | return ATN_ERROR; 345 | } 346 | 347 | if((m_state bitand atnFlag) and (ATN_CODE_UNLISTEN == c)) 348 | break; 349 | 350 | if(i >= ATN_CMD_MAX_LENGTH) { 351 | // Buffer is going to overflow, this is an error condition 352 | // FIXME: here we should propagate the error type being overflow so that reading error channel can give right code out. 353 | return ATN_ERROR; 354 | } 355 | cmd.str[i++] = c; 356 | } 357 | ret = ATN_CMD; 358 | } 359 | } 360 | else if (c == (ATN_CODE_TALK bitor m_deviceNumber)) { 361 | // Okay, we will talk soon, record cmd string while ATN is active 362 | // First byte is cmd code, that we CAN at least expect. All else depends on ATN. 363 | c = (ATNCommand)receiveByte(); 364 | if(m_state bitand errorFlag) { 365 | return ATN_ERROR; 366 | } 367 | cmd.code = c; 368 | 369 | while(not readATN()) { 370 | if(readCLOCK()) { 371 | c = (ATNCommand)receiveByte(); 372 | if(m_state bitand errorFlag) { 373 | return ATN_ERROR; 374 | } 375 | 376 | if(i >= ATN_CMD_MAX_LENGTH) { 377 | // Buffer is going to overflow, this is an error condition 378 | // FIXME: here we should propagate the error type being overflow so that reading error channel can give right code out. 379 | return ATN_ERROR; 380 | } 381 | cmd.str[i++] = c; 382 | } 383 | } 384 | 385 | // Now ATN has just been released, do bus turnaround 386 | if(not turnAround()) { 387 | return ATN_ERROR; 388 | } 389 | 390 | // We have received a CMD and we should talk now: 391 | ret = ATN_CMD_TALK; 392 | 393 | } 394 | else { 395 | // Either the message is not for us or insignificant, like unlisten. 396 | delayMicroseconds(TIMING_ATN_DELAY); 397 | writeDATA(false); 398 | writeCLOCK(false); 399 | 400 | // Wait for ATN to release and quit 401 | while(not readATN()); 402 | } 403 | } 404 | else { 405 | // No ATN, keep lines in a released state. 406 | writeDATA(false); 407 | writeCLOCK(false); 408 | } 409 | 410 | // some delay is required before more ATN business can take place. 411 | delayMicroseconds(TIMING_ATN_DELAY); 412 | 413 | cmd.strLen = i; 414 | return ret; 415 | } // checkATN 416 | 417 | // IEC_receive receives a byte 418 | // 419 | byte IEC::receive() 420 | { 421 | return receiveByte(); 422 | } // receive 423 | 424 | 425 | // IEC_send sends a byte 426 | // 427 | boolean IEC::send(byte data) 428 | { 429 | return sendByte(data, false); 430 | } // send 431 | 432 | 433 | // Same as IEC_send, but indicating that this is the last byte. 434 | // 435 | boolean IEC::sendEOI(byte data) 436 | { 437 | if(sendByte(data, true)) { 438 | // As we have just send last byte, turn bus back around 439 | if(undoTurnAround()) 440 | return true; 441 | } 442 | 443 | return false; 444 | } // sendEOI 445 | 446 | 447 | // A special send command that informs file not found condition 448 | // 449 | boolean IEC::sendFNF() 450 | { 451 | // Message file not found by just releasing lines 452 | writeDATA(false); 453 | writeCLOCK(false); 454 | 455 | // Hold back a little... 456 | delayMicroseconds(TIMING_FNF_DELAY); 457 | 458 | return true; 459 | } // sendFNF 460 | 461 | 462 | // Set all IEC_signal lines in the correct mode 463 | // 464 | boolean IEC::init() 465 | { 466 | // make sure the output states are initially LOW. 467 | pinMode(m_atnPin, OUTPUT); 468 | pinMode(m_dataPin, OUTPUT); 469 | pinMode(m_clockPin, OUTPUT); 470 | digitalWrite(m_atnPin, false); 471 | digitalWrite(m_dataPin, false); 472 | digitalWrite(m_clockPin, false); 473 | 474 | // initial pin modes in GPIO. 475 | pinMode(m_atnPin, INPUT); 476 | pinMode(m_dataPin, INPUT); 477 | pinMode(m_clockPin, INPUT); 478 | pinMode(m_resetPin, INPUT); 479 | 480 | #ifdef DEBUGLINES 481 | m_lastMillis = millis(); 482 | #endif 483 | 484 | // Set port low, we don't need internal pullup 485 | // and DDR input such that we release all signals 486 | // IEC_PORT and_eq compl(IEC_BIT_ATN bitor IEC_BIT_CLOCK bitor IEC_BIT_DATA); 487 | // IEC_DDR and_eq compl(IEC_BIT_ATN bitor IEC_BIT_CLOCK bitor IEC_BIT_DATA); 488 | 489 | m_state = noFlags; 490 | return true; 491 | } // init 492 | 493 | #ifdef DEBUGLINES 494 | void IEC::testINPUTS() 495 | { 496 | unsigned long now = millis(); 497 | // show states every second. 498 | if(now - m_lastMillis >= 1000) { 499 | m_lastMillis = now; 500 | // char buffer[80]; 501 | // sprintf(buffer, "Lines, ATN: %s CLOCK: %s DATA: %s", 502 | // (readATN() ? "HIGH" : "LOW"), (readCLOCK() ? "HIGH" : "LOW"), (readDATA() ? "HIGH" : "LOW")); 503 | // Log(buffer); 504 | } 505 | } // testINPUTS 506 | 507 | 508 | void IEC::testOUTPUTS() 509 | { 510 | static bool lowOrHigh = false; 511 | unsigned long now = millis(); 512 | // switch states every second. 513 | if(now - m_lastMillis >= 1000) { 514 | m_lastMillis = now; 515 | char buffer[80]; 516 | sprintf(buffer, "Lines: CLOCK: %s DATA: %s", (lowOrHigh ? "HIGH" : "LOW"), (lowOrHigh ? "HIGH" : "LOW")); 517 | // Log(buffer); 518 | writeCLOCK(lowOrHigh); 519 | writeDATA(lowOrHigh); 520 | lowOrHigh xor_eq true; 521 | } 522 | } // testOUTPUTS 523 | #endif 524 | 525 | 526 | byte IEC::deviceNumber() const 527 | { 528 | return m_deviceNumber; 529 | } // deviceNumber 530 | 531 | 532 | void IEC::setDeviceNumber(const byte deviceNumber) 533 | { 534 | m_deviceNumber = deviceNumber; 535 | } // setDeviceNumber 536 | 537 | 538 | void IEC::setPins(byte atn, byte clock, byte data, byte reset) 539 | { 540 | m_atnPin = atn; 541 | m_clockPin = clock; 542 | m_dataPin = data; 543 | m_resetPin = reset; 544 | } // setPins 545 | 546 | 547 | IEC::IECState IEC::state() const 548 | { 549 | return static_cast(m_state); 550 | } // state 551 | -------------------------------------------------------------------------------- /commodore_sketch/iec_driver.h: -------------------------------------------------------------------------------- 1 | #ifndef IEC_DRIVER_H 2 | #define IEC_DRIVER_H 3 | 4 | // Enable this to debug the IEC lines (checking soldering and physical connections). See project README.TXT 5 | //#define DEBUGLINES 6 | 7 | #include 8 | #include "cbmdefines.h" 9 | 10 | class IEC 11 | { 12 | public: 13 | 14 | enum IECState { 15 | noFlags = 0, 16 | eoiFlag = (1 << 0), // might be set by Iec_receive 17 | atnFlag = (1 << 1), // might be set by Iec_receive 18 | errorFlag = (1 << 2) // If this flag is set, something went wrong 19 | }; 20 | 21 | // Return values for checkATN: 22 | enum ATNCheck { 23 | ATN_IDLE = 0, // Nothing received of our concern 24 | ATN_CMD = 1, // A command is received 25 | ATN_CMD_LISTEN = 2, // A command is received and data is coming to us 26 | ATN_CMD_TALK = 3, // A command is received and we must talk now 27 | ATN_ERROR = 4, // A problem occoured, reset communication 28 | ATN_RESET = 5 // The IEC bus is in a reset state (RESET line) 29 | }; 30 | 31 | // IEC ATN commands: 32 | enum ATNCommand { 33 | ATN_CODE_LISTEN = 0x20, 34 | ATN_CODE_TALK = 0x40, 35 | ATN_CODE_DATA = 0x60, 36 | ATN_CODE_CLOSE = 0xE0, 37 | ATN_CODE_OPEN = 0xF0, 38 | ATN_CODE_UNLISTEN = 0x3F, 39 | ATN_CODE_UNTALK = 0x5F 40 | }; 41 | 42 | // ATN command struct maximum command length: 43 | enum { 44 | ATN_CMD_MAX_LENGTH = 40 45 | }; 46 | // default device number listening unless explicitly stated in ctor: 47 | enum { 48 | DEFAULT_IEC_DEVICE = 8 49 | }; 50 | 51 | typedef struct _tagATNCMD { 52 | byte code; 53 | byte str[ATN_CMD_MAX_LENGTH]; 54 | byte strLen; 55 | } ATNCmd; 56 | 57 | IEC(byte deviceNumber = DEFAULT_IEC_DEVICE); 58 | ~IEC() 59 | { } 60 | 61 | // Initialise iec driver 62 | // 63 | boolean init(); 64 | 65 | // Checks if CBM is sending an attention message. If this is the case, 66 | // the message is received and stored in atn_cmd. 67 | // 68 | ATNCheck checkATN(ATNCmd& cmd); 69 | 70 | // Sends a byte. The communication must be in the correct state: a load command 71 | // must just have been received. If something is not OK, FALSE is returned. 72 | // 73 | boolean send(byte data); 74 | 75 | // Same as IEC_send, but indicating that this is the last byte. 76 | // 77 | boolean sendEOI(byte data); 78 | 79 | // A special send command that informs file not found condition 80 | // 81 | boolean sendFNF(); 82 | 83 | // Receives a byte 84 | // 85 | byte receive(); 86 | 87 | byte deviceNumber() const; 88 | void setDeviceNumber(const byte deviceNumber); 89 | void setPins(byte atn, byte clock, byte data, byte reset); 90 | IECState state() const; 91 | 92 | //Needed for epyx fastload 93 | void setClock(boolean state); 94 | void setData(boolean state); 95 | byte getATN(); 96 | byte getData(); 97 | 98 | //Read byte using gijoe protocol 99 | int16_t gijoe_read_byte(void); 100 | 101 | #ifdef DEBUGLINES 102 | unsigned long m_lastMillis; 103 | void testINPUTS(); 104 | void testOUTPUTS(); 105 | #endif 106 | 107 | private: 108 | byte timeoutWait(byte waitBit, boolean whileHigh); 109 | byte receiveByte(void); 110 | boolean sendByte(byte data, boolean signalEOI); 111 | boolean turnAround(void); 112 | boolean undoTurnAround(void); 113 | 114 | // false = LOW, true == HIGH 115 | inline boolean readPIN(byte pinNumber) 116 | { 117 | // To be able to read line we must be set to input, not driving. 118 | pinMode(pinNumber, INPUT); 119 | return digitalRead(pinNumber) ? true : false; 120 | } 121 | 122 | inline boolean readATN() 123 | { 124 | return readPIN(m_atnPin); 125 | } 126 | 127 | inline boolean readDATA() 128 | { 129 | return readPIN(m_dataPin); 130 | } 131 | 132 | inline boolean readCLOCK() 133 | { 134 | return readPIN(m_clockPin); 135 | } 136 | 137 | inline boolean readRESET() 138 | { 139 | return !readPIN(m_resetPin); 140 | } 141 | 142 | // true == PULL == HIGH, false == RELEASE == LOW 143 | inline void writePIN(byte pinNumber, boolean state) 144 | { 145 | pinMode(pinNumber, state ? OUTPUT : INPUT); 146 | digitalWrite(pinNumber, state ? LOW : HIGH); 147 | } 148 | 149 | inline void writeATN(boolean state) 150 | { 151 | writePIN(m_atnPin, state); 152 | } 153 | 154 | inline void writeDATA(boolean state) 155 | { 156 | writePIN(m_dataPin, state); 157 | } 158 | 159 | inline void writeCLOCK(boolean state) 160 | { 161 | writePIN(m_clockPin, state); 162 | } 163 | 164 | // communication must be reset 165 | byte m_state; 166 | byte m_deviceNumber; 167 | 168 | byte m_atnPin; 169 | byte m_dataPin; 170 | byte m_clockPin; 171 | byte m_resetPin; 172 | }; 173 | 174 | #endif 175 | -------------------------------------------------------------------------------- /commodore_sketch/interface.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "interface.h" 3 | #include "atomic.h" 4 | #include "epyxfastload.h" 5 | #include "log.h" 6 | 7 | //Retrieve programs to fastload using serial or included binary rom file 8 | #define USE_SERIAL 9 | //#define USE_ROM 10 | 11 | #ifdef USE_ROM 12 | #include "incbin.h" 13 | INCBIN(ROMFile, "Pengo.prg"); 14 | #endif 15 | 16 | using namespace CBM; 17 | 18 | namespace { 19 | 20 | // Buffer for incoming and outgoing serial bytes and other stuff. 21 | char serCmdIOBuf[MAX_BYTES_PER_REQUEST]; 22 | 23 | } // unnamed namespace 24 | 25 | 26 | Interface::Interface(IEC& iec) 27 | : m_iec(iec) 28 | // NOTE: Householding with RAM bytes: We use the middle of serial buffer for the ATNCmd buffer info. 29 | // This is ok and won't be overwritten by actual serial data from the host, this is because when this ATNCmd data is in use 30 | // only a few bytes of the actual serial data will be used in the buffer. 31 | , m_cmd(*reinterpret_cast(&serCmdIOBuf[sizeof(serCmdIOBuf) / 2])) 32 | {} 33 | 34 | 35 | // send single basic line, including heading basic pointer and terminating zero. 36 | void Interface::sendLine(byte len, char* text, word& basicPtr) 37 | { 38 | // Increment next line pointer 39 | basicPtr += len + 5 - 2; // note: minus two here because the line number is included in the array already 40 | 41 | // Send that pointer 42 | m_iec.send(basicPtr bitand 0xFF); 43 | m_iec.send(basicPtr >> 8); 44 | 45 | // Send line contents 46 | for(byte i = 0; i < len; i++) 47 | m_iec.send(text[i]); 48 | 49 | // Finish line 50 | m_iec.send(0); 51 | } // sendLine 52 | 53 | 54 | void Interface::sendListing() 55 | { 56 | uint8_t bufLen, bytesRead, bufEnd; 57 | boolean firstLine = true; 58 | 59 | // Reset basic memory pointer: 60 | word basicPtr = C64_BASIC_START; 61 | 62 | do { 63 | //Serial read buffer will be populated in response to the first directory request and subsequent read requests using 'L' 64 | Serial.readBytes(serCmdIOBuf, 2); 65 | bufEnd = serCmdIOBuf[0]; 66 | bufLen = serCmdIOBuf[1]; 67 | if (bufLen > 0) { 68 | bytesRead = Serial.readBytes(serCmdIOBuf, bufLen); 69 | noInterrupts(); 70 | if (firstLine) { // Send load address 71 | m_iec.send(C64_BASIC_START bitand 0xff); 72 | m_iec.send((C64_BASIC_START >> 8) bitand 0xff); 73 | firstLine = false; 74 | } 75 | sendLine(bufLen, serCmdIOBuf, basicPtr); 76 | interrupts(); 77 | } 78 | if (bufEnd == 'L') { //'Normal' directory line types received are 'L', except for the last one 'l' 79 | Serial.write('L'); //Request another directory line 80 | } 81 | } while (bufEnd == 'L'); //Continue while 'normal' directory lines are being returned 82 | 83 | // End program with two zeros after last line. Last zero goes out as EOI. 84 | noInterrupts(); 85 | m_iec.send(0); 86 | m_iec.sendEOI(0); 87 | interrupts(); 88 | 89 | } // sendListing 90 | 91 | 92 | void Interface::sendFile() 93 | { 94 | uint8_t bufLen, bytesRead, bufEnd, i; 95 | bool ok = true; 96 | 97 | do { 98 | 99 | //Serial read buffer will be populated in response to the first open file request and subsequent read requests using 'R' 100 | bytesRead = Serial.readBytes(serCmdIOBuf, 2); // read the ack type ('B' or 'b') and length 101 | bufEnd = serCmdIOBuf[0]; 102 | bufLen = serCmdIOBuf[1]; 103 | bytesRead = Serial.readBytes(serCmdIOBuf, bufLen); // read the program data bytes 104 | 105 | //Ask for more bytes from the PC now, then send the current buffer load to the C64 106 | #if !defined(__AVR_ATmega328P__) //Not suitable for Arduino uno 107 | if (bufEnd != 'b') Serial.write('R'); 108 | #endif 109 | 110 | for (i = 0; i < bufLen and ok; i++) { 111 | noInterrupts(); 112 | if (bufEnd == 'b' and i == bufLen - 1) 113 | ok = m_iec.sendEOI(serCmdIOBuf[i]); // indicate end of file. 114 | else 115 | ok = m_iec.send(serCmdIOBuf[i]); 116 | interrupts(); 117 | } 118 | 119 | if (!ok) { 120 | sprintf_P(serCmdIOBuf, (PGM_P)F("sendFile send bytes problem: %u"), i); 121 | Log(serCmdIOBuf); 122 | } 123 | 124 | #if defined(__AVR_ATmega328P__) //Suitable for Arduino uno 125 | if (bufEnd != 'b') Serial.write('R'); 126 | #endif 127 | 128 | } while (bufEnd == 'B' and ok); // keep asking for more as long as we don't get the 'b' or something else (indicating out of sync). 129 | 130 | if (ok) { 131 | Log("sendFile completed"); 132 | } 133 | else { 134 | while (Serial.available()) //Flush out read buffer 135 | Serial.read(); 136 | } 137 | 138 | } // sendFile 139 | 140 | 141 | void Interface::saveFile() 142 | { 143 | boolean done = false; 144 | do { 145 | 146 | // Receive bytes from Commodore until EOI detected 147 | uint8_t bufLen = 2; //Allow for 'W'/'w' and length prefix bytes 148 | do { 149 | noInterrupts(); 150 | serCmdIOBuf[bufLen++] = m_iec.receive(); 151 | interrupts(); 152 | done = (m_iec.state() bitand IEC::eoiFlag) or (m_iec.state() bitand IEC::errorFlag); 153 | } while ((bufLen < 240) and not done); 154 | 155 | // Send the bytes onto the PC 156 | serCmdIOBuf[0] = 'W'; 157 | if (done) { 158 | serCmdIOBuf[0] = 'w'; 159 | } 160 | serCmdIOBuf[1] = bufLen; 161 | Serial.write((const byte*)serCmdIOBuf, bufLen); 162 | Serial.flush(); 163 | 164 | } while (not done); 165 | 166 | } // saveFile 167 | 168 | 169 | byte Interface::handler(void) 170 | { 171 | noInterrupts(); 172 | IEC::ATNCheck retATN = m_iec.checkATN(m_cmd); 173 | interrupts(); 174 | 175 | if(retATN == IEC::ATN_ERROR) { 176 | strcpy_P(serCmdIOBuf, (PGM_P)F("ATNCMD: IEC_ERROR!")); 177 | Log(serCmdIOBuf); 178 | } 179 | 180 | // Did anything happen from the host side? 181 | else if(retATN not_eq IEC::ATN_IDLE) { 182 | // A command is received, make cmd string null terminated 183 | m_cmd.str[m_cmd.strLen] = '\0'; 184 | #ifdef CONSOLE_DEBUG 185 | { 186 | sprintf_P(serCmdIOBuf, (PGM_P)F("ATN code:%d cmd: %s (len: %d) retATN: %d"), m_cmd.code, m_cmd.str, m_cmd.strLen, retATN); 187 | Log(serCmdIOBuf); 188 | } 189 | #endif 190 | 191 | // lower nibble is the channel. 192 | byte chan = m_cmd.code bitand 0x0F; 193 | 194 | // check upper nibble, the command itself. 195 | switch(m_cmd.code bitand 0xF0) { 196 | case IEC::ATN_CODE_OPEN: 197 | // Open either file or prg for reading, writing or single line command on the command channel. 198 | // In any case we just issue an 'OPEN' to the host and let it process. 199 | // Note: Some of the host response handling is done LATER, since we will get a TALK or LISTEN after this. 200 | // Also, simply issuing the request to the host and not waiting for any response here makes us more 201 | // responsive to the CBM here, when the DATA with TALK or LISTEN comes in the next sequence. 202 | handleATNCmdCodeOpen(m_cmd); 203 | break; 204 | 205 | case IEC::ATN_CODE_DATA: // data channel opened 206 | if(retATN == IEC::ATN_CMD_TALK) { 207 | // when the CMD channel is read (status), we first need to issue the host request. The data channel is opened directly. 208 | if(CMD_CHANNEL == chan) 209 | handleATNCmdCodeOpen(m_cmd); // Typically an empty command for channel 15 message to Commodore 210 | handleATNCmdCodeDataTalk(chan); // Talk to Commodore, sending file and listing data 211 | } 212 | else if(retATN == IEC::ATN_CMD_LISTEN) { 213 | handleATNCmdCodeDataListen(); // Listen for commands / data from the Commodore e.g. save data 214 | } 215 | else if(retATN == IEC::ATN_CMD) { // Here we are sending a command to PC and executing it, but not sending response 216 | if (CMD_CHANNEL == chan) { 217 | 218 | //Check if received M-E for semi-fast / gijoe mode, proceeding to epyx fastload 219 | if(strcmp(m_cmd.str,"M-E\xa9\x01\r") == 0) { 220 | #ifdef USE_SERIAL 221 | epyxFastloadProgram(); 222 | #endif 223 | #ifdef USE_ROM 224 | epyxFastloadROM(); 225 | #endif 226 | } 227 | } 228 | else 229 | handleATNCmdCodeOpen(m_cmd); // back to CBM, the result code of the command is however buffered on the PC side. 230 | } 231 | break; 232 | 233 | case IEC::ATN_CODE_CLOSE: 234 | // handle close with host. 235 | handleATNCmdClose(); 236 | break; 237 | 238 | case IEC::ATN_CODE_LISTEN: 239 | //Log("LISTEN"); 240 | break; 241 | case IEC::ATN_CODE_TALK: 242 | //Log("TALK"); 243 | break; 244 | case IEC::ATN_CODE_UNLISTEN: 245 | //Log("UNLISTEN"); 246 | break; 247 | case IEC::ATN_CODE_UNTALK: 248 | //Log("UNTALK"); 249 | break; 250 | default: 251 | if(retATN == IEC::ATN_CMD_TALK) { 252 | //Needed to handle epyx fastload cartridge shortcut dir command (type $ on C64) 253 | handleATNCmdCodeDataTalk(chan); // Talk to Commodore, sending file and listing data 254 | } 255 | break; 256 | } // switch 257 | } // IEC not idle 258 | 259 | return retATN; 260 | } // handler 261 | 262 | 263 | #ifdef USE_SERIAL 264 | //Open the program and fastload to C64 with data sent serially from PC 265 | void Interface::epyxFastloadProgram() 266 | { 267 | uint8_t bufLen, bytesRead, bufEnd, i, b; 268 | uint8_t checksum = 0; 269 | int16_t j; 270 | 271 | //Switchover to full epyx fastload via semi-fastload gijoe protocol 272 | 273 | //Initial handshake 274 | noInterrupts(); 275 | m_iec.setData(true); 276 | m_iec.setClock(false); 277 | 278 | while(m_iec.getData() != false) 279 | m_iec.setClock(true); 280 | interrupts(); 281 | 282 | //Receive and checksum stage 2 283 | for (j=0; j<256; j++) { 284 | noInterrupts(); 285 | b = m_iec.gijoe_read_byte(); 286 | interrupts(); 287 | if (b < 0) { 288 | Log("epyxFastloadProgram, read checksum error"); 289 | return; 290 | } 291 | 292 | if (j < 237) 293 | //Stage 2 has some junk bytes at the end, ignore them 294 | checksum ^= b; 295 | } 296 | 297 | //Check for known stage2 loaders 298 | if (checksum != 0x91 && checksum != 0x5b) { 299 | Log("epyxFastloadProgram, checksum total error"); 300 | } 301 | 302 | //Receive file name length 303 | noInterrupts(); 304 | j = m_iec.gijoe_read_byte(); 305 | interrupts(); 306 | if (j < 0) { 307 | Log("epyxFastloadProgram, file length error"); 308 | } 309 | bufLen = j + 3; //Allow for 'O' (open), file/command length and channel 310 | 311 | //Receive file name and build open command 312 | serCmdIOBuf[0] = 'O'; //Open instruction 313 | serCmdIOBuf[1] = bufLen; 314 | serCmdIOBuf[2] = IEC::ATN_CODE_OPEN bitand 0xF; //channel 315 | do { 316 | noInterrupts(); 317 | b = m_iec.gijoe_read_byte(); 318 | interrupts(); 319 | if (b < 0) { 320 | Log("epyxFastloadProgram, file name error"); 321 | break; 322 | } 323 | 324 | serCmdIOBuf[--bufLen] = b; 325 | } while (bufLen > 3); //Preserve first 3 bytes of serCmdIOBuf 326 | 327 | m_iec.setClock(true); 328 | 329 | //Request file open from PC which then returns a buffer load of data 330 | Serial.write((const byte*)serCmdIOBuf, serCmdIOBuf[1]); //send instruction to PC 331 | 332 | // Transfer data via full epyx fastload protocol 333 | do { 334 | 335 | ATOMIC_BLOCK(ATOMIC_FORCEON) { 336 | m_iec.setClock(true); 337 | m_iec.setData(true); 338 | } 339 | 340 | bytesRead = Serial.readBytes(serCmdIOBuf, 2); // read the ack type usually B/E or X if error 341 | bufEnd = serCmdIOBuf[0]; 342 | bufLen = serCmdIOBuf[1]; 343 | if (bufEnd == 'X') { // 344 | m_iec.sendFNF(); //Error, return file not found on Commodore 345 | while (Serial.available()) //Flush out read buffer 346 | Serial.read(); 347 | break; 348 | } 349 | bytesRead = Serial.readBytes(serCmdIOBuf, bufLen); // read the program data bytes 350 | 351 | //Ask for more bytes from the PC now, then send the current buffer load to the C64 352 | #if !defined(__AVR_ATmega328P__) //Not suitable for Arduino uno 353 | if (bufEnd != 'b') Serial.write('R'); 354 | #endif 355 | //Send the program data bytes via epyx fastload protocol 356 | ATOMIC_BLOCK(ATOMIC_FORCEON) { 357 | 358 | m_iec.setClock(true); 359 | m_iec.setData(true); 360 | 361 | // send number of bytes in sector 362 | if (asm_epyxcart_send_byte(bufLen)) { 363 | Log("epyxFastloadProgram, length fail"); 364 | break; 365 | } 366 | 367 | // send data 368 | for (i=0; i(gROMFileData); 405 | 406 | //Switchover to full epyx fastload via semi-fastload gijoe protocol 407 | 408 | //Initial handshake 409 | noInterrupts(); 410 | m_iec.setData(true); 411 | m_iec.setClock(false); 412 | 413 | while(m_iec.getData() != false) 414 | m_iec.setClock(true); 415 | interrupts(); 416 | 417 | //Receive and checksum stage 2 418 | for (j=0; j<256; j++) { 419 | noInterrupts(); 420 | b = m_iec.gijoe_read_byte(); 421 | interrupts(); 422 | if (b < 0) { 423 | Log("epyxFastloadROM, read checksum error"); 424 | return; 425 | } 426 | 427 | if (j < 237) 428 | //Stage 2 has some junk bytes at the end, ignore them 429 | checksum ^= b; 430 | } 431 | 432 | //Check for known stage2 loaders 433 | if (checksum != 0x91 && checksum != 0x5b) { 434 | Log("epyxFastloadROM, checksum total error"); 435 | } 436 | 437 | //Receive file name length 438 | noInterrupts(); 439 | j = m_iec.gijoe_read_byte(); 440 | interrupts(); 441 | if (j < 0) { 442 | Log("epyxFastloadROM, file length error"); 443 | } 444 | 445 | //Read the file name bytes, not needed for this ROM version 446 | do { 447 | noInterrupts(); 448 | b = m_iec.gijoe_read_byte(); 449 | interrupts(); 450 | if (b < 0) { 451 | Log("epyxFastloadROM, file name error"); 452 | break; 453 | } 454 | 455 | j--; 456 | } while (j > 0); 457 | 458 | m_iec.setClock(false); 459 | 460 | pos = 0; 461 | bufLen = min(gROMFileSize, MAX_BYTES_PER_REQUEST-2); 462 | 463 | // Transfer data via full epyx fastload protocol 464 | ATOMIC_BLOCK(ATOMIC_FORCEON) { 465 | 466 | while (1) { 467 | m_iec.setClock(true); 468 | m_iec.setData(true); 469 | 470 | // send number of bytes in sector 471 | if (asm_epyxcart_send_byte(lowByte(bufLen))) { //Just the low byte of length is needed (is max 255 $FF) 472 | Log("epyxFastloadROM, length fail"); 473 | break; 474 | } 475 | 476 | for (i=0; i= gROMFileSize) { 492 | break; 493 | } 494 | 495 | // read next sector 496 | m_iec.setClock(false); 497 | bufLen = min((gROMFileSize - pos), MAX_BYTES_PER_REQUEST-2); 498 | 499 | } // while 500 | } // atomic 501 | 502 | m_iec.setClock(true); 503 | m_iec.setData(true); 504 | 505 | } // epyxFastloadROM 506 | #endif 507 | 508 | 509 | //Open file section 510 | //cmd is a type 511 | // code is 2 nibbles upper = command, lower = channel 512 | // upper should be ATN_CODE_OPEN (value 240), lower should be read (value 0), i.e. set to ATN_CODE_OPEN 513 | // str will be the file name 514 | // strlen is the length of the file name string 515 | void Interface::handleATNCmdCodeOpen(IEC::ATNCmd& cmd) 516 | { 517 | uint8_t bufLen = 3; //Allow for 'O' (open), file/command length and channel 518 | 519 | serCmdIOBuf[0] = 'O'; 520 | serCmdIOBuf[2] = cmd.code bitand 0xF; //channel 521 | memcpy(&serCmdIOBuf[bufLen], cmd.str, cmd.strLen); 522 | bufLen += cmd.strLen; 523 | serCmdIOBuf[1] = bufLen; //file/command length 524 | Serial.write((const byte*)serCmdIOBuf, bufLen); //send instruction to PC 525 | 526 | } // handleATNCmdCodeOpen 527 | 528 | 529 | // Handle open command response and talk to Commodore, sending file and listing data 530 | void Interface::handleATNCmdCodeDataTalk(byte chan) 531 | { 532 | 533 | while (!Serial.available()); // Wait for a response from the PC 534 | char r = Serial.peek(); // Peek at response (this leaves the byte in the serial buffer) 535 | switch(r) { 536 | case 'B': case 'b': 537 | sendFile(); //Load program on Commodore 538 | break; 539 | 540 | case 'L': case 'l': 541 | sendListing(); //Directory listing on Commodore 542 | break; 543 | 544 | default: 545 | m_iec.sendFNF(); //Error, return file not found on Commodore 546 | } 547 | 548 | while (Serial.available()) //Flush out read buffer 549 | Serial.read(); 550 | 551 | } // handleATNCmdCodeDataTalk 552 | 553 | 554 | // Listen for commands / data from the Commodore 555 | void Interface::handleATNCmdCodeDataListen() 556 | { 557 | while (!Serial.available()); // Wait for a response from the PC 558 | char r = Serial.read(); // Read response (this removes the byte in the serial buffer) 559 | if (r == 'W') { 560 | saveFile(); 561 | } 562 | else { 563 | m_iec.sendFNF(); 564 | } 565 | 566 | while (Serial.available()) //Flush out read buffer 567 | Serial.read(); 568 | 569 | } // handleATNCmdCodeDataListen 570 | 571 | 572 | void Interface::handleATNCmdClose() 573 | { 574 | 575 | Serial.write('C'); //Tell PC to close the file, no response expected 576 | 577 | } // handleATNCmdClose 578 | -------------------------------------------------------------------------------- /commodore_sketch/interface.h: -------------------------------------------------------------------------------- 1 | #ifndef INTERFACE_H 2 | #define INTERFACE_H 3 | 4 | // Enable this for verbose logging of IEC and CBM interfaces. 5 | //#define CONSOLE_DEBUG 6 | 7 | #include "iec_driver.h" 8 | #include "cbmdefines.h" 9 | 10 | // The base pointer of basic. 11 | #define C64_BASIC_START 0x0801 12 | 13 | class Interface 14 | { 15 | public: 16 | Interface(IEC& iec); 17 | virtual ~Interface() {} 18 | 19 | // The handler returns the current IEC state, see the iec_driver.hpp for possible states. 20 | byte handler(void); 21 | 22 | private: 23 | void saveFile(); 24 | void sendFile(); 25 | void sendListing(); 26 | bool removeFilePrefix(void); 27 | void sendLine(byte len, char* text, word &basicPtr); 28 | 29 | // handler helpers 30 | void handleATNCmdCodeOpen(IEC::ATNCmd &cmd); 31 | void handleATNCmdCodeDataTalk(byte chan); 32 | void handleATNCmdCodeDataListen(); 33 | void handleATNCmdClose(); 34 | void epyxFastloadProgram(); 35 | void epyxFastloadROM(); 36 | 37 | // our iec low level driver: 38 | IEC& m_iec; 39 | 40 | // atn command buffer struct 41 | IEC::ATNCmd& m_cmd; 42 | 43 | }; 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /commodore_sketch/log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | 4 | // Comment out for no logging to host 5 | //#define LOG_ENABLED 6 | #ifdef LOG_ENABLED 7 | 8 | inline void Log(char* msg) 9 | { 10 | Serial.print("D:"); //D for debug 11 | Serial.println(msg); 12 | } 13 | 14 | #else 15 | 16 | #define Log(msg) 17 | 18 | #endif // LOG_ENABLED 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /commodore_sketch/promicro/epyxfastload.S: -------------------------------------------------------------------------------- 1 | #include "epyxfastload.h" 2 | #include 3 | 4 | #ifndef CONFIG_MCU_FREQ 5 | # error "CONFIG_MCU_FREQ must be defined for delay cycle calculation." 6 | #endif 7 | 8 | .section .text 9 | 10 | ;; =================================================================== 11 | ;; Utility routines 12 | ;; =================================================================== 13 | 14 | ; RCALL and RET each take an additional cycle on MCUs with >128kB 15 | ; program memory (and therefore a 22 bit PC). 16 | ; These are defined as offset to the (16 bit PC) base value instead of 17 | ; absolute cycles because that simplifies delay cycle calculation. 18 | #ifndef __AVR_3_BYTE_PC__ 19 | # define RCALL_OFFSET 0 20 | # define RET_OFFSET 0 21 | #else 22 | # define RCALL_OFFSET 1 23 | # define RET_OFFSET 1 24 | #endif 25 | 26 | ;; Used by the macros below, don't call directly 27 | nop 28 | nop 29 | delay_loop: 30 | dec r18 ; 1 (ldi) + 3*#r18 - 1 (last brne not taken) 31 | brne delay_loop 32 | delay_7: ; 3+4=7 (16 bit PC) / 4+5=9 (22 bit PC) 33 | ret 34 | 35 | ;; This macro waits for the specified number of cycles 36 | ;; Uses r18 37 | .macro delay_cycles num 38 | .if \num >= 10 + RCALL_OFFSET + RET_OFFSET 39 | ldi r18, (\num - (7 + RCALL_OFFSET + RET_OFFSET)) / 3 40 | rcall delay_loop - 2*((\num - (10 + RCALL_OFFSET + RET_OFFSET)) % 3) 41 | .elseif \num >= 7 + RCALL_OFFSET + RET_OFFSET 42 | rcall delay_7 43 | delay_cycles \num - (7 + RCALL_OFFSET + RET_OFFSET) 44 | .elseif \num >= 2 45 | rjmp .+0 46 | delay_cycles \num-2 47 | .elseif \num == 1 48 | nop 49 | .endif 50 | .endm 51 | 52 | 53 | ;; Delay in 0.5us resolution. Only used if the additional resolution is 54 | ;; necessary, most places use delay_us below (which calls this macro). 55 | ;; 56 | ;; offset is the number of cycles to add to the result of the 57 | ;; conversion from [us]. Can be either positive or negative, but a 58 | ;; negative value can result in an illegal (ie negative) delay. 59 | .macro delay_05us us05:req, offset=0 60 | .if ((\us05)*CONFIG_MCU_FREQ%(2*1000000) != 0) || \ 61 | ((\us05)*CONFIG_MCU_FREQ/(2*1000000)+\offset < 0) 62 | .error "Invalid delay value and/or CONFIG_MCU_FREQ" 63 | .endif 64 | delay_cycles (\us05) * CONFIG_MCU_FREQ/(2*1000000) + \offset 65 | .endm 66 | 67 | ;; Delay in 1us resolution. 68 | .macro delay_us us:req, offset=0 69 | delay_05us 2*(\us), \offset 70 | .endm 71 | 72 | ;; send bits 7 and 5 of r0 to clock/data 73 | ;; masked contents of IEC_OUTPUT expected in r19 74 | ;; 8 (or 9 for 22 bit PC MCUs) cycles from rcall to out, 4 (or 5) to return 75 | epyx_bitpair: 76 | ;; rcall - 3 or 4 77 | bst r0, 7 ; 1 ; Store bit 7 of r0 into T flag 78 | bld r19, IEC_OPIN_CLOCK ; 1 ; Load T flag into CLOCK bit of r19 79 | bst r0, 5 ; 1 ; Store bit 5 of r0 into T flag 80 | bld r19, IEC_OPIN_DATA ; 1 ; Load T flag into DATA bit of r19 81 | out _SFR_IO_ADDR(IEC_OUTPUT_F), r19 ; 1 82 | ret ; 4 or 5 83 | 84 | ;; 85 | ;; Send a byte using the Epyx Fastload cartridge protocol 86 | ;; 87 | .global asm_epyxcart_send_byte 88 | asm_epyxcart_send_byte: 89 | ;; DATA and CLOCK high 90 | sbi _SFR_IO_ADDR(IEC_OUTPUT_F), IEC_OPIN_DATA ;Set pin to high 91 | sbi _SFR_IO_ADDR(IEC_OUTPUT_F), IEC_OPIN_CLOCK ;Set pin to high 92 | delay_us 1 93 | 94 | ;; prepare data 95 | in r19, _SFR_IO_ADDR(IEC_OUTPUT_F) ;Read port F (set of pin values) into r19 96 | andi r19, ~(IEC_OBIT_DATA|IEC_OBIT_CLOCK|IEC_OBIT_SRQ) ;ORs the values of the pins (8 bit values each) and bitwise inverts the result. ATN is on its own port and excluded 97 | 98 | ;; wait for DATA high or ATN low 99 | 1: sbis _SFR_IO_ADDR(IEC_INPUT_B), IEC_OPIN_ATN 100 | rjmp epyxcart_atnabort 101 | sbis _SFR_IO_ADDR(IEC_INPUT_F), IEC_OPIN_DATA 102 | rjmp 1b 103 | 104 | com r24 ; 1 ; Flip all the bits / aka bitwise inversion / aka one's complement 105 | mov r0, r24 ; 1 106 | delay_us 10, -8-RCALL_OFFSET ; Used with the com r24 above 107 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 7 and 5 108 | 109 | lsl r0 ; 1 110 | delay_us 10, -13-RET_OFFSET-RCALL_OFFSET 111 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 6 and 4 112 | 113 | swap r24 ; 1 114 | mov r0, r24 ; 1 115 | delay_us 10, -14-RET_OFFSET-RCALL_OFFSET 116 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 3 and 1 117 | 118 | lsl r0 ; 1 119 | delay_us 10, -13-RET_OFFSET-RCALL_OFFSET 120 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 2 and 0 121 | 122 | delay_us 20, -RET_OFFSET ; final delay so the data stays valid long enough 123 | 124 | clr r24 125 | ret 126 | 127 | epyxcart_atnabort: 128 | ldi r24, 1 129 | ret 130 | 131 | .end 132 | -------------------------------------------------------------------------------- /commodore_sketch/promicro/epyxfastload.h: -------------------------------------------------------------------------------- 1 | //Mixed board pin to AVR chip pin mappings across port B and F 2 | # define IEC_INPUT_B PINB 3 | # define IEC_OUTPUT_B PORTB 4 | # define IEC_OPIN_ATN PB5 //pin 9 5 | 6 | # define IEC_INPUT_F PINF 7 | # define IEC_OUTPUT_F PORTF 8 | # define IEC_OPIN_SRQ PF5 //pin 20 9 | # define IEC_OPIN_DATA PF6 //pin 19 10 | # define IEC_OPIN_CLOCK PF7 //pin 18 11 | 12 | # define IEC_OBIT_ATN _BV(IEC_OPIN_ATN) 13 | # define IEC_OBIT_DATA _BV(IEC_OPIN_DATA) 14 | # define IEC_OBIT_CLOCK _BV(IEC_OPIN_CLOCK) 15 | # define IEC_OBIT_SRQ _BV(IEC_OPIN_SRQ) 16 | 17 | # define CONFIG_MCU_FREQ 16000000 18 | 19 | #ifdef __ASSEMBLER__ 20 | 21 | .global asm_epyxcart_send_byte 22 | 23 | #endif 24 | 25 | #ifndef __ASSEMBLER__ 26 | 27 | extern "C" uint8_t asm_epyxcart_send_byte(uint8_t byte); 28 | 29 | #endif -------------------------------------------------------------------------------- /commodore_sketch/uno/epyxfastload.S: -------------------------------------------------------------------------------- 1 | #include "epyxfastload.h" 2 | #include 3 | 4 | #ifndef CONFIG_MCU_FREQ 5 | # error "CONFIG_MCU_FREQ must be defined for delay cycle calculation." 6 | #endif 7 | 8 | .section .text 9 | 10 | ;; =================================================================== 11 | ;; Utility routines 12 | ;; =================================================================== 13 | 14 | ; RCALL and RET each take an additional cycle on MCUs with >128kB 15 | ; program memory (and therefore a 22 bit PC). 16 | ; These are defined as offset to the (16 bit PC) base value instead of 17 | ; absolute cycles because that simplifies delay cycle calculation. 18 | #ifndef __AVR_3_BYTE_PC__ 19 | # define RCALL_OFFSET 0 20 | # define RET_OFFSET 0 21 | #else 22 | # define RCALL_OFFSET 1 23 | # define RET_OFFSET 1 24 | #endif 25 | 26 | ;; Used by the macros below, don't call directly 27 | nop 28 | nop 29 | delay_loop: 30 | dec r18 ; 1 (ldi) + 3*#r18 - 1 (last brne not taken) 31 | brne delay_loop 32 | delay_7: ; 3+4=7 (16 bit PC) / 4+5=9 (22 bit PC) 33 | ret 34 | 35 | ;; This macro waits for the specified number of cycles 36 | ;; Uses r18 37 | .macro delay_cycles num 38 | .if \num >= 10 + RCALL_OFFSET + RET_OFFSET 39 | ldi r18, (\num - (7 + RCALL_OFFSET + RET_OFFSET)) / 3 40 | rcall delay_loop - 2*((\num - (10 + RCALL_OFFSET + RET_OFFSET)) % 3) 41 | .elseif \num >= 7 + RCALL_OFFSET + RET_OFFSET 42 | rcall delay_7 43 | delay_cycles \num - (7 + RCALL_OFFSET + RET_OFFSET) 44 | .elseif \num >= 2 45 | rjmp .+0 46 | delay_cycles \num-2 47 | .elseif \num == 1 48 | nop 49 | .endif 50 | .endm 51 | 52 | 53 | ;; Delay in 0.5us resolution. Only used if the additional resolution is 54 | ;; necessary, most places use delay_us below (which calls this macro). 55 | ;; 56 | ;; offset is the number of cycles to add to the result of the 57 | ;; conversion from [us]. Can be either positive or negative, but a 58 | ;; negative value can result in an illegal (ie negative) delay. 59 | .macro delay_05us us05:req, offset=0 60 | .if ((\us05)*CONFIG_MCU_FREQ%(2*1000000) != 0) || \ 61 | ((\us05)*CONFIG_MCU_FREQ/(2*1000000)+\offset < 0) 62 | .error "Invalid delay value and/or CONFIG_MCU_FREQ" 63 | .endif 64 | delay_cycles (\us05) * CONFIG_MCU_FREQ/(2*1000000) + \offset 65 | .endm 66 | 67 | ;; Delay in 1us resolution. 68 | .macro delay_us us:req, offset=0 69 | delay_05us 2*(\us), \offset 70 | .endm 71 | 72 | ;; send bits 7 and 5 of r0 to clock/data 73 | ;; masked contents of IEC_OUTPUT expected in r19 74 | ;; 8 (or 9 for 22 bit PC MCUs) cycles from rcall to out, 4 (or 5) to return 75 | epyx_bitpair: 76 | ;; rcall - 3 or 4 77 | bst r0, 7 ; 1 ; Store bit 7 of r0 into T flag 78 | bld r19, IEC_OPIN_CLOCK ; 1 ; Load T flag into CLOCK bit of r19 79 | bst r0, 5 ; 1 ; Store bit 5 of r0 into T flag 80 | bld r19, IEC_OPIN_DATA ; 1 ; Load T flag into DATA bit of r19 81 | out _SFR_IO_ADDR(IEC_OUTPUT_D), r19 ; 1 82 | ret ; 4 or 5 83 | 84 | ;; 85 | ;; Send a byte using the Epyx Fastload cartridge protocol 86 | ;; 87 | .global asm_epyxcart_send_byte 88 | asm_epyxcart_send_byte: 89 | ;; DATA and CLOCK high 90 | sbi _SFR_IO_ADDR(IEC_OUTPUT_D), IEC_OPIN_DATA ;Set pin to high 91 | sbi _SFR_IO_ADDR(IEC_OUTPUT_D), IEC_OPIN_CLOCK ;Set pin to high 92 | delay_us 1 93 | 94 | ;; prepare data 95 | in r19, _SFR_IO_ADDR(IEC_OUTPUT_D) ;Read port D (set of pin values) into r19 96 | andi r19, ~(IEC_OBIT_DATA|IEC_OBIT_CLOCK|IEC_OBIT_ATN|IEC_OBIT_SRQ) ;ORs the values of the pins (8 bit values each) and bitwise inverts the result 97 | 98 | ;; wait for DATA high or ATN low 99 | 1: sbis _SFR_IO_ADDR(IEC_INPUT_D), IEC_OPIN_ATN 100 | rjmp epyxcart_atnabort 101 | sbis _SFR_IO_ADDR(IEC_INPUT_D), IEC_OPIN_DATA 102 | rjmp 1b 103 | 104 | com r24 ; 1 ; Flip all the bits / aka bitwise inversion / aka one's complement 105 | mov r0, r24 ; 1 106 | delay_us 10, -8-RCALL_OFFSET ; Used with the com r24 above 107 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 7 and 5 108 | 109 | lsl r0 ; 1 110 | delay_us 10, -13-RET_OFFSET-RCALL_OFFSET 111 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 6 and 4 112 | 113 | swap r24 ; 1 114 | mov r0, r24 ; 1 115 | delay_us 10, -14-RET_OFFSET-RCALL_OFFSET 116 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 3 and 1 117 | 118 | lsl r0 ; 1 119 | delay_us 10, -13-RET_OFFSET-RCALL_OFFSET 120 | rcall epyx_bitpair ; 8+4 or 9+5 - bits 2 and 0 121 | 122 | delay_us 20, -RET_OFFSET ; final delay so the data stays valid long enough 123 | 124 | clr r24 125 | ret 126 | 127 | epyxcart_atnabort: 128 | ldi r24, 1 129 | ret 130 | 131 | .end 132 | -------------------------------------------------------------------------------- /commodore_sketch/uno/epyxfastload.h: -------------------------------------------------------------------------------- 1 | //All board pin to AVR chip pin mappings are located on port D 2 | # define IEC_INPUT_D PIND 3 | # define IEC_OUTPUT_D PORTD 4 | # define IEC_OPIN_ATN PD2 //pin 2 5 | # define IEC_OPIN_CLOCK PD3 //pin 3 6 | # define IEC_OPIN_DATA PD4 //pin 4 7 | # define IEC_OPIN_SRQ PD5 //pin 5 8 | 9 | # define IEC_OBIT_ATN _BV(IEC_OPIN_ATN) 10 | # define IEC_OBIT_DATA _BV(IEC_OPIN_DATA) 11 | # define IEC_OBIT_CLOCK _BV(IEC_OPIN_CLOCK) 12 | # define IEC_OBIT_SRQ _BV(IEC_OPIN_SRQ) 13 | 14 | # define CONFIG_MCU_FREQ 16000000 15 | 16 | #ifdef __ASSEMBLER__ 17 | 18 | .global asm_epyxcart_send_byte 19 | 20 | #endif 21 | 22 | #ifndef __ASSEMBLER__ 23 | 24 | extern "C" uint8_t asm_epyxcart_send_byte(uint8_t byte); 25 | 26 | #endif -------------------------------------------------------------------------------- /d64driver.js: -------------------------------------------------------------------------------- 1 | //D64 special constants 2 | 3 | //Track start locations 4 | // 17 with 21 sectors 5 | // 7 with 19 sectors 6 | // 6 with 18 sectors 7 | // 5 with 17 sectors 8 | //Each sector has 256 bytes 9 | const TRACK_START_POS = 10 | [ 11 | 0, 5376, 10752, 16128, 21504, 26880, 32256, 37632, 43008, 48384, 53760, 59136, 64512, 69888, 75264, 80640, 86016, 12 | 91392, 96256, 101120, 105984, 110848, 115712, 120576, 13 | 125440, 130048, 134656, 139264, 143872, 148480, 14 | 153088, 157440, 161792, 166144, 170496 15 | ]; 16 | 17 | const BYTES_IN_SECTOR = 256; 18 | const DISK_NAME_POS = TRACK_START_POS[17] + 144; //Track 18, Sector 0, Disk name offset 142. (((17*21)*256))+144 19 | const DIR_START_POS = TRACK_START_POS[17] + BYTES_IN_SECTOR; //Track 18, Sector 1 20 | 21 | class d64driver { 22 | constructor(file) { 23 | 24 | this.type = "D64"; 25 | this.fileRef = file; 26 | this.progName = file.name; 27 | this.progSize = file.size; 28 | this.subProgSize = 0; 29 | this.progPos = 0; 30 | this.dirPos = 0; 31 | this.linkTrack = 0; 32 | this.linkSector = 0; 33 | this.dirListing = []; 34 | 35 | /* 36 | A directory listing entry has this structure 37 | dirEntry { 38 | byte linkTrack // At the start of each block only, track and sector for the next directory block of max 8 entries 39 | byte linkSector 40 | byte fileType // Type of file within the D64 parent disk file, e.g. PRG, SEQ, REL 41 | byte startTrack // Track and sector where the program starts 42 | byte startSector 43 | byte[16] progName // File/program name 44 | byte sideTrack // For REL files only (unused here) 45 | byte sideSector // For REL files only (unused here) 46 | byte recordLength // For REL files only (unused here) 47 | byte[6] unused // For GEOS disks only (unused here) 48 | byte blocksLowByte // File/program block size (low byte) 49 | byte blocksHighByte // File/program block size (high byte) 50 | } // total of 32 bytes 51 | */ 52 | 53 | } 54 | 55 | //Read binary file into byte array 56 | async readBinaryFile() { 57 | 58 | const buffer = await this.fileRef.arrayBuffer(); //Read file contents into array buffer 59 | this.progBytes = new Uint8Array(buffer); //Get byte array for the array buffer 60 | 61 | } //readBinaryFile 62 | 63 | //Return buffer load of data from current program position 64 | async getBuffer() { 65 | 66 | this.linkTrack = this.progBytes[this.progPos]; 67 | this.linkSector = this.progBytes[this.progPos += 1]; 68 | const currentPos = this.progPos += 1; 69 | if (this.linkTrack != 0 ) { 70 | //Position to the next block given by the link track/sector 71 | this.progPos = TRACK_START_POS[this.linkTrack-1] + (this.linkSector * BYTES_IN_SECTOR); 72 | //Return entire block data 73 | return {protocol: Uint8Array.from([BUF_NORMAL, MAX_BYTES_PER_REQUEST]), payload: this.progBytes.slice(currentPos, currentPos + MAX_BYTES_PER_REQUEST)}; 74 | } 75 | else { 76 | // On the last block (no chain to another block), the sector byte holds the position of the last byte used in this block 77 | // linkSector is zero based (e.g. if equals 129, then 130 bytes are used in the block), so adjust packet size, data length and payload contents 78 | // Note that the track and sector (first 2 bytes, already read) are counted as bytes used, but are excluded in the payload sent 79 | return {protocol: Uint8Array.from([BUF_END, this.linkSector-1]), payload: this.progBytes.slice(currentPos, currentPos + this.linkSector-1)}; 80 | } 81 | 82 | } //getBuffer 83 | 84 | //Create a directory list 85 | async buildDirectory() { 86 | 87 | const dirEntry = new Uint8Array(new ArrayBuffer(32)); 88 | this.dirListing = []; 89 | 90 | //Add disk name 91 | dirEntry.set(this.progBytes.slice(DISK_NAME_POS, DISK_NAME_POS + 16),5); 92 | this.dirListing.push(Array.from(dirEntry)); 93 | 94 | //Add directory entry lines 95 | let progPos = DIR_START_POS; 96 | do { 97 | this.linkTrack = this.progBytes[progPos]; 98 | this.linkSector = this.progBytes[progPos += 1]; 99 | progPos --; 100 | 101 | //Get directory entries in block, max 8, each entry is 32 bytes 102 | for (let i = 0; i < 8; i++) { 103 | dirEntry.set(this.progBytes.slice(progPos, progPos += 32)); 104 | if (dirEntry[2] > 0) { //Only include non-deleted entries 105 | this.dirListing.push(Array.from(dirEntry)); 106 | } 107 | } 108 | 109 | //Continue to the next block given by the link track/sector 110 | if (this.linkTrack > 0) { 111 | progPos = TRACK_START_POS[this.linkTrack-1] + (this.linkSector * BYTES_IN_SECTOR); 112 | } 113 | 114 | } while (this.linkTrack > 0); 115 | 116 | } //buildDirectory 117 | 118 | //Open program by finding it 119 | async openProgram(bytesProgName) { 120 | 121 | let progName = String.fromCharCode(...bytesProgName).replace("\r", "").replace("\n", "").trim(); 122 | let found = this.#findProgram(progName); 123 | 124 | //Match not found, try again with less strict criteria 125 | if (!found) { 126 | //Remove part of name after comma, example "BTSCORES,S" becomes "BTSCORES" (S means SEQ file type) 127 | const pos = progName.indexOf(","); 128 | if (pos > 0) { 129 | progName = progName.substring(0, pos); 130 | found = this.#findProgram(progName); 131 | } 132 | } 133 | return found; 134 | 135 | } //openProgram 136 | 137 | //Return directory lines 138 | async getDirectoryLine() { 139 | 140 | const dirEntry = this.dirListing[this.dirPos]; 141 | 142 | let prefix = []; 143 | let suffix = []; 144 | if (this.dirPos == 0) { //Disk header is reverse-video in quotes 145 | prefix = [REVERSE_CHAR, QUOTE_CHAR]; 146 | suffix = [QUOTE_CHAR]; 147 | } 148 | else { //Normal entries are aligned after the block size, within quotes and end with the program type 149 | const fileBlocks = dirEntry[30] | dirEntry[31] << 8; 150 | prefix = new Array(5-String(fileBlocks).length).fill(SPACE_CHAR).concat([QUOTE_CHAR]); 151 | suffix = [QUOTE_CHAR, SPACE_CHAR, SPACE_CHAR].concat(FILE_TYPES[(dirEntry[2] & 0x07)] || []); 152 | } 153 | 154 | //Assemble the line and return it 155 | const line = Uint8Array.from(dirEntry.slice(30,32).concat(prefix).concat(dirEntry.slice(5,21).map(this.#replaceNonBreakSpace)).concat(suffix)); //block size (2 bytes) and entry text 156 | if (this.dirPos >= this.dirListing.length-1) { //Last directory entry 157 | this.dirPos = 0; 158 | return {protocol: Uint8Array.from([DIR_END, line.length]), payload: line}; 159 | } 160 | else { 161 | this.dirPos += 1; 162 | return {protocol: Uint8Array.from([DIR_NORMAL, line.length]), payload: line}; 163 | } 164 | 165 | } //getDirectoryLine 166 | 167 | //Private method: Match a program name in the directory list (or wildcard match), then set the program position to the resulting sector/track position 168 | #findProgram(progName) { 169 | 170 | let found = false; 171 | 172 | //Check for a program name match in the directory (excluding the header entry) 173 | for (let i = 1; i < this.dirListing.length; i++) { 174 | const dirEntry = this.dirListing[i]; 175 | const matchProgName = (String.fromCharCode(...dirEntry.slice(5,21))).trim(); 176 | 177 | const pos = progName.indexOf("*"); 178 | const matchPartial = pos > 0 ? matchProgName.substring(0, pos) + "*" : "!NOTFOUND!"; 179 | 180 | //If wildcard character is used, set the program position to the first PRG entry 181 | if (progName == "*" || progName == matchProgName || progName == matchPartial) { 182 | if ([129,130,193,194].includes(dirEntry[2])) { //type is SEQ, PRG, SEQ-locked, PRG-locked 183 | this.linkTrack = dirEntry[3]; 184 | this.linkSector = dirEntry[4]; 185 | this.progName = matchProgName; 186 | this.progPos = TRACK_START_POS[this.linkTrack-1] + (this.linkSector * BYTES_IN_SECTOR); 187 | this.subProgSize = (dirEntry[30] + (dirEntry[31] << 8)) * (BYTES_IN_SECTOR-2); //Block size less the track/sector bytes in each block 188 | found = true; 189 | break; 190 | } 191 | } 192 | } 193 | 194 | return found; 195 | 196 | } 197 | 198 | //Private method: Remove non breaking space character 199 | #replaceNonBreakSpace(element) { 200 | return element != NON_SPACE_CHAR ? element : SPACE_CHAR; 201 | } 202 | 203 | } -------------------------------------------------------------------------------- /docs/app-instructions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/docs/app-instructions.png -------------------------------------------------------------------------------- /docs/app-start-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/docs/app-start-screen.png -------------------------------------------------------------------------------- /docs/arduino-uno-to-6-pin-male-din.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/docs/arduino-uno-to-6-pin-male-din.png -------------------------------------------------------------------------------- /docs/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/docs/components.png -------------------------------------------------------------------------------- /docs/pro-micro-to-6-pin-male-din.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/docs/pro-micro-to-6-pin-male-din.png -------------------------------------------------------------------------------- /docs/pro-micro-usb-beetle-to-6-pin-male-din.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/docs/pro-micro-usb-beetle-to-6-pin-male-din.png -------------------------------------------------------------------------------- /docs/pro-micro-usb-beetle-with-case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/docs/pro-micro-usb-beetle-with-case.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Commodore Loader 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 | 30 |
31 |
32 | Home 33 |
34 | Open files 35 | 36 |
37 | Connect 38 |
39 | Search web 40 |
41 |
42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 | 52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_BAUD_RATE = 115200; 2 | const DEFAULT_RETRO = "c64"; //Used for logo and artwork images 3 | const ALTERNATE_RETRO = "vic20"; //Used for logo and artwork images 4 | const PROGRAM_SEARCH_URL = "https://www.google.com/search?q="; //Used on image artwork click 5 | const $ = id => document.getElementById(id); 6 | 7 | //Setup the main elements and invoke objLoader from class 8 | function setup() { 9 | 10 | const urlParams = new URLSearchParams(window.location.search); 11 | const testMode = urlParams.get('testMode') ? true : false; 12 | 13 | //Set default retro from local storage value 14 | let selectedRetro = localStorage.getItem("selectedRetro"); 15 | if (!selectedRetro) { 16 | selectedRetro = DEFAULT_RETRO; 17 | localStorage.setItem("selectedRetro", selectedRetro); 18 | } 19 | 20 | const objLoader = new jsCBMLoader(DEFAULT_BAUD_RATE); 21 | if (objLoader) { 22 | objLoader.on("data", receiveData); 23 | objLoader.on("progress", updateProgressBar); 24 | } 25 | 26 | $("imgConnect").onclick = function() { 27 | if (objLoader) { 28 | if (objLoader.port) { 29 | objLoader.closePort(); 30 | } 31 | else { 32 | objLoader.openPort(); 33 | } 34 | } 35 | }; 36 | 37 | $("imgFileSelect").onclick = function() { 38 | $("fileInput").click(); 39 | }; 40 | $("fileInput").oninput = function() { 41 | 42 | $("selProgList").length = 0; 43 | $("imgFileSelect").src = "./resources/files-empty.png"; 44 | $("divReceivedMsg").innerHTML = ""; 45 | $("divDirectory").style.display = "none"; 46 | $("divDirectory").innerHTML = ""; 47 | $("imgArt").style.display = "none"; 48 | $("imgArt").classList.remove("fade"); 49 | $("imgArt").classList.add("show"); 50 | $("divProgressBar").style.display = "none"; 51 | $("divTitle").innerText = ""; 52 | $("divInfo").innerHTML = ""; 53 | 54 | const file = $("fileInput").files[0]; 55 | if (!file) { 56 | return; 57 | } 58 | 59 | //Populate program selection list 60 | for (let i = 0; i < $("fileInput").files.length; i++) { 61 | $("selProgList").add(new Option($("fileInput").files[i].name)); 62 | } 63 | 64 | //Change icon to indicated selected 65 | if ($("selProgList").length > 0) { 66 | $("imgFileSelect").src = "./resources/files-available.png"; 67 | } 68 | else { 69 | $("imgFileSelect").src = "./resources/files-empty.png"; 70 | } 71 | 72 | //Only one item in the list, select it 73 | if ($("selProgList").length == 1) { 74 | if (objLoader) { 75 | objLoader.setDriverForFile($("fileInput").files[0], testMode) 76 | .catch(err => { 77 | console.error(`Error reading file:`, err); 78 | }); 79 | } 80 | $("selProgList")[0].selected = true; 81 | setImageArt(selectedRetro); 82 | } 83 | }; 84 | 85 | $("selProgList").onchange = function() { 86 | //console.log(`${$("selProgList").selectedIndex} ${$("selProgList").value}`); 87 | //console.log($("fileInput").files[$("selProgList").selectedIndex]); 88 | if (objLoader) { 89 | objLoader.setDriverForFile($("fileInput").files[$("selProgList").selectedIndex], testMode) 90 | .catch(err => { 91 | console.error(`Error reading file:`, err); 92 | }); 93 | setImageArt(selectedRetro); 94 | } 95 | }; 96 | 97 | //Assign the default logo and allow it to be changed 98 | $("imgLogo").src = `./resources/logo_${selectedRetro}.png`; 99 | $("imgLogo").onclick = function() { 100 | selectedRetro = (selectedRetro == DEFAULT_RETRO ? ALTERNATE_RETRO : DEFAULT_RETRO); 101 | localStorage.setItem("selectedRetro", selectedRetro); 102 | $("imgLogo").src = `./resources/logo_${selectedRetro}.png`; 103 | }; 104 | 105 | //Set the image art display attributes when found / not found 106 | $("imgArt").onerror = function() { 107 | this.style.display = "none"; 108 | $("imgArt").classList.add("fade"); 109 | $("imgArt").classList.remove("show"); 110 | $("divDirectory").style.display = ""; 111 | }; 112 | $("imgArt").onload = function() { 113 | this.style.display = ""; 114 | this.title = $("selProgList").value.split(".")[0]; 115 | }; 116 | 117 | //Toggle display of directory listing on top of image art 118 | $("imgArt").onclick = function() { 119 | toggleDir(); 120 | }; 121 | $("divDirectory").onclick = function() { 122 | toggleDir(); 123 | }; 124 | 125 | //Navigate to search URL for selected program 126 | $("imgWeb").onclick = function() { 127 | if ($("selProgList").selectedIndex > -1) { 128 | window.open(`${PROGRAM_SEARCH_URL}Commodore ${selectedRetro} ${$("imgArt").alt}`, '_blank').focus(); 129 | } 130 | }; 131 | 132 | } 133 | 134 | //Set image art for selected program file 135 | function setImageArt(selectedRetro) { 136 | const itemName = $("selProgList").value.split(".")[0]; 137 | $("imgArt").src = `./resources/art/${selectedRetro}/${itemName}-image.jpg`; 138 | $("imgArt").alt = itemName; 139 | $("divProgressBar").style.display = ""; 140 | $("divProgress").style.width = "0%"; 141 | $("divTitle").innerText = itemName; 142 | $("divInfo").innerHTML = `

${(selectedRetro == DEFAULT_RETRO ? info_c64 : info_vic20)[$("selProgList").value]||""}

`; 143 | } 144 | 145 | //Toggle show image or program directory 146 | function toggleDir() { 147 | if ($("divDirectory").style.display == "none") { 148 | $("imgArt").classList.add("fade"); 149 | $("imgArt").classList.remove("show"); 150 | $("divDirectory").style.display = ""; 151 | } 152 | else { 153 | $("divDirectory").style.display = "none"; 154 | $("imgArt").classList.remove("fade"); 155 | $("imgArt").classList.add("show"); 156 | } 157 | } 158 | 159 | //Update the received message span with Serial data received 160 | function receiveData(event) { 161 | 162 | switch (event.detail.code) { 163 | case "SERCON": 164 | $("imgConnect").src = "./resources/usb-connected.png"; 165 | break; 166 | case "SERDIS": 167 | $("imgConnect").src = "./resources/usb-disconnected.png"; 168 | break; 169 | case "ARDCON": 170 | $("imgConnect").src = "./resources/usb-handshake-ok.png"; 171 | break; 172 | } 173 | if (event.detail.code == "DIRLIST") { 174 | $("divDirectory").innerHTML = `
${event.detail.msg}
`; 175 | } 176 | else { 177 | $("divReceivedMsg").innerHTML = `

${event.detail.msg}

`; 178 | } 179 | 180 | } 181 | 182 | function updateProgressBar(event) { 183 | $("divProgress").style.width = `${event.detail.progress}%`; 184 | } 185 | 186 | //Run the setup function when the page is loaded 187 | document.addEventListener("DOMContentLoaded", setup); 188 | -------------------------------------------------------------------------------- /prgdriver.js: -------------------------------------------------------------------------------- 1 | class prgdriver { 2 | constructor(file) { 3 | 4 | this.type = "PRG"; 5 | this.fileRef = file; 6 | this.progName = file.name; 7 | this.progSize = file.size; 8 | this.subProgSize = file.size; 9 | this.blockSize = Math.ceil(file.size/256); 10 | 11 | } 12 | 13 | //Read binary file into byte array 14 | async readBinaryFile() { 15 | 16 | const buffer = await this.fileRef.arrayBuffer(); //Read file contents into array buffer 17 | this.progBytes = new Uint8Array(buffer); //Get byte array for the array buffer 18 | 19 | } //readBinaryFile 20 | 21 | //Return buffer load of data from current program position 22 | async getBuffer() { 23 | 24 | const currentPos = this.progPos; 25 | const packetSize = Math.min((this.progSize - currentPos), MAX_BYTES_PER_REQUEST); 26 | this.progPos += packetSize; 27 | return {protocol: Uint8Array.from([(packetSize == MAX_BYTES_PER_REQUEST ? BUF_NORMAL : BUF_END), packetSize]), payload: this.progBytes.slice(currentPos, currentPos + packetSize)}; 28 | 29 | } //getBuffer 30 | 31 | //Return directory line 32 | async getDirectoryLine() { 33 | 34 | const blockHigh = (this.blockSize >> 8) & 0xFF; 35 | const blockLow = this.blockSize & 0xFF; 36 | return {protocol: Uint8Array.from([DIR_END, this.progName.length+2]), payload: Uint8Array.from([blockLow,blockHigh].concat(Array.from(new TextEncoder().encode(this.progName.toUpperCase()))))}; 37 | 38 | } //getDirectoryLine 39 | 40 | //Open program, just position reset is required 41 | async openProgram(bytesProgName) { 42 | 43 | this.progPos = 0; 44 | return true; 45 | 46 | } //openProgram 47 | 48 | } -------------------------------------------------------------------------------- /resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/favicon.ico -------------------------------------------------------------------------------- /resources/files-available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/files-available.png -------------------------------------------------------------------------------- /resources/files-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/files-empty.png -------------------------------------------------------------------------------- /resources/info_c64.js: -------------------------------------------------------------------------------- 1 | const info_c64 = { 2 | "1942.d64":"Joystick port 2" 3 | ,"Abbaye Des Morts.d64":"Joystick port 2" 4 | ,"Alien 3.d64":"Joystick port 2" 5 | ,"Arc of Yesod.d64":"Joystick port 2" 6 | ,"Archon 2.d64":"Joystick port 2" 7 | ,"Archon.d64":"Joystick port 1" 8 | ,"Arkanoid.d64":"Joystick port 1" 9 | ,"Armalyte.d64":"Joystick port 2" 10 | ,"Bagman.d64":"Joystick port 2" 11 | ,"Barbarian 2.d64":"Joystick port 2" 12 | ,"Barbarian.d64":"Joystick port 2" 13 | ,"Battle Ships.d64":"Joystick port 2" 14 | ,"Beach Head 2.d64":"Joystick port 2" 15 | ,"Beach Head.d64":"Joystick port 2" 16 | ,"Biggles.d64":"Joystick port 2" 17 | ,"Biplanes.prg":"Joystick port 2" 18 | ,"Blue Max.d64":"Joystick port 2" 19 | ,"Bomb Jack.d64":"Joystick port 2" 20 | ,"Boulder Dash 2.d64":"Joystick port 1" 21 | ,"Boulder Dash 3.d64":"Joystick port 1" 22 | ,"Boulder Dash Construction Kit.d64":"Joystick port 1" 23 | ,"Boulder Dash.d64":"Joystick port 1" 24 | ,"Bruce Lee 2.d64":"Joystick port 2" 25 | ,"Bruce Lee Return of Fury.d64":"Joystick port 2" 26 | ,"Bruce Lee.d64":"Joystick port 1" 27 | ,"Bubble Bobble.d64":"Joystick port 2" 28 | ,"Burger Time.d64":"Joystick port 2" 29 | ,"Burnin Rubber.d64":"Joystick port 2" 30 | ,"C64anabalt.d64":"Joystick port 2" 31 | ,"Cab Hustle.d64":"Joystick port 2" 32 | ,"California Games.d64":"Joystick port 2" 33 | ,"Cauldron.d64":"Joystick port 2" 34 | ,"Cliff Hanger.d64":"Joystick port 2" 35 | ,"Colossus Chess 4.d64":"Joystick port 2. Wait 20 seconds after RUN, shift+J for joystick" 36 | ,"Commando.d64":"Joystick port 2" 37 | ,"Defender.d64":"Joystick port 1" 38 | ,"Desert Fox.d64":"Joystick port 2" 39 | ,"Dig Dug Revival.d64":"Joystick port 2" 40 | ,"Dino Eggs.d64":"Joystick port 2" 41 | ,"Doc Cosmos.d64":"Joystick port 2" 42 | ,"Donkey Kong Junior.d64":"Joystick port 2" 43 | ,"Donkey Kong.d64":"Joystick port 1" 44 | ,"Drip.d64":"Joystick port 2" 45 | ,"Druid.prg":"Joystick port 1" 46 | ,"Elite.d64":"Joystick port 2" 47 | ,"Emlyn Hughes Soccer.d64":"Joystick port 2. Game > Edit Team > Change COMPUTER to your name > Fire button twice > Tick > Play match" 48 | ,"Empire Strikes Back.d64":"Joystick port 2" 49 | ,"Enforcer.d64":"Joystick port 2" 50 | ,"Fist 2.d64":"Joystick port 2" 51 | ,"Fix-it Felix Jr.prg":"Joystick port 2" 52 | ,"Football Manager.d64":"Keyboard" 53 | ,"Fort Apocalypse 2.prg":"Joystick port 2" 54 | ,"Galencia.d64":"Joystick port 2" 55 | ,"Get Em DX.prg":"Joystick port 2" 56 | ,"Ghostbusters.d64":"Joystick port 1" 57 | ,"Ghosts 'n Goblins.d64":"Joystick port 2" 58 | ,"Ghouls 'n' Ghosts.d64":"Joystick port 2" 59 | ,"Great Giana Sisters.d64":"Joystick port 2" 60 | ,"Green Beret.d64":"Joystick port 2" 61 | ,"Grid Pix.prg":"Joystick port 2" 62 | ,"Gumshoe.d64":"Joystick port 2" 63 | ,"Hard 'n' Heavy.d64":"Joystick port 2" 64 | ,"Heli Rescue.d64":"Joystick port 2" 65 | ,"Iceblox.d64":"Joystick port 2" 66 | ,"Impossible Mission 2.d64":"Joystick port 2" 67 | ,"Impossible Mission Revised.d64":"Joystick port 2" 68 | ,"Impossible Mission.d64":"Joystick port 2" 69 | ,"International Football.d64":"Joystick port 2" 70 | ,"International Karate Plus.d64":"Joystick port 2" 71 | ,"International Karate.d64":"Joystick port 2. Press F1 to start" 72 | ,"Invert.d64":"Joystick port 2" 73 | ,"Jumpman.d64":"Joystick port 2" 74 | ,"Karateka.d64":"Joystick port 2" 75 | ,"Kobo 64.prg":"Joystick port 2" 76 | ,"Lady Pac.d64":"Joystick port 2" 77 | ,"Last Ninja 2.s1.d64":"Joystick port 2" 78 | ,"Last Ninja 2.s2.d64":"Joystick port 2" 79 | ,"Last Ninja.s1.d64":"Joystick port 2" 80 | ,"Last Ninja.s2.d64":"Joystick port 2" 81 | ,"Lester.prg":"Joystick port 2" 82 | ,"Lock 'n' Chase.d64":"Joystick port 2" 83 | ,"Lode Runner.d64":"Joystick port 2" 84 | ,"Manic Miner.d64":"Joystick port 2" 85 | ,"Mayhem in Monsterland.d64":"Joystick port 2" 86 | ,"Mediterranian Air War.prg":"Joystick port 1" 87 | ,"Microprose Soccer.d64":"Joystick port 2" 88 | ,"Mike Mech.d64":"Joystick port 2" 89 | ,"Millie and Molly.d64":"Joystick port 2" 90 | ,"Missile Defence.prg":"Joystick port 2" 91 | ,"Moon Cresta.d64":"Joystick port 2" 92 | ,"Mr Do.d64":"Joystick port 1" 93 | ,"Nebulus.d64":"Joystick port 2" 94 | ,"Night Knight.prg":"Joystick port 2" 95 | ,"Old Tower.prg":"Joystick port 2" 96 | ,"Ooze.d64":"Joystick port 2" 97 | ,"Orbital Rescue.d64":"Joystick port 2" 98 | ,"Outrun.d64":"Joystick port 1" 99 | ,"Oyup.d64":"Joystick port 2" 100 | ,"Pacman Arcade.d64":"Joystick port 2" 101 | ,"Pac-Man.d64":"Joystick port 2" 102 | ,"Paperboy.d64":"Joystick port 2" 103 | ,"Paradroid.d64":"Joystick port 2" 104 | ,"Pengo.prg":"Joystick port 2" 105 | ,"Penguin Tower.d64":"Joystick port 2" 106 | ,"Pitstop 2.d64":"Joystick port 2" 107 | ,"Pooyan.d64":"Joystick port 1" 108 | ,"Popeye.d64":"Joystick port 1" 109 | ,"Qix.d64":"Joystick port 1" 110 | ,"Qix.t64":"Joystick port 1" 111 | ,"Raid on Bungeling Bay.d64":"Joystick port 2" 112 | ,"Raid Over Moscow.d64":"Joystick port 2" 113 | ,"Rally Speedway.d64":"Joystick port 1" 114 | ,"RallyX.d64":"Joystick port 2" 115 | ,"Rambo First Blood Part 2.d64":"Joystick port 2" 116 | ,"Rampage.prg":"Joystick port 1" 117 | ,"Random Pac.d64":"Joystick port 2" 118 | ,"Rick Dangerous.d64":"Joystick port 2" 119 | ,"Robin of the Wood.d64":"Joystick port 2" 120 | ,"Robot Jet Action.prg":"Joystick port 2" 121 | ,"Robots Rumble.prg":"Joystick port 2" 122 | ,"Rodland.d64":"Joystick port 2" 123 | ,"R-Type.d64":"Joystick port 2" 124 | ,"Serpentine.d64":"Joystick port 1" 125 | ,"Shallow Domains.prg":"Joystick port 2" 126 | ,"Shamus.d64":"Joystick port 1" 127 | ,"Showdown.d64":"Joystick port 2" 128 | ,"Snafu 64.d64":"Joystick port 2" 129 | ,"Solomon's Key.d64":"Joystick port 2" 130 | ,"Space Pilot.d64":"Joystick port 2" 131 | ,"Spy Hunter.d64":"Joystick port 1" 132 | ,"Spy vs Spy 2.d64":"Joystick port 2" 133 | ,"Spy vs Spy 3.d64":"Joystick port 2" 134 | ,"Spy vs Spy.d64":"Joystick port 2" 135 | ,"Sub Hunter.d64":"Joystick port 2" 136 | ,"Summer Games 2.s1.d64":"Joystick port 2" 137 | ,"Summer Games 2.s2.d64":"Joystick port 2" 138 | ,"Summer Games.d64":"Joystick port 2" 139 | ,"Super Mario Bros.d64":"Joystick port 2" 140 | ,"Super Pipeline 2.d64":"Joystick port 2" 141 | ,"Super Pipeline.d64":"Joystick port 1" 142 | ,"Tenebra Macabre.prg":"Joystick port 2" 143 | ,"Train Escape to Normandy.d64":"Joystick port 2" 144 | ,"Turrican.s1.d64":"Joystick port 2" 145 | ,"Turrican.s2.d64":"Joystick port 2" 146 | ,"Tutankham Returns.d64":"Joystick port 2" 147 | ,"Uridium.d64":"Joystick port 2" 148 | ,"Vampire Vengeance.d64":"Joystick port 2" 149 | ,"Vegetables Deluxe.d64":"Joystick port 2" 150 | ,"Veggies vs Undead.prg":"Joystick port 2" 151 | ,"Volfied.d64":"Joystick port 2" 152 | ,"Way of the Exploding Fist.d64":"Joystick port 2" 153 | ,"Who Dares Wins 2.d64":"Joystick port 2" 154 | ,"Who Dares Wins.d64":"Joystick port 2" 155 | ,"WinGames.d64":"Joystick port 2" 156 | ,"Wizard of Wor.d64":"Joystick port 2" 157 | ,"Wizard of Wor.petscii.prg":"Joystick port 2" 158 | ,"Wizball.d64":"Joystick port 2" 159 | ,"Wolfling.prg":"Joystick port 2" 160 | ,"Yie Ar Kung-Fu.d64":"Joystick port 2" 161 | ,"Zeta Wing.d64":"Joystick port 2" 162 | ,"Zuma.prg":"Joystick port 2" 163 | } -------------------------------------------------------------------------------- /resources/info_vic20.js: -------------------------------------------------------------------------------- 1 | const info_vic20 = { 2 | "Adventure Land.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 3 | ,"AE.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 4 | ,"Alien Blitz.prg":"Memory expansion Dip ALL OFF load\"*\",8 run" 5 | ,"Amok.prg":"Memory expansion Dip ALL OFF load\"*\",8 run" 6 | ,"Arcadia.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 7 | ,"Astro Nell.prg":"Memory expansion Dip ALL OFF load\"*\",8 run" 8 | ,"Astroblitz.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 9 | ,"Atlantis.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 10 | ,"Attack of the Mutant Camels.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 11 | ,"Avenger.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 12 | ,"Bandits.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 13 | ,"Battlezone.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 14 | ,"Black Hole.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 15 | ,"Blitz.prg":"Memory expansion Dip ALL OFF load\"*\",8 run" 16 | ,"Bolder Dan.d64":"Memory expansion Dip ALL OFF load\"*\",8 run" 17 | ,"Buck Rogers.d64":"Memory expansion Dip ALL OFF load\"*\",8 run" 18 | ,"Capture the Flag.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 19 | ,"Cheese and Onion.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 20 | ,"Choplifter.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 21 | ,"Cosmic Cruncher.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 22 | ,"Creepy Corridors.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 23 | ,"Defender.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 24 | ,"Demon Attack.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 25 | ,"Donkey Kong.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 26 | ,"Dragonfire.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 27 | ,"Escape 2020.d64":"Memory expansion Dip ALL OFF load\"*\",8,1 run" 28 | ,"Final Orbit.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 29 | ,"Galaxian.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 30 | ,"Get More Diamonds.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 31 | ,"Gridrunner.prg":"Memory expansion Dip ALL OFF load\"*\",8 run" 32 | ,"Help Bodge.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 33 | ,"Hero.d64":"Memory expansion Dip ALL ON load\"*\",8,1 run" 34 | ,"Jelly Monsters.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 35 | ,"Jetpac.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 36 | ,"Lala Prologue.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 37 | ,"Laser Zone.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 38 | ,"Lode Runner.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 39 | ,"Manic Miner.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 40 | ,"Metagalactic Llamas.prg":"Memory expansion Dip ALL OFF load\"*\",8 run" 41 | ,"Mickey the Bricky.prg":"Memory expansion Dip ALL OFF load\"*\",8 run" 42 | ,"Miner 2049er.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 43 | ,"Mission Impossible.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 44 | ,"Moon Patrol.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 45 | ,"Moons of Jupiter.prg":"Memory expansion Dip ALL ON load\"*\",8,1 run" 46 | ,"Mosquito Infestation.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 47 | ,"Mountain King.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 48 | ,"Ms Pac-Man.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 49 | ,"Nibbler.prg":"Memory expansion Dip 6,7,8 ON load\"*\",8 run" 50 | ,"Omega Race.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 51 | ,"Pac-Man.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 52 | ,"Pentagorat.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 53 | ,"Perils of Willy.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 54 | ,"Pharaoh's Curse.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 55 | ,"Pirate Cove.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 56 | ,"Polaris.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 57 | ,"Pool.prg":"Memory expansion Dip ALL ON load\"*\",8,1 run" 58 | ,"Pumpkid.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 59 | ,"Radar Rat Race.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 60 | ,"Rigel Attack.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 61 | ,"Robotron.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 62 | ,"Robots Rumble.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 63 | ,"Rockman.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 64 | ,"Rodman.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 65 | ,"Sargon 2 Chess.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 66 | ,"Satellite Patrol.d64":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 67 | ,"Satellites and Meteorites.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 68 | ,"Scorpion.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 69 | ,"Seafox.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 70 | ,"Serpentine.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 71 | ,"Shamus.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 72 | ,"Skramble.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 73 | ,"Skyblazer.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 74 | ,"Spider City.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 75 | ,"Spiders of Mars.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 76 | ,"Squish'em.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 77 | ,"Star Battle.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 78 | ,"Star Defence.prg":"Memory expansion Dip ALL ON load\"*\",8 run F1 to start" 79 | ,"Super Amok.prg":"Memory expansion Dip ALL ON load\"*\",8,1 sys64802" 80 | ,"Sword of Fargoal.prg":"Memory expansion Dip ALL ON load\"*\",8 run" 81 | ,"Tenebra Macabre.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 82 | ,"TenTen.prg":"Memory expansion Dip ALL OFF load\"*\",8 run" 83 | ,"Tetris Deluxe.prg":"Memory expansion Dip 6,7,8 ON load\"*\",8 run" 84 | ,"The Count.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 85 | ,"Traxx.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 86 | ,"Tutankham.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 87 | ,"Vic Doom.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 88 | ,"Video Vermin.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 89 | ,"Voodoo Castle.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 90 | ,"Zombie Calavera.d64":"Memory expansion Dip ALL ON load\"*\",8 run" 91 | } -------------------------------------------------------------------------------- /resources/logo_c64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/logo_c64.png -------------------------------------------------------------------------------- /resources/logo_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/logo_home.png -------------------------------------------------------------------------------- /resources/logo_vic20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/logo_vic20.png -------------------------------------------------------------------------------- /resources/search-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/search-web.png -------------------------------------------------------------------------------- /resources/usb-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/usb-connected.png -------------------------------------------------------------------------------- /resources/usb-disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/usb-disconnected.png -------------------------------------------------------------------------------- /resources/usb-handshake-ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypioneer/JSCBMLoader/f065108b9941d7d4da3c030f71e2880cd4a34a5c/resources/usb-handshake-ok.png -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica", "Arial", sans-serif; 3 | font-size: 12pt; 4 | line-height: normal; 5 | max-width: max-content; 6 | background-color: darkgrey; 7 | } 8 | 9 | .layout { 10 | display: grid; 11 | justify-content: start; 12 | grid: 13 | "leftSide header rightHeader" 14 | "leftSide body rightSide" 15 | "leftSide footer footer"; 16 | gap: 5px; 17 | } 18 | 19 | .header { grid-area: header; } 20 | .rightHeader { grid-area: rightHeader; } 21 | .leftSide { grid-area: leftSide; } 22 | .body { grid-area: body; } 23 | .rightSide { grid-area: rightSide; } 24 | .footer { grid-area: footer; } 25 | 26 | input[type="file"] { 27 | display: none; 28 | } 29 | 30 | .select-class { 31 | min-width: 35ch; 32 | max-width: 35ch; 33 | border: none; 34 | overflow-y: auto; 35 | padding-top: 10px; 36 | padding-left: 10px; 37 | font-size: 12pt; 38 | cursor: pointer; 39 | background-color: beige; 40 | } 41 | 42 | .image-action-class { 43 | margin: 5px; 44 | cursor: pointer; 45 | } 46 | 47 | .logo-class { 48 | margin: 5px; 49 | cursor: pointer; 50 | } 51 | 52 | .pre-fixed-class { 53 | font-family: "Courier New", monospace; 54 | font-size: 11pt; 55 | font-weight: bold; 56 | color: white; 57 | } 58 | 59 | .art-class { 60 | margin: 5px; 61 | width: 400px; 62 | cursor: pointer; 63 | transition: opacity 0.3s ease-in-out; 64 | } 65 | 66 | .art-class.fade { 67 | opacity: 0.15; 68 | } 69 | 70 | .art-class.show { 71 | opacity: 1; 72 | } 73 | 74 | .progress-bar-class { 75 | width: 400px; 76 | height: 30px; 77 | margin: 5px; 78 | position: relative; 79 | text-align: center; 80 | line-height: 30px; 81 | color: black; 82 | background-color: beige; 83 | } 84 | 85 | .progress-class { 86 | width: 0%; 87 | height: 30px; 88 | position: absolute; 89 | top: 0px; 90 | left: 0px; 91 | opacity: 0.5; 92 | background-color: darkgreen; 93 | } -------------------------------------------------------------------------------- /t64driver.js: -------------------------------------------------------------------------------- 1 | //T64 special constants 2 | 3 | const DIR_ENTRIES_LOW_HIGH = 34; 4 | const TAPE_NAME_POS = 40; 5 | const TAPE_DIR_START_POS = 64; 6 | 7 | class t64driver { 8 | constructor(file) { 9 | 10 | this.type = "T64"; 11 | this.fileRef = file; 12 | this.progName = file.name; 13 | this.progSize = file.size; 14 | this.subProgSize = 0; 15 | this.subProgPos = 0; 16 | this.dirPos = 0; 17 | this.dirEntryTotal = 0; 18 | this.dirListing = []; 19 | 20 | /* 21 | A directory listing entry has this structure 22 | dirEntry { 23 | byte c64sFileType // C64S file type. Any non-zero is ok 24 | byte d64FileType // D64 file type e.g. PRG, SEQ. Any non-zero is ok 25 | byte startAddressLowByte // Start address of the file/program (low byte) 26 | byte startAddressHighByte // Start address of the file/program (high byte) 27 | byte endAddressLowByte // End address of the file/program (low byte) 28 | byte endAddressHighByte // End address of the file/program (high byte) 29 | byte[2] unused1 // Not used 30 | long fileStartPos // File start position on tape 31 | byte[4] unused2 // Not used 32 | byte[16] progName // File/program name 33 | } // total of 32 bytes 34 | */ 35 | 36 | } 37 | 38 | //Read binary file into byte array 39 | async readBinaryFile() { 40 | 41 | const buffer = await this.fileRef.arrayBuffer(); //Read file contents into array buffer 42 | this.progBytes = new Uint8Array(buffer); //Get byte array for the array buffer 43 | this.dirEntryTotal = this.progBytes[DIR_ENTRIES_LOW_HIGH]; 44 | this.dirEntryTotal |= this.progBytes[DIR_ENTRIES_LOW_HIGH+1] << 8; 45 | 46 | } //readBinaryFile 47 | 48 | //Return buffer load of data from current program position 49 | async getBuffer() { 50 | 51 | const currentPos = this.subProgPos; 52 | const packetSize = Math.min((this.subProgSize - currentPos), MAX_BYTES_PER_REQUEST); 53 | this.subProgPos += packetSize; 54 | return {protocol: Uint8Array.from([(packetSize == MAX_BYTES_PER_REQUEST ? BUF_NORMAL : BUF_END), packetSize]), payload: this.subProgBytes.slice(currentPos, currentPos + packetSize)}; 55 | 56 | } //getBuffer 57 | 58 | //Create a directory list 59 | async buildDirectory() { 60 | 61 | const dirEntry = new Uint8Array(new ArrayBuffer(32)); 62 | this.dirListing = []; 63 | 64 | //Add tape name 65 | dirEntry.set(this.progBytes.slice(TAPE_NAME_POS, TAPE_NAME_POS + 16),16); 66 | this.dirListing.push(Array.from(dirEntry)); 67 | 68 | //Add directory entry lines 69 | let progPos = TAPE_DIR_START_POS; 70 | for (let i = 0; i < this.dirEntryTotal; i++) { 71 | dirEntry.set(this.progBytes.slice(progPos, progPos += 32)); 72 | if (dirEntry[0] != 0 && dirEntry[1] != 0) { 73 | this.dirListing.push(Array.from(dirEntry)); 74 | } 75 | } 76 | 77 | } //buildDirectory 78 | 79 | //Open program, by matching a program name in the directory list (or wildcard match), then set the program position to the resulting sector/track position 80 | async openProgram(bytesProgName) { 81 | 82 | const progName = String.fromCharCode(...bytesProgName).replace("\r", "").replace("\n", ""); 83 | let found = false; 84 | 85 | //Check for a program name match in the directory (excluding the header entry) 86 | for (let i = 1; i < this.dirListing.length; i++) { 87 | const dirEntry = this.dirListing[i]; 88 | const matchProgName = (String.fromCharCode(...dirEntry.slice(16,32))).trim(); 89 | 90 | const pos = progName.indexOf("*"); 91 | const matchPartial = pos > 0 ? matchProgName.substring(0, pos) + "*" : "!NOTFOUND!"; 92 | 93 | //If wildcard character is used, set the program position to the first PRG entry 94 | if (progName == "*" || progName == matchProgName || progName == matchPartial) { 95 | this.progName = matchProgName; 96 | this.subProgSize = this.#fileLength(dirEntry)+2; //Allow for the two start address bytes 97 | this.subProgPos = 0; 98 | 99 | //Setup the sub program array with the two start address bytes 100 | let progPos = this.#fileStartPos(dirEntry); 101 | this.subProgBytes = new Uint8Array(this.subProgSize); 102 | this.subProgBytes.set([dirEntry[2], dirEntry[3]]); 103 | this.subProgBytes.set(this.progBytes.slice(progPos, progPos + this.subProgSize-2), 2); 104 | found = true; 105 | break; 106 | } 107 | } 108 | return found; 109 | 110 | } //openProgram 111 | 112 | //Return directory lines 113 | async getDirectoryLine() { 114 | 115 | const dirEntry = this.dirListing[this.dirPos]; 116 | const fileBlocks = Math.ceil(this.#fileLength(dirEntry)/256); 117 | const blockHigh = (fileBlocks >> 8) & 0xFF; 118 | const blockLow = fileBlocks & 0xFF; 119 | 120 | let prefix = []; 121 | let suffix = []; 122 | if (this.dirPos == 0) { //Disk header is reverse-video in quotes 123 | prefix = [REVERSE_CHAR, QUOTE_CHAR]; 124 | suffix = [QUOTE_CHAR]; 125 | } 126 | else { //Normal entries are aligned after the block size, within quotes and end with the program type 127 | prefix = new Array(5-String(fileBlocks).length).fill(SPACE_CHAR).concat([QUOTE_CHAR]); 128 | suffix = [QUOTE_CHAR, SPACE_CHAR, SPACE_CHAR].concat(FILE_TYPES[(dirEntry[1] & 0x07)] || []); 129 | } 130 | 131 | //Assemble the line and return it 132 | const line = Uint8Array.from([blockLow,blockHigh].concat(prefix).concat(dirEntry.slice(16,32).map(this.#replaceNonBreakSpace)).concat(suffix)); //block size (2 bytes) and entry text 133 | if (this.dirPos >= this.dirListing.length-1) { //Last directory entry 134 | this.dirPos = 0; 135 | return {protocol: Uint8Array.from([DIR_END, line.length]), payload: line}; 136 | } 137 | else { 138 | this.dirPos += 1; 139 | return {protocol: Uint8Array.from([DIR_NORMAL, line.length]), payload: line}; 140 | } 141 | 142 | } //getDirectoryLine 143 | 144 | //Private method: Remove non breaking space character 145 | #replaceNonBreakSpace(element) { 146 | return element != NON_SPACE_CHAR ? element : SPACE_CHAR; 147 | } 148 | 149 | //Private method: Calculate the number of bytes of a file/program from the directory entry 150 | #fileLength(dirEntry) { 151 | return (dirEntry[4] | (dirEntry[5] << 8)) - (dirEntry[2] | (dirEntry[3] << 8)); 152 | } 153 | 154 | //Private method: Calculate the start position of a file/program from the directory entry 155 | #fileStartPos(dirEntry) { 156 | let fileStartPos = 0; 157 | for (let b of [dirEntry[11], dirEntry[10], dirEntry[9], dirEntry[8]]) { 158 | // Shift previous value 8 bits to right and add it with next value 159 | fileStartPos = (fileStartPos << 8) + (b & 255); 160 | } 161 | return fileStartPos; 162 | } 163 | 164 | } --------------------------------------------------------------------------------