Foreword
We ended off the previous section with a very basic JavaScript 6502 emulator with two implemented instructions: Immediate LDA and zeropage STA.In this section we will be covering the other address modes of the 6502.
There is about 12 address modes supported on the 6502. Going into detail on each one can be quite overwhelming. So I will try to keep the description on each mode as short as possible. In each section I will also provide the JavaScript implementation code. Two lines of code can say a thousand words!
Once I have covered all the address modes I will implement all the address mode variants of the load and store instructions for our emulator.
I will end off this blog by running a test 6502 assembly program covering a big chunk of the address modes.
6502 Addressing Modes
If you study the address modes of the 6502 you will soon realise that 80% of these modes with the following goal in common: Figuring out an effective address.This functionality is a very good candidate for moving into a method of its own, aiding in reducing clutter in the switch-construct of our emulator for decoding instructions.
This noble idea have a bit of a snag. In order for this method to do its job, it will need a lookup table to know which address mode is associated for a given opcode. Knowing that this table will need to have 256 entries to cater for all 8-bit combination, this could end up been both a tedious and error prone task to do by hand.
There is however nothing wrong with a bit of automation😆 We can write a parser taking the content of the website detailing the opcode vs address mode info. In return for this info, the parser will spit out the tables for us.
I happen to write such a parser last year while busy with my Java emulator. So, let's re-use it!
The source is available on my GitHub site:
https://github.com/ovalcode/c64jjs/blob/master/src/co/za/jjs/CreateSource.java
In order to use this parser you will need to copy and paste the contents of Appendix A of the website http://www.masswerk.at/6502/6502_instruction_set.html into a text file. Running the parser against this text file will output the arrays to standard output. Here is a snippet of how the output will like:
Printing array Address Modes 5, 7, 0, 0, 0, 10, 10, 0, 5, 4, 0, 0, 0, 1, 1, 0, 9, 8, 0, 0, 0, 11, 11, 0, 5, 3, 0, 0, 0, 2, 2, 0, 1, 7, 0, 0, 10, 10, 10, 0, 5, 4, 0, 0, 1, 1, 1, 0, ... ... Printing array with Byte Lengths 1, 2, 0, 0, 0, 2, 2, 0, 1, 2, 1, 0, 0, 3, 3, 0, 2, 2, 0, 0, 0, 2, 2, 0, 1, 3, 0, 0, 0, 3, 3, 0, 3, 2, 0, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, 2, 2, 0, 0, 0, 2, 2, 0, 1, 3, 0, 0, 0, 3, 3, 0, .... .... Printing array with Instruction Cycles 7, 6, 0, 0, 0, 3, 5, 0, 3, 2, 2, 0, 0, 4, 6, 0, 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 6, 6, 0, 0, 3, 3, 5, 0, 4, 2, 2, 0, 4, 4, 6, 0, 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, .... ....
You will see in addition to the Address Mode array, two other arrays are outputted as well.We will making use of all three arrays soon.
The output of the Address mode array is a bit confusing. With the array filled with numbers, it is not easy to tell which address mode is which. The address mode mapping can be found in the method getAddMethod of the CreateSource class:
public static int getAddMode (String line) { String strippedLine = line.substring(5, 19).trim(); if (strippedLine.equals("accumulator")) return 0; else if (strippedLine.equals("absolute")) return 1; else if (strippedLine.equals("absolute,X")) return 2; else if (strippedLine.equals("absolute,Y")) return 3; else if (strippedLine.equals("immidiate")) return 4; else if (strippedLine.equals("implied")) return 5; else if (strippedLine.equals("indirect")) return 6; else if (strippedLine.equals("(indirect,X)")) return 7; else if (strippedLine.equals("(indirect),Y")) return 8; else if (strippedLine.equals("relative")) return 9; else if (strippedLine.equals("zeropage")) return 10; else if (strippedLine.equals("zeropage,X")) return 11; else if (strippedLine.equals("zeropage,Y")) return 12; return -1; }
To make our code more readable let's create constants in our CPU class for these Address modes:
const ADDRESS_MODE_ACCUMULATOR = 0; const ADDRESS_MODE_ABSOLUTE = 1; const ADDRESS_MODE_ABSOLUTE_X_INDEXED = 2; const ADDRESS_MODE_ABSOLUTE_Y_INDEXED = 3; const ADDRESS_MODE_IMMEDIATE = 4; const ADDRESS_MODE_IMPLIED = 5; const ADDRESS_MODE_INDIRECT = 6; const ADDRESS_MODE_X_INDEXED_INDIRECT = 7; const ADDRESS_MODE_INDIRECT_Y_INDEXED = 8; const ADDRESS_MODE_RELATIVE = 9; const ADDRESS_MODE_ZERO_PAGE = 10; const ADDRESS_MODE_ZERO_PAGE_X_INDEXED = 11; const ADDRESS_MODE_ZERO_PAGE_Y_INDEXED = 12;
We can now create an outline for our method that will figure out effective address
function calculateEffevtiveAdd(mode, argbyte1, argbyte2) { var tempAddress = 0; switch (mode) { } }
Let's stand still at the method signature for a moment. The first parameter is mode, so we will need to figure out the address mode outside of the method. argbyte1 and argbyte2 are the remaining bytes of the instruction. If the instruction to be decoded is a two byte instruction (e.g. one byte opcode and one byte operand), argbyte2 will be ignored.
We are now ready to discuss the different address modes. As we discuss each one, we will add it to our calculateEffewctiveAdd method
We are now ready to discuss the different address modes. As we discuss each one, we will add it to our calculateEffewctiveAdd method
Accumulator
With this mode of instruction, both the source and the destination is the accumulator. A typical example of an instruction implementing this mode is LSR A (Logical Shift Right).No address is associated with this instruction, so no need to add an entry to calculateEffewctiveAdd.
Absolute
An example of an absolute mode instruction is STA $2233. In this example the operand is memory location $2233.Let us implement this address mode in our calculateEffectiveAdd method.
Firstly, instructions of this mode are all three bytes long. One byte opcode and two bytes operand.
The address in the operand is stored in low byte, high order. So, to take our example of STA $2233 again: argbyte1 will be $33 and arbyte2 will be $22. The implementation of this mode in our method will look like this:
case ADDRESS_MODE_ABSOLUTE: return (argbyte2 * 256 + argbyte1); break;
Absolute, X-indexed
An example of an absolute, x-indexed instruction is STA $2233,X. The effective address is $2233 incremented by X.Suppose X is 9, the operand would be memory location $223C.
Again, this instruction is a three byte instruction, with the address stored in low, high byte order.
case ADDRESS_MODE_ABSOLUTE_X_INDEXED: tempAddress = (argbyte2 * 256 + argbyte1); tempAddress = tempAddress + x; return tempAddress; break;
Absolute, Y-indexed
This is same as Absolute, X-indexed. In this case, however Y register is used instead of X register. Implementation of this mode in our method:case ADDRESS_MODE_ABSOLUTE_Y_INDEXED: tempAddress = (argbyte2 * 256 + argbyte1); tempAddress = tempAddress + y; return tempAddress; break;
Immediate
An example of this mode of instruction is LDA #$25. In this example the accumulator is loaded with $25.As you can see, for these instructions the value is provided by the operand and no address resolution is involved. So, nothing to implement for this mode in our method.
Implied
Instructions with address mode Implied are single byte instructions (e.g. only opcode and no operand). Also, for this mode nothing to implement in our method.As you can see, for these instructions the value is provided by the operand and no address resolution is involved. So, nothing to implement for this mode in our method.
Indirect
An example of an instruction implementing this mode is STA ($1234). In this instance, the effective address is not $1234, but the address stored in location $1234 and $1235. Again address in $1234 and $1235 should be low, high order. Implementation in our code:case ADDRESS_MODE_INDIRECT: tempAddress = (argbyte2 * 256 + argbyte1); return (localMem.ReadMem(tempAddress + 1) * 256 + localMem.ReadMem(tempAddress)); break;
X-Indexed, indirect
An example of an instruction implementing this mode is LDA ($20,X). The effective address is stored at location $20 + X. The carry is ignored during addition. Implementation in our code:case ADDRESS_MODE_X_INDEXED_INDIRECT: tempAddress = (argbyte1 + x) & 0xff; return (localMem.ReadMem(tempAddress + 1) * 256 + localMem.ReadMem(tempAddress)); break;
Indirect, Y-Indexed
An example of an isntruction implementing this mode is STA ($20),Y. Address is looked up at location $20 and Register Y added to it.case ADDRESS_MODE_INDIRECT_Y_INDEXED: tempAddress = localMem.ReadMem(argbyte1 + 1) * 256 + localMem.ReadMem(argbyte1) + y; return tempAddress; break;
Zero Page
Same as absolute, but address is only one byte instead of twoRelative
We will cover this address mode in a future section
Zero-Page, X-Indexed
Effective address is operand plus X. Only low byte is takencase ADDRESS_MODE_ZERO_PAGE_X_INDEXED: return (argbyte1 + x) & 0xff; break;
Zero-Page, Y-Indexed
Same as Zero-Page, X-Indexed, but Y register is used instead.case ADDRESS_MODE_ZERO_PAGE_Y_INDEXED: return (argbyte1 + y) & 0xff; break;
Putting it all together
After the previous section, our completed calculateEffevtiveAdd method looks like this:
function calculateEffevtiveAdd(mode, argbyte1, argbyte2) { var tempAddress = 0; switch (mode) { case ADDRESS_MODE_ACCUMULATOR: return 0; break; case ADDRESS_MODE_ABSOLUTE: return (argbyte2 * 256 + argbyte1); break; case ADDRESS_MODE_ABSOLUTE_X_INDEXED: tempAddress = (argbyte2 * 256 + argbyte1); tempAddress = tempAddress + x; return tempAddress; break; case ADDRESS_MODE_ABSOLUTE_Y_INDEXED: tempAddress = (argbyte2 * 256 + argbyte1); tempAddress = tempAddress + y; return tempAddress; break; case ADDRESS_MODE_IMMEDIATE: break; case ADDRESS_MODE_IMPLIED: break; case ADDRESS_MODE_INDIRECT: tempAddress = (argbyte2 * 256 + argbyte1); return (localMem.ReadMem(tempAddress + 1) * 256 + localMem.ReadMem(tempAddress)); break; case ADDRESS_MODE_X_INDEXED_INDIRECT: tempAddress = (argbyte1 + x) & 0xff; return (localMem.ReadMem(tempAddress + 1) * 256 + localMem.ReadMem(tempAddress)); break; case ADDRESS_MODE_INDIRECT_Y_INDEXED: tempAddress = localMem.ReadMem(argbyte1 + 1) * 256 + localMem.ReadMem(argbyte1) + y; return tempAddress; break; case ADDRESS_MODE_RELATIVE: break; case ADDRESS_MODE_ZERO_PAGE: return argbyte1; break; case ADDRESS_MODE_ZERO_PAGE_X_INDEXED: return (argbyte1 + x) & 0xff; break; case ADDRESS_MODE_ZERO_PAGE_Y_INDEXED: return (argbyte1 + y) & 0xff; break; } }
We will need to write some code in our step() method to invoke this method:
var iLen = instructionLengths[opcode]; var arg1 = 0; var arg2 = 0; var effectiveAdrress = 0; if (iLen > 1) { arg1 = localMem.readMem(pc); pc = pc + 1; } if (iLen > 2) { arg2 = localMem.readMem(pc); pc = pc + 1; } effectiveAdrress = calculateEffevtiveAdd(addressModes[opcode], arg1, arg2);
As you can see, we make use of the tables we created earlier on to read just enough bytes for the arguments depending on the opcode at hand and adjusting the program counter accordingly. This simplifies our switch construct for decoding instructions by not worrying about adjusting the program counter in each and every case statement.
Let us have a go now at implementing all address mode variants of the LDA instruction:
/*LDA Load Accumulator with Memory M -> A N Z C I D V + + - - - - addressing assembler opc bytes cyles -------------------------------------------- immediate LDA #oper A9 2 2 zeropage LDA oper A5 2 3 zeropage,X LDA oper,X B5 2 4 absolute LDA oper AD 3 4 absolute,X LDA oper,X BD 3 4* absolute,Y LDA oper,Y B9 3 4* (indirect,X) LDA (oper,X) A1 2 6 (indirect),Y LDA (oper),Y B1 2 5* */ case 0xa9: acc = arg1; zeroflag = (acc == 0) ? 1 : 0; negativeflag = ((acc & 0x80) != 0) ? 1 : 0; break; case 0xA5: case 0xB5: case 0xAD: case 0xBD: case 0xB9: case 0xA1: case 0xB1: acc = localMem.readMem(effectiveAdrress); zeroflag = (acc == 0) ? 1 : 0; negativeflag = ((acc & 0x80) != 0) ? 1 : 0; break;
For reference, I have included a snippet for LDA in section A of the masswerk website.
Already we can see that our tables are giving tangible benefits. The opcodes A5, B5, AD, BD, B9, A1 and B1 shares the same piece of code.
Now, let's give a shot at LDX and LDY:
/*LDX Load Index X with Memory M -> X N Z C I D V + + - - - - addressing assembler opc bytes cyles -------------------------------------------- immediate LDX #oper A2 2 2 zeropage LDX oper A6 2 3 zeropage,Y LDX oper,Y B6 2 4 absolute LDX oper AE 3 4 absolute,Y LDX oper,Y BE 3 4**/ case 0xA2: x = arg1; zeroflag = (x == 0) ? 1 : 0; negativeflag = ((x & 0x80) != 0) ? 1 : 0; break; case 0xA6: case 0xB6: case 0xAE: case 0xBE: x = localMem.readMem(effectiveAdrress); zeroflag = (x == 0) ? 1 : 0; negativeflag = ((x & 0x80) != 0) ? 1 : 0;break;/*LDY Load Index Y with Memory M -> Y N Z C I D V + + - - - - addressing assembler opc bytes cyles -------------------------------------------- immediate LDY #oper A0 2 2 zeropage LDY oper A4 2 3 zeropage,X LDY oper,X B4 2 4 absolute LDY oper AC 3 4 absolute,X LDY oper,X BC 3 4*/ case 0xA0: y = arg1; zeroflag = (y == 0) ? 1 : 0; negativeflag = ((y & 0x80) != 0) ? 1 : 0; break; case 0xA4: case 0xB4: case 0xAC: case 0xBC: y = localMem.readMem(effectiveAdrress); zeroflag = (y == 0) ? 1 : 0; negativeflag = ((y & 0x80) != 0) ? 1 : 0; break;
Now, that was more like a copy and paste exercise!
Finally, let us give a shot at STA, STX, STY:
/*STA Store Accumulator in Memory A -> M N Z C I D V - - - - - - addressing assembler opc bytes cyles -------------------------------------------- zeropage STA oper 85 2 3 zeropage,X STA oper,X 95 2 4 absolute STA oper 8D 3 4 absolute,X STA oper,X 9D 3 5 absolute,Y STA oper,Y 99 3 5 (indirect,X) STA (oper,X) 81 2 6 (indirect),Y STA (oper),Y 91 2 6 */ case 0x85: case 0x95: case 0x8D: case 0x9D: case 0x99: case 0x81: case 0x91: localMem.writeMem(effectiveAdrress, acc); break; /*STX Store Index X in Memory X -> M N Z C I D V - - - - - - addressing assembler opc bytes cyles -------------------------------------------- zeropage STX oper 86 2 3 zeropage,Y STX oper,Y 96 2 4 absolute STX oper 8E 3 4 */ case 0x86: case 0x96: case 0x8E: localMem.writeMem(effectiveAdrress, x); break; /*STY Sore Index Y in Memory Y -> M N Z C I D V - - - - - - addressing assembler opc bytes cyles -------------------------------------------- zeropage STY oper 84 2 3 zeropage,X STY oper,X 94 2 4 absolute STY oper 8C 3 4 */ case 0x84: case 0x94: case 0x8C: localMem.writeMem(effectiveAdrress, y); break;
Testing it all
We write quite some code for our emulator, so its time for some testing. What we are most concerned about, is if we are more or less on track with address mode implementation, so I wrote the following 6502 assembly program to try and test of bunch of them:lda #$d0 a9 d0 ldx #$01 a2 01 sta $96 85 96 lda #$07 a9 07 sta $96,X 95 96 sta $99 85 99 lda #$d1 a9 d1 sta $98 85 98 lda #$55 a9 55 ldx #$02 a2 02 sta ($96,X) ; 81 96 ldy #$02 a0 02 lda #$56 a9 56 sta ($96),y ; 91 96 lda $07d1 ad d1 07 sta $07d1,x ; 9d d1 07
As you can see, I have placed the machine code version of each instruction to the right. Again, we will hardcode our program within the memory class via the mainMem private variable:
var mainMem = new Uint8Array ([0xa9, 0xd0, 0xa2, 0x01, 0x85, 0x96, 0xa9, 0x07, 0x95, 0x96, 0x85, 0x99, 0xa9, 0xd1, 0x85, 0x98, 0xa9, 0x55, 0xa2, 0x02, 0x81, 0x96, 0xa0, 0x02, 0xa9, 0x56, 0x91, 0x96, 0xad, 0xd1, 0x07, 0x9d, 0xd1, 0x07 ]);
This program consists of 16 instructions, so in index.html we hard code 16 calls to step() in our script block.
After this program has executed, we expect that memory location 2001 will contain the hexadecimal value 55, 2002 hexadecimal 56 and location 2003 the hexadecimal value 55. So, we will add some alert statements that will output the values of these locations to verify whether our program executed correctly on our emulator. With all these changes, your index.html should look like this:
<!DOCTYPE html> <html> <head> <title>6502 Emulator From Scratch</title> <script src="Memory.js"></script> <script src="Cpu.js"></script> </head> <body> <h1>6502 Emulator From Scratch</h1> <p>This is JavaScript Test</p> <script language="JavaScript"> var mymem = new memory(); var mycpu = new cpu(mymem); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); mycpu.step(); alert(mymem.readMem(0x7d1)); alert(mymem.readMem(0x7d2)); alert(mymem.readMem(0x7d3)); </script> </body> </html>
Now, let me give my experience when I tried to run this assembly program. I used Google chrome to run this test.
On my first attempt, only the html page opened with no alert at all. So, it is was time for me to do some javascript debugging! Luckily Chrome is equipped with a Javascript debugger.
To invoke the debugger, you right click on the page and select inspect:
Once the debugger opened it, the debugger immediately showed where the issue is:
And, the error is within our calculateEffevtiveAdd method we wrote. In this case it is a capitalisation issue. In our Memory class, we defined readMem, with a lowercase r, but we call it with an uppercase R. I have done it in a couple of places within the calculateEffevtiveAdd method.
So, lets go about and fix them all and hit refresh (arrow next to our URL bar) to restart the application.
This time around our alerts does fire, but all three show undefined. So, lets go into the debugger again!
This time, no error is reported, so we will need to set a breakpoint on the first alert. So click on the sources tab and select index.html. If you now click on the line number of the first alert statement, the breakpoint will be set:
Now, hit refresh again to restart the emulator. The debugger will no stop at the breakpoint you have defined. What we are interested at this point is the contents of our memory array defined within the memory object. At this level we can't really see anything meaningful. Stepping into the readMem function will give us this level of detail.
So, hit the Step-into button in the debugger.
The step-into button is the icon with the arrow pointing to the dot.
Once you step into the memory object, you can hover over any variable name with the mouse cursor, and the debugger will tell you the applicable variable's value.
If we inspect our mainMem object, the debugger conveniently show the values of elements for us. To our dismay, our array stops at element 33:
Ok, on the one hand not so much of a surprise. We have after all declared the mainMem array and assigned it our program which is 34 bytes!
But, an array in JavaScript is suppose to resize itself when accessing an element outside its declared range. Such a resize would have been expected when writing something memory location 2001, 2002 and 2003.
The answer to this catch 22 comes from the fact that Uint8array, is not a true JavaScript array, but a typed array. There is some subtle differences between a ordinary JavaScript array and a typed array, as pointed out by the following website:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
Firstly, calling isArray() on on typed array will return false. Also, a typed array doesn't support all the methods provided a normal array. Two of these methods an typed array doesn't support is push and pop.
Push is the method that allows you to append elements to an array. This gives us a clue that typed arrays doesn't really supports resizeable arrays!
With these limitations in mind, we will need to rethink how we declare our memory array. We will need to declare our array with the maximum size of 65536 bytes and then call set on the array to store our program:
var mainMem = new Uint8Array(65536); mainMem.set ([0xa9, 0xd0, 0xa2, 0x01, 0x85, 0x96, 0xa9, 0x07, 0x95, 0x96, 0x85, 0x99, 0xa9, 0xd1, 0x85, 0x98, 0xa9, 0x55, 0xa2, 0x02, 0x81, 0x96, 0xa0, 0x02, 0xa9, 0x56, 0x91, 0x96, 0xad, 0xd1, 0x07, 0x9d, 0xd1, 0x07 ]);
When we call set, our array will still have 65336 element aftwards. The contents of the first 34 bytes will just be replaces by our program.
If we now open up index.html again, The alerts with show 85, 86 and 85 in sequence, which is just the decimal equavlent of 0x55, 0x56 and 0x55.
Yippeee!!! Our program works
In Summary
In this post we have implemented all address variants of the load and store instructions.What will be covered next? What comes to mind is that our emulator currently doesn't give much output, apart from a couple of alerts we need to each time we execute a program.
So, in the next post I will try to improve on this limitation by implementing the following:
- Single stepping: Provide a button on our index page, allowing you to single yourself through a program
- Showing contents of the cpu registers and memory after each single step
That it for this post.
Till next time!
No comments:
Post a Comment