My original
thoughts about assembler directives would be that they would either start a
line or be right after a label. After thinking about how I want to use
directives in my assembler, I realize that they could also be used for
variables and special label handling, so what I am doing is having a process
directives function that will be called just after the parser determines if
there is a label on the line. This takes the tokens and has the ability to
alter the list of tokens if necessary to implement features like variables. For
example:
LDA #.HIGH
label
Would
remove the declaration and label tokens replacing them with a number token. If
the label is an address the number would be 0 but the directive processor would
add the appropriate link to the label list, while if label is a variable would
just have the high order byte of the value of the variable. This will be
discussed in more detail in a future article.
The code
for processing directives simply loops through the token list looking for
directive and label tokens. Label tokens will be processed by the variable
system which will be covered next week. Directives are ran through a when block
(the Kotlin equivalent of a switch statement) which then takes each directive
and processes the command. There are a number of directives that we will be
supporting, with my current list being .BANK, .ORG, .EQU, .HIGH, .LOW, .BYTE,
.WORD, .JUMPTABLE, .STARTMACRO, .ENDMACRO, .MACRO with additional directives
added as necessary.
The .BANK
directive will have the format .BANK number [origin [size]] with bank numbering
starting at 0 and going as high as the cartridges memory manager will allow. The
following test code shows how the bank directive would be used. Notice that
bank 1 and bank 2 overlap the same memory region.
.BANK 0 $1000 1024
JSR
bank1
; would have
bank switching code here
JSR
bank2
BRK
.bank 1 $2000 1024
bank1: LDA
#1
RTS
.BANK 2 $2000 1024
bank2:
LDA
#2
RTS
The bank directive
is simple to implement, though a bit longer than one would expect. To begin,
the number of the bank is required so if the next token isn’t a number then
there is an assembly problem.
"BANK" -> {
if
((indx >= tokens.size) or (tokens[indx].type != AssemblerTokenTypes.NUMBER))
{
throw
AssemblyException("Invalid bank specified $assemblyLine")
}
Getting the parameters for the bank turned out to be
trickier than it should have with the version of the Kotlin compiler that I was
using as it does not seem to shortcut ANDconditionals when the first condition
fails so will go on to attempt an invalid array index which was checked for in
the first part of the if statement. This is why my code is nested so deep
within the parameter checking routines. The basic idea here is that we don’t
want to alter the size and origin unless those parameters are provided so we
flag those changes as false. We then see if the next parameters are numbers and
if so assign them to the appropriate variable. This gives us the number of the
bank, the origin of the bank, and the size of the bank. Default values are
provided for cases where we are accessing a bank that has not been created yet.
val
bankID = tokens[indx].num
tokens.removeAt(indx)
var
bankOrg = 0
var
orginParamSet = false
var
bankSize = 4096
var
sizeParamSet = false
if
(indx < tokens.size)
if
(tokens[indx].type == AssemblerTokenTypes.NUMBER) {
bankOrg
= tokens[indx].num
orginParamSet
= true
tokens.removeAt(indx)
if
(indx < tokens.size)
if
(tokens[indx].type == AssemblerTokenTypes.NUMBER) {
bankSize
= tokens[indx].num
sizeParamSet
= true
tokens.removeAt(indx)
}
}
Once we have the desired parameters for the bank, we need to
process this by adjusting the existing bank if it exists or by creating a new
bank at the appropriate slot in the array if it doesn’t exist. While Kotlin has
an ensureCapacity function, which would be ideal for adjusting the size of an
array list, it doesn’t do anything so manually creating banks was done instead.
//
apply bank directive
if
(banks.size > bankID) {
currentBank
= banks[bankID]
if
((orginParamSet) and (currentBank.bankOrigin != bankOrg)) {
currentBank.bankOrigin
= bankOrg
currentBank.curAddress
= bankOrg
}
if
((sizeParamSet) and (currentBank.size != bankSize))
currentBank.resize(bankSize)
}
else {
while(banks.size
< bankID) {
val
skippedBank = banks.size
banks.add(AssemblyBank(skippedBank))
}
banks.add(AssemblyBank(bankID,
bankSize, bankOrg))
currentBank
= banks[bankID]
}
It is
possible to switch back an forth between banks with this technique, but good
assembly language code shouldn’t do that. Ideally the source code should have
the banks and their instructions laid out consecutively in the source file.
Having the flexibility to switch between banks may be useful in some
situations, but is not likely something I will ever take advantage of.
Having code
appear at specific locations within a bank is also possible, and for some bank
switching schemes may be required. The more common reason for needing to put
the generated machine language in specific locations is for data and the vector
table. Still, a .ORG directive is necessary and easy to test.
.BANK 0
$1000
JMP ten
.ORG 4106
ten: LDA
#10
BRK
The implementation of .ORG simply gets the next parameter and makes sure that the address specified is within the range for the current bank. If it is, that is set as the banks current address. I don’t think there is anything special about this so am not going to show the code, but if you want to look at the code feel free to grab it from the GitHub repository https://github.com/BillySpelchan/VM2600 . Next week we will look at variable directives.
No comments:
Post a Comment