Foreword
In the previous post we rewrote the back-end of our emulator to C and compiled it into a native module with the help of the Android NDK.In this post we will be adding more instructions to our emulator.
The instructions I will be adding will be the LOAD and STORE instructions. These instructions are the following (including all Address Mode variants): LDA, LDX, LDY, STA, STX, STY.
In this post we will be following more or less the same approach as Part 2 of my JavaScript emulator series: 6502 address modes. The syntax of JavaScript and C is very close to each other, so I will basically copying and pasting most of the JavaScript emulator code for this series. Of course there is quite a number of differences between the two languages, which I will cover in this post and in coming posts.
Preliminary Planning
Before we start with this post, let us do some quick planning on what we want to achieve.We start off by looking at the 6502 test program that we use in Part 2 of our JavaScript emulator series:
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 stated in the blog post, when 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, in order for our us to check whether this program has executed successfully, our emulator will need to provide us with a dump of the memory at location 2000.
Currently, our emulator is hardcoded to only show a dump starting at memory location 0, even though an edit box is provided for specifying a different address.
Therefore, as part of our goals of this post is to change our emulator to actually use the contents of the address box to know which part of memory should be dumped.
Also, currently our memory array is only as long as our machine language program we assign to it within memory.c. This means, for example, that with the above mentioned assembly program, our memory will only be 34 bytes.
This is a problem because this assembly program access the memory locations 2001, 2002 and 2003, well out of reach of 34 byte memory array!
We will get around this limitation by creating an empty 64kb memory array on startup, and then copying our program array array to it.
Extending Memory functionality
Let start to implement the mentioned new memory functionality.The first file we will visit is memory.c.
Within this file we make the following changes:
jchar my_program[] = {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}; jchar mainMem[65536];
As you can see the definition of the mainMem array changed a bit. We don't assign our machine language program directly to this array any more. We rather define our mainMem an 64k element array without assigning any value. Such an array declaration will automatically initialised as an all zero array on startup.
You will also see that we added a new array declaration my_program, which contains our actual machine language program.
What should happen in principle is that the contents of my_program should be copied to the first couple of locations within mainMem.
The trick comes in with the fact that C doesn't provide you with functionality like static initialisers that would have perform the task for us.
So, we need to create a method that the static initialiser of MainActivity.java can invoke to kick off the process.
So, first we create the following method within memory.c:
void Java_com_johan_emulator_MainActivity_memoryInit(JNIEnv* pEnv, jobject pObj) { memcpy(mainMem, my_program, sizeof(my_program)); }
Next, we need to create native stub within MainActivity.java:
... public native char[] dump(); public native void step(); public static native void memoryInit(); ...
And we invoke this method within the static initialiser of MainActivity:
static { System.loadLibrary("native_emu"); memoryInit(); }
One thing to take note of is that this code executes within a static context, e.g. no object instance exist and for that matter this doesn't exist. It is therefore important that the native method stub should contain the keyword static.
An interesting question, would be is that if this doesn't exist for this method, what does your native method receive as parameter pObj? And the answer is a Class instance. In this case the class instance is MainActivity.class.
The final memory-related task we need to do is let our emulator make use of the address edit box to know from which address to display the dump. For this purpose we modify the getMemDumpAsString of MainActivity a bit:
public String getMemDumpAsString(char[] memContents) { EditText editText = (EditText) findViewById(R.id.inputSearchEditText); String editAddress = "0"+editText.getText().toString(); int intaddress = Integer.parseInt(editAddress,16); String result = ""; byte[] temp = new byte[48]; for (int i=0; i < temp.length; i++) { temp[i] = (byte) memContents[i+intaddress]; } for (int i = 0; i < temp.length; i++) { if ((i % 16) == 0) { String numberStr = Integer.toHexString(i+intaddress); numberStr = "0000" + numberStr; result = result + "\n" + numberStr.substring(numberStr.length() - 4); } String number = "0" + Integer.toHexString(temp[i] & 0xff); number = number.substring(number.length() - 2); result = result + " " + number; } return result; }
We get the contents of the editbox and add a zero in front of it. This is just to cater for the scenario when the editbox is empty, the parsing process will yield a zero. We then convert the string to a integer and add the resulting address to different variables in the process.
Adding extra instructions
Now it is time to add all the different LOAD and STORE instructions to our emulator.There is basically 6 assembly mnemonics for the LOAD and STORE instructions: LDA, LDX, LDY, STA, STX and STY.
What makes life complicated with these mnemonics is that each mnemonic can have a couple of different flavours called address modes.
To simplify the landscape one just needs to remember that the different flavours of a mnemonic are just different ways to obtain a effective address to act on.
So, if one could move all this address resolution code into a method of its own, one can simplify the writing of your emulator a lot.
Also, to simplify code, we need to move the properties of the different properties of the different instructions into lookup tables. We will add these lookup tables to the beginning of the file cpu.c:
#define ADDRESS_MODE_ACCUMULATOR 0 #define ADDRESS_MODE_ABSOLUTE 1 #define ADDRESS_MODE_ABSOLUTE_X_INDEXED 2 #define ADDRESS_MODE_ABSOLUTE_Y_INDEXED 3 #define ADDRESS_MODE_IMMEDIATE 4 #define ADDRESS_MODE_IMPLIED 5 #define ADDRESS_MODE_INDIRECT 6 #define ADDRESS_MODE_X_INDEXED_INDIRECT 7 #define ADDRESS_MODE_INDIRECT_Y_INDEXED 8 #define ADDRESS_MODE_RELATIVE 9 #define ADDRESS_MODE_ZERO_PAGE 10 #define ADDRESS_MODE_ZERO_PAGE_X_INDEXED 11 #define ADDRESS_MODE_ZERO_PAGE_Y_INDEXED 12 const unsigned char addressModes[] = {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, 9, 8, 0, 0, 0, 11, 11, 0, 5, 3, 0, 0, 0, 2, 2, 0, 5, 7, 0, 0, 0, 10, 10, 0, 5, 4, 0, 0, 1, 1, 1, 0, 9, 8, 0, 0, 0, 11, 11, 0, 5, 3, 0, 0, 0, 2, 2, 0, 5, 7, 0, 0, 0, 10, 10, 0, 5, 4, 0, 0, 6, 1, 1, 0, 9, 8, 0, 0, 0, 11, 11, 0, 5, 3, 0, 0, 0, 2, 2, 0, 0, 7, 0, 0, 10, 10, 10, 0, 5, 0, 5, 0, 1, 1, 1, 0, 9, 8, 0, 0, 11, 11, 12, 0, 5, 3, 5, 0, 0, 2, 0, 0, 4, 7, 4, 0, 10, 10, 10, 0, 5, 4, 5, 0, 1, 1, 1, 0, 9, 8, 0, 0, 11, 11, 12, 0, 5, 3, 5, 0, 2, 2, 3, 0, 4, 7, 0, 0, 10, 10, 10, 0, 5, 4, 5, 0, 1, 1, 1, 0, 9, 8, 0, 0, 0, 11, 11, 0, 5, 3, 0, 0, 0, 2, 2, 0, 4, 7, 0, 0, 10, 10, 10, 0, 5, 4, 5, 0, 1, 1, 1, 0, 9, 8, 0, 0, 0, 11, 11, 0, 5, 3, 0, 0, 0, 2, 2, 0}; const unsigned char instructionLengths[] = {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, 1, 2, 0, 0, 0, 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, 1, 2, 0, 0, 0, 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, 0, 2, 0, 0, 2, 2, 2, 0, 1, 0, 1, 0, 3, 3, 3, 0, 2, 2, 0, 0, 2, 2, 2, 0, 1, 3, 1, 0, 0, 3, 0, 0, 2, 2, 2, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, 2, 2, 0, 0, 2, 2, 2, 0, 1, 3, 1, 0, 3, 3, 3, 0, 2, 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, 2, 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}; const unsigned char instructionCycles[] = {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, 6, 6, 0, 0, 0, 3, 5, 0, 3, 2, 2, 0, 3, 4, 6, 0, 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 6, 6, 0, 0, 0, 3, 5, 0, 4, 2, 2, 0, 5, 4, 6, 0, 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 0, 6, 0, 0, 3, 3, 3, 0, 2, 0, 2, 0, 4, 4, 4, 0, 2, 6, 0, 0, 4, 4, 4, 0, 2, 5, 2, 0, 0, 5, 0, 0, 2, 6, 2, 0, 3, 3, 3, 0, 2, 2, 2, 0, 4, 4, 4, 0, 2, 5, 0, 0, 4, 4, 4, 0, 2, 4, 2, 0, 4, 4, 4, 0, 2, 6, 0, 0, 3, 3, 5, 0, 2, 2, 2, 0, 4, 4, 3, 0, 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 2, 6, 0, 0, 3, 3, 5, 0, 2, 2, 2, 0, 4, 4, 6, 0, 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0};
This was also more or less a copy and paste exercise from my previous JavaScript emulator.
You will see see for all this constant information we declare by use of the keyword const, effectively declaring these information as read-only. The exception to the rule is the address mode constants in which each of them is declared as #define.
The reason for this is that we will be using these address mode constants as case labels within a switch statement. In switch statements C is a bit fuzzy and will only literals as case labels and constants simply wouldn't do.
Defines is our way out of this issue. When the C-preprocessor run during compilation, all instances where you use the define will be replaced by a physical literal.
We now need to define the method that will calculate the effective address for us for the relevant address mode:
int calculateEffevtiveAdd(unsigned char mode, int argbyte1, int argbyte2) { int tempAddress = 0; switch (mode) { case ADDRESS_MODE_ACCUMULATOR: return 0; break; case ADDRESS_MODE_ABSOLUTE: return ((argbyte2 << 8) + argbyte1); break; case ADDRESS_MODE_ABSOLUTE_X_INDEXED: tempAddress = ((argbyte2 << 8) + argbyte1); tempAddress = tempAddress + xReg; return tempAddress; break; case ADDRESS_MODE_ABSOLUTE_Y_INDEXED: tempAddress = ((argbyte2 << 8) + argbyte1); tempAddress = tempAddress + yReg; return tempAddress; break; case ADDRESS_MODE_IMMEDIATE: break; case ADDRESS_MODE_IMPLIED: break; case ADDRESS_MODE_INDIRECT: tempAddress = ((argbyte2 << 8) + argbyte1); return ((memory_read(tempAddress + 1) << 8) + memory_read(tempAddress)); break; case ADDRESS_MODE_X_INDEXED_INDIRECT: tempAddress = (argbyte1 + xReg) & 0xff; return ((memory_read(tempAddress + 1) << 8) + memory_read(tempAddress)); break; case ADDRESS_MODE_INDIRECT_Y_INDEXED: tempAddress = (memory_read(argbyte1 + 1) << 8) + memory_read(argbyte1) + yReg; 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 + xReg) & 0xff; break; case ADDRESS_MODE_ZERO_PAGE_Y_INDEXED: return (argbyte1 + yReg) & 0xff; break; } }
Next, we need to modify the step method within cpu.c:
void step() { int opcode = memory_read(pc); pc = pc + 1; int iLen = instructionLengths[opcode]; jchar arg1 = 0; jchar arg2 = 0; int effectiveAdrress = 0; if (iLen > 1) { arg1 = memory_read(pc); pc = pc + 1; } if (iLen > 2) { arg2 = memory_read(pc); pc = pc + 1; } effectiveAdrress = calculateEffevtiveAdd(addressModes[opcode], arg1, arg2); switch (opcode) { /*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; updateFlags(acc); break; case 0xA5: case 0xB5: case 0xAD: case 0xBD: case 0xB9: case 0xA1: case 0xB1: acc = memory_read(effectiveAdrress); updateFlags(acc); break; /*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: xReg = arg1; updateFlags(xReg); break; case 0xA6: case 0xB6: case 0xAE: case 0xBE: xReg = memory_read(effectiveAdrress); updateFlags(xReg); 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: yReg = arg1; updateFlags(yReg); break; case 0xA4: case 0xB4: case 0xAC: case 0xBC: yReg = memory_read(effectiveAdrress); updateFlags(yReg); break; /*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: memory_write(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: memory_write(effectiveAdrress, xReg); 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: memory_write(effectiveAdrress, yReg); break; } }
This cover all the code changes we need to do for this post.
Running the test program
Time for us to run the test 6502 program.When stepping through the application we should keep an eye on memory locations 2001, 2002 and 2003, as mentioned earlier.
So, before stepping enter 7D0 (e.g. hex equalavent of 2000) in the address box.
Now click the step button about sixteen times. After sixteen times your display will look as follows:
This is in effect what we want. So we know that we are more or less on track with our emulator.
In Summary
In this post we have implemented all address mode variants of the LOAD and STORE instructions.In the next blog we will adding all variants of the ADC and SBC instructions (Add with carry and Subtract with carry).
We will also be extending the functionality of our front end once more. This time around we will showing the contents of the CPU registers while stepping. We will also be showing the disassembled version of the next instruction to be executed.
Till next time!
Why do you not use a C enum to define your address modes? It's what they are for. In fact, many C programmers use enum to define all of their integer constants because they work everywhere a #define works but are part of the language proper.
ReplyDeleteThanks for the pointer. Will keep in mind in the future.
DeleteI must admit I am not a seasoned C programmer. I still learn new things about the language every day.