Foreword
In the previous section we implemented the remaining instructions of our CPU emulator.In this section we will run our emulator against the Klaus Test Suite.
But, before we start, we need to extend the debugging functionality of our emulator a bit...
Extending Debug functionality
Currently the only way to run our emulator is to single step it. A very painful way to run the Klaus Test Suite!So, it is obvious that we need to add a continuous run feature in our emulator. At the same time, we will also need a feature that will stop continuous execution at any time, allowing the user to single step again from that point onwards.
The stop functionality is important in running the Klaus Test Suite, as you will have no other way to evaluate at which point the tests failed.
Here is a list of two other nice debug features we will implement in this post:
- Breakpoints: For a first round we will give the option to add a single breakpoint
- Memory Dump: We will modify the exiting functionality so that you can specify a different area in memory to view
Run/Stop Functionality
Implementation time! Lets start by adding a Run and a Stop button next to the step button in index.html:
<button id="btnStep" onclick="step()">Step</button> <button id="btnRun" onclick="startEm()">Run</button> <button id="btnStop" onclick="stopEm()">Stop</button>
Before we head off and wrote code for these two methods, it is important to keep the threading model of JavaScript/Browser in mind.
The most important aspect of the threading model is that each page open in a browser can only have a single thread at any time. With this thread the page in the browser must do all its work: From rendering the content of the page to the screen, checking for button clicks, executing JavaScript code and so on.
This means that when a page is busy executing JavaScript, that page will appear unresponsive.
This single threaded nature has very important implications for our Run/Stop functionality. When our emulator is busy executing instructions continuously, it won't be able to accept the Stop command!
How do you get around this limitation? Well, JavaScript provides you with a method called setInterval(). To understand this method let's look at an example snippet of code:
myTimer = setInterval(runBatch, 200);
This method call basically says: Run the method runBatch every 200 milliseconds. In the runBatch method you will run a small batch of assembly instructions (e.g. calling step() a couple of times) and then exit. RunBatch will again be called when 200 milliseconds lapsed and the whole process will be repeated.
This whole process will continue running, until at some point in time you call clearInterval(). More on this method in a moment.
So, the question is: How many 6502 instructions do you execute in a batch? For this Blog Post, it doesn’t really matter. As long as his emulator web page is kind of responsive.
When I played with this method I started off with a batch of 100 instructions. After a while however, I found that the Test Suite takes ages to complete. So my patience ran out and I ended off with a batch of 100000 :-)
Obviously, in later blog posts where we will add a screen as output, we will want to get everything working at the expected speed of a C64. In this case you will need to tune the batch size and also the interval.
Back to our snippet of code. As you see the returned value of SetInterval() gets assigned to a variable. So the question is: What does this variable contain? It contains a instance of the timer you have just created.
What can you do with this timer instance? This is actually where clearInterval() comes in, as I mentioned earlier on. When you call clearInterval with the Timer Instance as a parameter, you actually stops continuous execution of the runBatch method.
With all this information, we have enough to implement the Run/Stop functionality for the emulator:
function startEm() { document.getElementById("btnStep").disabled = true; document.getElementById("btnRun").disabled = true; document.getElementById("btnStop").disabled = false; myTimer = setInterval(runBatch, 200); } function stopEm() { clearInterval(mytimer); displayEmuState(); document.getElementById("btnStep").disabled = false; document.getElementById("btnRun").disabled = false; document.getElementById("btnStop").disabled = true; }
You will notice a new method displayEmuState().This is basically the display code in index.html's step method that I moved into a method of its own. I was busy with a bit of code cleanup for index.html. You can have a look at the GitHub zip file for this post to get an idea of what I was up to with index.html.
myTimer you will need to declare as a global variable:
var mymem = new memory(); var mycpu = new cpu(mymem); var mytimer;
Finally, lets implement runBatch:
function runBatch() { for (i=0; i < 100; i++) { mycpu.step(); } }
Very minimalistic.
Breakpoints
Lets implement the single breakpoint functionality as I mentioned earlier.At the bottom of the page, lets add a input field allowing the user to enter a address at which the emulator should break:
<button id="btnStep" onclick="step()">Step</button> <button id="btnRun" onclick="startEm()">Run</button> <button id="btnStop" onclick="stopEm()">Stop</button> <br/> Break at: <input type="text" id="breakpoint">
The runBatch method should check the program counter after each call to Cpu.step() to determine whether this address was reached.
There is two foreseeable issues that we need to resolve first before implementing the checking in runBatch.
Firstly, as our Cpu class is currently written, outsiders don't have access to the program counter. So, lets implement a getter in the CPU class:
this.getPc = function () { return pc; }
Lets discuss the second problem. The value of the text input field for the breakpoint address is a String. This means that each time the runBatch method need to first convert the String to an integer, and then do the comparison with the program counter. Sounds like a bit of waste!
We can do better. When clicking run the startEm() method can actually do this conversion once and assign it to a global variable. The runBatch method can then use the value of this global variable each time.
Here is how the global variable and the startEm() method will look like:
var breakpoint = 0; ... function startEm() { document.getElementById("btnStep").disabled = true; document.getElementById("btnRun").disabled = true; document.getElementById("btnStop").disabled = false; var myBreak = document.getElementById("breakpoint"); breakpoint = parseInt(myBreak.value, 16); myTimer = setInterval(runBatch, 200); }
We are now ready to implement to breakpoint checking in runBatch:
function runBatch() { for (i=0; i < 100; i++) { mycpu.step(); if (mycpu.getPc() == breakpoint) { stopEm(); break; } } }
So, when breakpoint is reached, stopEm() is called which clears the interval timer and update the state display of the emulator. The break exits the loop so that no further instructions is executed for the batch.
Ok, this is about it for implementing breakpoint functionality. Or, is it really? When I used this breakpoint functionality to figure out why some test cases fails on emulator, I stumbled across a weird anomaly. This anomaly entailed that when the breakpoint point fired, clicking step wouldn't take you to the next instruction, but to the tenth or so instruction where you stopped.
It was as if there was a pending timer event that was still queued in the JavaScript event queue.
Googling didn't reveal much info on this issue. There was actually one useful piece of info suggesting the use of a global variable like running. When you click stop, you need to set this variable to false and in your runBatch method you actually check this variable. If runBatch sees running is false, it just exits. Here is how the changes will look like:
var running = false; ... function runBatch() { if (!running) return; for (i=0; i < 100000; i++) { mycpu.step(); if (mycpu.getPc() == breakpoint) { stopEm(); break; } } } function startEm() { document.getElementById("btnStep").disabled = true; document.getElementById("btnRun").disabled = true; document.getElementById("btnStop").disabled = false; var myBreak = document.getElementById("breakpoint"); breakpoint = parseInt(myBreak.value, 16); running = true; myTimer = setInterval(runBatch, 200); } function stopEm() { running = false; clearInterval(mytimer); displayEmuState(); document.getElementById("btnStep").disabled = false; document.getElementById("btnRun").disabled = false; document.getElementById("btnStop").disabled = true; }
Flexible Memory dump
Currently the emulator's memory dump functionality is limited to viewing memory from location 0. In this section we will add an input text field so that the user can choose to view a different area in memory.Lets start off by adding the input field and button to index.html:
<textarea id="memory" name="mem" rows="15" cols="60"></textarea> From Location: <input type="text" id="frommem"> <button onclick="showMem()">Refresh Dump</button><br/>
The showMem() is one of the methods that I wrote while doing code cleanup index.html. This method also needs to be adjusted to look at the frommem input field for the start address:
function showMem() { var m = document.getElementById("memory"); var location = document.getElementById("frommem"); locationInt = parseInt(location.value, 16); tempmemstr = "" for (i = locationInt; i < (160 + locationInt); i++) { if ((i % 16) == 0) { labelstr = ""; labelstr = labelstr + "0000" + i.toString(16); labelstr = labelstr.slice(-4); tempmemstr = tempmemstr + "\n" + labelstr; } currentByte = "00" + mymem.readMem(i).toString(16); currentByte = currentByte.slice(-2); tempmemstr = tempmemstr + " " + currentByte; } m.value = tempmemstr; }
We are now ready for preparing to run the Klaus Test Suite.
Preparing to run Test Suite
Firstly we need to get hold of the Klaus Test Suite. It is available via GitHub:https://github.com/Klaus2m5/6502_65C02_functional_tests
The assembly files (e.g. the ones with the a65) needs to be compiled with the a65 assembler.
For convenience, compiled binary files is also provided in the bin_files folder. These binaries are memory images that you copy straight to your 64KB emulated memory. I think I am going to take the convenience route :-)
You will also notice that together with compiled binary files in the bin_files folder, there are .lst files. The a65 assembler generates these listings when it is assembling the source. The beauty of these listing files is that it keeps all the comments together with the memory address where each instruction is in memory.
This info in the listing files comes in very handy when a test fails at a particular address, so you can look it up in the listing file and almost immediately know what the test that fail is about. Sadly, I only discovered this after I managed to run the Test Suite successfully :-(
From the assembly listing it stated that you need to start executing at memory location $400. So we will assign this address to the program counter when the CPU class is created.
As in the previous posts will add the Test Suite binary as an array in the Memory class. To assist in this task, I wrote a quick Java program taking the binary and spitting out the contents as a JavaScript array. In the GitHub zipfile for this post the Memory class will be populated with the Test Suite program.
While running the Test suite I also found it handy to have a disassembled listing of the binary at hand. This is nice for quick reference. There is a nice online tool that can assist you in the disassembly of the binary at the following link:
http://e-tradition.net/bytes/6502/disassembler.html
Just open the binary with a hex editor and copy the hex values from $400 till about $370D. You will see the succeeding memory locations is filled with Hex FF's.
Now paste the copied hex values in the left panel (called code) of the online disassembler, enter 400 as opcode start address and click disassemble. Your screen will look similar to the following:
We now have all the necessary resources to start running the Test Suite.
Running the Test Suite
With everything in place, lets hit run to kick off the Test Suite.The start off not so well. Right at the beginning I get a pop-up: Op code 216 not implemented. PC = 401
216 is D8 in hex and this is the CLD (clear decimal flag) the instruction.
OK, I must admit I didn't consider BCD at all in the emulator up now. To get pass this error let implement the decimal flag private variable and implement the CLD and SED instructions. For now I will not worry about BCD addition or subtraction:
var decimalflag = 0; ... /*CLD Clear Decimal Mode 0 -> D N Z C I D V - - - - 0 - addressing assembler opc bytes cyles -------------------------------------------- implied CLD D8 1 2 */ case 0xD8: decimalflag = 0; break; /*SED Set Decimal Flag 1 -> D N Z C I D V - - - - 1 - addressing assembler opc bytes cyles -------------------------------------------- implied SED F8 1 2 */ case 0xF8: decimalflag = 1; break;
With this added we run the test again. This time no alert pops up. I let it run for three minutes and then hit stop. At this point, this is how my browser window looks like:
Clicking step from this point onwards keeps jumping back to $5f0. A test has therefore failed. Lets investigate by looking at the surrounding code:
05E7 08 PHP 05E8 BA TSX 05E9 E0 FE CPX #$FE 05EB D0 FE BNE $05EB 05ED 68 PLA 05EE C9 FF CMP #$FF 05F0 D0 FE BNE $05F0 05F2 BA TSX 05F3 E0 FF CPX #$FF 05F5 D0 FE BNE $05F5
What is happening here? First the status register is pushed unto the stack, and then popped into the accumulator. The accumulator is then compared with $FF
So the applicable test tests that all flags of the status register are set.
From the above screen we see that the accumulator is $C3. This translates to 1100 0011 binary and means that in our case bits 2 - 5 our status register was cleared. These bits represents the following flags:
- bit 5: Ignored
- bit 4: Break
- bit 3: Decimal
- bit 2: Interrupt
From a couple of resources I found that bit 5 is always hardcoded to one.
Bit 4 (the break flag) is used during a interrupt do determine whether the interrupt was caused by an IRQ/NMI or whether was due to executing the BRK instruction. If the interrupt was due to a IRQ/NMI the flag will be cleared. For all other instances the BRK flag will be set. For normal operations it is therefore save to assume a BRK flag value of 1.
Bit 3 (Decimal). Ok, the previous fix we did implement SED and CLD, but we never adjusted the getStatusFlagsAsByte and setStatusFlagsAsByte method!
Bit 2. Interrupt flag. First of all we didn't implement the SEI and CLI instruction. But, our emulator didn't complain about missing these instructions. So it must be something todo with the initial state of the interrupt flag. Googling around it looks like the Interrupt disable flag is set on a fresh 6502 start.
With all this information, we make the following changes in the CPU class:
With this fixed lets run the emulator again. This time this alerts pops up: Op code 234 not implemented. PC = 870
The NOP is not implemented. So lets do that quickly:
At the next run, there is a complained about the BRK instruction not implemented. This instruction trigger an interrupt, so lets implement BRK and RTI together:
Next, the emulator gets stuck at $36E5. Lets look again at the surrounding code:
Looking further down in the code, I see the code actually ends off with a RTI, so this code is part of a interrupt service routine.
Looking back in the code I copied, I see the stack pointer gets copied to the X register and then the value of memory location $102,X is compared with value $30.
From the debug window I figured out the stackpointer and register X has value $FB. So the effective meomory location is $01FD. Since this is inside an interrupt service routine, we know pushed values on the stack should be the address and the value of the status register. So looking at everything I feel comfortable that the value we compare is a status register. The value at $01FD is $34 and the Test Suite expects $30.
The two values differ by a single bit; Bit 2, which is the interrupt disable register. So, my emulator set the interrupt disable flag and the Test Suite expects this flag to be cleared. After some reasoning I established the following:
Bit 2. Interrupt flag. First of all we didn't implement the SEI and CLI instruction. But, our emulator didn't complain about missing these instructions. So it must be something todo with the initial state of the interrupt flag. Googling around it looks like the Interrupt disable flag is set on a fresh 6502 start.
With all this information, we make the following changes in the CPU class:
var carryflag =0; var overflowflag =0; var decimalflag = 0; var interruptflag = 1; var breakflag = 1; ... function getStatusFlagsAsByte() { var result = (negativeflag << 7) | (overflowflag << 6) | (1 << 5) | (breakflag << 4) | (decimalflag << 3) | (interruptflag << 2) | (zeroflag << 1) | (carryflag); return result; } function setStatusFlagsAsByte(value) { negativeflag = (value >> 7) & 1; overflowflag = (value >> 6) & 1; decimalflag = (value >> 3) & 1; interruptflag = (value >> 2) & 1; zeroflag = (value >> 1) & 1; carryflag = (value) & 1; }
With this fixed lets run the emulator again. This time this alerts pops up: Op code 234 not implemented. PC = 870
The NOP is not implemented. So lets do that quickly:
/*NOP No Operation --- N Z C I D V - - - - - - addressing assembler opc bytes cyles -------------------------------------------- implied NOP EA 1 2 */ case 0xEA: break;
At the next run, there is a complained about the BRK instruction not implemented. This instruction trigger an interrupt, so lets implement BRK and RTI together:
/*BRK Force Break interrupt, N Z C I D V push PC+2, push SR - - - 1 - - addressing assembler opc bytes cyles -------------------------------------------- implied BRK 00 1 7 */ case 0x00: interruptflag = 1; var tempVal = pc + 1; Push(tempVal >> 8); Push(tempVal & 0xff); Push(getStatusFlagsAsByte()); tempVal = localMem.readMem(0xffff) * 256; tempVal = tempVal + localMem.readMem(0xfffe); pc = tempVal; break; /*RTI Return from Interrupt pull SR, pull PC N Z C I D V from stack addressing assembler opc bytes cyles -------------------------------------------- implied RTI 40 1 6 */ case 0x40: setStatusFlagsAsByte(Pop()); var tempVal = Pop(); tempVal = (Pop() << 8) + tempVal; pc = tempVal; break;
Next, the emulator gets stuck at $36E5. Lets look again at the surrounding code:
36C9 88 DEY 36CA 88 DEY 36CB 08 PHP 36CC 88 DEY 36CD 88 DEY 36CE 88 DEY 36CF C9 42 CMP #$42 36D1 D0 FE BNE $36D1 36D3 E0 52 CPX #$52 36D5 D0 FE BNE $36D5 36D7 C0 48 CPY #$48 36D9 D0 FE BNE $36D9 36DB 85 0A STA $0A 36DD 86 0B STX $0B 36DF BA TSX 36E0 BD 02 01 LDA $0102,X 36E3 C9 30 CMP #$30 36E5 D0 FE BNE $36E5
Looking further down in the code, I see the code actually ends off with a RTI, so this code is part of a interrupt service routine.
Looking back in the code I copied, I see the stack pointer gets copied to the X register and then the value of memory location $102,X is compared with value $30.
From the debug window I figured out the stackpointer and register X has value $FB. So the effective meomory location is $01FD. Since this is inside an interrupt service routine, we know pushed values on the stack should be the address and the value of the status register. So looking at everything I feel comfortable that the value we compare is a status register. The value at $01FD is $34 and the Test Suite expects $30.
The two values differ by a single bit; Bit 2, which is the interrupt disable register. So, my emulator set the interrupt disable flag and the Test Suite expects this flag to be cleared. After some reasoning I established the following:
- Calling BRK should set the I flag, but
- this doesn't necessarily means that the status register pushed onto the stack will have this bit set.
Keeping this in mind, I actually discovered that I set the interrupt flag to early in the case statement of the BRK instruction. The setting should actually happen after the status byte is pushed onto the stack:
case 0x00:interruptflag = 1;var tempVal = pc + 1; Push(tempVal >> 8); Push(tempVal & 0xff); Push(getStatusFlagsAsByte());
interruptflag = 1;tempVal = localMem.readMem(0xffff) * 256; tempVal = tempVal + localMem.readMem(0xfffe); pc = tempVal; break;
Next, issue: SEI and CLI not implemented. So lets implement them:
/*CLI Clear Interrupt Disable Bit 0 -> I N Z C I D V - - - 0 - - addressing assembler opc bytes cyles -------------------------------------------- implied CLI 58 1 2 */ case 0x58: interruptflag = 0; break; /*SEI Set Interrupt Disable Status 1 -> I N Z C I D V - - - 1 - - addressing assembler opc bytes cyles -------------------------------------------- implied SEI 78 1 2 */ case 0x78: interruptflag = 1; break;
Next failing point is at address $d23. Let look again at the surrounding code:
0D13 8D 00 02 STA $0200 0D16 A2 01 LDX #$01 0D18 A9 FF LDA #$FF 0D1A 48 PHA 0D1B 28 PLP 0D1C 9A TXS 0D1D 08 PHP 0D1E AD 01 01 LDA $0101 0D21 C9 FF CMP #$FF 0D23 D0 FE BNE $0D23
While debugging through this code, it seemed that the TXS effected some flags. Something just didn't seem right. I had a look again at the datasheet from masswerk and they actually stated that TXS should effect some flags:
X to Stack Register X -> SP N Z C I D V + + - - - - addressing assembler opc bytes cyles -------------------------------------------- implied TXS 9A 1 2
But still, something didn't seem right. I consulted other sources and Bingo! Flags shouldn't be effected for this instruction. I commented out the setting of flags for this instruction and restarted the Test Suite.
Next, the emulator got stuck at address $22B3. Here is the surrounding code:
22A5 28 PLP 22A6 4A LSR A 22A7 08 PHP 22A8 DD 19 02 CMP $0219,X 22AB D0 FE BNE $22AB 22AD 68 PLA 22AE 49 7C EOR #$7C 22B0 DD 29 02 CMP $0229,X 22B3 D0 FE BNE $22B3
From this code this test entails doing a LSR and checking effected flags. Id did some debugging and found EOR yields a result of 82. This gets compared to the effective address 22C which has a value of 02. It appears that after the LSR operation, the negative flag is set. This is strange because a LSR operation should always insert a zero in bit 7, yielding a positive number.
I looked carefully at the definition on the masswerk datasheet, and here we go: It states that the negative flag is unaffected. I again consulted other datasheets and all says that the Negative flag is set to 0 as part of the LSR operation.
I adjusted these Instructions accordingly:
case 0x4A: carryflag = ((acc & 0x1) != 0) ? 1 : 0; acc = acc >> 1; acc = acc & 0xff; zeroflag = (acc == 0) ? 1 : 0; negativeflag = 0; break; case 0x46: case 0x56: case 0x4E: case 0x5E: var tempVal = localMem.readMem(effectiveAdrress); carryflag = ((tempVal & 0x1) != 0) ? 1 : 0; tempVal = tempVal >> 1; tempVal = tempVal & 0xff; zeroflag = (tempVal == 0) ? 1 : 0; localMem.writeMem(effectiveAdrress, tempVal); negativeflag = 0; break;
I got two last issues I had to fix in order for all Test cases to succeed. I will try to give a summary on these two issues.
The first issue was that for SBC I applied two's complement for the operand instead of one's complement, so I had a off by one issue with SBC.
Finally I had to implement BCD mode addition and subtraction. For this I made use of code from my Java emulator and adopted it to JavaScript.
In Summary
In this blog we started off by extending the Debugging functionality. We then ran the Klaus Test Suite.As expected, there was a couple of issues that came out. In the end however, we managed to run all tests.
In my next Blog we will be booting the C64 system with its ROM's.
Till next time!
No comments:
Post a Comment