Foreword
In the previous post we implemented comparison and branching instructions.In this post we will be implementing the stack and related operations.
Introduction to the stack
You can view the stack as a kind of a Last-In-First-Out (LIFO) data structure.A typical real world example of a stack is a spike with pieces of paper on it. It is always easier to get hold of the last piece of paper you pushed onto it, than the initial piece of paper you pushed onto it.
On the 6502 CPU you can use the stack for a couple of things. For instance, when you run the instruction JSR to jump to a subroutine, the 6502 will save the current address (to be more specific: the address of the last byte of the JSR instruction) on the stack. So, when you return from the subroutine with RTS, the 6502 will retrieve the saved address from the stack, so it can return to the address just after the JSR instruction you have just called.
You are also free to push your own data on the stack, like the current value of the Accumulator. Later on you can then restore the accumulator with the saved value from the stack, after doing some temporary operations with the Accumulator.
How is the stack implemented on the 6502? First of all, page 1 of memory is used solely for the stack storage area (e.g. address 0x100-0x1ff). This means that the 6502 stack has a limit of 256 bytes.
Secondly, it is important to note that the stack grows downwards on the 6502. That is, it starts at address 0x1ff and as you push stuff on the stack, it will grow towards address 0x100.
How does the 6502 keeps track of the current position on the stack? The 6502 actually have a register for this purpose, called the Stack pointer, or SP for short. The initial value of this register is 0xff which in effect point to memory address 0x1ff, e.g. the preceding 1 of the resulting address is implied. So, every time you push a byte on the stack, the Stack Pointer get decremented by one. Likewise, when you pop a byte from the stack, the Stack Pointer gets incremented by one.
Implementing the Stack
It it time to implement the stack within our emulator.First of all we need to add the stack pointer as a variable to cpu.c:
jchar sp = 0xff;
As you can see, we initialise the stackpointer with 0xff since the stack starts at 0x1ff and then grow downwards to 0x100.
Next, we need to define two atomic stack operation methods: Push and Pop. All 6502 instructions manipulating the stack within our emulator, will need to call these methods.
Here is the implementation for Push and Pop:
void Push(char value) { memory_write((sp | 0x100), value); sp--; sp = sp & 0xff; } char Pop() { sp++; sp = sp & 0xff; char result = memory_read(sp | 0x100); return result; }
For the Push method you supply the value you want to push onto the stack. The value gets written to memory in the stack area and the stack pointer updated accordingly.
The Pop method read from memory the value on top of the stack and return it, updating the stackpointer afterwards.
Implementing 6502 Push and Pop instructions
As mentioned earlier, the 6502 provides you with instructions to directly Push and Pop values from the stack. These direct Push and Pop instructions can only be performed on two sources: The Accumulator and the Status Register.Let us start to implement the Push and Pop instructions for the Accumulator:
/*PHA Push Accumulator on Stack push A N Z C I D V - - - - - - addressing assembler opc bytes cyles -------------------------------------------- implied PHA 48 1 3 */ case 0x48: Push(acc); break; /*PLA Pull Accumulator from Stack pull A N Z C I D V + + - - - - addressing assembler opc bytes cyles -------------------------------------------- implied PLA 68 1 4 */ case 0x68: acc = Pop(); zeroFlag = (acc == 0) ? 1 : 0; negativeFlag = ((acc & 0x80) != 0) ? 1 : 0; break;
Next, let us implement the Push and Pop instructions for the Status Register.
I briefly touched the service of the Status Register in the previous post, mentioning that each flag is represented by particular bit within the Status Register. This is a bit of a contrast in how we currently have implemented flags within our emulator: Currently we do store all the flags within a single byte, but each flag has its own variable.
In order to implement the instructions for Pushing and Popping the Status register, we need to create the following methods to collate the separate flag variables into a single byte and vice-versa:
char getStatusFlagsAsByte() { char result = (negativeFlag << 7) | (overflowFlag << 6) | (zeroFlag << 1) | (carryFlag); return result; } void setStatusFlagsAsByte(char value) { negativeFlag = (value >> 7) & 1; overflowFlag = (value >> 6) & 1; zeroFlag = (value >> 1) & 1; carryFlag = (value) & 1; }
For now I am only working with a subset of the flags. We will move to a complete implementation when put our emulator through a Test Suite.
With all this implemented, we can now implement the Push and Pop instructions for the Status Register:
/*PHP Push Processor Status on Stack push SR N Z C I D V - - - - - - addressing assembler opc bytes cyles -------------------------------------------- implied PHP 08 1 3 */ case 0x08: Push(getStatusFlagsAsByte()); break; /*PLP Pull Processor Status from Stack pull SR N Z C I D V from stack addressing assembler opc bytes cyles -------------------------------------------- implied PHP 28 1 4 */ case 0x28: setStatusFlagsAsByte(Pop()); break;
Implementing JSR and RTS
Let us now implement the JSR and RTS instructions:/*JSR Jump to New Location Saving Return Address push (PC+2), N Z C I D V (PC+1) -> PCL - - - - - - (PC+2) -> PCH addressing assembler opc bytes cyles -------------------------------------------- absolute JSR oper 20 3 6 */ case 0x20: tempVal = pc - 1; Push((tempVal >> 8) & 0xff); Push(tempVal & 0xff); pc = effectiveAdrress; break; /*RTS Return from Subroutine pull PC, PC+1 -> PC N Z C I D V - - - - - - addressing assembler opc bytes cyles -------------------------------------------- implied RTS 60 1 6 */ case 0x60: tempVal = Pop(); tempVal = tempVal + Pop() * 256; pc = tempVal + 1; break;
Maybe worthwhile to mention that with JSR instruction, the 6502 pushes the address of the third byte of the JSR instruction on the stack. At the point when the push happens, the pc register within our emulator points to the next instruction. For that reason we subract one pc and pushes the result when we execute a JSR instruction.
Testing
Time to test the code we wrote for emulator in this post.Again, we will be using a test program:
0000 LDA #$52 A9 52 0002 PHA 48 0003 LDA #$07 A9 07 0005 JSR $000A 20 0A 00 0008 PLA 68 0009 00 00 000A SBC #$06 E9 06 000C RTS 60
This program will translate to the following array declaration within memory .c:
jchar my_program[] = {0xA9, 0x52, 0x48, 0xA9, 0x07, 0x20, 0x0A, 0x00, 0x68, 0x00, 0xE9, 0x06, 0x60 };
Before we jump in and run the test program, I just would like to focus on the fact we have added a new register: The Stack Register. It would rather be nice if we could also show the contents of this register while stepping through the program.
So, let us quickly adjust the GUI. First thing to do is to add a method to cpu.c so MainActivity can access the Stack Pointer:
jchar Java_com_johan_emulator_MainActivity_getSP(JNIEnv* pEnv, jobject pObj) { return sp; }
Similarly, we need to add a stub for this method to MainActivity:
public native char getSP();
Before we continue, we need to adjust the getRegisterDump() method to accept the StackPointer value as a parameter:
public String getRegisterDump(char acc, char xReg, char yReg, char SP) { String accStr = "00"+Integer.toHexString(acc); accStr = accStr.substring(accStr.length() - 2); String xRegStr = "00"+Integer.toHexString(xReg); xRegStr = xRegStr.substring(xRegStr.length() - 2); String yRegStr = "00"+Integer.toHexString(yReg); yRegStr = yRegStr.substring(yRegStr.length() - 2); String spStr = "00"+Integer.toHexString(SP); spStr = spStr.substring(spStr.length() - 2); return accStr + xRegStr + yRegStr + spStr; }
And finally we modify refreshControls to the native getSP() method and pass it as a parameter to getRegisterDump:
... TextView viewReg = (TextView) findViewById(R.id.Registers); viewReg.setText(getRegisterDump(getAcc(), getXreg(), getYreg(), getSP())); ...
It is is now time to build our Android Project again and run it on an Android device.
When stepping through this application, we are particularly interested in keeping an eye on the stack to see how our application behaves.So, before we start to step, specify 1f0 as the memory dump address. If you now click on Refresh Dump, our screen will look as follows:
I have indicated in red the new register, which is the Stack Pointer.
If you now click STEP, the LDA instruction will execute. The next instruction in line to execute, is PHA. When this instruction executes, some interesting stuff will happen on the stack. So, once this instruction has executed, the screen will look as follows:
A couple of things has happened here. First, the Stack Pointer has been reduced by one. You will also see, the value 52 has been pushed onto the stack, which happens to be at memory location 0x1ff.
Click STEP once more to execute the LDA instruction as in the above screen.
The next interesting to execute is JSR $000A. So hit STEP once more, so we can see what happens:
You will see something interesting has happened again on the stack. The value 0 was pushed, and then the value 7. This is the return address. Talking of return address, one might have expected that the second value pushed should have been an 8 instead of a 7. Remember, however that the JSR pushes the address of byte three of the JSR instruction.
One further instruction gets executed, then we get to RTS. When this instruction executes, our emulator jumps to the memory location just after the preceding JSR instruction we executed a while ago:
If we now execute this PLA instruction, our Accumulator will be restored to 52, the value we previously pushed onto the stack.
In Summary
In this post we have implemented the stack and related operations into our emulator.In the next post we will implement the remaining 6502 instructions into our emulator, after which we will be ready to run a Test Suite on our emulator.
Till next time!
No comments:
Post a Comment