At this
point I have everything necessary to write the assembly language tests for
implementing the 6502 emulator. My assembler is also in a good enough state
that it would be adequate for the 2600 portions of my emulator as well as
Coffee Quest 2600. The only features that I can think of that my assembler
doesn’t support are multiple files and macros. As I am planning on having this
on a web page which wouldn’t necessarily have access to a file system (there
are workarounds) this is not going to happen within the assembler. While macros
are not a requirement of the assembler they are handy to have as they can save
some work.
A macro
would need to be recorded using .MSTART and .MEND directives and would be
inserted back into the assembly language with the .MACRO directive. To make
macros more useful, parameters would be desired so I am reserving P0 through P9
as parameter variables for the macro so that when you issue a macro it would
let you specify parameters which would be replaced in the code so you could
have something like the following program:
.MSTART DELAY 255
LDX
#P0
delayLoop:
DEX
BNE
delayLoop
.MEND
.MSTART LONGDELAY 2 255
LDY
#P0
longDelayLoop:
.MACRO
DELAY P1
DEY
BNE
longDelayLoop
.MEND
.MACRO DELAY 1
.MACRO DELAY 3
.MACRO LONGDELAY 10 100
This demo
clearly shows how macros can be convenient but also demonstrates a problem with
just copying and inserting the code. The delayLoop will end up being created 3
times and the address linkage will be totally incorrect for two of the three
instances. This means that the macro will need to be smart enough to be able to
track which labels are internal to it and change link labels to point to the
updated version. This can be done simply by having a separate class that
manages macros which searches for label declarations and tracks them in a list.
When executing the macro, it then looks for any reference to that label (both
the original declaration and any link labels) and changes them adding a prefix.
The
AssemblerMacro class is a bit too long to cover in full so lets just look at
the relevant parts. As you can see, the code for adding a macro line also handles
the detection of the .MEND directive. The idea then is that when a macro is
started, all further token lines are sent to the macro being created until the
end is detected.
fun
addLine(line:ArrayList<AssemblerToken>):Boolean {
if
(line.size < 1)
return
false
//
TODO do we want to do a more thorough check here? Probably should allow
anywhere on line?
if
((line[0].type == AssemblerTokenTypes.DIRECTIVE) and (line[0].contents ==
"MEND")) {
return
true
}
if
(line[0].type == AssemblerTokenTypes.LABEL_DECLARATION)
labels.add(line[0].contents)
macroLines.add(line)
return
false
}
Executing
the macro simply searches through the tokens replacing the labels that are
internal links or parameters with the appropriate label, as demonstrated.
val token:AssemblerToken =
adjustedLine[cntrToken]
if (token.type ==
AssemblerTokenTypes.LABEL_DECLARATION)
adjustedLine[cntrToken]
= AssemblerToken(AssemblerTokenTypes.LABEL_DECLARATION,
prefix
+ token.contents, token.num)
else if (token.type ==
AssemblerTokenTypes.LABEL_LINK) {
if (labels.contains(token.contents))
adjustedLine[cntrToken]
= AssemblerToken(AssemblerTokenTypes.LABEL_LINK,
prefix
+ token.contents, token.num)
else
if (paramNames.contains(token.contents))
adjustedLine[cntrToken]
= params[paramNames.indexOf(token.contents)]
}
The prefix string
is simply a count of how many times the macro has been executed. Once the tokens
have been adjusted so they have the correct labels and parameters we simply
pass that array of tokens to the assembler’s parse method as if it were sent from
the assembler after tokenizing.
.MSTART
works by creating an instance of the AssemblerMacro class and passing in any
default parameters that were set in the call. There is a macroInProgress
variable which is a nullable variable so when it is not null we know a macro is
being recorded and the parse method calls the add method of that instance to
add any additional lines of tokens until the add method returns false at which
point the macroInProgress variable is added to the list of macros and
macroInProgress is set again to be null.
if (macroInProgress != null) {
if
(macroInProgress!!.addLine(tokens)) {
macroList.put(macroInProgress!!.label,
macroInProgress!!)
macroInProgress
= null
}
return
arrayOf()
}
The .MACRO function simply looks to see if there is a macro
using the indicated name. If there is then it copies over the rest of the
tokens on the line as parameters to the macro then calls the macro’s execute
method which “plays” back the recorded tokens using the indicated parameters.
There are all sorts of additional features that you may want
to have with a macro system but for my emulator project, and probably even for
my Coffee Quest 2600 project, this is more than enough of an assembler for my
needs and it is built right into my emulator! If the assembler was going to be
for production use, I would want to go through a few more hardening passes to
make sure that it is robust and stupid mistakes won’t crash it. I would
probably also want to add some advanced features such as mathematics support. I
wouldn’t mind going over the code to do a bit of clean up and refactoring to
make things smoother and more efficient. This is meant to be an internal tool for
making the test code for the emulators (6502 and TIA) easier.
So finally, we can start on the actual 6502 emulation, which
will be next week. As there are 56 instructions this will take a while to do.
My plan is to break the instructions down into groups of related
inststructions, cover the theory then implement as many instructions per week
as space permits trying to keep each post down to a few pages.
No comments:
Post a Comment