├── .gitignore ├── .sender_config.json ├── 6502a.jpg ├── LICENSE ├── Readme.md ├── Receiver └── Receiver.ino ├── Sender.js ├── bootloader.asm ├── examples ├── hello_world.asm └── using_rom_lib.asm ├── img ├── 6502.jpg ├── 6502b.jpg ├── 8bitnews.png ├── Sixty5o2_Logo.png └── Sixty5o2_MiniOS_th.jpg └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.out 3 | *.0 4 | node_modules 5 | package-lock.json 6 | bootloader.lst -------------------------------------------------------------------------------- /.sender_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "tty": "/dev/cu.usbmodem14201", 3 | "baudrate": 9600, 4 | "databits": 8, 5 | "parity": "none", 6 | "stopbits": 1 7 | } -------------------------------------------------------------------------------- /6502a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janroesner/sixty5o2/c7961b2c8ac2304fec5426306c276a2ac9fb9674/6502a.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jan Roesner 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 | ![Sixty 5o2 Logo](/img/Sixty5o2_Logo.png) 2 | 3 | # Introduction 4 | 5 | **Sixty/5o2** is a minimal bootloader / micro kernel / mini operating system (if you like) for [Ben Eaters 6502 Computer](https://eater.net/6502) on a breadboard. It is only **1.5kB** in size (assembled) but comes with quite a nice list of features: 6 | 7 | 1. __Load__ externally assembled __programs__ into RAM via serial connection to Arduino 8 | 2. __Run__ programs that were previously loaded into RAM 9 | 3. __Load & Run__ programs in one go 10 | 4. __Debug__ the full address space via an integrated __hex monitor__ (currently read only) 11 | 5. __Clean RAM__ for use with non-volatile RAM or during development 12 | 6. __Drive__ the __LCD__ display even at a clock rate of __1MHz__ flawlessly 13 | 7. __Drive__ the __mini keyboard__ for input 14 | 8. __Video RAM__ based __output routines__ for convenient text display single page / multipage w/ offset 15 | 9. Interrupt based loading routine to fetch data via the Arduino's serial connection 16 | 10. __Serial Sender__ (node.js) allowes to upload programs to the 6502 (error mitigation included) 17 | 11. __Fully documented source code__ 18 | 19 | ![Breadboard Image](/img/6502b.jpg) 20 | 21 | # Motivation 22 | 23 | Ben Eaters 6502 breadboard computer is a very special kind of animal and brought lots of fun and joy into my last weeks. A 45 years old processor design that is still able to get things done was fascinating enough for me, to give this project a go - especially, since my first machine was a **Commodore C64** which I programmed in Basic at the end of the 80'ies and never had the chance to get in touch with 6502 assembly. 24 | 25 | On my journey during the last weeks I soon surpassed the current state of development (thanks to Ben's schematics) and was able to write a few programs which I ended up burning onto the ROM using the programmer. Soon enough this became painful, because every codechange required to extract the ROM chip from the breadboard, put it into the programmer, burn it, put it back onto the board. This became time consuming and constraining pretty quickly, especially when I attempted to write slightly more complex programs. 26 | 27 | So early on I tested, whether I could use the Arduino, connect 8 of it's digital output pins to the VIA 6522 and transfer key strokes on my Mac serially and render them onto the LCD display. As soon as this worked, the path was clear: 28 | 29 | **I needed a bootloader that could leverage this power to load externally assembled object code / programs into the RAM and run it from there.** 30 | 31 | Luckily I was able to speed up the 6502's clock by just replacing the capacitor of the unstable 555 timer circuit by a smaller one such, that loading data serially was - lets say - at least stable enough. That paved the way to more complex subroutines which now make up my "Sixty/5o2" micro bootloader / micro kernel. It works very well with full clock speed of 1MHz and is hopefully helpful to other 6502 enthusiasts as well. 32 | 33 | Especially the serial data transfer is enormously stable, since error mitigation (not correction) is baked into a minimalistic protocol, where there the sender side is implemented in node.js. Unfortunately I was not able to get a stable serial connection with serial terminals like `screen`, `minicom` or `picocom`. Hence I decided to build something myself. On the positive side of things I had the opportunity to integrate content transform using base64 encoding as well as simple error mitigation via a checksum algorithm plus a "send packet again" function. 34 | 35 | **It's not perfect, in places not even nice. Last time I personally touched assembler was 20+ years ago, so please be gentle with criticism. PR's are king.** 36 | 37 | If you want to get a glimpse, check out the demo video on Youtube: 38 | 39 | [![Sixty5o2 - Mini OS for Ben Eater's 6502 Breadboard Computer](/img/Sixty5o2_MiniOS_th.jpg)](https://www.youtube.com/watch?v=sZl2wUgASyk) 40 | 41 | 42 | # Hardware Requirements 43 | 44 | There are only two requirements, both of them can be mitigated though: 45 | 46 | 1. Use the 1MHz clock (you **MUST** disconnect the clock module, otherwise it interferes) 47 | 2. Some people (including me initially) built the mini keyboard such, that the buttons are tied _low_ in normal state, when pushed they get tied _high_. This is opposite to Ben's design in his schematics. I updated my setup according to the schematics, and the default behaviour is now, that the buttons are tied __high__ and only when pushed, they get pulled __low__. 48 | 49 | ## Possible Mitigation Strategies 50 | 51 | 1. If you want to run at other clock speeds, you MUST adjust a global constant called `WAIT_C` in the bootloader code. It's a multiplier which is used to _sleep_ and just burns a number of cycles in a routine called `LIB__sleep`. If you run at lower clock speeds, adjust `WAIT_C` to a smaller number until keyboard and main menu become usable. 52 | 53 | 2. Should your keyboard not be built according to the schematics (so the buttons are normally tied _low_ and when pressed turn _high_), then you need to adjust the routine `VIA__read_keyboard_input`. Simply comment out line 555 in `bootloader.asm`. This will disable the XOR of the read value with 0xf. This way the keystrokes will be interpreted correctly. 54 | 55 | # Software Requirements 56 | 57 | The following software components are must have's: 58 | 59 | - Arduino IDE to be found [here](https://www.arduino.cc/en/main/software) 60 | - Minipro or XG GUI software for Windows for thr TL866 programmer available [here](http://www.autoelectric.cn/en/tl866_main.html) 61 | - The infamous and awesome [VASM Assembler](http://sun.hasenbraten.de/vasm/) to build for the 6502 (Ben's instructions to build and use to be found [here](https://www.youtube.com/watch?v=oO8_2JJV0B4)) 62 | - Node.js 8+ to be able to use the serial program loading functionality via the Arduino 63 | - npm or yarn (typically come with node.js) to install the senders dependencies 64 | 65 | # Installation 66 | 67 | The project comes with a number of files, whose functionality is the following: 68 | 69 | 1. `bootloader.asm` - the bootloader / micro kernel / mini os you wanna put into your ROM after assembly 70 | 2. `Receiver.ino` - Arduino source which turns the Arduino into a serial receiver / parallel converter 71 | 3. `Sender.js` - Node.js tool to read 6502 object code / programs and upload them to the 6502 via serial connection 72 | 4. `.sender_config.json` - config file for `Sender.js` (update your /dev/cu.whateverhere) 73 | 5. `package.json` - package dependencies for `Sender.js` 74 | 6. `/examples` - some example programs you can load into the RAM 75 | 76 | ## 1. Bootloader 77 | 78 | Assemble the bootloader: 79 | 80 | ``` 81 | vasm -Fbin -dotdir -o bootloader.out bootloader.asm 82 | ``` 83 | 84 | Burn it onto the EEPROM using your TL866 programmer in conjunction with minipro (Linux, Mac) or the respective Windows GUI tool provided by XG (see above). 85 | 86 | At this point you can install your ROM chip onto the board and celebrate. You will not remove it from it's breadboardy socket for a while. If your 1MHz clock, RAM, keyboard and LCD are assembled already, you can switch on your 6502 computer and enjoy the main menu of **Sixty/5o2**. If the assembly is not done yet, go read [Ben's schematics](https://eater.net/6502) and finish your hardware build. 87 | 88 | ## 2. Receiver (Arduino) 89 | 90 | - Load `Receiver.ino` into your Arduino IDE. 91 | - Open the IDE's package library and search and install the `Base64` package by Arturo Guadalupi v0.0.1 also to be found [here](https://github.com/agdl/Base64) 92 | - Compile the source 93 | - Upload the program to your Arduino 94 | 95 | ## 3. Sender (node.js) 96 | 97 | - Install the necessary npm packages via: 98 | 99 | ``` 100 | npm install 101 | ``` 102 | or 103 | ``` 104 | yarn 105 | ``` 106 | - Adjust the `tty` setting in `.sender_config.json` to match the device file which represents your connected Arduino 107 | - **DO NOT** adjust any other value in there, as it will render the serial link unstable (more on that later) 108 | 109 | # Usage 110 | 111 | ## Arduino Port Setup 112 | 113 | Before you can upload a program to the 6502 through the Arduino, you need to setup additional jumper wires between the Arduino and the VIA 6522 **AS WELL** as the 6502 processor. 114 | 115 | - You need 8 jumper wires connecting the digital output ports of the Arduino with the PORTB of the VIA 6522 (See table 1 below) 116 | - You need 1 jumper wire connecting one digital output port of the Arduino with the IRQ line of the 6502 (See table 2 below) 117 | - You need 1 jumper wire connecting one of the `GND` pins of the Arduino with common ground of your 6502 breadboard 118 | 119 | **Table 1: Port Setup VIA 6522** 120 | 121 | | Arduino | VIA 6522 | 122 | |---------|----------| 123 | | 31 | 10 | 124 | | 33 | 11 | 125 | | 35 | 12 | 126 | | 37 | 13 | 127 | | 39 | 14 | 128 | | 41 | 15 | 129 | | 43 | 16 | 130 | | 45 | 17 | 131 | 132 | If unsure, look up the pin setup of the VIA in the [official documentation](https://eater.net/datasheets/w65c22.pdf). 133 | 134 | **Table 2: Port Setup 6502** 135 | 136 | | Arduino | 6502 | 137 | |---------|-------------| 138 | | 53 | 4 (IRQB) | 139 | 140 | The pin setup of the 6502 can be found [here](https://eater.net/datasheets/w65c02s.pdf). 141 | 142 | **Important:** Make sure, you still have the IRQB pin (PIN 4) of the 6502 tied high via a 1k Ohm resistor as per the design. The jumper cable to pin 53 of the Arduino just pulls the pin low in short pulses. The line needs to be normal _high_. 143 | 144 | **Note:** Just one additional wire from the Arduinos power source to the 6502 board will free you of the need of any external power source. Just power your beast via USB and get rid of the power cord. 145 | 146 | ## Uploading a Program 147 | 148 | You can now write a program in 6502 assembly language like for example the `/examples/hello_world.asm` and assemble it like so: 149 | 150 | ``` 151 | vasm -Fbin -dotdir -o /examples/hello_world.out /examples/hello_world.asm 152 | ``` 153 | 154 | **Important:** Since your programs now target RAM instead of ROM your program needs to have a different entry vector specified: 155 | 156 | ``` 157 | .org $0200 158 | ``` 159 | 160 | More on **why $0200** later on. 161 | 162 | To upload and run your gem onto your 6502, first start up the machine, and reset it. Using the keyboard navigate to `Load` using the _UP_ and _DOWN_ keys in the main menu. To start the uploading process hit the _RIGHT_ key which acts as `Enter` in most cases. 163 | 164 | Now you can upload your program using the Sender.js CLI tool like so: 165 | 166 | ``` 167 | node Sender.js /examples/hello_world.out 168 | ``` 169 | 170 | The upload process will inform you, when it's done. The 6502 automatically switches back into the main menu after the upload finished. 171 | 172 | Should you encounter any errors during upload, check the `tty` setting in `.sender_config.json` and adjust it to your Arduinos device port. In addition you can lower the transfer speed to values to 4800, 2400 or 1200 baud. Don't use values above 9600 baud, they won't work. 173 | 174 | Navigate to the menu entry `Run` and hit the _RIGHT_ key to run your program. 175 | 176 | **Go celebrate!** You're just running your first uploaded program directly from RAM. 177 | 178 | **Note:** You can also use `Load & Run` to streamline the process during debugging. 179 | 180 | **Also note:** Resetting your 6502 **DOES NOT** erase the RAM. So you can reset any time, and still `Run` your program afterwards. 181 | 182 | **And note:** The `Sender.js` accepts two commandline parameters. If you want, you can also specify your Arduino port manually, whithout having to hardwire it in the `.sender_config.json` like so: 183 | 184 | ``` 185 | node Sender.js /examples/hello_world.out /dev/path_to_arduino_port 186 | ``` 187 | 188 | ## Link against routines in ROM 189 | 190 | This feature was added by **David Latham**. Big thank you! Since RAM is a scarse ressource and we do not use much of the ROM, you have the option to built and test routines in RAM first, and burn them into the ROM afterwards. This way you can build your very personal library of functions and extend the existing library of Sixty5o2. 191 | 192 | In order to do so, you have to rebuild the `bootloader.asm` as follows: 193 | 194 | ``` 195 | vasm -Fbin -dotdir -L bootloader.lst bootloader.asm -o bootloader.out 196 | ``` 197 | 198 | This generates the symbol list `bootloader.lst` with the hexadecimal addresses for all routines and labels. If you scroll down to the bottom, you will find the addresses of every routine, that Sixty5o2 provides. Now you can use these addresses in a new program, that you assemble and upload to RAM. 199 | 200 | Take a look at the example `using_rom_lib.asm` that David added to the examples folder. 201 | 202 | 203 | ## Using the Monitor 204 | 205 | The **hex monitor** is very useful during development and debugging. It lets you inspect the whole address space, RAM and ROM. you can navigate using the _UP_ and _DOWN_ keys. The _RIGHT_ key performs a bigger jump in time and space and the _LEFT_ key returns you to the main menu. The monitor is currently read only and the keyboard debouncing is far from being good. But it works. 206 | 207 | # Important to know - Allocated Ressources 208 | 209 | ## 1. $0200 210 | 211 | I choose $0200 as entry vector for user land programs. Why you ask? Two reasons: 212 | 213 | 1. The adresses from `$0000 up to $00ff` are the so called zero page addresses, which allow 8 bit addressing and faster processing. Use them wisely, don't waste them, don't put program code in here. 214 | 2. The adresses from `$0100 up to $01ff` are used by the 6502 as stack. You better don't mess with it, because not only does it hold values after any stack push operation (like pha), but the 6502 also stores return addresses here, when performing a jump to subroutine / return from subroutine (jsr/rts). 215 | 216 | Therefore RAM is usable in a meaningful fashion from $0200 upwards only. 217 | 218 | **Note:** Due to Ben's (IMO clever) design choice RAM ends already at $3fff, which leaves you with close to 16kByte of RAM for your programs. Should you hit that wall, there's always the option to outsource routines as "standard library", put them onto the ROM and link them from your programs via the VASM linker. 219 | 220 | ## 2. Used Zero Page Locations 221 | 222 | The bootloader needs to use some Zero Page locations: `$00 - $03`. Expect trouble if you overwrite / use them from within your own programs. 223 | 224 | ## 3. Used RAM 225 | 226 | The bootloader also occupies some RAM. Most part is used as VideoRam to talk to the LCD (consult the source). [In contrast to C64 design there is no interrupt driven scanline routine that updates the LCD automatically from the VideoRam contents yet. A feature to come.] Another few RAM cells are used by the bootloader itself. 227 | 228 | **However, don't use RAM from `$3fda upto $3fff`. Expect problems if you do so.** 229 | 230 | ## 4. Interrupt Service Routine - ISR 231 | 232 | The Interrupt Service Routine (ISR) implemented at the end of available ROM realizes the serial loading. The way it works is quite simple. As soon as the Arduino set up all 8 bit of a byte at the data ports, it pulls the interrupt pin of the 6502 low for 30 microseconds. This triggers the 6502 to halt the current program, put all registers onto the stack and execute any routine who's starting address can be found in the Interrupt Vector Address (`$fffe-$ffff`) - the ISR. This routine reads the byte, writes it into the RAM, increases the address pointer for the next byte to come and informs the main program that data is still flowing. Consult the source for further details, it's quite straight forward. 233 | 234 | # Shortcomings 235 | 236 | - The loader is slow. Quite slow. Even though 9600 baud as choosen transfer speed is not too bad, there are some significant idle timeouts implemented, to make the data transfer work reliably. You'll find it in `Receiver.ino`, the `Sender.js` does not have any timeouts left other than the necessary but unproblematic connection timeout once at the beginning. The worst is the timeout which allows to reliably read the UART buffer of the Arduino. When reduced, the whole data transfer becomes unreliable. 237 | Happy to accept PR's with improvement here. On the other hands, it's not that we transfer Gigabytes of data here ... not even Megabytes, so the current speed might suffice. 238 | 239 | # Known Problems 240 | 241 | Despite the fact that the bootloader and all of it's components are quite stable, there are some problems, which are to be found via a #TODO in the source. 242 | 243 | Worth mentioning are the following: 244 | 245 | - sub par keyboard debouncing simply via burning CPU cycles 246 | - LIB__sleep based EOF detection during data transfer - if more than a few packages fail to transfer and need to be repeated by the sender, it might happen, that the `BOOTLOADER__program_ram` routine interprets this as EOF, since no data is coming in no more. This problem can not be "easily" solved, since there are no control characters that can be transferred between the Arduino and the 6522. There are solutions, but first there needs to be a problem. 247 | - sub optimal register preservation - the (reduced) 6502 instruction set makes it hard to preserve all registers w/o wasting RAM locations. The current implementation does put focus on register preservation only where explicitly needed. 248 | - the ISR is currently static, so it can handle only interrupt requests which come from the Arduino. If you want to use other interrupts of the 6522 or software interrupts, you need to implement a priorization mechanism as well as a branching in the ISR, since (to my knowledge) there is only one interrupt vector, the 6502 can handle. 249 | 250 | # Future Plans 251 | 252 | - make Hex Monitor read / write 253 | - integrate Arduino Nano directly onto the board to replace all jumper wiring and power the board via USB 254 | - develop a standard library with useful functionality in ROM 255 | - (potentially) integrate a light color / bitmap display like for example the [Adafruit 0.96" 160x80 Color TFT Display](https://www.adafruit.com/product/3533) 256 | - implement a 3d engine using vector rotation and scalar multiplication (or quaternions) 257 | 258 | # Pull Requests 259 | 260 | If you would like to see any particular feature I might be able to provide it ... some day. Unfortunately my spare time is very limited, so you rather develop it yourself. I am happy to screen, test and merge any valuable PR. 261 | 262 | # Support 263 | 264 | My friend and founding partner Bastian and I decided, it's time to bring all the joy and fun that come with 8bit projects to a much broader audience. I quit a well paying contract and we both ditched a number of projects, to be able to focus on 8bit related content solely. 265 | 266 | We are currently working on a number of screencasts covering topics like: 267 | 268 | - **The worlds worst transistor clock implemented in Logisim** 269 | - **An 8bit computer implementation from scratch - in Logisim** 270 | - **Architecture updates to the 8bit machine for 64kb of RAM, more registers and instructions** 271 | - **A Ruby based assembler for homebrew 8bit projects in just a few lines of code** 272 | 273 | ... and we have numerous ideas for other, fresh content to come. 274 | 275 | If you actually want to support us, it would mean the world to us, if you joined our newsletter. [8bitnews.io](https://8bitnews.io) is the first real 8bit newsletter out there. On a regular basis we handcraft the __latest 8bit news__, __learning resources__ and the __best YouTube videos__ into an email, and deliver it to your inbox, when you have the time for tinkering. It's well selected and curated, and we personally guarantee, there will be no spam. 276 | 277 | [![8bitnews.io](/img/8bitnews.png)](https://8bitnews.io) 278 | 279 | By the way, there is no catch here, you can unsubscribe with a single click. Anytime. We won't be mad at you. 280 | 281 | # Contact 282 | 283 | I personally really enjoy talking to likeminded people. So if you want to share your thoughts regarding this project, other 8bit topics, our newsletter or our screencasts, feel free to drop me some lines [via Email](mailto:jan@8bitnews.io) any time. 284 | 285 | # Credits 286 | 287 | - Ben Eater 288 | - Steven Wozniak 289 | - Anke L. 290 | - Tim Miller (special thx) 291 | - P.A. Bäckström 292 | - Wilgert Velinga 293 | - Ed Gleys 294 | - Jonathan Shockley 295 | -------------------------------------------------------------------------------- /Receiver/Receiver.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | char chunk[100] = {}; 4 | int8_t chunkSize = 9; 5 | long counter; 6 | bool firstRun; 7 | 8 | // Protocol constants 9 | char PROTOCOL_OK = 'k'; 10 | char PROTOCOL_FAILURE = 'f'; 11 | 12 | // ************************************************************************** 13 | // ***** Important ** Change according to the Arduino PINs that you use ***** 14 | // ************************************************************************** 15 | 16 | // Data output pins connected to the 6522; values for the Arduino Mega 17 | const byte DATA[] = {31, 33, 35, 37, 39, 41, 43, 45}; 18 | 19 | // Data output pins connected to the 6522; values for Arduino Nano 20 | // const byte DATA[] = {5, 6, 7, 8, 9, 10, 11, 12}; 21 | 22 | // Interrupt PIN on the Arduino Mega connected directly to the IRQB pin (PIN4) of the 6502(!) 23 | #define INTERRUPT 53 24 | 25 | // Interrupt PIN on the Arduino Nano connected directly to the IRQB pin (PIN4) of the 6502(!) 26 | // #define INTERRUPT 3 27 | 28 | // ************************************************************************** 29 | // ***** Important ** End ***** 30 | // ************************************************************************** 31 | 32 | // Necessary delays 33 | int RESPONSE_DELAY = 5; // microseconds 34 | int READ_BUFFER_DELAY = 20; // milliseconds 35 | 36 | int INTERRUPT_LOW_DELAY = 30; // microseconds 37 | int INTERRUPT_HIGH_DELAY = 20; // microseconds 38 | 39 | long EOF_TIMEOUT = 150000; 40 | 41 | 42 | void setup() { 43 | 44 | // Setting Arduinos pins to input before start, otherwise we interfere with the LCD 45 | for (int n = 0; n < 8; n += 1) { 46 | pinMode(DATA[n], INPUT); 47 | } 48 | 49 | // Setting interrupt PIN to output is no problem, PIN must be normal HIGH 50 | pinMode(INTERRUPT, OUTPUT); 51 | digitalWrite(INTERRUPT, HIGH); 52 | 53 | // Setting up helper variables to determine EOF later on 54 | counter = 0; 55 | firstRun = true; 56 | 57 | Serial.begin(9600); 58 | } 59 | 60 | void loop() { 61 | if (Serial.available() >= 9) { 62 | 63 | // As soon as data arrives, all pins are set to output exactly once 64 | if (firstRun == true) { 65 | for (int n = 0; n < 8; n += 1) { 66 | digitalWrite(DATA[n], LOW); 67 | pinMode(DATA[n], OUTPUT); 68 | } 69 | firstRun = false; 70 | } 71 | 72 | // This delay is needed, otherwise the buffer can not be read reliably 73 | delay(READ_BUFFER_DELAY); 74 | 75 | // Reading in a chunk of 14 bytes ... a few more than necessary #TODO 76 | for (int i = 0; i <= 14; i+= 1) { 77 | chunk[i] = Serial.read(); 78 | } 79 | 80 | // Base64-decode the chunk 81 | int chunkLength = sizeof(chunk); 82 | int decodedLength = Base64.decodedLength(chunk, chunkLength); 83 | char decodedChunk[decodedLength]; 84 | Base64.decode(decodedChunk, chunk, chunkLength); 85 | 86 | // If the chunks checksum is correct, write the data, otherwise ask the sender to repeat the chunk 87 | if (decodedChunk[8] == checkSum(decodedChunk)) { 88 | if (writeData(decodedChunk)) { 89 | Serial.println(PROTOCOL_OK); 90 | } else { 91 | Serial.println(PROTOCOL_FAILURE); 92 | } 93 | } else { 94 | delayMicroseconds(RESPONSE_DELAY); 95 | Serial.println(PROTOCOL_FAILURE); 96 | } 97 | } else { 98 | // Detect EOF here and set pins to INPUT again to prevent interference with LCD display 99 | counter += 1; 100 | if (counter >= EOF_TIMEOUT) { 101 | counter = EOF_TIMEOUT; 102 | for (int n = 0; n < 8; n += 1) { 103 | pinMode(DATA[n], INPUT); 104 | } 105 | firstRun = true; 106 | } 107 | } 108 | } 109 | 110 | // Writing the data on the output pins and trigger the 6502's interrupt service routine to write byte by byte 111 | bool writeData(char ary[]) { 112 | // Loop through the 8 bytes of the given chunk 113 | for (int i = 0; i < 8; i += 1) { 114 | char data = ary[i]; 115 | 116 | // For each byte set up the byte's corresponding bits at the digital ports 117 | for (int n = 0; n < 8; n += 1) { 118 | digitalWrite(DATA[n], bitRead(data, n) ? HIGH : LOW); 119 | } 120 | 121 | // Pull the interrupt pin low to trigger the interrupt service routine 122 | digitalWrite(INTERRUPT, LOW); 123 | delayMicroseconds(INTERRUPT_LOW_DELAY); 124 | 125 | // Pull interrupt high again 126 | digitalWrite(INTERRUPT, HIGH); 127 | 128 | // Leave data on the lines for a while, since it seems to be read on rising edge 129 | delayMicroseconds(INTERRUPT_HIGH_DELAY); 130 | } 131 | delayMicroseconds(RESPONSE_DELAY); 132 | 133 | // Report back success 134 | return (true); 135 | } 136 | 137 | // Very simple 1-byte checksum algorithm - to be improved 138 | char checkSum(char buf[]) { 139 | int cs = 0; 140 | for (int i = 0; i < chunkSize - 1; i++) { 141 | cs = (cs << 1) + ((int)buf[i] & 1); 142 | } 143 | 144 | return (char)cs; 145 | } 146 | -------------------------------------------------------------------------------- /Sender.js: -------------------------------------------------------------------------------- 1 | const SerialPort = require('serialport') 2 | const Readline = require('@serialport/parser-readline') 3 | const fs = require('fs') 4 | const EventEmitter = require('events') 5 | class MyEmitter extends EventEmitter {} 6 | 7 | // Constants for events 8 | const START = 'start' 9 | const CHUNK_SUCCESS = 'chunkSuccess' 10 | const CHUNK_FAILURE = 'chunkFailure' 11 | const EOF = 'eof' 12 | 13 | // Constants for serial port communication 14 | const SERIAL_ERROR = 'error' 15 | const SERIAL_DATA = 'data' 16 | 17 | // Protocol constants 18 | const PROTOCOL_OK = 'k' 19 | const PROTOCOL_FAILURE = 'f' 20 | 21 | // JSON config file for all connection related details 22 | const CONFIG_FILE_NAME = '.sender_config.json' 23 | 24 | // Application timeout values 25 | const startTimeout = 2000 26 | const byteTimeout = 0 // not needed, data transfer is stable w/o positive values 27 | const chunkTimeout = 0 // not needed, since we wait for Arduino ack after every chunk anyways 28 | 29 | // get a serial connection with default params 30 | const connectSerial = (config, tty) => { 31 | const connection = new SerialPort(tty || config.tty, { 32 | baudRate: config.baudrate, 33 | databits: config.databits, 34 | parity: config.parity, 35 | stopbits: config.stopbits 36 | }) 37 | 38 | // Open errors will be emitted as an error event 39 | connection.on(SERIAL_ERROR, (err) => { 40 | console.log('Error on read: ', err.message) 41 | }) 42 | 43 | return connection 44 | } 45 | 46 | // establish a parser for data read from Arduino 47 | const establishParser = (connection, emitter) => { 48 | const parser = new Readline() 49 | connection.pipe(parser) 50 | 51 | parser.on(SERIAL_DATA, (data) => { 52 | const response = data.toString().trim() 53 | if (response == PROTOCOL_OK) { 54 | emitter.emit(CHUNK_SUCCESS) 55 | } else if (response == PROTOCOL_FAILURE) { 56 | emitter.emit(CHUNK_FAILURE) 57 | } else { 58 | console.log('Arduino Response: ', response) 59 | } 60 | }) 61 | } 62 | 63 | // write a single byte to the serial connection 64 | const sendByte = (connection, char) => { 65 | connection.write(char, (err) => { 66 | if (err) { 67 | return console.log('Error on write: ', err.message) 68 | } 69 | }) 70 | } 71 | 72 | // write a chunk of bytes to the serial connection including checksum 73 | const sendChunk = (connection, data) => { 74 | let idx = 0 75 | 76 | // both methods are destructive 77 | data = appendPadding(data) 78 | data = appendChecksum(data) 79 | 80 | base64 = data.toString('base64') 81 | 82 | // let decimals = data.join('-') 83 | // console.log('Data: ', decimals) 84 | // console.log('Base64: ', base64) 85 | 86 | return new Promise((res) => { 87 | setTimeout(() => { 88 | const interval = setInterval(() => { 89 | if (idx == base64.length) { 90 | clearInterval(interval) 91 | res() 92 | } else { 93 | sendByte(connection, base64[idx]) 94 | idx += 1 95 | } 96 | }, byteTimeout) 97 | }, chunkTimeout) 98 | }) 99 | } 100 | 101 | // simple 1-byte checksum algorithm 102 | const checkSum = (data) => { 103 | let cs = 0 104 | data.forEach((element) => { 105 | bin = element.toString(2) 106 | cs = (cs << 1) + parseInt(bin[bin.length - 1]) 107 | }) 108 | 109 | return cs 110 | } 111 | 112 | // appends checksum to given buffer 113 | const appendChecksum = (buf) => { 114 | return Buffer.concat([buf, Buffer.alloc(1, [checkSum(buf)])], 9) 115 | } 116 | 117 | // add 0x00 padding bytes to buffer of different length than 8 (potentially only the last) 118 | // necessary, because otherwise the checksum will not be in the right place and 119 | // the chunk will be requested forever 120 | const appendPadding = (buf) => { 121 | if (buf.length == 8) { 122 | return buf 123 | } else { 124 | return Buffer.concat([buf, Buffer.alloc((8 - buf.length), 0)], 8) 125 | } 126 | } 127 | 128 | // reads a file from filesystem and async returns it as a buffer 129 | const readFile = (fileName) => { 130 | return new Promise((res, rej) => { 131 | fs.readFile(fileName, (err, data) => { 132 | if (err) { rej('Error while reading file: ' + fileName) } 133 | res(data) 134 | }) 135 | }) 136 | } 137 | 138 | // cut the given array / buffer into chunks of given size 139 | const inGroupsOf = (ary, size) => { 140 | let result = [] 141 | for (let i = 0; i < ary.length; i += size) { 142 | let chunk = ary.slice(i, i + size) 143 | result.push(chunk) 144 | } 145 | 146 | return result 147 | } 148 | 149 | // simple index constructor function 150 | const Index = (m) => { 151 | let idx = 0 152 | const max = m - 1 153 | 154 | const get = () => { 155 | return idx 156 | } 157 | 158 | const increase = () => { 159 | if (idx >= max) { 160 | return null 161 | } else { 162 | idx += 1 163 | 164 | return idx 165 | } 166 | } 167 | 168 | return { 169 | get, 170 | increase 171 | } 172 | } 173 | 174 | // registering all event handler functions 175 | const establishEventHandlers = (connection, index, chunks) => { 176 | const myEmitter = new MyEmitter() 177 | 178 | myEmitter.on(START, (connection, chunk) => { 179 | sendFirstChunk(connection, chunk) 180 | }) 181 | 182 | myEmitter.on(CHUNK_SUCCESS, () => { 183 | sendNextChunk(connection, index, chunks, myEmitter) 184 | }) 185 | 186 | myEmitter.on(CHUNK_FAILURE, () => { 187 | repeatChunk(connection, index, chunks, myEmitter) 188 | }) 189 | 190 | myEmitter.on(EOF, () => { 191 | quit(connection) 192 | }) 193 | 194 | return myEmitter 195 | } 196 | 197 | // sends the very first chunk 198 | const sendFirstChunk = (connection, chunk) => { 199 | console.log('Sending chunk: 0') 200 | sendChunk(connection, chunk) 201 | } 202 | 203 | // increases index and sends next chunk 204 | // emits EOF event, when no chunk is left 205 | const sendNextChunk = (connection, index, chunks, emitter) => { 206 | const idx = index.increase() 207 | 208 | if (idx) { 209 | console.log('Sending chunk: ', idx) 210 | sendChunk(connection, chunks[idx]) 211 | } else { 212 | console.log('No chunk left!') 213 | emitter.emit(EOF) 214 | } 215 | } 216 | 217 | // gets current index and repeats the chunk sent before 218 | const repeatChunk = (connection, index, chunks, emitter) => { 219 | const idx = index.get() 220 | 221 | if (idx) { 222 | console.log('Repeating chunk: ', idx) 223 | sendChunk(connection, chunks[idx]) 224 | } else { 225 | emitter.emit(EOF) 226 | } 227 | } 228 | 229 | // gracefully quits the program 230 | const quit = (connection) => { 231 | console.log('Data transferred successfully! Quitting.') 232 | 233 | connection.close() 234 | process.exit(0) 235 | } 236 | 237 | // main 238 | (async () => { 239 | const inFileName = process.argv[2] 240 | const tty = process.argv[3] 241 | 242 | if (!inFileName) { 243 | console.log('No input file given!') 244 | process.exit(1) 245 | } 246 | 247 | try { 248 | console.log('Reading input file: ', inFileName) 249 | const inFile = await readFile(inFileName) 250 | console.log('Done.') 251 | const chunks = inGroupsOf(inFile, 8) 252 | 253 | console.log('Loading config file...') 254 | const config = JSON.parse(fs.readFileSync(CONFIG_FILE_NAME)) 255 | 256 | console.log('Establishing connection...') 257 | const connection = connectSerial(config, tty) 258 | 259 | const index = Index(chunks.length) 260 | 261 | console.log('Establishing event handlers...') 262 | emitter = establishEventHandlers(connection, index, chunks) 263 | console.log('Done.') 264 | 265 | setTimeout(() => { 266 | console.log('Connected.') 267 | console.log('Establishing parser...') 268 | establishParser(connection, emitter) 269 | console.log('Ready to send data.') 270 | 271 | emitter.emit(START, connection, chunks[0]) 272 | }, startTimeout) 273 | } catch(e) { 274 | console.log(e) 275 | process.exit(1) 276 | } 277 | })() -------------------------------------------------------------------------------- /bootloader.asm: -------------------------------------------------------------------------------- 1 | ;================================================================================ 2 | ; 3 | ; "Sixty/5o2" 4 | ; _________ 5 | ; 6 | ; v1.0 7 | ; 8 | ; Sixty/5o2 - minimal bootloader and monitor (r/o) w/ serial connection support 9 | ; 10 | ; Written by Jan Roesner for Ben Eater's "Project 6502" 11 | ; 12 | ; Credits: 13 | ; - Ben Eater (Project 6502) 14 | ; - Steven Wozniak (bin2hex routine) 15 | ; - Anke L. (love, patience & support) 16 | ; 17 | ;================================================================================ 18 | 19 | PORTB = $6000 ; VIA port B 20 | PORTA = $6001 ; VIA port A 21 | DDRB = $6002 ; Data Direction Register B 22 | DDRA = $6003 ; Data Direction Register A 23 | IER = $600e ; VIA Interrupt Enable Register 24 | 25 | E = %10000000 26 | RW = %01000000 27 | RS = %00100000 28 | 29 | Z0 = $00 ; General purpose zero page locations 30 | Z1 = $01 31 | Z2 = $02 32 | Z3 = $03 33 | 34 | VIDEO_RAM = $3fde ; $3fde - $3ffd - Video RAM for 32 char LCD display 35 | POSITION_MENU = $3fdc ; initialize positions for menu and cursor in RAM 36 | POSITION_CURSOR = $3fdd 37 | WAIT = $3fdb 38 | WAIT_C = $18 ; global sleep multiplicator (adjust for slower clock) 39 | ISR_FIRST_RUN = $3fda ; used to determine first run of the ISR 40 | 41 | PROGRAM_LOCATION = $0200 ; memory location for user programs 42 | 43 | .org $8000 44 | 45 | 46 | ;================================================================================ 47 | ; 48 | ; main - routine to initialize the bootloader 49 | ; 50 | ; Initializes the bootloader, LCD, VIA, Video Ram and prints a welcome message 51 | ; ———————————————————————————————————— 52 | ; Preparatory Ops: none 53 | ; 54 | ; Returned Values: none 55 | ; 56 | ; Destroys: .A, .Y, .X 57 | ; ———————————————————————————————————— 58 | ; 59 | ;================================================================================ 60 | 61 | main: ; boot routine, first thing loaded 62 | ldx #$ff ; initialize the stackpointer with 0xff 63 | txs 64 | 65 | jsr LCD__initialize 66 | jsr LCD__clear_video_ram 67 | 68 | lda #message 70 | jsr LCD__print 71 | 72 | ldx #$20 ; delay further progress for a bit longer 73 | lda #$ff 74 | .wait: 75 | jsr LIB__sleep 76 | dex 77 | bne .wait 78 | 79 | jsr MENU_main ; start the menu routine 80 | jmp main ; should the menu ever return ... 81 | 82 | 83 | ;================================================================================ 84 | ; 85 | ; MENU_main - renders a scrollable menu w/ dynamic number of entries 86 | ; 87 | ; ———————————————————————————————————— 88 | ; Preparatory Ops: none 89 | ; 90 | ; Returned Values: none 91 | ; 92 | ; Destroys: .A, .X, .Y 93 | ; ———————————————————————————————————— 94 | ; 95 | ;================================================================================ 96 | 97 | MENU_main: 98 | lda #0 ; since in RAM, positions need initialization 99 | sta POSITION_MENU 100 | sta POSITION_CURSOR 101 | 102 | jmp .start 103 | .MAX_SCREEN_POS: ; define some constants in ROM 104 | .byte $05 ; its always number of items - 2, here its 6 windows ($00-$05) in 7 items 105 | .OFFSETS: 106 | .byte $00, $10, $20, $30, $40, $50 ; content offsets for all 6 screen windows 107 | .start: ; and off we go 108 | jsr LCD__clear_video_ram 109 | ldx POSITION_MENU 110 | ldy .OFFSETS,X 111 | ; load first offset into Y 112 | ldx #0 ; set X to 0 113 | .loop: 114 | lda menu_items,Y ; load string char for Y 115 | sta VIDEO_RAM,X ; store in video ram at X 116 | iny 117 | inx 118 | cpx #$20 ; repeat 32 times 119 | bne .loop 120 | 121 | .render_cursor: ; render cursor position based on current state 122 | lda #">" 123 | ldy POSITION_CURSOR 124 | bne .lower_cursor 125 | sta VIDEO_RAM 126 | jmp .render 127 | 128 | .lower_cursor: 129 | sta VIDEO_RAM+$10 130 | 131 | .render: ; and update the screen 132 | jsr LCD__render 133 | 134 | .wait_for_input: ; handle keyboard input 135 | ldx #4 136 | lda #$ff ; debounce 137 | .wait: 138 | jsr LIB__sleep 139 | dex 140 | bne .wait 141 | 142 | lda #0 143 | jsr VIA__read_keyboard_input 144 | beq .wait_for_input ; no 145 | 146 | .handle_keyboard_input: 147 | cmp #$01 148 | beq .move_up ; UP key pressed 149 | cmp #$02 150 | beq .move_down ; DOWN key pressed 151 | cmp #$08 152 | beq .select_option ; RIGHT key pressed 153 | lda #0 ; explicitly setting A is a MUST here 154 | jmp .wait_for_input ; and go around 155 | 156 | .move_up: 157 | lda POSITION_CURSOR ; load cursor position 158 | beq .dec_menu_offset ; is cursor in up position? yes? 159 | lda #0 ; no? 160 | sta POSITION_CURSOR ; set cursor in up position 161 | jmp .start ; re-render the whole menu 162 | .dec_menu_offset: 163 | lda POSITION_MENU 164 | beq .wait_for_input ; yes, just re-render 165 | .decrease: 166 | dec POSITION_MENU ; decrease menu position by one 167 | jmp .start ; and re-render 168 | 169 | .move_down: 170 | lda POSITION_CURSOR ; load cursor position 171 | cmp #1 ; is cursor in lower position? 172 | beq .inc_menu_offset ; yes? 173 | lda #1 ; no? 174 | sta POSITION_CURSOR ; set cursor in lower position 175 | jmp .start ; and re-render the whole menu 176 | .inc_menu_offset: 177 | lda POSITION_MENU ; load current menu positions 178 | cmp .MAX_SCREEN_POS ; are we at the bottom yet? 179 | bne .increase ; no? 180 | jmp .wait_for_input ; yes 181 | .increase: 182 | adc #1 ; increase menu position 183 | sta POSITION_MENU 184 | jmp .start ; and re-render 185 | 186 | .select_option: 187 | clc 188 | lda #0 ; clear A 189 | adc POSITION_MENU 190 | adc POSITION_CURSOR ; calculate index of selected option 191 | cmp #0 ; branch trough all options 192 | beq .load_and_run 193 | cmp #1 194 | beq .load 195 | cmp #2 196 | beq .run 197 | cmp #3 198 | beq .monitor 199 | cmp #4 200 | beq .clear_ram 201 | cmp #5 202 | beq .about 203 | cmp #6 204 | beq .credits 205 | jmp .end ; should we have an invalid option, restart 206 | 207 | .load_and_run: ; load and directly run 208 | jsr .do_load ; load first 209 | jsr .do_run ; run immediately after 210 | jmp .start ; should a program ever return ... 211 | .load: ; load program and go back into menu 212 | jsr .do_load 213 | jmp .start 214 | .run: ; run a program already loaded 215 | jsr .do_run 216 | jmp .start 217 | .monitor: ; start up the monitor 218 | lda #PROGRAM_LOCATION ; can also be set as params during debugging 220 | jsr MONITOR__main 221 | jmp .start 222 | .clear_ram: ; start the clear ram routine 223 | jsr BOOTLOADER__clear_ram 224 | jmp .start 225 | .about: ; start the about routine 226 | lda #about 228 | ldx #3 229 | jsr LCD__print_text 230 | jmp .start 231 | .credits: ; start the credits routine 232 | lda #credits 234 | ldx #3 235 | jsr LCD__print_text 236 | jmp .start 237 | .do_load: ; orchestration of program loading 238 | lda #$ff ; wait a bit 239 | jsr LIB__sleep 240 | jsr BOOTLOADER__program_ram ; call the bootloaders programming routine 241 | 242 | rts 243 | .do_run: ; orchestration of running a program 244 | jmp BOOTLOADER__execute 245 | .end 246 | jmp .start ; should we ever reach this point ... 247 | 248 | 249 | ;================================================================================ 250 | ; 251 | ; BOOTLOADER__program_ram - writes serial data to RAM 252 | ; 253 | ; Used in conjunction w/ the ISR, orchestrates user program reading 254 | ; ———————————————————————————————————— 255 | ; Preparatory Ops: none 256 | ; 257 | ; Returned Values: none 258 | ; none 259 | ; Destroys: .A, .X, .Y 260 | ; ———————————————————————————————————— 261 | ; 262 | ;================================================================================ 263 | 264 | BOOTLOADER__program_ram: 265 | CURRENT_RAM_ADDRESS_L = Z0 266 | CURRENT_RAM_ADDRESS_H = Z1 267 | LOADING_STATE = Z2 268 | lda #%01111111 ; we disable all 6522 interrupts!!! 269 | sta IER 270 | 271 | lda #0 ; for a reason I dont get, the ISR is triggered... 272 | sta ISR_FIRST_RUN ; one time before the first byte arrives, so we mitigate here 273 | 274 | jsr LCD__clear_video_ram 275 | lda #message4 277 | jsr LCD__print 278 | 279 | lda #$00 ; initializing loading state byte 280 | sta LOADING_STATE 281 | 282 | lda #>PROGRAM_LOCATION ; initializing RAM address counter 283 | sta CURRENT_RAM_ADDRESS_H 284 | lda #message6 321 | jsr LCD__print 322 | 323 | ldx #$20 ; wait a moment before we return to main menu 324 | lda #$ff 325 | .loop_messagedisplay: 326 | jsr LIB__sleep 327 | dex 328 | bne .loop_messagedisplay 329 | 330 | rts 331 | 332 | 333 | ;================================================================================ 334 | ; 335 | ; BOOTLOADER__execute - executes a user program in RAM 336 | ; 337 | ; Program needs to be loaded via serial loader or other mechanism beforehand 338 | ; ———————————————————————————————————— 339 | ; Preparatory Ops: none 340 | ; 341 | ; Returned Values: none 342 | ; 343 | ; Destroys: .A, .Y 344 | ; ———————————————————————————————————— 345 | ; 346 | ;================================================================================ 347 | 348 | BOOTLOADER__execute: 349 | sei ; disable interrupt handling 350 | jsr LCD__clear_video_ram ; print a message 351 | lda #message7 353 | jsr LCD__print 354 | jmp PROGRAM_LOCATION ; and jump to program location 355 | 356 | ;================================================================================ 357 | ; 358 | ; BOOTLOADER__clear_ram - clears RAM from $0200 up to $3fff 359 | ; 360 | ; Useful during debugging or when using non-volatile RAM chips 361 | ; ———————————————————————————————————— 362 | ; Preparatory Ops: none 363 | ; 364 | ; Returned Values: none 365 | ; 366 | ; Destroys: .A, .Y 367 | ; ———————————————————————————————————— 368 | ; 369 | ;================================================================================ 370 | 371 | BOOTLOADER__clear_ram: 372 | jsr LCD__clear_video_ram ; render message 373 | lda #message8 375 | jsr LCD__print 376 | 377 | ldy #PROGRAM_LOCATION 380 | sta Z1 381 | lda #$00 ; load 0x00 cleaner byte 382 | .loop: 383 | sta (Z0),Y ; store it in current location 384 | iny ; increase 16 bit address by 0x01 385 | bne .loop 386 | inc Z1 387 | bit Z1 ; V is set on bit 6 (= $40) 388 | bvs .loop 389 | rts ; yes, return from subroutine 390 | 391 | ;================================================================================ 392 | ; 393 | ; MONITOR__main - RAM/ROM Hexmonitor (r/o) 394 | ; 395 | ; Currently read only, traverses RAM and ROM locations, shows hex data contents 396 | ; ———————————————————————————————————— 397 | ; Preparatory Ops: none 398 | ; 399 | ; Returned Values: none 400 | ; 401 | ; Destroys: .A, .X, .Y 402 | ; ———————————————————————————————————— 403 | ; 404 | ;================================================================================ 405 | 406 | MONITOR__main: 407 | sta Z0 ; store LSB 408 | sty Z1 ; store MSB 409 | 410 | .render_current_ram_location: 411 | jsr LCD__clear_video_ram 412 | 413 | lda #$00 ; select upper row of video ram 414 | sta Z3 ; #TODO 415 | jsr .transform_contents ; load and transform ram and address bytes 416 | 417 | clc ; add offset to address 418 | lda Z0 419 | adc #$04 420 | sta Z0 421 | bcc .skip 422 | inc Z1 423 | .skip: 424 | 425 | lda #$01 ; select lower row of video ram 426 | sta Z3 427 | jsr .transform_contents ; load and transform ram and address bytes there 428 | 429 | jsr LCD__render 430 | 431 | .wait_for_input: ; wait for key press 432 | ldx #$04 ; debounce #TODO 433 | .wait: 434 | lda #$ff 435 | jsr LIB__sleep 436 | dex 437 | bne .wait 438 | 439 | lda #0 440 | jsr VIA__read_keyboard_input 441 | beq .wait_for_input ; a key was pressed? no 442 | 443 | .handle_keyboard_input: ; determine action for key pressed 444 | cmp #$01 445 | beq .move_up ; UP key pressed 446 | cmp #$02 447 | beq .move_down ; DOWN key pressed 448 | cmp #$04 449 | beq .exit_monitor ; LEFT key pressed 450 | cmp #$08 451 | beq .fast_forward ; RIGHT key pressed 452 | lda #0 ; explicitly setting A is a MUST here 453 | jmp .wait_for_input 454 | .exit_monitor: 455 | lda #0 ; needed for whatever reason 456 | rts 457 | 458 | .move_down: 459 | jmp .render_current_ram_location ; no math needed, the address is up to date already 460 | .move_up: 461 | sec ; decrease the 16bit RAM Pointer 462 | lda Z0 463 | sbc #$08 464 | sta Z0 465 | lda Z1 466 | sbc #$00 467 | sta Z1 468 | jmp .render_current_ram_location ; and re-render 469 | .fast_forward: ; add $0800 to current RAM location 470 | sec 471 | lda Z0 472 | adc #$00 473 | sta Z0 474 | lda Z1 475 | adc #$04 476 | sta Z1 477 | jmp .render_current_ram_location ; and re-render 478 | .transform_contents: ; start reading address and ram contents into stack 479 | ldy #3 480 | .iterate_ram: ; transfer 4 ram bytes to stack 481 | lda (Z0),Y 482 | pha 483 | dey 484 | bne .iterate_ram 485 | lda (Z0),Y 486 | pha 487 | 488 | lda Z0 ; transfer the matching address bytes to stack too 489 | pha 490 | lda Z1 491 | pha 492 | 493 | ldy #0 494 | .iterate_stack: ; transform stack contents from bin to hex 495 | cpy #6 496 | beq .end 497 | sty Z2 ; preserve Y #TODO 498 | pla 499 | jsr LIB__bin_to_hex 500 | ldy Z2 ; restore Y 501 | pha ; push least sign. nibble (LSN) onto stack 502 | txa 503 | pha ; push most sign. nibble (MSN) too 504 | 505 | tya ; calculate nibble positions in video ram 506 | adc MON__position_map,Y ; use the static map for that 507 | tax 508 | pla 509 | jsr .store_nibble ; store MSN to video ram 510 | inx 511 | pla 512 | jsr .store_nibble ; store LSN to video ram 513 | 514 | iny 515 | jmp .iterate_stack ; repeat for all 6 bytes on stack 516 | .store_nibble: ; subroutine to store nibbles in two lcd rows 517 | pha 518 | lda Z3 519 | beq .store_upper_line ; should we store in upper line? yes 520 | pla ; no, store in lower line 521 | sta VIDEO_RAM+$10,X 522 | jmp .end_store 523 | .store_upper_line ; upper line storage 524 | pla 525 | sta VIDEO_RAM,X 526 | .end_store: 527 | rts 528 | .end: 529 | lda #":" ; writing the two colons 530 | sta VIDEO_RAM+$4 531 | sta VIDEO_RAM+$14 532 | 533 | rts 534 | 535 | 536 | ;================================================================================ 537 | ; 538 | ; VIA__read_keyboard_input - returns 4-key keyboard inputs 539 | ; 540 | ; Input is read, normalized and returned to the caller 541 | ; ———————————————————————————————————— 542 | ; Preparatory Ops: none 543 | ; 544 | ; Returned Values: .A: (UP: $1, DOWN: $2, LEFT: $4, RIGHT: $8) 545 | ; 546 | ; Destroys: .A 547 | ; ———————————————————————————————————— 548 | ; 549 | ;================================================================================ 550 | 551 | VIA__read_keyboard_input: 552 | lda PORTA ; load current key status from VIA 553 | ror ; normalize the input to $1, $2, $4 and $8 554 | and #$0f ; ignore first 4 bits 555 | eor #$0f ; deactivate / comment this line, if your keyboard 556 | ; is built with buttons tied normal low, when 557 | ; pushed turning high (in contrast to Ben's schematics) 558 | 559 | rts 560 | 561 | 562 | ;================================================================================ 563 | ; 564 | ; VIA__configure_ddrs - configures data direction registers of the VIA chip 565 | ; 566 | ; Expects one byte per register with bitwise setup input/output directions 567 | ; ———————————————————————————————————— 568 | ; Preparatory Ops: .A: Byte for DDRB 569 | ; .X: Byte for DDRA 570 | ; 571 | ; Returned Values: none 572 | ; 573 | ; Destroys: none 574 | ; ———————————————————————————————————— 575 | ; 576 | ;================================================================================ 577 | 578 | VIA__configure_ddrs: 579 | sta DDRB ; configure data direction for port B from A reg. 580 | stx DDRA ; configure data direction for port A from X reg. 581 | 582 | rts 583 | 584 | 585 | ;================================================================================ 586 | ; 587 | ; LCD__clear_video_ram - clears the Video Ram segment with 0x00 bytes 588 | ; 589 | ; Useful before rendering new contents by writing to the video ram 590 | ; ———————————————————————————————————— 591 | ; Preparatory Ops: none 592 | ; 593 | ; Returned Values: none 594 | ; 595 | ; Destroys: none 596 | ; ———————————————————————————————————— 597 | ; 598 | ;================================================================================ 599 | 600 | LCD__clear_video_ram: 601 | pha ; preserve A via stack 602 | tya ; same for Y 603 | pha 604 | ldy #$1f ; set index to 31 605 | lda #$20 ; set character to 'space' 606 | .loop: 607 | sta VIDEO_RAM,Y ; clean video ram 608 | dey ; decrease index 609 | bne .loop ; are we done? no, repeat 610 | sta VIDEO_RAM ; yes, write zero'th location manually 611 | pla ; restore Y 612 | tay 613 | pla ; restore A 614 | 615 | rts 616 | 617 | ;================================================================================ 618 | ; 619 | ; LCD__print - prints a string to the LCD (highlevel) 620 | ; 621 | ; String must be given as address pointer, subroutines are called 622 | ; The given string is automatically broken into the second display line and 623 | ; the render routines are called automatically 624 | ; 625 | ; Important: String MUST NOT be zero terminated 626 | ; ———————————————————————————————————— 627 | ; Preparatory Ops: .A: LSN String Address 628 | ; .Y: MSN String Address 629 | ; Returned Values: none 630 | ; 631 | ; Destroys: .A, .X, .Y 632 | ; ———————————————————————————————————— 633 | ; 634 | ;================================================================================ 635 | 636 | LCD__print: 637 | ldx #0 ; set offset to 0 as default 638 | jsr LCD__print_with_offset ; call printing subroutine 639 | 640 | rts 641 | 642 | 643 | ;================================================================================ 644 | ; 645 | ; LCD__print_with_offset - prints string on LCD screen at given offset 646 | ; 647 | ; String must be given as address pointer, subroutines are called 648 | ; The given string is automatically broken into the second display line and 649 | ; the render routines are called automatically 650 | ; 651 | ; Important: String MUST NOT be zero terminated 652 | ; ———————————————————————————————————— 653 | ; Preparatory Ops: .A: LSN String Address 654 | ; .Y: MSN String Address 655 | ; .X: Offset Byte 656 | ; Returned Values: none 657 | ; 658 | ; Destroys: .A, .X, .Y 659 | ; ———————————————————————————————————— 660 | ; 661 | ;================================================================================ 662 | 663 | LCD__print_with_offset: 664 | STRING_ADDRESS_PTR = Z0 665 | sta STRING_ADDRESS_PTR ; load t_string lsb 666 | sty STRING_ADDRESS_PTR+1 ; load t_string msb 667 | stx Z2 ; X can not directly be added to A, therefore we store it #TODO 668 | ldy #0 669 | .loop: 670 | clc 671 | tya 672 | adc Z2 ; compute offset based on given offset and current cursor position 673 | tax 674 | lda (STRING_ADDRESS_PTR),Y ; load char from given string at position Y 675 | beq .return ; is string terminated via 0x00? yes 676 | sta VIDEO_RAM,X ; no - store char to video ram 677 | iny 678 | jmp .loop ; loop until we find 0x00 679 | .return: 680 | jsr LCD__render ; render video ram contents to LCD screen aka scanline 681 | 682 | rts 683 | 684 | 685 | ;================================================================================ 686 | ; 687 | ; LCD__print_text - prints a scrollable / escapeable multiline text (highlevel) 688 | ; 689 | ; The text location must be given as memory pointer, the number of pages to 690 | ; be rendered needs to be given as well 691 | ; 692 | ; Important: The text MUST be zero terminated 693 | ; ———————————————————————————————————— 694 | ; Preparatory Ops: .A: LSN Text Address 695 | ; .Y: MSN Text Address 696 | ; .X: Page Number Byte 697 | ; Returned Values: none 698 | ; 699 | ; Destroys: .A, .X, .Y 700 | ; ———————————————————————————————————— 701 | ; 702 | ;================================================================================ 703 | 704 | LCD__print_text: 705 | sta Z0 ; store text pointer in zero page 706 | sty Z1 707 | dex ; reduce X by one to get cardinality of pages 708 | stx Z2 ; store given number of pages 709 | .CURRENT_PAGE = Z3 710 | lda #0 711 | sta Z3 712 | .render_page: 713 | jsr LCD__clear_video_ram ; clear video ram 714 | ldy #0 ; reset character index 715 | .render_chars: 716 | lda (Z0),Y ; load character from given text at current character index 717 | cmp #$00 718 | beq .do_render ; text ended? yes then render 719 | sta VIDEO_RAM,Y ; no, store char in video ram at current character index 720 | iny ; increase index 721 | bne .render_chars ; repeat with next char 722 | .do_render: 723 | jsr LCD__render ; render current content to screen 724 | 725 | .wait_for_input: ; handle keyboard input 726 | ldx #4 727 | .wait: 728 | lda #$ff ; debounce 729 | jsr LIB__sleep 730 | dex 731 | bne .wait 732 | 733 | lda #0 734 | jsr VIA__read_keyboard_input 735 | bne .handle_keyboard_input ; do we have input? yes? 736 | jmp .wait_for_input ; no 737 | 738 | .handle_keyboard_input: 739 | cmp #$01 740 | beq .move_up ; UP key pressed 741 | cmp #$02 742 | beq .move_down ; DOWN key pressed 743 | cmp #$04 744 | beq .exit ; LEFT key pressed 745 | lda #0 ; Explicitly setting A is a MUST here 746 | jmp .wait_for_input 747 | .exit: 748 | 749 | rts 750 | .move_up: 751 | lda .CURRENT_PAGE ; are we on the first page? 752 | beq .wait_for_input ; yes, just ignore the keypress and wait for next one 753 | 754 | dec .CURRENT_PAGE ; no, decrease current page by 1 755 | 756 | sec ; decrease reading pointer by 32 bytes 757 | lda Z0 758 | sbc #$20 759 | sta Z0 760 | bcs .skipdec 761 | dec Z1 762 | .skipdec: 763 | jmp .render_page ; and re-render 764 | 765 | .move_down: 766 | lda .CURRENT_PAGE ; load current page 767 | cmp Z2 ; are we on last page already 768 | beq .wait_for_input ; yes, just ignore keypress and wait for next one 769 | 770 | inc .CURRENT_PAGE ; no, increase current page by 1 771 | 772 | clc ; add 32 to the text pointer 773 | lda Z0 774 | adc #$20 775 | sta Z0 776 | bcc .skipinc 777 | inc Z1 778 | .skipinc: 779 | jmp .render_page ; and re-render 780 | 781 | ;================================================================================ 782 | ; 783 | ; LCD__initialize - initializes the LCD display 784 | ; 785 | ; ———————————————————————————————————— 786 | ; Preparatory Ops: none 787 | ; 788 | ; Returned Values: none 789 | ; 790 | ; Destroys: .A, .X 791 | ; ———————————————————————————————————— 792 | ; 793 | ;================================================================================ 794 | 795 | LCD__initialize: 796 | lda #%11111111 ; set all pins on port B to output 797 | ldx #%11100000 ; set top 3 pins and bottom ones to on port A to output, 5 middle ones to input 798 | jsr VIA__configure_ddrs 799 | 800 | lda #%00111000 ; set 8-bit mode, 2-line display, 5x8 font 801 | jsr LCD__send_instruction 802 | 803 | lda #%00001110 ; display on, cursor on, blink off 804 | jsr LCD__send_instruction 805 | 806 | lda #%00000110 ; increment and shift cursor, don't shift display 807 | jmp LCD__send_instruction 808 | 809 | 810 | ;================================================================================ 811 | ; 812 | ; LCD__set_cursor - sets the cursor on hardware level into upper or lower row 813 | ; 814 | ; Always positions the cursor in the first column of the chosen row 815 | ; ———————————————————————————————————— 816 | ; Preparatory Ops: .A: byte representing upper or lower row 817 | ; 818 | ; Returned Values: none 819 | ; 820 | ; Destroys: .A 821 | ; ———————————————————————————————————— 822 | ; 823 | ;================================================================================ 824 | 825 | LCD__set_cursor: 826 | jmp LCD__send_instruction 827 | 828 | ;================================================================================ 829 | ; 830 | ; LCD__set_cursor_second_line - sets cursor to second row, first column 831 | ; 832 | ; Low level convenience function 833 | ; ———————————————————————————————————— 834 | ; Preparatory Ops: none 835 | ; 836 | ; Returned Values: none 837 | ; 838 | ; Destroys: none 839 | ; ———————————————————————————————————— 840 | ; 841 | ;================================================================================ 842 | 843 | LCD__set_cursor_second_line: 844 | pha ; preserve A 845 | lda #%11000000 ; set cursor to line 2 hardly 846 | jsr LCD__send_instruction 847 | pla ; restore A 848 | 849 | rts 850 | 851 | ;================================================================================ 852 | ; 853 | ; LCD__render - transfers Video Ram contents onto the LCD display 854 | ; 855 | ; Automatically breaks text into the second row if necessary but takes the 856 | ; additional LCD memory into account 857 | ; ———————————————————————————————————— 858 | ; Preparatory Ops: Content in Video Ram needs to be available 859 | ; 860 | ; Returned Values: none 861 | ; 862 | ; Destroys: .A, .X, .Y 863 | ; ———————————————————————————————————— 864 | ; 865 | ;================================================================================ 866 | 867 | LCD__render: 868 | lda #%10000000 ; force cursor to first line 869 | jsr LCD__set_cursor 870 | ldx #0 871 | .write_char: ; start writing chars from video ram 872 | lda VIDEO_RAM,X ; read video ram char at X 873 | cpx #$10 ; are we done with the first line? 874 | beq .next_line ; yes - move on to second line 875 | cpx #$20 ; are we done with 32 chars? 876 | beq .return ; yes, return from routine 877 | jsr LCD__send_data ; no, send data to lcd 878 | inx 879 | jmp .write_char ; repeat with next char 880 | .next_line: 881 | jsr LCD__set_cursor_second_line ; set cursort into line 2 882 | jsr LCD__send_data ; send dataa to lcd 883 | inx 884 | jmp .write_char ; repear with next char 885 | .return: 886 | 887 | rts 888 | 889 | 890 | ;================================================================================ 891 | ; 892 | ; LCD__check_busy_flag - returns the LCD's busy status flag 893 | ; 894 | ; Since the LCD needs clock cycles internally to process instructions, it can 895 | ; not handle instructions at all times. Therefore it provides a busy flag, 896 | ; which when 0 signals, that the LCD is ready to accept the next instruction 897 | ; ———————————————————————————————————— 898 | ; Preparatory Ops: none 899 | ; 900 | ; Returned Values: .A: LCD's busy flag (busy: $01, ready: $00) 901 | ; 902 | ; Destroys: .A 903 | ; ———————————————————————————————————— 904 | ; 905 | ;================================================================================ 906 | 907 | LCD__check_busy_flag: 908 | lda #0 ; clear port A 909 | sta PORTA ; clear RS/RW/E bits 910 | 911 | lda #RW ; prepare read mode 912 | sta PORTA 913 | 914 | bit PORTB ; read data from LCD 915 | bpl .ready ; bit 7 not set -> ready 916 | lda #1 ; bit 7 set, LCD is still busy, need waiting 917 | rts 918 | .ready: 919 | lda #0 920 | .return: 921 | rts 922 | 923 | ;================================================================================ 924 | ; 925 | ; LCD__send_instruction - sends a control instruction to the LCD display 926 | ; 927 | ; In contrast to data, the LCD accepts a number of control instructions as well 928 | ; This routine can be used, to send arbitrary instructions following the LCD's 929 | ; specification 930 | ; ———————————————————————————————————— 931 | ; Preparatory Ops: .A: control byte (see LCD manual) 932 | ; 933 | ; Returned Values: none 934 | ; 935 | ; Destroys: .A 936 | ; ———————————————————————————————————— 937 | ; 938 | ;================================================================================ 939 | 940 | LCD__send_instruction: 941 | pha ; preserve A 942 | .loop ; wait until LCD becomes ready 943 | jsr LCD__check_busy_flag 944 | bne .loop 945 | pla ; restore A 946 | 947 | sta PORTB ; write accumulator content into PORTB 948 | lda #E 949 | sta PORTA ; set E bit to send instruction 950 | lda #0 951 | sta PORTA ; clear RS/RW/E bits 952 | 953 | rts 954 | 955 | 956 | ;================================================================================ 957 | ; 958 | ; LCD__send_data - sends content data to the LCD controller 959 | ; 960 | ; In contrast to instructions, there seems to be no constraint, and data can 961 | ; be sent at any rate to the display (see LCD__send_instruction) 962 | ; ———————————————————————————————————— 963 | ; Preparatory Ops: .A: Content Byte 964 | ; 965 | ; Returned Values: none 966 | ; 967 | ; Destroys: .A 968 | ; ———————————————————————————————————— 969 | ; 970 | ;================================================================================ 971 | 972 | LCD__send_data: 973 | sta PORTB ; write accumulator content into PORTB 974 | lda #(RS | E) 975 | sta PORTA ; set E bit AND register select bit to send instruction 976 | lda #0 977 | sta PORTA ; clear RS/RW/E bits 978 | 979 | rts 980 | 981 | ;================================================================================ 982 | ; 983 | ; LIB__bin_to_hex: CONVERT BINARY BYTE TO HEX ASCII CHARS - THX Woz! 984 | ; 985 | ; Slighty modified version - original from Steven Wozniak for Apple I 986 | ; ———————————————————————————————————— 987 | ; Preparatory Ops: .A: byte to convert 988 | ; 989 | ; Returned Values: .A: LSN ASCII char 990 | ; .X: MSN ASCII char 991 | ; ———————————————————————————————————— 992 | ; 993 | ;================================================================================ 994 | 995 | LIB__bin_to_hex: 996 | ldy #$ff ; state for output switching #TODO 997 | pha ; save A for LSD 998 | lsr 999 | lsr 1000 | lsr 1001 | lsr ; MSD to LSD position 1002 | jsr .to_hex ; output hex digit, using internal recursion 1003 | pla ; restore A 1004 | .to_hex 1005 | and #%00001111 ; mask LSD for hex print 1006 | ora #"0" ; add "0" 1007 | cmp #"9"+1 ; is it a decimal digit? 1008 | bcc .output ; yes! output it 1009 | adc #6 ; add offset for letter A-F 1010 | .output 1011 | iny ; set switch for second nibble processing 1012 | bne .return ; did we process second nibble already? yes 1013 | tax ; no 1014 | .return 1015 | 1016 | rts 1017 | 1018 | ;================================================================================ 1019 | ; 1020 | ; LIB__sleep - sleeps for a given amount of cycles 1021 | ; 1022 | ; The routine does not actually sleep, but wait by burning cycles in TWO(!) 1023 | ; nested loops. The user can configure the number of inner cycles via .A. 1024 | ; In addition there is an outer loop, which nests the inner one, hence multiplies 1025 | ; the number of burned cycles for ALL LIB__sleep calls by a globals multiplier. 1026 | ; 1027 | ; This way the whole codebase can easily be adjusted to other clock rates then 1028 | ; 1MHz. The global number of outer cycles for 1MHz is $18 and stored in WAIT 1029 | ; 1030 | ; Unfortunately this calls for errors, where the global wait is not set back 1031 | ; correctly. PR welcome 1032 | ; ———————————————————————————————————— 1033 | ; Preparatory Ops: .A: byte representing the sleep duration 1034 | ; 1035 | ; Returned Values: none 1036 | ; 1037 | ; Destroys: .Y 1038 | ; ———————————————————————————————————— 1039 | ; 1040 | ;================================================================================ 1041 | 1042 | LIB__sleep: 1043 | ldy #WAIT_C 1044 | sty WAIT 1045 | .outerloop: 1046 | tay 1047 | .loop: 1048 | dey 1049 | bne .loop 1050 | dec WAIT 1051 | bne .outerloop 1052 | rts 1053 | 1054 | message: 1055 | .asciiz "Sixty/5o2 Bootloader v0.2" 1056 | message2: 1057 | .asciiz "Enter Command..." 1058 | message3: 1059 | .asciiz "Programming RAM" 1060 | message4: 1061 | .asciiz "Awaiting data..." 1062 | message6: 1063 | .asciiz "Loading done!" 1064 | message7: 1065 | .asciiz "Running $0x200" 1066 | message8: 1067 | .asciiz "Cleaning RAM Patience please!" 1068 | MON__position_map: 1069 | .byte $00, $01, $03, $05, $07, $09 1070 | menu_items: 1071 | .text " Load & Run " 1072 | .text " Load " 1073 | .text " Run " 1074 | .text " Monitor " 1075 | .text " Clear RAM " 1076 | .text " About " 1077 | .text " Credits " 1078 | about: 1079 | .asciiz "Sixty/5o2 Bootloader and Monitor written by Jan Roesner git.io/JvTM1 " 1080 | credits: 1081 | .asciiz "Ben Eater 6502 Project Steven Wozniak bin2hex routine Anke L. love & patience" 1082 | 1083 | ;================================================================================ 1084 | ; 1085 | ; ISR - Interrupt Service Routine 1086 | ; 1087 | ; This might be the most naive approach to serial RAM writing ever, but it is 1088 | ; enormously stable and effective. 1089 | ; 1090 | ; Whenever the Arduino set up a data bit on the 8 data lines of VIA PortB, it 1091 | ; pulls the 6502's interrupt line low for 3 microseconds. This triggers an 1092 | ; interrupt, and causes the 6502 to lookup the ISR entry vector in memory 1093 | ; location $fffe and $ffff. This is, where this routines address is put, so 1094 | ; each time an interrupt is triggered, this routine is called. 1095 | ; 1096 | ; The routine reads the current byte from VIA PortB, writes it to the RAM and 1097 | ; increases the RAM address by $01. 1098 | ; 1099 | ; In addition it REsets the LOADING_STATE byte, so the BOOTLOADER__program_ram 1100 | ; routine knows, there is still data flowing in. Since there is no "Control Byte" 1101 | ; that can be used to determine EOF, it is ust assumed, that EOF is reached, when 1102 | ; no data came in for a defined number of cycles. 1103 | ; 1104 | ; Important: Due to the current hardware design (interrupt line) there is no 1105 | ; way to have the ISR service different interrupt calls. 1106 | ; 1107 | ; Important: The routine is put as close to the end of the ROM as possible to 1108 | ; not fragment the ROM for additional routines. In case of additional 1109 | ; operations, the entry address needs recalculation! 1110 | ; 1111 | ; ———————————————————————————————————— 1112 | ; Preparatory Ops: none 1113 | ; 1114 | ; Returned Values: none 1115 | ; 1116 | ; Destroys: none 1117 | ; ———————————————————————————————————— 1118 | ; 1119 | ;================================================================================ 1120 | 1121 | .org $FFC9 ; as close as possible to the ROM's end 1122 | 1123 | ISR: 1124 | CURRENT_RAM_ADDRESS = Z0 ; a RAM address handle for indirect writing 1125 | 1126 | pha 1127 | tya 1128 | pha 1129 | ; for a reason I dont get, the ISR is called once with 0x00 1130 | lda ISR_FIRST_RUN ; check whether we are called for the first time 1131 | bne .write_data ; if not, just continue writing 1132 | 1133 | lda #1 ; otherwise set the first time marker 1134 | sta ISR_FIRST_RUN ; and return from the interrupt 1135 | 1136 | jmp .doneisr 1137 | 1138 | .write_data: 1139 | lda #$01 ; progressing state of loading operation 1140 | sta LOADING_STATE ; so program_ram routine knows, data's still flowing 1141 | 1142 | lda PORTB ; load serial data byte 1143 | ldy #0 1144 | sta (CURRENT_RAM_ADDRESS),Y ; store byte at current RAM location 1145 | 1146 | ; increase the 16bit RAM location 1147 | inc CURRENT_RAM_ADDRESS_L 1148 | bne .doneisr 1149 | inc CURRENT_RAM_ADDRESS_H 1150 | .doneisr 1151 | 1152 | pla ; restore Y 1153 | tay 1154 | pla ; restore A 1155 | 1156 | rti 1157 | 1158 | .org $fffc 1159 | .word main ; entry vector main routine 1160 | .word ISR ; entry vector interrupt service routine 1161 | -------------------------------------------------------------------------------- /examples/hello_world.asm: -------------------------------------------------------------------------------- 1 | PORTB = $6000 ; VIA port B 2 | PORTA = $6001 ; VIA port A 3 | DDRB = $6002 ; Data Direction Register B 4 | DDRA = $6003 ; Data Direction Register A 5 | 6 | E = %10000000 7 | RW = %01000000 8 | RS = %00100000 9 | 10 | .org $0200 11 | 12 | ; 13 | ; main 14 | ; 15 | main: 16 | jsr init_via_ports ; Initialize VIA 17 | jsr init_lcd ; Initialize the LCD 18 | jsr clear_lcd ; Clear the LCD 19 | 20 | ldx #2 ; Clearing the LCD directly needs an idle timeout afterwards 21 | .wait: ; Instead of using this direct hardware call 22 | lda #$ff ; one option is to use a VideoRam based implementation and 23 | jsr sleep ; just clear the RAM cells - no sleep needed w/ such approach 24 | dex 25 | bne .wait 26 | 27 | lda #message 29 | jsr write_to_screen ; Call subroutine to print the message 30 | 31 | loop: 32 | jmp loop 33 | 34 | ; 35 | ; clear_lcd 36 | ; 37 | clear_lcd: 38 | pha 39 | lda #%00000001 ; Clear Display 40 | jsr send_lcd_instruction 41 | pla 42 | rts 43 | 44 | ; 45 | ; init_via_ports 46 | ; 47 | init_via_ports: 48 | lda #%11111111 ; Set all pins on port B to output 49 | sta DDRB 50 | 51 | lda #%11100001 ; Set top 3 pins and bottom ones to on port A to output, 4 middle ones to input 52 | sta DDRA 53 | rts 54 | 55 | ; 56 | ; init_lcd - initialize the display 57 | ; 58 | init_lcd: 59 | lda #%00111000 ; Set 8-bit mode; 2-line display; 5x8 font 60 | jsr send_lcd_instruction 61 | 62 | lda #%00001110 ; Display on; cursor on; blink off 63 | jsr send_lcd_instruction 64 | 65 | lda #%00000110 ; Increment and shift cursor; don't shift display 66 | jsr send_lcd_instruction 67 | 68 | rts 69 | 70 | ; 71 | ; write_to_screen - writes a message to the LCD screen 72 | ; 73 | write_to_screen: 74 | STRING = $fe ; string pointer needs to be in zero page for indirect indexed addressing 75 | sta STRING 76 | sty STRING+1 77 | ldy #0 78 | .write_chars: 79 | lda (STRING),Y 80 | beq .return 81 | jsr send_lcd_data 82 | iny 83 | jmp .write_chars 84 | .return: 85 | rts 86 | 87 | ; 88 | ; check_busy_flag - 89 | ; 90 | check_busy_flag: 91 | lda #0 ; clear port A 92 | sta PORTA ; clear RS/RW/E bits 93 | 94 | lda #RW ; prepare read mode 95 | sta PORTA 96 | 97 | bit PORTB ; read data from LCD 98 | bpl .ready ; bit 7 not set -> ready 99 | lda #1 ; bit 7 set, LCD is still busy, need waiting 100 | rts 101 | .ready: 102 | lda #0 103 | rts 104 | 105 | ; 106 | ; send_lcd_instruction - sends instruction commands to the LCD screen 107 | ; 108 | send_lcd_instruction: 109 | pha ; preserve A 110 | .loop ; wait until LCD becomes ready 111 | jsr check_busy_flag 112 | bne .loop 113 | pla ; restore A 114 | 115 | sta PORTB ; Write accumulator content into PORTB 116 | lda #0 117 | sta PORTA ; Clear RS/RW/E bits 118 | lda #E 119 | sta PORTA ; Set E bit to send instruction 120 | lda #0 121 | sta PORTA ; Clear RS/RW/E bits 122 | rts 123 | 124 | ; 125 | ; send_lcd_data - sends data to be written to the LCD screen 126 | ; 127 | send_lcd_data: 128 | sta PORTB ; Write accumulator content into PORTB 129 | lda #0 130 | sta PORTA ; Clear RS/RW/E bits 131 | lda #(RS | E) 132 | sta PORTA ; SET E bit AND register select bit to send instruction 133 | lda #0 134 | sta PORTA ; Clear RS/RW/E bits 135 | rts 136 | 137 | ; 138 | ; sleep - subroutine - sleeps for number of cycles read from accumulator 139 | ; 140 | sleep: 141 | tay 142 | loops: 143 | dey 144 | bne loops 145 | rts 146 | 147 | message: 148 | .asciiz "Hello, World!" -------------------------------------------------------------------------------- /examples/using_rom_lib.asm: -------------------------------------------------------------------------------- 1 | ; 2 | ; Example program that uses the subroutines available in ROM. 3 | ; 4 | ; The addresses for these routines are captured by running 5 | ; `vasm -dotdir -Fbin -L bootloader.lst bootloader.asm -o bootloader.out` 6 | ; to compile the bootloader bin which you have to do anyway. The resulting bootloader.lst 7 | ; file contains a list of all the lables together with their memory locations. 8 | ; 9 | ; with this information, we can create an example assembly that defines the addresses of 10 | ; the subroutines in ROM and jumps to them as normal. 11 | ; 12 | ; This program will display a message and indicate on the LCD which button you pressed. 13 | ; That's all it does. But it's a good start. 14 | ; 15 | 16 | PORTB = $6000 17 | PORTA = $6001 18 | DDRB = $6002 19 | DDRA = $6003 20 | 21 | E = %10000000 22 | RW = %01000000 23 | RS = %00100000 24 | 25 | ; ROM ADDRESSES 26 | VIA__read_keyboard_input = $8275 27 | VIA__configure_ddrs = $827E 28 | LCD__clear_video_ram = $8285 29 | LCD__print = $8299 30 | LCD__print_with_offset = $829F 31 | LCD__print_text = $82BB 32 | LCD__initialize = $832A 33 | LCD__set_cursor = $8340 34 | LCD__set_cursor_second_line = $8343 35 | LCD__render = $834B 36 | LCD__check_busy_flag = $836F 37 | LCD__send_instruction = $8384 38 | LCD__send_data = $8399 39 | LIB__bin_to_hex = $83A7 40 | LIB__sleep = $83C1 41 | VIDEO_RAM = $3FDE ; $3fde - $3ffd - Video RAM for 32 char LCD display 42 | WAIT = $3fdb 43 | WAIT_C = $ff ; global wait multiplier. 44 | 45 | .org $0200 46 | 47 | reset: 48 | ldx #$ff ; initialize the stackpointer with 0xff 49 | txs 50 | 51 | jsr LCD__initialize 52 | jsr LCD__clear_video_ram 53 | 54 | lda #message 56 | jsr LCD__print 57 | 58 | .wait_for_input: ; handle keyboard input 59 | ldx #4 60 | lda #$ff ; debounce 61 | .wait: 62 | jsr LIB__sleep 63 | dex 64 | bne .wait 65 | 66 | lda #0 67 | jsr VIA__read_keyboard_input 68 | beq .wait_for_input ; no 69 | 70 | .handle_keyboard_input: 71 | cmp #$01 72 | beq .up ; UP key pressed 73 | cmp #$02 74 | beq .down ; DOWN key pressed 75 | cmp #$04 76 | beq .left 77 | cmp #$08 78 | beq .right ; RIGHT key pressed 79 | lda #0 ; explicitly setting A is a MUST here 80 | jmp .wait_for_input ; and go around 81 | .up: 82 | lda #up 84 | jmp .do 85 | .down: 86 | lda #down 88 | jmp .do 89 | .left: 90 | lda #left 92 | jmp .do 93 | .right: 94 | lda #right 96 | jmp .do 97 | .do: 98 | jsr LCD__clear_video_ram 99 | jsr LCD__print 100 | 101 | ldx #$20 ; delay further progress for a bit longer 102 | lda #$ff 103 | .pause: 104 | jsr LIB__sleep 105 | dex 106 | bne .pause 107 | 108 | lda #message 110 | jsr LCD__print 111 | jmp .wait_for_input 112 | 113 | forever: 114 | jmp forever 115 | 116 | message: 117 | .asciiz "ROM Routines Work!" 118 | up: 119 | .asciiz "Up" 120 | down: 121 | .asciiz "Down" 122 | left: 123 | .asciiz "Left" 124 | right: 125 | .asciiz "Right" 126 | -------------------------------------------------------------------------------- /img/6502.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janroesner/sixty5o2/c7961b2c8ac2304fec5426306c276a2ac9fb9674/img/6502.jpg -------------------------------------------------------------------------------- /img/6502b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janroesner/sixty5o2/c7961b2c8ac2304fec5426306c276a2ac9fb9674/img/6502b.jpg -------------------------------------------------------------------------------- /img/8bitnews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janroesner/sixty5o2/c7961b2c8ac2304fec5426306c276a2ac9fb9674/img/8bitnews.png -------------------------------------------------------------------------------- /img/Sixty5o2_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janroesner/sixty5o2/c7961b2c8ac2304fec5426306c276a2ac9fb9674/img/Sixty5o2_Logo.png -------------------------------------------------------------------------------- /img/Sixty5o2_MiniOS_th.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janroesner/sixty5o2/c7961b2c8ac2304fec5426306c276a2ac9fb9674/img/Sixty5o2_MiniOS_th.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "receiver", 3 | "version": "1.0.0", 4 | "description": "Arduino - Serial Sender Receiver combo with simple error correction", 5 | "main": "Sender.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "run": "node Server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/janroesner/Receiver.git" 13 | }, 14 | "keywords": [ 15 | "Arduino", 16 | "Serial" 17 | ], 18 | "author": "Jan Roesner ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/janroesner/Receiver/issues" 22 | }, 23 | "homepage": "https://github.com/janroesner/Receiver#readme", 24 | "dependencies": { 25 | "serialport": "^8.0.6" 26 | } 27 | } 28 | --------------------------------------------------------------------------------